diff --git a/doom/.config/doom/README.org b/doom/.config/doom/README.org index 611aedf..325a513 100644 --- a/doom/.config/doom/README.org +++ b/doom/.config/doom/README.org @@ -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 diff --git a/doom/.config/doom/config.el b/doom/.config/doom/config.el index 9fa5b14..ae6600a 100644 --- a/doom/.config/doom/config.el +++ b/doom/.config/doom/config.el @@ -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