From d7e6c357357d59152eb198c0d18697d5591ff397 Mon Sep 17 00:00:00 2001 From: Alex Pasmantier <47638216+alexpasmantier@users.noreply.github.com> Date: Mon, 6 Jan 2025 13:03:50 +0100 Subject: [PATCH] feat(ui): add support for standard ANSI colors theming and update default theme (#221) Fixes #211 Screenshot 2025-01-06 at 13 01 57 --- .config/config.toml | 4 +- crates/television/config/themes.rs | 208 ++++++++++++++++++++- crates/television/config/themes/builtin.rs | 1 + themes/default.toml | 20 ++ 4 files changed, 225 insertions(+), 8 deletions(-) create mode 100644 themes/default.toml diff --git a/.config/config.toml b/.config/config.toml index e937afd..9e4d7c4 100644 --- a/.config/config.toml +++ b/.config/config.toml @@ -56,7 +56,7 @@ input_bar_position = "top" # A list of builtin themes can be found in the `themes` directory of the television # repository. You may also create your own theme by creating a new file in a `themes` # directory in your configuration directory (see the `config.toml` location above). -theme = "catppuccin" +theme = "default" # Previewers settings # ---------------------------------------------------------------------------- @@ -65,7 +65,7 @@ theme = "catppuccin" # Bulitin syntax highlighting uses the same syntax highlighting engine as bat. # To get a list of your currently available themes, run `bat --list-themes` # Note that setting the BAT_THEME environment variable will override this setting. -theme = "Coldark-Dark" +theme = "TwoDark" # Keybindings # ---------------------------------------------------------------------------- diff --git a/crates/television/config/themes.rs b/crates/television/config/themes.rs index 9a7ce61..7f52f04 100644 --- a/crates/television/config/themes.rs +++ b/crates/television/config/themes.rs @@ -12,14 +12,68 @@ use super::get_config_dir; pub mod builtin; -#[derive(Clone, Debug, Default)] -pub struct Color { +#[derive(Clone, Debug, PartialEq)] +pub enum Color { + Ansi(ANSIColor), + Rgb(RGBColor), +} + +impl Color { + pub fn from_str(s: &str) -> Option { + if s.starts_with('#') { + RGBColor::from_str(s).map(Self::Rgb) + } else { + match s.to_lowercase().as_str() { + "black" => Some(Self::Ansi(ANSIColor::Black)), + "red" => Some(Self::Ansi(ANSIColor::Red)), + "green" => Some(Self::Ansi(ANSIColor::Green)), + "yellow" => Some(Self::Ansi(ANSIColor::Yellow)), + "blue" => Some(Self::Ansi(ANSIColor::Blue)), + "magenta" => Some(Self::Ansi(ANSIColor::Magenta)), + "cyan" => Some(Self::Ansi(ANSIColor::Cyan)), + "white" => Some(Self::Ansi(ANSIColor::White)), + "bright-black" => Some(Self::Ansi(ANSIColor::BrightBlack)), + "bright-red" => Some(Self::Ansi(ANSIColor::BrightRed)), + "bright-green" => Some(Self::Ansi(ANSIColor::BrightGreen)), + "bright-yellow" => Some(Self::Ansi(ANSIColor::BrightYellow)), + "bright-blue" => Some(Self::Ansi(ANSIColor::BrightBlue)), + "bright-magenta" => Some(Self::Ansi(ANSIColor::BrightMagenta)), + "bright-cyan" => Some(Self::Ansi(ANSIColor::BrightCyan)), + "bright-white" => Some(Self::Ansi(ANSIColor::BrightWhite)), + _ => None, + } + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum ANSIColor { + Black, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + White, + BrightBlack, + BrightRed, + BrightGreen, + BrightYellow, + BrightBlue, + BrightMagenta, + BrightCyan, + BrightWhite, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct RGBColor { pub r: u8, pub g: u8, pub b: u8, } -impl Color { +impl RGBColor { pub fn new(r: u8, g: u8, b: u8) -> Self { Self { r, g, b } } @@ -90,11 +144,11 @@ impl Theme { } } -pub const DEFAULT_THEME: &str = "gruvbox-dark"; +pub const DEFAULT_THEME: &str = "default"; impl Default for Theme { fn default() -> Self { - let theme_content = include_str!("../../../themes/gruvbox-dark.toml"); + let theme_content = include_str!("../../../themes/default.toml"); toml::from_str(theme_content).unwrap() } } @@ -179,12 +233,46 @@ impl<'de> Deserialize<'de> for Theme { } #[allow(clippy::from_over_into)] -impl Into for &Color { +impl Into for &RGBColor { fn into(self) -> RatatuiColor { RatatuiColor::Rgb(self.r, self.g, self.b) } } +#[allow(clippy::from_over_into)] +impl Into for &ANSIColor { + fn into(self) -> RatatuiColor { + match self { + ANSIColor::Black => RatatuiColor::Black, + ANSIColor::Red => RatatuiColor::Red, + ANSIColor::Green => RatatuiColor::Green, + ANSIColor::Yellow => RatatuiColor::Yellow, + ANSIColor::Blue => RatatuiColor::Blue, + ANSIColor::Magenta => RatatuiColor::Magenta, + ANSIColor::Cyan => RatatuiColor::Cyan, + ANSIColor::White => RatatuiColor::Gray, + ANSIColor::BrightBlack => RatatuiColor::DarkGray, + ANSIColor::BrightRed => RatatuiColor::LightRed, + ANSIColor::BrightGreen => RatatuiColor::LightGreen, + ANSIColor::BrightYellow => RatatuiColor::LightYellow, + ANSIColor::BrightBlue => RatatuiColor::LightBlue, + ANSIColor::BrightMagenta => RatatuiColor::LightMagenta, + ANSIColor::BrightCyan => RatatuiColor::LightCyan, + ANSIColor::BrightWhite => RatatuiColor::White, + } + } +} + +#[allow(clippy::from_over_into)] +impl Into for &Color { + fn into(self) -> RatatuiColor { + match self { + Color::Ansi(ansi) => ansi.into(), + Color::Rgb(rgb) => rgb.into(), + } + } +} + #[allow(clippy::from_over_into)] impl Into for &Theme { fn into(self) -> Colorscheme { @@ -265,3 +353,111 @@ impl Into for &Theme { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_theme_deserialization() { + let theme_content = r##" + background = "#000000" + border_fg = "black" + text_fg = "white" + dimmed_text_fg = "bright-black" + input_text_fg = "bright-white" + result_count_fg = "bright-white" + result_name_fg = "bright-white" + result_line_number_fg = "bright-white" + result_value_fg = "bright-white" + selection_bg = "bright-white" + match_fg = "bright-white" + preview_title_fg = "bright-white" + channel_mode_fg = "bright-white" + remote_control_mode_fg = "bright-white" + send_to_channel_mode_fg = "bright-white" + "##; + let theme: Theme = toml::from_str(theme_content).unwrap(); + assert_eq!( + theme.background, + Some(Color::Rgb(RGBColor::from_str("000000").unwrap())) + ); + assert_eq!(theme.border_fg, Color::Ansi(ANSIColor::Black)); + assert_eq!(theme.text_fg, Color::Ansi(ANSIColor::White)); + assert_eq!(theme.dimmed_text_fg, Color::Ansi(ANSIColor::BrightBlack)); + assert_eq!(theme.input_text_fg, Color::Ansi(ANSIColor::BrightWhite)); + assert_eq!(theme.result_count_fg, Color::Ansi(ANSIColor::BrightWhite)); + assert_eq!(theme.result_name_fg, Color::Ansi(ANSIColor::BrightWhite)); + assert_eq!( + theme.result_line_number_fg, + Color::Ansi(ANSIColor::BrightWhite) + ); + assert_eq!(theme.result_value_fg, Color::Ansi(ANSIColor::BrightWhite)); + assert_eq!(theme.selection_bg, Color::Ansi(ANSIColor::BrightWhite)); + assert_eq!(theme.match_fg, Color::Ansi(ANSIColor::BrightWhite)); + assert_eq!( + theme.preview_title_fg, + Color::Ansi(ANSIColor::BrightWhite) + ); + assert_eq!(theme.channel_mode_fg, Color::Ansi(ANSIColor::BrightWhite)); + assert_eq!( + theme.remote_control_mode_fg, + Color::Ansi(ANSIColor::BrightWhite) + ); + assert_eq!( + theme.send_to_channel_mode_fg, + Color::Ansi(ANSIColor::BrightWhite) + ); + } + + #[test] + fn test_theme_deserialization_no_background() { + let theme_content = r##" + border_fg = "black" + text_fg = "white" + dimmed_text_fg = "bright-black" + input_text_fg = "bright-white" + result_count_fg = "#ffffff" + result_name_fg = "bright-white" + result_line_number_fg = "#ffffff" + result_value_fg = "bright-white" + selection_bg = "bright-white" + match_fg = "bright-white" + preview_title_fg = "bright-white" + channel_mode_fg = "bright-white" + remote_control_mode_fg = "bright-white" + send_to_channel_mode_fg = "bright-white" + "##; + let theme: Theme = toml::from_str(theme_content).unwrap(); + assert_eq!(theme.background, None); + assert_eq!(theme.border_fg, Color::Ansi(ANSIColor::Black)); + assert_eq!(theme.text_fg, Color::Ansi(ANSIColor::White)); + assert_eq!(theme.dimmed_text_fg, Color::Ansi(ANSIColor::BrightBlack)); + assert_eq!(theme.input_text_fg, Color::Ansi(ANSIColor::BrightWhite)); + assert_eq!( + theme.result_count_fg, + Color::Rgb(RGBColor::from_str("ffffff").unwrap()) + ); + assert_eq!(theme.result_name_fg, Color::Ansi(ANSIColor::BrightWhite)); + assert_eq!( + theme.result_line_number_fg, + Color::Rgb(RGBColor::from_str("ffffff").unwrap()) + ); + assert_eq!(theme.result_value_fg, Color::Ansi(ANSIColor::BrightWhite)); + assert_eq!(theme.selection_bg, Color::Ansi(ANSIColor::BrightWhite)); + assert_eq!(theme.match_fg, Color::Ansi(ANSIColor::BrightWhite)); + assert_eq!( + theme.preview_title_fg, + Color::Ansi(ANSIColor::BrightWhite) + ); + assert_eq!(theme.channel_mode_fg, Color::Ansi(ANSIColor::BrightWhite)); + assert_eq!( + theme.remote_control_mode_fg, + Color::Ansi(ANSIColor::BrightWhite) + ); + assert_eq!( + theme.send_to_channel_mode_fg, + Color::Ansi(ANSIColor::BrightWhite) + ); + } +} diff --git a/crates/television/config/themes/builtin.rs b/crates/television/config/themes/builtin.rs index 1287272..afca7f0 100644 --- a/crates/television/config/themes/builtin.rs +++ b/crates/television/config/themes/builtin.rs @@ -5,6 +5,7 @@ use lazy_static::lazy_static; lazy_static! { pub static ref BUILTIN_THEMES: HashMap<&'static str, &'static str> = { let mut m = HashMap::new(); + m.insert("default", include_str!("../../../../themes/default.toml")); m.insert( "television", include_str!("../../../../themes/television.toml"), diff --git a/themes/default.toml b/themes/default.toml new file mode 100644 index 0000000..edae0d0 --- /dev/null +++ b/themes/default.toml @@ -0,0 +1,20 @@ +# general +# background = 'black' +border_fg = 'bright-black' +text_fg = 'bright-blue' +dimmed_text_fg = 'white' +# input +input_text_fg = 'bright-red' +result_count_fg = 'bright-red' +# results +result_name_fg = 'bright-blue' +result_line_number_fg = 'bright-yellow' +result_value_fg = 'white' +selection_bg = 'bright-black' +match_fg = 'bright-red' +# preview +preview_title_fg = 'bright-magenta' +# modes +channel_mode_fg = 'green' +remote_control_mode_fg = 'yellow' +send_to_channel_mode_fg = 'cyan'