<feed xmlns='http://www.w3.org/2005/Atom'>
<title>dotemacs/modules/dev-fkeys.el, branch main</title>
<subtitle>My Emacs configuration
</subtitle>
<id>https://git.cjennings.net/dotemacs/atom?h=main</id>
<link rel='self' href='https://git.cjennings.net/dotemacs/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/'/>
<updated>2026-06-03T01:38:06+00:00</updated>
<entry>
<title>feat(ui): name the operation in completing-read prompts</title>
<updated>2026-06-03T01:38:06+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-03T01:38:06+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=13b053c2a99d30c1131d920a62febde6ee9a628b'/>
<id>urn:sha1:13b053c2a99d30c1131d920a62febde6ee9a628b</id>
<content type='text'>
A picker prompt is the last thing shown before a command commits, so a bare noun leaves a mis-keyed command ambiguous. Hitting C-f8 (project agenda) instead of C-f9 (AI-vterm picker) gave the same "Project:" prompt with no signal which one was about to run.

Reworded 17 prompts across 8 modules so each names the operation rather than just the thing being chosen: "Project:" becomes "Show agenda for project:", "F6:" becomes "Run tests:", the dwim-shell sub-prompts gain their context (checksum algorithm, PDF compression quality, text-to-speech voice, run dwim-shell command), the two contact pickers split into "Find contact:" and "Insert contact email:", and the dirvish ediff, org finalize, and custom-comments length/box-style prompts get the same treatment.

I audited all ~124 completing-read / read-* call sites; the rest already named their operation and were left alone. These are prompt-string changes only, no logic touched.
</content>
</entry>
<entry>
<title>refactor(load-graph): route C-; registration through the keymap API</title>
<updated>2026-05-25T00:59:28+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-25T00:59:28+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=08014b2f15e099a1c5e662a17a41290f37aeebf4'/>
<id>urn:sha1:08014b2f15e099a1c5e662a17a41290f37aeebf4</id>
<content type='text'>
Migrated all 31 cj/custom-keymap registration sites across 24 modules from direct (keymap-set cj/custom-keymap ...) calls to cj/register-prefix-map and cj/register-command. Consumers no longer reference cj/custom-keymap directly, so keybindings.el is the sole owner of the C-; prefix and modules reach it only through the API (each already requires keybindings from Phase 2).

Behavior-preserving: I dumped every C-; binding before and after the migration and they're identical: 279 bindings, each resolving to the same command. The which-key label blocks are untouched, since they use string key descriptions and never assumed the keymap existed. I byte-compiled all 24 files (no new free-variable warnings, because the cj/custom-keymap references are gone), and make test, validate-modules, and an init load all pass.
</content>
</entry>
<entry>
<title>refactor(load-graph): make hidden module dependencies explicit</title>
<updated>2026-05-24T23:36:19+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-24T23:36:19+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=36a453d2c1237b49f594b23433858a0146dbf31e'/>
<id>urn:sha1:36a453d2c1237b49f594b23433858a0146dbf31e</id>
<content type='text'>
Phase 2 of the load-graph project. I fixed the seven hidden dependencies the classification surfaced, so each module declares what it uses instead of relying on init order.

