Skip to main content

tui_pages/keybindings/
config.rs

1use std::fmt;
2
3use toml::{Value, map::Map};
4
5use crate::input::{
6    BindingCatalog, BindingConflict, BindingInfo, BindingLayer, BindingSource, InputRegistry,
7    KeyChord, ParseKeyError, analyze_keymap_bindings, try_parse_binding,
8};
9#[cfg(feature = "canvas")]
10use crate::runtime::InputLayerContext;
11use crate::runtime::ModeId;
12
13use super::preset::parse_string_list;
14use super::{ActionRegistry, NavigationPresetError, NavigationPresetIssue};
15
16#[cfg(feature = "canvas")]
17use crate::canvas::{
18    BuiltinCanvasKeybindingPreset, CanvasAction, CanvasKeybindingPresetError,
19    CanvasKeybindingProfile, analyze_canvas_overlaps, canvas_default_binding_catalog,
20};
21
22#[derive(Debug)]
23pub enum KeybindingConfigError {
24    Toml(toml::de::Error),
25    Serialize(toml::ser::Error),
26    Navigation(NavigationPresetError),
27    KeyBinding(ParseKeyError),
28    CanvasPreset {
29        preset: String,
30    },
31    CanvasAction {
32        action: String,
33    },
34    #[cfg(feature = "canvas")]
35    Canvas(CanvasKeybindingPresetError),
36}
37
38impl fmt::Display for KeybindingConfigError {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self {
41            Self::Toml(err) => write!(f, "invalid keybinding TOML: {err}"),
42            Self::Serialize(err) => write!(f, "failed to serialize keybinding TOML section: {err}"),
43            Self::Navigation(err) => write!(f, "invalid navigation keybindings: {err}"),
44            Self::KeyBinding(err) => write!(f, "invalid keybinding: {err}"),
45            Self::CanvasPreset { preset } => {
46                write!(f, "unknown canvas keybinding preset {preset:?}")
47            }
48            Self::CanvasAction { action } => {
49                write!(f, "unknown canvas keybinding action {action:?}")
50            }
51            #[cfg(feature = "canvas")]
52            Self::Canvas(err) => write!(f, "invalid canvas keybindings: {err}"),
53        }
54    }
55}
56
57impl std::error::Error for KeybindingConfigError {
58    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
59        match self {
60            Self::Toml(err) => Some(err),
61            Self::Serialize(err) => Some(err),
62            Self::Navigation(err) => Some(err),
63            Self::KeyBinding(err) => Some(err),
64            #[cfg(feature = "canvas")]
65            Self::Canvas(err) => Some(err),
66            Self::CanvasPreset { .. } => None,
67            Self::CanvasAction { .. } => None,
68        }
69    }
70}
71
72/// A mode keybinding table parsed but **not** yet resolved to an action type. The
73/// schema names actions as strings; resolution to the application's `A` happens
74/// later via an [`ActionRegistry`], so an app can bind its own action names —
75/// not just the built-in navigation ones.
76#[derive(Debug, Clone, PartialEq, Eq, Default)]
77pub struct RawKeymap {
78    pub sections: Vec<RawKeymapSection>,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct RawKeymapSection {
83    /// The TOML section name.
84    pub name: String,
85    /// The input mode this section binds in (defaults to the section name).
86    pub mode: String,
87    pub bindings: Vec<RawKeymapBinding>,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct RawKeymapBinding {
92    /// The config action name, resolved to `A` via an [`ActionRegistry`].
93    pub name: String,
94    /// The key bindings (each a chord-sequence string like `"ctrl+q"`).
95    pub keys: Vec<String>,
96}
97
98impl RawKeymap {
99    fn from_root(root: &Map<String, Value>) -> (Self, Vec<NavigationPresetIssue>) {
100        let (mut keymap, mut issues) = Self::from_value(root.get("keymap"));
101        for (section_name, section_value) in root {
102            if matches!(section_name.as_str(), "keymap" | "canvas") {
103                continue;
104            }
105            Self::push_section(
106                &mut keymap.sections,
107                section_name,
108                section_value,
109                &mut issues,
110            );
111        }
112        (keymap, issues)
113    }
114
115    /// Parse the `[keymap]` table, collecting malformed-entry issues. Unknown
116    /// *action names* are not flagged here — that needs the registry and happens
117    /// during resolution.
118    fn from_value(value: Option<&Value>) -> (Self, Vec<NavigationPresetIssue>) {
119        let mut issues = Vec::new();
120        let Some(value) = value else {
121            return (Self::default(), issues);
122        };
123        let Some(table) = value.as_table() else {
124            issues.push(NavigationPresetIssue::RootNotTable);
125            return (Self::default(), issues);
126        };
127
128        let mut sections = Vec::with_capacity(table.len());
129        for (section_name, section_value) in table {
130            Self::push_section(&mut sections, section_name, section_value, &mut issues);
131        }
132
133        (Self { sections }, issues)
134    }
135
136    fn push_section(
137        sections: &mut Vec<RawKeymapSection>,
138        section_name: &str,
139        section_value: &Value,
140        issues: &mut Vec<NavigationPresetIssue>,
141    ) {
142        let Some(section) = section_value.as_table() else {
143            issues.push(NavigationPresetIssue::SectionNotTable {
144                section: section_name.to_string(),
145            });
146            return;
147        };
148        let mode = match section.get("mode") {
149            Some(value) => value.as_str().map(ToString::to_string).unwrap_or_else(|| {
150                issues.push(NavigationPresetIssue::ModeNotString {
151                    section: section_name.to_string(),
152                });
153                section_name.to_string()
154            }),
155            None => section_name.to_string(),
156        };
157
158        let mut bindings = Vec::new();
159        for (action_name, bindings_value) in section {
160            if action_name == "mode" {
161                continue;
162            }
163            let Some(keys) = parse_string_list(section_name, action_name, bindings_value, issues)
164            else {
165                continue;
166            };
167            if keys.is_empty() {
168                issues.push(NavigationPresetIssue::EmptyBindings {
169                    section: section_name.to_string(),
170                    action: action_name.clone(),
171                });
172                continue;
173            }
174            bindings.push(RawKeymapBinding {
175                name: action_name.clone(),
176                keys,
177            });
178        }
179
180        sections.push(RawKeymapSection {
181            name: section_name.to_string(),
182            mode,
183            bindings,
184        });
185    }
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct KeybindingConfig {
190    pub keymap: RawKeymap,
191    pub keymap_issues: Vec<NavigationPresetIssue>,
192    #[cfg(feature = "canvas")]
193    pub canvas_preset: BuiltinCanvasKeybindingPreset,
194    #[cfg(feature = "canvas")]
195    pub canvas_overrides_toml: String,
196}
197
198impl KeybindingConfig {
199    pub fn from_toml(source: &str) -> Result<Self, KeybindingConfigError> {
200        let value = if source.trim().is_empty() {
201            Value::Table(Map::new())
202        } else {
203            toml::from_str::<Value>(source).map_err(KeybindingConfigError::Toml)?
204        };
205        let root = value.as_table().cloned().unwrap_or_else(Map::new);
206
207        #[cfg_attr(not(feature = "canvas"), allow(unused_mut))]
208        let (mut keymap, keymap_issues) = RawKeymap::from_root(&root);
209
210        #[cfg(feature = "canvas")]
211        {
212            let canvas = root.get("canvas").and_then(Value::as_table);
213            let canvas_preset = canvas
214                .and_then(|table| table.get("preset"))
215                .and_then(Value::as_str)
216                .map(|preset| {
217                    preset
218                        .parse::<BuiltinCanvasKeybindingPreset>()
219                        .map_err(|err| KeybindingConfigError::CanvasPreset {
220                            preset: err.name().to_string(),
221                        })
222                })
223                .transpose()?
224                .unwrap_or(BuiltinCanvasKeybindingPreset::Vim);
225
226            // Canvas overrides come from two places, merged into one table:
227            //   * the explicit `[canvas.bindings.<mode>]` sections, and
228            //   * canvas-action bindings written in modal keymap sections
229            //     (`[nor]`, `[keymap.nor]`, etc.). The latter let a consumer remap a
230            //     canvas key in the same place it binds everything else; we move
231            //     them out of the keymap here so they drive the canvas profile
232            //     rather than the navigation keymap.
233            let mut canvas_overrides: Map<String, Value> = canvas
234                .and_then(|table| table.get("bindings"))
235                .and_then(Value::as_table)
236                .cloned()
237                .unwrap_or_default();
238            fold_keymap_canvas_overrides(&mut keymap, &mut canvas_overrides);
239
240            let canvas_overrides_toml = if canvas_overrides.is_empty() {
241                String::new()
242            } else {
243                toml::to_string(&Value::Table(canvas_overrides))
244                    .map_err(KeybindingConfigError::Serialize)?
245            };
246
247            Ok(Self {
248                keymap,
249                keymap_issues,
250                canvas_preset,
251                canvas_overrides_toml,
252            })
253        }
254
255        #[cfg(not(feature = "canvas"))]
256        {
257            Ok(Self {
258                keymap,
259                keymap_issues,
260            })
261        }
262    }
263
264    #[cfg(feature = "canvas")]
265    pub fn canvas_profile(&self) -> Result<CanvasKeybindingProfile, KeybindingConfigError> {
266        CanvasKeybindingProfile::with_overrides_toml(
267            self.canvas_preset,
268            &self.canvas_overrides_toml,
269        )
270        .map_err(KeybindingConfigError::Canvas)
271    }
272}
273
274/// Whether an action *name* belongs to the canvas modal editor rather than the
275/// navigation keymap. `exit` / `enter_decider` are excluded: they are canvas
276/// vocabulary, but the keymap owns them (an app typically binds `exit` to quit),
277/// so they stay keymap bindings. Mirrors the routing the client used to do.
278#[cfg(feature = "canvas")]
279fn is_canvas_override_action(name: &str) -> bool {
280    use crate::canvas::CanvasKeyAction as C;
281    !matches!(
282        C::from_name(name),
283        C::Unknown(_) | C::Exit | C::EnterDecider
284    )
285}
286
287/// Move canvas-action bindings out of the keymap's modal sections
288/// (`nor`/`ins`/`sel`) and into `overrides`, keyed by mode name — the schema
289/// `CanvasKeybindingProfile::with_overrides_toml` expects. Non-canvas bindings
290/// (and the keymap-owned `exit`/`enter_decider`) are left in the keymap.
291#[cfg(feature = "canvas")]
292fn fold_keymap_canvas_overrides(keymap: &mut RawKeymap, overrides: &mut Map<String, Value>) {
293    for section in &mut keymap.sections {
294        if !matches!(section.mode.as_str(), "nor" | "ins" | "sel") {
295            continue;
296        }
297        let mut i = 0;
298        while i < section.bindings.len() {
299            if !is_canvas_override_action(&section.bindings[i].name) {
300                i += 1;
301                continue;
302            }
303            let binding = section.bindings.remove(i);
304            let mode_table = overrides
305                .entry(section.mode.clone())
306                .or_insert_with(|| Value::Table(Map::new()));
307            if let Value::Table(table) = mode_table {
308                table.insert(
309                    binding.name,
310                    Value::Array(binding.keys.into_iter().map(Value::String).collect()),
311                );
312            }
313        }
314    }
315}
316
317#[derive(Debug, Clone, PartialEq, Eq)]
318pub enum BindingNotice<A> {
319    UserOverridesDefault {
320        mode: String,
321        sequence: Vec<KeyChord>,
322        default_action: A,
323        user_action: A,
324    },
325    RuntimeOverrides {
326        mode: String,
327        sequence: Vec<KeyChord>,
328        previous_source: BindingSource,
329        previous_action: A,
330        runtime_action: A,
331    },
332    SameLayerConflict(BindingConflict<A>),
333    CrossLayerOverlap(BindingConflict<A>),
334    InvalidEntry(NavigationPresetIssue),
335}
336
337#[derive(Debug, Clone, PartialEq, Eq)]
338pub struct KeybindingReport<A> {
339    pub notices: Vec<BindingNotice<A>>,
340}
341
342impl<A> Default for KeybindingReport<A> {
343    fn default() -> Self {
344        Self {
345            notices: Vec::new(),
346        }
347    }
348}
349
350impl<A> KeybindingReport<A> {
351    pub fn is_empty(&self) -> bool {
352        self.notices.is_empty()
353    }
354}
355
356impl<A: fmt::Debug> fmt::Display for BindingNotice<A> {
357    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358        match self {
359            Self::UserOverridesDefault {
360                mode,
361                sequence,
362                default_action,
363                user_action,
364            } => write!(
365                f,
366                "user binding {sequence:?} in mode {mode:?} overrides default {default_action:?} with {user_action:?}"
367            ),
368            Self::RuntimeOverrides {
369                mode,
370                sequence,
371                previous_source,
372                previous_action,
373                runtime_action,
374            } => write!(
375                f,
376                "runtime binding {sequence:?} in mode {mode:?} overrides {previous_source:?} {previous_action:?} with {runtime_action:?}"
377            ),
378            Self::SameLayerConflict(conflict) => write!(f, "keybinding conflict: {conflict:?}"),
379            Self::CrossLayerOverlap(conflict) => {
380                write!(f, "cross-layer keybinding overlap: {conflict:?}")
381            }
382            Self::InvalidEntry(issue) => write!(f, "invalid keybinding entry skipped: {issue}"),
383        }
384    }
385}
386
387#[derive(Debug, Clone)]
388pub struct KeybindingInheritance<A> {
389    pub target_mode: ModeId,
390    pub target_action: A,
391    pub source_mode: ModeId,
392    pub source_action: A,
393}
394
395impl<A> KeybindingInheritance<A> {
396    pub fn new(
397        target_mode: impl Into<ModeId>,
398        target_action: A,
399        source_mode: impl Into<ModeId>,
400        source_action: A,
401    ) -> Self {
402        Self {
403            target_mode: target_mode.into(),
404            target_action,
405            source_mode: source_mode.into(),
406            source_action,
407        }
408    }
409}
410
411#[derive(Debug, Clone)]
412pub struct BindingStore<A> {
413    pub builtin_keymap: BindingCatalog<A>,
414    pub user_keymap: BindingCatalog<A>,
415    pub runtime_keymap: BindingCatalog<A>,
416    keymap_inheritances: Vec<KeybindingInheritance<A>>,
417    #[cfg(feature = "canvas")]
418    pub builtin_canvas: BindingCatalog<CanvasAction>,
419    #[cfg(feature = "canvas")]
420    pub user_canvas: BindingCatalog<CanvasAction>,
421    #[cfg(feature = "canvas")]
422    pub runtime_canvas: BindingCatalog<CanvasAction>,
423}
424
425impl<A> Default for BindingStore<A> {
426    fn default() -> Self {
427        Self {
428            builtin_keymap: BindingCatalog::new(),
429            user_keymap: BindingCatalog::new(),
430            runtime_keymap: BindingCatalog::new(),
431            keymap_inheritances: Vec::new(),
432            #[cfg(feature = "canvas")]
433            builtin_canvas: BindingCatalog::new(),
434            #[cfg(feature = "canvas")]
435            user_canvas: BindingCatalog::new(),
436            #[cfg(feature = "canvas")]
437            runtime_canvas: BindingCatalog::new(),
438        }
439    }
440}
441
442impl<A> BindingStore<A>
443where
444    A: Clone + PartialEq,
445{
446    pub fn from_registries(
447        builtin_keymap: &InputRegistry<A>,
448        user_keymap: &InputRegistry<A>,
449    ) -> Self {
450        Self {
451            builtin_keymap: BindingCatalog::from_registry(builtin_keymap, BindingSource::Builtin),
452            user_keymap: BindingCatalog::from_registry(user_keymap, BindingSource::Config),
453            ..Self::default()
454        }
455    }
456
457    pub fn effective_registry(&self) -> InputRegistry<A> {
458        let mut registry = InputRegistry::empty();
459        apply_catalog_to_registry(&mut registry, &self.builtin_keymap);
460        remap_catalog_to_registry(&mut registry, &self.user_keymap);
461        remap_catalog_to_registry(&mut registry, &self.runtime_keymap);
462        apply_keymap_inheritances(
463            &mut registry,
464            &self.user_keymap,
465            &self.runtime_keymap,
466            &self.keymap_inheritances,
467        );
468        registry
469    }
470
471    pub fn builtin_registry(&self) -> InputRegistry<A> {
472        let mut registry = InputRegistry::empty();
473        apply_catalog_to_registry(&mut registry, &self.builtin_keymap);
474        registry
475    }
476
477    pub fn report(&self, active_modes: &[impl AsRef<str>]) -> KeybindingReport<A> {
478        let mut notices = Vec::new();
479        notices.extend(user_override_notices(
480            &self.builtin_keymap,
481            &self.user_keymap,
482        ));
483        notices.extend(runtime_override_notices(
484            &self.builtin_keymap,
485            &self.user_keymap,
486            &self.runtime_keymap,
487        ));
488
489        let effective =
490            BindingCatalog::from_registry(&self.effective_registry(), BindingSource::Unknown);
491        notices.extend(
492            analyze_keymap_bindings(&effective, active_modes)
493                .conflicts
494                .into_iter()
495                .map(BindingNotice::SameLayerConflict),
496        );
497
498        #[cfg(feature = "canvas")]
499        {
500            let mut canvas = BindingCatalog::new();
501            canvas.extend(self.builtin_canvas.clone());
502            canvas.extend(self.user_canvas.clone());
503            canvas.extend(self.runtime_canvas.clone());
504            notices.extend(
505                analyze_canvas_overlaps(&effective, &canvas, InputLayerContext::Command)
506                    .into_iter()
507                    .chain(analyze_canvas_overlaps(
508                        &effective,
509                        &canvas,
510                        InputLayerContext::Text,
511                    ))
512                    .map(BindingNotice::CrossLayerOverlap),
513            );
514        }
515
516        KeybindingReport { notices }
517    }
518}
519
520impl<A> BindingStore<A>
521where
522    A: Clone + PartialEq,
523{
524    /// Build the layered store from a parsed config, resolving `[keymap.*]`
525    /// action names through `actions`. Unknown names become `InvalidEntry`
526    /// notices rather than aborting the load.
527    pub fn with_user_config(
528        builtin_keymap: &InputRegistry<A>,
529        config: &KeybindingConfig,
530        actions: &ActionRegistry<A>,
531    ) -> Result<(Self, InputRegistry<A>, KeybindingReport<A>), KeybindingConfigError> {
532        Self::with_user_config_and_inheritances(
533            builtin_keymap,
534            config,
535            actions,
536            Vec::<KeybindingInheritance<A>>::new(),
537        )
538    }
539
540    pub fn with_user_config_and_inheritances(
541        builtin_keymap: &InputRegistry<A>,
542        config: &KeybindingConfig,
543        actions: &ActionRegistry<A>,
544        keymap_inheritances: impl IntoIterator<Item = KeybindingInheritance<A>>,
545    ) -> Result<(Self, InputRegistry<A>, KeybindingReport<A>), KeybindingConfigError> {
546        let mut store = Self::default();
547        store.builtin_keymap =
548            BindingCatalog::from_registry(builtin_keymap, BindingSource::Builtin);
549        store.keymap_inheritances = keymap_inheritances.into_iter().collect();
550        let (user_keymap, unknown_issues) =
551            resolve_raw_keymap(&config.keymap, actions, BindingSource::Config);
552        store.user_keymap = user_keymap;
553
554        #[cfg(feature = "canvas")]
555        {
556            store.builtin_canvas = canvas_default_binding_catalog(config.canvas_preset);
557            let profile = config.canvas_profile()?;
558            store.user_canvas = canvas_profile_overrides_catalog(&profile);
559        }
560
561        let mut report = store.report(&["global", "general", "nor", "ins", "sel"]);
562        report.notices.extend(
563            config
564                .keymap_issues
565                .iter()
566                .cloned()
567                .chain(unknown_issues)
568                .map(BindingNotice::InvalidEntry),
569        );
570        let registry = store.effective_registry();
571        Ok((store, registry, report))
572    }
573}
574
575fn apply_catalog_to_registry<A: Clone>(
576    registry: &mut InputRegistry<A>,
577    catalog: &BindingCatalog<A>,
578) {
579    for binding in &catalog.bindings {
580        registry
581            .map_mut(binding.mode.as_str())
582            .bind(binding.sequence.clone(), binding.action.clone());
583    }
584}
585
586fn remap_catalog_to_registry<A>(registry: &mut InputRegistry<A>, catalog: &BindingCatalog<A>)
587where
588    A: Clone + PartialEq,
589{
590    let mut cleared = Vec::<(String, A)>::new();
591    for binding in &catalog.bindings {
592        if cleared
593            .iter()
594            .any(|(mode, action)| mode == &binding.mode && action == &binding.action)
595        {
596            continue;
597        }
598        registry
599            .map_mut(binding.mode.as_str())
600            .unbind_action(&binding.action);
601        cleared.push((binding.mode.clone(), binding.action.clone()));
602    }
603    apply_catalog_to_registry(registry, catalog);
604}
605
606fn catalog_has_action<A: PartialEq>(
607    catalog: &BindingCatalog<A>,
608    mode: &ModeId,
609    action: &A,
610) -> bool {
611    catalog
612        .bindings
613        .iter()
614        .any(|binding| binding.mode == mode.as_str() && &binding.action == action)
615}
616
617fn registry_bindings_for_action<A: PartialEq + Clone>(
618    registry: &InputRegistry<A>,
619    mode: &ModeId,
620    action: &A,
621) -> Vec<Vec<KeyChord>> {
622    registry
623        .maps
624        .get(mode.as_str())
625        .map(|map| {
626            map.bindings
627                .iter()
628                .filter_map(|(sequence, bound_action)| {
629                    if bound_action == action {
630                        Some(sequence.clone())
631                    } else {
632                        None
633                    }
634                })
635                .collect()
636        })
637        .unwrap_or_default()
638}
639
640fn apply_keymap_inheritances<A: Clone + PartialEq>(
641    registry: &mut InputRegistry<A>,
642    user_keymap: &BindingCatalog<A>,
643    runtime_keymap: &BindingCatalog<A>,
644    inheritances: &[KeybindingInheritance<A>],
645) {
646    for inheritance in inheritances {
647        let source_sequences = registry_bindings_for_action(
648            registry,
649            &inheritance.source_mode,
650            &inheritance.source_action,
651        );
652        if source_sequences.is_empty() {
653            continue;
654        }
655
656        if catalog_has_action(
657            user_keymap,
658            &inheritance.target_mode,
659            &inheritance.target_action,
660        ) || catalog_has_action(
661            runtime_keymap,
662            &inheritance.target_mode,
663            &inheritance.target_action,
664        ) {
665            continue;
666        }
667
668        registry
669            .map_mut(inheritance.target_mode.as_str())
670            .unbind_action(&inheritance.target_action);
671        for sequence in source_sequences {
672            registry
673                .map_mut(inheritance.target_mode.as_str())
674                .bind(sequence, inheritance.target_action.clone());
675        }
676    }
677}
678
679fn resolve_raw_keymap<A>(
680    keymap: &RawKeymap,
681    actions: &ActionRegistry<A>,
682    source: BindingSource,
683) -> (BindingCatalog<A>, Vec<NavigationPresetIssue>)
684where
685    A: Clone,
686{
687    let mut catalog = BindingCatalog::new();
688    let mut issues = Vec::new();
689    for section in &keymap.sections {
690        for binding in &section.bindings {
691            let Some(action) = actions.resolve(&binding.name) else {
692                issues.push(NavigationPresetIssue::UnknownAction {
693                    section: section.name.clone(),
694                    action: binding.name.clone(),
695                });
696                continue;
697            };
698            for key in &binding.keys {
699                let Ok(sequence) = try_parse_binding(key) else {
700                    continue;
701                };
702                catalog.push(BindingInfo {
703                    layer: BindingLayer::Keymap,
704                    mode: section.mode.clone(),
705                    sequence,
706                    action: action.clone(),
707                    source,
708                });
709            }
710        }
711    }
712    (catalog, issues)
713}
714
715fn user_override_notices<A>(
716    builtin: &BindingCatalog<A>,
717    user: &BindingCatalog<A>,
718) -> Vec<BindingNotice<A>>
719where
720    A: Clone + PartialEq,
721{
722    let mut notices = Vec::new();
723    for user_binding in &user.bindings {
724        let mut emitted = false;
725        for default_binding in
726            builtin.bindings_for_sequence(&user_binding.mode, &user_binding.sequence)
727        {
728            if default_binding.action != user_binding.action {
729                notices.push(BindingNotice::UserOverridesDefault {
730                    mode: user_binding.mode.clone(),
731                    sequence: user_binding.sequence.clone(),
732                    default_action: default_binding.action.clone(),
733                    user_action: user_binding.action.clone(),
734                });
735                emitted = true;
736            }
737        }
738        if emitted {
739            continue;
740        }
741
742        for default_binding in builtin.bindings_for_action(&user_binding.action) {
743            if default_binding.mode == user_binding.mode
744                && default_binding.sequence != user_binding.sequence
745            {
746                notices.push(BindingNotice::UserOverridesDefault {
747                    mode: user_binding.mode.clone(),
748                    sequence: user_binding.sequence.clone(),
749                    default_action: default_binding.action.clone(),
750                    user_action: user_binding.action.clone(),
751                });
752            }
753        }
754    }
755    notices
756}
757
758fn runtime_override_notices<A>(
759    builtin: &BindingCatalog<A>,
760    user: &BindingCatalog<A>,
761    runtime: &BindingCatalog<A>,
762) -> Vec<BindingNotice<A>>
763where
764    A: Clone + PartialEq,
765{
766    let mut notices = Vec::new();
767    for runtime_binding in &runtime.bindings {
768        for previous in builtin
769            .bindings_for_sequence(&runtime_binding.mode, &runtime_binding.sequence)
770            .into_iter()
771            .chain(user.bindings_for_sequence(&runtime_binding.mode, &runtime_binding.sequence))
772        {
773            if previous.action != runtime_binding.action {
774                notices.push(BindingNotice::RuntimeOverrides {
775                    mode: runtime_binding.mode.clone(),
776                    sequence: runtime_binding.sequence.clone(),
777                    previous_source: previous.source,
778                    previous_action: previous.action.clone(),
779                    runtime_action: runtime_binding.action.clone(),
780                });
781            }
782        }
783    }
784    notices
785}
786
787#[cfg(feature = "canvas")]
788fn canvas_profile_overrides_catalog(
789    profile: &CanvasKeybindingProfile,
790) -> BindingCatalog<CanvasAction> {
791    let mut catalog = BindingCatalog::new();
792    let default_entries = profile.defaults().entries();
793    for entry in profile.current().entries() {
794        let Some(action) = entry.action.to_canvas_action() else {
795            continue;
796        };
797        let default_same = default_entries.iter().any(|default| {
798            default.mode == entry.mode
799                && default.action == entry.action
800                && default.sequence == entry.sequence
801        });
802        if default_same {
803            continue;
804        }
805        catalog.push(BindingInfo {
806            layer: BindingLayer::Canvas,
807            mode: crate::canvas::mode_for_app_mode(entry.mode)
808                .as_str()
809                .to_string(),
810            sequence: entry
811                .sequence
812                .iter()
813                .map(|stroke| KeyChord::new(stroke.code, stroke.modifiers))
814                .collect(),
815            action,
816            source: BindingSource::Config,
817        });
818    }
819    catalog
820}
821
822/// Serialize the live override layers back to the unified TOML schema, the exact
823/// inverse of [`KeybindingConfig::from_toml`]. Only the user + runtime keymap
824/// overrides and the canvas preset-diff are emitted (builtin defaults come from
825/// the preset/registry on reload), so the document round-trips through
826/// `from_toml`. `actions` provides the action → name mapping.
827pub fn export_to_toml<A>(
828    store: &BindingStore<A>,
829    actions: &ActionRegistry<A>,
830    #[cfg(feature = "canvas")] profile: &CanvasKeybindingProfile,
831) -> Result<String, KeybindingConfigError>
832where
833    A: Clone + PartialEq,
834{
835    use std::collections::{BTreeMap, BTreeSet};
836
837    // mode -> action_name -> key strings.
838    let mut keymap: BTreeMap<String, BTreeMap<&'static str, Vec<String>>> = BTreeMap::new();
839    // (mode, name) pairs a runtime rebind owns, so the stale user binding for the
840    // same action is dropped rather than emitted alongside the new one.
841    let mut runtime_owned: BTreeSet<(String, &'static str)> = BTreeSet::new();
842
843    let mut record = |binding: &BindingInfo<A>| {
844        let Some(name) = actions.name_of(&binding.action) else {
845            return;
846        };
847        let sequence = binding
848            .sequence
849            .iter()
850            .map(KeyChord::display_string)
851            .collect::<Vec<_>>()
852            .join(" ");
853        keymap
854            .entry(binding.mode.clone())
855            .or_default()
856            .entry(name)
857            .or_default()
858            .push(sequence);
859    };
860
861    for binding in &store.runtime_keymap.bindings {
862        if actions.name_of(&binding.action).is_some() {
863            runtime_owned.insert((
864                binding.mode.clone(),
865                actions.name_of(&binding.action).unwrap(),
866            ));
867        }
868        record(binding);
869    }
870    for binding in &store.user_keymap.bindings {
871        if let Some(name) = actions.name_of(&binding.action) {
872            if runtime_owned.contains(&(binding.mode.clone(), name)) {
873                continue;
874            }
875        }
876        record(binding);
877    }
878
879    let mut root = Map::new();
880    for (mode, actions_map) in keymap {
881        let section = root.entry(mode).or_insert_with(|| Value::Table(Map::new()));
882        if let Value::Table(section) = section {
883            for (name, keys) in actions_map {
884                section.insert(
885                    name.to_string(),
886                    Value::Array(keys.into_iter().map(Value::String).collect()),
887                );
888            }
889        }
890    }
891
892    #[cfg(feature = "canvas")]
893    {
894        let mut canvas_table = Map::new();
895        if profile.preset() != BuiltinCanvasKeybindingPreset::Vim {
896            canvas_table.insert(
897                "preset".to_string(),
898                Value::String(profile.preset().to_string()),
899            );
900        }
901        let overrides = profile.overrides_toml();
902        if !overrides.trim().is_empty() {
903            let parsed =
904                toml::from_str::<Value>(&overrides).map_err(KeybindingConfigError::Toml)?;
905            if let Some(table) = parsed.as_table() {
906                for (mode, actions) in table {
907                    let section = root
908                        .entry(mode.clone())
909                        .or_insert_with(|| Value::Table(Map::new()));
910                    if let (Value::Table(section), Value::Table(actions)) = (section, actions) {
911                        for (name, keys) in actions {
912                            section.insert(name.clone(), keys.clone());
913                        }
914                    }
915                }
916            }
917        }
918        if !canvas_table.is_empty() {
919            root.insert("canvas".to_string(), Value::Table(canvas_table));
920        }
921    }
922
923    toml::to_string(&Value::Table(root)).map_err(KeybindingConfigError::Serialize)
924}
925
926#[cfg(test)]
927mod tests {
928    use super::*;
929    use crate::keybindings::NavigationAction;
930
931    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
932    enum TestAction {
933        Nav(NavigationAction),
934    }
935
936    impl From<NavigationAction> for TestAction {
937        fn from(value: NavigationAction) -> Self {
938            Self::Nav(value)
939        }
940    }
941
942    fn seq(binding: &str) -> Vec<KeyChord> {
943        try_parse_binding(binding).unwrap()
944    }
945
946    #[test]
947    fn keymap_resolves_app_specific_action_names_via_registry() {
948        // A name the crate has never heard of resolves through the app-supplied
949        // registry — the whole point of the registry indirection.
950        #[derive(Debug, Clone, PartialEq, Eq)]
951        enum App {
952            DoTheThing,
953            Nav(NavigationAction),
954        }
955        impl From<NavigationAction> for App {
956            fn from(value: NavigationAction) -> Self {
957                App::Nav(value)
958            }
959        }
960
961        let registry = ActionRegistry::from_entries(vec![crate::input::BindableActionInfo {
962            action: App::DoTheThing,
963            name: "do_the_thing",
964            description: "",
965            modes: &["global"],
966        }]);
967        let config = KeybindingConfig::from_toml(
968            r#"
969[keymap.global]
970do_the_thing = "ctrl+t"
971"#,
972        )
973        .unwrap();
974        let builtin = InputRegistry::<App>::empty();
975        let (_store, registry_out, report) =
976            BindingStore::with_user_config(&builtin, &config, &registry).unwrap();
977
978        assert_eq!(
979            registry_out
980                .maps
981                .get("global")
982                .unwrap()
983                .bindings
984                .get(&seq("ctrl+t")),
985            Some(&App::DoTheThing)
986        );
987        assert!(
988            report
989                .notices
990                .iter()
991                .all(|notice| !matches!(notice, BindingNotice::InvalidEntry(_)))
992        );
993    }
994
995    #[test]
996    fn parses_keymap_section_from_unified_toml() {
997        let config = KeybindingConfig::from_toml(
998            r#"
999[keymap.global]
1000quit = "ctrl+q"
1001
1002[keymap.general]
1003focus_next = ["j", "down"]
1004"#,
1005        )
1006        .unwrap();
1007
1008        let global = config
1009            .keymap
1010            .sections
1011            .iter()
1012            .find(|section| section.name == "global")
1013            .unwrap();
1014        assert_eq!(global.bindings.first().unwrap().name, "quit");
1015    }
1016
1017    #[test]
1018    fn parses_root_mode_sections_from_unified_toml() {
1019        let config = KeybindingConfig::from_toml(
1020            r#"
1021[global]
1022quit = "ctrl+q"
1023
1024[general]
1025focus_next = ["j", "down"]
1026"#,
1027        )
1028        .unwrap();
1029
1030        let global = config
1031            .keymap
1032            .sections
1033            .iter()
1034            .find(|section| section.name == "global")
1035            .unwrap();
1036        assert_eq!(global.bindings.first().unwrap().name, "quit");
1037    }
1038
1039    #[test]
1040    fn reports_user_override_default_and_effective_registry_uses_user_binding() {
1041        let mut builtin = InputRegistry::empty();
1042        builtin
1043            .map_mut("global")
1044            .bind(seq("ctrl+c"), TestAction::Nav(NavigationAction::Quit));
1045
1046        let config = KeybindingConfig::from_toml(
1047            r#"
1048[keymap.global]
1049quit = "ctrl+q"
1050"#,
1051        )
1052        .unwrap();
1053
1054        let (store, registry, report) = BindingStore::<TestAction>::with_user_config(
1055            &builtin,
1056            &config,
1057            &ActionRegistry::navigation(),
1058        )
1059        .unwrap();
1060
1061        assert_eq!(
1062            registry
1063                .maps
1064                .get("global")
1065                .unwrap()
1066                .bindings
1067                .get(&seq("ctrl+q")),
1068            Some(&TestAction::Nav(NavigationAction::Quit))
1069        );
1070        assert!(
1071            !registry
1072                .maps
1073                .get("global")
1074                .unwrap()
1075                .bindings
1076                .contains_key(&seq("ctrl+c"))
1077        );
1078        assert!(report.notices.iter().any(|notice| matches!(
1079            notice,
1080            BindingNotice::UserOverridesDefault {
1081                mode,
1082                sequence,
1083                default_action: TestAction::Nav(NavigationAction::Quit),
1084                user_action: TestAction::Nav(NavigationAction::Quit),
1085            } if mode == "global" && *sequence == seq("ctrl+q")
1086        )));
1087        assert_eq!(store.effective_registry().total_bindings(), 1);
1088    }
1089
1090    #[test]
1091    fn inherited_keybinding_follows_source_user_override() {
1092        #[derive(Debug, Clone, PartialEq, Eq)]
1093        enum App {
1094            Select,
1095            Execute,
1096        }
1097
1098        let actions = ActionRegistry::from_entries(vec![
1099            crate::input::BindableActionInfo {
1100                action: App::Select,
1101                name: "nav_select",
1102                description: "",
1103                modes: &["general"],
1104            },
1105            crate::input::BindableActionInfo {
1106                action: App::Execute,
1107                name: "command_execute",
1108                description: "",
1109                modes: &["command"],
1110            },
1111        ]);
1112        let mut builtin = InputRegistry::empty();
1113        builtin.map_mut("general").bind(seq("enter"), App::Select);
1114        builtin.map_mut("command").bind(seq("ctrl+x"), App::Execute);
1115        let config = KeybindingConfig::from_toml(
1116            r#"
1117[keymap.general]
1118nav_select = "ctrl+j"
1119"#,
1120        )
1121        .unwrap();
1122
1123        let (_store, registry, _report) = BindingStore::with_user_config_and_inheritances(
1124            &builtin,
1125            &config,
1126            &actions,
1127            [KeybindingInheritance::new(
1128                crate::runtime::modes::COMMAND,
1129                App::Execute,
1130                crate::runtime::modes::GENERAL,
1131                App::Select,
1132            )],
1133        )
1134        .unwrap();
1135
1136        let command = registry.maps.get("command").unwrap();
1137        assert_eq!(command.bindings.get(&seq("ctrl+j")), Some(&App::Execute));
1138        assert!(!command.bindings.contains_key(&seq("ctrl+x")));
1139    }
1140
1141    #[test]
1142    fn inherited_keybinding_does_not_replace_explicit_target() {
1143        #[derive(Debug, Clone, PartialEq, Eq)]
1144        enum App {
1145            Select,
1146            Execute,
1147        }
1148
1149        let actions = ActionRegistry::from_entries(vec![
1150            crate::input::BindableActionInfo {
1151                action: App::Select,
1152                name: "nav_select",
1153                description: "",
1154                modes: &["general"],
1155            },
1156            crate::input::BindableActionInfo {
1157                action: App::Execute,
1158                name: "command_execute",
1159                description: "",
1160                modes: &["command"],
1161            },
1162        ]);
1163        let mut builtin = InputRegistry::empty();
1164        builtin.map_mut("general").bind(seq("enter"), App::Select);
1165        builtin.map_mut("command").bind(seq("ctrl+x"), App::Execute);
1166        let config = KeybindingConfig::from_toml(
1167            r#"
1168[keymap.general]
1169nav_select = "ctrl+j"
1170
1171[keymap.command]
1172command_execute = "-"
1173"#,
1174        )
1175        .unwrap();
1176
1177        let (_store, registry, _report) = BindingStore::with_user_config_and_inheritances(
1178            &builtin,
1179            &config,
1180            &actions,
1181            [KeybindingInheritance::new(
1182                crate::runtime::modes::COMMAND,
1183                App::Execute,
1184                crate::runtime::modes::GENERAL,
1185                App::Select,
1186            )],
1187        )
1188        .unwrap();
1189
1190        let command = registry.maps.get("command").unwrap();
1191        assert_eq!(command.bindings.get(&seq("-")), Some(&App::Execute));
1192        assert!(!command.bindings.contains_key(&seq("ctrl+j")));
1193        assert!(!command.bindings.contains_key(&seq("ctrl+x")));
1194    }
1195
1196    #[test]
1197    fn invalid_navigation_entries_are_not_all_or_nothing() {
1198        let mut builtin = InputRegistry::empty();
1199        builtin
1200            .map_mut("global")
1201            .bind(seq("ctrl+c"), TestAction::Nav(NavigationAction::Quit));
1202
1203        let config = KeybindingConfig::from_toml(
1204            r#"
1205[keymap.global]
1206quit = "ctrl+q"
1207not_real = "x"
1208
1209[keymap.general]
1210focus_next = "not+a+key"
1211"#,
1212        )
1213        .unwrap();
1214
1215        let (_store, registry, report) = BindingStore::<TestAction>::with_user_config(
1216            &builtin,
1217            &config,
1218            &ActionRegistry::navigation(),
1219        )
1220        .unwrap();
1221
1222        assert_eq!(
1223            registry
1224                .maps
1225                .get("global")
1226                .unwrap()
1227                .bindings
1228                .get(&seq("ctrl+q")),
1229            Some(&TestAction::Nav(NavigationAction::Quit))
1230        );
1231        assert!(
1232            report
1233                .notices
1234                .iter()
1235                .any(|notice| matches!(notice, BindingNotice::InvalidEntry(_)))
1236        );
1237    }
1238
1239    /// Canvas-action names written in a modal keymap section are routed to the
1240    /// canvas profile, not the navigation keymap — so a consumer can remap a
1241    /// canvas key by editing `[keymap.nor]`. Non-canvas names in the same section
1242    /// stay in the keymap, and explicit `[canvas.bindings]` still works.
1243    #[cfg(feature = "canvas")]
1244    #[test]
1245    fn modal_section_canvas_actions_route_to_canvas_profile() {
1246        let config = KeybindingConfig::from_toml(
1247            r#"
1248[keymap.nor]
1249undo = ["ctrl+z"]
1250previous_entry = ["q"]
1251
1252[canvas]
1253preset = "helix"
1254
1255[canvas.bindings.nor]
1256redo = ["ctrl+y"]
1257"#,
1258        )
1259        .unwrap();
1260
1261        // `undo` (canvas) was pulled out of the keymap; `previous_entry` (not a
1262        // canvas action) stays.
1263        let nor = config
1264            .keymap
1265            .sections
1266            .iter()
1267            .find(|section| section.mode == "nor")
1268            .expect("nor keymap section survives");
1269        assert!(nor.bindings.iter().any(|b| b.name == "previous_entry"));
1270        assert!(nor.bindings.iter().all(|b| b.name != "undo"));
1271
1272        // The canvas profile reflects both the keymap-sourced `undo` remap and
1273        // the explicit `[canvas.bindings]` `redo` override.
1274        let overrides = config.canvas_profile().unwrap().overrides_toml();
1275        assert!(overrides.contains("undo"), "overrides: {overrides}");
1276        assert!(overrides.contains("redo"), "overrides: {overrides}");
1277    }
1278
1279    #[cfg(feature = "canvas")]
1280    #[test]
1281    fn root_modal_sections_split_canvas_and_app_actions() {
1282        #[derive(Debug, Clone, PartialEq, Eq)]
1283        enum App {
1284            MyAppAction,
1285        }
1286
1287        let actions = ActionRegistry::from_entries(vec![crate::input::BindableActionInfo {
1288            action: App::MyAppAction,
1289            name: "my_app_action",
1290            description: "",
1291            modes: &["nor"],
1292        }]);
1293        let config = KeybindingConfig::from_toml(
1294            r#"
1295[nor]
1296undo = ["ctrl+z"]
1297my_app_action = ["x"]
1298"#,
1299        )
1300        .unwrap();
1301        let builtin = InputRegistry::<App>::empty();
1302        let (_store, registry, report) =
1303            BindingStore::with_user_config(&builtin, &config, &actions).unwrap();
1304
1305        assert_eq!(
1306            registry.maps.get("nor").unwrap().bindings.get(&seq("x")),
1307            Some(&App::MyAppAction)
1308        );
1309        assert!(
1310            report
1311                .notices
1312                .iter()
1313                .all(|notice| !matches!(notice, BindingNotice::InvalidEntry(_)))
1314        );
1315
1316        let overrides = config.canvas_profile().unwrap().overrides_toml();
1317        assert!(
1318            overrides.contains("undo = [\"Ctrl+z\"]"),
1319            "overrides: {overrides}"
1320        );
1321    }
1322}