diff options
| -rwxr-xr-x | .claude/hooks/validate-el.sh | 16 | ||||
| -rw-r--r-- | assets/2026-06-07-dupre-diff-face-legibility-compare.png | bin | 0 -> 99512 bytes | |||
| -rw-r--r-- | docs/design/module-inventory.org | 2 | ||||
| -rw-r--r-- | init.el | 2 | ||||
| -rw-r--r-- | modules/linear-config.el | 58 | ||||
| -rw-r--r-- | modules/pearl-config.el | 66 | ||||
| -rw-r--r-- | tests/test-dupre-theme.el | 34 | ||||
| -rw-r--r-- | tests/test-init-module-headers.el | 2 | ||||
| -rw-r--r-- | themes/dupre-faces.el | 4 | ||||
| -rw-r--r-- | themes/dupre-palette.el | 2 | ||||
| -rw-r--r-- | todo.org | 18 |
11 files changed, 137 insertions, 67 deletions
diff --git a/.claude/hooks/validate-el.sh b/.claude/hooks/validate-el.sh index 782f04ca..d6999ac0 100755 --- a/.claude/hooks/validate-el.sh +++ b/.claude/hooks/validate-el.sh @@ -15,7 +15,11 @@ set -u # Emit a JSON failure payload and exit 2. Arguments: # $1 — short failure type (e.g. "PAREN CHECK FAILED") # $2 — file path -# $3 — emacs output (error body) +# $3 — emacs output (error body), always sent to Claude in additionalContext +# $4 — optional compact terminal echo; when set, the terminal shows this +# instead of the full $3 (Claude still gets the full $3). Used by the +# test runner so a failing suite prints a short summary to the pane +# rather than dumping every ERT backtrace. fail_json() { local ctx ctx="$(printf '%s: %s\n\n%s\n\nFix before proceeding.' "$1" "$2" "$3" \ @@ -23,7 +27,7 @@ fail_json() { cat <<EOF {"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": $ctx}} EOF - printf '%s: %s\n%s\n' "$1" "$2" "$3" >&2 + printf '%s: %s\n%s\n' "$1" "$2" "${4:-$3}" >&2 exit 2 } @@ -97,7 +101,13 @@ if [ "$count" -ge 1 ] && [ "$count" -le "$MAX_AUTO_TEST_FILES" ]; then --eval '(package-initialize)' \ -l ert "${load_args[@]}" \ --eval "(ert-run-tests-batch-and-exit '(not (tag :slow)))" 2>&1)"; then - fail_json "TESTS FAILED ($count test file(s))" "$f" "$output" + # Terminal gets a compact summary (the run tally + the failing test names); + # Claude still gets the full backtrace via additionalContext. Keeps the + # pane from drowning in ERT stack frames on every red test. + summary="$(printf '%s\n' "$output" \ + | grep -E '^Ran [0-9]+ tests|unexpected results:|^[[:space:]]+FAILED' || true)" + [ -n "$summary" ] && summary="${summary}"$'\n'"(full backtrace in Claude's context)" + fail_json "TESTS FAILED ($count test file(s))" "$f" "$output" "$summary" fi fi diff --git a/assets/2026-06-07-dupre-diff-face-legibility-compare.png b/assets/2026-06-07-dupre-diff-face-legibility-compare.png Binary files differnew file mode 100644 index 00000000..2821a074 --- /dev/null +++ b/assets/2026-06-07-dupre-diff-face-legibility-compare.png diff --git a/docs/design/module-inventory.org b/docs/design/module-inventory.org index 385bdbd5..2d4baf81 100644 --- a/docs/design/module-inventory.org +++ b/docs/design/module-inventory.org @@ -220,7 +220,7 @@ owns the intentional end-of-startup buffer-bury timer. | Module | Layer | Cat | Current | Target | Runtime requires | Top-level side effects | Direct load | |--------+-------+-----+---------+--------+------------------+------------------------+-------------| -| =linear-config= | 3 | D/P | eager | command | system-lib | package config | yes | +| =pearl-config= | 3 | D/P | eager | command | system-lib | package config | yes | | =local-repository= | 4 | O/D/P | eager | command | elpa-mirror | none | yes | | =lorem-optimum= | 4 | O/L | eager | command | cl-lib | none | yes | | =mail-config= | 3 | D/P | eager | command | user-constants, system-lib, mu4e-attachments, keybindings | cj/email-map under cj/custom-keymap, add-hook, 2 advice, 1 global key | yes | @@ -73,7 +73,7 @@ (require 'diff-config) ;; diff and merge functionality w/in Emacs (require 'erc-config) ;; seamless IRC client (require 'slack-config) ;; slack client via emacs-slack -(require 'linear-config) ;; Linear.app issue tracking (deepsat workspace) +(require 'pearl-config) ;; Linear.app issue tracking via pearl (deepsat + craigjennings) (require 'telega-config) ;; telegram client via telega.el (TDLib in docker) (require 'signal-config) ;; signal client via forked signel + signal-cli (require 'eshell-config) ;; emacs shell configuration diff --git a/modules/linear-config.el b/modules/linear-config.el deleted file mode 100644 index 8fbae30c..00000000 --- a/modules/linear-config.el +++ /dev/null @@ -1,58 +0,0 @@ -;;; linear-config.el --- Linear.app integration -*- lexical-binding: t; -*- -;; author: Craig Jennings <c@cjennings.net> - -;;; Commentary: -;; -;; Layer: 3 (Domain Workflow). -;; Category: D/P. -;; Load shape: deferred (command-loaded). -;; Top-level side effects: package configuration via use-package. -;; Runtime requires: none. -;; Direct test load: no. -;; -;; Near-vanilla pearl setup: close to what pearl's README documents for a -;; first-time install (local checkout instead of a package archive), with two -;; deliberate tweaks layered on after dogfooding the out-of-box experience — a -;; global C-; L prefix (see below) and the shorter assignee @-tag. -;; -;; pearl owns its own keymap. `pearl-mode' turns on automatically in any buffer -;; pearl renders (it carries a `#+LINEAR-SOURCE' header) and binds the whole -;; command surface under `pearl-keymap-prefix' (default "C-; L"). This config -;; also binds that same `pearl-prefix-map' globally under C-; L (`:bind-keymap'), -;; so the full command surface is reachable from any buffer; the first press -;; autoloads pearl. `M-x pearl-menu' / `M-x pearl-list-issues' still work too. -;; -;; Authentication: the Linear personal API key is read from authinfo.gpg. Add: -;; machine api.linear.app login apikey password lin_api_YOURKEYHERE -;; Generate it in Linear: Settings -> Security & access -> Personal API keys. - -;;; Code: - -(use-package pearl - :ensure nil ;; local checkout, not from an archive - :load-path "~/code/pearl" - :commands (pearl-menu pearl-list-issues pearl-create-issue pearl-run-linear-view) - ;; Bind pearl's command map globally under C-; L, so the full surface is - ;; reachable from any buffer (not only inside a pearl-rendered one). The - ;; first press autoloads pearl; it's the same `pearl-prefix-map' that - ;; `pearl-mode' binds in-buffer, so behavior is identical everywhere. - :bind-keymap ("C-; L" . pearl-prefix-map) - :custom - (pearl-org-file-path (expand-file-name "gtd/linear.org" org-directory)) - ;; Shorten the assignee @-tag to the first name only (e.g. @first instead of - ;; @first_last), trading disambiguation for a tighter tag line. - (pearl-assignee-tag-short t) - ;; Optional defaults — uncomment and fill in to skip the prompts. Set them - ;; HERE, at init level, not via M-x pearl-set-default-view / - ;; pearl-set-default-team: those persist through `customize-save-variable', - ;; and this config redirects `custom-file' to a throwaway temp file - ;; (system-defaults.el), so a setter's value is discarded on the next - ;; restart. These :custom lines re-apply on every startup instead. - ;; (pearl-default-view "My active work") ;; the local view `C-; L l' opens - ;; (pearl-default-team-id "9fca2cf6-390c-4102-a9ff-f94a4ed823c5") ;; DeepSat SE; skips the team prompt on create / by-project - :config - (setq pearl-api-key - (auth-source-pick-first-password :host "api.linear.app"))) - -(provide 'linear-config) -;;; linear-config.el ends here diff --git a/modules/pearl-config.el b/modules/pearl-config.el new file mode 100644 index 00000000..b58812ee --- /dev/null +++ b/modules/pearl-config.el @@ -0,0 +1,66 @@ +;;; pearl-config.el --- Linear.app integration via pearl -*- lexical-binding: t; -*- +;; author: Craig Jennings <c@cjennings.net> + +;;; Commentary: +;; +;; Layer: 3 (Domain Workflow). +;; Category: D/P. +;; Load shape: deferred (command-loaded). +;; Top-level side effects: package configuration via use-package. +;; Runtime requires: none. +;; Direct test load: no. +;; +;; Near-vanilla pearl setup (local checkout instead of a package archive), in +;; multi-account mode: two Linear workspaces, deepsat (work) and craigjennings +;; (personal), named by Linear's own urlKey. Each account renders to its own +;; Org file, deepsat.pearl.org / craigjennings.pearl.org, so they never collide. +;; `M-x pearl-switch-account' swaps the active one; the mode line shows it. +;; +;; pearl owns its own keymap. `pearl-mode' turns on automatically in any buffer +;; pearl renders (it carries a `#+LINEAR-SOURCE' header) and binds the whole +;; command surface under `pearl-keymap-prefix' (default "C-; L"). This config +;; also binds that same `pearl-prefix-map' globally under C-; L (`:bind-keymap'), +;; so the full command surface is reachable from any buffer; the first press +;; autoloads pearl. `M-x pearl-menu' / `M-x pearl-list-issues' still work too. +;; +;; Authentication: each account reads its key from authinfo.gpg by a distinct +;; login under the api.linear.app host: +;; machine api.linear.app login apikey password lin_api_<deepsat key> +;; machine api.linear.app login pearl-personal password lin_api_<personal key> +;; Generate keys in Linear: Settings -> Security & access -> Personal API keys. + +;;; Code: + +(use-package pearl + :ensure nil ;; local checkout, not from an archive + :load-path "~/code/pearl" + :commands (pearl-menu pearl-list-issues pearl-create-issue + pearl-run-linear-view pearl-switch-account) + ;; Bind pearl's command map globally under C-; L, so the full surface is + ;; reachable from any buffer (not only inside a pearl-rendered one). The + ;; first press autoloads pearl; it's the same `pearl-prefix-map' that + ;; `pearl-mode' binds in-buffer, so behavior is identical everywhere. + :bind-keymap ("C-; L" . pearl-prefix-map) + :custom + ;; Shorten the assignee @-tag to the first name only (e.g. @first instead of + ;; @first_last), trading disambiguation for a tighter tag line. + (pearl-assignee-tag-short t) + ;; Two workspaces, keyed by Linear's urlKey. Each resolves its API key from + ;; authinfo.gpg by its own login (see Commentary), renders to its own Org + ;; file, and carries a default team so create / by-project skip the prompt. + (pearl-accounts + '(("deepsat" + :api-key-source (:auth-source :host "api.linear.app" :user "apikey") + :org-file "~/org/gtd/deepsat.pearl.org" + :default-team-id "9fca2cf6-390c-4102-a9ff-f94a4ed823c5") ;; DeepSat SE + ("craigjennings" + :api-key-source (:auth-source :host "api.linear.app" :user "pearl-personal") + :org-file "~/org/gtd/craigjennings.pearl.org" + :default-team-id "ee285e6c-fcc9-4dd6-9292-c47f2df75b82"))) ;; Pearl + ;; Which workspace pearl opens into. Dogfooding the personal account through + ;; Sunday; flip back to "deepsat" to make work primary again (one string), or + ;; switch per-session at runtime with `M-x pearl-switch-account'. + (pearl-default-account "craigjennings")) + +(provide 'pearl-config) +;;; pearl-config.el ends here diff --git a/tests/test-dupre-theme.el b/tests/test-dupre-theme.el index dec648d1..4d0e786c 100644 --- a/tests/test-dupre-theme.el +++ b/tests/test-dupre-theme.el @@ -223,5 +223,39 @@ The defface registration in dupre-faces.el is what makes direct use work." (should (string= (face-attribute 'dupre-org-todo-dim :foreground nil 'default) "#869038"))) +;;; Diff face legibility (WCAG contrast) + +(defun dupre-test--channel-luminance (c) + "Linearize an 8-bit channel value C (0-255) per the WCAG formula." + (let ((x (/ c 255.0))) + (if (<= x 0.03928) (/ x 12.92) (expt (/ (+ x 0.055) 1.055) 2.4)))) + +(defun dupre-test--relative-luminance (hex) + "WCAG relative luminance of HEX color \"#rrggbb\"." + (+ (* 0.2126 (dupre-test--channel-luminance (string-to-number (substring hex 1 3) 16))) + (* 0.7152 (dupre-test--channel-luminance (string-to-number (substring hex 3 5) 16))) + (* 0.0722 (dupre-test--channel-luminance (string-to-number (substring hex 5 7) 16))))) + +(defun dupre-test--contrast (fg bg) + "WCAG contrast ratio between hex colors FG and BG." + (let ((l1 (dupre-test--relative-luminance fg)) + (l2 (dupre-test--relative-luminance bg))) + (/ (+ (max l1 l2) 0.05) (+ (min l1 l2) 0.05)))) + +(ert-deftest dupre-diff-changed-faces-meet-wcag-aa () + "Error/Regression: diff-changed and diff-refine-changed must stay legible as +standalone backgrounds (WCAG AA, >= 4.5:1 for normal text). Guards the bug +where diff-refine-changed rendered the default fg (#f0fef0) on the bright-gold +yellow-1 (#ffd700) at 1.35:1 -- unreadable wherever the face is used as a plain +background, not just inside diff-mode's own foreground overlay." + (require 'diff-mode) + (load-theme 'dupre t) + (dolist (face '(diff-changed diff-refine-changed)) + (let ((fg (face-attribute face :foreground nil t)) + (bg (face-attribute face :background nil t))) + (should (string-match-p "^#[0-9a-fA-F]\\{6\\}$" fg)) + (should (string-match-p "^#[0-9a-fA-F]\\{6\\}$" bg)) + (should (>= (dupre-test--contrast fg bg) 4.5))))) + (provide 'test-dupre-theme) ;;; test-dupre-theme.el ends here diff --git a/tests/test-init-module-headers.el b/tests/test-init-module-headers.el index 2680a19c..bbda2388 100644 --- a/tests/test-init-module-headers.el +++ b/tests/test-init-module-headers.el @@ -113,7 +113,7 @@ "jumper" "latex-config" ;; Batch 9 — Remaining domain / integration / optional modules (Layer 2-4) - "linear-config" + "pearl-config" "local-repository" "lorem-optimum" "mail-config" diff --git a/themes/dupre-faces.el b/themes/dupre-faces.el index 8fad4c62..17daa41c 100644 --- a/themes/dupre-faces.el +++ b/themes/dupre-faces.el @@ -164,10 +164,10 @@ ;;;;; Diff mode `(diff-added ((t (:foreground ,green :background ,green-2)))) `(diff-removed ((t (:foreground ,red :background ,red-3)))) - `(diff-changed ((t (:foreground ,yellow :background ,yellow-2)))) + `(diff-changed ((t (:foreground ,fg :background ,yellow-2)))) `(diff-refine-added ((t (:foreground ,fg :background ,green-1 :weight bold)))) `(diff-refine-removed ((t (:foreground ,fg :background ,red-2 :weight bold)))) - `(diff-refine-changed ((t (:foreground ,fg :background ,yellow-1 :weight bold)))) + `(diff-refine-changed ((t (:foreground ,fg :background ,yellow-2 :weight bold)))) `(diff-header ((t (:foreground ,fg :background ,bg+2)))) `(diff-file-header ((t (:foreground ,blue :background ,bg+2 :weight bold)))) `(diff-hunk-header ((t (:foreground ,gray+1 :background ,bg+1)))) diff --git a/themes/dupre-palette.el b/themes/dupre-palette.el index 3901ef82..d6715a78 100644 --- a/themes/dupre-palette.el +++ b/themes/dupre-palette.el @@ -95,7 +95,7 @@ Each entry is (NAME VALUE) where VALUE is a hex color string.") (diff-removed-bg red-3) (diff-removed-fg red) (diff-changed-bg yellow-2) - (diff-changed-fg yellow)) + (diff-changed-fg fg)) "Semantic color mappings for dupre-theme. Each entry maps a semantic name to a palette color name.") @@ -41,6 +41,24 @@ Tags are additive. For example, a small wrong-behavior fix can be =:bug:quick:=, and a feature that requires internal restructuring can be =:feature:refactor:=. * Emacs Open Work +** TODO [#B] Dupre diff-changed / diff-refine-changed legibility :bug:dupre: +Surfaced 2026-06-07 from a pearl session designing its modified-ticket indicator (pearl marks a changed field by inheriting =diff-changed=). dupre's =diff-refine-changed= is bright gold (#ffd700) under near-white text (#f0fef0) -- WCAG contrast ~1.35, unreadable as a plain background. It only looks fine inside diff-mode because diff-mode overlays its own dark foreground. =diff-changed= (#875f00 amber) is ~5.49, readable but off the modus model. Every modus variant keeps both faces legible (contrast 9-16) by pairing a dark low-saturation background with a hue-matched foreground. + +Ask: +1. Rework dupre's =diff-changed= and =diff-refine-changed= on modus lines: dark low-saturation background, legible foreground (plain default fg for simplicity, or hue-tinted per modus -- decide), and keep refine slightly stronger than changed (refine is the word-level emphasis inside a changed region; modus keeps them distinct). +2. While there, audit dupre's broader diff/palette faces against modus conventions (background/foreground tinting, contrast targets) and flag where it diverges. + +Reference values -- modus-vivendi: refine-changed bg #4a4a00 fg #efef80, changed bg #363300 fg #efef80. modus-operandi: refine-changed bg #fac090 fg #553d00, changed bg #ffdfa9 fg #553d00. + +Side-by-side legibility render: [[file:assets/2026-06-07-dupre-diff-face-legibility-compare.png][assets/2026-06-07-dupre-diff-face-legibility-compare.png]]. +** TODO [#B] dupre-theme test failures :bug:dupre:tests: +A full =make test= run (2026-06-07) is green across 516 of 517 files; the only failures are 4 tests in =tests/test-dupre-theme.el=, long pre-existing. Two root causes. For each, decide whether the palette or the test assertion is canonical, then fix the loser so =make test= goes fully green. + +*** TODO Background drift: 3 tests expect #151311, palette bg is #0d0b0a +=dupre-get-color-base= (test:46), =dupre-theme-default-face= (test:84), and =dupre-with-colors-binds-values= (test:62) all assert the default background is "#151311", but =themes/dupre-palette.el= defines =bg= as "#0d0b0a". The committed palette looks intentional, so the three assertions are likely just stale -- confirm #0d0b0a is the wanted background, then update the tests. + +*** TODO org-todo color mismatch: test expects #ff2a00, theme renders #a7502d +=dupre-theme-org-todo= (test:130) asserts the org-todo foreground is "#ff2a00" (intense-red), but the theme renders "#a7502d" (red-1). Design call: should org-todo be the bright intense-red or the muted red-1? Fix whichever side loses the decision. ** TODO [#B] Dashboard keybinding changes :quick: :PROPERTIES: :LAST_REVIEWED: 2026-06-06 |
