aboutsummaryrefslogtreecommitdiff
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
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>
-rw-r--r--docs/next/CHANGELOG.md3
-rw-r--r--docs/next/website/src/content/docs/configuration.mdx17
-rw-r--r--docs/next/website/src/content/docs/ja/configuration.mdx17
-rw-r--r--docs/next/website/src/content/docs/zh-cn/configuration.mdx17
-rw-r--r--docs/next/website/src/data/config-reference.json228
-rw-r--r--src/app/mod.rs90
-rw-r--r--src/app/state.rs62
-rw-r--r--src/config.rs2
-rw-r--r--src/config/theme.rs58
-rw-r--r--src/main.rs9
10 files changed, 495 insertions, 8 deletions
diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md
index 9bbdfc19..cad470a9 100644
--- a/docs/next/CHANGELOG.md
+++ b/docs/next/CHANGELOG.md
@@ -2,6 +2,9 @@
## Unreleased
+### Added
+- Custom themes can now define separate light and dark color overrides when automatic theme switching is enabled. (#837, thanks @aneym)
+
### Fixed
- Retained mouse selections now copy when Ctrl+C or Cmd+C arrives before a delayed mouse release instead of forwarding the copy shortcut to the pane. (#3100, thanks @moret)
- Removing a background worktree workspace no longer changes focus to its parent workspace. (#3098)
diff --git a/docs/next/website/src/content/docs/configuration.mdx b/docs/next/website/src/content/docs/configuration.mdx
index 5d4a7ab1..222a5d68 100644
--- a/docs/next/website/src/content/docs/configuration.mdx
+++ b/docs/next/website/src/content/docs/configuration.mdx
@@ -277,6 +277,23 @@ yellow = "#f9e2af"
Color values accept hex, named colors, `rgb(r,g,b)`, or reset aliases like `reset`, `default`, `none`, and `transparent`.
+When `auto_switch` is enabled, optional light and dark subtables layer on top of the shared custom colors:
+
+```toml
+[theme.custom]
+accent = "#89b4fa"
+
+[theme.custom.light]
+panel_bg = "#eff1f5"
+text = "#4c4f69"
+
+[theme.custom.dark]
+panel_bg = "#1e1e2e"
+text = "#cdd6f4"
+```
+
+The active palette is applied in this order: built-in theme, `[theme.custom]`, then `[theme.custom.light]` or `[theme.custom.dark]`. Omitting the mode subtables preserves the existing shared override behavior.
+
## UI and sidebar
The sidebar is the main Herdr dashboard. Search `ui.` in the [Config reference](/docs/config-reference/) for sizing, collapsed mode, Agent panel ordering, mouse behavior, pane borders, and other presentation settings.
diff --git a/docs/next/website/src/content/docs/ja/configuration.mdx b/docs/next/website/src/content/docs/ja/configuration.mdx
index e93c20bd..b2af36c8 100644
--- a/docs/next/website/src/content/docs/ja/configuration.mdx
+++ b/docs/next/website/src/content/docs/ja/configuration.mdx
@@ -273,6 +273,23 @@ yellow = "#f9e2af"
色の値には、hex、名前付きの色、`rgb(r,g,b)`、または `reset`、`default`、`none`、`transparent` のようなリセットエイリアスが使えます。
+`auto_switch` が有効な場合は、ライト/ダーク別のサブテーブルを共有カスタム色の上に重ねられます:
+
+```toml
+[theme.custom]
+accent = "#89b4fa"
+
+[theme.custom.light]
+panel_bg = "#eff1f5"
+text = "#4c4f69"
+
+[theme.custom.dark]
+panel_bg = "#1e1e2e"
+text = "#cdd6f4"
+```
+
+有効なパレットには、組み込みテーマ、`[theme.custom]`、`[theme.custom.light]` または `[theme.custom.dark]` の順で適用されます。モード別のサブテーブルを省略すると、既存の共有上書きの挙動が維持されます。
+
## UI とサイドバー
サイドバーは Herdr のメインダッシュボードです。サイズ、折りたたみモード、Agent パネルの並び順、マウスの挙動、ペインの境界線、その他の表示設定については、[設定リファレンス](/docs/config-reference/)で `ui.` を検索してください。
diff --git a/docs/next/website/src/content/docs/zh-cn/configuration.mdx b/docs/next/website/src/content/docs/zh-cn/configuration.mdx
index 97a923a0..46db8777 100644
--- a/docs/next/website/src/content/docs/zh-cn/configuration.mdx
+++ b/docs/next/website/src/content/docs/zh-cn/configuration.mdx
@@ -273,6 +273,23 @@ yellow = "#f9e2af"
颜色值支持十六进制、命名颜色、`rgb(r,g,b)`,以及 `reset`、`default`、`none`、`transparent` 等重置别名。
+启用 `auto_switch` 后,可以通过明暗模式子表在共享自定义颜色之上应用不同的覆盖值:
+
+```toml
+[theme.custom]
+accent = "#89b4fa"
+
+[theme.custom.light]
+panel_bg = "#eff1f5"
+text = "#4c4f69"
+
+[theme.custom.dark]
+panel_bg = "#1e1e2e"
+text = "#cdd6f4"
+```
+
+有效调色板按以下顺序应用:内置主题、`[theme.custom]`,然后是 `[theme.custom.light]` 或 `[theme.custom.dark]`。省略模式子表时,会保留现有的共享覆盖行为。
+
## UI 与侧边栏
侧边栏是 Herdr 的主仪表盘。在[配置参考](/docs/config-reference/)中搜索 `ui.`,可查看尺寸、折叠模式、Agent 面板排序、鼠标行为、窗格边框和其他显示设置。
diff --git a/docs/next/website/src/data/config-reference.json b/docs/next/website/src/data/config-reference.json
index 1765c10f..147d7056 100644
--- a/docs/next/website/src/data/config-reference.json
+++ b/docs/next/website/src/data/config-reference.json
@@ -171,6 +171,234 @@
"type": "color",
"default": "unset",
"description": "Override the peach color token on top of the base theme. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.light.accent",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the accent color token when auto_switch selects a light appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.light.panel_bg",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the panel_bg color token when auto_switch selects a light appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.light.sidebar_bg",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the sidebar_bg color token when auto_switch selects a light appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.light.active_row_bg",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the active_row_bg color token when auto_switch selects a light appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.light.selection_bg",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the selection_bg color token when auto_switch selects a light appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.light.surface0",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the surface0 color token when auto_switch selects a light appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.light.surface1",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the surface1 color token when auto_switch selects a light appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.light.surface_dim",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the surface_dim color token when auto_switch selects a light appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.light.overlay0",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the overlay0 color token when auto_switch selects a light appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.light.overlay1",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the overlay1 color token when auto_switch selects a light appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.light.text",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the text color token when auto_switch selects a light appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.light.subtext0",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the subtext0 color token when auto_switch selects a light appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.light.mauve",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the mauve color token when auto_switch selects a light appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.light.green",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the green color token when auto_switch selects a light appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.light.yellow",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the yellow color token when auto_switch selects a light appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.light.red",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the red color token when auto_switch selects a light appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.light.blue",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the blue color token when auto_switch selects a light appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.light.teal",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the teal color token when auto_switch selects a light appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.light.peach",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the peach color token when auto_switch selects a light appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.dark.accent",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the accent color token when auto_switch selects a dark appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.dark.panel_bg",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the panel_bg color token when auto_switch selects a dark appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.dark.sidebar_bg",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the sidebar_bg color token when auto_switch selects a dark appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.dark.active_row_bg",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the active_row_bg color token when auto_switch selects a dark appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.dark.selection_bg",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the selection_bg color token when auto_switch selects a dark appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.dark.surface0",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the surface0 color token when auto_switch selects a dark appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.dark.surface1",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the surface1 color token when auto_switch selects a dark appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.dark.surface_dim",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the surface_dim color token when auto_switch selects a dark appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.dark.overlay0",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the overlay0 color token when auto_switch selects a dark appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.dark.overlay1",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the overlay1 color token when auto_switch selects a dark appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.dark.text",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the text color token when auto_switch selects a dark appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.dark.subtext0",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the subtext0 color token when auto_switch selects a dark appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.dark.mauve",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the mauve color token when auto_switch selects a dark appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.dark.green",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the green color token when auto_switch selects a dark appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.dark.yellow",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the yellow color token when auto_switch selects a dark appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.dark.red",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the red color token when auto_switch selects a dark appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.dark.blue",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the blue color token when auto_switch selects a dark appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.dark.teal",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the teal color token when auto_switch selects a dark appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
+ },
+ {
+ "key": "theme.custom.dark.peach",
+ "type": "color",
+ "default": "unset",
+ "description": "Override the peach color token when auto_switch selects a dark appearance. Applied after theme.custom. Accepts hex, named colors, rgb(r,g,b), or reset aliases."
}
]
},
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)]
diff --git a/src/config.rs b/src/config.rs
index 0595ae2f..02ad2fbc 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -33,7 +33,7 @@ pub use self::{
},
sound::SoundConfig,
tab_bar::TabBarRightEntryConfig,
- theme::{parse_color, CustomThemeColors, ThemeConfig, THEME_NAMES},
+ theme::{parse_color, CustomThemeColors, ModeThemeColors, ThemeConfig, THEME_NAMES},
window_title::{WindowTitlePart, WindowTitleTemplate, WindowTitleToken},
};
diff --git a/src/config/theme.rs b/src/config/theme.rs
index 8f28390c..cb9e3498 100644
--- a/src/config/theme.rs
+++ b/src/config/theme.rs
@@ -119,6 +119,35 @@ pub struct CustomThemeColors {
pub blue: Option<String>,
pub teal: Option<String>,
pub peach: Option<String>,
+ /// Overrides applied when `auto_switch` selects a light appearance.
+ pub light: Option<ModeThemeColors>,
+ /// Overrides applied when `auto_switch` selects a dark appearance.
+ pub dark: Option<ModeThemeColors>,
+}
+
+/// Per-token color overrides for one auto-switch appearance.
+#[derive(Debug, Clone, Default, Deserialize)]
+#[serde(default)]
+pub struct ModeThemeColors {
+ pub accent: Option<String>,
+ pub panel_bg: Option<String>,
+ pub sidebar_bg: Option<String>,
+ pub active_row_bg: Option<String>,
+ pub selection_bg: Option<String>,
+ pub surface0: Option<String>,
+ pub surface1: Option<String>,
+ pub surface_dim: Option<String>,
+ pub overlay0: Option<String>,
+ pub overlay1: Option<String>,
+ pub text: Option<String>,
+ pub subtext0: Option<String>,
+ pub mauve: Option<String>,
+ pub green: Option<String>,
+ pub yellow: Option<String>,
+ pub red: Option<String>,
+ pub blue: Option<String>,
+ pub teal: Option<String>,
+ pub peach: Option<String>,
}
/// Parse a color string into a ratatui Color.
@@ -284,6 +313,35 @@ red = "rgb(255, 85, 85)"
}
#[test]
+ fn theme_custom_mode_overrides_parse() {
+ let toml = r##"
+[theme.custom]
+accent = "#010203"
+
+[theme.custom.light]
+accent = "#040506"
+text = "#070809"
+selection_bg = "#101112"
+
+[theme.custom.dark]
+panel_bg = "#0a0b0c"
+sidebar_bg = "#0d0e0f"
+active_row_bg = "#131415"
+"##;
+ let config: Config = toml::from_str(toml).unwrap();
+ let custom = config.theme.custom.as_ref().unwrap();
+ assert_eq!(custom.accent.as_deref(), Some("#010203"));
+ let light = custom.light.as_ref().unwrap();
+ assert_eq!(light.accent.as_deref(), Some("#040506"));
+ assert_eq!(light.text.as_deref(), Some("#070809"));
+ assert_eq!(light.selection_bg.as_deref(), Some("#101112"));
+ let dark = custom.dark.as_ref().unwrap();
+ assert_eq!(dark.panel_bg.as_deref(), Some("#0a0b0c"));
+ assert_eq!(dark.sidebar_bg.as_deref(), Some("#0d0e0f"));
+ assert_eq!(dark.active_row_bg.as_deref(), Some("#131415"));
+ }
+
+ #[test]
fn theme_defaults_when_missing() {
let config: Config = toml::from_str("").unwrap();
assert!(config.theme.name.is_none());
diff --git a/src/main.rs b/src/main.rs
index 56dbb8df..a6ba1c03 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -137,6 +137,15 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration
# red = "#ff6188"
# green = "#a6e3a1"
+# Layer appearance-specific overrides on top when auto_switch is enabled.
+# [theme.custom.light]
+# panel_bg = "#eff1f5"
+# text = "#4c4f69"
+#
+# [theme.custom.dark]
+# panel_bg = "#1e1e2e"
+# text = "#cdd6f4"
+
[terminal]
# Executable used for new interactive panes.
# Empty means $SHELL, then /bin/sh.