Skip to main content

slt/
keymap.rs

1use crate::{KeyCode, KeyModifiers};
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    }
149}
150
151fn display_for_code_mod(key: &KeyCode, mods: KeyModifiers) -> String {
152    let mut parts: Vec<&str> = Vec::new();
153    if mods.contains(KeyModifiers::CONTROL) {
154        parts.push("Ctrl");
155    }
156    if mods.contains(KeyModifiers::ALT) {
157        parts.push("Alt");
158    }
159    if mods.contains(KeyModifiers::SHIFT) {
160        parts.push("Shift");
161    }
162    if mods.contains(KeyModifiers::SUPER) {
163        parts.push("Super");
164    }
165    if mods.contains(KeyModifiers::HYPER) {
166        parts.push("Hyper");
167    }
168    if mods.contains(KeyModifiers::META) {
169        parts.push("Meta");
170    }
171
172    let key_label = display_for_key_code(key);
173    if parts.is_empty() {
174        key_label
175    } else {
176        format!("{}+{}", parts.join("+"), key_label)
177    }
178}
179
180fn display_for_mod_char(mods: KeyModifiers, key: char) -> String {
181    let mut parts: Vec<&str> = Vec::new();
182    if mods.contains(KeyModifiers::CONTROL) {
183        parts.push("Ctrl");
184    }
185    if mods.contains(KeyModifiers::ALT) {
186        parts.push("Alt");
187    }
188    if mods.contains(KeyModifiers::SHIFT) {
189        parts.push("Shift");
190    }
191    if mods.contains(KeyModifiers::SUPER) {
192        parts.push("Super");
193    }
194    if mods.contains(KeyModifiers::HYPER) {
195        parts.push("Hyper");
196    }
197    if mods.contains(KeyModifiers::META) {
198        parts.push("Meta");
199    }
200
201    if parts.is_empty() {
202        key.to_string()
203    } else {
204        format!("{}+{}", parts.join("+"), key.to_ascii_uppercase())
205    }
206}
207
208/// Opt-in trait for users to publish a widget's keymap to the framework so
209/// `Context::keymap_help_overlay` can list it on `?` press (issue #236).
210///
211/// # Scope
212///
213/// SLT does **not** implement this on built-in widgets — it is a user-facing
214/// extension point. Built-in widgets register their bindings directly inside
215/// their `impl Context::*` methods. To publish the keymap of your *own*
216/// custom widget, implement this trait, then call
217/// [`Context::publish_keymap`](crate::Context::publish_keymap) in your render
218/// method:
219///
220/// ```ignore
221/// ctx.publish_keymap("my_widget", MyState::key_help(&state));
222/// ```
223///
224/// In other words, this trait is the same shape as `std::fmt::Display`: zero
225/// blanket / built-in impls; you opt in by implementing it on your own type.
226/// If you prefer a free-function call, [`Context::publish_keymap`] takes the
227/// same `(name, &'static [...])` signature without the trait.
228///
229/// # Format
230///
231/// The returned slice must be `'static` — hardcode a `const` array per
232/// widget so the registration is allocation-free. Each tuple is
233/// `(key_combo, description)` using the same display style as
234/// [`Binding::display`] (e.g. `"↑/k"`, `"Ctrl+S"`, `"PgDn"`).
235///
236/// # Example
237///
238/// ```
239/// use slt::keymap::WidgetKeyHelp;
240///
241/// struct Counter;
242///
243/// impl WidgetKeyHelp for Counter {
244///     fn key_help(&self) -> &'static [(&'static str, &'static str)] {
245///         const HELP: &[(&str, &str)] = &[
246///             ("↑/k", "increment"),
247///             ("↓/j", "decrement"),
248///             ("r", "reset"),
249///         ];
250///         HELP
251///     }
252/// }
253///
254/// let counter = Counter;
255/// assert_eq!(counter.key_help().len(), 3);
256/// ```
257pub trait WidgetKeyHelp {
258    /// Return the keyboard shortcuts for this widget.
259    ///
260    /// Format: `(key_combo, description)`.
261    fn key_help(&self) -> &'static [(&'static str, &'static str)];
262}
263
264/// A single published keymap entry collected via
265/// [`Context::publish_keymap`](crate::Context::publish_keymap) (issue #236).
266///
267/// Once registered for the current frame, every entry is queryable through
268/// [`Context::published_keymaps`](crate::Context::published_keymaps) and is
269/// rendered automatically by the command-palette help overlay.
270#[derive(Debug, Clone)]
271pub struct PublishedKeymap {
272    /// Optional widget / scope name (e.g. `"rich_log"`, `"table"`).
273    pub name: &'static str,
274    /// `(key_combo, description)` pairs. Always `'static` — no per-frame
275    /// allocation.
276    pub bindings: &'static [(&'static str, &'static str)],
277}
278
279impl PublishedKeymap {
280    /// Construct a [`PublishedKeymap`] from a static slice of bindings.
281    pub const fn new(
282        name: &'static str,
283        bindings: &'static [(&'static str, &'static str)],
284    ) -> Self {
285        Self { name, bindings }
286    }
287}