Skip to main content

fresh/app/
keybinding_editor_actions.rs

1//! Keybinding editor action handling
2//!
3//! This module provides the action handlers for the keybinding editor modal.
4
5use super::keybinding_editor::KeybindingEditor;
6use super::Editor;
7use crate::input::handler::InputResult;
8use crate::view::keybinding_editor::{handle_keybinding_editor_input, KeybindingEditorAction};
9use crate::view::ui::point_in_rect;
10use crossterm::event::{KeyEvent, MouseButton, MouseEvent, MouseEventKind};
11
12impl Editor {
13    /// Open the keybinding editor modal
14    pub fn open_keybinding_editor(&mut self) {
15        use crate::config::MenuExt;
16        let config_path = self.dir_context.config_path().display().to_string();
17        let cmd_registry = self.command_registry.read().unwrap();
18        let keybindings = self.keybindings.read().unwrap();
19        // Enumerate top-level menu ids (File, Edit, …, plus plugin menus) so
20        // the action dropdown can offer `menu_open:<name>` variants instead of
21        // one un-parseable bare `menu_open` row.
22        let menu_names: Vec<String> = self
23            .menus
24            .menus
25            .iter()
26            .chain(self.menu_state.plugin_menus.iter())
27            .map(|m| m.match_id().to_string())
28            .collect();
29        self.keybinding_editor = Some(KeybindingEditor::new(
30            &self.config,
31            &keybindings,
32            &self.mode_registry,
33            &cmd_registry,
34            config_path,
35            &menu_names,
36        ));
37    }
38
39    /// Handle input when keybinding editor is active
40    pub fn handle_keybinding_editor_input(&mut self, event: &KeyEvent) -> InputResult {
41        let mut editor = match self.keybinding_editor.take() {
42            Some(e) => e,
43            None => return InputResult::Ignored,
44        };
45
46        let action = handle_keybinding_editor_input(&mut editor, event);
47
48        match action {
49            KeybindingEditorAction::Consumed => {
50                self.keybinding_editor = Some(editor);
51                InputResult::Consumed
52            }
53            KeybindingEditorAction::Close => {
54                // Close without saving
55                self.set_status_message("Keybinding editor closed".to_string());
56                InputResult::Consumed
57            }
58            KeybindingEditorAction::SaveAndClose => {
59                // Save custom bindings to config
60                self.save_keybinding_editor_changes(&editor);
61                InputResult::Consumed
62            }
63            KeybindingEditorAction::StatusMessage(msg) => {
64                self.set_status_message(msg);
65                self.keybinding_editor = Some(editor);
66                InputResult::Consumed
67            }
68        }
69    }
70
71    /// Save keybinding editor changes to config
72    fn save_keybinding_editor_changes(&mut self, editor: &KeybindingEditor) {
73        if !editor.has_changes {
74            return;
75        }
76
77        // Remove deleted custom bindings from config
78        for remove in editor.get_pending_removes() {
79            self.config_mut().keybindings.retain(|kb| {
80                !(kb.action == remove.action
81                    && kb.key == remove.key
82                    && kb.modifiers == remove.modifiers
83                    && kb.when == remove.when)
84            });
85        }
86
87        // Add new custom bindings
88        let new_bindings = editor.get_custom_bindings();
89        for binding in new_bindings {
90            self.config_mut().keybindings.push(binding);
91        }
92
93        // Rebuild the keybinding resolver
94        *self.keybindings.write().unwrap() =
95            crate::input::keybindings::KeybindingResolver::new(&self.config);
96
97        // Save to config file via the pending changes mechanism
98        let config_value = match serde_json::to_value(&self.config.keybindings) {
99            Ok(v) => v,
100            Err(e) => {
101                self.set_status_message(format!("Failed to serialize keybindings: {}", e));
102                return;
103            }
104        };
105
106        let mut changes = std::collections::HashMap::new();
107        changes.insert("/keybindings".to_string(), config_value);
108
109        let resolver = crate::config_io::ConfigResolver::new(
110            self.dir_context.clone(),
111            self.working_dir().to_path_buf(),
112        );
113
114        match resolver.save_changes_to_layer(
115            &changes,
116            &std::collections::HashSet::new(),
117            crate::config_io::ConfigLayer::User,
118        ) {
119            Ok(()) => {
120                self.set_status_message("Keybinding changes saved".to_string());
121            }
122            Err(e) => {
123                self.set_status_message(format!("Failed to save keybindings: {}", e));
124            }
125        }
126    }
127
128    /// Check if keybinding editor is active
129    pub fn is_keybinding_editor_active(&self) -> bool {
130        self.keybinding_editor.is_some()
131    }
132
133    /// Handle mouse events when keybinding editor is active
134    /// Returns Ok(true) if a re-render is needed
135    pub fn handle_keybinding_editor_mouse(
136        &mut self,
137        mouse_event: MouseEvent,
138    ) -> anyhow::Result<bool> {
139        let mut editor = match self.keybinding_editor.take() {
140            Some(e) => e,
141            None => return Ok(false),
142        };
143
144        let col = mouse_event.column;
145        let row = mouse_event.row;
146        let layout = &editor.layout;
147
148        // All mouse events inside modal are consumed (masked from reaching underlying editor)
149        // Events outside the modal are ignored (but still consumed to prevent leaking)
150        if !point_in_rect(layout.modal_area, col, row) {
151            self.keybinding_editor = Some(editor);
152            return Ok(false);
153        }
154
155        match mouse_event.kind {
156            MouseEventKind::ScrollUp => {
157                // Scroll the viewport without touching selection. Coupling
158                // wheel to selection meant any prior scrollbar drag snapped
159                // back via `ensure_visible` on the next wheel tick. Three
160                // rows per tick matches the settings modal.
161                if editor.edit_dialog.is_none() && !editor.showing_confirm_dialog {
162                    editor.scroll.scroll_by(-3);
163                }
164            }
165            MouseEventKind::ScrollDown => {
166                if editor.edit_dialog.is_none() && !editor.showing_confirm_dialog {
167                    editor.scroll.scroll_by(3);
168                }
169            }
170            MouseEventKind::Drag(MouseButton::Left) => {
171                // Continue dragging the scrollbar thumb (no selection or
172                // dialog disambiguation needed: the press that started the
173                // drag already gated those).
174                if let Some(sb) = editor.layout.table_scrollbar {
175                    let sb_state = scrollbar_state_for(&editor);
176                    if let Some(new_offset) = editor.scrollbar_mouse.drag(sb_state, sb, row) {
177                        editor.scroll.offset = new_offset as u16;
178                    }
179                }
180            }
181            MouseEventKind::Up(MouseButton::Left) => {
182                editor.scrollbar_mouse.release();
183            }
184            MouseEventKind::Down(MouseButton::Left) => {
185                // Handle confirm dialog clicks first
186                if editor.showing_confirm_dialog {
187                    if let Some((save_r, discard_r, cancel_r)) = layout.confirm_buttons {
188                        if point_in_rect(save_r, col, row) {
189                            self.save_keybinding_editor_changes(&editor);
190                            return Ok(true);
191                        } else if point_in_rect(discard_r, col, row) {
192                            self.set_status_message("Keybinding editor closed".to_string());
193                            return Ok(true);
194                        } else if point_in_rect(cancel_r, col, row) {
195                            editor.showing_confirm_dialog = false;
196                        }
197                    }
198                    self.keybinding_editor = Some(editor);
199                    return Ok(true);
200                }
201
202                // Handle edit dialog clicks
203                if editor.edit_dialog.is_some() {
204                    // Button clicks
205                    if let Some((save_r, cancel_r)) = layout.dialog_buttons {
206                        if point_in_rect(save_r, col, row) {
207                            // Save button
208                            if let Some(err) = editor.apply_edit_dialog() {
209                                self.set_status_message(err);
210                            }
211                            self.keybinding_editor = Some(editor);
212                            return Ok(true);
213                        } else if point_in_rect(cancel_r, col, row) {
214                            // Cancel button - close dialog
215                            editor.edit_dialog = None;
216                            self.keybinding_editor = Some(editor);
217                            return Ok(true);
218                        }
219                    }
220                    // Field clicks
221                    if let Some(r) = layout.dialog_key_field {
222                        if point_in_rect(r, col, row) {
223                            if let Some(ref mut dialog) = editor.edit_dialog {
224                                dialog.focus_area = 0;
225                                dialog.mode = crate::app::keybinding_editor::EditMode::RecordingKey;
226                            }
227                        }
228                    }
229                    if let Some(r) = layout.dialog_action_field {
230                        if point_in_rect(r, col, row) {
231                            if let Some(ref mut dialog) = editor.edit_dialog {
232                                dialog.focus_area = 1;
233                                dialog.mode =
234                                    crate::app::keybinding_editor::EditMode::EditingAction;
235                            }
236                        }
237                    }
238                    if let Some(r) = layout.dialog_context_field {
239                        if point_in_rect(r, col, row) {
240                            if let Some(ref mut dialog) = editor.edit_dialog {
241                                dialog.focus_area = 2;
242                                dialog.mode =
243                                    crate::app::keybinding_editor::EditMode::EditingContext;
244                            }
245                        }
246                    }
247                    self.keybinding_editor = Some(editor);
248                    return Ok(true);
249                }
250
251                // Click on search bar to focus it
252                if let Some(search_r) = layout.search_bar {
253                    if point_in_rect(search_r, col, row) {
254                        editor.start_search();
255                        self.keybinding_editor = Some(editor);
256                        return Ok(true);
257                    }
258                }
259
260                // Press on the scrollbar — delegate to the shared widget
261                // so press-on-thumb (no jump), press-on-track (recentre),
262                // and the follow-up drag all run through the same well-
263                // tested math. Checked before the row-click branch because
264                // the scrollbar overlaps the rightmost column of `table_area`.
265                if let Some(sb) = layout.table_scrollbar {
266                    let sb_state = scrollbar_state_for(&editor);
267                    if let Some(new_offset) = editor.scrollbar_mouse.press(sb_state, sb, col, row) {
268                        editor.scroll.offset = new_offset as u16;
269                        self.keybinding_editor = Some(editor);
270                        return Ok(true);
271                    }
272                }
273
274                // Click on table row to select (or toggle section header)
275                let table_area = layout.table_area;
276                let first_row_y = layout.table_first_row_y;
277                if point_in_rect(table_area, col, row) && row >= first_row_y {
278                    let clicked_row = (row - first_row_y) as usize;
279                    let new_selected = editor.scroll.offset as usize + clicked_row;
280                    if new_selected < editor.display_rows.len() {
281                        editor.selected = new_selected;
282                        if editor.selected_is_section_header() {
283                            editor.toggle_section_at_selected();
284                        }
285                    }
286                }
287            }
288            _ => {}
289        }
290
291        self.keybinding_editor = Some(editor);
292        Ok(true)
293    }
294}
295
296/// Snapshot the keybinding editor's scroll state as a `ScrollbarState`,
297/// so we can call into the shared scrollbar widget for click/drag math.
298fn scrollbar_state_for(editor: &KeybindingEditor) -> crate::view::ui::scrollbar::ScrollbarState {
299    crate::view::ui::scrollbar::ScrollbarState::new(
300        editor.scroll.content_height as usize,
301        editor.scroll.viewport as usize,
302        editor.scroll.offset as usize,
303    )
304}