adding content

This commit is contained in:
2026-04-19 23:11:00 +01:00
parent a5983242f9
commit c79938f1a2
310 changed files with 4169 additions and 62 deletions

5
Emacs/20241210004247-emacs_moc.org Executable file → Normal file
View File

@@ -14,8 +14,9 @@ The purpose of this file is to store things related to emacs (that being package
- [[id:45CC3AF5-5E20-4B03-A36C-8D4BDD5CBB13][Emacs Evil Mode]]
- [[id:7d2e867e-f091-4362-a583-453f732207fe][Emacs ORG Publish]]
- [[id:168b6b10-d670-4f9d-89cf-aeb59aa644a1][Emacs Org Noter]]
- [[id:10b1f18e-b221-4ab8-bce4-b6f20ad417db][emacs-calendar]]
- [[id:10b1f18e-b221-4ab8-bce4-b6f20ad417db][Emacs Calendar/Agenda]]
- [[id:1c87e4f1-fa7e-4200-9a29-a5a98d4c7ac9][Emacs Config]]
* Resources
- A link that contains all the key bindings for emacs: [[http://xahlee.info/emacs/emacs/emacs_keybinding_list.html][Keybindings list]]
- the theme 'ef-dream' repository: [[https://github.com/protesilaos/ef-themes][ef-themes]]

0
Emacs/20241210004329-org_roam.org Executable file → Normal file
View File

0
Emacs/20241210004453-gtd.org Executable file → Normal file
View File

3
Emacs/20250218174735-emacs_stuff_keybindings.org Executable file → Normal file
View File

@@ -16,3 +16,6 @@ Type `C-h m` to get information for a major mode.
* Projectile:
- `SPC-p p` - switch project
- `SPC-p f` - find project
* Use C-u SPC X P to run powershell with args

0
Emacs/20250326002128-emacs_stuff_elisp.org Executable file → Normal file
View File

0
Emacs/20250329231658-emacs_stuff_magit.org Executable file → Normal file
View File

0
Emacs/20250420012258-emacs_stuff_evil.org Executable file → Normal file
View File

0
Emacs/20250806155335-emacs_stuff_org_publish.org Executable file → Normal file
View File

View File

@@ -0,0 +1,80 @@
:PROPERTIES:
:ID: 168b6b10-d670-4f9d-89cf-aeb59aa644a1
:END:
#+title: Emacs Org Noter
#+filetags: :emacs:org:notes:
**For the config, see [[id:1c87e4f1-fa7e-4200-9a29-a5a98d4c7ac9][Emacs Config]] for the Source of truth**
In order to create notes for a book:
1. Create an empty org roam file
2. Add the relative file path in metadata for that node, with the title ~NOTER_DOCUMENT~
See the example below (auto save and page will get filled in)
3. Hover over the Notes heading created and click ~SPC-n n~ to open org noter
#+begin_src org
,* Ikigai Notes
:PROPERTIES:
:NOTER_DOCUMENT: ../../../pdfs/_misc/Ikigai.pdf
:NOTER_AUTO_SAVE_LAST_LOCATION: t
:NOTER_PAGE: 61
:END:
#+end_src
* Config needed:
#+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
For non pdfs notes:
#+begin_src emacs-lisp
(setq org-capture-templates
'(("d" "Daily TODO" entry
(file+headline (lambda () (z/find-current-week-file)) "TODOs")
"* TODO %?\nSCHEDULED: %t\n"
:empty-lines 1)
("c" "Add to calendar.org" entry
(file+headline "~/master-folder/org_files/calendar.org" "Calendar")
"* TODO %?\nSCHEDULED: %^t\n"
:empty-lines 1)
("n" "Add notes" entry
(file+headline (lambda () (z/find-current-week-file)) "Notes")
"* Note: %?\n"
:empty-lines 1)
("w" "Weekly Review" entry
(file+function (lambda () (z/find-current-week-file)) z/goto-weekly-review)
"* Review: %?\nEntered on %U\n"
:empty-lines 1)
("b" "Book Note" entry
(file+headline (lambda () (buffer-file-name)) "Notes")
"** p. %^{Page}\n[%^{Type|Insight|Important|Quote|Confusing}] %?\n")
))
#+end_src
* Keybindings:
#+begin_src emacs-lisp
(dt/leader-keys
"n" '(:ignore t :wk "Org Noter")
"n n" '(org-noter :wk "Open org-noter")
)
#+end_src

View File

@@ -0,0 +1,66 @@
:PROPERTIES:
:ID: 10b1f18e-b221-4ab8-bce4-b6f20ad417db
:END:
#+title: Emacs Calendar/Agenda
#+filetags: :emacs:workflow:work:
**For the config, see [[id:1c87e4f1-fa7e-4200-9a29-a5a98d4c7ac9][Emacs Config]] for the Source of truth**
* Config:
#+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
** Explanation:
- This configuration sets up Emacs to sync with both a Nextcloud calendar (via CalDAV) and an Outlook calendar (via ICS). It uses a Python script to fetch and convert the Outlook ICS data into an org file format that Emacs can read. The Nextcloud calendar is synced directly using the org-caldav package. Both calendars are included in the org-agenda files, so that I can view all events in one place. The sync for the Outlook calendar runs every 30 minutes to keep it up to date.
- To sync the Outlook calendar, you call the ~my/sync-outlook-calendar~ function, which runs the Python script to fetch and convert the ICS data. The Nextcloud calendar is synced automatically by org-caldav whenever you open Emacs or refresh the calendar.

View File

@@ -0,0 +1,599 @@
: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
* 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