Keep TUI sidebar selections visible.

This commit is contained in:
2026-08-07 15:32:00 +02:00
parent 358648378e
commit 0417aaf749
3 changed files with 53 additions and 12 deletions

View File

@@ -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.

View File

@@ -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::<Vec<_>>();
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();

View File

@@ -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 ─────────────────────────────────────