- system-defaults now requires host-environment and user-constants at runtime. They were eval-when-compile only, but env-bsd-p and user-home-dir are read at load, so the compiled module couldn't load standalone.
- custom-buffer-file, dev-fkeys, calendar-sync, and video-audio-recording require keybindings and drop their (when (boundp 'cj/custom-keymap) ...) shims. The shim silently dropped the C-; binding when the module loaded before keybindings. The explicit require makes the dependency real.
- flycheck-config and mail-config require keybindings for their cj/custom-keymap bindings (a use-package :map and a direct keymap-set).
- Removed a dead eval-when-compile (defvar cj/custom-keymap) in transcription-config; nothing there used the variable.

No init.el load-order change. keybindings and the foundation modules already load before these, so the requires are no-ops at startup and only fix standalone and test loading.

I verified each fix with a fresh emacs --batch (require 'X), then swept all modules standalone: every one loads or fails only with a clear missing-package message. Full make test, make validate-modules, and an init smoke all pass. Module headers and the inventory's hidden-dependency section are updated to mark the seven resolved.
</content>
</entry>
<entry>
<title>docs(load-graph): classify dev, diff, help, lint, and VC modules</title>
<updated>2026-05-24T21:30:20+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-24T21:30:20+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=61dad3bd7b327eb0cf5bb3a6a0421a7055ed126b'/>
<id>urn:sha1:61dad3bd7b327eb0cf5bb3a6a0421a7055ed126b</id>
<content type='text'>
Fifth classification batch: the development-workflow entry points and package config — coverage-core, coverage-elisp, dev-fkeys, diff-config, help-config, help-utils, flycheck-config, test-runner, vc-config. I annotated each header, added a Batch 5 table to the inventory, and extended the validation allowlist. 42 of 102 modules are now classified.

Two more hidden dependencies turned up, both about cj/custom-keymap. dev-fkeys repeats the custom-buffer-file boundp shim for its C-; P binding. flycheck-config binds (:map cj/custom-keymap ...) through use-package without requiring keybindings, so it fails to load standalone. Both recorded for the Phase 2 dependency pass.
</content>
</entry>
<entry>
<title>refactor(prog): six programming-track hygiene fixes from re-review</title>
<updated>2026-05-16T08:55:49+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-16T08:55:49+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=d84aa4374af5e3447445377a836c66cc07d7a223'/>
<id>urn:sha1:d84aa4374af5e3447445377a836c66cc07d7a223</id>
<content type='text'>
- prog-lsp.el: rename `cj/lsp--remove-eldoc-provider' →
  `cj/lsp--remove-eldoc-provider-global' and call it once from the
  lsp-mode `:config' block instead of attaching it per-buffer via
  `lsp-managed-mode-hook'.  The previous per-buffer remove with the
  buffer-local flag raced lsp-mode's own population of the local
  hook; removing the provider from the global default before any LSP
  buffer attaches makes the absence stick.  Two existing tests
  updated to the new contract (remove-from-default + idempotent
  re-run).

- prog-webdev.el / prog-python.el: warn at load time when
  `prettier' or `pyright' is missing on PATH via
  `cj/executable-find-or-warn'.  Both modules now `(require
  'system-lib)' to expose the helper.  Missing dependencies surface
  up front instead of mid-edit at first format/LSP attach.

- keyboard-compat.el: document existing idempotence.  The hook
  install uses a named function so `add-hook' deduplicates, and the
  hook body only calls `define-key' (latest binding wins, same
  value) -- adding a comment so future readers don't re-question.

- dev-fkeys.el: add a `typescript' clause to
  `cj/--f6-test-runner-cmd-for'.  F6 now runs `npx --no-install
  vitest &lt;path&gt;' when vitest is on PATH, otherwise `npx --no-install
  jest &lt;path&gt;'.  Updates the matching test from "returns nil" to
  cover both code paths; the impl-level test now asserts the routed
  command instead of expecting a user-error.

- flycheck-config.el: build the LanguageTool wrapper path with
  `(expand-file-name "scripts/languagetool-flycheck"
  user-emacs-directory)' instead of a hardcoded `~/.emacs.d/...'.
  Survives a non-standard `user-emacs-directory'.

- latex-config.el: replace the hardcoded Zathura viewer with
  `cj/--latex-select-pdf-viewer', which walks
  `cj/--latex-pdf-viewer-candidates' (zathura → evince → okular →
  SumatraPDF → xdg-open) and falls back to "PDF Tools" when nothing
  is on PATH.  Each entry maps an executable to the matching
  TeX-view-program-list name so AUCTeX's defaults handle the
  actual viewer invocation.
</content>
</entry>
<entry>
<title>refactor(system-lib): extract cj/shell-quote-argument-readable from dev-fkeys</title>
<updated>2026-05-10T19:13:56+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-10T19:13:56+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=f1e8f0898244bd2d834baf7541d10e5eff351d34'/>
<id>urn:sha1:f1e8f0898244bd2d834baf7541d10e5eff351d34</id>
<content type='text'>
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, `$', `;', `&amp;', 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.
</content>
</entry>
<entry>
<title>refactor: defer projectile revert advice until projectile loads</title>
<updated>2026-05-04T02:17:30+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-04T02:17:30+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=2f8d8989856073cbed5f9159d02089903bc5343e'/>
<id>urn:sha1:2f8d8989856073cbed5f9159d02089903bc5343e</id>
<content type='text'>
`dev-fkeys.el` was wiring its three Projectile cache-revert advices via top-level `advice-add` calls using `apply-partially #'cj/--projectile-around-revert &lt;map-symbol&gt;`. That had three problems. The advice values were anonymous closures, so `advice-member-p` couldn't find them and a re-load would silently double-install. The implicit dependency on Projectile was load-ordered by accident. If `dev-fkeys.el` happened to require before Projectile loaded, the advice still attached to unbound symbols. And a fresh batch require of `dev-fkeys.el` for tests would always force the advice attempt regardless of whether Projectile was around.

I gave each Projectile target a named advice wrapper (`cj/--projectile-compile-around-revert`, `cj/--projectile-test-around-revert`, `cj/--projectile-run-around-revert`) and put the (target . advice) pairs in a `cj/--projectile-revert-advice-specs` defconst. `cj/--projectile-install-revert-advice` walks the specs, checks `fboundp` plus `advice-member-p`, and only adds advice that's missing. The installer is idempotent on reload, and the named wrappers make it easy to tear down later by symbol name.

`cj/--projectile-register-revert-advice` is the entry point at module load time. It installs immediately when Projectile is already a `featurep`, otherwise it schedules the installer through `eval-after-load 'projectile`. Either way the advice is in place once Projectile is available, and `dev-fkeys.el` no longer relies on a particular load order.

Tests in the new `tests/test-dev-fkeys--projectile-advice-install.el` cover four cases. Registration defers via `eval-after-load` when Projectile isn't a feature yet. Registration installs immediately when it is. Install skips unbound Projectile functions. Install advises each bound Projectile command runner with the matching named wrapper. 23 projectile-related tests pass together.
</content>
</entry>
<entry>
<title>fix: scope projectile cache revert state to each compile</title>
<updated>2026-05-04T02:11:26+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-04T02:11:26+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=31edc86a54d20c3c73d0ebad247fde2c35e6a964'/>
<id>urn:sha1:31edc86a54d20c3c73d0ebad247fde2c35e6a964</id>
<content type='text'>
The projectile compile/test/run cache-revert protection in `dev-fkeys.el` used a single global variable, `cj/--projectile-revert-state`. Two overlapping compiles could clobber each other's state. The second compile's capture would overwrite the first's. So when the first compile finished and ran the global finish-hook, it'd act on the wrong project's state, or revert nothing because the keys had drifted.

I moved the state into a closure. `cj/--projectile-capture-cmd` now returns the state plist instead of mutating the global. `cj/--projectile-around-revert` captures the state into a local, calls the projectile cmd-runner, and installs a one-shot buffer-local finish hook on the returned compilation buffer. The hook closes over its own state plist, so two compiles can finish in any order and each one acts on the right project.

I extracted three small helpers along the way. `cj/--projectile-revert-state-on-fail` is the pure decision (revert when failed AND modified AND prior was non-nil). `cj/--projectile-make-revert-on-fail-hook` builds the closure-based one-shot hook. `cj/--projectile-compilation-buffer` normalizes a buffer-or-process result from projectile into a buffer.

The legacy `cj/--projectile-revert-on-fail` function still reads the global `cj/--projectile-revert-state`. It stays around for the existing direct tests, but its core logic now delegates to the extracted state-on-fail helper. No production caller adds it to `compilation-finish-functions` anymore.

I added one regression test in `test-dev-fkeys--projectile-around-revert.el`: two projectile invocations on different projects, finishes triggered out of order, each compile reverts its own project's cache and leaves the other alone. The capture and around-advice tests were rewritten to match the new return-style API and to assert hooks land buffer-locally rather than globally. 19 projectile-related tests pass together.
</content>
</entry>
<entry>
<title>fix: shell-quote F6 test-runner command arguments</title>
<updated>2026-05-04T00:44:05+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-04T00:44:05+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=59094e944b4a9e8c5e1a20724c2681ffe9b6155c'/>
<id>urn:sha1:59094e944b4a9e8c5e1a20724c2681ffe9b6155c</id>
<content type='text'>
`cj/--f6-test-runner-cmd-for` was building shell command strings with raw paths and stems via `format`. For ordinary names (`tests/test_foo.py`, `pkg/foo`) that worked fine. But a path with spaces or a stem with shell metacharacters would break or misbehave once the string hit `compile`. A Python test file under `dir with spaces/` would get tokenized as separate arguments.

I added `cj/--f6-shell-quote-argument` that escapes only when the argument doesn't match `cj/--f6-shell-safe-argument-regexp` (alphanumerics, slash, dot, dash, plus a small handful of safe punctuation). Ordinary paths skip the quoter and stay readable. Risky paths route through `shell-quote-argument`.

I wrapped the four interpolations in the test-runner builder: the elisp `FILE=` basename, the elisp `TEST=^test-stem-` regex, both pytest paths, and the Go `./rel-dir`. The Go branch also handles an empty rel-dir explicitly so the result stays `go test ./` instead of constructing `./` via format with an empty string.

I added three boundary tests: a Python path with spaces, an elisp stem with `;`, and a Go directory with spaces. Existing tests for ordinary paths continue to pass since the safe regex covers them.
</content>
</entry>
<entry>
<title>feat(dev-fkeys): revert projectile cache on failed-and-modified compile</title>
<updated>2026-05-03T23:25:58+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-03T23:25:58+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=475d6305e150c0a8ac61738eabe434c432acd991'/>
<id>urn:sha1:475d6305e150c0a8ac61738eabe434c432acd991</id>
<content type='text'>
Without this, a one-off typo at projectile's compile/test/run prompt poisons the per-project cache: every subsequent invocation pre-fills the broken value. I hit it during the Phase 2a live-test, where projectile's "All tests" prompt was replaying `go test ../.` and there was no clean way to get the prior known-good back.

Three pieces of machinery, all in `dev-fkeys.el`:

`cj/--projectile-capture-cmd' captures the current cached cmd at the project root before each invocation, stashing a plist with :map / :root / :prior in `cj/--projectile-revert-state'.

`cj/--projectile-revert-on-fail' is a `compilation-finish-functions' hook that reads that state. If the compile failed AND the cmd was modified from the captured prior value AND the prior was non-nil, it puts the prior back in projectile's cmd-map. Test-fails-because-of-real-bug (cmd unchanged through the run) leaves the cache alone. The hook self-removes on first invocation regardless of outcome and clears the state.

`cj/--projectile-around-revert' is the around-advice that wires the two together. I added the advice to all three projectile cmd-runners — `projectile-compile-project', `projectile-test-project', `projectile-run-project' — so the auto-revert applies whether the user invoked via F4 / F6 or directly via `M-x'.

Plus the manual escape-hatch: `cj/projectile-reset-cmds' clears compile/test/run cache for the current project. Bound to `C-; P' under the personal keymap. Use when projectile's auto-derived default was wrong from the start and you want to start fresh — the next F4 / F6 invocation re-derives projectile's project-type default.

TDD: 18 new tests across 4 files, one per helper. The around-advice tests build the capture/install/orig-fn flow against stub cmd-maps and verify state captured, hook installed, orig-fn invoked. The revert hook tests cover failure-and-modified (revert), success (leave alone), failure-but-unchanged (leave alone), nil prior (leave alone), nil state (no-op), and self-removal. The reset-cmds tests cover the all-three-maps clear, no-cached-entry no-op, and no-project user-error.
</content>
</entry>
</feed>
