From 39259be53ec39fcd24fe43b5ded49a085fb946f1 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Fri, 17 Jul 2026 14:32:45 -0400 Subject: [PATCH] fix: restore bookmark fns, require from separate file --- doom/.config/doom/README.org | 133 +++++++++---------- doom/.config/doom/config.el | 133 +++++++------------ doom/.config/doom/gortium-bookmarks.el | 170 +++++++++++++++++++++++++ 3 files changed, 276 insertions(+), 160 deletions(-) create mode 100644 doom/.config/doom/gortium-bookmarks.el diff --git a/doom/.config/doom/README.org b/doom/.config/doom/README.org index 55c90df..5613a20 100644 --- a/doom/.config/doom/README.org +++ b/doom/.config/doom/README.org @@ -89,7 +89,7 @@ available. You can either set ~doom-theme~ or manually load a theme with the ~load-theme~ function. This is the default: #+begin_src emacs-lisp -(setq doom-theme 'doom-gruvbox) +(setq doom-theme 'sanityinc-tomorrow-night) #+end_src ** Projectile (ExoKortex) @@ -99,9 +99,9 @@ No more auto-discovery of random =.git= repos. #+begin_src emacs-lisp (after! projectile - ;; Only .projectile files define a project root (setq projectile-project-root-files '(".projectile")) - ;; Stop auto-scanning everything — only explicit .projectile markers count + (setq projectile-project-root-files-bottom-up nil) + (setq projectile-project-root-files-top-down-recurring nil) (setq projectile-auto-discover nil)) #+end_src @@ -240,48 +240,32 @@ org-agenda-files (seq-uniq org-agenda-files) #+begin_src emacs-lisp (after! org - ;; Header size - (custom-set-faces! - '(org-document-title :height 2.0 :weight bold) - '(org-level-1 :foreground "#b8bb26" :weight semi-bold :height 1.45) ; green - '(org-level-2 :foreground "#83a598" :weight semi-bold :height 1.35) ; blue - '(org-level-3 :foreground "#d3869b" :weight semi-bold :height 1.25) ; purple - '(org-level-4 :foreground "#8ec07c" :weight semi-bold :height 1.15) ; aqua - '(org-level-5 :foreground "#fabd2f" :weight normal :height 1.10) ; yellow - '(org-level-6 :foreground "#fe8019" :weight normal :height 1.05) ; orange - '(org-level-7 :foreground "#cc241d" :weight normal :height 1.02) ; red - '(org-level-8 :foreground "#a89984" :weight normal :height 1.00)) ; grey-brown - ;; '(org-level-1 :weight semi-bold :height 1.45) - ;; '(org-level-2 :weight semi-bold :height 1.35) - ;; '(org-level-3 :weight semi-bold :height 1.25) - ;; '(org-level-4 :weight semi-bold :height 1.15) - ;; '(org-level-5 :weight normal :height 1.10) - ;; '(org-level-6 :weight normal :height 1.05) - ;; '(org-level-7 :weight normal :height 1.02) - ;; '(org-level-8 :weight normal :height 1.00)) + ;; 1. Update existing faces (Org defines 1-8 by default) + (set-face-attribute 'org-document-title nil :height 2.0 :weight 'bold) + (set-face-attribute 'org-level-1 nil :weight 'semi-bold :height 1.45) + (set-face-attribute 'org-level-2 nil :weight 'semi-bold :height 1.35) + (set-face-attribute 'org-level-3 nil :weight 'semi-bold :height 1.25) + (set-face-attribute 'org-level-4 nil :weight 'semi-bold :height 1.15) + (set-face-attribute 'org-level-5 nil :weight 'normal :height 1.10) + (set-face-attribute 'org-level-6 nil :weight 'normal :height 1.05) + (set-face-attribute 'org-level-7 nil :weight 'normal :height 1.02) + (set-face-attribute 'org-level-8 nil :weight 'normal :height 1.00) - ;; Clamp all levels beyond 8 to use org-level-8 face - (defvar gortium/org-level-8-face - '((t :foreground "#a89984" :weight normal :height 1.00))) - ;; '((t :weight normal :height 1.00))) - - ;; Définir org-level-9 à org-level-20 avec la même face que org-level-8 + ;; 2. Create missing faces (9-20) and inherit them directly from org-level-8 (cl-loop for lvl from 9 to 20 - do (eval - `(defface ,(intern (format "org-level-%d" lvl)) - ',gortium/org-level-8-face - ,(format "Face pour org heading level %d (cloné du niveau 8)." lvl) - :group 'org-faces))) + do (let ((face-name (intern (format "org-level-%d" lvl)))) + (custom-declare-face face-name + '((t :inherit org-level-8)) + (format "Face for org heading level %d." lvl) + :group 'org-faces))) - ;; Ajout des nouvelles faces dans org-level-faces + ;; 3. Add the newly created faces to Org's level list (setq org-level-faces (append org-level-faces (mapcar (lambda (lvl) (intern (format "org-level-%d" lvl))) (number-sequence 9 20)))) - ;; Mise à jour du nombre total de niveaux - (setq org-n-level-faces (length org-level-faces)) -) + (setq org-n-level-faces (length org-level-faces))) #+end_src ** Clock-in @@ -2182,7 +2166,7 @@ Useful for the user, but also when you have someone over your shoulder trying to #+begin_src emacs-lisp (after! mu4e - (setq mu4e-maildir-shortcuts mu4e-maildir-list mu4e-maildir-initial-input mu4e-maildir-info-delimiter) + (setq mu4e-maildir-initial-input mu4e-maildir-info-delimiter) (setq mu4e-update-interval 300) (setq mu4e-headers-auto-update t) (setq mu4e-contexts @@ -2203,12 +2187,12 @@ Useful for the user, but also when you have someone over your shoulder trying to (mu4e-trash-folder . "/TDNDE/Trash") (mu4e-refile-folder . "/TDNDE/Archive") - ;; Shortcuts - (mu4e-maildir-shortcuts . (("/TDNDE/Inbox" . ?i) - ("/TDNDE/Sent" . ?s) - ("/TDNDE/Drafts" . ?d) - ("/TDNDE/Trash" . ?t) - ("/TDNDE/Archive" . ?a))) + ;; Shortcuts (Updated to modern Plist format) + (mu4e-maildir-shortcuts . ((:maildir "/TDNDE/Inbox" :key ?i) + (:maildir "/TDNDE/Sent" :key ?s) + (:maildir "/TDNDE/Drafts" :key ?d) + (:maildir "/TDNDE/Trash" :key ?t) + (:maildir "/TDNDE/Archive" :key ?a))) ;; SMTP (smtpmail-smtp-user . "tpouplier@tdnde.com") @@ -2221,10 +2205,10 @@ Useful for the user, but also when you have someone over your shoulder trying to (org-msg-signature . " Cdlt, -,#+begin_signature +#+begin_signature -- -,*Thierry Pouplier* +*Thierry Pouplier* 1319 rue Bergar, Laval, Qc, Canada \\\\ H7L 4Z7 \\\\ @@ -2232,7 +2216,7 @@ Tél. : +1 (450) 667-1884 \\\\ Cell. : +1 (514) 887-1674 \\\\ tpouplier@tdnde.com \\\\ www.tdnde.com \\\\ -,#+end_signature"))) +#+end_signature"))) ;; --- CONTEXT: Perso --- (make-mu4e-context @@ -2240,18 +2224,20 @@ www.tdnde.com \\\\ :match-func (lambda (msg) (when msg (string-prefix-p "/Perso/" (mu4e-message-field msg :maildir)))) - :vars `((user-mail-address . "thierrypouplier@gmail.com") - (user-full-name . "Thierry Pouplier") - (mu4e-sent-folder . "/Perso/Sent") - (mu4e-drafts-folder . "/Perso/Drafts") - (mu4e-trash-folder . "/Perso/Trash") - (mu4e-refile-folder . "/Perso/Archive") + ;; Fixed standard single quote evaluation alignment + :vars '((user-mail-address . "thierrypouplier@gmail.com") + (user-full-name . "Thierry Pouplier") + (mu4e-sent-folder . "/Perso/Sent") + (mu4e-drafts-folder . "/Perso/Drafts") + (mu4e-trash-folder . "/Perso/Trash") + (mu4e-refile-folder . "/Perso/Archive") - (mu4e-maildir-shortcuts . (("/Perso/Inbox" . ?i) - ("/Perso/Sent" . ?s) - ("/Perso/Trash" . ?t) - ("/Perso/Drafts" . ?d) - ("/Perso/Archive" . ?a))) + ;; Shortcuts (Updated to modern Plist format) + (mu4e-maildir-shortcuts . ((:maildir "/Perso/Inbox" :key ?i) + (:maildir "/Perso/Sent" :key ?s) + (:maildir "/Perso/Trash" :key ?t) + (:maildir "/Perso/Drafts" :key ?d) + (:maildir "/Perso/Archive" :key ?a))) (smtpmail-smtp-user . "thierrypouplier@gmail.com") (smtpmail-smtp-server . "smtp.gmail.com") @@ -2261,11 +2247,11 @@ www.tdnde.com \\\\ (org-msg-signature . " Cordialement, -,#+begin_signature --- -,*Thierry Pouplier* \\ +#+begin_signature +-r +*Thierry Pouplier* \\ (514) 887-1674 | thierrypouplier@gmail.com -,#+end_signature"))))) ;; End of mu4e-contexts list +#+end_signature"))))) (setq mu4e-context-policy 'pick-first mu4e-compose-context-policy 'ask) @@ -3560,6 +3546,9 @@ to .org files under the project root." (map! :leader :desc "Project-aware agenda" "o P" #'gortium/project-aware-agenda) + +;; Load project bookmark functions +(require 'gortium-bookmarks nil t) #+end_src #+RESULTS: @@ -3741,14 +3730,11 @@ Need chrome... :( (map! :map org-mode-map :localleader :desc "Edit Edna rules" "E" #'org-edna-edit)) -#+end_src - - ** Project-Aware Agenda -#+begin_src emacs-lisp +#+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.") @@ -3778,11 +3764,13 @@ Need chrome... :( #'gortium/inject-project-agenda) ;; Also inject at startup and on file open in a project (add-hook 'find-file-hook #'gortium/inject-project-agenda) -#+end_src -** Project Bookmarks +;; Load project bookmark functions +(require 'gortium-bookmarks nil t) + +;; Load project bookmark functions +(require 'gortium-bookmarks nil t) -#+begin_src emacs-lisp (defvar gortium/project-bookmark-names nil "Bookmark names currently in the Plan file for the active project.") @@ -3834,9 +3822,7 @@ NAME is the display name, URL is the link target (file path or web URL)." (if (re-search-forward "^\* Bookmarks" nil t) (forward-line 1) (goto-char (point-max)) - (insert " -* Bookmarks -")) + (insert "\n* Bookmarks\n")) (insert (format "[[%s][%s]] " url name)) (write-region (point-min) (point-max) plan-file nil 'silent) @@ -3957,4 +3943,5 @@ in their native categories. On quit restores global bookmarks." "b m" #'gortium/bookmark-set :desc "Jump to project bookmark" "p RET" #'gortium/list-project-bookmarks) -#+end_src \ No newline at end of file + +#+end_src diff --git a/doom/.config/doom/config.el b/doom/.config/doom/config.el index b699c56..b087f19 100644 --- a/doom/.config/doom/config.el +++ b/doom/.config/doom/config.el @@ -10,12 +10,12 @@ (after! doom-big-font-mode (setq doom-big-font-increment 8)) -(setq doom-theme 'doom-gruvbox) +(setq doom-theme 'sanityinc-tomorrow-night) (after! projectile - ;; Only .projectile files define a project root (setq projectile-project-root-files '(".projectile")) - ;; Stop auto-scanning everything — only explicit .projectile markers count + (setq projectile-project-root-files-bottom-up nil) + (setq projectile-project-root-files-top-down-recurring nil) (setq projectile-auto-discover nil)) (setq bookmark-save-flag 1) @@ -105,48 +105,32 @@ org-agenda-files (seq-uniq org-agenda-files) ) (after! org - ;; Header size - (custom-set-faces! - '(org-document-title :height 2.0 :weight bold) - '(org-level-1 :foreground "#b8bb26" :weight semi-bold :height 1.45) ; green - '(org-level-2 :foreground "#83a598" :weight semi-bold :height 1.35) ; blue - '(org-level-3 :foreground "#d3869b" :weight semi-bold :height 1.25) ; purple - '(org-level-4 :foreground "#8ec07c" :weight semi-bold :height 1.15) ; aqua - '(org-level-5 :foreground "#fabd2f" :weight normal :height 1.10) ; yellow - '(org-level-6 :foreground "#fe8019" :weight normal :height 1.05) ; orange - '(org-level-7 :foreground "#cc241d" :weight normal :height 1.02) ; red - '(org-level-8 :foreground "#a89984" :weight normal :height 1.00)) ; grey-brown - ;; '(org-level-1 :weight semi-bold :height 1.45) - ;; '(org-level-2 :weight semi-bold :height 1.35) - ;; '(org-level-3 :weight semi-bold :height 1.25) - ;; '(org-level-4 :weight semi-bold :height 1.15) - ;; '(org-level-5 :weight normal :height 1.10) - ;; '(org-level-6 :weight normal :height 1.05) - ;; '(org-level-7 :weight normal :height 1.02) - ;; '(org-level-8 :weight normal :height 1.00)) + ;; 1. Update existing faces (Org defines 1-8 by default) + (set-face-attribute 'org-document-title nil :height 2.0 :weight 'bold) + (set-face-attribute 'org-level-1 nil :weight 'semi-bold :height 1.45) + (set-face-attribute 'org-level-2 nil :weight 'semi-bold :height 1.35) + (set-face-attribute 'org-level-3 nil :weight 'semi-bold :height 1.25) + (set-face-attribute 'org-level-4 nil :weight 'semi-bold :height 1.15) + (set-face-attribute 'org-level-5 nil :weight 'normal :height 1.10) + (set-face-attribute 'org-level-6 nil :weight 'normal :height 1.05) + (set-face-attribute 'org-level-7 nil :weight 'normal :height 1.02) + (set-face-attribute 'org-level-8 nil :weight 'normal :height 1.00) - ;; Clamp all levels beyond 8 to use org-level-8 face - (defvar gortium/org-level-8-face - '((t :foreground "#a89984" :weight normal :height 1.00))) - ;; '((t :weight normal :height 1.00))) - - ;; Définir org-level-9 à org-level-20 avec la même face que org-level-8 + ;; 2. Create missing faces (9-20) and inherit them directly from org-level-8 (cl-loop for lvl from 9 to 20 - do (eval - `(defface ,(intern (format "org-level-%d" lvl)) - ',gortium/org-level-8-face - ,(format "Face pour org heading level %d (cloné du niveau 8)." lvl) - :group 'org-faces))) + do (let ((face-name (intern (format "org-level-%d" lvl)))) + (custom-declare-face face-name + '((t :inherit org-level-8)) + (format "Face for org heading level %d." lvl) + :group 'org-faces))) - ;; Ajout des nouvelles faces dans org-level-faces + ;; 3. Add the newly created faces to Org's level list (setq org-level-faces (append org-level-faces (mapcar (lambda (lvl) (intern (format "org-level-%d" lvl))) (number-sequence 9 20)))) - ;; Mise à jour du nombre total de niveaux - (setq org-n-level-faces (length org-level-faces)) -) + (setq org-n-level-faces (length org-level-faces))) (after! org ;; Automatically clock in when switching to STRT @@ -1340,7 +1324,7 @@ DIFF: ) (after! mu4e - (setq mu4e-maildir-shortcuts mu4e-maildir-list mu4e-maildir-initial-input mu4e-maildir-info-delimiter) + (setq mu4e-maildir-initial-input mu4e-maildir-info-delimiter) (setq mu4e-update-interval 300) (setq mu4e-headers-auto-update t) (setq mu4e-contexts @@ -1361,12 +1345,12 @@ DIFF: (mu4e-trash-folder . "/TDNDE/Trash") (mu4e-refile-folder . "/TDNDE/Archive") - ;; Shortcuts - (mu4e-maildir-shortcuts . (("/TDNDE/Inbox" . ?i) - ("/TDNDE/Sent" . ?s) - ("/TDNDE/Drafts" . ?d) - ("/TDNDE/Trash" . ?t) - ("/TDNDE/Archive" . ?a))) + ;; Shortcuts (Updated to modern Plist format) + (mu4e-maildir-shortcuts . ((:maildir "/TDNDE/Inbox" :key ?i) + (:maildir "/TDNDE/Sent" :key ?s) + (:maildir "/TDNDE/Drafts" :key ?d) + (:maildir "/TDNDE/Trash" :key ?t) + (:maildir "/TDNDE/Archive" :key ?a))) ;; SMTP (smtpmail-smtp-user . "tpouplier@tdnde.com") @@ -1398,18 +1382,20 @@ www.tdnde.com \\\\ :match-func (lambda (msg) (when msg (string-prefix-p "/Perso/" (mu4e-message-field msg :maildir)))) - :vars `((user-mail-address . "thierrypouplier@gmail.com") - (user-full-name . "Thierry Pouplier") - (mu4e-sent-folder . "/Perso/Sent") - (mu4e-drafts-folder . "/Perso/Drafts") - (mu4e-trash-folder . "/Perso/Trash") - (mu4e-refile-folder . "/Perso/Archive") + ;; Fixed standard single quote evaluation alignment + :vars '((user-mail-address . "thierrypouplier@gmail.com") + (user-full-name . "Thierry Pouplier") + (mu4e-sent-folder . "/Perso/Sent") + (mu4e-drafts-folder . "/Perso/Drafts") + (mu4e-trash-folder . "/Perso/Trash") + (mu4e-refile-folder . "/Perso/Archive") - (mu4e-maildir-shortcuts . (("/Perso/Inbox" . ?i) - ("/Perso/Sent" . ?s) - ("/Perso/Trash" . ?t) - ("/Perso/Drafts" . ?d) - ("/Perso/Archive" . ?a))) + ;; Shortcuts (Updated to modern Plist format) + (mu4e-maildir-shortcuts . ((:maildir "/Perso/Inbox" :key ?i) + (:maildir "/Perso/Sent" :key ?s) + (:maildir "/Perso/Trash" :key ?t) + (:maildir "/Perso/Drafts" :key ?d) + (:maildir "/Perso/Archive" :key ?a))) (smtpmail-smtp-user . "thierrypouplier@gmail.com") (smtpmail-smtp-server . "smtp.gmail.com") @@ -1420,10 +1406,10 @@ www.tdnde.com \\\\ Cordialement, #+begin_signature --- +-r *Thierry Pouplier* \\ (514) 887-1674 | thierrypouplier@gmail.com -#+end_signature"))))) ;; End of mu4e-contexts list +#+end_signature"))))) (setq mu4e-context-policy 'pick-first mu4e-compose-context-policy 'ask) @@ -2626,6 +2612,9 @@ to .org files under the project root." :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." @@ -2679,33 +2668,3 @@ to .org files under the project root." ;; Make the frame more temporary-like (set-frame-parameter frame 'delete-before-kill-buffer t) (set-window-dedicated-p (selected-window) t)))) - -;; 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) diff --git a/doom/.config/doom/gortium-bookmarks.el b/doom/.config/doom/gortium-bookmarks.el new file mode 100644 index 0000000..ae15007 --- /dev/null +++ b/doom/.config/doom/gortium-bookmarks.el @@ -0,0 +1,170 @@ +;;; gortium-bookmarks.el — ExoKortex Project Bookmarks +;;; Loaded from README.org via (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 + ((looking-at "^\\*\\* \\(.+\\)\n\\[\\[file:\\(.+\\)\\]\\[\\(.+\\)\\]\\]") + (push (cons (match-string 1) (match-string 2)) bookmarks) + (forward-line 2)) + ((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]]\n" 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\n" (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) + (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]]\n" (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) + +(provide 'gortium-bookmarks)