;;; 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))))) ;; Main entrypoint (when noninteractive (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