PARA: rename 2-Assets to 2-Inputs (PINO) — ensure-node-structure blueprint + Home-Infra roam/bookmark/dict/rss paths
This commit is contained in:
@@ -128,7 +128,7 @@ No more auto-discovery of random =.git= repos.
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
(setq bookmark-save-flag 1)
|
||||
(setq bookmark-default-file (expand-file-name "~/ExoKortex/2-Areas/Home-Infra/2-Assets/bookmark"))
|
||||
(setq bookmark-default-file (expand-file-name "~/ExoKortex/2-Areas/Home-Infra/2-Inputs/bookmark"))
|
||||
#+end_src
|
||||
|
||||
** Buffer auto reload
|
||||
@@ -182,10 +182,10 @@ My current workflow consist in having the 3-5 files I work on open in vertical s
|
||||
(file-name-as-directory "~/ExoKortex/2-Areas/Home-Infra/4-Outputs/dotfiles"))
|
||||
|
||||
(defvar roam-base-path
|
||||
(file-name-as-directory "~/ExoKortex/2-Areas/Home-Infra/2-Assets/Roam"))
|
||||
(file-name-as-directory "~/ExoKortex/2-Areas/Home-Infra/2-Inputs/Roam"))
|
||||
|
||||
(defvar dict-base-path
|
||||
(file-name-as-directory "~/ExoKortex/2-Areas/Home-Infra/2-Assets/Dictionaries"))
|
||||
(file-name-as-directory "~/ExoKortex/2-Areas/Home-Infra/2-Inputs/Dictionaries"))
|
||||
|
||||
(defvar journal-base-path
|
||||
(file-name-as-directory "~/ExoKortex/2-Areas/Journal"))
|
||||
@@ -337,6 +337,35 @@ org-agenda-files (seq-uniq org-agenda-files)
|
||||
(setq org-stuck-projects
|
||||
'("TODO=\"PROJ\"" ("NEXT") nil ""))
|
||||
|
||||
(defun gortium/stuck-plans (&rest _)
|
||||
"Agenda block: list plan files that contain no NEXT headline = stuck.
|
||||
Each line jumps to the file (org-hd-marker on the first headline)."
|
||||
(org-agenda-prepare)
|
||||
(let ((inhibit-read-only t)
|
||||
(case-fold-search nil)
|
||||
(root (expand-file-name "~/ExoKortex")))
|
||||
(when org-agenda-overriding-header
|
||||
(insert (org-add-props
|
||||
(concat org-agenda-overriding-header "\n") nil
|
||||
'face 'org-agenda-structure)))
|
||||
(dolist (f (directory-files-recursively root "\\.org\\'" t))
|
||||
(when (and (string-match-p "1-Plan/" f)
|
||||
(not (string-match-p "\\.stversions\\|4-Archives" f)))
|
||||
(let ((stuck-p (with-temp-buffer
|
||||
(insert-file-contents f)
|
||||
(not (re-search-forward "^\\*+[ \t]+NEXT\\b" nil t)))))
|
||||
(when stuck-p
|
||||
(let ((txt (concat " ⚠ " (file-name-nondirectory f) " "))
|
||||
(marker (with-current-buffer (find-file-noselect f)
|
||||
(org-agenda-new-marker
|
||||
(save-excursion
|
||||
(goto-char (point-min))
|
||||
(if (re-search-forward "^\\*" nil t)
|
||||
(match-beginning 0) (point-min)))))))
|
||||
(org-add-props txt nil
|
||||
'org-marker marker 'org-hd-marker marker
|
||||
'txt txt 'type "stuck-plans" 'face 'org-agenda)
|
||||
(insert txt "\n"))))))))
|
||||
(setq org-agenda-exporter-settings '((ps-print-color-p 'nil)))
|
||||
(setq
|
||||
org-agenda-follow-mode nil
|
||||
@@ -438,11 +467,7 @@ org-agenda-files (seq-uniq org-agenda-files)
|
||||
(org-agenda-todo-keyword-format " %-5s") ;; fixed-width TODO
|
||||
))
|
||||
;; (todo "DONE" ((org-agenda-overriding-header gtd/complete-head)))
|
||||
(stuck "" ((org-stuck-projects '("TODO=\"PROJ\"" ("NEXT" "STRT") nil ""))
|
||||
(org-agenda-overriding-header gtd/project-head)
|
||||
(org-agenda-prefix-format " %12t") ;; reserve time space
|
||||
(org-agenda-todo-keyword-format " %-5s") ;; fixed-width TODO
|
||||
))
|
||||
(gortium/stuck-plans "" ((org-agenda-overriding-header gtd/project-head)))
|
||||
(todo "IDEA" ((org-agenda-overriding-header gtd/someday-head)
|
||||
(org-agenda-prefix-format " %12t") ;; reserve time space
|
||||
(org-agenda-todo-keyword-format " %-5s") ;; fixed-width TODO
|
||||
@@ -458,8 +483,7 @@ org-agenda-files (seq-uniq org-agenda-files)
|
||||
(todo "NEXT" ((org-agenda-overriding-header gtd/next-action-head)))
|
||||
(todo "WAIT" ((org-agenda-overriding-header gtd/waiting-head)))
|
||||
;; (todo "DONE" ((org-agenda-overriding-header gtd/complete-head)))
|
||||
(stuck "" ((org-stuck-projects '("TODO=\"PROJ\"" ("NEXT" "STRT") nil ""))
|
||||
(org-agenda-overriding-header gtd/project-head)))
|
||||
(gortium/stuck-plans "" ((org-agenda-overriding-header gtd/project-head)))
|
||||
(todo "IDEA" ((org-agenda-overriding-header gtd/someday-head)))
|
||||
)
|
||||
((org-agenda-tag-filter-preset '("+Perso")))
|
||||
@@ -723,6 +747,12 @@ org-agenda-files (seq-uniq org-agenda-files)
|
||||
"* [?] %? \n:PROPERTIES:\n:CREATED: %U\n:END:\n%a\n%i")
|
||||
("e" "Event" entry (file+olp +org-capture-todo-file "Events")
|
||||
"* %? %^t\n:PROPERTIES:\n:CREATED: %U\n:END:\n%a\n%i")
|
||||
;; Calendar event capture for gnus-icalendar (its insinuate appends a
|
||||
;; "#" template; we declare it here so it survives independent of load order
|
||||
;; and its name dedup-guard skips appending a duplicate).
|
||||
("#" "used by gnus-icalendar-org"
|
||||
entry (file+olp +org-capture-todo-file "Events")
|
||||
"%i" :immediate-finish t)
|
||||
("C" "Contact" entry (file+olp +org-capture-todo-file "Contacts") "* %?\n:PROPERTIES:\n:EMAIL: %:fromemail\n:ROLE: \n:END:\n%a\n%i")
|
||||
)
|
||||
)
|
||||
@@ -1131,6 +1161,88 @@ Broken for now..
|
||||
)
|
||||
#+end_src
|
||||
|
||||
** Typed link filter
|
||||
|
||||
Insert an [[id:..]] link restricted to the node types a property may point at,
|
||||
per the relationships table in types-ontology.org (CORE/KNOWLEDGE/COLLECTIVE
|
||||
types as tags). Candidates are filtered to nodes carrying one of the allowed
|
||||
target tags, so "what is linkable" is guided by the ontology, not every roam
|
||||
node. Binding: SPC n r t.
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
(defconst gortium/link-targets
|
||||
'(("DRIVES" . ("goal"))
|
||||
("APPLIED-TO" . ("goal"))
|
||||
("TARGETS" . ("outcome"))
|
||||
("RANKS-BEFORE" . ("goal"))
|
||||
("GOALS" . ("goal"))
|
||||
("STRATEGIES" . ("strategy"))
|
||||
("PARENT" . ("project"))
|
||||
("DEPENDS-ON" . ("task"))
|
||||
("CONTRADICTS" . ("claim"))
|
||||
("CONCLUDES" . ("claim"))
|
||||
("WITHIN" . ("frame"))
|
||||
;; SOURCE/INFORMS are ambiguous across FROM types; union of allowed targets
|
||||
("SOURCE" . ("argument" "data-source"))
|
||||
("IS-ABOUT" . ("problem" "threat" "risk"))
|
||||
("ENDORSES" . ("solution"))
|
||||
("SUPPORTS" . ("claim"))
|
||||
("CONTAINS" . ("problem"))
|
||||
("SOLVES" . ("problem"))
|
||||
("HAMPERS" . ("goal"))
|
||||
("MEASURES" . ("goal"))
|
||||
("TESTS" . ("claim"))
|
||||
("PRODUCES" . ("dataset"))
|
||||
("PREDICTS" . ("outcome"))
|
||||
("FROM" . ("experiment" "project"))
|
||||
("AGAINST" . ("outcome"))
|
||||
("SHAPES" . ("goal"))
|
||||
("INFORMS" . ("strategy" "narrative" "value"))
|
||||
("REVEALS" . ("challenge"))
|
||||
("COMPOSES" . ("model"))
|
||||
("USES" . ("model"))
|
||||
("SELECTS" . ("claim"))
|
||||
("OMITS" . ("claim"))
|
||||
("INVOLVED-IN" . ("project"))
|
||||
("MEMBER-OF" . ("organization"))
|
||||
("ASSERTS" . ("argument"))
|
||||
("HOLDS" . ("belief"))
|
||||
("GUIDED-BY" . ("mission"))
|
||||
("TELLS" . ("narrative"))
|
||||
("SUPPORTED-BY" . ("data-source"))
|
||||
("FUNDS" . ("project"))
|
||||
("SEEDS" . ("project"))
|
||||
("RESTS-ON" . ("claim"))
|
||||
("GENERATES" . ("strategy"))
|
||||
("THREATENS" . ("value" "goal" "project"))
|
||||
("CONSISTS-OF" . ("goal"))
|
||||
("MOTIVATES" . ("project"))))
|
||||
|
||||
(defun gortium/--property-under-point ()
|
||||
"Return the property key on the current :PROPERTIES: drawer line, or nil."
|
||||
(save-excursion
|
||||
(beginning-of-line)
|
||||
(and (looking-at "^:\\([A-Z0-9_-]+\\):")
|
||||
(match-string-no-properties 1))))
|
||||
|
||||
(defun gortium/typed-link-insert ()
|
||||
"Insert an [[id:..]] link filtered to the types the property under point allows.
|
||||
Unknown property falls back to an unfiltered org-roam-node-insert."
|
||||
(interactive)
|
||||
(let* ((prop (or (gortium/--property-under-point)
|
||||
(completing-read "Property: "
|
||||
(mapcar #'car gortium/link-targets))))
|
||||
(allowed (cdr (assoc prop gortium/link-targets)))
|
||||
(filter (when allowed
|
||||
(lambda (node)
|
||||
(cl-intersection (org-roam-node-tags node) allowed
|
||||
:test #'equal)))))
|
||||
(org-roam-node-insert filter)))
|
||||
|
||||
(map! :leader
|
||||
:desc "Insert typed node link" "n r t" #'gortium/typed-link-insert)
|
||||
#+end_src
|
||||
|
||||
** Custom function
|
||||
|
||||
*** New Entry
|
||||
@@ -2669,7 +2781,7 @@ only injects when the project's context-tag matches current mu4e context."
|
||||
#+begin_src emacs-lisp
|
||||
(after! elfeed
|
||||
;; Define where your feed sources are kept (Org-mode file integration)
|
||||
(setq rmh-elfeed-org-files '("~/ExoKortex/2-Areas/Home-Infra/2-Assets/rss-feed.org"))
|
||||
(setq rmh-elfeed-org-files '("~/ExoKortex/2-Areas/Home-Infra/2-Inputs/rss-feed.org"))
|
||||
|
||||
;; Set the default search filter (only show unread entries from the last x weeks)
|
||||
(setq-default elfeed-search-filter "@4-weeks-ago +unread")
|
||||
@@ -3675,16 +3787,43 @@ Suppresses all logging, notes, and custom odometer hooks."
|
||||
(defvar gortium/exokortex-root "~/ExoKortex"
|
||||
"The absolute root directory of your ExoKortex.")
|
||||
|
||||
(defun gortium/ensure-node-structure (node-dir &optional default-todo project-tag context-tag)
|
||||
(defun gortium/read-emoji ()
|
||||
"Prompt for a category label: an emoji or any short text.
|
||||
TAB completes emoji names (with glyph preview); RET accepts the typed text as-is.
|
||||
Returns the chosen emoji glyph when a name is picked, otherwise the raw input
|
||||
(or \"\" if blank)."
|
||||
(require 'emoji)
|
||||
(emoji--init)
|
||||
(let* ((candidates (hash-table-keys emoji--all-bases))
|
||||
(name (completing-read
|
||||
"Category label: "
|
||||
(completion-table-with-metadata
|
||||
candidates
|
||||
`((affixation-function
|
||||
. ,(lambda (strings)
|
||||
(mapcar (lambda (s)
|
||||
(list s ""
|
||||
(concat (or (gethash s emoji--all-bases) " ")
|
||||
"\t")))
|
||||
strings)))))
|
||||
nil nil "" nil nil t))
|
||||
(glyph (and (not (string-empty-p name))
|
||||
(gethash name emoji--all-bases))))
|
||||
(or glyph name)))
|
||||
|
||||
(defun gortium/ensure-node-structure (node-dir &optional default-todo project-tag context-tag category-emoji mu4e-context)
|
||||
"Ensure NODE-DIR conforms to the ExoKortex node blueprint.
|
||||
Creates folders, writes text-layer filters, and handles Git injection.
|
||||
When PROJECT-TAG is provided, also creates .projectile and .dir-locals.el
|
||||
with a project-tagged custom agenda entry and optional CONTEXT-TAG (mu4e/agenda)."
|
||||
with a project-tagged custom agenda entry and optional CONTEXT-TAG (mu4e/agenda).
|
||||
CATEGORY-EMOJI, when given, is written as the #+CATEGORY: of the plan file.
|
||||
MU4E-CONTEXT (Perso/TDNDE) overrides the dir-locals gortium/context-tag for mu4e
|
||||
gating; when nil it falls back to CONTEXT-TAG."
|
||||
(let* ((node-dir (expand-file-name node-dir))
|
||||
(node-name (file-name-nondirectory (directory-file-name node-dir)))
|
||||
;; Blueprint Paths
|
||||
(plan-dir (expand-file-name "1-Plan" node-dir))
|
||||
(assets-dir (expand-file-name "2-Assets" node-dir))
|
||||
(inputs-dir (expand-file-name "2-Inputs" node-dir))
|
||||
(notes-dir (expand-file-name "3-Notes" node-dir))
|
||||
(outputs-dir (expand-file-name "4-Outputs" node-dir))
|
||||
(org-file (expand-file-name (concat node-name ".org") plan-dir))
|
||||
@@ -3692,7 +3831,7 @@ with a project-tagged custom agenda entry and optional CONTEXT-TAG (mu4e/agenda)
|
||||
(git-existed-p (file-exists-p (expand-file-name ".git" node-dir))))
|
||||
|
||||
;; 1. Build directory layers smoothly (mkdir -p)
|
||||
(dolist (dir (list plan-dir assets-dir notes-dir outputs-dir))
|
||||
(dolist (dir (list plan-dir inputs-dir notes-dir outputs-dir))
|
||||
(make-directory dir t))
|
||||
|
||||
;; 2. Enforce text-only gitignore rules if absent
|
||||
@@ -3703,15 +3842,13 @@ with a project-tagged custom agenda entry and optional CONTEXT-TAG (mu4e/agenda)
|
||||
;; 3. Build index Org block if absent
|
||||
(unless (file-exists-p org-file)
|
||||
(with-temp-file org-file
|
||||
(insert (format "#+TITLE: %s\n#+DATE: %s\n#+CATEGORY: %s\n#+FILETAGS: :%s:%s:\n\n* Contacts\n** Contact Name\n:PROPERTIES:\n:EMAIL:[[mu4e:from:contact@example.com]]\n:ROLE: Client / Partner\n:END:\n\n* Tasks\n** TODO First task for %s\n"
|
||||
(insert (format "#+TITLE: %s\n#+DATE: %s\n#+CATEGORY: %s\n#+FILETAGS: :%s:%s:\n\n* Tasks\n** TODO First task for %s\n\n* Contacts\n\n* Notes\n\n* Questions\n\n* Events\n"
|
||||
node-name
|
||||
(format-time-string "[%Y-%m-%d %a]")
|
||||
node-name
|
||||
(or category-emoji node-name)
|
||||
(or project-tag "project")
|
||||
(or context-tag "")
|
||||
(or default-todo "Initialize project context")
|
||||
node-name
|
||||
node-name
|
||||
node-name))))
|
||||
|
||||
;; 4. ExoKortex project markers (if project-tag provided)
|
||||
@@ -3737,7 +3874,7 @@ with a project-tagged custom agenda entry and optional CONTEXT-TAG (mu4e/agenda)
|
||||
((org-agenda-tag-filter-preset
|
||||
(list ,(concat "+" project-tag)))))))
|
||||
,@(if context-tag
|
||||
(list (cons 'gortium/context-tag context-tag))
|
||||
(list (cons 'gortium/context-tag (or mu4e-context context-tag)))
|
||||
nil))))
|
||||
(princ "((nil . " (current-buffer))
|
||||
(prin1 vars (current-buffer))
|
||||
@@ -3778,28 +3915,106 @@ with a project-tagged custom agenda entry and optional CONTEXT-TAG (mu4e/agenda)
|
||||
;;; Interactive Entry Points
|
||||
;;; ====================================================================
|
||||
|
||||
(defun gortium/create-node (category node-name tag context-tag)
|
||||
"Create a brand new standardized PARA Node inside your ExoKortex vault.
|
||||
TAG is a short slug for agenda/email filtering (e.g. 'airbus_vision').
|
||||
CONTEXT-TAG is the mu4e/agenda context (e.g. 'TDNDE', 'Perso')."
|
||||
(interactive
|
||||
(list (completing-read "Category: " '("1-Projects" "2-Areas" "3-Resources"))
|
||||
(read-string "Node Name: ")
|
||||
(read-string "Project tag: ")
|
||||
(read-string "Context tag: ")))
|
||||
(let* ((category-dir (expand-file-name category gortium/exokortex-root))
|
||||
(node-dir (expand-file-name node-name category-dir))
|
||||
(org-file (gortium/ensure-node-structure node-dir
|
||||
"Initialize project context" tag context-tag)))
|
||||
(find-file org-file)
|
||||
(message "ExoKortex Node '%s' created in %s with tag '%s' (context: %s)."
|
||||
node-name category tag context-tag)))
|
||||
;; Area / project tag helpers
|
||||
(defun gortium/area-list ()
|
||||
"Return the sorted list of area node names under 2-Areas/."
|
||||
(let ((areas-dir (expand-file-name "2-Areas" gortium/exokortex-root)))
|
||||
(when (file-directory-p areas-dir)
|
||||
(sort (seq-filter (lambda (n) (not (string-prefix-p "." n)))
|
||||
(directory-files areas-dir nil "^[^.]"))
|
||||
#'string<))))
|
||||
|
||||
(defun gortium/convert-to-node (node-dirs &optional project-tag context-tag)
|
||||
(defun gortium/domain->mu4e (domain)
|
||||
"Map an area DOMAIN (personal/work) to its mu4e context name."
|
||||
(if (string= domain "work") "TDNDE" "Perso"))
|
||||
|
||||
(defun gortium/area-domain (area-name)
|
||||
"Return the domain (personal|work) of AREA-NAME from its plan FILETAGS."
|
||||
(let ((af (expand-file-name (concat area-name ".org")
|
||||
(expand-file-name "1-Plan"
|
||||
(expand-file-name area-name
|
||||
(expand-file-name "2-Areas" gortium/exokortex-root))))))
|
||||
(when (file-exists-p af)
|
||||
(with-temp-buffer
|
||||
(insert-file-contents af)
|
||||
(when (re-search-forward "^#\\+FILETAGS:.*:\\\(personal\\|work\\):" nil t)
|
||||
(match-string 1))))))
|
||||
|
||||
(defvar gortium/area-view-keys nil
|
||||
"List of injected per-area/aggregate agenda command keys (for exact removal).")
|
||||
|
||||
(defun gortium/area-tag (area-name)
|
||||
"Return AREA-NAME's own tag (FILETAGS slot-1); defaults to the area name."
|
||||
(let ((af (expand-file-name (concat area-name ".org")
|
||||
(expand-file-name "1-Plan"
|
||||
(expand-file-name area-name
|
||||
(expand-file-name "2-Areas" gortium/exokortex-root))))))
|
||||
(if (file-exists-p af)
|
||||
(with-temp-buffer
|
||||
(insert-file-contents af)
|
||||
(if (re-search-forward "^#\\+FILETAGS:[ \t]*:?\\\([^:]+\\):" nil t)
|
||||
(match-string 1)
|
||||
area-name))
|
||||
area-name)))
|
||||
|
||||
(defun gortium/create-node (category node-name category-emoji area-or-domain tag)
|
||||
"Create a brand new standardized PARA Node inside your ExoKortex vault.
|
||||
Category 2-Areas creates an AREA: gets Name, Emoji, Domain (personal/work)
|
||||
and writes FILETAGS _:name:domain:_. Other categories create a PROJECT:
|
||||
gets Name, Emoji, Area (dropdown of 2-Areas/*) and Tag, writes
|
||||
FILETAGS _:tag:area:_ and inherits the area's mu4e context for dir-locals."
|
||||
(interactive
|
||||
(let* ((category (completing-read "Category: " '("1-Projects" "2-Areas" "3-Resources")))
|
||||
(areas (gortium/area-list)))
|
||||
(if (string= category "2-Areas")
|
||||
(list category
|
||||
(read-string "Area Name: ")
|
||||
(gortium/read-emoji)
|
||||
(completing-read "Domain: " '("personal" "work"))
|
||||
(read-string "Area tag (default = area name): "))
|
||||
(list category
|
||||
(read-string "Node Name: ")
|
||||
(gortium/read-emoji)
|
||||
(completing-read "Area: " areas nil t)
|
||||
(read-string "Project tag: ")))))
|
||||
(let* ((category-dir (expand-file-name category gortium/exokortex-root))
|
||||
(node-dir (expand-file-name node-name category-dir)))
|
||||
(if (string= category "2-Areas")
|
||||
;; AREA: FILETAGS = :tag:domain: (tag chosen, defaults to area name)
|
||||
(let* ((atag (if (string-empty-p tag) node-name tag))
|
||||
(org-file (gortium/ensure-node-structure node-dir
|
||||
"Initialize project context"
|
||||
atag
|
||||
area-or-domain category-emoji
|
||||
(gortium/domain->mu4e area-or-domain))))
|
||||
(find-file org-file)
|
||||
(message "ExoKortex Area '%s' created (tag: %s, domain: %s, mu4e: %s)."
|
||||
node-name atag area-or-domain (gortium/domain->mu4e area-or-domain)))
|
||||
;; PROJECT: tag = tag, slot-2 = area; mu4e context inherited from area
|
||||
(let* ((area-file (expand-file-name (concat area-or-domain ".org")
|
||||
(expand-file-name
|
||||
"1-Plan"
|
||||
(expand-file-name area-or-domain
|
||||
(expand-file-name "2-Areas" gortium/exokortex-root)))))
|
||||
(area-mu4e (when (file-exists-p area-file)
|
||||
(with-temp-buffer
|
||||
(insert-file-contents area-file)
|
||||
(when (re-search-forward "^#\\+FILETAGS:.*:\\(personal\\|work\\):" nil t)
|
||||
(gortium/domain->mu4e (match-string 1)))))))
|
||||
(let ((org-file (gortium/ensure-node-structure node-dir
|
||||
"Initialize project context"
|
||||
tag area-or-domain category-emoji area-mu4e)))
|
||||
(find-file org-file)
|
||||
(message "ExoKortex Project '%s' created (tag: %s, area: %s, mu4e: %s)."
|
||||
node-name tag area-or-domain (or area-mu4e "?")))))))
|
||||
|
||||
(defun gortium/convert-to-node (node-dirs &optional project-tag area-or-domain category-emoji mu4e-context)
|
||||
"Retrofit existing workspace directories into tracked ExoKortex nodes.
|
||||
If folders are explicitly marked in Dirvish/Dired, it batch-processes all of them.
|
||||
Otherwise, prompts for a single directory, defaulting to the item under the cursor.
|
||||
Optional PROJECT-TAG and CONTEXT-TAG can be given to create .projectile and .dir-locals.el."
|
||||
Optional PROJECT-TAG and AREA-OR-DOMAIN set the plan FILETAGS (project: _:tag:area:_,
|
||||
area: _:name:domain:_). Optional CATEGORY-EMOJI is the #+CATEGORY:. Optional
|
||||
MU4E-CONTEXT (Perso/TDNDE) is the dir-locals gortium/context-tag."
|
||||
(interactive
|
||||
(let* ((dired-elements (when (derived-mode-p 'dired-mode)
|
||||
(dired-get-marked-files nil nil nil t)))
|
||||
@@ -3820,106 +4035,98 @@ Optional PROJECT-TAG and CONTEXT-TAG can be given to create .projectile and .dir
|
||||
(file-name-directory cursor-path))
|
||||
(t default-directory))))
|
||||
(list (read-directory-name "Directory to convert: " initial-path initial-path t)))))
|
||||
(tag (read-string "Project tag (blank to skip): "))
|
||||
(ctx (read-string "Context tag (or blank): ")))
|
||||
(list dirs tag ctx)))
|
||||
(is-area (string= (file-name-nondirectory
|
||||
(directory-file-name (expand-file-name ".." (car dirs))))
|
||||
"2-Areas"))
|
||||
(area-or-domain (if is-area
|
||||
(completing-read "Domain: " '("personal" "work"))
|
||||
(completing-read "Area: " (gortium/area-list) nil t)))
|
||||
(tag (unless is-area (read-string "Project tag: ")))
|
||||
(emoji (let ((gortium--emoji (gortium/read-emoji)))
|
||||
(if (string-empty-p gortium--emoji) nil gortium--emoji))))
|
||||
(list dirs tag area-or-domain emoji nil)))
|
||||
|
||||
(let ((count 0)
|
||||
last-org-file)
|
||||
(dolist (dir node-dirs)
|
||||
(let* ((node-dir (expand-file-name dir))
|
||||
(is-area (string= (file-name-nondirectory (directory-file-name (expand-file-name ".." node-dir)))
|
||||
"2-Areas"))
|
||||
(tag2 (unless (string-empty-p project-tag) project-tag))
|
||||
(mctx (or mu4e-context
|
||||
(when (and (not is-area) area-or-domain)
|
||||
(let ((af (expand-file-name
|
||||
(concat area-or-domain ".org")
|
||||
(expand-file-name "1-Plan"
|
||||
(expand-file-name area-or-domain (expand-file-name "2-Areas" gortium/exokortex-root))))))
|
||||
(when (file-exists-p af)
|
||||
(with-temp-buffer
|
||||
(insert-file-contents af)
|
||||
(when (re-search-forward "^#\\+FILETAGS:.*:\\(personal\\|work\\):" nil t)
|
||||
(gortium/domain->mu4e (match-string 1)))))))))
|
||||
(ctx2 (if is-area area-or-domain (or area-or-domain "")))
|
||||
(org-file (gortium/ensure-node-structure node-dir
|
||||
"Audit existing assets and reconcile tracking nodes"
|
||||
(unless (string-empty-p project-tag) project-tag)
|
||||
(unless (string-empty-p context-tag) context-tag))))
|
||||
(if is-area (file-name-nondirectory (directory-file-name node-dir)) project-tag)
|
||||
ctx2 category-emoji mctx)))
|
||||
(setq last-org-file org-file)
|
||||
(cl-incf count)))
|
||||
|
||||
(if (= count 1)
|
||||
(message "Directory successfully converted to ExoKortex node.")
|
||||
(message "Successfully batch-converted %d directories to ExoKortex nodes!" count))))
|
||||
#+end_src
|
||||
|
||||
** ExoKortex Archive / Restore
|
||||
(defun gortium/area-view-body ()
|
||||
"The block list shared by every area/aggregate GTD view."
|
||||
(list
|
||||
(list "agenda" ""
|
||||
(list (list 'org-agenda-span 'week)))
|
||||
(list "todo" "STRT" (list (list 'org-agenda-overriding-header gtd/started-head)))
|
||||
(list "todo" "NEXT" (list (list 'org-agenda-overriding-header gtd/next-action-head)))
|
||||
(list "todo" "WAIT" (list (list 'org-agenda-overriding-header gtd/waiting-head)))
|
||||
(list "gortium/stuck-plans" "" (list (list 'org-agenda-overriding-header gtd/project-head)))
|
||||
(list "todo" "IDEA" (list (list 'org-agenda-overriding-header gtd/someday-head)))))
|
||||
|
||||
Moves node(s) between categories. Operates on marked Dired items, or the
|
||||
item at cursor when nothing is marked.
|
||||
#+begin_src emacs-lisp
|
||||
(defun gortium/dired-node-dirs ()
|
||||
"Return marked Dired subdirectories, or the cursor dir if nothing marked."
|
||||
(when (derived-mode-p 'dired-mode)
|
||||
(let* ((elements (dired-get-marked-files nil nil nil t))
|
||||
(has-marks (or (and (listp elements) (> (length elements) 1))
|
||||
(and (consp elements) (eq (car elements) t))))
|
||||
(marked (when has-marks
|
||||
(cl-remove-if-not #'file-directory-p
|
||||
(if (eq (car-safe elements) t)
|
||||
(cdr elements)
|
||||
elements)))))
|
||||
(or marked
|
||||
(let ((cursor (dired-get-filename nil t)))
|
||||
(when (and cursor (file-directory-p cursor))
|
||||
(list cursor)))))))
|
||||
|
||||
(defun gortium/move-node (src-category dst-category node-dirs)
|
||||
"Move NODE-DIRS (absolute paths) from SRC-CATEGORY to DST-CATEGORY.
|
||||
Markers (.projectile/.dir-locals/.hermes-sessions/.git) move with each dir."
|
||||
(let ((root (expand-file-name gortium/exokortex-root))
|
||||
(moved 0))
|
||||
(dolist (dir node-dirs)
|
||||
(let* ((name (file-name-nondirectory (directory-file-name dir)))
|
||||
(src (expand-file-name name (expand-file-name src-category root)))
|
||||
(dst (expand-file-name name (expand-file-name dst-category root))))
|
||||
(unless (file-directory-p src)
|
||||
(user-error "No node '%s' in %s" name src-category))
|
||||
(when (file-exists-p dst)
|
||||
(user-error "%s already has a node named '%s'" dst-category name))
|
||||
(make-directory (file-name-directory dst) t)
|
||||
(rename-file src dst)
|
||||
;; Stale path: drop from the Projectile cache so the switch-list cleans up.
|
||||
(when (fboundp 'projectile-remove-known-project)
|
||||
(projectile-remove-known-project src))
|
||||
(cl-incf moved)))
|
||||
(message "Archived/restored %d node(s): %s → %s" moved src-category dst-category)))
|
||||
|
||||
(defun gortium/archive-node ()
|
||||
"Move marked (or cursor) nodes from 1-Projects to 4-Archives."
|
||||
(interactive)
|
||||
(let ((dirs (gortium/dired-node-dirs)))
|
||||
(unless dirs
|
||||
(user-error "No node under Dired cursor / no marked directories"))
|
||||
(gortium/move-node "1-Projects" "4-Archives" dirs)))
|
||||
|
||||
(defun gortium/restore-node (category)
|
||||
"Move marked (or cursor) archived nodes back into CATEGORY."
|
||||
(interactive
|
||||
(list (completing-read "Restore to category: "
|
||||
'("1-Projects" "2-Areas" "3-Resources"))))
|
||||
(let ((dirs (gortium/dired-node-dirs)))
|
||||
(unless dirs
|
||||
(user-error "No node under Dired cursor / no marked directories"))
|
||||
(gortium/move-node "4-Archives" category dirs)))
|
||||
#+end_src
|
||||
|
||||
** Project-Aware Agenda
|
||||
|
||||
Reads =.dir-locals.el= from the current Projectile root and injects the
|
||||
project-specific custom agenda command.
|
||||
|
||||
#+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/inject-area-views ()
|
||||
"Recompute per-area + aggregate (personal/work) agenda views from 2-Areas/*.
|
||||
Removes prior injected keys (gortium/area-view-keys) exactly, then rebuilds.
|
||||
Per-area key: \"a\"+area-name (unique). Aggregates: \"pw\" (all personal), \"ww\" (all work)."
|
||||
(let* ((areas (gortium/area-list))
|
||||
(personal-tags
|
||||
(mapcar (lambda (a) (concat "+" (gortium/area-tag a)))
|
||||
(seq-filter (lambda (a) (string= (gortium/area-domain a) "personal")) areas)))
|
||||
(work-tags
|
||||
(mapcar (lambda (a) (concat "+" (gortium/area-tag a)))
|
||||
(seq-filter (lambda (a) (string= (gortium/area-domain a) "work")) areas))))
|
||||
;; 1) drop stale injected keys
|
||||
(setq org-agenda-custom-commands
|
||||
(cl-remove-if (lambda (e) (member (car e) gortium/area-view-keys))
|
||||
org-agenda-custom-commands))
|
||||
(setq gortium/area-view-keys nil)
|
||||
;; 2) per-area views
|
||||
(dolist (a areas)
|
||||
(let ((key (concat "a" a)))
|
||||
(push key gortium/area-view-keys)
|
||||
(push (list key (format "GTD — %s" a)
|
||||
(gortium/area-view-body)
|
||||
(list (list 'org-agenda-tag-filter-preset (list (concat "+" (gortium/area-tag a))))))
|
||||
org-agenda-custom-commands)))
|
||||
;; 3) aggregates
|
||||
(when personal-tags
|
||||
(push "pw" gortium/area-view-keys)
|
||||
(push (list "pw" "📋 All personal areas — GTD"
|
||||
(gortium/area-view-body)
|
||||
(list (list 'org-agenda-tag-filter-preset personal-tags)))
|
||||
org-agenda-custom-commands))
|
||||
(when work-tags
|
||||
(push "ww" gortium/area-view-keys)
|
||||
(push (list "ww" "💼 All work areas — GTD"
|
||||
(gortium/area-view-body)
|
||||
(list (list 'org-agenda-tag-filter-preset work-tags)))
|
||||
org-agenda-custom-commands))))
|
||||
|
||||
(gortium/inject-area-views)
|
||||
(add-hook 'org-agenda-mode-hook #'gortium/inject-area-views)
|
||||
(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
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
(setq projectile-auto-discover nil))
|
||||
|
||||
(setq bookmark-save-flag 1)
|
||||
(setq bookmark-default-file (expand-file-name "~/ExoKortex/2-Areas/Home-Infra/2-Assets/bookmark"))
|
||||
(setq bookmark-default-file (expand-file-name "~/ExoKortex/2-Areas/Home-Infra/2-Inputs/bookmark"))
|
||||
|
||||
(setq global-auto-revert-mode 1)
|
||||
|
||||
@@ -69,10 +69,10 @@
|
||||
(file-name-as-directory "~/ExoKortex/2-Areas/Home-Infra/4-Outputs/dotfiles"))
|
||||
|
||||
(defvar roam-base-path
|
||||
(file-name-as-directory "~/ExoKortex/2-Areas/Home-Infra/2-Assets/Roam"))
|
||||
(file-name-as-directory "~/ExoKortex/2-Areas/Home-Infra/2-Inputs/Roam"))
|
||||
|
||||
(defvar dict-base-path
|
||||
(file-name-as-directory "~/ExoKortex/2-Areas/Home-Infra/2-Assets/Dictionaries"))
|
||||
(file-name-as-directory "~/ExoKortex/2-Areas/Home-Infra/2-Inputs/Dictionaries"))
|
||||
|
||||
(defvar journal-base-path
|
||||
(file-name-as-directory "~/ExoKortex/2-Areas/Journal"))
|
||||
@@ -196,6 +196,35 @@ org-agenda-files (seq-uniq org-agenda-files)
|
||||
(setq org-stuck-projects
|
||||
'("TODO=\"PROJ\"" ("NEXT") nil ""))
|
||||
|
||||
(defun gortium/stuck-plans (&rest _)
|
||||
"Agenda block: list plan files that contain no NEXT headline = stuck.
|
||||
Each line jumps to the file (org-hd-marker on the first headline)."
|
||||
(org-agenda-prepare)
|
||||
(let ((inhibit-read-only t)
|
||||
(case-fold-search nil)
|
||||
(root (expand-file-name "~/ExoKortex")))
|
||||
(when org-agenda-overriding-header
|
||||
(insert (org-add-props
|
||||
(concat org-agenda-overriding-header "\n") nil
|
||||
'face 'org-agenda-structure)))
|
||||
(dolist (f (directory-files-recursively root "\\.org\\'" t))
|
||||
(when (and (string-match-p "1-Plan/" f)
|
||||
(not (string-match-p "\\.stversions\\|4-Archives" f)))
|
||||
(let ((stuck-p (with-temp-buffer
|
||||
(insert-file-contents f)
|
||||
(not (re-search-forward "^\\*+[ \t]+NEXT\\b" nil t)))))
|
||||
(when stuck-p
|
||||
(let ((txt (concat " ⚠ " (file-name-nondirectory f) " "))
|
||||
(marker (with-current-buffer (find-file-noselect f)
|
||||
(org-agenda-new-marker
|
||||
(save-excursion
|
||||
(goto-char (point-min))
|
||||
(if (re-search-forward "^\\*" nil t)
|
||||
(match-beginning 0) (point-min)))))))
|
||||
(org-add-props txt nil
|
||||
'org-marker marker 'org-hd-marker marker
|
||||
'txt txt 'type "stuck-plans" 'face 'org-agenda)
|
||||
(insert txt "\n"))))))))
|
||||
(setq org-agenda-exporter-settings '((ps-print-color-p 'nil)))
|
||||
(setq
|
||||
org-agenda-follow-mode nil
|
||||
@@ -297,11 +326,7 @@ org-agenda-files (seq-uniq org-agenda-files)
|
||||
(org-agenda-todo-keyword-format " %-5s") ;; fixed-width TODO
|
||||
))
|
||||
;; (todo "DONE" ((org-agenda-overriding-header gtd/complete-head)))
|
||||
(stuck "" ((org-stuck-projects '("TODO=\"PROJ\"" ("NEXT" "STRT") nil ""))
|
||||
(org-agenda-overriding-header gtd/project-head)
|
||||
(org-agenda-prefix-format " %12t") ;; reserve time space
|
||||
(org-agenda-todo-keyword-format " %-5s") ;; fixed-width TODO
|
||||
))
|
||||
(gortium/stuck-plans "" ((org-agenda-overriding-header gtd/project-head)))
|
||||
(todo "IDEA" ((org-agenda-overriding-header gtd/someday-head)
|
||||
(org-agenda-prefix-format " %12t") ;; reserve time space
|
||||
(org-agenda-todo-keyword-format " %-5s") ;; fixed-width TODO
|
||||
@@ -317,8 +342,7 @@ org-agenda-files (seq-uniq org-agenda-files)
|
||||
(todo "NEXT" ((org-agenda-overriding-header gtd/next-action-head)))
|
||||
(todo "WAIT" ((org-agenda-overriding-header gtd/waiting-head)))
|
||||
;; (todo "DONE" ((org-agenda-overriding-header gtd/complete-head)))
|
||||
(stuck "" ((org-stuck-projects '("TODO=\"PROJ\"" ("NEXT" "STRT") nil ""))
|
||||
(org-agenda-overriding-header gtd/project-head)))
|
||||
(gortium/stuck-plans "" ((org-agenda-overriding-header gtd/project-head)))
|
||||
(todo "IDEA" ((org-agenda-overriding-header gtd/someday-head)))
|
||||
)
|
||||
((org-agenda-tag-filter-preset '("+Perso")))
|
||||
@@ -568,6 +592,12 @@ org-agenda-files (seq-uniq org-agenda-files)
|
||||
"* [?] %? \n:PROPERTIES:\n:CREATED: %U\n:END:\n%a\n%i")
|
||||
("e" "Event" entry (file+olp +org-capture-todo-file "Events")
|
||||
"* %? %^t\n:PROPERTIES:\n:CREATED: %U\n:END:\n%a\n%i")
|
||||
;; Calendar event capture for gnus-icalendar (its insinuate appends a
|
||||
;; "#" template; we declare it here so it survives independent of load order
|
||||
;; and its name dedup-guard skips appending a duplicate).
|
||||
("#" "used by gnus-icalendar-org"
|
||||
entry (file+olp +org-capture-todo-file "Events")
|
||||
"%i" :immediate-finish t)
|
||||
("C" "Contact" entry (file+olp +org-capture-todo-file "Contacts") "* %?\n:PROPERTIES:\n:EMAIL: %:fromemail\n:ROLE: \n:END:\n%a\n%i")
|
||||
)
|
||||
)
|
||||
@@ -713,6 +743,78 @@ org-agenda-files (seq-uniq org-agenda-files)
|
||||
)
|
||||
)
|
||||
|
||||
(defconst gortium/link-targets
|
||||
'(("DRIVES" . ("goal"))
|
||||
("APPLIED-TO" . ("goal"))
|
||||
("TARGETS" . ("outcome"))
|
||||
("RANKS-BEFORE" . ("goal"))
|
||||
("GOALS" . ("goal"))
|
||||
("STRATEGIES" . ("strategy"))
|
||||
("PARENT" . ("project"))
|
||||
("DEPENDS-ON" . ("task"))
|
||||
("CONTRADICTS" . ("claim"))
|
||||
("CONCLUDES" . ("claim"))
|
||||
("WITHIN" . ("frame"))
|
||||
;; SOURCE/INFORMS are ambiguous across FROM types; union of allowed targets
|
||||
("SOURCE" . ("argument" "data-source"))
|
||||
("IS-ABOUT" . ("problem" "threat" "risk"))
|
||||
("ENDORSES" . ("solution"))
|
||||
("SUPPORTS" . ("claim"))
|
||||
("CONTAINS" . ("problem"))
|
||||
("SOLVES" . ("problem"))
|
||||
("HAMPERS" . ("goal"))
|
||||
("MEASURES" . ("goal"))
|
||||
("TESTS" . ("claim"))
|
||||
("PRODUCES" . ("dataset"))
|
||||
("PREDICTS" . ("outcome"))
|
||||
("FROM" . ("experiment" "project"))
|
||||
("AGAINST" . ("outcome"))
|
||||
("SHAPES" . ("goal"))
|
||||
("INFORMS" . ("strategy" "narrative" "value"))
|
||||
("REVEALS" . ("challenge"))
|
||||
("COMPOSES" . ("model"))
|
||||
("USES" . ("model"))
|
||||
("SELECTS" . ("claim"))
|
||||
("OMITS" . ("claim"))
|
||||
("INVOLVED-IN" . ("project"))
|
||||
("MEMBER-OF" . ("organization"))
|
||||
("ASSERTS" . ("argument"))
|
||||
("HOLDS" . ("belief"))
|
||||
("GUIDED-BY" . ("mission"))
|
||||
("TELLS" . ("narrative"))
|
||||
("SUPPORTED-BY" . ("data-source"))
|
||||
("FUNDS" . ("project"))
|
||||
("SEEDS" . ("project"))
|
||||
("RESTS-ON" . ("claim"))
|
||||
("GENERATES" . ("strategy"))
|
||||
("THREATENS" . ("value" "goal" "project"))
|
||||
("CONSISTS-OF" . ("goal"))
|
||||
("MOTIVATES" . ("project"))))
|
||||
|
||||
(defun gortium/--property-under-point ()
|
||||
"Return the property key on the current :PROPERTIES: drawer line, or nil."
|
||||
(save-excursion
|
||||
(beginning-of-line)
|
||||
(and (looking-at "^:\\([A-Z0-9_-]+\\):")
|
||||
(match-string-no-properties 1))))
|
||||
|
||||
(defun gortium/typed-link-insert ()
|
||||
"Insert an [[id:..]] link filtered to the types the property under point allows.
|
||||
Unknown property falls back to an unfiltered org-roam-node-insert."
|
||||
(interactive)
|
||||
(let* ((prop (or (gortium/--property-under-point)
|
||||
(completing-read "Property: "
|
||||
(mapcar #'car gortium/link-targets))))
|
||||
(allowed (cdr (assoc prop gortium/link-targets)))
|
||||
(filter (when allowed
|
||||
(lambda (node)
|
||||
(cl-intersection (org-roam-node-tags node) allowed
|
||||
:test #'equal)))))
|
||||
(org-roam-node-insert filter)))
|
||||
|
||||
(map! :leader
|
||||
:desc "Insert typed node link" "n r t" #'gortium/typed-link-insert)
|
||||
|
||||
;; Org-roam-dailies new entry help function
|
||||
;; Adapted from https://rostre.bearblog.dev/building-my-ideal-emacs-journal/
|
||||
(defun gortium/org-roam-dailies-new-entry ()
|
||||
@@ -1812,7 +1914,7 @@ only injects when the project's context-tag matches current mu4e context."
|
||||
|
||||
(after! elfeed
|
||||
;; Define where your feed sources are kept (Org-mode file integration)
|
||||
(setq rmh-elfeed-org-files '("~/ExoKortex/2-Areas/Home-Infra/2-Assets/rss-feed.org"))
|
||||
(setq rmh-elfeed-org-files '("~/ExoKortex/2-Areas/Home-Infra/2-Inputs/rss-feed.org"))
|
||||
|
||||
;; Set the default search filter (only show unread entries from the last x weeks)
|
||||
(setq-default elfeed-search-filter "@4-weeks-ago +unread")
|
||||
@@ -2742,16 +2844,43 @@ Suppresses all logging, notes, and custom odometer hooks."
|
||||
(defvar gortium/exokortex-root "~/ExoKortex"
|
||||
"The absolute root directory of your ExoKortex.")
|
||||
|
||||
(defun gortium/ensure-node-structure (node-dir &optional default-todo project-tag context-tag)
|
||||
(defun gortium/read-emoji ()
|
||||
"Prompt for a category label: an emoji or any short text.
|
||||
TAB completes emoji names (with glyph preview); RET accepts the typed text as-is.
|
||||
Returns the chosen emoji glyph when a name is picked, otherwise the raw input
|
||||
(or \"\" if blank)."
|
||||
(require 'emoji)
|
||||
(emoji--init)
|
||||
(let* ((candidates (hash-table-keys emoji--all-bases))
|
||||
(name (completing-read
|
||||
"Category label: "
|
||||
(completion-table-with-metadata
|
||||
candidates
|
||||
`((affixation-function
|
||||
. ,(lambda (strings)
|
||||
(mapcar (lambda (s)
|
||||
(list s ""
|
||||
(concat (or (gethash s emoji--all-bases) " ")
|
||||
"\t")))
|
||||
strings)))))
|
||||
nil nil "" nil nil t))
|
||||
(glyph (and (not (string-empty-p name))
|
||||
(gethash name emoji--all-bases))))
|
||||
(or glyph name)))
|
||||
|
||||
(defun gortium/ensure-node-structure (node-dir &optional default-todo project-tag context-tag category-emoji mu4e-context)
|
||||
"Ensure NODE-DIR conforms to the ExoKortex node blueprint.
|
||||
Creates folders, writes text-layer filters, and handles Git injection.
|
||||
When PROJECT-TAG is provided, also creates .projectile and .dir-locals.el
|
||||
with a project-tagged custom agenda entry and optional CONTEXT-TAG (mu4e/agenda)."
|
||||
with a project-tagged custom agenda entry and optional CONTEXT-TAG (mu4e/agenda).
|
||||
CATEGORY-EMOJI, when given, is written as the #+CATEGORY: of the plan file.
|
||||
MU4E-CONTEXT (Perso/TDNDE) overrides the dir-locals gortium/context-tag for mu4e
|
||||
gating; when nil it falls back to CONTEXT-TAG."
|
||||
(let* ((node-dir (expand-file-name node-dir))
|
||||
(node-name (file-name-nondirectory (directory-file-name node-dir)))
|
||||
;; Blueprint Paths
|
||||
(plan-dir (expand-file-name "1-Plan" node-dir))
|
||||
(assets-dir (expand-file-name "2-Assets" node-dir))
|
||||
(inputs-dir (expand-file-name "2-Inputs" node-dir))
|
||||
(notes-dir (expand-file-name "3-Notes" node-dir))
|
||||
(outputs-dir (expand-file-name "4-Outputs" node-dir))
|
||||
(org-file (expand-file-name (concat node-name ".org") plan-dir))
|
||||
@@ -2759,7 +2888,7 @@ with a project-tagged custom agenda entry and optional CONTEXT-TAG (mu4e/agenda)
|
||||
(git-existed-p (file-exists-p (expand-file-name ".git" node-dir))))
|
||||
|
||||
;; 1. Build directory layers smoothly (mkdir -p)
|
||||
(dolist (dir (list plan-dir assets-dir notes-dir outputs-dir))
|
||||
(dolist (dir (list plan-dir inputs-dir notes-dir outputs-dir))
|
||||
(make-directory dir t))
|
||||
|
||||
;; 2. Enforce text-only gitignore rules if absent
|
||||
@@ -2770,15 +2899,13 @@ with a project-tagged custom agenda entry and optional CONTEXT-TAG (mu4e/agenda)
|
||||
;; 3. Build index Org block if absent
|
||||
(unless (file-exists-p org-file)
|
||||
(with-temp-file org-file
|
||||
(insert (format "#+TITLE: %s\n#+DATE: %s\n#+CATEGORY: %s\n#+FILETAGS: :%s:%s:\n\n* Contacts\n** Contact Name\n:PROPERTIES:\n:EMAIL:[[mu4e:from:contact@example.com]]\n:ROLE: Client / Partner\n:END:\n\n* Tasks\n** TODO First task for %s\n"
|
||||
(insert (format "#+TITLE: %s\n#+DATE: %s\n#+CATEGORY: %s\n#+FILETAGS: :%s:%s:\n\n* Tasks\n** TODO First task for %s\n\n* Contacts\n\n* Notes\n\n* Questions\n\n* Events\n"
|
||||
node-name
|
||||
(format-time-string "[%Y-%m-%d %a]")
|
||||
node-name
|
||||
(or category-emoji node-name)
|
||||
(or project-tag "project")
|
||||
(or context-tag "")
|
||||
(or default-todo "Initialize project context")
|
||||
node-name
|
||||
node-name
|
||||
node-name))))
|
||||
|
||||
;; 4. ExoKortex project markers (if project-tag provided)
|
||||
@@ -2804,7 +2931,7 @@ with a project-tagged custom agenda entry and optional CONTEXT-TAG (mu4e/agenda)
|
||||
((org-agenda-tag-filter-preset
|
||||
(list ,(concat "+" project-tag)))))))
|
||||
,@(if context-tag
|
||||
(list (cons 'gortium/context-tag context-tag))
|
||||
(list (cons 'gortium/context-tag (or mu4e-context context-tag)))
|
||||
nil))))
|
||||
(princ "((nil . " (current-buffer))
|
||||
(prin1 vars (current-buffer))
|
||||
@@ -2845,28 +2972,106 @@ with a project-tagged custom agenda entry and optional CONTEXT-TAG (mu4e/agenda)
|
||||
;;; Interactive Entry Points
|
||||
;;; ====================================================================
|
||||
|
||||
(defun gortium/create-node (category node-name tag context-tag)
|
||||
"Create a brand new standardized PARA Node inside your ExoKortex vault.
|
||||
TAG is a short slug for agenda/email filtering (e.g. 'airbus_vision').
|
||||
CONTEXT-TAG is the mu4e/agenda context (e.g. 'TDNDE', 'Perso')."
|
||||
(interactive
|
||||
(list (completing-read "Category: " '("1-Projects" "2-Areas" "3-Resources"))
|
||||
(read-string "Node Name: ")
|
||||
(read-string "Project tag: ")
|
||||
(read-string "Context tag: ")))
|
||||
(let* ((category-dir (expand-file-name category gortium/exokortex-root))
|
||||
(node-dir (expand-file-name node-name category-dir))
|
||||
(org-file (gortium/ensure-node-structure node-dir
|
||||
"Initialize project context" tag context-tag)))
|
||||
(find-file org-file)
|
||||
(message "ExoKortex Node '%s' created in %s with tag '%s' (context: %s)."
|
||||
node-name category tag context-tag)))
|
||||
;; Area / project tag helpers
|
||||
(defun gortium/area-list ()
|
||||
"Return the sorted list of area node names under 2-Areas/."
|
||||
(let ((areas-dir (expand-file-name "2-Areas" gortium/exokortex-root)))
|
||||
(when (file-directory-p areas-dir)
|
||||
(sort (seq-filter (lambda (n) (not (string-prefix-p "." n)))
|
||||
(directory-files areas-dir nil "^[^.]"))
|
||||
#'string<))))
|
||||
|
||||
(defun gortium/convert-to-node (node-dirs &optional project-tag context-tag)
|
||||
(defun gortium/domain->mu4e (domain)
|
||||
"Map an area DOMAIN (personal/work) to its mu4e context name."
|
||||
(if (string= domain "work") "TDNDE" "Perso"))
|
||||
|
||||
(defun gortium/area-domain (area-name)
|
||||
"Return the domain (personal|work) of AREA-NAME from its plan FILETAGS."
|
||||
(let ((af (expand-file-name (concat area-name ".org")
|
||||
(expand-file-name "1-Plan"
|
||||
(expand-file-name area-name
|
||||
(expand-file-name "2-Areas" gortium/exokortex-root))))))
|
||||
(when (file-exists-p af)
|
||||
(with-temp-buffer
|
||||
(insert-file-contents af)
|
||||
(when (re-search-forward "^#\\+FILETAGS:.*:\\\(personal\\|work\\):" nil t)
|
||||
(match-string 1))))))
|
||||
|
||||
(defvar gortium/area-view-keys nil
|
||||
"List of injected per-area/aggregate agenda command keys (for exact removal).")
|
||||
|
||||
(defun gortium/area-tag (area-name)
|
||||
"Return AREA-NAME's own tag (FILETAGS slot-1); defaults to the area name."
|
||||
(let ((af (expand-file-name (concat area-name ".org")
|
||||
(expand-file-name "1-Plan"
|
||||
(expand-file-name area-name
|
||||
(expand-file-name "2-Areas" gortium/exokortex-root))))))
|
||||
(if (file-exists-p af)
|
||||
(with-temp-buffer
|
||||
(insert-file-contents af)
|
||||
(if (re-search-forward "^#\\+FILETAGS:[ \t]*:?\\\([^:]+\\):" nil t)
|
||||
(match-string 1)
|
||||
area-name))
|
||||
area-name)))
|
||||
|
||||
(defun gortium/create-node (category node-name category-emoji area-or-domain tag)
|
||||
"Create a brand new standardized PARA Node inside your ExoKortex vault.
|
||||
Category 2-Areas creates an AREA: gets Name, Emoji, Domain (personal/work)
|
||||
and writes FILETAGS _:name:domain:_. Other categories create a PROJECT:
|
||||
gets Name, Emoji, Area (dropdown of 2-Areas/*) and Tag, writes
|
||||
FILETAGS _:tag:area:_ and inherits the area's mu4e context for dir-locals."
|
||||
(interactive
|
||||
(let* ((category (completing-read "Category: " '("1-Projects" "2-Areas" "3-Resources")))
|
||||
(areas (gortium/area-list)))
|
||||
(if (string= category "2-Areas")
|
||||
(list category
|
||||
(read-string "Area Name: ")
|
||||
(gortium/read-emoji)
|
||||
(completing-read "Domain: " '("personal" "work"))
|
||||
(read-string "Area tag (default = area name): "))
|
||||
(list category
|
||||
(read-string "Node Name: ")
|
||||
(gortium/read-emoji)
|
||||
(completing-read "Area: " areas nil t)
|
||||
(read-string "Project tag: ")))))
|
||||
(let* ((category-dir (expand-file-name category gortium/exokortex-root))
|
||||
(node-dir (expand-file-name node-name category-dir)))
|
||||
(if (string= category "2-Areas")
|
||||
;; AREA: FILETAGS = :tag:domain: (tag chosen, defaults to area name)
|
||||
(let* ((atag (if (string-empty-p tag) node-name tag))
|
||||
(org-file (gortium/ensure-node-structure node-dir
|
||||
"Initialize project context"
|
||||
atag
|
||||
area-or-domain category-emoji
|
||||
(gortium/domain->mu4e area-or-domain))))
|
||||
(find-file org-file)
|
||||
(message "ExoKortex Area '%s' created (tag: %s, domain: %s, mu4e: %s)."
|
||||
node-name atag area-or-domain (gortium/domain->mu4e area-or-domain)))
|
||||
;; PROJECT: tag = tag, slot-2 = area; mu4e context inherited from area
|
||||
(let* ((area-file (expand-file-name (concat area-or-domain ".org")
|
||||
(expand-file-name
|
||||
"1-Plan"
|
||||
(expand-file-name area-or-domain
|
||||
(expand-file-name "2-Areas" gortium/exokortex-root)))))
|
||||
(area-mu4e (when (file-exists-p area-file)
|
||||
(with-temp-buffer
|
||||
(insert-file-contents area-file)
|
||||
(when (re-search-forward "^#\\+FILETAGS:.*:\\(personal\\|work\\):" nil t)
|
||||
(gortium/domain->mu4e (match-string 1)))))))
|
||||
(let ((org-file (gortium/ensure-node-structure node-dir
|
||||
"Initialize project context"
|
||||
tag area-or-domain category-emoji area-mu4e)))
|
||||
(find-file org-file)
|
||||
(message "ExoKortex Project '%s' created (tag: %s, area: %s, mu4e: %s)."
|
||||
node-name tag area-or-domain (or area-mu4e "?")))))))
|
||||
|
||||
(defun gortium/convert-to-node (node-dirs &optional project-tag area-or-domain category-emoji mu4e-context)
|
||||
"Retrofit existing workspace directories into tracked ExoKortex nodes.
|
||||
If folders are explicitly marked in Dirvish/Dired, it batch-processes all of them.
|
||||
Otherwise, prompts for a single directory, defaulting to the item under the cursor.
|
||||
Optional PROJECT-TAG and CONTEXT-TAG can be given to create .projectile and .dir-locals.el."
|
||||
Optional PROJECT-TAG and AREA-OR-DOMAIN set the plan FILETAGS (project: _:tag:area:_,
|
||||
area: _:name:domain:_). Optional CATEGORY-EMOJI is the #+CATEGORY:. Optional
|
||||
MU4E-CONTEXT (Perso/TDNDE) is the dir-locals gortium/context-tag."
|
||||
(interactive
|
||||
(let* ((dired-elements (when (derived-mode-p 'dired-mode)
|
||||
(dired-get-marked-files nil nil nil t)))
|
||||
@@ -2887,18 +3092,40 @@ Optional PROJECT-TAG and CONTEXT-TAG can be given to create .projectile and .dir
|
||||
(file-name-directory cursor-path))
|
||||
(t default-directory))))
|
||||
(list (read-directory-name "Directory to convert: " initial-path initial-path t)))))
|
||||
(tag (read-string "Project tag (blank to skip): "))
|
||||
(ctx (read-string "Context tag (or blank): ")))
|
||||
(list dirs tag ctx)))
|
||||
(is-area (string= (file-name-nondirectory
|
||||
(directory-file-name (expand-file-name ".." (car dirs))))
|
||||
"2-Areas"))
|
||||
(area-or-domain (if is-area
|
||||
(completing-read "Domain: " '("personal" "work"))
|
||||
(completing-read "Area: " (gortium/area-list) nil t)))
|
||||
(tag (unless is-area (read-string "Project tag: ")))
|
||||
(emoji (let ((gortium--emoji (gortium/read-emoji)))
|
||||
(if (string-empty-p gortium--emoji) nil gortium--emoji))))
|
||||
(list dirs tag area-or-domain emoji nil)))
|
||||
|
||||
(let ((count 0)
|
||||
last-org-file)
|
||||
(dolist (dir node-dirs)
|
||||
(let* ((node-dir (expand-file-name dir))
|
||||
(is-area (string= (file-name-nondirectory (directory-file-name (expand-file-name ".." node-dir)))
|
||||
"2-Areas"))
|
||||
(tag2 (unless (string-empty-p project-tag) project-tag))
|
||||
(mctx (or mu4e-context
|
||||
(when (and (not is-area) area-or-domain)
|
||||
(let ((af (expand-file-name
|
||||
(concat area-or-domain ".org")
|
||||
(expand-file-name "1-Plan"
|
||||
(expand-file-name area-or-domain (expand-file-name "2-Areas" gortium/exokortex-root))))))
|
||||
(when (file-exists-p af)
|
||||
(with-temp-buffer
|
||||
(insert-file-contents af)
|
||||
(when (re-search-forward "^#\\+FILETAGS:.*:\\(personal\\|work\\):" nil t)
|
||||
(gortium/domain->mu4e (match-string 1)))))))))
|
||||
(ctx2 (if is-area area-or-domain (or area-or-domain "")))
|
||||
(org-file (gortium/ensure-node-structure node-dir
|
||||
"Audit existing assets and reconcile tracking nodes"
|
||||
(unless (string-empty-p project-tag) project-tag)
|
||||
(unless (string-empty-p context-tag) context-tag))))
|
||||
(if is-area (file-name-nondirectory (directory-file-name node-dir)) project-tag)
|
||||
ctx2 category-emoji mctx)))
|
||||
(setq last-org-file org-file)
|
||||
(cl-incf count)))
|
||||
|
||||
@@ -2906,74 +3133,57 @@ Optional PROJECT-TAG and CONTEXT-TAG can be given to create .projectile and .dir
|
||||
(message "Directory successfully converted to ExoKortex node.")
|
||||
(message "Successfully batch-converted %d directories to ExoKortex nodes!" count))))
|
||||
|
||||
(defun gortium/dired-node-dirs ()
|
||||
"Return marked Dired subdirectories, or the cursor dir if nothing marked."
|
||||
(when (derived-mode-p 'dired-mode)
|
||||
(let* ((elements (dired-get-marked-files nil nil nil t))
|
||||
(has-marks (or (and (listp elements) (> (length elements) 1))
|
||||
(and (consp elements) (eq (car elements) t))))
|
||||
(marked (when has-marks
|
||||
(cl-remove-if-not #'file-directory-p
|
||||
(if (eq (car-safe elements) t)
|
||||
(cdr elements)
|
||||
elements)))))
|
||||
(or marked
|
||||
(let ((cursor (dired-get-filename nil t)))
|
||||
(when (and cursor (file-directory-p cursor))
|
||||
(list cursor)))))))
|
||||
(defun gortium/area-view-body ()
|
||||
"The block list shared by every area/aggregate GTD view."
|
||||
(list
|
||||
(list "agenda" ""
|
||||
(list (list 'org-agenda-span 'week)))
|
||||
(list "todo" "STRT" (list (list 'org-agenda-overriding-header gtd/started-head)))
|
||||
(list "todo" "NEXT" (list (list 'org-agenda-overriding-header gtd/next-action-head)))
|
||||
(list "todo" "WAIT" (list (list 'org-agenda-overriding-header gtd/waiting-head)))
|
||||
(list "gortium/stuck-plans" "" (list (list 'org-agenda-overriding-header gtd/project-head)))
|
||||
(list "todo" "IDEA" (list (list 'org-agenda-overriding-header gtd/someday-head)))))
|
||||
|
||||
(defun gortium/move-node (src-category dst-category node-dirs)
|
||||
"Move NODE-DIRS (absolute paths) from SRC-CATEGORY to DST-CATEGORY.
|
||||
Markers (.projectile/.dir-locals/.hermes-sessions/.git) move with each dir."
|
||||
(let ((root (expand-file-name gortium/exokortex-root))
|
||||
(moved 0))
|
||||
(dolist (dir node-dirs)
|
||||
(let* ((name (file-name-nondirectory (directory-file-name dir)))
|
||||
(src (expand-file-name name (expand-file-name src-category root)))
|
||||
(dst (expand-file-name name (expand-file-name dst-category root))))
|
||||
(unless (file-directory-p src)
|
||||
(user-error "No node '%s' in %s" name src-category))
|
||||
(when (file-exists-p dst)
|
||||
(user-error "%s already has a node named '%s'" dst-category name))
|
||||
(make-directory (file-name-directory dst) t)
|
||||
(rename-file src dst)
|
||||
;; Stale path: drop from the Projectile cache so the switch-list cleans up.
|
||||
(when (fboundp 'projectile-remove-known-project)
|
||||
(projectile-remove-known-project src))
|
||||
(cl-incf moved)))
|
||||
(message "Archived/restored %d node(s): %s → %s" moved src-category dst-category)))
|
||||
|
||||
(defun gortium/archive-node ()
|
||||
"Move marked (or cursor) nodes from 1-Projects to 4-Archives."
|
||||
(interactive)
|
||||
(let ((dirs (gortium/dired-node-dirs)))
|
||||
(unless dirs
|
||||
(user-error "No node under Dired cursor / no marked directories"))
|
||||
(gortium/move-node "1-Projects" "4-Archives" dirs)))
|
||||
|
||||
(defun gortium/restore-node (category)
|
||||
"Move marked (or cursor) archived nodes back into CATEGORY."
|
||||
(interactive
|
||||
(list (completing-read "Restore to category: "
|
||||
'("1-Projects" "2-Areas" "3-Resources"))))
|
||||
(let ((dirs (gortium/dired-node-dirs)))
|
||||
(unless dirs
|
||||
(user-error "No node under Dired cursor / no marked directories"))
|
||||
(gortium/move-node "4-Archives" category dirs)))
|
||||
|
||||
(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/inject-area-views ()
|
||||
"Recompute per-area + aggregate (personal/work) agenda views from 2-Areas/*.
|
||||
Removes prior injected keys (gortium/area-view-keys) exactly, then rebuilds.
|
||||
Per-area key: \"a\"+area-name (unique). Aggregates: \"pw\" (all personal), \"ww\" (all work)."
|
||||
(let* ((areas (gortium/area-list))
|
||||
(personal-tags
|
||||
(mapcar (lambda (a) (concat "+" (gortium/area-tag a)))
|
||||
(seq-filter (lambda (a) (string= (gortium/area-domain a) "personal")) areas)))
|
||||
(work-tags
|
||||
(mapcar (lambda (a) (concat "+" (gortium/area-tag a)))
|
||||
(seq-filter (lambda (a) (string= (gortium/area-domain a) "work")) areas))))
|
||||
;; 1) drop stale injected keys
|
||||
(setq org-agenda-custom-commands
|
||||
(cl-remove-if (lambda (e) (member (car e) gortium/area-view-keys))
|
||||
org-agenda-custom-commands))
|
||||
(setq gortium/area-view-keys nil)
|
||||
;; 2) per-area views
|
||||
(dolist (a areas)
|
||||
(let ((key (concat "a" a)))
|
||||
(push key gortium/area-view-keys)
|
||||
(push (list key (format "GTD — %s" a)
|
||||
(gortium/area-view-body)
|
||||
(list (list 'org-agenda-tag-filter-preset (list (concat "+" (gortium/area-tag a))))))
|
||||
org-agenda-custom-commands)))
|
||||
;; 3) aggregates
|
||||
(when personal-tags
|
||||
(push "pw" gortium/area-view-keys)
|
||||
(push (list "pw" "📋 All personal areas — GTD"
|
||||
(gortium/area-view-body)
|
||||
(list (list 'org-agenda-tag-filter-preset personal-tags)))
|
||||
org-agenda-custom-commands))
|
||||
(when work-tags
|
||||
(push "ww" gortium/area-view-keys)
|
||||
(push (list "ww" "💼 All work areas — GTD"
|
||||
(gortium/area-view-body)
|
||||
(list (list 'org-agenda-tag-filter-preset work-tags)))
|
||||
org-agenda-custom-commands))))
|
||||
|
||||
(gortium/inject-area-views)
|
||||
(add-hook 'org-agenda-mode-hook #'gortium/inject-area-views)
|
||||
(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
|
||||
|
||||
Reference in New Issue
Block a user