refactor(bindings): merge key/event handling

This commit is contained in:
lalvarezt 2025-07-23 09:51:24 +02:00
parent fbd9920179
commit 78cc303003
3 changed files with 183 additions and 239 deletions

View File

@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
use std::fmt::Display; use std::fmt::Display;
use std::hash::Hash; use std::hash::Hash;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::str::FromStr;
// Legacy binding structure for backward compatibility with shell integration // Legacy binding structure for backward compatibility with shell integration
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Hash)] #[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)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Bindings<K>
where
K: Display + FromStr + Eq + Hash,
K::Err: Display,
{
#[serde(
flatten,
serialize_with = "serialize_bindings",
deserialize_with = "deserialize_bindings"
)]
pub bindings: FxHashMap<K, Actions>,
}
impl<K> Bindings<K>
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. /// A set of keybindings that maps keys directly to actions.
/// ///
/// This struct represents the new architecture where keybindings are structured as /// This struct represents the new architecture where keybindings are structured as
/// Key -> Action mappings in the configuration file. This eliminates the need for /// Key -> Action mappings in the configuration file. This eliminates the need for
/// runtime inversion and provides better discoverability. /// runtime inversion and provides better discoverability.
pub struct KeyBindings { pub type KeyBindings = Bindings<Key>;
#[serde(
flatten,
serialize_with = "serialize_key_bindings",
deserialize_with = "deserialize_key_bindings"
)]
pub bindings: FxHashMap<Key, Actions>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
@ -58,19 +78,27 @@ pub enum EventType {
Custom(String), Custom(String),
} }
impl FromStr for EventType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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 { impl<'de> serde::Deserialize<'de> for EventType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
{ {
let s = String::deserialize(deserializer)?; let s = String::deserialize(deserializer)?;
match s.as_str() { Ok(EventType::from_str(&s).unwrap())
"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())),
}
} }
} }
@ -86,23 +114,17 @@ impl Display for EventType {
} }
} }
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
/// A set of event bindings that maps events to actions. /// A set of event bindings that maps events to actions.
pub struct EventBindings { pub type EventBindings = Bindings<EventType>;
#[serde(
flatten,
serialize_with = "serialize_event_bindings",
deserialize_with = "deserialize_event_bindings"
)]
pub bindings: FxHashMap<EventType, Actions>,
}
impl<I> From<I> for KeyBindings impl<K, I> From<I> for Bindings<K>
where where
I: IntoIterator<Item = (Key, Action)>, K: Display + FromStr + Eq + Hash,
K::Err: Display,
I: IntoIterator<Item = (K, Action)>,
{ {
fn from(iter: I) -> Self { fn from(iter: I) -> Self {
KeyBindings { Bindings {
bindings: iter bindings: iter
.into_iter() .into_iter()
.map(|(k, a)| (k, Actions::from(a))) .map(|(k, a)| (k, Actions::from(a)))
@ -111,21 +133,11 @@ where
} }
} }
impl<I> From<I> for EventBindings impl<K> Hash for Bindings<K>
where where
I: IntoIterator<Item = (EventType, Action)>, 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<H: std::hash::Hasher>(&self, state: &mut H) { fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
// Hash based on the bindings map // Hash based on the bindings map
for (key, actions) in &self.bindings { for (key, actions) in &self.bindings {
@ -135,71 +147,43 @@ impl Hash for KeyBindings {
} }
} }
impl Hash for EventBindings { impl<K> Deref for Bindings<K>
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { where
// Hash based on the bindings map K: Display + FromStr + Eq + Hash,
for (event, actions) in &self.bindings { K::Err: Display,
event.hash(state); {
actions.hash(state); type Target = FxHashMap<K, Actions>;
}
}
}
impl Deref for KeyBindings {
type Target = FxHashMap<Key, Actions>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.bindings &self.bindings
} }
} }
impl DerefMut for KeyBindings { impl<K> DerefMut for Bindings<K>
where
K: Display + FromStr + Eq + Hash,
K::Err: Display,
{
fn deref_mut(&mut self) -> &mut Self::Target { fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.bindings &mut self.bindings
} }
} }
impl Deref for EventBindings { /// Generic merge function for bindings
type Target = FxHashMap<EventType, Actions>; pub fn merge_bindings<K>(
fn deref(&self) -> &Self::Target { mut bindings: Bindings<K>,
&self.bindings new_bindings: &Bindings<K>,
) -> Bindings<K>
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 { impl Default for Bindings<Key> {
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 {
fn default() -> Self { fn default() -> Self {
let mut bindings = FxHashMap::default(); let mut bindings = FxHashMap::default();
@ -252,11 +236,11 @@ impl Default for KeyBindings {
bindings.insert(Key::End, Action::GoToInputEnd.into()); bindings.insert(Key::End, Action::GoToInputEnd.into());
bindings.insert(Key::Ctrl('e'), Action::GoToInputEnd.into()); bindings.insert(Key::Ctrl('e'), Action::GoToInputEnd.into());
Self { bindings } Bindings { bindings }
} }
} }
impl Default for EventBindings { impl Default for Bindings<EventType> {
fn default() -> Self { fn default() -> Self {
let mut bindings = FxHashMap::default(); let mut bindings = FxHashMap::default();
@ -268,7 +252,7 @@ impl Default for EventBindings {
Action::ScrollPreviewDown.into(), Action::ScrollPreviewDown.into(),
); );
Self { bindings } Bindings { bindings }
} }
} }
@ -318,48 +302,62 @@ fn parse_key_code_with_modifiers(
raw: &str, raw: &str,
mut modifiers: KeyModifiers, mut modifiers: KeyModifiers,
) -> anyhow::Result<KeyEvent, String> { ) -> anyhow::Result<KeyEvent, String> {
let c = match raw { use rustc_hash::FxHashMap;
"esc" => KeyCode::Esc, use std::sync::LazyLock;
"enter" => KeyCode::Enter,
"left" => KeyCode::Left, static KEY_CODE_MAP: LazyLock<FxHashMap<&'static str, KeyCode>> =
"right" => KeyCode::Right, LazyLock::new(|| {
"up" => KeyCode::Up, [
"down" => KeyCode::Down, ("esc", KeyCode::Esc),
"home" => KeyCode::Home, ("enter", KeyCode::Enter),
"end" => KeyCode::End, ("left", KeyCode::Left),
"pageup" => KeyCode::PageUp, ("right", KeyCode::Right),
"pagedown" => KeyCode::PageDown, ("up", KeyCode::Up),
"backtab" => { ("down", KeyCode::Down),
modifiers.insert(KeyModifiers::SHIFT); ("home", KeyCode::Home),
KeyCode::BackTab ("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, KeyCode::Char(c)
"delete" => KeyCode::Delete, } else {
"insert" => KeyCode::Insert, return Err(format!("Unable to parse {raw}"));
"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}")),
}; };
Ok(KeyEvent::new(c, modifiers)) Ok(KeyEvent::new(c, modifiers))
} }
@ -437,29 +435,38 @@ pub fn key_event_to_string(key_event: &KeyEvent) -> String {
key key
} }
pub fn parse_key(raw: &str) -> anyhow::Result<Key, String> { impl FromStr for Key {
if raw.chars().filter(|c| *c == '>').count() type Err = String;
!= 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)?; fn from_str(raw: &str) -> Result<Self, Self::Err> {
Ok(convert_raw_event_to_key(key_event)) 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 pub fn parse_key(raw: &str) -> anyhow::Result<Key, String> {
fn serialize_key_bindings<S>( Key::from_str(raw)
bindings: &FxHashMap<Key, Actions>, }
/// Generic serializer that converts any key type to string for TOML compatibility
fn serialize_bindings<K, S>(
bindings: &FxHashMap<K, Actions>,
serializer: S, serializer: S,
) -> Result<S::Ok, S::Error> ) -> Result<S::Ok, S::Error>
where where
K: Display,
S: serde::Serializer, S: serde::Serializer,
{ {
use serde::ser::SerializeMap; use serde::ser::SerializeMap;
@ -470,20 +477,26 @@ where
map.end() map.end()
} }
/// Custom deserializer for `KeyBindings` that parses string keys back to `Key` enum /// Generic deserializer that parses string keys back to key enum
fn deserialize_key_bindings<'de, D>( fn deserialize_bindings<'de, K, D>(
deserializer: D, deserializer: D,
) -> Result<FxHashMap<Key, Actions>, D::Error> ) -> Result<FxHashMap<K, Actions>, D::Error>
where where
K: FromStr + Eq + std::hash::Hash,
K::Err: std::fmt::Display,
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
{ {
use serde::de::{MapAccess, Visitor}; use serde::de::{MapAccess, Visitor};
use std::fmt; use std::fmt;
struct KeyBindingsVisitor; struct BindingsVisitor<K>(std::marker::PhantomData<K>);
impl<'de> Visitor<'de> for KeyBindingsVisitor { impl<'de, K> Visitor<'de> for BindingsVisitor<K>
type Value = FxHashMap<Key, Actions>; where
K: FromStr + Eq + std::hash::Hash,
K::Err: std::fmt::Display,
{
type Value = FxHashMap<K, Actions>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter formatter
@ -501,11 +514,11 @@ where
while let Some((key_str, raw_value)) = while let Some((key_str, raw_value)) =
map.next_entry::<String, Value>()? map.next_entry::<String, Value>()?
{ {
let key = parse_key(&key_str).map_err(Error::custom)?; let key = K::from_str(&key_str).map_err(Error::custom)?;
match raw_value { match raw_value {
Value::Boolean(false) => { Value::Boolean(false) => {
// Explicitly unbind key // Explicitly unbind key/event
bindings.insert(key, Action::NoOp.into()); bindings.insert(key, Action::NoOp.into());
} }
Value::Boolean(true) => { Value::Boolean(true) => {
@ -523,75 +536,7 @@ where
} }
} }
deserializer.deserialize_map(KeyBindingsVisitor) deserializer.deserialize_map(BindingsVisitor(std::marker::PhantomData))
}
/// Custom serializer for `EventBindings` that converts `EventType` enum to string for TOML compatibility
fn serialize_event_bindings<S>(
bindings: &FxHashMap<EventType, Actions>,
serializer: S,
) -> Result<S::Ok, S::Error>
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<FxHashMap<EventType, Actions>, 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<EventType, Actions>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter
.write_str("a map with string keys and action/actions values")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
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::<String, Value>()?
{
// 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)
} }
#[cfg(test)] #[cfg(test)]
@ -766,7 +711,7 @@ mod tests {
(Key::PageDown, Action::SelectNextPage), (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 // Should contain both base and custom keybindings
assert!(merged.bindings.contains_key(&Key::Esc)); assert!(merged.bindings.contains_key(&Key::Esc));
@ -877,7 +822,7 @@ mod tests {
bindings: custom_bindings, 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 // Custom multiple actions should be present
assert_eq!( assert_eq!(

View File

@ -15,8 +15,7 @@ use std::{
use tracing::{debug, warn}; use tracing::{debug, warn};
pub use keybindings::{ pub use keybindings::{
Binding, EventBindings, EventType, KeyBindings, merge_event_bindings, Binding, EventBindings, EventType, KeyBindings, merge_bindings, parse_key,
merge_keybindings, parse_key,
}; };
pub use themes::Theme; pub use themes::Theme;
pub use ui::UiConfig; pub use ui::UiConfig;
@ -226,11 +225,11 @@ impl Config {
// merge keybindings with default keybindings // merge keybindings with default keybindings
let keybindings = let keybindings =
merge_keybindings(default.keybindings.clone(), &new.keybindings); merge_bindings(default.keybindings.clone(), &new.keybindings);
new.keybindings = keybindings; new.keybindings = keybindings;
// merge event bindings with default event bindings // 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; new.events = events;
Config { Config {
@ -243,11 +242,11 @@ impl Config {
} }
pub fn merge_keybindings(&mut self, other: &KeyBindings) { 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) { 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) { pub fn apply_prototype_ui_spec(&mut self, ui_spec: &UiSpec) {

View File

@ -18,7 +18,7 @@ use television::{
args::{Cli, Command}, args::{Cli, Command},
guess_channel_from_prompt, list_channels, guess_channel_from_prompt, list_channels,
}, },
config::{Config, ConfigEnv, merge_keybindings}, config::{Config, ConfigEnv, merge_bindings},
errors::os_error_exit, errors::os_error_exit,
features::FeatureFlags, features::FeatureFlags,
gh::update_local_channels, gh::update_local_channels,
@ -207,7 +207,7 @@ fn apply_cli_overrides(args: &PostProcessedCli, config: &mut Config) {
if let Some(keybindings) = &args.keybindings { if let Some(keybindings) = &args.keybindings {
config.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); config.ui.ui_scale = args.ui_scale.unwrap_or(config.ui.ui_scale);
if let Some(input_header) = &args.input_header { if let Some(input_header) = &args.input_header {