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::keybindings::{format_keybinding, Action, KeybindingResolver};
7use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
8use rust_i18n::t;
9use std::collections::HashMap;
10
11#[derive(Debug)]
13pub struct KeybindingEditor {
14 pub bindings: Vec<ResolvedBinding>,
16 pub filtered_indices: Vec<usize>,
18 pub selected: usize,
20 pub scroll: crate::view::ui::ScrollState,
22
23 pub search_active: bool,
25 pub search_focused: bool,
27 pub search_query: String,
29 pub search_mode: SearchMode,
31 pub search_key_display: String,
33 pub search_key_code: Option<KeyCode>,
35 pub search_modifiers: KeyModifiers,
37
38 pub context_filter: ContextFilter,
40 pub source_filter: SourceFilter,
42
43 pub edit_dialog: Option<EditBindingState>,
45
46 pub showing_help: bool,
48
49 pub active_keymap: String,
51 pub config_file_path: String,
53
54 pub pending_adds: Vec<Keybinding>,
56 pub pending_removes: Vec<Keybinding>,
58 pub has_changes: bool,
60
61 pub showing_confirm_dialog: bool,
63 pub confirm_selection: usize,
65
66 pub keymap_names: Vec<String>,
68
69 pub available_actions: Vec<String>,
71
72 pub layout: KeybindingEditorLayout,
74}
75
76impl KeybindingEditor {
77 pub fn new(config: &Config, resolver: &KeybindingResolver, config_file_path: String) -> Self {
79 let bindings = Self::resolve_all_bindings(config, resolver);
80 let filtered_indices: Vec<usize> = (0..bindings.len()).collect();
81
82 let available_actions = Self::collect_action_names();
84
85 let mut keymap_names: Vec<String> = config.keybinding_maps.keys().cloned().collect();
87 keymap_names.sort();
88
89 let mut editor = Self {
90 bindings,
91 filtered_indices,
92 selected: 0,
93 scroll: crate::view::ui::ScrollState::default(),
94 search_active: false,
95 search_focused: false,
96 search_query: String::new(),
97 search_mode: SearchMode::Text,
98 search_key_display: String::new(),
99 search_key_code: None,
100 search_modifiers: KeyModifiers::NONE,
101 context_filter: ContextFilter::All,
102 source_filter: SourceFilter::All,
103 edit_dialog: None,
104 showing_help: false,
105 active_keymap: config.active_keybinding_map.to_string(),
106 config_file_path,
107 pending_adds: Vec::new(),
108 pending_removes: Vec::new(),
109 has_changes: false,
110 showing_confirm_dialog: false,
111 confirm_selection: 0,
112 keymap_names,
113 available_actions,
114 layout: KeybindingEditorLayout::default(),
115 };
116
117 editor.apply_filters();
118 editor
119 }
120
121 fn resolve_all_bindings(
123 config: &Config,
124 resolver: &KeybindingResolver,
125 ) -> Vec<ResolvedBinding> {
126 let mut bindings = Vec::new();
127 let mut seen: HashMap<(String, String), usize> = HashMap::new(); let map_bindings = config.resolve_keymap(&config.active_keybinding_map);
131 for kb in &map_bindings {
132 if let Some(entry) = Self::keybinding_to_resolved(kb, BindingSource::Keymap, resolver) {
133 let key = (entry.key_display.clone(), entry.context.clone());
134 let idx = bindings.len();
135 seen.insert(key, idx);
136 bindings.push(entry);
137 }
138 }
139
140 for kb in &config.keybindings {
142 if let Some(entry) = Self::keybinding_to_resolved(kb, BindingSource::Custom, resolver) {
143 let key = (entry.key_display.clone(), entry.context.clone());
144 if let Some(&existing_idx) = seen.get(&key) {
145 bindings[existing_idx] = entry;
147 } else {
148 let idx = bindings.len();
149 seen.insert(key, idx);
150 bindings.push(entry);
151 }
152 }
153 }
154
155 let bound_actions: std::collections::HashSet<String> =
157 bindings.iter().map(|b| b.action.clone()).collect();
158 for action_name in Action::all_action_names() {
159 if !bound_actions.contains(&action_name) {
160 let action_display = KeybindingResolver::format_action_from_str(&action_name);
161 bindings.push(ResolvedBinding {
162 key_display: String::new(),
163 action: action_name,
164 action_display,
165 context: String::new(),
166 source: BindingSource::Unbound,
167 key_code: KeyCode::Null,
168 modifiers: KeyModifiers::NONE,
169 is_chord: false,
170 });
171 }
172 }
173
174 bindings.sort_by(|a, b| {
176 a.context
177 .cmp(&b.context)
178 .then(a.action_display.cmp(&b.action_display))
179 });
180
181 bindings
182 }
183
184 fn keybinding_to_resolved(
186 kb: &Keybinding,
187 source: BindingSource,
188 _resolver: &KeybindingResolver,
189 ) -> Option<ResolvedBinding> {
190 let context = kb.when.as_deref().unwrap_or("normal").to_string();
191
192 if !kb.keys.is_empty() {
193 let key_display = format_chord_keys(&kb.keys);
195 let action_display = KeybindingResolver::format_action_from_str(&kb.action);
196 Some(ResolvedBinding {
197 key_display,
198 action: kb.action.clone(),
199 action_display,
200 context,
201 source,
202 key_code: KeyCode::Null,
203 modifiers: KeyModifiers::NONE,
204 is_chord: true,
205 })
206 } else if !kb.key.is_empty() {
207 let key_code = KeybindingResolver::parse_key_public(&kb.key)?;
209 let modifiers = KeybindingResolver::parse_modifiers_public(&kb.modifiers);
210 let key_display = format_keybinding(&key_code, &modifiers);
211 let action_display = KeybindingResolver::format_action_from_str(&kb.action);
212 Some(ResolvedBinding {
213 key_display,
214 action: kb.action.clone(),
215 action_display,
216 context,
217 source,
218 key_code,
219 modifiers,
220 is_chord: false,
221 })
222 } else {
223 None
224 }
225 }
226
227 fn collect_action_names() -> Vec<String> {
229 Action::all_action_names()
230 }
231
232 pub fn update_autocomplete(&mut self) {
234 if let Some(ref mut dialog) = self.edit_dialog {
235 let query = dialog.action_text.to_lowercase();
236 if query.is_empty() {
237 dialog.autocomplete_suggestions.clear();
238 dialog.autocomplete_visible = false;
239 dialog.autocomplete_selected = None;
240 return;
241 }
242
243 dialog.autocomplete_suggestions = self
244 .available_actions
245 .iter()
246 .filter(|a| a.to_lowercase().contains(&query))
247 .cloned()
248 .collect();
249
250 let q = query.clone();
252 dialog.autocomplete_suggestions.sort_by(|a, b| {
253 let a_prefix = a.to_lowercase().starts_with(&q);
254 let b_prefix = b.to_lowercase().starts_with(&q);
255 match (a_prefix, b_prefix) {
256 (true, false) => std::cmp::Ordering::Less,
257 (false, true) => std::cmp::Ordering::Greater,
258 _ => a.cmp(b),
259 }
260 });
261
262 dialog.autocomplete_visible = !dialog.autocomplete_suggestions.is_empty();
263 dialog.autocomplete_selected = if dialog.autocomplete_visible {
265 Some(0)
266 } else {
267 None
268 };
269 dialog.action_error = None;
271 }
272 }
273
274 pub fn is_valid_action(&self, action_name: &str) -> bool {
276 self.available_actions.iter().any(|a| a == action_name)
277 }
278
279 pub fn apply_filters(&mut self) {
281 self.filtered_indices.clear();
282
283 for (i, binding) in self.bindings.iter().enumerate() {
284 if let ContextFilter::Specific(ref ctx) = self.context_filter {
286 if &binding.context != ctx {
287 continue;
288 }
289 }
290
291 match self.source_filter {
293 SourceFilter::KeymapOnly if binding.source != BindingSource::Keymap => continue,
294 SourceFilter::CustomOnly if binding.source != BindingSource::Custom => continue,
295 _ => {}
296 }
297
298 if self.search_active {
300 match self.search_mode {
301 SearchMode::Text => {
302 if !self.search_query.is_empty() {
303 let query = self.search_query.to_lowercase();
304 let matches = binding.action.to_lowercase().contains(&query)
305 || binding.action_display.to_lowercase().contains(&query)
306 || binding.key_display.to_lowercase().contains(&query)
307 || binding.context.to_lowercase().contains(&query);
308 if !matches {
309 continue;
310 }
311 }
312 }
313 SearchMode::RecordKey => {
314 if let Some(search_key) = self.search_key_code {
315 if !binding.is_chord {
316 let key_matches = binding.key_code == search_key
317 && binding.modifiers == self.search_modifiers;
318 if !key_matches {
319 continue;
320 }
321 } else {
322 continue; }
324 }
325 }
326 }
327 }
328
329 self.filtered_indices.push(i);
330 }
331
332 if self.selected >= self.filtered_indices.len() {
334 self.selected = self.filtered_indices.len().saturating_sub(1);
335 }
336 self.ensure_visible();
337 }
338
339 pub fn selected_binding(&self) -> Option<&ResolvedBinding> {
341 self.filtered_indices
342 .get(self.selected)
343 .and_then(|&i| self.bindings.get(i))
344 }
345
346 pub fn select_prev(&mut self) {
348 if self.selected > 0 {
349 self.selected -= 1;
350 self.ensure_visible();
351 }
352 }
353
354 pub fn select_next(&mut self) {
356 if self.selected + 1 < self.filtered_indices.len() {
357 self.selected += 1;
358 self.ensure_visible();
359 }
360 }
361
362 pub fn page_up(&mut self) {
364 let page = self.scroll.viewport as usize;
365 if self.selected > page {
366 self.selected -= page;
367 } else {
368 self.selected = 0;
369 }
370 self.ensure_visible();
371 }
372
373 pub fn page_down(&mut self) {
375 let page = self.scroll.viewport as usize;
376 self.selected = (self.selected + page).min(self.filtered_indices.len().saturating_sub(1));
377 self.ensure_visible();
378 }
379
380 pub fn ensure_visible_public(&mut self) {
382 self.ensure_visible();
383 }
384
385 fn ensure_visible(&mut self) {
387 self.scroll.ensure_visible(self.selected as u16, 1);
388 }
389
390 pub fn start_search(&mut self) {
392 if !self.search_active || self.search_mode != SearchMode::Text {
393 self.search_mode = SearchMode::Text;
395 if !self.search_active {
396 self.search_query.clear();
397 }
398 }
399 self.search_active = true;
400 self.search_focused = true;
401 }
402
403 pub fn start_record_key_search(&mut self) {
405 self.search_active = true;
406 self.search_focused = true;
407 self.search_mode = SearchMode::RecordKey;
408 self.search_key_display.clear();
409 self.search_key_code = None;
410 self.search_modifiers = KeyModifiers::NONE;
411 }
412
413 pub fn cancel_search(&mut self) {
415 self.search_active = false;
416 self.search_focused = false;
417 self.search_query.clear();
418 self.search_key_code = None;
419 self.search_key_display.clear();
420 self.apply_filters();
421 }
422
423 pub fn record_search_key(&mut self, event: &KeyEvent) {
425 self.search_key_code = Some(event.code);
426 self.search_modifiers = event.modifiers;
427 self.search_key_display = format_keybinding(&event.code, &event.modifiers);
428 self.apply_filters();
429 }
430
431 pub fn cycle_context_filter(&mut self) {
433 let contexts = vec![
434 ContextFilter::All,
435 ContextFilter::Specific("global".to_string()),
436 ContextFilter::Specific("normal".to_string()),
437 ContextFilter::Specific("prompt".to_string()),
438 ContextFilter::Specific("popup".to_string()),
439 ContextFilter::Specific("file_explorer".to_string()),
440 ContextFilter::Specific("menu".to_string()),
441 ContextFilter::Specific("terminal".to_string()),
442 ];
443
444 let current_idx = contexts
445 .iter()
446 .position(|c| c == &self.context_filter)
447 .unwrap_or(0);
448 let next_idx = (current_idx + 1) % contexts.len();
449 self.context_filter = contexts.into_iter().nth(next_idx).unwrap();
450 self.apply_filters();
451 }
452
453 pub fn cycle_source_filter(&mut self) {
455 self.source_filter = match self.source_filter {
456 SourceFilter::All => SourceFilter::CustomOnly,
457 SourceFilter::CustomOnly => SourceFilter::KeymapOnly,
458 SourceFilter::KeymapOnly => SourceFilter::All,
459 };
460 self.apply_filters();
461 }
462
463 pub fn open_add_dialog(&mut self) {
465 self.edit_dialog = Some(EditBindingState::new_add());
466 }
467
468 pub fn open_edit_dialog(&mut self) {
470 if let Some(binding) = self.selected_binding().cloned() {
471 let idx = self.filtered_indices[self.selected];
472 self.edit_dialog = Some(EditBindingState::new_edit(idx, &binding));
473 }
474 }
475
476 pub fn close_edit_dialog(&mut self) {
478 self.edit_dialog = None;
479 }
480
481 pub fn delete_selected(&mut self) -> DeleteResult {
491 let Some(&idx) = self.filtered_indices.get(self.selected) else {
492 return DeleteResult::NothingSelected;
493 };
494
495 match self.bindings[idx].source {
496 BindingSource::Custom => {
497 let binding = &self.bindings[idx];
498 let action_name = binding.action.clone();
499
500 let config_kb = self.resolved_to_config_keybinding(binding);
502
503 let found_in_adds = self.pending_adds.iter().position(|kb| {
507 kb.action == config_kb.action
508 && kb.key == config_kb.key
509 && kb.modifiers == config_kb.modifiers
510 && kb.when == config_kb.when
511 });
512 if let Some(pos) = found_in_adds {
513 self.pending_adds.remove(pos);
514 } else {
515 self.pending_removes.push(config_kb);
516 }
517
518 self.bindings.remove(idx);
519 self.has_changes = true;
520
521 let still_bound = self.bindings.iter().any(|b| b.action == action_name);
523 if !still_bound {
524 let action_display = KeybindingResolver::format_action_from_str(&action_name);
525 self.bindings.push(ResolvedBinding {
526 key_display: String::new(),
527 action: action_name,
528 action_display,
529 context: String::new(),
530 source: BindingSource::Unbound,
531 key_code: KeyCode::Null,
532 modifiers: KeyModifiers::NONE,
533 is_chord: false,
534 });
535 }
536
537 self.apply_filters();
538 DeleteResult::CustomRemoved
539 }
540 BindingSource::Keymap => {
541 let binding = &self.bindings[idx];
542 let action_name = binding.action.clone();
543
544 let noop_kb = Keybinding {
546 key: if binding.is_chord {
547 String::new()
548 } else {
549 key_code_to_config_name(binding.key_code)
550 },
551 modifiers: if binding.is_chord {
552 Vec::new()
553 } else {
554 modifiers_to_config_names(binding.modifiers)
555 },
556 keys: Vec::new(),
557 action: "noop".to_string(),
558 args: HashMap::new(),
559 when: if binding.context.is_empty() {
560 None
561 } else {
562 Some(binding.context.clone())
563 },
564 };
565 self.pending_adds.push(noop_kb);
566
567 let noop_display = KeybindingResolver::format_action_from_str("noop");
569 self.bindings[idx] = ResolvedBinding {
570 key_display: self.bindings[idx].key_display.clone(),
571 action: "noop".to_string(),
572 action_display: noop_display,
573 context: self.bindings[idx].context.clone(),
574 source: BindingSource::Custom,
575 key_code: self.bindings[idx].key_code,
576 modifiers: self.bindings[idx].modifiers,
577 is_chord: self.bindings[idx].is_chord,
578 };
579 self.has_changes = true;
580
581 let still_bound = self.bindings.iter().any(|b| b.action == action_name);
583 if !still_bound {
584 let action_display = KeybindingResolver::format_action_from_str(&action_name);
585 self.bindings.push(ResolvedBinding {
586 key_display: String::new(),
587 action: action_name,
588 action_display,
589 context: String::new(),
590 source: BindingSource::Unbound,
591 key_code: KeyCode::Null,
592 modifiers: KeyModifiers::NONE,
593 is_chord: false,
594 });
595 }
596
597 self.apply_filters();
598 DeleteResult::KeymapOverridden
599 }
600 BindingSource::Unbound => DeleteResult::CannotDelete,
601 }
602 }
603
604 fn resolved_to_config_keybinding(&self, binding: &ResolvedBinding) -> Keybinding {
606 Keybinding {
607 key: if binding.is_chord {
608 String::new()
609 } else {
610 key_code_to_config_name(binding.key_code)
611 },
612 modifiers: if binding.is_chord {
613 Vec::new()
614 } else {
615 modifiers_to_config_names(binding.modifiers)
616 },
617 keys: Vec::new(),
618 action: binding.action.clone(),
619 args: HashMap::new(),
620 when: if binding.context.is_empty() {
621 None
622 } else {
623 Some(binding.context.clone())
624 },
625 }
626 }
627
628 pub fn apply_edit_dialog(&mut self) -> Option<String> {
631 let dialog = self.edit_dialog.take()?;
632
633 if dialog.key_code.is_none() || dialog.action_text.is_empty() {
634 self.edit_dialog = Some(dialog);
635 return Some(t!("keybinding_editor.error_key_action_required").to_string());
636 }
637
638 if !self.is_valid_action(&dialog.action_text) {
640 let err_msg = t!(
641 "keybinding_editor.error_unknown_action",
642 action = &dialog.action_text
643 )
644 .to_string();
645 let mut dialog = dialog;
646 dialog.action_error = Some(
647 t!(
648 "keybinding_editor.error_unknown_action_short",
649 action = &dialog.action_text
650 )
651 .to_string(),
652 );
653 self.edit_dialog = Some(dialog);
654 return Some(err_msg);
655 }
656
657 let key_code = dialog.key_code.unwrap();
658 let modifiers = dialog.modifiers;
659 let key_name = key_code_to_config_name(key_code);
660 let modifier_names = modifiers_to_config_names(modifiers);
661
662 let new_binding = Keybinding {
663 key: key_name,
664 modifiers: modifier_names,
665 keys: Vec::new(),
666 action: dialog.action_text.clone(),
667 args: HashMap::new(),
668 when: Some(dialog.context.clone()),
669 };
670
671 self.pending_adds.push(new_binding.clone());
673 self.has_changes = true;
674
675 let key_display = format_keybinding(&key_code, &modifiers);
677 let action_display = KeybindingResolver::format_action_from_str(&dialog.action_text);
678
679 let resolved = ResolvedBinding {
680 key_display,
681 action: dialog.action_text,
682 action_display,
683 context: dialog.context,
684 source: BindingSource::Custom,
685 key_code,
686 modifiers,
687 is_chord: false,
688 };
689
690 if let Some(edit_idx) = dialog.editing_index {
691 if edit_idx < self.bindings.len() {
693 self.bindings[edit_idx] = resolved;
694 }
695 } else {
696 self.bindings.push(resolved);
698 }
699
700 self.apply_filters();
701 None
702 }
703
704 pub fn find_conflicts(
706 &self,
707 key_code: KeyCode,
708 modifiers: KeyModifiers,
709 context: &str,
710 ) -> Vec<String> {
711 let mut conflicts = Vec::new();
712
713 for binding in &self.bindings {
714 if !binding.is_chord
715 && binding.key_code == key_code
716 && binding.modifiers == modifiers
717 && (binding.context == context
718 || binding.context == "global"
719 || context == "global")
720 {
721 conflicts.push(format!(
722 "{} ({}, {})",
723 binding.action_display,
724 binding.context,
725 if binding.source == BindingSource::Custom {
726 "custom"
727 } else {
728 "keymap"
729 }
730 ));
731 }
732 }
733
734 conflicts
735 }
736
737 pub fn get_custom_bindings(&self) -> Vec<Keybinding> {
739 self.pending_adds.clone()
740 }
741
742 pub fn get_pending_removes(&self) -> &[Keybinding] {
744 &self.pending_removes
745 }
746
747 pub fn context_filter_display(&self) -> &str {
749 match &self.context_filter {
750 ContextFilter::All => "All",
751 ContextFilter::Specific(ctx) => ctx.as_str(),
752 }
753 }
754
755 pub fn source_filter_display(&self) -> &str {
757 match &self.source_filter {
758 SourceFilter::All => "All",
759 SourceFilter::KeymapOnly => "Keymap",
760 SourceFilter::CustomOnly => "Custom",
761 }
762 }
763}