aboutsummaryrefslogtreecommitdiff
path: root/modules
Commit message (Collapse)AuthorAgeFilesLines
* fix: validate mail transport executables and default debug offCraig Jennings2026-05-031-6/+58
| | | | | | | | | | `mail-config.el` had three related issues. SMTP transport debug was hard-coded to t, which is sensitive since mail bodies and headers land in debug buffers. The use-package `:config` was also setting `sendmail-program` and `mu4e-get-mail-command` directly from `executable-find` results. So a host without msmtp or mbsync silently got `nil` or `(concat nil " -a")` instead of a clear failure mode. I added `cj/smtpmail-debug-enabled` (default nil) plus `cj/set-smtpmail-debug` and `cj/toggle-smtpmail-debug` for temporary troubleshooting, mirroring the pattern from `auth-config.el`. I extracted `cj/mail--executable-or-warn` so a missing program emits a one-time `display-warning` and returns nil. `cj/mail-configure-smtpmail` and `cj/mail--mbsync-command` both use it. Missing msmtp now leaves `sendmail-program` nil with a warning. Missing mbsync produces a nil sync command instead of the broken `(concat nil " -a")` string. I also wrapped the mbsync executable path in `shell-quote-argument` so unusual install paths don't fall apart on the `" -a"` concat. I added `tests/test-mail-config-transport.el` with seven tests across Normal / Boundary / Error: debug-default-off, toggle wiring, msmtp present and missing, mbsync present, mbsync path with spaces, and mbsync missing. The `test-mail-config--with-executables` macro stubs `executable-find` from an alist so each test names its own environment.
* fix: shell-quote F6 test-runner command argumentsCraig Jennings2026-05-031-5/+27
| | | | | | | | | | `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.
* fix: clarify reset-auth-cache failure messageCraig Jennings2026-05-031-1/+1
| | | | | | | | `cj/reset-auth-cache`'s error path read "Failed to clear gpg-agent cache". A user seeing that warning could reasonably think nothing happened. But at that point the Emacs-side caches (auth-source + EPA file handler) have already been cleared. Only the gpg-agent cache failed. I rewrote the message as "Emacs caches cleared, but failed to clear gpg-agent cache" so the user sees both the partial success and the remaining problem. The error-path test from the previous commit asserts a substring match on "Failed to clear gpg-agent cache", so it still passes after the rewording.
* fix: default auth-source debug logging to disabledCraig Jennings2026-05-031-2/+25
| | | | | | | | | | `auth-config.el` was setting `auth-source-debug` to t at startup. That meant every credential lookup printed verbose context to *Messages*. The flag was useful while debugging GPG flow but not appropriate for steady state, since the same config handles Slack, AI, REST, mail, and transcription credentials. I added a `cj/auth-source-debug-enabled` defcustom (default nil) and wired the use-package block to read its value. For temporary troubleshooting I added two commands: `cj/set-auth-source-debug` (prompted on / off via `y-or-n-p`) and `cj/toggle-auth-source-debug` (M-x convenience). I also scanned the nearby auth callers. The visible failure messages name hosts and logins but don't print secret values directly. So this change closes the practical exposure path without losing useful diagnostics. I added `tests/test-auth-config-debug.el` covering the disabled-by-default invariant and the setter wiring through both public variables.
* fix: close drill capture template source linkCraig Jennings2026-05-031-2/+2
| | | | | | | | | | | | | The "d" drill capture template had a malformed source link with only one closing bracket and a literal `n` where a newline should have been: Source: [[%:link][%:description] nCaptured On: %U So new drill captures wrote a broken link and `nCaptured On:` instead of a clean `Captured On:` line on the next row. I closed the link with the missing `]`, and removed the stray `n` so the line break before "Captured On" is just a real newline. The "f" (PDF drill) template was already correct, so I left it alone. I added `tests/test-org-capture-config-drill-template.el`. It loads the module, picks up the "d" template, and asserts both the closed link and the `\nCaptured On: %U` form, plus negative assertions against the broken pre-fix shapes.
* fix: use file basename when moving buffer + fileCraig Jennings2026-05-031-2/+6
| | | | | | | | `cj/--move-buffer-and-file` was building the destination as `(concat dir "/" (buffer-name))`. If the buffer had been renamed via `M-x rename-buffer`, or uniquified by Emacs with a `<2>` suffix when a second buffer visited the same filename, the move wrote a file with the wrong name on disk. I derived the destination basename from `buffer-file-name` instead, in both the internal helper and the interactive wrapper. The wrapper's overwrite-prompt now also formats the real target filename rather than the buffer name. I added two regression tests: one for a renamed buffer visiting `original.txt`, and one for a `<2>` uniquified buffer with a trailing-slash target directory.
* fix: keep C-; ! as system command prefixCraig Jennings2026-05-031-13/+21
| | | | | | | | The module was binding `cj/system-command-map` under `C-; !`, then a few lines later overwriting the same prefix with `cj/system-command-menu`. The second bind won, so every documented subkey, like `C-; ! r` for reboot and `C-; ! l` for lock, was unreachable. I kept the prefix map and folded the completing-read menu into it at `C-; ! !`. So `C-; !` still opens the prefix, the menu is one extra `!` away, and the single-letter shortcuts work again. I also added which-key labels for every documented subkey so the popup actually says what each one does. I added `tests/test-system-commands-keymap.el`. It asserts the prefix stays mounted and that every binding (`!`, `L`, `r`, `s`, `S`, `l`, `E`, `e`) resolves to the right command.
* fix: set vc-follow-symlinks explicitly to tCraig Jennings2026-05-031-1/+1
| | | | | | | | The line read `(setq-default vc-follow-symlinks)` with no value. That left the variable at nil, so the comment "don't ask to follow symlinks if target is version controlled" was a lie. Opening any version-controlled symlink still prompted. I checked the Emacs docs first. The value `t` is the one that follows the link without asking, so that's what I set. I added `tests/test-system-defaults-vc-follow-symlinks.el` as a regression test. It loads the module with the unrelated side effects stubbed and asserts `vc-follow-symlinks` ends up as `t`.
* feat(dev-fkeys): revert projectile cache on failed-and-modified compileCraig Jennings2026-05-031-0/+93
| | | | | | | | | | | | | | | | 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.
* feat(dev-fkeys): propagate prefix-arg to projectile through F4 / F6Craig Jennings2026-05-031-4/+4
| | | | | | | | | | | | | I had four call sites passing literal nil to `projectile-compile-project' / `projectile-run-project' / `projectile-test-project'. The literal nil ignored whatever prefix arg the user gave. So `C-u F4 → Compile' or `C-u F6 → All tests' didn't actually force projectile's re-prompt — the prefix arg got swallowed at our wrapper layer. Switched all four to `current-prefix-arg': - `cj/--f4-dispatch' — `compile-only' and `run-only' actions. - `cj/f4-compile-only' — the C-F4 fast path's compiled-project branch. - `cj/f6-test-runner' — the "All tests" menu entry. Use case: when projectile's cached cmd is wrong (typo, stale, or whatever), `C-u' on any of these forces projectile to re-prompt instead of replaying the bad cmd silently. The compile-and-run and clean-rebuild paths still pass nil to their chained projectile calls because those run inside an async `compilation-finish-functions' hook, where `current-prefix-arg' has already reverted to nil. Refining those would need to capture the prefix at entry and thread it through; left for later. TDD: 4 new tests (one per call site) bind `current-prefix-arg' to t and verify projectile receives t. Each test failed against the literal-nil version and passes against `current-prefix-arg'.
* chore(prog-general): disable auto-close of *compilation* windowCraig Jennings2026-05-031-10/+15
| | | | | | I commented out the global `compilation-finish-functions' hook that closed the *compilation* window 1.5 seconds after a successful compile. With the F6 test runner now landing test output in *compilation*, I want the buffer to stay open afterward so I can read the results, not have it slide out from under me. The block stays in the file as a commented-out reference so I can flip it back on later if I want. A prog-mode-only variant is noted in the comment for the day I want the auto-close back for non-prog compiles (org-export, etc.) — that needs advice on `compile' to capture the originating buffer's `major-mode' at compile-start, since the hook fires after `compilation-mode' has already taken over the current buffer. Skipped for now per the simpler path.
* fix(dev-fkeys): F6 elisp runner uses basename, not rel-pathCraig Jennings2026-05-031-1/+3
| | | | | | | | | | I shipped Phase 2a with `cj/--f6-test-runner-cmd-for' building `make test-file FILE=<rel-path>' for elisp test files (e.g., FILE=tests/test-foo.el). The project Makefile prepends `tests/' to FILE itself, so the full invocation expands to `tests/tests/test-foo.el' and emacs reports "Cannot open load file". The bug surfaced on a live-test in step 7 of the Phase 2a smoke plan. Fix: pass `(file-name-nondirectory rel-path)' so the Makefile gets just `test-foo.el' and re-prepends `tests/' itself. Two unit tests in `test-dev-fkeys--f6-test-runner-cmd-for.el' had encoded the wrong expectation (the rel-path form). Two orchestrator tests in `test-dev-fkeys--f6-current-file-tests-impl.el' inherited the same wrong assertion via integration. Updated all four to assert the basename form. Verified: full suite green, including the 4 updated tests. Live re-test on `tests/test-dev-fkeys--f6-language-detect.el' should now produce the working `make test-file FILE=test-dev-fkeys--f6-language-detect.el'.
* feat(dev-fkeys): add F6 test runner menu (Phase 2a)Craig Jennings2026-05-031-12/+184
| | | | | | | | | | | | | | | | | I extended `dev-fkeys.el` with the F6 dispatcher half of the spec. F6 prompts via `completing-read` between two candidates: "All tests" delegates to `projectile-test-project`, and "Current file's tests" detects the buffer's language by extension, derives the runner command, and pipes through `compile' from the projectile root. C-F6 is the fast path straight to "Current file's tests". Per-language coverage: - Elisp source files map to `make test-name TEST=^test-<stem>-`. Elisp test files run with `make test-file FILE=<rel-path>` so a per-helper file like `test-foo--bar.el' runs only its own tests. - Python source files map to `pytest tests/test_<stem>.py'. Python test files run with `pytest <rel-path>'. - Go runs the package containing the file: `go test ./<rel-dir>'. Source and test files use the same command since Go test scope is per-package. Limit: this runs every `_test.go' in the package, not just the buffer's file. Phase 2b can refine via test-name discovery. - TypeScript and JavaScript are detected but punted for v1. The runner-command builder returns nil and the orchestrator signals a user-error rather than guessing. The F6 binding moved from the Phase 1 stopgap (`projectile-test-project') to `cj/f6-test-runner'. C-F6 is newly bound to `cj/f6-current-file-tests'. M-F6 stays unbound, reserved for Phase 2b's "Run a test..." menu entry. TDD: 68 new tests across 7 files. Production code split into small testable internals (`cj/--f6-language-detect', `cj/--f6-buffer-is-test-file-p', `cj/--f6-source-stem', `cj/--f6-test-runner-cmd-for', `cj/--f6-current-file-tests-impl') plus two thin interactive wrappers. Smoke tests confirm bindings register on load. I also updated the module commentary with the Phase 2b plan, the capture-then-filter approach for tree-sitter discovery, and a pointer to Emacs bug #79687. The bug is the predicate-syntax mismatch that breaks `:match' / `:equal' / `:pred' queries on Emacs 30.2 with libtree-sitter 0.26. The fix lives on Emacs master (commit b0143530), targets Emacs 31, and has not been backported to the emacs-30 branch as of today. Phase 2b will use queries without predicates and filter results in Elisp, sidestepping the issue. Mike Olson's `treesit-predicate-rewrite.el' applies the same idea to font-lock if you want it before Phase 2b lands.
* feat(dev-fkeys): add project-aware F4 compile/run dispatcherCraig Jennings2026-05-035-43/+236
| | | | | | | | | | | | | | | | | | | I added a new module `modules/dev-fkeys.el` that owns the dev F-key block. F4 prompts via `completing-read` with a candidate set filtered by project type (compiled / interpreted / unknown). C-F4 is the compile-only fast path. M-F4 is clean + rebuild. It runs a heuristic clean command derived from the project markers (go.mod, Cargo.toml, Eask, Makefile, CMakeLists.txt) and chains `projectile-compile-project` on success. S-F4 stays on `recompile` and now lives globally instead of duplicated across prog-general.el and prog-c.el. F6 is bound globally to `projectile-test-project` as a Phase 1 stopgap. Phase 2 replaces it with the polyglot test runner spec'd in todo.org. Project-type detection runs against the projectile root and falls back to `unknown` when no marker matches. Interpreted markers are checked first so a Python or Node project with a Makefile for tasks classifies as interpreted instead of compiled. Compile + Run sequencing uses a one-shot `compilation-finish-functions` hook that self-removes on first invocation and only fires the follow-up when the status string starts with `finished`. Cleanup in the same commit: - Dropped F4/F5/F6 from `prog-general.el`'s prog-mode-hook. They are now global. - Dropped F6→format bindings from prog-c.el / prog-python.el / prog-shell.el. C-; f was already bound in each, so this is pure removal. - Dropped the duplicate S-F4 from prog-c.el. The global binding covers it. - Updated the keybinding header in prog-general.el and the workflow comments in prog-c.el / prog-shell.el. - Wired `(require 'dev-fkeys)` in init.el alongside coverage-core. TDD: 73 tests across 11 files, one per helper. Production code is split into small testable internals (`cj/--detect-project-type`, `cj/--f4-candidates`, `cj/--f4-derive-clean-cmd`, `cj/--f4-make-once-hook`, `cj/--f4-dispatch`, `cj/--f4-compile-and-run-impl`, `cj/--f4-clean-rebuild-impl`, `cj/--f4-project-root`) plus three thin interactive wrappers. Smoke tests confirm bindings register on load. Known limitation: if another `compilation-finish-functions` hook fires between my add-hook and the compile finishing, the chain can fire on the wrong compile. The hook self-removes on first invocation regardless of which compile it sees. Documented in the impl docstring. Acceptable for v1. Phase 2 will replace F6 with the polyglot test runner (tree-sitter queries for Python/Go/TS, sexp scan for Elisp, buffer-local last-test memory).
* fix(line-paragraph): join-line-or-region strands space on next lineCraig Jennings2026-05-031-4/+10
| | | | | | | | | | The region branch's `(while (< (point) end) (join-line 1))` ran one iteration too many. After the final in-region join, point sat just before the end marker, so the loop fired once more. That extra `join-line 1` consumed the next line's preceding newline and replaced it with a space. Then `(goto-char end)` + `(newline)` reinserted a newline at the original end position, before the inserted space, so the space ended up stranded at BOL of the next line. I replaced the position-based loop with `count-lines` + `dotimes` to do exactly the right number of joins. I also swapped the trailing `(newline)` for `(forward-line 1)`. The bullet-list use case now lands directly on the next existing line with no blank gap. The trailing-newline change ripples to `cj/join-paragraph` (which delegates here), so paragraphs now also stop adding a trailing newline when the input lacks one. `require-final-newline` handles file-end discipline on save anyway. I added 3 new tests that fail against the old loop and pass against the fix. I also updated 11 existing tests whose assertions baked in the old trailing-newline behavior. While in there I wrapped the `cj/custom-keymap` defvar stub in `eval-and-compile` in both test files. The bare defvar wasn't evaluated at byte-compile time, so the `require` of `custom-line-paragraph` would hit a void symbol when the validate-el hook ran.
* chore(deps): add commented :load-path hints for local-dev swapsCraig Jennings2026-04-304-0/+4
| | | | | | | | | | | | Each :vc-installed package whose source repo is also cloned under ~/code now carries a commented :load-path line directly under the :vc form. Uncomment the :load-path and comment the :vc to flip into local development without rewriting the use-package block. Covered: gloss, org-drill, wttrin (emacs-wttrin), chime. Skipped: org-msg. The previous local clone at ~/code/org-msg is no longer present; if it gets re-cloned later, add the same hint there.
* chore(deps): move remaining packages from :load-path to :vc; drop archived ↵Craig Jennings2026-04-304-222/+7
| | | | | | | | | | | | | | | | | | | | | | | | | | org-gcal The three packages that still loaded from local checkouts now install via :vc: - chime → git@cjennings.net:chime.git (was :load-path "~/code/chime") - wttrin → git@cjennings.net:emacs-wttrin.git (was :load-path "/home/cjennings/code/emacs-wttrin") - org-msg → https://github.com/jeremy-compostella/org-msg (was :load-path "/home/cjennings/code/org-msg"; switching to upstream rather than a fork since the previous fork wasn't carrying any active changes) For the two cjennings.net repos this matches the org-drill (be3e227) and gloss (2e12131) shape: primary on cjennings.net, post-receive hook mirroring to GitHub. The previously-commented :vc URLs in chime and wttrin pointed at GitHub directly, which would have lost the cjennings-first convention if uncommented later. Also drops :ensure nil on chime (only relevant under package.el, not :vc) and removes modules/archived/org-gcal-config.el. Nothing in init.el or any module references org-gcal, so the file is genuinely unused.
* feat(gloss): wire gloss into init via :vc against cjennings.netCraig Jennings2026-04-301-0/+30
| | | | | | | | | | | | Adds modules/gloss-config.el with a use-package form that installs gloss from the cjennings.net bare repo. The bare's post-receive hook mirrors to GitHub, so the package shows up in both places. Eager-loaded so gloss-prefix-map exists at startup. :config calls gloss-install-prefix to bind C-h g. Lands in the "Modules In Test" section of init.el for v1. Can move out after the first-week shakedown shows the package is steady.
* fix(config-utilities): repair validate-org-agenda-timestamps property checkCraig Jennings2026-04-301-31/+65
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Two bugs in cj/validate-org-agenda-timestamps surfaced while extracting testable helpers. 1. The DEADLINE / SCHEDULED / TIMESTAMP property lookup used (intern (downcase prop)) as the key, producing 'deadline, 'scheduled, 'timestamp. org-element-property expects keywords (:deadline, :scheduled, :timestamp) and returns nil for plain symbols. The property-check branch had never reported anything since the function was written. Only inline-regex matches inside headline contents have ever been flagged. Fixed by building the keyword form: (intern (concat ":" (downcase prop))). 2. Once #1 is fixed, every property timestamp would also match the inline-timestamp regex during the contents scan (since the DEADLINE: / SCHEDULED: / TIMESTAMP lines fall inside contents-begin/end on a parsed headline), producing duplicate reports. Added a per-headline list of property timestamp strings and a member check before pushing an inline match. The function is also restructured into three pieces to make it testable: - cj/--validate-timestamps-in-buffer FILE — pure-ish: walks the current buffer, returns a list of (FILE POS HEAD PROP TS) tuples. - cj/--format-validation-report-section FILE INVALID — pure: returns the per-file org-formatted string. - cj/validate-org-agenda-timestamps (interactive) — orchestrates both helpers across org-agenda-files into a report buffer. The interactive entry-point's behaviour is unchanged from the user's side except that DEADLINE / SCHEDULED / TIMESTAMP property timestamps are now actually checked.
* refactor(config-utilities): extract cj/--recompile-emacs-homeCraig Jennings2026-04-301-24/+29
| | | | | | | | | | | | Splits the delete-then-recompile work out of cj/recompile-emacs-home so it takes DIR and an explicit NATIVE-P flag instead of probing boundp inside the work loop. Returns 'native or 'byte to surface which path actually ran. The interactive wrapper still asks `yes-or-no-p' against user-emacs-directory and probes `(boundp 'native-compile-async)' once to decide the dispatch and the prompt's wording. The cancellation message is unchanged.
* refactor(config-utilities): extract cj/--benchmark-methodCraig Jennings2026-04-301-8/+16
| | | | | | | | | | | | Splits the timer dispatch out of cj/benchmark-this-method so it can be tested with a known method symbol instead of going through read-string + completing-read. The interactive wrapper still prompts for both inputs, and now catches the user-error from the internal so the user-facing behaviour on invalid input is unchanged (the message goes to the echo area). Returns the funcall's value to the caller, which is observable through the timer.
* refactor(config-utilities): extract cj/--delete-compiled-files-in-dirCraig Jennings2026-04-301-8/+17
| | | | | | | | | | | | Splits the file-walking work out of cj/delete-emacs-home-compiled-files so it takes a directory parameter and returns a count. The interactive wrapper still hardcodes user-emacs-directory and prints the same status messages, just with the count interpolated. The split is scope-aligned with adding tests for the file-walking behaviour. The original function couldn't be tested without spawning files inside user-emacs-directory itself, which would pollute the running config.
* fix(host-environment): correct docstring order in cj/detect-system-timezoneCraig Jennings2026-04-301-2/+2
| | | | | | | | | The numbered list in the docstring had file-comparison and TZ env var swapped relative to what the code does. The code tries cj/match-localtime-to-zoneinfo first, then falls back to TZ. Updated the docstring so the numbering matches the actual `or' chain. Surfaced while writing tests for the priority chain.
* chore(deps): point org-drill :vc at cjennings.net primaryCraig Jennings2026-04-291-1/+1
| | | | The cjennings.net bare for org-drill has a GitHub mirror as of today's earlier remote migration. Update the :vc URL in modules/org-drill-config.el to point at the primary instead of the mirror so a fresh install clones from the source-of-truth.
* fix(eshell): correct call shape in eshell/find-using-diredCraig Jennings2026-04-291-2/+2
| | | | The body had `(find-name-dired . escaped-pattern)`, a dotted pair instead of a function call. The reader accepts it, but the form crashes the moment the `f` alias runs. find-name-dired takes (DIR PATTERN), so the right shape passes default-directory and the escaped pattern.
* feat(mail): add work account and reorganize C-; e bindingsCraig Jennings2026-04-271-29/+70
| | | | | | | | | | Adds a third mu4e context for a work email account. Reorganizes cj/email-map under C-; e: attach (A) and delete (D) move to uppercase to free c, d, g as account submaps. Each submap has i/u/s/l for inbox/unread/starred/large. Trims mu4e-bookmarks to one unread query per account on b c, b g, b d. The full grid lives under C-; e. mbsync and msmtp config for the new account lives in a separate dotfiles repo.
* fix(mail): default cj/custom-keymap so the file byte-compiles standaloneCraig Jennings2026-04-271-0/+8
| | | | | | | | | | | | cj/custom-keymap is defined in keybindings.el, which init.el loads before mail-config.el. The use-package org-msg :preface block calls keymap-set on it, and use-package wraps :preface in eval-and-compile. So byte-compiling mail-config.el on its own tries to call keymap-set when cj/custom-keymap is still void. Wrapping a defvar with a make-sparse-keymap default in eval-and-compile gives the symbol a value during compilation. At runtime keybindings.el has already populated the real keymap, so defvar does nothing.
* feat(lsp): add common build/cache dirs to file-watch ignore listCraig Jennings2026-04-261-1/+38
| | | | | | | | | | Extends `lsp-file-watch-ignored-directories' with thirteen build, cache, and tooling directories: `node_modules', `dist', `coverage', `target', `__pycache__', `.venv', `venv', `.pytest_cache', `.mypy_cache', `.ruff_cache', `test-results', `playwright-report', `tf/.terraform'. Uses `add-to-list', so lsp-mode's own defaults (`.git', `.svn', `.idea', etc.) stay in place. Setting these in a project's `.dir-locals.el' doesn't work. lsp-mode reads `lsp-file-watch-ignored-directories' once at workspace init, from the global value, so a buffer-local override never reaches the watch list. I confirmed this today: in a Python buffer where dir-locals had applied, `M-: lsp-file-watch-ignored-directories' returned the lsp-mode default, not the project's overrides. Setting it globally is what works. The goal is to push typical workspaces under `lsp-file-watch-threshold' (1000), so the "watch all files? (y or n)" prompt stops firing on every fresh LSP start. Also added a forward defvar for `lsp-enable-remote' to silence the matching free-variable warning under `make compile'.
* feat(dashboard): F1 lands point at the top of the bufferCraig Jennings2026-04-251-2/+3
| | | | cj/dashboard-only used to leave point wherever the dashboard buffer was last visited. Now it goes to point-min so the banner and navigator are visible on entry.
* refactor(system-utils): extract testable open-file helpersCraig Jennings2026-04-231-54/+57
| | | | | | | | | | | | | | | | | | | | Extracts two pure helpers from cj/open-file-with-command and cj/xdg-open so the file-resolution and launcher-detection logic becomes testable without mocking process launchers. New helpers: - cj/--file-from-context returns a file path from the current context, resolving in priority order (explicit arg, buffer-file-name, dired file at point). Returns nil when none apply. - cj/--open-with-is-launcher-p is a predicate for whether a command is a desktop launcher (xdg-open, open, start) that needs call-process detachment. Both commands now delegate. cj/open-file-with-command uses cj/--file-from-context with read-file-name as the final fallback, plus cj/--open-with-is-launcher-p for the launcher dispatch. cj/xdg-open uses cj/--file-from-context with user-error as the "no file" fallback. Behavior preserved. The existing system-utils test suites still pass, and the shape of each command's final effect is identical. New tests, 14 cases across two per-function files: - tests/test-system-utils--file-from-context.el covers: explicit wins over buffer-file, explicit wins over dired, buffer-file fallback, dired fallback, all-nil returns nil, explicit-nil uses chain, dired-mode-but-no-file-at-point. - tests/test-system-utils--open-with-is-launcher-p.el covers: each of the three launcher names returns t, non-launcher returns nil, empty string returns nil, case-sensitive check, nil input returns nil. Coverage: system-utils.el went from 10/52 (19.2%) to 15/52 (28.8%). The remaining uncovered lines are mostly in the process-launching paths of cj/open-file-with-command and cj/xdg-open. Those are testability-blocked. Mocking call-process, start-process-shell-command, and generate-new-buffer would give a lot of mock surface for low value. cj/server-shutdown is not meaningfully testable because it kills Emacs.
* feat(coverage): add whole-project scope to cj/coverage-reportCraig Jennings2026-04-231-6/+110
| | | | | | | | | | | | | | | Adds a fifth entry to the scope completing-read menu: "Whole project — all executable lines". Uses the existing cj/coverage-report flow, so the user still hits F7 and picks from the menu; the command dispatches based on the chosen scope. Two new pure helpers back the scope: - cj/--coverage-simplecov-executable-lines parses the simplecov JSON and returns every executable line per file (both hit lines and 0-hit lines, excluding null/non-executable entries). Symmetric with cj/--coverage-parse-simplecov, which returns only hit lines. - cj/--coverage-format-summary renders intersect records as a per-file percentage summary sorted ascending by coverage (worst-covered first). Used instead of the line-detail format-report because an entire project's uncovered lines would be thousands of entries. cj/--coverage-read-and-display now branches on scope: whole-project feeds executable-lines as the "changed" input to intersect; diff-aware scopes still shell git diff as before. cj/--coverage-render-to-buffer branches similarly to pick the format helper. Tests cover the two new helpers: Normal (basic extraction, sorted output, percentages), Boundary (all-null coverage, multiple test-name keys unioned, empty records, not-tracked files excluded), and Error (missing file signals user-error). Verified end-to-end on the current .coverage/simplecov.json: 2717 of 4559 lines covered across 44 files, sorted from keybindings.el at 0% up through high-coverage modules.
* feat(coverage): add cj/coverage-report command and F7 bindingCraig Jennings2026-04-231-0/+108
| | | | | | | | | | | | | | | | | | | Completes the coverage v1 user-facing path. cj/coverage-report is the interactive entry point: 1. Resolves the backend for the current project (honoring cj/coverage-backend from .dir-locals.el). 2. Prompts for a git-diff scope via completing-read (Working tree, Staged, Branch vs parent, Branch vs main). 3. Reads the cached simplecov report, intersects with the diff, renders records into a *Coverage Report* buffer. 4. If the report doesn't exist, prompts to run coverage first. With a prefix argument, re-runs regardless. The report buffer uses cj/coverage-report-mode, a compilation-mode derivative. Uncovered-line entries are formatted as path:line: uncovered so the standard gnu compilation-error-regexp-alist picks them up for next-error navigation. That means M-g n, M-g p, and C-x backtick walk through uncovered lines from any buffer without switching focus. F7 is bound to the command globally, matching the F-key layout ticket's design (F4 compile+run, F5 debug, F6 test, F7 coverage). Added to init.el: (require 'coverage-core) + (require 'coverage-elisp). Tests cover the pure scope-label helpers (label to symbol, symbol to label, roundtrip) plus a smoke test that exercises the full command with stubbed backend, stubbed completing-read, stubbed shell-command-to-string, and a prepared simplecov fixture. Coverage v1 is now functionally complete: make coverage produces the report, F7 drives the interactive flow.
* feat(coverage): add format-report helper for the report bufferCraig Jennings2026-04-231-0/+70
| | | | | | | | | | | | | | Pure helper that renders intersect records into the text shown in the coverage report buffer. Takes the list of per-file plists from cj/--coverage-intersect and a scope label, returns the formatted string. Output has three sections depending on what's present: - "Uncovered lines" — one line per uncovered line, formatted as "<path>:<line>: uncovered" so compilation-mode's default regex picks them up for next-error navigation. - "Not tracked" — files changed in the diff but absent from the coverage data (READMEs, test files, config). - "Fully covered" — tracked files where every changed line is covered. Files with empty :changed-lines (deletion-only hunks) are omitted. Summary counts cover only tracked files, so an all-README change shows "0 of 0" rather than a misleading percentage over nothing. Tests cover Normal (partial, fully covered, mixed sections), Boundary (empty records, 100% coverage with no uncovered section, only-not-tracked case, deletion-only exclusion), and the output format that next-error relies on.
* feat(coverage): wire make coverage target + simplecov pipelineCraig Jennings2026-04-222-52/+58
| | | | | | | | | | | | | | | | Completes the coverage v1 pipeline by adding the Makefile target, the undercover driver script, the exclusion list, and the .gitignore entry. Uses simplecov JSON rather than LCOV as the collection format. The LCOV vs simplecov choice: Undercover's :merge-report t option only supports simplecov. Since the pipeline runs tests per-file (matching test-unit's isolation pattern) and accumulates coverage across runs, merge-report is required. LCOV is better-supported by external coverage viewers, but for a primarily interactive workflow the on-disk format is an internal detail. Other moves in this commit: - Renamed cj/--coverage-parse-lcov to cj/--coverage-parse-simplecov and rewrote its tests for the JSON schema. Same signature, same semantics (file to set of covered lines), different parser. - Renamed the backend protocol's :lcov-path key to :report-path, format-neutral and matching the renamed cj/--coverage-elisp-report-path function. - The coverage target deletes modules/*.elc before running so undercover can instrument the .el sources. Without this, byte-compiled versions shadow the instrumentation and only a handful of pre-loaded modules end up with coverage data. - Excluded tests/test-all-comp-errors.el from make coverage runs. That test byte-compiles every module, which fails under undercover's instrumentation. Excluded only from coverage. Normal make test still runs it. - Updated docs/design/coverage.org to reflect the simplecov pivot with a historical note on why we moved off LCOV. Verified end-to-end: make coverage produces .coverage/simplecov.json with 2717 of 4559 executable lines hit across 44 tracked modules.
* feat(coverage): add elisp backendCraig Jennings2026-04-221-0/+74
| | | | | | | | | | | | | | | First of the pluggable coverage backends. Registers itself with coverage-core on load. - :name is elisp - :detect returns non-nil when the project root has a Makefile, Eask, or Cask alongside .el files at root or under modules/. The heuristic is deliberately loose. For anything unusual, .dir-locals.el can pin the backend with cj/coverage-backend. - :run invokes make coverage in a compilation buffer. On success the callback fires with the LCOV path. On failure the buffer stays visible so the user can read the error. - :lcov-path resolves to <project-root>/.coverage/lcov.info. undercover is declared via use-package with :defer t so it's installed but not loaded at Emacs startup. The make coverage target will require it explicitly. Tests cover Normal (Makefile + modules/, Eask + root .el, Cask + modules/), Boundary (no build file, Makefile without .el, empty directory), and Error (nonexistent root returns nil). The registration-on-load case is also verified. The Makefile coverage target and the cj/coverage-report user command arrive in follow-up commits.
* feat(coverage): add backend registryCraig Jennings2026-04-221-0/+53
| | | | | | | | | | | | | | | The coverage-core module now has a registry protocol so per-language backends can plug in without touching the core. A backend is a plist with :name, :detect, :run, and :lcov-path. cj/coverage-register-backend appends to cj/coverage-backends, or replaces an existing entry with the same :name at its original position (so first-registered wins on ties). cj/--coverage-backend-for-project resolves which backend applies to a project root. Resolution order: 1. An OVERRIDE argument (typically buffer-local cj/coverage-backend from .dir-locals.el) wins if supplied, and errors if it names an unregistered backend. 2. Otherwise, walk the registry in order and return the first backend whose :detect returns non-nil for the given root. Tests cover Normal (register and retrieve, re-register replaces in place, first detect wins), Boundary (empty registry, no match, override bypasses detect, detect receives the root), and Error (override names an unknown backend). With the registry in place, the elisp backend (and later python / typescript / go) can self-register on load without any changes to coverage-core.
* feat(coverage): add intersect helper to combine LCOV with diffCraig Jennings2026-04-221-0/+52
| | | | | | | | | | Third and final pure helper for the coverage-report command. Takes the hash tables produced by parse-lcov and parse-diff-output and returns per-file records ready for the report buffer. Output is a list of plists sorted by file path. Each record has :path, :changed-lines, :covered-lines, :uncovered-lines, and :tracked. A file that appears in the diff but not in the LCOV data is :tracked nil with both line lists empty. That way the reporter can distinguish "coverage isn't looking at this file" (README edits, test files, config) from "tests didn't exercise this code." Tests cover Normal (all covered, partial, multiple files sorted), Boundary (file not tracked, tracked file with no covered lines, empty changed-lines from deletion-only hunks, empty inputs), and Error (nil inputs return an empty list instead of erroring). With this helper in place, the core data pipeline is complete: LCOV file + git diff scope go in, per-file records come out. Next up is the backend registry and the elisp backend, then the cj/coverage-report command ties it all together.
* feat(coverage): add changed-lines helper and diff parserCraig Jennings2026-04-221-0/+64
| | | | | | | | | | | | Second of three pure helpers for the coverage-report command. cj/--coverage-parse-diff-output is pure. It takes a git unified-diff string and returns a hash table of file to set of added or modified line numbers (based on the +new_start,new_count hunk headers). Files with deletion-only hunks appear in the result with an empty set, so reporters can distinguish "coverage not tracked" from "no changes touched this file." cj/--coverage-changed-lines wraps that parser with scope dispatch. Scopes are working-tree, staged, branch-vs-main, and branch-vs-parent. Branch-vs-parent takes an optional BASE arg; if omitted, falls back to @{upstream}. Unknown scopes signal user-error. Tests cover Normal (single hunk, multiple files), Boundary (new file via @@ -0,0, deletion-only, binary markers, single-line hunks without a count, empty input), and Error (malformed hunk headers skipped; unknown scope). Git invocation is stubbed via cl-letf in the smoke test so the parser logic is exercised without shelling out. Part of the coverage-core work per docs/design/coverage.org.
* feat(coverage): add cj/--coverage-parse-lcov helperCraig Jennings2026-04-221-0/+58
| | | | | | | | First of three pure helpers for the coverage-report command. Reads an LCOV file and returns a hash table of file to set of covered line numbers. Only the SF, DA, and end_of_record fields are interpreted. Other LCOV fields (FN, FNDA, LF, LH, BRDA) are ignored. Malformed DA lines are skipped silently so partial runs still yield usable data. Tests cover Normal (single file, multiple files, mixed hit counts), Boundary (empty file, spaces in path, extra fields, all-zero hits), and Error (missing file, malformed DA lines). Part of the coverage-core work per docs/design/coverage.org.
* chore(org): move org-reveal to F2, freeing F5Craig Jennings2026-04-221-1/+1
| | | | F2 is now the universal preview key across modes. markdown-mode already binds F2 to markdown-preview. Org-mode now binds it to org-reveal instead of F5. A follow-up ticket reworks the rest of the F-key block to consolidate the dev-loop keys (compile+run, debug, test, coverage).
* docs(font): sync font-config module header with current codeCraig Jennings2026-04-221-3/+5
| | | | Several lines in the header were stale. The default preset is BerkeleyMono, not FiraCode, and the height is now machine-dependent (120 on laptops, 140 on desktops). The default preset's variable-pitch font is Lexend. Merriweather is only the fallback for unnamed presets. The fontaine preset keybinding is M-S-f, not M-F. The emoji bindings (C-c E i, C-c E l) weren't listed.
* fix(host-environment): detect battery correctly on Linux desktopsCraig Jennings2026-04-221-5/+30
| | | | | | | | `env-laptop-p` treated any `battery-format "%B"` value that wasn't literally "N/A" as "has a battery." On a Linux desktop using `battery-upower`, the result is "unknown". The AC adapter and USB-C power entries exist in /sys but there's no BAT*. That made desktops look like laptops. The per-machine font height switch in `font-config.el` broke as a result. The fix uses /sys/class/power_supply/BAT* as the canonical Linux signal. That's what the kernel exposes, and what upower itself reads. Other platforms keep the `battery-format` path, but the fallback now checks for a live battery status char ("!", "+", "-") instead of only excluding "N/A". Two pure helpers (`env--battery-status-char-indicates-battery-p`, `env--power-supply-has-battery-p`) keep the logic testable. The new test file covers Normal, Boundary, and Error cases for each helper.
* feat(font): set default font height per machineCraig Jennings2026-04-221-2/+2
| | | | | | The default fontaine preset now picks its height based on `env-laptop-p`. Laptop: 120 (12pt). Desktop: 140 (14pt), matches foot's `size=14`. Text reads at the same size across Emacs and the terminal. This reuses `env-laptop-p` from `host-environment.el` instead of adding a gitignored local override.
* fix(hugo): defer browser until server ready, report crashesCraig Jennings2026-04-221-3/+40
| | | | | | | | Two gaps in cj/hugo-preview surfaced during manual testing. First, the browser opened one second after start-process returned. On any non-trivial site, Hugo takes several seconds to finish its initial build before it binds port 1313. The browser requested the page before the server existed and got ERR_CONNECTION_REFUSED. Replaced the fixed one-second run-at-time with a process filter that watches Hugo's output for "Web Server is available at" — the line Hugo prints once it has actually bound the port. The browser now opens at the right moment regardless of build time. The filter clears itself after firing so subsequent output does not re-open tabs. Second, if Hugo exited on its own (for example a template error in the theme), the preview command went silent with no indication that anything was wrong. Added a process sentinel that clears cj/hugo--preview-process on any exit and prints "hugo server crashed (exit N) — see *hugo-server* buffer" when the exit status is non-zero. User-initiated stops arrive as signal status and remain silent because cj/hugo-preview already prints its own stop message.
* refactor(hugo): prune stale header, group functions by purposeCraig Jennings2026-04-221-38/+41
| | | | | | | | | | Two small cleanups on hugo-config.el after the feature commits. The file header listed only five bindings (n, e, o, O, d) and called the lowercase-d binding "Toggle draft." Both are now wrong. Replaced the listing with a pointer to the keybindings section at the bottom and to the which-key panel, which are the real sources of truth and do not rot. The functions used to live under one generic "Hugo Blog Functions" header. Regrouped them by purpose: Post Creation, Post Export, Directory Navigation, Draft Management, Preview and Publish. Moved the two picker helpers (cj/hugo--post-metadata and cj/hugo--collect-drafts) into the Draft Management section. No behavior change. All seven ERT tests still pass.
* chore: delete wip.el and remove its stale require from init.elCraig Jennings2026-04-221-60/+0
| | | | | | | | wip.el held commented-out scratch code (efrit, buffer-same-mode, easy-hugo) plus one active pomm use-package block. The require in init.el was itself commented out, so the file compiled on every make compile pass but nothing in it ever ran at startup. The easy-hugo block motivated the new preview and publish commands that landed in the previous commit. The other entries (efrit, buffer-same-mode) have been dead code for months. Also removes the "Cannot load pomm" warning that has been appearing on every make compile run.
* feat(hugo): draft picker, preview toggle, publish commandCraig Jennings2026-04-221-2/+79
| | | | | | | | | | | | | Put the full Hugo workflow inside Emacs. All of it lives in modules/hugo-config.el. New functions: - cj/hugo-open-draft reads all .org files under content-org/log, finds those with #+hugo_draft: true, and offers a completing-read picker. - cj/hugo-preview toggles a local hugo server subprocess and opens the preview URL in the browser. A second press stops the server. - cj/hugo-publish opens magit-status on the website repo. The server-side post-receive hook on cjennings.net already rebuilds and deploys on push, so committing and pushing is the deploy. Two pure helpers support the picker: cj/hugo--post-metadata parses the front matter region of a post, and cj/hugo--collect-drafts walks a directory and filters to drafts. Seven ERT tests cover both helpers across normal, boundary, and error cases. Keybinding note: C-; h d and C-; h D have swapped roles. Lowercase d now opens the draft picker. Uppercase D toggles the draft flag in the current buffer. The previous lowercase-d binding was toggle.
* fix: load freshness, wttrin path, compile-time package initCraig Jennings2026-04-221-1/+1
| | | | | | | | Set load-prefer-newer in early-init.el. Emacs was loading the older .elc files even when the .el source was newer, warning on every load but still using the stale byte code. Point weather-config.el's wttrin :load-path at /home/cjennings/code/emacs-wttrin. The previous value was /home/cjennings/code/wttrin, which does not exist, so use-package could not load the package. Add (package-initialize) to the Makefile compile target. Without it, batch byte-compile cannot see ELPA packages like git-gutter, git-timemachine, forge, and difftastic, which produced "Cannot load" warnings on every run.
* style(font): set default and fallback font heights to 120Craig Jennings2026-04-201-2/+2
| | | | Default preset (BerkeleyMono) 140→120, fallback preset (FiraCode) 110→120.
* refactor(transcription): extract running-transcriptions and format-entryCraig Jennings2026-04-191-20/+25
| | | | | | | | | | | Two cleanups round out the transcription-config refactor: - cj/--running-transcriptions: the 'status = running' filter used by cleanup and count helpers is now one function. Existing counter tests cover both callers. - cj/--format-transcription-entry: the 13-line dolist body inside cj/transcriptions-buffer becomes a testable pure function. 6 tests cover status-face mapping, basename-only rendering, duration format, trailing newline.