840 lines
34 KiB
EmacsLisp
840 lines
34 KiB
EmacsLisp
;;; org-x-dag.el --- Org-in-a-DAG -*- lexical-binding: t; -*-
|
|
|
|
;; Copyright (C) 2022 Nathan Dwarshuis
|
|
|
|
;; This program is free software; you can redistribute it and/or modify
|
|
;; it under the terms of the GNU General Public License as published by
|
|
;; the Free Software Foundation, either version 3 of the License, or
|
|
;; (at your option) any later version.
|
|
|
|
;; This program is distributed in the hope that it will be useful,
|
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
;; GNU General Public License for more details.
|
|
|
|
;; You should have received a copy of the GNU General Public License
|
|
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
;;; Commentary:
|
|
|
|
;; Welcome to Dagestan, you will be smeshed...
|
|
|
|
;;; Code:
|
|
|
|
;; TODO this depends on other stuff in org-x like the file and id operations
|
|
(require 'org)
|
|
(require 'org-ml)
|
|
(require 'dash)
|
|
(require 'dag)
|
|
(require 'ht)
|
|
|
|
;;; GLOBAL STATE
|
|
|
|
;; variables to store state
|
|
|
|
(defvar org-x-dag nil
|
|
"The org-x DAG.
|
|
|
|
Each node in this DAG represents a headline with the following
|
|
characteristics:
|
|
- contained in a file as given by `org-x-dag-get-files'
|
|
- has a keyword
|
|
- either has an immediate parent with a keyword or has no parents
|
|
with keywords
|
|
|
|
Each node is represented by a key, which is either a string
|
|
representing the headlines's ID property or a cons cell
|
|
like (FILE POS) representing the staring position in file/buffer
|
|
of the headline (aka a \"pseudo-marker\").")
|
|
|
|
(defvar org-x-dag-sync-state nil
|
|
"An alist representing the sync state of the DAG.
|
|
|
|
The car of each cell is the file path, and the cdr is the md5 of
|
|
that file as it currently sits on disk.")
|
|
|
|
;; functions to construct nodes within state
|
|
|
|
(defun org-x-dag-build-key (file point level todo tags toplevelp id)
|
|
(list :file file
|
|
:point point
|
|
:level level
|
|
:todo todo
|
|
:tags tags
|
|
:toplevelp toplevelp
|
|
:id id))
|
|
;; (if id (list :id file point id) (list :pm file point)))
|
|
|
|
(defun org-x-dag-key-get-file (key)
|
|
"Return file for KEY."
|
|
(plist-get key :file))
|
|
;; (nth 1 key))
|
|
|
|
(defun org-x-dag-key-get-point (key)
|
|
"Return point for KEY."
|
|
(plist-get key :point))
|
|
;; (nth 2 key))
|
|
|
|
;;; DAG SYNCHRONIZATION/CONSTRUCTION
|
|
|
|
(defun org-x-dag-get-files ()
|
|
"Return a list of all files to be used in the DAG."
|
|
;; (list "/mnt/data/Org/projects/router.org"
|
|
;; "/mnt/data/Org/projects/omnimacs.org"
|
|
;; ))
|
|
`(,(org-x-get-lifetime-goal-file)
|
|
,(org-x-get-endpoint-goal-file)
|
|
,@(org-x-get-action-files)))
|
|
|
|
(defun org-x-dag-get-md5 (path)
|
|
"Get the md5 checksum of PATH."
|
|
(with-temp-buffer
|
|
(let ((rc (call-process "md5sum" nil (current-buffer) nil path)))
|
|
(if (/= 0 rc) (error "Could not get md5 of %s" path)
|
|
(->> (buffer-string)
|
|
(s-match "^\\([0-9a-z]+\\)")
|
|
(cadr))))))
|
|
|
|
(defun org-x-dag-md5-matches-p (path md5)
|
|
"Return t if the md5 of PATH on disk `equal's MD5."
|
|
(equal (org-x-dag-get-md5 path) md5))
|
|
|
|
(defun org-x-dag-file-is-dirty (file md5)
|
|
"Return t if FILE with MD5 has been recently changed."
|
|
(or (org-x-with-file file (buffer-modified-p))
|
|
(not (org-x-dag-md5-matches-p file md5))))
|
|
|
|
(defun org-x-dag-set-sync-state ()
|
|
"Set the sync state to reflect the current files on disk."
|
|
(->> (org-x-dag-get-files)
|
|
(--map (cons it (org-x-dag-get-md5 it)))
|
|
(setq org-x-dag-sync-state)))
|
|
|
|
(defun org-x-dag-get-sync-state ()
|
|
"Return the sync state.
|
|
|
|
The returned value will be a list like (TO-REMOVE TO-INSERT
|
|
TO-UPDATE) which will contain the file paths the should be
|
|
removed from, added to, or edited within the DAG respectively."
|
|
(cl-flet
|
|
((lookup-md5
|
|
(path)
|
|
(alist-get path org-x-dag-sync-state nil nil #'equal)))
|
|
(-let* ((existing-files (org-x-dag-get-files))
|
|
(state-files (-map #'car org-x-dag-sync-state))
|
|
(to-remove (-difference state-files existing-files))
|
|
(to-insert (-difference existing-files state-files))
|
|
(to-update
|
|
(->> (-intersection existing-files state-files)
|
|
(--filter (org-x-dag-file-is-dirty it (lookup-md5 it))))))
|
|
;; (print (list to-remove to-insert to-update))
|
|
(list to-remove to-insert to-update))))
|
|
|
|
;; TODO this assumes the `org-id-locations' is synced
|
|
(defun org-x-dag-get-buffer-nodes (file kws)
|
|
"Return a list of nodes from FILE.
|
|
|
|
A node will only be returned if the headline to which it points
|
|
has a valid (meaning in KWS) keyword and either its parent has a
|
|
valid keyword or none of its parents have valid keywords."
|
|
(let ((more t)
|
|
cur-path this-point this-key this-level this-todo has-todo this-parent
|
|
tags toplevelp acc)
|
|
;; TODO add org-mode sanity check
|
|
(goto-char (point-min))
|
|
;; move forward until on a headline
|
|
(while (and (not (= ?* (following-char))) (= 0 (forward-line 1))))
|
|
;; Build alist; Keep track of how 'deep' we are in a given org-tree using a
|
|
;; stack. The stack will have members like (LEVEL KEY) where LEVEL is the
|
|
;; level of the headline and KEY is the node key if it has a keyword. Only
|
|
;; add a node to the accumulator if it has a keyword, and only include its
|
|
;; parent headline if the parent also has a keyword (add the link targets
|
|
;; regardless).
|
|
(while more
|
|
(when (= ?* (following-char))
|
|
(setq this-point (point)
|
|
this-key nil)
|
|
;; Get tags (must be done from the first column)
|
|
(setq this-tags (org--get-local-tags))
|
|
;; Get the level
|
|
(while (= ?* (following-char)) (forward-char 1))
|
|
(setq this-level (current-column))
|
|
;; Check if the headline has a keyword
|
|
(forward-char 1)
|
|
(while (not (memq (following-char) '(? ?\n))) (forward-char 1))
|
|
(setq this-todo (-> (+ 1 this-point this-level)
|
|
(buffer-substring (+ this-point (current-column))))
|
|
has-todo (member this-todo kws))
|
|
;; Adjust the stack so that the top headline is the parent of the
|
|
;; current headline
|
|
(while (and cur-path (<= this-level (nth 0 (car cur-path))))
|
|
(!cdr cur-path))
|
|
(setq this-parent (car cur-path)
|
|
toplevelp (not (nth 1 this-parent)))
|
|
;; Add the current headline to accumulator if it has a keyword, but only
|
|
;; if its parent has a keyword or none of its parents have keywords
|
|
(when (and has-todo (or (not toplevelp) (--none-p (nth 1 it) cur-path)))
|
|
;; If parent is not a todo and we want tag inheritance, store all tags
|
|
;; above this headline (sans file-tags which we can get later easily)
|
|
(setq tags (if (and toplevelp org-use-tag-inheritance)
|
|
(->> cur-path
|
|
(--mapcat (nth 2 it))
|
|
(append this-tags))
|
|
this-tags)
|
|
this-key (org-x-dag-build-key file
|
|
this-point
|
|
this-level
|
|
(substring-no-properties this-todo)
|
|
tags
|
|
toplevelp
|
|
(car (org--property-local-values "ID" nil))))
|
|
;; (org-entry-get nil "ID")))
|
|
;; TODO also get a list of link parent targets and add them to the
|
|
;; parent list
|
|
(!cons (cons this-key (-some-> (nth 1 this-parent) (list))) acc))
|
|
;; Add current headline to stack
|
|
;; (when (and (s-contains-p "general" file) (not (nth 1 this-parent)))
|
|
;; (print (--map (nth 2 it) cur-path)))
|
|
;; (print (list cur-path this-tags)))
|
|
(!cons (list this-level this-key this-tags) cur-path))
|
|
(setq more (= 0 (forward-line 1))))
|
|
(nreverse acc)))
|
|
|
|
(defun org-x-dag-get-file-nodes (file)
|
|
"Return all nodes in FILE in one pass."
|
|
(org-x-with-file file
|
|
(org-x-dag-get-buffer-nodes file org-todo-keywords-1)))
|
|
|
|
;; (defun org-x-dag-key-is-pseudo-marker (key)
|
|
;; "Return t if KEY is a pseudo marker."
|
|
;; (eq (car key) :pm))
|
|
;; ;; (= 2 (length key)))
|
|
;; ;; (and (consp key) (stringp (car key)) (numberp (cdr key))))
|
|
|
|
;; (defun org-x-dag-key-is-id (key)
|
|
;; "Return t if KEY is an ID."
|
|
;; ;; (= 3 (length key)))
|
|
;; (eq (car key) :id))
|
|
|
|
(defun org-x-dag-files-contains-key-p (key files)
|
|
"Return t if KEY represents a node contained in FILES."
|
|
(-if-let (other-file (org-x-dag-key-get-file key))
|
|
(--any-p (equal other-file it) files)
|
|
(error "Invalid key: %s" key)))
|
|
;; (cl-flet
|
|
;; ((contains-key
|
|
;; (files other-file)
|
|
;; (--any-p (equal other-file it) files)))
|
|
;; (cond
|
|
;; ((org-x-dag-key-is-id key)
|
|
;; (-some->> (ht-get org-id-locations key)
|
|
;; (contains-key files)))
|
|
;; ((org-x-dag-key-is-pseudo-marker key)
|
|
;; (contains-key files (car key)))
|
|
;; (t
|
|
;; (error "Invalid key: %s" key)))))
|
|
|
|
(defun org-x-dag-get-nodes-in-files (dag files)
|
|
(let ((x (->> (dag-get-nodes-and-edges-where org-x-dag
|
|
(org-x-dag-files-contains-key-p it files))
|
|
(-map #'car)))
|
|
(y (dag-get-floating-nodes-where org-x-dag
|
|
(org-x-dag-files-contains-key-p it files))))
|
|
;; (print (list x y))
|
|
;; (print x)
|
|
;; (print (list (length x) (length y) (length (-intersection x y))))
|
|
(append x y)))
|
|
|
|
;; TODO there is a HUGE DIFFERENCE between a 'key' (the things in the hash table
|
|
;; the look things up) and a 'node' (which is a cons cell, the car of which is a
|
|
;; 'key' and the cdr of which is a 'relation'). These names suck, but the point
|
|
;; is we need to distinguish between them otherwise really strange things happen
|
|
(defun org-x-dag-update (to-remove to-insert to-update)
|
|
"Update the DAG given files to add and remove.
|
|
|
|
TO-REMOVE, TO-INSERT, and TO-UPDATE are lists of files to remove
|
|
from, add to, and update with the DAG."
|
|
(let* ((files-to-insert (append to-update to-insert))
|
|
(nodes-to-insert (-mapcat #'org-x-dag-get-file-nodes files-to-insert)))
|
|
(if org-x-dag
|
|
(let* ((files-to-remove (append to-update to-remove))
|
|
(keys-to-remove (->> (org-x-dag-get-nodes-in-files
|
|
org-x-dag files-to-remove))))
|
|
(when (or keys-to-remove nodes-to-insert)
|
|
(setq org-x-dag (dag-edit-nodes keys-to-remove
|
|
nodes-to-insert
|
|
org-x-dag))))
|
|
(setq org-x-dag (dag-alist-to-dag nodes-to-insert)))))
|
|
|
|
(defun org-x-dag-sync (&optional force)
|
|
"Sync the DAG with files from `org-x-dag-get-files'.
|
|
|
|
If FORCE is non-nil, sync no matter what."
|
|
(when force
|
|
(setq org-x-dag-sync-state nil
|
|
org-x-dag nil))
|
|
(-let (((to-remove to-insert to-update) (org-x-dag-get-sync-state)))
|
|
(org-x-dag-update to-remove to-insert to-update)
|
|
(org-x-dag-set-sync-state)
|
|
nil))
|
|
|
|
;;; DAG -> HEADLINE RETRIEVAL
|
|
|
|
;; ;; TODO this is silly since there can only be one parent, this function may
|
|
;; ;; be doing too much
|
|
;; (defun org-x-dag-relation-has-parent-headlines-p (key relation)
|
|
;; ""
|
|
;; (let ((this-file (org-x-dag-key-get-file key)))
|
|
;; (->> (dag-relation-get-parents relation)
|
|
;; (--any-p (equal this-file (org-x-dag-key-get-file it))))))
|
|
|
|
(defun org-x-dag-relation-has-child-headlines-p (key relation)
|
|
""
|
|
(let ((this-file (org-x-dag-key-get-file key)))
|
|
(->> (dag-relation-get-children relation)
|
|
(--any-p (equal this-file (org-x-dag-key-get-file it))))))
|
|
|
|
;; (defun org-x-dag-key-has-child-headlines-p (key dag)
|
|
;; (org-x-dag-relation-has-child-headlines-p key (dag-get-relationships key dag)))
|
|
|
|
(defun org-x-dag-get-standalone-task-nodes (dag)
|
|
"Return the standalone task nodes of DAG."
|
|
(let* ((action-files (org-x-get-action-files))
|
|
(from-adjlist
|
|
(dag-get-nodes-and-edges-where dag
|
|
(and (org-x-dag-files-contains-key-p it action-files)
|
|
(plist-get it :toplevelp)
|
|
(not (org-x-dag-relation-has-child-headlines-p it it-rel)))))
|
|
(from-floating
|
|
(dag-get-floating-nodes-where dag
|
|
(org-x-dag-files-contains-key-p it action-files))))
|
|
(append (-map #'car from-adjlist) from-floating)))
|
|
|
|
(defun org-x-dag-get-toplevel-project-nodes (dag)
|
|
"Return the toplevel project nodes of DAG."
|
|
(let ((action-files (org-x-get-action-files)))
|
|
(dag-get-nodes-and-edges-where dag
|
|
(and (org-x-dag-files-contains-key-p it action-files)
|
|
(plist-get it :toplevelp)
|
|
(org-x-dag-relation-has-child-headlines-p it it-rel)))))
|
|
|
|
;;; DAG -> HEADLINE RETRIEVAL (CHILD/PARENT)
|
|
|
|
(defun org-x-dag-filter-children (dag key fun)
|
|
(declare (indent 2))
|
|
(-filter fun (dag-get-children key dag)))
|
|
|
|
(defun org-x-dag-separate-children (dag key fun)
|
|
(declare (indent 2))
|
|
(-separate fun (dag-get-children key dag)))
|
|
|
|
(defun org-x-dag-node-get-headline-children (dag key)
|
|
(let ((this-file (org-x-dag-key-get-file key)))
|
|
(org-x-dag-filter-children dag key
|
|
(lambda (it) (equal this-file (org-x-dag-key-get-file it))))))
|
|
|
|
;; TODO somewhere in here I need to filter based on headline like CANC
|
|
(defun org-x-dag-project-node-get-task-nodes (dag key)
|
|
(declare (indent 2))
|
|
;; NOTE if this is a standalone task it will return itself
|
|
(-if-let (cs (org-x-dag-node-get-headline-children dag key))
|
|
;; TODO don't hardcode this
|
|
(->> (--remove (member (plist-get it :todo) (list org-x-kw-canc org-x-kw-hold)) cs)
|
|
(--mapcat (org-x-dag-project-node-get-task-nodes dag it)))
|
|
(list key)))
|
|
|
|
(defun org-x-dag-get-project-task-nodes (fun dag)
|
|
"Return project task nodes of DAG."
|
|
(-let (((&plist :adjlist) dag))
|
|
(->> (org-x-dag-get-toplevel-project-nodes dag)
|
|
(-map #'car)
|
|
(-remove fun)
|
|
(--mapcat (org-x-dag-project-node-get-task-nodes dag it)))))
|
|
|
|
(defun org-x-dag-project-node-get-subproject-nodes (dag key)
|
|
(-when-let (cs (org-x-dag-node-get-headline-children dag key))
|
|
(cons key (--mapcat (org-x-dag-project-node-get-subproject-nodes dag it) cs))))
|
|
|
|
(defun org-x-dag-get-subproject-task-nodes (dag)
|
|
"Return subproject nodes of DAG."
|
|
;; ignore floating nodes since these by definition can't be part of projects
|
|
(-let (((&plist :adjlist) dag))
|
|
(->> (org-x-dag-get-toplevel-project-nodes dag)
|
|
(-map #'car)
|
|
(--mapcat (org-x-dag-project-node-get-subproject-nodes dag it)))))
|
|
|
|
;; (defmacro org-x-dag-with-key (key &rest body)
|
|
;; (declare (indent 1))
|
|
;; `(cond
|
|
;; ((org-x-dag-key-is-pseudo-marker ,key)
|
|
;; (org-x-with-file (car ,key)
|
|
;; (goto-char (cdr ,key))
|
|
;; ,@body))
|
|
;; ((org-x-dag-key-is-id ,key)
|
|
;; (org-x-with-id-target ,key
|
|
;; ,@body))))
|
|
|
|
;; NODE FORMATTING
|
|
|
|
(defun org-x-dag-get-headline-with-props (pos type face)
|
|
(goto-char pos)
|
|
(let* ((head (org-get-heading))
|
|
(level (-> (org-outline-level)
|
|
(org-reduced-level)
|
|
(1-)
|
|
(make-string ?.)))
|
|
(category (org-get-category))
|
|
(todo-state (org-get-todo-state))
|
|
(inherited-tags
|
|
(or (eq org-agenda-show-inherited-tags 'always)
|
|
(and (listp org-agenda-show-inherited-tags)
|
|
(memq 'agenda org-agenda-show-inherited-tags))
|
|
(and (eq org-agenda-show-inherited-tags t)
|
|
(or (eq org-agenda-use-tag-inheritance t)
|
|
(memq 'agenda
|
|
org-agenda-use-tag-inheritance)))))
|
|
(tags (org-get-tags nil (not inherited-tags)))
|
|
(item (org-agenda-format-item "" head level category tags nil nil nil))
|
|
(marker (org-agenda-new-marker pos)))
|
|
(org-add-props item nil
|
|
'org-marker marker
|
|
'org-hd-marker marker
|
|
'org-not-done-regexp org-not-done-regexp
|
|
'org-todo-regexp org-todo-regexp
|
|
'org-complex-heading-regexp org-complex-heading-regexp
|
|
'mouse-face 'highlight
|
|
'help-echo (format "mouse-2 or RET jump to Org file %s"
|
|
(abbreviate-file-name buffer-file-name))
|
|
'undone-face face
|
|
;; TODO in the case of scheduled headline this has other stuff in it
|
|
'priority (org-get-priority item)
|
|
'todo-state todo-state
|
|
'face face
|
|
'type type)))
|
|
|
|
(defun org-x-dag-nodes-to-headlines (nodes)
|
|
(->> (-group-by #'org-x-dag-key-get-file nodes)
|
|
(--map (-let (((path . nodes) it))
|
|
(org-x-with-file path
|
|
(->> (-map #'org-x-dag-key-get-point nodes)
|
|
(--map (progn (goto-char it)
|
|
(substring-no-properties (org-get-heading))))))))
|
|
;; (->> (-map #'org-x-dag-key-get-point nodes)
|
|
;; (-map #'org-x-dag-get-headline-with-props)))))
|
|
(-flatten-n 1)))
|
|
|
|
(defun org-x-dag-collapse-tags (tags)
|
|
"Return TAGS with duplicates removed.
|
|
|
|
In the case of mutually exclusive tags, only the first tag
|
|
encountered will be returned."
|
|
(-let (((x non-x) (--separate (memq (elt it 0) org-x-exclusive-prefixes) tags)))
|
|
(->> (--group-by (elt it 0) x)
|
|
(--map (car (cdr it)) )
|
|
(append (-uniq non-x)))))
|
|
|
|
(defun org-x-dag-add-default-props (item)
|
|
(org-add-props item nil
|
|
'org-not-done-regexp org-not-done-regexp
|
|
'org-todo-regexp org-todo-regexp
|
|
'org-complex-heading-regexp org-complex-heading-regexp
|
|
'mouse-face 'default))
|
|
|
|
(defun org-x-dag-format-tag-node (category tags key)
|
|
;; ASSUME I don't use subtree-level categories
|
|
(-let* (;; (category (org-get-category))
|
|
(head (org-get-heading))
|
|
(level (-> (plist-get key :level)
|
|
(make-string ?s)))
|
|
;; no idea what this does...
|
|
(help-echo (format "mouse-2 or RET jump to Org file %S"
|
|
(abbreviate-file-name
|
|
(or (buffer-file-name (buffer-base-buffer))
|
|
(buffer-name (buffer-base-buffer))))))
|
|
(marker (org-agenda-new-marker))
|
|
;; no idea what this function actually does
|
|
((ts . ts-type) (org-agenda-entry-get-agenda-timestamp (point)))
|
|
(item (org-agenda-format-item "" head level category tags))
|
|
(priority (org-get-priority item)))
|
|
(-> (org-x-dag-add-default-props item)
|
|
(org-add-props nil
|
|
;; face
|
|
'face 'default
|
|
'done-face 'org-agenda-done
|
|
'undone-face 'default
|
|
;; marker
|
|
'org-hd-marker marker
|
|
'org-marker marker
|
|
;; headline stuff
|
|
'todo-state (plist-get key :todo)
|
|
'priority priority
|
|
'ts-date ts
|
|
;; misc
|
|
'type (concat "tagsmatch" ts-type)
|
|
'help-echo help-echo))))
|
|
|
|
(defun org-x-dag-key-is-iterator (key)
|
|
(org-x-with-file (org-x-dag-key-get-file key)
|
|
(->> (org-entry-get (org-x-dag-key-get-point key) org-x-prop-parent-type)
|
|
(equal org-x-prop-parent-type-iterator))))
|
|
|
|
;; (defmacro org-x-dag-do-file-nodes (path keys form)
|
|
;; (declare (indent 2))
|
|
;; `(let ((acc))
|
|
;; (org-x-with-file ,path
|
|
;; ;; ;; TODO tbh this could just be the file basename since that's all
|
|
;; ;; ;; I ever use
|
|
;; ;; (let ((it-category (org-get-category)))
|
|
;; (--each keys
|
|
;; (goto-char (org-x-dag-key-get-point it))
|
|
;; ,form))
|
|
;; (nreverse acc)))
|
|
|
|
(defun org-x-headline-has-timestamp (re want-time)
|
|
(let ((end (save-excursion (outline-next-heading))))
|
|
(-when-let (p (save-excursion (re-search-forward re end t)))
|
|
(if want-time (org-2ft (match-string 1)) p))))
|
|
|
|
(defun org-x-headline-is-deadlined (want-time)
|
|
(org-x-headline-has-timestamp org-deadline-time-regexp want-time))
|
|
|
|
(defun org-x-headline-is-scheduled (want-time)
|
|
(org-x-headline-has-timestamp org-scheduled-time-regexp want-time))
|
|
|
|
(defun org-x-headline-is-closed (want-time)
|
|
(org-x-headline-has-timestamp org-closed-time-regexp want-time))
|
|
|
|
(defconst org-x-headline-task-status-priorities
|
|
'((:archivable . -1)
|
|
(:complete . -1)
|
|
(:expired . 0)
|
|
(:done-unclosed . 0)
|
|
(:undone-closed . 0)
|
|
(:active . 1)
|
|
(:inert . 2)))
|
|
|
|
(defconst org-x-project-status-priorities
|
|
'((:archivable . -1)
|
|
(:complete . -1)
|
|
(:scheduled-project . 0)
|
|
(:invalid-todostate . 0)
|
|
(:undone-complete . 0)
|
|
(:done-incomplete . 0)
|
|
(:stuck . 0)
|
|
(:wait . 1)
|
|
(:held . 2)
|
|
(:active . 3)
|
|
(:inert . 4)))
|
|
|
|
(defun org-x-headline-get-task-status-0 (kw)
|
|
(if (member kw org-x-done-keywords)
|
|
(-if-let (c (org-x-headline-is-closed t))
|
|
(if (< (- (float-time) c) (* 60 60 24 org-x-archive-delay))
|
|
:archivable
|
|
:complete)
|
|
:done-unclosed)
|
|
(cond
|
|
((org-x-headline-is-expired-p) :expired)
|
|
((org-x-headline-is-inert-p) :inert)
|
|
((org-x-headline-is-closed nil) :undone-closed)
|
|
(t :active))))
|
|
|
|
(defmacro org-x--descend-into-project (dag key children statuscode-tree get-task-status callback-form)
|
|
;; define "breaker-status" as the last of the allowed-statuscodes
|
|
;; when this is encountered the loop is broken because we are done
|
|
;; (the last entry trumps all others)
|
|
(declare (indent 3))
|
|
(let* ((allowed-statuscodes (-map #'car statuscode-tree))
|
|
(trans-tbl (->> statuscode-tree
|
|
(--map (-let (((a . bs) it)) (--map (cons it a) bs)))
|
|
(-flatten-n 1)))
|
|
(breaker-status (-last-item allowed-statuscodes))
|
|
(initial-status (car allowed-statuscodes)))
|
|
`(save-excursion
|
|
(let ((project-status ,initial-status)
|
|
(this-child nil)
|
|
(it-kw nil)
|
|
(new-status nil))
|
|
;; loop through tasks one level down until breaker-status found
|
|
(while (and children (not (eq project-status ,breaker-status)))
|
|
(setq this-child (car children)
|
|
it-kw (plist-get this-child :todo))
|
|
;; If project returns an allowed status then use that. Otherwise look
|
|
;; up the value in the translation table and return error if not
|
|
;; found.
|
|
(-if-let (cs (org-x-dag-node-get-headline-children dag this-child))
|
|
(unless (member (setq new-status
|
|
(funcall ,callback-form
|
|
,dag this-child cs))
|
|
',allowed-statuscodes)
|
|
(setq new-status (alist-get new-status ',trans-tbl)))
|
|
(goto-char (org-x-dag-key-get-point this-child))
|
|
(setq new-status (nth ,get-task-status ',allowed-statuscodes)))
|
|
(when (org-x--compare-statuscodes ',allowed-statuscodes
|
|
new-status > project-status)
|
|
(setq project-status new-status))
|
|
(!cdr children))
|
|
project-status))))
|
|
|
|
(defmacro org-x-dag-descend-into-project (dag keys parent-tags codetree
|
|
task-form callback)
|
|
(declare (indent 3))
|
|
(let ((allowed-codes (-map #'car codetree))
|
|
(trans-tbl (--mapcat (-let (((a . bs) it))
|
|
(--map (cons it a) bs))
|
|
codetree)))
|
|
`(cl-flet
|
|
((get-project-or-task-status
|
|
(key)
|
|
(-if-let (children (org-x-dag-node-get-headline-children ,dag key))
|
|
(let* ((tags (-> (plist-get key :tags)
|
|
(append ,parent-tags)
|
|
(org-x-dag-collapse-tags)))
|
|
(child-results (funcall ,callback ,dag key tags children))
|
|
;; ASSUME the car of the results will be the toplevel
|
|
;; key/status pair for this (sub)project
|
|
(top-status (plist-get (car child-results) :status))
|
|
(top-status* (if (member top-status ',allowed-codes)
|
|
top-status
|
|
(alist-get top-status ',trans-tbl))))
|
|
(cons top-status* child-results))
|
|
(let ((it-kw (plist-get key :todo)))
|
|
(goto-char (org-x-dag-key-get-point key))
|
|
(-> ,task-form
|
|
(nth ',allowed-codes)
|
|
(list))))))
|
|
(let* ((results (-map #'get-project-or-task-status ,keys))
|
|
(status (->> (-map #'car results)
|
|
(--max-by (> (-elem-index it ',allowed-codes)
|
|
(-elem-index other ',allowed-codes))))))
|
|
(cons status (-mapcat #'cdr results))))))
|
|
|
|
(defun org-x-dag-headline-get-project-status (dag key tags children)
|
|
;; ASSUME children will always be at least 1 long
|
|
(goto-char (org-x-dag-key-get-point key))
|
|
(let ((keyword (plist-get key :todo)))
|
|
(-let (((status . child-results)
|
|
(cond
|
|
((org-x-headline-is-scheduled nil)
|
|
(list :scheduled-project))
|
|
((equal keyword org-x-kw-hold)
|
|
(list (if (org-x-headline-is-inert-p) :inert :held)))
|
|
((member keyword org-x--project-invalid-todostates)
|
|
(list :invalid-todostate))
|
|
((equal keyword org-x-kw-canc)
|
|
(list (if (org-x-headline-is-archivable-p) :archivable :complete)))
|
|
((equal keyword org-x-kw-done)
|
|
(org-x-dag-descend-into-project dag children tags
|
|
((:archivable)
|
|
(:complete)
|
|
(:done-incomplete :stuck :inert :held :wait :active
|
|
:scheduled-project :invalid-todostate
|
|
:undone-complete))
|
|
(if (member it-kw org-x-done-keywords)
|
|
(if (org-x-headline-is-archivable-p) 0 1)
|
|
2)
|
|
#'org-x-dag-headline-get-project-status))
|
|
((equal keyword org-x-kw-todo)
|
|
(org-x-dag-descend-into-project dag children tags
|
|
((:undone-complete :complete :archivable)
|
|
(:stuck :scheduled-project :invalid-todostate :done-incomplete)
|
|
(:held)
|
|
(:wait)
|
|
(:inert)
|
|
(:active))
|
|
(cond
|
|
((and (not (member it-kw org-x-done-keywords))
|
|
(org-x-headline-is-inert-p))
|
|
4)
|
|
((equal it-kw org-x-kw-todo)
|
|
(if (org-x-headline-is-scheduled nil) 5 1))
|
|
((equal it-kw org-x-kw-hold)
|
|
2)
|
|
((equal it-kw org-x-kw-wait)
|
|
3)
|
|
((equal it-kw org-x-kw-next)
|
|
5)
|
|
(t 0))
|
|
#'org-x-dag-headline-get-project-status))
|
|
(t (error "Invalid keyword detected: %s" keyword)))))
|
|
(cons (list :key key :status status :tags tags) child-results))))
|
|
|
|
(defmacro org-x-dag-with-keys-in-files (keys form)
|
|
(declare (indent 1))
|
|
`(->> (-group-by #'org-x-dag-key-get-file ,keys)
|
|
(--mapcat (org-x-with-file (car it)
|
|
(--mapcat ,form (cdr it))))
|
|
(-non-nil)))
|
|
|
|
(defmacro org-x-dag-with-key (key &rest body)
|
|
(declare (indent 1))
|
|
`(progn
|
|
(goto-char (org-x-dag-key-get-point ,key))
|
|
,@body))
|
|
|
|
(defun org-x-dag-scan-projects ()
|
|
(cl-flet*
|
|
((format-result
|
|
(cat result)
|
|
(-let* (((&plist :key :status :tags) result)
|
|
(priority (alist-get status org-x-project-status-priorities)))
|
|
(when (>= priority 0)
|
|
(org-x-dag-with-key key
|
|
(-> (org-x-dag-format-tag-node cat tags key)
|
|
(org-add-props nil
|
|
'x-toplevelp (plist-get key :toplevelp)
|
|
'x-status status
|
|
'x-priority priority)))))))
|
|
(let ((keys (->> (org-x-dag-get-toplevel-project-nodes org-x-dag)
|
|
(-map #'car))))
|
|
(org-x-dag-with-keys-in-files keys
|
|
(org-x-dag-with-key it
|
|
(let ((cat (org-get-category))
|
|
(tags (-> (plist-get it :tags)
|
|
(append org-file-tags)
|
|
(org-x-dag-collapse-tags))))
|
|
;; TODO don't hardcode these things
|
|
(unless (or (member org-x-tag-incubated tags)
|
|
(save-excursion
|
|
(-> org-x-prop-parent-type
|
|
(org--property-local-values nil)
|
|
(car)
|
|
(equal org-x-prop-parent-type-iterator))))
|
|
(->> (org-x-dag-node-get-headline-children org-x-dag it)
|
|
(org-x-dag-headline-get-project-status org-x-dag it tags)
|
|
(--map (format-result cat it))))))))))
|
|
|
|
;; TODO making this an imperative-style loop doesn't speed it up 'that-much'
|
|
(defun org-x-dag-scan-tasks ()
|
|
(let* ((dag org-x-dag)
|
|
(sats (->> (org-x-dag-get-standalone-task-nodes dag)
|
|
(--map (cons it :is-standalone))))
|
|
(pts (->> (org-x-dag-get-project-task-nodes #'org-x-dag-key-is-iterator dag)
|
|
(--map (list it))))
|
|
(grouped (->> (append sats pts)
|
|
(--group-by (org-x-dag-key-get-file (car it)))))
|
|
acc path key-cells category key tags is-standalone)
|
|
(--each grouped
|
|
;; (-let (((path . key-cells) it))
|
|
(-setq (path . key-cells) it)
|
|
;; TODO this won't add the file to `org-agenda-new-buffers'
|
|
(org-x-with-file path
|
|
;; TODO tbh this could just be the file basename since that's all
|
|
;; I ever use
|
|
(setq category (org-get-category))
|
|
;; (let ((category (org-get-category)))
|
|
(--each key-cells
|
|
(-setq (key . is-standalone) it)
|
|
(setq tags (->> (org-x-dag-get-inherited-tags org-file-tags dag key)
|
|
(append (plist-get key :tags))
|
|
(org-x-dag-collapse-tags)))
|
|
;; (-let* (((key . is-standalone) it)
|
|
;; (tags (->> (org-x-dag-get-inherited-tags org-file-tags dag key)
|
|
;; (append (plist-get key :tags))
|
|
;; (org-x-dag-collapse-tags))))
|
|
;; filter out incubators
|
|
(goto-char (plist-get key :point))
|
|
(unless (or (member org-x-tag-incubated tags)
|
|
(org-x-headline-is-scheduled nil)
|
|
(org-x-headline-is-deadlined nil))
|
|
(let* ((s (org-x-headline-get-task-status-0 (plist-get key :todo)))
|
|
(p (alist-get s org-x-headline-task-status-priorities)))
|
|
(unless (= p -1)
|
|
(setq acc (-> (org-x-dag-format-tag-node category tags key)
|
|
(org-add-props nil
|
|
'x-is-standalone is-standalone
|
|
'x-status s)
|
|
(cons acc)))))))))
|
|
acc))
|
|
|
|
;; (defun org-x-dag-scan-tags ()
|
|
;; (let* ((dag org-x-dag)
|
|
;; (nodes (org-x-dag-get-toplevel-project-nodes dag)))
|
|
;; (->> (--group-by (org-x-dag-key-get-file (car it)) nodes)
|
|
;; (--mapcat
|
|
;; (-let (((path . nodes) it))
|
|
;; (org-x-with-file path
|
|
;; (->> (-map #'car nodes)
|
|
;; (--mapcat
|
|
;; (progn
|
|
;; (goto-char (org-x-dag-key-get-point it))
|
|
;; (org-x-dag-format-tag-node dag (org-get-tags (point)) it))))))))))
|
|
|
|
(defun org-x-dag-get-inherited-tags (init dag key)
|
|
(let* ((this-file (org-x-dag-key-get-file key)))
|
|
(cl-labels
|
|
((ascend
|
|
(k tags)
|
|
(-if-let (parent (->> (dag-get-parents k dag)
|
|
(--first (equal (org-x-dag-key-get-file it)
|
|
this-file))))
|
|
(->> (plist-get parent :tags)
|
|
(append tags)
|
|
(ascend parent))
|
|
tags)))
|
|
(org-x-dag-collapse-tags (append (ascend key nil) init)))))
|
|
|
|
;;; AGENDA VIEWS
|
|
|
|
(defun org-x-dag-get-day-entries (_ date &rest args)
|
|
"Like `org-agenda-get-day-entries' but better."
|
|
;; for now just return a list of standalone tasks
|
|
(->> (org-x-dag-get-standalone-task-nodes org-x-dag)
|
|
(org-x-dag-nodes-to-headlines)))
|
|
|
|
(defun org-x-dag-agenda-list ()
|
|
(let ((org-agenda-files (org-x-get-action-files)))
|
|
(nd/with-advice
|
|
(('org-agenda-get-day-entries :override #'org-x-dag-get-day-entries))
|
|
(org-agenda-list))))
|
|
|
|
;; (defun org-x-dag-tags-view (_match)
|
|
;; (org-x-dag-sync t)
|
|
;; (let ((org-agenda-files (org-x-get-action-files)))
|
|
;; (nd/with-advice
|
|
;; (('org-scan-tags :override (lambda (&rest _) (org-x-dag-scan-tags))))
|
|
;; (org-tags-view '(4) "TODO"))))
|
|
|
|
(defun org-x-dag-show-tasks (_match)
|
|
(org-x-dag-sync t)
|
|
;; hack to make the loop only run once
|
|
(let ((org-agenda-files (list (car (org-x-get-action-files)))))
|
|
(nd/with-advice
|
|
(('org-scan-tags :override (lambda (&rest _) (org-x-dag-scan-tasks))))
|
|
(org-tags-view '(4) "TODO"))))
|
|
|
|
(defun org-x-dag-show-nodes (get-nodes)
|
|
(org-x-dag-sync)
|
|
(let* ((org-tags-match-list-sublevels org-tags-match-list-sublevels)
|
|
(completion-ignore-case t)
|
|
rtnall files file pos matcher
|
|
buffer)
|
|
(catch 'exit
|
|
(org-agenda-prepare (concat "DAG-TAG"))
|
|
(org-compile-prefix-format 'tags)
|
|
(org-set-sorting-strategy 'tags)
|
|
(let ((org-agenda-redo-command `(org-x-dag-show-nodes ',get-nodes))
|
|
(rtnall (funcall get-nodes)))
|
|
(org-agenda--insert-overriding-header
|
|
(with-temp-buffer
|
|
(insert "Headlines with TAGS match: \n")
|
|
(add-text-properties (point-min) (1- (point))
|
|
(list 'face 'org-agenda-structure))
|
|
(buffer-string)))
|
|
(org-agenda-mark-header-line (point-min))
|
|
(when rtnall
|
|
(insert (org-agenda-finalize-entries rtnall 'tags) "\n"))
|
|
(goto-char (point-min))
|
|
(or org-agenda-multi (org-agenda-fit-window-to-buffer))
|
|
(add-text-properties
|
|
(point-min) (point-max)
|
|
`(org-agenda-type tags
|
|
org-last-args (,get-nodes)
|
|
org-redo-cmd ,org-agenda-redo-command
|
|
org-series-cmd ,org-cmd))
|
|
(org-agenda-finalize)
|
|
(setq buffer-read-only t)))))
|
|
|
|
(provide 'org-x-dag)
|
|
;;; org-x-dag.el ends here
|