aboutsummaryrefslogtreecommitdiff
path: root/modules
Commit message (Collapse)AuthorAgeFilesLines
* feat(custom-buffer-file): extend buffer-source dispatch to mu4e and InfoCraig Jennings2026-05-151-1/+22
| | | | | | | | | | | | | | | | | Add two dispatchers to cj/buffer-source-functions so C-; b p yields a useful link form in two more major modes. mu4e-view-mode returns "mu4e:msgid:<id>" so the result pastes into org as a clickable link and matches mu4e's own org-protocol handler. Falls through to buffer-file-name when point isn't on a real message. Info-mode returns "info:(manual)node" -- the form org-info-store-link produces. file-name-base only strips one extension, so a compressed "emacs.info.gz" comes back as "emacs.info"; trim the trailing ".info" to get the bare manual name. Falls through when Info hasn't populated its current-file / current-node vars yet. Tests cover normal + boundary fallthrough for each new mode.
* refactor(org-noter-config): rebind insert-note to n; sync to angle bracketsCraig Jennings2026-05-151-7/+11
| | | | | | | | | | | | | | | `cj/org-noter-insert-note-dwim' is the most-used action in a noter session; it deserves the doubled-prefix letter. Move it from `C-; n i' to `C-; n n'. Sibling-stepping moves off `n'/`p' (which were sync-next / sync-prev) onto the angle-bracket pair `>'/`<' to free up `n' and to read more naturally as direction. `.' stays as sync-current-note. Updated `which-key' labels to match. Four new ERT tests in `tests/test-org-noter-config-keymap.el' lock the keymap shape so a casual edit doesn't silently drift the layout.
* refactor(calibredb-epub): reorder defuns above their use-package consumersCraig Jennings2026-05-151-69/+69
| | | | | | | | | | | | | | | | | | | | | | | `make compile' had been flagging `cj/calibredb-clear-filters' and `cj/nov-jump-to-calibredb' as "defined multiple times in this file" since 2026-05-12. Investigation: there's only one `(defun ...)' of each in the source -- use-package's `:bind' expansion makes the byte-compiler count the referenced symbol as a definition when the function is defined in the same file, then it sees the real `defun' later and warns about a redefinition. Reorder so each `defun' appears before the `use-package' that references it via `:bind': - `cj/calibredb-clear-filters' moved above (use-package calibredb). - `cj/nov--metadata-get', `cj/nov--file-path', and `cj/nov-jump-to-calibredb' moved above (use-package nov). The two helpers had to move with the public function so the byte-compiler doesn't emit fresh free-function warnings. Source content unchanged; only line positions move. Both duplicate-definition warnings are gone after this; full unit suite still green.
* feat(custom-buffer-file): make C-; b p dispatch by major-mode, with testsCraig Jennings2026-05-151-11/+41
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Old behavior: `C-; b p' called `cj/copy-path-to-buffer-file-as-kill', which only worked in file-visiting buffers and errored otherwise. That meant the most useful "give me a clickable handle on this buffer" key did nothing in eww, elfeed, dired (file-at-point ≠ buffer's default-directory), and other browsing-shaped modes. Replace with a `major-mode'-aware dispatch: - `cj/buffer-source-functions' alist maps major-mode → thunk returning a string (or nil to fall through). - `cj/copy-buffer-source-as-kill' looks up the current mode, calls the thunk, falls back to `buffer-file-name', errors only when both yield nil. - `cj/copy-path-to-buffer-file-as-kill' kept as a `defalias' for backwards compat (the old name is referenced in adjacent tests). First-batch dispatches: - eww-mode -> (eww-current-url) - elfeed-show-mode -> (elfeed-entry-link elfeed-show-entry) - dired-mode -> (dired-get-filename nil t) - dirvish-mode -> same - doc-view / pdf-view: covered by the buffer-file-name fallback (they already set buffer-file-name correctly). 10 new ERT tests cover the dispatch paths, the buffer-file-name fallback, the user-error on nil source, the alias target, and the `C-; b p' keymap entry. which-key label flipped from "copy file path" to "copy buffer source" to match. Deferred to a follow-up task: mu4e-view-mode, org-mode at a heading, help-mode, Info-mode, magit-log/commit/status, xref/grep/ compilation, image-mode, archive-mode -- each needs a format decision before implementation.
* refactor(ai-vterm): retire M-F9 buffer picker; bind to cj/toggle-gptelCraig Jennings2026-05-141-65/+9
| | | | | | | | | | | | | | | | | | | | | | | | | | | M-F9 used to invoke `cj/ai-vterm-pick-buffer' (a buffer picker narrowed to alive AI-agent buffers). In practice the F9 plain-key toggle + C-F9 project picker covered the common cases, and the buffer picker rarely earned its keystroke. Rebind M-F9 to `cj/toggle-gptel' so the F9 family covers the two main in-Emacs AI surfaces at one keystroke each: <f9> ai-vterm toggle (unchanged) C-<f9> ai-vterm picker (unchanged) M-<f9> gptel *AI-Assistant* (NEW) Removed entirely: - `cj/ai-vterm-pick-buffer' (the command itself). - `cj/--ai-vterm-pick-buffer-candidates' (its helper). - `tests/test-ai-vterm--pick-buffer-candidates.el' (deleted). Updated: - `tests/test-ai-vterm--f9-in-vterm.el' binding assertions (vterm-mode-map and global) flipped to `cj/toggle-gptel'. - Module commentary + `cj/ai-vterm' docstring describe the new M-F9 behavior. - `cj/toggle-gptel' lives in `modules/ai-config.el'; the binding stays in `ai-vterm.el' next to the rest of the F9 family so the dispatch shape is visible in one place.
* chore(ai-config): refresh gptel model menusCraig Jennings2026-05-141-8/+7
| | | | | | | | | | | | | | | | | | | Anthropic: bump Opus 4.6 → 4.7 (current frontier). Sonnet 4.6 and Haiku 4.5 stay -- still current. Default `gptel-model' setq also bumped to claude-opus-4-7 in both places it was set. OpenAI: drop the cohort retired from ChatGPT on 2026-02-13 (gpt-4o, gpt-5 original, gpt-4.1, o1). Replace with the current lineup: gpt-5.5 (flagship), gpt-5.4-mini (fast/cheap), o3 (reasoning). All three are documented at developers.openai.com/api/docs/models/. Drive-by: stale docstring example in cj/gptel--current-model-selection bumped to match. gptel's bundled :models list only knows up to May-2025 IDs but gptel-make-anthropic / gptel-make-openai accept any string and pass it straight to the API, so newer names work without a gptel upgrade.
* feat(markdown-config): register markdown as an org src-block languageCraig Jennings2026-05-141-0/+7
| | | | | | | | | | | | | | | | | | | `#+begin_src markdown ... #+end_src' blocks rendered and exported fine but `org-lint' warned on every one of them ("Unknown source block language: 'markdown'"), and `C-c '' inside the block fell back to `fundamental-mode' instead of opening it in `markdown-mode' for editing. Add a `with-eval-after-load 'org' form that pushes `("markdown" . markdown)' onto `org-src-lang-modes'. New ERT test in `tests/test-markdown-config.el' asserts the entry resolves to `markdown' after `(require 'markdown-config)'. Surfaced while clearing `org-lint' on `todo.org' from 55 issues down to 1 -- the last one was this warning on a Linear ticket-body draft that was genuinely markdown. Registering the language is the right fix; relabeling the block as `text' or `example' would lose accuracy.
* refactor(org-config): surface narrowing + sparse-tree under C-; OCraig Jennings2026-05-141-5/+37
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Narrowing and sparse-tree commands existed in the `:bind' block on `C-c'-style shortcuts but nothing in `cj/org-map' surfaced them, so which-key never showed them and discoverability was poor. Add direct bindings under `C-; O', flat (no sub-prefixes for narrow / sparse-tree). Lowercase creates; capital of the same letter cancels: - `n' / `N' narrow-to-subtree / widen - `s' / `S' match-sparse-tree / show-all - `t' / `T' show-todo-tree / show-all - `>' / `<' forward / backward sibling narrow (kept as-is) - `R' reveal-context (no lowercase pair -- `r' is the table-row sub-prefix) Both `S' and `T' resolve to the same `org-show-all' command so the mental model is just "capital cancels the lowercase I just ran" without having to recall which letter the cancel actually lives on. Free up F2: the old `(<f2> . org-reveal)' binding in the org-mode `:bind' block is now redundant with `C-; O R'. Drop it; F2 becomes available for whatever wants it next. Four new ERT assertions in `test-org-config-keymap-ownership.el' lock the shape -- the old sparse-tree-submap test was rewritten for the flat layout and the narrow-submap test became narrow-bindings (also flat).
* refactor(dashboard): regroup launcher icons into 4/4/4 by purposeCraig Jennings2026-05-141-33/+39
| | | | | | | | | | | | | | | | | | | | | | Telegram had landed alone on a third row of one icon, with the first two rows holding a mixed bag (Code next to Email next to Agenda next to Files next to Music; Feeds next to IRC next to Slack next to Flashcards next to Books next to Terminal). No category showed up grouped, and the asymmetry was bugging me every dashboard open. Regroup by what the icons actually do. Three rows of four: - Row 1 Work: Code / Files / Terminal / Agenda - Row 2 Read & Learn: Feeds / Books / Flashcards / Music - Row 3 Communication: Email / IRC / Slack / Telegram Reorder the `define-key' calls on `dashboard-mode-map' to mirror the row layout -- reading the keymap top-to-bottom now matches reading the icons left-to-right. Drive-by fix in the same commit: Music had an icon but no `dashboard-mode-map' keybinding (mouse-only). Bound to `m'.
* feat(transcription): extend dired T to transcribe videos via ffmpeg, with testsCraig Jennings2026-05-142-17/+107
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Pressing `T' in dired/dirvish on an audio file already transcribed it; on a video file it bounced with "Not an audio file". Real recordings ship as .mp4 / .mkv at least as often as raw .m4a, so the one-key flow ended at the wrong place. Pipeline now: - audio path -> direct into `cj/--start-transcription-process' (unchanged). - video path -> async ffmpeg extracts the audio track to a temp .mp3 under `temporary-file-directory' (libmp3lame, VBR q:a 4, ~165kbps -- right size for speech, accepted by every backend), then transcribes that file with the temp marked for cleanup after the transcription sentinel fires. Surface changes: - `cj/video-file-extensions' added to user-constants.el (mp4, mkv, mov, webm, avi, m4v, wmv, flv, mpg, mpeg, 3gp, ogv). - New predicates `cj/--video-file-p' / `cj/--media-file-p'. - New `cj/--extract-audio-from-video' (async ffmpeg with success callback; surfaces `cj/--notify' on failure; user-errors if ffmpeg isn't on PATH). - `cj/--start-transcription-process' gains optional `cleanup-file'. Sentinel deletes it after the existing logic runs. Backwards compatible -- the audio flow doesn't pass it. - `cj/transcribe-audio' renamed to `cj/transcribe-media' (dispatcher on audio vs video). `cj/transcribe-audio-at-point' renamed to `cj/transcribe-media-at-point'. Both old names kept as `defalias' so M-x history and any external references still work. - `T' in dired-mode-map + dirvish-mode-map points at `cj/transcribe-media-at-point'. - Module commentary USAGE block updated. 15 new ERT tests in `tests/test-transcription-video.el' cover the predicates (happy/boundary/error), ffmpeg invocation (correct args + missing-ffmpeg path), the dispatcher (audio direct, video via extraction, non-media rejected), the aliases, and the T binding. One existing test in `test-transcription-status-and-commands.el' updated to stub the new delegate name. Verified locally that ffmpeg is on PATH with libmp3lame, and that the exact arg list my code uses produces a valid MP3 from a synthetic test video.
* refactor: clear transcription C-; T menu, move telega launcher to C-; TCraig Jennings2026-05-142-30/+13
| | | | | | | | | | | | | | | | The transcription menu wasn't earning its top-level keymap slot -- the commands (transcribe-audio, switch-backend, view-transcriptions, kill-transcription) are run rarely enough that `M-x' is fine. Drop the `cj/transcribe-map' keymap, its `(keymap-set cj/custom-keymap "T" ...)' binding, and the which-key labels. Commands stay callable by name. That frees `C-; T' for telega, where the mnemonic actually fits. Move the launcher from `C-; G' to `C-; T'. Update the which-key label, the module commentary, and the keymap-binding test assertion. The dashboard `g' single-letter binding stays put -- `t' there is vterm, so dashboard letters and the global `C-;' prefix don't share a key space anyway.
* refactor(org-config): flatten table ops directly under the org menuCraig Jennings2026-05-141-17/+14
| | | | | | | | | | | | | | Drop the `T' sub-prefix so table operations sit directly under `C-; O': `O r i' / `O r d' for rows, `O c i' / `O c d' for columns. Move `cj/org-clear-element-cache' from `c' (which now hosts the table-column sub-prefix) to capital `C'. Single-key org commands under this menu live on capitals from here on so the lowercase letters stay free for table sub-prefixes. Drop `cj/org-table-map' entirely -- its bindings now live directly on `cj/org-map'. Three tests in `test-org-config-keymap-ownership.el' updated/added: `C' for clear-cache, plus row and column binding assertions.
* refactor(org-config): move org-table-map under the org menuCraig Jennings2026-05-141-9/+11
| | | | | | | | | | | | | | | | `(keymap-set cj/custom-keymap "T" cj/org-table-map)' at top level silently collided with `cj/transcribe-map' bound to the same key in `modules/transcription-config.el'. Whichever module loaded last won, the other prefix became unreachable, and which-key still showed both labels in their respective sections -- so the visible documentation didn't match what actually fired. Move the table map under the existing `cj/org-map' (`C-; O') as the "T" sub-prefix, so `C-; T r i' becomes `C-; O T r i' and friends. The org menu only had one entry before (clear element cache); table operations are a natural neighbor. Frees `C-; T' at the top level for the transcription menu, which was the only other module fighting over it.
* feat(telega-config): guard launcher with a helpful message when telega is ↵Craig Jennings2026-05-142-3/+20
| | | | | | | | | | | | | | | | | | | | missing Without the guard, both `C-; G' and the dashboard Telegram icon trigger telega's autoload stub directly. When the package isn't installed yet the user sees `Cannot open load file: telega' in `*Messages*' with no hint about what to do. Wrap the launcher in `cj/telega' that checks `featurep' / `locate-library' first. If telega is present, delegate to it. Otherwise signal a `user-error' pointing at `scripts/setup-telega.sh' and the manual `M-x package-install RET telega' fallback. Rebind `C-; G' and the dashboard "g" key + Telegram icon callback to the wrapper. Two new tests in `test-telega-config.el' cover the wrapper paths (absent -> user-error with the recovery hint; present -> delegates to `telega') alongside the updated binding assertion.
* refactor(system-commands): use string interactive spec so undercover instrumentsCraig Jennings2026-05-141-2/+9
| | | | | | | | | | | | | | | | `cj/system-cmd' had `(interactive (list (read-shell-command "System command: ")))' -- the destructured-list interactive spec. Undercover.el relies on edebug instrumentation, which doesn't see past that form, so the entire function body registered as 0 hits under coverage even though tests call the function directly. Switch to the equivalent string spec `(interactive "sSystem command: ")'. Same UX (prompt, history, single-string result), but the body now instruments correctly and coverage moves from 34/49 to 50/51. Add one more test that captures the `run-at-time' lambda in `cj/system-cmd-restart-emacs' and invokes it directly so the inner `call-process-shell-command' branch registers, taking coverage to 51/51.
* fix(org-roam-config): save journal buffer after copying DONE taskCraig Jennings2026-05-141-2/+5
| | | | | | | | | | | | | | `cj/org-roam-copy-todo-to-today' tried to save the target journal buffer via `org-after-refile-insert-hook' bound to `#'save-buffer', but that value is the wrong shape (single function instead of a hook list), and the only other save mechanism -- the `:after' advice on `org-refile' that calls `org-save-all-org-buffers' -- doesn't attach until `:defer .5' elapses, so the very first DONE transition after startup leaves the journal unsaved. Drop the broken hook binding and save the target buffer explicitly after the refile call. New ERT test asserts `buffer-modified-p' on the journal buffer is nil after the function returns.
* fix(ai-vterm): force buffer swap after F9 toggle-off in lone windowCraig Jennings2026-05-141-1/+13
| | | | | | The original fix (9600611) set a flag at toggle-off and let the next toggle-on detect it. The flag mechanism is right, but the toggle-off itself wasn't observable when bury-buffer couldn't switch the lone window onto a different buffer -- `bury-buffer' falls back to `switch-to-prev-buffer', which no-ops when the window's prev-buffer history contains only the agent itself (common right after a `C-x 1' that cleared the surrounding windows' histories). Without an observable swap, the second F9 found the agent still displayed and routed back through toggle-off, looping the user with no visible effect. Dispatcher now explicitly forces the window onto another buffer (`(other-buffer agent-buf t)`) when the lone window is still showing the agent after `bury-buffer'. The round-trip test now exercises the real `bury-buffer' path instead of simulating it; a new test asserts the lone window's buffer is non-agent after toggle-off.
* feat(org-agenda): add VERIFICATION and IN-PROGRESS blocks around SCHEDULECraig Jennings2026-05-131-3/+22
| | | | | | | | | | The main "d" agenda view grows two new blocks. A VERIFICATION block lists tasks in the VERIFY TODO state, placed just above the day's SCHEDULE. An IN-PROGRESS block lists tasks in the DOING TODO state, placed just under SCHEDULE. The full block order is now: OVERDUE -> HIGH PRIORITY -> VERIFICATION -> SCHEDULE -> IN-PROGRESS -> PRIORITY B. Scope matches the other blocks (every entry in `org-agenda-files`). Scheduled and deadlined entries are included -- a VERIFY task with a date appears in both VERIFICATION and SCHEDULE, mirroring how HIGH PRIORITY behaves. Habits are skipped via `cj/org-skip-subtree-if-habit`; PROJECT-keyword parents wouldn't match `(todo "VERIFY")` exact-state filters anyway, so no extra skip there. Two new header defvars (`cj/main-agenda-verify-title`, `cj/main-agenda-doing-title`) for symmetry with the existing four. Both blocks reference the shared `cj/--main-agenda-prefix-format` so a format tweak still lands in one place. Five new tests in `test-org-agenda-config-skip-functions.el` lock the block order, each new block's header / prefix-format / skip-function, and the include-scheduled-entries contract.
* fix(telega): don't auto-install at init; let MELPA staleness slideCraig Jennings2026-05-131-0/+15
| | | | | | The use-package form defaulted to `:ensure t`, which made init crash when MELPA's archive index pointed at a dated snapshot tarball (`telega-20260503.1332.tar`) that had already rotated off the server -- the install fetch hit a 404 and took the whole startup down with a `file-error`. Switched to `:ensure nil` so the module configures telega without trying to install it. The `:commands telega` autoload stub still wires `C-; G`; pressing it before the package is installed signals a void-function instead. Commentary now documents the one-time install (`M-x package-refresh-contents` + `M-x package-install RET telega`).
* refactor(org-agenda): extract main-agenda prefix format into a defvarCraig Jennings2026-05-131-4/+10
| | | | | | `org-agenda-custom-commands` inlined ` %i %-15:c%?-15t% s` four times across the "d" command's overdue / high-priority / schedule / priority-B blocks. New `cj/--main-agenda-prefix-format` defvar holds the literal once; every block now references the symbol so a format tweak lands in one place. Regression test walks the "d" command's blocks and asserts each `org-agenda-prefix-format` cell resolves to the shared symbol -- a block that silently diverges fails the check.
* feat(dashboard): add Telegram launcher to a new third rowCraig Jennings2026-05-131-1/+8
| | | | | | `dashboard-navigator-buttons' grows a Row 3 with a single Telegram entry (using the `nf-fa-telegram' icon, launching `telega'). The dashboard-mode-map gets a single-letter `g' shortcut to match the other launcher keys. Two follow-up TODOs filed under the parent telegram task: the TDLib docker setup script (so a fresh-clone install can boot telega without a system-wide TDLib build) and a dashboard-icon-balance pass (Row 3 with one entry is asymmetric; decide whether to leave it or reorganize).
* feat(telega): add telega.el module with docker-backed TDLibCraig Jennings2026-05-131-0/+41
| | | | | | | | New `modules/telega-config.el` configures telega.el as an in-Emacs Telegram client. `telega-use-docker' is on so TDLib runs in a container instead of needing a system-level build -- pairs with a follow-up `scripts/setup-telega.sh' for fresh-clone installs. First-run auth (phone + verification code) is interactive inside `M-x telega' and isn't scripted here. Launcher binding: `C-; G` (mnemonic: teleGram). `C-; t` and `C-; m t` were both taken (test-runner, music's "repeat track"), so the launcher landed on a free top-level letter. Two tests cover the wiring: module loads, launcher is bound.
* feat(vterm): show tmux scrollback history in placeCraig Jennings2026-05-131-2/+7
| | | | | | `cj/vterm-tmux-history' previously used `pop-to-buffer', which routed the history view through display-buffer-alist -- in an agent window that often meant a split or a hand-off to another window, costing the agent its frame slot. `switch-to-buffer' instead drops the history into the selected window directly; the existing quit handler already restores the origin in that same window via `set-window-buffer'. New test asserts the in-place behavior: starting single-window with a vterm origin, invoking the command leaves `(one-window-p)` t with the history buffer in the original slot. The existing render test no longer needs its `pop-to-buffer' stub since `switch-to-buffer' works in batch.
* fix(ai-vterm): preserve single-window layout across F9 toggleCraig Jennings2026-05-131-10/+41
| | | | | | | | When the agent buffer is the only window in the frame, F9 buries it (correct) but the next F9 redisplayed it as a side split instead of restoring the full-frame layout -- the display-saved path always called `display-buffer-in-direction`, which insists on a split. New `cj/--ai-vterm-last-was-bury` flag tracks which toggle-off path ran. `cj/ai-vterm` sets it to t in the bury branch (one-window-p) and nil in the delete-window branch. `cj/--ai-vterm-display-saved` checks the flag at toggle-on: if t and the frame is still single-window, it replaces the selected window's buffer in place rather than splitting. Either branch consumes the flag so it never stays stale. 5 tests in test-ai-vterm--single-window-toggle.el cover the flag's set/clear paths, the still-one-window guard, and the end-to-end roundtrip.
* feat(org-agenda): use project name as todo.org categoryCraig Jennings2026-05-131-0/+44
| | | | | | The %c column on agenda blocks rendered every project's todo.org as "todo:" -- org defaults the buffer category to the filename without extension, so every entry looked alike. An org-mode-hook now overrides org-category with the parent directory's basename (stripping a single leading dot, so ~/.emacs.d/todo.org reads as "emacs.d") whenever a todo.org file opens and its category is still the filename default. Explicit #+CATEGORY: keywords still win. 14 tests in test-org-agenda-config-category.el cover the helper's normal/boundary/error paths and the hook's override + explicit-category-preserved cases.
* fix(ui-navigation): clear dedicated before toggling window splitCraig Jennings2026-05-131-22/+32
| | | | | | | | toggle-window-split swapped buffers between two windows via two set-window-buffer calls. If either window was strongly dedicated (e.g. *Org Agenda* via the display-buffer-alist rule), the dedicated window rejected the second swap. Both panes ended up showing the dedicated buffer. The non-dedicated buffer never made it across. The fix clears dedicated on both windows before the swap. The user explicitly invoked a layout change, so preserving per-window dedicated through the operation would just re-trigger the same wedge on the next toggle. Tests in tests/test-ui-navigation--toggle-window-split.el cover the no-dedicated baseline, the bug-trigger (dedicated selected window), the post-toggle cleared state, and the one-window / three-window no-op boundaries.
* fix(org-agenda): skip CANCELLED entries from main agenda SCHEDULECraig Jennings2026-05-131-0/+4
| | | | | | | | The "d" command's (agenda ...) block had no org-agenda-skip-function. The global org-agenda-skip-scheduled-if-done is nil. CANCELLED tasks with a SCHEDULED date rendered in the forward-looking schedule unfiltered. The fix adds an org-agenda-skip-function to the SCHEDULE block: (org-agenda-skip-entry-if 'todo '("CANCELLED")). The scope is deliberate. Only CANCELLED is filtered, not DONE or FAILED. A scheduled DONE task is a record of when something happened and stays visible. Tests cover the configuration: the form must appear on the agenda block and must not appear on the overdue, hi-pri, or priority-B blocks.
* fix(calibredb-epub): point cj/nov--file-path at nov-file-nameCraig Jennings2026-05-131-4/+4
| | | | | | The fallback chain was checking `nov-epub-filename` and `nov-epub-file`, but neither symbol exists in nov.el — the real var is `nov-file-name`, set by `nov-mode` from the visited file. Both `boundp` arms always returned nil, so the fallbacks were dead code. The bug was dormant rather than active: `buffer-file-name` always holds the EPUB path for normal nov buffers and covered the first arm of the `or`. I replaced both wrong-named arms with a single live arm on `nov-file-name`, and added a Boundary test that exercises it.
* refactor(calibredb-epub): extract the nov image-padding helperCraig Jennings2026-05-131-4/+12
| | | | I pulled the image-centering math out of `cj/nov-center-images` into `cj/--nov-image-padding-cols`. The wrapper still loops over the image text properties and writes the line/wrap-prefix. The helper takes col-width, img-px, and font-width-px and returns the left padding. With the math in a pure helper I can unit-test the centering rule directly instead of building a buffer with image-display properties.
* fix(prog-webdev): toggle electric-pair-local-mode in the setup hookCraig Jennings2026-05-121-1/+1
| | | | `cj/webdev-setup` called `(electric-pair-mode t)`, which flips the global minor mode every time a TS/JS/TSX buffer opens. I switched it to `electric-pair-local-mode` so the pairing stays scoped to those buffers, like the other buffer-local settings in the hook. (Same fix I just made in `prog-python.el`.)
* refactor(prog-webdev): extract the prettier format-command builderCraig Jennings2026-05-121-4/+7
| | | | I pulled the prettier command-string construction out of `cj/webdev-format-buffer` into `cj/--webdev-format-command`. The wrapper stays thin: resolve `(or buffer-file-name "file.ts")`, hand the command to `shell-command-on-region`, clamp point. With the command shape in a pure helper I can unit-test it directly.
* refactor(org-drill): hoist the commands out of :config and clear the ↵Craig Jennings2026-05-121-88/+94
| | | | | | | | byte-compile warnings The `cj/drill-*` defuns and `cj/drill-map` lived inside the `use-package org-drill` `:config` block, so the byte-compiler never registered them — every cross-reference between them warned ("function `cj/drill-this-file' is not known", and so on). I moved all of that to module top level, where the compiler sees it. The ten `(setq org-drill-...)` lines became a `:custom` block (no more "assignment to free variable"). Added `(require 'user-constants)` and `(require 'keybindings)` for `drill-dir` and `cj/custom-keymap`, plus `declare-function` for `org-drill`, `org-drill-resume`, `org-capture`, and `org-refile`. The module byte-compiles clean now, and `C-; D` still mounts the drill submenu with the same leaf keys. I also gave `tests/test-org-drill-first-function.el` a `cj/custom-keymap` stub: its "loads without error" test does a bare `load` of the module, which now runs the keymap mount at load time instead of deferring it inside `:config`.
* fix(ai-vterm): make F9 toggle the agent from inside an agent bufferCraig Jennings2026-05-121-0/+12
| | | | | | vterm binds `<f1>`..`<f12>` to `vterm--self-insert`, so a plain `<f9>` typed while point is in an agent buffer goes to the terminal program instead of the global toggle. That's invisible most of the time — you press F9 from another window — but it bites when the agent buffer is the only window in the frame, because there's nowhere else to press it from. I re-bound the F9 family in `vterm-mode-map` (via `with-eval-after-load 'vterm`) so that `<f9>`, `C-<f9>`, and `M-<f9>` reach `cj/ai-vterm`, `cj/ai-vterm-pick-project`, and `cj/ai-vterm-pick-buffer` from there too. The C-/M- variants aren't actually in vterm's intercept set, but binding them keeps things uniform. New `tests/test-ai-vterm--f9-in-vterm.el`: 4 ERT tests over the `vterm-mode-map` and global bindings. F12's `cj/vterm-toggle` has the same shape of bug and isn't touched here.
* feat(dirvish): start org-drill on the .org file at point with SCraig Jennings2026-05-121-1/+17
| | | | | | `S` ("study") in `dirvish-mode-map` opens the `.org` file at point and runs `cj/drill-this-file` on it, so I can drill any deck straight from the file list. It `user-error`s on no file, on a directory, or on a non-`.org` file. `D` and `O` were already taken (duplicate-file, open-with-command), so I went with `S`. It shadows dired's `dired-do-symlink`, which I never use from dirvish and which stays on `M-x` anyway. New `tests/test-dirvish-config-drill.el`: 6 ERT tests with `dired-get-filename`, `find-file`, and `cj/drill-this-file` mocked. I also fixed the stale `P` line in the module commentary — `P` is the print command now, not copy-path.
* feat(org-drill): drill any Org file from anywhereCraig Jennings2026-05-121-15/+41
| | | | | | | | `org-drill` has no fixed home — with `org-drill-scope` left at its default it just drills the current buffer. So the only thing in my config tied to a location was `cj/drill-start`, which forced a pick from `drill-dir`. `C-; D f` (`cj/drill-this-file`) drills whatever Org buffer is current, so a drill file living anywhere works. It `user-error`s when the buffer isn't an Org buffer. `C-u C-; D s` (and `C-u C-; D e`) now prompts for the directory to pick from instead of always using `drill-dir`. Bare `C-; D s` is unchanged. I pulled the picking logic into `cj/--drill-files-in`, `cj/--drill-pick-dir`, and `cj/--drill-pick-file` so it's unit-testable. New `tests/test-org-drill-config.el`: 12 ERT tests over those helpers, `cj/drill-this-file`, `cj/drill-start`, and the keymap.
* fix(prog-python): toggle electric-pair-local-mode in the setup hookCraig Jennings2026-05-121-1/+1
| | | | `cj/python-setup` called `(electric-pair-mode t)`, which flips the global minor mode every time a Python buffer opens. I switched it to `electric-pair-local-mode` so the pairing stays scoped to Python buffers, like the other buffer-local settings in the hook.
* refactor(prog-python): extract the mypy and pdb command buildersCraig Jennings2026-05-121-3/+10
| | | | I pulled the command-string construction out of `cj/python-mypy` and `cj/python-debug` into `cj/--python-mypy-command` and `cj/--python-debug-command`. The wrappers stay thin — resolve the target, hand off to `compile` or `pdb`. With the command shape in a pure helper I can unit-test it directly instead of stubbing `compile` and `pdb`.
* fix(nov): center the EPUB text by setting window margins directlyCraig Jennings2026-05-121-37/+55
| | | | | | | | | | | | The 80% width from `4d9a206' wasn't actually narrowing the page: `cj/nov-apply-preferences' set `nov-text-width' to t (nov renders the text unfilled, one long line per paragraph) and counted on `visual-fill-column-mode' to set the window's display margins, but those margins never got applied in nov-mode buffers (even after manually re-running the layout), so the text wrapped at the full window width. The cause is still unknown. This drops `visual-fill-column' from nov entirely: - `nov-text-width' is a column count (~80% of the window's natural width), so nov's `shr' fills the text itself. - `cj/nov-update-layout' sets the window's left/right margins directly to `(natural - text-width) / 2' each, centering the block, and pushes the fringes out to the window edge so they don't show as thin lines beside the text. When the width changes it re-renders, restoring the reading position approximately. - `cj/nov-apply-preferences' adds a `kill-buffer-hook' that drops the margins and fringes when the EPUB buffer goes away, so a later buffer in that window isn't left indented. - `+'/`=' and `-'/`_' adjust `cj/nov-margin-percent' and re-flow + re-center. The text-width math moved into a `cj/nov--natural-window-width' helper alongside the existing `cj/nov--text-width'. Known nit: the centering is a touch left-of-center because shr wraps at word boundaries, so the rendered text is a bit narrower than `nov-text-width' and the right margin ends up slightly larger. Logged as a follow-up.
* chore(ui): disable cursor blinking (keep a solid block)Craig Jennings2026-05-121-0/+4
|
* feat(nov): default the EPUB text column to 80% of the windowCraig Jennings2026-05-121-3/+3
| | | | `1c5c8bd' had set `cj/nov-margin-percent' to 12 (~76% text); 10 gives a round 80%. Adjust per-buffer with the `+'/`-' keys; clamp is unchanged (0..25, i.e. 50%..100%).
* feat(org): add reveal.js present command and header removalCraig Jennings2026-05-121-5/+81
| | | | | | `C-; p SPC' (`cj/reveal-present') is the new fast path: it inserts reveal.js headers if the buffer doesn't have them (prompting for a title), saves the buffer, exports to self-contained HTML, and opens it in the browser. It's the export-and-open you'd otherwise reach by doing `C-; p h' then `C-; p e'. `C-; p H' (`cj/reveal-remove-headers') strips the header block this module inserts, and `C-; p h' (`cj/reveal-insert-header') now errors instead of duplicating headers when a reveal block is already present. The header logic moved into small helpers (`cj/--reveal-has-header-p', `cj/--reveal-remove-headers', `cj/--reveal-ensure-header', `cj/--reveal-keyword-regexp', and the `cj/--reveal-header-keywords' list), and `test-org-reveal-config-headers.el' drives them directly: detection on a reveal vs. a plain Org buffer, removal of the generated block (and only the leading block), body preservation, and the no-duplicate-headers error.
* fix(org): give the F8 agenda window 75% of the frameCraig Jennings2026-05-121-4/+16
| | | | | | The agenda buffer's `display-buffer-alist' rule used `(window-height . fit-window-to-buffer)', so a sparse agenda opened as a sliver a few lines tall. The rule now takes `(window-height . cj/org-agenda-window-height)', a defcustom defaulting to 0.75 (the fraction of the frame the agenda window gets), and the rule itself moved into `cj/--org-agenda-display-rule' so it's testable. New `test-org-agenda-config-display.el' checks that the configured fraction flows through, that it's no longer `fit-window-to-buffer', and (integration) that `display-buffer' produces a window near that size. `(use-package alert)' gained an `:if (or (not noninteractive) (require 'alert nil t))' guard: the batch test runner loads this module without `package-initialize', so the optional notification package may be installed but not yet on the load path, and the unconditional `:config' setq's would error.
* fix(nov): rework the EPUB reading-width layoutCraig Jennings2026-05-121-14/+61
| | | | | | | | | | | | | | `cj/nov--text-width-for-window' computed the target column as a percentage of `(window-body-width)'. But body width is the column count *after* the display margins. `cj/nov-update-layout' runs from `window-configuration-change-hook': it sets `visual-fill-column''s margins, which changes the body width, which fires the hook, which re-runs the layout against the now-narrower body, and so on. It's a shrinking feedback loop that bottoms out at `cj/nov-min-text-width' (40 columns) no matter what `cj/nov-margin-percent' is. That's why the column was a thin strip regardless of the margin setting. The width is now computed from the window's *natural* column count (body width plus any margins already set), so re-running the layout is idempotent. The margin math moved into a pure `cj/nov--text-width' helper, which is what the unit tests drive, and there's a regression test that the result is the same whether or not margins are already in place. Also: - `+'/`=' (`cj/nov-widen-text') and `-'/`_' (`cj/nov-narrow-text') step `cj/nov-margin-percent' by `cj/nov-margin-step' and re-lay-out, reporting the new percentage. `cj/nov-margin-percent' is now clamped to 0..25, so the text column runs from 50% (the floor) to 100% (the full window). - `cj/nov-margin-percent' default is 12 (≈76% text) for a comfortable starting width. - `cj/nov-apply-preferences' re-renders the document at the end again. `b3b537f' removed that on the theory `visual-fill-column' would re-trigger the render. The first page came up off-center until a manual resize, so it's back. - `cj/nov-update-layout' is now a command. The visible result (a ~75% centered column on first open, `+`/`-` to adjust) needs a restart to confirm. The tests cover the width math and clamping, idempotency, the adjust commands and their keybindings, the command status, and the re-render.
* feat(dirvish): print the file at point with PCraig Jennings2026-05-121-1/+47
| | | | | | `P' in dirvish/dired now runs `cj/dirvish-print-file': it sends the file at point to the default printer via CUPS (`lp', falling back to `lpr'), after a confirmation prompt. It refuses directories and file types outside `cj/dirvish-print-extensions' (pdf, txt, org, images, source files, ...). CUPS filters handle PDFs directly, so `lp <file>' covers everything in the list, and no separate print dialog is needed. `P' was the project/home-relative variant of `cj/dired-copy-path-as-kill', now dropped (the `p' absolute-path and `l' org-link bindings stay, and `M-x cj/dired-copy-path-as-kill' still works). Plain dired's built-in `P' was `dired-do-print', which `dirvish-mode-map' had already shadowed, so this just replaces it with a type-aware version. Tests cover the predicates and the command's confirm / decline / no-file / directory / non-printable / no-printer / print-failure paths.
* fix(mail): clear the marks after saving from the attachment selectorCraig Jennings2026-05-121-1/+7
| | | | `cj/mu4e-attachment-selection-save-marked' left the `[x]' marks set after a successful save, so a second `s' silently re-saved the same files. It now unmarks every row and re-renders once the save returns, so the buffer stays open for another batch (and `q'/RET still exit). The save-marked test asserts the marks are cleared.
* fix(slack): error when adding a reaction outside a Slack bufferCraig Jennings2026-05-121-7/+7
| | | | `cj/slack-message-add-reaction' wrapped its whole body in `when-let*' on `slack-current-buffer', so invoking C-; S ! outside a Slack message view did nothing at all, and with no message it looked like the key wasn't even bound. It now `user-error's "Not in a Slack buffer". A test covers the case.
* refactor(lsp): rename cj/lsp--disable-eldoc-hover for accuracyCraig Jennings2026-05-121-3/+4
| | | | The helper removes lsp-mode's entry from `eldoc-documentation-functions' in the current buffer. It never touched hover display, so the old name was misleading. It's now `cj/lsp--remove-eldoc-provider', the two tests rename to match, and the docstring drops the "hover" wording.
* refactor(mail): fail fast on an attachment part with no MIME handleCraig Jennings2026-05-121-1/+1
| | | | `cj/mu4e--save-attachment-part' called `cj/mu4e--ensure-attachment-save-functions' first, so a part with no MIME handle triggered the `mu4e-mime-parts' load before the handle check could fail. The handle check now runs first, so the malformed input is caught right away and the user-error fires the same way whether or not mu4e's MIME support is loadable. The test for that case drops the mu4e stubs it only needed because the load used to come first.
* refactor(mail): extract the mu4e attachment workflow into its own moduleCraig Jennings2026-05-122-229/+248
| | | | | | The attachment-save UI (the MIME-part filters, the three save commands, and the `special-mode'-derived selection buffer) was ~230 lines in `mail-config.el' and depended on nothing else there. It moves to `modules/mu4e-attachments.el', which `mail-config' now requires. `cj/email-map' and its C-; e bindings stay put. The keymap just points at commands that now live next door. The unit tests move with it: `test-mail-config-attachments.el' becomes `test-mu4e-attachments.el' and requires the new module directly instead of pulling in the whole mu4e and org-msg use-package stack. The two tests that check `cj/email-map' wiring move to a new `test-mail-config.el', since that map belongs to `mail-config'. One of the moved tests quietly relied on a real mu4e install (it loaded `mu4e-mime-parts' through a load-path entry that loading `mail-config' happens to add), so it now stubs that path itself.
* docs: clarify the coverage-exclude and token-seed commentsCraig Jennings2026-05-111-0/+2
| | | | The Makefile's `COVERAGE_EXCLUDE' comment said why `test-all-comp-errors.el' is excluded but not why `test-lorem-optimum-benchmark.el' is. It now notes that undercover's instrumentation slows execution enough to fail the benchmark's wall-clock assertions. And `cj/markov-generate' now has a comment explaining why `tokens' is seeded reversed (`(list w2 w1)'): the list is built with `push' and `nreverse'd at the end, so without the note the reversed seed reads like a bug at a glance.