render: add theme.minimum_contrast #218

Closed
opened 2026-10-21 13:19:46 +09:00 by natsukium · 0 comments
Owner

Problem

A program that hard-codes SGR colors can render text that a user's theme makes unreadable (blue on a dark background, light gray on a light theme). No layer outside the terminal can fix this: the program does not know the theme, and the theme does not know the program. Ghostty (minimum-contrast), kitty (text_fg_override_threshold), WezTerm (text_min_contrast_ratio) and iTerm2 all offer a minimum-contrast setting; felis has none.

Proposal (0.3.x, additive, default off)

theme.minimum_contrast: a WCAG 2 contrast ratio in [1, 21], default 1 (output identical to today). Out-of-range values clamp with a warning.

  • Applied in resolve_cell (crates/felis-render-core/src/appearance.rs) after the faint step, so it judges the final fg against the final bg: reverse video, selection and block cursor follow without extra work.
  • Adjust fg lightness in a perceptual space keeping hue (OKLab L, as WezTerm does); fall back to black or white when the ratio is out of reach.
  • Skip cells where fg equals bg (deliberately hidden text) and bidi-marker cells (a security signal).
  • No exemption by glyph class: box-drawing, block elements and Powerline glyphs are adjusted like any text. Exempting them would select behavior by the characters a program printed, which principle 4's Test rejects. Color emoji and images are unaffected by construction (the emoji path ignores the tint; images never pass through resolve_cell).
  • Compare against the straight-alpha background. On a translucent surface ResolvedTheme holds the premultiplied default bg (premultiplied_bg / refresh_theme in crates/felis-render-wgpu/src/lib.rs), so the straight color must be carried alongside it. The desktop behind the window is not considered.
  • A theme reload must invalidate cached rows as other theme.* keys do.

Principle check: principle 3 puts colors with the client; principle 4 is not engaged because the rule reads resolved colors, never characters; principle 2 (parity with kitty) holds.

Cascade via the add-config-key skill: schema, docs/reference/config.md, home-manager module, CHANGELOG. Tests: unit tests beside the faint tests (default is identity, passing pairs untouched, failing pairs reach the target, fg == bg and bidi untouched, judged against the post-reverse/selection bg), a proptest that the result meets the ratio whenever it is reachable, and a golden row at 4.5.

## Problem A program that hard-codes SGR colors can render text that a user's theme makes unreadable (blue on a dark background, light gray on a light theme). No layer outside the terminal can fix this: the program does not know the theme, and the theme does not know the program. Ghostty (`minimum-contrast`), kitty (`text_fg_override_threshold`), WezTerm (`text_min_contrast_ratio`) and iTerm2 all offer a minimum-contrast setting; felis has none. ## Proposal (0.3.x, additive, default off) `theme.minimum_contrast`: a WCAG 2 contrast ratio in [1, 21], default `1` (output identical to today). Out-of-range values clamp with a warning. - Applied in `resolve_cell` (`crates/felis-render-core/src/appearance.rs`) after the faint step, so it judges the final fg against the final bg: reverse video, selection and block cursor follow without extra work. - Adjust fg lightness in a perceptual space keeping hue (OKLab L, as WezTerm does); fall back to black or white when the ratio is out of reach. - Skip cells where fg equals bg (deliberately hidden text) and bidi-marker cells (a security signal). - No exemption by glyph class: box-drawing, block elements and Powerline glyphs are adjusted like any text. Exempting them would select behavior by the characters a program printed, which principle 4's Test rejects. Color emoji and images are unaffected by construction (the emoji path ignores the tint; images never pass through `resolve_cell`). - Compare against the straight-alpha background. On a translucent surface `ResolvedTheme` holds the premultiplied default bg (`premultiplied_bg` / `refresh_theme` in `crates/felis-render-wgpu/src/lib.rs`), so the straight color must be carried alongside it. The desktop behind the window is not considered. - A theme reload must invalidate cached rows as other `theme.*` keys do. Principle check: principle 3 puts colors with the client; principle 4 is not engaged because the rule reads resolved colors, never characters; principle 2 (parity with kitty) holds. Cascade via the `add-config-key` skill: schema, `docs/reference/config.md`, home-manager module, CHANGELOG. Tests: unit tests beside the faint tests (default is identity, passing pairs untouched, failing pairs reach the target, fg == bg and bidi untouched, judged against the post-reverse/selection bg), a proptest that the result meets the ratio whenever it is reachable, and a golden row at 4.5.
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
felis-terminal/felis#218
No description provided.