aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
authorAlex <a.neyman17@gmail.com>2026-08-22 17:18:56 -0400
committerGitHub <noreply@github.com>2026-08-23 00:18:56 +0300
commitdb6f301f1301d85ca7a4271363760836f53ac0d0 (patch)
tree7ac8748407268190f3bea23190fb3cfb07bd4bf2 /src/app
parent1d604329b0ded93d18fd6b27f72c7473d1fee20f (diff)
feat: add per-mode theme overrides (#2324)
* feat: add per-mode theme overrides allow [theme.custom.light] and [theme.custom.dark] blocks so custom overrides can differ per resolved appearance when auto_switch flips. precedence: theme defaults, then [theme.custom], then the block for the active mode. configs without the new blocks resolve unchanged. refs #837 * docs: complete per-mode theme documentation --------- Co-authored-by: Ogulcan Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src/app')
-rw-r--r--src/app/mod.rs90
-rw-r--r--src/app/state.rs62
2 files changed, 145 insertions, 7 deletions
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 85ad4fa3..ff286c3f 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -337,6 +337,7 @@ fn resolve_palette_for_theme_name(
name: &str,
fallback_name: &str,
runtime: &state::ThemeRuntimeConfig,
+ mode_custom: Option<&crate::config::ModeThemeColors>,
) -> state::Palette {
let mut palette = state::Palette::from_name(name).unwrap_or_else(|| {
tracing::warn!(
@@ -353,6 +354,9 @@ fn resolve_palette_for_theme_name(
if let Some(accent) = &runtime.legacy_accent {
palette.accent = crate::config::parse_color(accent);
}
+ if let Some(custom) = mode_custom {
+ palette = palette.with_mode_overrides(custom);
+ }
palette
}
@@ -361,18 +365,30 @@ fn resolve_effective_theme(
runtime: &state::ThemeRuntimeConfig,
appearance: Option<crate::terminal_theme::HostAppearance>,
) -> (state::Palette, String) {
- let (name, fallback) = if runtime.auto_switch {
+ let (name, fallback, mode_custom) = if runtime.auto_switch {
match appearance.unwrap_or(crate::terminal_theme::HostAppearance::Dark) {
- crate::terminal_theme::HostAppearance::Dark => (&runtime.dark_name, "catppuccin"),
- crate::terminal_theme::HostAppearance::Light => {
- (&runtime.light_name, "catppuccin-latte")
- }
+ crate::terminal_theme::HostAppearance::Dark => (
+ &runtime.dark_name,
+ "catppuccin",
+ runtime
+ .custom
+ .as_ref()
+ .and_then(|custom| custom.dark.as_ref()),
+ ),
+ crate::terminal_theme::HostAppearance::Light => (
+ &runtime.light_name,
+ "catppuccin-latte",
+ runtime
+ .custom
+ .as_ref()
+ .and_then(|custom| custom.light.as_ref()),
+ ),
}
} else {
- (&runtime.manual_name, "catppuccin")
+ (&runtime.manual_name, "catppuccin", None)
};
(
- resolve_palette_for_theme_name(name, fallback, runtime),
+ resolve_palette_for_theme_name(name, fallback, runtime, mode_custom),
name.clone(),
)
}
@@ -2807,6 +2823,17 @@ mod tests {
fn theme_auto_switch_is_opt_in_and_preserves_manual_default() {
let mut config = Config::default();
config.theme.name = Some("tokyo-night".to_string());
+ config.theme.custom = Some(crate::config::CustomThemeColors {
+ light: Some(crate::config::ModeThemeColors {
+ accent: Some("#010203".to_string()),
+ ..Default::default()
+ }),
+ dark: Some(crate::config::ModeThemeColors {
+ accent: Some("#040506".to_string()),
+ ..Default::default()
+ }),
+ ..Default::default()
+ });
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let app = App::new(&config, true, None, api_rx, crate::api::EventHub::default());
@@ -2855,6 +2882,55 @@ mod tests {
}
#[test]
+ fn theme_auto_switch_layers_active_mode_overrides_last() {
+ let mut config = Config::default();
+ config.theme.name = Some("gruvbox".to_string());
+ config.theme.auto_switch = true;
+ config.theme.custom = Some(crate::config::CustomThemeColors {
+ accent: Some("#010203".to_string()),
+ text: Some("#040506".to_string()),
+ light: Some(crate::config::ModeThemeColors {
+ accent: Some("#070809".to_string()),
+ ..Default::default()
+ }),
+ dark: Some(crate::config::ModeThemeColors {
+ text: Some("#0a0b0c".to_string()),
+ sidebar_bg: Some("#0d0e0f".to_string()),
+ active_row_bg: Some("#101112".to_string()),
+ ..Default::default()
+ }),
+ ..Default::default()
+ });
+ let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
+ let mut app = App::new(&config, true, None, api_rx, crate::api::EventHub::default());
+
+ assert_eq!(
+ app.state.palette.accent,
+ ratatui::style::Color::Rgb(1, 2, 3)
+ );
+ assert_eq!(
+ app.state.palette.text,
+ ratatui::style::Color::Rgb(10, 11, 12)
+ );
+ assert_eq!(
+ app.state.palette.sidebar_bg,
+ ratatui::style::Color::Rgb(13, 14, 15)
+ );
+ assert_eq!(
+ app.state.palette.active_row_bg,
+ ratatui::style::Color::Rgb(16, 17, 18)
+ );
+
+ app.set_host_terminal_appearance(crate::terminal_theme::HostAppearance::Light, true);
+
+ assert_eq!(
+ app.state.palette.accent,
+ ratatui::style::Color::Rgb(7, 8, 9)
+ );
+ assert_eq!(app.state.palette.text, ratatui::style::Color::Rgb(4, 5, 6));
+ }
+
+ #[test]
fn inferred_background_appearance_does_not_override_explicit_report() {
let mut config = Config::default();
config.theme.name = Some("catppuccin".to_string());
diff --git a/src/app/state.rs b/src/app/state.rs
index d9dcfc9d..21ee8899 100644
--- a/src/app/state.rs
+++ b/src/app/state.rs
@@ -645,6 +645,68 @@ impl Palette {
}
self
}
+
+ pub fn with_mode_overrides(mut self, custom: &crate::config::ModeThemeColors) -> Self {
+ use crate::config::parse_color;
+ if let Some(c) = &custom.accent {
+ self.accent = parse_color(c);
+ }
+ if let Some(c) = &custom.panel_bg {
+ self.panel_bg = parse_color(c);
+ }
+ if let Some(c) = &custom.sidebar_bg {
+ self.sidebar_bg = parse_color(c);
+ }
+ if let Some(c) = &custom.active_row_bg {
+ self.active_row_bg = parse_color(c);
+ }
+ if let Some(c) = &custom.selection_bg {
+ self.selection_bg = parse_color(c);
+ }
+ if let Some(c) = &custom.surface0 {
+ self.surface0 = parse_color(c);
+ }
+ if let Some(c) = &custom.surface1 {
+ self.surface1 = parse_color(c);
+ }
+ if let Some(c) = &custom.surface_dim {
+ self.surface_dim = parse_color(c);
+ }
+ if let Some(c) = &custom.overlay0 {
+ self.overlay0 = parse_color(c);
+ }
+ if let Some(c) = &custom.overlay1 {
+ self.overlay1 = parse_color(c);
+ }
+ if let Some(c) = &custom.text {
+ self.text = parse_color(c);
+ }
+ if let Some(c) = &custom.subtext0 {
+ self.subtext0 = parse_color(c);
+ }
+ if let Some(c) = &custom.mauve {
+ self.mauve = parse_color(c);
+ }
+ if let Some(c) = &custom.green {
+ self.green = parse_color(c);
+ }
+ if let Some(c) = &custom.yellow {
+ self.yellow = parse_color(c);
+ }
+ if let Some(c) = &custom.red {
+ self.red = parse_color(c);
+ }
+ if let Some(c) = &custom.blue {
+ self.blue = parse_color(c);
+ }
+ if let Some(c) = &custom.teal {
+ self.teal = parse_color(c);
+ }
+ if let Some(c) = &custom.peach {
+ self.peach = parse_color(c);
+ }
+ self
+ }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]