feat(maestro-tui): trim chrome + walk sidebar in visual order (NAP-289)
ci / rust (push) Successful in 8m18s
ci / rust (push) Successful in 8m18s
Dogfood-driven layout fixes: - `j`/`k` now walk the same daemon-then-sessions order the sidebar emits (`visible_sidebar_order`), so the highlight follows what the user sees instead of jumping by Uuid order across daemons. - Tab bar collapses from 3 rows of box border to 1 row of plain text + 1 row of horizontal rule. The rule doubles as the pane's focus indicator (cyan top border when `focus = Pane`, default otherwise). Saves ~4 rows above the pane before any content shows. - Sidebar drops the full box border in favour of a single right-edge vertical rule (also cyan when focused). The "daemons" header now lives inline as a bold first line instead of in the block title. Visible content area grows by ~2 cols + ~4 rows on the typical layout without losing focus / panel-boundary affordances. Workspace clippy + 85 unit tests passing.
This commit is contained in:
@@ -193,24 +193,12 @@ impl App {
|
||||
self.state.focus = Focus::Pane;
|
||||
}
|
||||
Action::SidebarNext => {
|
||||
let order: Vec<Uuid> = 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<Uuid> = 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<Uuid> {
|
||||
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}")
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<Line> = Vec::new();
|
||||
let mut lines: Vec<Line> = 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) {
|
||||
|
||||
Reference in New Issue
Block a user