Skip to main content

fret_runtime/keymap/
display.rs

1use crate::{CommandId, InputContext, InputDispatchPhase, KeyChord};
2use std::collections::{HashMap, HashSet};
3use std::sync::Arc;
4
5use super::Keymap;
6
7impl Keymap {
8    /// Best-effort reverse lookup for UI display (command palette / menus).
9    ///
10    /// This applies the same platform + `when` matching rules as `resolve`, then finds any chord
11    /// whose effective command equals `command` under the provided context.
12    pub fn shortcut_for_command(
13        &self,
14        ctx: &InputContext,
15        command: &CommandId,
16    ) -> Option<KeyChord> {
17        self.shortcut_for_command_sequence(ctx, command)
18            .filter(|seq| seq.len() == 1)
19            .and_then(|seq| seq.first().copied())
20    }
21
22    pub fn shortcut_for_command_sequence(
23        &self,
24        ctx: &InputContext,
25        command: &CommandId,
26    ) -> Option<Vec<KeyChord>> {
27        self.shortcut_for_command_sequence_with_key_contexts(ctx, &[], command)
28    }
29
30    pub fn shortcut_for_command_sequence_with_key_contexts(
31        &self,
32        ctx: &InputContext,
33        key_contexts: &[Arc<str>],
34        command: &CommandId,
35    ) -> Option<Vec<KeyChord>> {
36        let mut order: Vec<Vec<KeyChord>> = Vec::new();
37        let mut seen: HashSet<Vec<KeyChord>> = HashSet::new();
38        let mut effective: HashMap<Vec<KeyChord>, Option<CommandId>> = HashMap::new();
39
40        for b in &self.bindings {
41            if !b.platform.matches(ctx.platform) {
42                continue;
43            }
44            if let Some(expr) = b.when.as_ref()
45                && !expr.eval_with_key_contexts(ctx, key_contexts)
46            {
47                continue;
48            }
49            if seen.insert(b.sequence.clone()) {
50                order.push(b.sequence.clone());
51            }
52            effective.insert(b.sequence.clone(), b.command.clone());
53        }
54
55        order.into_iter().find(|seq| {
56            effective
57                .get(seq)
58                .is_some_and(|c| c.as_ref() == Some(command))
59        })
60    }
61
62    /// Best-effort reverse lookup for UI display that is intentionally *stable* across focus
63    /// changes.
64    ///
65    /// This is intended for menu bar / command palette shortcut labels, where displaying different
66    /// shortcuts as focus moves is confusing. Instead of using the live focus state, we evaluate
67    /// bindings against a small set of "default" contexts derived from `base`:
68    ///
69    /// - non-modal + not text input
70    /// - non-modal + text input
71    /// - modal + not text input
72    /// - modal + text input
73    ///
74    /// Candidate sequences are ranked by:
75    ///
76    /// 1. first matching default context (earlier is preferred),
77    /// 2. shorter sequences (single-chord preferred),
78    /// 3. later-defined bindings (user/project overrides preferred).
79    pub fn display_shortcut_for_command(
80        &self,
81        base: &InputContext,
82        command: &CommandId,
83    ) -> Option<KeyChord> {
84        self.display_shortcut_for_command_sequence(base, command)
85            .filter(|seq| seq.len() == 1)
86            .and_then(|seq| seq.first().copied())
87    }
88
89    pub fn display_shortcut_for_command_sequence(
90        &self,
91        base: &InputContext,
92        command: &CommandId,
93    ) -> Option<Vec<KeyChord>> {
94        self.display_shortcut_for_command_sequence_with_key_contexts(base, &[], command)
95    }
96
97    pub fn display_shortcut_for_command_sequence_with_key_contexts(
98        &self,
99        base: &InputContext,
100        key_contexts: &[Arc<str>],
101        command: &CommandId,
102    ) -> Option<Vec<KeyChord>> {
103        #[derive(Debug)]
104        struct Candidate {
105            ctx_index: usize,
106            seq_len: usize,
107            binding_index: usize,
108            seq: Vec<KeyChord>,
109        }
110
111        fn default_display_contexts(base: &InputContext) -> [InputContext; 4] {
112            let mut c0 = base.clone();
113            c0.dispatch_phase = InputDispatchPhase::Bubble;
114            c0.ui_has_modal = false;
115            c0.focus_is_text_input = false;
116
117            let mut c1 = c0.clone();
118            c1.focus_is_text_input = true;
119
120            let mut c2 = c0.clone();
121            c2.ui_has_modal = true;
122            c2.focus_is_text_input = false;
123
124            let mut c3 = c2.clone();
125            c3.focus_is_text_input = true;
126
127            [c0, c1, c2, c3]
128        }
129
130        fn effective_command_for_sequence<'a>(
131            keymap: &'a Keymap,
132            ctx: &InputContext,
133            key_contexts: &[Arc<str>],
134            seq: &[KeyChord],
135        ) -> Option<(Option<&'a CommandId>, usize)> {
136            for (index, b) in keymap.bindings.iter().enumerate().rev() {
137                if b.sequence.as_slice() != seq {
138                    continue;
139                }
140                if !b.platform.matches(ctx.platform) {
141                    continue;
142                }
143                if let Some(expr) = b.when.as_ref()
144                    && !expr.eval_with_key_contexts(ctx, key_contexts)
145                {
146                    continue;
147                }
148                return Some((b.command.as_ref(), index));
149            }
150            None
151        }
152
153        let contexts = default_display_contexts(base);
154
155        let mut sequences: HashSet<Vec<KeyChord>> = HashSet::new();
156        for b in &self.bindings {
157            sequences.insert(b.sequence.clone());
158        }
159
160        let mut best: Option<Candidate> = None;
161        for seq in sequences.into_iter() {
162            for (ctx_index, ctx) in contexts.iter().enumerate() {
163                let Some((Some(cmd), binding_index)) =
164                    effective_command_for_sequence(self, ctx, key_contexts, &seq)
165                else {
166                    continue;
167                };
168                if cmd != command {
169                    continue;
170                }
171
172                let cand = Candidate {
173                    ctx_index,
174                    seq_len: seq.len(),
175                    binding_index,
176                    seq,
177                };
178
179                best = match best {
180                    None => Some(cand),
181                    Some(prev) => {
182                        let replace = (
183                            cand.ctx_index,
184                            cand.seq_len,
185                            std::cmp::Reverse(cand.binding_index),
186                        ) < (
187                            prev.ctx_index,
188                            prev.seq_len,
189                            std::cmp::Reverse(prev.binding_index),
190                        );
191                        if replace { Some(cand) } else { Some(prev) }
192                    }
193                };
194
195                break;
196            }
197        }
198
199        best.map(|c| c.seq)
200    }
201}