aboutsummaryrefslogtreecommitdiff
Commit message (Collapse)AuthorAgeFilesLines
...
* 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.
* 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-122-88/+100
| | | | | | | | 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-122-0/+54
| | | | | | 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-122-1/+103
| | | | | | `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-122-15/+193
| | | | | | | | `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.
* 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.
* 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-122-38/+115
| | | | | | | | | | | | 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-122-6/+6
| | | | `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-122-5/+145
| | | | | | `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-122-4/+65
| | | | | | 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-122-42/+180
| | | | | | | | | | | | | | `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-122-0/+123
|
* refactor(build): extract the shared test-runner loop into a make macroCraig Jennings2026-05-121-93/+70
| | | | | | | | `test-unit' and `test-integration' carried the same ~45-line shell loop (run each .el file in its own Emacs with the slow/perf-tagged tests skipped, count "Ran N", collect failures, dump a per-file log, print a ✓/✗ summary banner, exit 1 on any fail), differing only in the file list and the label. The `:perf' tag filter had to be added in both last week, which is the kind of drift this invites. Now a `define run-el-tests' macro (parametrized by the file list and the lowercase/uppercase labels) holds the loop once, and the two targets are one `@$(call run-el-tests,...)' line each. The banner string moved to a `BANNER' variable. No behavior change: `make test-unit', `make test-integration', and `make test' produce the same output. `coverage' keeps its own loop. It uses `run-coverage-file.el' instead of `-l ert', skips the pass-count and the per-file log, has a much shorter summary, and is bracketed by the .elc cleanup and the coverage-file check. Folding it in would need three more parameters and conditionals, which would cost more clarity than the duplication does.
* feat(dirvish): print the file at point with PCraig Jennings2026-05-122-1/+181
| | | | | | `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-123-39/+147
| | | | | | `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-122-2/+12
| | | | `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-122-7/+12
| | | | `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-122-8/+9
| | | | 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-122-7/+7
| | | | `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-124-308/+341
| | | | | | 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.
* build: add make benchmark target and skip :perf tests by defaultCraig Jennings2026-05-122-11/+22
| | | | | | `make test', `make coverage', and the editor's PostToolUse test runner all selected `(not (tag :slow))', which let the now-`:perf'-tagged benchmark suite run alongside the unit tests. Each of those three filters now also excludes `:perf', and a new `make benchmark' target runs the benchmark file on its own. `make test-file' and `make test-name' keep their old filters so the suite stays reachable for ad-hoc runs. `COVERAGE_EXCLUDE' loses `test-lorem-optimum-benchmark.el'. The tag filter handles it now, so the only entry left is `test-all-comp-errors.el', which byte-compiles modules and can't run under undercover's instrumentation.
* 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'.
* docs: clarify the coverage-exclude and token-seed commentsCraig Jennings2026-05-112-2/+5
| | | | 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.
* 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'.
* fix(org-capture): single-source the file+headline cache keyCraig Jennings2026-05-111-1/+1
| | | | `cj/org-capture--goto-file-headline' stored the marker under `(list (expand-file-name (buffer-file-name)) headline)' while the lookup helper `cj/org-capture--file-headline-cache-key' uses `(list (org-capture-expand-file path) headline)'. They agree for plain absolute paths, but a symlinked target (`expand-file-name' doesn't follow symlinks, `find-file' canonicalizes the buffer name) or a capture path that needs `org-capture-expand-file's special handling would make the put-key and get-key diverge, so the cache would silently never hit. Production now goes through the helper too, so the key is built one way.
* feat(mu4e): simpler attachment-save commands on C-; e S/s/mCraig Jennings2026-05-112-10/+474
| | | | 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-112-18/+89
| | | | 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.
* build(make): exclude the lorem-optimum benchmark from coverage runsCraig Jennings2026-05-111-1/+3
| | | | `test-lorem-optimum-benchmark.el' runs under undercover during `make coverage', and the instrumentation slows the path enough to fail the benchmark's timing assertions. Added it to `COVERAGE_EXCLUDE' alongside `test-all-comp-errors.el'. It still runs in the normal `make test' flow.
* fix(slack): harden and curate the C-; S ! reaction workflowCraig Jennings2026-05-112-1/+185
| | | | 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.
* feat(setup-email): add the deepsat work accountCraig Jennings2026-05-111-29/+62
| | | | `setup-email.sh' was still gmail+cmail only. Added `dmail' as a first-class maildir (`~/.mail/dmail') and the work address to the `mu init' list, and reworked password bootstrap to match the live config: the gmail and dmail password files stay encrypted (mbsync/msmtp decrypt them on use), while cmail decrypts to `~/.config/.cmailpass' for ProtonBridge. A missing password source now fails loudly instead of continuing silently. `bash -n' verified. The script itself wasn't run, since it decrypts credentials, runs mbsync, and reindexes mu.
* perf(lorem-optimum): speed up the Markov generation pathCraig Jennings2026-05-113-56/+70
| | | | `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-112-1/+36
| | | | 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-112-0/+195
| | | | 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-112-1/+26
| | | | 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-113-1/+96
| | | | `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-113-7/+98
| | | | | | `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-115-21/+151
| | | | | | 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(keymap): bind eval-buffer to C-; b eCraig Jennings2026-05-111-3/+5
| | | | `C-; b e' read best for `eval-buffer', but `e' was `cj/view-email-in-buffer' and the requested fallback `C-; b m' is `cj/move-buffer-and-file', so email-view moves to `C-; b E' (docstring and which-key updated).
* feat(vterm): unify the keys in vterm copy-mode and tmux historyCraig Jennings2026-05-113-45/+64
| | | | | | | | `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-112-0/+172
| | | | | | `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-112-6/+116
| | | | | | `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'.
* chore(hooks): eval load-prefer-newer before byte-compile and test runsCraig Jennings2026-05-111-1/+5
| | | | The validate-el.sh hook loaded stale `.elc` files when the matching `.el` was newer (e.g. after a sed-driven rename where the byte-compile hook never fires). `make test` already sets this. The hook didn't, so I added `(setq load-prefer-newer t)` to all three emacs invocations: the parens check, the byte-compile, and the test run.
* refactor(ai-vterm): rename Claude-specific names to a generic "agent"Craig Jennings2026-05-1122-445/+448
| | | | | | | | | | | | | | | | | 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-112-6/+36
| | | | 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-116-61/+344
| | | | | | 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.