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
---
.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'