1use ratatui::style::{Color, Modifier, Style};
13use scope::display::theme::{
14 AnsiColor, Direction, Role, SERIES_TONES, Scheme, Tone, scheme_role_tone, scheme_tone, tone,
15};
16
17const fn color(c: AnsiColor) -> Color {
19 match c {
20 AnsiColor::Black => Color::Black,
21 AnsiColor::Red => Color::Red,
22 AnsiColor::Green => Color::Green,
23 AnsiColor::Yellow => Color::Yellow,
24 AnsiColor::Blue => Color::Blue,
25 AnsiColor::Magenta => Color::Magenta,
26 AnsiColor::Cyan => Color::Cyan,
27 AnsiColor::White => Color::Gray,
28 AnsiColor::BrightBlack => Color::DarkGray,
29 AnsiColor::BrightRed => Color::LightRed,
30 AnsiColor::BrightGreen => Color::LightGreen,
31 AnsiColor::BrightYellow => Color::LightYellow,
32 AnsiColor::BrightBlue => Color::LightBlue,
33 AnsiColor::BrightMagenta => Color::LightMagenta,
34 AnsiColor::BrightCyan => Color::LightCyan,
35 AnsiColor::BrightWhite => Color::White,
36 }
37}
38
39pub const fn tone_color(t: Tone) -> Color {
42 match t.color {
43 Some(c) => color(c),
44 None => Color::Reset,
45 }
46}
47
48pub const DEFAULT_COLOR: Color = tone_color(Tone::PLAIN);
50
51pub const fn tone_style(t: Tone) -> Style {
53 let mut s = Style::new();
54 if let Some(c) = t.color {
55 s = s.fg(color(c));
56 }
57 if t.bold {
58 s = s.add_modifier(Modifier::BOLD);
59 }
60 if t.dim {
61 s = s.add_modifier(Modifier::DIM);
62 }
63 if t.underline {
64 s = s.add_modifier(Modifier::UNDERLINED);
65 }
66 if t.reverse {
67 s = s.add_modifier(Modifier::REVERSED);
68 }
69 s
70}
71
72pub const fn style(role: Role) -> Style {
74 tone_style(tone(role))
75}
76
77pub const fn scheme_style(scheme: Scheme, direction: Direction) -> Style {
79 tone_style(scheme_tone(scheme, direction))
80}
81
82pub const ACCENT: Style = style(Role::Accent);
84
85pub const KEY: Style = style(Role::Key);
87
88pub const OK: Style = style(Role::Ok);
90
91pub const WARN: Style = style(Role::Warn);
93
94pub const ERR: Style = style(Role::Err);
96
97pub const INFO: Style = style(Role::Info);
99
100pub const SECONDARY: Style = style(Role::Secondary);
103
104pub const SELECTED: Style = style(Role::Selected);
106
107pub const MATCH: Style = style(Role::Match);
109
110pub const LINK: Style = style(Role::Link);
112
113pub const fn series_style(i: usize) -> Style {
116 tone_style(SERIES_TONES[i % SERIES_TONES.len()])
117}
118
119pub const fn strong(role: Role) -> Style {
121 tone_style(tone(role).strong())
122}
123
124pub trait ColorSchemeExt {
130 fn palette(&self) -> ColorPalette;
131}
132
133impl ColorSchemeExt for Scheme {
134 fn palette(&self) -> ColorPalette {
138 let scheme = *self;
139 let role = |r| tone_color(scheme_role_tone(scheme, r));
140 ColorPalette {
141 up: tone_color(scheme_tone(scheme, Direction::Up)),
142 down: tone_color(scheme_tone(scheme, Direction::Down)),
143 neutral: tone_color(Tone::PLAIN),
144 header_fg: tone_color(Tone::PLAIN),
145 highlight: role(Role::Warn),
146 volume_bar: role(Role::Info),
147 sparkline: role(Role::Accent),
148 }
149 }
150}
151
152#[derive(Debug, Clone, Copy)]
154pub struct ColorPalette {
155 pub up: Color,
157 pub down: Color,
159 pub neutral: Color,
161 pub header_fg: Color,
163 pub highlight: Color,
165 pub volume_bar: Color,
167 pub sparkline: Color,
169}
170
171#[derive(Clone, Copy, Debug, PartialEq, Eq)]
173pub enum PaneState {
174 Idle,
176 Running,
178 Failed,
180}
181
182impl PaneState {
183 pub fn word(self) -> &'static str {
185 match self {
186 Self::Idle => "idle",
187 Self::Running => "running",
188 Self::Failed => "failed",
189 }
190 }
191
192 pub fn style(self) -> Style {
194 match self {
195 Self::Idle => ACCENT,
196 Self::Running => WARN,
197 Self::Failed => ERR,
198 }
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_secondary_is_dim_on_the_default_foreground() {
208 assert_eq!(SECONDARY.fg, None);
209 assert!(SECONDARY.add_modifier.contains(Modifier::DIM));
210 }
211
212 #[test]
213 fn test_only_named_ansi_colors_are_used() {
214 for style in [ACCENT, KEY, OK, WARN, ERR, INFO, SELECTED, MATCH] {
216 assert!(matches!(
217 style.fg,
218 Some(
219 Color::Cyan
220 | Color::Magenta
221 | Color::Green
222 | Color::Yellow
223 | Color::Red
224 | Color::Blue
225 )
226 ));
227 }
228 }
229
230 #[test]
231 fn test_pane_state_maps_to_a_rule_color_and_a_word() {
232 assert_eq!(PaneState::Idle.style(), ACCENT);
233 assert_eq!(PaneState::Running.style(), WARN);
234 assert_eq!(PaneState::Failed.style(), ERR);
235 assert_eq!(PaneState::Idle.word(), "idle");
236 assert_eq!(PaneState::Running.word(), "running");
237 assert_eq!(PaneState::Failed.word(), "failed");
238 }
239
240 #[test]
241 fn test_selected_and_match_are_reversed_cyan_and_yellow() {
242 assert_eq!(SELECTED.fg, Some(Color::Cyan));
243 assert!(SELECTED.add_modifier.contains(Modifier::REVERSED));
244 assert_eq!(MATCH.fg, Some(Color::Yellow));
245 assert!(MATCH.add_modifier.contains(Modifier::REVERSED));
246 }
247
248 fn parsed(role: Role) -> Style {
251 use ansi_to_tui::IntoText;
252 let painted = scope::display::theme::Painter::new(true).paint(role, "x");
253 let text = painted.into_text().unwrap();
254 text.lines[0].spans[0].style
255 }
256
257 #[test]
258 fn test_painted_text_parses_back_to_the_same_style() {
259 for role in [
262 Role::Key,
263 Role::Secondary,
264 Role::Accent,
265 Role::Ok,
266 Role::Err,
267 ] {
268 assert_eq!(parsed(role), style(role), "{role:?}");
269 }
270 }
271
272 #[test]
273 fn test_scheme_styles_follow_the_core_scheme_tones() {
274 assert_eq!(
275 scheme_style(Scheme::GreenRed, Direction::Up).fg,
276 Some(Color::Green)
277 );
278 assert_eq!(
279 scheme_style(Scheme::BlueOrange, Direction::Down).fg,
280 Some(Color::Yellow)
281 );
282 assert_eq!(
283 scheme_style(Scheme::Monochrome, Direction::Up),
284 Style::new()
285 );
286 }
287
288 #[test]
291 fn test_series_styles_are_no_health_color_and_repeat_after_four() {
292 let health = [OK.fg, WARN.fg, ERR.fg];
293 for i in 0..SERIES_TONES.len() {
294 let fg = series_style(i).fg;
295 assert!(!health.contains(&fg), "series {i}: {fg:?}");
296 assert!(
297 matches!(fg, None | Some(Color::Blue | Color::Magenta | Color::Cyan)),
298 "series {i}: {fg:?}"
299 );
300 }
301 assert_eq!(series_style(4), series_style(0));
302 let distinct: std::collections::HashSet<_> = (0..4).map(|i| series_style(i).fg).collect();
303 assert_eq!(distinct.len(), 4);
304 }
305
306 #[test]
307 fn test_key_is_bold_magenta() {
308 assert_eq!(KEY.fg, Some(Color::Magenta));
309 assert!(KEY.add_modifier.contains(Modifier::BOLD));
310 }
311}