Skip to main content

bubbles/
key.rs

1//! Keybinding definitions and matching utilities.
2//!
3//! This module provides types for defining keybindings and matching them against
4//! key events. It's useful for creating user-configurable keymaps in TUI applications.
5//!
6//! # Example
7//!
8//! ```rust
9//! use bubbles::key::{Binding, matches};
10//!
11//! let up = Binding::new()
12//!     .keys(&["k", "up"])
13//!     .help("↑/k", "move up");
14//!
15//! let down = Binding::new()
16//!     .keys(&["j", "down"])
17//!     .help("↓/j", "move down");
18//!
19//! // Check if a key matches
20//! assert!(matches("k", &[&up, &down]));
21//! assert!(matches("down", &[&up, &down]));
22//! assert!(!matches("x", &[&up, &down]));
23//! ```
24
25use std::fmt;
26
27/// Help information for a keybinding.
28#[derive(Debug, Clone, Default, PartialEq, Eq)]
29pub struct Help {
30    /// The key(s) to display in help text (e.g., "↑/k").
31    pub key: String,
32    /// Description of what the binding does.
33    pub desc: String,
34}
35
36impl Help {
37    /// Creates new help information.
38    #[must_use]
39    pub fn new(key: impl Into<String>, desc: impl Into<String>) -> Self {
40        Self {
41            key: key.into(),
42            desc: desc.into(),
43        }
44    }
45}
46
47/// A keybinding with associated help text.
48///
49/// Bindings can be enabled/disabled and contain zero or more key sequences
50/// that trigger the binding.
51#[derive(Debug, Clone, Default)]
52pub struct Binding {
53    keys: Vec<String>,
54    help: Help,
55    disabled: bool,
56}
57
58impl Binding {
59    /// Creates a new empty binding.
60    #[must_use]
61    pub fn new() -> Self {
62        Self::default()
63    }
64
65    /// Sets the keys for this binding.
66    ///
67    /// # Example
68    ///
69    /// ```rust
70    /// use bubbles::key::Binding;
71    ///
72    /// let binding = Binding::new().keys(&["k", "up", "ctrl+p"]);
73    /// assert_eq!(binding.get_keys(), &["k", "up", "ctrl+p"]);
74    /// ```
75    #[must_use]
76    pub fn keys(mut self, keys: &[&str]) -> Self {
77        self.keys = keys.iter().map(|&s| s.to_string()).collect();
78        self
79    }
80
81    /// Sets the help text for this binding.
82    ///
83    /// # Example
84    ///
85    /// ```rust
86    /// use bubbles::key::Binding;
87    ///
88    /// let binding = Binding::new()
89    ///     .keys(&["q"])
90    ///     .help("q", "quit");
91    /// assert_eq!(binding.get_help().key, "q");
92    /// assert_eq!(binding.get_help().desc, "quit");
93    /// ```
94    #[must_use]
95    pub fn help(mut self, key: impl Into<String>, desc: impl Into<String>) -> Self {
96        self.help = Help::new(key, desc);
97        self
98    }
99
100    /// Creates a disabled binding.
101    #[must_use]
102    pub fn disabled(mut self) -> Self {
103        self.disabled = true;
104        self
105    }
106
107    /// Sets the keys for this binding (mutable version).
108    pub fn set_keys(&mut self, keys: &[&str]) {
109        self.keys = keys.iter().map(|&s| s.to_string()).collect();
110    }
111
112    /// Returns the keys for this binding.
113    #[must_use]
114    pub fn get_keys(&self) -> &[String] {
115        &self.keys
116    }
117
118    /// Sets the help text for this binding (mutable version).
119    pub fn set_help(&mut self, key: impl Into<String>, desc: impl Into<String>) {
120        self.help = Help::new(key, desc);
121    }
122
123    /// Returns the help information for this binding.
124    #[must_use]
125    pub fn get_help(&self) -> &Help {
126        &self.help
127    }
128
129    /// Returns whether this binding is enabled.
130    ///
131    /// A binding is enabled if it's not explicitly disabled and has at least one key.
132    #[must_use]
133    pub fn enabled(&self) -> bool {
134        !self.disabled && !self.keys.is_empty()
135    }
136
137    /// Enables or disables the binding (mutable version).
138    pub fn enable(&mut self, enabled: bool) {
139        self.disabled = !enabled;
140    }
141
142    /// Enables or disables the binding (builder version).
143    #[must_use]
144    pub fn set_enabled(mut self, enabled: bool) -> Self {
145        self.disabled = !enabled;
146        self
147    }
148
149    /// Removes the keys and help from this binding, effectively nullifying it.
150    ///
151    /// This is a step beyond disabling - it removes the binding entirely.
152    /// Use this when you want to completely remove a keybinding from a keymap.
153    pub fn unbind(&mut self) {
154        self.keys.clear();
155        self.help = Help::default();
156    }
157}
158
159/// Checks if the given key matches any of the given bindings.
160///
161/// The key is compared against all keys in each binding. Only enabled bindings
162/// are considered.
163///
164/// # Example
165///
166/// ```rust
167/// use bubbles::key::{Binding, matches};
168///
169/// let quit = Binding::new().keys(&["q", "ctrl+c"]);
170/// let disabled = Binding::new().keys(&["x"]).disabled();
171///
172/// assert!(matches("q", &[&quit]));
173/// assert!(matches("ctrl+c", &[&quit]));
174/// assert!(!matches("x", &[&disabled])); // Disabled bindings don't match
175/// ```
176pub fn matches<K: fmt::Display>(key: K, bindings: &[&Binding]) -> bool {
177    let key_str = key.to_string();
178    for binding in bindings {
179        if binding.enabled() {
180            for k in &binding.keys {
181                if *k == key_str {
182                    return true;
183                }
184            }
185        }
186    }
187    false
188}
189
190/// Checks if the given key matches a single binding.
191///
192/// Convenience function for matching against a single binding.
193pub fn matches_one<K: fmt::Display>(key: K, binding: &Binding) -> bool {
194    matches(key, &[binding])
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_binding_new() {
203        let binding = Binding::new();
204        assert!(binding.get_keys().is_empty());
205        assert!(!binding.enabled());
206    }
207
208    #[test]
209    fn test_binding_with_keys() {
210        let binding = Binding::new().keys(&["k", "up"]);
211        assert_eq!(binding.get_keys(), &["k", "up"]);
212        assert!(binding.enabled());
213    }
214
215    #[test]
216    fn test_binding_with_help() {
217        let binding = Binding::new()
218            .keys(&["q"])
219            .help("q", "quit the application");
220        assert_eq!(binding.get_help().key, "q");
221        assert_eq!(binding.get_help().desc, "quit the application");
222    }
223
224    #[test]
225    fn test_binding_disabled() {
226        let binding = Binding::new().keys(&["q"]).disabled();
227        assert!(!binding.enabled());
228    }
229
230    #[test]
231    fn test_binding_set_enabled() {
232        let mut binding = Binding::new().keys(&["q"]).disabled();
233        assert!(!binding.enabled());
234        binding.enable(true);
235        assert!(binding.enabled());
236    }
237
238    #[test]
239    fn test_binding_set_enabled_builder() {
240        let binding = Binding::new().keys(&["q"]).set_enabled(false);
241        assert!(!binding.enabled());
242        let binding = binding.set_enabled(true);
243        assert!(binding.enabled());
244    }
245
246    #[test]
247    fn test_binding_unbind() {
248        let mut binding = Binding::new().keys(&["q"]).help("q", "quit");
249        binding.unbind();
250        assert!(binding.get_keys().is_empty());
251        assert!(binding.get_help().key.is_empty());
252    }
253
254    #[test]
255    fn test_matches() {
256        let up = Binding::new().keys(&["k", "up"]);
257        let down = Binding::new().keys(&["j", "down"]);
258
259        assert!(matches("k", &[&up, &down]));
260        assert!(matches("up", &[&up, &down]));
261        assert!(matches("j", &[&up, &down]));
262        assert!(matches("down", &[&up, &down]));
263        assert!(!matches("x", &[&up, &down]));
264    }
265
266    #[test]
267    fn test_matches_disabled() {
268        let binding = Binding::new().keys(&["q"]).disabled();
269        assert!(!matches("q", &[&binding]));
270    }
271
272    #[test]
273    fn test_matches_empty() {
274        let binding = Binding::new();
275        assert!(!matches("q", &[&binding]));
276    }
277
278    #[test]
279    fn test_matches_one() {
280        let quit = Binding::new().keys(&["q", "ctrl+c"]);
281        assert!(matches_one("q", &quit));
282        assert!(matches_one("ctrl+c", &quit));
283        assert!(!matches_one("x", &quit));
284    }
285}