mirror of
https://github.com/alexpasmantier/television.git
synced 2025-07-29 06:11:37 +00:00
feat(themes): allow users to add theme overrides using their config file
Fixes #560 ### Add theme color overrides in configuration file Link to #560 ### Problem Currently, customizing television's appearance to match personal style requires users to rebuild from source code to modify theme colors. This creates a significant barrier for users who want to personalize their experience without dealing with compilation and development setup. Users who want to adjust colors for better readability, match their terminal theme, or create custom color schemes must either: - Modify theme files in the source code - Rebuild the entire application - Maintain separate theme files This workflow is not user-friendly and prevents quick experimentation with different color combinations. ### Solution Added theme color overrides directly in the configuration file, allowing users to customize any theme color without rebuilding from source. The themes/*.toml files now serve as defaults, while the configuration file provides overrides that take precedence. ### Key features: - No rebuild required: Customize colors instantly through config - Easy testing: Try different color combinations without compilation - Theme flexibility: Mix and match colors from different themes - Backward compatible: Existing configs continue to work unchanged - User-friendly: Simple TOML syntax for color customization ### Usage Example Find your config file location: ``` Default locations: # Unix: ~/.config/television/config.toml # Windows: %APPDATA%\television\config.toml ``` Add theme overrides to your config: ``` [ui] theme = "catppuccin" # or any other theme [ui.theme_overrides] background = "#000000" # Pure black background for maximum contrast text_fg = "#00ff00" # Bright green for main text (high visibility) selection_bg = "#ff00ff" # Bright magenta for selected items (eye-catching) selection_fg = "#ffffff" # Pure white text on selected items (high contrast) match_fg = "#ffff00" # Bright yellow for search matches (stands out) border_fg = "#00ffff" # Bright cyan for borders (neon effect) result_name_fg = "#ff8800" # Bright orange for file names (warm highlight) result_value_fg = "#ff0080" # Bright pink for values (vibrant accent) ``` Test your changes: ``` # Run with your config ./target/release/tv --config-file ~/.config/television/config.toml ``` <img width="1420" alt="Screenshot 2025-07-08 at 08 14 42" src="https://github.com/user-attachments/assets/90563f86-74aa-460a-a7c2-82080be3dc0b" /> --------- Co-authored-by: Mohamed-Amine Bousahih <mohamed-aminebousahih@pc-66.home> Co-authored-by: Mohamed-Amine Bousahih <mohamed-aminebousahih@mohamedminesair.home>
This commit is contained in:
parent
23cf584f67
commit
4e90a357e8
@ -63,6 +63,20 @@ orientation = "landscape"
|
||||
# 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 = "default"
|
||||
|
||||
# Theme color overrides
|
||||
# ---------------------
|
||||
# You can override specific colors from the selected theme by adding them here.
|
||||
# This allows you to customize the appearance without creating a full theme file.
|
||||
# Colors can be specified as ANSI color names (e.g., "red", "bright-blue") or
|
||||
# as hex values (e.g., "#ff0000", "#1e1e2e").
|
||||
#
|
||||
# Example overrides:
|
||||
# [ui.theme_overrides]
|
||||
# background = "#000000"
|
||||
# text_fg = "#ffffff"
|
||||
# selection_bg = "#444444"
|
||||
# match_fg = "#ff0000"
|
||||
# The default size of the preview panel (in percentage of the screen)
|
||||
preview_size = 50
|
||||
|
||||
|
2
.github/workflows/test-deploy-docs.yml
vendored
2
.github/workflows/test-deploy-docs.yml
vendored
@ -26,4 +26,4 @@ jobs:
|
||||
working-directory: website
|
||||
- name: Build website
|
||||
run: pnpm run build
|
||||
working-directory: website
|
||||
working-directory: website
|
@ -6,6 +6,8 @@ Builtin themes are available in the [themes](https://github.com/alexpasmantier/t
|
||||
| :-----------------------------------------------------------------------------------: | :---------------------------------------------------------: |
|
||||
|  **solarized-dark** |  **nord** |
|
||||
|
||||
## Custom Themes
|
||||
|
||||
You may create your own custom themes by adding them to the `themes` directory in your configuration folder and then referring to them by file name (without the extension) in the configuration file.
|
||||
|
||||
```
|
||||
@ -43,3 +45,30 @@ remote_control_mode_bg = '#a6e3a1'
|
||||
send_to_channel_mode_fg = '#89dceb'
|
||||
```
|
||||
|
||||
## Theme Color Overrides
|
||||
|
||||
Override specific colors from any theme directly in your configuration:
|
||||
|
||||
```toml
|
||||
[ui]
|
||||
theme = "gruvbox-dark"
|
||||
|
||||
[ui.theme_overrides]
|
||||
background = "#000000"
|
||||
text_fg = "#ffffff"
|
||||
selection_bg = "#444444"
|
||||
match_fg = "#ff0000"
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
**General:** `background`, `border_fg`, `text_fg`, `dimmed_text_fg`
|
||||
**Input:** `input_text_fg`, `result_count_fg`
|
||||
**Results:** `result_name_fg`, `result_line_number_fg`, `result_value_fg`, `selection_bg`, `selection_fg`, `match_fg`
|
||||
**Preview:** `preview_title_fg`
|
||||
**Modes:** `channel_mode_fg`, `channel_mode_bg`, `remote_control_mode_fg`, `remote_control_mode_bg`
|
||||
|
||||
### Colors
|
||||
|
||||
ANSI: `"red"`, `"bright-blue"`, `"white"`
|
||||
Hex: `"#ff0000"`, `"#1e1e2e"`
|
||||
|
@ -144,6 +144,71 @@ impl Theme {
|
||||
let theme: Theme = toml::from_str(&theme)?;
|
||||
Ok(theme)
|
||||
}
|
||||
|
||||
/// Merge this theme with color overrides from the configuration
|
||||
pub fn merge_with_overrides(
|
||||
&self,
|
||||
overrides: &crate::config::ui::ThemeOverrides,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let mut merged_theme = self.clone();
|
||||
|
||||
macro_rules! apply_override {
|
||||
($field:ident, $override_field:expr) => {
|
||||
if let Some(ref color_str) = $override_field {
|
||||
merged_theme.$field = Color::from_str(color_str)
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"invalid {} color: {}",
|
||||
stringify!($field),
|
||||
color_str
|
||||
)
|
||||
})?;
|
||||
}
|
||||
};
|
||||
(opt $field:ident, $override_field:expr) => {
|
||||
if let Some(ref color_str) = $override_field {
|
||||
merged_theme.$field =
|
||||
Some(Color::from_str(color_str).ok_or_else(|| {
|
||||
format!(
|
||||
"invalid {} color: {}",
|
||||
stringify!($field),
|
||||
color_str
|
||||
)
|
||||
})?);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Apply overrides using the macro
|
||||
apply_override!(opt background, overrides.background);
|
||||
apply_override!(border_fg, overrides.border_fg);
|
||||
apply_override!(text_fg, overrides.text_fg);
|
||||
apply_override!(dimmed_text_fg, overrides.dimmed_text_fg);
|
||||
apply_override!(input_text_fg, overrides.input_text_fg);
|
||||
apply_override!(result_count_fg, overrides.result_count_fg);
|
||||
apply_override!(result_name_fg, overrides.result_name_fg);
|
||||
apply_override!(
|
||||
result_line_number_fg,
|
||||
overrides.result_line_number_fg
|
||||
);
|
||||
apply_override!(result_value_fg, overrides.result_value_fg);
|
||||
apply_override!(selection_bg, overrides.selection_bg);
|
||||
apply_override!(selection_fg, overrides.selection_fg);
|
||||
apply_override!(match_fg, overrides.match_fg);
|
||||
apply_override!(preview_title_fg, overrides.preview_title_fg);
|
||||
apply_override!(channel_mode_fg, overrides.channel_mode_fg);
|
||||
apply_override!(channel_mode_bg, overrides.channel_mode_bg);
|
||||
apply_override!(
|
||||
remote_control_mode_fg,
|
||||
overrides.remote_control_mode_fg
|
||||
);
|
||||
apply_override!(
|
||||
remote_control_mode_bg,
|
||||
overrides.remote_control_mode_bg
|
||||
);
|
||||
|
||||
Ok(merged_theme)
|
||||
}
|
||||
}
|
||||
|
||||
pub const DEFAULT_THEME: &str = "default";
|
||||
@ -461,6 +526,28 @@ impl Into<ModeColorscheme> for &Theme {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_test_theme() -> Theme {
|
||||
Theme {
|
||||
background: Some(Color::Ansi(ANSIColor::Black)),
|
||||
border_fg: Color::Ansi(ANSIColor::White),
|
||||
text_fg: Color::Ansi(ANSIColor::BrightWhite),
|
||||
dimmed_text_fg: Color::Ansi(ANSIColor::BrightBlack),
|
||||
input_text_fg: Color::Ansi(ANSIColor::BrightWhite),
|
||||
result_count_fg: Color::Ansi(ANSIColor::BrightWhite),
|
||||
result_name_fg: Color::Ansi(ANSIColor::BrightWhite),
|
||||
result_line_number_fg: Color::Ansi(ANSIColor::BrightWhite),
|
||||
result_value_fg: Color::Ansi(ANSIColor::BrightWhite),
|
||||
selection_bg: Color::Ansi(ANSIColor::BrightWhite),
|
||||
selection_fg: Color::Ansi(ANSIColor::BrightWhite),
|
||||
match_fg: Color::Ansi(ANSIColor::BrightWhite),
|
||||
preview_title_fg: Color::Ansi(ANSIColor::BrightWhite),
|
||||
channel_mode_fg: Color::Ansi(ANSIColor::BrightWhite),
|
||||
channel_mode_bg: Color::Ansi(ANSIColor::BrightBlack),
|
||||
remote_control_mode_fg: Color::Ansi(ANSIColor::BrightWhite),
|
||||
remote_control_mode_bg: Color::Ansi(ANSIColor::BrightBlack),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_theme_deserialization() {
|
||||
let theme_content = r##"
|
||||
@ -562,6 +649,94 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_theme_merge_with_overrides() {
|
||||
let base_theme = create_test_theme();
|
||||
let overrides = crate::config::ui::ThemeOverrides {
|
||||
background: Some("#ff0000".to_string()),
|
||||
text_fg: Some("red".to_string()),
|
||||
selection_bg: Some("#00ff00".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let merged_theme =
|
||||
base_theme.merge_with_overrides(&overrides).unwrap();
|
||||
|
||||
// Check that overridden colors are changed
|
||||
assert_eq!(
|
||||
merged_theme.background,
|
||||
Some(Color::Rgb(RGBColor::from_str("ff0000").unwrap()))
|
||||
);
|
||||
assert_eq!(merged_theme.text_fg, Color::Ansi(ANSIColor::Red));
|
||||
assert_eq!(
|
||||
merged_theme.selection_bg,
|
||||
Color::Rgb(RGBColor::from_str("00ff00").unwrap())
|
||||
);
|
||||
|
||||
// Check that non-overridden colors remain the same
|
||||
assert_eq!(merged_theme.border_fg, Color::Ansi(ANSIColor::White));
|
||||
assert_eq!(
|
||||
merged_theme.input_text_fg,
|
||||
Color::Ansi(ANSIColor::BrightWhite)
|
||||
);
|
||||
assert_eq!(merged_theme.match_fg, Color::Ansi(ANSIColor::BrightWhite));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_theme_merge_with_invalid_color() {
|
||||
let base_theme = create_test_theme();
|
||||
let overrides = crate::config::ui::ThemeOverrides {
|
||||
text_fg: Some("invalid-color".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = base_theme.merge_with_overrides(&overrides);
|
||||
assert!(result.is_err());
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("invalid text_fg color")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_theme_merge_with_empty_overrides() {
|
||||
let base_theme = create_test_theme();
|
||||
let overrides = crate::config::ui::ThemeOverrides::default();
|
||||
|
||||
let merged_theme =
|
||||
base_theme.merge_with_overrides(&overrides).unwrap();
|
||||
|
||||
// Check that all colors remain the same when no overrides are provided
|
||||
assert_eq!(merged_theme.background, base_theme.background);
|
||||
assert_eq!(merged_theme.border_fg, base_theme.border_fg);
|
||||
assert_eq!(merged_theme.text_fg, base_theme.text_fg);
|
||||
assert_eq!(merged_theme.dimmed_text_fg, base_theme.dimmed_text_fg);
|
||||
assert_eq!(merged_theme.input_text_fg, base_theme.input_text_fg);
|
||||
assert_eq!(merged_theme.result_count_fg, base_theme.result_count_fg);
|
||||
assert_eq!(merged_theme.result_name_fg, base_theme.result_name_fg);
|
||||
assert_eq!(
|
||||
merged_theme.result_line_number_fg,
|
||||
base_theme.result_line_number_fg
|
||||
);
|
||||
assert_eq!(merged_theme.result_value_fg, base_theme.result_value_fg);
|
||||
assert_eq!(merged_theme.selection_bg, base_theme.selection_bg);
|
||||
assert_eq!(merged_theme.selection_fg, base_theme.selection_fg);
|
||||
assert_eq!(merged_theme.match_fg, base_theme.match_fg);
|
||||
assert_eq!(merged_theme.preview_title_fg, base_theme.preview_title_fg);
|
||||
assert_eq!(merged_theme.channel_mode_fg, base_theme.channel_mode_fg);
|
||||
assert_eq!(merged_theme.channel_mode_bg, base_theme.channel_mode_bg);
|
||||
assert_eq!(
|
||||
merged_theme.remote_control_mode_fg,
|
||||
base_theme.remote_control_mode_fg
|
||||
);
|
||||
assert_eq!(
|
||||
merged_theme.remote_control_mode_bg,
|
||||
base_theme.remote_control_mode_bg
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_theme_deserialization_invalid_color() {
|
||||
let theme_content = r##"
|
||||
|
@ -66,6 +66,39 @@ impl Default for RemoteControlConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Theme color overrides that can be specified in the configuration file
|
||||
/// to customize the appearance of the selected theme
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash, Default)]
|
||||
#[serde(default)]
|
||||
pub struct ThemeOverrides {
|
||||
// General colors
|
||||
pub background: Option<String>,
|
||||
pub border_fg: Option<String>,
|
||||
pub text_fg: Option<String>,
|
||||
pub dimmed_text_fg: Option<String>,
|
||||
|
||||
// Input colors
|
||||
pub input_text_fg: Option<String>,
|
||||
pub result_count_fg: Option<String>,
|
||||
|
||||
// Result colors
|
||||
pub result_name_fg: Option<String>,
|
||||
pub result_line_number_fg: Option<String>,
|
||||
pub result_value_fg: Option<String>,
|
||||
pub selection_bg: Option<String>,
|
||||
pub selection_fg: Option<String>,
|
||||
pub match_fg: Option<String>,
|
||||
|
||||
// Preview colors
|
||||
pub preview_title_fg: Option<String>,
|
||||
|
||||
// Mode colors
|
||||
pub channel_mode_fg: Option<String>,
|
||||
pub channel_mode_bg: Option<String>,
|
||||
pub remote_control_mode_fg: Option<String>,
|
||||
pub remote_control_mode_bg: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash)]
|
||||
#[serde(default)]
|
||||
pub struct UiConfig {
|
||||
@ -82,6 +115,10 @@ pub struct UiConfig {
|
||||
pub preview_panel: PreviewPanelConfig,
|
||||
pub help_panel: HelpPanelConfig,
|
||||
pub remote_control: RemoteControlConfig,
|
||||
|
||||
// Theme color overrides
|
||||
#[serde(default)]
|
||||
pub theme_overrides: ThemeOverrides,
|
||||
}
|
||||
|
||||
impl Default for UiConfig {
|
||||
@ -98,6 +135,7 @@ impl Default for UiConfig {
|
||||
preview_panel: PreviewPanelConfig::default(),
|
||||
help_panel: HelpPanelConfig::default(),
|
||||
remote_control: RemoteControlConfig::default(),
|
||||
theme_overrides: ThemeOverrides::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ use std::fmt::Display;
|
||||
use tokio::sync::mpsc::{
|
||||
UnboundedReceiver, UnboundedSender, unbounded_channel,
|
||||
};
|
||||
use tracing::debug;
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[derive(PartialEq, Copy, Clone, Hash, Eq, Debug, Serialize, Deserialize)]
|
||||
pub enum Mode {
|
||||
@ -133,7 +133,14 @@ impl Television {
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
);
|
||||
let colorscheme = (&Theme::from_name(&config.ui.theme)).into();
|
||||
let base_theme = Theme::from_name(&config.ui.theme);
|
||||
let theme = base_theme
|
||||
.merge_with_overrides(&config.ui.theme_overrides)
|
||||
.unwrap_or_else(|e| {
|
||||
error!("Failed to apply theme overrides: {}", e);
|
||||
base_theme
|
||||
});
|
||||
let colorscheme = (&theme).into();
|
||||
|
||||
let patrnn = Television::preprocess_pattern(
|
||||
matching_mode,
|
||||
|
@ -3,7 +3,12 @@
|
||||
//! These tests verify Television's user interface customization capabilities,
|
||||
//! ensuring users can adapt the layout and appearance to their preferences and needs.
|
||||
|
||||
use television::tui::TESTING_ENV_VAR;
|
||||
use std::fs;
|
||||
use television::{
|
||||
config::{Config, ConfigEnv},
|
||||
tui::TESTING_ENV_VAR,
|
||||
};
|
||||
use tempfile::tempdir;
|
||||
|
||||
use super::common::*;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user