Sweep every first-party crate source (1956 .rs files) to the project comment guidelines: delete redundant restatements, decorative banners, change narration, and end-of-line comments; keep and tighten the crucial ones (invariants, bug rationale, SAFETY blocks, ported-source attribution). No functional code changed. Every edit is proven comment-only against the prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a separate doctest-fence check. Where removing a comment made rustfmt or clippy want to re-lay-out adjacent code, the minimal triggering comment is restored so code tokens stay byte-identical. Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy --workspace --all-targets (0 warnings). Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for these guidelines (flags banners, end-of-line comments, change narration, and commented-out code).
299 lines
11 KiB
Rust
299 lines
11 KiB
Rust
//! Terminal-native palette for minimal mode.
|
|
//!
|
|
//! Any RGB theme is designed for one background polarity, so composited on
|
|
//! the terminal's own canvas it can land dark-on-dark or light-on-light
|
|
//! (e.g. macOS in Light Mode + a dark terminal profile). Polarity detection
|
|
//! is not reliable either: OS appearance and OSC 11 both disagree with the
|
|
//! actual canvas in edge cases and can differ mid-session. Terminal
|
|
//! profiles, however, tune their **default** fg/bg to be legible against
|
|
//! their own background — this is how `git` and `ls` stay readable on any
|
|
//! terminal — so a palette built from `Reset` (body) + sparse named ANSI-16
|
|
//! accents is polarity-safe without detection.
|
|
//!
|
|
//! ## Grays / secondary text
|
|
//!
|
|
//! Do **not** paint body or status text as `DarkGray` (ANSI bright black).
|
|
//! Many dark profiles deliberately set that slot very dark for subtle
|
|
//! chrome, which washes out tool stdout and the prompt info bar. Instead:
|
|
//!
|
|
//! - **Primary content** (`text_primary`, `gray_bright`, …) → `Color::Reset`
|
|
//! (terminal default foreground).
|
|
//! - **Secondary chrome** (`gray`, `gray_dim`, `text_secondary`) → also
|
|
//! `Color::Reset`; [`Theme::muted`] / [`Theme::dim`] apply `Modifier::DIM`
|
|
//! so de-emphasis tracks the terminal's own fg (polarity-safe), unlike
|
|
//! hard-coding bright black.
|
|
|
|
use ratatui::style::{Color, Modifier};
|
|
|
|
use super::Theme;
|
|
|
|
impl Theme {
|
|
/// The fixed terminal-native palette used by minimal mode: every field
|
|
/// is `Color::Reset` or a named ANSI-16 color (see the module docs).
|
|
pub const fn terminal_default() -> Self {
|
|
// Secondary roles store Reset; Theme::muted / Theme::dim apply SGR dim.
|
|
const MUTED: Color = Color::Reset;
|
|
|
|
Self {
|
|
bg_base: Color::Reset,
|
|
bg_light: Color::Reset,
|
|
bg_dark: Color::Reset,
|
|
bg_highlight: Color::Reset,
|
|
bg_hover: Color::Reset,
|
|
bg_terminal: Color::Reset,
|
|
|
|
accent_user: Color::Reset,
|
|
accent_assistant: Color::Magenta,
|
|
accent_thinking: MUTED,
|
|
accent_tool: MUTED,
|
|
accent_system: Color::Blue,
|
|
accent_error: Color::Red,
|
|
accent_success: Color::Green,
|
|
accent_running: Color::Magenta,
|
|
accent_skill: Color::Blue,
|
|
|
|
text_primary: Color::Reset,
|
|
text_secondary: MUTED,
|
|
|
|
gray_dim: MUTED,
|
|
gray: MUTED,
|
|
gray_bright: Color::Reset,
|
|
|
|
command: Color::Yellow,
|
|
path: Color::Cyan,
|
|
running: Color::Cyan,
|
|
warning: Color::Yellow,
|
|
|
|
fuzzy_accent: Color::Cyan,
|
|
|
|
accent_plan: Color::Yellow,
|
|
accent_verify: Color::Magenta,
|
|
accent_feedback: Color::Cyan,
|
|
accent_remember: Color::Green,
|
|
|
|
selection_border: MUTED,
|
|
hover_border: MUTED,
|
|
prompt_border: MUTED,
|
|
prompt_border_active: Color::Reset,
|
|
|
|
accent_model: Color::Cyan,
|
|
|
|
scrollbar_bg: Color::Reset,
|
|
scrollbar_fg: MUTED,
|
|
|
|
diff_delete_bg: Color::Reset,
|
|
diff_delete_fg: Color::Red,
|
|
diff_insert_bg: Color::Reset,
|
|
diff_insert_fg: Color::Green,
|
|
diff_equal_fg: MUTED,
|
|
diff_gutter_fg: MUTED,
|
|
|
|
bg_visual: Color::Reset,
|
|
|
|
paste_bg: Color::Reset,
|
|
paste_fg: MUTED,
|
|
paste_dim: MUTED,
|
|
|
|
md_heading_h1: Color::Reset,
|
|
md_heading_h1_mod: Modifier::BOLD.union(Modifier::UNDERLINED),
|
|
md_heading_h2: Color::Reset,
|
|
md_heading_h2_mod: Modifier::BOLD,
|
|
md_heading_h3: Color::Reset,
|
|
md_heading_h3_mod: Modifier::BOLD,
|
|
md_heading_h4: Color::Reset,
|
|
md_heading_h4_mod: Modifier::BOLD,
|
|
md_heading_h5: Color::Reset,
|
|
md_heading_h5_mod: Modifier::BOLD,
|
|
md_heading_h6: MUTED,
|
|
md_heading_h6_mod: Modifier::BOLD,
|
|
md_code: Color::Cyan,
|
|
md_task_checked: Color::Green,
|
|
md_task_unchecked: MUTED,
|
|
md_muted: MUTED,
|
|
md_code_bg: Color::Reset,
|
|
md_text: Color::Reset,
|
|
link_fg: Color::Blue,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn all_colors(theme: &Theme) -> Vec<(&'static str, Color)> {
|
|
vec![
|
|
("bg_base", theme.bg_base),
|
|
("bg_light", theme.bg_light),
|
|
("bg_dark", theme.bg_dark),
|
|
("bg_highlight", theme.bg_highlight),
|
|
("bg_hover", theme.bg_hover),
|
|
("bg_terminal", theme.bg_terminal),
|
|
("accent_user", theme.accent_user),
|
|
("accent_assistant", theme.accent_assistant),
|
|
("accent_thinking", theme.accent_thinking),
|
|
("accent_tool", theme.accent_tool),
|
|
("accent_system", theme.accent_system),
|
|
("accent_error", theme.accent_error),
|
|
("accent_success", theme.accent_success),
|
|
("accent_running", theme.accent_running),
|
|
("accent_skill", theme.accent_skill),
|
|
("text_primary", theme.text_primary),
|
|
("text_secondary", theme.text_secondary),
|
|
("gray_dim", theme.gray_dim),
|
|
("gray", theme.gray),
|
|
("gray_bright", theme.gray_bright),
|
|
("command", theme.command),
|
|
("path", theme.path),
|
|
("running", theme.running),
|
|
("warning", theme.warning),
|
|
("fuzzy_accent", theme.fuzzy_accent),
|
|
("accent_plan", theme.accent_plan),
|
|
("accent_verify", theme.accent_verify),
|
|
("accent_feedback", theme.accent_feedback),
|
|
("accent_remember", theme.accent_remember),
|
|
("selection_border", theme.selection_border),
|
|
("hover_border", theme.hover_border),
|
|
("prompt_border", theme.prompt_border),
|
|
("prompt_border_active", theme.prompt_border_active),
|
|
("accent_model", theme.accent_model),
|
|
("scrollbar_bg", theme.scrollbar_bg),
|
|
("scrollbar_fg", theme.scrollbar_fg),
|
|
("diff_delete_bg", theme.diff_delete_bg),
|
|
("diff_delete_fg", theme.diff_delete_fg),
|
|
("diff_insert_bg", theme.diff_insert_bg),
|
|
("diff_insert_fg", theme.diff_insert_fg),
|
|
("diff_equal_fg", theme.diff_equal_fg),
|
|
("diff_gutter_fg", theme.diff_gutter_fg),
|
|
("bg_visual", theme.bg_visual),
|
|
("paste_bg", theme.paste_bg),
|
|
("paste_fg", theme.paste_fg),
|
|
("paste_dim", theme.paste_dim),
|
|
("md_heading_h1", theme.md_heading_h1),
|
|
("md_heading_h2", theme.md_heading_h2),
|
|
("md_heading_h3", theme.md_heading_h3),
|
|
("md_heading_h4", theme.md_heading_h4),
|
|
("md_heading_h5", theme.md_heading_h5),
|
|
("md_heading_h6", theme.md_heading_h6),
|
|
("md_code", theme.md_code),
|
|
("md_task_checked", theme.md_task_checked),
|
|
("md_task_unchecked", theme.md_task_unchecked),
|
|
("md_muted", theme.md_muted),
|
|
("md_code_bg", theme.md_code_bg),
|
|
("md_text", theme.md_text),
|
|
("link_fg", theme.link_fg),
|
|
]
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_default_uses_only_reset_and_named_ansi() {
|
|
let theme = Theme::terminal_default();
|
|
for (name, color) in all_colors(&theme) {
|
|
assert!(
|
|
!matches!(color, Color::Rgb(..) | Color::Indexed(_)),
|
|
"{name} must be Reset or a named ANSI color, got {color:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_default_backgrounds_are_transparent() {
|
|
let theme = Theme::terminal_default();
|
|
for (name, color) in [
|
|
("bg_base", theme.bg_base),
|
|
("bg_light", theme.bg_light),
|
|
("bg_dark", theme.bg_dark),
|
|
("bg_terminal", theme.bg_terminal),
|
|
("md_code_bg", theme.md_code_bg),
|
|
("diff_delete_bg", theme.diff_delete_bg),
|
|
("diff_insert_bg", theme.diff_insert_bg),
|
|
("paste_bg", theme.paste_bg),
|
|
] {
|
|
assert_eq!(color, Color::Reset, "{name} must defer to the canvas");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_default_leaves_cursor_color_alone() {
|
|
let theme = Theme::terminal_default();
|
|
assert_eq!(theme.accent_user, Color::Reset);
|
|
assert_eq!(
|
|
crate::render::color::resolve_to_rgb(theme.accent_user),
|
|
None
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_default_survives_quantization() {
|
|
use crate::theme::color_support::ColorLevel;
|
|
let theme = Theme::terminal_default();
|
|
for level in [
|
|
ColorLevel::Basic,
|
|
ColorLevel::Ansi256,
|
|
ColorLevel::TrueColor,
|
|
] {
|
|
let quantized = theme.quantized(level);
|
|
for ((name, before), (_, after)) in
|
|
all_colors(&theme).into_iter().zip(all_colors(&quantized))
|
|
{
|
|
assert_eq!(before, after, "{name} must survive {level:?}");
|
|
}
|
|
}
|
|
let stripped = theme.quantized(ColorLevel::None);
|
|
for (name, color) in all_colors(&stripped) {
|
|
assert_eq!(color, Color::Reset, "{name} must strip under NO_COLOR");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_default_primary_is_reset_not_dark_gray() {
|
|
let theme = Theme::terminal_default();
|
|
assert_eq!(theme.text_primary, Color::Reset);
|
|
assert_eq!(theme.gray, Color::Reset);
|
|
assert_eq!(theme.gray_dim, Color::Reset);
|
|
// Must not hard-code bright black for body/secondary roles.
|
|
assert_ne!(theme.text_primary, Color::DarkGray);
|
|
assert_ne!(theme.gray, Color::DarkGray);
|
|
assert_ne!(theme.gray_dim, Color::DarkGray);
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_default_muted_and_dim_use_sgr_dim_not_bright_black() {
|
|
use ratatui::style::Modifier;
|
|
let theme = Theme::terminal_default();
|
|
let muted = theme.muted();
|
|
let dim = theme.dim();
|
|
assert!(
|
|
muted.add_modifier.contains(Modifier::DIM),
|
|
"muted should DIM the terminal default fg: {muted:?}"
|
|
);
|
|
assert!(
|
|
dim.add_modifier.contains(Modifier::DIM),
|
|
"dim should DIM the terminal default fg: {dim:?}"
|
|
);
|
|
// No explicit DarkGray paint — dim tracks the host palette.
|
|
assert!(
|
|
muted.fg.is_none() || muted.fg == Some(Color::Reset),
|
|
"muted must not set a hard gray: {muted:?}"
|
|
);
|
|
assert!(
|
|
dim.fg.is_none() || dim.fg == Some(Color::Reset),
|
|
"dim must not set a hard gray: {dim:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rgb_theme_muted_keeps_explicit_gray_without_forced_dim() {
|
|
use ratatui::style::Modifier;
|
|
// KigiNight paints real RGB grays; muted/dim must not invent DIM.
|
|
let theme = Theme::kiginight();
|
|
assert!(!matches!(theme.gray, Color::Reset));
|
|
let muted = theme.muted();
|
|
assert_eq!(muted.fg, Some(theme.gray));
|
|
assert!(
|
|
!muted.add_modifier.contains(Modifier::DIM),
|
|
"RGB muted should not force SGR dim: {muted:?}"
|
|
);
|
|
}
|
|
}
|