返回 CodeWhale
deepseek_theme.rs
根目录 / crates / tui / src / deepseek_theme.rs
1 //! Codewhale terminal theme tokens (legacy module path).
2 //!
3 //! A small, deliberately flat module that names the color, border, and
4 //! padding choices the TUI is making. Values follow the semantic grammar
5 //! exposed by [`crate::palette`], keeping the older module path for source
6 //! compatibility.
7 //!
8 //! The only consumers today are tool cell renderers in [`crate::tui::history`]
9 //! and sidebar section chrome in [`crate::tui::ui`]. All other call sites
10 //! continue to use [`crate::palette`] directly until they are migrated.
11
12 use ratatui::style::{Color, Modifier, Style};
13 use ratatui::widgets::{BorderType, Borders, Padding};
14
15 use crate::palette;
16 use crate::palette::PaletteMode;
17 use crate::tui::history::ToolStatus;
18
19 /// Visual variant exposed by the theme.
20 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
21 pub enum Variant {
22 Dark,
23 Light,
24 Grayscale,
25 }
26
27 /// Centralized visual tokens for sidebar and tool rendering.
28 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
29 pub struct Theme {
30 pub variant: Variant,
31
32 // Sidebar / section chrome
33 pub section_borders: Borders,
34 pub section_border_type: BorderType,
35 pub section_border_color: Color,
36 pub section_bg: Color,
37 pub section_title_color: Color,
38 pub section_padding: Padding,
39
40 // Tool cell color tokens
41 pub tool_title_color: Color,
42 pub tool_value_color: Color,
43 pub tool_label_color: Color,
44 pub tool_running_accent: Color,
45 pub tool_success_accent: Color,
46 pub tool_failed_accent: Color,
47 }
48
49 impl Theme {
50 /// The current dark theme. Visible output today uses these values.
51 #[must_use]
52 pub const fn dark() -> Self {
53 Self {
54 variant: Variant::Dark,
55 section_borders: Borders::ALL,
56 section_border_type: BorderType::Plain,
57 section_border_color: palette::BORDER_COLOR,
58 section_bg: palette::WHALE_BG,
59 section_title_color: palette::WHALE_ACTION,
60 // Horizontal padding only. `Padding::uniform(1)` ate two rows of
61 // each sidebar panel — for compact terminals where Work/Tasks/Agents
62 // get ~3 rows total via the 25% layout split, that left zero rows
63 // for content (#63 follow-up: panels rendered as empty boxes even
64 // when "No todos" / "No active plan" should have shown).
65 section_padding: Padding::horizontal(1),
66 tool_title_color: palette::TEXT_SOFT,
67 tool_value_color: palette::TEXT_MUTED,
68 tool_label_color: palette::TEXT_DIM,
69 tool_running_accent: palette::ACCENT_TOOL_LIVE,
70 tool_success_accent: palette::STATUS_SUCCESS,
71 tool_failed_accent: palette::STATUS_ERROR,
72 }
73 }
74
75 /// Light theme tokens for sidebar and tool chrome.
76 #[must_use]
77 pub const fn light() -> Self {
78 Self {
79 variant: Variant::Light,
80 section_borders: Borders::ALL,
81 section_border_type: BorderType::Plain,
82 section_border_color: palette::LIGHT_BORDER,
83 section_bg: palette::LIGHT_PANEL,
84 section_title_color: palette::LIGHT_ACTION,
85 section_padding: Padding::horizontal(1),
86 tool_title_color: palette::LIGHT_TEXT_SOFT,
87 tool_value_color: palette::LIGHT_TEXT_MUTED,
88 tool_label_color: palette::LIGHT_TEXT_HINT,
89 tool_running_accent: palette::LIGHT_LIVE,
90 tool_success_accent: palette::LIGHT_SUCCESS_FG,
91 tool_failed_accent: palette::LIGHT_DANGER,
92 }
93 }
94
95 /// Solarized Light theme tokens — warm ivory tones, high contrast.
96 #[must_use]
97 pub const fn solarized_light() -> Self {
98 Self {
99 variant: Variant::Light,
100 section_borders: Borders::ALL,
101 section_border_type: BorderType::Plain,
102 section_border_color: palette::SOLARIZED_BORDER,
103 section_bg: palette::SOLARIZED_PANEL,
104 section_title_color: palette::SOLARIZED_BLUE,
105 section_padding: Padding::horizontal(1),
106 tool_title_color: palette::SOLARIZED_TEXT_SOFT,
107 tool_value_color: palette::SOLARIZED_TEXT_MUTED,
108 tool_label_color: palette::SOLARIZED_TEXT_DIM,
109 tool_running_accent: palette::SOLARIZED_BLUE,
110 tool_success_accent: palette::SOLARIZED_CYAN,
111 tool_failed_accent: palette::SOLARIZED_RED,
112 }
113 }
114
115 /// Neutral black/white tokens for users who want minimal brand color.
116 #[must_use]
117 pub const fn grayscale() -> Self {
118 Self {
119 variant: Variant::Grayscale,
120 section_borders: Borders::ALL,
121 section_border_type: BorderType::Plain,
122 section_border_color: palette::GRAYSCALE_BORDER,
123 section_bg: palette::GRAYSCALE_PANEL,
124 section_title_color: palette::GRAYSCALE_TEXT_SOFT,
125 section_padding: Padding::horizontal(1),
126 tool_title_color: palette::GRAYSCALE_TEXT_SOFT,
127 tool_value_color: palette::GRAYSCALE_TEXT_MUTED,
128 tool_label_color: palette::GRAYSCALE_TEXT_HINT,
129 tool_running_accent: palette::GRAYSCALE_TEXT_SOFT,
130 tool_success_accent: palette::GRAYSCALE_TEXT_HINT,
131 tool_failed_accent: palette::GRAYSCALE_TEXT_BODY,
132 }
133 }
134
135 #[must_use]
136 pub const fn for_palette_mode(mode: PaletteMode) -> Self {
137 match mode {
138 PaletteMode::Dark => Self::dark(),
139 PaletteMode::Light => Self::light(),
140 PaletteMode::Grayscale => Self::grayscale(),
141 PaletteMode::SolarizedLight => Self::solarized_light(),
142 }
143 }
144
145 /// Pick the right tool accent for a given [`ToolStatus`].
146 #[must_use]
147 pub const fn tool_status_color(self, status: ToolStatus) -> Color {
148 match status {
149 ToolStatus::Running => self.tool_running_accent,
150 ToolStatus::Success => self.tool_success_accent,
151 ToolStatus::Hydrated => self.tool_running_accent,
152 ToolStatus::Failed => self.tool_failed_accent,
153 }
154 }
155
156 /// Bold tool title style (e.g. "Plan", "Shell").
157 #[must_use]
158 pub fn tool_title_style(self) -> Style {
159 Style::default()
160 .fg(self.tool_title_color)
161 .add_modifier(Modifier::BOLD)
162 }
163
164 /// Right-side status text ("running", "done", "issue") style.
165 #[must_use]
166 pub fn tool_status_style(self, status: ToolStatus) -> Style {
167 Style::default().fg(self.tool_status_color(status))
168 }
169
170 /// Detail label style ("command:", "time:", step markers).
171 #[must_use]
172 pub fn tool_label_style(self) -> Style {
173 Style::default().fg(self.tool_label_color)
174 }
175
176 /// Default value style for tool detail rows.
177 #[must_use]
178 pub fn tool_value_style(self) -> Style {
179 Style::default().fg(self.tool_value_color)
180 }
181 }
182
183 /// Returns the active theme used by the TUI today.
184 #[must_use]
185 pub const fn active_theme() -> Theme {
186 Theme::dark()
187 }
188
189 #[cfg(test)]
190 mod tests {
191 use super::{Theme, Variant, active_theme};
192 use crate::palette;
193 use crate::tui::history::ToolStatus;
194
195 #[test]
196 fn active_theme_returns_dark() {
197 assert_eq!(active_theme(), Theme::dark());
198 }
199
200 #[test]
201 fn dark_theme_uses_codewhale_semantic_roles() {
202 let theme = Theme::dark();
203 assert_eq!(theme.variant, Variant::Dark);
204 assert_eq!(theme.section_border_color, palette::BORDER_COLOR);
205 assert_eq!(theme.section_bg, palette::WHALE_BG);
206 assert_eq!(theme.section_title_color, palette::WHALE_ACTION);
207 assert_eq!(theme.tool_title_color, palette::TEXT_SOFT);
208 assert_eq!(theme.tool_value_color, palette::TEXT_MUTED);
209 assert_eq!(theme.tool_label_color, palette::TEXT_DIM);
210 assert_eq!(theme.tool_running_accent, palette::ACCENT_TOOL_LIVE);
211 assert_eq!(theme.tool_success_accent, palette::STATUS_SUCCESS);
212 assert_eq!(theme.tool_failed_accent, palette::STATUS_ERROR);
213 }
214
215 #[test]
216 fn light_theme_uses_light_panel_tokens() {
217 let theme = Theme::for_palette_mode(crate::palette::PaletteMode::Light);
218 assert_eq!(theme.variant, Variant::Light);
219 assert_eq!(theme.section_bg, palette::LIGHT_PANEL);
220 assert_eq!(theme.section_border_color, palette::LIGHT_BORDER);
221 assert_eq!(theme.tool_title_color, palette::LIGHT_TEXT_SOFT);
222 assert_eq!(theme.tool_value_color, palette::LIGHT_TEXT_MUTED);
223 assert_eq!(theme.section_title_color, palette::LIGHT_ACTION);
224 assert_eq!(theme.tool_running_accent, palette::LIGHT_LIVE);
225 assert_eq!(theme.tool_success_accent, palette::LIGHT_SUCCESS_FG);
226 }
227
228 #[test]
229 fn grayscale_theme_uses_neutral_tokens() {
230 let theme = Theme::for_palette_mode(crate::palette::PaletteMode::Grayscale);
231 assert_eq!(theme.variant, Variant::Grayscale);
232 assert_eq!(theme.section_bg, palette::GRAYSCALE_PANEL);
233 assert_eq!(theme.section_border_color, palette::GRAYSCALE_BORDER);
234 assert_eq!(theme.tool_running_accent, palette::GRAYSCALE_TEXT_SOFT);
235 assert_eq!(theme.tool_failed_accent, palette::GRAYSCALE_TEXT_BODY);
236 }
237
238 #[test]
239 fn tool_status_color_maps_each_status() {
240 let theme = Theme::dark();
241 assert_eq!(
242 theme.tool_status_color(ToolStatus::Running),
243 theme.tool_running_accent
244 );
245 assert_eq!(
246 theme.tool_status_color(ToolStatus::Success),
247 theme.tool_success_accent
248 );
249 assert_eq!(
250 theme.tool_status_color(ToolStatus::Hydrated),
251 theme.tool_running_accent
252 );
253 assert_eq!(
254 theme.tool_status_color(ToolStatus::Failed),
255 theme.tool_failed_accent
256 );
257 }
258 }
259
259 lines RUST