1use ratatui::style::{Color, Modifier, Style};
13use scope::display::theme::{
14 AnsiColor, Direction, Role, 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 strong(role: Role) -> Style {
115 tone_style(tone(role).strong())
116}
117
118pub trait ColorSchemeExt {
124 fn palette(&self) -> ColorPalette;
125}
126
127impl ColorSchemeExt for Scheme {
128 fn palette(&self) -> ColorPalette {
132 let scheme = *self;
133 let role = |r| tone_color(scheme_role_tone(scheme, r));
134 ColorPalette {
135 up: tone_color(scheme_tone(scheme, Direction::Up)),
136 down: tone_color(scheme_tone(scheme, Direction::Down)),
137 neutral: tone_color(Tone::PLAIN),
138 header_fg: tone_color(Tone::PLAIN),
139 highlight: role(Role::Warn),
140 volume_bar: role(Role::Info),
141 sparkline: role(Role::Accent),
142 }
143 }
144}
145
146#[derive(Debug, Clone, Copy)]
148pub struct ColorPalette {
149 pub up: Color,
151 pub down: Color,
153 pub neutral: Color,
155 pub header_fg: Color,
157 pub highlight: Color,
159 pub volume_bar: Color,
161 pub sparkline: Color,
163}
164
165#[derive(Clone, Copy, Debug, PartialEq, Eq)]
167pub enum PaneState {
168 Idle,
170 Running,
172 Failed,
174}
175
176impl PaneState {
177 pub fn word(self) -> &'static str {
179 match self {
180 Self::Idle => "idle",
181 Self::Running => "running",
182 Self::Failed => "failed",
183 }
184 }
185
186 pub fn style(self) -> Style {
188 match self {
189 Self::Idle => ACCENT,
190 Self::Running => WARN,
191 Self::Failed => ERR,
192 }
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_secondary_is_dim_on_the_default_foreground() {
202 assert_eq!(SECONDARY.fg, None);
203 assert!(SECONDARY.add_modifier.contains(Modifier::DIM));
204 }
205
206 #[test]
207 fn test_only_named_ansi_colors_are_used() {
208 for style in [ACCENT, KEY, OK, WARN, ERR, INFO, SELECTED, MATCH] {
210 assert!(matches!(
211 style.fg,
212 Some(
213 Color::Cyan
214 | Color::Magenta
215 | Color::Green
216 | Color::Yellow
217 | Color::Red
218 | Color::Blue
219 )
220 ));
221 }
222 }
223
224 #[test]
225 fn test_pane_state_maps_to_a_rule_color_and_a_word() {
226 assert_eq!(PaneState::Idle.style(), ACCENT);
227 assert_eq!(PaneState::Running.style(), WARN);
228 assert_eq!(PaneState::Failed.style(), ERR);
229 assert_eq!(PaneState::Idle.word(), "idle");
230 assert_eq!(PaneState::Running.word(), "running");
231 assert_eq!(PaneState::Failed.word(), "failed");
232 }
233
234 #[test]
235 fn test_selected_and_match_are_reversed_cyan_and_yellow() {
236 assert_eq!(SELECTED.fg, Some(Color::Cyan));
237 assert!(SELECTED.add_modifier.contains(Modifier::REVERSED));
238 assert_eq!(MATCH.fg, Some(Color::Yellow));
239 assert!(MATCH.add_modifier.contains(Modifier::REVERSED));
240 }
241
242 fn parsed(role: Role) -> Style {
245 use ansi_to_tui::IntoText;
246 let painted = scope::display::theme::Painter::new(true).paint(role, "x");
247 let text = painted.into_text().unwrap();
248 text.lines[0].spans[0].style
249 }
250
251 #[test]
252 fn test_painted_text_parses_back_to_the_same_style() {
253 for role in [
256 Role::Key,
257 Role::Secondary,
258 Role::Accent,
259 Role::Ok,
260 Role::Err,
261 ] {
262 assert_eq!(parsed(role), style(role), "{role:?}");
263 }
264 }
265
266 #[test]
267 fn test_scheme_styles_follow_the_core_scheme_tones() {
268 assert_eq!(
269 scheme_style(Scheme::GreenRed, Direction::Up).fg,
270 Some(Color::Green)
271 );
272 assert_eq!(
273 scheme_style(Scheme::BlueOrange, Direction::Down).fg,
274 Some(Color::Yellow)
275 );
276 assert_eq!(
277 scheme_style(Scheme::Monochrome, Direction::Up),
278 Style::new()
279 );
280 }
281
282 #[test]
283 fn test_key_is_bold_magenta() {
284 assert_eq!(KEY.fg, Some(Color::Magenta));
285 assert!(KEY.add_modifier.contains(Modifier::BOLD));
286 }
287}