diff --git a/television/action.rs b/television/action.rs index 628c7ba..bcaede6 100644 --- a/television/action.rs +++ b/television/action.rs @@ -117,16 +117,78 @@ pub enum Action { MouseClickAt(u16, u16), } +/// Container for one or more actions that can be executed together. +/// +/// This enum enables binding single keys to multiple actions, allowing for +/// complex behaviors triggered by a single key press. It supports both +/// single action bindings (for backward compatibility) and multiple action +/// sequences. +/// +/// # Variants +/// +/// - `Single(Action)` - A single action binding +/// - `Multiple(Vec)` - Multiple actions executed in sequence +/// +/// # Configuration Examples +/// +/// ```toml +/// # Single action (traditional) +/// esc = "quit" +/// +/// # Multiple actions (new feature) +/// "ctrl-r" = ["reload_source", "copy_entry_to_clipboard"] +/// ``` +/// +/// # Usage +/// +/// ```rust +/// use television::action::{Action, Actions}; +/// +/// // Single action +/// let single = Actions::Single(Action::Quit); +/// assert_eq!(single.as_slice(), &[Action::Quit]); +/// +/// // Multiple actions +/// let multiple = Actions::Multiple(vec![Action::ReloadSource, Action::Quit]); +/// assert_eq!(multiple.as_slice(), &[Action::ReloadSource, Action::Quit]); +/// +/// // Convert to vector for execution +/// let actions_vec = multiple.into_vec(); +/// assert_eq!(actions_vec, vec![Action::ReloadSource, Action::Quit]); +/// ``` #[derive( Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, PartialOrd, Ord, )] #[serde(untagged)] pub enum Actions { + /// A single action binding Single(Action), + /// Multiple actions executed in sequence Multiple(Vec), } impl Actions { + /// Converts the `Actions` into a `Vec` for execution. + /// + /// This method consumes the `Actions` and returns a vector containing + /// all actions to be executed. For `Single`, it returns a vector with + /// one element. For `Multiple`, it returns the contained vector. + /// + /// # Returns + /// + /// A `Vec` containing all actions to execute. + /// + /// # Examples + /// + /// ```rust + /// use television::action::{Action, Actions}; + /// + /// let single = Actions::Single(Action::Quit); + /// assert_eq!(single.into_vec(), vec![Action::Quit]); + /// + /// let multiple = Actions::Multiple(vec![Action::ReloadSource, Action::Quit]); + /// assert_eq!(multiple.into_vec(), vec![Action::ReloadSource, Action::Quit]); + /// ``` pub fn into_vec(self) -> Vec { match self { Actions::Single(action) => vec![action], @@ -134,6 +196,26 @@ impl Actions { } } + /// Returns a slice view of the actions without consuming the `Actions`. + /// + /// This method provides efficient access to the contained actions as a slice, + /// useful for iteration and inspection without taking ownership. + /// + /// # Returns + /// + /// A `&[Action]` slice containing all actions. + /// + /// # Examples + /// + /// ```rust + /// use television::action::{Action, Actions}; + /// + /// let single = Actions::Single(Action::Quit); + /// assert_eq!(single.as_slice(), &[Action::Quit]); + /// + /// let multiple = Actions::Multiple(vec![Action::ReloadSource, Action::Quit]); + /// assert_eq!(multiple.as_slice(), &[Action::ReloadSource, Action::Quit]); + /// ``` pub fn as_slice(&self) -> &[Action] { match self { Actions::Single(action) => std::slice::from_ref(action), @@ -143,12 +225,54 @@ impl Actions { } impl From for Actions { + /// Converts a single `Action` into `Actions::Single`. + /// + /// This conversion allows seamless use of single actions where + /// `Actions` is expected, maintaining backward compatibility. + /// + /// # Examples + /// + /// ```rust + /// use television::action::{Action, Actions}; + /// + /// let actions: Actions = Action::Quit.into(); + /// assert_eq!(actions, Actions::Single(Action::Quit)); + /// ``` fn from(action: Action) -> Self { Actions::Single(action) } } impl From> for Actions { + /// Converts a `Vec` into `Actions`. + /// + /// This conversion optimizes single-element vectors into `Actions::Single` + /// for efficiency, while multi-element vectors become `Actions::Multiple`. + /// + /// # Arguments + /// + /// * `actions` - Vector of actions to convert + /// + /// # Returns + /// + /// - `Actions::Single` if the vector has exactly one element + /// - `Actions::Multiple` if the vector has zero or multiple elements + /// + /// # Examples + /// + /// ```rust + /// use television::action::{Action, Actions}; + /// + /// // Single element becomes Single + /// let single_vec = vec![Action::Quit]; + /// let actions: Actions = single_vec.into(); + /// assert_eq!(actions, Actions::Single(Action::Quit)); + /// + /// // Multiple elements become Multiple + /// let multi_vec = vec![Action::ReloadSource, Action::Quit]; + /// let actions: Actions = multi_vec.into(); + /// assert!(matches!(actions, Actions::Multiple(_))); + /// ``` fn from(actions: Vec) -> Self { if actions.len() == 1 { Actions::Single(actions.into_iter().next().unwrap()) @@ -159,6 +283,25 @@ impl From> for Actions { } impl Display for Action { + /// Formats the action as its string representation for configuration files. + /// + /// This implementation provides the `snake_case` string representation of each + /// action as used in TOML configuration files. The output matches the + /// `#[serde(rename_all = "snake_case")]` serialization format. + /// + /// # Returns + /// + /// The `snake_case` string representation of the action. + /// + /// # Examples + /// + /// ```rust + /// use television::action::Action; + /// + /// assert_eq!(Action::Quit.to_string(), "quit"); + /// assert_eq!(Action::SelectNextEntry.to_string(), "select_next_entry"); + /// assert_eq!(Action::TogglePreview.to_string(), "toggle_preview"); + /// ``` fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Action::AddInputChar(_) => write!(f, "add_input_char"), diff --git a/television/config/keybindings.rs b/television/config/keybindings.rs index e24fe32..1778453 100644 --- a/television/config/keybindings.rs +++ b/television/config/keybindings.rs @@ -34,6 +34,30 @@ impl Display for Binding { } /// Generic bindings structure that can map any key type to actions +/// Generic bindings structure that maps any key type to actions. +/// +/// This is the core structure for storing key/event bindings in Television. +/// It provides a flexible mapping system that can work with different key types +/// (keyboard keys, mouse events, etc.) and supports serialization to/from TOML. +/// +/// # Type Parameters +/// +/// * `K` - The key type (must implement `Display`, `FromStr`, `Eq`, `Hash`) +/// +/// # Examples +/// +/// ```rust +/// use television::config::keybindings::{Bindings, KeyBindings}; +/// use television::event::Key; +/// use television::action::Action; +/// +/// // Create new empty bindings +/// let mut bindings = KeyBindings::new(); +/// +/// // Add a binding +/// bindings.insert(Key::Enter, Action::ConfirmSelection.into()); +/// assert_eq!(bindings.len(), 1); +/// ``` #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Bindings where @@ -53,6 +77,20 @@ where K: Display + FromStr + Eq + Hash, K::Err: Display, { + /// Creates a new empty bindings collection. + /// + /// # Returns + /// + /// A new `Bindings` instance with no key mappings. + /// + /// # Examples + /// + /// ```rust + /// use television::config::keybindings::KeyBindings; + /// + /// let bindings = KeyBindings::new(); + /// assert!(bindings.is_empty()); + /// ``` pub fn new() -> Self { Bindings { bindings: FxHashMap::default(), @@ -60,16 +98,83 @@ where } } -/// A set of keybindings that maps keys directly to actions. +/// A set of keybindings that maps keyboard keys directly to actions. /// -/// This struct represents the new architecture where keybindings are structured as -/// Key -> Action mappings in the configuration file. This eliminates the need for -/// runtime inversion and provides better discoverability. +/// This type alias represents the primary keybinding system in Television, where +/// keyboard keys are mapped directly to actions. +/// +/// # Features +/// +/// - Direct key-to-action mapping +/// - Support for single and multiple actions per key +/// - Key unbinding support (setting to `false`) +/// - Modifier key combinations (Ctrl, Alt, Shift, Super/Cmd) +/// +/// # Configuration Format +/// +/// ```toml +/// # Single action +/// esc = "quit" +/// enter = "confirm_selection" +/// +/// # Multiple actions +/// "ctrl-s" = ["reload_source", "copy_entry_to_clipboard"] +/// +/// # Unbind a key +/// "ctrl-c" = false +/// ``` +/// +/// # Examples +/// +/// ```rust +/// use television::config::keybindings::KeyBindings; +/// use television::event::Key; +/// use television::action::Action; +/// +/// let mut bindings = KeyBindings::new(); +/// bindings.insert(Key::Enter, Action::ConfirmSelection.into()); +/// bindings.insert(Key::Esc, Action::Quit.into()); +/// assert_eq!(bindings.len(), 2); +/// ``` pub type KeyBindings = Bindings; +/// Types of events that can be bound to actions. +/// +/// This enum defines the various non-keyboard events that can trigger actions +/// in Television, such as mouse events and terminal resize events. +/// +/// # Variants +/// +/// - `MouseClick` - Mouse button click events +/// - `MouseScrollUp` - Mouse wheel scroll up +/// - `MouseScrollDown` - Mouse wheel scroll down +/// - `Resize` - Terminal window resize events +/// - `Custom(String)` - Custom event types for extensibility +/// +/// # Configuration +/// +/// Events use kebab-case naming in TOML configuration: +/// +/// ```toml +/// mouse-click = "confirm_selection" +/// mouse-scroll-up = "scroll_preview_up" +/// resize = "refresh_layout" +/// ``` +/// +/// # Examples +/// +/// ```rust +/// use television::config::keybindings::EventType; +/// use std::str::FromStr; +/// +/// let event = EventType::from_str("mouse-click").unwrap(); +/// assert_eq!(event, EventType::MouseClick); +/// +/// let custom = EventType::Custom("my-event".to_string()); +/// assert_eq!(custom.to_string(), "my-event"); +/// ``` #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] #[serde(rename_all = "kebab-case")] -/// Types of events that can be bound to actions pub enum EventType { MouseClick, MouseScrollUp, @@ -114,7 +219,36 @@ impl Display for EventType { } } -/// A set of event bindings that maps events to actions. +/// A set of event bindings that maps non-keyboard events to actions. +/// +/// This type alias provides bindings for mouse events, resize events, +/// and other non-keyboard interactions. It uses the same underlying +/// `Bindings` structure as `KeyBindings` but for `EventType` instead of `Key`. +/// +/// # Default Bindings +/// +/// - `mouse-scroll-up` → `scroll_preview_up` +/// - `mouse-scroll-down` → `scroll_preview_down` +/// +/// # Configuration Example +/// +/// ```toml +/// [event-bindings] +/// mouse-click = "confirm_selection" +/// mouse-scroll-up = "scroll_preview_up" +/// resize = "refresh_layout" +/// ``` +/// +/// # Examples +/// +/// ```rust +/// use television::config::keybindings::{EventBindings, EventType}; +/// use television::action::Action; +/// +/// let mut bindings = EventBindings::new(); +/// bindings.insert(EventType::MouseClick, Action::ConfirmSelection.into()); +/// assert_eq!(bindings.len(), 1); +/// ``` pub type EventBindings = Bindings; impl From for Bindings @@ -168,7 +302,39 @@ where } } -/// Generic merge function for bindings +/// Merges two binding collections, with new bindings taking precedence. +/// +/// # Arguments +/// +/// * `bindings` - The base bindings collection (will be consumed) +/// * `new_bindings` - The new bindings to merge in (higher precedence) +/// +/// # Returns +/// +/// A new `Bindings` collection with merged key mappings. +/// +/// # Examples +/// +/// ```rust +/// use television::config::keybindings::{KeyBindings, merge_bindings}; +/// use television::event::Key; +/// use television::action::Action; +/// +/// let base = KeyBindings::from(vec![ +/// (Key::Enter, Action::ConfirmSelection), +/// (Key::Esc, Action::Quit), +/// ]); +/// +/// let custom = KeyBindings::from(vec![ +/// (Key::Esc, Action::NoOp), // Override quit with no-op +/// (Key::Tab, Action::ToggleSelectionDown), // Add new binding +/// ]); +/// +/// let merged = merge_bindings(base, &custom); +/// assert_eq!(merged.get(&Key::Enter), Some(&Action::ConfirmSelection.into())); +/// assert_eq!(merged.get(&Key::Esc), Some(&Action::NoOp.into())); +/// assert_eq!(merged.get(&Key::Tab), Some(&Action::ToggleSelectionDown.into())); +/// ``` pub fn merge_bindings( mut bindings: Bindings, new_bindings: &Bindings, @@ -256,12 +422,69 @@ impl Default for Bindings { } } +/// Parses a string representation of a key event into a `KeyEvent`. +/// +/// This function converts human-readable key descriptions (like "ctrl-a", "alt-enter") +/// into crossterm `KeyEvent` structures. +/// +/// # Arguments +/// +/// * `raw` - String representation of the key (e.g., "ctrl-a", "shift-f1", "esc") +/// +/// # Returns +/// +/// * `Ok(KeyEvent)` - Successfully parsed key event +/// * `Err(String)` - Parse error with description +/// +/// # Supported Modifiers +/// +/// - `ctrl-` - Control key +/// - `alt-` - Alt key +/// - `shift-` - Shift key +/// - `cmd-` - Command key (macOS) +/// - `super-` - Super key (Linux/Windows) +/// +/// # Examples +/// +/// ```rust +/// use television::config::keybindings::parse_key_event; +/// use crossterm::event::{KeyCode, KeyModifiers}; +/// +/// let event = parse_key_event("ctrl-a").unwrap(); +/// assert_eq!(event.code, KeyCode::Char('a')); +/// assert_eq!(event.modifiers, KeyModifiers::CONTROL); +/// +/// let event = parse_key_event("alt-enter").unwrap(); +/// assert_eq!(event.code, KeyCode::Enter); +/// assert_eq!(event.modifiers, KeyModifiers::ALT); +/// ``` pub fn parse_key_event(raw: &str) -> anyhow::Result { let raw_lower = raw.to_ascii_lowercase(); let (remaining, modifiers) = extract_modifiers(&raw_lower); parse_key_code_with_modifiers(remaining, modifiers) } +/// Extracts modifier keys from a raw key string. +/// +/// This helper function parses modifier prefixes (ctrl-, alt-, shift-, etc.) +/// from a key string and returns the remaining key part along with the +/// extracted modifiers as a `KeyModifiers` bitfield. +/// +/// # Arguments +/// +/// * `raw` - The raw key string (already lowercased) +/// +/// # Returns +/// +/// A tuple of (`remaining_key_string`, `extracted_modifiers`) +/// +/// # Examples +/// +/// ```ignore +/// let (key, mods) = extract_modifiers("ctrl-alt-a"); +/// assert_eq!(key, "a"); +/// assert!(mods.contains(KeyModifiers::CONTROL | KeyModifiers::ALT)); +/// ``` fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) { let mut modifiers = KeyModifiers::empty(); let mut current = raw; @@ -298,6 +521,28 @@ fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) { (current, modifiers) } +/// Parses a key code string with pre-extracted modifiers into a `KeyEvent`. +/// +/// This function handles the actual key code parsing after modifiers have +/// been extracted. It supports named keys (like "esc", "enter") and +/// single character keys. +/// +/// # Arguments +/// +/// * `raw` - The key string with modifiers already removed +/// * `modifiers` - Pre-extracted modifier keys +/// +/// # Returns +/// +/// * `Ok(KeyEvent)` - Successfully parsed key event +/// * `Err(String)` - Parse error for unrecognized keys +/// +/// # Supported Keys +/// +/// - Named keys: esc, enter, left, right, up, down, home, end, etc. +/// - Function keys: f1-f12 +/// - Special keys: space, tab, backspace, delete, etc. +/// - Single characters: a-z, 0-9, punctuation fn parse_key_code_with_modifiers( raw: &str, mut modifiers: KeyModifiers, @@ -361,6 +606,32 @@ fn parse_key_code_with_modifiers( Ok(KeyEvent::new(c, modifiers)) } +/// Converts a `KeyEvent` back to its string representation. +/// +/// This function performs the reverse operation of `parse_key_event`, +/// converting a crossterm `KeyEvent` back into a human-readable string +/// format that can be used in configuration files. +/// +/// # Arguments +/// +/// * `key_event` - The key event to convert +/// +/// # Returns +/// +/// String representation of the key event (e.g., "ctrl-a", "alt-enter") +/// +/// # Examples +/// +/// ```rust +/// use television::config::keybindings::key_event_to_string; +/// use crossterm::event::{KeyEvent, KeyCode, KeyModifiers}; +/// +/// let event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL); +/// assert_eq!(key_event_to_string(&event), "ctrl-a"); +/// +/// let event = KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT); +/// assert_eq!(key_event_to_string(&event), "alt-enter"); +/// ``` #[allow(dead_code)] pub fn key_event_to_string(key_event: &KeyEvent) -> String { let char; @@ -456,11 +727,25 @@ impl FromStr for Key { } } -pub fn parse_key(raw: &str) -> anyhow::Result { - Key::from_str(raw) -} - -/// Generic serializer that converts any key type to string for TOML compatibility +/// Generic serializer that converts any key type to string for TOML compatibility. +/// +/// This function enables serialization of the bindings `HashMap` to TOML format +/// by converting keys to their string representation using the `Display` trait. +/// This is used internally by serde when serializing `Bindings` structs. +/// +/// # Arguments +/// +/// * `bindings` - The bindings `HashMap` to serialize +/// * `serializer` - The serde serializer instance +/// +/// # Type Parameters +/// +/// * `K` - Key type that implements `Display` +/// * `S` - Serializer type +/// +/// # Returns +/// +/// Result of the serialization operation fn serialize_bindings( bindings: &FxHashMap, serializer: S, @@ -477,7 +762,30 @@ where map.end() } -/// Generic deserializer that parses string keys back to key enum +/// Generic deserializer that parses string keys back to key enum. +/// +/// This function enables deserialization from TOML format by parsing string keys +/// back into their appropriate key types using the `FromStr` trait. It also handles +/// special cases like boolean values for key unbinding. +/// +/// # Special Value Handling +/// +/// - `false` - Binds the key to `Action::NoOp` (effectively unbinding) +/// - `true` - Ignores the binding (preserves existing or default binding) +/// - String/Array - Normal action binding +/// +/// # Arguments +/// +/// * `deserializer` - The serde deserializer instance +/// +/// # Type Parameters +/// +/// * `K` - Key type that implements `FromStr`, `Eq`, and `Hash` +/// * `D` - Deserializer type +/// +/// # Returns +/// +/// Result containing the parsed bindings `HashMap` or deserialization error fn deserialize_bindings<'de, K, D>( deserializer: D, ) -> Result, D::Error> diff --git a/television/config/mod.rs b/television/config/mod.rs index 4767574..69c2543 100644 --- a/television/config/mod.rs +++ b/television/config/mod.rs @@ -15,7 +15,7 @@ use std::{ use tracing::{debug, warn}; pub use keybindings::{ - Binding, EventBindings, EventType, KeyBindings, merge_bindings, parse_key, + Binding, EventBindings, EventType, KeyBindings, merge_bindings, }; pub use themes::Theme; pub use ui::UiConfig; @@ -371,6 +371,7 @@ mod tests { use super::*; use std::fs::File; use std::io::Write; + use std::str::FromStr; use tempfile::tempdir; #[test] @@ -477,7 +478,7 @@ mod tests { default_config.shell_integration.keybindings.insert( "command_history".to_string(), - Binding::SingleKey(parse_key("ctrl-h").unwrap()), + Binding::SingleKey(Key::from_str("ctrl-h").unwrap()), ); default_config.shell_integration.merge_triggers(); @@ -550,8 +551,6 @@ mod tests { #[test] fn test_shell_integration_keybindings_are_overwritten_by_user() { - use crate::config::parse_key; - let user_config = r#" [shell_integration.keybindings] "smart_autocomplete" = "ctrl-t" @@ -574,11 +573,11 @@ mod tests { let expected: rustc_hash::FxHashMap = [ ( "command_history".to_string(), - Binding::SingleKey(parse_key("ctrl-[").unwrap()), + Binding::SingleKey(Key::from_str("ctrl-[").unwrap()), ), ( "smart_autocomplete".to_string(), - Binding::SingleKey(parse_key("ctrl-t").unwrap()), + Binding::SingleKey(Key::from_str("ctrl-t").unwrap()), ), ] .iter() diff --git a/television/event.rs b/television/event.rs index 8e87d5d..49c79fc 100644 --- a/television/event.rs +++ b/television/event.rs @@ -1,11 +1,3 @@ -use std::{ - fmt::Display, - future::Future, - pin::Pin, - task::{Context, Poll as TaskPoll}, - time::Duration, -}; - use crossterm::event::{ KeyCode::{ BackTab, Backspace, Char, Delete, Down, End, Enter, Esc, F, Home, @@ -14,11 +6,17 @@ use crossterm::event::{ KeyEvent, KeyEventKind, KeyModifiers, MouseEvent, MouseEventKind, }; use serde::{Deserialize, Serialize}; +use std::{ + fmt::Display, + future::Future, + pin::Pin, + str::FromStr, + task::{Context, Poll as TaskPoll}, + time::Duration, +}; use tokio::{signal, sync::mpsc}; use tracing::{debug, trace, warn}; -use crate::config::parse_key; - #[derive(Debug, Clone, Copy)] pub enum Event { Closed, @@ -70,19 +68,89 @@ pub enum Key { Tab, } +/// Unified input event type that encompasses all possible inputs. +/// +/// This enum provides a unified interface for handling different types of input +/// events in Television, including keyboard input, mouse events, terminal resize +/// events, and custom events. It enables the new binding system to map any +/// type of input to actions. +/// +/// # Variants +/// +/// - `Key(Key)` - Keyboard input events +/// - `Mouse(MouseInputEvent)` - Mouse events with position information +/// - `Resize(u16, u16)` - Terminal resize events with new dimensions +/// - `Custom(String)` - Custom events for extensibility +/// +/// # Usage in Bindings +/// +/// ```rust +/// use television::event::{InputEvent, Key, MouseInputEvent}; +/// use television::keymap::InputMap; +/// +/// let input_map = InputMap::new(); +/// +/// // Handle keyboard input +/// let key_event = InputEvent::Key(Key::Enter); +/// let actions = input_map.get_actions_for_input(&key_event); +/// assert_eq!(actions, None); // No bindings in empty map +/// +/// // Handle mouse input +/// let mouse_event = InputEvent::Mouse(MouseInputEvent { +/// kind: crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left), +/// position: (10, 5), +/// }); +/// let actions = input_map.get_actions_for_input(&mouse_event); +/// assert_eq!(actions, None); // No bindings in empty map +/// ``` #[derive(Debug, Clone, PartialEq, Eq, Hash)] -/// Unified input event type that encompasses all possible inputs pub enum InputEvent { + /// Keyboard input event Key(Key), + /// Mouse event with position information Mouse(MouseInputEvent), + /// Terminal resize event with new dimensions (width, height) Resize(u16, u16), + /// Custom event for extensibility Custom(String), } +/// Mouse event with position information for input mapping. +/// +/// This structure combines a mouse event type with its screen coordinates, +/// enabling position-aware mouse handling in the binding system. It provides +/// the information needed to map mouse events to appropriate actions. +/// +/// # Fields +/// +/// - `kind` - The type of mouse event (click, scroll, etc.) +/// - `position` - Screen coordinates as (column, row) tuple +/// +/// # Examples +/// +/// ```rust +/// use television::event::MouseInputEvent; +/// use crossterm::event::{MouseEventKind, MouseButton}; +/// +/// // Left mouse button click at position (10, 5) +/// let click_event = MouseInputEvent { +/// kind: MouseEventKind::Down(MouseButton::Left), +/// position: (10, 5), +/// }; +/// assert_eq!(click_event.position, (10, 5)); +/// +/// // Mouse scroll up at position (20, 15) +/// let scroll_event = MouseInputEvent { +/// kind: MouseEventKind::ScrollUp, +/// position: (20, 15), +/// }; +/// assert_eq!(scroll_event.position, (20, 15)); +/// ``` #[derive(Debug, Clone, PartialEq, Eq, Hash)] -/// Mouse event with position information for input mapping pub struct MouseInputEvent { + /// The type of mouse event (click, scroll, etc.) pub kind: MouseEventKind, + /// Screen coordinates as (column, row) pub position: (u16, u16), } @@ -92,7 +160,7 @@ impl<'de> Deserialize<'de> for Key { D: serde::Deserializer<'de>, { let s = String::deserialize(deserializer)?; - parse_key(&s).map_err(serde::de::Error::custom) + Key::from_str(&s).map_err(serde::de::Error::custom) } } @@ -270,6 +338,39 @@ fn flush_resize_events(first_resize: (u16, u16)) -> ((u16, u16), (u16, u16)) { (first_resize, last_resize) } +/// Converts a crossterm `KeyEvent` into Television's internal `Key` representation. +/// +/// This function handles the conversion from crossterm's key event format into +/// Television's simplified key representation, applying modifier key combinations +/// and filtering out key release events. +/// +/// # Arguments +/// +/// * `event` - The crossterm `KeyEvent` to convert +/// +/// # Returns +/// +/// The corresponding `Key` enum variant, or `Key::Null` for unsupported events +/// +/// # Key Mapping +/// +/// - Modifier combinations are mapped to specific variants (e.g., `Ctrl+a` → `Key::Ctrl('a')`) +/// - Key release events are ignored (return `Key::Null`) +/// - Special keys are mapped directly (e.g., `Enter` → `Key::Enter`) +/// - Function keys preserve their number (e.g., `F1` → `Key::F(1)`) +/// +/// # Examples +/// +/// ```rust +/// use television::event::{convert_raw_event_to_key, Key}; +/// use crossterm::event::{KeyEvent, KeyCode, KeyModifiers, KeyEventKind}; +/// +/// let event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL); +/// assert_eq!(convert_raw_event_to_key(event), Key::Ctrl('a')); +/// +/// let event = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()); +/// assert_eq!(convert_raw_event_to_key(event), Key::Enter); +/// ``` pub fn convert_raw_event_to_key(event: KeyEvent) -> Key { trace!("Raw event: {:?}", event); if event.kind == KeyEventKind::Release { diff --git a/television/keymap.rs b/television/keymap.rs index aad9c45..5d4dbe7 100644 --- a/television/keymap.rs +++ b/television/keymap.rs @@ -6,27 +6,66 @@ use crate::{ use crossterm::event::MouseEventKind; use rustc_hash::FxHashMap; -#[derive(Default, Debug, Clone)] /// An input map that handles both keyboard and non-keyboard input events. /// -/// This replaces the old Keymap structure and provides unified access to -/// both key bindings and event bindings through a single interface. +/// This structure supports multiple-actions-per-key and handles various +/// input types including keyboard keys, mouse events, resize events, and +/// custom events. /// -/// # Example: -/// ```ignore -/// InputMap { -/// Key::Char('j') => Action::MoveDown, -/// Key::Char('k') => Action::MoveUp, -/// EventType::MouseClick => Action::ConfirmSelection, -/// } +/// # Fields +/// +/// - `key_actions` - Maps keyboard keys to actions +/// - `event_actions` - Maps non-keyboard events to actions +/// +/// # Examples +/// +/// ```rust +/// use television::keymap::InputMap; +/// use television::event::{Key, InputEvent}; +/// use television::action::{Action, Actions}; +/// use television::config::{KeyBindings, EventType}; +/// +/// // Create from key bindings +/// let keybindings = KeyBindings::from(vec![ +/// (Key::Char('j'), Action::SelectNextEntry), +/// (Key::Char('k'), Action::SelectPrevEntry), +/// ]); +/// let input_map: InputMap = (&keybindings).into(); +/// +/// // Query actions for input +/// let key_input = InputEvent::Key(Key::Char('j')); +/// let actions = input_map.get_actions_for_input(&key_input); +/// assert_eq!(actions, Some(vec![Action::SelectNextEntry])); /// ``` +/// ``` +#[derive(Default, Debug, Clone)] pub struct InputMap { + /// Maps keyboard keys to their associated actions pub key_actions: FxHashMap, + /// Maps non-keyboard events to their associated actions pub event_actions: FxHashMap, } impl InputMap { - /// Create a new empty `InputMap` + /// Creates a new empty `InputMap`. + /// + /// Returns an empty input map with no key or event bindings. + /// Bindings can be added using the `merge` methods or by directly + /// manipulating the `key_actions` and `event_actions` fields. + /// + /// # Returns + /// + /// A new empty `InputMap` instance. + /// + /// # Examples + /// + /// ```rust + /// use television::keymap::InputMap; + /// + /// let input_map = InputMap::new(); + /// assert!(input_map.key_actions.is_empty()); + /// assert!(input_map.event_actions.is_empty()); + /// ``` pub fn new() -> Self { Self { key_actions: FxHashMap::default(), @@ -34,12 +73,67 @@ impl InputMap { } } - /// Get the actions for a given key + /// Gets all actions bound to a specific key. + /// + /// Returns a reference to the `Actions` (single or multiple) bound to + /// the given key, or `None` if no binding exists. + /// + /// # Arguments + /// + /// * `key` - The key to look up + /// + /// # Returns + /// + /// - `Some(&Actions)` - The actions bound to the key + /// - `None` - No binding exists for this key + /// + /// # Examples + /// + /// ```rust + /// use television::keymap::InputMap; + /// use television::event::Key; + /// use television::action::{Action, Actions}; + /// + /// let mut input_map = InputMap::new(); + /// input_map.key_actions.insert(Key::Enter, Actions::Single(Action::ConfirmSelection)); + /// + /// let actions = input_map.get_actions_for_key(&Key::Enter).unwrap(); + /// assert_eq!(actions.as_slice(), &[Action::ConfirmSelection]); + /// ``` pub fn get_actions_for_key(&self, key: &Key) -> Option<&Actions> { self.key_actions.get(key) } - /// Get the actions for a given event type + /// Gets all actions bound to a specific event type. + /// + /// Returns a reference to the `Actions` (single or multiple) bound to + /// the given event type, or `None` if no binding exists. + /// + /// # Arguments + /// + /// * `event` - The event type to look up + /// + /// # Returns + /// + /// - `Some(&Actions)` - The actions bound to the event + /// - `None` - No binding exists for this event type + /// + /// # Examples + /// + /// ```rust + /// use television::keymap::InputMap; + /// use television::config::EventType; + /// use television::action::{Action, Actions}; + /// + /// let mut input_map = InputMap::new(); + /// input_map.event_actions.insert( + /// EventType::MouseClick, + /// Actions::Single(Action::ConfirmSelection) + /// ); + /// + /// let actions = input_map.get_actions_for_event(&EventType::MouseClick).unwrap(); + /// assert_eq!(actions.as_slice(), &[Action::ConfirmSelection]); + /// ``` pub fn get_actions_for_event( &self, event: &EventType, @@ -47,7 +141,37 @@ impl InputMap { self.event_actions.get(event) } - /// Get the action for a given key (backward compatibility) + /// Gets the first action bound to a specific key (backward compatibility). + /// + /// This method provides backward compatibility with the old single-action + /// binding system. For keys with multiple actions, it returns only the + /// first action. Use `get_actions_for_key()` to get all actions. + /// + /// # Arguments + /// + /// * `key` - The key to look up + /// + /// # Returns + /// + /// - `Some(Action)` - The first action bound to the key + /// - `None` - No binding exists for this key + /// + /// # Examples + /// + /// ```rust + /// use television::keymap::InputMap; + /// use television::event::Key; + /// use television::action::{Action, Actions}; + /// + /// let mut input_map = InputMap::new(); + /// input_map.key_actions.insert( + /// Key::Ctrl('r'), + /// Actions::Multiple(vec![Action::ReloadSource, Action::ClearScreen]) + /// ); + /// + /// // Returns only the first action + /// assert_eq!(input_map.get_action_for_key(&Key::Ctrl('r')), Some(Action::ReloadSource)); + /// ``` pub fn get_action_for_key(&self, key: &Key) -> Option { self.key_actions.get(key).and_then(|actions| match actions { Actions::Single(action) => Some(action.clone()), @@ -55,7 +179,40 @@ impl InputMap { }) } - /// Get the action for a given event type (backward compatibility) + /// Gets the first action bound to a specific event type (backward compatibility). + /// + /// This method provides backward compatibility with the old single-action + /// binding system. For events with multiple actions, it returns only the + /// first action. Use `get_actions_for_event()` to get all actions. + /// + /// # Arguments + /// + /// * `event` - The event type to look up + /// + /// # Returns + /// + /// - `Some(Action)` - The first action bound to the event + /// - `None` - No binding exists for this event type + /// + /// # Examples + /// + /// ```rust + /// use television::keymap::InputMap; + /// use television::config::EventType; + /// use television::action::{Action, Actions}; + /// + /// let mut input_map = InputMap::new(); + /// input_map.event_actions.insert( + /// EventType::MouseClick, + /// Actions::Multiple(vec![Action::ConfirmSelection, Action::TogglePreview]) + /// ); + /// + /// // Returns only the first action + /// assert_eq!( + /// input_map.get_action_for_event(&EventType::MouseClick), + /// Some(Action::ConfirmSelection) + /// ); + /// ``` pub fn get_action_for_event(&self, event: &EventType) -> Option { self.event_actions .get(event) @@ -65,7 +222,47 @@ impl InputMap { }) } - /// Get all actions for any input event + /// Gets all actions for any input event. + /// + /// This is the primary method for querying actions in the new input system. + /// It handles all types of input events and returns a vector of all actions + /// that should be executed in response to the input. + /// + /// # Arguments + /// + /// * `input` - The input event to look up + /// + /// # Returns + /// + /// - `Some(Vec)` - All actions bound to the input event + /// - `None` - No binding exists for this input + /// + /// # Supported Input Types + /// + /// - `InputEvent::Key` - Keyboard input + /// - `InputEvent::Mouse` - Mouse clicks and scrolling + /// - `InputEvent::Resize` - Terminal resize events + /// - `InputEvent::Custom` - Custom event types + /// + /// # Examples + /// + /// ```rust + /// use television::keymap::InputMap; + /// use television::event::{Key, InputEvent, MouseInputEvent}; + /// use television::config::EventType; + /// use television::action::{Action, Actions}; + /// use crossterm::event::MouseEventKind; + /// + /// let mut input_map = InputMap::new(); + /// input_map.key_actions.insert( + /// Key::Ctrl('s'), + /// Actions::Multiple(vec![Action::ReloadSource, Action::CopyEntryToClipboard]) + /// ); + /// + /// let key_input = InputEvent::Key(Key::Ctrl('s')); + /// let actions = input_map.get_actions_for_input(&key_input).unwrap(); + /// assert_eq!(actions, vec![Action::ReloadSource, Action::CopyEntryToClipboard]); + /// ``` pub fn get_actions_for_input( &self, input: &InputEvent, @@ -88,7 +285,47 @@ impl InputMap { } } - /// Get an action for any input event (backward compatibility) + /// Gets the first action for any input event (backward compatibility). + /// + /// This method provides backward compatibility with the old single-action + /// system. It handles all input types and returns only the first action + /// bound to the input. Use `get_actions_for_input()` to get all actions. + /// + /// # Arguments + /// + /// * `input` - The input event to look up + /// + /// # Returns + /// + /// - `Some(Action)` - The first action bound to the input + /// - `None` - No binding exists for this input + /// + /// # Supported Input Types + /// + /// - `InputEvent::Key` - Returns first action for the key + /// - `InputEvent::Mouse` - Maps to appropriate event type and returns first action + /// - `InputEvent::Resize` - Returns first action for resize events + /// - `InputEvent::Custom` - Returns first action for the custom event + /// + /// # Examples + /// + /// ```rust + /// use television::keymap::InputMap; + /// use television::event::{Key, InputEvent}; + /// use television::action::{Action, Actions}; + /// + /// let mut input_map = InputMap::new(); + /// input_map.key_actions.insert( + /// Key::Enter, + /// Actions::Multiple(vec![Action::ConfirmSelection, Action::Quit]) + /// ); + /// + /// let key_input = InputEvent::Key(Key::Enter); + /// assert_eq!( + /// input_map.get_action_for_input(&key_input), + /// Some(Action::ConfirmSelection) // Only first action + /// ); + /// ``` pub fn get_action_for_input(&self, input: &InputEvent) -> Option { match input { InputEvent::Key(key) => self.get_action_for_key(key), @@ -112,10 +349,38 @@ impl InputMap { } impl From<&KeyBindings> for InputMap { - /// Convert a `KeyBindings` into an `InputMap`. + /// Converts `KeyBindings` into an `InputMap`. /// - /// Since the new `KeyBindings` already store Key -> Action mappings, - /// we can directly copy the bindings without inversion. + /// This conversion creates an input map containing only keyboard bindings. + /// The event bindings will be empty. Since the new `KeyBindings` structure + /// already stores Key → Actions mappings, we can directly copy the bindings + /// without the inversion that was needed in the old system. + /// + /// # Arguments + /// + /// * `keybindings` - The key bindings to convert + /// + /// # Returns + /// + /// An `InputMap` with the key bindings and empty event bindings. + /// + /// # Examples + /// + /// ```rust + /// use television::keymap::InputMap; + /// use television::config::KeyBindings; + /// use television::event::Key; + /// use television::action::Action; + /// + /// let keybindings = KeyBindings::from(vec![ + /// (Key::Enter, Action::ConfirmSelection), + /// (Key::Esc, Action::Quit), + /// ]); + /// + /// let input_map: InputMap = (&keybindings).into(); + /// assert_eq!(input_map.get_action_for_key(&Key::Enter), Some(Action::ConfirmSelection)); + /// assert!(input_map.event_actions.is_empty()); + /// ``` fn from(keybindings: &KeyBindings) -> Self { Self { key_actions: keybindings.bindings.clone(), @@ -125,7 +390,39 @@ impl From<&KeyBindings> for InputMap { } impl From<&EventBindings> for InputMap { - /// Convert `EventBindings` into an `InputMap`. + /// Converts `EventBindings` into an `InputMap`. + /// + /// This conversion creates an input map containing only event bindings. + /// The key bindings will be empty. This is useful when you want to + /// handle only non-keyboard events. + /// + /// # Arguments + /// + /// * `event_bindings` - The event bindings to convert + /// + /// # Returns + /// + /// An `InputMap` with the event bindings and empty key bindings. + /// + /// # Examples + /// + /// ```rust + /// use television::keymap::InputMap; + /// use television::config::{EventBindings, EventType}; + /// use television::action::Action; + /// + /// let event_bindings = EventBindings::from(vec![ + /// (EventType::MouseClick, Action::ConfirmSelection), + /// (EventType::Resize, Action::ClearScreen), + /// ]); + /// + /// let input_map: InputMap = (&event_bindings).into(); + /// assert_eq!( + /// input_map.get_action_for_event(&EventType::MouseClick), + /// Some(Action::ConfirmSelection) + /// ); + /// assert!(input_map.key_actions.is_empty()); + /// ``` fn from(event_bindings: &EventBindings) -> Self { Self { key_actions: FxHashMap::default(), @@ -135,7 +432,43 @@ impl From<&EventBindings> for InputMap { } impl From<(&KeyBindings, &EventBindings)> for InputMap { - /// Convert both `KeyBindings` and `EventBindings` into an `InputMap`. + /// Converts both `KeyBindings` and `EventBindings` into an `InputMap`. + /// + /// This conversion creates a complete input map with both keyboard and + /// event bindings. This is the most common way to create a fully + /// configured input map that handles all types of input events. + /// + /// # Arguments + /// + /// * `keybindings` - The keyboard bindings to include + /// * `event_bindings` - The event bindings to include + /// + /// # Returns + /// + /// An `InputMap` containing both key and event bindings. + /// + /// # Examples + /// + /// ```rust + /// use television::keymap::InputMap; + /// use television::config::{KeyBindings, EventBindings, EventType}; + /// use television::event::Key; + /// use television::action::Action; + /// + /// let keybindings = KeyBindings::from(vec![ + /// (Key::Enter, Action::ConfirmSelection), + /// ]); + /// let event_bindings = EventBindings::from(vec![ + /// (EventType::MouseClick, Action::ConfirmSelection), + /// ]); + /// + /// let input_map: InputMap = (&keybindings, &event_bindings).into(); + /// assert_eq!(input_map.get_action_for_key(&Key::Enter), Some(Action::ConfirmSelection)); + /// assert_eq!( + /// input_map.get_action_for_event(&EventType::MouseClick), + /// Some(Action::ConfirmSelection) + /// ); + /// ``` fn from( (keybindings, event_bindings): (&KeyBindings, &EventBindings), ) -> Self { @@ -147,9 +480,40 @@ impl From<(&KeyBindings, &EventBindings)> for InputMap { } impl InputMap { - /// Merge another `InputMap` into this one. + /// Merges another `InputMap` into this one. /// - /// This will overwrite any existing keys/events in `self` with the mappings from `other`. + /// This method combines the bindings from another `InputMap` into this one, + /// with bindings from `other` taking precedence over existing bindings. + /// This is useful for applying configuration hierarchies and user customizations. + /// + /// # Arguments + /// + /// * `other` - The input map to merge into this one + /// + /// # Behavior + /// + /// - Existing keys/events in `self` are overwritten by those in `other` + /// - New keys/events from `other` are added to `self` + /// - Both key bindings and event bindings are merged + /// + /// # Examples + /// + /// ```rust + /// use television::keymap::InputMap; + /// use television::event::Key; + /// use television::action::{Action, Actions}; + /// + /// let mut base_map = InputMap::new(); + /// base_map.key_actions.insert(Key::Enter, Actions::Single(Action::ConfirmSelection)); + /// + /// let mut custom_map = InputMap::new(); + /// custom_map.key_actions.insert(Key::Enter, Actions::Single(Action::Quit)); // Override + /// custom_map.key_actions.insert(Key::Esc, Actions::Single(Action::Quit)); // New binding + /// + /// base_map.merge(&custom_map); + /// assert_eq!(base_map.get_action_for_key(&Key::Enter), Some(Action::Quit)); + /// assert_eq!(base_map.get_action_for_key(&Key::Esc), Some(Action::Quit)); + /// ``` pub fn merge(&mut self, other: &InputMap) { for (key, action) in &other.key_actions { self.key_actions.insert(*key, action.clone()); @@ -159,14 +523,66 @@ impl InputMap { } } - /// Merge key bindings into this `InputMap` + /// Merges key bindings into this `InputMap`. + /// + /// This method adds all key bindings from a `KeyBindings` configuration + /// into this input map, overwriting any existing bindings for the same keys. + /// + /// # Arguments + /// + /// * `keybindings` - The key bindings to merge + /// + /// # Examples + /// + /// ```rust + /// use television::keymap::InputMap; + /// use television::config::KeyBindings; + /// use television::event::Key; + /// use television::action::Action; + /// + /// let mut input_map = InputMap::new(); + /// let keybindings = KeyBindings::from(vec![ + /// (Key::Enter, Action::ConfirmSelection), + /// (Key::Esc, Action::Quit), + /// ]); + /// + /// input_map.merge_key_bindings(&keybindings); + /// assert_eq!(input_map.get_action_for_key(&Key::Enter), Some(Action::ConfirmSelection)); + /// ``` pub fn merge_key_bindings(&mut self, keybindings: &KeyBindings) { for (key, action) in &keybindings.bindings { self.key_actions.insert(*key, action.clone()); } } - /// Merge event bindings into this `InputMap` + /// Merges event bindings into this `InputMap`. + /// + /// This method adds all event bindings from an `EventBindings` configuration + /// into this input map, overwriting any existing bindings for the same events. + /// + /// # Arguments + /// + /// * `event_bindings` - The event bindings to merge + /// + /// # Examples + /// + /// ```rust + /// use television::keymap::InputMap; + /// use television::config::{EventBindings, EventType}; + /// use television::action::Action; + /// + /// let mut input_map = InputMap::new(); + /// let event_bindings = EventBindings::from(vec![ + /// (EventType::MouseClick, Action::ConfirmSelection), + /// (EventType::Resize, Action::ClearScreen), + /// ]); + /// + /// input_map.merge_event_bindings(&event_bindings); + /// assert_eq!( + /// input_map.get_action_for_event(&EventType::MouseClick), + /// Some(Action::ConfirmSelection) + /// ); + /// ``` pub fn merge_event_bindings(&mut self, event_bindings: &EventBindings) { for (event, action) in &event_bindings.bindings { self.event_actions.insert(event.clone(), action.clone());