1use super::helpers::{format_chord_keys, key_code_to_config_name, modifiers_to_config_names};
4use super::types::*;
5use crate::config::{Config, Keybinding};
6use crate::input::command_registry::CommandRegistry;
7use crate::input::keybindings::{format_keybinding, Action, KeyContext, KeybindingResolver};
8use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
9use rust_i18n::t;
10use std::collections::{HashMap, HashSet};
11
12#[derive(Debug)]
14pub struct KeybindingEditor {
15 pub bindings: Vec<ResolvedBinding>,
17 pub filtered_indices: Vec<usize>,
19 pub selected: usize,
21 pub scroll: crate::view::ui::ScrollState,
23
24 pub search_active: bool,
26 pub search_focused: bool,
28 pub search_query: String,
30 pub search_mode: SearchMode,
32 pub search_key_display: String,
34 pub search_key_code: Option<KeyCode>,
36 pub search_modifiers: KeyModifiers,
38
39 pub context_filter: ContextFilter,
41 pub source_filter: SourceFilter,
43
44 pub edit_dialog: Option<EditBindingState>,
46
47 pub showing_help: bool,
49
50 pub active_keymap: String,
52 pub config_file_path: String,
54
55 pub pending_adds: Vec<Keybinding>,
57 pub pending_removes: Vec<Keybinding>,
59 pub has_changes: bool,
61
62 pub showing_confirm_dialog: bool,
64 pub confirm_selection: usize,
66
67 pub keymap_names: Vec<String>,
69
70 pub available_actions: Vec<String>,
72
73 pub mode_contexts: Vec<String>,
75
76 pub display_rows: Vec<DisplayRow>,
78 pub collapsed_sections: HashSet<Option<String>>,
80
81 pub layout: KeybindingEditorLayout,
83}
84
85impl KeybindingEditor {
86 pub fn new(
88 config: &Config,
89 resolver: &KeybindingResolver,
90 mode_registry: &crate::input::buffer_mode::ModeRegistry,
91 command_registry: &CommandRegistry,
92 config_file_path: String,
93 ) -> Self {
94 let bindings =
95 Self::resolve_all_bindings(config, resolver, mode_registry, command_registry);
96 let filtered_indices: Vec<usize> = (0..bindings.len()).collect();
97
98 let mut available_actions = Self::collect_action_names();
100 for mode_bindings in resolver.get_plugin_defaults().values() {
101 for action in mode_bindings.values() {
102 let action_name = format!("{:?}", action);
103 let action_str = match action {
104 Action::PluginAction(name) => name.clone(),
105 other => format!("{:?}", other),
106 };
107 if !available_actions.contains(&action_str) {
108 available_actions.push(action_str);
109 }
110 let _ = action_name;
111 }
112 }
113 for cmd in command_registry.get_all() {
115 if let Action::PluginAction(ref name) = cmd.action {
116 if !available_actions.contains(name) {
117 available_actions.push(name.clone());
118 }
119 }
120 }
121 available_actions.sort();
122 available_actions.dedup();
123
124 let mut keymap_names: Vec<String> = config.keybinding_maps.keys().cloned().collect();
126 keymap_names.sort();
127
128 let mut mode_contexts: Vec<String> = resolver
130 .get_plugin_defaults()
131 .keys()
132 .filter_map(|ctx| {
133 if let KeyContext::Mode(name) = ctx {
134 Some(format!("mode:{}", name))
135 } else {
136 None
137 }
138 })
139 .collect();
140 mode_contexts.sort();
141
142 let mut collapsed_sections: HashSet<Option<String>> = HashSet::new();
144 for b in &bindings {
145 if b.plugin_name.is_some() {
146 collapsed_sections.insert(b.plugin_name.clone());
147 }
148 }
149
150 let mut editor = Self {
151 bindings,
152 filtered_indices,
153 selected: 0,
154 scroll: crate::view::ui::ScrollState::default(),
155 search_active: false,
156 search_focused: false,
157 search_query: String::new(),
158 search_mode: SearchMode::Text,
159 search_key_display: String::new(),
160 search_key_code: None,
161 search_modifiers: KeyModifiers::NONE,
162 context_filter: ContextFilter::All,
163 source_filter: SourceFilter::All,
164 edit_dialog: None,
165 showing_help: false,
166 active_keymap: config.active_keybinding_map.to_string(),
167 config_file_path,
168 pending_adds: Vec::new(),
169 pending_removes: Vec::new(),
170 has_changes: false,
171 showing_confirm_dialog: false,
172 confirm_selection: 0,
173 keymap_names,
174 available_actions,
175 mode_contexts,
176 display_rows: Vec::new(),
177 collapsed_sections,
178 layout: KeybindingEditorLayout::default(),
179 };
180
181 editor.apply_filters();
182 editor
183 }
184
185 fn resolve_all_bindings(
187 config: &Config,
188 resolver: &KeybindingResolver,
189 mode_registry: &crate::input::buffer_mode::ModeRegistry,
190 command_registry: &CommandRegistry,
191 ) -> Vec<ResolvedBinding> {
192 let mut bindings = Vec::new();
193 let mut seen: HashMap<(String, String), usize> = HashMap::new(); let map_bindings = config.resolve_keymap(&config.active_keybinding_map);
197 for kb in &map_bindings {
198 if let Some(entry) = Self::keybinding_to_resolved(kb, BindingSource::Keymap, resolver) {
199 let key = (entry.key_display.clone(), entry.context.clone());
200 let idx = bindings.len();
201 seen.insert(key, idx);
202 bindings.push(entry);
203 }
204 }
205
206 for kb in &config.keybindings {
208 if let Some(entry) = Self::keybinding_to_resolved(kb, BindingSource::Custom, resolver) {
209 let key = (entry.key_display.clone(), entry.context.clone());
210 if let Some(&existing_idx) = seen.get(&key) {
211 bindings[existing_idx] = entry;
213 } else {
214 let idx = bindings.len();
215 seen.insert(key, idx);
216 bindings.push(entry);
217 }
218 }
219 }
220
221 for (context, context_bindings) in resolver.get_plugin_defaults() {
223 if let KeyContext::Mode(mode_name) = context {
224 let context_str = format!("mode:{}", mode_name);
225 let section = mode_registry
227 .get(mode_name)
228 .and_then(|m| m.plugin_name.clone())
229 .unwrap_or_else(|| mode_name.clone());
230 for ((key_code, modifiers), action) in context_bindings {
231 let key_display = format_keybinding(key_code, modifiers);
232 let seen_key = (key_display.clone(), context_str.clone());
233 if seen.contains_key(&seen_key) {
235 continue;
236 }
237 let command = action.to_action_str();
238 let action_display = KeybindingResolver::format_action_from_str(&command);
239 let idx = bindings.len();
240 seen.insert(seen_key, idx);
241 bindings.push(ResolvedBinding {
242 key_display,
243 action: command,
244 action_display,
245 context: context_str.clone(),
246 source: BindingSource::Plugin,
247 key_code: *key_code,
248 modifiers: *modifiers,
249 is_chord: false,
250 plugin_name: Some(section.clone()),
251 command_name: None,
252 original_config: None,
253 });
254 }
255 }
256 }
257
258 let bound_actions: std::collections::HashSet<String> =
260 bindings.iter().map(|b| b.action.clone()).collect();
261 for action_name in Action::all_action_names() {
262 if !bound_actions.contains(&action_name) {
263 let action_display = KeybindingResolver::format_action_from_str(&action_name);
264 bindings.push(ResolvedBinding {
265 key_display: String::new(),
266 action: action_name,
267 action_display,
268 context: String::new(),
269 source: BindingSource::Unbound,
270 key_code: KeyCode::Null,
271 modifiers: KeyModifiers::NONE,
272 is_chord: false,
273 plugin_name: None,
274 command_name: None,
275 original_config: None,
276 });
277 }
278 }
279
280 for cmd in command_registry.get_all() {
282 if let Action::PluginAction(ref action_name) = cmd.action {
283 if !bound_actions.contains(action_name) {
284 let plugin_name = match &cmd.source {
285 crate::input::commands::CommandSource::Plugin(name) => Some(name.clone()),
286 _ => None,
287 };
288 bindings.push(ResolvedBinding {
289 key_display: String::new(),
290 action: action_name.clone(),
291 action_display: cmd.get_localized_name(),
292 context: String::new(),
293 source: BindingSource::Unbound,
294 key_code: KeyCode::Null,
295 modifiers: KeyModifiers::NONE,
296 is_chord: false,
297 plugin_name,
298 command_name: Some(cmd.get_localized_name()),
299 original_config: None,
300 });
301 }
302 }
303 }
304
305 {
307 let commands = command_registry.get_all();
308 let cmd_by_action: std::collections::HashMap<&str, &crate::input::commands::Command> =
309 commands
310 .iter()
311 .filter_map(|c| {
312 if let Action::PluginAction(ref name) = c.action {
313 Some((name.as_str(), c))
314 } else {
315 None
316 }
317 })
318 .collect();
319 for binding in &mut bindings {
320 if binding.command_name.is_none() {
321 if let Some(cmd) = cmd_by_action.get(binding.action.as_str()) {
322 let name = cmd.get_localized_name();
323 binding.action_display = name.clone();
326 binding.command_name = Some(name);
327 }
328 }
329 }
330 }
331
332 bindings.sort_by(|a, b| {
334 a.plugin_name
335 .cmp(&b.plugin_name)
336 .then(a.context.cmp(&b.context))
337 .then(a.action_display.cmp(&b.action_display))
338 });
339
340 bindings
341 }
342
343 fn keybinding_to_resolved(
345 kb: &Keybinding,
346 source: BindingSource,
347 _resolver: &KeybindingResolver,
348 ) -> Option<ResolvedBinding> {
349 let context = kb.when.as_deref().unwrap_or("normal").to_string();
350
351 if !kb.keys.is_empty() {
352 let key_display = format_chord_keys(&kb.keys);
354 let action_display = KeybindingResolver::format_action_from_str(&kb.action);
355 let original_config = if source == BindingSource::Custom {
356 Some(kb.clone())
357 } else {
358 None
359 };
360 Some(ResolvedBinding {
361 key_display,
362 action: kb.action.clone(),
363 action_display,
364 context,
365 source,
366 key_code: KeyCode::Null,
367 modifiers: KeyModifiers::NONE,
368 is_chord: true,
369 plugin_name: None,
370 command_name: None,
371 original_config,
372 })
373 } else if !kb.key.is_empty() {
374 let key_code = KeybindingResolver::parse_key_public(&kb.key)?;
376 let modifiers = KeybindingResolver::parse_modifiers_public(&kb.modifiers);
377 let key_display = format_keybinding(&key_code, &modifiers);
378 let action_display = KeybindingResolver::format_action_from_str(&kb.action);
379 let original_config = if source == BindingSource::Custom {
380 Some(kb.clone())
381 } else {
382 None
383 };
384 Some(ResolvedBinding {
385 key_display,
386 action: kb.action.clone(),
387 action_display,
388 context,
389 source,
390 key_code,
391 modifiers,
392 is_chord: false,
393 plugin_name: None,
394 command_name: None,
395 original_config,
396 })
397 } else {
398 None
399 }
400 }
401
402 fn collect_action_names() -> Vec<String> {
404 Action::all_action_names()
405 }
406
407 pub fn update_autocomplete(&mut self) {
409 if let Some(ref mut dialog) = self.edit_dialog {
410 let query = dialog.action_text.to_lowercase();
411 if query.is_empty() {
412 dialog.autocomplete_suggestions.clear();
413 dialog.autocomplete_visible = false;
414 dialog.autocomplete_selected = None;
415 return;
416 }
417
418 dialog.autocomplete_suggestions = self
419 .available_actions
420 .iter()
421 .filter(|a| a.to_lowercase().contains(&query))
422 .cloned()
423 .collect();
424
425 let q = query.clone();
427 dialog.autocomplete_suggestions.sort_by(|a, b| {
428 let a_prefix = a.to_lowercase().starts_with(&q);
429 let b_prefix = b.to_lowercase().starts_with(&q);
430 match (a_prefix, b_prefix) {
431 (true, false) => std::cmp::Ordering::Less,
432 (false, true) => std::cmp::Ordering::Greater,
433 _ => a.cmp(b),
434 }
435 });
436
437 dialog.autocomplete_visible = !dialog.autocomplete_suggestions.is_empty();
438 dialog.autocomplete_selected = if dialog.autocomplete_visible {
440 Some(0)
441 } else {
442 None
443 };
444 dialog.action_error = None;
446 }
447 }
448
449 pub fn is_valid_action(&self, action_name: &str) -> bool {
451 self.available_actions.iter().any(|a| a == action_name)
452 }
453
454 pub fn apply_filters(&mut self) {
456 self.filtered_indices.clear();
457
458 for (i, binding) in self.bindings.iter().enumerate() {
459 if let ContextFilter::Specific(ref ctx) = self.context_filter {
461 if &binding.context != ctx {
462 continue;
463 }
464 }
465
466 match self.source_filter {
468 SourceFilter::KeymapOnly if binding.source != BindingSource::Keymap => continue,
469 SourceFilter::CustomOnly if binding.source != BindingSource::Custom => continue,
470 SourceFilter::PluginOnly if binding.source != BindingSource::Plugin => continue,
471 _ => {}
472 }
473
474 if self.search_active {
476 match self.search_mode {
477 SearchMode::Text => {
478 if !self.search_query.is_empty() {
479 let query = self.search_query.to_lowercase();
480 let matches = binding.action.to_lowercase().contains(&query)
481 || binding.action_display.to_lowercase().contains(&query)
482 || binding.key_display.to_lowercase().contains(&query)
483 || binding.context.to_lowercase().contains(&query)
484 || binding
485 .command_name
486 .as_ref()
487 .is_some_and(|n| n.to_lowercase().contains(&query));
488 if !matches {
489 continue;
490 }
491 }
492 }
493 SearchMode::RecordKey => {
494 if let Some(search_key) = self.search_key_code {
495 if !binding.is_chord {
496 let key_matches = binding.key_code == search_key
497 && binding.modifiers == self.search_modifiers;
498 if !key_matches {
499 continue;
500 }
501 } else {
502 continue; }
504 }
505 }
506 }
507 }
508
509 self.filtered_indices.push(i);
510 }
511
512 self.build_display_rows();
514
515 if self.selected >= self.display_rows.len() {
517 self.selected = self.display_rows.len().saturating_sub(1);
518 }
519 self.ensure_visible();
520 }
521
522 fn build_display_rows(&mut self) {
524 self.display_rows.clear();
525
526 let has_active_filter = (self.search_active
527 && match self.search_mode {
528 SearchMode::Text => !self.search_query.is_empty(),
529 SearchMode::RecordKey => self.search_key_code.is_some(),
530 })
531 || !matches!(self.context_filter, ContextFilter::All)
532 || !matches!(self.source_filter, SourceFilter::All);
533
534 let mut sections: Vec<(Option<String>, Vec<usize>)> = Vec::new();
536 let mut current_section: Option<&Option<String>> = None;
537
538 for &idx in &self.filtered_indices {
539 let binding = &self.bindings[idx];
540 if current_section != Some(&binding.plugin_name) {
541 sections.push((binding.plugin_name.clone(), Vec::new()));
542 current_section = Some(&binding.plugin_name);
543 }
544 sections.last_mut().unwrap().1.push(idx);
545 }
546
547 for (plugin_name, indices) in sections {
548 let collapsed = if has_active_filter {
551 false
552 } else {
553 self.collapsed_sections.contains(&plugin_name)
554 };
555
556 self.display_rows.push(DisplayRow::SectionHeader {
557 plugin_name: plugin_name.clone(),
558 collapsed,
559 binding_count: indices.len(),
560 });
561
562 if !collapsed {
563 for idx in indices {
564 self.display_rows.push(DisplayRow::Binding(idx));
565 }
566 }
567 }
568 }
569
570 pub fn toggle_section_at_selected(&mut self) {
572 if let Some(DisplayRow::SectionHeader { plugin_name, .. }) =
573 self.display_rows.get(self.selected)
574 {
575 let key = plugin_name.clone();
576 if self.collapsed_sections.contains(&key) {
577 self.collapsed_sections.remove(&key);
578 } else {
579 self.collapsed_sections.insert(key);
580 }
581 self.build_display_rows();
582 if self.selected >= self.display_rows.len() {
584 self.selected = self.display_rows.len().saturating_sub(1);
585 }
586 self.ensure_visible();
587 }
588 }
589
590 pub fn selected_is_section_header(&self) -> bool {
592 matches!(
593 self.display_rows.get(self.selected),
594 Some(DisplayRow::SectionHeader { .. })
595 )
596 }
597
598 pub fn selected_binding(&self) -> Option<&ResolvedBinding> {
600 match self.display_rows.get(self.selected) {
601 Some(DisplayRow::Binding(idx)) => self.bindings.get(*idx),
602 _ => None,
603 }
604 }
605
606 fn selected_binding_index(&self) -> Option<usize> {
608 match self.display_rows.get(self.selected) {
609 Some(DisplayRow::Binding(idx)) => Some(*idx),
610 _ => None,
611 }
612 }
613
614 pub fn select_prev(&mut self) {
616 if self.selected > 0 {
617 self.selected -= 1;
618 self.ensure_visible();
619 }
620 }
621
622 pub fn select_next(&mut self) {
624 if self.selected + 1 < self.display_rows.len() {
625 self.selected += 1;
626 self.ensure_visible();
627 }
628 }
629
630 pub fn page_up(&mut self) {
632 let page = self.scroll.viewport as usize;
633 if self.selected > page {
634 self.selected -= page;
635 } else {
636 self.selected = 0;
637 }
638 self.ensure_visible();
639 }
640
641 pub fn page_down(&mut self) {
643 let page = self.scroll.viewport as usize;
644 self.selected = (self.selected + page).min(self.display_rows.len().saturating_sub(1));
645 self.ensure_visible();
646 }
647
648 pub fn ensure_visible_public(&mut self) {
650 self.ensure_visible();
651 }
652
653 fn ensure_visible(&mut self) {
655 self.scroll.ensure_visible(self.selected as u16, 1);
656 }
657
658 pub fn start_search(&mut self) {
660 if !self.search_active || self.search_mode != SearchMode::Text {
661 self.search_mode = SearchMode::Text;
663 if !self.search_active {
664 self.search_query.clear();
665 }
666 }
667 self.search_active = true;
668 self.search_focused = true;
669 }
670
671 pub fn start_record_key_search(&mut self) {
673 self.search_active = true;
674 self.search_focused = true;
675 self.search_mode = SearchMode::RecordKey;
676 self.search_key_display.clear();
677 self.search_key_code = None;
678 self.search_modifiers = KeyModifiers::NONE;
679 }
680
681 pub fn cancel_search(&mut self) {
683 self.search_active = false;
684 self.search_focused = false;
685 self.search_query.clear();
686 self.search_key_code = None;
687 self.search_key_display.clear();
688 self.apply_filters();
689 }
690
691 pub fn record_search_key(&mut self, event: &KeyEvent) {
693 self.search_key_code = Some(event.code);
694 self.search_modifiers = event.modifiers;
695 self.search_key_display = format_keybinding(&event.code, &event.modifiers);
696 self.apply_filters();
697 }
698
699 pub fn cycle_context_filter(&mut self) {
701 let mut contexts = vec![
702 ContextFilter::All,
703 ContextFilter::Specific("global".to_string()),
704 ContextFilter::Specific("normal".to_string()),
705 ContextFilter::Specific("prompt".to_string()),
706 ContextFilter::Specific("popup".to_string()),
707 ContextFilter::Specific("file_explorer".to_string()),
708 ContextFilter::Specific("menu".to_string()),
709 ContextFilter::Specific("terminal".to_string()),
710 ];
711 for mode_ctx in &self.mode_contexts {
713 contexts.push(ContextFilter::Specific(mode_ctx.clone()));
714 }
715
716 let current_idx = contexts
717 .iter()
718 .position(|c| c == &self.context_filter)
719 .unwrap_or(0);
720 let next_idx = (current_idx + 1) % contexts.len();
721 self.context_filter = contexts.into_iter().nth(next_idx).unwrap();
722 self.apply_filters();
723 }
724
725 pub fn cycle_source_filter(&mut self) {
727 self.source_filter = match self.source_filter {
728 SourceFilter::All => SourceFilter::CustomOnly,
729 SourceFilter::CustomOnly => SourceFilter::KeymapOnly,
730 SourceFilter::KeymapOnly => SourceFilter::PluginOnly,
731 SourceFilter::PluginOnly => SourceFilter::All,
732 };
733 self.apply_filters();
734 }
735
736 pub fn open_add_dialog(&mut self) {
738 self.edit_dialog = Some(EditBindingState::new_add_with_modes(&self.mode_contexts));
739 }
740
741 pub fn open_edit_dialog(&mut self) {
743 if let Some(idx) = self.selected_binding_index() {
744 let binding = self.bindings[idx].clone();
745 self.edit_dialog = Some(EditBindingState::new_edit_with_modes(
746 idx,
747 &binding,
748 &self.mode_contexts,
749 ));
750 }
751 }
752
753 pub fn close_edit_dialog(&mut self) {
755 self.edit_dialog = None;
756 }
757
758 pub fn delete_selected(&mut self) -> DeleteResult {
768 let Some(idx) = self.selected_binding_index() else {
769 return DeleteResult::NothingSelected;
770 };
771
772 match self.bindings[idx].source {
773 BindingSource::Custom => {
774 let binding = &self.bindings[idx];
775 let action_name = binding.action.clone();
776
777 let config_kb = binding
782 .original_config
783 .clone()
784 .unwrap_or_else(|| self.resolved_to_config_keybinding(binding));
785
786 let found_in_adds = self.pending_adds.iter().position(|kb| {
790 kb.action == config_kb.action
791 && kb.key == config_kb.key
792 && kb.modifiers == config_kb.modifiers
793 && kb.when == config_kb.when
794 });
795 if let Some(pos) = found_in_adds {
796 self.pending_adds.remove(pos);
797 } else {
798 self.pending_removes.push(config_kb);
799 }
800
801 self.bindings.remove(idx);
802 self.has_changes = true;
803
804 let still_bound = self.bindings.iter().any(|b| b.action == action_name);
806 if !still_bound {
807 let action_display = KeybindingResolver::format_action_from_str(&action_name);
808 self.bindings.push(ResolvedBinding {
809 key_display: String::new(),
810 action: action_name,
811 action_display,
812 context: String::new(),
813 source: BindingSource::Unbound,
814 key_code: KeyCode::Null,
815 modifiers: KeyModifiers::NONE,
816 is_chord: false,
817 plugin_name: None,
818 command_name: None,
819 original_config: None,
820 });
821 }
822
823 self.apply_filters();
824 DeleteResult::CustomRemoved
825 }
826 BindingSource::Keymap => {
827 let binding = &self.bindings[idx];
828 let action_name = binding.action.clone();
829
830 let noop_kb = Keybinding {
832 key: if binding.is_chord {
833 String::new()
834 } else {
835 key_code_to_config_name(binding.key_code)
836 },
837 modifiers: if binding.is_chord {
838 Vec::new()
839 } else {
840 modifiers_to_config_names(binding.modifiers)
841 },
842 keys: Vec::new(),
843 action: "noop".to_string(),
844 args: HashMap::new(),
845 when: if binding.context.is_empty() {
846 None
847 } else {
848 Some(binding.context.clone())
849 },
850 };
851 self.pending_adds.push(noop_kb);
852
853 let noop_display = KeybindingResolver::format_action_from_str("noop");
855 self.bindings[idx] = ResolvedBinding {
856 key_display: self.bindings[idx].key_display.clone(),
857 action: "noop".to_string(),
858 action_display: noop_display,
859 context: self.bindings[idx].context.clone(),
860 source: BindingSource::Custom,
861 key_code: self.bindings[idx].key_code,
862 modifiers: self.bindings[idx].modifiers,
863 is_chord: self.bindings[idx].is_chord,
864 plugin_name: self.bindings[idx].plugin_name.clone(),
865 command_name: None,
866 original_config: None,
867 };
868 self.has_changes = true;
869
870 let still_bound = self.bindings.iter().any(|b| b.action == action_name);
872 if !still_bound {
873 let action_display = KeybindingResolver::format_action_from_str(&action_name);
874 self.bindings.push(ResolvedBinding {
875 key_display: String::new(),
876 action: action_name,
877 action_display,
878 context: String::new(),
879 source: BindingSource::Unbound,
880 key_code: KeyCode::Null,
881 modifiers: KeyModifiers::NONE,
882 is_chord: false,
883 plugin_name: None,
884 command_name: None,
885 original_config: None,
886 });
887 }
888
889 self.apply_filters();
890 DeleteResult::KeymapOverridden
891 }
892 BindingSource::Plugin => {
893 let binding = &self.bindings[idx];
895 let action_name = binding.action.clone();
896
897 let noop_kb = Keybinding {
898 key: if binding.is_chord {
899 String::new()
900 } else {
901 key_code_to_config_name(binding.key_code)
902 },
903 modifiers: if binding.is_chord {
904 Vec::new()
905 } else {
906 modifiers_to_config_names(binding.modifiers)
907 },
908 keys: Vec::new(),
909 action: "noop".to_string(),
910 args: HashMap::new(),
911 when: if binding.context.is_empty() {
912 None
913 } else {
914 Some(binding.context.clone())
915 },
916 };
917 self.pending_adds.push(noop_kb);
918
919 let noop_display = KeybindingResolver::format_action_from_str("noop");
920 self.bindings[idx] = ResolvedBinding {
921 key_display: self.bindings[idx].key_display.clone(),
922 action: "noop".to_string(),
923 action_display: noop_display,
924 context: self.bindings[idx].context.clone(),
925 source: BindingSource::Custom,
926 key_code: self.bindings[idx].key_code,
927 modifiers: self.bindings[idx].modifiers,
928 is_chord: self.bindings[idx].is_chord,
929 plugin_name: self.bindings[idx].plugin_name.clone(),
930 command_name: None,
931 original_config: None,
932 };
933 self.has_changes = true;
934
935 let still_bound = self.bindings.iter().any(|b| b.action == action_name);
936 if !still_bound {
937 let action_display = KeybindingResolver::format_action_from_str(&action_name);
938 self.bindings.push(ResolvedBinding {
939 key_display: String::new(),
940 action: action_name,
941 action_display,
942 context: String::new(),
943 source: BindingSource::Unbound,
944 key_code: KeyCode::Null,
945 modifiers: KeyModifiers::NONE,
946 is_chord: false,
947 plugin_name: None,
948 command_name: None,
949 original_config: None,
950 });
951 }
952
953 self.apply_filters();
954 DeleteResult::KeymapOverridden
955 }
956 BindingSource::Unbound => DeleteResult::CannotDelete,
957 }
958 }
959
960 fn resolved_to_config_keybinding(&self, binding: &ResolvedBinding) -> Keybinding {
962 Keybinding {
963 key: if binding.is_chord {
964 String::new()
965 } else {
966 key_code_to_config_name(binding.key_code)
967 },
968 modifiers: if binding.is_chord {
969 Vec::new()
970 } else {
971 modifiers_to_config_names(binding.modifiers)
972 },
973 keys: Vec::new(),
974 action: binding.action.clone(),
975 args: HashMap::new(),
976 when: if binding.context.is_empty() {
977 None
978 } else {
979 Some(binding.context.clone())
980 },
981 }
982 }
983
984 pub fn apply_edit_dialog(&mut self) -> Option<String> {
987 let dialog = self.edit_dialog.take()?;
988
989 if dialog.key_code.is_none() || dialog.action_text.is_empty() {
990 self.edit_dialog = Some(dialog);
991 return Some(t!("keybinding_editor.error_key_action_required").to_string());
992 }
993
994 if !self.is_valid_action(&dialog.action_text) {
996 let err_msg = t!(
997 "keybinding_editor.error_unknown_action",
998 action = &dialog.action_text
999 )
1000 .to_string();
1001 let mut dialog = dialog;
1002 dialog.action_error = Some(
1003 t!(
1004 "keybinding_editor.error_unknown_action_short",
1005 action = &dialog.action_text
1006 )
1007 .to_string(),
1008 );
1009 self.edit_dialog = Some(dialog);
1010 return Some(err_msg);
1011 }
1012
1013 let key_code = dialog.key_code.unwrap();
1014 let modifiers = dialog.modifiers;
1015 let key_name = key_code_to_config_name(key_code);
1016 let modifier_names = modifiers_to_config_names(modifiers);
1017
1018 let new_binding = Keybinding {
1019 key: key_name,
1020 modifiers: modifier_names,
1021 keys: Vec::new(),
1022 action: dialog.action_text.clone(),
1023 args: HashMap::new(),
1024 when: Some(dialog.context.clone()),
1025 };
1026
1027 self.pending_adds.push(new_binding.clone());
1029 self.has_changes = true;
1030
1031 let key_display = format_keybinding(&key_code, &modifiers);
1033 let action_display = KeybindingResolver::format_action_from_str(&dialog.action_text);
1034
1035 let preserved_plugin_name = dialog
1038 .editing_index
1039 .and_then(|idx| self.bindings.get(idx))
1040 .and_then(|b| b.plugin_name.clone());
1041
1042 let resolved = ResolvedBinding {
1043 key_display,
1044 action: dialog.action_text,
1045 action_display,
1046 context: dialog.context,
1047 source: BindingSource::Custom,
1048 key_code,
1049 modifiers,
1050 is_chord: false,
1051 plugin_name: preserved_plugin_name,
1052 command_name: None,
1053 original_config: None,
1054 };
1055
1056 if let Some(edit_idx) = dialog.editing_index {
1057 if edit_idx < self.bindings.len() {
1059 self.bindings[edit_idx] = resolved;
1060 }
1061 } else {
1062 self.bindings.push(resolved);
1064 }
1065
1066 self.apply_filters();
1067 None
1068 }
1069
1070 pub fn find_conflicts(
1072 &self,
1073 key_code: KeyCode,
1074 modifiers: KeyModifiers,
1075 context: &str,
1076 ) -> Vec<String> {
1077 let mut conflicts = Vec::new();
1078
1079 for binding in &self.bindings {
1080 if !binding.is_chord
1081 && binding.key_code == key_code
1082 && binding.modifiers == modifiers
1083 && (binding.context == context
1084 || binding.context == "global"
1085 || context == "global")
1086 {
1087 conflicts.push(format!(
1088 "{} ({}, {})",
1089 binding.action_display,
1090 binding.context,
1091 match binding.source {
1092 BindingSource::Custom => "custom",
1093 BindingSource::Plugin => "plugin",
1094 _ => "keymap",
1095 }
1096 ));
1097 }
1098 }
1099
1100 conflicts
1101 }
1102
1103 pub fn get_custom_bindings(&self) -> Vec<Keybinding> {
1105 self.pending_adds.clone()
1106 }
1107
1108 pub fn get_pending_removes(&self) -> &[Keybinding] {
1110 &self.pending_removes
1111 }
1112
1113 pub fn context_filter_display(&self) -> &str {
1115 match &self.context_filter {
1116 ContextFilter::All => "All",
1117 ContextFilter::Specific(ctx) => ctx.as_str(),
1118 }
1119 }
1120
1121 pub fn source_filter_display(&self) -> &str {
1123 match &self.source_filter {
1124 SourceFilter::All => "All",
1125 SourceFilter::KeymapOnly => "Keymap",
1126 SourceFilter::CustomOnly => "Custom",
1127 SourceFilter::PluginOnly => "Plugin",
1128 }
1129 }
1130}