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 but hide from help bar display.
79    pub fn bind_hidden(mut self, key: char, description: &str) -> Self {
80        self.bindings.push(Binding {
81            key: KeyCode::Char(key),
82            modifiers: None,
83            display: key.to_string(),
84            description: description.to_string(),
85            visible: false,
86        });
87        self
88    }
89
90    /// Get visible bindings for help bar rendering.
91    pub fn visible_bindings(&self) -> impl Iterator<Item = &Binding> {
92        self.bindings.iter().filter(|binding| binding.visible)
93    }
94}
95
96fn display_for_key_code(key: &KeyCode) -> String {
97    match key {
98        KeyCode::Char(c) => c.to_string(),
99        KeyCode::Enter => "Enter".to_string(),
100        KeyCode::Backspace => "Backspace".to_string(),
101        KeyCode::Tab => "Tab".to_string(),
102        KeyCode::BackTab => "Shift+Tab".to_string(),
103        KeyCode::Esc => "Esc".to_string(),
104        KeyCode::Up => "↑".to_string(),
105        KeyCode::Down => "↓".to_string(),
106        KeyCode::Left => "←".to_string(),
107        KeyCode::Right => "→".to_string(),
108        KeyCode::Home => "Home".to_string(),
109        KeyCode::End => "End".to_string(),
110        KeyCode::PageUp => "PgUp".to_string(),
111        KeyCode::PageDown => "PgDn".to_string(),
112        KeyCode::Delete => "Del".to_string(),
113        KeyCode::Insert => "Ins".to_string(),
114        KeyCode::Null => "Null".to_string(),
115        KeyCode::CapsLock => "CapsLock".to_string(),
116        KeyCode::ScrollLock => "ScrollLock".to_string(),
117        KeyCode::NumLock => "NumLock".to_string(),
118        KeyCode::PrintScreen => "PrtSc".to_string(),
119        KeyCode::Pause => "Pause".to_string(),
120        KeyCode::Menu => "Menu".to_string(),
121        KeyCode::KeypadBegin => "KP5".to_string(),
122        KeyCode::F(n) => format!("F{n}"),
123    }
124}
125
126fn display_for_mod_char(mods: KeyModifiers, key: char) -> String {
127    let mut parts: Vec<&str> = Vec::new();
128    if mods.contains(KeyModifiers::CONTROL) {
129        parts.push("Ctrl");
130    }
131    if mods.contains(KeyModifiers::ALT) {
132        parts.push("Alt");
133    }
134    if mods.contains(KeyModifiers::SHIFT) {
135        parts.push("Shift");
136    }
137    if mods.contains(KeyModifiers::SUPER) {
138        parts.push("Super");
139    }
140    if mods.contains(KeyModifiers::HYPER) {
141        parts.push("Hyper");
142    }
143    if mods.contains(KeyModifiers::META) {
144        parts.push("Meta");
145    }
146
147    if parts.is_empty() {
148        key.to_string()
149    } else {
150        format!("{}+{}", parts.join("+"), key.to_ascii_uppercase())
151    }
152}