//! Theme and colour palette. //! //! Two themes are provided — one for light terminal backgrounds //! and one for dark — per NFR-7. The palette is intentionally //! small for the walking skeleton; it grows as more views are //! added. Contrast is chosen against the target background so //! that foreground text meets WCAG-AA (NFR-5) on both variants. //! //! Per-token-class colours (the `tok_*` fields) drive ambient //! typing assistance (ADR-0022 §3). Each token class has a //! distinct colour so the user can tell keywords from //! identifiers from string literals at a glance, with punct and //! identifier intentionally close to `fg` to keep the surface //! quiet for the dominant content. The `tok_error` colour //! reuses the existing error palette so lex-error tokens and //! parse-error overlays read consistently with `[error]` //! lines elsewhere. use ratatui::style::Color; use crate::dsl::grammar::HighlightClass; /// Foreground of the demonstration-mode overlays (ADR-0047 D4). /// /// Deliberately a fixed, theme-independent high-contrast pair — black /// on yellow — so the badge / caption boxes are hard to overlook in a /// screencast on any background. pub const DEMO_OVERLAY_FG: Color = Color::Black; /// Background of the demonstration-mode overlays (ADR-0047 D4); see /// [`DEMO_OVERLAY_FG`]. pub const DEMO_OVERLAY_BG: Color = Color::Rgb(0xFF, 0xD7, 0x00); #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Background { Light, Dark, } #[derive(Debug, Clone)] pub struct Theme { pub background: Background, pub bg: Color, pub fg: Color, pub muted: Color, pub border: Color, pub border_advanced: Color, pub mode_simple: Color, pub mode_advanced: Color, pub system: Color, pub error: Color, /// Validity-indicator WARNING colour (ADR-0027 §4) — an /// amber distinct from `error`'s red. Drives the `[WRN]` /// label; `[ERR]` reuses `error`. Also the query-plan /// "expensive step" colour (ADR-0028 §6). pub warning: Color, /// Query-plan "efficient step" colour (ADR-0028 §6) — a /// green deliberately distinct from `system` so green never /// reads as two things. Indexed lookups carry it; expensive /// steps reuse `warning`. pub plan_efficient: Color, // ---- Per-token-class colours (ADR-0022 §3) ------------------- pub tok_keyword: Color, pub tok_identifier: Color, /// Column data-type keyword colour (ADR-0022 Amendment 4) — /// a dedicated tone distinct from both `tok_keyword` and /// `tok_identifier` so a learner can tell a type from a /// clause keyword or an invented name at a glance. pub tok_type: Color, pub tok_number: Color, pub tok_string: Color, pub tok_punct: Color, pub tok_flag: Color, pub tok_error: Color, /// SQL function-name candidate colour (ADR-0022 Amendment 6, /// issue #15) — a dedicated tone distinct from `tok_keyword`, /// `tok_identifier`, and `tok_type` so a learner can tell a /// callable (`sum`, `upper`) apart from a clause keyword, a /// column reference, and a column type. Drives the /// `CandidateKind::Function` colour in the hint panel. pub tok_function: Color, } impl Theme { #[must_use] pub const fn dark() -> Self { Self { background: Background::Dark, bg: Color::Rgb(0x18, 0x1B, 0x22), fg: Color::Rgb(0xE6, 0xE6, 0xE6), muted: Color::Rgb(0x8B, 0x90, 0x9A), border: Color::Rgb(0x4A, 0x52, 0x65), border_advanced: Color::Rgb(0xE0, 0x60, 0x60), mode_simple: Color::Rgb(0x6E, 0xC4, 0xFF), mode_advanced: Color::Rgb(0xFF, 0x9E, 0x6B), system: Color::Rgb(0x9F, 0xD8, 0x91), error: Color::Rgb(0xFF, 0x6B, 0x6B), warning: Color::Rgb(0xF5, 0xA9, 0x4B), // amber plan_efficient: Color::Rgb(0x4D, 0xD0, 0xA8), // teal-green // Token classes — distinct enough to tell apart at a // glance, quiet enough that 80-char lines don't read // like a Christmas tree. Identifier and punct sit // close to `fg`/`muted` so the dominant content // remains restful; literals and flags get warm // accent tones; keyword takes a cool accent tone // distinct from the mode-banner blue. tok_keyword: Color::Rgb(0xC7, 0x92, 0xEA), // muted purple tok_identifier: Color::Rgb(0x56, 0xB6, 0xC2), // cyan-teal — identifiers are the user's content, deserve a vivid distinct colour tok_type: Color::Rgb(0xF0, 0x8F, 0xC0), // pink — types sit in the red-purple range, clearly apart from the lavender keyword and teal identifier tok_number: Color::Rgb(0xF7, 0x8C, 0x6C), // warm orange tok_string: Color::Rgb(0xC3, 0xE8, 0x8D), // soft green tok_punct: Color::Rgb(0x8B, 0x90, 0x9A), // == muted tok_flag: Color::Rgb(0xFF, 0xCB, 0x6B), // amber tok_error: Color::Rgb(0xFF, 0x6B, 0x6B), // == error tok_function: Color::Rgb(0x82, 0xCF, 0xFD), // sky blue — cool like keyword but bluer, clearly apart from purple keyword + teal identifier + pink type } } #[must_use] pub const fn light() -> Self { Self { background: Background::Light, bg: Color::Rgb(0xFA, 0xFA, 0xF7), fg: Color::Rgb(0x1A, 0x1F, 0x2C), muted: Color::Rgb(0x60, 0x66, 0x73), border: Color::Rgb(0xB6, 0xBC, 0xC8), border_advanced: Color::Rgb(0xC2, 0x3A, 0x3A), mode_simple: Color::Rgb(0x21, 0x69, 0xC7), mode_advanced: Color::Rgb(0xB0, 0x4A, 0x12), system: Color::Rgb(0x2E, 0x7C, 0x3C), error: Color::Rgb(0xC0, 0x39, 0x2B), warning: Color::Rgb(0xA6, 0x5A, 0x00), // burnt amber plan_efficient: Color::Rgb(0x0B, 0x80, 0x6A), // deep teal-green // Light-theme token palette: same intent as dark — // identifier/punct close to fg/muted; warm tones for // literals + flags; cool accent for keyword. tok_keyword: Color::Rgb(0x6F, 0x42, 0xC1), // royal purple tok_identifier: Color::Rgb(0x0F, 0x6B, 0x76), // deep teal — same role as dark variant: identifiers stand out tok_type: Color::Rgb(0xA8, 0x2D, 0x73), // deep magenta — red-purple, distinct from royal-purple keyword + teal identifier tok_number: Color::Rgb(0xBC, 0x4F, 0x1F), // burnt orange tok_string: Color::Rgb(0x22, 0x86, 0x3A), // forest green tok_punct: Color::Rgb(0x60, 0x66, 0x73), // == muted tok_flag: Color::Rgb(0xB0, 0x88, 0x00), // mustard tok_error: Color::Rgb(0xC0, 0x39, 0x2B), // == error tok_function: Color::Rgb(0x1A, 0x5F, 0xB0), // strong blue — cool like keyword but bluer, apart from royal-purple keyword + teal identifier + magenta type } } /// Map a walker `HighlightClass` to its display colour /// (ADR-0024 §architecture, Phase F). This is the walker-side /// equivalent of `token_color` — the renderer consumes /// `walker::highlight_runs` output, which produces /// `HighlightClass` per byte range, and looks up colours /// through this method. #[must_use] pub const fn highlight_class_color(&self, class: HighlightClass) -> Color { match class { HighlightClass::Keyword => self.tok_keyword, HighlightClass::Identifier => self.tok_identifier, HighlightClass::Type => self.tok_type, HighlightClass::Number => self.tok_number, HighlightClass::String => self.tok_string, HighlightClass::Punct => self.tok_punct, HighlightClass::Flag => self.tok_flag, HighlightClass::Function => self.tok_function, HighlightClass::Error => self.tok_error, } } } impl Default for Theme { fn default() -> Self { Self::dark() } } #[cfg(test)] mod tests { use super::*; #[test] fn dark_theme_token_colours_differ_from_background() { let t = Theme::dark(); for (name, c) in [ ("tok_keyword", t.tok_keyword), ("tok_type", t.tok_type), ("tok_number", t.tok_number), ("tok_string", t.tok_string), ("tok_flag", t.tok_flag), ("tok_error", t.tok_error), ("tok_function", t.tok_function), ("warning", t.warning), ] { assert_ne!( c, t.bg, "{name} must contrast against bg in dark theme", ); } } #[test] fn light_theme_token_colours_differ_from_background() { let t = Theme::light(); for (name, c) in [ ("tok_keyword", t.tok_keyword), ("tok_type", t.tok_type), ("tok_number", t.tok_number), ("tok_string", t.tok_string), ("tok_flag", t.tok_flag), ("tok_error", t.tok_error), ("tok_function", t.tok_function), ("warning", t.warning), ] { assert_ne!( c, t.bg, "{name} must contrast against bg in light theme", ); } } #[test] fn highlight_class_color_maps_each_variant() { let t = Theme::dark(); assert_eq!(t.highlight_class_color(HighlightClass::Keyword), t.tok_keyword); assert_eq!(t.highlight_class_color(HighlightClass::Identifier), t.tok_identifier); assert_eq!(t.highlight_class_color(HighlightClass::Type), t.tok_type); assert_eq!(t.highlight_class_color(HighlightClass::Number), t.tok_number); assert_eq!(t.highlight_class_color(HighlightClass::String), t.tok_string); assert_eq!(t.highlight_class_color(HighlightClass::Punct), t.tok_punct); assert_eq!(t.highlight_class_color(HighlightClass::Flag), t.tok_flag); assert_eq!(t.highlight_class_color(HighlightClass::Function), t.tok_function); assert_eq!(t.highlight_class_color(HighlightClass::Error), t.tok_error); } #[test] fn type_colour_is_distinct_from_keyword_and_identifier() { // ADR-0022 Amendment 4 / issue #8: the whole point of a // dedicated type class is that types do NOT share a colour // with clause keywords or invented identifiers. for t in [Theme::dark(), Theme::light()] { assert_ne!(t.tok_type, t.tok_keyword); assert_ne!(t.tok_type, t.tok_identifier); } } #[test] fn function_colour_is_distinct_from_keyword_identifier_and_type() { // ADR-0022 Amendment 6 / issue #15: function-name candidates // get their own tone so a callable reads apart from a clause // keyword, a column reference, and a column type. for t in [Theme::dark(), Theme::light()] { assert_ne!(t.tok_function, t.tok_keyword); assert_ne!(t.tok_function, t.tok_identifier); assert_ne!(t.tok_function, t.tok_type); } } }