docs(bindings): added docs

This commit is contained in:
lalvarezt 2025-07-23 11:51:02 +02:00
parent 78cc303003
commit bf3a22a7cf
5 changed files with 1025 additions and 58 deletions

View File

@ -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<Action>)` - 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<Action>),
}
impl Actions {
/// Converts the `Actions` into a `Vec<Action>` 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<Action>` 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<Action> {
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<Action> 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<Vec<Action>> for Actions {
/// Converts a `Vec<Action>` 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<Action>) -> Self {
if actions.len() == 1 {
Actions::Single(actions.into_iter().next().unwrap())
@ -159,6 +283,25 @@ impl From<Vec<Action>> 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"),

View File

@ -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<K>
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<Key>;
/// 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<EventType>;
impl<K, I> From<I> for Bindings<K>
@ -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<K>(
mut bindings: Bindings<K>,
new_bindings: &Bindings<K>,
@ -256,12 +422,69 @@ impl Default for Bindings<EventType> {
}
}
/// 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<KeyEvent, String> {
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, String> {
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<K, S>(
bindings: &FxHashMap<K, Actions>,
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<FxHashMap<K, Actions>, D::Error>

View File

@ -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<String, Binding> = [
(
"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()

View File

@ -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<I> {
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 {

View File

@ -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<Key, Actions>,
/// Maps non-keyboard events to their associated actions
pub event_actions: FxHashMap<EventType, Actions>,
}
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<Action> {
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<Action> {
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<Action>)` - 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<Action> {
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());