Improve application help
This commit is contained in:
+220
-1
@@ -284,7 +284,21 @@ fn run_screen_loop(
|
||||
&mut exiting,
|
||||
);
|
||||
} else {
|
||||
handle_key(code, worker, state, forms, &mut exiting);
|
||||
let (terminal_width, terminal_height) = if state.screen == Screen::Help {
|
||||
let size = terminal.size().map_err(|_| TerminalError::Io)?;
|
||||
(size.width, size.height)
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
handle_key(
|
||||
code,
|
||||
worker,
|
||||
state,
|
||||
forms,
|
||||
&mut exiting,
|
||||
terminal_width,
|
||||
terminal_height,
|
||||
);
|
||||
}
|
||||
}
|
||||
Event::Resize(_, _) => {}
|
||||
@@ -726,10 +740,15 @@ fn handle_key(
|
||||
state: &mut UiState,
|
||||
forms: &mut FormController,
|
||||
exiting: &mut bool,
|
||||
terminal_width: u16,
|
||||
terminal_height: u16,
|
||||
) {
|
||||
if state.pending.is_some() {
|
||||
return;
|
||||
}
|
||||
if handle_help_navigation_key(code, state, terminal_width, terminal_height) {
|
||||
return;
|
||||
}
|
||||
if handle_navigation_key(code, state) {
|
||||
return;
|
||||
}
|
||||
@@ -838,6 +857,40 @@ fn handle_key(
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_help_navigation_key(
|
||||
code: KeyCode,
|
||||
state: &mut UiState,
|
||||
terminal_width: u16,
|
||||
terminal_height: u16,
|
||||
) -> bool {
|
||||
if state.screen != Screen::Help {
|
||||
return false;
|
||||
}
|
||||
let metrics = render::help_navigation_metrics(terminal_width, terminal_height);
|
||||
state.scroll_offset = state.scroll_offset.min(metrics.max_offset);
|
||||
match code {
|
||||
KeyCode::Up => state.scroll_offset = state.scroll_offset.saturating_sub(1),
|
||||
KeyCode::Down => {
|
||||
state.scroll_offset = state
|
||||
.scroll_offset
|
||||
.saturating_add(1)
|
||||
.min(metrics.max_offset)
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
state.scroll_offset = state.scroll_offset.saturating_sub(metrics.page_rows)
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
state.scroll_offset = state
|
||||
.scroll_offset
|
||||
.saturating_add(metrics.page_rows)
|
||||
.min(metrics.max_offset)
|
||||
}
|
||||
KeyCode::Char('?') => state.scroll_offset = 0,
|
||||
_ => return false,
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn issue_summary(view: &SessionView) -> String {
|
||||
let mut issues = Vec::new();
|
||||
issues.extend(
|
||||
@@ -903,6 +956,9 @@ mod tests {
|
||||
use std::process::Command;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use ratatui::Terminal;
|
||||
use ratatui::backend::TestBackend;
|
||||
|
||||
use crate::app::ClientDisplay;
|
||||
use crate::runtime::RuntimeObservation;
|
||||
use crate::tui::events::OperationError;
|
||||
@@ -984,6 +1040,169 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn rendered_help(width: u16, height: u16, state: &UiState) -> String {
|
||||
let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| render::draw(frame, state, &FormController::default()))
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
(0..buffer.area.height)
|
||||
.map(|y| {
|
||||
(0..buffer.area.width)
|
||||
.map(|x| buffer[(x, y)].symbol())
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_navigation_uses_visual_rows_and_viewport_pages_with_bounded_edges() {
|
||||
for (width, height) in [(34, 8), (40, 9), (60, 16), (80, 24), (100, 30), (180, 50)] {
|
||||
let metrics = render::help_navigation_metrics(width, height);
|
||||
let mut state = UiState {
|
||||
screen: Screen::Help,
|
||||
..UiState::default()
|
||||
};
|
||||
assert!(handle_help_navigation_key(
|
||||
KeyCode::Up,
|
||||
&mut state,
|
||||
width,
|
||||
height
|
||||
));
|
||||
assert_eq!(state.scroll_offset, 0, "top bound at {width}x{height}");
|
||||
assert!(handle_help_navigation_key(
|
||||
KeyCode::Down,
|
||||
&mut state,
|
||||
width,
|
||||
height
|
||||
));
|
||||
assert_eq!(state.scroll_offset, usize::from(metrics.max_offset > 0));
|
||||
assert!(handle_help_navigation_key(
|
||||
KeyCode::PageUp,
|
||||
&mut state,
|
||||
width,
|
||||
height
|
||||
));
|
||||
assert_eq!(state.scroll_offset, 0);
|
||||
|
||||
for _ in 0..metrics.max_offset.div_ceil(metrics.page_rows) {
|
||||
assert!(handle_help_navigation_key(
|
||||
KeyCode::PageDown,
|
||||
&mut state,
|
||||
width,
|
||||
height
|
||||
));
|
||||
}
|
||||
assert_eq!(state.scroll_offset, metrics.max_offset);
|
||||
assert!(handle_help_navigation_key(
|
||||
KeyCode::PageDown,
|
||||
&mut state,
|
||||
width,
|
||||
height
|
||||
));
|
||||
assert_eq!(state.scroll_offset, metrics.max_offset);
|
||||
assert!(handle_help_navigation_key(
|
||||
KeyCode::Up,
|
||||
&mut state,
|
||||
width,
|
||||
height
|
||||
));
|
||||
assert_eq!(state.scroll_offset, metrics.max_offset.saturating_sub(1));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_navigation_reaches_final_section_and_resize_clamps_current_position() {
|
||||
let mut state = UiState {
|
||||
screen: Screen::Help,
|
||||
..UiState::default()
|
||||
};
|
||||
let (width, height) = (60, 16);
|
||||
let metrics = render::help_navigation_metrics(width, height);
|
||||
let mut reached_last_section = false;
|
||||
loop {
|
||||
let text = rendered_help(width, height, &state);
|
||||
reached_last_section |= text.contains("Navigation, cancellation");
|
||||
if state.scroll_offset == metrics.max_offset {
|
||||
break;
|
||||
}
|
||||
handle_help_navigation_key(KeyCode::Down, &mut state, width, height);
|
||||
}
|
||||
assert_eq!(state.scroll_offset, metrics.max_offset);
|
||||
assert!(
|
||||
reached_last_section,
|
||||
"one-row scrolling reaches the last section"
|
||||
);
|
||||
let text = rendered_help(width, height, &state);
|
||||
assert!(text.contains(&format!("Help {}-", metrics.max_offset + 1)));
|
||||
|
||||
let small = render::help_navigation_metrics(34, 8);
|
||||
let large = render::help_navigation_metrics(180, 50);
|
||||
state.scroll_offset = small.max_offset;
|
||||
assert!(handle_help_navigation_key(
|
||||
KeyCode::PageDown,
|
||||
&mut state,
|
||||
180,
|
||||
50
|
||||
));
|
||||
assert_eq!(state.scroll_offset, large.max_offset);
|
||||
assert!(rendered_help(180, 50, &state).contains("Help "));
|
||||
|
||||
state.scroll_offset = large.max_offset;
|
||||
assert!(handle_help_navigation_key(KeyCode::Down, &mut state, 34, 8));
|
||||
assert_eq!(state.scroll_offset, large.max_offset + 1);
|
||||
let small_text = rendered_help(34, 8, &state);
|
||||
assert!(small_text.contains("Help "));
|
||||
assert!(!small_text.contains("Terminal too small"));
|
||||
assert!(state.scroll_offset <= small.max_offset);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_reset_and_escape_preserve_selection_and_application_view_without_worker_actions() {
|
||||
let identity = crate::domain::PublicKey::from_bytes([1; 32]);
|
||||
let mut view = navigation_view();
|
||||
view.has_draft = true;
|
||||
view.applied = AppliedState::Verified;
|
||||
let mut state = UiState {
|
||||
screen: Screen::Help,
|
||||
selected_identity: Some(identity),
|
||||
selected_peer_index: Some(2),
|
||||
scroll_offset: 42,
|
||||
view: Some(view),
|
||||
..UiState::default()
|
||||
};
|
||||
assert!(handle_help_navigation_key(
|
||||
KeyCode::Char('?'),
|
||||
&mut state,
|
||||
80,
|
||||
24
|
||||
));
|
||||
assert_eq!(state.screen, Screen::Help);
|
||||
assert_eq!(state.scroll_offset, 0);
|
||||
|
||||
handle_help_navigation_key(KeyCode::PageDown, &mut state, 80, 24);
|
||||
let selected_identity = state.selected_identity;
|
||||
let selected_peer_index = state.selected_peer_index;
|
||||
let view_before = state.view.clone();
|
||||
assert!(handle_navigation_key(KeyCode::Esc, &mut state));
|
||||
assert_eq!(state.screen, Screen::ClientList);
|
||||
assert_eq!(state.scroll_offset, 0);
|
||||
assert_eq!(state.selected_identity, selected_identity);
|
||||
assert_eq!(state.selected_peer_index, selected_peer_index);
|
||||
assert_eq!(state.view, view_before);
|
||||
assert!(state.view.as_ref().unwrap().has_draft);
|
||||
assert_eq!(state.view.as_ref().unwrap().applied, AppliedState::Verified);
|
||||
// The scrolling helper accepts no worker or command sink, so Help input
|
||||
// cannot submit work or mutate saved/applied application state.
|
||||
assert!(!handle_help_navigation_key(
|
||||
KeyCode::Down,
|
||||
&mut state,
|
||||
80,
|
||||
24
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrows_highlight_only_enter_opens_selected_peer_and_escape_returns_to_list() {
|
||||
let identity = crate::domain::PublicKey::from_bytes([1; 32]);
|
||||
|
||||
Reference in New Issue
Block a user