diff --git a/television/config/keybindings.rs b/television/config/keybindings.rs index 4e8d0ca..e24fe32 100644 --- a/television/config/keybindings.rs +++ b/television/config/keybindings.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::Display; use std::hash::Hash; use std::ops::{Deref, DerefMut}; +use std::str::FromStr; // Legacy binding structure for backward compatibility with shell integration #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash)] @@ -32,20 +33,39 @@ impl Display for Binding { } } +/// Generic bindings structure that can map any key type to actions #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Bindings +where + K: Display + FromStr + Eq + Hash, + K::Err: Display, +{ + #[serde( + flatten, + serialize_with = "serialize_bindings", + deserialize_with = "deserialize_bindings" + )] + pub bindings: FxHashMap, +} + +impl Bindings +where + K: Display + FromStr + Eq + Hash, + K::Err: Display, +{ + pub fn new() -> Self { + Bindings { + bindings: FxHashMap::default(), + } + } +} + /// A set of keybindings that maps 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. -pub struct KeyBindings { - #[serde( - flatten, - serialize_with = "serialize_key_bindings", - deserialize_with = "deserialize_key_bindings" - )] - pub bindings: FxHashMap, -} +pub type KeyBindings = Bindings; #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] #[serde(rename_all = "kebab-case")] @@ -58,19 +78,27 @@ pub enum EventType { Custom(String), } +impl FromStr for EventType { + type Err = String; + + fn from_str(s: &str) -> Result { + Ok(match s { + "mouse-click" => EventType::MouseClick, + "mouse-scroll-up" => EventType::MouseScrollUp, + "mouse-scroll-down" => EventType::MouseScrollDown, + "resize" => EventType::Resize, + custom => EventType::Custom(custom.to_string()), + }) + } +} + impl<'de> serde::Deserialize<'de> for EventType { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let s = String::deserialize(deserializer)?; - match s.as_str() { - "mouse-click" => Ok(EventType::MouseClick), - "mouse-scroll-up" => Ok(EventType::MouseScrollUp), - "mouse-scroll-down" => Ok(EventType::MouseScrollDown), - "resize" => Ok(EventType::Resize), - custom => Ok(EventType::Custom(custom.to_string())), - } + Ok(EventType::from_str(&s).unwrap()) } } @@ -86,23 +114,17 @@ impl Display for EventType { } } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] /// A set of event bindings that maps events to actions. -pub struct EventBindings { - #[serde( - flatten, - serialize_with = "serialize_event_bindings", - deserialize_with = "deserialize_event_bindings" - )] - pub bindings: FxHashMap, -} +pub type EventBindings = Bindings; -impl From for KeyBindings +impl From for Bindings where - I: IntoIterator, + K: Display + FromStr + Eq + Hash, + K::Err: Display, + I: IntoIterator, { fn from(iter: I) -> Self { - KeyBindings { + Bindings { bindings: iter .into_iter() .map(|(k, a)| (k, Actions::from(a))) @@ -111,21 +133,11 @@ where } } -impl From for EventBindings +impl Hash for Bindings where - I: IntoIterator, + K: Display + FromStr + Eq + Hash, + K::Err: Display, { - fn from(iter: I) -> Self { - EventBindings { - bindings: iter - .into_iter() - .map(|(e, a)| (e, Actions::from(a))) - .collect(), - } - } -} - -impl Hash for KeyBindings { fn hash(&self, state: &mut H) { // Hash based on the bindings map for (key, actions) in &self.bindings { @@ -135,71 +147,43 @@ impl Hash for KeyBindings { } } -impl Hash for EventBindings { - fn hash(&self, state: &mut H) { - // Hash based on the bindings map - for (event, actions) in &self.bindings { - event.hash(state); - actions.hash(state); - } - } -} - -impl Deref for KeyBindings { - type Target = FxHashMap; +impl Deref for Bindings +where + K: Display + FromStr + Eq + Hash, + K::Err: Display, +{ + type Target = FxHashMap; fn deref(&self) -> &Self::Target { &self.bindings } } -impl DerefMut for KeyBindings { +impl DerefMut for Bindings +where + K: Display + FromStr + Eq + Hash, + K::Err: Display, +{ fn deref_mut(&mut self) -> &mut Self::Target { &mut self.bindings } } -impl Deref for EventBindings { - type Target = FxHashMap; - fn deref(&self) -> &Self::Target { - &self.bindings +/// Generic merge function for bindings +pub fn merge_bindings( + mut bindings: Bindings, + new_bindings: &Bindings, +) -> Bindings +where + K: Display + FromStr + Clone + Eq + Hash, + K::Err: Display, +{ + for (key, actions) in &new_bindings.bindings { + bindings.bindings.insert(key.clone(), actions.clone()); } + bindings } -impl DerefMut for EventBindings { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.bindings - } -} - -/// Merge two sets of keybindings together. -/// -/// The new keybindings will overwrite any existing ones for the same keys. -pub fn merge_keybindings( - mut keybindings: KeyBindings, - new_keybindings: &KeyBindings, -) -> KeyBindings { - for (key, actions) in &new_keybindings.bindings { - keybindings.bindings.insert(*key, actions.clone()); - } - keybindings -} - -/// Merge two sets of event bindings together. -/// -/// The new event bindings will overwrite any existing ones for the same event types. -pub fn merge_event_bindings( - mut event_bindings: EventBindings, - new_event_bindings: &EventBindings, -) -> EventBindings { - for (event_type, actions) in &new_event_bindings.bindings { - event_bindings - .bindings - .insert(event_type.clone(), actions.clone()); - } - event_bindings -} - -impl Default for KeyBindings { +impl Default for Bindings { fn default() -> Self { let mut bindings = FxHashMap::default(); @@ -252,11 +236,11 @@ impl Default for KeyBindings { bindings.insert(Key::End, Action::GoToInputEnd.into()); bindings.insert(Key::Ctrl('e'), Action::GoToInputEnd.into()); - Self { bindings } + Bindings { bindings } } } -impl Default for EventBindings { +impl Default for Bindings { fn default() -> Self { let mut bindings = FxHashMap::default(); @@ -268,7 +252,7 @@ impl Default for EventBindings { Action::ScrollPreviewDown.into(), ); - Self { bindings } + Bindings { bindings } } } @@ -318,48 +302,62 @@ fn parse_key_code_with_modifiers( raw: &str, mut modifiers: KeyModifiers, ) -> anyhow::Result { - let c = match raw { - "esc" => KeyCode::Esc, - "enter" => KeyCode::Enter, - "left" => KeyCode::Left, - "right" => KeyCode::Right, - "up" => KeyCode::Up, - "down" => KeyCode::Down, - "home" => KeyCode::Home, - "end" => KeyCode::End, - "pageup" => KeyCode::PageUp, - "pagedown" => KeyCode::PageDown, - "backtab" => { - modifiers.insert(KeyModifiers::SHIFT); - KeyCode::BackTab + use rustc_hash::FxHashMap; + use std::sync::LazyLock; + + static KEY_CODE_MAP: LazyLock> = + LazyLock::new(|| { + [ + ("esc", KeyCode::Esc), + ("enter", KeyCode::Enter), + ("left", KeyCode::Left), + ("right", KeyCode::Right), + ("up", KeyCode::Up), + ("down", KeyCode::Down), + ("home", KeyCode::Home), + ("end", KeyCode::End), + ("pageup", KeyCode::PageUp), + ("pagedown", KeyCode::PageDown), + ("backspace", KeyCode::Backspace), + ("delete", KeyCode::Delete), + ("insert", KeyCode::Insert), + ("f1", KeyCode::F(1)), + ("f2", KeyCode::F(2)), + ("f3", KeyCode::F(3)), + ("f4", KeyCode::F(4)), + ("f5", KeyCode::F(5)), + ("f6", KeyCode::F(6)), + ("f7", KeyCode::F(7)), + ("f8", KeyCode::F(8)), + ("f9", KeyCode::F(9)), + ("f10", KeyCode::F(10)), + ("f11", KeyCode::F(11)), + ("f12", KeyCode::F(12)), + ("space", KeyCode::Char(' ')), + (" ", KeyCode::Char(' ')), + ("hyphen", KeyCode::Char('-')), + ("minus", KeyCode::Char('-')), + ("tab", KeyCode::Tab), + ] + .into_iter() + .collect() + }); + + let c = if let Some(&key_code) = KEY_CODE_MAP.get(raw) { + key_code + } else if raw == "backtab" { + modifiers.insert(KeyModifiers::SHIFT); + KeyCode::BackTab + } else if raw.len() == 1 { + let mut c = raw.chars().next().unwrap(); + if modifiers.contains(KeyModifiers::SHIFT) { + c = c.to_ascii_uppercase(); } - "backspace" => KeyCode::Backspace, - "delete" => KeyCode::Delete, - "insert" => KeyCode::Insert, - "f1" => KeyCode::F(1), - "f2" => KeyCode::F(2), - "f3" => KeyCode::F(3), - "f4" => KeyCode::F(4), - "f5" => KeyCode::F(5), - "f6" => KeyCode::F(6), - "f7" => KeyCode::F(7), - "f8" => KeyCode::F(8), - "f9" => KeyCode::F(9), - "f10" => KeyCode::F(10), - "f11" => KeyCode::F(11), - "f12" => KeyCode::F(12), - "space" | " " => KeyCode::Char(' '), - "hyphen" | "minus" => KeyCode::Char('-'), - "tab" => KeyCode::Tab, - c if c.len() == 1 => { - let mut c = c.chars().next().unwrap(); - if modifiers.contains(KeyModifiers::SHIFT) { - c = c.to_ascii_uppercase(); - } - KeyCode::Char(c) - } - _ => return Err(format!("Unable to parse {raw}")), + KeyCode::Char(c) + } else { + return Err(format!("Unable to parse {raw}")); }; + Ok(KeyEvent::new(c, modifiers)) } @@ -437,29 +435,38 @@ pub fn key_event_to_string(key_event: &KeyEvent) -> String { key } -pub fn parse_key(raw: &str) -> anyhow::Result { - if raw.chars().filter(|c| *c == '>').count() - != raw.chars().filter(|c| *c == '<').count() - { - return Err(format!("Unable to parse `{raw}`")); - } - let raw = if raw.contains("><") { - raw - } else { - let raw = raw.strip_prefix('<').unwrap_or(raw); - raw.strip_suffix('>').unwrap_or(raw) - }; +impl FromStr for Key { + type Err = String; - let key_event = parse_key_event(raw)?; - Ok(convert_raw_event_to_key(key_event)) + fn from_str(raw: &str) -> Result { + if raw.chars().filter(|c| *c == '>').count() + != raw.chars().filter(|c| *c == '<').count() + { + return Err(format!("Unable to parse `{raw}`")); + } + let raw = if raw.contains("><") { + raw + } else { + let raw = raw.strip_prefix('<').unwrap_or(raw); + raw.strip_suffix('>').unwrap_or(raw) + }; + + let key_event = parse_key_event(raw)?; + Ok(convert_raw_event_to_key(key_event)) + } } -/// Custom serializer for `KeyBindings` that converts `Key` enum to string for TOML compatibility -fn serialize_key_bindings( - bindings: &FxHashMap, +pub fn parse_key(raw: &str) -> anyhow::Result { + Key::from_str(raw) +} + +/// Generic serializer that converts any key type to string for TOML compatibility +fn serialize_bindings( + bindings: &FxHashMap, serializer: S, ) -> Result where + K: Display, S: serde::Serializer, { use serde::ser::SerializeMap; @@ -470,20 +477,26 @@ where map.end() } -/// Custom deserializer for `KeyBindings` that parses string keys back to `Key` enum -fn deserialize_key_bindings<'de, D>( +/// Generic deserializer that parses string keys back to key enum +fn deserialize_bindings<'de, K, D>( deserializer: D, -) -> Result, D::Error> +) -> Result, D::Error> where + K: FromStr + Eq + std::hash::Hash, + K::Err: std::fmt::Display, D: serde::Deserializer<'de>, { use serde::de::{MapAccess, Visitor}; use std::fmt; - struct KeyBindingsVisitor; + struct BindingsVisitor(std::marker::PhantomData); - impl<'de> Visitor<'de> for KeyBindingsVisitor { - type Value = FxHashMap; + impl<'de, K> Visitor<'de> for BindingsVisitor + where + K: FromStr + Eq + std::hash::Hash, + K::Err: std::fmt::Display, + { + type Value = FxHashMap; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter @@ -501,11 +514,11 @@ where while let Some((key_str, raw_value)) = map.next_entry::()? { - let key = parse_key(&key_str).map_err(Error::custom)?; + let key = K::from_str(&key_str).map_err(Error::custom)?; match raw_value { Value::Boolean(false) => { - // Explicitly unbind key + // Explicitly unbind key/event bindings.insert(key, Action::NoOp.into()); } Value::Boolean(true) => { @@ -523,75 +536,7 @@ where } } - deserializer.deserialize_map(KeyBindingsVisitor) -} - -/// Custom serializer for `EventBindings` that converts `EventType` enum to string for TOML compatibility -fn serialize_event_bindings( - bindings: &FxHashMap, - serializer: S, -) -> Result -where - S: serde::Serializer, -{ - use serde::ser::SerializeMap; - let mut map = serializer.serialize_map(Some(bindings.len()))?; - for (event_type, actions) in bindings { - map.serialize_entry(&event_type.to_string(), actions)?; - } - map.end() -} - -/// Custom deserializer for `EventBindings` that parses string keys back to `EventType` enum -fn deserialize_event_bindings<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - use serde::de::{MapAccess, Visitor}; - use std::fmt; - - struct EventBindingsVisitor; - - impl<'de> Visitor<'de> for EventBindingsVisitor { - type Value = FxHashMap; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter - .write_str("a map with string keys and action/actions values") - } - - fn visit_map(self, mut map: A) -> Result - where - A: MapAccess<'de>, - { - use serde::de::Error; - use toml::Value; - - let mut bindings = FxHashMap::default(); - while let Some((event_str, raw_value)) = - map.next_entry::()? - { - // Parse the event string back to EventType - let event_type = match event_str.as_str() { - "mouse-click" => EventType::MouseClick, - "mouse-scroll-up" => EventType::MouseScrollUp, - "mouse-scroll-down" => EventType::MouseScrollDown, - "resize" => EventType::Resize, - custom => EventType::Custom(custom.to_string()), - }; - - // Try to deserialize as Actions (handles both single and multiple) - let actions = - Actions::deserialize(raw_value).map_err(Error::custom)?; - bindings.insert(event_type, actions); - } - Ok(bindings) - } - } - - deserializer.deserialize_map(EventBindingsVisitor) + deserializer.deserialize_map(BindingsVisitor(std::marker::PhantomData)) } #[cfg(test)] @@ -766,7 +711,7 @@ mod tests { (Key::PageDown, Action::SelectNextPage), ]); - let merged = merge_keybindings(base_keybindings, &custom_keybindings); + let merged = merge_bindings(base_keybindings, &custom_keybindings); // Should contain both base and custom keybindings assert!(merged.bindings.contains_key(&Key::Esc)); @@ -877,7 +822,7 @@ mod tests { bindings: custom_bindings, }; - let merged = merge_keybindings(base_keybindings, &custom_keybindings); + let merged = merge_bindings(base_keybindings, &custom_keybindings); // Custom multiple actions should be present assert_eq!( diff --git a/television/config/mod.rs b/television/config/mod.rs index a041b73..4767574 100644 --- a/television/config/mod.rs +++ b/television/config/mod.rs @@ -15,8 +15,7 @@ use std::{ use tracing::{debug, warn}; pub use keybindings::{ - Binding, EventBindings, EventType, KeyBindings, merge_event_bindings, - merge_keybindings, parse_key, + Binding, EventBindings, EventType, KeyBindings, merge_bindings, parse_key, }; pub use themes::Theme; pub use ui::UiConfig; @@ -226,11 +225,11 @@ impl Config { // merge keybindings with default keybindings let keybindings = - merge_keybindings(default.keybindings.clone(), &new.keybindings); + merge_bindings(default.keybindings.clone(), &new.keybindings); new.keybindings = keybindings; // merge event bindings with default event bindings - let events = merge_event_bindings(default.events.clone(), &new.events); + let events = merge_bindings(default.events.clone(), &new.events); new.events = events; Config { @@ -243,11 +242,11 @@ impl Config { } pub fn merge_keybindings(&mut self, other: &KeyBindings) { - self.keybindings = merge_keybindings(self.keybindings.clone(), other); + self.keybindings = merge_bindings(self.keybindings.clone(), other); } pub fn merge_event_bindings(&mut self, other: &EventBindings) { - self.events = merge_event_bindings(self.events.clone(), other); + self.events = merge_bindings(self.events.clone(), other); } pub fn apply_prototype_ui_spec(&mut self, ui_spec: &UiSpec) { diff --git a/television/main.rs b/television/main.rs index 084107b..81ca8f1 100644 --- a/television/main.rs +++ b/television/main.rs @@ -18,7 +18,7 @@ use television::{ args::{Cli, Command}, guess_channel_from_prompt, list_channels, }, - config::{Config, ConfigEnv, merge_keybindings}, + config::{Config, ConfigEnv, merge_bindings}, errors::os_error_exit, features::FeatureFlags, gh::update_local_channels, @@ -207,7 +207,7 @@ fn apply_cli_overrides(args: &PostProcessedCli, config: &mut Config) { if let Some(keybindings) = &args.keybindings { config.keybindings = - merge_keybindings(config.keybindings.clone(), keybindings); + merge_bindings(config.keybindings.clone(), keybindings); } config.ui.ui_scale = args.ui_scale.unwrap_or(config.ui.ui_scale); if let Some(input_header) = &args.input_header {