;;; daily-prep-agenda.el --- Standalone batch agenda extractor for daily-prep ;; ;; Usage: ;; emacs --batch -q -l daily-prep-agenda.el todo.org [pcal.org ...] ;; ;; Filters entries to TODO/DOING/WAITING/NEXT with [#A]/[#B] priority OR ;; DEADLINE/SCHEDULED present. Bucketizes into Overdue, Today, This Week, ;; Priority A (no date), Priority B (no date). Emits heading + body for each. (require 'org) (require 'cl-lib) ;; Declare the TODO keywords used across Craig's org files so org-mode parses ;; "DOING", "WAITING", "NEXT", "CANCELLED" headings as TODO states. With `-q`, ;; org-mode defaults to just "TODO"/"DONE" and will treat the others as plain ;; heading text (state comes back as nil). (setq org-todo-keywords '((sequence "TODO" "DOING" "WAITING" "NEXT" "|" "DONE" "CANCELLED"))) (defvar dp-today (format-time-string "%Y-%m-%d")) (defvar dp-week-end (format-time-string "%Y-%m-%d" (time-add (current-time) (days-to-time 7)))) (defun dp-iso-date (org-ts) "Extract YYYY-MM-DD from an org timestamp string like '<2026-04-25 Sat 16:00>'." (when (and org-ts (string-match "\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\)" org-ts)) (match-string 1 org-ts))) (defun dp-entry-info () "Return plist of metadata + body for org entry at point." (let* ((state (org-get-todo-state)) (el (org-element-at-point)) (priority (org-element-property :priority el)) (deadline-raw (org-entry-get (point) "DEADLINE")) (scheduled-raw (org-entry-get (point) "SCHEDULED")) (deadline (dp-iso-date deadline-raw)) (scheduled (dp-iso-date scheduled-raw)) (heading (org-get-heading t t t t)) (line (line-number-at-pos)) (file (or (buffer-file-name) (buffer-name))) (start (save-excursion (org-end-of-meta-data t) (point))) (end (save-excursion (or (outline-next-heading) (goto-char (point-max))) (point))) (body (and (< start end) (string-trim (buffer-substring-no-properties start end))))) (list :state state :priority priority :deadline deadline :deadline-raw deadline-raw :scheduled scheduled :scheduled-raw scheduled-raw :heading heading :line line :file file :body body))) (defun dp-active-candidate-p () "True if entry at point is an active state with [#A]/[#B] OR has DEADLINE/SCHEDULED." (let* ((state (org-get-todo-state)) (el (org-element-at-point)) (pri (org-element-property :priority el)) (dl (org-entry-get (point) "DEADLINE")) (sc (org-entry-get (point) "SCHEDULED"))) (and (member state '("TODO" "DOING" "WAITING" "NEXT")) (or (memq pri '(?A ?B)) dl sc)))) (defun dp-collect (files) "Walk FILES, return list of dp-entry-info plists for matching entries." (let (entries) (dolist (file files) (when (file-readable-p file) (with-current-buffer (find-file-noselect file) (org-mode) (org-map-entries (lambda () (when (dp-active-candidate-p) (push (dp-entry-info) entries))) nil 'file)))) (nreverse entries))) (defun dp-bucket (e) "Return bucket name for entry plist E." (let ((dl (plist-get e :deadline)) (sc (plist-get e :scheduled)) (pri (plist-get e :priority))) (cond ((and dl (string< dl dp-today)) 'overdue) ((or (equal dl dp-today) (equal sc dp-today)) 'today) ((and sc (string< sc dp-today)) 'overdue) ((or (and dl (string< dl dp-week-end)) (and sc (string< sc dp-week-end))) 'this-week) ((eq pri ?A) 'pri-a) ((eq pri ?B) 'pri-b) (t 'other)))) (defun dp-format-entry (e) "Format entry plist E as org-mode text." (concat (format "** %s%s %s\n" (or (plist-get e :state) "") (if-let ((p (plist-get e :priority))) (format " [#%c]" p) "") (plist-get e :heading)) (format " :LOC: %s:%d\n" (file-name-nondirectory (plist-get e :file)) (plist-get e :line)) (when-let ((d (plist-get e :deadline-raw))) (format " DEADLINE: %s\n" d)) (when-let ((s (plist-get e :scheduled-raw))) (format " SCHEDULED: %s\n" s)) (let ((b (plist-get e :body))) (if (and b (not (string-empty-p b))) (concat (replace-regexp-in-string "^" " " b) "\n") "")) "\n")) (defun dp-emit-bucket (label entries) (when entries (princ (format "* %s (%d)\n\n" label (length entries))) (dolist (e entries) (princ (dp-format-entry e))))) (defun dp--cli-invocation-p () "Non-nil when the trailing args look like a real invocation (readable files). Keeps the batch entrypoint from firing when this file is loaded under ERT, where the trailing args are ERT's own flags rather than org files." (and command-line-args-left (cl-every #'file-readable-p command-line-args-left))) ;; Main entrypoint (when (and noninteractive (dp--cli-invocation-p)) (let* ((files command-line-args-left) (entries (dp-collect files)) (groups (seq-group-by #'dp-bucket entries))) (princ (format "# Daily-Prep Extract — %s\n# Files: %s\n# Total candidates: %d\n\n" dp-today (mapconcat #'file-name-nondirectory files ", ") (length entries))) (dolist (bucket '(overdue today this-week pri-a pri-b other)) (dp-emit-bucket (pcase bucket ('overdue "Overdue") ('today "Today") ('this-week "This Week") ('pri-a "Priority A (undated)") ('pri-b "Priority B (undated)") ('other "Other")) (alist-get bucket groups))))) (provide 'daily-prep-agenda) ;;; daily-prep-agenda.el ends here