Files
dotfiles/org-agenda-api/configs/kat/custom-config.el
Ivan Malison 504ec1a105 Add org-agenda-api container builds and fly.io deployment
Consolidates container builds from colonelpanic-org-agenda-api repo:
- Add org-agenda-api input to nixos flake
- Add container-colonelpanic and container-kat package outputs
- Add org-agenda-api cachix as substituter
- Add org-agenda-api devShell for deployment work

New org-agenda-api directory contains:
- container.nix: Container build logic using mkContainer
- configs/: Instance configs (custom-config.el, fly.toml, secrets)
- deploy.sh: Fly.io deployment script
- secrets.nix: agenix secret declarations

Build with: nix build .#container-colonelpanic
Deploy with: cd org-agenda-api && ./deploy.sh colonelpanic

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 14:24:41 -08:00

172 lines
7.8 KiB
EmacsLisp

;;; custom-config.el --- Container config loader -*- lexical-binding: t; -*-
;; Helper function used by org-config (must be defined before loading preface)
(defun imalison:join-paths (&rest paths)
"Join PATHS together into a single path."
(let ((result (car paths)))
(dolist (p (cdr paths))
(setq result (expand-file-name p result)))
result))
;; Load tangled config files in order
(let ((config-dir (file-name-directory load-file-name)))
;; Load preface first (defines variables with default values)
(when (file-exists-p (expand-file-name "org-config-preface.el" config-dir))
(load (expand-file-name "org-config-preface.el" config-dir)))
;; Override paths for container environment AFTER loading preface
;; Use setq to ensure we override the defvar values from preface
(setq imalison:org-dir "/data/org")
(setq imalison:shared-org-dir "/data/shared")
;; Re-derive all path variables using the container org-dir
(setq imalison:org-gtd-file (imalison:join-paths imalison:org-dir "gtd.org"))
(setq imalison:org-habits-file (imalison:join-paths imalison:org-dir "habits.org"))
(setq imalison:org-calendar-file (imalison:join-paths imalison:org-dir "calendar.org"))
(setq imalison:org-inbox-file (imalison:join-paths imalison:org-dir "inbox.org"))
;; Shared paths derived from shared-org-dir
(setq imalison:shared-org-gtd-file (imalison:join-paths imalison:shared-org-dir "gtd.org"))
(setq imalison:shared-habits-file (imalison:join-paths imalison:shared-org-dir "habits.org"))
(setq imalison:shared-calendar-file (imalison:join-paths imalison:shared-org-dir "calendar.org"))
(setq imalison:shared-shopping-file (imalison:join-paths imalison:shared-org-dir "shopping.org"))
(setq imalison:shared-repeating-file (imalison:join-paths imalison:shared-org-dir "repeating.org"))
(setq imalison:orgzly-files (list (imalison:join-paths imalison:org-dir "orgzly.org")))
(setq imalison:repeating-org-files (list imalison:org-habits-file
imalison:shared-habits-file
imalison:shared-repeating-file))
;; org-config-custom.el uses customize format (var value), convert to setq
(when (file-exists-p (expand-file-name "org-config-custom.el" config-dir))
(with-temp-buffer
(insert-file-contents (expand-file-name "org-config-custom.el" config-dir))
(goto-char (point-min))
(condition-case nil
(while t
(let ((form (read (current-buffer))))
(when (and (listp form) (symbolp (car form)))
(set (car form) (eval (cadr form))))))
(end-of-file nil))))
;; Load main config (sets up org-agenda-files using the variables we just set)
(when (file-exists-p (expand-file-name "org-config-config.el" config-dir))
(load (expand-file-name "org-config-config.el" config-dir)))
;; Load optional overrides (instance-specific customizations)
(when (file-exists-p (expand-file-name "overrides.el" config-dir))
(load (expand-file-name "overrides.el" config-dir))))
;; Define no-op stubs for unavailable packages (overwrite autoloads)
(defun org-bullets-mode (&optional _arg)
"No-op stub for org-bullets-mode (package not available in container)."
nil)
;; Override shared-org-file-p to handle nil imalison:shared-org-dir
;; The original calls (file-truename imalison:shared-org-dir) which errors when nil
(defun imalison:shared-org-file-p ()
"Check if current file is in the shared org directory.
Returns nil if imalison:shared-org-dir is not set."
(and imalison:shared-org-dir
(string-prefix-p (file-truename imalison:shared-org-dir)
(file-truename default-directory))))
;; Helper functions used by org-agenda-custom-commands
;; These are defined in README.org but needed for custom views
(defun imalison:compare-int-list (a b)
"Compare two lists of integers lexicographically."
(when (and a b)
(cond ((> (car a) (car b)) 1)
((< (car a) (car b)) -1)
(t (imalison:compare-int-list (cdr a) (cdr b))))))
(defun get-date-created-from-agenda-entry (agenda-entry)
"Get the CREATED property timestamp from an agenda entry."
(org-time-string-to-time
(org-entry-get (get-text-property 1 'org-marker agenda-entry) "CREATED")))
;; Auto-convert org-capture-templates to org-agenda-api-capture-templates
(defun imalison:extract-template-string (template-spec)
"Extract the template string from TEMPLATE-SPEC.
Handles string templates, function templates, and file templates."
(let ((template-part (nth 4 template-spec)))
(cond
((stringp template-part) template-part)
((and (listp template-part)
(eq (car template-part) 'function))
;; Try to evaluate the function to get template string
(condition-case nil
(let ((result (funcall (eval (cadr template-part)))))
(if (stringp result) result ""))
(error "")))
((and (listp template-part)
(eq (car template-part) 'file))
;; File template - read file contents
(condition-case nil
(with-temp-buffer
(insert-file-contents (eval (cadr template-part)))
(buffer-string))
(error "")))
(t ""))))
(defun imalison:infer-prompt-type (prompt-match)
"Infer the prompt type from PROMPT-MATCH.
PROMPT-MATCH is the full match string like \"%^{Name}t\" or \"%^{Title}\"."
(let ((suffix (substring prompt-match (1- (length prompt-match)))))
(cond
((string-match-p "[tTuU]$" prompt-match) 'date)
((string= "g" suffix) 'tag)
((string= "G" suffix) 'tag)
((string= "C" suffix) 'string) ; completion
(t 'string))))
(defun imalison:extract-prompts-from-template (template-string)
"Extract prompt definitions from TEMPLATE-STRING.
Returns a list of (NAME :type TYPE :required t) for each %^{...} found."
(let ((prompts '())
(seen-names '())
(pos 0))
(while (string-match "%\\^{\\([^}|]+\\)\\(?:|[^}]*\\)?}\\([tTuUgGC]\\)?" template-string pos)
(let* ((name (match-string 1 template-string))
(full-match (match-string 0 template-string))
(type (imalison:infer-prompt-type full-match)))
(unless (member name seen-names)
(push name seen-names)
(push (list name :type type :required t) prompts))
(setq pos (match-end 0))))
(nreverse prompts)))
(defun imalison:convert-capture-template (template-spec)
"Convert a single org-capture TEMPLATE-SPEC to API format.
Returns nil for non-entry templates or templates that can't be converted."
(when (and (listp template-spec)
(>= (length template-spec) 4))
(let* ((key (nth 0 template-spec))
(description (nth 1 template-spec))
(type (nth 2 template-spec)))
;; Only convert entry-type templates, skip menu items (no type)
(when (and (stringp key)
(stringp description)
(eq type 'entry))
(let* ((template-string (imalison:extract-template-string template-spec))
(prompts (imalison:extract-prompts-from-template template-string))
;; Create a unique API key from the hotkey
(api-key (concat "capture-" key)))
(list api-key
:name description
:template template-spec
:prompts prompts))))))
(defun imalison:convert-all-capture-templates ()
"Convert all org-capture-templates to org-agenda-api-capture-templates format."
(let ((converted '()))
(dolist (template org-capture-templates)
(let ((api-template (imalison:convert-capture-template template)))
(when api-template
(push api-template converted))))
(nreverse converted)))
;; Auto-generate API capture templates from org-capture-templates
(setq org-agenda-api-capture-templates (imalison:convert-all-capture-templates))
;;; custom-config.el ends here