use ratatui::style::{Color, Modifier, Style};
use scope::display::theme::{
AnsiColor, Direction, Role, SERIES_TONES, Scheme, Tone, scheme_role_tone, scheme_tone, tone,
};
const fn color(c: AnsiColor) -> Color {
match c {
AnsiColor::Black => Color::Black,
AnsiColor::Red => Color::Red,
AnsiColor::Green => Color::Green,
AnsiColor::Yellow => Color::Yellow,
AnsiColor::Blue => Color::Blue,
AnsiColor::Magenta => Color::Magenta,
AnsiColor::Cyan => Color::Cyan,
AnsiColor::White => Color::Gray,
AnsiColor::BrightBlack => Color::DarkGray,
AnsiColor::BrightRed => Color::LightRed,
AnsiColor::BrightGreen => Color::LightGreen,
AnsiColor::BrightYellow => Color::LightYellow,
AnsiColor::BrightBlue => Color::LightBlue,
AnsiColor::BrightMagenta => Color::LightMagenta,
AnsiColor::BrightCyan => Color::LightCyan,
AnsiColor::BrightWhite => Color::White,
}
}
pub const fn tone_color(t: Tone) -> Color {
match t.color {
Some(c) => color(c),
None => Color::Reset,
}
}
pub const DEFAULT_COLOR: Color = tone_color(Tone::PLAIN);
pub const fn tone_style(t: Tone) -> Style {
let mut s = Style::new();
if let Some(c) = t.color {
s = s.fg(color(c));
}
if t.bold {
s = s.add_modifier(Modifier::BOLD);
}
if t.dim {
s = s.add_modifier(Modifier::DIM);
}
if t.underline {
s = s.add_modifier(Modifier::UNDERLINED);
}
if t.reverse {
s = s.add_modifier(Modifier::REVERSED);
}
s
}
pub const fn style(role: Role) -> Style {
tone_style(tone(role))
}
pub const fn scheme_style(scheme: Scheme, direction: Direction) -> Style {
tone_style(scheme_tone(scheme, direction))
}
pub const ACCENT: Style = style(Role::Accent);
pub const KEY: Style = style(Role::Key);
pub const OK: Style = style(Role::Ok);
pub const WARN: Style = style(Role::Warn);
pub const ERR: Style = style(Role::Err);
pub const INFO: Style = style(Role::Info);
pub const SECONDARY: Style = style(Role::Secondary);
pub const SELECTED: Style = style(Role::Selected);
pub const MATCH: Style = style(Role::Match);
pub const LINK: Style = style(Role::Link);
pub const fn series_style(i: usize) -> Style {
tone_style(SERIES_TONES[i % SERIES_TONES.len()])
}
pub const fn strong(role: Role) -> Style {
tone_style(tone(role).strong())
}
pub trait ColorSchemeExt {
fn palette(&self) -> ColorPalette;
}
impl ColorSchemeExt for Scheme {
fn palette(&self) -> ColorPalette {
let scheme = *self;
let role = |r| tone_color(scheme_role_tone(scheme, r));
ColorPalette {
up: tone_color(scheme_tone(scheme, Direction::Up)),
down: tone_color(scheme_tone(scheme, Direction::Down)),
neutral: tone_color(Tone::PLAIN),
header_fg: tone_color(Tone::PLAIN),
highlight: role(Role::Warn),
volume_bar: role(Role::Info),
sparkline: role(Role::Accent),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ColorPalette {
pub up: Color,
pub down: Color,
pub neutral: Color,
pub header_fg: Color,
pub highlight: Color,
pub volume_bar: Color,
pub sparkline: Color,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PaneState {
Idle,
Running,
Failed,
}
impl PaneState {
pub fn word(self) -> &'static str {
match self {
Self::Idle => "idle",
Self::Running => "running",
Self::Failed => "failed",
}
}
pub fn style(self) -> Style {
match self {
Self::Idle => ACCENT,
Self::Running => WARN,
Self::Failed => ERR,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_secondary_is_dim_on_the_default_foreground() {
assert_eq!(SECONDARY.fg, None);
assert!(SECONDARY.add_modifier.contains(Modifier::DIM));
}
#[test]
fn test_only_named_ansi_colors_are_used() {
for style in [ACCENT, KEY, OK, WARN, ERR, INFO, SELECTED, MATCH] {
assert!(matches!(
style.fg,
Some(
Color::Cyan
| Color::Magenta
| Color::Green
| Color::Yellow
| Color::Red
| Color::Blue
)
));
}
}
#[test]
fn test_pane_state_maps_to_a_rule_color_and_a_word() {
assert_eq!(PaneState::Idle.style(), ACCENT);
assert_eq!(PaneState::Running.style(), WARN);
assert_eq!(PaneState::Failed.style(), ERR);
assert_eq!(PaneState::Idle.word(), "idle");
assert_eq!(PaneState::Running.word(), "running");
assert_eq!(PaneState::Failed.word(), "failed");
}
#[test]
fn test_selected_and_match_are_reversed_cyan_and_yellow() {
assert_eq!(SELECTED.fg, Some(Color::Cyan));
assert!(SELECTED.add_modifier.contains(Modifier::REVERSED));
assert_eq!(MATCH.fg, Some(Color::Yellow));
assert!(MATCH.add_modifier.contains(Modifier::REVERSED));
}
fn parsed(role: Role) -> Style {
use ansi_to_tui::IntoText;
let painted = scope::display::theme::Painter::new(true).paint(role, "x");
let text = painted.into_text().unwrap();
text.lines[0].spans[0].style
}
#[test]
fn test_painted_text_parses_back_to_the_same_style() {
for role in [
Role::Key,
Role::Secondary,
Role::Accent,
Role::Ok,
Role::Err,
] {
assert_eq!(parsed(role), style(role), "{role:?}");
}
}
#[test]
fn test_scheme_styles_follow_the_core_scheme_tones() {
assert_eq!(
scheme_style(Scheme::GreenRed, Direction::Up).fg,
Some(Color::Green)
);
assert_eq!(
scheme_style(Scheme::BlueOrange, Direction::Down).fg,
Some(Color::Yellow)
);
assert_eq!(
scheme_style(Scheme::Monochrome, Direction::Up),
Style::new()
);
}
#[test]
fn test_series_styles_are_no_health_color_and_repeat_after_four() {
let health = [OK.fg, WARN.fg, ERR.fg];
for i in 0..SERIES_TONES.len() {
let fg = series_style(i).fg;
assert!(!health.contains(&fg), "series {i}: {fg:?}");
assert!(
matches!(fg, None | Some(Color::Blue | Color::Magenta | Color::Cyan)),
"series {i}: {fg:?}"
);
}
assert_eq!(series_style(4), series_style(0));
let distinct: std::collections::HashSet<_> = (0..4).map(|i| series_style(i).fg).collect();
assert_eq!(distinct.len(), 4);
}
#[test]
fn test_key_is_bold_magenta() {
assert_eq!(KEY.fg, Some(Color::Magenta));
assert!(KEY.add_modifier.contains(Modifier::BOLD));
}
}