feat: .bookmarks file, mu4e maildirs, dir-locals format

This commit is contained in:
2026-07-17 14:54:16 -04:00
parent e997fc4608
commit 3c59427c15
2 changed files with 546 additions and 377 deletions

View File

@@ -48,11 +48,8 @@ they are implemented.
Place your private configurations. It is optional.
#+begin_src emacs-lisp
(setq org-src-coderef-format nil)
(setq org-src-coderef-format nil)
(setq user-full-name "Thierry Pouplier"
user-mail-address "tpouplier@tdnde.com"
user-mail-address "tpouplier@tdnde.com")
#+end_src
@@ -2294,39 +2291,48 @@ Cordialement,
"Cached project mu4e maildir shortcuts for re-injection when mu4e loads.")
(defun gortium/inject-project-mu4e-bookmarks ()
"Inject project-specific mu4e bookmark shortcuts [PI]/[PS]/[PD]/[PT]/[PA].
Caches them for re-injection when mu4e loads."
"Inject project mu4e maildirs into mu4e-maildir-shortcuts.
Keys [PI]/[PS]/[PD]/[PT]/[PA] (uppercase). Filters by context-tag:
only injects when the project's context-tag matches current mu4e context."
(let* ((proj-root (and (projectile-project-p) (projectile-project-root)))
(proj-tag (when proj-root
(let ((default-directory proj-root))
(gortium/read-dir-local 'gortium/project-tag))))
(context-tag (when proj-root
(let ((default-directory proj-root))
(gortium/read-dir-local 'gortium/context-tag))))
(cur-context (and (boundp 'mu4e-context-current)
(car-safe mu4e-context-current)))
(project-bm '()))
(when proj-tag
;; Only inject if context-tag matches current mu4e context
;; (if no context-tag set, always inject)
(when (and proj-tag
(or (null context-tag)
(and cur-context
(string= context-tag
(mu4e-context-name cur-context)))))
(dolist (folder '(("Inbox" . ?I)
("Sent" . ?S)
("Drafts" . ?D)
("Trash" . ?T)
("Archive" . ?A)))
(push (list :name (format "[%s] %s" proj-tag (car folder))
:query (format "maildir:/%s.%s" (car folder) proj-tag)
:key (cdr folder))
(push (cons (format "[%s] %s" proj-tag (car folder))
(format "maildir:/%s.%s" (car folder) proj-tag))
project-bm)))
;; Cache for later re-injection
;; Inject into mu4e-maildir-shortcuts
(setq gortium/project-mu4e-bookmarks project-bm)
;; Inject now if mu4e is already loaded
(when (boundp 'mu4e-bookmarks)
(setq mu4e-bookmarks
(append
(seq-filter (lambda (bm)
(if (and (listp bm) (keywordp (car-safe bm)))
(let ((key (plist-get bm :key)))
(not (member key gortium/project-mu4e-keys)))
t))
mu4e-bookmarks)
gortium/project-mu4e-bookmarks))
;; Clear caches so new bookmarks appear
(when (and (boundp 'mu4e-maildir-shortcuts) project-bm)
;; Remove stale project entries
(setq mu4e-maildir-shortcuts
(seq-filter (lambda (s)
(not (and (consp s) (stringp (car s))
(string-match "^\\[[A-Z]+\\]" (car s)))))
mu4e-maildir-shortcuts))
;; Add current project entries
(setq mu4e-maildir-shortcuts
(append mu4e-maildir-shortcuts project-bm))
;; Refresh
(setq mu4e--bookmark-items-cached nil)
;; Re-render main view if visible
(when (get-buffer "*mu4e-main*")
(with-current-buffer "*mu4e-main*"
(mu4e--main-redraw))))))
@@ -3340,23 +3346,24 @@ with a project-tagged custom agenda entry and optional CONTEXT-TAG (mu4e/agenda)
(let ((dir-locals-file (expand-file-name ".dir-locals.el" node-dir)))
(unless (file-exists-p dir-locals-file)
(with-temp-file dir-locals-file
(prin1
`((nil . ((gortium/project-tag . ,project-tag)
(gortium/project-agenda-entry .
(("P" ,node-name
((agenda "" ((org-agenda-span 14)
(org-agenda-overriding-header
,(format "📅 %s" node-name))))
(todo "TODO|NEXT|DELG|WAIT|HOLD|STRT|IDEA"
((org-agenda-overriding-header "📋 Unscheduled")
(org-agenda-skip-function
'(org-agenda-skip-entry-if 'scheduled)))))
((org-agenda-tag-filter-preset (list ,(concat "+" project-tag)))))))
,@(if context-tag
(list (cons 'gortium/context-tag context-tag))
nil))))
(current-buffer))))))
(let* ((vars `((gortium/project-tag . ,project-tag)
(gortium/project-agenda-entry .
(("P" ,node-name
((agenda "" ((org-agenda-span 14)
(org-agenda-overriding-header
,(format "📅 %s" node-name))))
(todo "TODO|NEXT|DELG|WAIT|HOLD|STRT|IDEA"
((org-agenda-overriding-header "📋 Unscheduled")
(org-agenda-skip-function
'(org-agenda-skip-entry-if 'scheduled)))))
((org-agenda-tag-filter-preset
(list ,(concat "+" project-tag)))))))
,@(if context-tag
(list (cons 'gortium/context-tag context-tag))
nil))))
(princ "((nil . " (current-buffer))
(prin1 vars (current-buffer))
(princ ")" (current-buffer)))))))
;; 5. Unified Git Integration Loop
(let ((default-directory node-dir))
@@ -3446,6 +3453,235 @@ Otherwise, prompts for a single directory, defaulting to the item under the curs
(message "Successfully batch-converted %d directories to ExoKortex nodes!" count))))
#+end_src
** Project-Aware Agenda
Reads =.dir-locals.el= from the current Projectile root and injects the
project-specific custom agenda command. Use ~SPC o P~ instead of ~SPC o a~ to
get a context-scoped agenda.
#+begin_src emacs-lisp
(defun gortium/read-dir-local (var)
"Read variable VAR from .dir-locals.el in `default-directory'.
Returns nil if file doesn't exist or VAR isn't set."
(let ((dir-locals (expand-file-name ".dir-locals.el" default-directory)))
(when (file-exists-p dir-locals)
(with-temp-buffer
(insert-file-contents dir-locals)
(goto-char (point-min))
(let* ((sexp (ignore-errors (read (current-buffer))))
(nil-entry (assq nil sexp)))
(when nil-entry
(cdr (assq var (cdr nil-entry)))))))))
(defun gortium/project-aware-agenda ()
"Launch org-agenda scoped to the current Projectile project.
Reads `gortium/project-agenda-entry' from .dir-locals.el and merges
it into `org-agenda-custom-commands'. Restricts `org-agenda-files'
to .org files under the project root."
(interactive)
(if (projectile-project-p)
(let* ((proj-root (projectile-project-root))
(default-directory proj-root)
(agenda-entry (gortium/read-dir-local 'gortium/project-agenda-entry))
(org-agenda-files (directory-files-recursively proj-root "\\.org$"))
(org-agenda-custom-commands
(append org-agenda-custom-commands
(when agenda-entry (list agenda-entry)))))
(org-agenda))
(org-agenda)))
(map! :leader
:desc "Project-aware agenda"
"o P" #'gortium/project-aware-agenda)
;; Remove stale project entries and inject current project's agenda
(defvar gortium/project-agenda-commands nil
"Cached project agenda commands.")
(defun gortium/inject-project-agenda ()
"Inject the current project's custom agenda commands into
`org-agenda-custom-commands', or remove them if no project."
(let* ((proj-root (and (projectile-project-p) (projectile-project-root)))
(project-commands nil))
(when proj-root
(let ((default-directory proj-root))
(setq project-commands
(gortium/read-dir-local 'gortium/project-agenda-entry))))
;; Remove stale project entries (key "P")
(setq org-agenda-custom-commands
(seq-filter (lambda (entry)
(not (and (stringp (car entry))
(string= "P" (car entry)))))
org-agenda-custom-commands))
;; Add current project commands
(when project-commands
(setq org-agenda-custom-commands
(append org-agenda-custom-commands project-commands)))))
;; Hook into project switching
(add-hook 'projectile-after-switch-project-hook
#'gortium/inject-project-agenda)
;; Also inject at startup and on file open in a project
(add-hook 'find-file-hook #'gortium/inject-project-agenda)
(defvar gortium/project-bookmark-names nil
"Bookmark names currently in the Plan file for the active project.")
(defun gortium//project-plan-file ()
"Return the first .org file under 1-Plan/ at the project root, or nil."
(when-let* ((root (projectile-project-root)))
(car (directory-files (expand-file-name "1-Plan" root) t "\.org\'" t))))
(defun gortium//project-tag ()
"Read the project tag from .dir-locals.el in the current project."
(when (projectile-project-p)
(let ((default-directory (projectile-project-root)))
(gortium/read-dir-local 'gortium/project-tag))))
(defun gortium//read-plan-bookmarks ()
"Parse * Bookmarks from the Plan file.
Returns alist ((NAME . URL) ...) where URL is the link target."
(when-let* ((plan-file (gortium//project-plan-file))
(_ (file-exists-p plan-file)))
(with-temp-buffer
(insert-file-contents plan-file)
(goto-char (point-min))
(when (re-search-forward "^\* Bookmarks" nil t)
(forward-line 1)
(let ((bookmarks '()))
(while (not (or (eobp) (looking-at "^\* ")))
(cond
;; ** Name\n[[file:path][Name]]
((looking-at "^\*\* \(.+\)
\[\[file:\(.+\)\]\[\(.+\)\]\]")
(push (cons (match-string 1) (match-string 2)) bookmarks)
(forward-line 2))
;; standalone [[url][name]]
((looking-at "\[\[\(.+\)\]\[\(.+\)\]\]")
(unless (string-prefix-p "mu4e:" (match-string 1))
(push (cons (match-string 2) (match-string 1)) bookmarks))
(forward-line 1))
(t (forward-line 1))))
(setq gortium/project-bookmark-names (mapcar #'car bookmarks))
(nreverse bookmarks))))))
(defun gortium//append-bookmark-to-plan (name url)
"Add a bookmark NAME with URL to the project's .bookmarks file."
(when (projectile-project-p)
(let ((proj-file (expand-file-name ".bookmarks" (projectile-project-root))))
(bookmark-store name `((filename . ,url)) nil)
(bookmark-save nil proj-file)
(message "Bookmark added to .bookmarks"))))
(defun gortium//remove-bookmark-from-plan (name)
"Remove a bookmark entry from * Bookmarks in the Plan file."
(when-let* ((plan-file (gortium//project-plan-file)))
(with-temp-buffer
(insert-file-contents plan-file)
(goto-char (point-min))
(when (re-search-forward (format "^\*\* %s
" (regexp-quote name)) nil t)
(forward-line -1)
(let ((start (point)))
(forward-line 2)
(delete-region start (point)))
(write-region (point-min) (point-max) plan-file nil 'silent)
(message "Bookmark '%s' removed from Plan file" name)
(setq gortium/project-bookmark-names
(delete name gortium/project-bookmark-names))))))
(defun gortium/bookmark-set ()
"Save a bookmark. Prompts: (g)lobal or (p)roject.
Project bookmarks go to .bookmarks file in project root."
(interactive)
(let ((choice (read-char-choice "Save: (g)lobal / (p)roject? " '(?g ?p))))
(pcase choice
(?g (call-interactively 'bookmark-set))
(?p
(if (projectile-project-p)
(let* ((tag (or (gortium//project-tag) "project"))
(name (read-string (format "[%s] Bookmark name: " tag)))
(proj-file (expand-file-name
".bookmarks" (projectile-project-root)))
(bm (bookmark-make-record)))
(bookmark-store name (cdr bm) nil)
(bookmark-save nil proj-file)
(message "Project bookmark saved to .bookmarks"))
(user-error "Not in a Projectile project"))))))
(defun gortium/list-project-bookmarks ()
"Show project bookmarks from .bookmarks in project root."
(interactive)
(if (not (projectile-project-p))
(user-error "Not in a project")
(let ((proj-file (expand-file-name ".bookmarks" (projectile-project-root))))
(if (not (file-exists-p proj-file))
(message "No .bookmarks file in this project.")
(let ((saved-alist (copy-tree bookmark-alist))
(saved-file bookmark-default-file))
(setq bookmark-default-file proj-file)
(bookmark-load proj-file t)
(bookmark-bmenu-list)
(with-current-buffer "*Bookmark List*"
(add-hook 'kill-buffer-hook
(lambda ()
(ignore-errors (bookmark-save))
(setq bookmark-default-file saved-file)
(setq bookmark-alist saved-alist))
nil t)))))))
(defun gortium//sync-plan-to-bookmark-file (bookmarks)
"Write BOOKMARKS alist back to the Plan file under * Bookmarks."
(when-let* ((plan-file (gortium//project-plan-file)))
(with-temp-buffer
(insert-file-contents plan-file)
(when (re-search-forward "^\* Bookmarks" nil t)
(forward-line 1)
(let ((start (point)))
(if (re-search-forward "^\* " nil t)
(forward-line -1)
(goto-char (point-max)))
(delete-region start (point)))
(dolist (bm (nreverse (copy-tree bookmarks)))
(insert (format "[[%s][%s]]
" (cdr bm) (car bm))))
(write-region (point-min) (point-max) plan-file nil 'silent)))))
(defun gortium/transfer-to-project ()
"Transfer a global bookmark to the current project's Plan file."
(interactive)
(if (projectile-project-p)
(let* ((name (completing-read "Transfer to project: " (bookmark-all-names) nil t))
(file (bookmark-location name)))
(when (and name file (y-or-n-p (format "Transfer '%s' to project? " name)))
(gortium//append-bookmark-to-plan name file)
(bookmark-delete name)
(bookmark-save)
(message "Transferred '%s' to project %s" name
(or (gortium//project-tag) ""))))
(user-error "Not in a Projectile project")))
(defun gortium/transfer-to-global ()
"Transfer a project bookmark to the global bookmarks list."
(interactive)
(let* ((bookmarks (gortium//read-plan-bookmarks))
(names (mapcar #'car bookmarks)))
(if names
(let* ((name (completing-read "Transfer to global: " names nil t))
(url (cdr (assoc name bookmarks))))
(when (and name url (y-or-n-p (format "Transfer '%s' to global? " name)))
(bookmark-store name `((filename . ,url)) t)
(bookmark-save)
(gortium//remove-bookmark-from-plan name)
(message "Transferred '%s' to global" name)))
(message "No project bookmarks to transfer."))))
;; Bindings
(map! :leader
:desc "Set bookmark (global/project)"
"b m" #'gortium/bookmark-set
:desc "Jump to project bookmark"
"p RET" #'gortium/list-project-bookmarks)
#+end_src
** Refile task to Roam dailies
#+begin_src emacs-lisp
@@ -3510,54 +3746,6 @@ fallback to today's daily note. Ensures the daily has an Org-roam ID."
headline
(file-name-nondirectory daily-file)))))
#+end_src
** Project-Aware Agenda
Reads =.dir-locals.el= from the current Projectile root and injects the
project-specific custom agenda command. Use ~SPC o P~ instead of ~SPC o a~ to
get a context-scoped agenda.
#+begin_src emacs-lisp
(defun gortium/read-dir-local (var)
"Read variable VAR from .dir-locals.el in `default-directory'.
Returns nil if file doesn't exist or VAR isn't set."
(let ((dir-locals (expand-file-name ".dir-locals.el" default-directory)))
(when (file-exists-p dir-locals)
(with-temp-buffer
(insert-file-contents dir-locals)
(goto-char (point-min))
(let* ((sexp (ignore-errors (read (current-buffer))))
(nil-entry (assq nil sexp)))
(when nil-entry
(cdr (assq var (cdr nil-entry)))))))))
(defun gortium/project-aware-agenda ()
"Launch org-agenda scoped to the current Projectile project.
Reads `gortium/project-agenda-entry' from .dir-locals.el and merges
it into `org-agenda-custom-commands'. Restricts `org-agenda-files'
to .org files under the project root."
(interactive)
(if (projectile-project-p)
(let* ((proj-root (projectile-project-root))
(default-directory proj-root)
(agenda-entry (gortium/read-dir-local 'gortium/project-agenda-entry))
(org-agenda-files (directory-files-recursively proj-root "\\.org$"))
(org-agenda-custom-commands
(append org-agenda-custom-commands
(when agenda-entry (list agenda-entry)))))
(org-agenda))
(org-agenda)))
(map! :leader
:desc "Project-aware agenda"
"o P" #'gortium/project-aware-agenda)
;; Load project bookmark functions
(require 'gortium-bookmarks nil t)
#+end_src
#+RESULTS:
: gortium/project-aware-agenda
** Emacs-Everywhere for Hyprland
#+begin_src emacs-lisp
@@ -3738,214 +3926,4 @@ Need chrome... :(
** Project-Aware Agenda
#+begin_src emacs-lisp :tangle no
;; Remove stale project entries and inject current project's agenda
(defvar gortium/project-agenda-commands nil
"Cached project agenda commands.")
(defun gortium/inject-project-agenda ()
"Inject the current project's custom agenda commands into
`org-agenda-custom-commands', or remove them if no project."
(let* ((proj-root (and (projectile-project-p) (projectile-project-root)))
(project-commands nil))
(when proj-root
(let ((default-directory proj-root))
(setq project-commands
(gortium/read-dir-local 'gortium/project-agenda-entry))))
;; Remove stale project entries (key "P")
(setq org-agenda-custom-commands
(seq-filter (lambda (entry)
(not (and (stringp (car entry))
(string= "P" (car entry)))))
org-agenda-custom-commands))
;; Add current project commands
(when project-commands
(setq org-agenda-custom-commands
(append org-agenda-custom-commands project-commands)))))
;; Hook into project switching
(add-hook 'projectile-after-switch-project-hook
#'gortium/inject-project-agenda)
;; Also inject at startup and on file open in a project
(add-hook 'find-file-hook #'gortium/inject-project-agenda)
;; Load project bookmark functions
(require 'gortium-bookmarks nil t)
;; Load project bookmark functions
(require 'gortium-bookmarks nil t)
(defvar gortium/project-bookmark-names nil
"Bookmark names currently in the Plan file for the active project.")
(defun gortium//project-plan-file ()
"Return the first .org file under 1-Plan/ at the project root, or nil."
(when-let* ((root (projectile-project-root)))
(car (directory-files (expand-file-name "1-Plan" root) t "\.org\'" t))))
(defun gortium//project-tag ()
"Read the project tag from .dir-locals.el in the current project."
(when (projectile-project-p)
(let ((default-directory (projectile-project-root)))
(gortium/read-dir-local 'gortium/project-tag))))
(defun gortium//read-plan-bookmarks ()
"Parse * Bookmarks from the Plan file.
Returns alist ((NAME . URL) ...) where URL is the link target."
(when-let* ((plan-file (gortium//project-plan-file))
(_ (file-exists-p plan-file)))
(with-temp-buffer
(insert-file-contents plan-file)
(goto-char (point-min))
(when (re-search-forward "^\* Bookmarks" nil t)
(forward-line 1)
(let ((bookmarks '()))
(while (not (or (eobp) (looking-at "^\* ")))
(cond
;; ** Name\n[[file:path][Name]]
((looking-at "^\*\* \(.+\)
\[\[file:\(.+\)\]\[\(.+\)\]\]")
(push (cons (match-string 1) (match-string 2)) bookmarks)
(forward-line 2))
;; standalone [[url][name]]
((looking-at "\[\[\(.+\)\]\[\(.+\)\]\]")
(unless (string-prefix-p "mu4e:" (match-string 1))
(push (cons (match-string 2) (match-string 1)) bookmarks))
(forward-line 1))
(t (forward-line 1))))
(setq gortium/project-bookmark-names (mapcar #'car bookmarks))
(nreverse bookmarks))))))
(defun gortium//append-bookmark-to-plan (name url)
"Append a bookmark as [[url][name]] under * Bookmarks in the Plan file.
NAME is the display name, URL is the link target (file path or web URL)."
(when-let* ((plan-file (gortium//project-plan-file)))
(with-temp-buffer
(insert-file-contents plan-file)
(goto-char (point-min))
(if (re-search-forward "^\* Bookmarks" nil t)
(forward-line 1)
(goto-char (point-max))
(insert "\n* Bookmarks\n"))
(insert (format "[[%s][%s]]
" url name))
(write-region (point-min) (point-max) plan-file nil 'silent)
(message "Bookmark added to %s" (file-name-nondirectory plan-file))
(push name gortium/project-bookmark-names))))
(defun gortium//remove-bookmark-from-plan (name)
"Remove a bookmark entry from * Bookmarks in the Plan file."
(when-let* ((plan-file (gortium//project-plan-file)))
(with-temp-buffer
(insert-file-contents plan-file)
(goto-char (point-min))
(when (re-search-forward (format "^\*\* %s
" (regexp-quote name)) nil t)
(forward-line -1)
(let ((start (point)))
(forward-line 2)
(delete-region start (point)))
(write-region (point-min) (point-max) plan-file nil 'silent)
(message "Bookmark '%s' removed from Plan file" name)
(setq gortium/project-bookmark-names
(delete name gortium/project-bookmark-names))))))
(defun gortium/bookmark-set ()
"Save a bookmark. Prompts: (g)lobal or (p)roject.
Project bookmarks go to * Bookmarks in the Plan file.
The prompt shows the project tag so you can verify the target."
(interactive)
(let ((choice (read-char-choice "Save: (g)lobal / (p)roject? " '(?g ?p))))
(pcase choice
(?g (call-interactively 'bookmark-set))
(?p
(if (projectile-project-p)
(let* ((tag (or (gortium//project-tag) "project"))
(name (read-string (format "[%s] Bookmark name: " tag)))
(file (buffer-file-name)))
(gortium//append-bookmark-to-plan name file)
(message "Project bookmark saved to Plan file."))
(user-error "Not in a Projectile project"))))))
(defun gortium/list-project-bookmarks ()
"Show project bookmarks in the bookmark menu (with categories).
Temporarily replaces bookmark-alist so the menu shows project bookmarks
in their native categories. On quit restores global bookmarks."
(interactive)
(let* ((bookmarks (gortium//read-plan-bookmarks))
(names (mapcar #'car bookmarks)))
(if (not names)
(message "No bookmarks found in this project's Plan file.")
(let ((saved-alist (copy-tree bookmark-alist)))
(setq bookmark-alist nil)
(dolist (bm (nreverse (copy-tree bookmarks)))
(bookmark-store (car bm) `((filename . ,(cdr bm))) nil))
(bookmark-bmenu-list)
(with-current-buffer "*Bookmark List*"
(add-hook 'kill-buffer-hook
(lambda ()
(setq bookmark-alist saved-alist)
;; Write changes back to Plan file
(when-let* ((plan-file (gortium//project-plan-file))
(entries (bookmark-maybe-sort-alist bookmark-alist)))
(gortium//sync-plan-to-bookmark-file
(mapcar (lambda (e)
(cons (car e)
(alist-get 'filename (cdr e))))
entries))))
nil t))))))
(defun gortium//sync-plan-to-bookmark-file (bookmarks)
"Write BOOKMARKS alist back to the Plan file under * Bookmarks."
(when-let* ((plan-file (gortium//project-plan-file)))
(with-temp-buffer
(insert-file-contents plan-file)
(when (re-search-forward "^\* Bookmarks" nil t)
(forward-line 1)
(let ((start (point)))
(if (re-search-forward "^\* " nil t)
(forward-line -1)
(goto-char (point-max)))
(delete-region start (point)))
(dolist (bm (nreverse (copy-tree bookmarks)))
(insert (format "[[%s][%s]]
" (cdr bm) (car bm))))
(write-region (point-min) (point-max) plan-file nil 'silent)))))
(defun gortium/transfer-to-project ()
"Transfer a global bookmark to the current project's Plan file."
(interactive)
(if (projectile-project-p)
(let* ((name (completing-read "Transfer to project: " (bookmark-all-names) nil t))
(file (bookmark-location name)))
(when (and name file (y-or-n-p (format "Transfer '%s' to project? " name)))
(gortium//append-bookmark-to-plan name file)
(bookmark-delete name)
(bookmark-save)
(message "Transferred '%s' to project %s" name
(or (gortium//project-tag) ""))))
(user-error "Not in a Projectile project")))
(defun gortium/transfer-to-global ()
"Transfer a project bookmark to the global bookmarks list."
(interactive)
(let* ((bookmarks (gortium//read-plan-bookmarks))
(names (mapcar #'car bookmarks)))
(if names
(let* ((name (completing-read "Transfer to global: " names nil t))
(url (cdr (assoc name bookmarks))))
(when (and name url (y-or-n-p (format "Transfer '%s' to global? " name)))
(bookmark-store name `((filename . ,url)) t)
(bookmark-save)
(gortium//remove-bookmark-from-plan name)
(message "Transferred '%s' to global" name)))
(message "No project bookmarks to transfer."))))
;; Bindings
(map! :leader
:desc "Set bookmark (global/project)"
"b m" #'gortium/bookmark-set
:desc "Jump to project bookmark"
"p RET" #'gortium/list-project-bookmarks)
#+end_src

View File

@@ -1,10 +1,7 @@
;;; $DOOMDIR/config.el -*- lexical-binding: t; -*-
(setq org-src-coderef-format nil)
(setq org-src-coderef-format nil)
(setq user-full-name "Thierry Pouplier"
user-mail-address "tpouplier@tdnde.com"
user-mail-address "tpouplier@tdnde.com")
(setq doom-font (font-spec :family "JetBrainsMono Nerd Font" :size 16)
@@ -1448,39 +1445,48 @@ Cordialement,
"Cached project mu4e maildir shortcuts for re-injection when mu4e loads.")
(defun gortium/inject-project-mu4e-bookmarks ()
"Inject project-specific mu4e bookmark shortcuts [PI]/[PS]/[PD]/[PT]/[PA].
Caches them for re-injection when mu4e loads."
"Inject project mu4e maildirs into mu4e-maildir-shortcuts.
Keys [PI]/[PS]/[PD]/[PT]/[PA] (uppercase). Filters by context-tag:
only injects when the project's context-tag matches current mu4e context."
(let* ((proj-root (and (projectile-project-p) (projectile-project-root)))
(proj-tag (when proj-root
(let ((default-directory proj-root))
(gortium/read-dir-local 'gortium/project-tag))))
(context-tag (when proj-root
(let ((default-directory proj-root))
(gortium/read-dir-local 'gortium/context-tag))))
(cur-context (and (boundp 'mu4e-context-current)
(car-safe mu4e-context-current)))
(project-bm '()))
(when proj-tag
;; Only inject if context-tag matches current mu4e context
;; (if no context-tag set, always inject)
(when (and proj-tag
(or (null context-tag)
(and cur-context
(string= context-tag
(mu4e-context-name cur-context)))))
(dolist (folder '(("Inbox" . ?I)
("Sent" . ?S)
("Drafts" . ?D)
("Trash" . ?T)
("Archive" . ?A)))
(push (list :name (format "[%s] %s" proj-tag (car folder))
:query (format "maildir:/%s.%s" (car folder) proj-tag)
:key (cdr folder))
(push (cons (format "[%s] %s" proj-tag (car folder))
(format "maildir:/%s.%s" (car folder) proj-tag))
project-bm)))
;; Cache for later re-injection
;; Inject into mu4e-maildir-shortcuts
(setq gortium/project-mu4e-bookmarks project-bm)
;; Inject now if mu4e is already loaded
(when (boundp 'mu4e-bookmarks)
(setq mu4e-bookmarks
(append
(seq-filter (lambda (bm)
(if (and (listp bm) (keywordp (car-safe bm)))
(let ((key (plist-get bm :key)))
(not (member key gortium/project-mu4e-keys)))
t))
mu4e-bookmarks)
gortium/project-mu4e-bookmarks))
;; Clear caches so new bookmarks appear
(when (and (boundp 'mu4e-maildir-shortcuts) project-bm)
;; Remove stale project entries
(setq mu4e-maildir-shortcuts
(seq-filter (lambda (s)
(not (and (consp s) (stringp (car s))
(string-match "^\\[[A-Z]+\\]" (car s)))))
mu4e-maildir-shortcuts))
;; Add current project entries
(setq mu4e-maildir-shortcuts
(append mu4e-maildir-shortcuts project-bm))
;; Refresh
(setq mu4e--bookmark-items-cached nil)
;; Re-render main view if visible
(when (get-buffer "*mu4e-main*")
(with-current-buffer "*mu4e-main*"
(mu4e--main-redraw))))))
@@ -2418,23 +2424,24 @@ with a project-tagged custom agenda entry and optional CONTEXT-TAG (mu4e/agenda)
(let ((dir-locals-file (expand-file-name ".dir-locals.el" node-dir)))
(unless (file-exists-p dir-locals-file)
(with-temp-file dir-locals-file
(prin1
`((nil . ((gortium/project-tag . ,project-tag)
(gortium/project-agenda-entry .
(("P" ,node-name
((agenda "" ((org-agenda-span 14)
(org-agenda-overriding-header
,(format "📅 %s" node-name))))
(todo "TODO|NEXT|DELG|WAIT|HOLD|STRT|IDEA"
((org-agenda-overriding-header "📋 Unscheduled")
(org-agenda-skip-function
'(org-agenda-skip-entry-if 'scheduled)))))
((org-agenda-tag-filter-preset (list ,(concat "+" project-tag)))))))
,@(if context-tag
(list (cons 'gortium/context-tag context-tag))
nil))))
(current-buffer))))))
(let* ((vars `((gortium/project-tag . ,project-tag)
(gortium/project-agenda-entry .
(("P" ,node-name
((agenda "" ((org-agenda-span 14)
(org-agenda-overriding-header
,(format "📅 %s" node-name))))
(todo "TODO|NEXT|DELG|WAIT|HOLD|STRT|IDEA"
((org-agenda-overriding-header "📋 Unscheduled")
(org-agenda-skip-function
'(org-agenda-skip-entry-if 'scheduled)))))
((org-agenda-tag-filter-preset
(list ,(concat "+" project-tag)))))))
,@(if context-tag
(list (cons 'gortium/context-tag context-tag))
nil))))
(princ "((nil . " (current-buffer))
(prin1 vars (current-buffer))
(princ ")" (current-buffer)))))))
;; 5. Unified Git Integration Loop
(let ((default-directory node-dir))
@@ -2523,6 +2530,227 @@ Otherwise, prompts for a single directory, defaulting to the item under the curs
(message "Directory successfully converted to ExoKortex node."))
(message "Successfully batch-converted %d directories to ExoKortex nodes!" count))))
(defun gortium/read-dir-local (var)
"Read variable VAR from .dir-locals.el in `default-directory'.
Returns nil if file doesn't exist or VAR isn't set."
(let ((dir-locals (expand-file-name ".dir-locals.el" default-directory)))
(when (file-exists-p dir-locals)
(with-temp-buffer
(insert-file-contents dir-locals)
(goto-char (point-min))
(let* ((sexp (ignore-errors (read (current-buffer))))
(nil-entry (assq nil sexp)))
(when nil-entry
(cdr (assq var (cdr nil-entry)))))))))
(defun gortium/project-aware-agenda ()
"Launch org-agenda scoped to the current Projectile project.
Reads `gortium/project-agenda-entry' from .dir-locals.el and merges
it into `org-agenda-custom-commands'. Restricts `org-agenda-files'
to .org files under the project root."
(interactive)
(if (projectile-project-p)
(let* ((proj-root (projectile-project-root))
(default-directory proj-root)
(agenda-entry (gortium/read-dir-local 'gortium/project-agenda-entry))
(org-agenda-files (directory-files-recursively proj-root "\\.org$"))
(org-agenda-custom-commands
(append org-agenda-custom-commands
(when agenda-entry (list agenda-entry)))))
(org-agenda))
(org-agenda)))
(map! :leader
:desc "Project-aware agenda"
"o P" #'gortium/project-aware-agenda)
;; Remove stale project entries and inject current project's agenda
(defvar gortium/project-agenda-commands nil
"Cached project agenda commands.")
(defun gortium/inject-project-agenda ()
"Inject the current project's custom agenda commands into
`org-agenda-custom-commands', or remove them if no project."
(let* ((proj-root (and (projectile-project-p) (projectile-project-root)))
(project-commands nil))
(when proj-root
(let ((default-directory proj-root))
(setq project-commands
(gortium/read-dir-local 'gortium/project-agenda-entry))))
;; Remove stale project entries (key "P")
(setq org-agenda-custom-commands
(seq-filter (lambda (entry)
(not (and (stringp (car entry))
(string= "P" (car entry)))))
org-agenda-custom-commands))
;; Add current project commands
(when project-commands
(setq org-agenda-custom-commands
(append org-agenda-custom-commands project-commands)))))
;; Hook into project switching
(add-hook 'projectile-after-switch-project-hook
#'gortium/inject-project-agenda)
;; Also inject at startup and on file open in a project
(add-hook 'find-file-hook #'gortium/inject-project-agenda)
(defvar gortium/project-bookmark-names nil
"Bookmark names currently in the Plan file for the active project.")
(defun gortium//project-plan-file ()
"Return the first .org file under 1-Plan/ at the project root, or nil."
(when-let* ((root (projectile-project-root)))
(car (directory-files (expand-file-name "1-Plan" root) t "\.org\'" t))))
(defun gortium//project-tag ()
"Read the project tag from .dir-locals.el in the current project."
(when (projectile-project-p)
(let ((default-directory (projectile-project-root)))
(gortium/read-dir-local 'gortium/project-tag))))
(defun gortium//read-plan-bookmarks ()
"Parse * Bookmarks from the Plan file.
Returns alist ((NAME . URL) ...) where URL is the link target."
(when-let* ((plan-file (gortium//project-plan-file))
(_ (file-exists-p plan-file)))
(with-temp-buffer
(insert-file-contents plan-file)
(goto-char (point-min))
(when (re-search-forward "^\* Bookmarks" nil t)
(forward-line 1)
(let ((bookmarks '()))
(while (not (or (eobp) (looking-at "^\* ")))
(cond
;; ** Name\n[[file:path][Name]]
((looking-at "^\*\* \(.+\)
\[\[file:\(.+\)\]\[\(.+\)\]\]")
(push (cons (match-string 1) (match-string 2)) bookmarks)
(forward-line 2))
;; standalone [[url][name]]
((looking-at "\[\[\(.+\)\]\[\(.+\)\]\]")
(unless (string-prefix-p "mu4e:" (match-string 1))
(push (cons (match-string 2) (match-string 1)) bookmarks))
(forward-line 1))
(t (forward-line 1))))
(setq gortium/project-bookmark-names (mapcar #'car bookmarks))
(nreverse bookmarks))))))
(defun gortium//append-bookmark-to-plan (name url)
"Add a bookmark NAME with URL to the project's .bookmarks file."
(when (projectile-project-p)
(let ((proj-file (expand-file-name ".bookmarks" (projectile-project-root))))
(bookmark-store name `((filename . ,url)) nil)
(bookmark-save nil proj-file)
(message "Bookmark added to .bookmarks"))))
(defun gortium//remove-bookmark-from-plan (name)
"Remove a bookmark entry from * Bookmarks in the Plan file."
(when-let* ((plan-file (gortium//project-plan-file)))
(with-temp-buffer
(insert-file-contents plan-file)
(goto-char (point-min))
(when (re-search-forward (format "^\*\* %s
" (regexp-quote name)) nil t)
(forward-line -1)
(let ((start (point)))
(forward-line 2)
(delete-region start (point)))
(write-region (point-min) (point-max) plan-file nil 'silent)
(message "Bookmark '%s' removed from Plan file" name)
(setq gortium/project-bookmark-names
(delete name gortium/project-bookmark-names))))))
(defun gortium/bookmark-set ()
"Save a bookmark. Prompts: (g)lobal or (p)roject.
Project bookmarks go to .bookmarks file in project root."
(interactive)
(let ((choice (read-char-choice "Save: (g)lobal / (p)roject? " '(?g ?p))))
(pcase choice
(?g (call-interactively 'bookmark-set))
(?p
(if (projectile-project-p)
(let* ((tag (or (gortium//project-tag) "project"))
(name (read-string (format "[%s] Bookmark name: " tag)))
(proj-file (expand-file-name
".bookmarks" (projectile-project-root)))
(bm (bookmark-make-record)))
(bookmark-store name (cdr bm) nil)
(bookmark-save nil proj-file)
(message "Project bookmark saved to .bookmarks"))
(user-error "Not in a Projectile project"))))))
(defun gortium/list-project-bookmarks ()
"Show project bookmarks from .bookmarks in project root."
(interactive)
(if (not (projectile-project-p))
(user-error "Not in a project")
(let ((proj-file (expand-file-name ".bookmarks" (projectile-project-root))))
(if (not (file-exists-p proj-file))
(message "No .bookmarks file in this project.")
(let ((saved-alist (copy-tree bookmark-alist))
(saved-file bookmark-default-file))
(setq bookmark-default-file proj-file)
(bookmark-load proj-file t)
(bookmark-bmenu-list)
(with-current-buffer "*Bookmark List*"
(add-hook 'kill-buffer-hook
(lambda ()
(ignore-errors (bookmark-save))
(setq bookmark-default-file saved-file)
(setq bookmark-alist saved-alist))
nil t)))))))
(defun gortium//sync-plan-to-bookmark-file (bookmarks)
"Write BOOKMARKS alist back to the Plan file under * Bookmarks."
(when-let* ((plan-file (gortium//project-plan-file)))
(with-temp-buffer
(insert-file-contents plan-file)
(when (re-search-forward "^\* Bookmarks" nil t)
(forward-line 1)
(let ((start (point)))
(if (re-search-forward "^\* " nil t)
(forward-line -1)
(goto-char (point-max)))
(delete-region start (point)))
(dolist (bm (nreverse (copy-tree bookmarks)))
(insert (format "[[%s][%s]]
" (cdr bm) (car bm))))
(write-region (point-min) (point-max) plan-file nil 'silent)))))
(defun gortium/transfer-to-project ()
"Transfer a global bookmark to the current project's Plan file."
(interactive)
(if (projectile-project-p)
(let* ((name (completing-read "Transfer to project: " (bookmark-all-names) nil t))
(file (bookmark-location name)))
(when (and name file (y-or-n-p (format "Transfer '%s' to project? " name)))
(gortium//append-bookmark-to-plan name file)
(bookmark-delete name)
(bookmark-save)
(message "Transferred '%s' to project %s" name
(or (gortium//project-tag) ""))))
(user-error "Not in a Projectile project")))
(defun gortium/transfer-to-global ()
"Transfer a project bookmark to the global bookmarks list."
(interactive)
(let* ((bookmarks (gortium//read-plan-bookmarks))
(names (mapcar #'car bookmarks)))
(if names
(let* ((name (completing-read "Transfer to global: " names nil t))
(url (cdr (assoc name bookmarks))))
(when (and name url (y-or-n-p (format "Transfer '%s' to global? " name)))
(bookmark-store name `((filename . ,url)) t)
(bookmark-save)
(gortium//remove-bookmark-from-plan name)
(message "Transferred '%s' to global" name)))
(message "No project bookmarks to transfer."))))
;; Bindings
(map! :leader
:desc "Set bookmark (global/project)"
"b m" #'gortium/bookmark-set
:desc "Jump to project bookmark"
"p RET" #'gortium/list-project-bookmarks)
(defun gortium/refile-to-today-daily ()
"Refile the current subtree under the 'Journal' headline in today's daily note.
This version uses `org-roam-dailies-capture-today` to ensure the daily note
@@ -2582,43 +2810,6 @@ fallback to today's daily note. Ensures the daily has an Org-roam ID."
headline
(file-name-nondirectory daily-file)))))
(defun gortium/read-dir-local (var)
"Read variable VAR from .dir-locals.el in `default-directory'.
Returns nil if file doesn't exist or VAR isn't set."
(let ((dir-locals (expand-file-name ".dir-locals.el" default-directory)))
(when (file-exists-p dir-locals)
(with-temp-buffer
(insert-file-contents dir-locals)
(goto-char (point-min))
(let* ((sexp (ignore-errors (read (current-buffer))))
(nil-entry (assq nil sexp)))
(when nil-entry
(cdr (assq var (cdr nil-entry)))))))))
(defun gortium/project-aware-agenda ()
"Launch org-agenda scoped to the current Projectile project.
Reads `gortium/project-agenda-entry' from .dir-locals.el and merges
it into `org-agenda-custom-commands'. Restricts `org-agenda-files'
to .org files under the project root."
(interactive)
(if (projectile-project-p)
(let* ((proj-root (projectile-project-root))
(default-directory proj-root)
(agenda-entry (gortium/read-dir-local 'gortium/project-agenda-entry))
(org-agenda-files (directory-files-recursively proj-root "\\.org$"))
(org-agenda-custom-commands
(append org-agenda-custom-commands
(when agenda-entry (list agenda-entry)))))
(org-agenda))
(org-agenda)))
(map! :leader
:desc "Project-aware agenda"
"o P" #'gortium/project-aware-agenda)
;; Load project bookmark functions
(require 'gortium-bookmarks nil t)
;; Like Emacs everywhere, but work in hyprland
(defun thanos/wtype-text (text)
"Process TEXT for wtype, handling newlines properly."