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    pub bindings: Vec<Binding>,
33}
34
35impl KeyMap {
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Bind a character key.
41    pub fn bind(mut self, key: char, description: &str) -> Self {
42        self.bindings.push(Binding {
43            key: KeyCode::Char(key),
44            modifiers: None,
45            display: key.to_string(),
46            description: description.to_string(),
47            visible: true,
48        });
49        self
50    }
51
52    /// Bind a special key (Enter, Esc, Up, Down, etc.).
53    pub fn bind_code(mut self, key: KeyCode, description: &str) -> Self {
54        self.bindings.push(Binding {
55            display: display_for_key_code(&key),
56            key,
57            modifiers: None,
58            description: description.to_string(),
59            visible: true,
60        });
61        self
62    }
63
64    /// Bind a key with modifier (Ctrl+S, etc.).
65    pub fn bind_mod(mut self, key: char, mods: KeyModifiers, description: &str) -> Self {
66        self.bindings.push(Binding {
67            key: KeyCode::Char(key),
68            modifiers: Some(mods),
69            display: display_for_mod_char(mods, key),
70            description: description.to_string(),
71            visible: true,
72        });
73        self
74    }
75
76    /// Bind but hide from help bar display.
77    pub fn bind_hidden(mut self, key: char, description: &str) -> Self {
78        self.bindings.push(Binding {
79            key: KeyCode::Char(key),
80            modifiers: None,
81            display: key.to_string(),
82            description: description.to_string(),
83            visible: false,
84        });
85        self
86    }
87
88    /// Get visible bindings for help bar rendering.
89    pub fn visible_bindings(&self) -> impl Iterator<Item = &Binding> {
90        self.bindings.iter().filter(|binding| binding.visible)
91    }
92}
93
94fn display_for_key_code(key: &KeyCode) -> String {
95    match key {
96        KeyCode::Char(c) => c.to_string(),
97        KeyCode::Enter => "Enter".to_string(),
98        KeyCode::Backspace => "Backspace".to_string(),
99        KeyCode::Tab => "Tab".to_string(),
100        KeyCode::BackTab => "Shift+Tab".to_string(),
101        KeyCode::Esc => "Esc".to_string(),
102        KeyCode::Up => "↑".to_string(),
103        KeyCode::Down => "↓".to_string(),
104        KeyCode::Left => "←".to_string(),
105        KeyCode::Right => "→".to_string(),
106        KeyCode::Home => "Home".to_string(),
107        KeyCode::End => "End".to_string(),
108        KeyCode::PageUp => "PgUp".to_string(),
109        KeyCode::PageDown => "PgDn".to_string(),
110        KeyCode::Delete => "Del".to_string(),
111        KeyCode::F(n) => format!("F{n}"),
112    }
113}
114
115fn display_for_mod_char(mods: KeyModifiers, key: char) -> String {
116    let mut parts: Vec<&str> = Vec::new();
117    if mods.contains(KeyModifiers::CONTROL) {
118        parts.push("Ctrl");
119    }
120    if mods.contains(KeyModifiers::ALT) {
121        parts.push("Alt");
122    }
123    if mods.contains(KeyModifiers::SHIFT) {
124        parts.push("Shift");
125    }
126
127    if parts.is_empty() {
128        key.to_string()
129    } else {
130        format!("{}+{}", parts.join("+"), key.to_ascii_uppercase())
131    }
132}