diff options
45 files changed, 995 insertions, 447 deletions
diff --git a/modules/auth-config.el b/modules/auth-config.el index f18c0c1fd..62d773057 100644 --- a/modules/auth-config.el +++ b/modules/auth-config.el @@ -35,6 +35,15 @@ (require 'system-lib) (require 'user-constants) ;; defines authinfo-file, read at load time below +;; Lazily-loaded oauth2-auto / plstore internals used by the cache-fix advice +;; below. oauth2-auto is required at runtime inside the advised function; these +;; declarations satisfy the byte-compiler without forcing an eager load. +(declare-function oauth2-auto--compute-id "oauth2-auto") +(declare-function plstore-get "plstore") +(declare-function plstore-close "plstore") +(defvar oauth2-auto--plstore-cache) +(defvar oauth2-auto-plstore) + (defcustom cj/auth-source-debug-enabled nil "Non-nil means enable verbose auth-source debug logging. diff --git a/modules/browser-config.el b/modules/browser-config.el index 0312cdd18..d596b9e9d 100644 --- a/modules/browser-config.el +++ b/modules/browser-config.el @@ -145,7 +145,8 @@ Persists the choice for future sessions." (defun cj/--do-initialize-browser () "Initialize browser configuration. Returns: (cons \\='loaded browser-plist) if saved choice was loaded, - (cons \\='first-available browser-plist) if using first discovered browser, + (cons \\='first-available browser-plist) if using first + discovered browser, (cons \\='no-browsers nil) if no browsers found." (let ((saved-choice (cj/load-browser-choice))) (if saved-choice diff --git a/modules/calendar-sync.el b/modules/calendar-sync.el index 2ff535668..8d7552d3e 100644 --- a/modules/calendar-sync.el +++ b/modules/calendar-sync.el @@ -223,7 +223,7 @@ Example: -21600 for CST (UTC-6), -28800 for PST (UTC-8)." (defun calendar-sync--format-timezone-offset (offset) "Format timezone OFFSET (in seconds) as human-readable string. -Example: -21600 → 'UTC-6' or 'UTC-6:00'." +Example: -21600 → `UTC-6' or `UTC-6:00'." (if (null offset) "unknown" (let* ((hours (/ offset 3600)) @@ -289,7 +289,7 @@ Example: -21600 → 'UTC-6' or 'UTC-6:00'." "Normalize line endings in CONTENT to Unix format (LF only). Removes all carriage return characters (\\r) from CONTENT. The iCalendar format (RFC 5545) uses CRLF line endings, but Emacs -and 'org-mode' expect LF only. This function ensures consistent line +and `org-mode' expect LF only. This function ensures consistent line endings throughout the parsing pipeline. Returns CONTENT with all \\r characters removed." @@ -423,14 +423,16 @@ Handles both simple values and values with parameters like TZID." (defun calendar-sync--get-recurrence-id-line (event-str) "Extract full RECURRENCE-ID line from EVENT-STR, including parameters. -Returns the complete line like 'RECURRENCE-ID;TZID=Europe/Tallinn:20260203T170000'. +Returns the complete line like +`RECURRENCE-ID;TZID=Europe/Tallinn:20260203T170000'. Returns nil if not found." (when (and event-str (stringp event-str)) (calendar-sync--get-property-line event-str "RECURRENCE-ID"))) (defun calendar-sync--parse-ics-datetime (value) "Parse iCal datetime VALUE into (year month day hour minute) list. -Returns nil for invalid input. For date-only values, returns (year month day nil nil). +Returns nil for invalid input. For date-only values, returns +(year month day nil nil). Handles formats: 20260203T090000Z, 20260203T090000, 20260203." (when (and value (stringp value) @@ -493,7 +495,8 @@ start time fail to parse. The plist holds :recurrence-id (localized), (defun calendar-sync--collect-recurrence-exceptions (ics-content) "Collect all RECURRENCE-ID events from ICS-CONTENT. Returns hash table mapping UID to list of exception event plists. -Each exception plist contains :recurrence-id (parsed), :start, :end, :summary, etc." +Each exception plist contains :recurrence-id (parsed), :start, :end, +:summary, etc." (let ((exceptions (make-hash-table :test 'equal))) (when (and ics-content (stringp ics-content)) (dolist (event-str (calendar-sync--split-events ics-content)) @@ -571,7 +574,8 @@ Returns new list with matching occurrences replaced by exception times." (defun calendar-sync--get-exdates (event-str) "Extract all EXDATE values from EVENT-STR. -Returns list of datetime strings (without TZID parameters), or nil if none found. +Returns list of datetime strings (without TZID parameters), or nil if +none found. Handles both simple values and values with parameters like TZID." (when (and event-str (stringp event-str) (not (string-empty-p event-str))) (let ((exdates '()) @@ -584,7 +588,8 @@ Handles both simple values and values with parameters like TZID." (defun calendar-sync--get-exdate-line (event-str exdate-value) "Find the full EXDATE line containing EXDATE-VALUE from EVENT-STR. -Returns the complete line like 'EXDATE;TZID=America/New_York:20260210T130000'. +Returns the complete line like +`EXDATE;TZID=America/New_York:20260210T130000'. Returns nil if not found." (when (and event-str (stringp event-str) exdate-value) (let ((pattern (format "^\\(EXDATE[^:]*:%s\\)" (regexp-quote exdate-value)))) @@ -618,7 +623,8 @@ Converts TZID-qualified and UTC times to local time." (defun calendar-sync--exdate-matches-p (occurrence-start exdate) "Check if OCCURRENCE-START matches EXDATE. OCCURRENCE-START is (year month day hour minute). -EXDATE is (year month day hour minute) or (year month day nil nil) for date-only. +EXDATE is (year month day hour minute) or (year month day nil nil) for +date-only. Date-only EXDATE matches any time on that day." (and occurrence-start exdate (= (nth 0 occurrence-start) (nth 0 exdate)) ; year @@ -682,7 +688,8 @@ Returns nil if property not found." (defun calendar-sync--get-property-line (event property) "Extract full PROPERTY line from EVENT string, including parameters. -Returns the complete line like 'DTSTART;TZID=Europe/Lisbon:20260202T190000'. +Returns the complete line like +`DTSTART;TZID=Europe/Lisbon:20260202T190000'. Returns nil if property not found." (when (string-match (format "^\\(%s[^\n]*\\)$" (regexp-quote property)) event) (match-string 1 event))) @@ -790,8 +797,8 @@ Returns URL string or nil." (defun calendar-sync--extract-tzid (property-line) "Extract TZID parameter value from PROPERTY-LINE. -PROPERTY-LINE is like 'DTSTART;TZID=Europe/Lisbon:20260202T190000'. -Returns timezone string like 'Europe/Lisbon', or nil if no TZID. +PROPERTY-LINE is like `DTSTART;TZID=Europe/Lisbon:20260202T190000'. +Returns timezone string like `Europe/Lisbon', or nil if no TZID. Returns nil for malformed lines (missing colon separator)." (when (and property-line (stringp property-line) @@ -813,7 +820,7 @@ Returns list (year month day hour minute) in local timezone." (defun calendar-sync--convert-tz-to-local (year month day hour minute source-tz) "Convert datetime from SOURCE-TZ timezone to local time. -SOURCE-TZ is a timezone name like 'Europe/Lisbon' or 'Asia/Yerevan'. +SOURCE-TZ is a timezone name like `Europe/Lisbon' or `Asia/Yerevan'. Returns list (year month day hour minute) in local timezone, or nil on error. Uses Emacs built-in timezone support (encode-time/decode-time with ZONE @@ -837,8 +844,10 @@ TZ database as the `date' command." "Convert PARSED datetime to local time using timezone info. PARSED is (year month day hour minute) or (year month day nil nil). IS-UTC non-nil means the value had a Z suffix. + TZID is a timezone string like \"Europe/Lisbon\", or nil. -Returns PARSED converted to local time, or PARSED unchanged if no conversion needed." +Returns PARSED converted to local time, or PARSED unchanged if no +conversion needed." (cond (is-utc (calendar-sync--convert-utc-to-local @@ -856,7 +865,8 @@ Returns PARSED converted to local time, or PARSED unchanged if no conversion nee "Parse iCal timestamp string TIMESTAMP-STR. Returns (year month day hour minute) or (year month day) for all-day events. Converts UTC times (ending in Z) to local time. -If TZID is provided (e.g., 'Europe/Lisbon'), converts from that timezone to local. +If TZID is provided (e.g., `Europe/Lisbon'), converts from that timezone +to local. Returns nil if parsing fails." (cond ;; DateTime format: 20251116T140000Z or 20251116T140000 @@ -913,7 +923,8 @@ Returns string like '<2025-11-16 Sun 14:00-15:00>' or '<2025-11-16 Sun>'." (defun calendar-sync--date-to-time (date) "Convert DATE to time value for comparison. DATE should be a list starting with (year month day ...). -Only the first three elements are used; extra elements (hour, minute) are ignored." +Only the first three elements are used; extra elements (hour, minute) are +ignored." (let ((day (nth 2 date)) (month (nth 1 date)) (year (nth 0 date))) @@ -1082,7 +1093,8 @@ Returns nil if event lacks required fields (DTSTART, SUMMARY). Skips events with RECURRENCE-ID (individual instances of recurring events are handled separately via exception collection). Handles TZID-qualified timestamps by converting to local time. -Cleans text fields (description, location, summary) via `calendar-sync--clean-text'." +Cleans text fields (description, location, summary) via +`calendar-sync--clean-text'." ;; Skip individual instances of recurring events (they're collected as exceptions) (unless (calendar-sync--get-property event-str "RECURRENCE-ID") (let* ((uid (calendar-sync--get-property event-str "UID")) diff --git a/modules/calibredb-epub-config.el b/modules/calibredb-epub-config.el index 6d5963515..6c69ca0e8 100644 --- a/modules/calibredb-epub-config.el +++ b/modules/calibredb-epub-config.el @@ -77,6 +77,13 @@ (defvar calibredb-show-entry-switch) ; from calibredb-show.el (defvar calibredb-sort-by) ; from calibredb-core.el (defvar calibredb-search-filter) ; from calibredb-search.el +;; calibredb filter-state vars (set by cj/calibredb-clear-filters and friends) +(defvar calibredb-tag-filter-p) ; from calibredb-search.el +(defvar calibredb-favorite-filter-p) ; from calibredb-search.el +(defvar calibredb-author-filter-p) ; from calibredb-search.el +(defvar calibredb-date-filter-p) ; from calibredb-search.el +(defvar calibredb-format-filter-p) ; from calibredb-search.el +(defvar calibredb-search-current-page) ; from calibredb-search.el ;; -------------------------- CalibreDB Ebook Manager -------------------------- diff --git a/modules/chrono-tools.el b/modules/chrono-tools.el index 6f88b2018..744781268 100644 --- a/modules/chrono-tools.el +++ b/modules/chrono-tools.el @@ -22,6 +22,11 @@ (require 'user-constants) +;; Declared by the lazily-loaded `tmr' package; quiet the byte-compiler +;; without forcing the package to load. +(defvar tmr-sound-file) +(defvar tmr-descriptions-list) + ;; -------------------------------- Time Zones --------------------------------- (use-package time-zones diff --git a/modules/config-utilities.el b/modules/config-utilities.el index b3eec5d3d..f448327c1 100644 --- a/modules/config-utilities.el +++ b/modules/config-utilities.el @@ -21,6 +21,19 @@ (require 'find-lisp) (require 'profiler) +;; External variables referenced at runtime only (org and the native +;; compiler are loaded lazily; declare to quiet the byte-compiler). +(defvar comp-async-report-warnings-errors) +(defvar org-ts-regexp) +(defvar org-agenda-files) + +;; External functions referenced at runtime only. +(declare-function org-element-parse-buffer "org-element") +(declare-function org-element-map "org-element") +(declare-function org-element-property "org-element-ast") +(declare-function org-time-string-to-absolute "org") +(declare-function org-alert-check "org-alert") + ;;; -------------------------------- Debug Keymap ------------------------------- (defvar-keymap cj/debug-config-keymap @@ -65,13 +78,15 @@ (with-eval-after-load 'emacsql-sqlite-builtin (cl-defmethod emacsql-close :around ((connection emacsql-sqlite-builtin-connection)) - (when (oref connection handle) + ;; The class is loaded lazily, so the slot is unknown at compile time. + (when (with-no-warnings (oref connection handle)) (cl-call-next-method)))) (with-eval-after-load 'emacsql-sqlite-module (cl-defmethod emacsql-close :around ((connection emacsql-sqlite-module-connection)) - (when (oref connection handle) + ;; The class is loaded lazily, so the slot is unknown at compile time. + (when (with-no-warnings (oref connection handle)) (cl-call-next-method)))) ;;; -------------------------------- Benchmarking ------------------------------- diff --git a/modules/custom-ordering.el b/modules/custom-ordering.el index a2423742d..0a499a35a 100644 --- a/modules/custom-ordering.el +++ b/modules/custom-ordering.el @@ -49,10 +49,10 @@ buffer region and must reject an inverted one before reading it." (defun cj/--ordering-replace-region (start end insertion) "Replace the buffer text between START and END with INSERTION. -Point is left after the inserted text. Shared tail for the interactive ordering commands, -which all compute a transformed string from the original region then swap it -in. INSERTION is evaluated by the caller before this runs, so the transform -reads the pre-deletion text." +Point is left after the inserted text. Shared tail for the interactive +ordering commands, which all compute a transformed string from the +original region then swap it in. INSERTION is evaluated by the caller +before this runs, so the transform reads the pre-deletion text." (delete-region start end) (goto-char start) (insert insertion)) diff --git a/modules/dashboard-config.el b/modules/dashboard-config.el index 38510e801..2629fe0e0 100644 --- a/modules/dashboard-config.el +++ b/modules/dashboard-config.el @@ -23,6 +23,56 @@ (autoload 'cj/make-buffer-undead "undead-buffers" nil t) (declare-function ghostel "ghostel" (&optional arg)) +;; ------------------------------ Declarations ------------------------------- +;; These functions and variables belong to lazily-loaded packages or to other +;; cj modules; declaring them keeps the byte-compiler quiet without forcing an +;; eager require. Behavior is unchanged -- the symbols still resolve at runtime +;; once their owning package/module loads. + +;; dashboard package internals used by the bookmark-insertion override. +(declare-function dashboard-insert-section "dashboard") +(declare-function dashboard-subseq "dashboard") +(declare-function dashboard-get-shortcut "dashboard") +(declare-function dashboard-shorten-path "dashboard") +(declare-function dashboard--align-length-by-type "dashboard") +(declare-function dashboard--generate-align-format "dashboard") +(declare-function dashboard-refresh-buffer "dashboard") +(declare-function dashboard-open "dashboard") +(defvar dashboard-bookmarks-show-path) +(defvar dashboard--bookmarks-cache-item-format) + +;; bookmark.el (required at runtime inside `dashboard-insert-bookmarks'). +(declare-function bookmark-all-names "bookmark") +(declare-function bookmark-get-filename "bookmark") + +;; recentf.el (required at runtime inside the exclude helper). +(defvar recentf-exclude) + +;; nerd-icons glyph functions used in the launcher table. +(declare-function nerd-icons-faicon "nerd-icons") +(declare-function nerd-icons-devicon "nerd-icons") +(declare-function nerd-icons-mdicon "nerd-icons") +(declare-function nerd-icons-octicon "nerd-icons") + +;; user-constants.el provides the home-directory constant. +(defvar user-home-dir) + +;; Launcher actions defined in other cj modules. +(declare-function cj/main-agenda-display "org-agenda-config") +(declare-function cj/elfeed-open "elfeed-config") +(declare-function cj/drill-start "org-drill-config") +(declare-function cj/music-playlist-toggle "music-config") +(declare-function cj/music-playlist-load "music-config") +(declare-function cj/erc-switch-to-buffer-with-completion "erc-config") +(declare-function cj/telega "telega-config") +(declare-function cj/slack-start "slack-config") +(declare-function cj/signel-message "signal-config") +(declare-function cj/kill-all-other-buffers-and-windows "undead-buffers") + +;; External package commands invoked by launchers. +(declare-function mu4e "mu4e") +(declare-function pearl-list-issues "pearl") + ;; ------------------------ Dashboard Bookmarks Override ----------------------- ;; overrides the bookmark insertion from the dashboard package to provide an ;; option that only shows the bookmark name, avoiding the path. Paths are often @@ -35,8 +85,11 @@ ;; `el' is bound dynamically by dashboard's section-insertion machinery, which the ;; override below plugs into. Declare it so the byte-compiler reads the -;; references as that special variable rather than a free variable. -(defvar el) +;; references as that special variable rather than a free variable. The name is +;; dashboard's, not ours, so the missing-prefix lint is suppressed rather than +;; renamed (renaming would break the dynamic binding dashboard supplies). +(with-suppressed-warnings ((lexical el)) + (defvar el)) (defun dashboard-insert-bookmarks (list-size) "Add the list of LIST-SIZE items of bookmarks." diff --git a/modules/diff-config.el b/modules/diff-config.el index 75869a73f..0c09b9516 100644 --- a/modules/diff-config.el +++ b/modules/diff-config.el @@ -28,6 +28,12 @@ ;;; Code: +(declare-function ediff-setup-keymap "ediff") +(declare-function ediff-next-difference "ediff") +(declare-function ediff-previous-difference "ediff") +(declare-function cj/ediff-hook "diff-config") +(declare-function winner-undo "winner") + (use-package ediff :ensure nil ;; built-in :defer t diff --git a/modules/dirvish-config.el b/modules/dirvish-config.el index 04f9ce20e..f33e8cf74 100644 --- a/modules/dirvish-config.el +++ b/modules/dirvish-config.el @@ -41,6 +41,24 @@ (declare-function cj/drill-this-file "org-drill-config") +;; Dirvish/Dired functions called from lazy-loaded packages. +(declare-function dirvish-peek-mode "dirvish") +(declare-function dirvish-side-follow-mode "dirvish") +(declare-function dirvish-quit "dirvish") +(declare-function dired-get-marked-files "dired") +(declare-function dired-dwim-target-directory "dired-aux") +(declare-function dired-get-file-for-visit "dired") +(declare-function dired-get-filename "dired") +(declare-function dired-mark "dired") +(declare-function dired-current-directory "dired") +(declare-function dired-file-name-at-point "dired-x") +(declare-function dired-find-file "dired") +(declare-function project-roots "project") + +;; External package variables referenced before their package loads. +(defvar ediff-after-quit-hook-internal) +(defvar dirvish-side-attributes) + ;; mark files in dirvish, attach in mu4e (add-hook 'dired-mode-hook 'turn-on-gnus-dired-mode) @@ -349,7 +367,8 @@ Shadows dired's `P' (`dired-do-print') with this type-aware version." (defun cj/dirvish-drill-file () "Open the Org file at point and start an `org-drill' session on it. -Bound to `S' (\"study\") in `dirvish-mode-map'; refuses anything but a `.org' file." +Bound to `S' (\"study\") in `dirvish-mode-map'; refuses anything but +a `.org' file." (interactive) (let ((file (dired-get-filename nil t))) (unless (and file (not (file-directory-p file)) (string-suffix-p ".org" file t)) diff --git a/modules/dwim-shell-config.el b/modules/dwim-shell-config.el index 230a8532c..014194c7b 100644 --- a/modules/dwim-shell-config.el +++ b/modules/dwim-shell-config.el @@ -100,6 +100,16 @@ (require 'cl-lib) (require 'system-lib) ;; cj/confirm-strong (permanent file destruction confirm) +;; Function declarations (lazily-loaded packages and sibling modules). +(declare-function dwim-shell-command-on-marked-files "dwim-shell-command") +(declare-function dwim-shell-command-read-file-name "dwim-shell-command") +(declare-function dwim-shell-command--files "dwim-shell-command") +(declare-function cj/xdg-open "external-open") +(declare-function dwim-shell-commands-menu "dwim-shell-config") + +;; Forward declaration: external variable provided by the dirvish package. +(defvar dirvish-mode-map) + ;; --------------------------- Password-file helpers --------------------------- (defun cj/dwim-shell--password-cleanup-callback (temp-file) diff --git a/modules/elfeed-config.el b/modules/elfeed-config.el index 7712f48db..7b4d7d745 100644 --- a/modules/elfeed-config.el +++ b/modules/elfeed-config.el @@ -29,21 +29,26 @@ (require 'system-lib) (require 'media-utils) +(declare-function elfeed "elfeed") +(declare-function elfeed-update "elfeed") +(declare-function elfeed-entry-link "elfeed") +(declare-function elfeed-untag "elfeed") +(declare-function elfeed-search-selected "elfeed") +(declare-function elfeed-search-tag-all "elfeed") +(declare-function elfeed-search-update-entry "elfeed") +(declare-function elfeed-search-update--force "elfeed") +(declare-function elfeed-search-untag-all-unread "elfeed") +(declare-function eww-browse-url "eww") +(declare-function eww-readable "eww") + ;; ------------------------------- Elfeed Config ------------------------------- (use-package elfeed :bind - ("M-S-r" . cj/elfeed-open) ;; was M-R (:map elfeed-show-mode-map ("w" . eww-open-in-new-buffer)) (:map elfeed-search-mode-map - ("w" . cj/elfeed-eww-open) ;; opens in eww - ("b" . cj/elfeed-browser-open) ;; opens in external browser - ("d" . cj/elfeed-youtube-dl) ;; async download with yt-dlp and tsp - ("v" . cj/play-with-video-player)) ;; async play with mpv - ("V" . cj/select-media-player) ;; Capital V to select player - ("R" . cj/elfeed-mark-all-as-read) ;; capital marks all as read, since upper case marks one as read - ("U" . cj/elfeed-mark-all-as-unread) ;; capital marks all as unread, since lower case marks one as unread + ("V" . cj/select-media-player)) ;; Capital V to select player :config (setq elfeed-db-directory (concat user-emacs-directory ".elfeed-db")) (setq-default elfeed-search-title-max-width 150) @@ -90,19 +95,22 @@ (elfeed) (elfeed-update) (elfeed-search-update--force)) +(keymap-global-set "M-S-r" #'cj/elfeed-open) ;; was M-R ;; -------------------------- Elfeed Filter Functions -------------------------- (defun cj/elfeed-mark-all-as-read () "Remove the \='unread\=' tag from all visible entries in search buffer." (interactive) - (mark-whole-buffer) + (goto-char (point-min)) + (push-mark (point-max) nil t) (elfeed-search-untag-all-unread)) (defun cj/elfeed-mark-all-as-unread () "Add the \='unread\=' tag from all visible entries in the search buffer." (interactive) - (mark-whole-buffer) + (goto-char (point-min)) + (push-mark (point-max) nil t) (elfeed-search-tag-all 'unread)) (defun cj/elfeed-set-filter-and-update (filterstring) @@ -302,5 +310,18 @@ TYPE should be either \='channel or \='playlist." (insert result)) result)) +;; --------------------------- Search-Mode Keybindings ------------------------- +;; Bound here (not in use-package :bind) because these commands are defined in +;; this file; a :bind autoload stub plus the defun triggers a "defined multiple +;; times" byte-compile warning. + +(with-eval-after-load 'elfeed + (keymap-set elfeed-search-mode-map "w" #'cj/elfeed-eww-open) ;; opens in eww + (keymap-set elfeed-search-mode-map "b" #'cj/elfeed-browser-open) ;; opens in external browser + (keymap-set elfeed-search-mode-map "d" #'cj/elfeed-youtube-dl) ;; async download with yt-dlp and tsp + (keymap-set elfeed-search-mode-map "v" #'cj/play-with-video-player) ;; async play with mpv + (keymap-set elfeed-search-mode-map "R" #'cj/elfeed-mark-all-as-read) ;; capital R marks all read (lower case marks one) + (keymap-set elfeed-search-mode-map "U" #'cj/elfeed-mark-all-as-unread)) ;; capital U marks all unread (lower case marks one) + (provide 'elfeed-config) ;;; elfeed-config.el ends here. diff --git a/modules/erc-config.el b/modules/erc-config.el index c89e46bb3..3e98a66a3 100644 --- a/modules/erc-config.el +++ b/modules/erc-config.el @@ -33,6 +33,33 @@ ;; is read at load time below (erc-user-full-name), so a standalone .elc needs it. (require 'user-constants) +;; ERC loads lazily (use-package :commands), so these symbols aren't bound at +;; this file's compile time. Declare them to keep the byte-compiler quiet +;; without forcing an eager require. + +;; Functions provided by the erc package. +(declare-function erc-buffer-list "erc") +(declare-function erc-server-process-alive "erc") +(declare-function erc-server-or-unjoined-channel-buffer-p "erc") +(declare-function erc-current-nick "erc") +(declare-function erc-join-channel "erc") +(declare-function erc-part-from-channel "erc") +(declare-function erc-quit-server "erc") + +;; Variables read/set in the use-package :config block below. +(defvar erc-log-channels-directory) +(defvar erc-track-exclude-types) +(defvar erc-track-exclude-server-buffer) +(defvar erc-track-visibility) +(defvar erc-track-switch-direction) +(defvar erc-track-showcount) +;; NOTE: erc-unique-buffers and erc-generate-buffer-name-function are not ERC +;; variables in Emacs 30.x (no defcustom/defvar in the package); the setq below +;; only creates inert globals. Declared here to silence the warning without +;; changing the existing (no-op) behavior -- see the SUSPICIOUS note. +(defvar erc-unique-buffers) +(defvar erc-generate-buffer-name-function) + ;; ------------------------------------ ERC ------------------------------------ ;; Server definitions and connection settings @@ -99,7 +126,7 @@ Change this value to use a different nickname.") (let ((server-buffers '())) (dolist (buf (erc-buffer-list)) (with-current-buffer buf - (when (and (erc-server-buffer-p) (erc-server-process-alive)) + (when (and (erc-server-or-unjoined-channel-buffer-p) (erc-server-process-alive)) (unless (member (buffer-name) server-buffers) (push (buffer-name) server-buffers))))) @@ -132,7 +159,7 @@ Buffer names are shown with server context for clarity." "Return t if the current buffer is an active ERC server buffer." (and (derived-mode-p 'erc-mode) (erc-server-process-alive) - (erc-server-buffer-p))) + (erc-server-or-unjoined-channel-buffer-p))) (defun cj/erc-get-channels-for-current-server () @@ -158,7 +185,7 @@ Auto-adds # prefix if missing. Offers completion from configured channels." (let ((server-buffers (cl-remove-if-not (lambda (buf) (with-current-buffer buf - (and (erc-server-buffer-p) + (and (erc-server-or-unjoined-channel-buffer-p) (erc-server-process-alive)))) (erc-buffer-list)))) (if server-buffers diff --git a/modules/eshell-config.el b/modules/eshell-config.el index d3c8ccdfd..723a7e61e 100644 --- a/modules/eshell-config.el +++ b/modules/eshell-config.el @@ -26,6 +26,32 @@ (require 'system-utils) +;; Eshell is loaded lazily (:commands eshell), so its vars and functions are +;; not defined when this file is byte-compiled standalone. Declare them to +;; silence compile-time free-variable / undefined-function warnings. +(defvar eshell-banner-message) +(defvar eshell-scroll-to-bottom-on-input) +(defvar eshell-error-if-no-glob) +(defvar eshell-hist-ignoredups) +(defvar eshell-save-history-on-exit) +(defvar eshell-prefer-lisp-functions) +(defvar eshell-destroy-buffer-when-process-dies) +(defvar eshell-prompt-function) +(defvar eshell-cmpl-cycle-completions) +(defvar eshell-modules-list) +(defvar eshell-hist-mode-map) +(defvar eshell-visual-commands) +(defvar eshell-visual-subcommands) +(defvar eshell-visual-options) +(defvar eshell-history-ring) +(defvar eshell-preoutput-filter-functions) +(defvar eshell-output-filter-functions) + +(declare-function ring-elements "ring") +(declare-function eshell-send-input "esh-mode") +(declare-function eshell/pwd "em-dirs") +(declare-function eshell/alias "em-alias") + (defgroup cj/eshell nil "Personal Eshell configuration." :group 'eshell) diff --git a/modules/eww-config.el b/modules/eww-config.el index a41a9a76e..a5271f6bc 100644 --- a/modules/eww-config.el +++ b/modules/eww-config.el @@ -32,6 +32,8 @@ (require 'cl-lib) +(declare-function eww-add-bookmark "eww") + (defgroup my-eww-user-agent nil "EWW-only User-Agent management." :group 'eww) diff --git a/modules/flycheck-config.el b/modules/flycheck-config.el index 5626095c5..1afd3ae6c 100644 --- a/modules/flycheck-config.el +++ b/modules/flycheck-config.el @@ -45,6 +45,14 @@ (require 'keybindings) ;; provides cj/custom-keymap (use-package :map below) +;; ------------------------------- Declarations -------------------------------- + +(declare-function flycheck-mode "flycheck") +(declare-function flycheck-list-errors "flycheck") +(declare-function flycheck-add-mode "flycheck") +(declare-function flycheck-buffer "flycheck") +(declare-function cj/flycheck-prose-on-demand "flycheck-config") + (defun cj/prose-helpers-on () "Ensure that `abbrev-mode' and `flycheck-mode' are on in the current buffer." (interactive) diff --git a/modules/font-config.el b/modules/font-config.el index 1c431c864..3272a946e 100644 --- a/modules/font-config.el +++ b/modules/font-config.el @@ -56,6 +56,9 @@ (require 'host-environment) (require 'keybindings) ;; establishes the C-z prefix used for "C-z F" below +(defvar text-scale-mode-step) +(declare-function cj/disable-emojify-mode "font-config") + ;; ---------------------- HarfBuzz Font Cache Crash Fix ----------------------- ;; Prevents Emacs from compacting font caches during GC. Without this, GC can ;; free font cache entries that HarfBuzz still references, causing SIGSEGV diff --git a/modules/games-config.el b/modules/games-config.el index aa26d31ee..0ff01c809 100644 --- a/modules/games-config.el +++ b/modules/games-config.el @@ -25,6 +25,8 @@ (require 'user-constants) ;; org-dir +(defvar malyon-stories-directory) + (with-eval-after-load 'malyon (setq malyon-stories-directory (concat org-dir "text.games/"))) diff --git a/modules/help-utils.el b/modules/help-utils.el index f9f5d1427..3e31efffe 100644 --- a/modules/help-utils.el +++ b/modules/help-utils.el @@ -32,6 +32,10 @@ ;; ;;; Code: +;; Lazily-loaded functions referenced below. +(declare-function devdocs-go-back "devdocs") +(declare-function devdocs-go-forward "devdocs") + ;; ---------------------------------- Devdocs ---------------------------------- (use-package devdocs diff --git a/modules/httpd-config.el b/modules/httpd-config.el index c90399425..60baf7e82 100644 --- a/modules/httpd-config.el +++ b/modules/httpd-config.el @@ -19,13 +19,13 @@ (use-package simple-httpd :defer 1 :preface - (defconst wwwdir (concat user-emacs-directory "www")) - (defun check-or-create-wwwdir () - (unless (file-exists-p wwwdir) - (make-directory wwwdir))) - :init (check-or-create-wwwdir) + (defconst cj/httpd-wwwdir (concat user-emacs-directory "www")) + (defun cj/httpd-check-or-create-wwwdir () + (unless (file-exists-p cj/httpd-wwwdir) + (make-directory cj/httpd-wwwdir))) + :init (cj/httpd-check-or-create-wwwdir) :config - (setq httpd-root wwwdir) + (setq httpd-root cj/httpd-wwwdir) (setq httpd-show-backtrace-when-error t) (setq httpd-serve-files t)) diff --git a/modules/ledger-config.el b/modules/ledger-config.el index c268fa368..5b2712b57 100644 --- a/modules/ledger-config.el +++ b/modules/ledger-config.el @@ -3,6 +3,14 @@ ;;; Commentary: +;;; Code: + +;; ------------------------------- Declarations -------------------------------- + +(declare-function ledger-mode-clean-buffer "ledger-mode") +(defvar ledger-mode-map) +(defvar company-backends) + ;; -------------------------------- Ledger Mode -------------------------------- ;; edit files in ledger format @@ -16,7 +24,8 @@ (interactive) (save-excursion (when (buffer-modified-p) - (with-demoted-errors (ledger-mode-clean-buffer)) + (with-demoted-errors "Error cleaning ledger buffer: %S" + (ledger-mode-clean-buffer)) (save-buffer)))) :bind (:map ledger-mode-map diff --git a/modules/local-repository.el b/modules/local-repository.el index b97b74f41..6376d9f73 100644 --- a/modules/local-repository.el +++ b/modules/local-repository.el @@ -25,23 +25,33 @@ ;; ------------------------------- Customizations ------------------------------ +(defgroup localrepo nil + "Local last-known-good package repository." + :group 'package) + (defcustom localrepo-repository-id "localrepo" "The name used to identify the local repository internally. -Used for the package-archive and package-archive-priorities lists.") +Used for the package-archive and package-archive-priorities lists." + :type 'string + :group 'localrepo) (defcustom localrepo-repository-priority 100 "The value for the local repository in the package-archive-priority list. A higher value means higher priority. If you want your local packages to be -preferred, this must be a higher number than any other repositories.") +preferred, this must be a higher number than any other repositories." + :type 'integer + :group 'localrepo) (defcustom localrepo-repository-location (concat user-emacs-directory "/.localrepo") "The location of the local repository. It's a good idea to keep this with the rest of your configuration files and -keep them in source control.") +keep them in source control." + :type 'directory + :group 'localrepo) (defun cj/update-localrepo-repository () "Update the local repository with currently installed packages." diff --git a/modules/mail-config.el b/modules/mail-config.el index 08f50b12f..1d8a98c97 100644 --- a/modules/mail-config.el +++ b/modules/mail-config.el @@ -50,6 +50,31 @@ (declare-function mu4e-message-field "mu4e-message") +;; ----------------------------- Declarations ---------------------------------- +;; mu4e/org-msg load lazily, so the byte-compiler can't see these package +;; functions and variables when this module is compiled standalone. Declare +;; them to silence free-variable / undefined-function warnings without forcing +;; an eager require (which would defeat lazy loading). The cj/... entries are +;; forward references: defined later in this file's `:config' block, or in +;; mu4e-org-contacts-integration (required at load time inside that block). + +(declare-function mu4e-headers-mark-for-each-if "mu4e-mark") +(declare-function mu4e-search "mu4e-search") +(declare-function mu4e-view-refresh "mu4e-view") +(declare-function message-add-header "message") +(declare-function org-msg-edit-mode "org-msg") +(declare-function no-auto-fill "mail-config") +(declare-function cj/disable-company-in-mu4e-compose "mail-config") +(declare-function cj/disable-ispell-in-email-headers "mail-config") +(declare-function cj/activate-mu4e-org-contacts-integration + "mu4e-org-contacts-integration") + +;; Package variables assigned in the lazy `:config' blocks below. +(defvar mu4e-compose-keep-self-cc) +(defvar mu4e-root-maildir) +(defvar mu4e-show-images) +(defvar org-msg-extra-css) + ;; Refile (archive) target dispatch. A per-context `mu4e-refile-folder' string ;; is unsafe: mu4e context :vars are sticky, so a value set when one context is ;; active leaks into a later context that doesn't set its own -- archiving one @@ -197,12 +222,16 @@ Prompts user for the action when executing." ;; (setq mu4e-compose-format-flowed t) ;; plain text mails must flow correctly for recipients (setq mu4e-compose-keep-self-cc t) ;; keep me in the cc list - (setq mu4e-compose-signature-auto-include nil) ;; don't include signature by default + (with-suppressed-warnings ((obsolete mu4e-compose-signature-auto-include) + (free-vars mu4e-compose-signature-auto-include)) + (setq mu4e-compose-signature-auto-include nil)) ;; don't include signature by default (setq mu4e-confirm-quit nil) ;; don't ask when quitting (setq mu4e-context-policy 'pick-first) ;; start with the first (default) context (setq mu4e-headers-auto-update nil) ;; updating headers buffer on email is too jarring (setq mu4e-root-maildir mail-dir) ;; root directory for all email accounts - (setq mu4e-maildir mail-dir) ;; same as above (for newer mu4e) + (with-suppressed-warnings ((obsolete mu4e-maildir) + (free-vars mu4e-maildir)) + (setq mu4e-maildir mail-dir)) ;; same as above (for newer mu4e) (setq mu4e-sent-messages-behavior 'delete) ;; don't save to "Sent", IMAP does this already (setq mu4e-show-images t) ;; show embedded images ;; (setq mu4e-update-interval 600) ;; check for new mail every 10 minutes (600 seconds) @@ -214,12 +243,16 @@ Prompts user for the action when executing." ;; This will be automatically disabled when org-msg is active (setq mu4e-compose-format-flowed t) - (setq mu4e-html2text-command 'mu4e-shr2text) ;; email conversion to html via shr2text + (with-suppressed-warnings ((obsolete mu4e-html2text-command) + (free-vars mu4e-html2text-command)) + (setq mu4e-html2text-command 'mu4e-shr2text)) ;; email conversion to html via shr2text (setq mu4e-mu-binary (executable-find "mu")) (setq mu4e-get-mail-command (cj/mail--mbsync-command)) ;; command to sync mail - (setq mu4e-user-mail-address-list '("c@cjennings.net" - "craigmartinjennings@gmail.com" - "craig.jennings@deepsat.com")) + (with-suppressed-warnings ((obsolete mu4e-user-mail-address-list) + (free-vars mu4e-user-mail-address-list)) + (setq mu4e-user-mail-address-list '("c@cjennings.net" + "craigmartinjennings@gmail.com" + "craig.jennings@deepsat.com"))) (setq mu4e-index-update-error-warning nil) ;; don't warn me about spurious sync issues ;; ------------------------------ Mu4e Contexts ------------------------------ @@ -295,7 +328,7 @@ Prompts user for the action when executing." :key ?d))) (defun no-auto-fill () - "Turn off \'auto-fill-mode\'." + "Turn off `auto-fill-mode'." (auto-fill-mode -1)) (add-hook 'mu4e-compose-mode-hook #'no-auto-fill) @@ -317,19 +350,23 @@ Prompts user for the action when executing." ;; also see org-msg below ;; Prefer HTML over plain text when both are available - (setq mu4e-view-prefer-html t) + (with-suppressed-warnings ((obsolete mu4e-view-prefer-html) + (free-vars mu4e-view-prefer-html)) + (setq mu4e-view-prefer-html t)) ;; Use a better HTML renderer with more control - (setq mu4e-html2text-command - (cond - ;; Best option: pandoc (if available) - ((executable-find "pandoc") - "pandoc -f html -t plain --reference-links") - ;; Good option: w3m (better tables/formatting) - ((executable-find "w3m") - "w3m -dump -T text/html -cols 72 -o display_link_number=true") - ;; Fallback: built-in shr - (t 'mu4e-shr2text))) + (with-suppressed-warnings ((obsolete mu4e-html2text-command) + (free-vars mu4e-html2text-command)) + (setq mu4e-html2text-command + (cond + ;; Best option: pandoc (if available) + ((executable-find "pandoc") + "pandoc -f html -t plain --reference-links") + ;; Good option: w3m (better tables/formatting) + ((executable-find "w3m") + "w3m -dump -T text/html -cols 72 -o display_link_number=true") + ;; Fallback: built-in shr + (t 'mu4e-shr2text)))) ;; Configure shr (built-in HTML renderer) for better display (setq shr-use-colors nil) ; Don't use colors in terminal @@ -339,8 +376,10 @@ Prompts user for the action when executing." (setq shr-bullet "• ") ; Nice bullet points ;; Block remote images by default (privacy/security) - (setq mu4e-view-show-images t) - (setq mu4e-view-image-max-width 800) + (with-suppressed-warnings ((obsolete mu4e-view-show-images mu4e-view-image-max-width) + (free-vars mu4e-view-show-images mu4e-view-image-max-width)) + (setq mu4e-view-show-images t) + (setq mu4e-view-image-max-width 800)) ;; ------------------------------- View Actions ------------------------------ ;; define view and article menus diff --git a/modules/markdown-config.el b/modules/markdown-config.el index 16935425d..424c09cc8 100644 --- a/modules/markdown-config.el +++ b/modules/markdown-config.el @@ -20,14 +20,13 @@ :mode (("README\\.md\\'" . gfm-mode) ("\\.md\\'" . markdown-mode) ("\\.markdown\\'" . markdown-mode)) - :bind (:map markdown-mode-map - ("<f2>" . cj/markdown-preview)) ;; use same key as compile for consistency :init (setq markdown-command "multimarkdown")) ;; Register markdown as a known org-src-block language so `org-lint' ;; stops warning on `#+begin_src markdown ... #+end_src' and `C-c '' ;; inside such a block opens it in `markdown-mode' instead of falling ;; back to fundamental-mode. +(defvar org-src-lang-modes) (with-eval-after-load 'org (add-to-list 'org-src-lang-modes '("markdown" . markdown))) @@ -40,6 +39,8 @@ ;;;; --------------------- WIP: Markdown-Preview --------------------- +(declare-function imp--notify-clients "impatient-mode") + (defun cj/markdown-preview-server-start () "Start the simple-httpd listener that serves the live markdown preview. Idempotent: re-running while the server is already up is a no-op." @@ -75,5 +76,12 @@ lives in a separate command." (buffer-substring-no-properties (point-min) (point-max)))) (current-buffer))) +;; Bind the preview key after the defun so use-package's `:bind' autoload +;; stub doesn't collide with this file's own definition of the command +;; (that collision is the "defined multiple times" byte-compile warning). +;; Same key as compile, for consistency. +(with-eval-after-load 'markdown-mode + (keymap-set markdown-mode-map "<f2>" #'cj/markdown-preview)) + (provide 'markdown-config) ;;; markdown-config.el ends here diff --git a/modules/mousetrap-mode.el b/modules/mousetrap-mode.el index 99475fcde..3817e0081 100644 --- a/modules/mousetrap-mode.el +++ b/modules/mousetrap-mode.el @@ -67,7 +67,8 @@ Categories can be combined in profiles to allow specific interaction patterns.") "Mouse interaction profiles for different use cases. Each profile specifies which event categories are allowed. -Available categories: primary-click, secondary-click, drags, multi-clicks, scroll. +Available categories: primary-click, secondary-click, drags, +multi-clicks, scroll. Profiles: - disabled: Block all mouse events @@ -88,7 +89,7 @@ Modes not listed here will use `mouse-trap-default-profile'. When checking, the mode hierarchy is respected via `derived-mode-p'.") (defvar mouse-trap-default-profile 'disabled - "Default profile to use when current major mode is not in `mouse-trap-mode-profiles'.") + "Default profile when the major mode is not in `mouse-trap-mode-profiles'.") ;;; Keymap Builder @@ -187,6 +188,11 @@ Used via `emulation-mode-map-alists' so each buffer gets its own keymap.") ;;; Minor Mode Definition +;; Forward declaration: the minor-mode variable is defined by the +;; `define-minor-mode' form below, but referenced earlier in the lighter +;; keymap and lighter-string helpers. +(defvar mouse-trap-mode) + (defvar mouse-trap--lighter-keymap (let ((map (make-sparse-keymap))) (define-key map [mode-line mouse-1] diff --git a/modules/mu4e-org-contacts-integration.el b/modules/mu4e-org-contacts-integration.el index 6aed3d4cf..daa12701a 100644 --- a/modules/mu4e-org-contacts-integration.el +++ b/modules/mu4e-org-contacts-integration.el @@ -32,7 +32,6 @@ This function is designed to work with mu4e's compose buffers." (re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*" nil t) (goto-char (match-end 0)) (point))) - (initial (buffer-substring-no-properties start end)) (contacts (cj/get-all-contact-emails))) (when contacts (list start end diff --git a/modules/mu4e-org-contacts-setup.el b/modules/mu4e-org-contacts-setup.el index 034e74574..64e9a611f 100644 --- a/modules/mu4e-org-contacts-setup.el +++ b/modules/mu4e-org-contacts-setup.el @@ -7,6 +7,10 @@ ;;; Code: +(defvar mu4e-compose-complete-only-personal) +(defvar mu4e-compose-complete-only-after) +(declare-function cj/activate-mu4e-org-contacts-integration "mu4e-org-contacts-integration") + ;; Load the integration module. Activation only runs when the module loaded ;; cleanly AND mu4e is present; otherwise this file is a no-op so the rest ;; of the config can load without mu4e installed. diff --git a/modules/music-config.el b/modules/music-config.el index 7c3af0e13..0874c4982 100644 --- a/modules/music-config.el +++ b/modules/music-config.el @@ -103,6 +103,37 @@ ;; orderless never see the binding (the lexical-binding foreign-special-var trap). (defvar orderless-smart-case) (defvar emms-source-playlist-ask-before-overwrite) +(defvar emms-playlist-buffer-p) +(defvar emms-playlist-buffer) +(defvar emms-random-playlist) +(defvar emms-playlist-selected-marker) +(defvar emms-source-file-default-directory) +(defvar emms-player-mpv-parameters) +(defvar emms-player-mpv-regexp) +(defvar emms-player-playing-p) +(defvar emms-player-paused-p) +(defvar emms-playlist-mode-map) +(defvar dirvish-mode-map) + +;; Foreign functions used lazily after their packages load. +(declare-function emms-playlist-mode "emms-playlist-mode") +(declare-function emms-playlist-track-at "emms-playlist-mode") +(declare-function emms-playlist-mode-kill-track "emms-playlist-mode") +(declare-function emms-track-name "emms") +(declare-function emms-track-type "emms") +(declare-function emms-track-get "emms") +(declare-function emms-track-simple-description "emms") +(declare-function emms-playlist-current-selected-track "emms") +(declare-function emms-playlist-select "emms") +(declare-function emms-playlist-clear "emms") +(declare-function emms-playlist-save "emms-source-playlist") +(declare-function emms-start "emms") +(declare-function emms-random "emms") +(declare-function emms-next "emms") +(declare-function emms-previous "emms") +(declare-function dired-get-marked-files "dired") +(declare-function dired-get-file-for-visit "dired") +(declare-function face-remap-remove-relative "face-remap") ;;; Settings (no Customize) @@ -619,26 +650,26 @@ Initializes EMMS if needed." ;;; Dired/Dirvish integration -(with-eval-after-load 'dirvish - (defun cj/music-add-dired-selection () - "Add selected files/dirs in Dired/Dirvish to the EMMS playlist. +(defun cj/music-add-dired-selection () + "Add selected files/dirs in Dired/Dirvish to the EMMS playlist. Dirs added recursively." - (interactive) - (unless (derived-mode-p 'dired-mode) - (user-error "This command must be run in a Dired buffer")) - (cj/music--ensure-playlist-buffer) - (let ((files (if (use-region-p) - (dired-get-marked-files) - (list (dired-get-file-for-visit))))) - (when (null files) - (user-error "No files selected")) - (dolist (file files) - (cond - ((file-directory-p file) (cj/music-add-directory-recursive file)) - ((cj/music--valid-file-p file) (emms-add-file file)) - (t (message "Skipping non-music file: %s" file)))) - (message "Added %d item(s) to playlist" (length files)))) + (interactive) + (unless (derived-mode-p 'dired-mode) + (user-error "This command must be run in a Dired buffer")) + (cj/music--ensure-playlist-buffer) + (let ((files (if (use-region-p) + (dired-get-marked-files) + (list (dired-get-file-for-visit))))) + (when (null files) + (user-error "No files selected")) + (dolist (file files) + (cond + ((file-directory-p file) (cj/music-add-directory-recursive file)) + ((cj/music--valid-file-p file) (emms-add-file file)) + (t (message "Skipping non-music file: %s" file)))) + (message "Added %d item(s) to playlist" (length files)))) +(with-eval-after-load 'dirvish (keymap-set dirvish-mode-map "+" #'cj/music-add-dired-selection)) ;;; EMMS setup and keybindings @@ -680,6 +711,130 @@ Dirs added recursively." "C-; m z" "random" "C-; m x" "consume")) +;;; Playlist display helpers +;; +;; Defined at top level (not inside the `emms' use-package `:config') so the +;; byte-compiler sees them; they touch EMMS only at call time, after load. + +(defun cj/music--after-playlist-clear (&rest _) + "Forget the associated M3U file after the playlist is cleared." + (when-let ((buf (get-buffer cj/music-playlist-buffer-name))) + (with-current-buffer buf + (setq cj/music-playlist-file nil)))) + +(defun cj/music--format-duration (seconds) + "Convert SECONDS to a \"M:SS\" string." + (when (and seconds (numberp seconds) (> seconds 0)) + (format "%d:%02d" (/ seconds 60) (mod seconds 60)))) + +(defun cj/music--track-description (track) + "Return a human-readable description of TRACK. +For tagged tracks: \"Artist - Title [M:SS]\". +For file tracks without tags: filename without path or extension. +For URL tracks: decoded URL." + (let ((type (emms-track-type track)) + (title (emms-track-get track 'info-title)) + (artist (emms-track-get track 'info-artist)) + (duration (emms-track-get track 'info-playing-time)) + (name (emms-track-name track))) + (cond + ;; Tagged track with title + (title + (let ((dur-str (cj/music--format-duration duration)) + (parts '())) + (when artist (push artist parts)) + (push title parts) + (let ((desc (string-join (nreverse parts) " - "))) + (if dur-str (format "%s [%s]" desc dur-str) desc)))) + ;; File without tags — show clean filename + ((eq type 'file) + (file-name-sans-extension (file-name-nondirectory name))) + ;; URL — decode percent-encoded characters + ((eq type 'url) + (decode-coding-string (url-unhex-string name) 'utf-8)) + ;; Fallback + (t (emms-track-simple-description track))))) + +;; Multi-line header overlay +(defvar-local cj/music--header-overlay nil + "Overlay displaying the playlist header.") + +(defun cj/music--header-text () + "Build a multi-line header string for the playlist buffer overlay." + (let* ((pl-name (if cj/music-playlist-file + (file-name-sans-extension + (file-name-nondirectory cj/music-playlist-file)) + "Untitled")) + (track-count (count-lines (point-min) (point-max))) + (now-playing (cond + ((not emms-player-playing-p) "Stopped") + (emms-player-paused-p "Paused") + (t (let ((track (emms-playlist-current-selected-track))) + (if track + (cj/music--track-description track) + "Playing"))))) + (mode-indicator + (lambda (key label active) + (let ((face (if active 'cj/music-mode-on-face 'cj/music-mode-off-face))) + (propertize (format "[%s] %s" key label) 'face face))))) + (concat + (propertize "Playlist" 'face 'cj/music-header-face) + (propertize " : " 'face 'cj/music-header-face) + (propertize (format "%s (%d)" pl-name track-count) 'face 'cj/music-header-value-face) + "\n" + (propertize "Current " 'face 'cj/music-header-face) + (propertize " : " 'face 'cj/music-header-face) + (propertize now-playing 'face 'cj/music-header-value-face) + "\n" + (propertize "Mode " 'face 'cj/music-header-face) + (propertize " : " 'face 'cj/music-header-face) + (funcall mode-indicator "r" "repeat" (bound-and-true-p emms-repeat-playlist)) + " " + (funcall mode-indicator "t" "single" (bound-and-true-p emms-repeat-track)) + " " + (funcall mode-indicator "z" "random" (bound-and-true-p emms-random-playlist)) + " " + (funcall mode-indicator "x" "consume" cj/music-consume-mode) + "\n" + (propertize "Keys " 'face 'cj/music-header-face) + (propertize " : " 'face 'cj/music-header-face) + (propertize "a:add c:clear L:load S:save SPC:pause <>:skip ↑↓:move C-↑↓:reorder q:dismiss" + 'face 'cj/music-keyhint-face) + "\n\n"))) + +(defun cj/music--update-header () + "Insert or update the multi-line header overlay in the playlist buffer." + (when-let ((buf (get-buffer cj/music-playlist-buffer-name))) + (with-current-buffer buf + (unless cj/music--header-overlay + (setq cj/music--header-overlay (make-overlay (point-min) (point-min))) + (overlay-put cj/music--header-overlay 'priority 100)) + (move-overlay cj/music--header-overlay (point-min) (point-min)) + (overlay-put cj/music--header-overlay 'before-string + (cj/music--header-text))))) + +(defvar-local cj/music--bg-remap-cookie nil + "Cookie for the active-window background face remapping.") + +(defun cj/music--update-active-bg (&rest _) + "Toggle playlist buffer background based on whether its window is selected." + (when-let ((buf (get-buffer cj/music-playlist-buffer-name))) + (with-current-buffer buf + (let ((active (eq buf (window-buffer (selected-window))))) + (cond + ((and active (not cj/music--bg-remap-cookie)) + (setq cj/music--bg-remap-cookie + (face-remap-add-relative 'default :background "#1d1b19"))) + ((and (not active) cj/music--bg-remap-cookie) + (face-remap-remove-relative cj/music--bg-remap-cookie) + (setq cj/music--bg-remap-cookie nil))))))) + +(defun cj/music--setup-playlist-display () + "Set up header overlay and focus tracking in the playlist buffer." + (setq header-line-format nil) + (cj/music--update-header) + (add-hook 'window-selection-change-functions #'cj/music--update-active-bg nil t)) + (use-package emms :defer t :init @@ -704,7 +859,7 @@ Dirs added recursively." (emms-all) ;; Disable modeline display (keep modeline clean) - (emms-playing-time-disable-display) + (emms-playing-time-display-mode -1) (emms-mode-line-mode -1) ;; MPV configuration @@ -718,134 +873,16 @@ Dirs added recursively." (regexp-opt cj/music-file-extensions) "\\'\\)")) - ;; Keep cj/music-playlist-file in sync if playlist is cleared - (defun cj/music--after-playlist-clear (&rest _) - (when-let ((buf (get-buffer cj/music-playlist-buffer-name))) - (with-current-buffer buf - (setq cj/music-playlist-file nil)))) - - ;; Ensure we don't stack duplicate advice on reload + ;; Keep cj/music-playlist-file in sync if playlist is cleared. + ;; Ensure we don't stack duplicate advice on reload. (advice-remove 'emms-playlist-clear #'cj/music--after-playlist-clear) (advice-add 'emms-playlist-clear :after #'cj/music--after-playlist-clear) ;;; Playlist display ;; Track description: show "Artist - Title [M:SS]" instead of file paths - (defun cj/music--format-duration (seconds) - "Convert SECONDS to a \"M:SS\" string." - (when (and seconds (numberp seconds) (> seconds 0)) - (format "%d:%02d" (/ seconds 60) (mod seconds 60)))) - - (defun cj/music--track-description (track) - "Return a human-readable description of TRACK. -For tagged tracks: \"Artist - Title [M:SS]\". -For file tracks without tags: filename without path or extension. -For URL tracks: decoded URL." - (let ((type (emms-track-type track)) - (title (emms-track-get track 'info-title)) - (artist (emms-track-get track 'info-artist)) - (duration (emms-track-get track 'info-playing-time)) - (name (emms-track-name track))) - (cond - ;; Tagged track with title - (title - (let ((dur-str (cj/music--format-duration duration)) - (parts '())) - (when artist (push artist parts)) - (push title parts) - (let ((desc (string-join (nreverse parts) " - "))) - (if dur-str (format "%s [%s]" desc dur-str) desc)))) - ;; File without tags — show clean filename - ((eq type 'file) - (file-name-sans-extension (file-name-nondirectory name))) - ;; URL — decode percent-encoded characters - ((eq type 'url) - (decode-coding-string (url-unhex-string name) 'utf-8)) - ;; Fallback - (t (emms-track-simple-description track))))) - (setq emms-track-description-function #'cj/music--track-description) - ;; Multi-line header overlay - (defvar-local cj/music--header-overlay nil - "Overlay displaying the playlist header.") - - (defun cj/music--header-text () - "Build a multi-line header string for the playlist buffer overlay." - (let* ((pl-name (if cj/music-playlist-file - (file-name-sans-extension - (file-name-nondirectory cj/music-playlist-file)) - "Untitled")) - (track-count (count-lines (point-min) (point-max))) - (now-playing (cond - ((not emms-player-playing-p) "Stopped") - (emms-player-paused-p "Paused") - (t (let ((track (emms-playlist-current-selected-track))) - (if track - (cj/music--track-description track) - "Playing"))))) - (mode-indicator - (lambda (key label active) - (let ((face (if active 'cj/music-mode-on-face 'cj/music-mode-off-face))) - (propertize (format "[%s] %s" key label) 'face face))))) - (concat - (propertize "Playlist" 'face 'cj/music-header-face) - (propertize " : " 'face 'cj/music-header-face) - (propertize (format "%s (%d)" pl-name track-count) 'face 'cj/music-header-value-face) - "\n" - (propertize "Current " 'face 'cj/music-header-face) - (propertize " : " 'face 'cj/music-header-face) - (propertize now-playing 'face 'cj/music-header-value-face) - "\n" - (propertize "Mode " 'face 'cj/music-header-face) - (propertize " : " 'face 'cj/music-header-face) - (funcall mode-indicator "r" "repeat" (bound-and-true-p emms-repeat-playlist)) - " " - (funcall mode-indicator "t" "single" (bound-and-true-p emms-repeat-track)) - " " - (funcall mode-indicator "z" "random" (bound-and-true-p emms-random-playlist)) - " " - (funcall mode-indicator "x" "consume" cj/music-consume-mode) - "\n" - (propertize "Keys " 'face 'cj/music-header-face) - (propertize " : " 'face 'cj/music-header-face) - (propertize "a:add c:clear L:load S:save SPC:pause <>:skip ↑↓:move C-↑↓:reorder q:dismiss" - 'face 'cj/music-keyhint-face) - "\n\n"))) - - (defun cj/music--update-header () - "Insert or update the multi-line header overlay in the playlist buffer." - (when-let ((buf (get-buffer cj/music-playlist-buffer-name))) - (with-current-buffer buf - (unless cj/music--header-overlay - (setq cj/music--header-overlay (make-overlay (point-min) (point-min))) - (overlay-put cj/music--header-overlay 'priority 100)) - (move-overlay cj/music--header-overlay (point-min) (point-min)) - (overlay-put cj/music--header-overlay 'before-string - (cj/music--header-text))))) - - (defvar-local cj/music--bg-remap-cookie nil - "Cookie for the active-window background face remapping.") - - (defun cj/music--update-active-bg (&rest _) - "Toggle playlist buffer background based on whether its window is selected." - (when-let ((buf (get-buffer cj/music-playlist-buffer-name))) - (with-current-buffer buf - (let ((active (eq buf (window-buffer (selected-window))))) - (cond - ((and active (not cj/music--bg-remap-cookie)) - (setq cj/music--bg-remap-cookie - (face-remap-add-relative 'default :background "#1d1b19"))) - ((and (not active) cj/music--bg-remap-cookie) - (face-remap-remove-relative cj/music--bg-remap-cookie) - (setq cj/music--bg-remap-cookie nil))))))) - - (defun cj/music--setup-playlist-display () - "Set up header overlay and focus tracking in the playlist buffer." - (setq header-line-format nil) - (cj/music--update-header) - (add-hook 'window-selection-change-functions #'cj/music--update-active-bg nil t)) - (add-hook 'emms-playlist-mode-hook #'cj/music--setup-playlist-display) (add-hook 'emms-player-started-hook #'cj/music--record-random-history) (add-hook 'emms-player-started-hook #'cj/music--update-header) @@ -897,8 +934,6 @@ For URL tracks: decoded URL." ("S-<down>" . emms-playlist-mode-shift-track-down) ("C-<up>" . emms-playlist-mode-shift-track-up) ("C-<down>" . emms-playlist-mode-shift-track-down) - ;; Radio - ("R" . cj/music-create-radio-station) ;; Volume ("+" . emms-volume-raise) ("=" . emms-volume-raise) @@ -927,5 +962,10 @@ For URL tracks: decoded URL." (insert content)) (message "Created radio station: %s" (file-name-nondirectory file)))) +;; Bound here rather than in the emms `:bind' so use-package does not emit a +;; redundant autoload that collides with this same-file definition. +(with-eval-after-load 'emms + (keymap-set emms-playlist-mode-map "R" #'cj/music-create-radio-station)) + (provide 'music-config) ;;; music-config.el ends here diff --git a/modules/org-agenda-config-debug.el b/modules/org-agenda-config-debug.el index a9c713a13..4c1b1dd84 100644 --- a/modules/org-agenda-config-debug.el +++ b/modules/org-agenda-config-debug.el @@ -18,6 +18,9 @@ (require 'user-constants) (require 'system-lib) +(defvar org-agenda-files) +(declare-function cj/build-org-agenda-list "org-agenda-config") + ;; ---------------------------- Debug Functions -------------------------------- ;;;###autoload diff --git a/modules/org-agenda-config.el b/modules/org-agenda-config.el index e20c7a6b5..3234cc929 100644 --- a/modules/org-agenda-config.el +++ b/modules/org-agenda-config.el @@ -59,7 +59,8 @@ (defcustom cj/org-agenda-window-height 0.75 "Fraction of the selected frame used for the org agenda window." - :type 'number) + :type 'number + :group 'org-agenda) (defun cj/--org-agenda-display-rule () "Return the display-buffer rule for the org agenda buffer." diff --git a/modules/org-capture-config.el b/modules/org-capture-config.el index 2f245185f..9f5bfbe7f 100644 --- a/modules/org-capture-config.el +++ b/modules/org-capture-config.el @@ -30,6 +30,7 @@ (defvar org-complex-heading-regexp-format) (declare-function cj/--drill-pick-file "org-drill-config") +(declare-function cj/org-capture--date-prefix "org-capture-config") (declare-function org-at-encrypted-entry-p "org-crypt") (declare-function org-at-heading-p "org") (declare-function org-back-to-heading "org") @@ -170,7 +171,7 @@ letter upcased: \"~/.emacs.d/\" -> \"Emacs.d\", \"~/code/duet/\" -> \"Duet\"." ROOT is the projectile project root (or nil); INBOX is the global inbox file path. Return a plist (:file F :open-work BOOL :project NAME :warn MSG): - ROOT with a todo.org -> F is that todo.org, :open-work t. -- ROOT without a todo.org -> F is INBOX, :open-work nil, :warn names the project. +- ROOT without a todo.org -> F is INBOX, :open-work nil, :warn names project. - ROOT nil -> F is INBOX, :open-work nil, :warn nil." (if (and (stringp root) (not (string-empty-p root))) (let ((todo (expand-file-name "todo.org" root)) diff --git a/modules/org-config.el b/modules/org-config.el index 8d722ad46..f316ee0df 100644 --- a/modules/org-config.el +++ b/modules/org-config.el @@ -17,6 +17,72 @@ (require 'keybindings) ;; provides cj/custom-keymap (used in :init below) +;; Declare org variables and functions used before org is loaded so this module +;; byte-compiles standalone. Plain `defvar' (no value) marks the symbol special +;; without assigning anything, so org's own defaults still apply at runtime. +(defvar org-dir) +(defvar org-mode-map) +(defvar org-mouse-map) +(defvar org-modules) +(defvar org-startup-folded) +(defvar org-cycle-open-archived-trees) +(defvar org-cycle-hide-drawers) +(defvar org-id-locations-file) +(defvar org-return-follows-link) +(defvar org-list-allow-alphabetical) +(defvar org-startup-indented) +(defvar org-adapt-indentation) +(defvar org-startup-with-inline-images) +(defvar org-image-actual-width) +(defvar org-yank-image-save-method) +(defvar org-bookmark-names-plist) +(defvar org-file-apps) +(defvar org-ellipsis) +(defvar org-hide-emphasis-markers) +(defvar org-hide-leading-stars) +(defvar org-pretty-entities) +(defvar org-pretty-entities-include-sub-superscripts) +(defvar org-fontify-emphasized-text) +(defvar org-fontify-whole-heading-line) +(defvar org-tags-column) +(defvar org-agenda-tags-column) +(defvar org-todo-keywords) +(defvar org-highest-priority) +(defvar org-lowest-priority) +(defvar org-default-priority) +(defvar org-enforce-todo-dependencies) +(defvar org-enforce-todo-checkbox-dependencies) +(defvar org-deadline-warning-days) +(defvar org-treat-insert-todo-heading-as-state-change) +(defvar org-log-into-drawer) +(defvar org-log-done) +(defvar org-use-property-inheritance) + +(declare-function org-current-level "org") +(declare-function org-add-planning-info "org") +(declare-function org-get-heading "org") +(declare-function org-edit-headline "org") +(declare-function org-priority "org") +(declare-function org-heading-components "org") +(declare-function org-todo "org") +(declare-function org-get-todo-state "org") +(declare-function org-back-to-heading "org") +(declare-function org-sort-entries "org") +(declare-function org-eval-in-calendar "org") +(declare-function org-open-at-point "org") +(declare-function org-backward-heading-same-level "org") +(declare-function org-forward-heading-same-level "org") +(declare-function org-reveal "org") +(declare-function org-show-todo-tree "org") +(declare-function org-fold-show-all "org-fold") +(declare-function outline-next-heading "outline") +(declare-function org-element-cache-reset "org-element") +(declare-function org-element-context "org-element") +(declare-function org-element-type "org-element-ast") +(declare-function org-superstar-configure-like-org-bullets "org-superstar") +(declare-function cj/--org-follow-link-same-window "org-config") +(declare-function cj/org-follow-link-at-mouse-same-window "org-config") + ;; ---------------------------- Org General Settings --------------------------- (defun cj/org-general-settings () @@ -250,14 +316,14 @@ whole row line." (keymap-set cj/org-map "<" #'cj/org-narrow-backwards) ;; Sparse trees: lowercase creates, capital of the same letter cancels. - ;; Both `S' and `T' resolve to `org-show-all' -- same cancel command, + ;; Both `S' and `T' resolve to `org-fold-show-all' -- same cancel command, ;; paired with each lowercase create so the mental model is "capital ;; cancels the lowercase command I just ran" without having to recall ;; which letter the cancel actually lives on. (keymap-set cj/org-map "s" #'org-match-sparse-tree) - (keymap-set cj/org-map "S" #'org-show-all) + (keymap-set cj/org-map "S" #'org-fold-show-all) (keymap-set cj/org-map "t" #'org-show-todo-tree) - (keymap-set cj/org-map "T" #'org-show-all) + (keymap-set cj/org-map "T" #'org-fold-show-all) (keymap-set cj/org-map "R" #'org-reveal) :bind ("C-c c" . org-capture) @@ -273,8 +339,7 @@ whole row line." ("C-c N" . org-narrow-to-subtree) ("C-c >" . cj/org-narrow-forward) ("C-c <" . cj/org-narrow-backwards) - ("C-c <ESC>" . widen) - ("C-c C-a" . cj/org-appear-toggle)) + ("C-c <ESC>" . widen)) (:map cj/org-map ("r i" . org-table-insert-row) ("r d" . org-table-kill-row) @@ -401,6 +466,11 @@ especially in tables with long URLs)." (org-appear-mode 1) (message "org-appear enabled (links/emphasis show when editing)"))) +;; Bound here (after the defun) rather than in the org use-package `:bind' so +;; the command isn't autoloaded into a stub that shadows this definition. +(with-eval-after-load 'org + (keymap-set org-mode-map "C-c C-a" #'cj/org-appear-toggle)) + ;; --------------------------------- Org-Tidy ---------------------------------- ;; Hide :PROPERTIES: drawers behind a small inline marker so headings stay @@ -444,7 +514,7 @@ with a file, the function will throw an error." "Clear the org-element cache for the current buffer or all buffers. By default, clear cache for all org buffers. With prefix argument, clear only the current buffer's cache. Useful when encountering parsing errors like -'wrong-type-argument stringp nil' during agenda generation." +\"wrong-type-argument stringp nil\" during agenda generation." (interactive) (if current-prefix-arg (if (derived-mode-p 'org-mode) diff --git a/modules/org-contacts-config.el b/modules/org-contacts-config.el index 556530eb2..64abb9fb5 100644 --- a/modules/org-contacts-config.el +++ b/modules/org-contacts-config.el @@ -22,6 +22,36 @@ (require 'user-constants) +;; Function declarations -- these live in lazily-loaded packages, so the +;; byte-compiler can't see their definitions when this module compiles +;; standalone. +(declare-function org-contacts-db "org-contacts") +(declare-function org-contacts-anniversaries "org-contacts") +(declare-function org-contacts-files "org-contacts") +(declare-function org-columns "org-colview") +(declare-function org-reveal "org") +(declare-function org-fold-show-entry "org-fold") +(declare-function org-heading-components "org") +(declare-function org-map-entries "org") +(declare-function org-entry-get "org") +(declare-function outline-next-heading "outline") +(declare-function calendar-current-date "calendar") +(declare-function mu4e-message-at-point "mu4e-message") +(declare-function mu4e-message-field "mu4e-message") +(declare-function which-key-add-key-based-replacements "which-key") + +;; External package variables referenced below; declared so the compiler +;; treats them as special rather than free. +(defvar org-capture-plist) +(defvar org-capture-templates) +(defvar mu4e~view-message) +(defvar org-agenda-include-diary) +(defvar org-agenda-custom-commands) +(defvar mu4e-org-contacts-file) +(defvar mu4e-headers-actions) +(defvar mu4e-view-actions) +(defvar mu4e-compose-complete-addresses) + ;; Set `org-contacts-files' eagerly at require time. Setting it in the ;; `use-package' form below would only apply when org-contacts loads, which is ;; deferred behind `:after (org mu4e)' -- later than the first @@ -42,10 +72,13 @@ (defun cj/org-contacts-anniversaries-safe () "Safely call org-contacts-anniversaries with required bindings." (require 'diary-lib) - ;; These need to be dynamically bound for diary functions - (defvar date) - (defvar entry) - (defvar original-date) + ;; `date', `entry', and `original-date' are diary special vars that the + ;; diary functions read dynamically. Declare them special locally; the + ;; suppressed warning is the unprefixed-name lint on these calendar names. + (with-suppressed-warnings ((lexical date entry original-date)) + (defvar date) + (defvar entry) + (defvar original-date)) (let ((date (calendar-current-date)) (entry "") (original-date (calendar-current-date))) @@ -186,9 +219,10 @@ Added: %U" (defun cj/--parse-email-string (name email-string) "Parse EMAIL-STRING and return formatted entries for NAME. -EMAIL-STRING may contain multiple emails separated by commas, semicolons, or spaces. -Returns a list of strings formatted as 'Name <email>'. -Returns nil if EMAIL-STRING is nil or contains only whitespace." +EMAIL-STRING may contain multiple emails separated by commas, +semicolons, or spaces. Returns a list of strings formatted as +\"Name <email>\". Returns nil if EMAIL-STRING is nil or contains only +whitespace." (when (and email-string (string-match-p "[^[:space:]]" email-string)) (let ((emails (split-string email-string "[,;[:space:]]+" t))) (mapcar (lambda (email) diff --git a/modules/org-noter-config.el b/modules/org-noter-config.el index 4e5bd1778..b9b7bbff2 100644 --- a/modules/org-noter-config.el +++ b/modules/org-noter-config.el @@ -39,9 +39,32 @@ ;; Forward declarations (declare-function org-id-uuid "org-id") +(declare-function org-entry-get "org") (declare-function nov-mode "ext:nov") (declare-function pdf-view-mode "ext:pdf-view") +;; pdf-tools fit commands (lazily loaded with pdf-tools) +(declare-function pdf-view-fit-width-to-window "pdf-view") +(declare-function pdf-view-fit-height-to-window "pdf-view") +(declare-function pdf-view-fit-page-to-window "pdf-view") +;; face-remap is built in but loaded lazily +(declare-function face-remap-remove-relative "face-remap") +;; org-noter session/sync/skeleton commands (lazily loaded with org-noter) +(declare-function org-noter--get-notes-window "org-noter") +(declare-function org-noter--get-doc-window "org-noter") +(declare-function org-noter-insert-note "org-noter") +(declare-function org-noter-enable-org-roam-integration "org-noter") +(declare-function org-noter-sync-next-note "org-noter") +(declare-function org-noter-sync-prev-note "org-noter") +(declare-function org-noter-sync-current-note "org-noter") +(declare-function org-noter-create-skeleton "org-noter") +(declare-function org-noter-kill-session "org-noter") +(declare-function org-noter-toggle-notes-window-location "org-noter") (defvar nov-file-name) +;; org-noter package variables assigned at session start / config time +(defvar org-noter-notes-window-location) +(defvar org-noter-use-pdftools-link-location) +(defvar org-noter-use-org-id) +(defvar org-noter-use-unique-org-id) ;;; Configuration Variables (defvar cj/org-noter-notes-directory roam-dir diff --git a/modules/org-roam-config.el b/modules/org-roam-config.el index 40df688d9..c6083c8fb 100644 --- a/modules/org-roam-config.el +++ b/modules/org-roam-config.el @@ -32,6 +32,24 @@ ;; capture template never reaches org-roam-dailies (the foreign-special-var trap). (defvar org-roam-dailies-capture-templates) +;; External variables, declared special so byte-compilation doesn't treat them +;; as free references/assignments. Owned by org and org-roam-dailies. +(defvar org-agenda-timegrid-use-ampm) +(defvar org-roam-dailies-map) +(defvar org-last-state) + +;; External functions, declared so the byte-compiler knows they're defined at +;; runtime by their respective packages. +(declare-function org-roam-node-tags "org-roam") +(declare-function org-roam-node-file "org-roam") +(declare-function org-roam-node-list "org-roam") +(declare-function org-roam-dailies--capture "org-roam-dailies") +(declare-function org-capture-get "org-capture") +(declare-function org-at-heading-p "org") +(declare-function org-heading-components "org") +(declare-function org-copy-subtree "org") +(declare-function org-cut-subtree "org") + ;; ---------------------------------- Org Roam --------------------------------- (defconst cj/--org-roam-dailies-head @@ -76,8 +94,6 @@ FILETAGS and TITLE must sit on separate lines so Org parses the :bind (("C-c n l" . org-roam-buffer-toggle) ("C-c n f" . org-roam-node-find) ("C-c n p" . cj/org-roam-find-node-project) - ("C-c n r" . cj/org-roam-find-node-recipe) - ("C-c n t" . cj/org-roam-find-node-topic) ("C-c n i" . org-roam-node-insert) ("C-c n w" . cj/org-roam-find-node-webclip) :map org-mode-map @@ -191,6 +207,11 @@ created in that subdirectory of `org-roam-directory'." (interactive) (cj/org-roam-find-node "Recipe" "r" (concat roam-dir "templates/recipe.org") "recipes/")) +;; Bound after their defuns (not in the use-package :bind) so the byte-compiler +;; doesn't see both a :bind autoload and the real defun as two definitions. +(keymap-global-set "C-c n r" #'cj/org-roam-find-node-recipe) +(keymap-global-set "C-c n t" #'cj/org-roam-find-node-topic) + ;; ---------------------- Org Capture After Finalize Hook ---------------------- (defun cj/org-roam-add-node-to-agenda-files-finalize-hook () diff --git a/modules/pdf-config.el b/modules/pdf-config.el index ca2312307..233a610d5 100644 --- a/modules/pdf-config.el +++ b/modules/pdf-config.el @@ -14,6 +14,22 @@ ;; ;;; Code: +;; ------------------------------- Declarations -------------------------------- + +(declare-function pdf-tools-install "pdf-tools") +(declare-function pdf-view-midnight-minor-mode "pdf-view") +(declare-function pdf-view-enlarge "pdf-view") +(declare-function pdf-view-shrink "pdf-view") +(declare-function pdf-view-next-page "pdf-view") +(declare-function pdf-view-previous-page "pdf-view") +(declare-function image-next-line "image-mode") +(declare-function image-previous-line "image-mode") +(declare-function image-bob "image-mode") +(declare-function image-eob "image-mode") +(declare-function org-store-link "ol") +(declare-function cj/open-file-with-command "system-utils") +(declare-function cj/org-noter-insert-note-dwim "org-noter-config") + ;; --------------------------------- PDF Tools --------------------------------- (use-package pdf-tools @@ -61,9 +77,9 @@ (define-key pdf-view-mode-map "i" #'cj/org-noter-insert-note-dwim) ;; Page change: C-up/C-down go to top of prev/next page (define-key pdf-view-mode-map (kbd "C-<down>") - (lambda () (interactive) (pdf-view-next-page-command) (image-bob))) + (lambda () (interactive) (pdf-view-next-page) (image-bob))) (define-key pdf-view-mode-map (kbd "C-<up>") - (lambda () (interactive) (pdf-view-previous-page-command) (image-eob)))) + (lambda () (interactive) (pdf-view-previous-page) (image-eob)))) ;; ------------------------------ PDF View Restore ----------------------------- diff --git a/modules/prog-general.el b/modules/prog-general.el index 968032831..99b3cbfab 100644 --- a/modules/prog-general.el +++ b/modules/prog-general.el @@ -59,13 +59,16 @@ (declare-function treesit-auto-add-to-auto-mode-alist "treesit-auto") (declare-function treesit-auto-recipe-lang "treesit-auto") (declare-function highlight-indent-guides-mode "highlight-indent-guides") +(declare-function electric-pair-default-inhibit "elec-pair") +(declare-function yas-reload-all "yasnippet") +(declare-function yas-activate-extra-mode "yasnippet") ;; Forward declarations for treesit-auto variables (defvar treesit-auto-recipe-list) +(defvar electric-pair-inhibit-predicate) ;; Forward declarations for functions defined later in this file (declare-function cj/project-switch-actions "prog-general") -(declare-function cj/deadgrep--initial-term "prog-general") (defun cj/find-project-root-file (regexp) "Return first file in the current Projectile project root matching REGEXP. diff --git a/modules/selection-framework.el b/modules/selection-framework.el index a567e8003..464654a20 100644 --- a/modules/selection-framework.el +++ b/modules/selection-framework.el @@ -26,6 +26,12 @@ ;; ;;; Code: +;; External variables and lazily-loaded functions referenced below. +(defvar xref-show-xrefs-function) +(defvar xref-show-definitions-function) +(declare-function consult-dir-projectile-dirs "consult-dir") +(declare-function prescient-persist-mode "prescient") + ;; ---------------------------------- Vertico ---------------------------------- ;; Vertical completion UI diff --git a/modules/tramp-config.el b/modules/tramp-config.el index 23010b3e4..e3b835f1f 100644 --- a/modules/tramp-config.el +++ b/modules/tramp-config.el @@ -23,6 +23,15 @@ ;;; Code: +;; Silence byte-compiler "assignment to free variable" warnings for vars +;; defined by lazily-loaded packages (tramp, dirtrack, magit). These are +;; only set inside the use-package :config block, after the package loads. +(defvar tramp-copy-size-limit) +(defvar tramp-use-ssh-controlmaster-options) +(defvar tramp-cleanup-idle-time) +(defvar dirtrack-list) +(defvar magit-git-executable) + (use-package tramp :defer .5 :ensure nil ;; built-in diff --git a/modules/user-constants.el b/modules/user-constants.el index dab12dcbe..b392212ed 100644 --- a/modules/user-constants.el +++ b/modules/user-constants.el @@ -154,15 +154,18 @@ Syncthing-synced `org-dir' — see the 2026-06-10 transport migration.") (defvar gcal-file (expand-file-name "data/gcal.org" user-emacs-directory) "The location of the org file containing Google Calendar information. -Stored in .emacs.d/data/ so each machine syncs independently from Google Calendar.") +Stored in .emacs.d/data/ so each machine syncs independently from +Google Calendar.") (defvar pcal-file (expand-file-name "data/pcal.org" user-emacs-directory) "The location of the org file containing Proton Calendar information. -Stored in .emacs.d/data/ so each machine syncs independently from Proton Calendar.") +Stored in .emacs.d/data/ so each machine syncs independently from +Proton Calendar.") (defvar dcal-file (expand-file-name "data/dcal.org" user-emacs-directory) "The location of the org file containing DeepSat Calendar information. -Stored in .emacs.d/data/ so each machine syncs independently from Google Calendar.") +Stored in .emacs.d/data/ so each machine syncs independently from +Google Calendar.") (defvar reference-file (expand-file-name "reference.org" org-dir) "The location of the org file containing reference information.") diff --git a/modules/vc-config.el b/modules/vc-config.el index 654116c59..fcca7e07b 100644 --- a/modules/vc-config.el +++ b/modules/vc-config.el @@ -27,6 +27,27 @@ (require 'user-constants) ;; provides code-dir (require 'keybindings) ;; provides cj/custom-keymap +;; Forward declaration: cj/vc-map is defined later in this file (see +;; `defvar-keymap' below) but referenced earlier in a use-package :bind form. +(defvar cj/vc-map) + +;; External package variables (assigned in :config blocks of lazily-loaded +;; packages, so not loaded at byte-compile time). +(defvar forge-pull-notifications) +(defvar forge-topic-list-limit) + +;; External package functions (from lazily-loaded packages). +(declare-function git-gutter:next-hunk "git-gutter") +(declare-function git-gutter:previous-hunk "git-gutter") +(declare-function git-timemachine--start "git-timemachine") +(declare-function git-timemachine--revisions "git-timemachine") +(declare-function git-timemachine-show-revision "git-timemachine") +(declare-function forge-current-repository "forge") +(declare-function forge-create-issue "forge") + +;; Defined later in this file; referenced earlier in `cj/git-timemachine'. +(declare-function cj/git-timemachine-show-selected-revision "vc-config") + ;; ---------------------------- Magit Configuration ---------------------------- (use-package magit diff --git a/modules/video-audio-recording.el b/modules/video-audio-recording.el index 4c934ef17..1672529f7 100644 --- a/modules/video-audio-recording.el +++ b/modules/video-audio-recording.el @@ -174,9 +174,10 @@ Checks if process is actually alive, not just if variable is set." (defun cj/recording-process-sentinel (process event) "Sentinel for recording processes — handles unexpected exits. PROCESS is the ffmpeg shell process, EVENT describes what happened. -This is called by Emacs when the process changes state (exits, is killed, etc.). -It clears the process variable and updates the modeline so the recording indicator -disappears even if the recording crashes or is killed externally." +This is called by Emacs when the process changes state (exits, is +killed, etc.). It clears the process variable and updates the modeline +so the recording indicator disappears even if the recording crashes or +is killed externally." (when (memq (process-status process) '(exit signal)) (cond ((eq process cj/audio-recording-ffmpeg-process) diff --git a/tests/test-erc-config-connected-servers.el b/tests/test-erc-config-connected-servers.el index 7d4540d68..394367c3e 100644 --- a/tests/test-erc-config-connected-servers.el +++ b/tests/test-erc-config-connected-servers.el @@ -5,8 +5,9 @@ ;; process. The original test compared a buffer's own erc-server-process to the ;; same buffer-local value inside `with-current-buffer', which is always true, so ;; it returned every ERC buffer (channels, queries, dead connections). These -;; tests stub `erc-buffer-list' and the two ERC predicates so the classification -;; is exercised without a real IRC connection. +;; tests stub `erc-buffer-list' and the two ERC predicates +;; (`erc-server-or-unjoined-channel-buffer-p' and `erc-server-process-alive') +;; so the classification is exercised without a real IRC connection. ;;; Code: @@ -25,7 +26,7 @@ returned; a channel buffer and a dead-connection server buffer are excluded." (unwind-protect (cl-letf (((symbol-function 'erc-buffer-list) (lambda (&rest _) (list b-server b-channel b-dead))) - ((symbol-function 'erc-server-buffer-p) + ((symbol-function 'erc-server-or-unjoined-channel-buffer-p) (lambda (&rest _) (memq (current-buffer) (list b-server b-dead)))) ((symbol-function 'erc-server-process-alive) (lambda (&rest _) (eq (current-buffer) b-server)))) @@ -39,7 +40,7 @@ returned; a channel buffer and a dead-connection server buffer are excluded." (unwind-protect (cl-letf (((symbol-function 'erc-buffer-list) (lambda (&rest _) (list b-channel))) - ((symbol-function 'erc-server-buffer-p) (lambda (&rest _) nil)) + ((symbol-function 'erc-server-or-unjoined-channel-buffer-p) (lambda (&rest _) nil)) ((symbol-function 'erc-server-process-alive) (lambda (&rest _) nil))) (should (null (cj/erc-connected-servers)))) (kill-buffer b-channel)))) diff --git a/tests/test-org-config-keymap-ownership.el b/tests/test-org-config-keymap-ownership.el index 729d497cb..81f1ccd46 100644 --- a/tests/test-org-config-keymap-ownership.el +++ b/tests/test-org-config-keymap-ownership.el @@ -60,14 +60,14 @@ at the top level." "Sparse-tree commands sit directly under `C-; O' (flat). Lowercase creates, capital of the same letter cancels: `s' / `S' for match-sparse-tree, `t' / `T' for show-todo-tree. Both -capitals resolve to `org-show-all' -- the user's mental model is +capitals resolve to `org-fold-show-all' -- the user's mental model is \"capital cancels the lowercase I just ran\" without having to remember which letter the cancel actually lives on. `R' is `org-reveal' (no lowercase pair -- `r' is the table-row sub-prefix)." (should (eq (keymap-lookup cj/org-map "s") #'org-match-sparse-tree)) - (should (eq (keymap-lookup cj/org-map "S") #'org-show-all)) + (should (eq (keymap-lookup cj/org-map "S") #'org-fold-show-all)) (should (eq (keymap-lookup cj/org-map "t") #'org-show-todo-tree)) - (should (eq (keymap-lookup cj/org-map "T") #'org-show-all)) + (should (eq (keymap-lookup cj/org-map "T") #'org-fold-show-all)) (should (eq (keymap-lookup cj/org-map "R") #'org-reveal))) (ert-deftest test-org-config-keymap-ownership-regression-no-duplicate-org-keymap () @@ -55,13 +55,6 @@ Tags are additive. For example, a small wrong-behavior fix can be =:bug:quick:=, and a feature that requires internal restructuring can be =:feature:refactor:=. * Emacs Open Work -** DONE [#B] C-<left>/<right>/<down> wrongly enter terminal copy-mode :bug:quick: -CLOSED: [2026-06-24 Wed] -Fixed 2026-06-24: per Craig, only C-<up> enters copy-mode now — all other arrows (C-<down>/<left>/<right> and the M-arrows) were dropped from both the ghostel-mode-map binding and ghostel-keymap-exceptions in modules/term-config.el, so C-<left>/C-<right> reach the shell as readline word-motion again. Also per Craig: C-<up> pressed while already in copy-mode just moves up — cj/term-copy-mode-up checks tmux pane_in_mode (and ghostel--input-mode without tmux) and skips re-entry, which would otherwise reset the cursor. 6 ERT tests rewritten; byte-compile clean; the live daemon was stripped of the stale bindings/exceptions and reloaded (C-<up> bound + an exception, C-<left> forwarded to the pty). Real-terminal scroll is the VERIFY under Manual testing and validation. -** DONE [#B] ai-term wrap-teardown + shutdown functions :feature: -CLOSED: [2026-06-24 Wed] -Done 2026-06-24: added the three headless functions to =modules/ai-term.el= per the rulesets contract — =cj/ai-term-quit= (kill aiv- session + agent buffer + restore layout, idempotent), =cj/ai-term-live-count= (integer gate), =cj/ai-term-shutdown-countdown= (gate re-check → abort-able run-at-time countdown → =cj/ai-term-shutdown-command=, a defcustom). Reused the existing kill/close helpers. 13 ERT tests (live-count parsing, quit kill+idempotency, gate-abort/cancel/tick); byte-compile + validate-modules + launch smoke clean; headless contracts verified live in the daemon (live-count→3, quit no-op returns the session name, countdown aborted with sessions live — no shutdown). The tmux/shutdown side effects and the both-sides end-to-end are a VERIFY under Manual testing and validation. Original task body: -The .emacs.d half of the rulesets wrap-it-up teardown / shutdown feature. Implement three functions in =modules/ai-term.el=, all callable headlessly via =emacsclient -e= (no interactive frame): =cj/ai-term-quit "<project>"= (teardown a project's aiv- tmux session + buffer + geometry restore), =cj/ai-term-live-count= (integer, the safety gate), =cj/ai-term-shutdown-countdown= (run-at-time timer). Craig's 2026-06-23 decisions: non-destructive qualifier = "with summary"/"and summarize"; countdown is a run-at-time timer (not a tty writer); safety gate uses cj/ai-term-live-count. Lands with the rulesets half (workflow + Stop hook already built/pushed). Spec: =inbox/PROCESSED-2026-06-23-2331-from-rulesets-ai-term-teardown-companion.org= (rulesets proposal: docs/design/2026-06-23-wrap-teardown-shutdown-proposal.org). Own focused session. ** TODO [#C] ai-term multi-LLM support — Claude / Codex / ollama :feature: Allow creating an ai-term that launches any of Claude, Codex, or a local LLM via ollama, switchable at session start. From rulesets/Craig via the roam inbox. Spec note: =inbox/PROCESSED-2026-06-23-2123-from-rulesets-ai-term-multi-llm-support-from-craig.org=. ** TODO [#C] theme-studio: package coverage for pearl, wttrin, chime :feature:studio: @@ -74,78 +67,10 @@ Was hardcoded "gray60"; now four customizable faces (branch =feature/themeable-f Four modeline faces shipped (081d76e). =inbox/PROCESSED-2026-06-23-2326-from-chime-chime-added-four-themeable-modeline.org=. ** TODO [#D] Evaluate google-keep Emacs package :quick: From the roam inbox. Look at the google-keep Emacs package — worth adding for in-editor Keep, or does the existing google-keep MCP cover it? Triage / shortlist, not a commitment. -** DONE [#C] README holistic pass -CLOSED: [2026-06-24 Wed] -Holistic pass over README.org, changes approved by Craig: bumped the Emacs floor to 30 (developed on 30.2); corrected the module count (~100 → ~120); added docs/ to the layout and reworded scripts/ (now also theme-studio); added Theme Studio, the ghostel native terminal, and ai-term to Features; added make coverage-summary to the dev targets. From the roam inbox. -** DONE [#B] Theme-driven nerd-icons colors + filetype legend :feature: -CLOSED: [2026-06-24 Wed] -Dropped the runtime nerd-icons tint so icon color is theme-driven, and added a -theme-studio filetype-legend representation over the 34 =nerd-icons-*= color -faces. Spec: -[[file:docs/specs/theme-studio-nerd-icons-colors-spec.org][theme-studio-nerd-icons-colors-spec.org]]. -Three Codex spec-review rounds (3 + 6 + 1 findings) incorporated; findings -[10/10], decisions [6/6]. Ready confirmed 2026-06-24 and implemented in a -no-approvals speedrun as the four dated phases below — full run-tests.sh and -=make test= green, all pushed. Live visual confirmation is a VERIFY under -Manual testing and validation. vNext follow-ups promoted to their own [#D] task. -*** 2026-06-24 Wed @ 05:54:34 -0400 Phase 1 — legend capture shipped -=scripts/theme-studio/build-nerd-icons-legend.el= resolves the 13 v1 rows from the live nerd-icons alists into =nerd-icons-legend.json= (committed); =generate.py='s =load_nerd_icons_legend= validates and falls back to the generic app on absent/malformed/empty/bad-row, with a warning. 7 Python tests. Committed (feat phase 1). -*** 2026-06-24 Wed @ 05:54:34 -0400 Phase 2 — bespoke legend preview shipped -nerd-icons registers as a bespoke app whenever the legend is valid (=add_nerd_icons_app=); =renderNerdIconsPreview= draws each row's glyph in its mapped face color through the shared registry, so recolor repaints live; the 34 faces stay editable. =#nerdiconstest= gate covers the wiring, the dir-row owner, and the recolor-repaint. Committed (feat phase 2). -*** 2026-06-24 Wed @ 05:54:34 -0400 Phase 3 — tint removed, theme drives color -Removed =cj/nerd-icons-tint-color= + =cj/--nerd-icons-color-faces= + =cj/nerd-icons-apply-tint= and both call sites from =nerd-icons-config.el=; the WIP theme already owned the 34 faces (theme-studio auto-discovered them), so color is theme-driven now. Kept =cj/--nerd-icons-color-dir=. Deleted the apply-tint test. validate-modules + launch smoke clean. Committed (feat phase 3). -*** 2026-06-24 Wed @ 05:54:34 -0400 Phase 4 — dir-precedence probe + round-trip -ERT probe locks the dir-precedence decision (prepended =nerd-icons-yellow= is first in the face list, wins over =nerd-icons-completion-dir-face=); =#nerdiconstest= extended with the export/import round-trip over an assigned nerd-icons color and a dir-face-stays-out check. Full run-tests.sh + =make test= green. Committed (test phase 4). Live visual is the VERIFY under Manual testing. ** TODO [#D] Theme Studio nerd-icons vNext follow-ups :feature: Deferred from [[file:docs/specs/theme-studio-nerd-icons-colors-spec.org][theme-studio-nerd-icons-colors-spec.org]]: extend the legend to buffer-mode and command/symbol categories if the file set proves insufficient; add a "reset to nerd-icons native palette" button. -** DONE [#B] ai-term keybinding home :feature: -CLOSED: [2026-06-23 Tue] -Done 2026-06-23 (commit be772bc0): family moved to C-; a (a toggle, s select/launch, n next, k kill), swap also on M-SPC, F9 family retired, jumper's M-SPC binding removed (rehome pending). cj/ai-term-next now opens the picker when no agent is running instead of erroring. Bindings verified live in the daemon; Craig's hands-on check is filed under Manual testing and validation. -Move the ai-term commands off the F9 family. F9 sits somewhere semi-dangerous -to hit, and F8 (org-agenda) is slow to load, which reads as Emacs being -unresponsive. Craig wants three commands on an easy near-home-row chord: open -the ai-term selection menu, switch to the next agent, and kill the current one -(=cj/ai-term=, =cj/ai-term-next=, =cj/ai-term-close=). Explore C-, M-, and C-M- -with SPC. Likely collides with jumper, but ai-term is used far more, so jumper -yields. Archiving gptel this session freed the =C-; a= prefix, so the whole -ai-term family could live under =C-; a= (or another near-home-row key). -Related: the s-F9 detached-agent landing task and the tmux copy-mode binding -task elsewhere in this section. From the roam inbox. -** DONE [#C] Face coloring completion-read icons :quick:solo: -CLOSED: [2026-06-23 Tue] -Answered 2026-06-23 (investigation, no code change). There is no single -"completion icon" face — each icon inherits a per-type =nerd-icons-*= color -face (a .el file icon inherits =nerd-icons-purple=, an M-x command icon -=nerd-icons-blue=, etc.; nerd-icons picks the face per glyph/filetype). What -makes every completion icon render the SAME color here is this config's bulk -tint: =cj/nerd-icons-tint-color= (defcustom in =nerd-icons-config.el=, default -"darkgoldenrod") sets the foreground of all ~33 =nerd-icons-*= color faces via -=cj/nerd-icons-apply-tint=, applied in the =nerd-icons= =:config=. Verified live: -=nerd-icons-icon-for-file "init.el"= -> =:inherit nerd-icons-purple=, and that -face's foreground is "darkgoldenrod". Directory icons additionally get -=nerd-icons-yellow= layered on by =cj/--nerd-icons-color-dir= advice -(=nerd-icons-completion-dir-face= is unset, so it isn't the driver here). -To theme: change =cj/nerd-icons-tint-color= (one color for all icons, then call -=cj/nerd-icons-apply-tint=), or drop the bulk tint and set the individual -=nerd-icons-*= color faces for per-filetype colors. For theme-studio, the knob -to expose is =cj/nerd-icons-tint-color= plus the =nerd-icons-*= face family. -** DONE [#C] Org formatting inside cj comments :feature: -CLOSED: [2026-06-23 Tue] -Done 2026-06-23: mapped the "cj:" src-block language to org-mode via -=org-src-lang-modes= in =org-babel-config.el=. Effect: a cj comment block's -prose now gets org font-lock in place (links, *bold*, lists styled — verified -live, the link inside a block carries the =org-link= face), and =C-c '= opens a -full org-mode buffer to edit it. Approach A from the design walk: non-breaking, -the =cj:= grep marker and the whole cj-processing pipeline are unchanged. The -block stays a src block, so org's parser still treats its body as code — links -are followed from the =C-c '= buffer rather than clicked in place. If that -in-place limitation bites, Approach B (migrate to a =#+begin_cj= special block) -is the documented escalation. -Craig writes free-form prose inside cj comment blocks (=#+begin_src cj: ...=) -and wants org formatting available there. -From the roam inbox. ** TODO [#C] VAMP — extract music-config into a standalone player :feature:refactor: :PROPERTIES: :LAST_REVIEWED: 2026-06-21 @@ -175,15 +100,6 @@ Allow creating an ai-term backed by any of Claude, Codex, or a local LLM via oll :END: Research how completely each of EAT, vterm, and ghostel can be themed — in particular how far theme studio can theme each terminal and what it leaves out. Produce a comparison document, then review it with an eye to whether ai-term should move off ghostel (current) to EAT or vterm. Connects to the chime/emacs-wttrin/pearl face-exposure theme-studio thread. From the roam inbox. -** DONE [#C] term: M-<arrow> enters tmux copy-mode :feature: -CLOSED: [2026-06-24 Wed] -:PROPERTIES: -:LAST_REVIEWED: 2026-06-22 -:END: -Done 2026-06-24: C-<up>/<down>/<left>/<right> and M-<arrow> in =ghostel-mode-map= enter copy-mode and carry their direction in one stroke (=cj/term-copy-mode-up= & friends -> =cj/term-copy-mode-move= -> =cj/term-copy-mode-dwim= then =cj/--term-copy-mode-move-step=). tmux path writes the arrow escape sequence into the pty; non-tmux path moves point in =ghostel-copy-mode=. All 8 keys added to =ghostel-keymap-exceptions= + =ghostel--rebuild-semi-char-keymap= (the gotcha). Ghostel-only. 6 new ERT tests; bindings + exceptions + the dwim sequence verified live in the daemon. The real tmux copy-mode scroll is a VERIFY under Manual testing and validation. - -Folded 2026-06-23 from the roam inbox: Craig also wants C-<up> (control + up arrow) to enter tmux copy-mode and move up in one stroke — i.e. a modified arrow both enters copy-mode and passes the movement (copy-mode + arrow). So the binding set is the modified arrow keys (M-arrow and/or C-arrow), each entering copy-mode and carrying its own direction. - ** TODO [#B] Un-pin ghostel from 0.33.0 once upstream fixes #422/#423 :bug: :PROPERTIES: :LAST_REVIEWED: 2026-06-20 @@ -218,13 +134,6 @@ From the 2026-06 config audit, =modules/transcription-config.el=: - =:210= — =make-process :stderr= with a file PATH creates a BUFFER named like the path (verified by probe); the "Errored. Logs in <file>" notification points at a log without the error text, and the hidden stderr buffer leaks per transcription. Route stderr into the process buffer or write it out in the sentinel. - =:370-374= — video path derives txt/log from the temp mp3's /tmp path; the transcript lands in /tmp and dies on reboot, contradicting the "alongside the source" docstring. Pass the video's path as the output base. -** CANCELLED [#C] page-signal pager account deregistered — re-registration needs your hands -CLOSED: [2026-06-21 Sun] -:PROPERTIES: -:LAST_REVIEWED: 2026-06-12 -:END: -Reported by .emacs.d 2026-06-12 01:01: the dedicated pager number (+15045173983, the Claude Pager Google Voice number on signal-cli) returns "User ... is not registered" on every send — Signal appears to have deregistered it (GV numbers get periodically re-verified). Re-registration requires captcha/SMS, which only you can do. Until then every page-signal call fails; .emacs.d's config-audit page fell back to email. Wrapper lives at claude-templates/bin/page-signal. - ** VERIFY [#C] Remove unused system-power keybindings :refactor:quick:next: :PROPERTIES: :LAST_REVIEWED: 2026-06-20 @@ -232,21 +141,6 @@ Reported by .emacs.d 2026-06-12 01:01: the dedicated pager number (+15045173983, Needs from Craig: the task says "confirm the exact set to keep before unbinding." Under C-; ! the bindings are shutdown (s), reboot (r), restart-Emacs (e), and friends. Tell me which to keep bound and which to drop (the completing-read menu still reaches the rare ones), and I'll unbind the rest. =modules/system-commands.el= binds shutdown (=C-; ! s=), reboot (=C-; ! r=), restart-Emacs (=C-; ! e=) and friends under the =C-; != prefix. Craig rarely uses them and wants the key real-estate back. Drop the bindings he doesn't use; the completing-read menu can still reach the rare ones. Confirm the exact set to keep before unbinding. From the roam inbox. -** DONE [#B] mu4e: cmail can't trash, no account can refile :bug:quick:solo: -CLOSED: [2026-06-24 Wed] -:PROPERTIES: -:LAST_REVIEWED: 2026-06-13 -:END: -=modules/mail-config.el:217-220= — the cmail context (primary account) sets only drafts/sent, so D falls back to default "/trash" which doesn't exist under ~/.mail (=/cmail/Trash= does); and NO context sets =mu4e-refile-folder=, so r targets nonexistent "/archive" everywhere. Accepting mu4e's offer to create the maildir strands mail in a directory mbsync never syncs — messages silently vanish from the server's view. Add =mu4e-trash-folder= to cmail + per-context =mu4e-refile-folder=. From the 2026-06 config audit. -Fixed 2026-06-13: cmail gets =mu4e-trash-folder= "/cmail/Trash"; refile is a per-message function (=cj/mu4e--refile-folder=) instead of a per-context string — mu4e context :vars are sticky, so a per-context refile leaks one account's archive folder into another. cmail → "/cmail/Archive"; gmail/dmail signal a =user-error= rather than move mail into an unsynced phantom folder (Craig chose the fail-safe over syncing [Gmail]/All Mail — the All Mail option means a multi-GB pull + cross-folder duplicates; revisit if local Gmail archiving is wanted). Applies on next mu4e open; pure dispatch helper covered by tests. - -** CANCELLED [#C] Lock screen silently fails — slock is X11-only :bug:quick: -CLOSED: [2026-06-21 Sun] -:PROPERTIES: -:LAST_REVIEWED: 2026-06-13 -:END: -=modules/system-commands.el:105= binds the lockscreen command to =slock=, which can't grab a Wayland session; =cj/system-cmd= launches it detached with output silenced, so C-; ! l does nothing and the screen never locks. Security issue: Craig believes the screen locks when it doesn't. Fix: =hyprlock= (or =swaylock=), ideally resolved per session type via =env-wayland-p= so an X11 fallback survives for other machines. From the 2026-06 config audit. -Fixed 2026-06-13: lockscreen-cmd resolves to =loginctl lock-session= on Wayland (logind Lock → hypridle → hyprlock, the path idle/sleep locking already uses), =slock= on X11; also added the missing =(require 'host-environment)=. Live in the daemon; manual lock test under the Manual testing parent. ** PROJECT [#A] Manual testing and validation Exercised once the phases above land. *** VERIFY ai-term keybindings land on C-; a + M-SPC @@ -730,28 +624,6 @@ Phase 1. Palette anchors + OKLCH shade generation (reusing colormath.js), the RO Phase 2. Initial state from seed() plus seedPkgmap for the non-org packages; all-tier reseed button with a scope-named overwrite warning, resetting non-org to their APPS defaults; regenerate dupre-revised.json. Gate: #selftest PASS; default-on-open equals seed(); artifact round-trip (regenerated dupre-revised.json imports back to the same seeded state); Chrome eyeball. **** TODO Seeding-engine test surface :solo:test: Keep #seedtest, #selftest, the default-on-open check, the dupre-revised round-trip, node --check, and Chrome validation green. -** CANCELLED [#B] AI Open Work -CLOSED: [2026-06-23 Tue] -gptel archived 2026-06-23 to archive/gptel/ (rarely used). The child issues below — ai-rewrite directive plumbing, ai-conversations bugs, the stale-elpa / gptel-magit shadow, model-switch dedup — are all moot against archived code. Kept for reference; detail also in git history. -*** CANCELLED [#B] ai-rewrite: chosen directive never reaches the request :bug:solo: -=modules/ai-rewrite.el:64= — the directive is let-bound around =(call-interactively #'gptel-rewrite)=, but gptel-rewrite is a transient prefix that returns when the menu shows; the send resolves the directive AFTER the binding unwound (verified against ~/code/gptel/gptel-rewrite.el:780-799). The picker's choice is silently dropped — the module's core feature is inert. Set =gptel--rewrite-directive= buffer-locally (restore via =gptel-post-rewrite-functions=) or use a self-removing global hook entry. From the 2026-06 config audit. - -*** CANCELLED [#B] Stale elpa gptel shadows the local fork — likely the gptel-magit root :bug:quick:solo:next: -Needs from Craig: can't be done standalone. I tried deleting elpa/gptel-0.9.8.5 — the fork loaded fine and gptel-magit still worked via use-package autoloads, but package activation then printed "Unable to activate gptel-magit / Required gptel-0.9.8 unavailable" on every startup, so I reverted. To remove the shadow we must also resolve gptel-magit's package dependency: either drop gptel-magit's package dep (load it via load-path like the gptel fork), or repackage the fork into .localrepo as gptel. Tell me which and I'll do it; this pairs with the gptel-magit investigation. -=elpa/gptel-0.9.8.5= is still installed alongside the =~/code/gptel= fork (=ai-config.el:383=); package activation puts the elpa dir + autoloads on load-path, so which copy wins depends on ordering, and a mixed load (fork .el + elpa .elc) produces "impossible" bugs. =gptel-magit= (elpa) declares gptel as a dependency, so IT may be pulling the stale copy — check this first when working the open "[#B] Investigate gptel-magit not working properly" task. Fix: =package-delete= the elpa gptel + remove from .localrepo so the fork is the only copy on disk. From the 2026-06 config audit. - -2026-06-15: tried deleting =elpa/gptel-0.9.8.5= standalone. The fork loaded correctly and gptel-magit still worked via use-package =:commands= autoloads, BUT package activation then printed "Unable to activate package gptel-magit / Required package gptel-0.9.8 unavailable" on every startup and test run (gptel-magit declares gptel as a package dependency that no longer resolves). Reverted. This can't be done standalone — it must be paired with the gptel-magit dependency fix (drop gptel-magit's package dep, or repackage the fork into .localrepo as gptel). Do it together with the gptel-magit investigation task. - -*** CANCELLED [#C] ai-conversations: dead-buffer load, role flattening, non-atomic writes :bug:solo: -From the 2026-06 config audit, =modules/ai-conversations.el=: -- =:324= — load in a fresh session does =get-buffer-create "*AI-Assistant*"= (plain fundamental-mode buffer); =--ensure-ai-buffer= then sees it exists and never calls =(gptel)=. Sending doesn't work, autosave self-cancels (requires gptel-mode). Use =get-buffer= for the check; let ensure create. The browser RET/l path inherits this. -- =:240= — persistence drops gptel's =response= text properties, so a reloaded history replays to the model as ONE user message (model re-reads its own answers as Craig's words). Adopt gptel's native bounds persistence or re-mark on load from the "* Backend:" headings. -- =:248= — =write-region= straight at the target; crash mid-write truncates the only copy of the history (autosave hits this constantly). Temp + rename. -- =:140= — three overlapping autosave mechanisms (after-send advice that fires before the response exists, post-response hook, 60s timer). Keep the hook; drop the advice (and likely the timer). - -*** CANCELLED [#C] Dedup gptel model-switch commands — keep switch-backend or fold into change-model :bug: -=cj/gptel-change-model= (C-; a m) already does backend+model switching and interns correctly, so =cj/gptel-switch-backend= (C-; a B) is arguably redundant now that its crash is fixed. Decision for Craig: keep both, or delete =cj/gptel-switch-backend= plus its C-; a B binding and keep one model-switch command. From the 2026-06 config-audit follow-up. - ** PROJECT [#B] Architecture review follow-up from 2026-05-03 :refactor: High-level pass over =init.el=, =early-init.el=, and all 104 files in @@ -3119,15 +2991,6 @@ one config-wide policy. From the roam inbox. (B) Messenger unification (first application of the policy above). Spec: [[file:docs/specs/messenger-unification-spec.org][messenger-unification-spec.org]] ([[id:4bfc2011-8ffc-4765-8886-91df12141171][by id]], Draft, 2026-06-11; keybinding-alphabet section + smoke-first parity added 2026-06-16). One library (=cj-messenger-lib.el=) gives every messenger the same shape: chat windows rise from the bottom (the signel rule, generalized), C-c C-c confirms, C-c C-k cancels, C-c C-a attaches — dispatched per backend through a registry + minor mode. Signel already conforms (reference backend); telega and slack join in phases 2-3; ERC later. All eight decisions settled 2026-06-11 (cancel closes an idle window; telega's filter-cancel shadow accepted; slack rooms join the bottom rule). Spec held open — Craig has more ideas to fold in before it's marked Ready. -** DONE [#B] agenda sources: roam Projects missing, no existence filtering :bug:solo: -CLOSED: [2026-06-24 Wed] -:PROPERTIES: -:LAST_REVIEWED: 2026-06-20 -:END: -Done 2026-06-24, both parts: (1) per Craig, corrected the docs rather than implementing roam-Project agenda scanning — the commentary + two docstrings claimed org-roam "Project" nodes are agenda sources, but they were never scanned; roam Project/Topic notes are refile targets (org-refile-config.el), not agenda sources. (2) =cj/--org-agenda-base-files= now drops non-existent files and =org-agenda-skip-unavailable-files= is set as a backstop, in the one shared helper so the agenda builders, single-project view, and chime initializer all get it. base-files tests reworked to drive real temp files (+ a drops-missing case); byte-compile clean; live-verified (skip var t, base-files returns only existing). From the 2026-06 config audit, =modules/org-agenda-config.el=: -- =:182-191= — commentary and docstrings promise org-roam nodes tagged "Project" as agenda sources, but =cj/--org-agenda-scan-files= never scans them, and files added by the roam finalize-hook are wiped on the next =cj/build-org-agenda-list= cache rebuild (≤1h). Add a roam Project pass (mirror =org-refile-config.el:101-109=) or correct the docs. -- =:186,456= — agenda file list built unconditionally (inbox/calendars may not exist on a fresh machine) and =org-agenda-skip-unavailable-files= is unset — the exact interactive-prompt class that once hung the chime daemon. Filter with =file-exists-p= + set the var as backstop. - ** TODO [#B] Auto-dim: org headings, links, and tags do not dim in unfocused windows :bug: :PROPERTIES: :LAST_REVIEWED: 2026-06-20 @@ -3154,14 +3017,6 @@ Ask: Reference values -- modus-vivendi: refine-changed bg #4a4a00 fg #efef80, changed bg #363300 fg #efef80. modus-operandi: refine-changed bg #fac090 fg #553d00, changed bg #ffdfa9 fg #553d00. Side-by-side legibility render: [[file:assets/2026-06-07-dupre-diff-face-legibility-compare.png][assets/2026-06-07-dupre-diff-face-legibility-compare.png]]. -** DONE [#B] F7 diff-aware coverage classifies every changed file "not tracked" :bug:solo: -CLOSED: [2026-06-22 Mon] -:PROPERTIES: -:LAST_REVIEWED: 2026-06-20 -:END: -Fixed 2026-06-22: simplecov keys are absolute, git-diff keys repo-relative, so the exact-key intersect never matched. Added =cj/--coverage-relativize-keys= and normalize both tables to repo-relative in =cj/--coverage-read-and-display= before the intersect; intersect unchanged. New =test-coverage-core--relativize-keys.el= (5 unit + 1 integration through the real parsers). Full suite green. -=modules/coverage-core.el:252= — =cj/--coverage-intersect= joins covered×changed by exact string key, but simplecov.json keys are ABSOLUTE paths while the git-diff parser returns repo-RELATIVE ones — zero matches ever, so working-tree/staged/branch scopes report ":tracked nil" for everything and F7's main feature is inert (whole-project scope works, same-source keys). Unit tests hand-build matching keys so they pass; add one integration test feeding a real undercover report + real diff. Normalize both sides to repo-relative. From the 2026-06 config audit. - ** TODO [#C] Migrate tests off mocking primitives (native-comp robustness) :test:refactor: :PROPERTIES: :LAST_REVIEWED: 2026-06-21 @@ -3678,17 +3533,6 @@ Tie this into the existing coverage work: - Tests cover adapter detection, command building, scope resolution, result storage, and key interactive paths. -** DONE [#B] jumper: register collisions and dead-marker errors :bug:solo: -CLOSED: [2026-06-22 Mon] -:PROPERTIES: -:LAST_REVIEWED: 2026-06-13 -:END: -Fixed 2026-06-22: (1) store now allocates the first unused register char in the live slice (=jumper--first-free-register=) instead of by next-index, and removal clears the freed register, so a store after a removal no longer overwrites a surviving slot's marker; (2) =jumper--with-marker-at= guards =(buffer-live-p (marker-buffer marker))= so killed-buffer entries are skipped instead of signaling wrong-type errors; (3) the single-location toggle jumps back to the last-location register when set (returns =jumped-back=). New =test-jumper--register-hygiene.el= (8 tests); all 42 jumper tests green. Pre-existing unused-lexical =i= warning in =jumper--location-exists-p= left alone (separate nit). -Two related defects from the 2026-06 config audit: -- =modules/jumper.el:155= — removal shifts the vector without renumbering registers, so a later store allocates a register still held by a surviving location and silently overwrites it. Allocate the first free register char in the live slice; =set-register nil= on removal so freed markers don't pin buffers. -- =modules/jumper.el:117,132= — guards check =(markerp marker)= but not =(buffer-live-p (marker-buffer marker))=; after killing a buffer holding a location, M-SPC SPC and M-SPC j signal wrong-type errors. Treat dead entries as skippable/removable. -Also =jumper.el:178= — the promised single-location toggle never toggles back ('already-there branch should =jump-to-register= z when set). - ** TODO [#B] Keymap consolidation — resolve decisions, run Phase 1-2 :feature:refactor:solo: :PROPERTIES: :LAST_REVIEWED: 2026-06-13 @@ -3742,12 +3586,6 @@ F2 is the universal preview key. Currently bound only in markdown-mode (markdown Keep the binding mode-local so F2 stays available as a global candidate where no preview makes sense. -** DONE [#C] face-diagnostic: face-name buttons + header allowlist :feature:quick:solo: -CLOSED: [2026-06-24 Wed] -:PROPERTIES: -:LAST_REVIEWED: 2026-06-21 -:END: -Done 2026-06-24: (a) =cj/--face-diag-face-button= renders each real face name in the report as a =buttonize='d button that runs =describe-face= on it (carries the face as button-data); anonymous specs and non-faces stay plain. Routed through the stack, overlay, remap, and provenance render sites. (b) Added =face-diagnostic= to =test-init-header--classified-modules= (it's required in init.el and already carries the header contract). 5 new ERT tests; button text properties confirmed live in a rendered *Face Diagnosis* buffer. Click/RET sign-off is a VERIFY under Manual testing and validation. Spec: [[id:98f065cf-8bd5-46a0-ac24-da94d66855ad][face-font-diagnostic-popup-spec-implemented.org]]. ** TODO [#C] Gold text in auto-dimmed buffers :bug: :PROPERTIES: :LAST_REVIEWED: 2026-06-21 @@ -3765,13 +3603,6 @@ From the 2026-06-11 brainstorm. Goal: keep [[file:~/sync/org/contacts.org][conta :END: From the 2026-06-11 messenger-unification brainstorm. Google Voice has no official API; the viable routes ride the Matrix bridge ecosystem's reverse engineering (mautrix-gvoice). Research pass to establish the 2026 state of play: (1) is mautrix-gvoice healthy and what does its auth flow look like now; (2) any better-maintained alternative (CLI/daemon) for the signel-pattern architecture (external daemon + JSON-RPC + thin Emacs chat client); (3) does call initiation (ring-linked-phone-then-connect, Emacs as dialer) survive in the current protocol — two-way audio in Emacs is out of scope (WebRTC); (4) ToS/account-flag risk assessment for Craig's account. Output: a recommendation doc in docs/design/ naming the architecture (signel-pattern daemon vs Matrix bridge + ement.el) or a no-go with reasons. If go, GV becomes a registered backend under the messenger-unification convention (see the [#B] task below). -** DONE [#C] latexmk workflow never activates (two breaks) :bug:quick:solo: -CLOSED: [2026-06-24 Wed] -:PROPERTIES: -:LAST_REVIEWED: 2026-06-21 -:END: -Done 2026-06-24: changed the :hook key from =TeX-mode-hook= to =TeX-mode= (use-package appends "-hook" only to non-"-mode" symbols, so this now registers on the real =TeX-mode-hook= instead of the unbound =TeX-mode-hook-hook=), and auctex-latexmk from =:defer t= to =:after tex= so =auctex-latexmk-setup= runs when AUCTeX loads. Confirmed both breaks via macroexpand (the dump showed =add-hook 'TeX-mode-hook-hook= before, =TeX-mode-hook= after). 2 new regression ERT tests; live-verified in a real .tex buffer: =TeX-command-default= is "latexmk" and "LatexMk" is in =TeX-command-list=. Actual C-c C-c compile is a VERIFY under Manual testing and validation. From the 2026-06 config audit. - ** TODO [#C] Org-noter custom workflow — fix and finish :feature:bug: :PROPERTIES: :LAST_REVIEWED: 2026-06-21 @@ -3878,36 +3709,6 @@ These may override useful defaults - review and pick better bindings: :END: Display slack.el message and thread buffers in a dedicated popup window (side or bottom) and reuse that one window instead of spawning a new window per buffer. Likely a =display-buffer-alist= rule (or popper integration) in =modules/slack-config.el=. -** CANCELLED [#C] the preview splits an already split window into 3 temporarily. :bug: -CLOSED: [2026-06-21 Sun] -looks strange. potentially problematic for ai-terms. - -** CANCELLED [#C] TRAMP/dirvish "?" for remote dates — verify the fix per host :bug: -CLOSED: [2026-06-21 Sun] -:PROPERTIES: -:LAST_REVIEWED: 2026-06-02 -:END: - -Root cause is traced (see the dated investigation entry below). What's left needs a live remote: open each remote host in dirvish and run the three diagnostic evals to find which gate is closed, then close it. - -Diagnostics (run with point in a remote dirvish buffer): -- =M-: (dirvish-prop :remote-async)= — nil means =tramp-direct-async-process-p= is failing for this method/host, so dirvish's remote attribute fetch never runs. -- =M-: (dirvish-prop :gnuls)= — nil means the remote has no GNU =ls= (the =ls --version= probe failed), so the parser gate stays shut. Likely on truenas (FreeBSD). -- =M-: (tramp-direct-async-process-p)= — confirms whether direct-async is actually active for the connection. - -Likely fixes, by which gate is closed: -- =:gnuls= nil → install GNU coreutils on the remote (FreeBSD: =pkg install coreutils=) and make =ls= resolve to GNU on the TRAMP path, or accept "?" on that host. - - - Constraint: nothing gets installed on the remote host, so the =:gnuls= gate is resolved by accepting "?" on that host rather than installing coreutils. -- =:remote-async= nil → the scp/sshx method isn't advertising direct-async; switch to a method that supports it or check =tramp-direct-async-process= is taking effect for that protocol. - -Files involved: =modules/tramp-config.el=, =modules/dirvish-config.el=. - -*** 2026-05-22 Fri @ 20:24:44 -0500 Traced the root cause through dirvish source -Remote dates/sizes don't come from the dired =ls= listing or =dired-listing-switches=. They come from =dirvish-data-for-dir= (=dirvish-tramp.el:95=), which runs =ls -1lahi= on the remote and parses the columns into the attribute cache. That method only fires when both =(dirvish-prop :remote-async)= is a number and =(dirvish-prop :gnuls)= is a string. When either gate is shut, dirvish falls back to its default, which deliberately skips =(file-attributes f-name)= for remote files (=dirvish.el:904=, a perf guard) — leaving attrs nil, so the file-size and file-time widgets render "?" (=dirvish-widgets.el:216,247=). - -That explains why every prior fix missed: dired-listing-switches feed a different code path entirely, and disabling =tramp-direct-async-process= shuts the =:remote-async= gate, which is the one path that populates remote attributes — exactly backwards. The config already enables direct-async for ssh/sshx (=tramp-config.el:79-88=), so the remaining closed gate is per-host: =:gnuls= (no GNU ls on FreeBSD-based truenas) or direct-async not taking effect for the method. Could not verify on a live remote from the work session — handed the per-host diagnostics up into the task body. - ** TODO [#D] Dashboard over-scroll: pin last line to window bottom :bug: :PROPERTIES: :LAST_REVIEWED: 2026-05-22 @@ -9033,3 +8834,192 @@ Re-reviewed [[id:b54c94a0-d762-4b41-afd7-cf5593ce6675][docs/specs/vterm-to-ghost ** DONE [#A] erc-yank silently publishes >5-line pastes as public gists :bug:quick:solo: CLOSED: [2026-06-20 Sat] Dropped erc-yank 2026-06-20 (Craig's call: drop, not harden). The package turned a >5-line paste into a PUBLIC gist (=gist -P=, the clipboard-paste flag, no =--private=) behind a single y-or-n-p, with no executable-find guard for =gist=. It also gisted the system clipboard rather than the kill-ring text being yanked. No replacement binding needed: =erc-mode-map= defines no C-y of its own, so removing the package lets C-y fall through to the ordinary global =yank=. Verified live: effective C-y in an ERC buffer = =yank=. (Audit's "no confirmation" was slightly off — the package did prompt — but public-by-default + one-keystroke confirm + no guard made dropping it the clean fix.) +** DONE [#B] C-<left>/<right>/<down> wrongly enter terminal copy-mode :bug:quick: +CLOSED: [2026-06-24 Wed] +Fixed 2026-06-24: per Craig, only C-<up> enters copy-mode now — all other arrows (C-<down>/<left>/<right> and the M-arrows) were dropped from both the ghostel-mode-map binding and ghostel-keymap-exceptions in modules/term-config.el, so C-<left>/C-<right> reach the shell as readline word-motion again. Also per Craig: C-<up> pressed while already in copy-mode just moves up — cj/term-copy-mode-up checks tmux pane_in_mode (and ghostel--input-mode without tmux) and skips re-entry, which would otherwise reset the cursor. 6 ERT tests rewritten; byte-compile clean; the live daemon was stripped of the stale bindings/exceptions and reloaded (C-<up> bound + an exception, C-<left> forwarded to the pty). Real-terminal scroll is the VERIFY under Manual testing and validation. +** DONE [#B] ai-term wrap-teardown + shutdown functions :feature: +CLOSED: [2026-06-24 Wed] +Done 2026-06-24: added the three headless functions to =modules/ai-term.el= per the rulesets contract — =cj/ai-term-quit= (kill aiv- session + agent buffer + restore layout, idempotent), =cj/ai-term-live-count= (integer gate), =cj/ai-term-shutdown-countdown= (gate re-check → abort-able run-at-time countdown → =cj/ai-term-shutdown-command=, a defcustom). Reused the existing kill/close helpers. 13 ERT tests (live-count parsing, quit kill+idempotency, gate-abort/cancel/tick); byte-compile + validate-modules + launch smoke clean; headless contracts verified live in the daemon (live-count→3, quit no-op returns the session name, countdown aborted with sessions live — no shutdown). The tmux/shutdown side effects and the both-sides end-to-end are a VERIFY under Manual testing and validation. Original task body: +The .emacs.d half of the rulesets wrap-it-up teardown / shutdown feature. Implement three functions in =modules/ai-term.el=, all callable headlessly via =emacsclient -e= (no interactive frame): =cj/ai-term-quit "<project>"= (teardown a project's aiv- tmux session + buffer + geometry restore), =cj/ai-term-live-count= (integer, the safety gate), =cj/ai-term-shutdown-countdown= (run-at-time timer). Craig's 2026-06-23 decisions: non-destructive qualifier = "with summary"/"and summarize"; countdown is a run-at-time timer (not a tty writer); safety gate uses cj/ai-term-live-count. Lands with the rulesets half (workflow + Stop hook already built/pushed). Spec: =inbox/PROCESSED-2026-06-23-2331-from-rulesets-ai-term-teardown-companion.org= (rulesets proposal: docs/design/2026-06-23-wrap-teardown-shutdown-proposal.org). Own focused session. +** DONE [#C] README holistic pass +CLOSED: [2026-06-24 Wed] +Holistic pass over README.org, changes approved by Craig: bumped the Emacs floor to 30 (developed on 30.2); corrected the module count (~100 → ~120); added docs/ to the layout and reworded scripts/ (now also theme-studio); added Theme Studio, the ghostel native terminal, and ai-term to Features; added make coverage-summary to the dev targets. From the roam inbox. +** DONE [#B] Theme-driven nerd-icons colors + filetype legend :feature: +CLOSED: [2026-06-24 Wed] +Dropped the runtime nerd-icons tint so icon color is theme-driven, and added a +theme-studio filetype-legend representation over the 34 =nerd-icons-*= color +faces. Spec: +[[file:docs/specs/theme-studio-nerd-icons-colors-spec.org][theme-studio-nerd-icons-colors-spec.org]]. +Three Codex spec-review rounds (3 + 6 + 1 findings) incorporated; findings +[10/10], decisions [6/6]. Ready confirmed 2026-06-24 and implemented in a +no-approvals speedrun as the four dated phases below — full run-tests.sh and +=make test= green, all pushed. Live visual confirmation is a VERIFY under +Manual testing and validation. vNext follow-ups promoted to their own [#D] task. +*** 2026-06-24 Wed @ 05:54:34 -0400 Phase 1 — legend capture shipped +=scripts/theme-studio/build-nerd-icons-legend.el= resolves the 13 v1 rows from the live nerd-icons alists into =nerd-icons-legend.json= (committed); =generate.py='s =load_nerd_icons_legend= validates and falls back to the generic app on absent/malformed/empty/bad-row, with a warning. 7 Python tests. Committed (feat phase 1). +*** 2026-06-24 Wed @ 05:54:34 -0400 Phase 2 — bespoke legend preview shipped +nerd-icons registers as a bespoke app whenever the legend is valid (=add_nerd_icons_app=); =renderNerdIconsPreview= draws each row's glyph in its mapped face color through the shared registry, so recolor repaints live; the 34 faces stay editable. =#nerdiconstest= gate covers the wiring, the dir-row owner, and the recolor-repaint. Committed (feat phase 2). +*** 2026-06-24 Wed @ 05:54:34 -0400 Phase 3 — tint removed, theme drives color +Removed =cj/nerd-icons-tint-color= + =cj/--nerd-icons-color-faces= + =cj/nerd-icons-apply-tint= and both call sites from =nerd-icons-config.el=; the WIP theme already owned the 34 faces (theme-studio auto-discovered them), so color is theme-driven now. Kept =cj/--nerd-icons-color-dir=. Deleted the apply-tint test. validate-modules + launch smoke clean. Committed (feat phase 3). +*** 2026-06-24 Wed @ 05:54:34 -0400 Phase 4 — dir-precedence probe + round-trip +ERT probe locks the dir-precedence decision (prepended =nerd-icons-yellow= is first in the face list, wins over =nerd-icons-completion-dir-face=); =#nerdiconstest= extended with the export/import round-trip over an assigned nerd-icons color and a dir-face-stays-out check. Full run-tests.sh + =make test= green. Committed (test phase 4). Live visual is the VERIFY under Manual testing. +** DONE [#B] ai-term keybinding home :feature: +CLOSED: [2026-06-23 Tue] +Done 2026-06-23 (commit be772bc0): family moved to C-; a (a toggle, s select/launch, n next, k kill), swap also on M-SPC, F9 family retired, jumper's M-SPC binding removed (rehome pending). cj/ai-term-next now opens the picker when no agent is running instead of erroring. Bindings verified live in the daemon; Craig's hands-on check is filed under Manual testing and validation. +Move the ai-term commands off the F9 family. F9 sits somewhere semi-dangerous +to hit, and F8 (org-agenda) is slow to load, which reads as Emacs being +unresponsive. Craig wants three commands on an easy near-home-row chord: open +the ai-term selection menu, switch to the next agent, and kill the current one +(=cj/ai-term=, =cj/ai-term-next=, =cj/ai-term-close=). Explore C-, M-, and C-M- +with SPC. Likely collides with jumper, but ai-term is used far more, so jumper +yields. Archiving gptel this session freed the =C-; a= prefix, so the whole +ai-term family could live under =C-; a= (or another near-home-row key). +Related: the s-F9 detached-agent landing task and the tmux copy-mode binding +task elsewhere in this section. From the roam inbox. +** DONE [#C] Face coloring completion-read icons :quick:solo: +CLOSED: [2026-06-23 Tue] +Answered 2026-06-23 (investigation, no code change). There is no single +"completion icon" face — each icon inherits a per-type =nerd-icons-*= color +face (a .el file icon inherits =nerd-icons-purple=, an M-x command icon +=nerd-icons-blue=, etc.; nerd-icons picks the face per glyph/filetype). What +makes every completion icon render the SAME color here is this config's bulk +tint: =cj/nerd-icons-tint-color= (defcustom in =nerd-icons-config.el=, default +"darkgoldenrod") sets the foreground of all ~33 =nerd-icons-*= color faces via +=cj/nerd-icons-apply-tint=, applied in the =nerd-icons= =:config=. Verified live: +=nerd-icons-icon-for-file "init.el"= -> =:inherit nerd-icons-purple=, and that +face's foreground is "darkgoldenrod". Directory icons additionally get +=nerd-icons-yellow= layered on by =cj/--nerd-icons-color-dir= advice +(=nerd-icons-completion-dir-face= is unset, so it isn't the driver here). +To theme: change =cj/nerd-icons-tint-color= (one color for all icons, then call +=cj/nerd-icons-apply-tint=), or drop the bulk tint and set the individual +=nerd-icons-*= color faces for per-filetype colors. For theme-studio, the knob +to expose is =cj/nerd-icons-tint-color= plus the =nerd-icons-*= face family. +** DONE [#C] Org formatting inside cj comments :feature: +CLOSED: [2026-06-23 Tue] +Done 2026-06-23: mapped the "cj:" src-block language to org-mode via +=org-src-lang-modes= in =org-babel-config.el=. Effect: a cj comment block's +prose now gets org font-lock in place (links, *bold*, lists styled — verified +live, the link inside a block carries the =org-link= face), and =C-c '= opens a +full org-mode buffer to edit it. Approach A from the design walk: non-breaking, +the =cj:= grep marker and the whole cj-processing pipeline are unchanged. The +block stays a src block, so org's parser still treats its body as code — links +are followed from the =C-c '= buffer rather than clicked in place. If that +in-place limitation bites, Approach B (migrate to a =#+begin_cj= special block) +is the documented escalation. +Craig writes free-form prose inside cj comment blocks (=#+begin_src cj: ...=) +and wants org formatting available there. +From the roam inbox. +** DONE [#C] term: M-<arrow> enters tmux copy-mode :feature: +CLOSED: [2026-06-24 Wed] +:PROPERTIES: +:LAST_REVIEWED: 2026-06-22 +:END: +Done 2026-06-24: C-<up>/<down>/<left>/<right> and M-<arrow> in =ghostel-mode-map= enter copy-mode and carry their direction in one stroke (=cj/term-copy-mode-up= & friends -> =cj/term-copy-mode-move= -> =cj/term-copy-mode-dwim= then =cj/--term-copy-mode-move-step=). tmux path writes the arrow escape sequence into the pty; non-tmux path moves point in =ghostel-copy-mode=. All 8 keys added to =ghostel-keymap-exceptions= + =ghostel--rebuild-semi-char-keymap= (the gotcha). Ghostel-only. 6 new ERT tests; bindings + exceptions + the dwim sequence verified live in the daemon. The real tmux copy-mode scroll is a VERIFY under Manual testing and validation. + +Folded 2026-06-23 from the roam inbox: Craig also wants C-<up> (control + up arrow) to enter tmux copy-mode and move up in one stroke — i.e. a modified arrow both enters copy-mode and passes the movement (copy-mode + arrow). So the binding set is the modified arrow keys (M-arrow and/or C-arrow), each entering copy-mode and carrying its own direction. +** CANCELLED [#C] page-signal pager account deregistered — re-registration needs your hands +CLOSED: [2026-06-21 Sun] +:PROPERTIES: +:LAST_REVIEWED: 2026-06-12 +:END: +Reported by .emacs.d 2026-06-12 01:01: the dedicated pager number (+15045173983, the Claude Pager Google Voice number on signal-cli) returns "User ... is not registered" on every send — Signal appears to have deregistered it (GV numbers get periodically re-verified). Re-registration requires captcha/SMS, which only you can do. Until then every page-signal call fails; .emacs.d's config-audit page fell back to email. Wrapper lives at claude-templates/bin/page-signal. +** DONE [#B] mu4e: cmail can't trash, no account can refile :bug:quick:solo: +CLOSED: [2026-06-24 Wed] +:PROPERTIES: +:LAST_REVIEWED: 2026-06-13 +:END: +=modules/mail-config.el:217-220= — the cmail context (primary account) sets only drafts/sent, so D falls back to default "/trash" which doesn't exist under ~/.mail (=/cmail/Trash= does); and NO context sets =mu4e-refile-folder=, so r targets nonexistent "/archive" everywhere. Accepting mu4e's offer to create the maildir strands mail in a directory mbsync never syncs — messages silently vanish from the server's view. Add =mu4e-trash-folder= to cmail + per-context =mu4e-refile-folder=. From the 2026-06 config audit. +Fixed 2026-06-13: cmail gets =mu4e-trash-folder= "/cmail/Trash"; refile is a per-message function (=cj/mu4e--refile-folder=) instead of a per-context string — mu4e context :vars are sticky, so a per-context refile leaks one account's archive folder into another. cmail → "/cmail/Archive"; gmail/dmail signal a =user-error= rather than move mail into an unsynced phantom folder (Craig chose the fail-safe over syncing [Gmail]/All Mail — the All Mail option means a multi-GB pull + cross-folder duplicates; revisit if local Gmail archiving is wanted). Applies on next mu4e open; pure dispatch helper covered by tests. +** CANCELLED [#C] Lock screen silently fails — slock is X11-only :bug:quick: +CLOSED: [2026-06-21 Sun] +:PROPERTIES: +:LAST_REVIEWED: 2026-06-13 +:END: +=modules/system-commands.el:105= binds the lockscreen command to =slock=, which can't grab a Wayland session; =cj/system-cmd= launches it detached with output silenced, so C-; ! l does nothing and the screen never locks. Security issue: Craig believes the screen locks when it doesn't. Fix: =hyprlock= (or =swaylock=), ideally resolved per session type via =env-wayland-p= so an X11 fallback survives for other machines. From the 2026-06 config audit. +Fixed 2026-06-13: lockscreen-cmd resolves to =loginctl lock-session= on Wayland (logind Lock → hypridle → hyprlock, the path idle/sleep locking already uses), =slock= on X11; also added the missing =(require 'host-environment)=. Live in the daemon; manual lock test under the Manual testing parent. +** CANCELLED [#B] AI Open Work +CLOSED: [2026-06-23 Tue] +gptel archived 2026-06-23 to archive/gptel/ (rarely used). The child issues below — ai-rewrite directive plumbing, ai-conversations bugs, the stale-elpa / gptel-magit shadow, model-switch dedup — are all moot against archived code. Kept for reference; detail also in git history. +*** CANCELLED [#B] ai-rewrite: chosen directive never reaches the request :bug:solo: +=modules/ai-rewrite.el:64= — the directive is let-bound around =(call-interactively #'gptel-rewrite)=, but gptel-rewrite is a transient prefix that returns when the menu shows; the send resolves the directive AFTER the binding unwound (verified against ~/code/gptel/gptel-rewrite.el:780-799). The picker's choice is silently dropped — the module's core feature is inert. Set =gptel--rewrite-directive= buffer-locally (restore via =gptel-post-rewrite-functions=) or use a self-removing global hook entry. From the 2026-06 config audit. + +*** CANCELLED [#B] Stale elpa gptel shadows the local fork — likely the gptel-magit root :bug:quick:solo:next: +Needs from Craig: can't be done standalone. I tried deleting elpa/gptel-0.9.8.5 — the fork loaded fine and gptel-magit still worked via use-package autoloads, but package activation then printed "Unable to activate gptel-magit / Required gptel-0.9.8 unavailable" on every startup, so I reverted. To remove the shadow we must also resolve gptel-magit's package dependency: either drop gptel-magit's package dep (load it via load-path like the gptel fork), or repackage the fork into .localrepo as gptel. Tell me which and I'll do it; this pairs with the gptel-magit investigation. +=elpa/gptel-0.9.8.5= is still installed alongside the =~/code/gptel= fork (=ai-config.el:383=); package activation puts the elpa dir + autoloads on load-path, so which copy wins depends on ordering, and a mixed load (fork .el + elpa .elc) produces "impossible" bugs. =gptel-magit= (elpa) declares gptel as a dependency, so IT may be pulling the stale copy — check this first when working the open "[#B] Investigate gptel-magit not working properly" task. Fix: =package-delete= the elpa gptel + remove from .localrepo so the fork is the only copy on disk. From the 2026-06 config audit. + +2026-06-15: tried deleting =elpa/gptel-0.9.8.5= standalone. The fork loaded correctly and gptel-magit still worked via use-package =:commands= autoloads, BUT package activation then printed "Unable to activate package gptel-magit / Required package gptel-0.9.8 unavailable" on every startup and test run (gptel-magit declares gptel as a package dependency that no longer resolves). Reverted. This can't be done standalone — it must be paired with the gptel-magit dependency fix (drop gptel-magit's package dep, or repackage the fork into .localrepo as gptel). Do it together with the gptel-magit investigation task. + +*** CANCELLED [#B] ai-conversations: dead-buffer load, role flattening, non-atomic writes :bug:solo: +From the 2026-06 config audit, =modules/ai-conversations.el=: +- =:324= — load in a fresh session does =get-buffer-create "*AI-Assistant*"= (plain fundamental-mode buffer); =--ensure-ai-buffer= then sees it exists and never calls =(gptel)=. Sending doesn't work, autosave self-cancels (requires gptel-mode). Use =get-buffer= for the check; let ensure create. The browser RET/l path inherits this. +- =:240= — persistence drops gptel's =response= text properties, so a reloaded history replays to the model as ONE user message (model re-reads its own answers as Craig's words). Adopt gptel's native bounds persistence or re-mark on load from the "* Backend:" headings. +- =:248= — =write-region= straight at the target; crash mid-write truncates the only copy of the history (autosave hits this constantly). Temp + rename. +- =:140= — three overlapping autosave mechanisms (after-send advice that fires before the response exists, post-response hook, 60s timer). Keep the hook; drop the advice (and likely the timer). + +*** CANCELLED [#B] Dedup gptel model-switch commands — keep switch-backend or fold into change-model :bug: +=cj/gptel-change-model= (C-; a m) already does backend+model switching and interns correctly, so =cj/gptel-switch-backend= (C-; a B) is arguably redundant now that its crash is fixed. Decision for Craig: keep both, or delete =cj/gptel-switch-backend= plus its C-; a B binding and keep one model-switch command. From the 2026-06 config-audit follow-up. +** DONE [#B] agenda sources: roam Projects missing, no existence filtering :bug:solo: +CLOSED: [2026-06-24 Wed] +:PROPERTIES: +:LAST_REVIEWED: 2026-06-20 +:END: +Done 2026-06-24, both parts: (1) per Craig, corrected the docs rather than implementing roam-Project agenda scanning — the commentary + two docstrings claimed org-roam "Project" nodes are agenda sources, but they were never scanned; roam Project/Topic notes are refile targets (org-refile-config.el), not agenda sources. (2) =cj/--org-agenda-base-files= now drops non-existent files and =org-agenda-skip-unavailable-files= is set as a backstop, in the one shared helper so the agenda builders, single-project view, and chime initializer all get it. base-files tests reworked to drive real temp files (+ a drops-missing case); byte-compile clean; live-verified (skip var t, base-files returns only existing). From the 2026-06 config audit, =modules/org-agenda-config.el=: +- =:182-191= — commentary and docstrings promise org-roam nodes tagged "Project" as agenda sources, but =cj/--org-agenda-scan-files= never scans them, and files added by the roam finalize-hook are wiped on the next =cj/build-org-agenda-list= cache rebuild (≤1h). Add a roam Project pass (mirror =org-refile-config.el:101-109=) or correct the docs. +- =:186,456= — agenda file list built unconditionally (inbox/calendars may not exist on a fresh machine) and =org-agenda-skip-unavailable-files= is unset — the exact interactive-prompt class that once hung the chime daemon. Filter with =file-exists-p= + set the var as backstop. +** DONE [#B] F7 diff-aware coverage classifies every changed file "not tracked" :bug:solo: +CLOSED: [2026-06-22 Mon] +:PROPERTIES: +:LAST_REVIEWED: 2026-06-20 +:END: +Fixed 2026-06-22: simplecov keys are absolute, git-diff keys repo-relative, so the exact-key intersect never matched. Added =cj/--coverage-relativize-keys= and normalize both tables to repo-relative in =cj/--coverage-read-and-display= before the intersect; intersect unchanged. New =test-coverage-core--relativize-keys.el= (5 unit + 1 integration through the real parsers). Full suite green. +=modules/coverage-core.el:252= — =cj/--coverage-intersect= joins covered×changed by exact string key, but simplecov.json keys are ABSOLUTE paths while the git-diff parser returns repo-RELATIVE ones — zero matches ever, so working-tree/staged/branch scopes report ":tracked nil" for everything and F7's main feature is inert (whole-project scope works, same-source keys). Unit tests hand-build matching keys so they pass; add one integration test feeding a real undercover report + real diff. Normalize both sides to repo-relative. From the 2026-06 config audit. +** DONE [#B] jumper: register collisions and dead-marker errors :bug:solo: +CLOSED: [2026-06-22 Mon] +:PROPERTIES: +:LAST_REVIEWED: 2026-06-13 +:END: +Fixed 2026-06-22: (1) store now allocates the first unused register char in the live slice (=jumper--first-free-register=) instead of by next-index, and removal clears the freed register, so a store after a removal no longer overwrites a surviving slot's marker; (2) =jumper--with-marker-at= guards =(buffer-live-p (marker-buffer marker))= so killed-buffer entries are skipped instead of signaling wrong-type errors; (3) the single-location toggle jumps back to the last-location register when set (returns =jumped-back=). New =test-jumper--register-hygiene.el= (8 tests); all 42 jumper tests green. Pre-existing unused-lexical =i= warning in =jumper--location-exists-p= left alone (separate nit). +Two related defects from the 2026-06 config audit: +- =modules/jumper.el:155= — removal shifts the vector without renumbering registers, so a later store allocates a register still held by a surviving location and silently overwrites it. Allocate the first free register char in the live slice; =set-register nil= on removal so freed markers don't pin buffers. +- =modules/jumper.el:117,132= — guards check =(markerp marker)= but not =(buffer-live-p (marker-buffer marker))=; after killing a buffer holding a location, M-SPC SPC and M-SPC j signal wrong-type errors. Treat dead entries as skippable/removable. +Also =jumper.el:178= — the promised single-location toggle never toggles back ('already-there branch should =jump-to-register= z when set). +** DONE [#C] face-diagnostic: face-name buttons + header allowlist :feature:quick:solo: +CLOSED: [2026-06-24 Wed] +:PROPERTIES: +:LAST_REVIEWED: 2026-06-21 +:END: +Done 2026-06-24: (a) =cj/--face-diag-face-button= renders each real face name in the report as a =buttonize='d button that runs =describe-face= on it (carries the face as button-data); anonymous specs and non-faces stay plain. Routed through the stack, overlay, remap, and provenance render sites. (b) Added =face-diagnostic= to =test-init-header--classified-modules= (it's required in init.el and already carries the header contract). 5 new ERT tests; button text properties confirmed live in a rendered *Face Diagnosis* buffer. Click/RET sign-off is a VERIFY under Manual testing and validation. Spec: [[id:98f065cf-8bd5-46a0-ac24-da94d66855ad][face-font-diagnostic-popup-spec-implemented.org]]. +** DONE [#C] latexmk workflow never activates (two breaks) :bug:quick:solo: +CLOSED: [2026-06-24 Wed] +:PROPERTIES: +:LAST_REVIEWED: 2026-06-21 +:END: +Done 2026-06-24: changed the :hook key from =TeX-mode-hook= to =TeX-mode= (use-package appends "-hook" only to non-"-mode" symbols, so this now registers on the real =TeX-mode-hook= instead of the unbound =TeX-mode-hook-hook=), and auctex-latexmk from =:defer t= to =:after tex= so =auctex-latexmk-setup= runs when AUCTeX loads. Confirmed both breaks via macroexpand (the dump showed =add-hook 'TeX-mode-hook-hook= before, =TeX-mode-hook= after). 2 new regression ERT tests; live-verified in a real .tex buffer: =TeX-command-default= is "latexmk" and "LatexMk" is in =TeX-command-list=. Actual C-c C-c compile is a VERIFY under Manual testing and validation. From the 2026-06 config audit. +** CANCELLED [#C] the preview splits an already split window into 3 temporarily. :bug: +CLOSED: [2026-06-21 Sun] +looks strange. potentially problematic for ai-terms. +** CANCELLED [#C] TRAMP/dirvish "?" for remote dates — verify the fix per host :bug: +CLOSED: [2026-06-21 Sun] +:PROPERTIES: +:LAST_REVIEWED: 2026-06-02 +:END: + +Root cause is traced (see the dated investigation entry below). What's left needs a live remote: open each remote host in dirvish and run the three diagnostic evals to find which gate is closed, then close it. + +Diagnostics (run with point in a remote dirvish buffer): +- =M-: (dirvish-prop :remote-async)= — nil means =tramp-direct-async-process-p= is failing for this method/host, so dirvish's remote attribute fetch never runs. +- =M-: (dirvish-prop :gnuls)= — nil means the remote has no GNU =ls= (the =ls --version= probe failed), so the parser gate stays shut. Likely on truenas (FreeBSD). +- =M-: (tramp-direct-async-process-p)= — confirms whether direct-async is actually active for the connection. + +Likely fixes, by which gate is closed: +- =:gnuls= nil → install GNU coreutils on the remote (FreeBSD: =pkg install coreutils=) and make =ls= resolve to GNU on the TRAMP path, or accept "?" on that host. + + - Constraint: nothing gets installed on the remote host, so the =:gnuls= gate is resolved by accepting "?" on that host rather than installing coreutils. +- =:remote-async= nil → the scp/sshx method isn't advertising direct-async; switch to a method that supports it or check =tramp-direct-async-process= is taking effect for that protocol. + +Files involved: =modules/tramp-config.el=, =modules/dirvish-config.el=. + +*** 2026-05-22 Fri @ 20:24:44 -0500 Traced the root cause through dirvish source +Remote dates/sizes don't come from the dired =ls= listing or =dired-listing-switches=. They come from =dirvish-data-for-dir= (=dirvish-tramp.el:95=), which runs =ls -1lahi= on the remote and parses the columns into the attribute cache. That method only fires when both =(dirvish-prop :remote-async)= is a number and =(dirvish-prop :gnuls)= is a string. When either gate is shut, dirvish falls back to its default, which deliberately skips =(file-attributes f-name)= for remote files (=dirvish.el:904=, a perf guard) — leaving attrs nil, so the file-size and file-time widgets render "?" (=dirvish-widgets.el:216,247=). + +That explains why every prior fix missed: dired-listing-switches feed a different code path entirely, and disabling =tramp-direct-async-process= shuts the =:remote-async= gate, which is the one path that populates remote attributes — exactly backwards. The config already enables direct-async for ssh/sshx (=tramp-config.el:79-88=), so the remaining closed gate is per-host: =:gnuls= (no GNU ls on FreeBSD-based truenas) or direct-async not taking effect for the method. Could not verify on a live remote from the work session — handed the per-host diagnostics up into the task body. |
