summaryrefslogtreecommitdiff
Commit message (Collapse)AuthorAgeFilesLines
* fix(line-paragraph): join-line-or-region strands space on next lineCraig Jennings2026-05-033-27/+108
| | | | | | | | | | 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.
* test(prog): drop ignore-errors on prog-module require, add featurep checkCraig Jennings2026-04-304-4/+27
| | | | | | | | | | | | | | | | | | Two small tightening passes on the formatter wiring tests just shipped. Drops `(ignore-errors ...)` from each test file's `(require 'prog-PKG)' call. Soft use-package warnings (e.g. lsp-pyright not being installed) still emit messages without aborting the load. A hard load failure (syntax error, missing required dep) would now surface as a test error rather than being silently swallowed. Adds a `(should (featurep PKG))' assertion per language so the test output makes "package loaded" visible alongside the fboundp and binding checks. For webdev the assertion is `(featurep 'prog-webdev)' since the formatter command is defined directly in prog-webdev.el (it shells out to the prettier CLI, no separate package to load). 17 tests total now (up from 13), all passing.
* test(prog): cover formatter wiring for python, go, shell, typescriptCraig Jennings2026-04-305-0/+226
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Four test files plus a shared testutil that locks in the formatter bindings on C-; f across the four languages. Each test file checks: - the format command is fboundp after the relevant package loads - the C-; f binding resolves to that command (in the relevant mode-map, or in the buffer-local map for hook-based wiring) - the underlying executable is on PATH (skipped via ert-skip if not installed) No production change. The bindings were already at C-; f via two mechanisms. Use-package :bind handles python and shell. The other two install via local-set-key inside a hook. This regression net catches silent breakage if any of those wirings get reshaped later. The shared tests/testutil-format-wiring.el carries format-test--ensure-packages-init, which calls package-initialize once per batch run, since make test runs Emacs with --no-site-file --no-site-lisp. Without it, use-package can't find blacken / shfmt / go-mode in elpa/. Also format-test--skip-unless-executable wraps ert-skip with a clear "not on PATH" message so missing tools fail informatively. Per-language wiring inventory (no changes, just locked in): - Python: blacken-buffer in python-ts-mode-map (use-package :bind) - Shell: shfmt-buffer in sh-mode-map and bash-ts-mode-map (use-package :bind, gated on :if executable-find) - Go: gofmt via cj/go-mode-keybindings hook - TS/JS/Web: cj/webdev-format-buffer via cj/webdev-keybindings hook 13 tests across 4 files, all passing.
* 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-302-0/+31
| | | | | | | | | | | | 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.
* test(config-utilities): cover validate-timestamps and format-report helpersCraig Jennings2026-04-302-0/+190
| | | | | | | | | | | | | | | | | | Two test files covering the extracted timestamp-validation helpers. cj/--validate-timestamps-in-buffer (8 tests): empty buffer no-op, buffer with no timestamps, buffer with all valid timestamps, DEADLINE flagged with "DEADLINE" property, SCHEDULED flagged with "SCHEDULED", inline-timestamp flagged with "inline timestamp", multiple invalid collected in document order, mixed valid+invalid returning only the invalid one. Tests use real org parsing and mock org-time-string-to-absolute at the boundary so an arbitrary timestamp can be marked invalid for a given test. cj/--format-validation-report-section (4 tests): no-entries says "No invalid timestamps found", single-entry produces the file: link + Property/Type + Invalid timestamp lines, multiple-entry preserves input order, every section ends with a trailing blank line.
* 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.
* test(config-utilities): cover cj/--recompile-emacs-homeCraig Jennings2026-04-301-0/+122
| | | | | | | | | | | | Six tests against real temp directories. Mocks only the actual compile invocations (native-compile-async, byte-recompile-directory) so the deletion side runs end-to-end against real files. Covers: native dispatch returns 'native and calls native-compile-async, byte dispatch returns 'byte and calls byte-recompile-directory, recursive deletion of every .elc/.eln (including in subdirs), removal of the eln cache dir on the native path, removal of the elc cache dir on the byte path, and the missing-cache-dir no-op.
* 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.
* test(config-utilities): cover cj/--benchmark-methodCraig Jennings2026-04-301-0/+57
| | | | | | | Four tests against a temporarily fbound test target: runs the symbol once and propagates its return value, raises user-error on a nil symbol, raises user-error naming the symbol when it isn't fboundp, and verifies the with-timer announce/done messages fire.
* 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.
* test(config-utilities): cover cj/--delete-compiled-files-in-dirCraig Jennings2026-04-301-0/+91
| | | | | | | | Five tests against real temp directories: mixed .el/.elc/.eln content, recursive descent through subdirectories, no-compiled-files no-op, empty directory, and the suffix-vs-substring boundary that ensures a file like "looks.elc.bak" isn't deleted (string-suffix-p, not string-match).
* 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.
* test(org-noter-config): cover preferred-split, slug, template, predicatesCraig Jennings2026-04-304-0/+289
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Four new test files for the pure-and-near-pure helpers in org-noter-config.el. The interactive heavyweights (cj/org-noter-start, cj/org-noter-insert-note-dwim, cj/org-noter--toggle-notes-window, cj/org-noter--find-notes-file, cj/org-noter--create-notes-file) are out of scope for this pass — they need org-noter loaded and a real session, or a substantial internal/wrapper split before they can be tested without spinning up real PDFs and EPUBs. - preferred-split: 5 tests walking the 1.4 width-to-height threshold from both sides plus a square-frame boundary. - title-to-slug: 7 tests covering multi-word, single-word, mixed punctuation, leading/trailing junk, collapsed runs, digits, and empty input. - generate-notes-template: 5 tests with org-id-uuid mocked, asserting the rendered template carries the UUID, the dual ROAM_REFS / NOTER_DOCUMENT pointers, the title-and-category lines, the ReadingNotes filetag, and the trailing Notes heading. - predicates: 13 tests covering cj/org-noter--in-document-p, cj/org-noter--in-notes-file-p, cj/org-noter--session-active-p, cj/org-noter--get-document-path, and cj/org-noter--extract-document-title. Mocks derived-mode-p, org-entry-get, and buffer-file-name at the boundary so the suite doesn't require pdf-tools or nov. The require chain in each test file is user-constants → keybindings → org-noter-config because cj/org-noter-notes-directory captures roam-dir from user-constants and the prefix-map binding at the bottom of org-noter-config references cj/custom-keymap. 30 new tests, all passing. Full suite green.
* test(config-utilities): cover with-timer, compile-buffer, summary, info commandsCraig Jennings2026-04-304-0/+286
| | | | | | | | | | | | | | | | | | | | | | | | | | Four new test files extending the existing coverage of cj/emacs-build--format-build-time. The interactive heavyweights (cj/recompile-emacs-home, cj/delete-emacs-home-compiled-files, cj/benchmark-this-method, cj/validate-org-agenda-timestamps) are out of scope for this pass — each needs an internal/wrapper split first before tests can exercise the logic without UI. - with-timer macro: 4 tests asserting it returns the FORMS' value, evaluates the body exactly once, emits both announce and done messages, and returns the last form when given multiple. - cj/compile-this-elisp-buffer: 6 tests dispatching across native-async, native-sync, and byte-compile fallbacks, plus the not-elisp / no-buffer-file-name error paths and the sync-native error catch. - cj/emacs-build--summary-string: 5 tests asserting the shape of the multi-line report (Version, System, Build date, Capabilities section, yes/no flag rendering) without locking exact wording. - info-commands smoke: 5 tests exercising cj/info-emacs-build, cj/info-loaded-packages, cj/info-loaded-features, cj/reload-init-file, and cj/org-alert-list-timers via boundary-mocked pop-to-buffer and load-file, asserting buffer creation, content shape, or echo-area message as appropriate. 20 new tests, all passing. Full suite green.
* test(host-environment): cover laptop/desktop, platform, display, timezone ↵Craig Jennings2026-04-304-0/+345
| | | | | | | | | | | | | | | | | | | | | | | | | | predicates Four new test files extending the existing test-host-environment.el (which already covered the two battery helpers). - platform-predicates: env-linux-p, env-bsd-p, env-macos-p, env-windows-p walked across every supported system-type value. 8 tests. - display-predicates: env-x-p, env-x11-p, env-wayland-p, env-terminal-p, env-gui-p exercised under every relevant combination of window-system, WAYLAND_DISPLAY, and display-graphic-p. 13 tests. - env-laptop-p: composition over the helpers, with Linux dispatch isolated from non-Linux dispatch via system-type binding. 8 tests including env-desktop-p as the inverse. battery-status-function is forward-declared in this test file (initialized to nil) so cl-letf's symbol-value place can read the prior value without hitting void-variable. - detect-system-timezone: the four-method priority chain. Mocks cj/match-localtime-to-zoneinfo and getenv at the boundary; uses cl-letf on file-exists-p / insert-file-contents to exercise the /etc/timezone fall-through without touching real system files. 5 tests. 34 new tests for host-environment, all passing. Full suite green.
* 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.
* test(keybindings): cover cj/jump-open-var and the jump-commands wiringCraig Jennings2026-04-302-0/+142
| | | | | | | | | | | | | | | | | | | Two test files for keybindings.el. cj/jump-open-var gets full N/B/E coverage (6 tests): existing-file happy path, plus error paths for unbound symbol, nil value, non-string value, empty string, and missing file. The smoke file for the auto-generated cj/jump-to-NAME commands asserts that each spec entry has an fbound command, that the command is bound in cj/jump-map at the spec's key, that calling each command invokes cj/jump-open-var with the spec's var, and that cj/jump-map is mounted under cj/custom-keymap at "j". The test fixture variable is declared at top level. If it were let-bound inside a test under lexical-binding, the let would create a lexical binding that shadows the dynamic one. The production code's symbol-value would then miss what setq writes. find-file is mocked at the boundary so the existing-file test doesn't actually open a buffer. 10 tests pass. No production change in keybindings.el.
* 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.
* docs(design): add gloss package design docCraig Jennings2026-04-291-0/+316
| | | | Captures the v1 design for the gloss Emacs package: layered five-module split, Wiktionary REST as the online source, side-buffer picker for ambiguous terms, libxml HTML strip, mtime-based cache invalidation. The implementation is a separate repo, but the design work happened in this tree, so the doc lives alongside the other design archives here.
* chore(hooks): skip out-of-tree .el files in validate-el.shCraig Jennings2026-04-291-0/+7
| | | | This PostToolUse hook validates .el edits via check-parens and byte-compile-file. It was firing on edits to files outside the project root too, which meant byte-compiling without the foreign package's load-path set up and leaving .elc droppings in the wrong tree. Added a four-line PROJECT_ROOT guard so out-of-tree files exit silently.
* 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.
* docs(design): add debug-profiling.el module brainstorm outputCraig Jennings2026-04-261-0/+203
| | | | | | | | | | Captures the agreed v1 shape for a new =debug-profiling.el= module: targeted slow-command investigation, two features ("profile next command" and "time region or sexp"), each split into pure helper plus interactive wrapper. Migrates the existing =profiler-*= bindings and =cj/benchmark-this-method= out of =config-utilities.el=. Stays on the existing =C-c d= debug umbrella prefix. Six approaches were considered: three conventional, plus three tail samples (macro-first, log-and-grep, treesit picker). Recommendation is the boring named-operation surface backed by a thin wrapper over the built-in =profiler.el= and =benchmark.el=. The other five options are recorded with reasons-rejected so a future reader can see what was weighed. Design covers architecture, data flow, error handling, testing approach, and observability. Two open questions are parked: default REPS for =cj/time--expr=, and whether to capture =cpu+mem= or just =cpu=. Both are fine to defer until v1 has been used on the queued org-capture target-building investigation. Implementation will run via =/start-work= against this design.
* feat(lsp): add common build/cache dirs to file-watch ignore listCraig Jennings2026-04-262-1/+125
| | | | | | | | | | 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-233-54/+185
| | | | | | | | | | | | | | | | | | | | 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.
* test(system-utils): cover identify-external-open-command and eval-bufferCraig Jennings2026-04-232-0/+121
| | | | | | | | | | | | | | | Adds two per-function test files covering the easily-testable functions in system-utils.el. Takes the module from 0/52 (0%) to 10/52 (19.2%) coverage. tests/test-system-utils-identify-external-open-command.el — five tests for cj/identify-external-open-command: one each for the three supported platforms (linux, macos, windows), one documenting the dispatch-order invariant when multiple predicates return t, and one for the unsupported-host error case. env-*-p predicates are stubbed via cl-letf so the tests run correctly on any host. tests/test-system-utils-eval-buffer.el — three tests for cj/eval-buffer-with-confirmation-or-error-message: the valid-elisp success path, an empty-buffer boundary case, and the broken-elisp error path (confirming the condition-case wrapper catches errors rather than propagating). A small macro stubs message to capture what the user would see. Split into two files per the project's per-function test convention (elisp-testing.md). Not covered in this commit and flagged for follow-up: - cj/open-file-with-command and cj/xdg-open mix user-input resolution with process launching. Per elisp-testing.md's "split interactive from internal" rule, a pure file-resolution helper should be extracted before tests are written. - cj/server-shutdown kills Emacs, so it isn't meaningfully testable.
* feat(coverage): add whole-project scope to cj/coverage-reportCraig Jennings2026-04-232-6/+247
| | | | | | | | | | | | | | | 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-233-0/+200
| | | | | | | | | | | | | | | | | | | 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-232-0/+194
| | | | | | | | | | | | | | 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-2210-234/+348
| | | | | | | | | | | | | | | | 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-222-0/+173
| | | | | | | | | | | | | | | 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-222-0/+162
| | | | | | | | | | | | | | | 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-222-0/+196
| | | | | | | | | | 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-222-0/+257
| | | | | | | | | | | | 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-222-0/+207
| | | | | | | | 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.
* docs: add design docs for coverage and dev-setup-projectCraig Jennings2026-04-222-0/+318
| | | | | | | | | | Two new design docs for pending todo.org tickets. docs/design/coverage.org describes diff-aware coverage reporting with pluggable backends. Primary use case is pre-commit feedback on in-flight changes. LCOV is the shared output format across languages. docs/design/dev-setup-project.org describes an interactive helper that detects a project's shape and writes per-subdirectory .dir-locals.el files for the F4/F6/F7 dev block, with optional starter Makefile generation. Three-tier detection: existing Makefile, existing package.json or pyproject.toml scripts, or fall-back generation. Both tickets in todo.org reference their design docs via org file: links.
* 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-222-5/+98
| | | | | | | | `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-222-61/+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-223-2/+224
| | | | | | | | | | | | | 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-223-1/+10
| | | | | | | | 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.
* docs: add README.orgCraig Jennings2026-04-201-0/+89
|