aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-08-14 05:01:12 +0300
committerGitHub <noreply@github.com>2026-08-14 05:01:12 +0300
commitd2c317adc4b5fd73a7054d74f92d633a31069391 (patch)
treec3db5184138d7eaaff9be05049d120a727a4e128 /src
parent1af1a5b86f642ae7b053037bc472913cfbc1e206 (diff)
fix: keep active sidebar rows visible (#2794)
refs #2792
Diffstat (limited to 'src')
-rw-r--r--src/app/state.rs83
-rw-r--r--src/config/theme.rs3
-rw-r--r--src/main.rs1
-rw-r--r--src/ui.rs2
-rw-r--r--src/ui/sidebar.rs55
-rw-r--r--src/ui/tab_surface.rs2
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"
diff --git a/src/ui.rs b/src/ui.rs
index 81964422..8adc31ef 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -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"
);
}