aboutsummaryrefslogtreecommitdiff
path: root/tests
Commit message (Collapse)AuthorAgeFilesLines
* fix(latex): activate the latexmk workflowCraig Jennings29 hours1-0/+62
| | | | | | Two breaks kept latexmk from ever engaging. The :hook key TeX-mode-hook expanded to the unbound TeX-mode-hook-hook, since use-package appends -hook to any symbol not ending in -mode, so TeX-command-default was never set; name the mode TeX-mode instead. Separately auctex-latexmk was :defer t with no trigger, so auctex-latexmk-setup never ran and latexmk never joined TeX-command-list; load it :after tex. Claude-Session: https://claude.ai/code/session_01BqrdWUo9GcznYX2pZr76gZ
* feat(face-diagnostic): make report face names describe-face buttonsCraig Jennings29 hours2-0/+26
| | | | | | Render each real face name in the Face Diagnosis report as a button that runs describe-face on it, carrying the face as button data; anonymous specs and non-faces stay plain text. Also add face-diagnostic to the module-header allowlist now that it is required in init.el and carries the header contract. Claude-Session: https://claude.ai/code/session_01BqrdWUo9GcznYX2pZr76gZ
* feat(term): modified arrows enter copy-mode and carry directionCraig Jennings29 hours1-0/+81
| | | | | | C-<arrow> and M-<arrow> in a ghostel buffer now enter copy-mode and move one step in that direction in a single stroke. The tmux path writes the arrow escape sequence into the pty so the copy cursor follows it; without tmux the same keys enter ghostel-copy-mode and move point. All eight keys join ghostel-keymap-exceptions and the semi-char map is rebuilt, so they reach Emacs instead of the terminal program. Claude-Session: https://claude.ai/code/session_01BqrdWUo9GcznYX2pZr76gZ
* feat(ai-term): move keybindings to C-; a and M-SPC, retire F9Craig Jennings31 hours3-58/+93
| | | | | | | | I moved the ai-term family off the F9 keys onto the C-; a prefix, vacated when gptel was archived: a toggles the agent, s opens the project picker, n swaps to the next agent, k closes one. The frequent swap also gets M-SPC as a fast chord, bound in ghostel-mode-map and added to the semi-char exceptions so it reaches Emacs from inside an agent buffer. cj/ai-term-next now opens the project picker when no agent is running instead of erroring, so the swap key doubles as a "start an agent" key. To free M-SPC, I removed jumper's M-SPC binding. Jumper's commands stay reachable via M-x, with a cleverer home pending review.
* chore(ai): archive gptel and remove it from the live configCraig Jennings32 hours33-5220/+0
| | | | | | | | | | I archived gptel to archive/gptel/ since I rarely use it. Moved there: the six gptel modules (ai-config, ai-conversations, ai-conversations-browser, ai-mcp, ai-quick-ask, ai-rewrite), the gptel-tools/ directory, custom/gptel-prompts.el, their test files and utilities, and the four gptel-only specs. Scrubbed from the live config: the ai-config require in init.el, which also drops the whole C-; a keymap; the gptel-mode emojify hook in font-config.el; the gptel-tools entries in the Makefile clean target and the coverage runner; and the gptel feature notes in README. Cancelled the open gptel tasks in todo.org (the AI Open Work issues, the feature-extension brainstorm, the velox gptel-magit bug). ai-term stays. It is the ghostel Claude launcher, independent of gptel. Verified: every module loads, a batch init launch reaches completion clean, and the full test suite shows only pre-existing coverage failures unrelated to this change.
* fix(jumper): free registers on removal, skip dead markers, toggle backCraig Jennings3 days1-0/+179
| | | | Three defects in the saved-location store: removal shifted the slot vector but never freed the dropped register, and a later store allocated by next-index — a char a surviving slot still held — so it silently overwrote that slot's marker. jumper--with-marker-at also guarded only markerp, so a location whose buffer was killed made store and jump signal wrong-type errors. And the single-location toggle never returned: its already-there branch did nothing. Store now takes the first unused register char in the live slice, removal clears the freed register so its marker stops pinning the buffer, the marker guard checks buffer liveness so dead entries are skipped, and the toggle jumps to the last-location register when one is set.
* fix(coverage): normalize report and diff paths before intersectingCraig Jennings3 days1-0/+123
| | | | simplecov reports absolute source paths while git diff emits repo-relative ones, so cj/--coverage-intersect joined them by exact key and matched nothing — every changed file read ":tracked nil" under the working-tree, staged, and branch scopes (whole-project worked only because both sides came from the same simplecov source). A new cj/--coverage-relativize-keys normalizes both tables to repo-relative in cj/--coverage-read-and-display before the intersect; the intersect stays pure. Covered by 5 unit tests plus an integration test that drives the real parsers with an absolute-key report and a relative-key diff.
* feat(dirvish): add Hyprland Super+F popup with focus-loss dismissCraig Jennings3 days1-0/+248
| | | | A single-instance Dirvish popup frame (named "dirvish") for a Hyprland Super+F launcher, mirroring the org-capture popup. q closes the frame; in the popup, RET opens files through the OS handler so they launch independently, and the frame dismisses itself on focus loss. A second launch reuses the open popup instead of spawning another frame.
* test: make subr mocks variadic for native-comp, add arity meta-testCraig Jennings4 days61-202/+315
| | | | | | | | Re-enabling native-comp surfaced a suite-wide fragility. When a test redefines a C primitive (or a native-compiled function), native-comp routes native callers through a trampoline that calls the mock with the primitive's maximum arity. A fixed-arity mock narrower than the primitive then throws wrong-number-of-arguments, intermittently, as the eln-cache fills. I swept every arity-narrow subr mock to append &rest _ (188 sites, preserving any named args the body uses), and added tests/test-meta-subr-mock-arity.el, which fails make test on any subr mock too narrow for the primitive's arity. The rule isn't "never mock a subr". The suite mocks message and completing-read freely. It's "a subr mock must accept the primitive's arity." Background, the three failure modes, and the research are in docs/native-comp-subr-mocking.org.
* fix: load games-config via the malyon hook, not an autoload chainCraig Jennings4 days1-14/+22
| | | | | | | | The previous deferral (03d8b587) autoloaded malyon to games-config, but games-config doesn't define malyon. It leaves the command to the malyon package, so M-x malyon loaded games-config, found malyon still undefined, and errored "Autoloading games-config.el failed to define function malyon". Emacs won't chain through a second autoload. malyon and 2048-game autoload their own commands via package.el, so games-config should never own them. init.el now loads games-config via (with-eval-after-load 'malyon ...), and games-config just sets malyon-stories-directory when malyon loads. M-x malyon loads the package as a real command, then games-config applies its config. The earlier batch check loaded the files by hand and missed the autoload failure. The new test resolves the autoload the way M-x does (autoload-do-load), so the real path is covered now.
* refactor: defer games-config behind autoloads (load-graph Phase 4)Craig Jennings4 days1-0/+38
| | | | | | init.el eagerly required games-config at startup just to configure two on-demand game packages. package.el already autoloads malyon and 2048-game, so the eager require bought nothing but the one setting the module adds (malyon-stories-directory). init.el now autoloads malyon and 2048-game to games-config instead of requiring it. The first game command loads the module, which configures then loads the package. Startup no longer touches games-config, and both the commands and the stories-directory setting still work. This is the first module of the Phase 4 low-risk batch.
* perf: re-enable native-comp JIT and hand GC to gcmhCraig Jennings4 days2-27/+0
| | | | | | | | early-init.el disabled JIT native compilation with (setq native-comp-deferred-compilation nil), the obsolete alias of native-comp-jit-compilation. Despite the comment, setting it nil turns JIT off entirely rather than making it synchronous. Most modules then ran interpreted for the daemon's lifetime, and the native-comp-speed/jobs settings in system-defaults.el were dead. The "Selecting deleted buffer" async race that prompted the disable was an Emacs 28/29 issue. This is 30.2. I re-enabled it with native-comp-jit-compilation t and silent async warnings. GC was pinned at the stock 800KB: early-init restored it post-startup and the minibuffer setup/exit hooks bounced back to it. That's Emacs's bare-editor default, far too low for 184 packages, so GC pauses fired often during completion, agenda, and LSP/AI work. I replaced both hand-rolled mechanisms with gcmh, which keeps the threshold at 1GB during activity and collects on idle. Verified a clean full launch in a throwaway daemon (JIT on, gcmh active, no backtrace) and gcmh's threshold cycle in batch.
* test(term): fix F10-exceptions test after C-<f10> shutdown moveCraig Jennings4 days1-8/+9
| | | | The s-F9 commit moved server-shutdown off C-<f10> to C-x C and dropped C-<f10> from the ghostel keymap-exceptions. The regression test still asserted C-<f10> was present, so the full suite went red. I updated it to assert <f10> (music) stays an exception and C-<f10> is now absent, since C-x C deliberately forwards to the terminal program inside an agent buffer.
* feat(ai-term): add s-F9 step-to-next-agent, drop C-S-F9 close aliasCraig Jennings4 days2-8/+83
| | | | | | s-F9 (cj/ai-term-next) steps through the open agent buffers in name order. It's the "switch among existing agents" surface F9's toggle never provided. The cycle logic lives in a pure helper (cj/--ai-term-next-agent-buffer) with Normal/Boundary/Error coverage. The command is a thin window-mutating wrapper. I dropped the C-S-F9 close alias, leaving M-F9 as the sole close binding. I moved cj/server-shutdown off C-<f10> to C-x C so the key keeps forwarding to the terminal program inside an agent buffer. I also removed the now-unused F10 entries from term-config's ghostel exceptions.
* refactor(dwim-shell): extract the branching command-string buildersCraig Jennings5 days1-0/+55
| | | | | | | | | | Lift the command-string construction out of three :config commands whose templates branch — video-trim (Beginning/End/Both), tar-gzip (single vs multi), text-to-speech (darwin say vs espeak) — into top-level pure builders cj/dwim-shell--video-trim-command / --tar-gzip-command / --text-to-speech-command, leaving thin interactive wrappers that prompt and delegate. The builders are now testable under make test (the :config defuns aren't), mirroring the existing dated-backup/zip-single builders. Adds 8 Normal/Boundary/Error tests.
* fix(ai-term): keep the F9 toggle reversible in a 3+ window layoutCraig Jennings5 days1-0/+41
| | | | | | | | | | | | | | | | In a layout where the agent had its own split window (e.g. code on top, a working window, and the agent below), toggling the agent off deleted its window correctly, but toggling back on reused the working window at the edge -- displacing its buffer and collapsing three windows to two. The slot-reuse that avoids a third window on a fresh show was firing on a re-show after the agent's own window was already deleted. Flag the toggle-off that deletes the agent's own window; on the next toggle-on, reuse-edge-window consumes the flag and falls through to a fresh re-split, so the agent returns to its own window and the other windows are untouched. The flag only changes the 3+ window case -- after a delete in a 2-window slot-reuse layout one window remains, where re-split and reuse-edge already coincide, so the existing reuse-edge tests are unaffected.
* fix(ai-term): stop F9 toggle shrinking the agent window each cycleCraig Jennings5 days4-21/+31
| | | | | | | | | | | | | | | | | The F9 toggle captured the agent window's body-height and replayed it as body-lines. Body-height subtracts the mode line's pixel height, which differs between an active and an inactive mode line; the agent is captured active but redisplayed inactive, so under a theme whose mode-line-inactive is shorter than a text line the window lost ~1 line per toggle. Capture and replay total-height for the vertical axis instead, via the renamed cj/window-replay-size. Total-height is identical active or inactive and has no mode-line-pixel dependence, so the round-trip is a fixed point. Width keeps body-width (total-width has the position-dependent divider problem that total- height does not). The shared lib fix covers the F12 terminal toggle too. The shrink only manifests in a GUI frame, so it is not reproducible in the batch harness; the unit tests pin the new total-height contract.
* refactor(dirvish): extract playlist-target resolution from the create commandCraig Jennings5 days1-0/+55
| | | | | | | Lift the name-validate plus overwrite-prompt loop out of cj/dired-create-playlist-from-marked into cj/--playlist-resolve-target, leaving the command a flat filter -> resolve -> write. Add Normal/Boundary/Error tests for the new seam (real temp music-dir, stubbed prompts only).
* refactor(calendar-sync): extract per-event recurrence-exception parserCraig Jennings5 days1-0/+64
| | | | | | | Lift the 14-binding let* body out of calendar-sync--collect-recurrence-exceptions into calendar-sync--parse-exception-event, which returns the exception plist (or nil) for one VEVENT; the collector's dolist becomes a thin uid + puthash. Add Normal/Boundary/Error tests for the new pure helper.
* refactor(elfeed): extract HTML-entity decoder, drop leftover DEBUG loggingCraig Jennings5 days1-0/+31
| | | | cj/youtube-to-elfeed-feed-format hand-decoded an og:title with six sequential replace-regexp-in-string calls; extract cj/--decode-html-entities (alist-driven, &amp; first) and call it. Also remove the leftover DEBUG cj/log-silently instrumentation from cj/extract-stream-url. Behavior unchanged; adds coverage of the decoder.
* refactor(prog-general): dedup the deadgrep search tail, lift its helpersCraig Jennings5 days1-0/+44
| | | | cj/deadgrep-here and cj/deadgrep-in-dir repeated the same normalize-directory + read-term + invoke-deadgrep tail. Lift cj/deadgrep--initial-term out of :config and add cj/--deadgrep-run for the shared tail; each command resolves its root then delegates. Adds coverage for the term seeding and the run helper.
* refactor(prog-general): lift cj/find-project-root-file out of :configCraig Jennings5 days1-0/+49
| | | | The pure project-root file finder lived inside the projectile use-package :config, so it was unreachable under make test. Move it to top level (its forward declaration already existed); cj/open-project-root-todo and cj/project-switch-actions still call it. Adds unit coverage for string regexps, rx forms, no-match, and no-project.
* refactor(font-config): lift frame/icon helpers out of :configCraig Jennings5 days1-0/+75
| | | | cj/apply-font-settings-to-frame, cj/cleanup-frame-list (inside with-eval-after-load 'fontaine) and cj/maybe-install-all-the-icons-fonts (inside all-the-icons :config) carried real branching but were unreachable under make test. Move all three (and the cj/fontaine-configured-frames state) to top level; the :config/eval-after-load blocks keep only the hook wiring. Adds declare-function for the package calls and coverage of the apply/cleanup/install branches.
* refactor(erc): lift cj/erc-generate-buffer-name out of :configCraig Jennings5 days1-0/+31
| | | | The buffer-name function lived inside the erc use-package :config, so it was unreachable under make test (no package-initialize). Move it to top level; :config keeps the erc-generate-buffer-name-function setq. Adds unit coverage for the server-and-channel, server-only, and missing-piece cases.
* refactor(mousetrap): extract per-category event-binding loopCraig Jennings5 days1-0/+41
| | | | mouse-trap--build-keymap-1 nested its event-binding cond/dolists five deep. Extract mouse-trap--bind-events-to-ignore (spec prefixes map); build-keymap-1 now just walks the categories and delegates the binding. Adds coverage for the wheel and click-event paths.
* refactor(chrono-tools): extract tmr sound-file helpers, flatten the commandCraig Jennings5 days1-0/+54
| | | | cj/tmr-select-sound-file nested cond/let five deep, mixing the current-sound lookup, the completing-read, the setq, and the message. Extract cj/tmr--current-sound-name and cj/tmr--apply-sound-file (the testable parts) and flatten the inner conds to ifs. The command keeps the prefix-arg/no-dir/no-files guards and the prompt. Adds coverage for both helpers.
* refactor(jumper): extract marker-traversal and location-candidates helpersCraig Jennings5 days1-0/+52
| | | | jumper--location-exists-p and jumper--format-location both opened a marker with the same save-current-buffer/set-buffer/save-excursion/goto-char dance; jumper-jump-to-location and jumper-remove-location shared a verbatim candidate-list cl-loop. Extract jumper--with-marker-at (index fn) and jumper--location-candidates; the four callers delegate. Adds direct coverage of the candidate list.
* refactor(modeline): extract the clickable-segment keymap builderCraig Jennings5 days1-0/+29
| | | | The buffer-name, vc, and major-mode segments each hand-rolled the same make-sparse-keymap + define-key [mode-line mouse-1/3] construction. Extract cj/--modeline-click-map (mouse-1 &optional mouse-3); the three segments call it. Adds coverage for the one- and two-button cases.
* refactor(ai-config): extract gptel model-apply step, drop dead branchCraig Jennings5 days1-0/+45
| | | | cj/gptel-change-model applied the selection inline (scope dispatch + message) and re-checked (stringp model) on a value already interned to a symbol. Extract cj/--gptel-apply-model-selection (scope backend model backend-name), which sets the vars globally or buffer-locally and returns the message; the dead stringp branch is gone. Adds direct coverage of both scopes.
* refactor(org-agenda): extract the agenda base-file listCraig Jennings5 days1-0/+36
| | | | The fixed base list (inbox, schedule, and the three calendars) was spelled out as a literal in cj/--org-agenda-scan-files, cj/todo-list-single-project, and the chime initializer. Extract cj/--org-agenda-base-files so adding a calendar source is a one-place change; the single-project view prepends its todo.org with cons. Adds a test for the helper's contents and order.
* refactor(mail-config): build the account-nav keymaps from one templateCraig Jennings5 days1-0/+53
| | | | The cmail/dmail/gmail navigation maps were three near-identical defvar-keymap blocks differing only by maildir prefix, with the unread/flagged/large query clauses repeated in each. Add cj/--mail-account-search-queries (account -> the four search strings) and cj/--mail-make-account-map (builds the keymap), wrapped in eval-and-compile so org-msg's :preface can call the builder during byte-compilation. The three maps become one-line builder calls. Adds direct coverage of the query strings and the per-account closures.
* refactor(org-capture): extract the find-or-create-top-heading blockCraig Jennings5 days1-0/+45
| | | | cj/org-capture--goto-file-headline, cj/--org-capture-goto-open-work, and cj/--org-capture-goto-exact-headline each repeated the same positioning block: search from point-min, jump to the heading on a match, else append it at end of buffer and back up. Extract cj/--org-find-or-create-top-heading taking the search regexp and the heading line; the three sites delegate. Behavior unchanged; adds direct coverage of the helper with a plain regexp.
* refactor(custom-text-enclose): extract the region-or-word dispatchCraig Jennings5 days1-0/+62
| | | | cj/surround/wrap/unwrap-word-or-region each repeated the same skeleton: target the active region, else the word at point, else show a message; then delete and re-insert the transformed text. Extract cj/--enclose-region-or-word, which takes the transform as a function and the no-target message, so each command reads its prompts then delegates. Behavior and messages unchanged; adds direct coverage of the dispatch helper.
* refactor(custom-datetime): generate the six inserters from one macroCraig Jennings5 days1-0/+14
| | | | The six cj/insert-* commands were identical except their format variable and a one-word docstring noun. Replace them with a cj/--define-datetime-inserter macro and six table-style calls; the format defvars and the keymap are unchanged. Adds a test asserting all six stay interactive commands.
* refactor(custom-ordering): dedupe region guard, replace tail, and ↵Craig Jennings5 days1-0/+52
| | | | | | arrayify-python Extract cj/--ordering-validate-region (the start>end guard copy-pasted across all seven pure helpers) and cj/--ordering-replace-region (the delete-region + insert tail repeated in every interactive command). Alias cj/arrayify-python to cj/arrayify-json, which it duplicated verbatim, leaving both keybindable. Behavior unchanged; adds direct Normal/Boundary/Error coverage for the two new helpers.
* refactor: extract shared format-region helper into system-libCraig Jennings5 days1-0/+68
| | | | prog-json and prog-yaml each carried a byte-identical cj/--<lang>-format-region that runs a formatter over the buffer via call-process-region and replaces it on exit 0. Hoist it to system-lib as cj/format-region-with-program with a generic output buffer, and point both formatters at it. Adds the first direct unit coverage of the helper (Normal, Boundary, Error).
* refactor: remove dead wrappers and commented-out blocksCraig Jennings5 days2-41/+0
| | | | Drop cj/apply-browser-choice (browser-config) and cj/load-fallback-theme (ui-theme), orphaned wrappers with no caller that just duplicated logic the live paths already inline, plus their tests. Delete commented-out blocks: a duplicate contact capture template (org-contacts-config), a disabled personal-info-dir :init (help-config), a stale TODO setq (org-config), and an old commented regex (test-runner).
* fix(windows): keep the pulled-away window on the arrow's edgeCraig Jennings5 days1-16/+18
| | | | The sole-window pull split toward the arrow at 50/50, so a fullscreen terminal jumped above the revealed buffer at half height. Now the reveal opens on the opposite side and is minimized to a sliver, so the current window keeps the arrow's edge near-full and the sticky windsize arrows shrink it step by step, matching the feel of resizing an existing split.
* feat(windows): pull a window away from a sole window with C-; b + arrowCraig Jennings5 days1-3/+36
| | | | When the selected window fills the frame there is no divider to resize, so the arrow now splits toward its direction with the previous buffer and the original window shrinks from that edge. Multi-window resize is unchanged.
* feat(dirvish): bind d to duplicate, D to guarded force-deleteCraig Jennings5 days1-0/+47
| | | | Drop delete-to-trash. d now duplicates the file at point. D force-deletes the marked files via sudo rm -rf behind a yes-or-no-p that names the targets, and reports success only when rm exits 0.
* feat(windows): dock companion panels by a shared min-column ruleCraig Jennings5 days4-21/+148
| | | | | | The F9 agent always docked as a right-side column on a landscape frame. On this 138-column frame that left ~68-column panes, too cramped to read code and the agent side by side. The F12 terminal and F10 playlist hardcoded a bottom split with no width-aware path. I added cj/preferred-dock-direction and the cj/window-dock-min-columns defcustom (default 80) to the window-geometry lib: dock side-by-side only when the narrower pane keeps at least the minimum width, otherwise stack below. All three toggles now route through it. F9 drops its pixel-aspect rule. F12 and F10 gain a right-column width default and become adaptive. F10 keeps width and height size memory in separate vars so a resize on one axis doesn't leak to the other.
* test: cover pure-logic gaps found by the coverage auditCraig Jennings6 days15-0/+751
| | | | | | | | I ran make coverage and worked the report function by function, separating real gaps from interactive/IO wrappers that aren't unit-test targets. These tests fill the genuine pure-logic holes: predicates, parsers, formatters, transforms, and three modules that had no test file at all. New files cover car-member (local-repository), show-kill-insert-item (show-kill-ring), the oauth2-auto plstore cache fix (auth-config), the coverage-core project-root fallback, reconcile--dirty-p, and the recurrence-frequency dispatch in calendar-sync. Extended files add the missing branches: coverage-core's merge-base and diff /dev/null handling plus the staged and branch-vs-main scopes, the detect-system-timezone symlink path, user-constants no-op and optional-failure branches, the elfeed playlist branch with HTML-entity decoding, the duplicate-line no-comment-syntax guard, and several calendar-sync edges (exception field overrides, timestamp seconds and TZID fallback, property-line position advancement, parse-ics nil and out-of-range inputs). Mocks sit at the real boundaries (plstore, url-retrieve, process-file, git) so each function's own logic runs. Dates come from relative helpers. About 65 tests added across 15 files, and the full suite stays green.
* refactor(theme-studio): cut the face model over to weight/slant/objectsCraig Jennings6 days1-0/+13
| | | | | | | | | | I replaced the legacy bold/italic/underline/strike booleans with the final model shape across both sides of the tool. weight (light/normal/medium/semibold/bold/heavy) and slant (normal/italic/oblique) replace the bold/italic flags, underline becomes {style: line|wave, color}, strike becomes {color}, and null means unset. A single migration converts a legacy face on the way in, mirrored as migrateLegacyFace in app-core.js and migrate_legacy in face_specs.py so the JS and Python models can't drift. It runs on import (applyImported, mergePackagesInto) and on every seed that face_spec touches. The captured-snapshot seed (default_faces.seed) narrows the same way it did before. Only bold and italic survive, as weight "bold" and slant "italic", so the generated themes stay byte-identical. The B/I/U/S toggle buttons keep working through a transitional bridge (legacyStyleOn / toggleLegacyStyle). The weight/slant dropdowns and underline/strike controls that replace them land next. The live previews read the new shape, with a weight name mapped to a numeric CSS font-weight. The cutover is proven emit-neutral two ways. An ERT test asserts the migrated shapes emit the same attributes as the legacy booleans, and deep-migrating every face in dupre, distinguished, sterling, now, theme, and WIP then running build-theme yields byte-identical output. Full suite green: Python 59, Node 200, ERT 41, plus the browser hash gates.
* feat(theme-studio): emit the full face-attribute model from build-themeCraig Jennings6 days1-28/+188
| | | | | | | | I extended build-theme's emitter to the full attribute set: family, distant-foreground, a weight and slant range, structured underline (color and wave), overline, strike color, inverse-video, extend, and inherit/height on every tier. It still reads the legacy boolean bold/italic/underline/strike fields, so every committed preset round-trips unchanged. The emitter is the first piece of widening the studio to all face attributes; the model and UI that produce these fields come next. To keep the change clean I refactored --attrs from nine positional arguments to a single face-spec object and lifted the accessor helpers above their callers. Added 40 ERT tests covering legacy compatibility, each new attribute, the coercion helpers' edge cases, and an end-to-end round-trip that loads a theme and reads the attributes back off the faces. They run in the theme-studio suite as a new stage.
* refactor(ui): remove buffer-state coloring of the buffer nameCraig Jennings6 days4-188/+0
| | | | The modeline colored the buffer name by write and modification state (red read-only, green modified, gold overwrite) through a classifier in user-constants.el. I removed it, the same way the matching cursor coloring was removed earlier for being more confusing than useful. The classifier had no other live user, so it and its two test files go with it. The buffer name now renders in the normal mode-line color.
* feat(ui-navigation): split from the dashboard opens scratch, not the dashboardCraig Jennings9 days1-0/+21
| | | | C-x 2 / C-x 3 already show the dashboard in the new window while point stays put. The one dead spot was splitting from the dashboard itself, which put the dashboard in both panes. Now the new window shows *scratch* when the current buffer is the dashboard, and the dashboard everywhere else. I pulled the choice into a pure predicate (cj/--split-from-dashboard-p) and a companion helper, both tested.
* refactor(system-utils): remove the *scratch* background tintCraig Jennings9 days1-30/+0
| | | | I dropped the buffer-local face remap that lightened the *scratch* background 5% above the theme default. Scratch now uses the plain theme background like every other buffer. The startup hook still moves the cursor to end-of-scratch. Only the tint call, its two helpers, the defcustom, the color require, and the now-orphaned test go.
* feat(keybindings): mirror the C-; command family under C-c ;Craig Jennings9 days1-0/+33
| | | | C-; is GUI-only: terminals can't encode Control-semicolon, so the whole custom command family (calendar, AI, Slack, org, pearl, jump, and the rest) was unreachable in a terminal frame (emacs -nw, emacsclient -nw, or Emacs inside vterm/tmux). I bound the single cj/custom-keymap under C-c ; alongside C-;, so the same leaf keys reach the identical map in both GUI and TTY with no relearning and no per-module edits. C-c is the standard user prefix and always TTY-encodable. I audited every leaf key in the family and they're all TTY-safe (letters, digits, punctuation, SPC, and arrow keys), so nothing needed remapping.
* fix(mail): theme mu4e buffers via a shared font-lock exclusionCraig Jennings9 days1-0/+46
| | | | mu4e paints its header lines, main menu, and view headers with manual `face' text properties. Global font-lock stripped them, so the buffers rendered unthemed, the same failure the dashboard hit. I extracted the dashboard's one-off exclusion into a shared, additive cj/exclude-from-global-font-lock helper in system-lib and repointed the dashboard to it. Then I excluded mu4e-headers-mode, mu4e-main-mode, and mu4e-view-mode. The view body renders through gnus's own washing rather than font-lock, so excluding it is safe.
* fix(dashboard): exclude dashboard-mode from global font-lockCraig Jennings9 days1-0/+35
| | | | | | The dashboard banner title and section headings render in the default face instead of their theme colors. The faces are defined correctly. Global font-lock fontifies the dashboard buffer and strips the `face' text properties dashboard sets by hand: it owns `face' and clears props it didn't apply. Items and the navigator survive because their color rides a dashboard-items-face overlay, which font-lock leaves alone. Excluding dashboard-mode from global font-lock keeps the banner and heading faces. I set it at top level so it applies regardless of the use-package body.