Files
org_roam/Emacs/20260407083539-emacs_config.org
Zaine Arch 74449c345a
All checks were successful
Build Roam Site / build (push) Successful in 30s
commits
2026-05-13 22:48:34 +01:00

652 lines
27 KiB
Org Mode
Raw 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
* 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)
(when (and dir (file-directory-p dir))
(directory-files-recursively dir (zq/org--image-regexp))))
(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."
(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))))
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