refactor(bindigns): migrate to OneOrMany

This commit is contained in:
lalvarezt 2025-07-23 17:12:40 +02:00
parent 3cca8ad9bc
commit 088dc6ba83
4 changed files with 97 additions and 122 deletions

View File

@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize};
use serde_with::{OneOrMany, serde_as};
use std::fmt::Display;
/// The different actions that can be performed by the application.
@ -145,34 +146,44 @@ pub enum Action {
/// use television::action::{Action, Actions};
///
/// // Single action
/// let single = Actions::Single(Action::Quit);
/// let single = Actions::single(Action::Quit);
/// assert_eq!(single.as_slice(), &[Action::Quit]);
///
/// // Multiple actions
/// let multiple = Actions::Multiple(vec![Action::ReloadSource, Action::Quit]);
/// 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]);
/// ```
#[serde_as]
#[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>),
#[serde(transparent)]
pub struct Actions {
#[serde_as(as = "OneOrMany<_>")]
inner: Vec<Action>,
}
impl Actions {
/// Creates a new `Actions` from a single action.
pub fn single(action: Action) -> Self {
Self {
inner: vec![action],
}
}
/// Creates a new `Actions` from multiple actions.
pub fn multiple(actions: Vec<Action>) -> Self {
Self { inner: 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.
/// all actions to be executed.
///
/// # Returns
///
@ -183,17 +194,14 @@ impl Actions {
/// ```rust
/// use television::action::{Action, Actions};
///
/// let single = Actions::Single(Action::Quit);
/// let single = Actions::single(Action::Quit);
/// assert_eq!(single.into_vec(), vec![Action::Quit]);
///
/// let multiple = Actions::Multiple(vec![Action::ReloadSource, 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],
Actions::Multiple(actions) => actions,
}
self.inner
}
/// Returns a slice view of the actions without consuming the `Actions`.
@ -210,22 +218,34 @@ impl Actions {
/// ```rust
/// use television::action::{Action, Actions};
///
/// let single = Actions::Single(Action::Quit);
/// let single = Actions::single(Action::Quit);
/// assert_eq!(single.as_slice(), &[Action::Quit]);
///
/// let multiple = Actions::Multiple(vec![Action::ReloadSource, 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),
Actions::Multiple(actions) => actions.as_slice(),
&self.inner
}
/// Returns `true` if this contains only a single action.
pub fn is_single(&self) -> bool {
self.inner.len() == 1
}
/// Returns `true` if this contains multiple actions.
pub fn is_multiple(&self) -> bool {
self.inner.len() > 1
}
/// Gets the first action, if any.
pub fn first(&self) -> Option<&Action> {
self.inner.first()
}
}
impl From<Action> for Actions {
/// Converts a single `Action` into `Actions::Single`.
/// Converts a single `Action` into `Actions`.
///
/// This conversion allows seamless use of single actions where
/// `Actions` is expected, maintaining backward compatibility.
@ -236,49 +256,35 @@ impl From<Action> for Actions {
/// use television::action::{Action, Actions};
///
/// let actions: Actions = Action::Quit.into();
/// assert_eq!(actions, Actions::Single(Action::Quit));
/// assert_eq!(actions, Actions::single(Action::Quit));
/// ```
fn from(action: Action) -> Self {
Actions::Single(action)
Self::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));
/// assert_eq!(actions, Action::Quit.into());
///
/// // Multiple elements become Multiple
/// let multi_vec = vec![Action::ReloadSource, Action::Quit];
/// let actions: Actions = multi_vec.into();
/// assert!(matches!(actions, Actions::Multiple(_)));
/// assert_eq!(actions, vec![Action::ReloadSource, Action::Quit].into());
/// ```
fn from(actions: Vec<Action>) -> Self {
if actions.len() == 1 {
Actions::Single(actions.into_iter().next().unwrap())
} else {
Actions::Multiple(actions)
}
Self::multiple(actions)
}
}
@ -367,13 +373,13 @@ mod tests {
#[test]
fn test_actions_single() {
let single_action = Actions::Single(Action::Quit);
let single_action = Actions::single(Action::Quit);
assert_eq!(single_action.into_vec(), vec![Action::Quit]);
let single_from_action = Actions::from(Action::SelectNextEntry);
assert_eq!(
single_from_action,
Actions::Single(Action::SelectNextEntry)
Actions::single(Action::SelectNextEntry)
);
assert_eq!(single_from_action.as_slice(), &[Action::SelectNextEntry]);
}
@ -381,40 +387,20 @@ mod tests {
#[test]
fn test_actions_multiple() {
let actions_vec = vec![Action::CopyEntryToClipboard, Action::Quit];
let multiple_actions = Actions::Multiple(actions_vec.clone());
let multiple_actions: Actions = Actions::multiple(actions_vec.clone());
assert_eq!(multiple_actions.into_vec(), actions_vec);
let multiple_from_vec = Actions::from(actions_vec.clone());
assert_eq!(multiple_from_vec, Actions::Multiple(actions_vec.clone()));
assert_eq!(multiple_from_vec, Actions::multiple(actions_vec.clone()));
assert_eq!(multiple_from_vec.as_slice(), actions_vec.as_slice());
}
#[test]
fn test_actions_from_single_vec_becomes_single() {
// When creating Actions from a Vec with only one element, it should become Single
let single_vec = vec![Action::TogglePreview];
let actions = Actions::from(single_vec);
assert_eq!(actions, Actions::Single(Action::TogglePreview));
}
#[test]
fn test_actions_from_multi_vec_becomes_multiple() {
// When creating Actions from a Vec with multiple elements, it should become Multiple
let multi_vec = vec![
Action::ReloadSource,
Action::SelectNextEntry,
Action::TogglePreview,
];
let actions = Actions::from(multi_vec.clone());
assert_eq!(actions, Actions::Multiple(multi_vec));
}
#[test]
fn test_actions_as_slice() {
let single = Actions::Single(Action::DeleteLine);
let single: Actions = Actions::single(Action::DeleteLine);
assert_eq!(single.as_slice(), &[Action::DeleteLine]);
let multiple = Actions::Multiple(vec![
let multiple: Actions = Actions::multiple(vec![
Action::ScrollPreviewUp,
Action::ScrollPreviewDown,
]);
@ -426,10 +412,10 @@ mod tests {
#[test]
fn test_actions_into_vec() {
let single = Actions::Single(Action::ConfirmSelection);
let single: Actions = Actions::single(Action::ConfirmSelection);
assert_eq!(single.into_vec(), vec![Action::ConfirmSelection]);
let multiple = Actions::Multiple(vec![
let multiple: Actions = Actions::multiple(vec![
Action::ToggleHelp,
Action::ToggleStatusBar,
]);
@ -443,12 +429,12 @@ mod tests {
fn test_actions_hash_and_eq() {
use std::collections::HashMap;
let actions1 = Actions::Single(Action::Quit);
let actions2 = Actions::Single(Action::Quit);
let actions3 =
Actions::Multiple(vec![Action::Quit, Action::ClearScreen]);
let actions4 =
Actions::Multiple(vec![Action::Quit, Action::ClearScreen]);
let actions1: Actions = Actions::single(Action::Quit);
let actions2: Actions = Actions::single(Action::Quit);
let actions3: Actions =
Actions::multiple(vec![Action::Quit, Action::ClearScreen]);
let actions4: Actions =
Actions::multiple(vec![Action::Quit, Action::ClearScreen]);
assert_eq!(actions1, actions2);
assert_eq!(actions3, actions4);

View File

@ -11,7 +11,6 @@ use std::ops::{Deref, DerefMut};
use std::str::FromStr;
use tracing::{debug, trace};
/// 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.
@ -1087,7 +1086,7 @@ mod tests {
// Multiple actions should work
assert_eq!(
keybindings.bindings.get(&Key::Ctrl('s')),
Some(&Actions::Multiple(vec![
Some(&Actions::multiple(vec![
Action::ReloadSource,
Action::CopyEntryToClipboard
]))
@ -1096,7 +1095,7 @@ mod tests {
// Three actions should work
assert_eq!(
keybindings.bindings.get(&Key::F(1)),
Some(&Actions::Multiple(vec![
Some(&Actions::multiple(vec![
Action::ToggleHelp,
Action::TogglePreview,
Action::ToggleStatusBar
@ -1114,7 +1113,7 @@ mod tests {
let mut custom_bindings = FxHashMap::default();
custom_bindings.insert(
Key::Ctrl('s'),
Actions::Multiple(vec![
Actions::multiple(vec![
Action::ReloadSource,
Action::CopyEntryToClipboard,
]),
@ -1129,7 +1128,7 @@ mod tests {
// Custom multiple actions should be present
assert_eq!(
merged.bindings.get(&Key::Ctrl('s')),
Some(&Actions::Multiple(vec![
Some(&Actions::multiple(vec![
Action::ReloadSource,
Action::CopyEntryToClipboard
]))
@ -1171,7 +1170,7 @@ mod tests {
// Verify all binding types work correctly
assert_eq!(
keybindings.bindings.get(&Key::Esc),
Some(&Actions::Single(Action::Quit))
Some(&Actions::single(Action::Quit))
);
assert_eq!(
keybindings.bindings.get(&Key::Enter),
@ -1179,14 +1178,14 @@ mod tests {
);
assert_eq!(
keybindings.bindings.get(&Key::Ctrl('s')),
Some(&Actions::Multiple(vec![
Some(&Actions::multiple(vec![
Action::ReloadSource,
Action::CopyEntryToClipboard
]))
);
assert_eq!(
keybindings.bindings.get(&Key::F(1)),
Some(&Actions::Multiple(vec![
Some(&Actions::multiple(vec![
Action::ToggleHelp,
Action::TogglePreview,
Action::ToggleStatusBar
@ -1194,11 +1193,11 @@ mod tests {
);
assert_eq!(
keybindings.bindings.get(&Key::Ctrl('c')),
Some(&Actions::Single(Action::NoOp))
Some(&Actions::single(Action::NoOp))
);
assert_eq!(
keybindings.bindings.get(&Key::Tab),
Some(&Actions::Multiple(vec![Action::ToggleSelectionDown]))
Some(&Actions::multiple(vec![Action::ToggleSelectionDown]))
);
}
}

View File

@ -95,7 +95,7 @@ impl InputMap {
/// use television::action::{Action, Actions};
///
/// let mut input_map = InputMap::new();
/// input_map.key_actions.insert(Key::Enter, Actions::Single(Action::ConfirmSelection));
/// 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]);
@ -128,7 +128,7 @@ impl InputMap {
/// let mut input_map = InputMap::new();
/// input_map.event_actions.insert(
/// EventType::MouseClick,
/// Actions::Single(Action::ConfirmSelection)
/// Actions::single(Action::ConfirmSelection)
/// );
///
/// let actions = input_map.get_actions_for_event(&EventType::MouseClick).unwrap();
@ -166,17 +166,16 @@ impl InputMap {
/// let mut input_map = InputMap::new();
/// input_map.key_actions.insert(
/// Key::Ctrl('r'),
/// Actions::Multiple(vec![Action::ReloadSource, Action::ClearScreen])
/// 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()),
Actions::Multiple(actions_vec) => actions_vec.first().cloned(),
})
self.key_actions
.get(key)
.and_then(|actions| actions.first().cloned())
}
/// Gets the first action bound to a specific event type (backward compatibility).
@ -204,7 +203,7 @@ impl InputMap {
/// let mut input_map = InputMap::new();
/// input_map.event_actions.insert(
/// EventType::MouseClick,
/// Actions::Multiple(vec![Action::ConfirmSelection, Action::TogglePreview])
/// Actions::multiple(vec![Action::ConfirmSelection, Action::TogglePreview])
/// );
///
/// // Returns only the first action
@ -216,10 +215,7 @@ impl InputMap {
pub fn get_action_for_event(&self, event: &EventType) -> Option<Action> {
self.event_actions
.get(event)
.and_then(|actions| match actions {
Actions::Single(action) => Some(action.clone()),
Actions::Multiple(actions_vec) => actions_vec.first().cloned(),
})
.and_then(|actions| actions.first().cloned())
}
/// Gets all actions for any input event.
@ -256,7 +252,7 @@ impl InputMap {
/// let mut input_map = InputMap::new();
/// input_map.key_actions.insert(
/// Key::Ctrl('s'),
/// Actions::Multiple(vec![Action::ReloadSource, Action::CopyEntryToClipboard])
/// Actions::multiple(vec![Action::ReloadSource, Action::CopyEntryToClipboard])
/// );
///
/// let key_input = InputEvent::Key(Key::Ctrl('s'));
@ -317,7 +313,7 @@ impl InputMap {
/// let mut input_map = InputMap::new();
/// input_map.key_actions.insert(
/// Key::Enter,
/// Actions::Multiple(vec![Action::ConfirmSelection, Action::Quit])
/// Actions::multiple(vec![Action::ConfirmSelection, Action::Quit])
/// );
///
/// let key_input = InputEvent::Key(Key::Enter);
@ -504,11 +500,11 @@ impl InputMap {
/// use television::action::{Action, Actions};
///
/// let mut base_map = InputMap::new();
/// base_map.key_actions.insert(Key::Enter, Actions::Single(Action::ConfirmSelection));
/// 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
/// 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));
@ -712,12 +708,12 @@ mod tests {
let mut key_actions = FxHashMap::default();
key_actions.insert(
Key::Ctrl('s'),
Actions::Multiple(vec![
Actions::multiple(vec![
Action::ReloadSource,
Action::CopyEntryToClipboard,
]),
);
key_actions.insert(Key::Esc, Actions::Single(Action::Quit));
key_actions.insert(Key::Esc, Actions::single(Action::Quit));
let input_map = InputMap {
key_actions,
@ -750,7 +746,7 @@ mod tests {
let mut input_map = InputMap::new();
input_map.key_actions.insert(
Key::Char('j'),
Actions::Multiple(vec![
Actions::multiple(vec![
Action::SelectNextEntry,
Action::ScrollPreviewDown,
]),
@ -775,7 +771,7 @@ mod tests {
let mut event_actions = FxHashMap::default();
event_actions.insert(
EventType::MouseClick,
Actions::Multiple(vec![
Actions::multiple(vec![
Action::ConfirmSelection,
Action::TogglePreview,
]),
@ -809,16 +805,16 @@ mod tests {
let mut input_map1 = InputMap::new();
input_map1
.key_actions
.insert(Key::Char('a'), Actions::Single(Action::SelectNextEntry));
.insert(Key::Char('a'), Actions::single(Action::SelectNextEntry));
let mut input_map2 = InputMap::new();
input_map2.key_actions.insert(
Key::Char('a'),
Actions::Multiple(vec![Action::ReloadSource, Action::Quit]), // This should overwrite
Actions::multiple(vec![Action::ReloadSource, Action::Quit]), // This should overwrite
);
input_map2.key_actions.insert(
Key::Char('b'),
Actions::Multiple(vec![Action::TogglePreview, Action::ToggleHelp]),
Actions::multiple(vec![Action::TogglePreview, Action::ToggleHelp]),
);
input_map1.merge(&input_map2);
@ -845,9 +841,9 @@ mod tests {
let mut bindings = FxHashMap::default();
bindings.insert(
Key::Ctrl('r'),
Actions::Multiple(vec![Action::ReloadSource, Action::ClearScreen]),
Actions::multiple(vec![Action::ReloadSource, Action::ClearScreen]),
);
bindings.insert(Key::Esc, Actions::Single(Action::Quit));
bindings.insert(Key::Esc, Actions::single(Action::Quit));
let keybindings = KeyBindings { bindings };
let input_map: InputMap = (&keybindings).into();

View File

@ -179,16 +179,10 @@ pub fn find_keys_for_single_action(
.iter()
.filter_map(|(key, actions)| {
// Check if this actions contains the target action
match actions {
Actions::Single(action) if action == target_action => {
if actions.as_slice().contains(target_action) {
Some(key.to_string())
}
Actions::Multiple(action_list)
if action_list.contains(target_action) =>
{
Some(key.to_string())
}
_ => None,
} else {
None
}
})
.collect()