Skip to content

feat(app): let users pick per-mode indicator colors - #70

Merged
pilyang merged 2 commits into
mainfrom
feat/mode-indicator-colors
Sep 16, 2026
Merged

pilyang merged 2 commits into
mainfrom
feat/mode-indicator-colors

Conversation

@pilyang

@pilyang pilyang commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Summary

Appearance follow-up to the on-screen mode indicator (PRD §7.7, roadmap Stage 4): the pills and borders can now be colored per mode. Settled in decisions 20260916_mode-indicator-per-mode-colors.md and 20260916_mode-indicator-insert-color.md.

  • Three colors, alpha included. Normal, Insert and Visual (shared by VISUAL and V-LINE) each get a ColorPicker with opacity in the Settings Indicator tab, under a new "Colors" section, plus a Reset to system accent button. Insert has no persistent indicator, so its color only shows in the flash on a mode change. The whole section is disabled with the indicator toggle.
  • Nothing changes by default. All three colors are unset unless the user picks one; unset means the dynamic system accent, exactly as before. Keys: onScreenModeIndicatorNormalColor / onScreenModeIndicatorInsertColor / onScreenModeIndicatorVisualColor, sRGB #RRGGBBAA strings (uppercase, defaults write-friendly); a missing or unreadable value folds to unset, and resetting removes the key.
  • Color changes repaint without an accessibility re-read. The color is not part of Presentation (that would trip desired != current and cost an AX round trip for a purely visual change); normalColor/insertColor/visualColor persist in didSet and push the color straight into the live flash, badge and border panels. Presentation now carries the mode instead of the label (label is derived), so the controller knows which color a panel on screen needs. Because the self-pid path leaves current and the previous app's persistent panel alone, picking a color with Settings open recolors that badge or border in place.
  • Text stays opaque and derives from the effective color through the existing luminance rule, alpha ignored, so a translucent background still carries a legible label (PRD NFR: never color alone).

New pure layer ModeIndicatorColor (hex parse/serialize with sRGB clamping — ColorPicker can hand back Display P3 colors whose sRGB components fall outside 0…1 — and the mode→color table). The picker binding ignores a value whose hex equals the current effective color, so merely opening the color panel does not freeze the accent into a stored snapshot.

Verification

  • xcodebuild test -only-testing:VimActionTests: pass, re-run independently by the supervising session. New ModeIndicatorColorTests (16): hex round-trip, case normalization, RGBA order, eight malformed inputs, out-of-gamut Display P3 color, mode→color table for all four modes, per-key persistence for each of the three keys with the object(forKey:) != nil guard, reset removes all three keys, unreadable stored value, harmless without inputs, black text on a bright user color, alpha does not change the text verdict. xcodebuild build … CODE_SIGNING_ALLOWED=NO: 0 warnings.
  • On-device, scripted (Developer ID Release build installed to /Applications, defaults write + relaunch, window capture + pixel sampling): default badge = accent (0,122,255); Normal #FF0000FF → badge (255,0,0) while the INSERT flash stays (0,122,255) when Insert is unset; Insert #8000FFFF → INSERT flash (128,0,255) with no persistent panel afterwards; Visual #00A000FF → VISUAL badge (0,160,0) and back to red on Esc; Normal #FF000080 → captured alpha 128; window-border and screen-border strokes and their corner labels red with white text; keys removed → accent again. Product defaults restored afterwards.
  • Remaining hands-on checks (Settings UI, appearance): live recolor with Settings open, opening the color panel without picking leaves the keys unset, translucent and very light colors, Reset, dark/light switch with unset colors, the tab fitting 460×560.

Docs

mode-indicator-overlay.md (color flow, mode in Presentation, text-color rule, new pure layer, tests), profiles-and-config.md (three keys), app-shell.md (Colors section). Decisions recorded on main (the Insert color one partially supersedes the first, only the "Insert keeps the accent" clause).

🤖 Generated with Claude Code

pilyang and others added 2 commits September 16, 2026 10:18
normal and visual (visual + v-line) get their own color, alpha
included, chosen with two color pickers in the indicator tab; insert
keeps the system accent. both are unset by default, so nothing changes
for existing users. the colors live in userdefaults as sRGB #RRGGBBAA
strings owned by ModeIndicatorController, and a color change repaints
the panels on screen without an accessibility re-read: the presentation
now carries the mode instead of the label so the controller knows which
color a live panel needs. the pure ModeIndicatorColor layer (hex
round-trip with sRGB clamping, mode-to-color table) is table-tested,
the persistence tests keep the object(forKey:) != nil guard, and the
architecture references describe the new state.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
the insert flash gets its own color next to normal and visual, chosen
in the same colors section (normal / insert / visual order); reset
clears all three. same storage format, unset default and repaint path
as the other two. this follows the insert color decision that partially
supersedes the morning's per-mode color decision.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@pilyang pilyang added enhancement New feature or request area:app App shell: settings, menu bar, permissions, config, updates labels Sep 16, 2026
@pilyang
pilyang merged commit 0fde2f0 into main Sep 16, 2026
3 checks passed
@pilyang
pilyang deleted the feat/mode-indicator-colors branch September 16, 2026 12:29
pilyang added a commit that referenced this pull request Sep 16, 2026
the plan reached its goal: four prs merged (#66 flash, #67 persistent
badge, #68 caret anchor and border styles, #70 per-mode colors) and the
roadmap item checked. its two open follow-ups moved to github issues so
nothing is lost with the plan file:

- #71 (cold-launched apps miss observer registration)
- #72 (one unreproduced flash placement after a cross-display move)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:app App shell: settings, menu bar, permissions, config, updates enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant