use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use super::app::{App, AppScreen, ClipOp, FileOpMode, InputMode};
use tui_file_explorer::ExplorerOutcome;
pub fn handle_key(app: &mut App, key: KeyEvent) -> bool {
match key.code {
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
if app.screen == AppScreen::Flashing {
app.cancel_flash();
} else {
app.should_quit = true;
}
return true;
}
KeyCode::Char('q') if key.modifiers.contains(KeyModifiers::CONTROL) => {
app.should_quit = true;
return true;
}
KeyCode::Char('t') if key.modifiers.contains(KeyModifiers::CONTROL) => {
app.next_explorer_theme();
return true;
}
KeyCode::Char('T') if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT => {
if app.show_app_theme_panel {
app.close_app_theme_panel();
} else {
app.open_app_theme_panel();
}
return true;
}
KeyCode::Char('t') if key.modifiers.is_empty() => {
let in_text_edit =
app.screen == AppScreen::SelectImage && app.input_mode == InputMode::Editing;
if !in_text_edit {
app.next_explorer_theme();
return true;
}
}
_ => {}
}
if app.show_app_theme_panel {
match key.code {
KeyCode::Up | KeyCode::Char('k') => {
app.theme_panel_up();
return true;
}
KeyCode::Down | KeyCode::Char('j') => {
app.theme_panel_down();
return true;
}
KeyCode::Enter => {
app.theme_panel_confirm();
return true;
}
KeyCode::Esc => {
app.close_app_theme_panel();
return true;
}
_ => {}
}
}
match app.screen {
AppScreen::SelectImage => handle_select_image(app, key),
AppScreen::BrowseImage => handle_browse_image(app, key),
AppScreen::SelectDrive => handle_select_drive(app, key),
AppScreen::DriveInfo => handle_drive_info(app, key),
AppScreen::ConfirmFlash => handle_confirm_flash(app, key),
AppScreen::Flashing => handle_flashing(app, key),
AppScreen::Complete => handle_complete(app, key),
AppScreen::Error => handle_error(app, key),
}
}
fn handle_select_image(app: &mut App, key: KeyEvent) -> bool {
match app.input_mode {
InputMode::Editing => match key.code {
KeyCode::Tab => {
app.open_file_explorer();
true
}
KeyCode::Char('f') if key.modifiers.contains(KeyModifiers::CONTROL) => {
app.open_file_explorer();
true
}
KeyCode::Enter => {
match app.confirm_image() {
Ok(()) => {
start_drive_detection(app);
}
Err(msg) => {
app.error_message = msg;
app.screen = AppScreen::Error;
}
}
true
}
KeyCode::Char(c) => {
app.image_insert(c);
true
}
KeyCode::Backspace => {
app.image_backspace();
true
}
KeyCode::Delete => {
app.image_cursor_right();
app.image_backspace();
true
}
KeyCode::Left => {
app.image_cursor_left();
true
}
KeyCode::Right => {
app.image_cursor_right();
true
}
KeyCode::Home => {
app.image_cursor = 0;
true
}
KeyCode::End => {
app.image_cursor = app.image_input.chars().count();
true
}
KeyCode::Esc => {
app.should_quit = true;
true
}
_ => false,
},
InputMode::Normal => match key.code {
KeyCode::Enter | KeyCode::Char('i') => {
app.input_mode = InputMode::Editing;
true
}
KeyCode::Tab => {
app.open_file_explorer();
true
}
KeyCode::Esc | KeyCode::Char('q') => {
app.should_quit = true;
true
}
_ => false,
},
}
}
fn handle_browse_image(app: &mut App, key: KeyEvent) -> bool {
app.file_op_status.clear();
match std::mem::replace(&mut app.file_op_mode, FileOpMode::Normal) {
FileOpMode::ConfirmDelete(path) => {
match key.code {
KeyCode::Char('y') | KeyCode::Char('Y') => app.explorer_do_delete(path),
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
app.file_op_status = "Delete cancelled.".to_string();
}
_ => {
app.file_op_mode = FileOpMode::ConfirmDelete(path);
}
}
return true;
}
FileOpMode::ConfirmOverwrite { src, dst, op } => {
match key.code {
KeyCode::Char('y') | KeyCode::Char('Y') => {
app.explorer_do_paste(&src.clone(), &dst.clone(), op);
}
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
app.file_op_status = "Paste cancelled.".to_string();
}
_ => {
app.file_op_mode = FileOpMode::ConfirmOverwrite { src, dst, op };
}
}
return true;
}
FileOpMode::Normal => {}
}
let searching = app.file_explorer.is_searching();
if !searching {
match key.code {
KeyCode::Char('t') if key.modifiers.is_empty() => {
app.next_explorer_theme();
return true;
}
KeyCode::Char('[') => {
app.prev_explorer_theme();
return true;
}
KeyCode::Char('y') if key.modifiers.is_empty() => {
app.explorer_yank(ClipOp::Copy);
return true;
}
KeyCode::Char('x') if key.modifiers.is_empty() => {
app.explorer_yank(ClipOp::Cut);
return true;
}
KeyCode::Char('p') if key.modifiers.is_empty() => {
app.explorer_initiate_paste();
return true;
}
KeyCode::Char('d') if key.modifiers.is_empty() => {
app.explorer_initiate_delete();
return true;
}
_ => {}
}
}
let outcome = app.file_explorer.handle_key(key);
match outcome {
ExplorerOutcome::Selected(path) => {
app.apply_explorer_selection(path);
true
}
ExplorerOutcome::Dismissed => {
app.screen = AppScreen::SelectImage;
app.input_mode = InputMode::Editing;
true
}
ExplorerOutcome::Pending => true,
ExplorerOutcome::Unhandled => false,
ExplorerOutcome::MkdirCreated(_)
| ExplorerOutcome::TouchCreated(_)
| ExplorerOutcome::RenameCompleted(_) => true,
}
}
fn handle_select_drive(app: &mut App, key: KeyEvent) -> bool {
match key.code {
KeyCode::Up | KeyCode::Char('k') => {
app.drive_up();
true
}
KeyCode::Down | KeyCode::Char('j') => {
app.drive_down();
true
}
KeyCode::Enter | KeyCode::Char(' ') => {
if app.available_drives.is_empty() {
return true; }
match app.confirm_drive() {
Ok(()) => {}
Err(msg) => {
app.error_message = msg;
app.screen = AppScreen::Error;
}
}
true
}
KeyCode::Char('r') | KeyCode::F(5) => {
start_drive_detection(app);
true
}
KeyCode::Backspace | KeyCode::Esc | KeyCode::Char('b') => {
app.go_back();
true
}
_ => false,
}
}
fn handle_drive_info(app: &mut App, key: KeyEvent) -> bool {
match key.code {
KeyCode::Enter | KeyCode::Char('f') => {
app.advance_to_confirm();
true
}
KeyCode::Backspace | KeyCode::Esc | KeyCode::Char('b') => {
app.go_back();
true
}
_ => false,
}
}
fn handle_confirm_flash(app: &mut App, key: KeyEvent) -> bool {
match key.code {
KeyCode::Enter | KeyCode::Char('y') | KeyCode::Char('Y') => {
match app.begin_flash() {
Ok(()) => {}
Err(msg) => {
app.error_message = msg;
app.screen = AppScreen::Error;
}
}
true
}
KeyCode::Esc | KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Char('b') => {
app.go_back();
true
}
_ => false,
}
}
fn handle_flashing(app: &mut App, key: KeyEvent) -> bool {
match key.code {
KeyCode::Char('c') | KeyCode::Esc => {
app.cancel_flash();
true
}
_ => false,
}
}
fn handle_complete(app: &mut App, key: KeyEvent) -> bool {
match key.code {
KeyCode::Up | KeyCode::Char('k') => {
app.contents_up();
true
}
KeyCode::Down | KeyCode::Char('j') => {
app.contents_down();
true
}
KeyCode::PageUp => {
for _ in 0..10 {
app.contents_up();
}
true
}
KeyCode::PageDown => {
for _ in 0..10 {
app.contents_down();
}
true
}
KeyCode::Char('r') | KeyCode::Char('R') => {
app.reset();
true
}
KeyCode::Char('q') | KeyCode::Esc => {
app.should_quit = true;
true
}
_ => false,
}
}
fn handle_error(app: &mut App, key: KeyEvent) -> bool {
match key.code {
KeyCode::Char('r') | KeyCode::Enter => {
app.reset();
true
}
KeyCode::Char('q') | KeyCode::Esc => {
app.should_quit = true;
true
}
_ => false,
}
}
fn start_drive_detection(app: &mut App) {
use tokio::sync::mpsc;
app.drives_loading = true;
app.available_drives.clear();
app.drive_cursor = 0;
let (tx, rx) = mpsc::unbounded_channel::<Vec<crate::domain::DriveInfo>>();
app.drives_rx = Some(rx);
tokio::spawn(async move {
let drives = crate::core::commands::load_drives().await;
let _ = tx.send(drives);
});
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{DriveInfo, ImageInfo};
use crate::tui::app::UsbEntry;
fn key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
fn ctrl(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::CONTROL)
}
fn make_drive(name: &str, device: &str) -> DriveInfo {
DriveInfo::new(name.into(), format!("/media/{name}"), 16.0, device.into())
}
fn app_on(screen: AppScreen) -> App {
let mut app = App::new();
app.screen = screen;
app
}
fn app_with_drives(screen: AppScreen) -> App {
let mut app = app_on(screen);
app.available_drives = vec![
make_drive("USB_A", "/dev/sdb"),
make_drive("USB_B", "/dev/sdc"),
make_drive("USB_C", "/dev/sdd"),
];
app.selected_image = Some(ImageInfo {
path: std::path::PathBuf::from("/tmp/test.img"),
name: "test.img".into(),
size_mb: 1.0,
});
app
}
fn app_complete_with_entries(n: usize) -> App {
let mut app = app_on(AppScreen::Complete);
app.usb_contents = (0..n)
.map(|i| UsbEntry {
name: format!("file_{i}"),
size_bytes: 0,
is_dir: false,
depth: 0,
})
.collect();
app
}
#[test]
fn ctrl_c_sets_should_quit_on_non_flash_screen() {
let mut app = App::new(); let consumed = handle_key(&mut app, ctrl(KeyCode::Char('c')));
assert!(consumed, "Ctrl-C must be consumed");
assert!(app.should_quit, "Ctrl-C must set should_quit");
assert_eq!(app.screen, AppScreen::SelectImage, "screen must not change");
}
#[test]
fn ctrl_c_cancels_flash_when_flashing() {
let mut app = app_on(AppScreen::Flashing);
let consumed = handle_key(&mut app, ctrl(KeyCode::Char('c')));
assert!(consumed);
assert!(!app.should_quit);
assert_eq!(app.screen, AppScreen::Error);
}
#[test]
fn ctrl_q_sets_should_quit_always() {
for screen in [
AppScreen::SelectImage,
AppScreen::SelectDrive,
AppScreen::DriveInfo,
AppScreen::ConfirmFlash,
AppScreen::Complete,
AppScreen::Error,
] {
let mut app = app_on(screen.clone());
let consumed = handle_key(&mut app, ctrl(KeyCode::Char('q')));
assert!(consumed, "Ctrl-Q must be consumed on {screen:?}");
assert!(app.should_quit, "Ctrl-Q must quit on {screen:?}");
}
}
#[test]
fn select_image_editing_char_inserts_and_consumes() {
let mut app = App::new();
assert_eq!(app.input_mode, InputMode::Editing);
let consumed = handle_key(&mut app, key(KeyCode::Char('a')));
assert!(consumed);
assert_eq!(app.image_input, "a");
assert_eq!(app.image_cursor, 1);
}
#[test]
fn select_image_editing_multiple_chars_build_string() {
let mut app = App::new();
for c in "hello".chars() {
handle_key(&mut app, key(KeyCode::Char(c)));
}
assert_eq!(app.image_input, "hello");
assert_eq!(app.image_cursor, 5);
}
#[test]
fn select_image_editing_backspace_deletes_char() {
let mut app = App::new();
for c in "path".chars() {
app.image_insert(c);
}
let consumed = handle_key(&mut app, key(KeyCode::Backspace));
assert!(consumed);
assert_eq!(app.image_input, "pat");
}
#[test]
fn select_image_editing_delete_removes_char_under_cursor() {
let mut app = App::new();
for c in "abc".chars() {
app.image_insert(c);
}
app.image_cursor = 1;
let consumed = handle_key(&mut app, key(KeyCode::Delete));
assert!(consumed);
assert_eq!(app.image_input.len(), 2);
}
#[test]
fn select_image_editing_left_moves_cursor() {
let mut app = App::new();
app.image_insert('x');
app.image_insert('y');
handle_key(&mut app, key(KeyCode::Left));
assert_eq!(app.image_cursor, 1);
}
#[test]
fn select_image_editing_right_moves_cursor() {
let mut app = App::new();
app.image_insert('x');
app.image_insert('y');
app.image_cursor = 0;
handle_key(&mut app, key(KeyCode::Right));
assert_eq!(app.image_cursor, 1);
}
#[test]
fn select_image_editing_home_moves_cursor_to_start() {
let mut app = App::new();
for c in "some/path".chars() {
app.image_insert(c);
}
assert_eq!(app.image_cursor, 9);
handle_key(&mut app, key(KeyCode::Home));
assert_eq!(app.image_cursor, 0);
}
#[test]
fn select_image_editing_end_moves_cursor_to_end() {
let mut app = App::new();
for c in "some/path".chars() {
app.image_insert(c);
}
app.image_cursor = 0;
handle_key(&mut app, key(KeyCode::End));
assert_eq!(app.image_cursor, 9);
}
#[test]
fn select_image_editing_esc_quits() {
let mut app = App::new();
assert_eq!(app.input_mode, InputMode::Editing);
handle_key(&mut app, key(KeyCode::Esc));
assert!(app.should_quit);
}
#[test]
fn select_image_editing_enter_with_missing_file_goes_to_error() {
let mut app = App::new();
app.image_input = "/definitely/nonexistent/path.iso".into();
handle_key(&mut app, key(KeyCode::Enter));
assert_eq!(app.screen, AppScreen::Error);
assert!(!app.error_message.is_empty());
}
#[tokio::test]
async fn select_image_editing_enter_with_valid_file_advances() {
use std::io::Write;
let path = std::env::temp_dir().join("fk_evt_test.img");
{
let mut f = std::fs::File::create(&path).expect("create");
f.write_all(&[0u8; 1024]).expect("write");
}
let mut app = App::new();
app.image_input = path.to_string_lossy().into();
handle_key(&mut app, key(KeyCode::Enter));
let _ = std::fs::remove_file(&path);
assert_eq!(app.screen, AppScreen::SelectDrive);
}
#[test]
fn select_image_normal_enter_switches_to_editing() {
let mut app = App::new();
app.input_mode = InputMode::Normal;
handle_key(&mut app, key(KeyCode::Enter));
assert_eq!(app.input_mode, InputMode::Editing);
}
#[test]
fn select_image_normal_i_switches_to_editing() {
let mut app = App::new();
app.input_mode = InputMode::Normal;
handle_key(&mut app, key(KeyCode::Char('i')));
assert_eq!(app.input_mode, InputMode::Editing);
}
#[test]
fn select_image_normal_esc_quits() {
let mut app = App::new();
app.input_mode = InputMode::Normal;
handle_key(&mut app, key(KeyCode::Esc));
assert!(app.should_quit);
}
#[test]
fn select_image_normal_q_quits() {
let mut app = App::new();
app.input_mode = InputMode::Normal;
handle_key(&mut app, key(KeyCode::Char('q')));
assert!(app.should_quit);
}
#[test]
fn select_drive_down_increments_cursor() {
let mut app = app_with_drives(AppScreen::SelectDrive);
handle_key(&mut app, key(KeyCode::Down));
assert_eq!(app.drive_cursor, 1);
}
#[test]
fn select_drive_j_increments_cursor() {
let mut app = app_with_drives(AppScreen::SelectDrive);
handle_key(&mut app, key(KeyCode::Char('j')));
assert_eq!(app.drive_cursor, 1);
}
#[test]
fn select_drive_up_decrements_cursor() {
let mut app = app_with_drives(AppScreen::SelectDrive);
app.drive_cursor = 2;
handle_key(&mut app, key(KeyCode::Up));
assert_eq!(app.drive_cursor, 1);
}
#[test]
fn select_drive_k_decrements_cursor() {
let mut app = app_with_drives(AppScreen::SelectDrive);
app.drive_cursor = 2;
handle_key(&mut app, key(KeyCode::Char('k')));
assert_eq!(app.drive_cursor, 1);
}
#[test]
fn select_drive_enter_confirms_valid_drive() {
let mut app = app_with_drives(AppScreen::SelectDrive);
handle_key(&mut app, key(KeyCode::Enter));
assert_eq!(app.screen, AppScreen::DriveInfo);
assert!(app.selected_drive.is_some());
}
#[test]
fn select_drive_space_confirms_valid_drive() {
let mut app = app_with_drives(AppScreen::SelectDrive);
handle_key(&mut app, key(KeyCode::Char(' ')));
assert_eq!(app.screen, AppScreen::DriveInfo);
}
#[test]
fn select_drive_enter_on_empty_list_is_noop() {
let mut app = app_on(AppScreen::SelectDrive);
handle_key(&mut app, key(KeyCode::Enter));
assert_eq!(app.screen, AppScreen::SelectDrive);
}
#[test]
fn select_drive_backspace_goes_back_to_select_image() {
let mut app = app_with_drives(AppScreen::SelectDrive);
handle_key(&mut app, key(KeyCode::Backspace));
assert_eq!(app.screen, AppScreen::SelectImage);
assert_eq!(app.input_mode, InputMode::Editing);
}
#[test]
fn select_drive_esc_goes_back_to_select_image() {
let mut app = app_with_drives(AppScreen::SelectDrive);
handle_key(&mut app, key(KeyCode::Esc));
assert_eq!(app.screen, AppScreen::SelectImage);
}
#[test]
fn select_drive_b_goes_back_to_select_image() {
let mut app = app_with_drives(AppScreen::SelectDrive);
handle_key(&mut app, key(KeyCode::Char('b')));
assert_eq!(app.screen, AppScreen::SelectImage);
}
#[tokio::test]
async fn select_drive_refresh_retriggers_loading() {
let mut app = app_with_drives(AppScreen::SelectDrive);
app.drives_loading = false;
handle_key(&mut app, key(KeyCode::Char('r')));
assert!(app.drives_loading);
assert!(app.drives_rx.is_some());
}
#[tokio::test]
async fn select_drive_f5_retriggers_loading() {
let mut app = app_with_drives(AppScreen::SelectDrive);
app.drives_loading = false;
handle_key(&mut app, key(KeyCode::F(5)));
assert!(app.drives_loading);
}
#[test]
fn drive_info_enter_advances_to_confirm_flash() {
let mut app = app_on(AppScreen::DriveInfo);
handle_key(&mut app, key(KeyCode::Enter));
assert_eq!(app.screen, AppScreen::ConfirmFlash);
}
#[test]
fn drive_info_f_advances_to_confirm_flash() {
let mut app = app_on(AppScreen::DriveInfo);
handle_key(&mut app, key(KeyCode::Char('f')));
assert_eq!(app.screen, AppScreen::ConfirmFlash);
}
#[test]
fn drive_info_esc_goes_back_to_select_drive() {
let mut app = app_on(AppScreen::DriveInfo);
handle_key(&mut app, key(KeyCode::Esc));
assert_eq!(app.screen, AppScreen::SelectDrive);
}
#[test]
fn drive_info_backspace_goes_back_to_select_drive() {
let mut app = app_on(AppScreen::DriveInfo);
handle_key(&mut app, key(KeyCode::Backspace));
assert_eq!(app.screen, AppScreen::SelectDrive);
}
#[test]
fn drive_info_b_goes_back_to_select_drive() {
let mut app = app_on(AppScreen::DriveInfo);
handle_key(&mut app, key(KeyCode::Char('b')));
assert_eq!(app.screen, AppScreen::SelectDrive);
}
#[test]
fn confirm_flash_n_goes_back_to_drive_info() {
let mut app = app_on(AppScreen::ConfirmFlash);
handle_key(&mut app, key(KeyCode::Char('n')));
assert_eq!(app.screen, AppScreen::DriveInfo);
}
#[test]
fn confirm_flash_capital_n_goes_back_to_drive_info() {
let mut app = app_on(AppScreen::ConfirmFlash);
handle_key(&mut app, key(KeyCode::Char('N')));
assert_eq!(app.screen, AppScreen::DriveInfo);
}
#[test]
fn confirm_flash_esc_goes_back_to_drive_info() {
let mut app = app_on(AppScreen::ConfirmFlash);
handle_key(&mut app, key(KeyCode::Esc));
assert_eq!(app.screen, AppScreen::DriveInfo);
}
#[test]
fn confirm_flash_b_goes_back_to_drive_info() {
let mut app = app_on(AppScreen::ConfirmFlash);
handle_key(&mut app, key(KeyCode::Char('b')));
assert_eq!(app.screen, AppScreen::DriveInfo);
}
#[test]
fn confirm_flash_y_without_selections_shows_error() {
let mut app = app_on(AppScreen::ConfirmFlash);
handle_key(&mut app, key(KeyCode::Char('y')));
assert_eq!(app.screen, AppScreen::Error);
assert!(!app.error_message.is_empty());
}
#[test]
fn confirm_flash_capital_y_without_selections_shows_error() {
let mut app = app_on(AppScreen::ConfirmFlash);
handle_key(&mut app, key(KeyCode::Char('Y')));
assert_eq!(app.screen, AppScreen::Error);
}
#[test]
fn flashing_c_cancels_operation() {
let mut app = app_on(AppScreen::Flashing);
let consumed = handle_key(&mut app, key(KeyCode::Char('c')));
assert!(consumed);
assert_eq!(app.screen, AppScreen::Error);
assert!(!app.error_message.is_empty());
}
#[test]
fn flashing_esc_cancels_operation() {
let mut app = app_on(AppScreen::Flashing);
handle_key(&mut app, key(KeyCode::Esc));
assert_eq!(app.screen, AppScreen::Error);
}
#[test]
fn flashing_other_keys_not_consumed() {
let mut app = app_on(AppScreen::Flashing);
let consumed = handle_key(&mut app, key(KeyCode::Char('x')));
assert!(!consumed, "unrelated keys should not be consumed");
assert_eq!(app.screen, AppScreen::Flashing);
}
#[test]
fn complete_down_increments_scroll() {
let mut app = app_complete_with_entries(10);
handle_key(&mut app, key(KeyCode::Down));
assert_eq!(app.contents_scroll, 1);
}
#[test]
fn complete_j_increments_scroll() {
let mut app = app_complete_with_entries(10);
handle_key(&mut app, key(KeyCode::Char('j')));
assert_eq!(app.contents_scroll, 1);
}
#[test]
fn complete_up_decrements_scroll() {
let mut app = app_complete_with_entries(10);
app.contents_scroll = 5;
handle_key(&mut app, key(KeyCode::Up));
assert_eq!(app.contents_scroll, 4);
}
#[test]
fn complete_k_decrements_scroll() {
let mut app = app_complete_with_entries(10);
app.contents_scroll = 5;
handle_key(&mut app, key(KeyCode::Char('k')));
assert_eq!(app.contents_scroll, 4);
}
#[test]
fn complete_page_down_scrolls_by_ten() {
let mut app = app_complete_with_entries(25);
handle_key(&mut app, key(KeyCode::PageDown));
assert_eq!(app.contents_scroll, 10);
}
#[test]
fn complete_page_up_scrolls_by_ten() {
let mut app = app_complete_with_entries(25);
app.contents_scroll = 15;
handle_key(&mut app, key(KeyCode::PageUp));
assert_eq!(app.contents_scroll, 5);
}
#[test]
fn complete_page_up_clamps_at_zero() {
let mut app = app_complete_with_entries(25);
app.contents_scroll = 3;
handle_key(&mut app, key(KeyCode::PageUp));
assert_eq!(app.contents_scroll, 0);
}
#[test]
fn complete_r_resets_to_select_image() {
let mut app = app_complete_with_entries(5);
handle_key(&mut app, key(KeyCode::Char('r')));
assert_eq!(app.screen, AppScreen::SelectImage);
}
#[test]
fn complete_capital_r_resets_to_select_image() {
let mut app = app_complete_with_entries(5);
handle_key(&mut app, key(KeyCode::Char('R')));
assert_eq!(app.screen, AppScreen::SelectImage);
}
#[test]
fn complete_q_quits() {
let mut app = app_complete_with_entries(5);
handle_key(&mut app, key(KeyCode::Char('q')));
assert!(app.should_quit);
}
#[test]
fn complete_esc_quits() {
let mut app = app_complete_with_entries(5);
handle_key(&mut app, key(KeyCode::Esc));
assert!(app.should_quit);
}
#[test]
fn error_r_resets_to_select_image() {
let mut app = app_on(AppScreen::Error);
handle_key(&mut app, key(KeyCode::Char('r')));
assert_eq!(app.screen, AppScreen::SelectImage);
}
#[test]
fn error_enter_resets_to_select_image() {
let mut app = app_on(AppScreen::Error);
handle_key(&mut app, key(KeyCode::Enter));
assert_eq!(app.screen, AppScreen::SelectImage);
}
#[test]
fn error_q_quits() {
let mut app = app_on(AppScreen::Error);
handle_key(&mut app, key(KeyCode::Char('q')));
assert!(app.should_quit);
}
#[test]
fn error_esc_quits() {
let mut app = app_on(AppScreen::Error);
handle_key(&mut app, key(KeyCode::Esc));
assert!(app.should_quit);
}
#[test]
fn error_unrelated_key_not_consumed() {
let mut app = app_on(AppScreen::Error);
let consumed = handle_key(&mut app, key(KeyCode::Char('x')));
assert!(!consumed);
assert_eq!(app.screen, AppScreen::Error);
}
#[test]
fn consumed_flag_true_for_recognised_keys() {
let mut app = App::new();
assert!(handle_key(&mut app, ctrl(KeyCode::Char('q'))));
}
#[test]
fn consumed_flag_false_for_unrecognised_keys_on_drive_info() {
let mut app = app_on(AppScreen::DriveInfo);
assert!(!handle_key(&mut app, key(KeyCode::F(2))));
}
#[test]
fn select_image_editing_tab_opens_file_explorer() {
let mut app = App::new();
assert_eq!(app.input_mode, InputMode::Editing);
let consumed = handle_key(&mut app, key(KeyCode::Tab));
assert!(consumed);
assert_eq!(app.screen, AppScreen::BrowseImage);
}
#[test]
fn select_image_editing_ctrl_f_opens_file_explorer() {
let mut app = App::new();
assert_eq!(app.input_mode, InputMode::Editing);
let consumed = handle_key(&mut app, ctrl(KeyCode::Char('f')));
assert!(consumed);
assert_eq!(app.screen, AppScreen::BrowseImage);
}
#[test]
fn select_image_normal_tab_opens_file_explorer() {
let mut app = App::new();
app.input_mode = InputMode::Normal;
let consumed = handle_key(&mut app, key(KeyCode::Tab));
assert!(consumed);
assert_eq!(app.screen, AppScreen::BrowseImage);
}
#[test]
fn browse_image_esc_returns_to_select_image() {
let mut app = app_on(AppScreen::BrowseImage);
let consumed = handle_key(&mut app, key(KeyCode::Esc));
assert!(consumed);
assert_eq!(app.screen, AppScreen::SelectImage);
assert_eq!(app.input_mode, InputMode::Editing);
}
#[test]
fn browse_image_q_dismisses_to_select_image() {
let mut app = app_on(AppScreen::BrowseImage);
let consumed = handle_key(&mut app, key(KeyCode::Char('q')));
assert!(consumed);
assert_eq!(app.screen, AppScreen::SelectImage);
assert_eq!(app.input_mode, InputMode::Editing);
}
#[test]
fn browse_image_down_is_consumed_and_stays_on_browse() {
let mut app = app_on(AppScreen::BrowseImage);
let consumed = handle_key(&mut app, key(KeyCode::Down));
assert!(consumed);
assert_eq!(app.screen, AppScreen::BrowseImage);
}
#[test]
fn browse_image_j_is_consumed_and_stays_on_browse() {
let mut app = app_on(AppScreen::BrowseImage);
let consumed = handle_key(&mut app, key(KeyCode::Char('j')));
assert!(consumed);
assert_eq!(app.screen, AppScreen::BrowseImage);
}
#[test]
fn browse_image_up_is_consumed_and_stays_on_browse() {
let mut app = app_on(AppScreen::BrowseImage);
let consumed = handle_key(&mut app, key(KeyCode::Up));
assert!(consumed);
assert_eq!(app.screen, AppScreen::BrowseImage);
}
#[test]
fn browse_image_k_is_consumed_and_stays_on_browse() {
let mut app = app_on(AppScreen::BrowseImage);
let consumed = handle_key(&mut app, key(KeyCode::Char('k')));
assert!(consumed);
assert_eq!(app.screen, AppScreen::BrowseImage);
}
#[test]
fn browse_image_page_down_is_consumed() {
let mut app = app_on(AppScreen::BrowseImage);
let consumed = handle_key(&mut app, key(KeyCode::PageDown));
assert!(consumed);
assert_eq!(app.screen, AppScreen::BrowseImage);
}
#[test]
fn browse_image_page_up_is_consumed() {
let mut app = app_on(AppScreen::BrowseImage);
let consumed = handle_key(&mut app, key(KeyCode::PageUp));
assert!(consumed);
assert_eq!(app.screen, AppScreen::BrowseImage);
}
#[test]
fn browse_image_unrecognised_key_not_consumed() {
let mut app = app_on(AppScreen::BrowseImage);
let consumed = handle_key(&mut app, key(KeyCode::F(5)));
assert!(!consumed);
assert_eq!(app.screen, AppScreen::BrowseImage);
}
#[test]
fn browse_image_selected_file_populates_input_and_returns() {
use std::fs;
let tmp = tempfile::tempdir().unwrap();
let iso = tmp.path().join("chosen.iso");
fs::write(&iso, b"fake").unwrap();
let mut app = app_on(AppScreen::BrowseImage);
app.file_explorer.navigate_to(tmp.path().to_path_buf());
let iso_idx = app
.file_explorer
.entries
.iter()
.position(|e| e.name == "chosen.iso")
.expect("chosen.iso must be in entries");
app.file_explorer.cursor = iso_idx;
let consumed = handle_key(&mut app, key(KeyCode::Enter));
assert!(consumed);
assert_eq!(app.screen, AppScreen::SelectImage);
assert_eq!(app.input_mode, InputMode::Editing);
assert_eq!(app.image_input, iso.to_string_lossy().as_ref());
assert_eq!(app.image_cursor, iso.to_string_lossy().chars().count());
}
#[test]
fn browse_image_backspace_ascends_directory() {
use std::fs;
let tmp = tempfile::tempdir().unwrap();
let subdir = tmp.path().join("inner");
fs::create_dir(&subdir).unwrap();
let mut app = app_on(AppScreen::BrowseImage);
app.file_explorer.navigate_to(subdir.clone());
assert_eq!(app.file_explorer.current_dir, subdir);
let consumed = handle_key(&mut app, key(KeyCode::Backspace));
assert!(consumed);
assert_eq!(app.screen, AppScreen::BrowseImage);
assert_eq!(app.file_explorer.current_dir, tmp.path());
}
#[test]
fn t_cycles_theme_on_select_drive() {
let mut app = app_on(AppScreen::SelectDrive);
let initial_idx = app.explorer_theme_idx;
let consumed = handle_key(&mut app, key(KeyCode::Char('t')));
assert!(consumed, "'t' must be consumed on SelectDrive");
assert_eq!(
app.explorer_theme_idx,
(initial_idx + 1) % app.explorer_themes.len(),
"theme index must advance by 1"
);
}
#[test]
fn t_cycles_theme_on_drive_info() {
let mut app = app_on(AppScreen::DriveInfo);
let initial_idx = app.explorer_theme_idx;
let consumed = handle_key(&mut app, key(KeyCode::Char('t')));
assert!(consumed);
assert_eq!(
app.explorer_theme_idx,
(initial_idx + 1) % app.explorer_themes.len()
);
}
#[test]
fn t_cycles_theme_on_confirm_flash() {
let mut app = app_on(AppScreen::ConfirmFlash);
let initial_idx = app.explorer_theme_idx;
let consumed = handle_key(&mut app, key(KeyCode::Char('t')));
assert!(consumed);
assert_eq!(
app.explorer_theme_idx,
(initial_idx + 1) % app.explorer_themes.len()
);
}
#[test]
fn t_cycles_theme_on_complete() {
let mut app = app_on(AppScreen::Complete);
let initial_idx = app.explorer_theme_idx;
let consumed = handle_key(&mut app, key(KeyCode::Char('t')));
assert!(consumed);
assert_eq!(
app.explorer_theme_idx,
(initial_idx + 1) % app.explorer_themes.len()
);
}
#[test]
fn t_cycles_theme_on_error() {
let mut app = app_on(AppScreen::Error);
let initial_idx = app.explorer_theme_idx;
let consumed = handle_key(&mut app, key(KeyCode::Char('t')));
assert!(consumed);
assert_eq!(
app.explorer_theme_idx,
(initial_idx + 1) % app.explorer_themes.len()
);
}
#[test]
fn t_cycles_theme_on_select_image_normal_mode() {
let mut app = App::new();
app.input_mode = InputMode::Normal;
let initial_idx = app.explorer_theme_idx;
let consumed = handle_key(&mut app, key(KeyCode::Char('t')));
assert!(
consumed,
"'t' must be consumed in Normal mode on SelectImage"
);
assert_eq!(
app.explorer_theme_idx,
(initial_idx + 1) % app.explorer_themes.len()
);
}
#[test]
fn t_inserts_char_on_select_image_editing_mode() {
let mut app = App::new();
app.input_mode = InputMode::Editing;
let initial_idx = app.explorer_theme_idx;
let consumed = handle_key(&mut app, key(KeyCode::Char('t')));
assert!(
consumed,
"'t' must be consumed (as text input) in Editing mode"
);
assert_eq!(
app.explorer_theme_idx, initial_idx,
"theme index must NOT change when typing"
);
assert_eq!(
app.image_input, "t",
"character must be inserted into input"
);
}
#[test]
fn ctrl_t_cycles_theme_even_in_editing_mode() {
let mut app = App::new();
app.input_mode = InputMode::Editing;
let initial_idx = app.explorer_theme_idx;
let consumed = handle_key(
&mut app,
KeyEvent::new(KeyCode::Char('t'), KeyModifiers::CONTROL),
);
assert!(consumed, "Ctrl+T must be consumed in Editing mode");
assert_eq!(
app.explorer_theme_idx,
(initial_idx + 1) % app.explorer_themes.len(),
"theme index must advance even in Editing mode"
);
assert_eq!(
app.image_input, "",
"Ctrl+T must NOT insert text into the input field"
);
}
#[test]
fn ctrl_t_cycles_theme_on_every_screen() {
for screen in [
AppScreen::SelectImage,
AppScreen::SelectDrive,
AppScreen::DriveInfo,
AppScreen::ConfirmFlash,
AppScreen::Complete,
AppScreen::Error,
] {
let mut app = app_on(screen.clone());
let initial_idx = app.explorer_theme_idx;
let consumed = handle_key(
&mut app,
KeyEvent::new(KeyCode::Char('t'), KeyModifiers::CONTROL),
);
assert!(consumed, "Ctrl+T must be consumed on {screen:?}");
assert_eq!(
app.explorer_theme_idx,
(initial_idx + 1) % app.explorer_themes.len(),
"theme must advance on {screen:?}"
);
}
}
#[test]
fn t_wraps_theme_index_around() {
let mut app = app_on(AppScreen::SelectDrive);
let total = app.explorer_themes.len();
app.explorer_theme_idx = total - 1;
handle_key(&mut app, key(KeyCode::Char('t')));
assert_eq!(app.explorer_theme_idx, 0, "theme index must wrap to 0");
}
#[test]
fn t_and_app_palette_stay_in_sync() {
let mut app = app_on(AppScreen::SelectDrive);
handle_key(&mut app, key(KeyCode::Char('t')));
let _pal = app.palette();
let expected_name = &app.explorer_themes[app.explorer_theme_idx].0;
assert_eq!(app.current_theme_name(), expected_name.as_str());
}
#[test]
fn capital_t_opens_theme_panel_on_any_screen() {
for screen in [
AppScreen::SelectImage,
AppScreen::SelectDrive,
AppScreen::DriveInfo,
AppScreen::ConfirmFlash,
AppScreen::Complete,
AppScreen::Error,
] {
let mut app = app_on(screen.clone());
assert!(
!app.show_app_theme_panel,
"panel must start closed on {screen:?}"
);
let consumed = handle_key(&mut app, key(KeyCode::Char('T')));
assert!(consumed, "T must be consumed on {screen:?}");
assert!(app.show_app_theme_panel, "panel must open on {screen:?}");
}
}
#[test]
fn capital_t_closes_panel_when_already_open() {
let mut app = app_on(AppScreen::SelectDrive);
app.open_app_theme_panel();
assert!(app.show_app_theme_panel);
let consumed = handle_key(&mut app, key(KeyCode::Char('T')));
assert!(consumed);
assert!(!app.show_app_theme_panel, "second T must close the panel");
}
#[test]
fn open_app_theme_panel_positions_cursor_at_active_theme() {
let mut app = app_on(AppScreen::SelectDrive);
app.explorer_theme_idx = 5;
app.open_app_theme_panel();
assert_eq!(
app.app_theme_panel_cursor, 5,
"panel cursor must start at the active theme index"
);
}
#[test]
fn theme_panel_down_moves_cursor() {
let mut app = app_on(AppScreen::SelectDrive);
app.open_app_theme_panel();
let initial = app.app_theme_panel_cursor;
let consumed = handle_key(&mut app, key(KeyCode::Down));
assert!(consumed);
assert_eq!(app.app_theme_panel_cursor, initial + 1);
}
#[test]
fn theme_panel_j_moves_cursor_down() {
let mut app = app_on(AppScreen::SelectDrive);
app.open_app_theme_panel();
let initial = app.app_theme_panel_cursor;
let consumed = handle_key(&mut app, key(KeyCode::Char('j')));
assert!(consumed);
assert_eq!(app.app_theme_panel_cursor, initial + 1);
}
#[test]
fn theme_panel_up_moves_cursor() {
let mut app = app_on(AppScreen::SelectDrive);
app.explorer_theme_idx = 3;
app.open_app_theme_panel();
let initial = app.app_theme_panel_cursor;
let consumed = handle_key(&mut app, key(KeyCode::Up));
assert!(consumed);
assert_eq!(app.app_theme_panel_cursor, initial - 1);
}
#[test]
fn theme_panel_k_moves_cursor_up() {
let mut app = app_on(AppScreen::SelectDrive);
app.explorer_theme_idx = 3;
app.open_app_theme_panel();
let initial = app.app_theme_panel_cursor;
let consumed = handle_key(&mut app, key(KeyCode::Char('k')));
assert!(consumed);
assert_eq!(app.app_theme_panel_cursor, initial - 1);
}
#[test]
fn theme_panel_up_clamps_at_zero() {
let mut app = app_on(AppScreen::SelectDrive);
app.open_app_theme_panel();
app.app_theme_panel_cursor = 0;
handle_key(&mut app, key(KeyCode::Up));
assert_eq!(app.app_theme_panel_cursor, 0, "cursor must clamp at 0");
}
#[test]
fn theme_panel_down_clamps_at_last() {
let mut app = app_on(AppScreen::SelectDrive);
app.open_app_theme_panel();
let last = app.explorer_themes.len() - 1;
app.app_theme_panel_cursor = last;
handle_key(&mut app, key(KeyCode::Down));
assert_eq!(
app.app_theme_panel_cursor, last,
"cursor must clamp at last"
);
}
#[test]
fn theme_panel_enter_applies_cursor_theme_and_closes() {
let mut app = app_on(AppScreen::SelectDrive);
app.open_app_theme_panel();
app.app_theme_panel_cursor = 4;
let consumed = handle_key(&mut app, key(KeyCode::Enter));
assert!(consumed);
assert_eq!(
app.explorer_theme_idx, 4,
"active theme must update to cursor position"
);
assert!(!app.show_app_theme_panel, "panel must close after Enter");
}
#[test]
fn theme_panel_esc_closes_without_changing_theme() {
let mut app = app_on(AppScreen::SelectDrive);
app.explorer_theme_idx = 2;
app.open_app_theme_panel();
app.app_theme_panel_cursor = 7;
let consumed = handle_key(&mut app, key(KeyCode::Esc));
assert!(consumed);
assert!(!app.show_app_theme_panel, "panel must close on Esc");
assert_eq!(
app.explorer_theme_idx, 2,
"active theme must NOT change on Esc"
);
}
#[test]
fn theme_panel_navigation_does_not_affect_active_theme_until_enter() {
let mut app = app_on(AppScreen::SelectDrive);
app.explorer_theme_idx = 1;
app.open_app_theme_panel();
handle_key(&mut app, key(KeyCode::Down));
handle_key(&mut app, key(KeyCode::Down));
handle_key(&mut app, key(KeyCode::Down));
assert_eq!(
app.explorer_theme_idx, 1,
"active theme must not change while navigating the panel"
);
}
#[test]
fn theme_panel_intercepts_jk_even_on_complete_screen() {
let mut app = app_complete_with_entries(20);
app.open_app_theme_panel();
app.app_theme_panel_cursor = 3;
let scroll_before = app.contents_scroll;
handle_key(&mut app, key(KeyCode::Char('j')));
assert_eq!(
app.app_theme_panel_cursor, 4,
"j must move panel cursor down"
);
assert_eq!(
app.contents_scroll, scroll_before,
"j must NOT scroll USB contents while panel is open"
);
}
}