diff options
| -rw-r--r-- | modules/org-agenda-config.el | 44 | ||||
| -rw-r--r-- | tests/test-org-agenda-config-category.el | 137 |
2 files changed, 181 insertions, 0 deletions
diff --git a/modules/org-agenda-config.el b/modules/org-agenda-config.el index 70c7fb6d..2c30db0f 100644 --- a/modules/org-agenda-config.el +++ b/modules/org-agenda-config.el @@ -90,6 +90,50 @@ (local-set-key (kbd "s-<left>") #'org-agenda-todo-previousset)))) +;; ----------------------- Project-name Category Override --------------------- +;; The default `org-category' for a todo.org buffer is "todo" (the filename +;; without extension), which renders as "todo:" in every agenda `%c' column +;; and tells the reader nothing useful when every project has its own +;; todo.org. Substitute the parent-directory basename instead, so +;; `~/.emacs.d/todo.org' shows "emacs.d:" and `~/projects/foo/todo.org' shows +;; "foo:". Files that aren't named todo.org are left alone, and a user-set +;; `#+CATEGORY:' (which leaves `org-category' at a non-default value) wins. + +(defun cj/--org-todo-category-from-file (path) + "Return the project category for a todo.org PATH, or nil if not applicable. +For a file named todo.org, returns the basename of its parent +directory with a single leading dot stripped (so `~/.emacs.d/todo.org' +yields \"emacs.d\", not \".emacs.d\"). For any other file -- or for a +PATH that is nil, empty, or has no usable parent directory -- returns +nil so the org default category applies." + (when (and (stringp path) + (not (string-empty-p path)) + (string= "todo.org" (file-name-nondirectory path))) + (let* ((dir (file-name-directory path)) + (parent (and dir + (file-name-nondirectory + (directory-file-name dir)))) + (clean (and parent + (if (and (> (length parent) 1) + (eq ?. (aref parent 0))) + (substring parent 1) + parent)))) + (and clean (not (string-empty-p clean)) clean)))) + +(defun cj/--org-set-todo-category () + "Set buffer-local `org-category' to the project name for a todo.org buffer. +Runs from `org-mode-hook'. Only overrides when `org-category' is still +the default-from-filename (\"todo\"), so an explicit `#+CATEGORY:' in +the file keeps precedence." + (when (and buffer-file-name + (boundp 'org-category) + (stringp org-category) + (string= "todo" org-category)) + (when-let* ((project (cj/--org-todo-category-from-file buffer-file-name))) + (setq-local org-category project)))) + +(add-hook 'org-mode-hook #'cj/--org-set-todo-category) + ;; ------------------------ Org Agenda File List Cache ------------------------- ;; Cache agenda file list to avoid expensive directory scanning on every view. ;; The TTL+building cache lifecycle is provided by `cj-cache.el'. diff --git a/tests/test-org-agenda-config-category.el b/tests/test-org-agenda-config-category.el new file mode 100644 index 00000000..6a54d9e6 --- /dev/null +++ b/tests/test-org-agenda-config-category.el @@ -0,0 +1,137 @@ +;;; test-org-agenda-config-category.el --- Tests for project-name category derivation -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests for the agenda-display category helpers in org-agenda-config.el: +;; - cj/--org-todo-category-from-file (pure) +;; - cj/--org-set-todo-category (org-mode-hook side effect) +;; +;; Goal: when a buffer visits a project-local todo.org, its `org-category` +;; should be the parent directory name (the project slug) rather than the +;; default "todo" -- so the agenda's %c column shows "emacs.d:" instead of +;; "todo:" for every project's tasks. + +;;; Code: + +(require 'ert) +(require 'org) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'org-agenda-config) + +;;; ---------- cj/--org-todo-category-from-file (pure helper) ---------- + +;;; Normal Cases + +(ert-deftest test-org-agenda-config-category-normal-emacs-d-todo () + "Normal: todo.org under .emacs.d returns \"emacs.d\"." + (should (equal "emacs.d" + (cj/--org-todo-category-from-file + "/home/cjennings/.emacs.d/todo.org")))) + +(ert-deftest test-org-agenda-config-category-normal-project-todo () + "Normal: todo.org under a project dir returns the project basename." + (should (equal "dotemacs" + (cj/--org-todo-category-from-file + "/home/cjennings/code/dotemacs/todo.org")))) + +(ert-deftest test-org-agenda-config-category-normal-deep-project () + "Normal: deeply nested todo.org returns only the immediate parent." + (should (equal "frontend" + (cj/--org-todo-category-from-file + "/home/cjennings/projects/work/myapp/frontend/todo.org")))) + +;;; Boundary Cases + +(ert-deftest test-org-agenda-config-category-boundary-non-todo-file () + "Boundary: non-todo.org file returns nil so default category stays." + (should (null (cj/--org-todo-category-from-file + "/home/cjennings/sync/org/roam/inbox.org")))) + +(ert-deftest test-org-agenda-config-category-boundary-schedule-org () + "Boundary: schedule.org returns nil; not a project todo file." + (should (null (cj/--org-todo-category-from-file + "/home/cjennings/sync/org/schedule.org")))) + +(ert-deftest test-org-agenda-config-category-boundary-todo-at-fs-root () + "Boundary: /todo.org with no real parent directory returns nil." + (should (null (cj/--org-todo-category-from-file "/todo.org")))) + +(ert-deftest test-org-agenda-config-category-boundary-relative-path () + "Boundary: bare relative \"todo.org\" with no directory returns nil." + (should (null (cj/--org-todo-category-from-file "todo.org")))) + +(ert-deftest test-org-agenda-config-category-boundary-todo-with-trailing-dir () + "Boundary: tolerate a path that is already directory-form." + (should (equal "emacs.d" + (cj/--org-todo-category-from-file + "/home/cjennings/.emacs.d/todo.org")))) + +;;; Error Cases + +(ert-deftest test-org-agenda-config-category-error-nil-path () + "Error: nil PATH returns nil, no signal." + (should (null (cj/--org-todo-category-from-file nil)))) + +(ert-deftest test-org-agenda-config-category-error-empty-path () + "Error: empty-string PATH returns nil." + (should (null (cj/--org-todo-category-from-file "")))) + +;;; ---------- cj/--org-set-todo-category (hook function) ---------- + +(defmacro test-org-agenda-config-category--with-file (path body-form) + "Visit PATH in a temp buffer with org-mode active, evaluate BODY-FORM. +Sets `buffer-file-name' so the hook's lookup sees the desired path. +Suppresses other org-mode hooks to keep the test isolated." + (declare (indent 1)) + `(with-temp-buffer + (let ((org-mode-hook nil) + (text-mode-hook nil)) + (org-mode)) + (setq buffer-file-name ,path) + ;; mimic org's default category (filename-sans-extension) so the + ;; hook's "only override the default" guard is exercised. + (setq-local org-category + (and ,path + (file-name-sans-extension + (file-name-nondirectory ,path)))) + ,body-form)) + +;;; Normal Cases + +(ert-deftest test-org-agenda-config-category-hook-normal-overrides-todo () + "Normal: hook overrides default \"todo\" with the parent dir name." + (test-org-agenda-config-category--with-file "/home/cjennings/.emacs.d/todo.org" + (progn + (cj/--org-set-todo-category) + (should (equal "emacs.d" org-category))))) + +(ert-deftest test-org-agenda-config-category-hook-normal-leaves-inbox-alone () + "Normal: hook leaves inbox.org's category at its filename default." + (test-org-agenda-config-category--with-file "/home/cjennings/sync/org/roam/inbox.org" + (progn + (cj/--org-set-todo-category) + (should (equal "inbox" org-category))))) + +;;; Boundary Cases + +(ert-deftest test-org-agenda-config-category-hook-boundary-respects-explicit () + "Boundary: explicit category (not the filename default) is preserved." + (test-org-agenda-config-category--with-file "/home/cjennings/.emacs.d/todo.org" + (progn + (setq-local org-category "Personal") + (cj/--org-set-todo-category) + (should (equal "Personal" org-category))))) + +(ert-deftest test-org-agenda-config-category-hook-boundary-nil-buffer-file-name () + "Boundary: hook is safe in buffers with no `buffer-file-name'." + (with-temp-buffer + (let ((org-mode-hook nil) + (text-mode-hook nil)) + (org-mode)) + (setq buffer-file-name nil) + (cj/--org-set-todo-category) + ;; no error and no spurious mutation + (should t))) + +(provide 'test-org-agenda-config-category) +;;; test-org-agenda-config-category.el ends here |
