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}