fix: dir-locals bookmarks mu4e project-agenda (user+gemini)

This commit is contained in:
2026-07-17 20:48:33 -04:00
parent 0aae255348
commit 11fb27b641
2 changed files with 240 additions and 143 deletions

View File

@@ -2287,60 +2287,68 @@ Cordialement,
(defconst gortium/project-mu4e-keys '(?I ?S ?D ?T ?A)
"Character keys used for project mu4e maildir shortcuts (uppercase avoids conflicts).")
(defvar gortium/project-mu4e-bookmarks nil
"Cached project mu4e maildir shortcuts for re-injection when mu4e loads.")
(defun gortium/inject-project-mu4e-bookmarks ()
"Inject project mu4e maildirs into mu4e-maildir-shortcuts.
Keys [PI]/[PS]/[PD]/[PT]/[PA] (uppercase). Filters by context-tag:
Keys [I]/[S]/[D]/[T]/[A] (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 '()))
;; 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 (cons (format "maildir:/%s.%s" (car folder) proj-tag)
(cdr folder))
project-bm)))
;; Inject into mu4e-maildir-shortcuts
(setq gortium/project-mu4e-bookmarks project-bm)
(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)
(when (get-buffer "*mu4e-main*")
(with-current-buffer "*mu4e-main*"
(mu4e--main-redraw))))))
(interactive)
(when (boundp 'mu4e-maildir-shortcuts)
(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))))
;; Extract the context name safely
(cur-context-name (and (boundp 'mu4e-context-current)
mu4e-context-current
(mu4e-context-name mu4e-context-current)))
(project-shortcuts '()))
(add-hook 'projectile-after-switch-project-hook
#'gortium/inject-project-mu4e-bookmarks)
;; 1. Always strip out previous project shortcuts safely
(setq mu4e-maildir-shortcuts
(seq-filter (lambda (shortcut)
(let ((key (if (listp (cdr shortcut))
(plist-get shortcut :key)
(cdr shortcut))))
(not (memq key gortium/project-mu4e-keys))))
mu4e-maildir-shortcuts))
;; 2. Determine whether to inject and log the reason to *Messages*
(cond
((not proj-tag)
;; No tag found, do nothing silently (prevents spam on non-project files)
nil)
((and context-tag cur-context-name (not (string= context-tag cur-context-name)))
(message "[mu4e Project] Skipped: .dir-locals context '%s' does not match active mu4e context '%s'."
context-tag cur-context-name))
(t
;; 3. Build and inject the shortcuts
(dolist (folder '(("Inbox" . ?I)
("Sent" . ?S)
("Drafts" . ?D)
("Trash" . ?T)
("Archive" . ?A)))
(push `(:maildir ,(format "/%s.%s" (car folder) proj-tag)
:key ,(cdr folder)
:name ,(format "[%s] %s" proj-tag (car folder)))
project-shortcuts))
(setq mu4e-maildir-shortcuts
(append mu4e-maildir-shortcuts (nreverse project-shortcuts)))
(message "[mu4e Project] Successfully injected maildirs for project tag: %s" proj-tag))))))
;; Apply the hooks
(add-hook 'projectile-after-switch-project-hook #'gortium/inject-project-mu4e-bookmarks)
(add-hook 'find-file-hook #'gortium/inject-project-mu4e-bookmarks)
(add-hook 'mu4e-main-mode-hook #'gortium/inject-project-mu4e-bookmarks)
;; CRITICAL: Re-inject AFTER mu4e applies its context variables
(add-hook 'mu4e-context-changed-hook #'gortium/inject-project-mu4e-bookmarks)
(after! mu4e
(gortium/inject-project-mu4e-bookmarks))
#+end_src
@@ -3590,6 +3598,7 @@ Saves only project bookmarks without contaminating global list."
;; Restore globals
(setq bookmark-alist saved-alist)
(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)))
@@ -3637,6 +3646,34 @@ Project bookmarks go to .bookmarks file in project root."
(message "Project bookmark \"%s\" saved to .bookmarks" name))
(user-error "Not in a Projectile project"))))))
(defun gortium/bookmark-delete ()
"Delete a bookmark. Prompts: (g)lobal or (p)roject.
Project bookmarks are deleted from the .bookmarks file in the project root."
(interactive)
(let ((choice (read-char-choice "Delete: (g)lobal / (p)roject? " '(?g ?p))))
(pcase choice
(?g (call-interactively 'bookmark-delete))
(?p
(if (not (projectile-project-p))
(user-error "Not in a Projectile project")
(let ((proj-file (expand-file-name ".bookmarks" (projectile-project-root))))
(if (not (file-exists-p proj-file))
(message "No project bookmarks found.")
;; Dynamically bind the project bookmarks
(let* ((bookmark-alist (with-temp-buffer
(insert-file-contents proj-file)
(read (current-buffer))))
(names (bookmark-all-names)))
(if (not names)
(message "No bookmarks in project.")
(let ((bm-name (completing-read "Delete Project Bookmark: " names nil t)))
(when (and bm-name (not (string-empty-p bm-name)))
;; Delete from the temporarily bound alist and save
(bookmark-delete bm-name nil)
(bookmark-save nil proj-file t)
(message "Deleted project bookmark: %s" bm-name))))))))))))
(defun gortium/sync-plan-to-bookmark-file (bookmarks)
"Write BOOKMARKS alist back to the Plan file under * Bookmarks."
@@ -3700,34 +3737,48 @@ Project bookmarks go to .bookmarks file in project root."
(message "Transferred '%s' to global" name)))))))
(defun gortium/list-project-bookmarks ()
"Show project bookmarks menu — restores globals after RET jump and on q."
"Jump to a project bookmark using standard completion.
Keeps global bookmarks completely uncontaminated."
(interactive)
(if (not (projectile-project-p))
(user-error "Not in a project")
(let* ((proj-file (expand-file-name ".bookmarks" (projectile-project-root)))
(saved-alist (copy-tree bookmark-alist))
(saved-file bookmark-default-file))
(let ((proj-file (expand-file-name ".bookmarks" (projectile-project-root))))
(if (not (file-exists-p proj-file))
(message "No .bookmarks file in this project.")
;; Read project bookmarks directly (read, not bookmark-load)
(setq bookmark-alist
(with-temp-buffer
(insert-file-contents proj-file)
(read (current-buffer))))
(bookmark-bmenu-list)
;; Restore globals after each RET jump AND on q
(with-current-buffer "*Bookmark List*"
(local-set-key (kbd "RET")
(lambda ()
(interactive)
(bookmark-bmenu-this-window)
(setq bookmark-alist saved-alist)
(setq bookmark-default-file saved-file)))
(add-hook 'kill-buffer-hook
(lambda ()
(setq bookmark-alist saved-alist)
(setq bookmark-default-file saved-file))
nil t))))))
;; 1. Dynamically bind bookmark-alist.
;; Emacs will ONLY see these bookmarks inside this let block.
(let* ((bookmark-alist (with-temp-buffer
(insert-file-contents proj-file)
(read (current-buffer))))
;; 2. Fetch names based on the temporary project alist
(names (bookmark-all-names))
;; 3. Prompt the user for a selection
(bm-name (when names
(completing-read "Project Bookmark: " names nil t))))
;; 4. Jump to it. Once this let block ends, global bookmarks instantly return.
(if bm-name
(bookmark-jump bm-name)
(message "No bookmarks found in project.")))))))
(map! :leader
:desc "Set global/project bookmark"
"b m" #'gortium/bookmark-set
:desc "Delete global/project bookmark"
"b M" #'gortium/bookmark-delete)
;; Example using General.el / Doom Emacs map! macro
(map! :leader
:desc "List project bookmarks" "p RET" #'gortium/list-project-bookmarks)
#+end_src
** Refile done task
#+begin_src emacs-lisp
(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
@@ -3966,6 +4017,5 @@ Need chrome... :(
:desc "Edit Edna rules" "E" #'org-edna-edit))
** Project-Aware Agenda

View File

@@ -1441,60 +1441,68 @@ Cordialement,
(defconst gortium/project-mu4e-keys '(?I ?S ?D ?T ?A)
"Character keys used for project mu4e maildir shortcuts (uppercase avoids conflicts).")
(defvar gortium/project-mu4e-bookmarks nil
"Cached project mu4e maildir shortcuts for re-injection when mu4e loads.")
(defun gortium/inject-project-mu4e-bookmarks ()
"Inject project mu4e maildirs into mu4e-maildir-shortcuts.
Keys [PI]/[PS]/[PD]/[PT]/[PA] (uppercase). Filters by context-tag:
Keys [I]/[S]/[D]/[T]/[A] (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 '()))
;; 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 (cons (format "maildir:/%s.%s" (car folder) proj-tag)
(cdr folder))
project-bm)))
;; Inject into mu4e-maildir-shortcuts
(setq gortium/project-mu4e-bookmarks project-bm)
(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)
(when (get-buffer "*mu4e-main*")
(with-current-buffer "*mu4e-main*"
(mu4e--main-redraw))))))
(interactive)
(when (boundp 'mu4e-maildir-shortcuts)
(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))))
;; Extract the context name safely
(cur-context-name (and (boundp 'mu4e-context-current)
mu4e-context-current
(mu4e-context-name mu4e-context-current)))
(project-shortcuts '()))
(add-hook 'projectile-after-switch-project-hook
#'gortium/inject-project-mu4e-bookmarks)
;; 1. Always strip out previous project shortcuts safely
(setq mu4e-maildir-shortcuts
(seq-filter (lambda (shortcut)
(let ((key (if (listp (cdr shortcut))
(plist-get shortcut :key)
(cdr shortcut))))
(not (memq key gortium/project-mu4e-keys))))
mu4e-maildir-shortcuts))
;; 2. Determine whether to inject and log the reason to *Messages*
(cond
((not proj-tag)
;; No tag found, do nothing silently (prevents spam on non-project files)
nil)
((and context-tag cur-context-name (not (string= context-tag cur-context-name)))
(message "[mu4e Project] Skipped: .dir-locals context '%s' does not match active mu4e context '%s'."
context-tag cur-context-name))
(t
;; 3. Build and inject the shortcuts
(dolist (folder '(("Inbox" . ?I)
("Sent" . ?S)
("Drafts" . ?D)
("Trash" . ?T)
("Archive" . ?A)))
(push `(:maildir ,(format "/%s.%s" (car folder) proj-tag)
:key ,(cdr folder)
:name ,(format "[%s] %s" proj-tag (car folder)))
project-shortcuts))
(setq mu4e-maildir-shortcuts
(append mu4e-maildir-shortcuts (nreverse project-shortcuts)))
(message "[mu4e Project] Successfully injected maildirs for project tag: %s" proj-tag))))))
;; Apply the hooks
(add-hook 'projectile-after-switch-project-hook #'gortium/inject-project-mu4e-bookmarks)
(add-hook 'find-file-hook #'gortium/inject-project-mu4e-bookmarks)
(add-hook 'mu4e-main-mode-hook #'gortium/inject-project-mu4e-bookmarks)
;; CRITICAL: Re-inject AFTER mu4e applies its context variables
(add-hook 'mu4e-context-changed-hook #'gortium/inject-project-mu4e-bookmarks)
(after! mu4e
(gortium/inject-project-mu4e-bookmarks))
@@ -2657,6 +2665,7 @@ Saves only project bookmarks without contaminating global list."
;; Restore globals
(setq bookmark-alist saved-alist)
(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)))
@@ -2704,6 +2713,34 @@ Project bookmarks go to .bookmarks file in project root."
(message "Project bookmark \"%s\" saved to .bookmarks" name))
(user-error "Not in a Projectile project"))))))
(defun gortium/bookmark-delete ()
"Delete a bookmark. Prompts: (g)lobal or (p)roject.
Project bookmarks are deleted from the .bookmarks file in the project root."
(interactive)
(let ((choice (read-char-choice "Delete: (g)lobal / (p)roject? " '(?g ?p))))
(pcase choice
(?g (call-interactively 'bookmark-delete))
(?p
(if (not (projectile-project-p))
(user-error "Not in a Projectile project")
(let ((proj-file (expand-file-name ".bookmarks" (projectile-project-root))))
(if (not (file-exists-p proj-file))
(message "No project bookmarks found.")
;; Dynamically bind the project bookmarks
(let* ((bookmark-alist (with-temp-buffer
(insert-file-contents proj-file)
(read (current-buffer))))
(names (bookmark-all-names)))
(if (not names)
(message "No bookmarks in project.")
(let ((bm-name (completing-read "Delete Project Bookmark: " names nil t)))
(when (and bm-name (not (string-empty-p bm-name)))
;; Delete from the temporarily bound alist and save
(bookmark-delete bm-name nil)
(bookmark-save nil proj-file t)
(message "Deleted project bookmark: %s" bm-name))))))))))))
(defun gortium/sync-plan-to-bookmark-file (bookmarks)
"Write BOOKMARKS alist back to the Plan file under * Bookmarks."
@@ -2767,34 +2804,44 @@ Project bookmarks go to .bookmarks file in project root."
(message "Transferred '%s' to global" name)))))))
(defun gortium/list-project-bookmarks ()
"Show project bookmarks menu — restores globals after RET jump and on q."
"Jump to a project bookmark using standard completion.
Keeps global bookmarks completely uncontaminated."
(interactive)
(if (not (projectile-project-p))
(user-error "Not in a project")
(let* ((proj-file (expand-file-name ".bookmarks" (projectile-project-root)))
(saved-alist (copy-tree bookmark-alist))
(saved-file bookmark-default-file))
(let ((proj-file (expand-file-name ".bookmarks" (projectile-project-root))))
(if (not (file-exists-p proj-file))
(message "No .bookmarks file in this project.")
;; Read project bookmarks directly (read, not bookmark-load)
(setq bookmark-alist
(with-temp-buffer
(insert-file-contents proj-file)
(read (current-buffer))))
(bookmark-bmenu-list)
;; Restore globals after each RET jump AND on q
(with-current-buffer "*Bookmark List*"
(local-set-key (kbd "RET")
(lambda ()
(interactive)
(bookmark-bmenu-this-window)
(setq bookmark-alist saved-alist)
(setq bookmark-default-file saved-file)))
(add-hook 'kill-buffer-hook
(lambda ()
(setq bookmark-alist saved-alist)
(setq bookmark-default-file saved-file))
nil t))))))
;; 1. Dynamically bind bookmark-alist.
;; Emacs will ONLY see these bookmarks inside this let block.
(let* ((bookmark-alist (with-temp-buffer
(insert-file-contents proj-file)
(read (current-buffer))))
;; 2. Fetch names based on the temporary project alist
(names (bookmark-all-names))
;; 3. Prompt the user for a selection
(bm-name (when names
(completing-read "Project Bookmark: " names nil t))))
;; 4. Jump to it. Once this let block ends, global bookmarks instantly return.
(if bm-name
(bookmark-jump bm-name)
(message "No bookmarks found in project.")))))))
(map! :leader
:desc "Set global/project bookmark"
"b m" #'gortium/bookmark-set
:desc "Delete global/project bookmark"
"b M" #'gortium/bookmark-delete)
;; Example using General.el / Doom Emacs map! macro
(map! :leader
:desc "List project bookmarks" "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