Skip to main content

claudectl_core/
theme.rs

1use ratatui::style::Color;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum ThemeMode {
5    Dark,
6    Light,
7    None,
8}
9
10impl ThemeMode {
11    pub fn parse(s: &str) -> Option<Self> {
12        match s.to_lowercase().as_str() {
13            "dark" => Some(Self::Dark),
14            "light" => Some(Self::Light),
15            "none" => Some(Self::None),
16            _ => None,
17        }
18    }
19
20    /// Detect theme mode: CLI flag > config > NO_COLOR env > default (dark).
21    pub fn detect(cli_theme: Option<&str>) -> Self {
22        if let Some(t) = cli_theme.and_then(Self::parse) {
23            return t;
24        }
25        if std::env::var_os("NO_COLOR").is_some() {
26            return Self::None;
27        }
28        Self::Dark
29    }
30}
31
32/// Semantic color palette for the TUI.
33#[derive(Debug, Clone, Copy)]
34pub struct Theme {
35    #[allow(dead_code)]
36    pub mode: ThemeMode,
37
38    // Status colors
39    pub status_needs_input: Color,
40    pub status_processing: Color,
41    pub status_waiting: Color,
42    pub status_unknown: Color,
43    pub status_idle: Color,
44    pub status_finished: Color,
45
46    // UI chrome
47    pub border: Color,
48    pub header: Color,
49    pub footer: Color,
50    pub highlight_key: Color,
51    pub text_primary: Color,
52    pub text_muted: Color,
53
54    // Data colors
55    pub cost: Color,
56    pub cost_warning: Color,
57    pub cost_danger: Color,
58    pub context_ok: Color,
59    pub context_warning: Color,
60    pub context_danger: Color,
61    pub burn_rate_low: Color,
62    pub burn_rate_mid: Color,
63    pub burn_rate_high: Color,
64    pub sparkline: Color,
65    pub input_accent: Color,
66    pub success: Color,
67    pub error: Color,
68}
69
70impl Theme {
71    pub fn from_mode(mode: ThemeMode) -> Self {
72        match mode {
73            ThemeMode::Dark => Self::dark(),
74            ThemeMode::Light => Self::light(),
75            ThemeMode::None => Self::none(),
76        }
77    }
78
79    fn dark() -> Self {
80        Self {
81            mode: ThemeMode::Dark,
82            status_needs_input: Color::Magenta,
83            status_processing: Color::Green,
84            status_waiting: Color::Yellow,
85            status_unknown: Color::Blue,
86            status_idle: Color::DarkGray,
87            status_finished: Color::Red,
88            border: Color::DarkGray,
89            header: Color::Cyan,
90            footer: Color::DarkGray,
91            highlight_key: Color::Yellow,
92            text_primary: Color::White,
93            text_muted: Color::DarkGray,
94            cost: Color::Yellow,
95            cost_warning: Color::LightRed,
96            cost_danger: Color::Red,
97            context_ok: Color::Green,
98            context_warning: Color::Yellow,
99            context_danger: Color::Red,
100            burn_rate_low: Color::DarkGray,
101            burn_rate_mid: Color::Yellow,
102            burn_rate_high: Color::Red,
103            sparkline: Color::Blue,
104            input_accent: Color::Cyan,
105            success: Color::Green,
106            error: Color::Red,
107        }
108    }
109
110    fn light() -> Self {
111        Self {
112            mode: ThemeMode::Light,
113            status_needs_input: Color::Magenta,
114            status_processing: Color::Blue,
115            status_waiting: Color::Rgb(180, 140, 0), // Dark yellow
116            status_unknown: Color::Gray,
117            status_idle: Color::Gray,
118            status_finished: Color::Red,
119            border: Color::Gray,
120            header: Color::Blue,
121            footer: Color::Gray,
122            highlight_key: Color::Blue,
123            text_primary: Color::Black,
124            text_muted: Color::Gray,
125            cost: Color::Rgb(180, 140, 0),
126            cost_warning: Color::Red,
127            cost_danger: Color::LightRed,
128            context_ok: Color::Blue,
129            context_warning: Color::Rgb(180, 140, 0),
130            context_danger: Color::Red,
131            burn_rate_low: Color::Gray,
132            burn_rate_mid: Color::Rgb(180, 140, 0),
133            burn_rate_high: Color::Red,
134            sparkline: Color::Blue,
135            input_accent: Color::Blue,
136            success: Color::Blue,
137            error: Color::Red,
138        }
139    }
140
141    fn none() -> Self {
142        // Monochrome — no color, use terminal defaults
143        Self {
144            mode: ThemeMode::None,
145            status_needs_input: Color::Reset,
146            status_processing: Color::Reset,
147            status_waiting: Color::Reset,
148            status_unknown: Color::Reset,
149            status_idle: Color::Reset,
150            status_finished: Color::Reset,
151            border: Color::Reset,
152            header: Color::Reset,
153            footer: Color::Reset,
154            highlight_key: Color::Reset,
155            text_primary: Color::Reset,
156            text_muted: Color::Reset,
157            cost: Color::Reset,
158            cost_warning: Color::Reset,
159            cost_danger: Color::Reset,
160            context_ok: Color::Reset,
161            context_warning: Color::Reset,
162            context_danger: Color::Reset,
163            burn_rate_low: Color::Reset,
164            burn_rate_mid: Color::Reset,
165            burn_rate_high: Color::Reset,
166            sparkline: Color::Reset,
167            input_accent: Color::Reset,
168            success: Color::Reset,
169            error: Color::Reset,
170        }
171    }
172
173    /// Get the color for a session status.
174    pub fn status_color(&self, status: &crate::session::SessionStatus) -> Color {
175        use crate::session::SessionStatus;
176        match status {
177            SessionStatus::NeedsInput => self.status_needs_input,
178            SessionStatus::Processing => self.status_processing,
179            SessionStatus::WaitingInput => self.status_waiting,
180            SessionStatus::Unknown => self.status_unknown,
181            SessionStatus::Idle => self.status_idle,
182            SessionStatus::Finished => self.status_finished,
183        }
184    }
185}