feat: .bookmarks file, mu4e maildirs, dir-locals format
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
Reference in New Issue
Block a user