use anyhow::Result;
use crossterm::event::{
Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
};
use crate::app::Status;
use crate::config::Bindings;
use crate::event::{EventAction, FmEvents};
use crate::modes::{
Direction as FuzzyDirection, Display, InputSimple, LeaveMenu, MarkAction, Menu, Navigate,
};
pub struct EventDispatcher {
binds: Bindings,
}
impl EventDispatcher {
pub fn new(binds: Bindings) -> Self {
Self { binds }
}
pub fn dispatch(&self, status: &mut Status, ev: FmEvents) -> Result<()> {
match ev {
FmEvents::Term(Event::Paste(pasted)) => EventAction::paste(status, pasted),
FmEvents::Term(Event::Key(key)) => self.match_key_event(status, key),
FmEvents::Term(Event::Mouse(mouse)) => self.match_mouse_event(status, mouse),
FmEvents::Term(Event::Resize(width, height)) => {
EventAction::resize(status, width, height)
}
FmEvents::BulkExecute => EventAction::bulk_confirm(status),
FmEvents::Refresh => EventAction::refresh_if_needed(status),
FmEvents::FileCopied(done_copy_moves) => EventAction::file_copied(status, done_copy_moves),
FmEvents::UpdateTick => EventAction::check_preview_fuzzy_tick(status),
FmEvents::Action(action) => action.matcher(status, &self.binds),
FmEvents::Ipc(msg) => EventAction::parse_rpc(status, msg),
_ => Ok(()),
}
}
fn match_key_event(&self, status: &mut Status, key: KeyEvent) -> Result<()> {
match key {
KeyEvent {
code: KeyCode::Char(c),
modifiers,
kind: _,
state: _,
} if !status.focus.is_file() && modifier_is_shift_or_none(modifiers) => {
self.menu_char_key_matcher(status, c)?
}
KeyEvent {
code: KeyCode::Char('h'),
modifiers: KeyModifiers::ALT,
kind: _,
state: _,
} if !status.focus.is_file() => status.open_picker()?,
key => self.file_key_matcher(status, key)?,
};
Ok(())
}
fn match_mouse_event(&self, status: &mut Status, mouse_event: MouseEvent) -> Result<()> {
match mouse_event.kind {
MouseEventKind::ScrollUp => {
EventAction::wheel_up(status, mouse_event.row, mouse_event.column)
}
MouseEventKind::ScrollDown => {
EventAction::wheel_down(status, mouse_event.row, mouse_event.column)
}
MouseEventKind::Down(MouseButton::Left) => {
EventAction::left_click(status, &self.binds, mouse_event.row, mouse_event.column)
}
MouseEventKind::Down(MouseButton::Middle) => {
EventAction::middle_click(status, &self.binds, mouse_event.row, mouse_event.column)
}
MouseEventKind::Down(MouseButton::Right) => {
EventAction::right_click(status, &self.binds, mouse_event.row, mouse_event.column)
}
MouseEventKind::Moved => {
EventAction::focus_follow_mouse(status, mouse_event.row, mouse_event.column)
}
_ => Ok(()),
}
}
fn file_key_matcher(&self, status: &mut Status, key: KeyEvent) -> Result<()> {
if matches!(status.current_tab().display_mode, Display::Fuzzy) {
if let Ok(success) = self.fuzzy_matcher(status, key) {
if success {
return Ok(());
}
}
}
let Some(action) = self.binds.get(&key) else {
return Ok(());
};
action.matcher(status, &self.binds)
}
fn fuzzy_matcher(&self, status: &mut Status, key: KeyEvent) -> Result<bool> {
let Some(fuzzy) = &mut status.fuzzy else {
status
.current_tab_mut()
.set_display_mode(Display::Directory);
status.refresh_status()?;
return Ok(false);
};
match key {
KeyEvent {
code: KeyCode::Char(mut c),
modifiers,
kind: _,
state: _,
} if modifier_is_shift_or_none(modifiers) => {
c = to_correct_case(c, modifiers);
fuzzy.input.insert(c);
fuzzy.update_input(true);
Ok(true)
}
key => self.fuzzy_key_matcher(status, key),
}
}
#[rustfmt::skip]
fn fuzzy_key_matcher(&self, status: &mut Status, key: KeyEvent) -> Result<bool> {
if let KeyEvent{code:KeyCode::Char(' '), modifiers: KeyModifiers::CONTROL, kind:_, state:_} = key {
status.fuzzy_toggle_flag_selected()?;
return Ok(true);
}
if let KeyEvent{code:KeyCode::Enter, modifiers: KeyModifiers::ALT, kind:_, state:_} = key {
status.fuzzy_open_file()?;
return Ok(true);
}
let KeyEvent {
code,
modifiers: KeyModifiers::NONE,
kind: _,
state: _,
} = key
else {
return Ok(false);
};
match code {
KeyCode::Enter => status.fuzzy_select()?,
KeyCode::Esc => status.fuzzy_leave()?,
KeyCode::Backspace => status.fuzzy_backspace()?,
KeyCode::Delete => status.fuzzy_delete()?,
KeyCode::Left => status.fuzzy_left()?,
KeyCode::Right => status.fuzzy_right()?,
KeyCode::Up => status.fuzzy_navigate(FuzzyDirection::Up)?,
KeyCode::Down => status.fuzzy_navigate(FuzzyDirection::Down)?,
KeyCode::PageUp => status.fuzzy_navigate(FuzzyDirection::PageUp)?,
KeyCode::PageDown => status.fuzzy_navigate(FuzzyDirection::PageDown)?,
_ => return Ok(false),
}
Ok(true)
}
fn menu_char_key_matcher(&self, status: &mut Status, c: char) -> Result<()> {
let tab = status.current_tab_mut();
match tab.menu_mode {
Menu::InputSimple(InputSimple::Sort) => status.sort_by_char(c),
Menu::InputSimple(InputSimple::RegexMatch) => status.input_regex(c),
Menu::InputSimple(InputSimple::Filter) => status.input_filter(c),
Menu::InputSimple(_) => status.menu.input_insert(c),
Menu::InputCompleted(input_completed) => status.input_and_complete(input_completed, c),
Menu::NeedConfirmation(confirmed_action) => status.confirm(c, confirmed_action),
Menu::Navigate(navigate) => self.navigate_char(navigate, status, c),
_ if matches!(tab.display_mode, Display::Preview) => tab.reset_display_mode_and_view(),
Menu::Nothing => Ok(()),
}
}
fn navigate_char(&self, navigate: Navigate, status: &mut Status, c: char) -> Result<()> {
match navigate {
Navigate::Trash if c == 'x' => status.menu.trash_delete_permanently(),
Navigate::Mount if c == 'm' => status.mount_normal_device(),
Navigate::Mount if c == 'g' => status.go_to_normal_drive(),
Navigate::Mount if c == 'u' => status.umount_normal_device(),
Navigate::Mount if c == 'e' => status.eject_removable_device(),
Navigate::Mount if c.is_ascii_digit() => status.go_to_mount_per_index(c),
Navigate::Marks(MarkAction::Jump) => status.marks_jump_char(c),
Navigate::Marks(MarkAction::New) => status.marks_new(c),
Navigate::TempMarks(MarkAction::Jump) if c.is_ascii_digit() => {
status.temp_marks_jump_char(c)
}
Navigate::TempMarks(MarkAction::New) if c.is_ascii_digit() => status.temp_marks_new(c),
Navigate::Shortcut if status.menu.shortcut_from_char(c) => {
LeaveMenu::leave_menu(status, &self.binds)
}
Navigate::Compress if status.menu.compression_method_from_char(c) => {
LeaveMenu::leave_menu(status, &self.binds)
}
Navigate::Context if status.menu.context_from_char(c) => {
LeaveMenu::leave_menu(status, &self.binds)
}
Navigate::CliApplication if status.menu.cli_applications_from_char(c) => {
LeaveMenu::leave_menu(status, &self.binds)
}
Navigate::TuiApplication if status.menu.tui_applications_from_char(c) => {
LeaveMenu::leave_menu(status, &self.binds)
}
Navigate::Cloud if c == 'l' => status.cloud_disconnect(),
Navigate::Cloud if c == 'd' => EventAction::cloud_enter_newdir_mode(status),
Navigate::Cloud if c == 'u' => status.cloud_upload_selected_file(),
Navigate::Cloud if c == 'x' => EventAction::cloud_enter_delete_mode(status),
Navigate::Cloud if c == '?' => status.cloud_update_metadata(),
Navigate::Flagged if c == 'u' => {
status.menu.flagged.clear();
Ok(())
}
Navigate::Flagged if c == 'x' => status.menu.remove_selected_flagged(),
Navigate::Flagged if c == 'j' => status.jump_flagged(),
_ => {
status.reset_menu_mode()?;
status.current_tab_mut().reset_display_mode_and_view()
}
}
}
}
fn modifier_is_shift_or_none(modifiers: KeyModifiers) -> bool {
modifiers == KeyModifiers::NONE || modifiers == KeyModifiers::SHIFT
}
fn to_correct_case(c: char, modifiers: KeyModifiers) -> char {
if matches!(modifiers, KeyModifiers::SHIFT) {
c.to_ascii_uppercase()
} else {
c
}
}