Skip to main content

fm/event/
event_dispatch.rs

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
13/// Struct which dispatch the received events according to the state of the application.
14/// Holds a mapping which can't be static since it's read from a config file.
15/// All keys are mapped to relevent events on tabs.selected().
16/// Keybindings are read from `Config`.
17pub struct EventDispatcher {
18    binds: Bindings,
19}
20
21impl EventDispatcher {
22    /// Creates a new event dispatcher with those bindings.
23    pub fn new(binds: Bindings) -> Self {
24        Self { binds }
25    }
26
27    /// Reaction to received events.
28    /// Only non keyboard events are dealt here directly.
29    /// Keyboard events are configurable and are sent to specific functions
30    /// which needs to know those keybindings.
31    #[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    /// Ensure only a few actions can be executed from keyboard while in curso selection.
108    /// - Leave the mode, same key as leave menu,
109    /// - Toggle the cursor state, same key as entering cursor selection,
110    /// - Quit fm, same key as quit
111    /// - Move left, right, up, down. Same keys as moving.
112    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    /// Returns `Ok(true)` iff the key event matched a fuzzy event.
147    /// If the event isn't a fuzzy event, it should be dealt elewhere.
148    fn fuzzy_matcher(&self, status: &mut Status, key: KeyEvent) -> Result<bool> {
149        let Some(fuzzy) = &mut status.fuzzy else {
150            // fuzzy isn't set anymore and current_tab should be reset.
151            // This occurs when two fuzzy windows are opened and one is closed.
152            // The other tab hangs with nothing to do as long as the user doesn't press Escape
153            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
280/// True iff the keymodifier is either SHIFT or nothing (no modifier pressed).
281fn modifier_is_shift_or_none(modifiers: KeyModifiers) -> bool {
282    modifiers == KeyModifiers::NONE || modifiers == KeyModifiers::SHIFT
283}
284
285/// If the modifier is shift, upercase, otherwise lowercase
286fn 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}