From 38200c6683e55860b044568cd70004dcbc7c4031 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sat, 4 Jul 2026 15:38:00 -0500 Subject: docs(specs): adopt status-heading lifecycle convention across specs Migrate 29 legacy specs off the old shape (a status suffix in the filename plus a :STATUS: property drawer) onto the docs-lifecycle status heading: a top-level heading carrying the org lifecycle keyword and a dated history line, with the two #+TODO sequences in the header. Dropping the -doing/-implemented/-superseded suffixes means a status change no longer forces a rename and link surgery. Each keyword comes from the spec's own recorded status. The four specs already on the heading form are untouched, and every inbound reference now points at the new names. The status board is one grep: rg '^\* (DRAFT|READY|DOING|IMPLEMENTED|SUPERSEDED|CANCELLED) ' docs/specs/ --- .../specs/flycheck-modeline-customization-spec.org | 323 +++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 docs/specs/flycheck-modeline-customization-spec.org (limited to 'docs/specs/flycheck-modeline-customization-spec.org') diff --git a/docs/specs/flycheck-modeline-customization-spec.org b/docs/specs/flycheck-modeline-customization-spec.org new file mode 100644 index 00000000..2a58b447 --- /dev/null +++ b/docs/specs/flycheck-modeline-customization-spec.org @@ -0,0 +1,323 @@ +#+TITLE: Design: Flycheck modeline customization +#+AUTHOR: Craig Jennings +#+DATE: 2026-05-15 +#+OPTIONS: toc:nil num:nil +#+TODO: TODO | DONE +#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED + +* IMPLEMENTED Design: Flycheck modeline customization +:PROPERTIES: +:ID: 76979608-956e-474f-90a8-8d0c958101a0 +:END: +- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword IMPLEMENTED from existing :STATUS: implemented + -implemented filename (Craig's prior determination) + +* Status + +Draft. Supersedes the earlier =flycheck-modeline-customization-spec.org= +draft in =.ai/= (2025-11-14), which used stale line numbers and conflated +Option 3's risky-local-variable requirement with Option 4. + +* Problem + +Flycheck's status (error / warning counts, "checking" indicator) is not +visible in the custom modeline. The cause is a deliberate choice in +=modules/modeline-config.el=: =mode-line-format= is built from explicit +segments (=cj/modeline-buffer-name=, =cj/modeline-position=, +=cj/modeline-vc-branch=, etc.) and does not include =minor-mode-alist= +or =mode-line-modes=. Flycheck publishes its lighter into +=minor-mode-alist=, so the custom modeline never picks it up. + +The fix is to add a flycheck-aware segment to =mode-line-format=. + +* Goals + +1. Flycheck status appears in the custom modeline when =flycheck-mode= is on. +2. The display picks up flycheck's existing color logic (error count in =error= face, warning count in =warning= face). +3. The display gates on active window, matching the convention used by =cj/modeline-vc-branch= and =cj/modeline-misc-info=. +4. The customization is small enough that swapping prefix / success indicator is a one-line edit. + +* Non-Goals + +- A "minor modes" segment that surfaces every lighter from =minor-mode-alist=. Flycheck is the specific case we care about; the rest stay invisible. +- Reworking =flycheck-config.el= beyond the two =:custom= additions. +- Adding flycheck-side checkers or changing what gets checked. + +* Current State + +** =modules/flycheck-config.el:47-97= + +#+begin_src emacs-lisp +(use-package flycheck + :defer t + :commands (flycheck-list-errors cj/flycheck-list-errors) + :hook ((sh-mode emacs-lisp-mode) . flycheck-mode) + :bind (:map cj/custom-keymap ("?" . cj/flycheck-list-errors)) + :custom + (checkdoc-arguments + '(("sentence-end-double-space" nil) + ("warn-escape" nil))) + :config + ...) +#+end_src + +No flycheck-modeline customization. Defaults are in force: + +| Variable | Default | +|-------------------------------------+-------------------------------------------| +| =flycheck-mode-line-prefix= | ="FlyC"= | +| =flycheck-mode-success-indicator= | =":0"= | +| =flycheck-mode-line-color= | =t= (apply error / warning faces) | +| =flycheck-mode-line= | ='(:eval (flycheck-mode-line-status-text))= | + +** =modules/modeline-config.el:220-237= + +=mode-line-format= layout (left → right, with right-align edge): + +#+begin_src emacs-lisp +(setq-default mode-line-format + '("%e" + " " + cj/modeline-major-mode + " " + cj/modeline-buffer-name + " " + cj/modeline-position + mode-line-format-right-align + (:eval (when (fboundp 'cj/recording-modeline-indicator) + (cj/recording-modeline-indicator))) + cj/modeline-vc-branch + " " + cj/modeline-misc-info + " ")) +#+end_src + +Risky-local-variable list (=modeline-config.el:240-246=): + +#+begin_src emacs-lisp +(dolist (construct '(cj/modeline-buffer-name + cj/modeline-position + cj/modeline-vc-branch + cj/modeline-vc-faces + cj/modeline-major-mode + cj/modeline-misc-info)) + (put construct 'risky-local-variable t)) +#+end_src + +Note: =cj/modeline-vc-branch= and =cj/modeline-misc-info= both gate on +=(mode-line-window-selected-p)= so they appear only in the active window. + +** Flycheck lighter outputs (for reference) + +Flycheck status text values that =flycheck-mode-line-status-text= +returns, depending on =flycheck-last-status-change= and current errors: + +| Status | Display (with default prefix / indicator) | +|------------------------------+----------------------------------------------------| +| Not yet checked | =FlyC= | +| Currently checking | =FlyC*= | +| Finished, no errors | =FlyC:0= | +| Finished, 3 errors, 5 warns | =FlyC:3|5= | +| Checker errored | =FlyC!= | +| Interrupted | =FlyC.= | +| Suspicious | =FlyC?= | +| No checker available | =FlyC-= | + +With =flycheck-mode-line-color= = =t= (the default), the count portion +is colored: error count in the =error= face, warning count in =warning=. + +* Approaches Considered + +** Option 1 (Reject): customize prefix / indicator only + +Setting =flycheck-mode-line-prefix= and =flycheck-mode-success-indicator= +in =:custom= changes the lighter content, but the lighter still publishes +to =minor-mode-alist=, which the custom modeline doesn't read. The lighter +becomes prettier wherever it does show (e.g. doom-modeline if reinstated) +but not here. Doesn't solve the visibility problem. + +** Option 2 (Reject): add the raw =flycheck-mode-line= variable + +Inserting =flycheck-mode-line= into =mode-line-format= directly works, +but the form has no =flycheck-mode= guard. In a buffer where flycheck +isn't loaded or not enabled, the =:eval (flycheck-mode-line-status-text)= +call still fires and either errors or returns junk. Needs a wrapping +guard, which is what Option 4 does. + +** Option 3 (Reject for now): custom segment with full control + +Define =cj/modeline-flycheck= as a =defvar-local= holding a =(:eval ...)= +form that pulls error / warning counts directly from +=flycheck-current-errors=, builds a per-status string, propertizes it +with =error= / =warning= faces, and returns it. Reimplements what +=flycheck-mode-line-status-text= already does, with bespoke formatting. + +Pros: full control over format. Cons: maintenance burden, drifts from +flycheck's status model if flycheck changes it. + +If the Option 4 result ever stops being good enough -- e.g. you want a +different layout (=E:3 W:5= instead of =:3|5=) -- come back to this. +Until then, more code than the problem deserves. + +** Option 4 (Recommended): hybrid -- customize variables + add guarded segment + +Two changes: + +1. =modules/flycheck-config.el= =:custom= block gets prefix and success-indicator overrides. (Optional: also =flycheck-mode-line-color=.) + +2. =modules/modeline-config.el= adds a small =(:eval ...)= form inline in =mode-line-format= that guards on =flycheck-mode= and calls =(flycheck-mode-line-status-text)= directly. + +Pros: minimal code, uses flycheck's logic verbatim, prefix / indicator +swappable with a one-line edit, picks up flycheck's face colors +automatically. + +Cons: layout fixed to flycheck's =PREFIX[indicator|counts]= shape. +Acceptable. + +* Recommended Implementation (Option 4) + +** Step 1: =modules/flycheck-config.el= + +Add to the =:custom= block (currently lines 55-59 in the file): + +#+begin_src emacs-lisp +;; Modeline customization (rendered via mode-line-format in modeline-config.el). +(flycheck-mode-line-prefix "🐛") +(flycheck-mode-success-indicator " ✓") +;; flycheck-mode-line-color stays t (default) so counts keep their face coloring. +#+end_src + +Prefix and success indicator are taste; the **Emoji Reference** section +below catalogs the candidates. Note that the prefix emoji itself does +not inherit the =error= / =warning= face -- only the count portion does +(via =flycheck-mode-line-color=). That trade-off is fine for a static +prefix; an emoji prefix gives a recognizable shape that you scan for, +and the colored count carries the alert signal. + +** Step 2: =modules/modeline-config.el= + +Insert a =(:eval ...)= form into =mode-line-format= (currently lines +220-237). Recommended placement: between the recording indicator and +=cj/modeline-vc-branch= so flycheck status sits with the other +right-aligned status segments. + +After the change, the right-side block reads: + +#+begin_src emacs-lisp +;; RIGHT SIDE +mode-line-format-right-align +(:eval (when (fboundp 'cj/recording-modeline-indicator) + (cj/recording-modeline-indicator))) +(:eval (when (and (mode-line-window-selected-p) + (bound-and-true-p flycheck-mode)) + (flycheck-mode-line-status-text))) +" " +cj/modeline-vc-branch +" " +cj/modeline-misc-info +" ") +#+end_src + +Two design choices baked in: + +- =(mode-line-window-selected-p)= gates the segment to the active window, matching the convention used by =cj/modeline-vc-branch= and =cj/modeline-misc-info=. +- =(bound-and-true-p flycheck-mode)= prevents the function call in buffers where flycheck never loaded; safer than asking =flycheck-mode= directly. + +** Risky-local-variable: not needed here + +This implementation places =(:eval ...)= inline inside =mode-line-format= +rather than wrapping it in a =defvar-local=. Inline forms are evaluated +by mode-line processing without a risky-local-variable marker. The +existing risky list (=modeline-config.el:240-246=) does not need to +grow. + +(If you ever refactor this to a named segment -- =defvar-local cj/modeline-flycheck= -- then add it to the risky list. Option 3 above is the path that needs that step.) + +* Emoji Reference + +** Prefix candidates (=flycheck-mode-line-prefix=) + +| Glyph | Codepoint | Name | +|-------+-----------+---------------------------------------| +| 🪰 | U+1FAB0 | FLY (literal "fly" for flycheck) | +| 🐛 | U+1F41B | BUG (recommended -- broadest font support) | +| 🐞 | U+1F41E | LADY BEETLE | +| ⚠ | U+26A0 | WARNING SIGN | +| 🔍 | U+1F50D | MAGNIFYING GLASS | +| 📝 | U+1F4DD | MEMO | +| ✓ | U+2713 | CHECK MARK (text) | + +🪰 (U+1FAB0) is from Unicode 13.0 (2020) and needs an up-to-date emoji +font. 🐛 (U+1F41B) is older and renders everywhere. Default to 🐛 unless +the fly is a strong preference and the GUI fonts are known to cover it. + +** Success indicator candidates (=flycheck-mode-success-indicator=) + +| Glyph | Codepoint | Name | +|-------+-----------+---------------------------------------| +| ✓ | U+2713 | CHECK MARK (text) | +| ✔ | U+2714 | HEAVY CHECK MARK | +| ✅ | U+2705 | WHITE HEAVY CHECK MARK (green box) | +| 🟢 | U+1F7E2 | GREEN CIRCLE | +| ⭐ | U+2B50 | WHITE MEDIUM STAR | + +Note the leading space in the recommended setting (=" ✓"=): flycheck +joins the prefix and the success indicator with no separator, so a +leading space in the indicator gives breathing room between the emoji +prefix and the check mark. + +** Suggested combinations + +| Mood | Prefix | Success indicator | Result example | +|---------------------+--------+-------------------+----------------| +| Recommended default | 🐛 | " ✓" | =🐛 ✓= / =🐛:3|5= | +| Literal Flycheck | 🪰 | " ✓" | =🪰 ✓= / =🪰:3|5= | +| Minimal | "" | " ✓" | = ✓= / =:3|5= | +| Status light | "" | " 🟢" | = 🟢= / =:3|5= | + +* Testing + +** Manual + +1. Open =modules/flycheck-config.el= (an =emacs-lisp-mode= buffer with =flycheck-mode= auto-enabled per the existing =:hook=). The right side of the modeline shows the prefix + success indicator when there are no errors. +2. Introduce a deliberate parse error (drop a paren). Save. The modeline updates to show =:1|0= (or whatever count) in the =error= face. +3. Trigger =M-x flycheck-buffer= in a fresh =sh-mode= buffer. The "currently checking" state (=PREFIX*=) flashes briefly before settling on success or counts. +4. Open a second window onto the same buffer (=C-x 2=). The flycheck segment appears in the active window only; the inactive copy drops it. Confirms the active-window gate. +5. Open a buffer where flycheck never engages (e.g. =*scratch*= in fundamental-mode, or a =dired= buffer). No segment, no errors. +6. Run =cj/flycheck-prose-on-demand= in an org buffer (=C-; ?= in org-mode). The LanguageTool checker engages and the segment appears with prose-error counts. + +** Regression watch + +- The custom-modeline width should not jump distractingly as flycheck cycles "checking → finished". The status text is short (one to seven chars), so this should be invisible -- worth a glance. +- Inactive-window display: confirm the segment disappears, not just greys out. The current pattern is "hide entirely" via the =mode-line-window-selected-p= guard. +- =cj/modeline-misc-info= keeps showing chime / notification text. The flycheck segment sits to its left; verify the visual order matches the spec. + +* Files to Modify + +- =modules/flycheck-config.el= -- add two =:custom= lines. +- =modules/modeline-config.el= -- insert one =(:eval ...)= form into =mode-line-format=. + +Two-line / one-form change. No new tests required (the existing tests +don't lock the modeline content; they exercise behavior elsewhere). If +you want a smoke test, add one assertion in =tests/test-modeline-config.el= +(if that file exists or you create it) that =mode-line-format='s sexp +contains a form mentioning =flycheck-mode-line-status-text=. Optional. + +* Risks + +| Risk | Mitigation | +|-----------------------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------| +| Emoji renders as a tofu square in terminal Emacs | The user runs GUI Emacs primarily; if terminal use matters, set the prefix to a text glyph (=""= or =":"=) instead. | +| Modeline width thrash when flycheck transitions running → finished | Status text is one to seven chars; jitter is negligible. Confirm during manual testing. | +| Prefix emoji doesn't pick up =error= / =warning= face | Expected: =flycheck-mode-line-color= colors the count portion only. The static prefix is intentionally unstyled. If you want a colored prefix, switch to Option 3. | +| Flycheck not yet loaded when modeline first evaluates | The =(bound-and-true-p flycheck-mode)= guard returns nil in that case, the =(:eval ...)= returns nil, mode-line skips the slot. | +| Active-window gate is wrong for some workflow (e.g. multi-window comparison) | Drop =(mode-line-window-selected-p)=. One-line change. Decide after living with the default. | + +* Rollback + +Revert the commit. Two-file change, no schema impact. Idempotent. + +* Effort estimate + +S (under 1 hour). Two lines in =flycheck-config.el=, one form in +=modeline-config.el=, plus the manual verification walk-through. The +emoji selection is the time sink, not the code. -- cgit v1.2.3