03d8a09457
OutputLine gains an optional styled-runs payload — a
Vec<OutputSpan> of { byte_range, OutputStyleClass } over the
line text. render_output_line gains a branch: when the payload
is present it renders the text span-by-span, each run's
semantic class (Neutral / Efficient / Expensive /
AutomaticIndex) resolved to a theme colour at render time;
otherwise the existing whole-line kind styling. The echo path
is untouched.
Theme gains `plan_efficient` — a green deliberately distinct
from `system` so green never reads as two things (ADR-0028 §6);
`warning` is reused for expensive steps.
A general per-span output-styling capability (ADR-0016's OOS-3
realized); the query-plan renderer will be its first consumer.
No user-visible change on its own. 1133 passing, clippy clean.
202 lines
7.9 KiB
Rust
202 lines
7.9 KiB
Rust
//! 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;
|
|
|
|
#[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,
|
|
pub tok_number: Color,
|
|
pub tok_string: Color,
|
|
pub tok_punct: Color,
|
|
pub tok_flag: Color,
|
|
pub tok_error: 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_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
|
|
}
|
|
}
|
|
|
|
#[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_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
|
|
}
|
|
}
|
|
|
|
/// 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::Number => self.tok_number,
|
|
HighlightClass::String => self.tok_string,
|
|
HighlightClass::Punct => self.tok_punct,
|
|
HighlightClass::Flag => self.tok_flag,
|
|
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_number", t.tok_number),
|
|
("tok_string", t.tok_string),
|
|
("tok_flag", t.tok_flag),
|
|
("tok_error", t.tok_error),
|
|
("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_number", t.tok_number),
|
|
("tok_string", t.tok_string),
|
|
("tok_flag", t.tok_flag),
|
|
("tok_error", t.tok_error),
|
|
("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::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::Error), t.tok_error);
|
|
}
|
|
}
|