Files
org_roam/Emacs/20260407083539-emacs_config.org
Zaine a7285933c3
All checks were successful
Build Roam Site / build (push) Successful in 31s
29-05
2026-05-29 08:41:15 +01:00

822 lines
34 KiB
Org Mode
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
:PROPERTIES:
:ID: 1c87e4f1-fa7e-4200-9a29-a5a98d4c7ac9
:END:
#+title: Emacs Config
#+filetags: :emacs:org:functions:
* Org Noter
#+begin_src emacs-lisp
(setq org-noter-notes-search-path
'("D:\\_nextcloud\\master-folder\\org_files\\org_roam\\"))
(setq org-noter-always-create-frame nil)
(setq org-noter-property-doc-file "NOTER_DOCUMENT")
(require 'pdf-tools)
(pdf-tools-install)
(use-package pdf-view-restore
:after pdf-tools
:config
(add-hook 'pdf-view-mode-hook 'pdf-view-restore-mode)
(setq pdf-view-restore-filename "~/.emacs.d/.pdf-view-restore"))
#+end_src
** Saving images in epdf mode (org roam):
#+begin_src emacs-lisp
;; epdf stuff
(require 'seq)
(require 'subr-x)
(defvar zaine/pdf-image-save-dir
"d:/_nextcloud/master-folder/org_files/org_roam/assets/_books/"
"Folder where cropped PDF images should be saved.")
(defvar zaine/org-roam-root
"d:/_nextcloud/master-folder/org_files/org_roam/"
"Root folder used for relative Org image links.")
(defvar zaine/pdf-image-kill-buffer-after-save t
"If non-nil, close the *PDF image* buffer after saving.")
(defvar zaine/pdf-image-insert-link-into-org t
"If non-nil, insert the image link into a visible Org buffer.")
(defun zaine/pdf-image--slugify (s)
"Turn S into a safe filename slug."
(let* ((s (downcase (string-trim (or s ""))))
(s (replace-regexp-in-string "['`]" "" s))
(s (replace-regexp-in-string "[^[:alnum:]]+" "-" s))
(s (replace-regexp-in-string "-+" "-" s))
(s (replace-regexp-in-string "\\`-+\\|-+\\'" "" s)))
(if (string-empty-p s) "pdf-image" s)))
(defun zaine/pdf-image--current-pdf-title ()
"Try to get the title from the most recently used PDF buffer."
(when-let ((buf
(seq-find
(lambda (b)
(with-current-buffer b
(derived-mode-p 'pdf-view-mode)))
(delq (current-buffer) (buffer-list)))))
(with-current-buffer buf
(if buffer-file-name
(file-name-base buffer-file-name)
(buffer-name buf)))))
(defun zaine/pdf-image--unique-file (dir stem ext)
"Return a unique filename in DIR using STEM and EXT."
(let ((file (expand-file-name (concat stem ext) dir))
(n 1))
(while (file-exists-p file)
(setq file
(expand-file-name
(format "%s-%02d%s" stem n ext)
dir))
(setq n (1+ n)))
file))
(defun zaine/pdf-image--org-link (file)
"Create a relative Org file link for FILE."
(format "[[file:%s]]"
(file-relative-name
(expand-file-name file)
(file-name-as-directory (expand-file-name zaine/org-roam-root)))))
(defun zaine/pdf-image--visible-org-buffer ()
"Find a visible Org buffer, usually your org-noter notes buffer."
(seq-find
(lambda (buf)
(with-current-buffer buf
(derived-mode-p 'org-mode)))
(delete-dups (mapcar #'window-buffer (window-list)))))
(defun zaine/pdf-image--insert-link-into-org (link)
"Insert LINK into a visible Org buffer, if one exists."
(when-let ((buf (zaine/pdf-image--visible-org-buffer)))
(with-current-buffer buf
(goto-char (point))
(unless (bolp)
(insert "\n"))
(insert link "\n")
(when (fboundp 'org-display-inline-images)
(org-display-inline-images)))
t))
(defun zaine/pdf-image--visible-org-window ()
"Find a visible Org window, usually the org-noter notes window."
(seq-find
(lambda (win)
(with-current-buffer (window-buffer win)
(derived-mode-p 'org-mode)))
(window-list nil 'no-minibuf)))
(defun zaine/pdf-image-save-and-insert (&optional ask-title)
"Save the current *PDF image* buffer into `_books` and insert an Org link.
With prefix argument, prompt for the title/name instead of using the PDF filename.
Also closes the temporary *PDF image* window afterwards."
(interactive "P")
(unless (or (derived-mode-p 'image-mode)
(string-match-p "\\`\\*PDF image\\*" (buffer-name)))
(user-error "Run this from the *PDF image* buffer"))
(let* ((image-buffer (current-buffer))
(image-windows (get-buffer-window-list image-buffer nil t))
(org-window (zaine/pdf-image--visible-org-window))
(default-title (or (zaine/pdf-image--current-pdf-title)
"pdf-image"))
(title (if ask-title
(read-string "Image/article name: " default-title)
default-title))
(stem (concat (format-time-string "%Y-%m-%d")
"-"
(zaine/pdf-image--slugify title)))
(file (zaine/pdf-image--unique-file
zaine/pdf-image-save-dir
stem
".png"))
(link (zaine/pdf-image--org-link file)))
(make-directory (file-name-directory file) t)
;; Save the raw image data from the *PDF image* buffer.
(let ((coding-system-for-write 'binary)
(require-final-newline nil))
(with-current-buffer image-buffer
(save-restriction
(widen)
(write-region (point-min) (point-max) file nil 'silent))
(set-buffer-modified-p nil)))
;; Copy the link just in case.
(kill-new link)
;; Insert the link into the visible Org notes window.
(when org-window
(with-selected-window org-window
(unless (bolp)
(insert "\n"))
(insert link "\n")
(when (fboundp 'org-display-inline-images)
(org-display-inline-images))))
;; Close the temporary *PDF image* window instead of letting Emacs reuse it.
(dolist (win image-windows)
(when (window-live-p win)
(ignore-errors
(delete-window win))))
;; Kill the temporary image buffer after its window has gone.
(when (buffer-live-p image-buffer)
(kill-buffer image-buffer))
;; Put focus back on the notes window if it still exists.
(when (and org-window (window-live-p org-window))
(select-window org-window))
(message "Saved %s and inserted Org link"
(file-name-nondirectory file))))
(with-eval-after-load 'image-mode
(define-key image-mode-map (kbd "C-c i p") #'zaine/pdf-image-save-and-insert))
#+end_src
* CALDAV and org agenda calendars:
#+begin_src emacs-lisp
(require 'org-caldav)
(require 'url)
(require 'icalendar)
(modify-coding-system-alist 'file "caldav-work\\.org\\'" 'utf-8-unix)
(setq org-icalendar-timezone "Europe/London")
(setq calendar-time-zone 60) ;; minutes offset from UTC (BST = +60)
(setq calendar-standard-time-zone-name "GMT")
(setq calendar-daylight-time-zone-name "BST")
(setq org-caldav-timezone "Europe/London")
(defvar my/outlook-ics-url
"https://outlook.office365.com/owa/calendar/b910ea835e414663831e505f3d10baa2@microlise.com/d2568a0134fb49af92d39c1669c4729317662288011094411305/calendar.ics")
(defvar my/outlook-org-file
"D:\\_nextcloud\\master-folder\\org_files\\calendar\\caldav-work.org")
(defvar my/ics-python-script
"D:\\_nextcloud\\master-folder\\org_files\\calendar\\ics_to_org.py")
(defun my/sync-outlook-calendar ()
"Fetch and expand Outlook ICS (including recurrences) into org file."
(interactive)
(message "Syncing Outlook calendar...")
(let* ((cmd (format "python \"%s\" \"%s\" \"%s\""
my/ics-python-script
my/outlook-ics-url
my/outlook-org-file))
(result (shell-command-to-string cmd)))
(message "Outlook calendar sync: %s" (string-trim result))))
;; Sync on startup and every 30 minutes
(my/sync-outlook-calendar)
(run-with-timer (* 30 60) (* 30 60) #'my/sync-outlook-calendar)
;; --- CalDAV for native Nextcloud calendars ---
(setq org-caldav-url "https://nextcloud.zainezq.com/remote.php/dav/calendars/zaine/")
(setq org-caldav-calendars
'((:calendar-id "personal"
:inbox "D:\\_nextcloud\\master-folder\\org_files\\calendar\\caldav-personal.org"
:files nil)
(:calendar-id "zxh"
:inbox "D:\\_nextcloud\\master-folder\\org_files\\calendar\\caldav-family.org"
:files nil)))
;; --- Agenda files ---
(setq org-agenda-files
'("D:\\_nextcloud\\master-folder\\org_files\\calendar\\caldav-personal.org"
"D:\\_nextcloud\\master-folder\\org_files\\calendar\\caldav-work.org"
"D:\\_nextcloud\\master-folder\\org_files\\calendar\\caldav-family.org"
"D:\\_nextcloud\\master-folder\\org_files\\org_roam\\Books\\20250724230557-books_org_agenda.org"))
#+end_src
* Notes when reading physical book
#+begin_src emacs-lisp
("b" "Book Note" entry
(file+headline (lambda () (buffer-file-name)) "Notes")
"** p. %^{Page}\n[%^{Type|Insight|Important|Quote|Confusing}] %?\n")
#+end_src
* Org image inserter:
#+begin_src emacs-lisp
;;; --- Org image inserter (improved search UX) -----------------------------
(use-package vertico
:init (vertico-mode))
(use-package orderless
:custom
(completion-styles '(orderless basic))
(completion-category-defaults nil))
(defgroup zq/org-assets nil
"Insert image links from predefined asset profiles."
:group 'org)
(defcustom zq/org-asset-profiles
'(("org-roam" . "D:\\_nextcloud\\master-folder\\org_files\\org_roam\\assets")
("org-web" . "D:\\_nextcloud\\master-folder\\org_files\\org_web\\assets"))
"Alist of (PROFILE-NAME . ASSETS-DIR)."
:type '(alist :key-type string :value-type directory)
:group 'zq/org-assets)
(defcustom zq/org-image-extensions
'("png" "jpg" "jpeg" "gif" "svg" "webp" "mov" "mp4" "mp3")
"File extensions considered when offering candidates."
:type '(repeat string)
:group 'zq/org-assets)
(defun zq/org--image-regexp ()
(concat "\\." (regexp-opt zq/org-image-extensions t) "\\'"))
(defun zq/org--image-files (dir)
"Return image/media files in DIR, newest modified first."
(when (and dir (file-directory-p dir))
(sort
(directory-files-recursively dir (zq/org--image-regexp))
#'file-newer-than-file-p)))
(defun zq/org--parse-name (filename)
"Split FILENAME into (DATE . REST)."
(if (string-match "^\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\)-\\(.*\\)" filename)
(cons (match-string 1 filename)
(match-string 2 filename))
(cons nil filename)))
(defun zq/org--display-name (path)
(let* ((name (file-name-nondirectory path))
(dir (file-name-nondirectory (directory-file-name (file-name-directory path))))
(parsed (zq/org--parse-name name))
(date (car parsed))
(rest (cdr parsed)))
(format "%s (%s | %s)" rest (or date "no-date") dir)))
(defun zq/org--build-candidates (files)
"Return an alist of (DISPLAY . FULLPATH), handling duplicates.
Preserves the order of FILES, so newest files can appear first."
(let ((table (make-hash-table :test 'equal))
result)
(dolist (path files)
(let* ((display (zq/org--display-name path))
(existing (gethash display table)))
(if existing
;; duplicate → include full filename to disambiguate
(push (cons (file-name-nondirectory path) path) result)
(puthash display t table)
(push (cons display path) result))))
(nreverse result)))
(defun zq/org-insert-asset-image (&optional profile use-file-prefix)
"Insert an Org link to an image chosen from a PROFILEs assets dir.
Features:
- Fuzzy search (with Vertico/Orderless/Ivy/Helm)
- Clean names (date stripped)
- Relative paths inserted
With prefix arg (C-u), use file: links."
(interactive
(list (completing-read
"Profile: "
(mapcar #'car zq/org-asset-profiles)
nil t nil nil "org-roam")
current-prefix-arg))
(let* ((base-dir (cdr (assoc profile zq/org-asset-profiles)))
(_ (unless (and base-dir (file-directory-p base-dir))
(user-error "Invalid directory for profile '%s'" profile)))
(files (or (zq/org--image-files base-dir)
(user-error "No assets found in %s" base-dir)))
(candidates (zq/org--build-candidates files))
;; user selects clean name, we resolve full path
(selected (completing-read "Image: " candidates nil t))
(abs-path (cdr (assoc selected candidates)))
(here (or (buffer-file-name) default-directory))
(rel (file-relative-name abs-path (file-name-directory here)))
(link (format "[[%s%s]]"
(if use-file-prefix "file:" "")
rel)))
(insert link)
(when (derived-mode-p 'org-mode)
(org-display-inline-images t t))))
#+end_src
* Weekly org review file:
#+begin_src emacs-lisp
;; Create weekly review files for Sundays, with catch-up command to fill in any missing ones up to the current week.
(require 'seq)
(defvar zaine/weekly-review-base-dir
"D:/_nextcloud/master-folder/org_files/org_web/blogs/"
"Base directory for weekly review blog entries.")
(defun zaine/weekly-review--month-dir (time)
"Return the directory for TIME in the format YYYY/MM-month."
(let ((year (format-time-string "%Y" time))
(month-num (format-time-string "%m" time))
(month-name (downcase (format-time-string "%B" time))))
(expand-file-name (format "%s/%s-%s" year month-num month-name)
zaine/weekly-review-base-dir)))
(defun zaine/weekly-review--normalize-to-review-time (time)
"Normalize TIME to 12:00 on the same date."
(let* ((decoded (decode-time time))
(day (nth 3 decoded))
(month (nth 4 decoded))
(year (nth 5 decoded)))
(encode-time 0 0 12 day month year)))
(defun zaine/weekly-review--sunday-of-week (&optional time)
"Return the most recent Sunday on or before TIME, normalized to 12:00."
(let* ((time (or time (current-time)))
(decoded (decode-time time))
(dow (nth 6 decoded))
(day (nth 3 decoded))
(month (nth 4 decoded))
(year (nth 5 decoded)))
(encode-time 0 0 12 (- day dow) month year)))
(defun zaine/weekly-review--next-sunday (&optional time)
"Return the next Sunday strictly after TIME, normalized to 12:00.
If TIME is a Sunday, returns the following Sunday."
(let* ((time (or time (current-time)))
(decoded (decode-time time))
(dow (nth 6 decoded))
(days-ahead (if (= dow 0) 7 (- 7 dow))))
(zaine/weekly-review--normalize-to-review-time
(time-add time (days-to-time days-ahead)))))
(defun zaine/weekly-review--coming-sunday (&optional time)
"Return the coming Sunday relative to TIME, normalized to 12:00.
If TIME is a Sunday, return that day. Otherwise return the next Sunday."
(let* ((time (or time (current-time)))
(decoded (decode-time time))
(dow (nth 6 decoded)))
(if (= dow 0)
(zaine/weekly-review--normalize-to-review-time time)
(zaine/weekly-review--next-sunday time))))
(defun zaine/weekly-review--file-name (time)
"Return the weekly review filename for TIME."
(format-time-string "%d-%m-week-review.org" time))
(defun zaine/weekly-review--slug (time)
"Return the weekly review slug for TIME."
(format-time-string "%d-%m-%y-week-review" time))
(defun zaine/weekly-review--title (time)
"Return the title for TIME."
(format "%s - Weekly Review"
(format-time-string "[%d-%m-%Y]" time)))
(defun zaine/weekly-review--header (time)
"Return Org header text for weekly review at TIME."
(concat
(format "#+TITLE: %s\n" (zaine/weekly-review--title time))
"#+OPTIONS: num:nil\n"
(format "#+DATE: %s\n" (format-time-string "<%Y-%m-%d %a 12:00>" time))
"#+filetags: :review:\n"
"#+COMMENTS: t\n"
(format "#+SLUG: %s\n\n" (zaine/weekly-review--slug time))))
(defun zaine/weekly-review--all-review-files ()
"Return all weekly review files under `zaine/weekly-review-base-dir`."
(when (file-directory-p zaine/weekly-review-base-dir)
(directory-files-recursively
zaine/weekly-review-base-dir
"^[0-9]\\{2\\}-[0-9]\\{2\\}-week-review\\.org$")))
(defun zaine/weekly-review--extract-date-from-file (file)
"Extract review date from FILE path.
Expected path like:
.../blogs/YYYY/MM-month/DD-MM-week-review.org"
(when (string-match
"/blogs/\\([0-9]\\{4\\}\\)/[0-9]\\{2\\}-[[:lower:]]+/\\([0-9]\\{2\\}\\)-\\([0-9]\\{2\\}\\)-week-review\\.org$"
file)
(let* ((year (string-to-number (match-string 1 file)))
(day (string-to-number (match-string 2 file)))
(month (string-to-number (match-string 3 file))))
(encode-time 0 0 12 day month year))))
(defun zaine/weekly-review--latest-existing-sunday ()
"Return the latest Sunday review time found, or nil if none exist."
(let* ((files (zaine/weekly-review--all-review-files))
(dates (delq nil (mapcar #'zaine/weekly-review--extract-date-from-file files))))
(when dates
(seq-reduce
(lambda (latest current)
(if (time-less-p latest current) current latest))
(cdr dates)
(car dates)))))
(defun zaine/weekly-review--create-file-for-sunday (sunday)
"Create a weekly review file for SUNDAY if it doesn't exist.
Opens the file and returns its path. If it already exists, just opens it."
(let* ((target-dir (zaine/weekly-review--month-dir sunday))
(file-name (zaine/weekly-review--file-name sunday))
(file-path (expand-file-name file-name target-dir)))
(make-directory target-dir t)
(unless (file-exists-p file-path)
(with-temp-file file-path
(insert (zaine/weekly-review--header sunday))))
file-path))
;;; Public commands
(defun zaine/weekly-review-catchup ()
"Create all missing weekly review files from the oldest gap up to this week's Sunday.
If today is Monday 30 March, catches up through Sunday 29 March.
Creates one file per call so you can review each before continuing."
(interactive)
(let* ((past-sunday (zaine/weekly-review--sunday-of-week (current-time)))
(latest (zaine/weekly-review--latest-existing-sunday))
(next-needed (if latest
(zaine/weekly-review--normalize-to-review-time
(time-add latest (days-to-time 7)))
past-sunday)))
(if (time-less-p past-sunday next-needed)
(message "Weekly reviews are already up to date through %s."
(format-time-string "%d-%m-%Y" past-sunday))
(let ((file-path (zaine/weekly-review--create-file-for-sunday next-needed)))
(find-file file-path)
(message "Created missing review for Sunday %s — call again to catch up further."
(format-time-string "%d-%m-%Y" next-needed))))))
(defun zaine/weekly-review-create-coming ()
"Create the weekly review for the coming Sunday.
If today is Wednesday 1 April, creates the file for Sunday 5 April
in the April directory, creating it if needed.
If today is Sunday, creates today's review.
If the file already exists, just opens it."
(interactive)
(let* ((coming-sunday (zaine/weekly-review--coming-sunday (current-time)))
(file-path (zaine/weekly-review--create-file-for-sunday coming-sunday)))
(find-file file-path)
(message "%s review for Sunday %s."
(if (file-exists-p file-path) "Opened existing" "Created")
(format-time-string "%d-%m-%Y" coming-sunday))))
(global-set-key (kbd "C-c w r") #'zaine/weekly-review-catchup)
(global-set-key (kbd "C-c w c") #'zaine/weekly-review-create-coming)
#+end_src
* Insert org web entries
#+begin_src emacs-lisp
;; Create org web entries for blogs and posts with a consistent template and naming convention.
(defun zaine/create-org-web-entry ()
"Create a new org web entry in either blogs or posts."
(interactive)
(let* ((entry-type (completing-read "Choose type (blogs/posts): " '("blogs" "posts") nil t))
(slug (read-string "Filename / slug: "))
(title (read-string "Title: "))
(now (current-time))
(date-for-title (format-time-string "[%d-%m-%Y]" now))
(date-for-org (format-time-string "<%Y-%m-%d %a %H:%M>" now))
(year (format-time-string "%Y" now))
(month-num (format-time-string "%m" now))
(month-name (downcase (format-time-string "%B" now)))
(blog-dir (format "D:/_nextcloud/master-folder/org_files/org_web/blogs/%s/%s-%s"
year month-num month-name))
(post-dir "D:/_nextcloud/master-folder/org_files/org_web/posts/career")
(target-dir (if (string= entry-type "blogs") blog-dir post-dir))
(file-path (expand-file-name (concat slug ".org") target-dir))
(filetags (if (string= entry-type "blogs") ":life:" ":post:")))
;; Create directory if it does not exist
(unless (file-directory-p target-dir)
(make-directory target-dir t))
;; Open file and insert template if new
(find-file file-path)
(when (= (buffer-size) 0)
(insert (format "#+TITLE: %s\n" title))
(insert "#+OPTIONS: num:nil\n")
(insert (format "#+DATE: %s\n" date-for-org))
(insert (format "#+filetags: %s\n" filetags))
(insert "#+COMMENTS: t\n")
(insert (format "#+SLUG: %s\n\n" slug))
(save-buffer))))
#+end_src
* Insert org macros:
#+begin_src emacs-lisp
(defun org-insert-macro ()
"Interactively select and insert an Org macro with prompted values."
(interactive)
(let* ((macro-defs
'(("sidenote" . ("ID" "Content"))
("epigraph" . ("Quote" "Footer"))
("epigraph_single" . ("Quote"))
("epigraph3" . ("Quote" "Author" "Work"))
("kbd" . ("Key"))
("margimg" . ("Image URL" "Alt text" "Caption (optional)"))
("countdown" . ("ISO datetime (e.g. 2025-12-31T00:00:00)" "Label"))))
(name (completing-read "Insert macro: " (mapcar #'car macro-defs) nil t))
(args (cdr (assoc name macro-defs)))
(values (mapcar (lambda (prompt)
(read-string (format "%s: " prompt)))
args))
(macro (format "{{{%s(%s)}}}"
name
(mapconcat #'identity values ","))))
(insert macro)))
(define-key org-mode-map (kbd "C-c m") #'org-insert-macro)
#+end_src
* Insert Dailies into ~20260421100223-dailies.org~
#+begin_src emacs-lisp
;; ─── org-roam daily auto-linker ───────────────────────────────────────────
(defvar my/org-roam-last-capture-file nil
"Stores the daily file path captured via org-roam-capture-new-node-hook.")
(defun my/org-roam-capture-store-file ()
"Store file path when a new org-roam node is created."
(when-let ((f (buffer-file-name)))
(setq my/org-roam-last-capture-file f)))
(defun my/org-roam-daily-already-linked-p (index-file date-str)
"Return t if DATE-STR already appears in INDEX-FILE."
(when (file-exists-p index-file)
(with-temp-buffer
(insert-file-contents index-file)
(search-forward date-str nil t))))
(defun my/org-roam-daily-add-link (index-file date-str)
"Append a heading linking DATE-STR daily into INDEX-FILE."
(message " Adding link for %s..." date-str)
(with-current-buffer (find-file-noselect index-file)
(save-excursion
(goto-char (point-max))
(unless (bolp) (insert "\n"))
(insert (format "** [[file:daily/%s.org][%s]]\n" date-str date-str)))
(save-buffer))
(org-roam-db-update-file index-file))
(defun my/org-roam-daily-link-to-index ()
"After capture, link today's daily into the index file."
(let ((captured-file
(or my/org-roam-last-capture-file
(ignore-errors
(plist-get org-capture-current-plist :original-file)))))
(when (and captured-file
(string-match-p (regexp-quote "daily") captured-file))
(let* ((today-date (file-name-base captured-file))
(index-file (expand-file-name
"20260421100223-dailies.org"
org-roam-directory)))
(if (my/org-roam-daily-already-linked-p index-file today-date)
(message " %s already linked, skipping." today-date)
(my/org-roam-daily-add-link index-file today-date)))
(setq my/org-roam-last-capture-file nil))))
(add-hook 'org-roam-capture-new-node-hook #'my/org-roam-capture-store-file)
(add-hook 'org-capture-after-finalize-hook #'my/org-roam-daily-link-to-index)
#+end_src
* Old stuff:
** org image inserter:
#+begin_src emacs-lisp
;; Core group & settings (dedupe: define these only once)
;; (defgroup zq/org-assets nil
;; "Insert image links from predefined asset profiles."
;; :group 'org)
;; (defcustom zq/org-asset-profiles
;; '(("org-roam" . "D:\\_nextcloud\\master-folder\\org_files\\org_roam\\assets")
;; ("org-web" . "D:\\_nextcloud\\master-folder\\org_files\\org_web\\assets"))
;; "Alist of (PROFILE-NAME . ASSETS-DIR)."
;; :type '(alist :key-type string :value-type directory)
;; :group 'zq/org-assets)
;; (defcustom zq/org-image-extensions '("png" "jpg" "jpeg" "gif" "svg" "webp" "mov" "mp4" "mp3")
;; "Image file extensions considered when offering candidates."
;; :type '(repeat string)
;; :group 'zq/org-assets)
;; (defun zq/org--image-regexp ()
;; "Build a regexp that matches configured image extensions."
;; (concat "\\." (regexp-opt zq/org-image-extensions t) "\\'"))
;; (defun zq/org--image-files (dir)
;; "Return a list of image files under DIR (recursively)."
;; (when (and dir (file-directory-p dir))
;; (directory-files-recursively dir (zq/org--image-regexp))))
;; (defun zq/org-insert-asset-image (&optional profile use-file-prefix)
;; "Insert an Org link to an image chosen from a PROFILEs assets dir.
;; Interactively:
;; - Prompts for PROFILE (from `zq/org-asset-profiles`).
;; - Presents only image files under that profiles assets directory.
;; - Inserts a relative link like [[path/to/image.png]].
;; - With prefix arg (C-u), uses [[file:path/to/image.png]].
;; Non-interactively:
;; PROFILE is the profile name string, USE-FILE-PREFIX non-nil forces file: prefix."
;; (interactive
;; (list (completing-read "Profile: "
;; (mapcar #'car zq/org-asset-profiles)
;; nil t nil nil "org-roam")
;; current-prefix-arg))
;; (let* ((base-dir (cdr (assoc profile zq/org-asset-profiles)))
;; (_ (unless (and base-dir (file-directory-p base-dir))
;; (user-error "Profile '%s' has no valid directory: %s" profile base-dir)))
;; (candidates (or (zq/org--image-files base-dir)
;; (user-error "No images found in %s" base-dir)))
;; (abs-path (expand-file-name (completing-read "Image: " candidates nil t)))
;; (here (or (buffer-file-name) default-directory))
;; (rel (file-relative-name abs-path (file-name-directory here)))
;; (link (format "[[%s%s]]"
;; (if use-file-prefix "file:" "")
;; rel)))
;; (insert link)
;; (when (derived-mode-p 'org-mode)
;; (org-display-inline-images t t))))
;;; --- Robust paste-into-profile without org-download--clipboard-image-p ---
;; (require 'url) ;; for url-copy-file
;; (require 'org-download) ;; for org-download-clipboard
;; (defun zq/org--ensure-dir (dir)
;; "Ensure DIR exists and is a directory; error if invalid."
;; (unless (and dir (file-directory-p dir))
;; (user-error "Profile directory invalid or not found: %s" dir))
;; (make-directory dir t)
;; dir)
;; (defun zq/org--unique-image-filename (&optional ext)
;; "Return a unique filename like 20260227_160404.png. Default EXT is png."
;; (concat (format-time-string "%Y%m%d_%H%M%S") "." (or ext "png")))
;; (defun zq/org--rel-link-to (abs-path)
;; "Return a relative Org link for ABS-PATH from current buffer."
;; (let* ((here (or (buffer-file-name) default-directory))
;; (rel (file-relative-name abs-path (file-name-directory here))))
;; (format "[[%s]]" rel)))
;; (defun zq/org--clipboard-text ()
;; "Return current clipboard text (or nil) without altering kill ring."
;; (let ((str (current-kill 0 t)))
;; (when (and str (stringp str) (> (length str) 0)) str)))
;; (defun zq/org--grab-clipboard-image-to-temp ()
;; "Try to capture clipboard image into a temporary PNG file.
;; Return the temp filename on success; nil otherwise. Temp file is caller-managed."
;; (let ((tmp (make-temp-file "zq-clip-" nil ".png"))
;; (ok nil))
;; (unwind-protect
;; (progn
;; ;; Try to save clipboard to TMP; some systems signal error when no image
;; (condition-case _err
;; (org-download-clipboard tmp)
;; (error
;; ;; ignore; ok remains nil
;; ))
;; ;; Validate: file exists, non-empty, and Emacs recognizes image type
;; (when (and (file-exists-p tmp)
;; (> (nth 7 (file-attributes tmp)) 0)
;; (ignore-errors (image-type-from-file tmp)))
;; (setq ok t)))
;; (unless ok
;; (when (file-exists-p tmp) (delete-file tmp))))
;; (when ok tmp)))
;; (defun zq/org--save-data-uri (data-uri target)
;; "Save a data:image/...;base64,... DATA-URI to TARGET."
;; (unless (string-match "\\`data:image/\\([a-zA-Z0-9+.-]+\\);base64,\\(.*\\)\\'" data-uri)
;; (user-error "Clipboard looks like a data URI but not an image/base64"))
;; (let ((bytes (base64-decode-string (match-string 2 data-uri))))
;; (with-temp-file target
;; (set-buffer-multibyte nil)
;; (insert bytes))))
;; (defun zq/org--download-url-to-file (url target)
;; "Download image from URL to TARGET (synchronously)."
;; (url-copy-file url target t))
;; (defun zq/org-paste-image-into-profile (&optional profile)
;; "Paste image (clipboard/URL/data-URI) into PROFILE assets and insert a relative Org link."
;; (interactive
;; (list (completing-read "Profile: "
;; (mapcar #'car zq/org-asset-profiles)
;; nil t nil nil "org-roam")))
;; (let* ((assets-dir (cdr (assoc profile zq/org-asset-profiles))))
;; (zq/org--ensure-dir assets-dir)
;; (let* ((filename (zq/org--unique-image-filename "png"))
;; (target (expand-file-name filename assets-dir))
;; (handled nil))
;; ;; 1) Preferred: raw image from clipboard via org-download backends
;; (let ((tmp (zq/org--grab-clipboard-image-to-temp)))
;; (when tmp
;; (rename-file tmp target t) ;; move tmp → target
;; (setq handled t)))
;; ;; 2) If no raw image, try clipboard text: data URI or URL
;; (unless handled
;; (let ((clip (zq/org--clipboard-text)))
;; (cond
;; ;; data URI
;; ((and clip (string-prefix-p "data:image/" clip))
;; (zq/org--save-data-uri clip target)
;; (setq handled t))
;; ;; http(s) URL
;; ((and clip (string-match-p "\\`https?://" clip))
;; (zq/org--download-url-to-file clip target)
;; (setq handled t)))))
;; (unless handled
;; (user-error
;; (concat
;; "No image found in clipboard.\n\n"
;; "Tips:\n"
;; "• In your browser, use **Copy image** (not “Copy image address”)\n"
;; "• Or copy from an image viewer / screenshot tool (to clipboard)\n"
;; "• On Wayland: install `wl-clipboard` (provides `wl-paste`)\n"
;; "• On X11: install `xclip` or `xsel`\n"
;; "• Alternatively: copy an image URL or a data:image/...;base64,... and retry")))
;; ;; Insert the relative link and display inline
;; (insert (zq/org--rel-link-to target))
;; (when (derived-mode-p 'org-mode)
;; (org-display-inline-images t t))
;; (message "Saved image → %s" target))))
#+end_src