aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/ai-term.el192
-rw-r--r--modules/auth-config.el31
-rw-r--r--modules/browser-config.el6
-rw-r--r--modules/calendar-sync.el74
-rw-r--r--modules/calibredb-epub-config.el134
-rw-r--r--modules/config-utilities.el5
-rw-r--r--modules/custom-comments.el61
-rw-r--r--modules/custom-datetime.el30
-rw-r--r--modules/custom-line-paragraph.el15
-rw-r--r--modules/custom-ordering.el20
-rw-r--r--modules/custom-text-enclose.el21
-rw-r--r--modules/custom-whitespace.el19
-rw-r--r--modules/dashboard-config.el9
-rw-r--r--modules/dev-fkeys.el47
-rw-r--r--modules/dwim-shell-config.el96
-rw-r--r--modules/eat-config.el13
-rw-r--r--modules/elfeed-config.el7
-rw-r--r--modules/erc-config.el4
-rw-r--r--modules/eww-config.el6
-rw-r--r--modules/flycheck-config.el39
-rw-r--r--modules/flyspell-and-abbrev.el46
-rw-r--r--modules/font-config.el50
-rw-r--r--modules/jumper.el69
-rw-r--r--modules/keyboard-compat.el90
-rw-r--r--modules/local-repository.el11
-rw-r--r--modules/mousetrap-mode.el25
-rw-r--r--modules/mu4e-org-contacts-integration.el9
-rw-r--r--modules/mu4e-org-contacts-setup.el6
-rw-r--r--modules/music-config.el207
-rw-r--r--modules/nerd-icons-config.el15
-rw-r--r--modules/org-agenda-config.el49
-rw-r--r--modules/org-contacts-config.el22
-rw-r--r--modules/org-webclipper.el48
-rw-r--r--modules/selection-framework.el1
-rw-r--r--modules/signal-config.el8
-rw-r--r--modules/system-commands.el37
-rw-r--r--modules/system-lib.el23
-rw-r--r--modules/system-utils.el6
-rw-r--r--modules/test-runner.el69
-rw-r--r--modules/tramp-config.el1
-rw-r--r--modules/transcription-config.el27
-rw-r--r--modules/ui-navigation.el14
-rw-r--r--modules/ui-theme.el8
-rw-r--r--modules/undead-buffers.el29
-rw-r--r--modules/video-audio-recording.el105
-rw-r--r--modules/weather-config.el21
46 files changed, 639 insertions, 1186 deletions
diff --git a/modules/ai-term.el b/modules/ai-term.el
index 3beabe6b5..6dfb669a9 100644
--- a/modules/ai-term.el
+++ b/modules/ai-term.el
@@ -1,4 +1,4 @@
-;;; ai-term.el --- In-Emacs AI-agent launcher with vertical-split terminal -*- lexical-binding: t; -*-
+;;; ai-term.el --- AI-agent terminals backed by EAT and tmux -*- lexical-binding: t; -*-
;; Author: Craig Jennings <c@cjennings.net>
@@ -7,70 +7,18 @@
;; Layer: 3 (Domain Workflow).
;; Category: D.
;; Load shape: eager.
-;; Eager reason: registers four global keys for the AI-agent terminal launcher; a
-;; command-loaded deferral candidate.
-;; Top-level side effects: four global key bindings.
-;; Runtime requires: cl-lib, seq, cj-window-geometry-lib, cj-window-toggle-lib,
-;; host-environment.
+;; Eager reason: binds M-SPC and the C-; a AI-agent prefix.
+;; Top-level side effects: global M-SPC binding and C-; a prefix map.
+;; Runtime requires: cl-lib, seq, window-toggle/geometry helpers, host-environment.
;; Direct test load: yes.
;;
-;; Picks an AI-agent project (a dir under ~/.emacs.d, ~/code/*, or
-;; ~/projects/* containing .ai/protocols.org), opens or reuses a terminal
-;; buffer named "agent [<basename>]", sends the agent's startup
-;; instruction to it, and routes the buffer to a side window via
-;; display-buffer-alist. When the frame already has a window forming the
-;; half the agent would occupy (a right column on a desktop, a bottom row
-;; on a laptop), the agent reuses that slot rather than splitting a third
-;; window in; toggling off restores the displaced buffer to the slot.
-;; Otherwise placement is a host-aware split: a right-side split at 50%
-;; width on a desktop, a bottom split at 75% height on a laptop (see
-;; `cj/--ai-term-default-direction'). Multiple
-;; projects produce multiple coexisting buffers that share the same
-;; slot; switching among them is a buffer-switch, not a
-;; kill-and-recreate.
+;; Opens project-scoped AI agents in EAT buffers backed by tmux sessions. Project
+;; candidates come from configured roots that contain .ai/protocols.org.
;;
-;; Each project's agent runs inside a tmux session named
-;; "<cj/ai-term-tmux-session-prefix><basename>" (default prefix "aiv-").
-;; The prefix lets `tmux ls' be filtered to AI-term's own sessions, so
-;; after an Emacs crash the project picker can match surviving sessions
-;; back to their directories: matched projects sort to the top of the
-;; picker (flagged "[detached]" -- session alive, no Emacs buffer -- or
-;; "[running]" when a live terminal buffer exists), the rest follow in
-;; alphabetical order.
-;;
-;; Four F-key entry points:
-;;
-;; - F9 `cj/ai-term' -- DWIM dispatch. If an agent buffer is
-;; currently displayed in this frame, F9 toggles it off: when it
-;; took over an existing window (a reused slot) the buffer it
-;; displaced returns to that slot, when it was split into its own
-;; window that window is removed, and when it fills the frame it
-;; is buried. Otherwise, if exactly one agent buffer is alive,
-;; F9 re-displays it; if zero or two-plus are alive, F9 falls
-;; through to the project picker.
-;; - C-F9 `cj/ai-term-pick-project' -- always show the project
-;; picker, even when an agent buffer is currently displayed.
-;; Used when the user wants to start a new project session
-;; instead of toggling the current one.
-;; - s-F9 `cj/ai-term-next' -- step to the next active agent in the
-;; queue. The queue is every active agent in buffer-name order
-;; (a stable rotation): attached agents (a live buffer) and
-;; detached ones (a live tmux session with no Emacs buffer).
-;; Stepping onto a detached agent attaches it. When an agent
-;; window is on screen, swap it to the next agent and focus it,
-;; wrapping after the last; when none is shown but agents exist,
-;; show the first. This is the "switch among existing agents"
-;; surface F9 deliberately doesn't provide.
-;; - M-F9 `cj/ai-term-close' -- gracefully close an agent: kill its
-;; tmux session (stopping the agent process), then its terminal
-;; buffer. Its window stays in the layout (swapped to the
-;; working buffer), so closing never collapses a split. Confirms
-;; first. Targets the current agent, the sole live agent, or
-;; prompts among several.
-;;
-;; Existing windmove (Shift-arrows) handles code <-> agent focus
-;; toggling. Buffer-move (C-M-arrows) handles side-swap. Neither
-;; needs anything new from this module.
+;; Agent display reuses the host-appropriate side slot when possible, otherwise
+;; splits right on desktop frames and below on laptop frames. Attached buffers
+;; and detached tmux sessions share the same rotation; selecting a detached
+;; agent recreates its EAT buffer and attaches to the live session.
;;; Code:
@@ -82,6 +30,7 @@
(require 'keybindings) ;; provides cj/register-prefix-map (C-; a)
(declare-function eat "eat" (&optional program arg))
+(declare-function cj/make-buffer-pattern-undead "undead-buffers")
(defvar eat-buffer-name)
(defvar eat-semi-char-mode-map)
@@ -215,7 +164,7 @@ which the step materializes by attaching."
Walks `buffer-list' (most-recently-selected first) and returns the
first buffer that is not an AI-term agent buffer (per
`cj/--ai-term-buffer-p') and is not an internal buffer (name starting
-with a space). Used by the single-window F9 toggle-off so dismissing a
+with a space). Used by the single-window toggle-off so dismissing a
full-frame agent returns to the file the user was working in (e.g.
todo.org) rather than swapping in another agent."
(seq-find (lambda (b)
@@ -287,7 +236,7 @@ looked up in SESSIONS, so the lossy whitespace->hyphen transform in
(defun cj/--ai-term-launch-command (dir)
"Return the shell command line that runs the AI tool in a project tmux session.
-Uses `tmux new-session -A' so a second F9 on the same project reattaches
+Uses `tmux new-session -A' so a second toggle on the same project reattaches
to the running session instead of spawning a new one. The session name
comes from `cj/--ai-term-tmux-session-name'; the first window is named
`cj/ai-term-tmux-window-name' (default \"ai\") so a later hand-opened
@@ -452,7 +401,7 @@ direction applies. Captured at toggle-off by
`cj/--ai-term-display-saved'.")
(defvar cj/--ai-term-last-was-bury nil
- "Non-nil when the last F9 toggle-off used `bury-buffer'.
+ "Non-nil when the last toggle-off used `bury-buffer'.
Set by `cj/ai-term' in its `toggle-off' branch: t when the agent
window was the only window in the frame (so toggle-off buried
@@ -462,7 +411,7 @@ buried agent in the current window (the only one) or splitting per
the saved direction.")
(defvar cj/--ai-term-last-toggle-deleted-split nil
- "Non-nil when the last F9 toggle-off deleted the agent's own split window.
+ "Non-nil when the last toggle-off deleted the agent's own split window.
Set t by `cj/--ai-term-toggle-off' only when it actually `delete-window's
the agent (a multi-window layout where the agent had its own window);
@@ -474,7 +423,7 @@ working window at the edge, displacing its buffer and collapsing the layout
-- the toggle must be reversible (off then on returns the same windows).")
(defvar cj/--ai-term-last-hidden-buffer nil
- "The agent buffer hidden by the most recent F9 toggle-off.
+ "The agent buffer hidden by the most recent toggle-off.
Captured in `cj/ai-term' just before an agent window is torn down, and
consumed by `cj/--ai-term-dispatch' so the next toggle-on reopens the
@@ -516,11 +465,24 @@ and a fraction-of-frame produces the wrong size on replay
(squeezes the other windows). An integer is unambiguous, at the
cost of not auto-scaling if the frame itself resizes.")
+(defvar cj/--ai-term-last-fullscreen nil
+ "Non-nil when the agent window was last seen filling its frame.
+
+Maintained by `cj/--ai-term-track-geometry' on
+`window-configuration-change-hook': set t whenever a live agent window is
+the sole window in its frame, cleared when the agent is shown as a split
+\(its dock direction and size are captured then instead). Consulted by
+`cj/--ai-term-display-saved' so a summon into a single-window frame
+restores the agent fullscreen rather than docking it -- the sole-window
+state isn't a representable dock size, so this flag is how it round-trips.
+Unlike `cj/--ai-term-last-was-bury' it does not depend on a toggle-off, so
+it also covers leaving the agent by switching buffers or `C-x 1'.")
+
(defun cj/--ai-term-capture-state (window)
"Capture WINDOW's direction and size into module-level state.
Sets `cj/--ai-term-last-direction' and `cj/--ai-term-last-size'
-so a subsequent F9 display can restore the user's chosen orientation
+so a subsequent display can restore the user's chosen orientation
and size. Called at toggle-off (just before the window is torn
down). The default direction is host-aware via
`cj/--ai-term-default-direction' (used only when WINDOW fills its
@@ -532,6 +494,35 @@ is not live."
'cj/--ai-term-last-size
'(right below left)))
+(defun cj/--ai-term-window-sole-p (window)
+ "Return non-nil when WINDOW is the only live window in its frame.
+A frame's sole window is its root window; once split, the root is an
+internal window and no live window equals it."
+ (and (window-live-p window)
+ (eq window (frame-root-window (window-frame window)))))
+
+(defun cj/--ai-term-track-geometry (&rest _)
+ "Track whether the displayed agent window is fullscreen.
+
+Run from `window-configuration-change-hook'. Sets
+`cj/--ai-term-last-fullscreen' to whether a live agent window is the sole
+window in its frame, and leaves it untouched when no agent window is
+displayed -- that retained value is the just-left state a later summon
+replays. Dock direction and size stay owned by the toggle-off capture
+\(`cj/--ai-term-capture-state'); this hook must not re-capture them, or the
+repeated capture/replay drifts the dock height a couple rows per cycle."
+ (let ((win (cj/--ai-term-displayed-agent-window)))
+ (when (window-live-p win)
+ (setq cj/--ai-term-last-fullscreen (cj/--ai-term-window-sole-p win)))))
+
+(add-hook 'window-configuration-change-hook #'cj/--ai-term-track-geometry)
+
+;; Agent buffers ("agent [<project>]") are buried, not killed, by the
+;; kill-all sweep (F1 / `cj/dashboard-only'). Register the family pattern so
+;; every agent -- however and whenever created -- survives with its session.
+(with-eval-after-load 'undead-buffers
+ (cj/make-buffer-pattern-undead "\\`agent \\["))
+
(defun cj/--ai-term-reuse-existing-agent (buffer _alist)
"Display-buffer action: reuse any window in this frame already showing
an agent buffer.
@@ -544,7 +535,7 @@ action in the chain runs.
This is more specific than `display-buffer-use-some-window', which
would happily steal any non-selected window (e.g. a code window
above the agent split) when the user is focused in agent and
-swaps projects via C-F9. The selective lookup here keeps non-agent
+swaps projects via C-; a s. The selective lookup here keeps non-agent
windows undisturbed and preserves the user's split geometry across
project changes."
(let ((win (cj/--ai-term-displayed-agent-window)))
@@ -592,19 +583,27 @@ keeping the toggle reversible."
win))))
(defun cj/--ai-term-display-saved (buffer alist)
- "Display-buffer action: split per saved direction and size.
+ "Display-buffer action: restore fullscreen in a single-window frame,
+otherwise split per saved direction and size.
-When the prior toggle-off was a bury (single-window state, flagged
-via `cj/--ai-term-last-was-bury') and the frame is still single-
-window, restore the agent into the selected window in place rather
-than splitting -- preserves the user's lone-window layout across
-F9 toggles.
+When the frame is a single window and the agent was last fullscreen
+\(`cj/--ai-term-last-fullscreen', tracked by `cj/--ai-term-track-geometry')
+or the prior toggle-off was a single-window bury
+\(`cj/--ai-term-last-was-bury'), restore the agent into the selected window
+in place rather than splitting. This round-trips a fullscreen agent --
+left by toggle-off, `C-x 1', or switching buffers -- since the sole-window
+state isn't a representable dock size.
Otherwise delegates to `cj/window-toggle-display-saved' against the
-F9 state vars, falling back to the host-aware defaults from
+toggle state vars, falling back to the host-aware defaults from
`cj/--ai-term-default-direction' and `cj/--ai-term-default-size'."
(cond
- ((and cj/--ai-term-last-was-bury (one-window-p))
+ ;; NOMINI t: don't count an active minibuffer as a second window. A summon
+ ;; can run with a picker prompt up, and a bare `one-window-p' then returns
+ ;; nil on a structurally single-window frame, misfiring the fullscreen
+ ;; restore into a dock -- which clears the fullscreen flag and cascades.
+ ((and (or cj/--ai-term-last-fullscreen cj/--ai-term-last-was-bury)
+ (one-window-p t))
(setq cj/--ai-term-last-was-bury nil)
(let ((win (selected-window)))
(set-window-buffer win buffer)
@@ -627,7 +626,7 @@ through four actions in order:
2. `cj/--ai-term-reuse-existing-agent' -- otherwise, if any
window in this frame already shows an agent-prefixed buffer,
swap its buffer for the new one (preserves geometry across
- project changes via C-F9).
+ project changes via C-; a s).
3. `cj/--ai-term-reuse-edge-window' -- otherwise, if the frame
already has a window forming the half the agent would occupy
(the right column on a desktop, the bottom row on a laptop),
@@ -773,17 +772,17 @@ Signals `user-error' when no candidates exist."
(expand-file-name chosen)))))
(defun cj/--ai-term-dispatch ()
- "Compute the F9 (`cj/ai-term') action without performing it.
+ "Compute the `cj/ai-term' (C-; a a) action without performing it.
Returns one of:
- (toggle-off . WINDOW) -- agent is displayed in WINDOW; quit it.
- (redisplay-recent . BUFFER) -- 1+ alive agent buffers; show MRU.
- (pick-project) -- zero alive agent buffers; prompt.
-When 2+ agent buffers are alive, F9 redisplays the most-recently-
-selected one rather than opening the project picker. C-F9 is the
-explicit \"start a different project\" surface; M-F9 is the explicit
-\"switch among existing agents\" surface. F9 keeps a single, simple
+When 2+ agent buffers are alive, C-; a a redisplays the most-recently-
+selected one rather than opening the project picker. C-; a s is the
+explicit \"start a different project\" surface; C-; a n is the explicit
+\"switch among existing agents\" surface. C-; a a keeps a single, simple
job: toggle whichever agent was last in use.
A pure-decision helper so the dispatch logic is exercisable in tests
@@ -816,7 +815,7 @@ buffers; reinvoking on the same project reuses its existing terminal.
With prefix ARG, display the buffer without selecting its window.
-Bound to C-F9 -- always shows the project picker, even when an agent
+Bound to C-; a s -- always shows the project picker, even when an agent
buffer is currently displayed.
EAT renders in terminal frames as well as GUI frames, so this
@@ -842,7 +841,7 @@ the agent itself."
(other-buffer (window-buffer win) t)))))
(defun cj/--ai-term-toggle-off (win)
- "Hide the agent shown in WIN for an F9 toggle-off. Always returns nil.
+ "Hide the agent shown in WIN for a toggle-off. Always returns nil.
Two cases, by window count:
@@ -855,7 +854,7 @@ Two cases, by window count:
force a swap to a non-agent buffer to keep the toggle observable.
- Multi-window: collapse the agent split outright by deleting its window, so
- the working buffer (e.g. todo.org) reclaims the space. F9 is a pure
+ the working buffer (e.g. todo.org) reclaims the space. The toggle is a pure
show/hide toggle of THE agent split -- it must never surface a different
agent. `quit-restore-window' can't guarantee that here: switching among
several agents reuses the one slot via `set-window-buffer' (see
@@ -897,21 +896,21 @@ Two cases, by window count:
nil)
(defun cj/ai-term (&optional arg)
- "Smart F9 dispatch for the AI-term launcher.
+ "DWIM dispatch for the AI-term launcher. Bound to C-; a a.
Behavior depends on the current state:
-- If an AI-term buffer is currently displayed in this frame, F9
+- If an AI-term buffer is currently displayed in this frame, it
quits its window (toggle off, buffer stays alive).
-- Else, if exactly one alive AI-term buffer exists, F9 re-displays
+- Else, if exactly one alive AI-term buffer exists, it re-displays
it (DWIM -- the obvious next step is to look at it).
-- Else (zero or 2+), F9 falls through to `cj/ai-term-pick-project'.
+- Else (zero or 2+), it falls through to `cj/ai-term-pick-project'.
With prefix ARG, display the buffer without selecting its window
when a buffer is being shown (no effect on the toggle-off branch).
-See `cj/ai-term-pick-project' (C-F9) to force the project picker.
-M-F9 closes an agent via `cj/ai-term-close'."
+See `cj/ai-term-pick-project' (C-; a s) to force the project picker.
+C-; a k closes an agent via `cj/ai-term-close'."
(interactive "P")
(pcase (cj/--ai-term-dispatch)
(`(toggle-off . ,win)
@@ -945,7 +944,7 @@ Derives the tmux session name from BUFFER's `default-directory' (the
project dir the terminal was created in) and kills it so the agent
process stops. When BUFFER is shown, swaps its window to a non-agent
buffer (the working file) rather than deleting the window -- closing an
-agent must not collapse the user's window layout; the F9 hide toggle is
+agent must not collapse the user's window layout; the hide toggle is
what collapses the split. Then kills BUFFER (suppressing the
process-still-running prompt -- the session is already down). No-op
when BUFFER isn't an AI-term buffer."
@@ -959,6 +958,8 @@ when BUFFER isn't an AI-term buffer."
(let ((kill-buffer-query-functions nil))
(kill-buffer buffer))))
+(require 'system-lib)
+
(defun cj/--ai-term-close-target ()
"Return the AI-term buffer `cj/ai-term-close' should act on, or nil.
@@ -973,7 +974,8 @@ buffers; nil when none are alive."
((null (cdr buffers)) (car buffers))
(t (get-buffer
(completing-read "Close AI terminal: "
- (mapcar #'buffer-name buffers) nil t))))))))
+ (cj/completion-table 'buffer (mapcar #'buffer-name buffers))
+ nil t))))))))
(defun cj/ai-term-close ()
"Gracefully close an AI-term agent: kill its tmux session and buffer.
@@ -981,7 +983,7 @@ buffers; nil when none are alive."
Targets the current agent buffer, the sole live agent, or prompts when
several are alive (see `cj/--ai-term-close-target'). Asks for
confirmation first -- this kills the running agent process, which can
-interrupt work in progress. Bound to M-<f9>."
+interrupt work in progress. Bound to C-; a k."
(interactive)
(let ((buffer (cj/--ai-term-close-target)))
(unless buffer
diff --git a/modules/auth-config.el b/modules/auth-config.el
index 62d773057..c2df244b5 100644
--- a/modules/auth-config.el
+++ b/modules/auth-config.el
@@ -1,4 +1,4 @@
-;; auth-config.el --- Configuration for Authentication Utilities -*- lexical-binding: t; coding: utf-8; -*-
+;;; auth-config.el --- Authentication and GPG integration -*- lexical-binding: t; coding: utf-8; -*-
;; author Craig Jennings <c@cjennings.net>
;;; Commentary:
@@ -6,29 +6,16 @@
;; Layer: 1 (Foundation).
;; Category: F/D.
;; Load shape: eager.
-;; Eager reason: auth-source and GPG/epa setup that other modules rely on for
-;; credentials early in the session.
-;; Top-level side effects: auth-source/epa configuration via use-package and setq.
+;; Eager reason: credentials and GPG setup are needed by other modules early.
+;; Top-level side effects: auth-source/epa setup and oauth2-auto cache advice.
;; Runtime requires: system-lib, user-constants.
-;; Direct test load: yes (configuration only).
+;; Direct test load: yes.
;;
-;; Configuration for Emacs authentication and GPG integration:
-
-;; • auth-source
-;; – Forces use of your default authinfo file
-;; – Disable external GPG agent in favor of Emacs's own prompt
-;; – Keeps auth-source debug logging disabled by default
-
-;; • Easy PG Assistant (epa)
-;; – Force using the 'gpg2' executable for encryption/decryption operations
-
-;; • oauth2-auto cache fix (via advice)
-;; – oauth2-auto version 20250624.1919 has caching bug on line 206
-;; – Function oauth2-auto--plstore-read has `or nil` disabling cache
-;; – This caused GPG passphrase prompts every ~15 minutes during gcal-sync
-;; – Fix: Advice to enable hash-table cache without modifying package
-;; – Works across package updates
-;; – Fixed 2025-11-11
+;; Central auth-source, GPG, and credential-debug setup. Auth lookups use the
+;; configured authinfo file; passphrase caching is left to gpg-agent.
+;;
+;; Advises oauth2-auto's plstore reader to restore in-memory caching and avoid
+;; repeated GPG prompts during calendar/mail refreshes.
;;; Code:
diff --git a/modules/browser-config.el b/modules/browser-config.el
index d596b9e9d..564e7a275 100644
--- a/modules/browser-config.el
+++ b/modules/browser-config.el
@@ -76,7 +76,10 @@ Includes built-in Emacs browsers (those with nil executable)."
(defun cj/save-browser-choice (browser-plist)
"Save BROWSER-PLIST to the persistence file."
(with-temp-file cj/browser-choice-file
- (insert ";;; Browser choice - Auto-generated\n")
+ (insert ";;; browser-choice.el --- Generated browser selection -*- lexical-binding: t; -*-\n")
+ (insert ";;\n")
+ (insert ";; Generated by browser-config.el. Do not edit by hand; use\n")
+ (insert ";; `cj/choose-browser' to rewrite this file.\n")
(insert (format "(setq cj/saved-browser-choice '%S)\n" browser-plist))))
(defun cj/load-browser-choice ()
@@ -102,7 +105,6 @@ Returns: \\='success if applied successfully,
(program-var (plist-get browser-plist :program-var)))
(if (null browse-fn)
'invalid-plist
- ;; Set the main browse-url function
(setq browse-url-browser-function browse-fn)
;; Set the specific browser program variable if it exists
(when program-var
diff --git a/modules/calendar-sync.el b/modules/calendar-sync.el
index c0e0e935a..b684330c8 100644
--- a/modules/calendar-sync.el
+++ b/modules/calendar-sync.el
@@ -8,75 +8,17 @@
;; Layer: 3 (Domain Workflow).
;; Category: D/S.
;; Load shape: eager only when calendar-sync.local.el configures calendars.
-;; Eager reason: daily-driver workflow; calendars are expected synced at the
-;; first session. Timers and network fetches are guarded for batch/test loads.
-;; Top-level side effects: defines a calendar keymap and conditionally registers
-;; it under cj/custom-keymap; timer and network fetches guarded by
-;; config/noninteractive checks.
+;; Eager reason: daily agenda workflow; timers and network fetches are guarded.
+;; Top-level side effects: defines C-; g map; starts sync only when configured.
;; Runtime requires: cl-lib, subr-x, system-lib, cj-org-text-lib, keybindings.
-;; Direct test load: yes (private config optional; degrades cleanly when absent).
+;; Direct test load: yes.
;;
-;; Simple, reliable one-way sync from multiple calendars to Org mode.
-;; Downloads .ics files from calendar URLs (Google, Proton, etc.) and
-;; converts to Org format. No OAuth, no API complexity, just file conversion.
+;; One-way calendar synchronization from configured .ics/API sources into Org
+;; files. Feed URLs may be inline or resolved from auth-source via :secret-host.
;;
-;; Features:
-;; - Multi-calendar support (sync multiple calendars to separate files)
-;; - Pure Emacs Lisp .ics parser (no external dependencies)
-;; - Recurring event support (RRULE expansion)
-;; - Timer-based automatic sync (every 60 minutes, configurable)
-;; - Self-contained in .emacs.d (no cron, portable across machines)
-;; - Read-only (can't corrupt source calendars)
-;; - Works with Chime for event notifications
-;;
-;; Recurring Events (RRULE):
-;;
-;; Calendar recurring events are defined once with an RRULE
-;; (recurrence rule) rather than as individual event instances. This
-;; module expands recurring events into individual org entries.
-;;
-;; Expansion uses a rolling window approach:
-;; - Past: 3 months before today
-;; - Future: 12 months after today
-;;
-;; Every sync regenerates the entire file based on the current date,
-;; so the window automatically advances as time passes. Old events
-;; naturally fall off after 3 months, and new future events appear
-;; as you approach them.
-;;
-;; Supported RRULE patterns:
-;; - FREQ=DAILY: Daily events
-;; - FREQ=WEEKLY;BYDAY=MO,WE,FR: Weekly on specific days
-;; - FREQ=MONTHLY: Monthly events (same day each month)
-;; - FREQ=YEARLY: Yearly events (anniversaries, birthdays)
-;; - INTERVAL: Repeat every N periods (e.g., every 2 weeks)
-;; - UNTIL: End date for recurrence
-;; - COUNT: Maximum occurrences (combined with date range limit)
-;;
-;; Setup:
-;; 1. Configure calendars in your init.el:
-;; (setq calendar-sync-calendars
-;; '((:name "google"
-;; :url "https://calendar.google.com/calendar/ical/.../basic.ics"
-;; :file gcal-file)
-;; (:name "proton"
-;; :url "https://calendar.proton.me/api/calendar/v1/url/.../calendar.ics"
-;; :file pcal-file)))
-;;
-;; 2. Load and start:
-;; (require 'calendar-sync)
-;; (calendar-sync-start)
-;;
-;; 3. Add to org-agenda (optional):
-;; (dolist (cal calendar-sync-calendars)
-;; (add-to-list 'org-agenda-files (plist-get cal :file)))
-;;
-;; Usage:
-;; - M-x calendar-sync-now ; Sync all or select specific calendar
-;; - M-x calendar-sync-start ; Start auto-sync
-;; - M-x calendar-sync-stop ; Stop auto-sync
-;; - M-x calendar-sync-toggle ; Toggle auto-sync
-;; - M-x calendar-sync-status ; Show sync status for all calendars
+;; The parser expands recurring events into a rolling window around today,
+;; regenerates target Org files on each sync, and keeps source calendars
+;; read-only. Commands under C-; g start, stop, toggle, inspect, and run syncs.
;;; Code:
diff --git a/modules/calibredb-epub-config.el b/modules/calibredb-epub-config.el
index 1e6437d26..38aa0de05 100644
--- a/modules/calibredb-epub-config.el
+++ b/modules/calibredb-epub-config.el
@@ -6,46 +6,17 @@
;; Layer: 4 (Optional).
;; Category: O/D/P.
;; Load shape: eager.
-;; Eager reason: none; optional ebook workflow, a command-loaded deferral
-;; candidate for Phase 4.
-;; Top-level side effects: one add-hook, one advice-add, package config.
-;; Runtime requires: user-constants, subr-x.
+;; Eager reason: none; ebook commands can load by command.
+;; Top-level side effects: one hook, one advice, package config.
+;; Runtime requires: user-constants, subr-x, transient.
;; Direct test load: yes.
;;
-;; This module provides a comprehensive ebook management and reading experience
-;; within Emacs, integrating CalibreDB for library management and Nov for EPUB
-;; reading.
+;; CalibreDB and Nov integration for browsing the Calibre library and reading
+;; EPUBs inside Emacs. The module adds a curated CalibreDB transient, filter
+;; helpers, Nov typography, image centering, and reader-to-library navigation.
;;
-;; FEATURES:
-;; - CalibreDB integration for managing your Calibre ebook library
-;; - Nov mode for reading EPUB files with customized typography and layout
-;; - Seamless navigation between Nov reading buffers and CalibreDB entries
-;; - Image centering in EPUB documents without modifying buffer text
-;; - Quick filtering and searching within your ebook library
-;;
-;; KEY BINDINGS:
-;; - M-B: Open CalibreDB library browser
-;; - In CalibreDB search mode:
-;; - l: Filter by tag
-;; - L: Clear all filters
-;; - In Nov mode:
-;; - z: Open current EPUB in external viewer (zathura)
-;; - C-c C-b: Jump to CalibreDB entry for current book
-;; - m: Set bookmark
-;; - b: List bookmarks
-;;
-;; WORKFLOW:
-;; 1. Press M-B to browse your Calibre library
-;; 2. Use filters (l for tags, L to clear) to narrow results
-;; 3. Open an EPUB to read it in Nov with optimized typography
-;; 4. While reading, use C-c C-b to jump back to the book's metadata
-;; 5. Use z to open in external reader when needed
-;;
-;; CONFIGURATION NOTES:
-;; - Prefers EPUB format when available, falls back to PDF
-;; - Centers images in EPUB documents using display properties
-;; - Applies custom typography with larger fonts for comfortable reading
-;; - Uses visual-fill-column for centered text with appropriate margins
+;; EPUB is preferred when available; external opening remains available for
+;; formats or workflows better handled outside Emacs.
;;; Code:
@@ -62,6 +33,15 @@
;; calibredb commands the curated menu drives (all autoloaded by calibredb)
(declare-function calibredb-switch-library "calibredb" ())
+(declare-function calibredb-search-keyword-filter "calibredb-search")
+
+;; calibredb's filter-scope flags (set in `cj/--calibredb-open-to-favorites');
+;; declared special so the assignments compile clean when calibredb is absent.
+(defvar calibredb-tag-filter-p)
+(defvar calibredb-favorite-filter-p)
+(defvar calibredb-author-filter-p)
+(defvar calibredb-date-filter-p)
+(defvar calibredb-format-filter-p)
(declare-function calibredb-filter-by-book-format "calibredb" ())
(declare-function calibredb-filter-by-author-sort "calibredb" ())
(declare-function calibredb-search-clear-filter "calibredb" ())
@@ -116,6 +96,26 @@ which re-applies `calibredb-search-filter' instead."
(setq calibredb-sort-by field)
(calibredb-search-refresh-or-resume))
+(defun cj/--calibredb-open-to-favorites (&rest _)
+ "Filter the calibredb search to books tagged `calibredb-favorite-keyword'.
+Advice (:after) on `calibredb' so every launch lands on the favorite-keyword
+books (Craig's \"in-progress\" reading list); clear with L / x to see the
+whole library. Scopes to the tag field (sets `calibredb-tag-filter-p',
+clears the other filter-scope flags), because a bare keyword filter matches
+the keyword in any field -- title, author, or the description -- and would
+surface books that merely mention it. No-op unless a non-empty string
+keyword is set."
+ (when (and (boundp 'calibredb-favorite-keyword)
+ (stringp calibredb-favorite-keyword)
+ (not (string-empty-p calibredb-favorite-keyword))
+ (fboundp 'calibredb-search-keyword-filter))
+ (setq calibredb-tag-filter-p t
+ calibredb-favorite-filter-p nil
+ calibredb-author-filter-p nil
+ calibredb-date-filter-p nil
+ calibredb-format-filter-p nil)
+ (calibredb-search-keyword-filter calibredb-favorite-keyword)))
+
(use-package calibredb
:commands calibredb
:bind
@@ -184,7 +184,10 @@ which re-applies `calibredb-search-filter' instead."
(setq calibredb-order "asc")
(setq calibredb-id-width 7)
(setq calibredb-favorite-icon "🔖")
- (setq calibredb-favorite-keyword "in-progress"))
+ (setq calibredb-favorite-keyword "in-progress")
+ ;; Open every calibredb launch (dashboard, M-x, elsewhere) filtered to the
+ ;; in-progress favorites; L / x clears to the whole library.
+ (advice-add 'calibredb :after #'cj/--calibredb-open-to-favorites))
;; ------------------------------ Nov Epub Reader ------------------------------
@@ -207,7 +210,6 @@ Adjust it live with `cj/nov-widen-text' and `cj/nov-narrow-text'.")
(if (and buffer-file-name
(string-match-p "\\.epub\\'" buffer-file-name))
(progn
- ;; Load nov if not already loaded
(unless (featurep 'nov)
(require 'nov nil t))
;; Call nov-mode if available, otherwise fallback to default behavior
@@ -404,6 +406,12 @@ Try to use the Calibre book id from the parent folder name (for example,
(calibredb-search-keyword-filter "")
(message "CalibreDB: no metadata; showing all"))))))
+(require 'system-lib)
+;; nov renders epub via shr, which paints with manual `face' properties. Left in
+;; `global-font-lock-mode' font-lock overwrites them and the book loses its
+;; colors, the same issue as elfeed-show and mu4e-view. Exclude nov-mode.
+(cj/exclude-from-global-font-lock 'nov-mode)
+
(use-package nov
:mode
("\\.epub\\'" . nov-mode)
@@ -432,14 +440,15 @@ Try to use the Calibre book id from the parent folder name (for example,
;; ------------------------- Nov bookmark naming -------------------------------
;; In a nov buffer "m" is bound to `bookmark-set' (above). nov's
-;; `nov-bookmark-make-record' names the record after `(buffer-name)' -- the EPUB
-;; filename, extension and all. Rebuild it as "Author, Title" parsed from the
-;; filename: under Calibre's "<Title> - <Author>.epub" naming the filename is
-;; more complete than the EPUB's embedded metadata (which carries truncated
-;; titles and author-sort "Last, First" forms).
-
-(defun cj/--nov-clean-title (s)
- "Clean a title or author S parsed from an EPUB filename, or nil when blank.
+;; Both nov (EPUB) and pdf-view (PDF) name a new bookmark after the buffer --
+;; the file's name, extension and all. Rebuild it as "Author, Title" parsed
+;; from the filename: under Calibre's "<Title> - <Author>.<ext>" naming the
+;; filename is more complete than the file's embedded metadata (which carries
+;; truncated titles and author-sort "Last, First" forms). One :filter-return
+;; advice serves both record functions; the parser is extension-agnostic.
+
+(defun cj/--reading-clean-title (s)
+ "Clean a title or author S parsed from a book filename, or nil when blank.
Restores a colon where Calibre sanitized \":\" to \"_\" (\"Frege_ A Guide\"
-> \"Frege: A Guide\"), turns any leftover underscore into a space, and
collapses runs of whitespace."
@@ -449,34 +458,39 @@ collapses runs of whitespace."
(out (string-trim (replace-regexp-in-string "[ \t]+" " " spaced))))
(and (not (string-empty-p out)) out))))
-(defun cj/--nov-bookmark-name-from-file (path)
- "Return \"Author, Title\" derived from an EPUB PATH's filename, or nil.
+(defun cj/--reading-bookmark-name-from-file (path)
+ "Return \"Author, Title\" derived from a book PATH's filename, or nil.
Splits the filename (sans extension) on its last \" - \" into title and
author per Calibre's \"<Title> - <Author>\" convention, restoring colons and
reordering to \"Author, Title\". Falls back to the cleaned whole name when
-there is no \" - \" separator."
+there is no \" - \" separator. Extension-agnostic, so it serves EPUB and PDF."
(when (and (stringp path) (not (string-empty-p path)))
(let ((base (file-name-sans-extension (file-name-nondirectory path))))
(if (string-match "\\`\\(.+\\) - \\(.+\\)\\'" base)
- (let ((title (cj/--nov-clean-title (match-string 1 base)))
- (author (cj/--nov-clean-title (match-string 2 base))))
+ (let ((title (cj/--reading-clean-title (match-string 1 base)))
+ (author (cj/--reading-clean-title (match-string 2 base))))
(cond ((and author title) (format "%s, %s" author title))
(title title)
(author author)
(t nil)))
- (cj/--nov-clean-title base)))))
-
-(defun cj/--nov-bookmark-rename-record (record)
- "Replace RECORD's bookmark name with \"Author, Title\" from its EPUB filename.
-Advice (:filter-return) on `nov-bookmark-make-record'. RECORD is
-\(NAME . ALIST) carrying a `filename'; left unchanged when no name derives."
- (let ((name (cj/--nov-bookmark-name-from-file
+ (cj/--reading-clean-title base)))))
+
+(defun cj/--reading-bookmark-rename-record (record)
+ "Replace RECORD's bookmark name with \"Author, Title\" from its filename.
+Advice (:filter-return) on `nov-bookmark-make-record' and
+`pdf-view-bookmark-make-record'. RECORD is (NAME . ALIST) carrying a
+`filename'; left unchanged when no name derives."
+ (let ((name (cj/--reading-bookmark-name-from-file
(alist-get 'filename (cdr record)))))
(if name (cons name (cdr record)) record)))
(with-eval-after-load 'nov
(advice-add 'nov-bookmark-make-record :filter-return
- #'cj/--nov-bookmark-rename-record))
+ #'cj/--reading-bookmark-rename-record))
+
+(with-eval-after-load 'pdf-view
+ (advice-add 'pdf-view-bookmark-make-record :filter-return
+ #'cj/--reading-bookmark-rename-record))
(defun cj/--nov-image-padding-cols (col-width img-px font-width-px)
"Return left-padding columns to center an IMG-PX-wide image in COL-WIDTH cols.
diff --git a/modules/config-utilities.el b/modules/config-utilities.el
index f448327c1..0c98a896c 100644
--- a/modules/config-utilities.el
+++ b/modules/config-utilities.el
@@ -114,11 +114,14 @@ Signals `user-error' if METHOD-SYMBOL is nil or not fboundp."
(with-timer title
(funcall method-symbol)))
+(require 'system-lib)
+
(defun cj/benchmark-this-method ()
"Prompt for a title and method name, then time the execution of the method."
(interactive)
(let* ((title (read-string "Enter the title for the timing: "))
- (method-name (completing-read "Enter the method name to time: " obarray
+ (method-name (completing-read "Enter the method name to time: "
+ (cj/completion-table 'function obarray)
#'fboundp t))
(method-symbol (intern-soft method-name)))
(condition-case err
diff --git a/modules/custom-comments.el b/modules/custom-comments.el
index 231a03860..a2604a558 100644
--- a/modules/custom-comments.el
+++ b/modules/custom-comments.el
@@ -5,62 +5,17 @@
;; Layer: 2 (Core UX).
;; Category: L/C.
;; Load shape: eager.
-;; Eager reason: registers its C-; C comment submap at load. Currently eager by
-;; init order; a deferral candidate for Phase 3/4 (command/autoload +
-;; registration API).
-;; Top-level side effects: defines cj/comment-map, registers it under C-; C.
+;; Eager reason: registers C-; C comment helpers.
+;; Top-level side effects: defines and registers cj/comment-map.
;; Runtime requires: keybindings.
-;; Direct test load: yes (requires keybindings explicitly).
+;; Direct test load: yes.
;;
-;; This module provides custom comment formatting and manipulation utilities for code editing.
-;;
-;; Functions include:
-;; - deleting all comments in a buffer,
-;; - reformatting commented text into single-line paragraphs,
-;; - creating centered comment headers with customizable separator characters,
-;; - creating comment boxes around text
-;; - inserting hyphen-style centered comments.
-;;
-;; These utilities help create consistent, well-formatted code comments and section headers.
-;; Bound to keymap prefix: C-; C
-;;
-;; Comment Style Patterns:
-;;
-;; inline-border:
-;; ========== inline-border ==========
-;;
-;; simple-divider:
-;; ====================================
-;; simple-divider
-;; ====================================
-;;
-;; padded-divider:
-;; ====================================
-;; padded-divider
-;; ====================================
-;;
-;; box:
-;; ************************************
-;; * box *
-;; ************************************
-;;
-;; heavy-box:
-;; ************************************
-;; * *
-;; * heavy-box *
-;; * *
-;; ************************************
-;;
-;; unicode-box:
-;; ┌──────────────────────────────────┐
-;; │ unicode-box │
-;; └──────────────────────────────────┘
-;;
-;; block-banner:
-;; /************************************
-;; * block-banner
-;; ************************************/
+;; Comment editing helpers: delete comments, reflow commented regions, and insert
+;; consistent section headers or boxes using the current mode's comment syntax.
;;
+;; Public commands live under C-; C. Decoration helpers validate single printable
+;; characters before generating comment borders.
+
;;; Code:
(require 'keybindings) ;; provides cj/custom-keymap
diff --git a/modules/custom-datetime.el b/modules/custom-datetime.el
index 6bca494d8..0528688c2 100644
--- a/modules/custom-datetime.el
+++ b/modules/custom-datetime.el
@@ -1,4 +1,4 @@
-;;; custom-datetime.el --- -*- coding: utf-8; lexical-binding: t; -*-
+;;; custom-datetime.el --- Insert formatted date and time strings -*- coding: utf-8; lexical-binding: t; -*-
;;; Commentary:
;;
@@ -12,32 +12,8 @@
;; Runtime requires: keybindings.
;; Direct test load: yes (requires keybindings explicitly).
;;
-;; Utilities for inserting date/time stamps in multiple formats.
-;;
-;; Interactive commands:
-;; - cj/insert-readable-date-time
-;; - cj/insert-sortable-date-time
-;; - cj/insert-sortable-time
-;; - cj/insert-readable-time
-;; - cj/insert-sortable-date
-;; - cj/insert-readable-date
-;;
-;; Each command is generated by `cj/--define-datetime-inserter' from a
-;; corresponding format variable:
-;; readable-date-time-format, sortable-date-time-format,
-;; sortable-time-format, readable-time-format,
-;; sortable-date-format, readable-date-format.
-;; Customize these (see `format-time-string') to change output.
-;; Some defaults include a trailing space for convenient typing.
-;;
-;; Key bindings:
-;; A prefix map `cj/datetime-map' is installed on "d" under `cj/custom-keymap':
-;; r → readable date+time
-;; s → sortable date+time
-;; t → sortable time
-;; T → readable time
-;; d → sortable date
-;; D → readable date
+;; Date/time insertion commands under C-; d. Each command is generated from a
+;; customizable format variable and inserts format-time-string output at point.
;;
;;; Code:
diff --git a/modules/custom-line-paragraph.el b/modules/custom-line-paragraph.el
index 2cbcecc16..dd2999c4e 100644
--- a/modules/custom-line-paragraph.el
+++ b/modules/custom-line-paragraph.el
@@ -1,4 +1,4 @@
-;;; custom-line-paragraph.el --- -*- coding: utf-8; lexical-binding: t; -*-
+;;; custom-line-paragraph.el --- Line and paragraph editing commands -*- coding: utf-8; lexical-binding: t; -*-
;; Author: Craig Jennings <c@cjennings.net>
;;
;;; Commentary:
@@ -14,16 +14,9 @@
;; Runtime requires: keybindings (expand-region on demand via declare-function).
;; Direct test load: yes (requires keybindings explicitly).
;;
-;; This module provides the following line and paragraph manipulation utilities:
-;;
-;; - joining lines in a region or the current line with the previous one
-;; - joining separate lines into a single paragraph
-;; - duplicating lines or regions (optional commenting)
-;; - removing duplicate lines
-;; - removing lines containing specific text
-;; - underlining text with a custom character
-;;
-;; Bound to keymap prefix C-; l
+;; Line and paragraph transforms under C-; l: join, duplicate, delete matching
+;; lines, remove duplicates, and underline text. Commands operate on the active
+;; region when present and otherwise on the current line or paragraph.
;;
;;; Code:
diff --git a/modules/custom-ordering.el b/modules/custom-ordering.el
index 0a499a35a..4dc5bff84 100644
--- a/modules/custom-ordering.el
+++ b/modules/custom-ordering.el
@@ -1,4 +1,4 @@
-;;; custom-ordering.el --- -*- coding: utf-8; lexical-binding: t; -*-
+;;; custom-ordering.el --- Region sorting and list-format transforms -*- coding: utf-8; lexical-binding: t; -*-
;;; Commentary:
;;
@@ -13,22 +13,10 @@
;; declare-function).
;; Direct test load: yes (requires keybindings explicitly).
;;
-;; Text transformation and sorting utilities for reformatting data structures.
+;; Region transforms under C-; o for sorting, reversing, numbering, quote
+;; toggling, and converting between line lists and comma-separated arrays.
+;; Helpers preserve trailing newlines where line-oriented callers expect them.
;;
-;; Array/list formatting:
-;; - arrayify/listify - convert lines to comma-separated format (with/without quotes, brackets)
-;; - unarrayify - convert arrays back to separate lines
-;;
-;; Line manipulation:
-;; - toggle-quotes - swap double ↔ single quotes
-;; - reverse-lines - reverse line order
-;; - number-lines - add line numbers with custom format (supports zero-padding)
-;; - alphabetize-region - sort words alphabetically
-;; - comma-separated-text-to-lines - split CSV text into lines
-;;
-;; Convenience functions: listify, arrayify-json, arrayify-python
-;; Bound to keymap prefix C-; o
-
;;; Code:
(require 'cl-lib)
diff --git a/modules/custom-text-enclose.el b/modules/custom-text-enclose.el
index 5b1b00a71..4d72347d1 100644
--- a/modules/custom-text-enclose.el
+++ b/modules/custom-text-enclose.el
@@ -1,4 +1,4 @@
-;;; custom-text-enclose.el --- -*- coding: utf-8; lexical-binding: t; -*-
+;;; custom-text-enclose.el --- Wrap, unwrap, and prefix text ranges -*- coding: utf-8; lexical-binding: t; -*-
;;; Commentary:
;;
@@ -12,23 +12,10 @@
;; Runtime requires: keybindings (change-inner on demand via declare-function).
;; Direct test load: yes (requires keybindings explicitly).
;;
-;; Text enclosure utilities for wrapping and line manipulation.
+;; Text enclosure commands under C-; s. Commands wrap or unwrap the active
+;; region/word at point, and add prefixes, suffixes, indentation, or dedentation
+;; across selected lines.
;;
-;; Wrapping functions:
-;; - surround-word-or-region - wrap text with same delimiter on both sides
-;; - wrap-word-or-region - wrap with different opening/closing delimiters
-;; - unwrap-word-or-region - remove surrounding delimiters
-;;
-;; Line manipulation:
-;; - append-to-lines - add suffix to each line
-;; - prepend-to-lines - add prefix to each line
-;; - indent-lines - add leading whitespace (spaces or tabs)
-;; - dedent-lines - remove leading whitespace
-;;
-;; Most functions work on region or entire buffer when no region is active.
-;;
-;; Bound to keymap prefix C-; s
-
;;; Code:
(require 'keybindings) ;; provides cj/custom-keymap
diff --git a/modules/custom-whitespace.el b/modules/custom-whitespace.el
index 0d4d1cc06..cbf3eff12 100644
--- a/modules/custom-whitespace.el
+++ b/modules/custom-whitespace.el
@@ -1,4 +1,4 @@
-;;; custom-whitespace.el --- -*- coding: utf-8; lexical-binding: t; -*-
+;;; custom-whitespace.el --- Whitespace cleanup commands -*- coding: utf-8; lexical-binding: t; -*-
;;; Commentary:
;;
@@ -12,19 +12,10 @@
;; Runtime requires: keybindings.
;; Direct test load: yes (requires keybindings explicitly).
;;
-;; This module provides whitespace manipulation operations for cleaning and transforming whitespace in text.
-
-;; Functions include:
-
-;; - removing leading and trailing whitespace
-;; - collapsing multiple spaces to single spaces
-;; - deleting blank lines
-;; - converting whitespace to hyphens.
-
-;; All operations work on the current line, active region, or entire buffer depending on context.
-
-;; Bound to keymap prefix C-; w
-
+;; Whitespace cleanup under C-; w: trim line edges, collapse runs of spaces,
+;; delete blank lines, enforce a single blank line, and hyphenate whitespace.
+;; Commands choose region, buffer, or current line based on prefix/mark state.
+;;
;;; Code:
(require 'keybindings) ;; provides cj/custom-keymap
diff --git a/modules/dashboard-config.el b/modules/dashboard-config.el
index 17a0e2c4a..53f19b72b 100644
--- a/modules/dashboard-config.el
+++ b/modules/dashboard-config.el
@@ -54,6 +54,7 @@
(declare-function nerd-icons-mdicon "nerd-icons")
(declare-function nerd-icons-codicon "nerd-icons")
(declare-function nerd-icons-octicon "nerd-icons")
+(declare-function nerd-icons-wicon "nerd-icons")
;; user-constants.el provides the home-directory constant.
(defvar user-home-dir)
@@ -139,6 +140,7 @@ Adjust this if the title doesn't appear centered under the banner image.")
(list "d" #'nerd-icons-faicon "nf-fa-folder_o" "Files" "Dirvish File Manager" (lambda () (dirvish user-home-dir)))
(list "t" #'nerd-icons-devicon "nf-dev-terminal" "Terminal" "Launch Terminal" (lambda () (cj/term-toggle)))
(list "a" #'nerd-icons-mdicon "nf-md-calendar" "Agenda" "Main Org Agenda" (lambda () (cj/main-agenda-display)))
+ (list "w" #'nerd-icons-wicon "nf-weather-day_sunny_overcast" "Weather" "Wttrin Weather Forecast" (lambda () (call-interactively #'wttrin)))
(list "r" #'nerd-icons-faicon "nf-fa-rss_square" "Feeds" "Elfeed Feed Reader" (lambda () (cj/elfeed-open)))
(list "b" #'nerd-icons-codicon "nf-cod-library" "Books" "Calibre Ebook Reader" (lambda () (calibredb)))
(list "f" #'nerd-icons-mdicon "nf-md-school" "Flashcards" "Org-Drill" (lambda () (cj/drill-start)))
@@ -152,9 +154,10 @@ Adjust this if the title doesn't appear centered under the banner image.")
"Dashboard launcher table: (KEY ICON-FN ICON-NAME LABEL TOOLTIP ACTION).
Drives both `dashboard-navigator-buttons' and the dashboard-mode-map keys.")
-(defconst cj/dashboard--row-sizes '(4 4 3 3)
+(defconst cj/dashboard--row-sizes '(5 4 3 3)
"Navigator row lengths. Must sum to the number of `cj/dashboard--launchers'.
-The last row groups Slack, Linear, and Signal together.")
+The top row carries Weather alongside the core tools; the last row groups
+Slack, Linear, and Signal together.")
(defun cj/dashboard--navigator-button (l)
"Build a `dashboard-navigator-buttons' entry from launcher L."
@@ -272,7 +275,7 @@ system-defaults) are preserved rather than overwritten."
(setq initial-buffer-choice (lambda () (get-buffer "*dashboard*")))) ;; don't display dashboard if opening a file
(setq dashboard-display-icons-p t) ;; display icons on both GUI and terminal
(setq dashboard-icon-type 'nerd-icons) ;; use `nerd-icons' package
- (setq dashboard-set-file-icons t) ;; per-filetype icons on the list items (nerd-icons colors them by type)
+ (setq dashboard-set-file-icons nil) ;; no per-item icons on the list entries: URL bookmarks have no filename, so they'd render iconless next to file items -- dropping them all keeps the lists uniform
(setq dashboard-set-heading-icons t) ;; nerd-icons on the section titles (Projects/Bookmarks/Recent)
(setq dashboard-center-content t) ;; horizontally center dashboard content
(setq dashboard-bookmarks-show-path nil) ;; don't show paths in bookmarks
diff --git a/modules/dev-fkeys.el b/modules/dev-fkeys.el
index 9fdfa5b3f..80b43600b 100644
--- a/modules/dev-fkeys.el
+++ b/modules/dev-fkeys.el
@@ -5,48 +5,17 @@
;; Layer: 2 (Core UX).
;; Category: C.
;; Load shape: eager.
-;; Eager reason: the F4/F6 developer command entry points.
-;; Top-level side effects: six global F-key bindings; conditionally registers a
-;; C-; P binding.
+;; Eager reason: binds the F4/F6 developer command entry points.
+;; Top-level side effects: global F-key bindings and optional C-; P binding.
;; Runtime requires: cl-lib, system-lib, keybindings.
-;; Direct test load: yes (requires keybindings explicitly).
+;; Direct test load: yes.
;;
-;; Project-aware F-key block for developer workflows:
+;; Project-aware F-key dispatchers. F4 chooses compile/run/clean commands by
+;; project markers; C-F4 and M-F4 are fast paths. F6 runs all project tests or
+;; the current file's tests using language-specific command builders.
;;
-;; F4 completing-read of compile/run candidates filtered by project type
-;; C-F4 fast path: compile only (no-op on interpreted projects)
-;; M-F4 fast path: clean + rebuild (no-op on interpreted projects)
-;; S-F4 recompile (built-in)
-;; F6 completing-read of test candidates: All tests / Current file's tests
-;; C-F6 fast path: current file's tests
-;;
-;; F4 project-type detection runs against the projectile root and falls back
-;; to \\='unknown when no marker matches. Interpreted markers are checked
-;; before compiled markers, so a Python or Node project that also has a
-;; Makefile for tasks classifies as interpreted.
-;;
-;; F6 \"All tests\" delegates to `projectile-test-project'. F6 \"Current
-;; file's tests\" detects the language by extension, derives the runner
-;; command (elisp via the project Makefile, Python via pytest, Go via the
-;; package), and pipes through `compile' from the projectile root.
-;; TypeScript / JavaScript are detected but punted for v1 — the function
-;; signals a user-error rather than guessing a runner.
-;;
-;; M-F6 is reserved for Phase 2b (\"Run a test...\" menu entry with
-;; per-language test-name discovery). Phase 2b also adds buffer-local
-;; last-test memory and tree-sitter-based discovery for Python / Go /
-;; TypeScript. The tree-sitter discovery uses a capture-then-filter pattern
-;; (queries without `:match' / `:equal' / `:pred' predicates, with the
-;; pattern filter applied in Elisp) to sidestep Emacs bug #79687 — Emacs
-;; 30.2 emits unsuffixed `#match' predicates that libtree-sitter 0.26
-;; rejects. The fix lives on Emacs master (commit b0143530) and is
-;; targeted at Emacs 31; it has not been backported to the emacs-30
-;; branch as of 2026-05-03. See Mike Olson's writeup at
-;; https://mwolson.org/blog/emacs/2026-04-20-fixing-typescript-ts-mode-in-emacs-30-2/
-;; for the same workaround applied to font-lock.
-;;
-;; F7 (coverage) is wired in coverage-core.el. F5 is reserved for the debug
-;; ticket and intentionally left unbound here.
+;; Interpreted markers win over compiled markers so task Makefiles do not turn
+;; Python/Node projects into compile-first projects.
;;; Code:
diff --git a/modules/dwim-shell-config.el b/modules/dwim-shell-config.el
index 014194c7b..e8790a489 100644
--- a/modules/dwim-shell-config.el
+++ b/modules/dwim-shell-config.el
@@ -1,99 +1,23 @@
-;; dwim-shell-config.el --- Dired Shell Commands -*- coding: utf-8; lexical-binding: t; -*-
+;;; dwim-shell-config.el --- Dired shell command menu -*- coding: utf-8; lexical-binding: t; -*-
;;
;;; Commentary:
;;
;; Layer: 3 (Domain Workflow).
;; Category: D/P.
;; Load shape: eager.
-;; Eager reason: none; Dired/Dirvish shell commands, a command-loaded deferral
-;; candidate.
+;; Eager reason: none; Dired/Dirvish shell commands can load by command.
;; Top-level side effects: package configuration via use-package.
-;; Runtime requires: cl-lib.
+;; Runtime requires: cl-lib, system-lib.
;; Direct test load: yes.
;;
-;; This module provides a collection of DWIM (Do What I Mean) shell commands
-;; for common file operations in Dired and other buffers. It leverages the
-;; `dwim-shell-command' package to execute shell commands on marked files
-;; with smart templating and progress tracking.
-;;
-;; Features:
-;; - Audio/Video conversion (mp3, opus, webp, HEVC)
-;; - Image manipulation (resize, flip, format conversion)
-;; - PDF operations (merge, split, password protection, OCR)
-;; - Archive management (zip/unzip)
-;; - Document conversion (epub to org, docx to pdf, pdf to txt)
-;; - Git operations (clone from clipboard)
-;; - External file opening with context awareness
-;;
-;; Workflow:
-;; 1. *Mark files in Dired/Dirvish*
-;; - Use =m= to mark individual files
-;; - Use =* .= to mark by extension
-;; - Use =% m= to mark by regexp
-;; - Or operate on the file under cursor if nothing is marked
-;;
-;; 2. *Execute a DWIM command*
-;; - Call the command via =M-x dwim-shell-commands-[command-name]=
-;; - Or bind frequently used commands to keys
-;;
-;; 3. *Command execution*
-;; - The command runs asynchronously in the background
-;; - A =*Async Shell Command*= buffer shows progress
-;; - Files are processed with smart templating (replacing =<<f>>=, =<<fne>>=, etc.)
-;;
-;; 4. *Results*
-;; - New files appear in the Dired/Dirvish buffer
-;; - Buffer auto-refreshes when command completes
-;; - Errors appear in the async buffer if something fails
-;;
-;; Requirements:
-;; The commands rely on various external utilities that need to be installed:
-;; - ffmpeg: Audio/video conversion
-;; - imagemagick (convert): Image manipulation
-;; - qpdf: PDF operations (requires version 8.x+ for secure password handling)
-;; - tesseract: OCR functionality
-;; - pandoc: Document conversion
-;; - atool: Archive extraction
-;; - rsvg-convert: SVG to PNG conversion
-;; - pdftotext: PDF text extraction
-;; - git: Version control operations
-;; - gpgconf: GPG agent management
-;; - 7z (p7zip): Secure password-protected archives
-;;
-;; On Arch Linux, install the requirements with:
-;; #+begin_src bash
-;; sudo pacman -S --needed ffmpeg imagemagick qpdf tesseract tesseract-data-eng pandoc atool librsvg poppler git gnupg p7zip zip unzip mkvtoolnix-cli mpv ruby
-;; #+end_src
-;;
-;; On MacOS, install the requirements with:
-;; #+begin_src bash
-;; brew install ffmpeg imagemagick qpdf tesseract pandoc atool librsvg poppler gnupg p7zip mkvtoolnix mpv
-;; #+end_src
-;;
-;; Usage:
-;; Commands operate on marked files in Dired or the current file in other modes.
-;; The package automatically replaces standard shell commands with DWIM versions
-;; for a more intuitive experience.
-;;
-;; Security:
-;; Password-protected operations (PDF encryption, archive encryption) use secure
-;; methods to avoid exposing passwords in process lists or command history:
-;; - PDF operations: Use temporary files with restrictive permissions (mode 600)
-;; - Archive operations: Use 7z instead of zip for better password handling
-;; - Temporary password files are automatically cleaned up after use
-;; - Note: Switched from zip to 7z for encryption due to zip's insecure -P flag
-;;
-;; Template Variables:
-;; - <<f>>: Full path to file
-;; - <<fne>>: File name without extension
-;; - <<e>>: File extension
-;; - <<b>>: Base name (file name with extension, no directory)
-;; - <<d>>: Directory path
-;; - <<n>>: Sequential number (for batch renaming)
-;; - <<td>>: Temporary directory
-;; - <<cb>>: Clipboard contents
-;; - <<*>>: All marked files
+;; Configures dwim-shell-command actions for marked Dired/Dirvish files:
+;; media conversion, archive/PDF/document operations, external opening, and a
+;; curated transient menu. Commands use dwim-shell templates for marked files or
+;; the current buffer file.
;;
+;; Password-bearing operations avoid command-line secrets by writing temporary
+;; password files with restrictive permissions and deleting them from the process
+;; sentinel after the spawned command exits.
;;; Code:
diff --git a/modules/eat-config.el b/modules/eat-config.el
index ee83adf10..f53baed31 100644
--- a/modules/eat-config.el
+++ b/modules/eat-config.el
@@ -479,8 +479,21 @@ pty; without tmux, moves point up in EAT's emacs-mode buffer."
(defvar eat-mode-map)
(declare-function eat-semi-char-mode "eat")
(declare-function eat-self-input "eat")
+
+(defun cj/eat-text-scale-reset ()
+ "Reset the text scale to its default in the current buffer."
+ (interactive)
+ (text-scale-set 0))
+
(with-eval-after-load 'eat
(keymap-set eat-semi-char-mode-map "C-<up>" #'cj/term-copy-mode-up)
+ ;; Zoom-out and reset reach Emacs, not the pty. EAT binds C-- to
+ ;; eat-self-input (forwarded to the terminal), so without this the font can
+ ;; only grow: C-= / C-+ pass through and zoom in, but C-- never reaches
+ ;; text-scale-decrease. Low cost -- the Claude TUI and tmux don't use Ctrl+-,
+ ;; and C-0 shadows digit-argument inside eat buffers only.
+ (keymap-set eat-semi-char-mode-map "C--" #'text-scale-decrease)
+ (keymap-set eat-semi-char-mode-map "C-0" #'cj/eat-text-scale-reset)
;; Escape forwards ESC to the pty, so it cancels tmux copy-mode (tmux binds
;; Escape to cancel) and works in TUIs; in EAT's own emacs/char mode it returns
;; to semi-char. One key gets out of either copy view.
diff --git a/modules/elfeed-config.el b/modules/elfeed-config.el
index eb2659ab5..e5cbb36c0 100644
--- a/modules/elfeed-config.el
+++ b/modules/elfeed-config.el
@@ -41,6 +41,13 @@
(declare-function eww-browse-url "eww")
(declare-function eww-readable "eww")
+;; elfeed paints its search and entry buffers with manual `face' text properties
+;; (the date, title, feed, and tag faces the theme styles). Left in
+;; `global-font-lock-mode', font-lock overwrites those with syntactic string
+;; fontification, so the buffer loses the theme colors. Exclude both modes, the
+;; same reason dashboard and mu4e are excluded.
+(cj/exclude-from-global-font-lock 'elfeed-search-mode 'elfeed-show-mode)
+
;; ------------------------------- Elfeed Config -------------------------------
(use-package elfeed
diff --git a/modules/erc-config.el b/modules/erc-config.el
index 3e98a66a3..4eac812c4 100644
--- a/modules/erc-config.el
+++ b/modules/erc-config.el
@@ -140,6 +140,8 @@ Change this value to use a different nickname.")
server-buffers))
+(require 'system-lib)
+
(defun cj/erc-switch-to-buffer-with-completion ()
"Switch to an ERC buffer using completion.
If no ERC buffers exist, prompt to connect to a server.
@@ -148,7 +150,7 @@ Buffer names are shown with server context for clarity."
(let* ((erc-buffers (erc-buffer-list))
(buffer-names (mapcar #'buffer-name erc-buffers)))
(if buffer-names
- (let ((selected (completing-read "Switch to ERC buffer: " buffer-names nil t)))
+ (let ((selected (completing-read "Switch to ERC buffer: " (cj/completion-table 'buffer buffer-names) nil t)))
(switch-to-buffer selected))
(message "No ERC buffers found.")
(when (y-or-n-p "Connect to an IRC server? ")
diff --git a/modules/eww-config.el b/modules/eww-config.el
index ff7ddc211..3b0e22dcd 100644
--- a/modules/eww-config.el
+++ b/modules/eww-config.el
@@ -73,6 +73,12 @@
;; --------------------------------- EWW Config --------------------------------
+(require 'system-lib)
+;; eww renders pages with shr, which paints with manual `face' properties. Left
+;; in `global-font-lock-mode' font-lock overwrites them and the page loses its
+;; colors, the same issue as elfeed-show and mu4e-view. Exclude eww-mode.
+(cj/exclude-from-global-font-lock 'eww-mode)
+
(use-package eww
:ensure nil ;; built-in
:bind
diff --git a/modules/flycheck-config.el b/modules/flycheck-config.el
index 1afd3ae6c..613817444 100644
--- a/modules/flycheck-config.el
+++ b/modules/flycheck-config.el
@@ -6,40 +6,17 @@
;; Layer: 2 (Core UX).
;; Category: C/P.
;; Load shape: eager.
-;; Eager reason: general linting setup; spec target is hook-loaded, a deferral
-;; candidate.
-;; Top-level side effects: package configuration via use-package, binds into
-;; cj/custom-keymap through use-package :map.
+;; Eager reason: linting keymap and mode hooks; could become hook-loaded.
+;; Top-level side effects: package config and C-; ? binding.
;; Runtime requires: keybindings.
-;; Direct test load: yes (requires keybindings explicitly).
+;; Direct test load: yes.
;;
-;; This file configures Flycheck for on-demand syntax and grammar checking.
-;; - Flycheck starts automatically only in sh-mode and emacs-lisp-mode
-
-;; - This binds a custom helper (=cj/flycheck-list-errors=) to "C-; ?"
-;; for popping up Flycheck's error list in another window.
-
-;; - It also customizes Checkdoc to suppress only the "sentence-end-double-space"
-;; and "warn-escape" warnings.
-
-;; - It registers LanguageTool for comprehensive grammar checking of prose files
-;; (text-mode, markdown-mode, gfm-mode, org-mode).
-
-;; Note: Grammar checking is on-demand only to avoid performance issues.
-;; Hitting "C-; ?" runs cj/flycheck-prose-on-demand if in an org buffer.
-
-;; The cj/flycheck-prose-on-demand function:
-;; - Turns on flycheck for the local buffer
-;; - Enables LanguageTool checker
-;; - Triggers an immediate check
-;; - Displays errors in the *Flycheck errors* buffer
-
-;; Installation:
-;; On Arch Linux:
-;; sudo pacman -S languagetool
+;; Flycheck configuration for automatic shell/Elisp linting and on-demand prose
+;; grammar checks. C-; ? opens the Flycheck error list, enabling prose checking
+;; first when appropriate.
;;
-;; The wrapper script at scripts/languagetool-flycheck formats LanguageTool's
-;; JSON output into flycheck-compatible format. It requires Python 3.
+;; LanguageTool uses scripts/languagetool-flycheck to adapt JSON output to
+;; Flycheck's checker protocol.
;;; Code:
diff --git a/modules/flyspell-and-abbrev.el b/modules/flyspell-and-abbrev.el
index 376a9dc51..b73bfdf32 100644
--- a/modules/flyspell-and-abbrev.el
+++ b/modules/flyspell-and-abbrev.el
@@ -6,48 +6,18 @@
;; Layer: 2 (Core UX).
;; Category: C/P.
;; Load shape: eager.
-;; Eager reason: text-mode spelling and abbrev hooks; spec target is hook-loaded.
-;; Top-level side effects: package configuration via use-package (mode hooks).
+;; Eager reason: text-mode spelling and abbrev hooks.
+;; Top-level side effects: package configuration via use-package.
;; Runtime requires: cl-lib.
;; Direct test load: yes.
;;
-;; WORKFLOW:
-;; This module provides intelligent spell checking with automatic abbreviation
-;; creation to prevent repeated misspellings.
+;; On-demand Flyspell workflow with automatic abbrev creation from accepted
+;; corrections. C-' checks/corrects nearby misspellings; C-c f toggles Flyspell
+;; with mode-aware behavior.
;;
-;; KEYBINDINGS:
-;; C-' - Main spell check interface (cj/flyspell-then-abbrev)
-;; C-c f - Toggle flyspell on/off (cj/flyspell-toggle)
-;; M-o - Access 'other options' during correction (save to dictionary, etc.)
-;;
-;; SPELL CHECKING WORKFLOW:
-;; 1. Press C-' to start spell checking
-;; 2. Finds the nearest misspelled word above the cursor
-;; 3. Prompts for correction or allows saving to personal dictionary
-;; 4. Press C-' again to move to the next misspelling
-;; 5. Each correction automatically creates an abbrev for future auto-expansion
-;;
-;; FLYSPELL ACTIVATION:
-;; Flyspell is NOT automatically enabled. You activate it manually:
-;; - C-c f - Toggle flyspell on (uses smart mode detection) or off
-;; - C-' - Runs flyspell-buffer then starts correction workflow
-;;
-;; When enabled, flyspell adapts to the buffer type:
-;; - Programming modes (prog-mode): Only checks comments and strings
-;; - Text modes (text-mode): Checks all text
-;; - Other modes: Must enable manually with C-c f
-;;
-;; ABBREVIATION AUTO-EXPANSION:
-;; Each spell correction creates an abbrev that auto-expands the misspelling
-;; to the correct spelling when you type it in the future. This significantly
-;; increases typing speed over time.
-;;
-;; Original idea from Artur Malabarba:
-;; http://endlessparentheses.com/ispell-and-abbrev-the-perfect-auto-correct.html
-;;
-;; NOTES:
-;; The default flyspell keybinding "C-;" is unbound in this config as it's
-;; used for the custom keymap (cj/custom-keymap).
+;; Flyspell is not enabled globally. Programming buffers check comments/strings
+;; when enabled; prose buffers check all text. The default C-; Flyspell binding
+;; is intentionally left free for cj/custom-keymap.
;;; Code:
diff --git a/modules/font-config.el b/modules/font-config.el
index 3272a946e..7617ba01e 100644
--- a/modules/font-config.el
+++ b/modules/font-config.el
@@ -6,51 +6,18 @@
;; Layer: 2 (Core UX).
;; Category: C/P/S.
;; Load shape: eager.
-;; Eager reason: font setup for the first frame, plus font keybindings.
-;; Top-level side effects: binds five global font keys, runs font-installation
-;; checks, configures packages via use-package.
+;; Eager reason: first-frame font setup and font keybindings.
+;; Top-level side effects: font keys, font checks, package config.
;; Runtime requires: host-environment, keybindings.
;; Direct test load: yes.
;;
-;; This module provides font configuration, including:
-;;
-;; 1. Font Management:
-;; - Dynamic font preset switching via `fontaine' package
-;; - Separate configurations for fixed-pitch and variable-pitch fonts
-;; - Multiple size presets for different viewing contexts
-;; - Per-frame font configuration tracking for daemon mode compatibility
-;;
-;; 2. Icon Support:
-;; - All-the-icons integration with automatic font installation
-;; - Nerd fonts support for enhanced icons in terminals and GUI
-;; - Platform-specific emoji font configuration (Noto, Apple, Segoe)
-;; - Emojify package for emoji rendering and insertion
-;;
-;; 3. Typography Enhancements:
-;; - Programming ligatures via `ligature' package
-;; - Mode-specific ligature rules for markdown and programming
-;; - Text scaling keybindings for quick size adjustments
-;;
-;; 4. Utility Functions:
-;; - `cj/font-installed-p': Check font availability
-;; - `cj/display-available-fonts': Interactive font browser with samples
-;; - Frame-aware font application for client/server setups
-;;
-;; Configuration Notes:
-;; - Default preset: BerkeleyMono Nerd Font; height 120 on laptops, 140 on desktops
-;; - Variable pitch: Lexend in the default preset; Merriweather for fallback presets
-;; - Handles both standalone and daemon mode Emacs instances
-;; - Emoji fonts selected based on OS availability
-;;
-;; Keybindings:
-;; - M-S-f: Select font preset (fontaine-set-preset)
-;; - C-z F: Display available fonts
-;; - C-+/C-=: Increase text scale
-;; - C--/C-_: Decrease text scale
-;; - C-c E i: Insert emoji
-;; - C-c E l: List emojis
-;;
+;; Configures fontaine presets, text scaling keys, icon/emoji fonts, and
+;; programming ligatures. Presets are applied per frame so daemon clients get
+;; the intended fixed/variable pitch sizes.
;;
+;; Also carries font-rendering safeguards for known HarfBuzz/font-cache crashes
+;; triggered by emoji and Arabic shaping in this setup.
+
;;; Code:
(require 'host-environment)
@@ -226,7 +193,6 @@ If FRAME is nil, uses the selected frame."
(all-the-icons-nerd-fonts-prefer))
;; ----------------------------- Emoji Fonts Per OS ----------------------------
-;; Set emoji fonts in priority order (first found wins)
(defun cj/setup-emoji-fontset (&optional _frame)
"Set emoji fonts in priority order (first found wins).
diff --git a/modules/jumper.el b/modules/jumper.el
index 61b6464a5..1fbd1293b 100644
--- a/modules/jumper.el
+++ b/modules/jumper.el
@@ -11,72 +11,17 @@
;; Layer: 4 (Optional).
;; Category: O/L.
;; Load shape: eager.
-;; Eager reason: none; navigation helper, a command-loaded deferral candidate.
-;; Top-level side effects: defines a jumper keymap.
+;; Eager reason: none; jump commands can autoload.
+;; Top-level side effects: defines jumper keymap.
;; Runtime requires: cl-lib.
;; Direct test load: yes.
;;
-;; Jumper provides a simple way to store and jump between locations
-;; in your codebase without needing to remember register assignments.
+;; Small register-backed jump list. Locations are stored in numbered registers,
+;; shown through completion with file/line context, and removed explicitly when
+;; no longer useful.
;;
-;; PURPOSE:
-;;
-;; When working on large codebases, you often need to jump between
-;; multiple related locations: a function definition, its tests, its
-;; callers, configuration files, etc. Emacs registers are perfect for
-;; this, but require you to remember which register you assigned to
-;; which location. Jumper automates register management, letting you
-;; focus on your work instead of bookkeeping.
-;;
-;; WORKFLOW:
-;;
-;; 1. Navigate to an important location in your code
-;; 2. Press M-SPC SPC to store it (automatically assigned to register 0)
-;; 3. Continue working, storing more locations as needed (registers 1-9)
-;; 4. Press M-SPC j to jump back to any stored location
-;; 5. Select from the list using completion (shows file, line, context)
-;; 6. Press M-SPC d to remove locations you no longer need
-;;
-;; RECOMMENDED USAGE:
-;;
-;; Store locations temporarily while working on a feature:
-;; - Store the main function you're implementing
-;; - Store the test file where you're writing tests
-;; - Store the caller that needs updating
-;; - Store the documentation that needs changes
-;; - Jump between them freely as you work
-;; - Clear them when done with the feature
-;;
-;; SPECIAL BEHAVIORS:
-;;
-;; - Duplicate prevention: Storing the same location twice shows a message
-;; instead of wasting a register slot.
-;;
-;; - Single location toggle: When only one location is stored, M-SPC j
-;; toggles between that location and your current position. Perfect for
-;; rapid back-and-forth between two related files.
-;;
-;; - Last location tracking: The last position before each jump is saved
-;; in register 'z', allowing quick "undo" of navigation.
-;;
-;; - Smart selection: With multiple locations, completing-read shows
-;; helpful context: "[0] filename.el:42 - function definition..."
-;;
-;; KEYBINDINGS:
-;;
-;; M-SPC SPC Store current location in next available register
-;; M-SPC j Jump to a stored location (with completion)
-;; M-SPC d Delete a stored location from the list
-;;
-;; CONFIGURATION:
-;;
-;; You can customize the prefix key and maximum locations:
-;;
-;; (setq jumper-prefix-key "C-c j") ; Change prefix key
-;; (setq jumper-max-locations 20) ; Store up to 20 locations
-;;
-;; Note: Changing jumper-max-locations requires restarting Emacs or
-;; manually reinitializing jumper--registers.
+;; A single stored location toggles with the current point; each jump records the
+;; previous point in register z for a quick return path.
;;; Code:
diff --git a/modules/keyboard-compat.el b/modules/keyboard-compat.el
index 914a343a6..172f96c7b 100644
--- a/modules/keyboard-compat.el
+++ b/modules/keyboard-compat.el
@@ -6,90 +6,18 @@
;; Layer: 1 (Foundation).
;; Category: F/S.
;; Load shape: eager.
-;; Eager reason: normalizes terminal/GUI key input so the first session's
-;; keybindings resolve consistently.
-;; Top-level side effects: adds `cj/keyboard-compat-terminal-setup' to
-;; `emacs-startup-hook'.
+;; Eager reason: normalizes terminal/GUI key input before custom bindings matter.
+;; Top-level side effects: adds cj/keyboard-compat-terminal-setup to startup.
;; Runtime requires: host-environment.
-;; Direct test load: yes (registers a startup hook; batch-safe).
+;; Direct test load: yes.
;;
-;; This module fixes keyboard input differences between terminal and GUI Emacs.
+;; Normalizes Meta+Shift bindings across GUI and terminal frames. GUI frames
+;; translate M-uppercase events to explicit M-S-lowercase keys; terminal frames
+;; decode arrow escape sequences before key lookup so ESC O prefixes do not trip
+;; M-S bindings.
;;
-;; THE PROBLEM: Meta+Shift keybindings behave differently in terminal vs GUI
-;; =========================================================================
-;;
-;; In Emacs, there are two ways to express "Meta + Shift + o":
-;;
-;; 1. M-O (Meta + uppercase O) - key code 134217807
-;; 2. M-S-o (Meta + explicit Shift modifier + lowercase o) - key code 167772271
-;;
-;; These are NOT the same key in Emacs!
-;;
-;; GUI Emacs behavior:
-;; When you press Meta+Shift+o on your keyboard, GUI Emacs receives M-O
-;; (uppercase O). It does NOT receive M-S-o. This is because the keyboard
-;; sends Shift+o as uppercase 'O', not as a Shift modifier plus lowercase 'o'.
-;;
-;; Terminal Emacs behavior:
-;; Terminals send escape sequences for special keys. Arrow keys send:
-;; - Up: ESC O A
-;; - Down: ESC O B
-;; - Right: ESC O C
-;; - Left: ESC O D
-;;
-;; The problem: ESC O is interpreted as M-O by Emacs! So if you bind M-O
-;; to a function, pressing the up arrow sends "ESC O A", Emacs sees "M-O"
-;; and triggers your function instead of moving up. Arrow keys break.
-;;
-;; THE SOLUTION: Different handling for each display type
-;; ======================================================
-;;
-;; For terminal mode (handled by cj/keyboard-compat-terminal-setup):
-;; - Use input-decode-map to translate arrow escape sequences BEFORE
-;; any keybinding lookup. ESC O A becomes [up], not M-O followed by A.
-;; - Keybindings use M-S-o syntax (some terminals support explicit Shift)
-;; - Disable graphical icons that show as unicode artifacts
-;;
-;; For GUI mode (handled by cj/keyboard-compat-gui-setup):
-;; - Use key-translation-map to translate M-O to M-S-o BEFORE lookup
-;; - This way, pressing Meta+Shift+o (which sends M-O) gets translated
-;; to M-S-o, matching the keybinding definitions
-;; - All 18 Meta+Shift keybindings work correctly
-;;
-;; WHY NOT JUST USE M-O FOR KEYBINDINGS?
-;; =====================================
-;;
-;; We could bind to M-O directly, but:
-;; 1. Terminal arrow keys would break (ESC O prefix conflict)
-;; 2. We'd need to maintain two sets of bindings (M-O for GUI, something
-;; else for terminal)
-;;
-;; By using M-S-o syntax everywhere and translating M-O -> M-S-o in GUI mode,
-;; we have one consistent set of keybindings that work everywhere.
-;;
-;; KEYBINDINGS AFFECTED:
-;; ====================
-;;
-;; The following M-S- keybindings are translated from M-uppercase in GUI:
-;;
-;; M-O -> M-S-o cj/kill-other-window (undead-buffers.el)
-;; M-M -> M-S-m cj/kill-all-other-buffers-and-windows (undead-buffers.el)
-;; M-Y -> M-S-y yank-media (keybindings.el)
-;; M-F -> M-S-f fontaine-set-preset (font-config.el)
-;; M-W -> M-S-w wttrin (weather-config.el)
-;; M-E -> M-S-e eww (eww-config.el)
-;; M-L -> M-S-l cj/switch-themes (ui-theme.el)
-;; M-R -> M-S-r cj/elfeed-open (elfeed-config.el)
-;; M-V -> M-S-v cj/split-and-follow-right (ui-navigation.el)
-;; M-H -> M-S-h cj/split-and-follow-below (ui-navigation.el)
-;; M-T -> M-S-t toggle-window-split (ui-navigation.el)
-;; M-Z -> M-S-z cj/undo-kill-buffer (ui-navigation.el)
-;; M-U -> M-S-u winner-undo (ui-navigation.el)
-;; M-D -> M-S-d dwim-shell-commands-menu (dwim-shell-config.el)
-;; M-I -> M-S-i edit-indirect-region (text-config.el)
-;; M-C -> M-S-c time-zones (chrono-tools.el)
-;; M-B -> M-S-b calibredb (calibredb-epub-config.el)
-;; M-K -> M-S-k show-kill-ring (show-kill-ring.el)
+;; Also provides terminal-specific display fallbacks, such as hiding icon glyphs
+;; that render poorly outside GUI frames.
;;; Code:
diff --git a/modules/local-repository.el b/modules/local-repository.el
index 9ce7a1af3..a9df09d38 100644
--- a/modules/local-repository.el
+++ b/modules/local-repository.el
@@ -1,4 +1,4 @@
-;;; local-repository.el --- local repository functionality -*- lexical-binding: t; coding: utf-8; -*-
+;;; local-repository.el --- Local package archive helpers -*- lexical-binding: t; coding: utf-8; -*-
;; author Craig Jennings <c@cjennings.net>
;;; Commentary:
@@ -6,12 +6,15 @@
;; Layer: 4 (Optional).
;; Category: O/D/P.
;; Load shape: eager.
-;; Eager reason: none; local package-mirror workflow, a command-loaded deferral
-;; candidate.
+;; Eager reason: none; local package mirror commands can autoload.
;; Top-level side effects: none.
-;; Runtime requires: elpa-mirror.
+;; Runtime requires: elpa-mirror when updating the mirror.
;; Direct test load: yes.
;;
+;; Adds the checked-in local package archive to package-archives with high
+;; priority, and provides a command to refresh that archive from installed
+;; packages via elpa-mirror.
+
;;; Code:
(require 'elpa-mirror nil t) ;; optional; cj/update-localrepo-repository fails at call-time if absent
diff --git a/modules/mousetrap-mode.el b/modules/mousetrap-mode.el
index 3817e0081..656d49e2f 100644
--- a/modules/mousetrap-mode.el
+++ b/modules/mousetrap-mode.el
@@ -1,4 +1,4 @@
-;;; mousetrap-mode.el --- -*- coding: utf-8; lexical-binding: t; -*-
+;;; mousetrap-mode.el --- Profile-based mouse event blocking -*- coding: utf-8; lexical-binding: t; -*-
;;
;;; Commentary:
;;
@@ -11,25 +11,12 @@
;; Runtime requires: cl-lib.
;; Direct test load: yes.
;;
-;; Mouse Trap Mode is a minor mode for Emacs that disables most mouse and
-;; trackpad events to prevent accidental text modifications. Hitting the
-;; trackpad and finding my text is being inserted in an unintended place is
-;; quite annoying, especially when you're overcaffeinated.
+;; Global minor mode that blocks accidental mouse edits while preserving allowed
+;; interaction categories per major-mode profile: scroll, click, drag, and
+;; multi-click.
;;
-;; The mode uses a profile-based architecture to selectively enable/disable
-;; mouse events based on the current major mode. Profiles define which
-;; event categories are allowed (scrolling, clicks, drags, etc.), and modes
-;; are mapped to profiles.
-;;
-;; The keymap is built dynamically when the mode is toggled, so you can
-;; change profiles or mode mappings and re-enable the mode without reloading
-;; your Emacs configuration.
-;;
-;; Keymaps are buffer-local via `emulation-mode-map-alists', so each buffer
-;; gets the correct profile for its major mode independently.
-;;
-;; Inspired by this blog post from Malabarba
-;; https://endlessparentheses.com/disable-mouse-only-inside-emacs.html
+;; The mode builds buffer-local emulation keymaps from profiles, so changing a
+;; profile or mode mapping takes effect after toggling the mode.
;;
;;; Code:
diff --git a/modules/mu4e-org-contacts-integration.el b/modules/mu4e-org-contacts-integration.el
index daa12701a..6062b8cf5 100644
--- a/modules/mu4e-org-contacts-integration.el
+++ b/modules/mu4e-org-contacts-integration.el
@@ -2,8 +2,13 @@
;; author: Craig Jennings <c@cjennings.net>
;;; Commentary:
-;; This module provides seamless integration between org-contacts and mu4e's
-;; email composition, enabling automatic contact completion in email fields.
+;;
+;; Completion-at-point integration between org-contacts and mu4e/org-msg compose
+;; buffers. Header fields complete against org contact email strings; message
+;; bodies keep their normal TAB behavior.
+;;
+;; Dependencies are optional at file load. Activation is a no-op when mu4e or
+;; org-contacts is unavailable so the wider config can still load.
;;; Code:
diff --git a/modules/mu4e-org-contacts-setup.el b/modules/mu4e-org-contacts-setup.el
index 64e9a611f..bfb9b1f24 100644
--- a/modules/mu4e-org-contacts-setup.el
+++ b/modules/mu4e-org-contacts-setup.el
@@ -2,8 +2,10 @@
;; author: Craig Jennings <c@cjennings.net>
;;; Commentary:
-;; Simple setup file to enable org-contacts integration with mu4e.
-;; Add this to your mail-config.el or load it after both mu4e and org-contacts.
+;;
+;; Thin activation wrapper for mu4e-org-contacts-integration. If mu4e is loaded,
+;; enable org-contacts completion and disable mu4e's internal contact collector
+;; so completion has one source of truth.
;;; Code:
diff --git a/modules/music-config.el b/modules/music-config.el
index 76fff283b..86f6eb130 100644
--- a/modules/music-config.el
+++ b/modules/music-config.el
@@ -5,90 +5,18 @@
;; Layer: 4 (Optional).
;; Category: O/D/P/S.
;; Load shape: eager.
-;; Eager reason: none; optional music workflow that registers a music keymap, a
-;; command-loaded deferral candidate. EMMS hooks should run only after EMMS.
-;; Top-level side effects: defines a music keymap under cj/custom-keymap, one
-;; global key, package config.
+;; Eager reason: none; optional music workflow that registers a music keymap.
+;; Top-level side effects: defines C-; m map, one global key, package config.
;; Runtime requires: subr-x, user-constants, keybindings.
-;; Direct test load: yes (requires keybindings explicitly).
+;; Direct test load: yes.
;;
-;; Music management in Emacs via EMMS with MPV backend.
-;; Focus: simple, modular helpers; consistent error handling; streamlined UX.
-;;
-;; Highlights:
-;; - Fuzzy add: select files/dirs; dirs have trailing /; case-insensitive; stable order
-;; - Recursive directory add
-;; - Dired/Dirvish integration (add selection)
-;; - M3U playlist save/load/edit/reload
-;; - Radio station M3U creation (streaming URLs supported)
-;; - Playlist window toggling
-;; - Consume mode (remove tracks after playback)
-;; - MPV as player (no daemon required)
-;;
-;; Keybindings (playlist-mode-map):
-;;
-;; Aligned with ncmpcpp defaults where possible (83% match).
-;; Additional EMMS-specific bindings for features ncmpcpp lacks.
-;;
-;; Key Action ncmpcpp default Match
-;; ─── ────── ─────────────── ─────
-;; Playback
-;; SPC pause add_item *
-;; s stop stop ✓
-;; > / n next track next ✓
-;; < / P previous track previous ✓
-;; p play selected (enter) ✓
-;; f seek forward seek_forward ✓
-;; b seek backward seek_backward ✓
-;;
-;; Toggles
-;; r repeat playlist toggle_repeat ✓
-;; t repeat track (none) +
-;; z random toggle_random ✓
-;; x consume toggle_crossfade *
-;; Z shuffle shuffle ✓
-;;
-;; Volume
-;; + / = volume up volume_up ✓
-;; - volume down volume_down ✓
-;;
-;; Info
-;; i song info show_song_info ✓
-;; o jump to playing jump_to_playing ✓
-;;
-;; Playlist management
-;; a add music (fuzzy) add_selected ✓
-;; c / C clear playlist clear_playlist ✓
-;; S save playlist (none) +
-;; L load playlist (none) +
-;; E edit playlist M3U (none) +
-;; g reload playlist (none) +
-;; A append track to M3U (none) +
-;; q quit/bury quit ✓
-;;
-;; Track reordering
-;; S-up move track up (shift-up) ✓
-;; S-down move track down (shift-down) ✓
-;; C-up move track up (alias) (none) +
-;; C-down move track down (alias) (none) +
-;;
-;; Other
-;; R create radio station (none) +
-;;
-;; Legend: ✓ = matches ncmpcpp default
-;; * = intentional divergence (see below)
-;; + = EMMS-only feature
-;;
-;; Intentional divergences from ncmpcpp defaults:
-;;
-;; SPC/p swap: ncmpcpp defaults p=pause, SPC=add_item_to_playlist.
-;; This config uses SPC=pause (more natural in Emacs) and p=play
-;; selected track. Pause via SPC is a common media player convention.
-;;
-;; x=consume vs crossfade: ncmpcpp's crossfade is an mpd daemon
-;; feature. EMMS uses mpv directly, so consume mode (remove tracks
-;; after playback) is more useful here.
+;; EMMS setup using an mpv subprocess player, M3U playlist helpers, fuzzy
+;; file/directory adds, Dired/Dirvish integration, radio-station creation, and
+;; playlist window toggling.
;;
+;; The playlist keymap intentionally follows ncmpcpp where it maps cleanly, with
+;; EMMS-specific additions for M3U editing and consume mode.
+
;;; Code:
(require 'subr-x)
@@ -108,8 +36,6 @@
(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)
@@ -146,9 +72,98 @@
(defvar cj/music-file-extensions '("aac" "flac" "m4a" "mp3" "ogg" "opus" "wav")
"List of valid music file extensions.")
+(defvar cj/music-seek-seconds 5
+ "Seconds to move when seeking forward or backward in the current track.")
+
(defvar cj/music-playlist-buffer-name "*EMMS-Playlist*"
"Name of the EMMS playlist buffer used by this configuration.")
+;;; Subprocess mpv player (reliable playback)
+
+;; The IPC player (emms-player-mpv) drives mpv over a socket -- start mpv idle,
+;; connect, send loadfile. That handshake was leaving mpv loaded but never
+;; streaming, so playback silently failed. Driving mpv with the track as a
+;; direct argument -- the invocation that plays every time -- is the reliable
+;; path. --no-config isolates this mpv from the interactive/video mpv setup so
+;; the two cannot interfere. Pause is in place via process signals; in-track
+;; seek is not available with a subprocess player (the trade for reliability).
+
+(declare-function emms-player "emms")
+(declare-function emms-player-set "emms")
+(declare-function emms-player-simple-start "emms-player-simple")
+(declare-function emms-player-simple-stop "emms-player-simple")
+(defvar emms-player-simple-process-name)
+(defvar emms-player-cj/music-mpv)
+
+(defvar cj/music--mpv-regex
+ (concat "\\(?:\\." (regexp-opt cj/music-file-extensions) "\\'\\)"
+ "\\|\\`\\(?:https?\\|mms\\)://")
+ "Track names the subprocess mpv player handles: music files or stream URLs.")
+
+(defvar cj/music--mpv-socket
+ (expand-file-name "emms/mpv-control.sock" user-emacs-directory)
+ "IPC control socket for the subprocess mpv player.
+mpv opens it per playback via --input-ipc-server. It does NOT affect startup:
+mpv still plays the track passed as a direct argument, so the reliable start is
+unchanged. The socket only carries control commands (seek) to the already
+playing process, which is where the old idle + loadfile handshake failed.")
+
+(defun cj/music--mpv-start (track)
+ "Play TRACK by running mpv with the track name as a direct argument."
+ (emms-player-simple-start (emms-track-name track)
+ 'emms-player-cj/music-mpv
+ "mpv"
+ (list "--no-video" "--no-config" "--really-quiet"
+ (concat "--input-ipc-server=" cj/music--mpv-socket))))
+
+(defun cj/music--mpv-stop ()
+ "Stop the mpv subprocess."
+ (emms-player-simple-stop))
+
+(defun cj/music--mpv-playable-p (track)
+ "Return non-nil if the subprocess mpv player can play TRACK."
+ (and (executable-find "mpv")
+ (memq (emms-track-type track) '(file url))
+ (string-match cj/music--mpv-regex (emms-track-name track))))
+
+(defun cj/music--mpv-pause ()
+ "Pause the mpv subprocess in place by stopping it (SIGSTOP)."
+ (let ((proc (get-process emms-player-simple-process-name)))
+ (when (and proc (process-live-p proc))
+ (signal-process proc 'SIGSTOP))))
+
+(defun cj/music--mpv-resume ()
+ "Resume the paused mpv subprocess (SIGCONT)."
+ (let ((proc (get-process emms-player-simple-process-name)))
+ (when (and proc (process-live-p proc))
+ (signal-process proc 'SIGCONT))))
+
+(defun cj/music--mpv-command (json)
+ "Send JSON (a one-line mpv IPC command) to the control socket.
+A no-op when nothing is playing or the socket is gone, so it never errors."
+ (when (file-exists-p cj/music--mpv-socket)
+ (ignore-errors
+ (let ((proc (make-network-process :name "cj-music-mpv-cmd"
+ :family 'local
+ :service cj/music--mpv-socket
+ :noquery t)))
+ (unwind-protect
+ (progn (process-send-string proc (concat json "\n"))
+ (accept-process-output proc 0.1))
+ (delete-process proc))))))
+
+(defun cj/music-seek-forward ()
+ "Seek `cj/music-seek-seconds' seconds forward in the current track."
+ (interactive)
+ (cj/music--mpv-command
+ (format "{\"command\": [\"seek\", %d, \"relative\"]}" cj/music-seek-seconds)))
+
+(defun cj/music-seek-backward ()
+ "Seek `cj/music-seek-seconds' seconds backward in the current track."
+ (interactive)
+ (cj/music--mpv-command
+ (format "{\"command\": [\"seek\", %d, \"relative\"]}" (- cj/music-seek-seconds))))
+
;;; Buffer-local state
(defvar-local cj/music-playlist-file nil
@@ -843,7 +858,7 @@ For URL tracks: decoded URL."
:commands (emms-mode-line-mode)
:config
(require 'emms-setup)
- (require 'emms-player-mpv)
+ (require 'emms-player-simple)
(require 'emms-playlist-mode)
(require 'emms-source-file)
(require 'emms-source-playlist)
@@ -852,8 +867,13 @@ For URL tracks: decoded URL."
(setq emms-source-file-default-directory cj/music-root)
(setq emms-playlist-default-major-mode 'emms-playlist-mode)
- ;; Use MPV as player - MUST be set before emms-all
- (setq emms-player-list '(emms-player-mpv))
+ ;; Use the reliable subprocess mpv player (built above) - MUST be set before emms-all
+ (setq emms-player-cj/music-mpv
+ (emms-player #'cj/music--mpv-start #'cj/music--mpv-stop
+ #'cj/music--mpv-playable-p))
+ (emms-player-set emms-player-cj/music-mpv 'pause #'cj/music--mpv-pause)
+ (emms-player-set emms-player-cj/music-mpv 'resume #'cj/music--mpv-resume)
+ (setq emms-player-list '(emms-player-cj/music-mpv))
;; Now initialize EMMS
(emms-all)
@@ -862,17 +882,6 @@ For URL tracks: decoded URL."
(emms-playing-time-display-mode -1)
(emms-mode-line-mode -1)
- ;; MPV configuration
- ;; MPV supports both local files and stream URLs
- (setq emms-player-mpv-parameters
- '("--quiet" "--no-video" "--audio-display=no"))
-
- ;; Update supported file types for mpv player
- (setq emms-player-mpv-regexp
- (concat "\\(?:\\`\\(?:https?\\|mms\\)://\\)\\|\\(?:\\."
- (regexp-opt cj/music-file-extensions)
- "\\'\\)"))
-
;; 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)
@@ -908,8 +917,8 @@ For URL tracks: decoded URL."
(">" . cj/music-next)
("P" . cj/music-previous)
("<" . cj/music-previous)
- ("f" . emms-seek-forward)
- ("b" . emms-seek-backward)
+ ("f" . cj/music-seek-forward)
+ ("b" . cj/music-seek-backward)
("q" . emms-playlist-mode-bury-buffer)
("a" . cj/music-fuzzy-select-and-add)
;; Toggles (aligned with ncmpcpp)
diff --git a/modules/nerd-icons-config.el b/modules/nerd-icons-config.el
index e2edb0717..e38db7d80 100644
--- a/modules/nerd-icons-config.el
+++ b/modules/nerd-icons-config.el
@@ -72,7 +72,20 @@ every call. The `memq' check skips when the face is already present."
:after (nerd-icons marginalia)
:hook (marginalia-mode . nerd-icons-completion-marginalia-setup)
:config
- (nerd-icons-completion-mode))
+ (nerd-icons-completion-mode)
+ ;; The `cj/--nerd-icons-color-dir' advice forces `nerd-icons-yellow' onto every
+ ;; dir icon, so the package's inherit-behind `nerd-icons-completion-dir-face'
+ ;; can never win. Redefine the file-category icon so completing-read folders
+ ;; carry the dir face: copy the icon first (the memoized original stays
+ ;; untouched, so dired/dirvish folders are unaffected) and prepend the dir face
+ ;; so it takes the foreground. Files keep their own type face.
+ (cl-defmethod nerd-icons-completion-get-icon (cand (_cat (eql file)))
+ (if (string-suffix-p "/" cand)
+ (let ((icon (copy-sequence
+ (nerd-icons-icon-for-dir cand :height nerd-icons-completion-icon-size))))
+ (add-face-text-property 0 (length icon) 'nerd-icons-completion-dir-face nil icon)
+ (concat icon " "))
+ (concat (nerd-icons-icon-for-file cand :height nerd-icons-completion-icon-size) " "))))
(use-package nerd-icons-ibuffer
:after nerd-icons
diff --git a/modules/org-agenda-config.el b/modules/org-agenda-config.el
index 3234cc929..9ccd21d7b 100644
--- a/modules/org-agenda-config.el
+++ b/modules/org-agenda-config.el
@@ -6,51 +6,18 @@
;; Layer: 3 (Domain Workflow).
;; Category: D/S.
;; Load shape: eager.
-;; Eager reason: daily agenda workflow; the user expects agenda available at the
-;; first session.
-;; Top-level side effects: one add-hook and an idle timer that builds the agenda
-;; file cache 10s after startup (guarded; spec tracks the cache lifecycle).
+;; Eager reason: agenda should be available in the first session.
+;; Top-level side effects: agenda hooks plus guarded idle cache build.
;; Runtime requires: user-constants, system-lib, cj-cache-lib.
;; Direct test load: yes.
;;
-;; Performance:
-;; - Caches agenda file list to avoid scanning projects directory on every view
-;; - Cache builds asynchronously 10 seconds after Emacs startup (non-blocking)
-;; - First agenda view uses cache if ready, otherwise builds synchronously
-;; - Subsequent views are instant (cached)
-;; - Cache auto-refreshes after 1 hour
-;; - Manual refresh: M-x cj/org-agenda-refresh-files (e.g., after adding projects)
+;; Org agenda configuration for global, project-scoped, and buffer-scoped task
+;; views. F8 opens the main agenda; modified F8 bindings narrow by project,
+;; current buffer, or task list.
;;
-;; Agenda views are tied to the F8 (fate) key.
-;;
-;; "We are what we repeatedly do.
-;; Excellence, then, is not an act, but a habit"
-;; -- Aristotle
-;;
-;; "...watch your actions, they become habits;
-;; watch your habits, they become character;
-;; watch your character, for it becomes your destiny."
-;; -- Lao Tzu
-;;
-;;
-;; f8 - MAIN AGENDA which organizes all tasks and events into:
-;; - all unfinished priority A tasks
-;; - the weekly schedule, including the habit consistency graph
-;; - all priority B tasks
-;;
-;; C-f8 - PROJECT AGENDA showing the main agenda filtered to a single project.
-;; Prompts for project selection, then shows overdue/hi-pri/schedule/B tasks
-;; scoped to that project's todo.org plus all calendars and inbox.
-;;
-;; s-f8 - TASK LIST containing all tasks from all agenda targets.
-;;
-;; M-f8 - TASK LIST containing all tasks from just the current org-mode buffer.
-;;
-;; NOTE:
-;; Files that contain information relevant to the agenda are: the inbox, the
-;; schedule-file, the synced calendars, and the per-project todo.org files found
-;; in immediate subdirectories of projects-dir. (org-roam notes are refile
-;; targets, not agenda sources -- see org-refile-config.el.)
+;; Agenda files come from inbox, schedule files, synced calendars, and immediate
+;; project todo.org files. The file list is cached and rebuilt asynchronously to
+;; keep normal agenda opens fast.
;;; Code:
(require 'user-constants)
diff --git a/modules/org-contacts-config.el b/modules/org-contacts-config.el
index 64abb9fb5..944d75c10 100644
--- a/modules/org-contacts-config.el
+++ b/modules/org-contacts-config.el
@@ -168,15 +168,29 @@ Added: %U"
;;; ------------------------- Quick Contact Functions ---------------------------
+(require 'system-lib)
+
(defun cj/org-contacts-find ()
"Find and open a contact."
(interactive)
(find-file contacts-file)
(goto-char (point-min))
- (let ((contact (completing-read "Find contact: "
- (org-map-entries
- (lambda () (nth 4 (org-heading-components)))
- nil (list contacts-file)))))
+ (let* ((alist (org-map-entries
+ (lambda ()
+ (cons (nth 4 (org-heading-components))
+ (or (org-entry-get nil "EMAIL")
+ (org-entry-get nil "PHONE"))))
+ nil (list contacts-file)))
+ (contact (completing-read
+ "Find contact: "
+ (cj/completion-table-annotated
+ 'contact
+ (lambda (cand)
+ (let ((info (cdr (assoc cand alist))))
+ (when (and info (> (length info) 0))
+ (concat " " (propertize info 'face
+ 'completions-annotations)))))
+ alist))))
(goto-char (point-min))
(search-forward contact)
(org-fold-show-entry)
diff --git a/modules/org-webclipper.el b/modules/org-webclipper.el
index f32cad3fd..40ceada76 100644
--- a/modules/org-webclipper.el
+++ b/modules/org-webclipper.el
@@ -5,50 +5,17 @@
;; Layer: 4 (Optional).
;; Category: O/D/P.
;; Load shape: eager.
-;; Eager reason: none; web clipping runs via org-protocol/command, a Phase 4
-;; protocol/command-loaded deferral candidate.
+;; Eager reason: none; protocol and direct clipping can load on command.
;; Top-level side effects: org-protocol handler registration via use-package.
-;; Runtime requires: none (configures packages via use-package).
+;; Runtime requires: none.
;; Direct test load: yes.
;;
-;; This package provides a seamless "fire-and-forget" workflow for clipping
-;; web pages from the browser directly into an Org file using org-protocol
-;; and org-web-tools.
+;; Captures web pages into Org from org-protocol, EWW, or W3M. The protocol path
+;; records URL/title dynamically around org-capture; the direct path clips the
+;; current browser buffer.
;;
-;; Features:
-;; - Browser bookmarklet integration via org-protocol
-;; - Automatic conversion to Org format using eww-readable and Pandoc
-;; - One-click capture from any web page
-;; - Preserves page structure and formatting
-;; - Smart heading adjustment (removes page title, demotes remaining headings)
-;;
-;; Setup:
-;; 1. Ensure this file is loaded in your Emacs configuration
-;; 2. Make sure emacsclient is configured for org-protocol
-;; 3. Add the following bookmarklet to your browser's bookmarks bar:
-;;
-;; javascript:location.href='org-protocol://webclip?url='+encodeURIComponent(location.href)+'&title='+encodeURIComponent(document.title);void(0);
-;;
-;; To add the bookmarklet:
-;; a. Create a new bookmark in your browser
-;; b. Set the name to: Clip to Org (or your preference)
-;; c. Set the URL to the JavaScript code above
-;; d. Save it to your bookmarks bar for easy access
-;;
-;; 4. Click the bookmarklet on any web page to clip its content
-;;
-;; The clipped content will be added to the file specified by `webclipped-file`
-;; under the "Webclipped Inbox" heading with proper formatting and metadata.
-;;
-;; Architecture:
-;; - cj/--process-webclip-content: Pure function for content processing
-;; - cj/org-protocol-webclip-handler: Handles URL fetching and capture
-;; - cj/org-webclipper-EWW: Direct capture from EWW/W3M buffers
-;;
-;; Requirements:
-;; - org-web-tools package
-;; - Pandoc installed on your system
-;; - Emacs server running (M-x server-start)
+;; Content is converted to readable Org, normalized, and filed under the
+;; configured webclip inbox heading.
;;; Code:
@@ -85,7 +52,6 @@ See `cj/--webclip-url' for the binding contract.")
(defun cj/webclipper-ensure-initialized ()
"Ensure webclipper is initialized when first used."
(unless cj/webclipper-initialized
- ;; Load required packages now
(require 'org-protocol)
(require 'org-capture)
(require 'org-web-tools)
diff --git a/modules/selection-framework.el b/modules/selection-framework.el
index 464654a20..7f7f9a475 100644
--- a/modules/selection-framework.el
+++ b/modules/selection-framework.el
@@ -128,7 +128,6 @@
;; Optionally tweak the register preview window.
(advice-add #'register-preview :override #'consult-register-window)
- ;; Configure other variables and modes
(setq xref-show-xrefs-function #'consult-xref
xref-show-definitions-function #'consult-xref)
diff --git a/modules/signal-config.el b/modules/signal-config.el
index 86cb523ce..edb7d0dc3 100644
--- a/modules/signal-config.el
+++ b/modules/signal-config.el
@@ -309,7 +309,13 @@ opens the chosen recipient in `signel-chat'."
(candidates (cons note-self cj/signel--contact-cache))
(table (lambda (string pred action)
(if (eq action 'metadata)
- '(metadata
+ `(metadata
+ (category . signal-contact)
+ (annotation-function
+ . ,(lambda (cand)
+ (let ((r (cdr (assoc cand candidates))))
+ (when r
+ (concat " " (propertize r 'face 'completions-annotations))))))
(display-sort-function . identity)
(cycle-sort-function . identity))
(complete-with-action action candidates string pred))))
diff --git a/modules/system-commands.el b/modules/system-commands.el
index 44ac3ae89..de5e88535 100644
--- a/modules/system-commands.el
+++ b/modules/system-commands.el
@@ -6,14 +6,14 @@
;; Layer: 3 (Domain Workflow).
;; Category: D/S.
;; Load shape: eager.
-;; Eager reason: registers the C-; ! system-command keymap; high-impact commands
+;; Eager reason: binds C-; ! to the system-command menu; high-impact commands
;; that should run only by command (command-loaded target).
-;; Top-level side effects: defines a system-command keymap under cj/custom-keymap.
+;; Top-level side effects: binds C-; ! to the system-command menu in cj/custom-keymap.
;; Runtime requires: keybindings, host-environment, rx.
;; Direct test load: yes (requires keybindings explicitly).
;;
;; System commands for logout, lock, suspend, shutdown, reboot, and Emacs
-;; exit/restart. Provides both a keymap (C-; !) and a completing-read menu.
+;; exit/restart. C-; ! opens a completing-read menu of all commands.
;;
;; Commands include:
;; - Logout (terminate user session)
@@ -28,8 +28,8 @@
;;
;;; Code:
-;; `keybindings' provides `cj/custom-keymap', which is referenced at load
-;; time by the `keymap-set' call at the tail of this file. An
+;; `keybindings' provides `cj/custom-keymap' and `cj/register-command',
+;; referenced at load time by the binding call at the tail of this file. An
;; `eval-when-compile' require would silence the byte-compiler but leave
;; the load-time reference void if anything required `system-commands'
;; before `keybindings'. Make the dependency explicit.
@@ -181,29 +181,10 @@ daemon alive rather than killing the session blindly."
(when-let ((cmd (alist-get choice commands nil nil #'equal)))
(call-interactively cmd))))
-(defvar-keymap cj/system-command-map
- :doc "Keymap for system commands."
- "!" #'cj/system-command-menu
- "L" #'cj/system-cmd-logout
- "r" #'cj/system-cmd-reboot
- "s" #'cj/system-cmd-shutdown
- "S" #'cj/system-cmd-suspend
- "l" #'cj/system-cmd-lock
- "E" #'cj/system-cmd-exit-emacs
- "e" #'cj/system-cmd-restart-emacs)
-(cj/register-prefix-map "!" cj/system-command-map)
-
-(with-eval-after-load 'which-key
- (which-key-add-key-based-replacements
- "C-; !" "system commands"
- "C-; ! !" "system command menu"
- "C-; ! L" "logout"
- "C-; ! E" "exit Emacs"
- "C-; ! S" "suspend"
- "C-; ! e" "restart Emacs"
- "C-; ! l" "lock screen"
- "C-; ! r" "reboot"
- "C-; ! s" "shutdown"))
+;; C-; ! opens the completing-read menu directly. The per-command leaf
+;; keys (s/r/e/l/L/E/S) were removed 2026-06-28 to reclaim the key
+;; real-estate; every command stays reachable through the menu.
+(cj/register-command "!" #'cj/system-command-menu "system commands")
(provide 'system-commands)
;;; system-commands.el ends here
diff --git a/modules/system-lib.el b/modules/system-lib.el
index 49bb6cd1a..8b954c6a9 100644
--- a/modules/system-lib.el
+++ b/modules/system-lib.el
@@ -164,6 +164,29 @@ contributes its own modes regardless of load order."
(setq font-lock-global-modes
(cj/--font-lock-global-modes-excluding font-lock-global-modes mode))))
+(defun cj/completion-table (category collection)
+ "Return a completion table over COLLECTION tagged with completion CATEGORY.
+COLLECTION is anything `completing-read' accepts (list, alist, obarray, hash
+table, or another table). The table reports CATEGORY in its metadata so
+marginalia (and embark, consult, sorting) can recognize and annotate the
+candidates. Use a standard category (file, buffer, function, theme, ...) when
+the candidates match one; marginalia then annotates them with no further work."
+ (lambda (string predicate action)
+ (if (eq action 'metadata)
+ `(metadata (category . ,category))
+ (complete-with-action action collection string predicate))))
+
+(defun cj/completion-table-annotated (category annotate collection)
+ "Like `cj/completion-table' but also attach ANNOTATE as the annotation function.
+ANNOTATE is called with a candidate string and returns its annotation suffix, or
+nil. Use this for a custom CATEGORY that marginalia has no built-in annotator
+for: marginalia falls back to the table's own annotation function."
+ (lambda (string predicate action)
+ (if (eq action 'metadata)
+ `(metadata (category . ,category)
+ (annotation-function . ,annotate))
+ (complete-with-action action collection string predicate))))
+
(defun cj/format-region-with-program (program &rest args)
"Replace the current buffer with PROGRAM ARGS run over its contents, via argv.
Runs PROGRAM (with ARGS) on the whole buffer through `call-process-region'
diff --git a/modules/system-utils.el b/modules/system-utils.el
index 00be88906..b393aa33f 100644
--- a/modules/system-utils.el
+++ b/modules/system-utils.el
@@ -157,6 +157,12 @@ detached from Emacs."
(keymap-set ibuffer-mode-map "d" #'ibuffer-diff-with-file)
(keymap-set ibuffer-mode-map "D" #'ibuffer-mark-for-delete))
+;; ibuffer paints its rows with manual `face' properties (nerd-icons + ibuffer
+;; faces). Left in `global-font-lock-mode', font-lock leaks keyword fontification
+;; onto buffer and mode names, mixing wrong colors in. Exclude it, the same fix
+;; as the shr-rendered reader modes.
+(cj/exclude-from-global-font-lock 'ibuffer-mode)
+
;;; -------------------------- Scratch Buffer Happiness -------------------------
(defvar scratch-emacs-version-and-system
diff --git a/modules/test-runner.el b/modules/test-runner.el
index 50d4f7e40..48a2b09fe 100644
--- a/modules/test-runner.el
+++ b/modules/test-runner.el
@@ -6,69 +6,18 @@
;; Layer: 2 (Core UX).
;; Category: C/L.
;; Load shape: eager.
-;; Eager reason: the test keymap entry point and project-scoped runner state.
-;; Top-level side effects: defines a test keymap, registers it under cj/custom-keymap.
+;; Eager reason: registers the C-; t test runner entry point and state.
+;; Top-level side effects: defines and registers cj/test-map.
;; Runtime requires: ert, cl-lib, keybindings.
-;; Direct test load: yes (requires keybindings explicitly).
+;; Direct test load: yes.
;;
-;; This module provides a powerful ERT test runner with focus/unfocus workflow
-;; for efficient test-driven development in Emacs Lisp projects.
-;;
-;; PURPOSE:
-;;
-;; When working on large Emacs Lisp projects with many test files, you often
-;; want to focus on running just the tests relevant to your current work without
-;; waiting for the entire suite to run. This module provides a smart test runner
-;; that supports both running all tests and focusing on specific test files.
-;;
-;; WORKFLOW:
-;;
-;; 1. Run all tests initially to establish baseline (C-; t R)
-;; 2. Add test files to focus while working on a feature (C-; t a)
-;; 3. Run focused tests repeatedly as you develop (C-; t r)
-;; 4. Add more test files as needed (C-; t b from within test buffer)
-;; 5. View your focused test list at any time (C-; t v)
-;; 6. Clear focus and run all tests before finishing (C-; t c, then C-; t R)
-;;
-;; PROJECT INTEGRATION:
-;;
-;; - Automatically discovers test directories in Projectile projects
-;; (looks for "test" or "tests" under project root)
-;; - Falls back to ~/.emacs.d/tests if not in a Projectile project
-;; - Test files must match pattern: test-*.el
-;;
-;; SPECIAL BEHAVIORS:
-;;
-;; - Smart test running: Automatically runs all or focused tests based on mode
-;; - Test extraction: Discovers test names via regex to run specific tests
-;; - At-point execution: Run individual test at cursor position (C-; t .)
-;; - Error handling: Continues loading tests even if individual files fail
-;;
-;; KEYBINDINGS:
-;;
-;; C-; t L Load all test files
-;; C-; t R Run all tests (full suite)
-;; C-; t r Run tests smartly (all or focused based on mode)
-;; C-; t . Run test at point
-;; C-; t a Add test file to focus (with completion)
-;; C-; t b Add current buffer's test file to focus
-;; C-; t c Clear all focused test files
-;; C-; t v View list of focused test files
-;; C-; t t Toggle mode between 'all and 'focused
-;;
-;; RECOMMENDED USAGE:
-;;
-;; While implementing a feature:
-;; - Add the main test file for the feature you're working on
-;; - Add any related test files that might be affected
-;; - Use C-; t r to repeatedly run just those focused tests
-;; - This provides fast feedback during development
-;;
-;; Before committing:
-;; - Clear the focus with C-; t c
-;; - Run the full suite with C-; t R to ensure nothing broke
-;; - Verify all tests pass before pushing changes
+;; Project-aware ERT runner with two modes: all tests or a focused file set.
+;; Test roots come from Projectile projects, falling back to the config's tests
+;; directory, and test files are discovered by the test-*.el convention.
;;
+;; Commands under C-; t load tests, run all/focused tests, run the test at point,
+;; and manage the per-project focus list.
+
;;; Code:
(require 'ert)
diff --git a/modules/tramp-config.el b/modules/tramp-config.el
index e3b835f1f..f2bc8457c 100644
--- a/modules/tramp-config.el
+++ b/modules/tramp-config.el
@@ -57,7 +57,6 @@
(setq tramp-auto-save-directory
(expand-file-name "tramp-auto-save" user-emacs-directory))
- ;; Create directory if it doesn't exist
(unless (file-exists-p tramp-auto-save-directory)
(make-directory tramp-auto-save-directory t))
diff --git a/modules/transcription-config.el b/modules/transcription-config.el
index e00306d1e..944063b88 100644
--- a/modules/transcription-config.el
+++ b/modules/transcription-config.el
@@ -195,6 +195,8 @@ transcript lands alongside the source, not next to the temp /tmp audio."
(txt-file (car outputs))
(log-file (cdr outputs))
(buffer-name (format " *transcribe-%s*" (file-name-nondirectory audio-file)))
+ (stderr-buffer-name (format " *transcribe-stderr-%s*"
+ (file-name-nondirectory audio-file)))
(process-name (format "transcribe-%s" (file-name-nondirectory audio-file))))
(unless (file-executable-p script)
@@ -203,15 +205,25 @@ transcript lands alongside the source, not next to the temp /tmp audio."
(cj/--init-log-file log-file audio-file script)
(let* ((process-environment (cj/--build-process-environment cj/transcribe-backend))
+ ;; A live, explicitly-managed buffer for stderr. Passing a file PATH
+ ;; to :stderr makes Emacs create a phantom buffer named after the
+ ;; path, so the error text never reaches the log file and that buffer
+ ;; leaks per run; the sentinel drains this buffer into the log and
+ ;; kills it. Keeping stderr off the stdout :buffer leaves the
+ ;; transcript (stdout) clean.
+ (stderr-buffer (with-current-buffer (get-buffer-create stderr-buffer-name)
+ (erase-buffer)
+ (current-buffer)))
(process (make-process
:name process-name
:buffer (get-buffer-create buffer-name)
:command (list script audio-file)
:sentinel (lambda (proc event)
- (cj/--transcription-sentinel proc event audio-file txt-file log-file)
+ (cj/--transcription-sentinel proc event audio-file
+ txt-file log-file stderr-buffer)
(when cleanup-file
(ignore-errors (delete-file cleanup-file))))
- :stderr log-file)))
+ :stderr stderr-buffer)))
(cj/--track-transcription process audio-file)
(cj/--notify "Transcription"
(format "Started on %s" (file-name-nondirectory audio-file)))
@@ -294,20 +306,25 @@ References TXT-FILE on success (normal urgency), LOG-FILE on failure
(format "Errored. Logs in %s" (file-name-nondirectory log-file))
'critical)))
-(defun cj/--transcription-sentinel (process event _audio-file txt-file log-file)
+(defun cj/--transcription-sentinel (process event _audio-file txt-file log-file stderr-buffer)
"Sentinel for transcription PROCESS.
EVENT is the process event string. TXT-FILE and LOG-FILE are the
-associated output files."
+associated output files. STDERR-BUFFER holds the process's stderr; its
+contents are appended to LOG-FILE so the \"Logs in <file>\" notification
+points at real error text, and the buffer is then killed so it does not
+leak per run."
(let* ((success-p (and (string-match-p "finished" event)
(= 0 (process-exit-status process))))
(process-buffer (process-buffer process)))
(cj/--write-transcript-on-success process-buffer success-p txt-file)
- (cj/--append-to-log process-buffer log-file event)
+ (cj/--append-to-log stderr-buffer log-file event)
(cj/--update-transcription-status process success-p)
(when (and success-p (not (cj/--should-keep-log success-p)))
(delete-file log-file))
(when (buffer-live-p process-buffer)
(kill-buffer process-buffer))
+ (when (buffer-live-p stderr-buffer)
+ (kill-buffer stderr-buffer))
(cj/--notify-completion success-p txt-file log-file)
(run-at-time 600 nil #'cj/--cleanup-completed-transcriptions)
(force-mode-line-update t)))
diff --git a/modules/ui-navigation.el b/modules/ui-navigation.el
index cb0fc5697..76dd686a6 100644
--- a/modules/ui-navigation.el
+++ b/modules/ui-navigation.el
@@ -110,7 +110,9 @@ existing split does. No-op when SIDE is nil."
(defun cj/window-resize-sticky ()
"Resize the active window's divider in the just-pressed arrow's direction
\(via `windsize'), then keep `cj/window-resize-map' active so bare arrows keep
-nudging until any other key. Bound to `C-; b <left>/<right>/<up>/<down>'.
+nudging until any other key. Bound to `C-; b <arrow>' and to the global
+`M-<arrow>' keys (each direction); the arrow is read with `event-basic-type',
+so the Meta modifier on the M-<arrow> path is stripped and both behave alike.
When the selected window is the sole window in the frame there is no
divider to move, so the first arrow instead splits a sliver away on the
@@ -119,13 +121,21 @@ buffer; the current window keeps almost the whole frame and the following
arrows shrink it via `windsize', so it reads the same as resizing an
existing split."
(interactive)
- (let ((key (key-description (vector last-command-event))))
+ (let ((key (format "<%s>" (event-basic-type last-command-event))))
(if (one-window-p)
(cj/window--pull-away (cj/window-pull-side key))
(let ((cmd (keymap-lookup cj/window-resize-map key)))
(when cmd (call-interactively cmd)))))
(set-transient-map cj/window-resize-map t))
+;; M-<arrow> mirrors `C-; b <arrow>': one chord to pull a split from a sole
+;; window or nudge a divider. M-<up>/<down> are otherwise unbound; M-<left>/
+;; <right> shed their word-motion, which stays on `C-<left>'/`C-<right>'.
+(keymap-global-set "M-<left>" #'cj/window-resize-sticky)
+(keymap-global-set "M-<right>" #'cj/window-resize-sticky)
+(keymap-global-set "M-<up>" #'cj/window-resize-sticky)
+(keymap-global-set "M-<down>" #'cj/window-resize-sticky)
+
;; ------------------------------ Window Splitting -----------------------------
(defun cj/split-and-follow-right ()
diff --git a/modules/ui-theme.el b/modules/ui-theme.el
index eb4efd9b5..499e71a49 100644
--- a/modules/ui-theme.el
+++ b/modules/ui-theme.el
@@ -37,13 +37,17 @@
;; ------------------------------- Switch Themes -------------------------------
;; loads themes in completing read, then persists via the functions below
+(require 'system-lib)
+
(defun cj/switch-themes ()
"Function to switch themes and save chosen theme name for persistence.
Unloads any other applied themes before applying the chosen theme."
(interactive)
(let ((chosentheme (completing-read "Load custom theme: "
- (mapcar #'symbol-name
- (custom-available-themes)))))
+ (cj/completion-table
+ 'theme
+ (mapcar #'symbol-name
+ (custom-available-themes))))))
(cj/theme-disable-all)
(cj/theme-load-name chosentheme))
(cj/save-theme-to-file))
diff --git a/modules/undead-buffers.el b/modules/undead-buffers.el
index fe43575e9..4780ef227 100644
--- a/modules/undead-buffers.el
+++ b/modules/undead-buffers.el
@@ -32,7 +32,13 @@
(defvar cj/undead-buffer-list
'("*scratch*" "*EMMS-Playlist*" "*Messages*" "*ert*"
"*AI-Assistant*")
- "Buffers to bury instead of killing.")
+ "Buffer names to bury instead of killing (exact match).")
+
+(defvar cj/undead-buffer-regexps nil
+ "Regexps for buffer names to bury instead of killing, alongside
+`cj/undead-buffer-list'. Use for dynamically-named buffer families where an
+exact name can't be pre-listed -- e.g. ai-term agents, named \"agent [<project>]\".
+Register one with `cj/make-buffer-pattern-undead'.")
(defun cj/make-buffer-undead (name)
"Append NAME to `cj/undead-buffer-list' if not present.
@@ -41,6 +47,23 @@ Signal an error if NAME is not a non-empty string. Return the updated list."
(error "cj/bury-alive-add: NAME must be a non-empty string"))
(add-to-list 'cj/undead-buffer-list name t))
+(defun cj/make-buffer-pattern-undead (regexp)
+ "Append REGEXP to `cj/undead-buffer-regexps' if not present.
+A buffer whose name matches REGEXP is buried instead of killed. Signal an
+error if REGEXP is not a non-empty string. Return the updated list."
+ (unless (and (stringp regexp) (> (length regexp) 0))
+ (error "cj/make-buffer-pattern-undead: REGEXP must be a non-empty string"))
+ (add-to-list 'cj/undead-buffer-regexps regexp t))
+
+(defun cj/--buffer-undead-p (name)
+ "Return non-nil when buffer NAME should be buried instead of killed.
+NAME is undead when it is in `cj/undead-buffer-list' (exact) or matches any
+regexp in `cj/undead-buffer-regexps'."
+ (and (stringp name)
+ (or (member name cj/undead-buffer-list)
+ (seq-some (lambda (re) (string-match-p re name))
+ cj/undead-buffer-regexps))))
+
(defun cj/kill-buffer-or-bury-alive (buffer)
"Kill BUFFER or bury it if it's in `cj/undead-buffer-list'."
(interactive "bBuffer to kill or bury: ")
@@ -49,7 +72,7 @@ Signal an error if NAME is not a non-empty string. Return the updated list."
(progn
(add-to-list 'cj/undead-buffer-list (buffer-name))
(message "Added %s to bury-alive-list" (buffer-name)))
- (if (member (buffer-name) cj/undead-buffer-list)
+ (if (cj/--buffer-undead-p (buffer-name))
(bury-buffer)
(kill-buffer)))))
(keymap-global-set "<remap> <kill-buffer>" #'cj/kill-buffer-or-bury-alive)
@@ -60,7 +83,7 @@ Undead-buffers are buffers in `cj/undead-buffer-list'."
(let* ((buf (current-buffer))
(name (buffer-name buf)))
(and
- (not (member name cj/undead-buffer-list))
+ (not (cj/--buffer-undead-p name))
(buffer-file-name buf)
(buffer-modified-p buf))))
diff --git a/modules/video-audio-recording.el b/modules/video-audio-recording.el
index 1672529f7..fce3d9033 100644
--- a/modules/video-audio-recording.el
+++ b/modules/video-audio-recording.el
@@ -6,104 +6,19 @@
;; Layer: 4 (Optional).
;; Category: O/D/S.
;; Load shape: eager.
-;; Eager reason: none; registers a recording keymap, but device probing should
-;; run only on command (command-loaded target).
-;; Top-level side effects: defines cj/record-map and conditionally registers it
-;; under C-; r.
+;; Eager reason: none; records only on command, but registers C-; r at load.
+;; Top-level side effects: defines cj/record-map and registers it when possible.
;; Runtime requires: system-lib, keybindings.
-;; Direct test load: yes (requires keybindings explicitly).
+;; Direct test load: yes.
;;
-;; Desktop video and audio recording from within Emacs using ffmpeg.
-;; Records from both microphone and system audio simultaneously, which
-;; makes it suitable for capturing meetings, presentations, and desktop activity.
-;;
-;; Architecture:
-;; - Audio recordings use ffmpeg directly with PulseAudio inputs → M4A/AAC
-;; - Video recordings differ by display server:
-;; - X11: ffmpeg with x11grab + PulseAudio → MKV
-;; - Wayland: wf-recorder piped to ffmpeg for audio mixing → MKV
-;; (wf-recorder captures the compositor, ffmpeg mixes in audio)
-;;
-;; Process lifecycle:
-;; - Start: `start-process-shell-command` creates a shell running the
-;; ffmpeg (or wf-recorder|ffmpeg) pipeline. Process ref is stored in
-;; `cj/video-recording-ffmpeg-process' or `cj/audio-recording-ffmpeg-process'.
-;; - Stop: SIGINT is sent to the shell's process group so all pipeline
-;; children (wf-recorder, ffmpeg) receive it. We then poll until the
-;; process actually exits, giving ffmpeg time to finalize the container.
-;; - Cleanup: A process sentinel auto-clears the process variable and
-;; updates the modeline if the process dies unexpectedly.
-;;
-;; Note: video-recordings-dir and audio-recordings-dir are defined
-;; (and directory created) in user-constants.el
-;;
-;; Quick Start
-;; ===========
-;; 1. Press C-; r s to run quick setup
-;; 2. Pick a microphone from the list
-;; 3. Pick an audio output — [in use] shows which apps are playing
-;; 4. Press C-; r a to start/stop audio recording
-;; 5. Recording starts - you'll see 󰍬 in your modeline
-;; 6. Press C-; r a again to stop (🔴 disappears)
-;;
-;; Device Setup
-;; ============
-;; C-; r a automatically prompts for device selection on first use.
-;; Device selection lasts for the current Emacs session only.
-;;
-;; Manual device selection:
-;;
-;; C-; r s (cj/recording-quick-setup) - RECOMMENDED
-;; Two-step setup: pick a mic, then pick an audio output to capture.
-;; Both steps show status: [in use], [ready], [available], [muted].
-;; Audio outputs also show which apps are playing through them.
-;; Sorted: in use → ready → available → muted.
-;;
-;; C-; r S (cj/recording-select-devices) - ADVANCED
-;; Manual selection: choose mic and monitor separately.
-;; Use when you need different devices for input/output.
-;;
-;; C-; r d (cj/recording-list-devices)
-;; List all available audio devices and current configuration.
-;;
-;; C-; r w (cj/recording-show-active-audio) - DIAGNOSTIC TOOL
-;; Show which apps are currently playing audio and through which device.
-;; Use this DURING a phone call to see if the call audio is going through
-;; the device you think it is. Helps diagnose "missing one side" issues.
-;;
-;; Pre-Recording Validation
-;; ========================
-;; Every time you start a recording, the system audio device is
-;; validated automatically:
-;; 1. If the configured monitor device no longer exists (e.g.
-;; USB DAC unplugged), it's auto-updated to the current
-;; default sink's monitor.
-;; 2. If no audio is currently playing through the monitored sink,
-;; a warning is shown in the echo area. Recording proceeds
-;; without interruption — run C-; r s to see active streams.
-;;
-;; Testing Devices Before Important Recordings
-;; ============================================
-;; Always test devices before important recordings:
-;;
-;; C-; r t b (cj/recording-test-both) - RECOMMENDED
-;; Guided test: mic only, monitor only, then both together.
-;; Catches hardware issues before they ruin recordings!
-;;
-;; C-; r t m (cj/recording-test-mic)
-;; Quick 5-second mic test with playback.
-;;
-;; C-; r t s (cj/recording-test-monitor)
-;; Quick 5-second system audio test with playback.
-;;
-;; To adjust volumes:
-;; - Use =M-x cj/recording-adjust-volumes= (or your keybinding =r l=)
-;; - Or customize permanently: =M-x customize-group RET cj-recording RET=
-;; - Or in your config:
-;; #+begin_src emacs-lisp
-;; (setq cj/recording-mic-boost 1.5) ; 50% louder
-;; (setq cj/recording-system-volume 0.7) ; 30% quieter
+;; Starts and stops ffmpeg-backed audio/video recordings from Emacs. Audio
+;; captures microphone plus system monitor; video uses x11grab on X11 and
+;; wf-recorder piped into ffmpeg on Wayland.
;;
+;; Recording processes are tracked in module variables, stopped with SIGINT so
+;; containers finalize cleanly, and reflected in the modeline. Device selection
+;; is session-local; quick setup and device tests live under C-; r.
+
;;; Code:
(require 'system-lib)
diff --git a/modules/weather-config.el b/modules/weather-config.el
index 416db0323..017d9e31b 100644
--- a/modules/weather-config.el
+++ b/modules/weather-config.el
@@ -1,4 +1,4 @@
-;;; weather-config.el --- -*- lexical-binding: t; coding: utf-8; -*-
+;;; weather-config.el --- wttrin weather display and modeline setup -*- lexical-binding: t; coding: utf-8; -*-
;; author: Craig Jennings <c@cjennings.net>
;;; Commentary:
;;
@@ -11,9 +11,8 @@
;; Runtime requires: none (configures packages via use-package).
;; Direct test load: yes.
;;
-;; Call M-W to open wttrin with your preferred location list immediately.
-;; Adjust the city list by editing `wttrin-default-locations` or answering wttrin prompts when asked.
-;; Forecasts arrive in an Emacs buffer, so you can stay keyboard-only while checking weather.
+;; Configures wttrin for favorite-location forecasts, mode-line weather, and
+;; whereami-backed geolocation. M-S-w opens the weather buffer.
;;
;;; Code:
@@ -22,10 +21,13 @@
;; ----------------------------------- Wttrin ----------------------------------
(use-package wttrin
- :vc (:url "git@cjennings.net:emacs-wttrin.git"
- :branch "main"
- :rev :newest)
- ;; :load-path "~/code/emacs-wttrin" ;; uncomment + comment :vc above for local dev
+ ;; Load from the local checkout (currently release/0.4.0) so recent wttrin
+ ;; changes are testable without a package pull. Swap back to :vc below for
+ ;; production tracking.
+ :load-path "~/code/emacs-wttrin"
+ ;; :vc (:url "git@cjennings.net:emacs-wttrin.git"
+ ;; :branch "release/0.4.0"
+ ;; :rev :newest)
:demand t ;; REQUIRED: mode-line must start at Emacs startup
:preface
;; Change this to t to enable debug logging
@@ -39,6 +41,9 @@
;; colors) is unchanged.
(setopt wttrin-display-options "F")
(setopt wttrin-favorite-location "New Orleans, LA")
+ ;; Scale the weather font to fit the window width, clamped to a floor/cap
+ ;; (wttrin-font-height-min/-max, default 100/200).
+ (setopt wttrin-auto-fit-font t)
;; Higher-accuracy geolocation via the whereami WiFi-scan script (Google-backed),
;; far better than IP behind a VPN or cellular hotspot. Used by the picker's
;; "Current location (detect)" entry; wttrin falls back to its IP provider if the