652 lines
27 KiB
Org Mode
Executable File
652 lines
27 KiB
Org Mode
Executable File
: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 PROFILE’s 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 PROFILE’s assets dir.
|
||
|
||
;; Interactively:
|
||
;; - Prompts for PROFILE (from `zq/org-asset-profiles`).
|
||
;; - Presents only image files under that profile’s 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
|