bubbletea_widgets/key.rs
1//! Key binding component for managing keybindings, ported from the Go version.
2//!
3//! This module provides types and functions for defining keybindings that can be
4//! used for both input handling and generating help views. It offers a type-safe
5//! alternative to string-based key matching.
6
7use bubbletea_rs::KeyMsg;
8use crossterm::event::{KeyCode, KeyModifiers};
9
10/// Represents a specific key press, combining a `KeyCode` and `KeyModifiers`.
11///
12/// This provides a structured, type-safe way to define key combinations that can
13/// be used throughout the application for consistent key handling.
14///
15/// # Examples
16///
17/// ```rust
18/// use bubbletea_widgets::key::KeyPress;
19/// use crossterm::event::{KeyCode, KeyModifiers};
20///
21/// // Create a Ctrl+C key press
22/// let ctrl_c = KeyPress {
23/// code: KeyCode::Char('c'),
24/// mods: KeyModifiers::CONTROL,
25/// };
26///
27/// // Create from tuple
28/// let alt_f4: KeyPress = (KeyCode::F(4), KeyModifiers::ALT).into();
29///
30/// // Create from string
31/// let escape: KeyPress = "esc".into();
32/// ```
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct KeyPress {
35 /// The key code representing the physical key pressed.
36 pub code: KeyCode,
37 /// The modifier keys (Ctrl, Alt, Shift) held during the key press.
38 pub mods: KeyModifiers,
39}
40
41/// Creates a `KeyPress` from a tuple of `(KeyCode, KeyModifiers)`.
42///
43/// This provides a convenient way to create key press instances from tuples,
44/// making it easy to define key combinations inline.
45///
46/// # Examples
47///
48/// ```rust
49/// use bubbletea_widgets::key::KeyPress;
50/// use crossterm::event::{KeyCode, KeyModifiers};
51///
52/// let save_key: KeyPress = (KeyCode::Char('s'), KeyModifiers::CONTROL).into();
53/// let quit_key: KeyPress = (KeyCode::Char('q'), KeyModifiers::CONTROL).into();
54/// ```
55impl From<(KeyCode, KeyModifiers)> for KeyPress {
56 fn from((code, mods): (KeyCode, KeyModifiers)) -> Self {
57 Self { code, mods }
58 }
59}
60
61/// Creates a `KeyPress` from just a `KeyCode` with no modifiers.
62///
63/// This is useful for simple keys that don't require modifier combinations,
64/// such as arrow keys, function keys, or single character keys.
65///
66/// # Examples
67///
68/// ```rust
69/// use bubbletea_widgets::key::KeyPress;
70/// use crossterm::event::KeyCode;
71///
72/// let enter_key: KeyPress = KeyCode::Enter.into();
73/// let up_arrow: KeyPress = KeyCode::Up.into();
74/// let letter_a: KeyPress = KeyCode::Char('a').into();
75/// ```
76impl From<KeyCode> for KeyPress {
77 fn from(code: KeyCode) -> Self {
78 Self {
79 code,
80 mods: KeyModifiers::NONE,
81 }
82 }
83}
84
85/// Creates a `KeyPress` from a string representation.
86///
87/// This provides a human-readable way to define key combinations using string
88/// names. Supports both simple keys and modifier combinations.
89///
90/// # Supported Formats
91///
92/// - Simple keys: "enter", "tab", "esc", "space", "up", "down", etc.
93/// - Function keys: "f1", "f2", ..., "f12"
94/// - Single characters: "a", "1", "?", "/"
95/// - Modifier combinations: "ctrl+c", "alt+f4", "shift+tab"
96/// - Complex combinations: "ctrl+alt+a"
97///
98/// # Examples
99///
100/// ```rust
101/// use bubbletea_widgets::key::KeyPress;
102///
103/// let enter: KeyPress = "enter".into();
104/// let ctrl_c: KeyPress = "ctrl+c".into();
105/// let alt_f4: KeyPress = "alt+f4".into();
106/// let page_up: KeyPress = "pgup".into();
107/// ```
108///
109/// # Panics
110///
111/// This function does not panic. Unknown key combinations will result in
112/// a `KeyPress` with `KeyCode::Null`.
113impl From<&str> for KeyPress {
114 fn from(s: &str) -> Self {
115 parse_key_string(s)
116 }
117}
118
119/// Help information for displaying keybinding documentation.
120///
121/// This structure contains the human-readable representation of a key binding
122/// that will be shown in help views and documentation.
123///
124/// # Examples
125///
126/// ```rust
127/// use bubbletea_widgets::key::Help;
128///
129/// let help = Help {
130/// key: "ctrl+s".to_string(),
131/// desc: "Save file".to_string(),
132/// };
133/// ```
134#[derive(Debug, Clone, Default, PartialEq, Eq)]
135pub struct Help {
136 /// The human-readable representation of the key combination (e.g., "ctrl+s", "enter").
137 pub key: String,
138 /// A brief description of what the key binding does.
139 pub desc: String,
140}
141
142/// Describes a set of keybindings and their associated help text. A `Binding`
143/// represents a single semantic action that can be triggered by one or more
144/// physical key presses.
145#[derive(Debug, Clone, Default)]
146pub struct Binding {
147 /// The key press combinations that trigger this binding.
148 keys: Vec<KeyPress>,
149 /// The help information for displaying in help views.
150 help: Help,
151 /// Whether the binding is currently enabled and should match key events.
152 enabled: bool,
153}
154
155/// Option type for configuring a Binding during initialization.
156pub type BindingOpt = Box<dyn FnOnce(&mut Binding)>;
157
158impl Binding {
159 /// Creates a new keybinding with a set of associated key presses.
160 ///
161 /// The input can be anything convertible into a `Vec<KeyPress>`, making it
162 /// ergonomic to define keys with or without modifiers. The binding is
163 /// created in an enabled state with empty help text.
164 ///
165 /// # Arguments
166 ///
167 /// * `keys` - A vector of items that can be converted to `KeyPress`
168 ///
169 /// # Examples
170 ///
171 /// ```rust
172 /// use bubbletea_widgets::key::Binding;
173 /// use crossterm::event::{KeyCode, KeyModifiers};
174 ///
175 /// // Binding with multiple keys, some with modifiers
176 /// let save_binding = Binding::new(vec![
177 /// (KeyCode::Char('s'), KeyModifiers::CONTROL), // Ctrl+S
178 /// (KeyCode::F(2), KeyModifiers::NONE), // F2 key
179 /// ]);
180 ///
181 /// // Binding from string representations
182 /// let quit_binding = Binding::new(vec!["q", "ctrl+c"]);
183 /// ```
184 pub fn new<K: Into<KeyPress>>(keys: Vec<K>) -> Self {
185 Self {
186 keys: keys.into_iter().map(Into::into).collect(),
187 help: Help::default(),
188 enabled: true,
189 }
190 }
191
192 /// Creates a new binding using builder options.
193 ///
194 /// This provides a flexible way to create bindings with various configuration
195 /// options applied. Similar to Go's NewBinding function.
196 ///
197 /// # Arguments
198 ///
199 /// * `opts` - A vector of builder options to configure the binding
200 ///
201 /// # Examples
202 ///
203 /// ```rust
204 /// use bubbletea_widgets::key::{Binding, with_keys_str, with_help};
205 ///
206 /// let save_binding = Binding::new_binding(vec![
207 /// with_keys_str(&["ctrl+s", "f2"]),
208 /// with_help("ctrl+s", "Save the current file"),
209 /// ]);
210 /// ```
211 pub fn new_binding(opts: Vec<BindingOpt>) -> Self {
212 let mut binding = Self {
213 keys: Vec::new(),
214 help: Help::default(),
215 enabled: true,
216 };
217 for opt in opts {
218 opt(&mut binding);
219 }
220 binding
221 }
222
223 /// Sets the help text for the keybinding using a builder pattern.
224 ///
225 /// # Arguments
226 ///
227 /// * `key` - The human-readable key representation for help display
228 /// * `desc` - A brief description of what the binding does
229 ///
230 /// # Examples
231 ///
232 /// ```rust
233 /// use bubbletea_widgets::key::Binding;
234 /// use crossterm::event::KeyCode;
235 ///
236 /// let binding = Binding::new(vec![KeyCode::Enter])
237 /// .with_help("enter", "Confirm selection");
238 /// ```
239 pub fn with_help(mut self, key: impl Into<String>, desc: impl Into<String>) -> Self {
240 self.help = Help {
241 key: key.into(),
242 desc: desc.into(),
243 };
244 self
245 }
246
247 /// Sets the initial enabled state of the keybinding.
248 ///
249 /// Disabled bindings will not match key events and will not appear in help views.
250 ///
251 /// # Arguments
252 ///
253 /// * `enabled` - Whether the binding should be enabled
254 ///
255 /// # Examples
256 ///
257 /// ```rust
258 /// use bubbletea_widgets::key::Binding;
259 /// use crossterm::event::KeyCode;
260 ///
261 /// let disabled_binding = Binding::new(vec![KeyCode::F(1)])
262 /// .with_enabled(false);
263 /// ```
264 pub fn with_enabled(mut self, enabled: bool) -> Self {
265 self.enabled = enabled;
266 self
267 }
268
269 /// Sets the keybinding to disabled state (convenience method).
270 ///
271 /// This is equivalent to calling `with_enabled(false)` but more readable
272 /// when you specifically want to disable a binding.
273 ///
274 /// # Examples
275 ///
276 /// ```rust
277 /// use bubbletea_widgets::key::Binding;
278 /// use crossterm::event::KeyCode;
279 ///
280 /// let disabled_binding = Binding::new(vec![KeyCode::F(1)])
281 /// .with_disabled();
282 /// ```
283 pub fn with_disabled(mut self) -> Self {
284 self.enabled = false;
285 self
286 }
287
288 /// Sets the keys for this binding from string representations.
289 ///
290 /// This provides a convenient way to set multiple keys using human-readable
291 /// string representations.
292 ///
293 /// # Arguments
294 ///
295 /// * `keys` - Array of string key representations
296 ///
297 /// # Examples
298 ///
299 /// ```rust
300 /// use bubbletea_widgets::key::Binding;
301 ///
302 /// let binding = Binding::new::<&str>(vec![])
303 /// .with_keys(&["ctrl+s", "f2", "alt+s"]);
304 /// ```
305 pub fn with_keys(mut self, keys: &[&str]) -> Self {
306 self.keys = keys.iter().map(|k| parse_key_string(k)).collect();
307 self
308 }
309
310 /// Sets the keys for the keybinding (mutable version).
311 ///
312 /// # Arguments
313 ///
314 /// * `keys` - A vector of items that can be converted to `KeyPress`
315 ///
316 /// # Examples
317 ///
318 /// ```rust
319 /// use bubbletea_widgets::key::Binding;
320 /// use crossterm::event::KeyCode;
321 ///
322 /// let mut binding = Binding::new::<&str>(vec![]);
323 /// binding.set_keys(vec![KeyCode::Enter, KeyCode::Char(' ')]);
324 /// ```
325 pub fn set_keys<K: Into<KeyPress>>(&mut self, keys: Vec<K>) {
326 self.keys = keys.into_iter().map(Into::into).collect();
327 }
328
329 /// Returns the key presses associated with this binding.
330 ///
331 /// # Examples
332 ///
333 /// ```rust
334 /// use bubbletea_widgets::key::Binding;
335 /// use crossterm::event::KeyCode;
336 ///
337 /// let binding = Binding::new(vec![KeyCode::Enter]);
338 /// let keys = binding.keys();
339 /// assert_eq!(keys.len(), 1);
340 /// ```
341 pub fn keys(&self) -> &[KeyPress] {
342 &self.keys
343 }
344
345 /// Sets the help text for the keybinding (mutable version).
346 ///
347 /// # Arguments
348 ///
349 /// * `key` - The human-readable key representation for help display
350 /// * `desc` - A brief description of what the binding does
351 ///
352 /// # Examples
353 ///
354 /// ```rust
355 /// use bubbletea_widgets::key::Binding;
356 /// use crossterm::event::KeyCode;
357 ///
358 /// let mut binding = Binding::new(vec![KeyCode::Enter]);
359 /// binding.set_help("enter", "Confirm selection");
360 /// ```
361 pub fn set_help(&mut self, key: impl Into<String>, desc: impl Into<String>) {
362 self.help = Help {
363 key: key.into(),
364 desc: desc.into(),
365 };
366 }
367
368 /// Returns the help information for this binding.
369 ///
370 /// # Examples
371 ///
372 /// ```rust
373 /// use bubbletea_widgets::key::Binding;
374 /// use crossterm::event::KeyCode;
375 ///
376 /// let binding = Binding::new(vec![KeyCode::Enter])
377 /// .with_help("enter", "Confirm selection");
378 /// let help = binding.help();
379 /// assert_eq!(help.key, "enter");
380 /// assert_eq!(help.desc, "Confirm selection");
381 /// ```
382 pub fn help(&self) -> &Help {
383 &self.help
384 }
385
386 /// Returns `true` if the keybinding is enabled and has keys configured.
387 ///
388 /// Disabled bindings or bindings with no keys will not match key events
389 /// and will not appear in help views.
390 ///
391 /// # Examples
392 ///
393 /// ```rust
394 /// use bubbletea_widgets::key::Binding;
395 /// use crossterm::event::KeyCode;
396 ///
397 /// let binding = Binding::new(vec![KeyCode::Enter]);
398 /// assert!(binding.enabled());
399 ///
400 /// let disabled = Binding::new(vec![KeyCode::F(1)]).with_disabled();
401 /// assert!(!disabled.enabled());
402 ///
403 /// let empty = Binding::new::<KeyCode>(vec![]);
404 /// assert!(!empty.enabled());
405 /// ```
406 pub fn enabled(&self) -> bool {
407 self.enabled && !self.keys.is_empty()
408 }
409
410 /// Sets the enabled state of the keybinding (mutable version).
411 ///
412 /// # Arguments
413 ///
414 /// * `enabled` - Whether the binding should be enabled
415 ///
416 /// # Examples
417 ///
418 /// ```rust
419 /// use bubbletea_widgets::key::Binding;
420 /// use crossterm::event::KeyCode;
421 ///
422 /// let mut binding = Binding::new(vec![KeyCode::F(1)]);
423 /// binding.set_enabled(false);
424 /// assert!(!binding.enabled());
425 /// ```
426 pub fn set_enabled(&mut self, enabled: bool) {
427 self.enabled = enabled;
428 }
429
430 /// Removes all keys and help text, effectively nullifying the binding.
431 ///
432 /// After calling this method, the binding will be disabled and will not
433 /// match any key events or appear in help views.
434 ///
435 /// # Examples
436 ///
437 /// ```rust
438 /// use bubbletea_widgets::key::Binding;
439 /// use crossterm::event::KeyCode;
440 ///
441 /// let mut binding = Binding::new(vec![KeyCode::Enter])
442 /// .with_help("enter", "Confirm");
443 /// assert!(binding.enabled());
444 ///
445 /// binding.unbind();
446 /// assert!(!binding.enabled());
447 /// assert!(binding.keys().is_empty());
448 /// ```
449 pub fn unbind(&mut self) {
450 self.keys.clear();
451 self.help = Help::default();
452 }
453
454 /// Checks if a `KeyMsg` from `bubbletea-rs` matches this binding.
455 ///
456 /// The match is successful if the binding is enabled and the event's
457 /// `code` and `modifiers` match one of the `KeyPress` entries exactly.
458 ///
459 /// # Arguments
460 ///
461 /// * `key_msg` - The key message to test against this binding
462 ///
463 /// # Returns
464 ///
465 /// `true` if the key message matches this binding, `false` otherwise.
466 ///
467 /// # Examples
468 ///
469 /// ```rust
470 /// use bubbletea_widgets::key::Binding;
471 /// use bubbletea_rs::KeyMsg;
472 /// use crossterm::event::{KeyCode, KeyModifiers};
473 ///
474 /// let binding = Binding::new(vec![(KeyCode::Char('s'), KeyModifiers::CONTROL)]);
475 ///
476 /// let ctrl_s = KeyMsg {
477 /// key: KeyCode::Char('s'),
478 /// modifiers: KeyModifiers::CONTROL,
479 /// };
480 ///
481 /// assert!(binding.matches(&ctrl_s));
482 /// ```
483 pub fn matches(&self, key_msg: &KeyMsg) -> bool {
484 if !self.enabled() {
485 return false;
486 }
487 for key_press in &self.keys {
488 if key_msg.key == key_press.code && key_msg.modifiers == key_press.mods {
489 return true;
490 }
491 }
492 false
493 }
494
495 /// A convenience function that checks if a `KeyMsg` matches any of the provided bindings.
496 ///
497 /// # Arguments
498 ///
499 /// * `key_msg` - The key message to test
500 /// * `bindings` - A slice of binding references to check against
501 ///
502 /// # Returns
503 ///
504 /// `true` if the key message matches any of the bindings, `false` otherwise.
505 ///
506 /// # Examples
507 ///
508 /// ```rust
509 /// use bubbletea_widgets::key::Binding;
510 /// use bubbletea_rs::KeyMsg;
511 /// use crossterm::event::{KeyCode, KeyModifiers};
512 ///
513 /// let save = Binding::new(vec![(KeyCode::Char('s'), KeyModifiers::CONTROL)]);
514 /// let quit = Binding::new(vec![(KeyCode::Char('q'), KeyModifiers::CONTROL)]);
515 ///
516 /// let bindings = vec![&save, &quit];
517 /// let ctrl_s = KeyMsg {
518 /// key: KeyCode::Char('s'),
519 /// modifiers: KeyModifiers::CONTROL,
520 /// };
521 ///
522 /// assert!(Binding::matches_any(&ctrl_s, &bindings));
523 /// ```
524 pub fn matches_any(key_msg: &KeyMsg, bindings: &[&Self]) -> bool {
525 for binding in bindings {
526 if binding.matches(key_msg) {
527 return true;
528 }
529 }
530 false
531 }
532}
533
534/// KeyMap trait for components that provide help information.
535///
536/// This trait should be implemented by any component that wants to provide
537/// contextual help information through the help component. It matches the
538/// Go implementation's KeyMap interface.
539///
540/// # Examples
541///
542/// ```rust
543/// use bubbletea_widgets::key::{KeyMap, Binding};
544/// use crossterm::event::KeyCode;
545///
546/// struct MyComponent {
547/// save: Binding,
548/// quit: Binding,
549/// }
550///
551/// impl KeyMap for MyComponent {
552/// fn short_help(&self) -> Vec<&Binding> {
553/// vec![&self.save, &self.quit]
554/// }
555///
556/// fn full_help(&self) -> Vec<Vec<&Binding>> {
557/// vec![
558/// vec![&self.save], // File operations column
559/// vec![&self.quit], // Application column
560/// ]
561/// }
562/// }
563/// ```
564pub trait KeyMap {
565 /// Returns a slice of bindings to be displayed in the short version of help.
566 ///
567 /// This should return the most important or commonly used key bindings
568 /// that will fit on a single line.
569 fn short_help(&self) -> Vec<&Binding>;
570
571 /// Returns an extended group of help items, grouped by columns.
572 ///
573 /// Each inner vector represents a column of related key bindings. This
574 /// allows for organized display of comprehensive help information.
575 fn full_help(&self) -> Vec<Vec<&Binding>>;
576}
577
578/// Checks if the given KeyMsg matches any of the given bindings.
579///
580/// This is a standalone function similar to Go's Matches function that provides
581/// a convenient way to test a key message against multiple bindings at once.
582///
583/// # Arguments
584///
585/// * `key_msg` - The key message to test
586/// * `bindings` - A slice of binding references to check against
587///
588/// # Returns
589///
590/// `true` if the key message matches any of the bindings, `false` otherwise.
591///
592/// # Examples
593///
594/// ```rust
595/// use bubbletea_widgets::key::{matches, Binding};
596/// use bubbletea_rs::KeyMsg;
597/// use crossterm::event::{KeyCode, KeyModifiers};
598///
599/// let save = Binding::new(vec![(KeyCode::Char('s'), KeyModifiers::CONTROL)]);
600/// let quit = Binding::new(vec![(KeyCode::Char('q'), KeyModifiers::CONTROL)]);
601///
602/// let bindings = vec![&save, &quit];
603/// let ctrl_s = KeyMsg {
604/// key: KeyCode::Char('s'),
605/// modifiers: KeyModifiers::CONTROL,
606/// };
607///
608/// assert!(matches(&ctrl_s, &bindings));
609/// ```
610pub fn matches(key_msg: &KeyMsg, bindings: &[&Binding]) -> bool {
611 for binding in bindings {
612 if (*binding).matches(key_msg) {
613 return true;
614 }
615 }
616 false
617}
618
619/// Creates a new binding from options - Go compatibility function.
620///
621/// This function provides Go-style binding creation using a vector of options.
622/// It's equivalent to calling `Binding::new_binding()` but provides a more
623/// functional programming style interface.
624///
625/// # Arguments
626///
627/// * `opts` - A vector of builder options to configure the binding
628///
629/// # Examples
630///
631/// ```rust
632/// use bubbletea_widgets::key::{new_binding, with_keys_str, with_help};
633///
634/// let save_binding = new_binding(vec![
635/// with_keys_str(&["ctrl+s", "f2"]),
636/// with_help("ctrl+s", "Save the current file"),
637/// ]);
638/// ```
639pub fn new_binding(opts: Vec<BindingOpt>) -> Binding {
640 Binding::new_binding(opts)
641}
642
643/// Creates a binding option that sets the keys from string names.
644///
645/// This function provides Go-style `WithKeys` functionality, allowing you to
646/// set multiple key bindings using human-readable string representations.
647///
648/// # Arguments
649///
650/// * `keys` - Array of string key representations
651///
652/// # Examples
653///
654/// ```rust
655/// use bubbletea_widgets::key::{new_binding, with_keys_str, with_help};
656///
657/// let binding = new_binding(vec![
658/// with_keys_str(&["ctrl+s", "alt+s", "f2"]),
659/// with_help("ctrl+s", "Save file"),
660/// ]);
661/// ```
662pub fn with_keys_str(keys: &[&str]) -> BindingOpt {
663 // Pre-parse into KeyPress so the closure can be 'static
664 let parsed: Vec<KeyPress> = keys.iter().map(|s| parse_key_string(s)).collect();
665 Box::new(move |b: &mut Binding| {
666 b.keys = parsed.clone();
667 })
668}
669
670/// Checks if a KeyMsg matches a specific binding - Go compatibility.
671///
672/// This is a convenience function that provides Go-style compatibility for
673/// checking if a single binding matches a key message.
674///
675/// # Arguments
676///
677/// * `key_msg` - The key message to test
678/// * `binding` - The binding to check against
679///
680/// # Returns
681///
682/// `true` if the key message matches the binding, `false` otherwise.
683///
684/// # Examples
685///
686/// ```rust
687/// use bubbletea_widgets::key::{matches_binding, Binding};
688/// use bubbletea_rs::KeyMsg;
689/// use crossterm::event::{KeyCode, KeyModifiers};
690///
691/// let binding = Binding::new(vec![(KeyCode::Char('s'), KeyModifiers::CONTROL)]);
692/// let ctrl_s = KeyMsg {
693/// key: KeyCode::Char('s'),
694/// modifiers: KeyModifiers::CONTROL,
695/// };
696///
697/// assert!(matches_binding(&ctrl_s, &binding));
698/// ```
699pub fn matches_binding(key_msg: &KeyMsg, binding: &Binding) -> bool {
700 binding.matches(key_msg)
701}
702
703/// Creates a binding option that sets the keys from KeyPress values.
704///
705/// This function allows you to set key bindings using strongly-typed KeyPress
706/// values rather than string representations.
707///
708/// # Arguments
709///
710/// * `keys` - A vector of items that can be converted to KeyPress
711///
712/// # Examples
713///
714/// ```rust
715/// use bubbletea_widgets::key::{new_binding, with_keys, with_help};
716/// use crossterm::event::{KeyCode, KeyModifiers};
717///
718/// let binding = new_binding(vec![
719/// with_keys(vec![
720/// (KeyCode::Char('s'), KeyModifiers::CONTROL),
721/// (KeyCode::F(2), KeyModifiers::NONE),
722/// ]),
723/// with_help("ctrl+s", "Save file"),
724/// ]);
725/// ```
726pub fn with_keys<K: Into<KeyPress> + Clone + 'static>(keys: Vec<K>) -> BindingOpt {
727 Box::new(move |b: &mut Binding| {
728 b.keys = keys.into_iter().map(Into::into).collect();
729 })
730}
731
732/// Creates a binding option that sets the help text.
733///
734/// This function provides Go-style `WithHelp` functionality for setting
735/// the help text that will be displayed in help views.
736///
737/// # Arguments
738///
739/// * `key` - The human-readable key representation for help display
740/// * `desc` - A brief description of what the binding does
741///
742/// # Examples
743///
744/// ```rust
745/// use bubbletea_widgets::key::{new_binding, with_keys_str, with_help};
746///
747/// let binding = new_binding(vec![
748/// with_keys_str(&["ctrl+s"]),
749/// with_help("ctrl+s", "Save the current file"),
750/// ]);
751/// ```
752pub fn with_help(
753 key: impl Into<String> + 'static,
754 desc: impl Into<String> + 'static,
755) -> BindingOpt {
756 Box::new(move |b: &mut Binding| {
757 b.help = Help {
758 key: key.into(),
759 desc: desc.into(),
760 };
761 })
762}
763
764/// Creates a binding option that disables the binding.
765///
766/// This function provides Go-style `WithDisabled` functionality for creating
767/// bindings that are initially disabled.
768///
769/// # Examples
770///
771/// ```rust
772/// use bubbletea_widgets::key::{new_binding, with_keys_str, with_disabled};
773///
774/// let binding = new_binding(vec![
775/// with_keys_str(&["f1"]),
776/// with_disabled(),
777/// ]);
778///
779/// assert!(!binding.enabled());
780/// ```
781pub fn with_disabled() -> BindingOpt {
782 Box::new(|b: &mut Binding| {
783 b.enabled = false;
784 })
785}
786
787/// Parses string representations of keys into KeyPress instances.
788///
789/// This function converts human-readable key descriptions into structured
790/// KeyPress objects. It supports a wide variety of key formats and combinations.
791///
792/// # Supported Formats
793///
794/// ## Simple Keys
795/// - Arrow keys: "up", "down", "left", "right"
796/// - Special keys: "enter", "tab", "esc"/"escape", "space", "backspace"
797/// - Navigation: "home", "end", "pgup"/"pageup", "pgdown"/"pagedown"/"pgdn"
798/// - Function keys: "f1" through "f12"
799/// - Characters: "a", "1", "?", "/", etc.
800///
801/// ## Modifier Combinations
802/// - Single modifier: "ctrl+c", "alt+f4", "shift+tab"
803/// - Double modifiers: "ctrl+alt+a", "ctrl+shift+s"
804///
805/// # Arguments
806///
807/// * `s` - The string representation of the key
808///
809/// # Returns
810///
811/// A `KeyPress` representing the parsed key combination. Unknown keys
812/// result in `KeyCode::Null`.
813///
814/// # Examples
815///
816/// ```rust
817/// use bubbletea_widgets::key::parse_key_string;
818/// use crossterm::event::{KeyCode, KeyModifiers};
819///
820/// let enter = parse_key_string("enter");
821/// let ctrl_c = parse_key_string("ctrl+c");
822/// let alt_f4 = parse_key_string("alt+f4");
823/// let complex = parse_key_string("ctrl+alt+a");
824///
825/// assert_eq!(enter.code, KeyCode::Enter);
826/// assert_eq!(ctrl_c.code, KeyCode::Char('c'));
827/// assert_eq!(ctrl_c.mods, KeyModifiers::CONTROL);
828/// ```
829///
830/// # Panics
831///
832/// This function does not panic. Invalid or unknown key combinations will
833/// result in a KeyPress with `KeyCode::Null`.
834pub fn parse_key_string(s: &str) -> KeyPress {
835 match s {
836 "up" => KeyPress::from(KeyCode::Up),
837 "down" => KeyPress::from(KeyCode::Down),
838 "left" => KeyPress::from(KeyCode::Left),
839 "right" => KeyPress::from(KeyCode::Right),
840 "enter" => KeyPress::from(KeyCode::Enter),
841 "tab" => KeyPress::from(KeyCode::Tab),
842 "backspace" => KeyPress::from(KeyCode::Backspace),
843 "delete" | "del" => KeyPress::from(KeyCode::Delete),
844 "esc" | "escape" => KeyPress::from(KeyCode::Esc),
845 "home" => KeyPress::from(KeyCode::Home),
846 "end" => KeyPress::from(KeyCode::End),
847 "pgup" | "pageup" => KeyPress::from(KeyCode::PageUp),
848 "pgdown" | "pagedown" | "pgdn" => KeyPress::from(KeyCode::PageDown),
849 "f1" => KeyPress::from(KeyCode::F(1)),
850 "f2" => KeyPress::from(KeyCode::F(2)),
851 "f3" => KeyPress::from(KeyCode::F(3)),
852 "f4" => KeyPress::from(KeyCode::F(4)),
853 "f5" => KeyPress::from(KeyCode::F(5)),
854 "f6" => KeyPress::from(KeyCode::F(6)),
855 "f7" => KeyPress::from(KeyCode::F(7)),
856 "f8" => KeyPress::from(KeyCode::F(8)),
857 "f9" => KeyPress::from(KeyCode::F(9)),
858 "f10" => KeyPress::from(KeyCode::F(10)),
859 "f11" => KeyPress::from(KeyCode::F(11)),
860 "f12" => KeyPress::from(KeyCode::F(12)),
861 // Handle compound key combinations
862 key if key.contains('+') => {
863 let parts: Vec<&str> = key.split('+').collect();
864 if parts.len() == 2 {
865 let (modifier_str, key_str) = (parts[0], parts[1]);
866 let modifiers = match modifier_str {
867 "ctrl" => KeyModifiers::CONTROL,
868 "alt" => KeyModifiers::ALT,
869 "shift" => KeyModifiers::SHIFT,
870 _ => KeyModifiers::NONE,
871 };
872
873 let keycode = match key_str {
874 "tab" if modifier_str == "shift" => KeyCode::BackTab,
875 "space" | " " => KeyCode::Char(' '),
876 key_str if key_str.len() == 1 => KeyCode::Char(key_str.chars().next().unwrap()),
877 _ => parse_key_string(key_str).code, // Recursive parse for complex keys
878 };
879
880 KeyPress::from((keycode, modifiers))
881 } else if parts.len() == 3 {
882 // Handle ctrl+alt+key combinations
883 let key_str = parts[2];
884 let mut modifiers = KeyModifiers::NONE;
885
886 for part in &parts[0..2] {
887 match *part {
888 "ctrl" => modifiers |= KeyModifiers::CONTROL,
889 "alt" => modifiers |= KeyModifiers::ALT,
890 "shift" => modifiers |= KeyModifiers::SHIFT,
891 _ => {}
892 }
893 }
894
895 let keycode = if key_str.len() == 1 {
896 KeyCode::Char(key_str.chars().next().unwrap())
897 } else {
898 parse_key_string(key_str).code
899 };
900
901 KeyPress::from((keycode, modifiers))
902 } else {
903 KeyPress::from(KeyCode::Null)
904 }
905 }
906 " " | "space" => KeyPress::from(KeyCode::Char(' ')),
907 "?" => KeyPress::from(KeyCode::Char('?')),
908 "/" => KeyPress::from(KeyCode::Char('/')),
909 "insert" => KeyPress::from(KeyCode::Insert),
910 "null" => KeyPress::from(KeyCode::Null),
911 // Single characters
912 s if s.len() == 1 => {
913 let ch = s.chars().next().unwrap();
914 KeyPress::from(KeyCode::Char(ch))
915 }
916 _ => KeyPress::from(KeyCode::Null), // Unknown key
917 }
918}