Skip to main content

fresh/app/keybinding_editor/
editor.rs

1//! KeybindingEditor - the main editor state and logic.
2
3use 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/// The main keybinding editor state
12#[derive(Debug)]
13pub struct KeybindingEditor {
14    /// All resolved bindings
15    pub bindings: Vec<ResolvedBinding>,
16    /// Indices into `bindings` after filtering/searching
17    pub filtered_indices: Vec<usize>,
18    /// Currently selected index (within filtered list)
19    pub selected: usize,
20    /// Scroll state (offset, viewport, content_height) — shared with render
21    pub scroll: crate::view::ui::ScrollState,
22
23    /// Whether search is active (search bar visible)
24    pub search_active: bool,
25    /// Whether search input is focused (accepting keystrokes)
26    pub search_focused: bool,
27    /// Search query string
28    pub search_query: String,
29    /// Search mode (text or record key)
30    pub search_mode: SearchMode,
31    /// Recorded search key display (when in RecordKey mode)
32    pub search_key_display: String,
33    /// Recorded search key code (when in RecordKey mode)
34    pub search_key_code: Option<KeyCode>,
35    /// Recorded search modifiers (when in RecordKey mode)
36    pub search_modifiers: KeyModifiers,
37
38    /// Context filter
39    pub context_filter: ContextFilter,
40    /// Source filter
41    pub source_filter: SourceFilter,
42
43    /// Edit/add binding dialog state (None = not open)
44    pub edit_dialog: Option<EditBindingState>,
45
46    /// Whether help overlay is showing
47    pub showing_help: bool,
48
49    /// Active keymap name
50    pub active_keymap: String,
51    /// Config file path for display
52    pub config_file_path: String,
53
54    /// Custom bindings that have been added (pending save)
55    pub pending_adds: Vec<Keybinding>,
56    /// Custom bindings to remove from config (pending save)
57    pub pending_removes: Vec<Keybinding>,
58    /// Whether there are unsaved changes
59    pub has_changes: bool,
60
61    /// Showing unsaved changes confirmation dialog
62    pub showing_confirm_dialog: bool,
63    /// Selected button in confirm dialog (0=Save, 1=Discard, 2=Cancel)
64    pub confirm_selection: usize,
65
66    /// Named keymaps info for display
67    pub keymap_names: Vec<String>,
68
69    /// Available action names (for autocomplete)
70    pub available_actions: Vec<String>,
71
72    /// Layout info for mouse hit testing (updated during render)
73    pub layout: KeybindingEditorLayout,
74}
75
76impl KeybindingEditor {
77    /// Create a new keybinding editor from config and resolver
78    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        // Collect available action names
83        let available_actions = Self::collect_action_names();
84
85        // Collect keymap names
86        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    /// Resolve all bindings from the active keymap + custom overrides
122    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(); // (key_display, context) -> index
128
129        // First, load bindings from the active keymap
130        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        // Then, load custom bindings (these override keymap bindings)
141        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                    // Override the existing binding
146                    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        // Add entries for actions that have no keybinding
156        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        // Sort by context, then by action name
175        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    /// Convert a Keybinding config entry to a ResolvedBinding
185    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            // Chord binding
194            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            // Single key binding
208            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    /// Collect all available action names (delegates to the macro-generated source of truth)
228    fn collect_action_names() -> Vec<String> {
229        Action::all_action_names()
230    }
231
232    /// Update autocomplete suggestions based on current action text
233    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            // Sort: exact prefix matches first, then contains matches
251            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            // Reset selection when text changes
264            dialog.autocomplete_selected = if dialog.autocomplete_visible {
265                Some(0)
266            } else {
267                None
268            };
269            // Clear any previous error
270            dialog.action_error = None;
271        }
272    }
273
274    /// Check if the given action name is valid
275    pub fn is_valid_action(&self, action_name: &str) -> bool {
276        self.available_actions.iter().any(|a| a == action_name)
277    }
278
279    /// Apply current search and filter criteria
280    pub fn apply_filters(&mut self) {
281        self.filtered_indices.clear();
282
283        for (i, binding) in self.bindings.iter().enumerate() {
284            // Apply context filter
285            if let ContextFilter::Specific(ref ctx) = self.context_filter {
286                if &binding.context != ctx {
287                    continue;
288                }
289            }
290
291            // Apply source filter
292            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            // Apply search
299            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; // Skip chords in key search mode
323                            }
324                        }
325                    }
326                }
327            }
328
329            self.filtered_indices.push(i);
330        }
331
332        // Reset selection if it's out of bounds
333        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    /// Get the currently selected binding
340    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    /// Move selection up
347    pub fn select_prev(&mut self) {
348        if self.selected > 0 {
349            self.selected -= 1;
350            self.ensure_visible();
351        }
352    }
353
354    /// Move selection down
355    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    /// Page up
363    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    /// Page down
374    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    /// Ensure the selected item is visible (public version)
381    pub fn ensure_visible_public(&mut self) {
382        self.ensure_visible();
383    }
384
385    /// Ensure the selected item is visible
386    fn ensure_visible(&mut self) {
387        self.scroll.ensure_visible(self.selected as u16, 1);
388    }
389
390    /// Start text search (preserves existing query when re-focusing)
391    pub fn start_search(&mut self) {
392        if !self.search_active || self.search_mode != SearchMode::Text {
393            // Starting fresh or switching from record mode
394            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    /// Start record-key search
404    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    /// Cancel search (clear everything)
414    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    /// Record a search key
424    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    /// Cycle context filter
432    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    /// Cycle source filter
454    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    /// Open the add binding dialog
464    pub fn open_add_dialog(&mut self) {
465        self.edit_dialog = Some(EditBindingState::new_add());
466    }
467
468    /// Open the edit binding dialog for the selected binding
469    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    /// Close the edit dialog
477    pub fn close_edit_dialog(&mut self) {
478        self.edit_dialog = None;
479    }
480
481    /// Delete the selected binding.
482    ///
483    /// * **Custom** bindings are removed outright (tracked in `pending_removes`
484    ///   or dropped from `pending_adds` when added in the same session).
485    /// * **Keymap** bindings cannot be removed from the built-in map, so a
486    ///   custom `noop` override is created for the same key, which shadows the
487    ///   default binding in the resolver.
488    ///
489    /// Returns `DeleteResult` indicating what happened.
490    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                // Build config-level Keybinding for matching during save
501                let config_kb = self.resolved_to_config_keybinding(binding);
502
503                // If this binding was added in the current session, just
504                // remove it from pending_adds. Otherwise track for removal
505                // from the persisted config.
506                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                // If no other binding exists for this action, re-add as unbound
522                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                // Build a noop custom override for the same key+context
545                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                // Replace the keymap entry with a noop custom entry in the display
568                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                // The original action may now be unbound
582                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    /// Convert a ResolvedBinding to a config-level Keybinding (for matching).
605    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    /// Apply the edit dialog to create/update a binding.
629    /// Returns an error message if validation fails.
630    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        // Validate the action name
639        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        // Add as custom binding
672        self.pending_adds.push(new_binding.clone());
673        self.has_changes = true;
674
675        // Update display
676        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            // Editing existing - replace it
692            if edit_idx < self.bindings.len() {
693                self.bindings[edit_idx] = resolved;
694            }
695        } else {
696            // Adding new
697            self.bindings.push(resolved);
698        }
699
700        self.apply_filters();
701        None
702    }
703
704    /// Check for conflicts with the given key combination
705    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    /// Get the custom bindings to save to config
738    pub fn get_custom_bindings(&self) -> Vec<Keybinding> {
739        self.pending_adds.clone()
740    }
741
742    /// Get the custom bindings to remove from config
743    pub fn get_pending_removes(&self) -> &[Keybinding] {
744        &self.pending_removes
745    }
746
747    /// Get the context filter display string
748    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    /// Get the source filter display string
756    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}