From add6c14f1ee244502bd1a9599f2cd08320f935b7 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Wed, 5 Aug 2026 03:41:38 +0300 Subject: [PATCH] feat(theme): add optional sidebar background --- docs/next/CHANGELOG.md | 1 + .../src/content/docs/configuration.mdx | 3 ++ .../src/content/docs/ja/configuration.mdx | 3 ++ .../src/content/docs/zh-cn/configuration.mdx | 3 ++ .../website/src/data/config-reference.json | 6 +++ src/app/state.rs | 50 ++++++++++++++++++- src/config/theme.rs | 3 ++ src/main.rs | 1 + src/ui/sidebar.rs | 37 ++++++++++++++ 9 files changed, 106 insertions(+), 1 deletion(-) diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index 45216433..5ec122d5 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased ### Added +- `theme.custom.sidebar_bg` can now give the desktop sidebar its own background without changing built-in theme defaults. - Settings and `ui.status_indicators = "symbols"` can now use distinct static shapes for blocked, working, done, idle, and unknown agent states. (#2260) - The plugin marketplace now discovers valid manifests at repository roots and subdirectories, groups multiple plugins under each repository, and publishes their versions and exact default-branch commits. diff --git a/docs/next/website/src/content/docs/configuration.mdx b/docs/next/website/src/content/docs/configuration.mdx index 2aa445e0..e0c41c28 100644 --- a/docs/next/website/src/content/docs/configuration.mdx +++ b/docs/next/website/src/content/docs/configuration.mdx @@ -242,6 +242,7 @@ You can override individual colors: ```toml [theme.custom] +sidebar_bg = "#181825" panel_bg = "reset" accent = "#a6e3a1" green = "#a6e3a1" @@ -250,6 +251,8 @@ red = "#f38ba8" yellow = "#f9e2af" ``` +`sidebar_bg` optionally gives the desktop sidebar its own background. When omitted, the sidebar keeps the host terminal background. + Color values accept hex, named colors, `rgb(r,g,b)`, or reset aliases like `reset`, `default`, `none`, and `transparent`. ## UI and sidebar diff --git a/docs/next/website/src/content/docs/ja/configuration.mdx b/docs/next/website/src/content/docs/ja/configuration.mdx index 3069ebef..acc958fb 100644 --- a/docs/next/website/src/content/docs/ja/configuration.mdx +++ b/docs/next/website/src/content/docs/ja/configuration.mdx @@ -238,6 +238,7 @@ dark_name = "catppuccin" ```toml [theme.custom] +sidebar_bg = "#181825" panel_bg = "reset" accent = "#a6e3a1" green = "#a6e3a1" @@ -246,6 +247,8 @@ red = "#f38ba8" yellow = "#f9e2af" ``` +`sidebar_bg` を使うと、デスクトップのサイドバーだけに背景色を設定できます。省略した場合、サイドバーはホストターミナルの背景を使います。 + 色の値には、hex、名前付きの色、`rgb(r,g,b)`、または `reset`、`default`、`none`、`transparent` のようなリセットエイリアスが使えます。 ## 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 257cb127..2247dff8 100644 --- a/docs/next/website/src/content/docs/zh-cn/configuration.mdx +++ b/docs/next/website/src/content/docs/zh-cn/configuration.mdx @@ -238,6 +238,7 @@ dark_name = "catppuccin" ```toml [theme.custom] +sidebar_bg = "#181825" panel_bg = "reset" accent = "#a6e3a1" green = "#a6e3a1" @@ -246,6 +247,8 @@ red = "#f38ba8" yellow = "#f9e2af" ``` +`sidebar_bg` 可单独设置桌面侧边栏的背景色。省略时,侧边栏继续使用宿主终端背景。 + 颜色值支持十六进制、命名颜色、`rgb(r,g,b)`,以及 `reset`、`default`、`none`、`transparent` 等重置别名。 ## UI 与侧边栏 diff --git a/docs/next/website/src/data/config-reference.json b/docs/next/website/src/data/config-reference.json index 8cd6f0a1..e7a0539b 100644 --- a/docs/next/website/src/data/config-reference.json +++ b/docs/next/website/src/data/config-reference.json @@ -52,6 +52,12 @@ "default": "unset", "description": "Override the panel_bg color token on top of the base theme. Accepts hex, named colors, rgb(r,g,b), or reset aliases." }, + { + "key": "theme.custom.sidebar_bg", + "type": "color", + "default": "unset", + "description": "Set the desktop sidebar background without changing other panel surfaces. Accepts hex, named colors, rgb(r,g,b), or reset aliases." + }, { "key": "theme.custom.surface0", "type": "color", diff --git a/src/app/state.rs b/src/app/state.rs index 7a8837ee..0e1a8439 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -107,8 +107,10 @@ use crate::workspace::Workspace; pub struct Palette { /// Primary accent (highlight, active borders). pub accent: Color, - /// Background for floating panels, overlays, and modals. + /// Background for the tab bar, floating panels, overlays, and modals. pub panel_bg: Color, + /// Optional desktop sidebar background. Reset preserves the terminal background. + pub sidebar_bg: Color, /// Subtle surface background for selected/focused items. pub surface0: Color, /// Slightly lighter surface for hover/active states. @@ -145,6 +147,7 @@ impl Palette { Self { accent: Color::Rgb(137, 180, 250), // blue panel_bg: Color::Rgb(24, 24, 37), + sidebar_bg: Color::Reset, surface0: Color::Rgb(49, 50, 68), surface1: Color::Rgb(69, 71, 90), surface_dim: Color::Rgb(30, 30, 46), @@ -167,6 +170,7 @@ impl Palette { Self { accent: Color::Rgb(30, 102, 245), panel_bg: Color::Rgb(239, 241, 245), + sidebar_bg: Color::Reset, surface0: Color::Rgb(204, 208, 218), surface1: Color::Rgb(188, 192, 204), surface_dim: Color::Rgb(230, 233, 239), @@ -189,6 +193,7 @@ impl Palette { Self { accent: Color::Blue, panel_bg: Color::Reset, + sidebar_bg: Color::Reset, surface0: Color::Reset, surface1: Color::DarkGray, surface_dim: Color::DarkGray, @@ -211,6 +216,7 @@ impl Palette { Self { accent: Color::Rgb(122, 162, 247), // blue panel_bg: Color::Rgb(26, 27, 38), + sidebar_bg: Color::Reset, surface0: Color::Rgb(36, 40, 59), surface1: Color::Rgb(65, 72, 104), surface_dim: Color::Rgb(26, 27, 38), @@ -233,6 +239,7 @@ impl Palette { Self { accent: Color::Rgb(46, 125, 233), panel_bg: Color::Rgb(225, 226, 231), + sidebar_bg: Color::Reset, surface0: Color::Rgb(196, 200, 218), surface1: Color::Rgb(168, 174, 203), surface_dim: Color::Rgb(210, 211, 218), @@ -255,6 +262,7 @@ impl Palette { Self { accent: Color::Rgb(189, 147, 249), // purple panel_bg: Color::Rgb(40, 42, 54), + sidebar_bg: Color::Reset, surface0: Color::Rgb(68, 71, 90), surface1: Color::Rgb(98, 114, 164), surface_dim: Color::Rgb(40, 42, 54), @@ -277,6 +285,7 @@ impl Palette { Self { accent: Color::Rgb(136, 192, 208), // frost panel_bg: Color::Rgb(46, 52, 64), + sidebar_bg: Color::Reset, surface0: Color::Rgb(59, 66, 82), surface1: Color::Rgb(67, 76, 94), surface_dim: Color::Rgb(46, 52, 64), @@ -299,6 +308,7 @@ impl Palette { Self { accent: Color::Rgb(215, 153, 33), // yellow panel_bg: Color::Rgb(40, 40, 40), + sidebar_bg: Color::Reset, surface0: Color::Rgb(60, 56, 54), surface1: Color::Rgb(80, 73, 69), surface_dim: Color::Rgb(40, 40, 40), @@ -321,6 +331,7 @@ impl Palette { Self { accent: Color::Rgb(7, 102, 120), panel_bg: Color::Rgb(251, 241, 199), + sidebar_bg: Color::Reset, surface0: Color::Rgb(235, 219, 178), surface1: Color::Rgb(213, 196, 161), surface_dim: Color::Rgb(242, 229, 188), @@ -343,6 +354,7 @@ impl Palette { Self { accent: Color::Rgb(97, 175, 239), // blue panel_bg: Color::Rgb(40, 44, 52), + sidebar_bg: Color::Reset, surface0: Color::Rgb(44, 49, 58), surface1: Color::Rgb(62, 68, 81), surface_dim: Color::Rgb(40, 44, 52), @@ -365,6 +377,7 @@ impl Palette { Self { accent: Color::Rgb(64, 120, 242), panel_bg: Color::Rgb(250, 250, 250), + sidebar_bg: Color::Reset, surface0: Color::Rgb(240, 240, 241), surface1: Color::Rgb(229, 229, 230), surface_dim: Color::Rgb(245, 245, 246), @@ -387,6 +400,7 @@ impl Palette { Self { accent: Color::Rgb(38, 139, 210), // blue panel_bg: Color::Rgb(0, 43, 54), + sidebar_bg: Color::Reset, surface0: Color::Rgb(7, 54, 66), surface1: Color::Rgb(88, 110, 117), surface_dim: Color::Rgb(0, 43, 54), @@ -409,6 +423,7 @@ impl Palette { Self { accent: Color::Rgb(38, 139, 210), panel_bg: Color::Rgb(253, 246, 227), + sidebar_bg: Color::Reset, surface0: Color::Rgb(238, 232, 213), surface1: Color::Rgb(147, 161, 161), surface_dim: Color::Rgb(238, 232, 213), @@ -431,6 +446,7 @@ impl Palette { Self { accent: Color::Rgb(126, 156, 216), // blue panel_bg: Color::Rgb(31, 31, 40), + sidebar_bg: Color::Reset, surface0: Color::Rgb(42, 42, 55), surface1: Color::Rgb(54, 54, 70), surface_dim: Color::Rgb(31, 31, 40), @@ -453,6 +469,7 @@ impl Palette { Self { accent: Color::Rgb(77, 105, 155), panel_bg: Color::Rgb(242, 236, 188), + sidebar_bg: Color::Reset, surface0: Color::Rgb(220, 213, 172), surface1: Color::Rgb(201, 203, 209), surface_dim: Color::Rgb(213, 206, 163), @@ -475,6 +492,7 @@ impl Palette { Self { accent: Color::Rgb(196, 167, 231), // iris panel_bg: Color::Rgb(25, 23, 36), + sidebar_bg: Color::Reset, surface0: Color::Rgb(31, 29, 46), surface1: Color::Rgb(38, 35, 58), surface_dim: Color::Rgb(38, 35, 58), @@ -497,6 +515,7 @@ impl Palette { Self { accent: Color::Rgb(144, 122, 169), panel_bg: Color::Rgb(250, 244, 237), + sidebar_bg: Color::Reset, surface0: Color::Rgb(242, 233, 225), surface1: Color::Rgb(255, 250, 243), surface_dim: Color::Rgb(242, 233, 225), @@ -519,6 +538,7 @@ impl Palette { Self { accent: Color::Rgb(255, 199, 153), panel_bg: Color::Rgb(26, 26, 26), + sidebar_bg: Color::Reset, surface0: Color::Rgb(35, 35, 35), surface1: Color::Rgb(40, 40, 40), surface_dim: Color::Rgb(16, 16, 16), @@ -570,6 +590,9 @@ impl Palette { 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.surface0 { self.surface0 = parse_color(c); } @@ -2397,6 +2420,31 @@ mod tests { } } + #[test] + fn built_in_themes_leave_sidebar_background_unset() { + for name in THEME_NAMES { + let palette = Palette::from_name(name).unwrap(); + assert_eq!( + palette.sidebar_bg, + Color::Reset, + "built-in theme changed the sidebar background: {name}" + ); + } + } + + #[test] + fn custom_sidebar_background_overrides_the_default() { + let custom = crate::config::CustomThemeColors { + sidebar_bg: Some("#181825".to_string()), + ..Default::default() + }; + + assert_eq!( + Palette::catppuccin().with_overrides(&custom).sidebar_bg, + Color::Rgb(24, 24, 37) + ); + } + #[test] fn light_theme_aliases_resolve() { for name in ["light", "latte", "tokyo-day", "onelight", "lotus", "dawn"] { diff --git a/src/config/theme.rs b/src/config/theme.rs index fc601d3f..e98c9dfb 100644 --- a/src/config/theme.rs +++ b/src/config/theme.rs @@ -32,6 +32,7 @@ pub struct ThemeConfig { pub struct CustomThemeColors { pub accent: Option, pub panel_bg: Option, + pub sidebar_bg: Option, pub surface0: Option, pub surface1: Option, pub surface_dim: Option, @@ -164,6 +165,7 @@ name = "nord" [theme.custom] panel_bg = "#1e1e2e" +sidebar_bg = "#181825" accent = "#ff79c6" red = "rgb(255, 85, 85)" "##; @@ -171,6 +173,7 @@ red = "rgb(255, 85, 85)" assert_eq!(config.theme.name.as_deref(), Some("nord")); let custom = config.theme.custom.as_ref().unwrap(); assert_eq!(custom.panel_bg.as_deref(), Some("#1e1e2e")); + assert_eq!(custom.sidebar_bg.as_deref(), Some("#181825")); assert_eq!(custom.accent.as_deref(), Some("#ff79c6")); assert_eq!(custom.red.as_deref(), Some("rgb(255, 85, 85)")); assert!(custom.green.is_none()); diff --git a/src/main.rs b/src/main.rs index da905311..b6cc62b8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -128,6 +128,7 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration # Override individual color tokens on top of the base theme. # Accepts: hex (#rrggbb), named colors, rgb(r,g,b), or panel_bg = "reset" # [theme.custom] +# sidebar_bg = "#181825" # panel_bg = "reset" # accent = "#f5c2e7" # red = "#ff6188" diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs index 50796e81..7ce1b0ad 100644 --- a/src/ui/sidebar.rs +++ b/src/ui/sidebar.rs @@ -757,6 +757,9 @@ pub(super) fn render_sidebar_collapsed(app: &AppState, frame: &mut Frame, area: let is_navigating = matches!(app.mode, Mode::Navigate); let p = &app.palette; + frame + .buffer_mut() + .set_style(area, Style::default().bg(p.sidebar_bg)); let sep_style = if is_navigating { Style::default().fg(p.accent) } else { @@ -960,6 +963,9 @@ pub(super) fn render_sidebar( area: Rect, ) { let p = &app.palette; + frame + .buffer_mut() + .set_style(area, Style::default().bg(p.sidebar_bg)); let is_navigating = matches!(app.mode, Mode::Navigate); let sep_style = if is_navigating { Style::default().fg(p.accent) @@ -1591,6 +1597,37 @@ mod tests { }) } + #[test] + fn expanded_and_collapsed_sidebars_use_custom_background() { + let mut app = crate::app::state::AppState::test_new(); + app.workspaces.clear(); + app.active = None; + app.palette.sidebar_bg = ratatui::style::Color::Rgb(12, 34, 56); + let area = Rect::new(0, 0, 26, 20); + + let mut expanded = Terminal::new(TestBackend::new(26, 20)).unwrap(); + expanded + .draw(|frame| render_sidebar(&app, &TerminalRuntimeRegistry::new(), frame, area)) + .unwrap(); + assert!(expanded + .backend() + .buffer() + .content + .iter() + .all(|cell| cell.bg == app.palette.sidebar_bg)); + + let mut collapsed = Terminal::new(TestBackend::new(26, 20)).unwrap(); + collapsed + .draw(|frame| render_sidebar_collapsed(&app, frame, area)) + .unwrap(); + assert!(collapsed + .backend() + .buffer() + .content + .iter() + .all(|cell| cell.bg == app.palette.sidebar_bg)); + } + #[test] fn default_agent_rows_remove_redundant_state_text() { let mut app = crate::app::state::AppState::test_new();