diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-29 08:42:10 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-29 08:42:10 -0400 |
| commit | 4d64437433ed221a08a5258243c01e3a05980db9 (patch) | |
| tree | 5fe8b4ebe02310d33890545bc020b2c163141d40 /modules | |
| parent | 2cec4136bcef7d984ecbafe95ad5280d7fa463f0 (diff) | |
| download | dotemacs-4d64437433ed221a08a5258243c01e3a05980db9.tar.gz dotemacs-4d64437433ed221a08a5258243c01e3a05980db9.zip | |
feat(completion): annotate the file-basename pickers with size and date
Eight completing-read pickers listed bare file basenames, so marginalia had no directory to resolve and couldn't annotate them. Add cj/completion-file-annotator to system-lib — an annotation-function factory that takes a candidate->path resolver and yields a size + modification-date suffix (or "dir" for directories, nil for missing files). Wire each picker through cj/completion-table-annotated with a per-site category and resolver: timer sounds, drill flashcards, Info files, the test-runner focus add/remove, vc clone dirs, hugo drafts, and agenda projects (the project's todo.org mtime). music-config's existing completion table gains the category and annotator inline, keeping its sort metadata.
The candidate strings and every return value are unchanged — this only adds completion metadata — so all downstream logic is untouched. The six modules that didn't already pull in system-lib now require it.
Tests: cj/completion-file-annotator gets Normal/Boundary/Error coverage (file, directory, nil path, missing file). Full suite green at 5394.
Claude-Session: https://claude.ai/code/session_014fyKMTTqLrZpL3rDF3dYc3
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/chrono-tools.el | 10 | ||||
| -rw-r--r-- | modules/help-config.el | 9 | ||||
| -rw-r--r-- | modules/hugo-config.el | 10 | ||||
| -rw-r--r-- | modules/music-config.el | 26 | ||||
| -rw-r--r-- | modules/org-agenda-config.el | 11 | ||||
| -rw-r--r-- | modules/org-drill-config.el | 11 | ||||
| -rw-r--r-- | modules/system-lib.el | 21 | ||||
| -rw-r--r-- | modules/test-runner.el | 17 | ||||
| -rw-r--r-- | modules/vc-config.el | 11 |
9 files changed, 104 insertions, 22 deletions
diff --git a/modules/chrono-tools.el b/modules/chrono-tools.el index 744781268..57309178d 100644 --- a/modules/chrono-tools.el +++ b/modules/chrono-tools.el @@ -9,7 +9,7 @@ ;; Eager reason: none; calendar/timer commands, a command-loaded deferral ;; candidate. ;; Top-level side effects: package configuration via use-package. -;; Runtime requires: user-constants. +;; Runtime requires: user-constants, system-lib. ;; Direct test load: yes. ;; ;; This module centralizes configuration for Emacs time-related tools: @@ -21,6 +21,7 @@ ;;; Code: (require 'user-constants) +(require 'system-lib) ;; provides cj/completion-table-annotated, cj/completion-file-annotator ;; Declared by the lazily-loaded `tmr' package; quiet the byte-compiler ;; without forcing the package to load. @@ -107,7 +108,12 @@ Present all audio files in the sounds directory and set the chosen file as (if current-file (format " (current: %s)" current-file) "")) - sound-files nil t nil nil current-file))) + (cj/completion-table-annotated + 'cj-sound-file + (cj/completion-file-annotator + (lambda (c) (expand-file-name c sounds-dir))) + sound-files) + nil t nil nil current-file))) (if (or (null selected-file) (string-empty-p selected-file)) (message "No file selected") (message "%s" (cj/tmr--apply-sound-file selected-file))))))))) diff --git a/modules/help-config.el b/modules/help-config.el index 4858abcf2..114b264ed 100644 --- a/modules/help-config.el +++ b/modules/help-config.el @@ -9,7 +9,7 @@ ;; Eager reason: help/info/man configuration and its keybindings; eager only by ;; init order, a deferral candidate. ;; Top-level side effects: two global keys, package configuration via use-package. -;; Runtime requires: none. +;; Runtime requires: system-lib. ;; Direct test load: yes. ;; ;; This module enhances Emacs' built-in help system and documentation features. @@ -25,6 +25,7 @@ ;;; Code: +(require 'system-lib) ;; completion table + file annotator (setq help-window-select t) ;; Always select the help buffer in a separate window @@ -90,7 +91,11 @@ Preserves any unsaved changes and checks if the file exists." info-files)) (chosen-name (completing-read "Select Info file: " - (mapcar #'car files-alist) + (cj/completion-table-annotated + 'cj-info-file + (cj/completion-file-annotator + (lambda (c) (cdr (assoc c files-alist)))) + (mapcar #'car files-alist)) nil t)) (chosen-file (cdr (assoc chosen-name files-alist)))) (when chosen-file diff --git a/modules/hugo-config.el b/modules/hugo-config.el index 7afa45a7b..b26398c69 100644 --- a/modules/hugo-config.el +++ b/modules/hugo-config.el @@ -9,7 +9,7 @@ ;; Eager reason: none; blog publishing is a command-loaded deferral candidate ;; for Phase 4. ;; Top-level side effects: package configuration via use-package. -;; Runtime requires: user-constants, host-environment. +;; Runtime requires: user-constants, host-environment, system-lib. ;; Direct test load: yes. ;; ;; Integrates ox-hugo for publishing Org files to a Hugo website. @@ -27,6 +27,7 @@ (require 'user-constants) (require 'host-environment) +(require 'system-lib) ;; completion table + file annotator ;; --------------------------------- Constants --------------------------------- @@ -166,7 +167,12 @@ Switches #+hugo_draft between true and false." (if (null drafts) (message "No drafts found in %s" cj/hugo-content-org-dir) (let ((choice (completing-read "Open draft: " - (mapcar #'car drafts) nil t))) + (cj/completion-table-annotated + 'cj-hugo-draft + (cj/completion-file-annotator + (lambda (c) (cdr (assoc c drafts)))) + (mapcar #'car drafts)) + nil t))) (find-file (cdr (assoc choice drafts))))))) ;; ---------------------------- Preview and Publish ---------------------------- diff --git a/modules/music-config.el b/modules/music-config.el index c627d799a..d16e2bb2f 100644 --- a/modules/music-config.el +++ b/modules/music-config.el @@ -219,14 +219,24 @@ Directories are suffixed with /; files are plain. Hidden dirs/files skipped." (sort acc #'string-lessp))) (defun cj/music--completion-table (candidates) - "Completion table for CANDIDATES preserving order and case-insensitive match." - (lambda (string pred action) - (if (eq action 'metadata) - '(metadata - (display-sort-function . identity) - (cycle-sort-function . identity) - (completion-ignore-case . t)) - (complete-with-action action candidates string pred)))) + "Completion table for CANDIDATES preserving order and case-insensitive match. +Tags the `cj-music-file' category and annotates each candidate (a path relative +to `cj/music-root', with a trailing slash for directories) with its size and +modification date so marginalia can show them." + (let ((annotate (cj/completion-file-annotator + (lambda (c) + (expand-file-name + (if (string-suffix-p "/" c) (substring c 0 -1) c) + cj/music-root))))) + (lambda (string pred action) + (if (eq action 'metadata) + `(metadata + (category . cj-music-file) + (annotation-function . ,annotate) + (display-sort-function . identity) + (cycle-sort-function . identity) + (completion-ignore-case . t)) + (complete-with-action action candidates string pred))))) (defun cj/music--ensure-playlist-buffer () "Ensure EMMS playlist buffer exists and is in playlist mode. Return buffer." diff --git a/modules/org-agenda-config.el b/modules/org-agenda-config.el index 51f269090..207c286e6 100644 --- a/modules/org-agenda-config.el +++ b/modules/org-agenda-config.el @@ -245,7 +245,16 @@ scoped to that project's todo.org plus calendars, schedule, and inbox." (file-exists-p (expand-file-name "todo.org" dir)))) all-dirs)) (project-names (mapcar #'file-name-nondirectory project-dirs)) - (chosen (completing-read "Show agenda for project: " project-names nil t)) + (chosen (completing-read + "Show agenda for project: " + (cj/completion-table-annotated + 'cj-agenda-project + (cj/completion-file-annotator + (lambda (c) + (expand-file-name "todo.org" + (expand-file-name c projects-dir)))) + project-names) + nil t)) (todo-file (expand-file-name "todo.org" (expand-file-name chosen projects-dir))) (org-agenda-files (cons todo-file (cj/--org-agenda-base-files)))) diff --git a/modules/org-drill-config.el b/modules/org-drill-config.el index 2c6e400e0..29f6130a2 100644 --- a/modules/org-drill-config.el +++ b/modules/org-drill-config.el @@ -8,7 +8,7 @@ ;; Eager reason: none; optional flashcard workflow, a command-loaded deferral ;; candidate for Phase 4. ;; Top-level side effects: defines a drill keymap, registers it under cj/custom-keymap. -;; Runtime requires: user-constants, keybindings. +;; Runtime requires: user-constants, keybindings, system-lib. ;; Direct test load: yes (requires keybindings explicitly). ;; ;; Notes: Org-Drill @@ -29,6 +29,7 @@ (require 'user-constants) ;; `drill-dir' (require 'keybindings) ;; provides `cj/custom-keymap' +(require 'system-lib) ;; completion table + file annotator (declare-function org-drill "org-drill" (&optional scope drill-match resume-p)) (declare-function org-drill-resume "org-drill" ()) (declare-function org-capture "org-capture" (&optional goto keys)) @@ -57,7 +58,13 @@ drill commands and the drill capture templates share." (defun cj/--drill-pick-file (dir) "Prompt for one of the drill Org files in DIR; return its absolute path." (expand-file-name - (completing-read "Choose flashcard file: " (cj/--drill-files-or-error dir) nil t) + (completing-read "Choose flashcard file: " + (cj/completion-table-annotated + 'cj-drill-file + (cj/completion-file-annotator + (lambda (c) (expand-file-name c dir))) + (cj/--drill-files-or-error dir)) + nil t) dir)) (defun cj/--drill-pick-dir (other-dir) diff --git a/modules/system-lib.el b/modules/system-lib.el index 8b954c6a9..f1049c021 100644 --- a/modules/system-lib.el +++ b/modules/system-lib.el @@ -187,6 +187,27 @@ for: marginalia falls back to the table's own annotation function." (annotation-function . ,annotate)) (complete-with-action action collection string predicate)))) +(defun cj/completion-file-annotator (candidate->path) + "Return an annotation function for completion candidates backed by files. +CANDIDATE->PATH maps a candidate string to its absolute file path, or nil when +the candidate has no backing file. The returned function, suitable as a +completion table's annotation function (see `cj/completion-table-annotated'), +yields a suffix with the file size and modification date for a regular file, +the marker \"dir\" plus the date for a directory, or nil when the path is nil +or the file is missing -- so marginalia then shows no suffix for that +candidate." + (lambda (cand) + (let ((path (funcall candidate->path cand))) + (when (and path (file-exists-p path)) + (let* ((attrs (file-attributes path)) + (dirp (eq t (file-attribute-type attrs))) + (size (if dirp "dir" + (file-size-human-readable (file-attribute-size attrs)))) + (date (format-time-string + "%Y-%m-%d" + (file-attribute-modification-time attrs)))) + (format " %8s %s" size date)))))) + (defun cj/format-region-with-program (program &rest args) "Replace the current buffer with PROGRAM ARGS run over its contents, via argv. Runs PROGRAM (with ARGS) on the whole buffer through `call-process-region' diff --git a/modules/test-runner.el b/modules/test-runner.el index 48a2b09fe..e05145e4e 100644 --- a/modules/test-runner.el +++ b/modules/test-runner.el @@ -8,7 +8,7 @@ ;; Load shape: eager. ;; Eager reason: registers the C-; t test runner entry point and state. ;; Top-level side effects: defines and registers cj/test-map. -;; Runtime requires: ert, cl-lib, keybindings. +;; Runtime requires: ert, cl-lib, keybindings, system-lib. ;; Direct test load: yes. ;; ;; Project-aware ERT runner with two modes: all tests or a focused file set. @@ -23,6 +23,7 @@ (require 'ert) (require 'cl-lib) (require 'keybindings) ;; provides cj/custom-keymap +(require 'system-lib) ;; completion table + file annotator ;;; External Variables and Functions @@ -209,7 +210,11 @@ Returns: \\='success if added successfully, :test #'string=)) (selected (if unfocused-files (completing-read "Add test file to focus: " - unfocused-files + (cj/completion-table-annotated + 'cj-test-file + (cj/completion-file-annotator + (lambda (c) (expand-file-name c dir))) + unfocused-files) nil t) (user-error "All test files are already focused")))) (pcase (cj/test--do-focus-add selected available-files focused-files) @@ -278,7 +283,13 @@ Returns: \\='success if removed successfully, (if (null focused-files) (user-error "No focused files to remove") (let ((selected (completing-read "Remove from focus: " - focused-files + (cj/completion-table-annotated + 'cj-test-file + (cj/completion-file-annotator + (lambda (c) + (expand-file-name + c (cj/test--get-test-directory)))) + focused-files) nil t))) (pcase (cj/test--do-focus-remove selected focused-files) ('success diff --git a/modules/vc-config.el b/modules/vc-config.el index fcca7e07b..60fcaeb89 100644 --- a/modules/vc-config.el +++ b/modules/vc-config.el @@ -8,7 +8,7 @@ ;; Eager reason: the C-x g Magit entry point and the git keymap. ;; Top-level side effects: defines two keymaps, registers under cj/custom-keymap, ;; package configuration via use-package. -;; Runtime requires: user-constants, keybindings. +;; Runtime requires: user-constants, keybindings, system-lib. ;; Direct test load: yes (requires keybindings explicitly). ;; ;; C-x g is my general entry to Magit's version control via the status page. @@ -26,6 +26,7 @@ (require 'user-constants) ;; provides code-dir (require 'keybindings) ;; provides cj/custom-keymap +(require 'system-lib) ;; completion table + file annotator ;; Forward declaration: cj/vc-map is defined later in this file (see ;; `defvar-keymap' below) but referenced earlier in a use-package :bind form. @@ -199,7 +200,13 @@ repository's README if found, else `dired's the clone." (read-directory-name "Clone to: " code-dir)) ;; C-u: Choose from configured list (current-prefix-arg - (completing-read "Clone to: " cj/git-clone-dirs nil t)) + (completing-read "Clone to: " + (cj/completion-table-annotated + 'cj-clone-dir + (cj/completion-file-annotator + (lambda (c) (expand-file-name c))) + cj/git-clone-dirs) + nil t)) ;; No prefix: Use default (first in list) (t (car cj/git-clone-dirs))))) |
