diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-08-14 05:01:12 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-14 05:01:12 +0300 |
| commit | d2c317adc4b5fd73a7054d74f92d633a31069391 (patch) | |
| tree | c3db5184138d7eaaff9be05049d120a727a4e128 /src | |
| parent | 1af1a5b86f642ae7b053037bc472913cfbc1e206 (diff) | |
fix: keep active sidebar rows visible (#2794)
refs #2792
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/state.rs | 83 | ||||
| -rw-r--r-- | src/config/theme.rs | 3 | ||||
| -rw-r--r-- | src/main.rs | 1 | ||||
| -rw-r--r-- | src/ui.rs | 2 | ||||
| -rw-r--r-- | src/ui/sidebar.rs | 55 | ||||
| -rw-r--r-- | src/ui/tab_surface.rs | 2 |
6 files changed, 126 insertions, 20 deletions
diff --git a/src/app/state.rs b/src/app/state.rs index cb687363..ea550472 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -73,6 +73,8 @@ pub struct Palette { pub panel_bg: Color, /// Optional desktop sidebar background. Reset preserves the terminal background. pub sidebar_bg: Color, + /// Background for the active workspace and focused agent rows. + pub active_row_bg: Color, /// Subtle surface background for selected/focused items. pub surface0: Color, /// Slightly lighter surface for hover/active states. @@ -110,6 +112,7 @@ impl Palette { accent: Color::Rgb(137, 180, 250), // blue panel_bg: Color::Rgb(24, 24, 37), sidebar_bg: Color::Reset, + active_row_bg: Color::Rgb(49, 50, 68), surface0: Color::Rgb(49, 50, 68), surface1: Color::Rgb(69, 71, 90), surface_dim: Color::Rgb(30, 30, 46), @@ -133,6 +136,7 @@ impl Palette { accent: Color::Rgb(30, 102, 245), panel_bg: Color::Rgb(239, 241, 245), sidebar_bg: Color::Reset, + active_row_bg: Color::Rgb(204, 208, 218), surface0: Color::Rgb(204, 208, 218), surface1: Color::Rgb(188, 192, 204), surface_dim: Color::Rgb(230, 233, 239), @@ -156,6 +160,7 @@ impl Palette { accent: Color::Blue, panel_bg: Color::Reset, sidebar_bg: Color::Reset, + active_row_bg: Color::DarkGray, surface0: Color::Reset, surface1: Color::DarkGray, surface_dim: Color::DarkGray, @@ -179,6 +184,7 @@ impl Palette { accent: Color::Rgb(122, 162, 247), // blue panel_bg: Color::Rgb(26, 27, 38), sidebar_bg: Color::Reset, + active_row_bg: Color::Rgb(47, 51, 77), surface0: Color::Rgb(36, 40, 59), surface1: Color::Rgb(65, 72, 104), surface_dim: Color::Rgb(26, 27, 38), @@ -202,6 +208,7 @@ impl Palette { accent: Color::Rgb(46, 125, 233), panel_bg: Color::Rgb(225, 226, 231), sidebar_bg: Color::Reset, + active_row_bg: Color::Rgb(196, 200, 218), surface0: Color::Rgb(196, 200, 218), surface1: Color::Rgb(168, 174, 203), surface_dim: Color::Rgb(210, 211, 218), @@ -225,6 +232,7 @@ impl Palette { accent: Color::Rgb(189, 147, 249), // purple panel_bg: Color::Rgb(40, 42, 54), sidebar_bg: Color::Reset, + active_row_bg: Color::Rgb(68, 71, 90), surface0: Color::Rgb(68, 71, 90), surface1: Color::Rgb(98, 114, 164), surface_dim: Color::Rgb(40, 42, 54), @@ -248,6 +256,7 @@ impl Palette { accent: Color::Rgb(136, 192, 208), // frost panel_bg: Color::Rgb(46, 52, 64), sidebar_bg: Color::Reset, + active_row_bg: Color::Rgb(67, 76, 94), surface0: Color::Rgb(59, 66, 82), surface1: Color::Rgb(67, 76, 94), surface_dim: Color::Rgb(46, 52, 64), @@ -271,6 +280,7 @@ impl Palette { accent: Color::Rgb(215, 153, 33), // yellow panel_bg: Color::Rgb(40, 40, 40), sidebar_bg: Color::Reset, + active_row_bg: Color::Rgb(80, 73, 69), surface0: Color::Rgb(60, 56, 54), surface1: Color::Rgb(80, 73, 69), surface_dim: Color::Rgb(40, 40, 40), @@ -294,6 +304,7 @@ impl Palette { accent: Color::Rgb(7, 102, 120), panel_bg: Color::Rgb(251, 241, 199), sidebar_bg: Color::Reset, + active_row_bg: Color::Rgb(213, 196, 161), surface0: Color::Rgb(235, 219, 178), surface1: Color::Rgb(213, 196, 161), surface_dim: Color::Rgb(242, 229, 188), @@ -317,6 +328,7 @@ impl Palette { accent: Color::Rgb(97, 175, 239), // blue panel_bg: Color::Rgb(40, 44, 52), sidebar_bg: Color::Reset, + active_row_bg: Color::Rgb(62, 68, 81), surface0: Color::Rgb(44, 49, 58), surface1: Color::Rgb(62, 68, 81), surface_dim: Color::Rgb(40, 44, 52), @@ -340,6 +352,7 @@ impl Palette { accent: Color::Rgb(64, 120, 242), panel_bg: Color::Rgb(250, 250, 250), sidebar_bg: Color::Reset, + active_row_bg: Color::Rgb(216, 219, 226), surface0: Color::Rgb(240, 240, 241), surface1: Color::Rgb(229, 229, 230), surface_dim: Color::Rgb(245, 245, 246), @@ -363,6 +376,7 @@ impl Palette { accent: Color::Rgb(38, 139, 210), // blue panel_bg: Color::Rgb(0, 43, 54), sidebar_bg: Color::Reset, + active_row_bg: Color::Rgb(22, 75, 87), surface0: Color::Rgb(7, 54, 66), surface1: Color::Rgb(88, 110, 117), surface_dim: Color::Rgb(0, 43, 54), @@ -386,6 +400,7 @@ impl Palette { accent: Color::Rgb(38, 139, 210), panel_bg: Color::Rgb(253, 246, 227), sidebar_bg: Color::Reset, + active_row_bg: Color::Rgb(222, 216, 198), surface0: Color::Rgb(238, 232, 213), surface1: Color::Rgb(147, 161, 161), surface_dim: Color::Rgb(238, 232, 213), @@ -409,6 +424,7 @@ impl Palette { accent: Color::Rgb(126, 156, 216), // blue panel_bg: Color::Rgb(31, 31, 40), sidebar_bg: Color::Reset, + active_row_bg: Color::Rgb(54, 54, 70), surface0: Color::Rgb(42, 42, 55), surface1: Color::Rgb(54, 54, 70), surface_dim: Color::Rgb(31, 31, 40), @@ -432,6 +448,7 @@ impl Palette { accent: Color::Rgb(77, 105, 155), panel_bg: Color::Rgb(242, 236, 188), sidebar_bg: Color::Reset, + active_row_bg: Color::Rgb(201, 203, 209), surface0: Color::Rgb(220, 213, 172), surface1: Color::Rgb(201, 203, 209), surface_dim: Color::Rgb(213, 206, 163), @@ -455,6 +472,7 @@ impl Palette { accent: Color::Rgb(196, 167, 231), // iris panel_bg: Color::Rgb(25, 23, 36), sidebar_bg: Color::Reset, + active_row_bg: Color::Rgb(64, 61, 82), surface0: Color::Rgb(31, 29, 46), surface1: Color::Rgb(38, 35, 58), surface_dim: Color::Rgb(38, 35, 58), @@ -478,6 +496,7 @@ impl Palette { accent: Color::Rgb(144, 122, 169), panel_bg: Color::Rgb(250, 244, 237), sidebar_bg: Color::Reset, + active_row_bg: Color::Rgb(227, 217, 207), surface0: Color::Rgb(242, 233, 225), surface1: Color::Rgb(255, 250, 243), surface_dim: Color::Rgb(242, 233, 225), @@ -501,6 +520,7 @@ impl Palette { accent: Color::Rgb(255, 199, 153), panel_bg: Color::Rgb(26, 26, 26), sidebar_bg: Color::Reset, + active_row_bg: Color::Rgb(51, 51, 51), surface0: Color::Rgb(35, 35, 35), surface1: Color::Rgb(40, 40, 40), surface_dim: Color::Rgb(16, 16, 16), @@ -555,6 +575,9 @@ impl Palette { 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.surface0 { self.surface0 = parse_color(c); } @@ -2331,6 +2354,30 @@ mod tests { assert_eq!(navigator_first_row_at_or_after(&lines, 4), None); } + fn rgb_luminance(color: Color) -> f64 { + let Color::Rgb(r, g, b) = color else { + panic!("expected RGB color, got {color:?}"); + }; + let channel = |value: u8| { + let value = f64::from(value) / 255.0; + if value <= 0.04045 { + value / 12.92 + } else { + ((value + 0.055) / 1.055).powf(2.4) + } + }; + 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b) + } + + fn contrast_ratio(a: Color, b: Color) -> f64 { + let (lighter, darker) = { + let a = rgb_luminance(a); + let b = rgb_luminance(b); + (a.max(b), a.min(b)) + }; + (lighter + 0.05) / (darker + 0.05) + } + #[test] fn built_in_theme_names_resolve() { for name in THEME_NAMES { @@ -2342,6 +2389,32 @@ mod tests { } #[test] + fn built_in_active_rows_remain_visible_with_matching_terminal_backgrounds() { + for name in THEME_NAMES + .iter() + .copied() + .filter(|name| *name != "terminal") + { + let palette = Palette::from_name(name).unwrap(); + let background_contrast = contrast_ratio(palette.panel_bg, palette.active_row_bg); + assert!( + background_contrast >= 1.25, + "active row blends into the matching terminal background for {name}: {background_contrast:.2}:1" + ); + + let text_contrast = contrast_ratio(palette.text, palette.active_row_bg); + assert!( + text_contrast >= 3.0, + "active row text loses contrast for {name}: {text_contrast:.2}:1" + ); + assert_ne!( + palette.active_row_bg, palette.surface_dim, + "active row still shares the separator color for {name}" + ); + } + } + + #[test] fn built_in_themes_leave_sidebar_background_unset() { for name in THEME_NAMES { let palette = Palette::from_name(name).unwrap(); @@ -2354,16 +2427,16 @@ mod tests { } #[test] - fn custom_sidebar_background_overrides_the_default() { + fn custom_sidebar_colors_override_the_defaults() { let custom = crate::config::CustomThemeColors { sidebar_bg: Some("#181825".to_string()), + active_row_bg: Some("#313244".to_string()), ..Default::default() }; + let palette = Palette::catppuccin().with_overrides(&custom); - assert_eq!( - Palette::catppuccin().with_overrides(&custom).sidebar_bg, - Color::Rgb(24, 24, 37) - ); + assert_eq!(palette.sidebar_bg, Color::Rgb(24, 24, 37)); + assert_eq!(palette.active_row_bg, Color::Rgb(49, 50, 68)); } #[test] diff --git a/src/config/theme.rs b/src/config/theme.rs index e89fe99c..4b91b526 100644 --- a/src/config/theme.rs +++ b/src/config/theme.rs @@ -103,6 +103,7 @@ pub struct CustomThemeColors { pub accent: Option<String>, pub panel_bg: Option<String>, pub sidebar_bg: Option<String>, + pub active_row_bg: Option<String>, pub surface0: Option<String>, pub surface1: Option<String>, pub surface_dim: Option<String>, @@ -264,6 +265,7 @@ name = "nord" [theme.custom] panel_bg = "#1e1e2e" sidebar_bg = "#181825" +active_row_bg = "#313244" accent = "#ff79c6" red = "rgb(255, 85, 85)" "##; @@ -272,6 +274,7 @@ red = "rgb(255, 85, 85)" 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.active_row_bg.as_deref(), Some("#313244")); 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 868c0b5d..8505e5cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -130,6 +130,7 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration # Accepts: hex (#rrggbb), named colors, rgb(r,g,b), or panel_bg = "reset" # [theme.custom] # sidebar_bg = "#181825" +# active_row_bg = "#313244" # panel_bg = "reset" # accent = "#f5c2e7" # red = "#ff6188" @@ -1072,7 +1072,7 @@ mod tests { let active_row = ws_area.y + 1; let active_style = buffer[(ws_area.x, active_row)].style(); - assert_eq!(active_style.bg, Some(app.palette.surface_dim)); + assert_eq!(active_style.bg, Some(app.palette.active_row_bg)); } #[test] diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs index bc294cfe..252fc507 100644 --- a/src/ui/sidebar.rs +++ b/src/ui/sidebar.rs @@ -790,14 +790,14 @@ pub(super) fn render_sidebar_collapsed(app: &AppState, frame: &mut Frame, area: let row_style = if is_selected { Style::default().bg(p.surface0) } else if is_active { - Style::default().bg(p.surface_dim) + Style::default().bg(p.active_row_bg) } else { Style::default() }; let num_style = if is_selected { Style::default().fg(p.overlay1).bg(p.surface0) } else if is_active { - Style::default().fg(p.text).bg(p.surface_dim) + Style::default().fg(p.text).bg(p.active_row_bg) } else { Style::default().fg(p.overlay0) }; @@ -846,7 +846,7 @@ pub(super) fn render_sidebar_collapsed(app: &AppState, frame: &mut Frame, area: let position = detail_idx + 1; let is_active = app.is_active_pane(detail.ws_idx, detail.tab_idx, detail.pane_id); let position_style = if is_active { - Style::default().fg(p.text).bg(p.surface_dim) + Style::default().fg(p.text).bg(p.active_row_bg) } else { Style::default().fg(p.overlay0) }; @@ -856,7 +856,7 @@ pub(super) fn render_sidebar_collapsed(app: &AppState, frame: &mut Frame, area: if is_active { let buf = frame.buffer_mut(); for x in detail_content_area.x..detail_content_area.x + detail_content_area.width { - buf[(x, y)].set_style(Style::default().bg(p.surface_dim)); + buf[(x, y)].set_style(Style::default().bg(p.active_row_bg)); } } @@ -1252,7 +1252,7 @@ fn render_workspace_list( } else if is_dragged { p.surface1 } else { - p.surface_dim + p.active_row_bg }; let buf = frame.buffer_mut(); for y in row_y..row_y + row_height { @@ -1493,7 +1493,7 @@ fn render_agent_detail( let is_active = app.is_active_pane(detail.ws_idx, detail.tab_idx, detail.pane_id); let row_style = if is_active { - Style::default().bg(p.surface_dim) + Style::default().bg(p.active_row_bg) } else { Style::default() }; @@ -1677,14 +1677,14 @@ mod tests { assert_eq!(workspace_style.fg, Some(app.palette.text)); assert!(workspace_style.add_modifier.contains(Modifier::BOLD)); assert!(!workspace_style.add_modifier.contains(Modifier::DIM)); - assert_eq!(workspace_style.bg, Some(app.palette.surface_dim)); + assert_eq!(workspace_style.bg, Some(app.palette.active_row_bg)); let agent_x = find_symbol_x(buffer, body.y + 1, body.width, "p"); let agent_style = buffer[(agent_x, body.y + 1)].style(); assert_eq!(agent_style.fg, Some(app.palette.overlay0)); assert!(agent_style.add_modifier.contains(Modifier::DIM)); assert!(!agent_style.add_modifier.contains(Modifier::BOLD)); - assert_eq!(agent_style.bg, Some(app.palette.surface_dim)); + assert_eq!(agent_style.bg, Some(app.palette.active_row_bg)); } #[test] @@ -1745,7 +1745,7 @@ rows = [[{ token = "workspace", bold = false }, { token = "agent", dim = false } assert_eq!(active.fg, Some(app.palette.text)); assert!(active.add_modifier.contains(Modifier::BOLD)); assert!(!active.add_modifier.contains(Modifier::DIM)); - assert_eq!(active.bg, Some(app.palette.surface_dim)); + assert_eq!(active.bg, Some(app.palette.active_row_bg)); let inactive = buffer[(find_symbol_x(buffer, second_row, 25, "t"), second_row)].style(); assert_eq!(inactive.fg, Some(app.palette.subtext0)); @@ -1756,6 +1756,35 @@ rows = [[{ token = "workspace", bold = false }, { token = "agent", dim = false } } #[test] + fn navigate_selection_keeps_its_existing_background_beside_active_workspace() { + let mut app = crate::app::state::AppState::test_new(); + app.workspaces = vec![Workspace::test_new("one"), Workspace::test_new("two")]; + app.active = Some(0); + app.selected = 1; + app.mode = Mode::Navigate; + let area = Rect::new(0, 0, 26, 20); + app.view.workspace_card_areas = compute_workspace_card_areas(&app, area); + let active_row = app.view.workspace_card_areas[0].rect.y; + let selected_row = app.view.workspace_card_areas[1].rect.y; + let mut terminal = Terminal::new(TestBackend::new(26, 20)).unwrap(); + terminal + .draw(|frame| render_sidebar(&app, &TerminalRuntimeRegistry::new(), frame, area)) + .unwrap(); + let buffer = terminal.backend().buffer(); + + assert_eq!( + buffer[(0, active_row)].bg, + app.palette.active_row_bg, + "active workspace should keep its dedicated background" + ); + assert_eq!( + buffer[(0, selected_row)].bg, + app.palette.surface0, + "navigate selection should keep its existing surface0 background" + ); + } + + #[test] fn space_occurrence_style_applies_without_styling_separator() { let config: crate::config::Config = toml::from_str( r##" @@ -1791,12 +1820,12 @@ rows = [[{ token = "$hype", fg = "#abcdef", bold = true, dim = false }, "workspa assert_eq!(style.fg, Some(ratatui::style::Color::Rgb(0xab, 0xcd, 0xef))); assert!(style.add_modifier.contains(Modifier::BOLD)); assert!(!style.add_modifier.contains(Modifier::DIM)); - assert_eq!(style.bg, Some(app.palette.surface_dim)); + assert_eq!(style.bg, Some(app.palette.active_row_bg)); } assert_eq!(separator.fg, Some(app.palette.overlay0)); assert!(separator.add_modifier.contains(Modifier::DIM)); assert!(!separator.add_modifier.contains(Modifier::BOLD)); - assert_eq!(separator.bg, Some(app.palette.surface_dim)); + assert_eq!(separator.bg, Some(app.palette.active_row_bg)); } #[test] @@ -2237,7 +2266,7 @@ rows = [[{ token = "git_status", fg = "#123456" }]] .filter(|cells| { cells .iter() - .all(|style| style.bg == Some(app.palette.surface_dim)) + .all(|style| style.bg == Some(app.palette.active_row_bg)) }) .collect(); assert_eq!( @@ -2269,7 +2298,7 @@ rows = [[{ token = "git_status", fg = "#123456" }]] for cells in rows { assert_eq!(cells[0].fg, Some(app.palette.overlay0)); for style in cells { - assert_ne!(style.bg, Some(app.palette.surface_dim)); + assert_ne!(style.bg, Some(app.palette.active_row_bg)); } } } diff --git a/src/ui/tab_surface.rs b/src/ui/tab_surface.rs index 99ca9727..dd627711 100644 --- a/src/ui/tab_surface.rs +++ b/src/ui/tab_surface.rs @@ -304,7 +304,7 @@ mod tests { assert_eq!(frame.hyperlinks, vec![uri.to_owned()]); assert_eq!( frame_digest(&frame), - "a7c21fa42305a41231c7ae254f264f6ef923f46301d8fc4cd35ab6dfdd651b6b" + "f692e425877ef32cd0f3435dd8e252f33fff4e6dd5088a9324620895a6bd4c13" ); } |
