From 0417aaf7498e38ce3ef909011ae171596b04fd7b Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Fri, 7 Aug 2026 15:32:00 +0200 Subject: [PATCH] Keep TUI sidebar selections visible. --- README.md | 2 +- crates/bds-server/src/tui.rs | 60 +++++++++++++++++++++++++++++------- specs/tui.allium | 3 ++ 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index c395dee..72c46d2 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The project is under active development. Core blogging workflows are broadly ava - Project-scoped typed domain events synchronize desktop views and cached runtime settings with shared-engine and CLI mutations even when Preferences is closed; persisted CLI notifications are consumed once, and the selected UI language is shared through settings. - Headless `bds-cli` automation for rebuild/repair/render, with live terminal progress through metadata comparison, site validation, and page rendering; publishing and Git sync; post/media/gallery creation; effective shared settings with secret-presence redaction; projects; utility Lua tasks; JSON I/O; airplane-mode AI routing; and package-linked CLI installation from Settings → Data or `bds-cli install`. - Local MCP automation over stdio or a localhost-only stateless HTTP endpoint, with project resources, read/search/count tools, uniquely identified inert write proposals, clean duplicate-pending rejection, explicit desktop approval, and opt-in Claude Code/Copilot configuration. -- A fully localized Ratatui terminal workspace, available locally through `bds-cli tui`/`BDS_MODE=tui` and remotely through authenticated SSH shell sessions, with shared post/template/script editing and publishing, project/search/command overlays, settings, tags, Git, reports, task progress, live multi-client locale updates, and airplane-mode AI gating. +- A fully localized Ratatui terminal workspace, available locally through `bds-cli tui`/`BDS_MODE=tui` and remotely through authenticated SSH shell sessions, with selection-following scrollable sidebars, shared post/template/script editing and publishing, project/search/command overlays, settings, tags, Git, reports, task progress, live multi-client locale updates, and airplane-mode AI gating. - `bds-cli server` hosting the shared application engines over a loopback-by-default, public-key-only SSH service, with restrictive private key material, live authorization updates, terminal-session transport, CLI-change synchronization, ordered domain/task events, and native desktop remote-project selection. - bDS2-compatible Markdown/Liquid rendering with built-in macros rendered from bundled Liquid templates in isolated scopes (customizable with `macros/*` partial-template slugs), project-description homepage headings, category-controlled list title visibility, descriptive category archive titles, canonical multilingual and flat page routes for every configured blog language, recursive menus, calendar archives, feeds, a root hreflang sitemap, Pagefind, shared cached multicore full-site rendering, change-aware and forced full renders from the native Blog menu/CLI/TUI, and fast route/mtime-based incremental validation whose targeted repair refreshes affected aggregate pages through cancellable section task groups with bDS2-style per-URL progress. - Navigable generated-route preview in the app or system browser, with draft database overlays and published filesystem content. diff --git a/crates/bds-server/src/tui.rs b/crates/bds-server/src/tui.rs index 46cf037..d880468 100644 --- a/crates/bds-server/src/tui.rs +++ b/crates/bds-server/src/tui.rs @@ -25,7 +25,9 @@ use ratatui::layout::Size; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span, Text}; -use ratatui::widgets::{Block, Borders, Clear, Paragraph, Widget, Wrap}; +use ratatui::widgets::{ + Block, Borders, Clear, List, ListItem, ListState, Paragraph, StatefulWidget, Widget, Wrap, +}; use ratatui_image::picker::Picker; use ratatui_image::protocol::Protocol; use ratatui_image::{Image as TerminalImage, Resize}; @@ -3302,14 +3304,8 @@ impl TuiApp { let rows = self .items .iter() - .enumerate() - .map(|(index, item)| { - let style = if index == self.selected_index && self.focus == Focus::Sidebar { - Style::default() - .bg(Color::Rgb(54, 61, 78)) - .fg(Color::White) - .add_modifier(Modifier::BOLD) - } else if matches!(item, SidebarItem::Header(_)) { + .map(|item| { + let style = if matches!(item, SidebarItem::Header(_)) { Style::default() .fg(Color::Rgb(126, 137, 160)) .add_modifier(Modifier::BOLD) @@ -3321,10 +3317,27 @@ impl TuiApp { } else { " " }; - Line::styled(format!("{prefix}{}", item.label(self.locale)), style) + ListItem::new(Line::styled( + format!("{prefix}{}", item.label(self.locale)), + style, + )) }) .collect::>(); - Paragraph::new(rows).render(inner, buffer); + let highlight = if self.focus == Focus::Sidebar { + Style::default() + .bg(Color::Rgb(54, 61, 78)) + .fg(Color::White) + .add_modifier(Modifier::BOLD) + } else { + Style::default() + }; + let mut state = ListState::default().with_selected(Some(self.selected_index)); + StatefulWidget::render( + List::new(rows).highlight_style(highlight), + inner, + buffer, + &mut state, + ); } fn render_main(&self, area: Rect, buffer: &mut Buffer) { @@ -4198,6 +4211,31 @@ mod tests { assert_eq!(app.view, TuiView::Settings); } + #[test] + fn sidebar_scroll_follows_j_and_k_selection() { + let fixture = Fixture::new(); + let mut app = fixture.app(false); + app.items = (0..20) + .map(|index| SidebarItem::Post(index.to_string(), format!("Post {index:02}"))) + .collect(); + app.selected_index = 0; + + for _ in 0..12 { + app.handle_input(TuiInput::plain(TuiKey::Char('j'))) + .unwrap(); + } + let scrolled_down = rendered_text(&app, 50, 10); + assert!(scrolled_down.contains("Post 12")); + assert!(!scrolled_down.contains("Post 00")); + + for _ in 0..12 { + app.handle_input(TuiInput::plain(TuiKey::Char('k'))) + .unwrap(); + } + let scrolled_up = rendered_text(&app, 50, 10); + assert!(scrolled_up.contains("Post 00")); + } + #[test] fn posts_templates_and_scripts_edit_validate_save_and_publish_through_shared_engines() { let fixture = Fixture::new(); diff --git a/specs/tui.allium b/specs/tui.allium index 2986702..7d4d74c 100644 --- a/specs/tui.allium +++ b/specs/tui.allium @@ -53,6 +53,9 @@ surface TuiSurface { -- viewport width, keeps the cursor's visual row visible while -- scrolling, and highlights the cursor's source line. There is -- no line-number gutter. + + @guarantee SidebarSelectionVisibility + -- The sidebar viewport follows the selected entry. } -- ─── Sidebar navigation ─────────────────────────────────────