diff --git a/crates/maestro-tui/src/app.rs b/crates/maestro-tui/src/app.rs index 7191546..323eefe 100644 --- a/crates/maestro-tui/src/app.rs +++ b/crates/maestro-tui/src/app.rs @@ -193,24 +193,12 @@ impl App { self.state.focus = Focus::Pane; } Action::SidebarNext => { - let order: Vec = self - .catalog - .snapshot() - .sessions - .iter() - .map(|s| s.id) - .collect(); + let order = visible_sidebar_order(&self.catalog.snapshot()); self.state.move_sidebar_highlight(&order, 1); self.state.focus = Focus::Sidebar; } Action::SidebarPrev => { - let order: Vec = self - .catalog - .snapshot() - .sessions - .iter() - .map(|s| s.id) - .collect(); + let order = visible_sidebar_order(&self.catalog.snapshot()); self.state.move_sidebar_highlight(&order, -1); self.state.focus = Focus::Sidebar; } @@ -432,6 +420,19 @@ impl App { } } +/// Order the sidebar walks for `j`/`k`: each daemon's sessions, in the same +/// order [`crate::sidebar::Sidebar::render`] emits them. Keeps highlight +/// movement in visual sync with what the user sees. +fn visible_sidebar_order(snap: &crate::catalog::CatalogSnapshot) -> Vec { + let mut out = Vec::with_capacity(snap.sessions.len()); + for d in &snap.daemons { + for s in snap.sessions.iter().filter(|s| s.daemon_id == d.daemon_id) { + out.push(s.id); + } + } + out +} + fn ws_base(http: &str) -> String { if let Some(rest) = http.strip_prefix("http://") { format!("ws://{rest}") diff --git a/crates/maestro-tui/src/render.rs b/crates/maestro-tui/src/render.rs index 645b9ad..2f5e5a0 100644 --- a/crates/maestro-tui/src/render.rs +++ b/crates/maestro-tui/src/render.rs @@ -103,26 +103,35 @@ pub fn draw(f: &mut Frame, inputs: RenderInputs<'_>) -> RegionMap { map.sidebar = Some(sidebar_area); } + // 1-row tab bar + 1-row horizontal rule (doubles as the pane's focus + // indicator) + flex pane area below. Saves the 4 rows the old + // `Length(3) + Borders::ALL` layout was eating before any content showed. let rows = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Length(3), Constraint::Min(0)]) + .constraints([ + Constraint::Length(1), + Constraint::Length(1), + Constraint::Min(0), + ]) .split(right); let tab_area = rows[0]; - let pane_area = rows[1]; + let separator_area = rows[1]; + let pane_area = rows[2]; inputs.tab_bar.render(f, tab_area); map.tab_bar = Some(tab_area); let pane_focused = inputs.state.focus == Focus::Pane; - let pane_block = Block::default() - .borders(Borders::ALL) - .border_style(focus_style(pane_focused)); - let pane_inner = pane_block.inner(pane_area); - f.render_widget(pane_block, pane_area); + f.render_widget( + Block::default() + .borders(Borders::TOP) + .border_style(focus_style(pane_focused)), + separator_area, + ); if let Some(p) = inputs.pane { - p.render(f, pane_inner); + p.render(f, pane_area); } - map.pane = Some(pane_inner); + map.pane = Some(pane_area); // Status bar — always visible, never eclipsed by overlays so the user can // always see the current mode + key hints. diff --git a/crates/maestro-tui/src/sidebar.rs b/crates/maestro-tui/src/sidebar.rs index fdc2f4a..f0bcf09 100644 --- a/crates/maestro-tui/src/sidebar.rs +++ b/crates/maestro-tui/src/sidebar.rs @@ -26,9 +26,10 @@ impl Sidebar { self.render_with_focus(f, area, false); } - /// Same as [`Sidebar::render`] but draws the border in the focus colour - /// when `focused`. Used by the top-level orchestrator to surface which - /// panel currently consumes navigation keys. + /// Same as [`Sidebar::render`] but tints the right-edge separator in the + /// focus colour when `focused`. Box borders eat too many rows/cols for + /// the v1 layout — a single vertical rule keeps the panel boundary + /// without stealing space. pub fn render_with_focus(&self, f: &mut Frame, area: Rect, focused: bool) { let border_style = if focused { Style::default().fg(Color::Cyan) @@ -36,13 +37,15 @@ impl Sidebar { Style::default() }; let block = Block::default() - .title(" daemons ") - .borders(Borders::ALL) + .borders(Borders::RIGHT) .border_style(border_style); let inner = block.inner(area); f.render_widget(block, area); - let mut lines: Vec = Vec::new(); + let mut lines: Vec = vec![Line::from(Span::styled( + "daemons", + Style::default().add_modifier(Modifier::BOLD), + ))]; for d in &self.daemons { lines.push(Line::from(format!("● {}", d.hostname))); for s in self.sessions.iter().filter(|s| s.daemon_id == d.daemon_id) {