Skip to main content

sparrow/tui/
theme.rs

1use ratatui::style::Color;
2
3// ─── Color tokens from §9.2 ─────────────────────────────────────────────────────
4
5pub struct Theme {
6    pub brand: Color,      // #f2a93c amber — brand, cost
7    pub coral: Color,      // #f0674a coral — secondary accent
8    pub agent: Color,      // #4ec9b0 teal — active agent / coder
9    pub planner: Color,    // #6fa6e6 blue — routing / planner
10    pub verifier: Color,   // #c9a14e sand — verifier
11    pub add: Color,        // #74c258 green — diff +
12    pub rem: Color,        // #d96a63 red — diff -
13    pub gold: Color,       // #f2c94c — pirate hoop / highlights
14    pub steel: Color,      // #b9b0a3 — tool metal
15    pub supervised: Color, // #74c258 green
16    pub trusted: Color,    // #f2a93c amber
17    pub autonomous: Color, // #d96a63 red
18    pub bg: Color,         // #0e0b08 near-black
19    pub panel: Color,      // #16120d panel bg
20    pub line: Color,       // #2c251c hairline
21    pub fg: Color,         // #ece2cf text
22    pub dim: Color,        // #897d6c muted
23    pub dimmer: Color,     // #5c5346 faint
24}
25
26/// Built-in theme names. `by_name` accepts any of these (case-insensitive).
27pub const THEME_NAMES: &[&str] = &["captain", "midnight", "paper"];
28
29/// Resolve a theme by name. Unknown names fall back to `captain`.
30pub fn by_name(name: &str) -> Theme {
31    match name.trim().to_lowercase().as_str() {
32        "midnight" => THEME_MIDNIGHT,
33        "paper" => THEME_PAPER,
34        _ => THEME_CAPTAIN,
35    }
36}
37
38pub const THEME_CAPTAIN: Theme = Theme {
39    brand: Color::Rgb(0xf2, 0xa9, 0x3c),
40    coral: Color::Rgb(0xf0, 0x67, 0x4a),
41    agent: Color::Rgb(0x4e, 0xc9, 0xb0),
42    planner: Color::Rgb(0x6f, 0xa6, 0xe6),
43    verifier: Color::Rgb(0xc9, 0xa1, 0x4e),
44    add: Color::Rgb(0x74, 0xc2, 0x58),
45    rem: Color::Rgb(0xd9, 0x6a, 0x63),
46    gold: Color::Rgb(0xf2, 0xc9, 0x4c),
47    steel: Color::Rgb(0xb9, 0xb0, 0xa3),
48    supervised: Color::Rgb(0x74, 0xc2, 0x58),
49    trusted: Color::Rgb(0xf2, 0xa9, 0x3c),
50    autonomous: Color::Rgb(0xd9, 0x6a, 0x63),
51    bg: Color::Rgb(0x0e, 0x0b, 0x08),
52    panel: Color::Rgb(0x16, 0x12, 0x0d),
53    line: Color::Rgb(0x2c, 0x25, 0x1c),
54    fg: Color::Rgb(0xec, 0xe2, 0xcf),
55    dim: Color::Rgb(0x89, 0x7d, 0x6c),
56    dimmer: Color::Rgb(0x5c, 0x53, 0x46),
57};
58
59/// Cool / dark variant: lower-saturation blues for late-night work.
60pub const THEME_MIDNIGHT: Theme = Theme {
61    brand: Color::Rgb(0x6f, 0xa6, 0xe6),
62    coral: Color::Rgb(0x9b, 0x7e, 0xd1),
63    agent: Color::Rgb(0x4e, 0xc9, 0xb0),
64    planner: Color::Rgb(0x6f, 0xa6, 0xe6),
65    verifier: Color::Rgb(0xc9, 0xa1, 0x4e),
66    add: Color::Rgb(0x74, 0xc2, 0x58),
67    rem: Color::Rgb(0xd9, 0x6a, 0x63),
68    gold: Color::Rgb(0xf2, 0xc9, 0x4c),
69    steel: Color::Rgb(0xb9, 0xb0, 0xa3),
70    supervised: Color::Rgb(0x74, 0xc2, 0x58),
71    trusted: Color::Rgb(0x6f, 0xa6, 0xe6),
72    autonomous: Color::Rgb(0xd9, 0x6a, 0x63),
73    bg: Color::Rgb(0x06, 0x08, 0x0e),
74    panel: Color::Rgb(0x0e, 0x12, 0x1a),
75    line: Color::Rgb(0x20, 0x26, 0x33),
76    fg: Color::Rgb(0xd6, 0xde, 0xeb),
77    dim: Color::Rgb(0x7a, 0x84, 0x99),
78    dimmer: Color::Rgb(0x4a, 0x52, 0x63),
79};
80
81/// Light variant: cream paper background, dark fg. For bright environments.
82pub const THEME_PAPER: Theme = Theme {
83    brand: Color::Rgb(0xa6, 0x5a, 0x1a),
84    coral: Color::Rgb(0xa6, 0x3a, 0x2a),
85    agent: Color::Rgb(0x2f, 0x7d, 0x67),
86    planner: Color::Rgb(0x2e, 0x5a, 0x9c),
87    verifier: Color::Rgb(0x7a, 0x5e, 0x2c),
88    add: Color::Rgb(0x3a, 0x7d, 0x2e),
89    rem: Color::Rgb(0xa6, 0x3a, 0x2a),
90    gold: Color::Rgb(0xa6, 0x80, 0x1a),
91    steel: Color::Rgb(0x5e, 0x55, 0x47),
92    supervised: Color::Rgb(0x3a, 0x7d, 0x2e),
93    trusted: Color::Rgb(0xa6, 0x5a, 0x1a),
94    autonomous: Color::Rgb(0xa6, 0x3a, 0x2a),
95    bg: Color::Rgb(0xf3, 0xee, 0xe1),
96    panel: Color::Rgb(0xe7, 0xe0, 0xcc),
97    line: Color::Rgb(0xc6, 0xbc, 0xa3),
98    fg: Color::Rgb(0x2a, 0x24, 0x18),
99    dim: Color::Rgb(0x6a, 0x60, 0x4c),
100    dimmer: Color::Rgb(0x9a, 0x90, 0x7c),
101};
102
103impl Theme {
104    pub fn autonomy_color(&self, level: &crate::event::AutonomyLevel) -> Color {
105        match level {
106            crate::event::AutonomyLevel::Supervised => self.supervised,
107            crate::event::AutonomyLevel::Trusted => self.trusted,
108            crate::event::AutonomyLevel::Autonomous => self.autonomous,
109        }
110    }
111
112    pub fn agent_color(&self, role: &str) -> Color {
113        match role {
114            "planner" => self.planner,
115            "coder" => self.agent,
116            "verifier" => self.verifier,
117            "swarm" => self.gold,
118            _ => self.steel,
119        }
120    }
121
122    pub fn spinner_frame(&self, _index: usize) -> &str {
123        const FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
124        FRAMES[_index % FRAMES.len()]
125    }
126
127    pub fn flight_verb(&self, index: usize) -> &str {
128        const VERBS: &[&str] = &[
129            "Soaring", "Gliding", "Diving", "Scouting", "Perching", "Foraging", "Wheeling",
130        ];
131        VERBS[index % VERBS.len()]
132    }
133}
134
135impl Default for Theme {
136    fn default() -> Self {
137        THEME_CAPTAIN
138    }
139}
140
141// ─── ASCII Logo (§9.4) ──────────────────────────────────────────────────────────
142
143// Validated console mascot (§9.4): two-feather crest, thick eyebrow, open eye +
144// pirate patch, coral beak, cheek blush, cream belly, feet + key in the wing.
145pub const ASCII_SPARROW: &str = r#"
146        ^^
147      .-~~~-.
148     /__     \
149    | o   ██  |
150    |    v    |
151    | .       |
152     \ \__/  /
153      '-..-'
154      /|  |\  ╤━o
155     '_|  |_'
156"#;
157
158pub const ASCII_WORDMARK: &str = "S P A R R O W";
159
160pub fn ascii_sparrow_at_frame(frame: u64) -> String {
161    let blink = frame % 88 == 0;
162    let bob = frame % 60 == 0;
163    let mut art = ASCII_SPARROW.to_string();
164    if blink {
165        // eye line is "| o   ██  |" → close the eye to a dash
166        art = art.replace("| o ", "| - ");
167    }
168    if bob {
169        art.lines()
170            .map(|line| {
171                if line.is_empty() {
172                    String::new()
173                } else {
174                    format!(" {}", line)
175                }
176            })
177            .collect::<Vec<_>>()
178            .join("\n")
179    } else {
180        art
181    }
182}
183
184pub fn boot_sequence() -> Vec<String> {
185    vec![
186        format!("{}", ASCII_SPARROW),
187        format!("  {}  ", ASCII_WORDMARK),
188        String::new(),
189        "one cli · grows with you".to_string(),
190        String::new(),
191        "boot: router · surfaces · sandbox · skills · memory · autonomy · ready".to_string(),
192    ]
193}