use crate::app::{AfterSave, Answer, AppState, Clashes, Dialog, FindFiles, PromptKind, READ_ONLY, Saved, Screen, TextInput, TransferKind};
use crate::fs_ops::{check_destinations, create_directory, create_file, create_symlink, is_plain_name, is_same_entry, path_exists, rename_path, transfer_targets, typed_path};
use crate::options::{OnExisting, SortKey};
use crate::strip::StripHit;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use ratatui::layout::Position;
use ratatui::widgets::TableState;
use std::io::Result;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
mod file_ops;
mod launch;
mod mouse;
mod panel;
mod screens;
#[cfg(test)]
mod tests;
pub(crate) use self::launch::open_with_default;
use self::{file_ops::*, launch::*, mouse::*, panel::*, screens::*};
pub fn handle_input(app_state: &mut AppState) -> Result<bool> {
if !event::poll(Duration::from_millis(100))? {
return Ok(true);
}
match event::read()? {
Event::Key(key) if key.kind != KeyEventKind::Release => return Ok(handle_key(app_state, key)),
Event::Paste(text) => handle_paste(app_state, &text),
Event::Mouse(mouse_event) => return Ok(mouse_to(app_state, mouse_event)),
_ => {}
}
Ok(true)
}
fn handle_key(app_state: &mut AppState, key: KeyEvent) -> bool {
app_state.find_note = None;
if let KeyCode::Char(c) = key.code
&& key.modifiers.contains(KeyModifiers::CONTROL) != key.modifiers.contains(KeyModifiers::ALT)
{
handle_chord(app_state, c, key.modifiers);
return true;
}
if app_state.job.is_some() {
return job_key(app_state, key);
}
if key.code == KeyCode::F(10) {
return !request_quit(app_state);
}
if app_state.error.is_some() {
if matches!(key.code, KeyCode::Esc | KeyCode::Enter) {
app_state.reset_error();
}
return true;
}
if app_state.dialog.is_some() {
return dialog_key(app_state, key);
}
match app_state.screen {
Screen::Panels => panel_key(app_state, key),
Screen::Viewer(_) => viewer_key(app_state, key),
Screen::Editor(_) => editor_key(app_state, key),
Screen::Diff(_) => diff_key(app_state, key),
}
true
}
fn handle_chord(app_state: &mut AppState, c: char, modifiers: KeyModifiers) {
let clear = app_state.dialog.is_none() && app_state.error.is_none();
let in_editor = clear && app_state.is_editing();
let in_viewer = clear && app_state.is_viewing();
let in_panel = clear && app_state.job.is_none() && matches!(app_state.screen, Screen::Panels) && !app_state.panel_busy(app_state.is_left_active);
if c == '*' && in_panel {
app_state.invert_selection(true);
} else if modifiers.contains(KeyModifiers::ALT) {
if in_panel && let Some(digit) = c.to_digit(10).filter(|&digit| digit > 0) {
let is_left = app_state.is_left_active;
let count = if is_left { app_state.tabs_left.list.len() } else { app_state.tabs_right.list.len() };
let index = if digit == 9 { count - 1 } else { digit as usize - 1 };
app_state.select_tab(is_left, index);
}
if in_panel && c == 'a' {
let items = chosen_items(app_state);
if !items.is_empty() && !app_state.refuse_in_archive(app_state.is_left_active) {
app_state.open_attributes(items);
}
}
if in_panel && c == 'c' {
copy_names(app_state, false);
}
if in_panel && c == 'i' {
app_state.same_dir_in_other_panel();
}
} else {
match c {
'f' if in_editor || in_viewer => app_state.open_prompt(PromptKind::Find),
'g' if in_editor || in_viewer => app_state.open_prompt(PromptKind::GoToLine),
'r' if in_editor => app_state.open_replace(),
's' if in_editor => {
save_then(app_state, AfterSave::Stay);
}
'a' if in_editor => app_state.editor_select_all(),
'z' if in_editor && !modifiers.contains(KeyModifiers::SHIFT) => app_state.editor_undo(),
'z' | 'Z' if in_editor => app_state.editor_redo(),
'y' | 'Y' if in_editor => app_state.editor_redo(),
'c' if in_editor => app_state.editor_copy(),
'c' if app_state.is_viewing() => app_state.viewer_copy(),
'c' if in_panel => copy_names(app_state, true),
'x' if in_editor => app_state.editor_cut(),
'v' if in_editor => app_state.editor_paste(),
'r' if !app_state.is_modal_open() => {
app_state.reload_panel(true, None);
app_state.reload_panel(false, None);
}
'l' if in_panel => app_state.open_go_to(),
'o' if in_panel => open_cursor_externally(app_state),
'd' if in_panel => app_state.open_bookmarks(),
'd' if app_state.error.is_none() && matches!(app_state.dialog, Some(Dialog::Bookmarks { .. })) => app_state.dialog = None,
'u' if in_panel => app_state.swap_panels(),
't' if in_panel => app_state.new_tab(),
'w' if in_panel => {
let is_left = app_state.is_left_active;
let active = if is_left { app_state.tabs_left.active } else { app_state.tabs_right.active };
app_state.close_tab(is_left, active);
}
_ => {}
}
}
}
fn job_key(app_state: &mut AppState, key: KeyEvent) -> bool {
let Some(job) = app_state.job.as_mut() else {
return true;
};
let clash = job.clash.is_some();
match key.code {
KeyCode::Char('y') | KeyCode::Char('Y') if clash => job.answer(Answer::Overwrite),
KeyCode::Char('n') | KeyCode::Char('N') if clash => job.answer(Answer::Skip),
KeyCode::Char('a') | KeyCode::Char('A') if clash => job.answer(Answer::OverwriteAll),
KeyCode::Char('s') | KeyCode::Char('S') if clash => job.answer(Answer::SkipAll),
KeyCode::Char('o') | KeyCode::Char('O') if clash => job.answer(Answer::OverwriteOlder),
KeyCode::Char('r') | KeyCode::Char('R') if job.problem.is_some() => job.answer(Answer::Retry),
KeyCode::Char('s') | KeyCode::Char('S') if job.problem.is_some() => job.answer(Answer::Skip),
KeyCode::Char('a') | KeyCode::Char('A') if job.problem.is_some() => job.answer(Answer::SkipAll),
KeyCode::Char('d') | KeyCode::Char('D') if job.problem.is_some() && job.kind == TransferKind::Trash => job.answer(Answer::Delete),
KeyCode::Esc => app_state.cancel_transfer(),
KeyCode::F(10) if app_state.quit_armed => return false,
KeyCode::F(10) => app_state.quit_armed = true,
_ => {}
}
true
}
fn dialog_key(app_state: &mut AppState, key: KeyEvent) -> bool {
let code = key.code;
let yes = matches!(code, KeyCode::Enter | KeyCode::Char('y') | KeyCode::Char('Y'));
let finding = match &app_state.dialog {
Some(Dialog::FindFiles(find)) => Some(matches!(**find, FindFiles::Asking { .. })),
_ => None,
};
let no = matches!(code, KeyCode::Esc | KeyCode::Char('n') | KeyCode::Char('N'));
match app_state.dialog.as_ref() {
None => {}
Some(Dialog::Help) => {
let page = app_state.help_room.max(1) as isize;
match code {
KeyCode::Esc | KeyCode::F(1) => app_state.dialog = None,
KeyCode::Down => scroll_help(app_state, 1),
KeyCode::Up => scroll_help(app_state, -1),
KeyCode::PageDown => scroll_help(app_state, page),
KeyCode::PageUp => scroll_help(app_state, -page),
KeyCode::Home => scroll_help(app_state, isize::MIN),
KeyCode::End => scroll_help(app_state, isize::MAX),
_ => {}
}
}
Some(Dialog::Options { editing: Some(_) }) => match code {
KeyCode::Esc => app_state.options_cancel_edit(),
KeyCode::Enter => app_state.options_commit_edit(),
_ => edit_text(app_state, code),
},
Some(Dialog::Options { editing: None }) => match code {
KeyCode::Esc | KeyCode::F(11) => app_state.dialog = None,
KeyCode::Up => app_state.options_move(false),
KeyCode::Down => app_state.options_move(true),
KeyCode::Enter | KeyCode::Char(' ') | KeyCode::Right => app_state.options_change(true),
KeyCode::Left => app_state.options_change(false),
_ => {}
},
Some(Dialog::Rename(_)) => match code {
KeyCode::Esc | KeyCode::F(2) => app_state.dialog = None,
KeyCode::Enter => handle_rename(app_state),
_ => edit_text(app_state, code),
},
Some(Dialog::Create { .. }) => match code {
KeyCode::Esc => app_state.dialog = None,
KeyCode::F(7) => toggle_create(app_state, true),
KeyCode::F(4) if key.modifiers.contains(KeyModifiers::SHIFT) => toggle_create(app_state, false),
KeyCode::Enter => handle_create_confirm(app_state),
_ => edit_text(app_state, code),
},
Some(Dialog::Delete { .. }) if yes => handle_delete_confirm(app_state),
Some(Dialog::Transfer { .. }) => match code {
KeyCode::Enter => handle_transfer_confirm(app_state),
KeyCode::Esc => app_state.dialog = None,
_ => edit_text(app_state, code),
},
Some(Dialog::Symlink { .. }) => match code {
KeyCode::Enter => handle_symlink_confirm(app_state),
KeyCode::Esc => app_state.dialog = None,
_ => edit_text(app_state, code),
},
Some(Dialog::LargeFile(_)) if yes => app_state.confirm_large_file(),
Some(Dialog::Delete { .. } | Dialog::LargeFile(_)) if no => app_state.dialog = None,
Some(&Dialog::SaveChanges { then_quit }) => match code {
KeyCode::Char('y') | KeyCode::Char('Y') => {
app_state.dialog = None;
return save_then(app_state, if then_quit { AfterSave::Quit } else { AfterSave::Close });
}
KeyCode::Char('n') | KeyCode::Char('N') => {
app_state.dialog = None;
app_state.close_editor();
return !then_quit;
}
KeyCode::Esc => app_state.dialog = None,
_ => {}
},
Some(Dialog::Replacing { replaced, .. }) => {
let replaced = *replaced;
match code {
KeyCode::Char('y') | KeyCode::Char('Y') => {
app_state.replace_step(true);
}
KeyCode::Char('n') | KeyCode::Char('N') => {
app_state.replace_step(false);
}
KeyCode::Char('a') | KeyCode::Char('A') => app_state.replace_all(),
KeyCode::Esc => app_state.finish_replace(replaced),
_ => {}
}
}
Some(&Dialog::DiskChanged { then, now, .. }) => match code {
KeyCode::Char('o') | KeyCode::Char('O') => {
app_state.dialog = None;
match app_state.editor_save_over() {
Ok(()) => return after_save(app_state, then),
Err(e) => app_state.display_error(e),
}
}
KeyCode::Char('r') | KeyCode::Char('R') if now.is_some() => {
app_state.dialog = None;
if let Err(e) = app_state.editor_reload() {
app_state.display_error(e);
}
}
KeyCode::Esc | KeyCode::Char('n') | KeyCode::Char('N') => app_state.dialog = None,
_ => {}
},
Some(Dialog::Attributes(_)) => match code {
KeyCode::Esc => app_state.dialog = None,
KeyCode::Enter => app_state.apply_attributes(),
KeyCode::Tab | KeyCode::Down => app_state.attributes_switch_field(true),
KeyCode::BackTab | KeyCode::Up => app_state.attributes_switch_field(false),
_ => edit_text(app_state, code),
},
Some(Dialog::Bookmarks { .. }) => {
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
match code {
KeyCode::Esc => app_state.dialog = None,
KeyCode::Enter => app_state.go_to_bookmark(),
KeyCode::Up if shift => app_state.shift_bookmark(true),
KeyCode::Down if shift => app_state.shift_bookmark(false),
KeyCode::Up => app_state.move_bookmark_cursor(-1),
KeyCode::Down => app_state.move_bookmark_cursor(1),
KeyCode::Home => app_state.move_bookmark_cursor(isize::MIN),
KeyCode::End => app_state.move_bookmark_cursor(isize::MAX),
KeyCode::Insert | KeyCode::Char('a') | KeyCode::Char('A') => app_state.add_bookmark(),
KeyCode::Delete | KeyCode::Char('d') | KeyCode::Char('D') => app_state.remove_bookmark(),
KeyCode::Char(c @ '1'..='9') => {
let wanted = c as usize - '1' as usize;
if let Some(Dialog::Bookmarks { list, index }) = &mut app_state.dialog
&& wanted < list.len()
{
*index = wanted;
app_state.go_to_bookmark();
}
}
_ => {}
}
}
Some(Dialog::Branches(_)) => match code {
KeyCode::Esc => app_state.dialog = None,
KeyCode::Enter => app_state.switch_branch(),
KeyCode::Up => app_state.move_branch_cursor(-1),
KeyCode::Down => app_state.move_branch_cursor(1),
KeyCode::PageUp => app_state.move_branch_cursor(-10),
KeyCode::PageDown => app_state.move_branch_cursor(10),
KeyCode::Home => app_state.move_branch_cursor(isize::MIN),
KeyCode::End => app_state.move_branch_cursor(isize::MAX),
_ => {}
},
Some(Dialog::DrivePicker { .. }) => match code {
KeyCode::Esc => app_state.dialog = None,
KeyCode::Enter => app_state.confirm_drive_picker(),
KeyCode::F(1) | KeyCode::F(2) | KeyCode::Right | KeyCode::Down => app_state.move_drive_picker(true),
KeyCode::Left | KeyCode::Up => app_state.move_drive_picker(false),
_ => {}
},
Some(Dialog::FindFiles(_)) if finding == Some(true) => match code {
KeyCode::Esc => app_state.dialog = None,
KeyCode::Enter => app_state.start_find_files(),
KeyCode::Tab | KeyCode::BackTab | KeyCode::Up | KeyCode::Down => app_state.find_files_switch_field(),
_ => edit_text(app_state, code),
},
Some(Dialog::FindFiles(_)) => {
let page = app_state.find_files_page.max(1) as isize;
match code {
KeyCode::Esc => app_state.dialog = None,
KeyCode::Enter => app_state.find_files_go(),
KeyCode::Up => app_state.find_files_move(-1),
KeyCode::Down => app_state.find_files_move(1),
KeyCode::PageUp => app_state.find_files_move(-page),
KeyCode::PageDown => app_state.find_files_move(page),
KeyCode::Home => app_state.find_files_move(isize::MIN),
KeyCode::End => app_state.find_files_move(isize::MAX),
_ => {}
}
}
Some(&Dialog::Prompt(kind, _)) => match code {
KeyCode::Esc => app_state.dialog = None,
KeyCode::Enter => app_state.confirm_prompt(),
KeyCode::Char(c) if kind == PromptKind::GoToLine && !c.is_ascii_digit() => {}
KeyCode::Tab if kind == PromptKind::GoTo => app_state.complete_go_to(),
_ => edit_text(app_state, code),
},
Some(_) => {}
}
true
}
fn edit_text(app_state: &mut AppState, code: KeyCode) {
let Some(input) = app_state.dialog_input() else {
return;
};
match code {
KeyCode::Char(c) => input.insert(c),
KeyCode::Backspace => input.backspace(),
KeyCode::Delete => input.delete_forward(),
KeyCode::Left => input.move_left(),
KeyCode::Right => input.move_right(),
KeyCode::Home => input.move_home(),
KeyCode::End => input.move_end(),
_ => {}
}
}
fn handle_paste(app_state: &mut AppState, text: &str) {
if app_state.error.is_some() {
return;
}
if let Some(input) = app_state.dialog_input() {
for character in text.chars().filter(|character| !character.is_control()) {
input.insert(character);
}
} else if app_state.dialog.is_none() && app_state.is_editing() {
app_state.editor_insert_text(text);
}
}
fn scroll_help(app_state: &mut AppState, lines: isize) {
let last = crate::ui::help_lines().len().saturating_sub(app_state.help_room);
app_state.help_scroll = app_state.help_scroll.saturating_add_signed(lines).min(last);
}
fn save_then(app_state: &mut AppState, then: AfterSave) -> bool {
match app_state.editor_save() {
Ok(Saved::Done) => after_save(app_state, then),
Ok(Saved::ChangedOnDisk { opened, now }) => {
app_state.dialog = Some(Dialog::DiskChanged { then, opened, now });
true
}
Err(e) => {
app_state.display_error(e);
true
}
}
}
fn after_save(app_state: &mut AppState, then: AfterSave) -> bool {
match then {
AfterSave::Stay => true,
AfterSave::Close => {
app_state.close_editor();
true
}
AfterSave::Quit => {
app_state.close_editor();
false
}
}
}
fn request_quit(app_state: &mut AppState) -> bool {
if !app_state.editor_is_modified() {
return true;
}
app_state.reset_error();
app_state.dialog = Some(Dialog::SaveChanges { then_quit: true });
false
}