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::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<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.
///
/// 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<Key, Actions>,
}
pub type KeyBindings = Bindings<Key>;
#[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<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 {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<EventType, Actions>,
}
pub type EventBindings = Bindings<EventType>;
impl<I> From<I> for KeyBindings
impl<K, I> From<I> for Bindings<K>
where
I: IntoIterator<Item = (Key, Action)>,
K: Display + FromStr + Eq + Hash,
K::Err: Display,
I: IntoIterator<Item = (K, Action)>,
{
fn from(iter: I) -> Self {
KeyBindings {
Bindings {
bindings: iter
.into_iter()
.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
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) {
// 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<H: std::hash::Hasher>(&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<Key, Actions>;
impl<K> Deref for Bindings<K>
where
K: Display + FromStr + Eq + Hash,
K::Err: Display,
{
type Target = FxHashMap<K, Actions>;
fn deref(&self) -> &Self::Target {
&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 {
&mut self.bindings
}
}
impl Deref for EventBindings {
type Target = FxHashMap<EventType, Actions>;
fn deref(&self) -> &Self::Target {
&self.bindings
/// Generic merge function for bindings
pub fn merge_bindings<K>(
mut bindings: Bindings<K>,
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 {
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<Key> {
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<EventType> {
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<KeyEvent, String> {
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" => {
use rustc_hash::FxHashMap;
use std::sync::LazyLock;
static KEY_CODE_MAP: LazyLock<FxHashMap<&'static str, KeyCode>> =
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
}
"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();
} else if raw.len() == 1 {
let mut c = raw.chars().next().unwrap();
if modifiers.contains(KeyModifiers::SHIFT) {
c = c.to_ascii_uppercase();
}
KeyCode::Char(c)
}
_ => return Err(format!("Unable to parse {raw}")),
} else {
return Err(format!("Unable to parse {raw}"));
};
Ok(KeyEvent::new(c, modifiers))
}
@ -437,7 +435,10 @@ pub fn key_event_to_string(key_event: &KeyEvent) -> String {
key
}
pub fn parse_key(raw: &str) -> anyhow::Result<Key, String> {
impl FromStr for Key {
type Err = String;
fn from_str(raw: &str) -> Result<Self, Self::Err> {
if raw.chars().filter(|c| *c == '>').count()
!= raw.chars().filter(|c| *c == '<').count()
{
@ -453,13 +454,19 @@ pub fn parse_key(raw: &str) -> anyhow::Result<Key, String> {
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<S>(
bindings: &FxHashMap<Key, Actions>,
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
fn serialize_bindings<K, S>(
bindings: &FxHashMap<K, Actions>,
serializer: S,
) -> Result<S::Ok, S::Error>
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<FxHashMap<Key, Actions>, D::Error>
) -> Result<FxHashMap<K, Actions>, 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<K>(std::marker::PhantomData<K>);
impl<'de> Visitor<'de> for KeyBindingsVisitor {
type Value = FxHashMap<Key, Actions>;
impl<'de, K> Visitor<'de> for BindingsVisitor<K>
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 {
formatter
@ -501,11 +514,11 @@ where
while let Some((key_str, raw_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 {
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<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)
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!(

View File

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

View File

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