use crate::app::NavState;
use crate::app::actions::{ActionMode, InputMode};
use crate::app::keymap::{FileAction, NavAction, PrefixCommand};
use crate::app::state::{AppState, KeypressResult};
use crate::core::FileInfo;
use crate::core::proc::{complete_dirs_with_fd, fd_binary};
use crate::ui::overlays::Overlay;
use crate::utils::{
clean_display_path, expand_home_path, expand_home_path_buf, get_home, normalize_relative_path,
open_in_editor,
};
use crossterm::event::{KeyCode::*, KeyEvent};
use std::path::MAIN_SEPARATOR;
use std::time::{Duration, Instant};
impl<'a> AppState<'a> {
pub(crate) fn handle_input_mode(&mut self, key: KeyEvent) -> KeypressResult {
let mode = if let ActionMode::Input { mode, .. } = &self.actions().mode() {
*mode
} else {
return KeypressResult::Continue;
};
match key.code {
Enter => {
match mode {
InputMode::NewFile => self.create_file(),
InputMode::NewFolder => self.create_folder(),
InputMode::Rename => self.rename_entry(),
InputMode::Filter => self.apply_filter(),
InputMode::ConfirmDelete { .. } => self.confirm_delete(),
InputMode::Find => self.handle_find(),
InputMode::MoveFile => self.handle_move(),
InputMode::GoToPath => self.handle_go_to_path(),
}
self.exit_input_mode();
KeypressResult::Consumed
}
Esc => {
self.exit_input_mode();
KeypressResult::Consumed
}
Left => {
self.actions.action_move_cursor_left();
KeypressResult::Consumed
}
Up => {
if matches!(mode, InputMode::Find) {
self.actions.find_state_mut().select_prev();
KeypressResult::Consumed
} else {
KeypressResult::Continue
}
}
Down => {
if matches!(mode, InputMode::Find) {
self.actions.find_state_mut().select_next();
KeypressResult::Consumed
} else {
KeypressResult::Continue
}
}
Right => {
self.actions.action_move_cursor_right();
KeypressResult::Consumed
}
Home => {
self.actions.action_cursor_home();
KeypressResult::Consumed
}
End => {
self.actions.action_cursor_end();
KeypressResult::Consumed
}
Backspace => {
self.actions.action_backspace_at_cursor();
if matches!(mode, InputMode::Filter) {
self.apply_filter();
}
if matches!(mode, InputMode::Find) {
self.actions.find_debounce(Duration::from_millis(90));
}
KeypressResult::Consumed
}
Tab => {
if matches!(mode, InputMode::MoveFile | InputMode::GoToPath) {
if fd_binary().is_ok() {
self.tab_autocomplete();
KeypressResult::Consumed
} else {
KeypressResult::Continue
}
} else {
KeypressResult::Continue
}
}
Char(c) => match mode {
InputMode::ConfirmDelete { .. } => {
self.process_confirm_delete_char(c);
KeypressResult::Consumed
}
InputMode::Filter => {
self.actions.action_insert_at_cursor(c);
self.apply_filter();
KeypressResult::Consumed
}
InputMode::Rename | InputMode::NewFile | InputMode::NewFolder => {
self.actions.action_insert_at_cursor(c);
KeypressResult::Consumed
}
InputMode::Find => {
self.actions.action_insert_at_cursor(c);
self.actions.find_debounce(Duration::from_millis(120));
KeypressResult::Consumed
}
InputMode::MoveFile => {
self.actions.action_insert_at_cursor(c);
KeypressResult::Consumed
}
InputMode::GoToPath => {
self.actions.action_insert_at_cursor(c);
KeypressResult::Consumed
}
},
_ => KeypressResult::Consumed,
}
}
pub(crate) fn handle_nav_action(&mut self, action: NavAction) -> KeypressResult {
match action {
NavAction::GoUp => {
self.move_nav_if_possible(|nav| nav.move_up());
self.refresh_show_info_if_open();
}
NavAction::GoDown => {
self.move_nav_if_possible(|nav| nav.move_down());
self.refresh_show_info_if_open();
}
NavAction::GoParent => {
let res = self.handle_go_parent();
self.refresh_show_info_if_open();
return res;
}
NavAction::GoIntoDir => {
let res = self.handle_go_into_dir();
self.refresh_show_info_if_open();
return res;
}
NavAction::ToggleMarker => {
let marker_jump = self.config.display().toggle_marker_jump();
let clipboard = self.actions.clipboard_mut();
self.nav.toggle_marker_advance(clipboard, marker_jump);
self.request_preview();
}
NavAction::ClearMarker => {
self.nav.clear_markers();
self.request_preview();
}
NavAction::ClearFilter => {
self.nav.clear_filters();
self.request_preview();
}
_ => {}
}
KeypressResult::Continue
}
pub(crate) fn handle_file_action(&mut self, action: FileAction) -> KeypressResult {
match action {
FileAction::Open => return self.handle_open_file(),
FileAction::Delete => {
let is_trash = self.config.general().move_to_trash();
self.prompt_delete(is_trash);
}
FileAction::AlternateDelete => {
let is_trash = !self.config.general().move_to_trash();
self.prompt_delete(is_trash);
}
FileAction::Copy => {
self.actions.action_copy(&self.nav, false);
self.handle_timed_message(Duration::from_secs(15));
}
FileAction::Paste => {
let fileop_tx = self.workers.fileop_tx();
self.actions.action_paste(&mut self.nav, fileop_tx);
}
FileAction::Rename => self.prompt_rename(),
FileAction::Create => self.prompt_create_file(),
FileAction::CreateDirectory => self.prompt_create_folder(),
FileAction::Filter => self.prompt_filter(),
FileAction::ShowInfo => self.toggle_file_info(),
FileAction::Find => self.prompt_find(),
FileAction::MoveFile => self.prompt_move(),
}
KeypressResult::Continue
}
pub(crate) fn handle_prefix_action(&mut self, prefix: PrefixCommand) -> bool {
match prefix {
PrefixCommand::Nav(NavAction::GoToTop) => self.handle_go_to_top(),
PrefixCommand::Nav(NavAction::GoToHome) => self.handle_go_to_home(),
PrefixCommand::Nav(NavAction::GoToPath) => self.prompt_go_to_path(),
_ => return false,
}
true
}
pub(crate) fn handle_prefix_key(&mut self, key: &KeyEvent) -> Option<PrefixCommand> {
if self.actions.is_input_mode() {
return None;
}
let gmap = self.keymap.gmap();
let (started, exited, result) = {
let prefix = self.actions.prefix_recognizer_mut();
let result = prefix.feed(key, gmap);
(prefix.started_prefix(), prefix.exited_prefix(), result)
};
if started {
self.show_prefix_help();
}
if exited {
self.hide_prefix_help();
}
result
}
pub(crate) fn enter_input_mode(
&mut self,
mode: InputMode,
prompt: String,
initial: Option<String>,
) {
let buffer = initial.unwrap_or_default();
self.actions
.enter_mode(ActionMode::Input { mode, prompt }, buffer);
}
fn move_nav_if_possible<F>(&mut self, f: F)
where
F: FnOnce(&mut NavState) -> bool,
{
if f(&mut self.nav) {
if self.config.display().instant_preview() {
self.request_preview();
} else {
self.preview.mark_pending();
}
}
}
fn handle_go_parent(&mut self) -> KeypressResult {
let current = self.nav.current_dir();
let Some(parent) = current.parent() else {
return KeypressResult::Continue;
};
let parent_path = parent.to_path_buf();
if std::fs::metadata(&parent_path).is_err() {
self.push_overlay_message(
"Parent directory is unreachable".to_string(),
Duration::from_secs(3),
);
return KeypressResult::Consumed;
}
let exited_name = current.file_name().map(|n| n.to_os_string());
self.nav.save_position();
self.nav.set_path(parent_path);
self.request_dir_load(exited_name);
self.request_parent_content();
KeypressResult::Continue
}
fn handle_go_into_dir(&mut self) -> KeypressResult {
let Some(entry) = self.nav.selected_shown_entry() else {
return KeypressResult::Continue;
};
let entry_path = self.nav.current_dir().join(entry.name());
let Ok(meta) = std::fs::metadata(&entry_path) else {
return KeypressResult::Continue;
};
if !meta.is_dir() {
return KeypressResult::Continue;
}
match std::fs::read_dir(&entry_path) {
Ok(_) => {
self.nav.save_position();
self.nav.set_path(entry_path);
self.request_dir_load(None);
self.request_parent_content();
KeypressResult::Continue
}
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
let msg = format!("Permission Denied: {}", e);
self.push_overlay_message(msg, std::time::Duration::from_secs(3));
KeypressResult::Consumed
}
Err(_) => KeypressResult::Continue,
}
}
fn handle_open_file(&mut self) -> KeypressResult {
let editor = self.config.editor();
if !editor.exists() {
let msg = format!("Editor '{}' not found", editor.cmd());
self.push_overlay_message(msg, Duration::from_secs(3));
return KeypressResult::Continue;
}
if let Some(entry) = self.nav.selected_shown_entry() {
let path = self.nav.current_dir().join(entry.name());
match open_in_editor(self.config.editor(), &path) {
Ok(_) => {
self.request_preview();
KeypressResult::OpenedEditor
}
Err(e) => {
let error_msg = e.to_string();
self.push_overlay_message(error_msg, Duration::from_secs(3));
KeypressResult::Recovered
}
}
} else {
KeypressResult::Continue
}
}
fn handle_find(&mut self) {
let Some(r) = self
.actions
.find_results()
.get(self.actions.find_selected())
else {
return;
};
let path = r.path();
let (target_dir, focus) = if path.is_dir() {
(path.to_path_buf(), None)
} else {
let Some(parent) = path.parent() else {
return;
};
let file_name = path.file_name().map(|n| n.to_os_string());
(parent.to_path_buf(), file_name)
};
if let Err(e) = std::fs::read_dir(&target_dir) {
let msg = format!("Access Denied: {}", e);
self.push_overlay_message(msg, Duration::from_secs(3));
return;
}
self.nav.save_position();
self.nav.set_path(target_dir);
self.request_dir_load(focus);
self.request_parent_content();
self.exit_input_mode();
}
fn handle_move(&mut self) {
let dest_dir = self.actions.input_buffer();
if dest_dir.trim().is_empty() {
self.push_overlay_message(
"Move failed: target directory cannot be empty".to_string(),
Duration::from_secs(3),
);
return;
}
let input_path = expand_home_path_buf(dest_dir.trim());
let resolved_path = if input_path.is_absolute() {
input_path
} else {
self.nav.current_dir().join(input_path)
};
let absolute_dest = match resolved_path.canonicalize() {
Ok(p) => p,
Err(e) => {
let norm_msg = normalize_relative_path(&resolved_path);
self.push_overlay_message(
format!("Move failed: {}: {}", e, norm_msg),
Duration::from_secs(3),
);
return;
}
};
if !absolute_dest.is_dir() {
self.push_overlay_message(
"Move failed: not a directory".into(),
Duration::from_secs(3),
);
return;
}
if let Err(e) = std::fs::read_dir(&absolute_dest) {
let norm_msg = normalize_relative_path(&absolute_dest);
self.push_overlay_message(
format!("Move failed: Permission denied in {}: {}", norm_msg, e),
Duration::from_secs(3),
);
return;
}
if !absolute_dest.is_dir() {
let norm_msg = normalize_relative_path(&absolute_dest);
self.push_overlay_message(
format!("Move failed: not a directory: {}", norm_msg),
Duration::from_secs(3),
);
return;
}
let targets = self.nav.get_action_targets();
for src in &targets {
if let Ok(absolute_src) = src.canonicalize()
&& absolute_dest.starts_with(&absolute_src)
{
let msg = if absolute_dest == absolute_src {
"Move failed: source and destination are the same".to_string()
} else {
let normalized = normalize_relative_path(&absolute_src);
let display_path = clean_display_path(&normalized);
format!(
"Move failed: cannot move directory into its own sub directory: {}",
display_path
)
};
self.push_overlay_message(msg, Duration::from_secs(3));
return;
}
}
let fileop_tx = self.workers.fileop_tx();
let move_msg = format!(
"Files moved to: {}",
clean_display_path(&absolute_dest.to_string_lossy())
);
self.actions
.actions_move(&mut self.nav, absolute_dest, fileop_tx);
self.exit_input_mode();
self.push_overlay_message(move_msg, Duration::from_secs(3));
}
fn handle_go_to_home(&mut self) {
if let Some(home_path) = get_home() {
self.nav.save_position();
self.nav.set_path(home_path.clone());
self.request_dir_load(None);
self.request_parent_content();
}
}
fn handle_go_to_path(&mut self) {
let path = self.actions.input_buffer();
if path.trim().is_empty() {
self.push_overlay_message("Error: No path entered".to_string(), Duration::from_secs(3));
return;
}
let expaned = expand_home_path_buf(path);
let abs_path = if expaned.is_absolute() {
expaned
} else {
self.nav.current_dir().join(expaned)
};
if let Ok(meta) = std::fs::metadata(&abs_path) {
if meta.is_dir() {
self.nav.save_position();
self.nav.set_path(abs_path.clone());
self.request_dir_load(None);
self.request_parent_content();
} else {
self.push_overlay_message(
"Error: Not a directory".to_string(),
Duration::from_secs(3),
);
}
} else {
self.push_overlay_message("Error: Invalid path".to_string(), Duration::from_secs(3));
}
}
fn handle_go_to_top(&mut self) {
self.nav.first_selected();
self.request_preview();
}
fn handle_timed_message(&mut self, duration: Duration) {
self.notification_time = Some(Instant::now() + duration);
}
fn process_confirm_delete_char(&mut self, c: char) {
if matches!(c, 'y' | 'Y') {
self.confirm_delete();
}
self.exit_input_mode();
}
fn exit_input_mode(&mut self) {
self.actions.exit_mode();
}
fn create_file(&mut self) {
if !self.actions.input_buffer().is_empty() {
let fileop_tx = self.workers.fileop_tx();
self.actions.action_create(&mut self.nav, false, fileop_tx);
}
}
fn create_folder(&mut self) {
if !self.actions.input_buffer().is_empty() {
let fileop_tx = self.workers.fileop_tx();
self.actions.action_create(&mut self.nav, true, fileop_tx);
}
}
fn rename_entry(&mut self) {
let fileop_tx = self.workers.fileop_tx();
self.actions.action_rename(&mut self.nav, fileop_tx);
}
fn apply_filter(&mut self) {
self.actions.action_filter(&mut self.nav);
self.request_preview();
}
fn confirm_delete(&mut self) {
let move_to_trash = if let ActionMode::Input {
mode: InputMode::ConfirmDelete { is_trash },
..
} = self.actions.mode()
{
*is_trash
} else {
self.config.general().move_to_trash()
};
let fileop_tx = self.workers.fileop_tx();
self.actions
.action_delete(&mut self.nav, fileop_tx, move_to_trash);
}
fn prompt_delete(&mut self, is_trash: bool) {
let targets = self.nav.get_action_targets();
let count = targets.len();
if targets.is_empty() {
return;
}
let action_word = if is_trash { "Trash" } else { "Delete" };
let item_label = if count > 1 {
format!("{} items", count)
} else {
"item".to_string()
};
let prompt_text = format!("{} {}? [Y/n]", action_word, item_label);
self.enter_input_mode(InputMode::ConfirmDelete { is_trash }, prompt_text, None);
}
fn prompt_rename(&mut self) {
if let Some(entry) = self.nav.selected_shown_entry() {
let name = entry.name().to_string_lossy().to_string();
self.enter_input_mode(InputMode::Rename, "Rename: ".to_string(), Some(name));
}
}
fn prompt_create_file(&mut self) {
self.enter_input_mode(InputMode::NewFile, "New File: ".to_string(), None);
}
fn prompt_create_folder(&mut self) {
self.enter_input_mode(InputMode::NewFolder, "New Folder: ".to_string(), None);
}
fn prompt_filter(&mut self) {
let current_filter = self.nav.filter().to_string();
self.enter_input_mode(
InputMode::Filter,
"Filter: ".to_string(),
Some(current_filter),
);
}
fn prompt_find(&mut self) {
if fd_binary().is_err() {
self.push_overlay_message(
"Fuzzy Find requires the `fd` tool.".to_string(),
Duration::from_secs(5),
);
return;
}
self.enter_input_mode(InputMode::Find, "".to_string(), None);
}
fn prompt_move(&mut self) {
let prompt = "Move to directory: ".to_string();
self.enter_input_mode(InputMode::MoveFile, prompt, None);
}
fn prompt_go_to_path(&mut self) {
self.enter_input_mode(InputMode::GoToPath, "Go To Path:".to_string(), None);
}
pub(crate) fn refresh_show_info_if_open(&mut self) {
let maybe_idx = self
.overlays()
.find_index(|o| matches!(o, Overlay::ShowInfo { .. }));
if let Some(i) = maybe_idx
&& let Some(entry) = self.nav.selected_shown_entry()
{
let path = self.nav.current_dir().join(entry.name());
if let Ok(file_info) = FileInfo::get_file_info(&path)
&& let Some(Overlay::ShowInfo { info }) = self.overlays_mut().get_mut(i)
{
*info = file_info;
}
}
}
fn show_file_info(&mut self) {
if let Some(entry) = self.nav.selected_shown_entry() {
let path = self.nav.current_dir().join(entry.name());
if let Ok(file_info) = FileInfo::get_file_info(&path) {
self.overlays_mut()
.push(Overlay::ShowInfo { info: file_info });
}
}
}
fn toggle_file_info(&mut self) {
let is_open = self
.overlays()
.iter()
.any(|o| matches!(o, Overlay::ShowInfo { .. }));
if is_open {
self.overlays_mut()
.retain(|o| !matches!(o, Overlay::ShowInfo { .. }));
} else {
self.show_file_info();
}
}
fn show_prefix_help(&mut self) {
if !matches!(self.overlays().top(), Some(Overlay::PreifxHelp)) {
self.overlays_mut().push(Overlay::PreifxHelp);
}
}
pub(crate) fn hide_prefix_help(&mut self) {
if matches!(self.overlays().top(), Some(Overlay::PreifxHelp)) {
self.overlays_mut().pop();
}
}
fn tab_autocomplete(&mut self) {
if fd_binary().is_err() {
return;
}
let input = self.actions.input_buffer().to_string();
let expanded = expand_home_path(input.trim());
let (base_dir, prefix) = if let Some(idx) = expanded.rfind(MAIN_SEPARATOR) {
let (base, frag) = expanded.split_at(idx + 1);
(std::path::Path::new(base), frag)
} else {
(self.nav.current_dir(), expanded.as_str())
};
let show_hidden = self.config.general().show_hidden();
let suggestion_opt = {
let ac = self.actions.autocomplete_mut();
let needs_update = ac.last_input() != input || ac.suggestions().is_empty();
if needs_update {
let suggestions =
complete_dirs_with_fd(base_dir, prefix, show_hidden).unwrap_or_default();
ac.update(suggestions, &input);
}
let suggestion = ac.current().cloned();
if suggestion.is_some() {
ac.advance();
}
suggestion
};
if let Some(suggestion) = suggestion_opt {
let mut completed_path = base_dir.to_path_buf();
completed_path.push(&suggestion);
let mut out = completed_path.to_string_lossy().to_string();
if !out.ends_with(MAIN_SEPARATOR) {
out.push(MAIN_SEPARATOR);
}
self.actions.set_input_buffer(out);
}
}
pub(crate) fn push_overlay_message(&mut self, text: String, duration: Duration) {
self.notification_time = Some(Instant::now() + duration);
if matches!(self.overlays.top(), Some(Overlay::Message { .. })) {
self.overlays_mut().pop();
}
self.overlays_mut().push(Overlay::Message { text });
}
}