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