mod confirm;
mod message;
mod number;
mod picker;
mod render;
mod text_input;
pub use confirm::{Question, confirm, confirm_default};
pub use message::message;
pub use number::{number_input, number_input_bounded};
pub use picker::{
ListAction, multi_select, multi_select_styled, select, select_reorderable,
select_styled,
};
pub use text_input::{input, input_wide};
pub enum ModalSignal<T> {
Value(T),
Cancelled,
Quit,
}
#[cfg(test)]
mod tests {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::layout::Rect;
use super::{
confirm::{Question, confirm_key},
picker::navigate_list,
render::{HINT_BLOCK_ROWS, hinted_box_height, picker_area},
text_input::{input_area, input_area_wide},
};
use crate::{overlay::PopupFlow, shortcut_hints};
fn key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
#[test]
fn navigate_list_wraps_on_arrows_and_clamps_on_page_and_ends() {
let mut cursor = 0;
assert!(navigate_list(&mut cursor, key(KeyCode::Up), 5, 2));
assert_eq!(cursor, 4);
assert!(navigate_list(&mut cursor, key(KeyCode::PageUp), 5, 2));
assert_eq!(cursor, 2);
assert!(navigate_list(&mut cursor, key(KeyCode::PageUp), 5, 2));
assert_eq!(cursor, 0);
assert!(navigate_list(&mut cursor, key(KeyCode::End), 5, 2));
assert_eq!(cursor, 4);
assert!(navigate_list(&mut cursor, key(KeyCode::Home), 5, 2));
assert_eq!(cursor, 0);
assert!(!navigate_list(&mut cursor, key(KeyCode::Enter), 5, 2));
}
fn ctrl(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::CONTROL)
}
#[test]
fn navigate_list_leaves_ctrl_chords_to_the_caller() {
let mut cursor = 2;
for code in [
KeyCode::Char('j'),
KeyCode::Char('k'),
KeyCode::Up,
KeyCode::Down,
KeyCode::Home,
KeyCode::End,
KeyCode::PageUp,
KeyCode::PageDown,
] {
assert!(
!navigate_list(&mut cursor, ctrl(code), 5, 2),
"Ctrl+{code:?} must not be consumed as navigation"
);
assert_eq!(cursor, 2, "Ctrl+{code:?} must not move the cursor");
}
}
#[test]
fn a_modified_y_does_not_confirm_a_prompt() {
let altgr = |ch| {
KeyEvent::new(
KeyCode::Char(ch),
KeyModifiers::CONTROL | KeyModifiers::ALT,
)
};
for modified in [
ctrl(KeyCode::Char('y')),
ctrl(KeyCode::Char('Y')),
altgr('y'),
KeyEvent::new(KeyCode::Char('y'), KeyModifiers::ALT),
] {
assert!(
matches!(confirm_key(modified, false), PopupFlow::Continue),
"{modified:?} must not confirm"
);
}
assert!(matches!(
confirm_key(key(KeyCode::Char('y')), false),
PopupFlow::Done(true)
));
assert!(matches!(
confirm_key(
KeyEvent::new(KeyCode::Char('Y'), KeyModifiers::SHIFT),
false
),
PopupFlow::Done(true)
));
}
#[test]
fn a_modified_n_does_not_decline_a_prompt() {
assert!(matches!(
confirm_key(ctrl(KeyCode::Char('n')), true),
PopupFlow::Continue
));
assert!(matches!(
confirm_key(key(KeyCode::Char('n')), true),
PopupFlow::Done(false)
));
}
#[test]
fn confirm_honours_the_default_and_esc_declines() {
assert!(matches!(
confirm_key(key(KeyCode::Enter), true),
PopupFlow::Done(true)
));
assert!(matches!(
confirm_key(key(KeyCode::Enter), false),
PopupFlow::Done(false)
));
assert!(matches!(
confirm_key(key(KeyCode::Esc), true),
PopupFlow::Done(false)
));
}
#[test]
fn popup_geometry_survives_a_terminal_below_its_minimum() {
for (width, height) in [(1, 1), (4, 2), (20, 6), (27, 10)] {
let area = Rect::new(0, 0, width, height);
for rect in [
picker_area(area, 40),
input_area(area),
input_area_wide(area),
] {
assert!(rect.width <= area.width, "{rect:?} in {area:?}");
assert!(rect.height <= area.height, "{rect:?} in {area:?}");
}
}
}
#[test]
fn a_roomy_terminal_still_gets_the_preferred_size() {
let area = Rect::new(0, 0, 100, 40);
let picker = picker_area(area, 4);
assert_eq!(picker.width, 50); assert_eq!(picker.height, 6); }
#[test]
fn a_plain_question_lets_enter_confirm() {
let question = Question::new("Save the file?");
assert!(question.default_yes);
assert_eq!(question.hints(), [("enter/y", "yes"), ("n", "no")]);
}
#[test]
fn a_declining_question_lets_enter_decline() {
let question = Question::declining("Delete everything?");
assert!(!question.default_yes);
assert_eq!(question.hints(), [("y", "yes"), ("enter/n", "no")]);
}
#[test]
fn a_confirm_box_keeps_its_hint_rows_while_global_hints_are_hidden() {
assert_eq!(hinted_box_height(), 3 + HINT_BLOCK_ROWS);
shortcut_hints::set_visible(false);
assert_eq!(hinted_box_height(), 3 + HINT_BLOCK_ROWS);
shortcut_hints::set_visible(true);
}
}