Skip to main content

slt/
keymap.rs

1use crate::{KeyCode, KeyModifiers, ModifierKey};
2
3/// A single key binding with display text and description.
4#[derive(Debug, Clone)]
5pub struct Binding {
6    /// The key code for matching.
7    pub key: KeyCode,
8    /// Optional modifier (Ctrl, Alt, Shift).
9    pub modifiers: Option<KeyModifiers>,
10    /// Display text shown in help bar (e.g., "q", "Ctrl+S", "↑").
11    pub display: String,
12    /// Description of what this binding does.
13    pub description: String,
14    /// Whether to show in help bar.
15    pub visible: bool,
16}
17
18/// Declarative key binding map.
19///
20/// # Examples
21/// ```
22/// use slt::KeyMap;
23///
24/// let km = KeyMap::new()
25///     .bind('q', "Quit")
26///     .bind_code(slt::KeyCode::Up, "Move up")
27///     .bind_mod('s', slt::KeyModifiers::CONTROL, "Save")
28///     .bind_hidden('?', "Toggle help");
29/// ```
30#[derive(Debug, Clone, Default)]
31pub struct KeyMap {
32    /// Registered key bindings.
33    pub bindings: Vec<Binding>,
34}
35
36impl KeyMap {
37    /// Create an empty key map.
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Bind a character key.
43    pub fn bind(mut self, key: char, description: &str) -> Self {
44        self.bindings.push(Binding {
45            key: KeyCode::Char(key),
46            modifiers: None,
47            display: key.to_string(),
48            description: description.to_string(),
49            visible: true,
50        });
51        self
52    }
53
54    /// Bind a special key (Enter, Esc, Up, Down, etc.).
55    pub fn bind_code(mut self, key: KeyCode, description: &str) -> Self {
56        self.bindings.push(Binding {
57            display: display_for_key_code(&key),
58            key,
59            modifiers: None,
60            description: description.to_string(),
61            visible: true,
62        });
63        self
64    }
65
66    /// Bind a key with modifier (Ctrl+S, etc.).
67    pub fn bind_mod(mut self, key: char, mods: KeyModifiers, description: &str) -> Self {
68        self.bindings.push(Binding {
69            key: KeyCode::Char(key),
70            modifiers: Some(mods),
71            display: display_for_mod_char(mods, key),
72            description: description.to_string(),
73            visible: true,
74        });
75        self
76    }
77
78    /// Bind a special key with modifier keys (Ctrl+Enter, Alt+Up, Shift+F5, etc.).
79    ///
80    /// Unlike [`KeyMap::bind_mod`], which is restricted to character keys, this
81    /// accepts any [`KeyCode`] together with optional modifier keys.
82    ///
83    /// # Example
84    /// ```
85    /// use slt::{KeyMap, KeyCode, KeyModifiers};
86    ///
87    /// let km = KeyMap::new()
88    ///     .bind_code_mod(KeyCode::Enter, KeyModifiers::CONTROL, "Submit")
89    ///     .bind_code_mod(KeyCode::Up, KeyModifiers::ALT, "Jump to top");
90    /// ```
91    pub fn bind_code_mod(mut self, key: KeyCode, mods: KeyModifiers, description: &str) -> Self {
92        let display = display_for_code_mod(&key, mods);
93        self.bindings.push(Binding {
94            key,
95            modifiers: Some(mods),
96            display,
97            description: description.to_string(),
98            visible: true,
99        });
100        self
101    }
102
103    /// Bind but hide from help bar display.
104    pub fn bind_hidden(mut self, key: char, description: &str) -> Self {
105        self.bindings.push(Binding {
106            key: KeyCode::Char(key),
107            modifiers: None,
108            display: key.to_string(),
109            description: description.to_string(),
110            visible: false,
111        });
112        self
113    }
114
115    /// Get visible bindings for help bar rendering.
116    pub fn visible_bindings(&self) -> impl Iterator<Item = &Binding> {
117        self.bindings.iter().filter(|binding| binding.visible)
118    }
119}
120
121fn display_for_key_code(key: &KeyCode) -> String {
122    match key {
123        KeyCode::Char(c) => c.to_string(),
124        KeyCode::Enter => "Enter".to_string(),
125        KeyCode::Backspace => "Backspace".to_string(),
126        KeyCode::Tab => "Tab".to_string(),
127        KeyCode::BackTab => "Shift+Tab".to_string(),
128        KeyCode::Esc => "Esc".to_string(),
129        KeyCode::Up => "↑".to_string(),
130        KeyCode::Down => "↓".to_string(),
131        KeyCode::Left => "←".to_string(),
132        KeyCode::Right => "→".to_string(),
133        KeyCode::Home => "Home".to_string(),
134        KeyCode::End => "End".to_string(),
135        KeyCode::PageUp => "PgUp".to_string(),
136        KeyCode::PageDown => "PgDn".to_string(),
137        KeyCode::Delete => "Del".to_string(),
138        KeyCode::Insert => "Ins".to_string(),
139        KeyCode::Null => "Null".to_string(),
140        KeyCode::CapsLock => "CapsLock".to_string(),
141        KeyCode::ScrollLock => "ScrollLock".to_string(),
142        KeyCode::NumLock => "NumLock".to_string(),
143        KeyCode::PrintScreen => "PrtSc".to_string(),
144        KeyCode::Pause => "Pause".to_string(),
145        KeyCode::Menu => "Menu".to_string(),
146        KeyCode::KeypadBegin => "KP5".to_string(),
147        KeyCode::F(n) => format!("F{n}"),
148        KeyCode::Modifier(m) => display_for_modifier_key(*m).to_string(),
149    }
150}
151
152fn display_for_modifier_key(m: ModifierKey) -> &'static str {
153    match m {
154        ModifierKey::LeftShift => "LShift",
155        ModifierKey::LeftCtrl => "LCtrl",
156        ModifierKey::LeftAlt => "LAlt",
157        ModifierKey::LeftSuper => "LSuper",
158        ModifierKey::RightShift => "RShift",
159        ModifierKey::RightCtrl => "RCtrl",
160        ModifierKey::RightAlt => "RAlt",
161        ModifierKey::RightSuper => "RSuper",
162        ModifierKey::LeftHyper => "LHyper",
163        ModifierKey::LeftMeta => "LMeta",
164        ModifierKey::RightHyper => "RHyper",
165        ModifierKey::RightMeta => "RMeta",
166        ModifierKey::IsoLevel3Shift => "ISO3",
167        ModifierKey::IsoLevel5Shift => "ISO5",
168    }
169}
170
171fn display_for_code_mod(key: &KeyCode, mods: KeyModifiers) -> String {
172    let mut parts: Vec<&str> = Vec::new();
173    if mods.contains(KeyModifiers::CONTROL) {
174        parts.push("Ctrl");
175    }
176    if mods.contains(KeyModifiers::ALT) {
177        parts.push("Alt");
178    }
179    if mods.contains(KeyModifiers::SHIFT) {
180        parts.push("Shift");
181    }
182    if mods.contains(KeyModifiers::SUPER) {
183        parts.push("Super");
184    }
185    if mods.contains(KeyModifiers::HYPER) {
186        parts.push("Hyper");
187    }
188    if mods.contains(KeyModifiers::META) {
189        parts.push("Meta");
190    }
191
192    let key_label = display_for_key_code(key);
193    if parts.is_empty() {
194        key_label
195    } else {
196        format!("{}+{}", parts.join("+"), key_label)
197    }
198}
199
200fn display_for_mod_char(mods: KeyModifiers, key: char) -> String {
201    let mut parts: Vec<&str> = Vec::new();
202    if mods.contains(KeyModifiers::CONTROL) {
203        parts.push("Ctrl");
204    }
205    if mods.contains(KeyModifiers::ALT) {
206        parts.push("Alt");
207    }
208    if mods.contains(KeyModifiers::SHIFT) {
209        parts.push("Shift");
210    }
211    if mods.contains(KeyModifiers::SUPER) {
212        parts.push("Super");
213    }
214    if mods.contains(KeyModifiers::HYPER) {
215        parts.push("Hyper");
216    }
217    if mods.contains(KeyModifiers::META) {
218        parts.push("Meta");
219    }
220
221    if parts.is_empty() {
222        key.to_string()
223    } else {
224        format!("{}+{}", parts.join("+"), key.to_ascii_uppercase())
225    }
226}
227
228/// Opt-in trait for users to publish a widget's keymap to the framework so
229/// `Context::keymap_help_overlay` can list it on `?` press (issue #236).
230///
231/// # Scope
232///
233/// SLT does **not** implement this on built-in widgets — it is a user-facing
234/// extension point. Built-in widgets register their bindings directly inside
235/// their `impl Context::*` methods. To publish the keymap of your *own*
236/// custom widget, implement this trait, then call
237/// [`Context::publish_keymap`](crate::Context::publish_keymap) in your render
238/// method:
239///
240/// ```ignore
241/// ctx.publish_keymap("my_widget", MyState::key_help(&state));
242/// ```
243///
244/// In other words, this trait is the same shape as `std::fmt::Display`: zero
245/// blanket / built-in impls; you opt in by implementing it on your own type.
246/// If you prefer a free-function call, [`Context::publish_keymap`] takes the
247/// same `(name, &'static [...])` signature without the trait.
248///
249/// # Format
250///
251/// The returned slice must be `'static` — hardcode a `const` array per
252/// widget so the registration is allocation-free. Each tuple is
253/// `(key_combo, description)` using the same display style as
254/// [`Binding::display`] (e.g. `"↑/k"`, `"Ctrl+S"`, `"PgDn"`).
255///
256/// # Example
257///
258/// ```
259/// use slt::keymap::WidgetKeyHelp;
260///
261/// struct Counter;
262///
263/// impl WidgetKeyHelp for Counter {
264///     fn key_help(&self) -> &'static [(&'static str, &'static str)] {
265///         const HELP: &[(&str, &str)] = &[
266///             ("↑/k", "increment"),
267///             ("↓/j", "decrement"),
268///             ("r", "reset"),
269///         ];
270///         HELP
271///     }
272/// }
273///
274/// let counter = Counter;
275/// assert_eq!(counter.key_help().len(), 3);
276/// ```
277pub trait WidgetKeyHelp {
278    /// Return the keyboard shortcuts for this widget.
279    ///
280    /// Format: `(key_combo, description)`.
281    fn key_help(&self) -> &'static [(&'static str, &'static str)];
282}
283
284/// A single published keymap entry collected via
285/// [`Context::publish_keymap`](crate::Context::publish_keymap) (issue #236).
286///
287/// Once registered for the current frame, every entry is queryable through
288/// [`Context::published_keymaps`](crate::Context::published_keymaps) and is
289/// rendered automatically by the command-palette help overlay.
290#[derive(Debug, Clone)]
291pub struct PublishedKeymap {
292    /// Optional widget / scope name (e.g. `"rich_log"`, `"table"`).
293    pub name: &'static str,
294    /// `(key_combo, description)` pairs. Always `'static` — no per-frame
295    /// allocation.
296    pub bindings: &'static [(&'static str, &'static str)],
297}
298
299impl PublishedKeymap {
300    /// Construct a [`PublishedKeymap`] from a static slice of bindings.
301    pub const fn new(
302        name: &'static str,
303        bindings: &'static [(&'static str, &'static str)],
304    ) -> Self {
305        Self { name, bindings }
306    }
307}