diff --git a/television/action.rs b/television/action.rs index bcaede6..8ce4d2a 100644 --- a/television/action.rs +++ b/television/action.rs @@ -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), +#[serde(transparent)] +pub struct Actions { + #[serde_as(as = "OneOrMany<_>")] + inner: Vec, } 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) -> Self { + Self { inner: actions } + } + /// Converts the `Actions` into a `Vec` 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 { - 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 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 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> for Actions { /// Converts a `Vec` 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) -> 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); diff --git a/television/config/keybindings.rs b/television/config/keybindings.rs index 95a63be..2252973 100644 --- a/television/config/keybindings.rs +++ b/television/config/keybindings.rs @@ -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])) ); } } diff --git a/television/keymap.rs b/television/keymap.rs index 5d4dbe7..44e3854 100644 --- a/television/keymap.rs +++ b/television/keymap.rs @@ -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 { - 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 { 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(); diff --git a/television/screen/keybindings.rs b/television/screen/keybindings.rs index 01f31f0..9831609 100644 --- a/television/screen/keybindings.rs +++ b/television/screen/keybindings.rs @@ -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 => { - Some(key.to_string()) - } - Actions::Multiple(action_list) - if action_list.contains(target_action) => - { - Some(key.to_string()) - } - _ => None, + if actions.as_slice().contains(target_action) { + Some(key.to_string()) + } else { + None } }) .collect()