summaryrefslogtreecommitdiff
path: root/tests
Commit message (Collapse)AuthorAgeFilesLines
...
* test(ui-config): cover transparency apply/toggle and cursor-type setterCraig Jennings2026-05-131-0/+126
| | | | The previous test file covered only `cj/--buffer-cursor-state` and the cursor-color hook. `cj/apply-transparency`, `cj/toggle-transparency`, and `cj/set-cursor-type` were the gap. New tests stub the frame-mutating primitives so they run in batch and cover: alpha cons construction in both enabled and disabled states, the terminal-frame skip, the error-message path on `set-frame-parameter` failure, the toggle's flip + re-apply, double-toggle idempotence, and the cursor-type pass-through (including nil for invisible).
* test(system-defaults): cover the helper functionsCraig Jennings2026-05-131-0/+155
| | | | | | system-defaults.el is mostly `setq` configuration, but the testable helpers (`cj/disabled`, the two minibuffer-gc hooks, `unpropertize-kill-ring`, `cj/log-comp-warning`) were uncovered. New test file mirrors the sandbox pattern from test-system-defaults-vc-follow-symlinks.el and asserts each function's observable behavior: gc-threshold flip on minibuffer entry/exit, kill-ring property strip with empty-ring boundary, comp warning written with timestamp + non-comp type ignored so the default *Warnings* path still works. 8 new tests across Normal / Boundary cases.
* test(browser-config): cover the public wrappersCraig Jennings2026-05-131-0/+68
| | | | The internal `cj/--do-*` helpers were already tested. The `cj/apply-browser-choice` and `cj/initialize-browser` wrappers were the gap -- they call `cj/--do-*` then `message` and own the wiring logic. New tests stub `message` and `cj/--do-apply-browser-choice` to cover each branch (success, invalid plist, loaded, first-available, no-browsers).
* refactor(org-agenda): extract main-agenda prefix format into a defvarCraig Jennings2026-05-131-0/+17
| | | | | | `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(setup): scripts/setup-telega.sh prepares the docker environment for telegaCraig Jennings2026-05-131-0/+87
| | | | | | Verifies docker is installed and the daemon is reachable, then either pulls a public image (when `TELEGA_DOCKER_IMAGE` is set) or announces the in-Emacs build path (`M-x telega-server-build`) for the user to run once. Telegram auth (phone + verification code) is interactive on first `M-x telega` and not scripted here. Same shape as setup-email.sh: helpers are sourceable for bats, `main` runs only under direct execution. 7 bats tests stub `docker` and `command` so the suite never talks to the real daemon.
* feat(telega): add telega.el module with docker-backed TDLibCraig Jennings2026-05-131-0/+25
| | | | | | | | 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-8/+40
| | | | | | `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-0/+170
| | | | | | | | 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/+137
| | | | | | 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-0/+110
| | | | | | | | 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/+35
| | | | | | | | 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-0/+9
| | | | | | 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.
* test(calibredb-epub): cover seven untested ebook helpersCraig Jennings2026-05-131-0/+216
| | | | | | I added tests for `cj/calibredb-clear-filters`, `cj/force-nov-mode-for-epub`, `cj/--nov-image-padding-cols` (the helper extracted in the previous commit), `cj/nov--natural-window-width`, `cj/nov--metadata-get`, `cj/nov--file-path`, and `cj/nov-jump-to-calibredb`. Each gets Normal, Boundary, and Error cases where they apply: 22 tests in all. The calibredb stubs sit in a new `test-calibredb-epub--with-calibredb-stubs` macro so the three jump-to-calibredb cases stay focused on their branch. Coverage on `calibredb-epub-config.el` moves from 7/104 (6.7%) to 95/133 (71.4%).
* test(prog-webdev): cover cj/webdev-format-bufferCraig Jennings2026-05-121-0/+93
| | | | I added `tests/test-prog-webdev-format.el` — 7 ERT tests over `cj/--webdev-format-command` (plain path, spaces-quoted, `.tsx`) and `cj/webdev-format-buffer` (prettier present → the region command targets `buffer-file-name`; the `file.ts` fallback when the buffer has no file; point clamps to `point-max` when the format shrinks the buffer; prettier missing → `user-error`). `executable-find` and `shell-command-on-region` are stubbed. `cj/webdev-keybindings` was already covered by `test-prog-webdev--format-wiring.el`.
* test(keyboard-compat): cover the terminal and GUI setup functionsCraig Jennings2026-05-121-0/+101
| | | | | | `cj/keyboard-compat-terminal-setup` (8 `define-key`s into `input-decode-map` for arrow escape sequences) and `cj/keyboard-compat-gui-setup` (18 `M-<UPPER>` → `M-S-<lower>` translations into `key-translation-map`) had no tests — that's the bulk of the module's executable lines. I added `tests/test-keyboard-compat-setup.el` — 7 ERT tests that `let`-bind those keymaps to fresh copies, stub `env-terminal-p` / `env-gui-p`, and check the decode/translate entries land, with completeness loops over all 8 arrow sequences and all 18 Meta-Shift letters, plus the gate-off boundary for each. `cj/--icon-blank-in-terminal` was already covered. `lookup-key` on an ESC-prefixed string can return a meta-prefix event count instead of nil, so the "no-op when not on a terminal" case asserts the keymap is still empty rather than checking individual lookups.
* test(selection-framework): cover both branches of the C-s dispatchCraig Jennings2026-05-121-0/+68
| | | | `cj/consult-line-or-repeat` had only a keybinding test. I added `tests/test-selection-framework--consult-line-or-repeat.el` — 4 ERT tests covering the fresh-search branch, the repeat-on-second-call branch, the nil `last-command` boundary, and `commandp`, with `consult-line` and `vertico-repeat` stubbed. It reuses the `use-package`-shadow trick from the keybindings test, so the module loads without dragging in the vertico/consult/embark/company/prescient stack.
* refactor(org-drill): hoist the commands out of :config and clear the ↵Craig Jennings2026-05-121-0/+6
| | | | | | | | 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/+42
| | | | | | 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-0/+86
| | | | | | `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-0/+152
| | | | | | | | `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.
* test(prog-python): cover the command builders and the S-f5/S-f6 wiringCraig Jennings2026-05-121-0/+144
| | | | I added tests/test-prog-python-commands.el — 13 ERT tests over `cj/--python-mypy-command`, `cj/--python-debug-command`, `cj/python-mypy`, `cj/python-debug`, and `cj/python-mode-keybindings`. Normal, boundary, and error case per function. `executable-find`, `compile`, and `pdb` are stubbed at the boundary. None of the module's functions had coverage before this.
* fix(nov): center the EPUB text by setting window margins directlyCraig Jennings2026-05-121-1/+60
| | | | | | | | | | | | 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.
* 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-0/+64
| | | | | | `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-0/+49
| | | | | | 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-28/+119
| | | | | | | | | | | | | | `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.
* test: add terminal coverage summary helperCraig Jennings2026-05-121-0/+66
|
* feat(dirvish): print the file at point with PCraig Jennings2026-05-121-0/+134
| | | | | | `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.
* test(scripts): add bats coverage for setup-email.sh password helpersCraig Jennings2026-05-121-0/+82
| | | | | | `setup-email.sh' ran top to bottom, so the only way to exercise `install_encrypted_password' / `decrypt_password' was to run the whole new-machine setup (mbsync, mu init). Its procedural body now lives in a `main()' function guarded by the usual `[[ "${BASH_SOURCE[0]}" == "${0}" ]]' check, so sourcing the script just defines the helpers, and running it directly is unchanged. New `tests/test-setup-email.bats' sources the script, points the password dirs at a per-test tmpdir, and covers both helpers across the normal / skip-existing / missing-source / (for decrypt) gpg-failure paths, stubbing `gpg' so no real key is needed. `make test-bash' runs the bats files, and `make test' picks them up after the Elisp suite when bats is installed.
* fix(mail): clear the marks after saving from the attachment selectorCraig Jennings2026-05-121-1/+5
| | | | `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-0/+5
| | | | `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-5/+5
| | | | 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-6/+6
| | | | `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-79/+93
| | | | | | 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.
* test(lorem-optimum): tag benchmarks :perf and assert on scaling ratiosCraig Jennings2026-05-121-75/+71
| | | | | | The benchmark suite carried no tag, so `make test' ran it every time, and three of its tests asserted absolute wall-clock times (`(should (< time 5000.0))' and friends). Those numbers hold on my laptop and break on a slower box. Every benchmark is now `:perf'-tagged so the default test run skips it. The three absolute learn thresholds collapse into one `benchmark-learn-scaling' test: it times 1K, 10K, and 100K learns and requires each 10x jump in input to cost under 40x the time. Linear scaling lands near 10x, and the 40x ceiling tolerates GC pauses and slow hardware while still catching an O(N^2) regression. The rest drop their absolute `should's and stay as timing reports for `make benchmark'.
* test: close coverage gaps from the preceding batchCraig Jennings2026-05-115-0/+103
| | | | | | | | | Untested paths surfaced while reviewing the preceding feature/fix commits: - calendar-sync: a test that `-L' precedes `-l' in the worker command (separate `member' checks wouldn't catch a swap), plus a `:slow' tag on the real-subprocess worker test so it stays out of the default `make test' run. - org-capture cache: a killed marker buffer invalidates the entry and the next resolution rescans without erroring on the stale marker, `cj/org-capture-clear-target-cache' actually empties the hash, and non-`file+headline' targets (`file', `file+olp', `file+function') fall through to the original `org-capture-set-target-location'. - lorem-optimum: `cj/lipsum-title' on an empty chain returns "", not an error. - calibredb-epub: a negative `cj/nov-margin-percent' is clamped up to 0 (text takes the full window width). - mu4e attachments: the default save directory comes from the part's `:target-dir' and falls back to `~/Downloads/', and asking for the attachment at point on a header line fails with a `user-error'.
* feat(mu4e): simpler attachment-save commands on C-; e S/s/mCraig Jennings2026-05-111-0/+237
| | | | Three project-owned commands that reuse mu4e's MIME metadata (`mu4e-view-mime-parts') and save primitives (`mm-save-part-to-file', `mu4e-uniquify-save-file-name-function') directly instead of driving mu4e's completion UI. `cj/mu4e-save-all-attachments' (`C-; e S') prompts once for a directory and saves every attachment-like part. `cj/mu4e-save-attachment-here' (`C-; e s') saves one attachment, picked by display label, with duplicate filenames shown as "name <part N>" so they don't collapse into one completion candidate. `cj/mu4e-save-some-attachments' (`C-; e m') opens a `*mu4e attachments*' selection buffer showing mark state, label, MIME type, and size per row, where `RET' toggles a row, `a' / `u' mark / unmark all, `s' saves the marked ones, and `q' quits. Replaced the old Embark/Vertico-workaround comment. Tests cover the attachment filtering, the duplicate-filename disambiguation, save-path construction, the no-handle error, command prompting, and the email-map bindings.
* refactor(epub): clean up calibredb-epub-config.elCraig Jennings2026-05-111-0/+52
| | | | Dropped the 1-second `:defer' from the calibredb use-package and the redundant explicit `nov-render-document' call in `cj/nov-apply-preferences'. Nov / visual-fill-column text width now recalculates on `window-configuration-change-hook'. `cj/nov--text-width-for-window' computes the (clamped, minimum-readable) width and `cj/nov-update-layout' installs it buffer-locally. Lowered `calibredb-search-page-max-rows' from 20000 to 500 (pagination was effectively disabled). Replaced the anonymous zathura keybinding with `cj/nov-open-external'. Tests cover the width computation and the external-open binding.
* fix(slack): harden and curate the C-; S ! reaction workflowCraig Jennings2026-05-111-0/+91
| | | | Two problems with `C-; S !'. First, emacs-slack's `slack-reaction-echo-description' runs in a buffer-local `post-command-hook' and can error on every keystroke when a reaction widget's text properties are malformed, which makes it hard to leave the Slack buffer or recover with C-g. It's now wrapped in `condition-case' that, on error, removes the hook from that buffer's local `post-command-hook' and messages once. Second, without emojify the upstream reaction picker is a flat completing-read over 1600+ names. `cj/slack-message-add-reaction' now offers a curated common list (`thumbsup', `pray', `eyes', `white_check_mark', `heart', `joy', `thinking_face', `rocket', `tada') with "Other..." delegating back to `slack-message-reaction-input' for the full set. Tests cover the hook hardening, the curated picker, and the `C-; S !' rebinding.
* perf(lorem-optimum): speed up the Markov generation pathCraig Jennings2026-05-112-11/+13
| | | | `cj/markov-join-tokens' collects tokens in a list and `mapconcat's once instead of repeated string concatenation. `cj/markov-generate' uses `push'/`nreverse' instead of repeated `append'. The Markov keys are cached as a vector so random key selection is O(1). Re-enabled the benchmark tests (the `:slow' tags were stale) and added a `cj/lipsum-title' test after byte-compilation flagged a malformed form there. `assets/liber-primus.txt' is left as-is (36 KB / 5,374 words, small enough not to need trimming). 100K-word learning now measures about 196 ms.
* refactor(prog-lsp): replace obsolete lsp-eldoc-hookCraig Jennings2026-05-111-0/+28
| | | | lsp-mode 9.0.0 made `lsp-eldoc-hook' an obsolete alias for Emacs's `eldoc-documentation-functions', and `lsp-managed-mode' already adds `lsp-eldoc-function' to that buffer-local hook. Dropped the obsolete `(setq lsp-eldoc-hook nil)'. `cj/lsp--disable-eldoc-hover' now removes `lsp-eldoc-function' from the buffer-local `eldoc-documentation-functions' via `lsp-managed-mode-hook', which clears the obsolete-variable byte-compile warning. Tests cover the hook removal, leaving the default `eldoc-documentation-functions' value alone, and the module no longer naming `lsp-eldoc-hook'.
* perf(org-capture): cache file+headline target markersCraig Jennings2026-05-111-0/+89
| | | | A task capture took 15-20 seconds: Org resolves a `(file+headline FILE "Headline")' target by opening the file, widening, and regex-scanning from the top for the headline, and the inbox template captures to `(file+headline inbox-file "Inbox")' over and over. An `:around' advice on `org-capture-set-target-location' caches a marker per resolved file/headline. On the next capture it validates the marker (still live, still in an Org buffer, still at a heading, headline text still matches) and jumps straight there. On any mismatch it falls back to the normal scan/create and refreshes the cache. `M-x cj/org-capture-clear-target-cache' resets it. Tests cover the cache hit, marker invalidation after the headline text changes, and creating a missing headline.
* fix(calendar-sync): give the no-init .ics worker its module load-pathCraig Jennings2026-05-111-0/+23
| | | | The async .ics-to-Org worker runs `emacs --batch --no-site-file --no-site-lisp' and loads `calendar-sync.el' by absolute path, but that doesn't make its sibling `(require 'cj-org-text-lib)' resolvable, so the conversion died with "Cannot open load file: cj-org-text-lib". `calendar-sync--worker-command' now inserts `-L <module-dir>' before `-l calendar-sync.el', which keeps the worker isolated from `init.el' while letting it load its local module deps. Updated the worker-command test and added a regression test that runs the real no-init worker shape.
* feat(window): kill the other window's buffer with C-; b KCraig Jennings2026-05-111-0/+74
| | | | `cj/kill-other-window-buffer' (in undead-buffers.el, on `C-; b K') kills or buries the buffer shown in the other window and leaves that window and the split alone. The window just shows whatever bury/kill surfaces next. It reuses `cj/kill-buffer-or-bury-alive', so buffers in `cj/undead-buffer-list' (like `*scratch*') get buried. With more than two windows it acts on `next-window'. Sibling of `cj/kill-other-window' (M-S-o), which deletes the other window. This one keeps it.
* feat(window): resize the split with C-; b <arrow>Craig Jennings2026-05-111-0/+51
| | | | | | `C-; b <left>/<right>/<up>/<down>' moves the active window's divider that way (via `windsize'), then keeps `cj/window-resize-map' active so bare arrows keep nudging until any other key (or `C-g'/`<escape>'). `C-u N C-; b <right>' resizes by N. windsize was on `C-s-<arrow>' (Ctrl+Super), which a tiling WM intercepts, so those keys were useless. I dropped that binding. The package is now `:commands'-deferred, and `windsize-cols'/`windsize-rows' drop to 2 (8/4 overshoots in a held nudge loop). `cj/window-resize-sticky' dispatches on the arrow that triggered it and arms the loop.
* feat(ai-vterm): order the project picker by most-recently-usedCraig Jennings2026-05-114-10/+94
| | | | | | The picker's active group (projects with a live tmux session) used to sort alphabetically. It now leads with projects opened this session, most-recent first, then the rest of the active group alpha, then the no-session group alpha. An in-session list (`cj/--ai-vterm-mru'), pushed to the front by `cj/--ai-vterm-show-or-create' on every open, drives the order. An empty list reproduces the old alphabetical behavior. I also pulled in a fix: `cj/--ai-vterm-tmux-session-name' now sanitizes `.' and `:' in the basename to `_'. tmux disallows those chars in session names and silently rewrites them, so `.emacs.d' really runs in session `aiv-_emacs_d', not `aiv-.emacs.d'. The computed name never matched, so `.emacs.d' was wrongly treated as having no session and landed in the no-session picker group. (A crash-recovery would also spawn a duplicate instead of reattaching.) Sanitizing the same way tmux does keeps the names in sync.
* feat(vterm): unify the keys in vterm copy-mode and tmux historyCraig Jennings2026-05-112-19/+33
| | | | | | | | `vterm-copy-mode' and the `C-; x h' tmux-history buffer now share one key story. `M-w' copies the active region and stays put, so I can copy several things in a row. `C-g', `<escape>', or `q' leaves (resuming the live terminal, or closing the history buffer) without copying. `RET' is unbound (no special "copy and exit"). In copy-mode that meant removing vterm's default `RET' -> `vterm-copy-mode-done' binding. Before, `M-w' exited and copied as it went, which made grabbing more than one selection awkward. The history buffer's `cj/vterm-tmux-history-copy-and-quit' was the copy-and-exit one-shot. It's gone. `M-w' then `q' is the equivalent. I also moved `cj/vterm-tmux-history' from `C-; x C' to `C-; x h' (unshifted, and it frees `C') and refreshed the file's stale commentary header, which still referenced the old `C-; V' prefix and `<pause>'.
* feat(ai-vterm): keep emacsclient files out of the agent windowCraig Jennings2026-05-111-0/+127
| | | | | | `server-start' leaves `server-window' nil, so `server-switch-buffer' opens an `emacsclient -n' file in the selected window. When I'm typing in the agent vterm, the selected window is the agent window, so "tell the agent to open something" replaced the agent buffer with that file. I wired `server-window' to a function. When the selected window shows an `agent [...]' buffer, it puts the file in a non-agent window instead, splitting one off to the left of the agent when the agent is the only window. emacsclient invocations from anywhere else still go through `pop-to-buffer' unchanged. `cj/--ai-vterm-non-agent-window' picks the target window. It skips the minibuffer, dedicated windows, and any window already showing an agent buffer.
* 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.