aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-x.claude/hooks/validate-el.sh16
-rw-r--r--assets/2026-06-07-dupre-diff-face-legibility-compare.pngbin0 -> 99512 bytes
-rw-r--r--docs/design/module-inventory.org2
-rw-r--r--init.el2
-rw-r--r--modules/linear-config.el58
-rw-r--r--modules/pearl-config.el66
-rw-r--r--tests/test-dupre-theme.el34
-rw-r--r--tests/test-init-module-headers.el2
-rw-r--r--themes/dupre-faces.el4
-rw-r--r--themes/dupre-palette.el2
-rw-r--r--todo.org18
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
new file mode 100644
index 00000000..2821a074
--- /dev/null
+++ b/assets/2026-06-07-dupre-diff-face-legibility-compare.png
Binary files differ
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 |
diff --git a/init.el b/init.el
index 390de45e..fb6d55af 100644
--- a/init.el
+++ b/init.el
@@ -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.")
diff --git a/todo.org b/todo.org
index 4936ec78..ea660e10 100644
--- a/todo.org
+++ b/todo.org
@@ -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