1use anyhow::Result;
2use crossterm::event::{
3 Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
4};
5
6use crate::app::Status;
7use crate::config::Bindings;
8use crate::event::{EventAction, FmEvents};
9use crate::modes::{
10 Direction as FuzzyDirection, Display, InputSimple, LeaveMenu, MarkAction, Menu, Navigate,
11};
12
13pub struct EventDispatcher {
18 binds: Bindings,
19}
20
21impl EventDispatcher {
22 pub fn new(binds: Bindings) -> Self {
24 Self { binds }
25 }
26
27 pub fn dispatch(&self, status: &mut Status, ev: FmEvents) -> Result<()> {
32 match ev {
33 FmEvents::Term(Event::Paste(pasted)) => EventAction::paste(status, pasted),
34 FmEvents::Term(Event::Key(key)) => self.match_key_event(status, key),
35 FmEvents::Term(Event::Mouse(mouse)) => self.match_mouse_event(status, mouse),
36 FmEvents::Term(Event::Resize(width, height)) => {
37 EventAction::resize(status, width, height)
38 }
39 FmEvents::BulkExecute => EventAction::bulk_confirm(status),
40 FmEvents::Refresh => EventAction::refresh_if_needed(status),
41 FmEvents::FileCopied => EventAction::file_copied(status),
42 FmEvents::UpdateTick => EventAction::check_preview_fuzzy_tick(status),
43 FmEvents::Action(action) => action.matcher(status, &self.binds),
44 FmEvents::Ipc(msg) => EventAction::parse_rpc(status, msg),
45 _ => Ok(()),
46 }
47 }
48
49 fn match_key_event(&self, status: &mut Status, key: KeyEvent) -> Result<()> {
50 match key {
51 KeyEvent {
52 code: KeyCode::Char(c),
53 modifiers,
54 kind: _,
55 state: _,
56 } if !status.focus.is_file() && modifier_is_shift_or_none(modifiers) => {
57 self.menu_char_key_matcher(status, c)?
58 }
59 KeyEvent {
60 code: KeyCode::Char('h'),
61 modifiers: KeyModifiers::ALT,
62 kind: _,
63 state: _,
64 } if !status.focus.is_file() => status.open_picker()?,
65 key => self.file_key_matcher(status, key)?,
66 };
67 Ok(())
68 }
69
70 fn match_mouse_event(&self, status: &mut Status, mouse_event: MouseEvent) -> Result<()> {
71 match mouse_event.kind {
72 MouseEventKind::ScrollUp => {
73 EventAction::wheel_up(status, mouse_event.row, mouse_event.column)
74 }
75 MouseEventKind::ScrollDown => {
76 EventAction::wheel_down(status, mouse_event.row, mouse_event.column)
77 }
78 MouseEventKind::Down(MouseButton::Left) => {
79 EventAction::left_click(status, &self.binds, mouse_event.row, mouse_event.column)
80 }
81 MouseEventKind::Down(MouseButton::Middle) => {
82 EventAction::middle_click(status, &self.binds, mouse_event.row, mouse_event.column)
83 }
84 MouseEventKind::Down(MouseButton::Right) => {
85 EventAction::right_click(status, &self.binds, mouse_event.row, mouse_event.column)
86 }
87 MouseEventKind::Moved => {
88 EventAction::focus_follow_mouse(status, mouse_event.row, mouse_event.column)
89 }
90 _ => Ok(()),
91 }
92 }
93
94 fn file_key_matcher(&self, status: &mut Status, key: KeyEvent) -> Result<()> {
95 if matches!(status.current_tab().display_mode, Display::Fuzzy) {
96 if let Ok(success) = self.fuzzy_matcher(status, key) {
97 if success {
98 return Ok(());
99 }
100 }
101 }
102 let Some(action) = self.binds.get(&key) else {
103 return Ok(());
104 };
105 action.matcher(status, &self.binds)
106 }
107
108 fn fuzzy_matcher(&self, status: &mut Status, key: KeyEvent) -> Result<bool> {
111 let Some(fuzzy) = &mut status.fuzzy else {
112 status
116 .current_tab_mut()
117 .set_display_mode(Display::Directory);
118 status.refresh_status()?;
119 return Ok(false);
120 };
121 match key {
122 KeyEvent {
123 code: KeyCode::Char(mut c),
124 modifiers,
125 kind: _,
126 state: _,
127 } if modifier_is_shift_or_none(modifiers) => {
128 c = to_correct_case(c, modifiers);
129 fuzzy.input.insert(c);
130 fuzzy.update_input(true);
131 Ok(true)
132 }
133 key => self.fuzzy_key_matcher(status, key),
134 }
135 }
136
137 #[rustfmt::skip]
138 fn fuzzy_key_matcher(&self, status: &mut Status, key: KeyEvent) -> Result<bool> {
139 if let KeyEvent{code:KeyCode ::Char(' '),modifiers:KeyModifiers::CONTROL, kind:_,state:_} = key {
140 status.fuzzy_toggle_flag_selected()?;
141 return Ok(true);
142 }
143 let KeyEvent {
144 code,
145 modifiers: KeyModifiers::NONE,
146 kind: _,
147 state: _,
148 } = key
149 else {
150 return Ok(false);
151 };
152 match code {
153 KeyCode::Enter => status.fuzzy_select()?,
154 KeyCode::Esc => status.fuzzy_leave()?,
155 KeyCode::Backspace => status.fuzzy_backspace()?,
156 KeyCode::Delete => status.fuzzy_delete()?,
157 KeyCode::Left => status.fuzzy_left()?,
158 KeyCode::Right => status.fuzzy_right()?,
159 KeyCode::Up => status.fuzzy_navigate(FuzzyDirection::Up)?,
160 KeyCode::Down => status.fuzzy_navigate(FuzzyDirection::Down)?,
161 KeyCode::PageUp => status.fuzzy_navigate(FuzzyDirection::PageUp)?,
162 KeyCode::PageDown => status.fuzzy_navigate(FuzzyDirection::PageDown)?,
163 _ => return Ok(false),
164 }
165 Ok(true)
166 }
167
168 fn menu_char_key_matcher(&self, status: &mut Status, c: char) -> Result<()> {
169 let tab = status.current_tab_mut();
170 match tab.menu_mode {
171 Menu::InputSimple(InputSimple::Sort) => status.sort_by_char(c),
172 Menu::InputSimple(InputSimple::RegexMatch) => status.input_regex(c),
173 Menu::InputSimple(InputSimple::Filter) => status.input_filter(c),
174 Menu::InputSimple(_) => status.menu.input_insert(c),
175 Menu::InputCompleted(input_completed) => status.input_and_complete(input_completed, c),
176 Menu::NeedConfirmation(confirmed_action) => status.confirm(c, confirmed_action),
177 Menu::Navigate(navigate) => self.navigate_char(navigate, status, c),
178 _ if matches!(tab.display_mode, Display::Preview) => tab.reset_display_mode_and_view(),
179 Menu::Nothing => Ok(()),
180 }
181 }
182
183 fn navigate_char(&self, navigate: Navigate, status: &mut Status, c: char) -> Result<()> {
184 match navigate {
185 Navigate::Trash if c == 'x' => status.menu.trash_delete_permanently(),
186
187 Navigate::Mount if c == 'm' => status.mount_normal_device(),
188 Navigate::Mount if c == 'g' => status.go_to_normal_drive(),
189 Navigate::Mount if c == 'u' => status.umount_normal_device(),
190 Navigate::Mount if c == 'e' => status.eject_removable_device(),
191 Navigate::Mount if c.is_ascii_digit() => status.go_to_mount_per_index(c),
192
193 Navigate::Marks(MarkAction::Jump) => status.marks_jump_char(c),
194 Navigate::Marks(MarkAction::New) => status.marks_new(c),
195
196 Navigate::TempMarks(MarkAction::Jump) if c.is_ascii_digit() => {
197 status.temp_marks_jump_char(c)
198 }
199 Navigate::TempMarks(MarkAction::New) if c.is_ascii_digit() => status.temp_marks_new(c),
200
201 Navigate::Shortcut if status.menu.shortcut_from_char(c) => {
202 LeaveMenu::leave_menu(status, &self.binds)
203 }
204 Navigate::Compress if status.menu.compression_method_from_char(c) => {
205 LeaveMenu::leave_menu(status, &self.binds)
206 }
207 Navigate::Context if status.menu.context_from_char(c) => {
208 LeaveMenu::leave_menu(status, &self.binds)
209 }
210 Navigate::CliApplication if status.menu.cli_applications_from_char(c) => {
211 LeaveMenu::leave_menu(status, &self.binds)
212 }
213 Navigate::TuiApplication if status.menu.tui_applications_from_char(c) => {
214 LeaveMenu::leave_menu(status, &self.binds)
215 }
216
217 Navigate::Cloud if c == 'l' => status.cloud_disconnect(),
218 Navigate::Cloud if c == 'd' => EventAction::cloud_enter_newdir_mode(status),
219 Navigate::Cloud if c == 'u' => status.cloud_upload_selected_file(),
220 Navigate::Cloud if c == 'x' => EventAction::cloud_enter_delete_mode(status),
221 Navigate::Cloud if c == '?' => status.cloud_update_metadata(),
222
223 Navigate::Flagged if c == 'u' => {
224 status.menu.flagged.clear();
225 Ok(())
226 }
227 Navigate::Flagged if c == 'x' => status.menu.remove_selected_flagged(),
228 Navigate::Flagged if c == 'j' => status.jump_flagged(),
229
230 _ => {
231 status.reset_menu_mode()?;
232 status.current_tab_mut().reset_display_mode_and_view()
233 }
234 }
235 }
236}
237
238fn modifier_is_shift_or_none(modifiers: KeyModifiers) -> bool {
240 modifiers == KeyModifiers::NONE || modifiers == KeyModifiers::SHIFT
241}
242
243fn to_correct_case(c: char, modifiers: KeyModifiers) -> char {
245 if matches!(modifiers, KeyModifiers::SHIFT) {
246 c.to_ascii_uppercase()
247 } else {
248 c
249 }
250}