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::{ActionMap, 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 #[rustfmt::skip]
32 pub fn dispatch(&self, status: &mut Status, ev: FmEvents) -> Result<()> {
33 match ev {
34 FmEvents::Term(Event::Paste(pasted)) => EventAction::paste(status, pasted),
35 FmEvents::Term(Event::Key(key)) => self.match_key_event(status, key),
36 FmEvents::Term(Event::Mouse(mouse)) => self.match_mouse_event(status, mouse),
37 FmEvents::Term(Event::Resize(width, height)) => EventAction::resize(status, width, height),
38 FmEvents::BulkExecute => EventAction::bulk_confirm(status),
39 FmEvents::Refresh => EventAction::refresh_if_needed(status),
40 FmEvents::FileCopied(done_copy_moves) => EventAction::file_copied(status, done_copy_moves),
41 FmEvents::UpdateTick => EventAction::check_preview_fuzzy_tick(status),
42 FmEvents::Action(action) => action.matcher(status, &self.binds),
43 FmEvents::Ipc(msg) => EventAction::parse_rpc(status, msg),
44 _ => Ok(()),
45 }
46 }
47
48 fn match_key_event(&self, status: &mut Status, key: KeyEvent) -> Result<()> {
49 if status.internal_settings.cursor.is_active() {
50 return self.cursor_key_matcher(status, key);
51 }
52 match key {
53 KeyEvent {
54 code: KeyCode::Char(c),
55 modifiers,
56 kind: _,
57 state: _,
58 } if !status.focus.is_file() && modifier_is_shift_or_none(modifiers) => {
59 self.menu_char_key_matcher(status, c)?
60 }
61 KeyEvent {
62 code: KeyCode::Char('h'),
63 modifiers: KeyModifiers::ALT,
64 kind: _,
65 state: _,
66 } if !status.focus.is_file() => status.open_picker()?,
67 key => self.file_key_matcher(status, key)?,
68 };
69 Ok(())
70 }
71
72 fn match_mouse_event(&self, status: &mut Status, mouse_event: MouseEvent) -> Result<()> {
73 match mouse_event.kind {
74 MouseEventKind::ScrollUp => {
75 EventAction::wheel_up(status, mouse_event.row, mouse_event.column)
76 }
77 MouseEventKind::ScrollDown => {
78 EventAction::wheel_down(status, mouse_event.row, mouse_event.column)
79 }
80 MouseEventKind::Drag(MouseButton::Left)
81 if status.internal_settings.cursor.is_selecting() =>
82 {
83 EventAction::mouse_drag(status, mouse_event.row, mouse_event.column)
84 }
85 MouseEventKind::Up(MouseButton::Left)
86 if status.internal_settings.cursor.is_selecting()
87 && status.internal_settings.cursor.is_dragging =>
88 {
89 EventAction::mouse_up(status, mouse_event.row, mouse_event.column)
90 }
91 MouseEventKind::Down(MouseButton::Left) => {
92 EventAction::left_click(status, &self.binds, mouse_event.row, mouse_event.column)
93 }
94 MouseEventKind::Down(MouseButton::Middle) => {
95 EventAction::middle_click(status, &self.binds, mouse_event.row, mouse_event.column)
96 }
97 MouseEventKind::Down(MouseButton::Right) => {
98 EventAction::right_click(status, &self.binds, mouse_event.row, mouse_event.column)
99 }
100 MouseEventKind::Moved => {
101 EventAction::focus_follow_mouse(status, mouse_event.row, mouse_event.column)
102 }
103 _ => Ok(()),
104 }
105 }
106
107 fn cursor_key_matcher(&self, status: &mut Status, key: KeyEvent) -> Result<()> {
113 let Some(action) = self.binds.get(&key) else {
114 return Ok(());
115 };
116 match action {
117 ActionMap::CopyPaste => EventAction::copy_paste(status),
118 ActionMap::Cursor => EventAction::cursor(status),
119 ActionMap::Quit => EventAction::quit(status),
120 ActionMap::ResetMode => EventAction::reset_mode(status),
121 ActionMap::MoveLeft => EventAction::move_left(status),
122 ActionMap::MoveRight => EventAction::move_right(status),
123 ActionMap::MoveUp => EventAction::move_up(status),
124 ActionMap::MoveDown => EventAction::move_down(status),
125 _ => Ok(()),
126 }
127 }
128
129 fn file_key_matcher(&self, status: &mut Status, key: KeyEvent) -> Result<()> {
130 if let Some(ActionMap::Cursor) = self.binds.get(&key) {
131 return EventAction::cursor(status);
132 }
133 if status.current_tab().display_mode.is_fuzzy() {
134 if let Ok(success) = self.fuzzy_matcher(status, key) {
135 if success {
136 return Ok(());
137 }
138 }
139 }
140 let Some(action) = self.binds.get(&key) else {
141 return Ok(());
142 };
143 action.matcher(status, &self.binds)
144 }
145
146 fn fuzzy_matcher(&self, status: &mut Status, key: KeyEvent) -> Result<bool> {
149 let Some(fuzzy) = &mut status.fuzzy else {
150 status
154 .current_tab_mut()
155 .set_display_mode(Display::Directory);
156 status.refresh_status()?;
157 return Ok(false);
158 };
159 match key {
160 KeyEvent {
161 code: KeyCode::Char(mut c),
162 modifiers,
163 kind: _,
164 state: _,
165 } if modifier_is_shift_or_none(modifiers) => {
166 c = to_correct_case(c, modifiers);
167 fuzzy.input.insert(c);
168 fuzzy.update_input(true);
169 Ok(true)
170 }
171 key => self.fuzzy_key_matcher(status, key),
172 }
173 }
174
175 #[rustfmt::skip]
176 fn fuzzy_key_matcher(&self, status: &mut Status, key: KeyEvent) -> Result<bool> {
177 if let KeyEvent{code:KeyCode::Char(' '), modifiers: KeyModifiers::CONTROL, kind:_, state:_} = key {
178 status.fuzzy_toggle_flag_selected()?;
179 return Ok(true);
180 }
181 if let KeyEvent{code:KeyCode::Enter, modifiers: KeyModifiers::ALT, kind:_, state:_} = key {
182 status.fuzzy_open_file()?;
183 return Ok(true);
184 }
185 let KeyEvent {
186 code,
187 modifiers: KeyModifiers::NONE,
188 kind: _,
189 state: _,
190 } = key
191 else {
192 return Ok(false);
193 };
194 match code {
195 KeyCode::Enter => status.fuzzy_select()?,
196 KeyCode::Esc => status.fuzzy_leave()?,
197 KeyCode::Backspace => status.fuzzy_backspace()?,
198 KeyCode::Delete => status.fuzzy_delete()?,
199 KeyCode::Left => status.fuzzy_left()?,
200 KeyCode::Right => status.fuzzy_right()?,
201 KeyCode::Up => status.fuzzy_navigate(FuzzyDirection::Up)?,
202 KeyCode::Down => status.fuzzy_navigate(FuzzyDirection::Down)?,
203 KeyCode::PageUp => status.fuzzy_navigate(FuzzyDirection::PageUp)?,
204 KeyCode::PageDown => status.fuzzy_navigate(FuzzyDirection::PageDown)?,
205 _ => return Ok(false),
206 }
207 Ok(true)
208 }
209
210 fn menu_char_key_matcher(&self, status: &mut Status, c: char) -> Result<()> {
211 let tab = status.current_tab_mut();
212 match tab.menu_mode {
213 Menu::InputSimple(InputSimple::Sort) => status.sort_by_char(c),
214 Menu::InputSimple(InputSimple::RegexMatch) => status.input_regex(c),
215 Menu::InputSimple(InputSimple::Filter) => status.input_filter(c),
216 Menu::InputSimple(_) => status.menu.input_insert(c),
217 Menu::InputCompleted(input_completed) => status.input_and_complete(input_completed, c),
218 Menu::NeedConfirmation(confirmed_action) => status.confirm(c, confirmed_action),
219 Menu::Navigate(navigate) => self.navigate_char(navigate, status, c),
220 _ if matches!(tab.display_mode, Display::Preview) => tab.reset_display_mode_and_view(),
221 Menu::Nothing => Ok(()),
222 }
223 }
224
225 fn navigate_char(&self, navigate: Navigate, status: &mut Status, c: char) -> Result<()> {
226 match navigate {
227 Navigate::Trash if c == 'x' => status.menu.trash_delete_permanently(),
228
229 Navigate::Mount if c == 'm' => status.mount_normal_device(),
230 Navigate::Mount if c == 'g' => status.go_to_normal_drive(),
231 Navigate::Mount if c == 'u' => status.umount_normal_device(),
232 Navigate::Mount if c == 'e' => status.eject_removable_device(),
233 Navigate::Mount if c.is_ascii_digit() => status.go_to_mount_per_index(c),
234
235 Navigate::Marks(MarkAction::Jump) => status.marks_jump_char(c),
236 Navigate::Marks(MarkAction::New) => status.marks_new(c),
237
238 Navigate::TempMarks(MarkAction::Jump) if c.is_ascii_digit() => {
239 status.temp_marks_jump_char(c)
240 }
241 Navigate::TempMarks(MarkAction::New) if c.is_ascii_digit() => status.temp_marks_new(c),
242
243 Navigate::Shortcut if status.menu.shortcut_from_char(c) => {
244 LeaveMenu::leave_menu(status, &self.binds)
245 }
246 Navigate::Compress if status.menu.compression_method_from_char(c) => {
247 LeaveMenu::leave_menu(status, &self.binds)
248 }
249 Navigate::Context if status.menu.context_from_char(c) => {
250 LeaveMenu::leave_menu(status, &self.binds)
251 }
252 Navigate::CliApplication if status.menu.cli_applications_from_char(c) => {
253 LeaveMenu::leave_menu(status, &self.binds)
254 }
255 Navigate::TuiApplication if status.menu.tui_applications_from_char(c) => {
256 LeaveMenu::leave_menu(status, &self.binds)
257 }
258
259 Navigate::Cloud if c == 'l' => status.cloud_disconnect(),
260 Navigate::Cloud if c == 'd' => EventAction::cloud_enter_newdir_mode(status),
261 Navigate::Cloud if c == 'u' => status.cloud_upload_selected_file(),
262 Navigate::Cloud if c == 'x' => EventAction::cloud_enter_delete_mode(status),
263 Navigate::Cloud if c == '?' => status.cloud_update_metadata(),
264
265 Navigate::Flagged if c == 'u' => {
266 status.menu.flagged.clear();
267 Ok(())
268 }
269 Navigate::Flagged if c == 'x' => status.menu.remove_selected_flagged(),
270 Navigate::Flagged if c == 'j' => status.jump_flagged(),
271
272 _ => {
273 status.reset_menu_mode()?;
274 status.current_tab_mut().reset_display_mode_and_view()
275 }
276 }
277 }
278}
279
280fn modifier_is_shift_or_none(modifiers: KeyModifiers) -> bool {
282 modifiers == KeyModifiers::NONE || modifiers == KeyModifiers::SHIFT
283}
284
285fn to_correct_case(c: char, modifiers: KeyModifiers) -> char {
287 if matches!(modifiers, KeyModifiers::SHIFT) {
288 c.to_ascii_uppercase()
289 } else {
290 c
291 }
292}