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::{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    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    /// Returns `Ok(true)` iff the key event matched a fuzzy event.
109    /// If the event isn't a fuzzy event, it should be dealt elewhere.
110    fn fuzzy_matcher(&self, status: &mut Status, key: KeyEvent) -> Result<bool> {
111        let Some(fuzzy) = &mut status.fuzzy else {
112            // fuzzy isn't set anymore and current_tab should be reset.
113            // This occurs when two fuzzy windows are opened and one is closed.
114            // The other tab hangs with nothing to do as long as the user doesn't press Escape
115            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
238/// True iff the keymodifier is either SHIFT or nothing (no modifier pressed).
239fn modifier_is_shift_or_none(modifiers: KeyModifiers) -> bool {
240    modifiers == KeyModifiers::NONE || modifiers == KeyModifiers::SHIFT
241}
242
243/// If the modifier is shift, upercase, otherwise lowercase
244fn 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}