Skip to content

Commit 9e944ab

Browse files
committed
feat(tui): add PageUp/PageDown scrolling to all panes
Add PageUp/PageDown key support to the policy, logs, and draft/rules views. All three panes now scroll by one viewport height per keypress. Also fix scroll_policy() clamping to stop at the last viewport of content instead of the last line, preventing a blank-screen overshoot on G and PageDown. Signed-off-by: Major Hayden <major@redhat.com>
1 parent 28ee296 commit 9e944ab

2 files changed

Lines changed: 51 additions & 7 deletions

File tree

crates/openshell-tui/src/app.rs

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,8 @@ pub struct App {
495495
pub sandbox_providers_list: Vec<String>,
496496
pub policy_lines: Vec<ratatui::text::Line<'static>>,
497497
pub policy_scroll: usize,
498+
/// Visible line count in the policy pane, set during draw for PageUp/PageDown.
499+
pub policy_viewport_height: usize,
498500

499501
// Create sandbox modal
500502
pub create_form: Option<CreateSandboxForm>,
@@ -656,6 +658,7 @@ impl App {
656658
sandbox_providers_list: Vec::new(),
657659
policy_lines: Vec::new(),
658660
policy_scroll: 0,
661+
policy_viewport_height: 0,
659662
create_form: None,
660663
pending_create_sandbox: false,
661664
pending_forward_ports: Vec::new(),
@@ -1155,9 +1158,21 @@ impl App {
11551158
KeyCode::Char('k') | KeyCode::Up => {
11561159
self.scroll_policy(-1);
11571160
}
1161+
// Page-scroll by one viewport height.
1162+
KeyCode::PageDown => {
1163+
let delta = self.policy_viewport_height.max(1).cast_signed();
1164+
self.scroll_policy(delta);
1165+
}
1166+
KeyCode::PageUp => {
1167+
let delta = self.policy_viewport_height.max(1).cast_signed();
1168+
self.scroll_policy(-delta);
1169+
}
11581170
KeyCode::Char('G') => {
1159-
// Scroll to bottom.
1160-
self.policy_scroll = self.policy_lines.len().saturating_sub(1);
1171+
// Scroll to bottom, keeping a full viewport visible.
1172+
self.policy_scroll = self
1173+
.policy_lines
1174+
.len()
1175+
.saturating_sub(self.policy_viewport_height.max(1));
11611176
}
11621177
KeyCode::Char('g') => {
11631178
self.policy_scroll = 0;
@@ -1426,6 +1441,21 @@ impl App {
14261441
self.draft_scroll -= 1;
14271442
}
14281443
}
1444+
// Page-scroll by one viewport height, clamping the cursor to
1445+
// stay within the visible range.
1446+
KeyCode::PageDown if total > 0 => {
1447+
let page = vh.max(1);
1448+
let max_scroll = total.saturating_sub(vh.min(total));
1449+
self.draft_scroll = (self.draft_scroll + page).min(max_scroll);
1450+
let visible = total.saturating_sub(self.draft_scroll).min(vh);
1451+
self.draft_selected = self.draft_selected.min(visible.saturating_sub(1));
1452+
}
1453+
KeyCode::PageUp => {
1454+
let page = vh.max(1);
1455+
self.draft_scroll = self.draft_scroll.saturating_sub(page);
1456+
let visible = total.saturating_sub(self.draft_scroll).min(vh);
1457+
self.draft_selected = self.draft_selected.min(visible.saturating_sub(1));
1458+
}
14291459
KeyCode::Char('g') => {
14301460
self.draft_scroll = 0;
14311461
self.draft_selected = 0;
@@ -1490,8 +1520,11 @@ impl App {
14901520
}
14911521

14921522
/// Scroll policy pane by a delta (positive = down, negative = up).
1523+
///
1524+
/// Clamps so at least one viewport of content remains visible.
14931525
pub fn scroll_policy(&mut self, delta: isize) {
1494-
let max = self.policy_lines.len().saturating_sub(1);
1526+
let total = self.policy_lines.len();
1527+
let max = total.saturating_sub(self.policy_viewport_height.max(1));
14951528
if delta < 0 {
14961529
self.policy_scroll = self.policy_scroll.saturating_sub(delta.unsigned_abs());
14971530
} else {
@@ -1608,6 +1641,15 @@ impl App {
16081641
}
16091642
self.log_autoscroll = false;
16101643
}
1644+
// Page-scroll by one viewport height.
1645+
KeyCode::PageDown => {
1646+
let delta = vh.max(1).cast_signed();
1647+
self.scroll_logs(delta);
1648+
}
1649+
KeyCode::PageUp => {
1650+
let delta = vh.max(1).cast_signed();
1651+
self.scroll_logs(-delta);
1652+
}
16111653
KeyCode::Char('G' | 'f') => {
16121654
self.log_selection_anchor = None;
16131655
self.sandbox_log_scroll = self.log_autoscroll_offset();

crates/openshell-tui/src/ui/sandbox_policy.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,18 @@ use crate::app::App;
1111
/// Draw the scrollable policy viewer pane (bottom ~80% of the sandbox screen).
1212
///
1313
/// Always focused when visible (the metadata pane above is non-interactive).
14-
pub fn draw(frame: &mut Frame<'_>, app: &App, area: Rect) {
14+
pub fn draw(frame: &mut Frame<'_>, app: &mut App, area: Rect) {
15+
// Calculate inner dimensions (borders + padding) and store the viewport
16+
// height so PageUp/PageDown key handlers know how far to scroll.
17+
let inner_height = area.height.saturating_sub(2) as usize;
18+
app.policy_viewport_height = inner_height;
19+
1520
let t = &app.theme;
1621
let version = app.sandbox_policy.as_ref().map_or(0, |p| p.version);
1722

1823
let tab_title = super::sandbox_settings::draw_policy_tab_title(app);
1924
let version_hint = format!(" (v{version}) ");
2025

21-
// Calculate inner dimensions (borders + padding).
22-
let inner_height = area.height.saturating_sub(2) as usize;
23-
2426
if app.policy_lines.is_empty() {
2527
let lines = vec![Line::from(Span::styled("Loading...", t.muted))];
2628
let block = Block::default()

0 commit comments

Comments
 (0)