aboutsummaryrefslogtreecommitdiff
path: root/tests
Commit message (Collapse)AuthorAgeFilesLines
* fix(ui-config): use the writeable cursor color in a live vtermCraig Jennings2026-05-111-0/+93
| | | | | | `vterm-mode' sets `buffer-read-only', so `cj/set-cursor-color-according-to-mode' painted the cursor with the read-only color (orange) whenever point was in a vterm. That includes the live terminal, not just `vterm-copy-mode'. But a live terminal takes input: keystrokes go to the process, not the buffer. So a live vterm now reports `unmodified' instead. `vterm-copy-mode' still reports `read-only': there it really is a read-only Emacs buffer the user navigates, and the orange cursor is the right signal. I pulled the state cond out of `cj/set-cursor-color-according-to-mode' into `cj/--buffer-cursor-state' so it's unit-testable without a real frame or `set-cursor-color'.
* refactor(ai-vterm): rename Claude-specific names to a generic "agent"Craig Jennings2026-05-1120-369/+369
| | | | | | | | | | | | | | | | | I may add other terminal agents to this launcher (aider, an open-source LLM TUI), so the buffer prefix, the user knob, and the internal helpers shouldn't say "Claude". The module name (ai-vterm) and the `cj/ai-vterm-*` customs were already generic. This finishes the job: - buffer prefix `claude [<basename>]` -> `agent [<basename>]` (the `defconst` and the matching display-buffer-alist regex move together) - `cj/ai-vterm-claude-command` -> `cj/ai-vterm-agent-command` (the default still runs the `claude` CLI, with a docstring note on swapping it) - `cj/--ai-vterm-claude-buffers` / `-displayed-claude-window` / `-reuse-existing-claude` -> `-agent-*`, and their test files renamed to match - prose in the module commentary and docstrings, plus the matching test docstrings and buffer-name literals `vterm-config.el` hardcodes the same buffer prefix in `cj/--vterm-toggle-buffer-p` (F12 excludes agent buffers from its candidate set), so that literal moved too. Collapsing it into the shared `cj/--ai-vterm-name-prefix` is a cleanup for another day. After a reload, a project's buffer opens as `agent [foo]` instead of `claude [foo]`. Old buffers keep their names until killed. I also corrected two stale `eshell-vterm-config.el` references in ai-vterm.el docstrings (that module was split into `vterm-config.el`). Two things keep saying "Claude": the `cj/ai-vterm-agent-command` default value (the actual CLI), and the "Claude Code" example in `vterm-config.el`'s cursor-restore docstring (a concrete TUI example, not branding). 90 tests pass. `make validate-modules` clean.
* feat(ai-vterm): name the tmux session's first window "ai"Craig Jennings2026-05-111-0/+16
| | | | I pass `tmux new-session -n` so the window running the AI tool shows up as "ai" in the window list instead of auto-naming after the running program. A shell opened by hand in a later window still auto-names (e.g. "zsh"), so the two read distinctly. The name is a new `defcustom` (`cj/ai-vterm-tmux-window-name`), symmetric with the session-prefix custom.
* feat(ai-vterm): surface surviving tmux sessions in the project pickerCraig Jennings2026-05-115-40/+225
| | | | | | Each project's tmux session is now named `<cj/ai-vterm-tmux-session-prefix><basename>` (default `aiv-`), so `tmux ls` can be filtered to AI-vterm's own sessions. After an Emacs crash the C-F9 project picker reads `tmux list-sessions`, matches surviving sessions back to their directories, and sorts those to the top: `[detached]` when only the tmux session is alive, `[running]` when a vterm buffer exists. The rest follow alphabetically. With tmux missing or no server running, it falls back to a plain alphabetical list. The picker's collection is a completion table that pins display order so Vertico doesn't re-sort and undo the active-first grouping. The prefix is a new `defcustom` rather than `claude-`, which collides with hand-rolled tmux sessions. Sessions named before this change use the bare basename and won't be matched afterward. One `tmux kill-server` clears any orphans.
* test(vterm): cover AI-vterm inheritance of the vterm copy pathCraig Jennings2026-05-111-0/+32
| | | | I added three ERT cases around `cj/vterm--current-tmux-pane-id` and `cj/vterm-copy-mode-cancel`: the pane-id lookup rejects a non-`vterm-mode` buffer, it still resolves a `claude [...]`-named buffer by process TTY, and the copy-mode cancel command errors outside copy mode. The pane-id-by-TTY case pins the contract that AI-vterm buffers get the copy commands because they're `vterm-mode` buffers, not because of any buffer-name check.
* refactor(external-open): extract external-open-lib for shared helpersCraig Jennings2026-05-102-10/+10
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Same shared-helpers split-pattern that ai-vterm/vterm-config use through cj-window-toggle-lib and that calendar-sync uses through cj-org-text-lib. Pull the two pure dispatch helpers out of the external-open feature module into a sibling library so consumers that only need the dispatch don't have to require the whole feature. New `modules/external-open-lib.el' carries: - `cj/external-open-command' - `cj/external-open-launcher-p' `modules/external-open.el' stays as the feature module: the `default-open-extensions' defcustom, the `find-file' advice (`cj/find-file-auto'), and the interactive commands (`cj/xdg-open', `cj/open-this-file-with'). It now requires external-open-lib for the dispatch helpers. Migrate consumers: - system-utils.el used to require `external-open' for `cj/external-open-launcher-p' alone -- now requires `external-open-lib' directly. - dirvish-config.el calls `cj/external-open-command' from `cj/dirvish-open-file-manager-here' -- add an explicit `(require \='external-open-lib)'. Test files renamed to match the system-lib naming pattern (test-<library>-<feature>.el): - test-external-open-command.el -> test-external-open-lib-command.el - test-external-open-launcher-p.el -> test-external-open-lib-launcher-p.el No behavior change.
* refactor(tests): rename test files to match cj-*-lib.el modulesCraig Jennings2026-05-102-7/+7
| | | | | | | | | | | | The earlier cj-cache and cj-org-text rename commits renamed the modules but missed renaming the test files. Bring them in line: - tests/test-cj-cache.el -> tests/test-cj-cache-lib.el - tests/test-cj-org-text-sanitize.el -> tests/test-cj-org-text-lib-sanitize.el Update file headers, provide forms, and the in-commentary references. No behavior change.
* refactor(cj-window-geometry): rename to cj-window-geometry-libCraig Jennings2026-05-101-5/+5
| | | | | | | | | | | | Same naming-convention fix as the other library renames in this series. Rename modules/cj-window-geometry.el -> modules/cj-window-geometry-lib.el and tests/test-cj-window-geometry.el -> tests/test-cj-window-geometry-lib.el. Update provide forms, file headers, and the (require 'cj-window-geometry) call sites in cj-window-toggle-lib.el, ai-vterm.el, vterm-config.el, and the test file. No behavior change.
* refactor(cj-window-toggle): rename to cj-window-toggle-lib for naming ↵Craig Jennings2026-05-101-5/+5
| | | | | | | | | | | | consistency Rename modules/cj-window-toggle.el -> modules/cj-window-toggle-lib.el and tests/test-cj-window-toggle.el -> tests/test-cj-window-toggle-lib.el. Update provide forms, file headers, and the (require 'cj-window-toggle) call sites in ai-vterm.el, vterm-config.el, and the test file. Same rationale as the cj-cache and cj-org-text renames -- library files in this codebase are suffixed `-lib'. No behavior change.
* refactor(cj-org-text): rename to cj-org-text-lib for naming consistencyCraig Jennings2026-05-101-1/+1
| | | | | | | | | | | Same naming-convention fix as the cj-cache rename. Org-safe text sanitizers extracted in Phase 3 went into modules/cj-org-text.el, should have followed the established `-lib' suffix convention. Rename modules/cj-org-text.el -> modules/cj-org-text-lib.el; update provide form, file header, and the two (require 'cj-org-text) call sites in calendar-sync and test-cj-org-text-sanitize. No behavior change.
* refactor(cj-cache): rename to cj-cache-lib for naming consistencyCraig Jennings2026-05-101-1/+1
| | | | | | | | | | | | | Library files in this codebase are suffixed `-lib' (system-lib.el is the established precedent). The Phase 5 cache helper landed as cj-cache.el; the spec's table proposed names without the suffix and I followed it without checking against convention. Fix the inconsistency now while there are only two consumers and one test. Rename modules/cj-cache.el -> modules/cj-cache-lib.el; update provide form, file header, and the three (require 'cj-cache) call sites in org-agenda-config, org-refile-config, and test-cj-cache. No behavior change.
* refactor(org-refile): migrate to cj-cache helperCraig Jennings2026-05-101-257/+95
| | | | | | | | | | | | | | Phase 5 step 3 of utility-consolidation, second of two cache migrations. Same shape as the agenda migration: drop four state vars, replace the build-with-cache function with a thin wrapper around `cj/cache-value-or-rebuild', extract the slow scan into a pure-ish helper. Add `cj/--org-refile-scan-targets' as the slow filesystem walk (org-roam node enumeration plus 30,000+ todo.org files across code-dir and projects-dir). `cj/build-org-refile-targets' now reads as: detect background-build state, ask the cache helper for the value with the scan helper as BUILD-FN, route the original log lines through :on-hit / :on-build-success. Drop four module-level state vars: - `cj/org-refile-targets-cache' - `cj/org-refile-targets-cache-time' - `cj/org-refile-targets-cache-ttl' - `cj/org-refile-targets-building' Rewrite the existing test file to test wrapper behavior at the contract level (stub the scan helper, verify wrapper outcomes). 8 tests parallel the agenda test set: first-call builds, second-call uses cache, force-rebuild bypass, TTL expiration, empty scan, building-flag cleanup on success and error, and error propagation.
* refactor(org-agenda): migrate to cj-cache helperCraig Jennings2026-05-101-255/+97
| | | | | | | | | | | | | | | | Phase 5 step 2 of utility-consolidation. The agenda-files cache (four module-level vars + 35-line build-with-cache function) now delegates to `cj-cache.el'. Behavior preserved: cache-hit logging, "Building..." background message, building-flag cleanup on error. Add `cj/--org-agenda-scan-files' as a pure-ish helper that returns the file list (the slow filesystem walk). `cj/build-org-agenda-list' becomes a thin wrapper that calls `cj/cache-value-or-rebuild' with the scan helper as BUILD-FN and routes the original log lines through :on-hit / :on-build-success. Drop four module-level state vars: - `cj/org-agenda-files-cache' - `cj/org-agenda-files-cache-time' - `cj/org-agenda-files-cache-ttl' - `cj/org-agenda-files-building' Replace with a single `cj/--org-agenda-files-cache' plist held by the helper. Rewrite the existing test file to test wrapper behavior at the contract level (stub the scan helper, verify wrapper outcomes) instead of poking at internal state vars. 8 tests cover first-call builds, second-call uses cache, force-rebuild bypass, TTL expiration, empty scan, building-flag cleanup on success and error, and error propagation.
* feat(cj-cache): add TTL+building cache helperCraig Jennings2026-05-101-0/+163
| | | | | | | | Phase 5 step 2 of utility-consolidation. Add `modules/cj-cache.el' implementing the API specified in `docs/design/cache-helper-design.org': `cj/cache-make' / `cj/cache-valid-p' / `cj/cache-value-or-rebuild' / `cj/cache-building-p' / `cj/cache-invalidate'. The helper captures the TTL+building-guard pattern that org-agenda-config and org-refile-config currently hand-roll. Both consumers will migrate in follow-up commits. No call-site changes in this commit -- helper plus its 15 tests only. Tests cover: default and custom TTL, fresh/recent/expired/nil-value validity, miss calls build / hit skips build, force-rebuild overrides hit, the four log callbacks (on-hit / on-build-start / on-build-success / on-build-error), error-rethrow and building-flag cleanup on both success and error paths.
* refactor(external-open): consolidate OS-open dispatch in external-open.elCraig Jennings2026-05-105-165/+120
| | | | | | | | | | | | | | | | Phase 4 of utility-consolidation. Three previously-overlapping helpers (system-utils' `cj/identify-external-open-command' and `cj/--open-with-is-launcher-p', plus the dirvish-only `cj/--file-manager-program-for' shipped earlier today) all answered "which OS-open program should I run?". Pull the answer into one place: external-open.el. Move and rename: - `cj/--open-with-is-launcher-p' (system-utils) -> `cj/external-open-launcher-p' (external-open). Public name now matches its module. - `cj/identify-external-open-command' (system-utils) -> `cj/external-open-command' (external-open). Returns nil for unsupported hosts instead of signaling -- callers that need a command must handle nil explicitly. The wrapper `cj/xdg-open' (also moved into external-open) converts nil to a `user-error' with a clear message, preserving the user-facing failure shape. - Delete dirvish's `cj/--file-manager-program-for' helper. `cj/dirvish-open-file-manager-here' now calls `cj/external-open-command' directly. The shell-command fallback for nil-program preserves the previous escape hatch. Break the system-utils <-> external-open recursive require by moving `cj/xdg-open' (the only system-utils function that external-open used) into external-open along with the dispatch. Tests reorganized to match the move. Two new test files (`test-external-open-command.el', `test-external-open-launcher-p.el') replace the two system-utils-named test files. The dirvish file-manager-program test goes away with the helper. 11 tests covering Normal/Boundary/Error for the dispatch (plus the new "unsupported host returns nil" contract). Add `(require \='external-open)' to system-utils.el and `(require \='system-lib)' to external-open.el (for `cj/file-from-context' which xdg-open uses).
* refactor(cj-org-text): extract Org-safe text sanitizers from calendar-syncCraig Jennings2026-05-102-98/+111
| | | | | | | | | | | | | | Phase 3 of utility-consolidation. Three sanitizers moved from calendar-sync.el into a new cj-org-text.el module so other consumers (web-clipper, AI conversation, mail-to-org capture) can compose Org content from external text without depending on calendar: - `calendar-sync--sanitize-org-body' -> `cj/org-sanitize-body-text' - `calendar-sync--sanitize-org-property-value' -> `cj/org-sanitize-property-value' - `calendar-sync--sanitize-org-heading' -> `cj/org-sanitize-heading' The helpers stay pure (string in, string out, nil-safe) and have no Org-mode dependency, so they work in batch and in tests without loading Org. Migrate calendar-sync.el to use the new public names: drop the three local defuns, add `(require \='cj-org-text)', update the six call sites in `calendar-sync--make-event-entry'. Move the existing 17-test file to `tests/test-cj-org-text-sanitize.el', rename test names to match the new helpers, add 1 nil-input test for `cj/org-sanitize-heading' that wasn't in the original file. Total: 18 Normal/Boundary tests across the three helpers.
* refactor(system-lib): extract cj/file-from-context from system-utilsCraig Jennings2026-05-101-28/+28
| | | | | | | | Phase 2.4 of utility-consolidation, the last item in the spec's recommended order. `cj/--file-from-context' resolves "the current file" via a three-step fallback chain (explicit arg, `buffer-file-name', dired file at point) -- a useful pattern for any command that operates on the current file regardless of which kind of buffer the user is in. Promote to public `cj/file-from-context' and re-home in system-lib.el so other modules (mail capture, external-open, AI conversation, dirvish helpers) can use it without an awkward dependency on system-utils. Migrate the two callers in system-utils.el (`cj/open-this-file-with' and `cj/open-file-with-command') and add `(require \='system-lib)' there per the Phase 2 exit criterion. Move the existing 7-test file to `tests/test-system-lib-file-from-context.el' and update its references to the new public name. The test shape is unchanged: 4 Normal + 3 Boundary cases covering explicit-arg precedence, buffer-file-name fallback, dired fallback, and the all-nil case.
* refactor(system-lib): extract cj/process-output-or-error and ↵Craig Jennings2026-05-101-0/+91
| | | | | | | | | | | | cj/git-output-or-error from coverage-core Phase 2.3 of utility-consolidation. `cj/--coverage-git-string' was a generic argv-based runner ("run program, return stdout, raise user-error on non-zero with status+output in the message") trapped inside coverage-core. Lift the generic shape into `cj/process-output-or-error' and add `cj/git-output-or-error' as a one-line wrapper that supplies "git" as the program. Both live in system-lib.el. Future callers I have in mind: reconcile-open-repos shell-style git calls (the high-priority data-safety task), vc-config clipboard cloning, mail integrations that touch git for commit signatures. Six Normal/Boundary/Error tests cover success/no-args/non-zero-exit for the generic runner, the user-error message content (program name, exit status, trimmed output), and the git wrapper's program argument routing. Migrate coverage-core's `cj/--coverage-git-merge-base' and `cj/--coverage-git-diff' to call the new git wrapper. Drop the local `cj/--coverage-git-string' definition. Add `(require \='system-lib)' to coverage-core.el per the Phase 2 exit criterion.
* refactor(system-lib): extract cj/shell-quote-argument-readable from dev-fkeysCraig Jennings2026-05-101-0/+55
| | | | | | | | Phase 2.2 of utility-consolidation. The "quote only when shell-unsafe characters appear, otherwise leave the argument readable" pattern was trapped in dev-fkeys as `cj/--f6-shell-quote-argument' alongside its `cj/--f6-shell-safe-argument-regexp' constant. Lift both into system-lib.el under their generic names; the F6 branding hid that the same shape is useful for any generated compile/test command where the surrounding line ends up in a *compilation* buffer the user reads. Six Normal/Boundary tests cover safe inputs that pass through unchanged (alphanumeric paths, test regexes, `FLAG=value', `host:port'), unsafe inputs that get quoted (spaces, `$', `;', `&', backticks, `*'), and the empty-string boundary. Migrate dev-fkeys's five callers to the new name and add `(require \='system-lib)' per the Phase 2 exit criterion.
* docs: update test and coverage documentationCraig Jennings2026-05-101-1/+1
|
* refactor(system-lib): extract cj/executable-find-or-warn from mail-configCraig Jennings2026-05-101-0/+80
| | | | | | | | | | Phase 2 of utility-consolidation, first commit per the spec's recommended order. `cj/mail--executable-or-warn' was the right pattern -- check executable-find, return the path, otherwise emit a clear `display-warning' naming the feature -- but it was trapped in mail-config and only mail callers benefited. Lift it into `cj/executable-find-or-warn' in system-lib.el with one new argument: an optional GROUP symbol that flows through to `display-warning' (defaulting to `cj/system-lib') so per-feature warning filters keep working. Mail callers pass `mail-config' explicitly. Migrate `cj/mail--mbsync-command' and `cj/mail-configure-smtpmail' to the new helper. Drop the local definition. Add `(require \='system-lib)' to mail-config.el per the spec's Phase 2 exit criterion ("consumer modules explicitly require system-lib"). Five Normal/Boundary tests cover the four return-shape cases (program found / program missing / warning content / default vs explicit group). Other consumers (prog-*.el, dirvish-config.el, browser-config.el) still call `executable-find' directly. Migrating them is a follow-up commit, audited per call site -- the spec flags some `:if' silent checks as intentional and they should NOT switch to the warning helper.
* refactor(dirvish): extract cj/--dired-line-is-directory-pCraig Jennings2026-05-101-0/+56
| | | | | | `cj/dired-mark-all-visible-files' classified the current line as a directory via `(looking-at "^. d")' inline. Lift the classification into `cj/--dired-line-is-directory-p', a string predicate that takes a line and returns non-nil when it's a directory listing. The wrapper still walks the dired buffer line by line and calls `dired-mark' -- that iteration is dired-coupled and stays untested -- but the format-aware predicate is now isolated and verified. Six Normal/Boundary tests cover unmarked directories, marked directories (`*' prefix), regular files (`-' instead of `d'), symlinks (`l'), empty lines, and dired header lines (` /path:' and ` total N').
* refactor(dirvish): extract cj/--html-file-p; match HTML case-insensitivelyCraig Jennings2026-05-101-0/+53
| | | | | | `cj/dirvish-open-html-in-eww' inlined a `string-match-p' against `\.html?\=' to decide whether to hand a file to eww. The check was case-sensitive, so `.HTML' fell through to the "Not an HTML file" message even though every browser treats it as HTML. Lift the predicate into `cj/--html-file-p' and bind `case-fold-search' to t so uppercase and mixed-case extensions match. The trailing-`\=' anchor stays so files like `html-thing.org' still don't match. Seven Normal/Boundary/Error tests cover lowercase `.html', `.htm', uppercase `.HTML', mixed-case `.Html', embedded `html' (no match), non-html extensions (no match), and no-extension files (no match).
* refactor(dirvish): extract cj/--ediff-pair-from-files; fix 0-files crashCraig Jennings2026-05-101-0/+71
| | | | | | | | `cj/dired-ediff-files' had its pair-determination logic inline: count check, prompt fallback when only one file was marked, and the older-first ordering for `ediff-files'. Lift it into `cj/--ediff-pair-from-files' -- pure given the file list, an injected prompt thunk, and a newer-than-p comparator -- so tests stay independent of mtimes and the dired prompt. While extracting, surface a latent bug: with zero marked files the original code fell through to `(file-newer-than-file-p nil nil)' and crashed with a wrong-type-argument error. Replace the crash with a clear `user-error' ("No files marked"), and add a regression test. The 3+ files case keeps its existing user-error message. Five Normal/Boundary/Error tests cover both ordering directions, the one-file prompt path, and both error counts.
* refactor(dirvish): extract playlist filter and sanitize helpersCraig Jennings2026-05-101-0/+82
| | | | | | | | | `cj/dired-create-playlist-from-marked' had its audio-file filtering and trailing-`.m3u' stripping inline among the dired marking, prompting, overwrite-loop, and file-write logic. Lift each into its own pure helper: - `cj/--playlist-filter-audio (files extensions)' returns only files whose extension matches one of EXTENSIONS (lowercase, no dot). Case-insensitive on the file side. - `cj/--playlist-sanitize-name (name)' strips a trailing `.m3u' suffix; embedded `.m3u' that isn't at the end is preserved. Ten Normal/Boundary tests cover keep-only-audio, case-insensitivity, files-without-extension excluded, empty inputs, and the sanitize edge cases (bare name, embedded `.m3u', empty string, just `.m3u').
* test(dirvish): cover cj/get-project-root with stubbed APIsCraig Jennings2026-05-101-0/+71
| | | | `cj/get-project-root' was already in the right shape (pure dispatch over `projectile-project-root' and `project-current'), but had no tests. Add four covering the four meaningful states: projectile finds a root, projectile returns nil and project.el finds one, projectile errors and the wrapper falls through to project.el, and neither finds anything. The tests stub all three external symbols (`projectile-project-root', `project-current', `project-root') so they exercise the function without a project on disk.
* refactor(dirvish): extract cj/--file-manager-program-for helperCraig Jennings2026-05-101-0/+54
| | | | | | `cj/dirvish-open-file-manager-here' had its platform-dispatch -- xdg-open / open / explorer / shell fallback -- inline as a four-arm cond branching on `executable-find' and `system-type'. Lift the choice into `cj/--file-manager-program-for', a pure function from (HAS-XDG-OPEN-P SYSTEM-TYPE) to a program-name string or nil. The wrapper resolves the live values, asks the helper, and either calls the program or falls back to a shell-command. Six Normal/Boundary tests cover the four return shapes (xdg-open across system types, darwin/windows-nt without xdg-open, the nil fallback for plain Linux without xdg-open and for unknown system-types like Haiku).
* refactor(dirvish): extract cj/--wallpaper-program-for helperCraig Jennings2026-05-101-0/+42
| | | | `cj/set-wallpaper' had two parallel cond arms hardcoding the X11/Wayland dispatch and the success/failure messages inline. Lift the program-and-args choice into `cj/--wallpaper-program-for' -- a pcase from a display-server symbol to a (PROGRAM ARG...) list, or nil for unknown environments. The wrapper now: detect env, ask helper for the command, surface the right message (unknown server / executable missing / success). Adding a third backend (e.g. xdg-desktop-portal) becomes one pcase clause + one test.
* refactor(dirvish): extract cj/--dired-resolve-display-path helperCraig Jennings2026-05-101-0/+86
| | | | | | `cj/dired-copy-path-as-kill' was a 57-line procedural body with the path-resolution branching mixed into the kill-ring write and the user-visible message. Lift the four-way decision (force-absolute / project-relative / home-relative / bare absolute) into `cj/--dired-resolve-display-path', a pure function from (FILE PROJECT-ROOT HOME-DIR FORCE-ABSOLUTE) to (PATH . PATH-TYPE). The wrapper now reads as: get the dired file, ask the helper how to display it, format-or-pass-through, kill-new, message. Seven Normal/Boundary tests cover each branch: project-relative, home-relative, the home-itself "~" glyph, absolute fallback, force-absolute beating both project and home, and project taking precedence over home when both apply.
* refactor(dirvish): extract cj/--duplicate-file-name helperCraig Jennings2026-05-101-0/+59
| | | | | | The name-mangling logic in `cj/dirvish-duplicate-file' was inline -- inseparable from the dired side effects (existence check, copy, revert). Extract to `cj/--duplicate-file-name', a pure function from FILE to FILE-WITH-COPY-SUFFIX. Seven Normal/Boundary tests cover the cases I care about: typical extension, elisp file, no extension, multi-dot extensions (only the last dot counts), leading-dot dotfiles, relative paths, spaces in the base name. The wrapper retains the dired-mode interactive shape and now reads as a thin shell over the pure helper.
* fix(dirvish): rename pw -> wp to clear quick-access key collisionCraig Jennings2026-05-101-0/+80
| | | | | | Pressing `g' in dirvish (`dirvish-quick-access') failed with `Wrong type argument: command, (keymap (107 . transient:dirvish-quick-access::N))'. Transient auto-groups multi-character keys into nested prefix keymaps; when one entry's KEY is also the prefix of a longer entry's KEY, the shorter slot resolves to the auto-generated sub-prefix keymap rather than to the intended leaf command. The "wallpaper" entry on `pw' and the "project work" entry on `pwk' (added in 4ece1eb) collided. Rename `pw' to `wp'; `pwk' is now the only entry under the `pw'-prefix, so transient builds a clean leaf. Add `tests/test-dirvish-config-quick-access.el' with five tests: four cover a small `test-dirvish-config--key-collisions' helper against synthetic inputs (collision detected, clean list, empty, single-entry), and one regression test asserts the live `dirvish-quick-access-entries' contains no shorter-key-is-prefix-of-longer-key pair.
* refactor(vterm): move vterm prefix to C-; x and add prompt navCraig Jennings2026-05-101-5/+18
| | | | | | | | | | The personal vterm map was on `C-; V'. The capital V costs a Shift on every keystroke into the menu, which adds up for the daily `C-; V c' / `C-; V C' bindings. Move the prefix to lowercase `C-; x' -- free, no Shift, faintly mnemonic (xterm/execute). The lowercase `C-; v' stays the version-control menu. Wire `vterm-next-prompt' and `vterm-previous-prompt' into the menu so they're reachable everywhere, not only inside vterm-copy-mode-map. Lowercase `n' and `p' match Emacs's idiom for next/previous; bump "new vterm" up to capital `N' for the rare new-buffer case. Drop the `<pause>' binding for `vterm-copy-mode' from `vterm-mode-map'. Modern keyboards rarely have a Pause key and `C-; x c' is the canonical entry now. Update which-key labels and tests; `test-vterm-keymap-includes-history-and-copy-bindings' now asserts the new prefix, and two new tests cover prompt-nav bindings and the dropped `<pause>' binding.
* fix(vterm): use a block cursor in vterm-copy-modeCraig Jennings2026-05-101-11/+11
| | | | | | The 3-pixel bar was visible but a block matches the rest of my Emacs cursor and lets the standard cursor color and `blink-cursor-mode' behavior carry through unchanged. Same enter/exit semantics: forced visible on entry, buffer-local override killed on exit so the live terminal goes back to the TUI's chosen state. Update the test expectations and rename the "prior-was-box" boundary test to "prior-was-hbar" so it still proves the override does something (the prior and the override would otherwise both be `box').
* test(vterm): cover the copy-mode exit chain end-to-endCraig Jennings2026-05-101-0/+93
| | | | | | | | | | | | | The unit tests for the cursor-restoration hook only exercised the helper in isolation. The real integration -- toggling the minor mode and watching the hook fire as part of the chain -- wasn't covered. If `vterm-copy-mode-done' or `cj/vterm-copy-mode-cancel' broke their exit semantics (or our hook stopped firing on `vterm-copy-mode-hook'), the unit tests would still pass but the cursor would stay stuck on the bar in real use. Add five integration tests that toggle the actual minor mode through stubbed `vterm--enter-copy-mode' / `vterm--exit-copy-mode' (so we don't need a live vterm process) and assert the cursor moves through the full lifecycle: nil -> bar on enter, bar -> killed-local on exit. Cover all three exit paths Craig hits in normal use: - vterm-copy-mode -1 directly (the toggle) - vterm-copy-mode-done with an active region (M-w / RET) - vterm-copy-mode-done with no region (line-selection branch) - cj/vterm-copy-mode-cancel (C-g / <escape>) Plus a multi-cycle test so a regression in `kill-local-variable' handling shows up.
* fix(vterm): force a visible cursor in vterm-copy-modeCraig Jennings2026-05-101-0/+51
| | | | | | | | vterm's C module sets `cursor-type' to nil whenever the underlying TUI sends DECTCEM (`\e[?25l') to hide the terminal cursor. Most full-screen TUIs do this on startup — Claude Code in an ai-vterm being a daily example. Once the cursor is hidden at the buffer level, vterm-copy-mode inherits that nil and the user can't see where point is when navigating to select text. Selection still works, but you're flying blind. Add a `vterm-copy-mode-hook' that forces `cursor-type' to a 3-pixel bar on entry and kills the buffer-local override on exit. The bar shape is drawn between characters rather than by inverting one, so heavy TUI face properties don't hide it either. On exit the live terminal goes back to whatever vterm's tracking says, so the TUI's chosen cursor state resumes. 4 ERT tests cover the hook's enter/exit behavior and confirm registration on `vterm-copy-mode-hook'.
* refactor: split eshell-vterm-config into eshell-config and vterm-configCraig Jennings2026-05-104-4/+4
| | | | | | The combined module had grown to 573 lines covering two unrelated subsystems with no shared state — the eshell shell-mode commands and the vterm/F12 toggle. The header even rendered this with two `;; ----` dividers. Split into two focused modules. eshell-config.el keeps the eshell user commands and package wiring (~170 lines). vterm-config.el keeps the vterm package, the tmux history capture command, the F12 toggle, and the C-; V keymap (~400 lines). Update init.el to require both, point the four vterm test files at vterm-config, and refresh the cross-module commentary in cj-window-geometry.el and cj-window-toggle.el. No behavior change. Full test suite green; validate-modules clean.
* refactor: extract toggle-state helpers shared by F9 and F12Craig Jennings2026-05-101-0/+188
| | | | | | The F12 commit (554b32d) flagged this as a follow-up: ~120 lines of capture-state and display-saved logic were duplicated between modules/ai-vterm.el and modules/eshell-vterm-config.el. The only differences were the default direction (right for F9, below for F12) and the customization name for the fallback size. Extract the shared logic into modules/cj-window-toggle.el so both consumers reduce to thin delegates that pass their state-var symbols and defaults. The state vars stay where they were, so existing tests against each consumer's helpers keep working. 10 new tests cover the parameterized helpers in isolation. All consumer tests still pass.
* Keep calendar sync off the UI threadCraig Jennings2026-05-101-0/+154
| | | | Move calendar feed conversion into an isolated batch Emacs worker so large parse/write cycles do not freeze interactive editing. Cover the worker command, isolated logging, quoted settings, and sync success/failure paths with focused ERTs.
* Add Emacs-native vterm copy workflowsCraig Jennings2026-05-101-0/+141
| | | | Add an Emacs-first copy workflow for vterm and tmux. C-; V c enters vterm copy mode, C-; V C captures tmux pane scrollback into a temporary Emacs buffer, M-w copies and returns, and C-g/Escape cancel without copying. This also adds clickable URLs, removes the bad vtermf binding, unbinds C-c C-t, and tests the vterm/tmux keymap behavior.
* Make repo reconciliation review-firstCraig Jennings2026-05-107-164/+214
| | | | Stop automatically stashing, pulling, and popping dirty repos during reconciliation. Clean repos still pull, dirty repos open Magit for review, and results now include structured statuses, skip reasons, pruning, and a summary.
* Clean up Org keymap ownershipCraig Jennings2026-05-102-0/+59
| | | | Remove the duplicate Org cache keymap and keep C-; O owned by the shared Org map. The cache clear command now clears all Org buffers by default, with a prefix argument for the current buffer.
* Move GPTel tool loading into AI configCraig Jennings2026-05-102-0/+63
| | | | Move the local GPTel tool wiring out of init.el and into ai-config. The tools directory and feature list are now configurable, missing optional tools are non-fatal, and focused tests cover the loading behavior.
* Add tests for early archive setupCraig Jennings2026-05-101-11/+121
| | | | Cover the startup archive paths and refresh behavior. This locks down the local-first archive setup, online archive opt-in, cache freshness checks, and priority ordering so package startup changes are easier to reason about later.
* refactor(tests): extract shared buffer-cleanup and fake-vterm helpersCraig Jennings2026-05-099-101/+101
| | | | | | | | | | | | | | Eight test files across the ai-vterm and vterm-toggle suites each shipped a small variant of the same cleanup loop: walk `buffer-list`, kill any buffer whose name starts with a given prefix. Each file also re-implemented the `(string-prefix-p ...)` check inline. One file additionally had its own fake-vterm-mode-buffer constructor for tests that needed `cj/--vterm-toggle-buffer-p` to fire. I pulled the shared logic into `tests/testutil-vterm-buffers.el`: - `cj/test--kill-buffers-matching-prefix` is the primitive. - `cj/test--kill-claude-buffers` and `cj/test--kill-test-vterm-buffers` are thin wrappers for the two prefixes that actually appear. - `cj/test--make-fake-vterm-buffer` constructs a buffer with `major-mode` set to `vterm-mode` without launching a real vterm process. Each affected test file now `(require 'testutil-vterm-buffers)` and calls the shared helpers directly. `test-vterm-toggle--buffer-filter.el` keeps a 3-line wrapper that calls both kill helpers in sequence (the only place that needs both prefixes). Net diff: -116 / +72 across 8 test files, plus ~30 lines in the new testutil. Roughly -45 lines after the abstraction is paid for. No behavior change. 80 ai-vterm tests, 15 vterm-toggle tests, 15 cj-window-geometry tests all pass. Full make test green.
* refactor: extract window-geometry helpers shared by F9 and F12Craig Jennings2026-05-093-117/+103
| | | | | | | | | | | | | | `ai-vterm.el` (F9) and `eshell-vterm-config.el` (F12) both grew the same geometry-preservation pattern: classify a window's position, capture its body size, map cardinal direction to its frame-edge variant. The shared helpers were sitting as near-duplicates in both modules. With two real consumers established, the abstraction has the right shape. I pulled them into `cj-window-geometry.el`. The new module exposes three pure helpers: - `cj/window-direction` returns right/below/left/above based on edges relative to `frame-root-window`. Takes an optional DEFAULT for the single-window-frame fallback so each consumer picks its own (ai-vterm wants 'right, vterm-toggle wants 'below). - `cj/window-body-size` returns body-cols (right/left) or body-lines (below/above). Same body-vs-total reasoning as before: divider-independent, matches what the user sees. - `cj/cardinal-to-edge-direction` maps right/left/below/above to rightmost/leftmost/bottom/top, used by each consumer's `display-saved` action. `ai-vterm.el` and `eshell-vterm-config.el` now `(require 'cj-window-geometry)` and call the shared helpers directly. The consumer-specific `capture-state` and `display-saved` bodies stay in each module because they bind to consumer-specific state vars. Extracting those would either need parameter-passing-via-symbol or a macro, both heavier than the duplication they would remove. Tests: 15 in `test-cj-window-geometry.el` covering all four directions, body-size on both axes, cardinal-to-edge mapping, default-arg fallback, and the unknown-direction nil case. Deleted `test-ai-vterm--window-geometry.el` (now redundant) and trimmed four duplicate window-direction/size tests from `test-vterm-toggle--display.el`. Net LOC: each consumer ~40-50 lines lighter, with the new module + tests paying roughly half that back. Full make test green. make validate-modules green.
* feat(ai-vterm): show [running] in picker and F9 redisplays last-usedCraig Jennings2026-05-092-9/+34
| | | | | | | | The C-F9 project picker now flags projects whose claude buffer is alive with a " [running]" suffix on the abbreviated path. I added `cj/--ai-vterm-format-candidate` to compute the display name and routed the picker through it. Before the change, the picker showed every candidate identically, so you couldn't tell at a glance whether picking a project would attach to an existing session or start a fresh one. F9 with two or more alive claude buffers used to open the project picker. That meant after toggling claude-A off, opening claude-B via C-F9, then toggling B off, the next F9 dropped into a picker rather than redisplaying B (the one you just toggled off). I renamed `redisplay-single` to `redisplay-recent` in `cj/--ai-vterm-dispatch` and broadened the trigger from "exactly one alive" to "one or more alive". F9 now redisplays the MRU claude buffer, so it consistently means "toggle THE claude I was last using". The project picker stays explicit on C-F9 for "start a different project", and M-F9 still picks among existing claudes. 2 new tests for the indicator (`format-candidate` flagged + unflagged), 2 dispatch tests renamed to match the new contract. 80 ai-vterm tests pass. Full make test green.
* feat(vterm): F12 toggle that excludes claude and preserves geometryCraig Jennings2026-05-093-0/+277
| | | | | | | | | | | | | | | | | vterm-toggle picked the most-recently-selected vterm buffer as F12's toggle target. After using F9 on a claude vterm, the most-recent vterm IS claude, so F12 ended up toggling claude, which has its own F9 / C-F9 / M-F9 surface in ai-vterm.el and shouldn't be affected. The display rule also had a hard-coded `(window-height . 0.7)` that overrode mouse-resize and orientation flips on every toggle. I replaced the F12 binding with `cj/vterm-toggle` in `eshell-vterm-config.el`, mirroring the pattern shipped in ai-vterm.el: - `cj/--vterm-toggle-buffer-p` excludes claude-prefixed buffers from F12's candidate set. - `cj/--vterm-toggle-capture-state` records direction + body size at toggle-off. - `cj/--vterm-toggle-display-saved` replays via `(body-columns . N)` / `(body-lines . N)` cons forms with the cardinal direction mapped to its frame-edge variant (`right` -> `rightmost`, `below` -> `bottom`, etc.) so vterm always lands at the captured edge regardless of selected window. - `cj/vterm-toggle` uses `delete-window` (with `one-window-p` guard) on toggle-off so buffer-move scenarios don't leak ghost windows. Default direction is `'below` to match F12's traditional bottom split. The vterm-toggle package stays installed so `M-x vterm-toggle` still works. Only the F12 binding changes. 19 new tests across three files: buffer-filter, dispatch, display. Full make test green. Tradeoff: ~150 lines of geometry helpers and capture/display action logic are duplicated from ai-vterm.el. Worth extracting into a shared module now that two consumers exist. I filed it as a follow-up rather than blocking this ship.
* fix(ai-vterm): harden F9 toggle across multi-window and buffer-moveCraig Jennings2026-05-093-26/+303
| | | | | | | | | | | | | | | | Live-testing surfaced four edge-case failures in the F9 toggle geometry preservation. Each gets a dedicated regression test. - Multi-window squeeze: a captured fraction-of-frame replayed at the wrong size in 3+ window layouts because `display-buffer-in-direction` interprets float widths as fractions of the new window's parent, not the frame. In a flat 3-window layout the parent is the root, but in nested splits it's a sub-tree, and the captured fraction blew the layout up. I switched to absolute integer body-cols and body-lines as the captured unit. The unit is layout-independent. - One-col peek: a claude window captured rightmost (no right divider, body=total) replayed into a middle position (with divider, body=total-1) showed 1 col of the sibling buffer peeking through where claude should have ended. I wrap the integer size in a `(body-columns . N)` / `(body-lines . N)` cons so `display-buffer-in-direction` sets the body explicitly, divider-independent. - Position swap and compounding gap: `direction=right` in `display-buffer-in-direction` splits the selected window, not the frame edge. In multi-window layouts the new claude landed mid-frame instead of where it came from. Each toggle compounded a 1-col loss because the new position picked up a divider the original lacked. I map the cardinal direction to its frame-edge variant (`right` -> `rightmost`, `below` -> `bottom`, etc.) so claude always returns to the captured edge. - Extra window after buffer-move: buffer-move (C-M-arrows) doesn't update the claude window's `quit-restore` parameter, so `quit-window` falls through to bury rather than delete. The window stays alive showing some other buffer. Toggle-on doesn't recognize it and creates a fresh side window, landing at N+1 windows. I switched to `delete-window` with a `one-window-p` guard for the single-window-frame case. One tradeoff: in a layout where claude was deliberately in a middle position (e.g. agenda | claude | todo), the next toggle pulls it to the frame edge rather than the middle. The side-panel pattern is the design intent and the common case. 7 new regression tests covering each scenario. 80 ai-vterm tests pass. Full make test green.
* feat(ai-vterm): F9 toggle/redisplay/pick + persistent split geometryCraig Jennings2026-05-089-0/+667
| | | | | | | | | | | | | | | | | | | | | | | F9 was a single command that always opened the project picker. Three small frustrations stacked up. With one claude buffer open and not visible, F9 was a redundant prompt to pick a project that already had a session. With claude visible, there was no way to bury it without M-x quit-window. With two projects' buffers alive, swapping between them was a buffer-switch chore. F9 is now a dispatch: - Claude visible in this frame: quit the window (toggle off) and capture the geometry first. - Exactly one claude buffer alive but hidden: re-display it (DWIM single-buffer case). - Zero or two-plus alive: fall through to the project picker. C-F9 is the always-pick-project entry point for explicit project switches. M-F9 is a buffer picker over the alive claude buffers. If a claude window is currently shown, the picked buffer replaces it in that window so the split orientation and size carry over. The shown buffer sorts last in the picker with a [shown] marker so RET picks "the other one." Split geometry persists across toggles. Two module-level vars (cj/--ai-vterm-last-direction, cj/--ai-vterm-last-size) capture at toggle-off and feed a custom display action. After M-S-t flips claude from right to bottom, F9 toggle-off-then-on returns it at the bottom. After a mouse resize, the next toggle restores that fraction. State is per-session. Restarts reset to default right/0.5. Two display-buffer fixes came out of testing: - save-window-excursion around (vterm name) keeps the dashboard from being buried on a fresh F9 at startup. vterm calls pop-to-buffer-same-window internally, which would otherwise replace the selected window's buffer before the alist could route the new one. - The action chain swaps display-buffer-use-some-window for a more specific cj/--ai-vterm-reuse-existing-claude. The generic version stole non-claude windows on C-F9 when the user was focused inside claude (claude on bottom, code on top -> new project landed in the code window). The specific version only reuses windows that already show a claude buffer. I reclaimed C-F9 from the gptel toggle in ai-config.el. C-; a t still binds gptel. I added eight new test files (claude-buffers, displayed-claude-window, dispatch, pick-buffer-candidates, window-geometry, capture-state, display-saved, reuse-existing-claude) plus a regression test on cj/--ai-vterm-show-or-create for the dashboard-preservation fix. All 73 ai-vterm tests pass and the full make test suite is green.
* fix(ai-vterm): direction-based display + per-project tmux session namesCraig Jennings2026-05-073-3/+109
| | | | | | | | | | | | Two post-ship issues blocked practical use of the new launcher. The display rule used `display-buffer-in-side-window` with `(dedicated . t)`. Side-window dedication caused `set-window-buffer` to error during `buffer-move` (C-M-arrows), which left a half-finished swap with both sides showing the claude buffer. Then `switch-to-buffer` on a non-claude buffer in that dedicated window split instead of replacing. I rewrote the rule as `display-buffer-reuse-window -> display-buffer-use-some-window -> display-buffer-in-direction (right)`. The resulting window is ordinary, not dedicated, so swap and replace work normally. I also narrowed `vterm-toggle`'s broad lambda (which matches any vterm-mode buffer) to exclude `claude [` buffers. Otherwise vterm-toggle's `:defer` made it install last and capture our buffers first with its own bottom-split + dedicated treatment. The tmux side: vterm's auto-launch hook ran a bare `tmux\n`, so each session got an auto-named one. After an Emacs crash the tmux session would survive but I couldn't find it. A second F9 just spawned another. The launcher now sends `tmux new-session -A -s <basename> -c <dir> '<claude>; exec bash'`. The `-A` reattaches to a same-named session if it already exists. The `exec bash` keeps the tmux window alive if claude itself exits. A `cj/--ai-vterm-suppress-tmux` flag tells the existing vterm hook to skip its bare tmux step so the named launch runs instead. 11 new tests across 2 files cover the session-name and launch-command helpers. I updated tests for show-or-create and the display rule. All 34 ai-vterm tests are green.