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.clone(),
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 table
158                if editor.edit_dialog.is_none() && !editor.showing_confirm_dialog {
159                    editor.select_prev();
160                }
161            }
162            MouseEventKind::ScrollDown => {
163                if editor.edit_dialog.is_none() && !editor.showing_confirm_dialog {
164                    editor.select_next();
165                }
166            }
167            MouseEventKind::Down(MouseButton::Left) => {
168                // Handle confirm dialog clicks first
169                if editor.showing_confirm_dialog {
170                    if let Some((save_r, discard_r, cancel_r)) = layout.confirm_buttons {
171                        if point_in_rect(save_r, col, row) {
172                            self.save_keybinding_editor_changes(&editor);
173                            return Ok(true);
174                        } else if point_in_rect(discard_r, col, row) {
175                            self.set_status_message("Keybinding editor closed".to_string());
176                            return Ok(true);
177                        } else if point_in_rect(cancel_r, col, row) {
178                            editor.showing_confirm_dialog = false;
179                        }
180                    }
181                    self.keybinding_editor = Some(editor);
182                    return Ok(true);
183                }
184
185                // Handle edit dialog clicks
186                if editor.edit_dialog.is_some() {
187                    // Button clicks
188                    if let Some((save_r, cancel_r)) = layout.dialog_buttons {
189                        if point_in_rect(save_r, col, row) {
190                            // Save button
191                            if let Some(err) = editor.apply_edit_dialog() {
192                                self.set_status_message(err);
193                            }
194                            self.keybinding_editor = Some(editor);
195                            return Ok(true);
196                        } else if point_in_rect(cancel_r, col, row) {
197                            // Cancel button - close dialog
198                            editor.edit_dialog = None;
199                            self.keybinding_editor = Some(editor);
200                            return Ok(true);
201                        }
202                    }
203                    // Field clicks
204                    if let Some(r) = layout.dialog_key_field {
205                        if point_in_rect(r, col, row) {
206                            if let Some(ref mut dialog) = editor.edit_dialog {
207                                dialog.focus_area = 0;
208                                dialog.mode = crate::app::keybinding_editor::EditMode::RecordingKey;
209                            }
210                        }
211                    }
212                    if let Some(r) = layout.dialog_action_field {
213                        if point_in_rect(r, col, row) {
214                            if let Some(ref mut dialog) = editor.edit_dialog {
215                                dialog.focus_area = 1;
216                                dialog.mode =
217                                    crate::app::keybinding_editor::EditMode::EditingAction;
218                            }
219                        }
220                    }
221                    if let Some(r) = layout.dialog_context_field {
222                        if point_in_rect(r, col, row) {
223                            if let Some(ref mut dialog) = editor.edit_dialog {
224                                dialog.focus_area = 2;
225                                dialog.mode =
226                                    crate::app::keybinding_editor::EditMode::EditingContext;
227                            }
228                        }
229                    }
230                    self.keybinding_editor = Some(editor);
231                    return Ok(true);
232                }
233
234                // Click on search bar to focus it
235                if let Some(search_r) = layout.search_bar {
236                    if point_in_rect(search_r, col, row) {
237                        editor.start_search();
238                        self.keybinding_editor = Some(editor);
239                        return Ok(true);
240                    }
241                }
242
243                // Click on table row to select (or toggle section header)
244                let table_area = layout.table_area;
245                let first_row_y = layout.table_first_row_y;
246                if point_in_rect(table_area, col, row) && row >= first_row_y {
247                    let clicked_row = (row - first_row_y) as usize;
248                    let new_selected = editor.scroll.offset as usize + clicked_row;
249                    if new_selected < editor.display_rows.len() {
250                        editor.selected = new_selected;
251                        if editor.selected_is_section_header() {
252                            editor.toggle_section_at_selected();
253                        }
254                    }
255                }
256            }
257            _ => {}
258        }
259
260        self.keybinding_editor = Some(editor);
261        Ok(true)
262    }
263}