mirror of
https://github.com/alexpasmantier/television.git
synced 2025-07-29 14:21:43 +00:00
feat(bindings): add support for running multiple actions per key/event
This commit is contained in:
parent
f305f4a55e
commit
fbd9920179
@ -117,6 +117,47 @@ pub enum Action {
|
|||||||
MouseClickAt(u16, u16),
|
MouseClickAt(u16, u16),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, PartialOrd, Ord,
|
||||||
|
)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum Actions {
|
||||||
|
Single(Action),
|
||||||
|
Multiple(Vec<Action>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Actions {
|
||||||
|
pub fn into_vec(self) -> Vec<Action> {
|
||||||
|
match self {
|
||||||
|
Actions::Single(action) => vec![action],
|
||||||
|
Actions::Multiple(actions) => actions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_slice(&self) -> &[Action] {
|
||||||
|
match self {
|
||||||
|
Actions::Single(action) => std::slice::from_ref(action),
|
||||||
|
Actions::Multiple(actions) => actions.as_slice(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Action> for Actions {
|
||||||
|
fn from(action: Action) -> Self {
|
||||||
|
Actions::Single(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<Action>> for Actions {
|
||||||
|
fn from(actions: Vec<Action>) -> Self {
|
||||||
|
if actions.len() == 1 {
|
||||||
|
Actions::Single(actions.into_iter().next().unwrap())
|
||||||
|
} else {
|
||||||
|
Actions::Multiple(actions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Display for Action {
|
impl Display for Action {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
@ -176,3 +217,106 @@ impl Display for Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_actions_single() {
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
assert_eq!(single_from_action.as_slice(), &[Action::SelectNextEntry]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_actions_multiple() {
|
||||||
|
let actions_vec = vec![Action::CopyEntryToClipboard, Action::Quit];
|
||||||
|
let multiple_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.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);
|
||||||
|
assert_eq!(single.as_slice(), &[Action::DeleteLine]);
|
||||||
|
|
||||||
|
let multiple = Actions::Multiple(vec![
|
||||||
|
Action::ScrollPreviewUp,
|
||||||
|
Action::ScrollPreviewDown,
|
||||||
|
]);
|
||||||
|
assert_eq!(
|
||||||
|
multiple.as_slice(),
|
||||||
|
&[Action::ScrollPreviewUp, Action::ScrollPreviewDown]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_actions_into_vec() {
|
||||||
|
let single = Actions::Single(Action::ConfirmSelection);
|
||||||
|
assert_eq!(single.into_vec(), vec![Action::ConfirmSelection]);
|
||||||
|
|
||||||
|
let multiple = Actions::Multiple(vec![
|
||||||
|
Action::ToggleHelp,
|
||||||
|
Action::ToggleStatusBar,
|
||||||
|
]);
|
||||||
|
assert_eq!(
|
||||||
|
multiple.into_vec(),
|
||||||
|
vec![Action::ToggleHelp, Action::ToggleStatusBar]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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]);
|
||||||
|
|
||||||
|
assert_eq!(actions1, actions2);
|
||||||
|
assert_eq!(actions3, actions4);
|
||||||
|
assert_ne!(actions1, actions3);
|
||||||
|
|
||||||
|
// Test that they can be used as HashMap keys
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert(actions1.clone(), "single");
|
||||||
|
map.insert(actions3.clone(), "multiple");
|
||||||
|
|
||||||
|
assert_eq!(map.get(&actions2), Some(&"single"));
|
||||||
|
assert_eq!(map.get(&actions4), Some(&"multiple"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -412,7 +412,8 @@ impl App {
|
|||||||
> 0
|
> 0
|
||||||
{
|
{
|
||||||
for event in event_buf.drain(..) {
|
for event in event_buf.drain(..) {
|
||||||
if let Some(action) = self.convert_event_to_action(event) {
|
let actions = self.convert_event_to_actions(event);
|
||||||
|
for action in actions {
|
||||||
if action != Action::Tick {
|
if action != Action::Tick {
|
||||||
debug!("Queuing new action: {action:?}");
|
debug!("Queuing new action: {action:?}");
|
||||||
}
|
}
|
||||||
@ -481,24 +482,26 @@ impl App {
|
|||||||
/// mode the television is in.
|
/// mode the television is in.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `event` - The event to convert to an action.
|
/// * `event` - The event to convert to actions.
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// The action that corresponds to the given event.
|
/// A vector of actions that correspond to the given event. Multiple actions
|
||||||
fn convert_event_to_action(&self, event: Event<Key>) -> Option<Action> {
|
/// will be returned for keys/events bound to action sequences.
|
||||||
let action = match event {
|
fn convert_event_to_actions(&self, event: Event<Key>) -> Vec<Action> {
|
||||||
|
let actions = match event {
|
||||||
Event::Input(keycode) => {
|
Event::Input(keycode) => {
|
||||||
// First try to get action based on keybindings
|
// First try to get actions based on keybindings
|
||||||
if let Some(action) =
|
if let Some(actions) =
|
||||||
self.input_map.get_action_for_key(&keycode)
|
self.input_map.get_actions_for_key(&keycode)
|
||||||
{
|
{
|
||||||
debug!("Keybinding found: {action:?}");
|
let actions_vec = actions.as_slice().to_vec();
|
||||||
action.clone()
|
debug!("Keybinding found: {actions_vec:?}");
|
||||||
|
actions_vec
|
||||||
} else {
|
} else {
|
||||||
// fallback to text input events
|
// fallback to text input events
|
||||||
match keycode {
|
match keycode {
|
||||||
Key::Char(c) => Action::AddInputChar(c),
|
Key::Char(c) => vec![Action::AddInputChar(c)],
|
||||||
_ => Action::NoOp,
|
_ => vec![Action::NoOp],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -510,29 +513,32 @@ impl App {
|
|||||||
position: (mouse_event.column, mouse_event.row),
|
position: (mouse_event.column, mouse_event.row),
|
||||||
});
|
});
|
||||||
self.input_map
|
self.input_map
|
||||||
.get_action_for_input(&input_event)
|
.get_actions_for_input(&input_event)
|
||||||
.unwrap_or(Action::NoOp)
|
.unwrap_or_else(|| vec![Action::NoOp])
|
||||||
} else {
|
} else {
|
||||||
Action::NoOp
|
vec![Action::NoOp]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// terminal events
|
// terminal events
|
||||||
Event::Tick => Action::Tick,
|
Event::Tick => vec![Action::Tick],
|
||||||
Event::Resize(x, y) => Action::Resize(x, y),
|
Event::Resize(x, y) => vec![Action::Resize(x, y)],
|
||||||
Event::FocusGained => Action::Resume,
|
Event::FocusGained => vec![Action::Resume],
|
||||||
Event::FocusLost => Action::Suspend,
|
Event::FocusLost => vec![Action::Suspend],
|
||||||
Event::Closed => Action::NoOp,
|
Event::Closed => vec![Action::NoOp],
|
||||||
};
|
};
|
||||||
|
|
||||||
if action != Action::Tick {
|
// Filter out Tick actions for logging
|
||||||
trace!("Converted {event:?} to action: {action:?}");
|
let non_tick_actions: Vec<&Action> =
|
||||||
|
actions.iter().filter(|a| **a != Action::Tick).collect();
|
||||||
|
if !non_tick_actions.is_empty() {
|
||||||
|
trace!("Converted {event:?} to actions: {non_tick_actions:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if action == Action::NoOp {
|
// Filter out NoOp actions
|
||||||
None
|
actions
|
||||||
} else {
|
.into_iter()
|
||||||
Some(action)
|
.filter(|action| *action != Action::NoOp)
|
||||||
}
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle actions.
|
/// Handle actions.
|
||||||
|
@ -67,7 +67,7 @@ impl Cable {
|
|||||||
match binding {
|
match binding {
|
||||||
Binding::SingleKey(key) => Some((
|
Binding::SingleKey(key) => Some((
|
||||||
*key,
|
*key,
|
||||||
Action::SwitchToChannel(name.clone()),
|
Action::SwitchToChannel(name.clone()).into(),
|
||||||
)),
|
)),
|
||||||
// For multiple keys, use the first one
|
// For multiple keys, use the first one
|
||||||
Binding::MultipleKeys(keys)
|
Binding::MultipleKeys(keys)
|
||||||
@ -75,7 +75,8 @@ impl Cable {
|
|||||||
{
|
{
|
||||||
Some((
|
Some((
|
||||||
keys[0],
|
keys[0],
|
||||||
Action::SwitchToChannel(name.clone()),
|
Action::SwitchToChannel(name.clone())
|
||||||
|
.into(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
Binding::MultipleKeys(_) => None,
|
Binding::MultipleKeys(_) => None,
|
||||||
|
@ -589,38 +589,41 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let keybindings = prototype.keybindings.unwrap();
|
let keybindings = prototype.keybindings.unwrap();
|
||||||
assert_eq!(keybindings.bindings.get(&Key::Esc), Some(&Action::Quit));
|
assert_eq!(
|
||||||
|
keybindings.bindings.get(&Key::Esc),
|
||||||
|
Some(&Action::Quit.into())
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keybindings.bindings.get(&Key::Ctrl('c')),
|
keybindings.bindings.get(&Key::Ctrl('c')),
|
||||||
Some(&Action::Quit)
|
Some(&Action::Quit.into())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keybindings.bindings.get(&Key::Down),
|
keybindings.bindings.get(&Key::Down),
|
||||||
Some(&Action::SelectNextEntry)
|
Some(&Action::SelectNextEntry.into())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keybindings.bindings.get(&Key::Ctrl('n')),
|
keybindings.bindings.get(&Key::Ctrl('n')),
|
||||||
Some(&Action::SelectNextEntry)
|
Some(&Action::SelectNextEntry.into())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keybindings.bindings.get(&Key::Ctrl('j')),
|
keybindings.bindings.get(&Key::Ctrl('j')),
|
||||||
Some(&Action::SelectNextEntry)
|
Some(&Action::SelectNextEntry.into())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keybindings.bindings.get(&Key::Up),
|
keybindings.bindings.get(&Key::Up),
|
||||||
Some(&Action::SelectPrevEntry)
|
Some(&Action::SelectPrevEntry.into())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keybindings.bindings.get(&Key::Ctrl('p')),
|
keybindings.bindings.get(&Key::Ctrl('p')),
|
||||||
Some(&Action::SelectPrevEntry)
|
Some(&Action::SelectPrevEntry.into())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keybindings.bindings.get(&Key::Ctrl('k')),
|
keybindings.bindings.get(&Key::Ctrl('k')),
|
||||||
Some(&Action::SelectPrevEntry)
|
Some(&Action::SelectPrevEntry.into())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keybindings.bindings.get(&Key::Enter),
|
keybindings.bindings.get(&Key::Enter),
|
||||||
Some(&Action::ConfirmSelection)
|
Some(&Action::ConfirmSelection.into())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -650,9 +650,9 @@ mod tests {
|
|||||||
let post_processed_cli = post_process(cli, false);
|
let post_processed_cli = post_process(cli, false);
|
||||||
|
|
||||||
let mut expected = KeyBindings::default();
|
let mut expected = KeyBindings::default();
|
||||||
expected.insert(Key::Esc, Action::Quit);
|
expected.insert(Key::Esc, Action::Quit.into());
|
||||||
expected.insert(Key::Down, Action::SelectNextEntry);
|
expected.insert(Key::Down, Action::SelectNextEntry.into());
|
||||||
expected.insert(Key::Ctrl('j'), Action::SelectNextEntry);
|
expected.insert(Key::Ctrl('j'), Action::SelectNextEntry.into());
|
||||||
|
|
||||||
assert_eq!(post_processed_cli.keybindings, Some(expected));
|
assert_eq!(post_processed_cli.keybindings, Some(expected));
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
action::Action,
|
action::{Action, Actions},
|
||||||
event::{Key, convert_raw_event_to_key},
|
event::{Key, convert_raw_event_to_key},
|
||||||
};
|
};
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
@ -44,7 +44,7 @@ pub struct KeyBindings {
|
|||||||
serialize_with = "serialize_key_bindings",
|
serialize_with = "serialize_key_bindings",
|
||||||
deserialize_with = "deserialize_key_bindings"
|
deserialize_with = "deserialize_key_bindings"
|
||||||
)]
|
)]
|
||||||
pub bindings: FxHashMap<Key, Action>,
|
pub bindings: FxHashMap<Key, Actions>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
|
||||||
@ -94,7 +94,7 @@ pub struct EventBindings {
|
|||||||
serialize_with = "serialize_event_bindings",
|
serialize_with = "serialize_event_bindings",
|
||||||
deserialize_with = "deserialize_event_bindings"
|
deserialize_with = "deserialize_event_bindings"
|
||||||
)]
|
)]
|
||||||
pub bindings: FxHashMap<EventType, Action>,
|
pub bindings: FxHashMap<EventType, Actions>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<I> From<I> for KeyBindings
|
impl<I> From<I> for KeyBindings
|
||||||
@ -103,7 +103,10 @@ where
|
|||||||
{
|
{
|
||||||
fn from(iter: I) -> Self {
|
fn from(iter: I) -> Self {
|
||||||
KeyBindings {
|
KeyBindings {
|
||||||
bindings: iter.into_iter().collect(),
|
bindings: iter
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, a)| (k, Actions::from(a)))
|
||||||
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -114,7 +117,10 @@ where
|
|||||||
{
|
{
|
||||||
fn from(iter: I) -> Self {
|
fn from(iter: I) -> Self {
|
||||||
EventBindings {
|
EventBindings {
|
||||||
bindings: iter.into_iter().collect(),
|
bindings: iter
|
||||||
|
.into_iter()
|
||||||
|
.map(|(e, a)| (e, Actions::from(a)))
|
||||||
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -122,9 +128,9 @@ where
|
|||||||
impl Hash for KeyBindings {
|
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, action) in &self.bindings {
|
for (key, actions) in &self.bindings {
|
||||||
key.hash(state);
|
key.hash(state);
|
||||||
action.hash(state);
|
actions.hash(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -132,15 +138,15 @@ impl Hash for KeyBindings {
|
|||||||
impl Hash for EventBindings {
|
impl Hash for EventBindings {
|
||||||
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 (event, action) in &self.bindings {
|
for (event, actions) in &self.bindings {
|
||||||
event.hash(state);
|
event.hash(state);
|
||||||
action.hash(state);
|
actions.hash(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for KeyBindings {
|
impl Deref for KeyBindings {
|
||||||
type Target = FxHashMap<Key, Action>;
|
type Target = FxHashMap<Key, Actions>;
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.bindings
|
&self.bindings
|
||||||
}
|
}
|
||||||
@ -153,7 +159,7 @@ impl DerefMut for KeyBindings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for EventBindings {
|
impl Deref for EventBindings {
|
||||||
type Target = FxHashMap<EventType, Action>;
|
type Target = FxHashMap<EventType, Actions>;
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.bindings
|
&self.bindings
|
||||||
}
|
}
|
||||||
@ -172,8 +178,8 @@ pub fn merge_keybindings(
|
|||||||
mut keybindings: KeyBindings,
|
mut keybindings: KeyBindings,
|
||||||
new_keybindings: &KeyBindings,
|
new_keybindings: &KeyBindings,
|
||||||
) -> KeyBindings {
|
) -> KeyBindings {
|
||||||
for (key, action) in &new_keybindings.bindings {
|
for (key, actions) in &new_keybindings.bindings {
|
||||||
keybindings.bindings.insert(*key, action.clone());
|
keybindings.bindings.insert(*key, actions.clone());
|
||||||
}
|
}
|
||||||
keybindings
|
keybindings
|
||||||
}
|
}
|
||||||
@ -185,10 +191,10 @@ pub fn merge_event_bindings(
|
|||||||
mut event_bindings: EventBindings,
|
mut event_bindings: EventBindings,
|
||||||
new_event_bindings: &EventBindings,
|
new_event_bindings: &EventBindings,
|
||||||
) -> EventBindings {
|
) -> EventBindings {
|
||||||
for (event_type, action) in &new_event_bindings.bindings {
|
for (event_type, actions) in &new_event_bindings.bindings {
|
||||||
event_bindings
|
event_bindings
|
||||||
.bindings
|
.bindings
|
||||||
.insert(event_type.clone(), action.clone());
|
.insert(event_type.clone(), actions.clone());
|
||||||
}
|
}
|
||||||
event_bindings
|
event_bindings
|
||||||
}
|
}
|
||||||
@ -198,52 +204,53 @@ impl Default for KeyBindings {
|
|||||||
let mut bindings = FxHashMap::default();
|
let mut bindings = FxHashMap::default();
|
||||||
|
|
||||||
// Quit actions
|
// Quit actions
|
||||||
bindings.insert(Key::Esc, Action::Quit);
|
bindings.insert(Key::Esc, Action::Quit.into());
|
||||||
bindings.insert(Key::Ctrl('c'), Action::Quit);
|
bindings.insert(Key::Ctrl('c'), Action::Quit.into());
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
bindings.insert(Key::Down, Action::SelectNextEntry);
|
bindings.insert(Key::Down, Action::SelectNextEntry.into());
|
||||||
bindings.insert(Key::Ctrl('n'), Action::SelectNextEntry);
|
bindings.insert(Key::Ctrl('n'), Action::SelectNextEntry.into());
|
||||||
bindings.insert(Key::Ctrl('j'), Action::SelectNextEntry);
|
bindings.insert(Key::Ctrl('j'), Action::SelectNextEntry.into());
|
||||||
bindings.insert(Key::Up, Action::SelectPrevEntry);
|
bindings.insert(Key::Up, Action::SelectPrevEntry.into());
|
||||||
bindings.insert(Key::Ctrl('p'), Action::SelectPrevEntry);
|
bindings.insert(Key::Ctrl('p'), Action::SelectPrevEntry.into());
|
||||||
bindings.insert(Key::Ctrl('k'), Action::SelectPrevEntry);
|
bindings.insert(Key::Ctrl('k'), Action::SelectPrevEntry.into());
|
||||||
|
|
||||||
// History navigation
|
// History navigation
|
||||||
bindings.insert(Key::CtrlUp, Action::SelectPrevHistory);
|
bindings.insert(Key::CtrlUp, Action::SelectPrevHistory.into());
|
||||||
bindings.insert(Key::CtrlDown, Action::SelectNextHistory);
|
bindings.insert(Key::CtrlDown, Action::SelectNextHistory.into());
|
||||||
|
|
||||||
// Selection actions
|
// Selection actions
|
||||||
bindings.insert(Key::Enter, Action::ConfirmSelection);
|
bindings.insert(Key::Enter, Action::ConfirmSelection.into());
|
||||||
bindings.insert(Key::Tab, Action::ToggleSelectionDown);
|
bindings.insert(Key::Tab, Action::ToggleSelectionDown.into());
|
||||||
bindings.insert(Key::BackTab, Action::ToggleSelectionUp);
|
bindings.insert(Key::BackTab, Action::ToggleSelectionUp.into());
|
||||||
|
|
||||||
// Preview actions
|
// Preview actions
|
||||||
bindings.insert(Key::PageDown, Action::ScrollPreviewHalfPageDown);
|
bindings
|
||||||
bindings.insert(Key::PageUp, Action::ScrollPreviewHalfPageUp);
|
.insert(Key::PageDown, Action::ScrollPreviewHalfPageDown.into());
|
||||||
|
bindings.insert(Key::PageUp, Action::ScrollPreviewHalfPageUp.into());
|
||||||
|
|
||||||
// Clipboard and toggles
|
// Clipboard and toggles
|
||||||
bindings.insert(Key::Ctrl('y'), Action::CopyEntryToClipboard);
|
bindings.insert(Key::Ctrl('y'), Action::CopyEntryToClipboard.into());
|
||||||
bindings.insert(Key::Ctrl('r'), Action::ReloadSource);
|
bindings.insert(Key::Ctrl('r'), Action::ReloadSource.into());
|
||||||
bindings.insert(Key::Ctrl('s'), Action::CycleSources);
|
bindings.insert(Key::Ctrl('s'), Action::CycleSources.into());
|
||||||
|
|
||||||
// UI Features
|
// UI Features
|
||||||
bindings.insert(Key::Ctrl('t'), Action::ToggleRemoteControl);
|
bindings.insert(Key::Ctrl('t'), Action::ToggleRemoteControl.into());
|
||||||
bindings.insert(Key::Ctrl('o'), Action::TogglePreview);
|
bindings.insert(Key::Ctrl('o'), Action::TogglePreview.into());
|
||||||
bindings.insert(Key::Ctrl('h'), Action::ToggleHelp);
|
bindings.insert(Key::Ctrl('h'), Action::ToggleHelp.into());
|
||||||
bindings.insert(Key::F(12), Action::ToggleStatusBar);
|
bindings.insert(Key::F(12), Action::ToggleStatusBar.into());
|
||||||
|
|
||||||
// Input field actions
|
// Input field actions
|
||||||
bindings.insert(Key::Backspace, Action::DeletePrevChar);
|
bindings.insert(Key::Backspace, Action::DeletePrevChar.into());
|
||||||
bindings.insert(Key::Ctrl('w'), Action::DeletePrevWord);
|
bindings.insert(Key::Ctrl('w'), Action::DeletePrevWord.into());
|
||||||
bindings.insert(Key::Ctrl('u'), Action::DeleteLine);
|
bindings.insert(Key::Ctrl('u'), Action::DeleteLine.into());
|
||||||
bindings.insert(Key::Delete, Action::DeleteNextChar);
|
bindings.insert(Key::Delete, Action::DeleteNextChar.into());
|
||||||
bindings.insert(Key::Left, Action::GoToPrevChar);
|
bindings.insert(Key::Left, Action::GoToPrevChar.into());
|
||||||
bindings.insert(Key::Right, Action::GoToNextChar);
|
bindings.insert(Key::Right, Action::GoToNextChar.into());
|
||||||
bindings.insert(Key::Home, Action::GoToInputStart);
|
bindings.insert(Key::Home, Action::GoToInputStart.into());
|
||||||
bindings.insert(Key::Ctrl('a'), Action::GoToInputStart);
|
bindings.insert(Key::Ctrl('a'), Action::GoToInputStart.into());
|
||||||
bindings.insert(Key::End, Action::GoToInputEnd);
|
bindings.insert(Key::End, Action::GoToInputEnd.into());
|
||||||
bindings.insert(Key::Ctrl('e'), Action::GoToInputEnd);
|
bindings.insert(Key::Ctrl('e'), Action::GoToInputEnd.into());
|
||||||
|
|
||||||
Self { bindings }
|
Self { bindings }
|
||||||
}
|
}
|
||||||
@ -254,8 +261,12 @@ impl Default for EventBindings {
|
|||||||
let mut bindings = FxHashMap::default();
|
let mut bindings = FxHashMap::default();
|
||||||
|
|
||||||
// Mouse events
|
// Mouse events
|
||||||
bindings.insert(EventType::MouseScrollUp, Action::ScrollPreviewUp);
|
bindings
|
||||||
bindings.insert(EventType::MouseScrollDown, Action::ScrollPreviewDown);
|
.insert(EventType::MouseScrollUp, Action::ScrollPreviewUp.into());
|
||||||
|
bindings.insert(
|
||||||
|
EventType::MouseScrollDown,
|
||||||
|
Action::ScrollPreviewDown.into(),
|
||||||
|
);
|
||||||
|
|
||||||
Self { bindings }
|
Self { bindings }
|
||||||
}
|
}
|
||||||
@ -445,7 +456,7 @@ pub fn parse_key(raw: &str) -> anyhow::Result<Key, String> {
|
|||||||
|
|
||||||
/// Custom serializer for `KeyBindings` that converts `Key` enum to string for TOML compatibility
|
/// Custom serializer for `KeyBindings` that converts `Key` enum to string for TOML compatibility
|
||||||
fn serialize_key_bindings<S>(
|
fn serialize_key_bindings<S>(
|
||||||
bindings: &FxHashMap<Key, Action>,
|
bindings: &FxHashMap<Key, Actions>,
|
||||||
serializer: S,
|
serializer: S,
|
||||||
) -> Result<S::Ok, S::Error>
|
) -> Result<S::Ok, S::Error>
|
||||||
where
|
where
|
||||||
@ -453,8 +464,8 @@ where
|
|||||||
{
|
{
|
||||||
use serde::ser::SerializeMap;
|
use serde::ser::SerializeMap;
|
||||||
let mut map = serializer.serialize_map(Some(bindings.len()))?;
|
let mut map = serializer.serialize_map(Some(bindings.len()))?;
|
||||||
for (key, action) in bindings {
|
for (key, actions) in bindings {
|
||||||
map.serialize_entry(&key.to_string(), action)?;
|
map.serialize_entry(&key.to_string(), actions)?;
|
||||||
}
|
}
|
||||||
map.end()
|
map.end()
|
||||||
}
|
}
|
||||||
@ -462,7 +473,7 @@ where
|
|||||||
/// Custom deserializer for `KeyBindings` that parses string keys back to `Key` enum
|
/// Custom deserializer for `KeyBindings` that parses string keys back to `Key` enum
|
||||||
fn deserialize_key_bindings<'de, D>(
|
fn deserialize_key_bindings<'de, D>(
|
||||||
deserializer: D,
|
deserializer: D,
|
||||||
) -> Result<FxHashMap<Key, Action>, D::Error>
|
) -> Result<FxHashMap<Key, Actions>, D::Error>
|
||||||
where
|
where
|
||||||
D: serde::Deserializer<'de>,
|
D: serde::Deserializer<'de>,
|
||||||
{
|
{
|
||||||
@ -472,10 +483,11 @@ where
|
|||||||
struct KeyBindingsVisitor;
|
struct KeyBindingsVisitor;
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for KeyBindingsVisitor {
|
impl<'de> Visitor<'de> for KeyBindingsVisitor {
|
||||||
type Value = FxHashMap<Key, Action>;
|
type Value = FxHashMap<Key, Actions>;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
formatter.write_str("a map with string keys and action values")
|
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>
|
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||||
@ -494,16 +506,16 @@ where
|
|||||||
match raw_value {
|
match raw_value {
|
||||||
Value::Boolean(false) => {
|
Value::Boolean(false) => {
|
||||||
// Explicitly unbind key
|
// Explicitly unbind key
|
||||||
bindings.insert(key, Action::NoOp);
|
bindings.insert(key, Action::NoOp.into());
|
||||||
}
|
}
|
||||||
Value::Boolean(true) => {
|
Value::Boolean(true) => {
|
||||||
// True means do nothing (keep current binding or ignore)
|
// True means do nothing (keep current binding or ignore)
|
||||||
}
|
}
|
||||||
action => {
|
actions_value => {
|
||||||
// Try to deserialize as Action
|
// Try to deserialize as Actions (handles both single and multiple)
|
||||||
let action = Action::deserialize(action)
|
let actions = Actions::deserialize(actions_value)
|
||||||
.map_err(Error::custom)?;
|
.map_err(Error::custom)?;
|
||||||
bindings.insert(key, action);
|
bindings.insert(key, actions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -516,7 +528,7 @@ where
|
|||||||
|
|
||||||
/// Custom serializer for `EventBindings` that converts `EventType` enum to string for TOML compatibility
|
/// Custom serializer for `EventBindings` that converts `EventType` enum to string for TOML compatibility
|
||||||
fn serialize_event_bindings<S>(
|
fn serialize_event_bindings<S>(
|
||||||
bindings: &FxHashMap<EventType, Action>,
|
bindings: &FxHashMap<EventType, Actions>,
|
||||||
serializer: S,
|
serializer: S,
|
||||||
) -> Result<S::Ok, S::Error>
|
) -> Result<S::Ok, S::Error>
|
||||||
where
|
where
|
||||||
@ -524,8 +536,8 @@ where
|
|||||||
{
|
{
|
||||||
use serde::ser::SerializeMap;
|
use serde::ser::SerializeMap;
|
||||||
let mut map = serializer.serialize_map(Some(bindings.len()))?;
|
let mut map = serializer.serialize_map(Some(bindings.len()))?;
|
||||||
for (event_type, action) in bindings {
|
for (event_type, actions) in bindings {
|
||||||
map.serialize_entry(&event_type.to_string(), action)?;
|
map.serialize_entry(&event_type.to_string(), actions)?;
|
||||||
}
|
}
|
||||||
map.end()
|
map.end()
|
||||||
}
|
}
|
||||||
@ -533,7 +545,7 @@ where
|
|||||||
/// Custom deserializer for `EventBindings` that parses string keys back to `EventType` enum
|
/// Custom deserializer for `EventBindings` that parses string keys back to `EventType` enum
|
||||||
fn deserialize_event_bindings<'de, D>(
|
fn deserialize_event_bindings<'de, D>(
|
||||||
deserializer: D,
|
deserializer: D,
|
||||||
) -> Result<FxHashMap<EventType, Action>, D::Error>
|
) -> Result<FxHashMap<EventType, Actions>, D::Error>
|
||||||
where
|
where
|
||||||
D: serde::Deserializer<'de>,
|
D: serde::Deserializer<'de>,
|
||||||
{
|
{
|
||||||
@ -543,19 +555,23 @@ where
|
|||||||
struct EventBindingsVisitor;
|
struct EventBindingsVisitor;
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for EventBindingsVisitor {
|
impl<'de> Visitor<'de> for EventBindingsVisitor {
|
||||||
type Value = FxHashMap<EventType, Action>;
|
type Value = FxHashMap<EventType, Actions>;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
formatter.write_str("a map with string keys and action values")
|
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>
|
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||||
where
|
where
|
||||||
A: MapAccess<'de>,
|
A: MapAccess<'de>,
|
||||||
{
|
{
|
||||||
|
use serde::de::Error;
|
||||||
|
use toml::Value;
|
||||||
|
|
||||||
let mut bindings = FxHashMap::default();
|
let mut bindings = FxHashMap::default();
|
||||||
while let Some((event_str, action)) =
|
while let Some((event_str, raw_value)) =
|
||||||
map.next_entry::<String, Action>()?
|
map.next_entry::<String, Value>()?
|
||||||
{
|
{
|
||||||
// Parse the event string back to EventType
|
// Parse the event string back to EventType
|
||||||
let event_type = match event_str.as_str() {
|
let event_type = match event_str.as_str() {
|
||||||
@ -565,7 +581,11 @@ where
|
|||||||
"resize" => EventType::Resize,
|
"resize" => EventType::Resize,
|
||||||
custom => EventType::Custom(custom.to_string()),
|
custom => EventType::Custom(custom.to_string()),
|
||||||
};
|
};
|
||||||
bindings.insert(event_type, action);
|
|
||||||
|
// 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)
|
Ok(bindings)
|
||||||
}
|
}
|
||||||
@ -750,21 +770,21 @@ mod tests {
|
|||||||
|
|
||||||
// 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));
|
||||||
assert_eq!(merged.bindings.get(&Key::Esc), Some(&Action::Quit));
|
assert_eq!(merged.bindings.get(&Key::Esc), Some(&Action::Quit.into()));
|
||||||
assert!(merged.bindings.contains_key(&Key::Down));
|
assert!(merged.bindings.contains_key(&Key::Down));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
merged.bindings.get(&Key::Down),
|
merged.bindings.get(&Key::Down),
|
||||||
Some(&Action::SelectNextEntry)
|
Some(&Action::SelectNextEntry.into())
|
||||||
);
|
);
|
||||||
assert!(merged.bindings.contains_key(&Key::Ctrl('j')));
|
assert!(merged.bindings.contains_key(&Key::Ctrl('j')));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
merged.bindings.get(&Key::Ctrl('j')),
|
merged.bindings.get(&Key::Ctrl('j')),
|
||||||
Some(&Action::SelectNextEntry)
|
Some(&Action::SelectNextEntry.into())
|
||||||
);
|
);
|
||||||
assert!(merged.bindings.contains_key(&Key::PageDown));
|
assert!(merged.bindings.contains_key(&Key::PageDown));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
merged.bindings.get(&Key::PageDown),
|
merged.bindings.get(&Key::PageDown),
|
||||||
Some(&Action::SelectNextPage)
|
Some(&Action::SelectNextPage.into())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -781,19 +801,157 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Normal action binding should work
|
// Normal action binding should work
|
||||||
assert_eq!(keybindings.bindings.get(&Key::Esc), Some(&Action::Quit));
|
assert_eq!(
|
||||||
|
keybindings.bindings.get(&Key::Esc),
|
||||||
|
Some(&Action::Quit.into())
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keybindings.bindings.get(&Key::Down),
|
keybindings.bindings.get(&Key::Down),
|
||||||
Some(&Action::SelectNextEntry)
|
Some(&Action::SelectNextEntry.into())
|
||||||
);
|
);
|
||||||
|
|
||||||
// false should bind to NoOp (unbinding)
|
// false should bind to NoOp (unbinding)
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keybindings.bindings.get(&Key::Ctrl('c')),
|
keybindings.bindings.get(&Key::Ctrl('c')),
|
||||||
Some(&Action::NoOp)
|
Some(&Action::NoOp.into())
|
||||||
);
|
);
|
||||||
|
|
||||||
// true should be ignored (no binding created)
|
// true should be ignored (no binding created)
|
||||||
assert_eq!(keybindings.bindings.get(&Key::Up), None);
|
assert_eq!(keybindings.bindings.get(&Key::Up), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_multiple_actions_per_key() {
|
||||||
|
let keybindings: KeyBindings = toml::from_str(
|
||||||
|
r#"
|
||||||
|
"esc" = "quit"
|
||||||
|
"ctrl-s" = ["reload_source", "copy_entry_to_clipboard"]
|
||||||
|
"f1" = ["toggle_help", "toggle_preview", "toggle_status_bar"]
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Single action should work
|
||||||
|
assert_eq!(
|
||||||
|
keybindings.bindings.get(&Key::Esc),
|
||||||
|
Some(&Action::Quit.into())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Multiple actions should work
|
||||||
|
assert_eq!(
|
||||||
|
keybindings.bindings.get(&Key::Ctrl('s')),
|
||||||
|
Some(&Actions::Multiple(vec![
|
||||||
|
Action::ReloadSource,
|
||||||
|
Action::CopyEntryToClipboard
|
||||||
|
]))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Three actions should work
|
||||||
|
assert_eq!(
|
||||||
|
keybindings.bindings.get(&Key::F(1)),
|
||||||
|
Some(&Actions::Multiple(vec![
|
||||||
|
Action::ToggleHelp,
|
||||||
|
Action::TogglePreview,
|
||||||
|
Action::ToggleStatusBar
|
||||||
|
]))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merge_keybindings_with_multiple_actions() {
|
||||||
|
let base_keybindings = KeyBindings::from(vec![
|
||||||
|
(Key::Esc, Action::Quit),
|
||||||
|
(Key::Enter, Action::ConfirmSelection),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let mut custom_bindings = FxHashMap::default();
|
||||||
|
custom_bindings.insert(
|
||||||
|
Key::Ctrl('s'),
|
||||||
|
Actions::Multiple(vec![
|
||||||
|
Action::ReloadSource,
|
||||||
|
Action::CopyEntryToClipboard,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
custom_bindings.insert(Key::Esc, Action::NoOp.into()); // Override
|
||||||
|
let custom_keybindings = KeyBindings {
|
||||||
|
bindings: custom_bindings,
|
||||||
|
};
|
||||||
|
|
||||||
|
let merged = merge_keybindings(base_keybindings, &custom_keybindings);
|
||||||
|
|
||||||
|
// Custom multiple actions should be present
|
||||||
|
assert_eq!(
|
||||||
|
merged.bindings.get(&Key::Ctrl('s')),
|
||||||
|
Some(&Actions::Multiple(vec![
|
||||||
|
Action::ReloadSource,
|
||||||
|
Action::CopyEntryToClipboard
|
||||||
|
]))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Override should work
|
||||||
|
assert_eq!(merged.bindings.get(&Key::Esc), Some(&Action::NoOp.into()));
|
||||||
|
|
||||||
|
// Original binding should be preserved
|
||||||
|
assert_eq!(
|
||||||
|
merged.bindings.get(&Key::Enter),
|
||||||
|
Some(&Action::ConfirmSelection.into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_complex_configuration_with_all_features() {
|
||||||
|
let keybindings: KeyBindings = toml::from_str(
|
||||||
|
r#"
|
||||||
|
# Single actions
|
||||||
|
esc = "quit"
|
||||||
|
enter = "confirm_selection"
|
||||||
|
|
||||||
|
# Multiple actions
|
||||||
|
ctrl-s = ["reload_source", "copy_entry_to_clipboard"]
|
||||||
|
f1 = ["toggle_help", "toggle_preview", "toggle_status_bar"]
|
||||||
|
|
||||||
|
# Unbinding
|
||||||
|
ctrl-c = false
|
||||||
|
|
||||||
|
# Single action in array format (should work)
|
||||||
|
tab = ["toggle_selection_down"]
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(keybindings.bindings.len(), 6); // ctrl-c=false creates NoOp binding
|
||||||
|
|
||||||
|
// Verify all binding types work correctly
|
||||||
|
assert_eq!(
|
||||||
|
keybindings.bindings.get(&Key::Esc),
|
||||||
|
Some(&Actions::Single(Action::Quit))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
keybindings.bindings.get(&Key::Enter),
|
||||||
|
Some(&Action::ConfirmSelection.into())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
keybindings.bindings.get(&Key::Ctrl('s')),
|
||||||
|
Some(&Actions::Multiple(vec![
|
||||||
|
Action::ReloadSource,
|
||||||
|
Action::CopyEntryToClipboard
|
||||||
|
]))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
keybindings.bindings.get(&Key::F(1)),
|
||||||
|
Some(&Actions::Multiple(vec![
|
||||||
|
Action::ToggleHelp,
|
||||||
|
Action::TogglePreview,
|
||||||
|
Action::ToggleStatusBar
|
||||||
|
]))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
keybindings.bindings.get(&Key::Ctrl('c')),
|
||||||
|
Some(&Actions::Single(Action::NoOp))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
keybindings.bindings.get(&Key::Tab),
|
||||||
|
Some(&Actions::Multiple(vec![Action::ToggleSelectionDown]))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -474,7 +474,7 @@ mod tests {
|
|||||||
default_config
|
default_config
|
||||||
.keybindings
|
.keybindings
|
||||||
.bindings
|
.bindings
|
||||||
.insert(Key::CtrlEnter, Action::ConfirmSelection);
|
.insert(Key::CtrlEnter, Action::ConfirmSelection.into());
|
||||||
|
|
||||||
default_config.shell_integration.keybindings.insert(
|
default_config.shell_integration.keybindings.insert(
|
||||||
"command_history".to_string(),
|
"command_history".to_string(),
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
action::Action,
|
action::{Action, Actions},
|
||||||
config::{EventBindings, EventType, KeyBindings},
|
config::{EventBindings, EventType, KeyBindings},
|
||||||
event::{InputEvent, Key},
|
event::{InputEvent, Key},
|
||||||
};
|
};
|
||||||
@ -21,8 +21,8 @@ use rustc_hash::FxHashMap;
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub struct InputMap {
|
pub struct InputMap {
|
||||||
pub key_actions: FxHashMap<Key, Action>,
|
pub key_actions: FxHashMap<Key, Actions>,
|
||||||
pub event_actions: FxHashMap<EventType, Action>,
|
pub event_actions: FxHashMap<EventType, Actions>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputMap {
|
impl InputMap {
|
||||||
@ -34,20 +34,46 @@ impl InputMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the action for a given key
|
/// Get the actions for a given key
|
||||||
pub fn get_action_for_key(&self, key: &Key) -> Option<&Action> {
|
pub fn get_actions_for_key(&self, key: &Key) -> Option<&Actions> {
|
||||||
self.key_actions.get(key)
|
self.key_actions.get(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the action for a given event type
|
/// Get the actions for a given event type
|
||||||
pub fn get_action_for_event(&self, event: &EventType) -> Option<&Action> {
|
pub fn get_actions_for_event(
|
||||||
|
&self,
|
||||||
|
event: &EventType,
|
||||||
|
) -> Option<&Actions> {
|
||||||
self.event_actions.get(event)
|
self.event_actions.get(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get an action for any input event
|
/// Get the action for a given key (backward compatibility)
|
||||||
pub fn get_action_for_input(&self, input: &InputEvent) -> Option<Action> {
|
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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the action for a given event type (backward compatibility)
|
||||||
|
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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all actions for any input event
|
||||||
|
pub fn get_actions_for_input(
|
||||||
|
&self,
|
||||||
|
input: &InputEvent,
|
||||||
|
) -> Option<Vec<Action>> {
|
||||||
match input {
|
match input {
|
||||||
InputEvent::Key(key) => self.get_action_for_key(key).cloned(),
|
InputEvent::Key(key) => self
|
||||||
|
.get_actions_for_key(key)
|
||||||
|
.map(|actions| actions.as_slice().to_vec()),
|
||||||
InputEvent::Mouse(mouse_event) => {
|
InputEvent::Mouse(mouse_event) => {
|
||||||
let event_type = match mouse_event.kind {
|
let event_type = match mouse_event.kind {
|
||||||
MouseEventKind::Down(_) => EventType::MouseClick,
|
MouseEventKind::Down(_) => EventType::MouseClick,
|
||||||
@ -55,14 +81,32 @@ impl InputMap {
|
|||||||
MouseEventKind::ScrollDown => EventType::MouseScrollDown,
|
MouseEventKind::ScrollDown => EventType::MouseScrollDown,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
self.get_action_for_event(&event_type).cloned()
|
self.get_actions_for_event(&event_type)
|
||||||
|
.map(|actions| actions.as_slice().to_vec())
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an action for any input event (backward compatibility)
|
||||||
|
pub fn get_action_for_input(&self, input: &InputEvent) -> Option<Action> {
|
||||||
|
match input {
|
||||||
|
InputEvent::Key(key) => self.get_action_for_key(key),
|
||||||
|
InputEvent::Mouse(mouse_event) => {
|
||||||
|
let event_type = match mouse_event.kind {
|
||||||
|
MouseEventKind::Down(_) => EventType::MouseClick,
|
||||||
|
MouseEventKind::ScrollUp => EventType::MouseScrollUp,
|
||||||
|
MouseEventKind::ScrollDown => EventType::MouseScrollDown,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
self.get_action_for_event(&event_type)
|
||||||
}
|
}
|
||||||
InputEvent::Resize(_, _) => {
|
InputEvent::Resize(_, _) => {
|
||||||
self.get_action_for_event(&EventType::Resize).cloned()
|
self.get_action_for_event(&EventType::Resize)
|
||||||
|
}
|
||||||
|
InputEvent::Custom(name) => {
|
||||||
|
self.get_action_for_event(&EventType::Custom(name.clone()))
|
||||||
}
|
}
|
||||||
InputEvent::Custom(name) => self
|
|
||||||
.get_action_for_event(&EventType::Custom(name.clone()))
|
|
||||||
.cloned(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,15 +192,15 @@ mod tests {
|
|||||||
let input_map: InputMap = (&keybindings).into();
|
let input_map: InputMap = (&keybindings).into();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
input_map.get_action_for_key(&Key::Char('j')),
|
input_map.get_action_for_key(&Key::Char('j')),
|
||||||
Some(&Action::SelectNextEntry)
|
Some(Action::SelectNextEntry)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
input_map.get_action_for_key(&Key::Char('k')),
|
input_map.get_action_for_key(&Key::Char('k')),
|
||||||
Some(&Action::SelectPrevEntry)
|
Some(Action::SelectPrevEntry)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
input_map.get_action_for_key(&Key::Char('q')),
|
input_map.get_action_for_key(&Key::Char('q')),
|
||||||
Some(&Action::Quit)
|
Some(Action::Quit)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,11 +214,11 @@ mod tests {
|
|||||||
let input_map: InputMap = (&event_bindings).into();
|
let input_map: InputMap = (&event_bindings).into();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
input_map.get_action_for_event(&EventType::MouseClick),
|
input_map.get_action_for_event(&EventType::MouseClick),
|
||||||
Some(&Action::ConfirmSelection)
|
Some(Action::ConfirmSelection)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
input_map.get_action_for_event(&EventType::Resize),
|
input_map.get_action_for_event(&EventType::Resize),
|
||||||
Some(&Action::ClearScreen)
|
Some(Action::ClearScreen)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,34 +256,196 @@ mod tests {
|
|||||||
let mut input_map1 = InputMap::new();
|
let mut input_map1 = InputMap::new();
|
||||||
input_map1
|
input_map1
|
||||||
.key_actions
|
.key_actions
|
||||||
.insert(Key::Char('a'), Action::SelectNextEntry);
|
.insert(Key::Char('a'), Action::SelectNextEntry.into());
|
||||||
input_map1
|
input_map1
|
||||||
.key_actions
|
.key_actions
|
||||||
.insert(Key::Char('b'), Action::SelectPrevEntry);
|
.insert(Key::Char('b'), Action::SelectPrevEntry.into());
|
||||||
|
|
||||||
let mut input_map2 = InputMap::new();
|
let mut input_map2 = InputMap::new();
|
||||||
input_map2.key_actions.insert(Key::Char('c'), Action::Quit);
|
input_map2
|
||||||
input_map2.key_actions.insert(Key::Char('a'), Action::Quit); // This should overwrite
|
.key_actions
|
||||||
|
.insert(Key::Char('c'), Action::Quit.into());
|
||||||
|
input_map2
|
||||||
|
.key_actions
|
||||||
|
.insert(Key::Char('a'), Action::Quit.into()); // This should overwrite
|
||||||
input_map2
|
input_map2
|
||||||
.event_actions
|
.event_actions
|
||||||
.insert(EventType::MouseClick, Action::ConfirmSelection);
|
.insert(EventType::MouseClick, Action::ConfirmSelection.into());
|
||||||
|
|
||||||
input_map1.merge(&input_map2);
|
input_map1.merge(&input_map2);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
input_map1.get_action_for_key(&Key::Char('a')),
|
input_map1.get_action_for_key(&Key::Char('a')),
|
||||||
Some(&Action::Quit)
|
Some(Action::Quit)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
input_map1.get_action_for_key(&Key::Char('b')),
|
input_map1.get_action_for_key(&Key::Char('b')),
|
||||||
Some(&Action::SelectPrevEntry)
|
Some(Action::SelectPrevEntry)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
input_map1.get_action_for_key(&Key::Char('c')),
|
input_map1.get_action_for_key(&Key::Char('c')),
|
||||||
Some(&Action::Quit)
|
Some(Action::Quit)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
input_map1.get_action_for_event(&EventType::MouseClick),
|
input_map1.get_action_for_event(&EventType::MouseClick),
|
||||||
Some(&Action::ConfirmSelection)
|
Some(Action::ConfirmSelection)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_input_map_multiple_actions_per_key() {
|
||||||
|
let mut key_actions = FxHashMap::default();
|
||||||
|
key_actions.insert(
|
||||||
|
Key::Ctrl('s'),
|
||||||
|
Actions::Multiple(vec![
|
||||||
|
Action::ReloadSource,
|
||||||
|
Action::CopyEntryToClipboard,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
key_actions.insert(Key::Esc, Actions::Single(Action::Quit));
|
||||||
|
|
||||||
|
let input_map = InputMap {
|
||||||
|
key_actions,
|
||||||
|
event_actions: FxHashMap::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test getting all actions for multiple action binding
|
||||||
|
let ctrl_s_actions =
|
||||||
|
input_map.get_actions_for_key(&Key::Ctrl('s')).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
ctrl_s_actions.as_slice(),
|
||||||
|
&[Action::ReloadSource, Action::CopyEntryToClipboard]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test backward compatibility method returns first action
|
||||||
|
assert_eq!(
|
||||||
|
input_map.get_action_for_key(&Key::Ctrl('s')),
|
||||||
|
Some(Action::ReloadSource)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test single action still works
|
||||||
|
assert_eq!(
|
||||||
|
input_map.get_action_for_key(&Key::Esc),
|
||||||
|
Some(Action::Quit)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_input_map_multiple_actions_for_input_event() {
|
||||||
|
let mut input_map = InputMap::new();
|
||||||
|
input_map.key_actions.insert(
|
||||||
|
Key::Char('j'),
|
||||||
|
Actions::Multiple(vec![
|
||||||
|
Action::SelectNextEntry,
|
||||||
|
Action::ScrollPreviewDown,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let key_input = InputEvent::Key(Key::Char('j'));
|
||||||
|
let actions = input_map.get_actions_for_input(&key_input).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(actions.len(), 2);
|
||||||
|
assert_eq!(actions[0], Action::SelectNextEntry);
|
||||||
|
assert_eq!(actions[1], Action::ScrollPreviewDown);
|
||||||
|
|
||||||
|
// Test backward compatibility returns first action
|
||||||
|
assert_eq!(
|
||||||
|
input_map.get_action_for_input(&key_input),
|
||||||
|
Some(Action::SelectNextEntry)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_input_map_multiple_actions_for_events() {
|
||||||
|
let mut event_actions = FxHashMap::default();
|
||||||
|
event_actions.insert(
|
||||||
|
EventType::MouseClick,
|
||||||
|
Actions::Multiple(vec![
|
||||||
|
Action::ConfirmSelection,
|
||||||
|
Action::TogglePreview,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let input_map = InputMap {
|
||||||
|
key_actions: FxHashMap::default(),
|
||||||
|
event_actions,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test mouse input with multiple actions
|
||||||
|
let mouse_input = InputEvent::Mouse(MouseInputEvent {
|
||||||
|
kind: MouseEventKind::Down(crossterm::event::MouseButton::Left),
|
||||||
|
position: (10, 10),
|
||||||
|
});
|
||||||
|
|
||||||
|
let actions = input_map.get_actions_for_input(&mouse_input).unwrap();
|
||||||
|
assert_eq!(actions.len(), 2);
|
||||||
|
assert_eq!(actions[0], Action::ConfirmSelection);
|
||||||
|
assert_eq!(actions[1], Action::TogglePreview);
|
||||||
|
|
||||||
|
// Test backward compatibility returns first action
|
||||||
|
assert_eq!(
|
||||||
|
input_map.get_action_for_input(&mouse_input),
|
||||||
|
Some(Action::ConfirmSelection)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_input_map_merge_multiple_actions() {
|
||||||
|
let mut input_map1 = InputMap::new();
|
||||||
|
input_map1
|
||||||
|
.key_actions
|
||||||
|
.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
|
||||||
|
);
|
||||||
|
input_map2.key_actions.insert(
|
||||||
|
Key::Char('b'),
|
||||||
|
Actions::Multiple(vec![Action::TogglePreview, Action::ToggleHelp]),
|
||||||
|
);
|
||||||
|
|
||||||
|
input_map1.merge(&input_map2);
|
||||||
|
|
||||||
|
// Verify the multiple actions overwrite worked
|
||||||
|
let actions_a =
|
||||||
|
input_map1.get_actions_for_key(&Key::Char('a')).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
actions_a.as_slice(),
|
||||||
|
&[Action::ReloadSource, Action::Quit]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify the new multiple actions were added
|
||||||
|
let actions_b =
|
||||||
|
input_map1.get_actions_for_key(&Key::Char('b')).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
actions_b.as_slice(),
|
||||||
|
&[Action::TogglePreview, Action::ToggleHelp]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_input_map_from_keybindings_with_multiple_actions() {
|
||||||
|
let mut bindings = FxHashMap::default();
|
||||||
|
bindings.insert(
|
||||||
|
Key::Ctrl('r'),
|
||||||
|
Actions::Multiple(vec![Action::ReloadSource, Action::ClearScreen]),
|
||||||
|
);
|
||||||
|
bindings.insert(Key::Esc, Actions::Single(Action::Quit));
|
||||||
|
|
||||||
|
let keybindings = KeyBindings { bindings };
|
||||||
|
let input_map: InputMap = (&keybindings).into();
|
||||||
|
|
||||||
|
// Test multiple actions are preserved
|
||||||
|
let ctrl_r_actions =
|
||||||
|
input_map.get_actions_for_key(&Key::Ctrl('r')).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
ctrl_r_actions.as_slice(),
|
||||||
|
&[Action::ReloadSource, Action::ClearScreen]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test single actions still work
|
||||||
|
let esc_actions = input_map.get_actions_for_key(&Key::Esc).unwrap();
|
||||||
|
assert_eq!(esc_actions.as_slice(), &[Action::Quit]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,7 +154,7 @@ fn apply_cli_overrides(args: &PostProcessedCli, config: &mut Config) {
|
|||||||
config.ui.features.disable(FeatureFlags::PreviewPanel);
|
config.ui.features.disable(FeatureFlags::PreviewPanel);
|
||||||
remove_action_bindings(
|
remove_action_bindings(
|
||||||
&mut config.keybindings,
|
&mut config.keybindings,
|
||||||
&Action::TogglePreview,
|
&Action::TogglePreview.into(),
|
||||||
);
|
);
|
||||||
} else if args.hide_preview {
|
} else if args.hide_preview {
|
||||||
config.ui.features.hide(FeatureFlags::PreviewPanel);
|
config.ui.features.hide(FeatureFlags::PreviewPanel);
|
||||||
@ -171,7 +171,7 @@ fn apply_cli_overrides(args: &PostProcessedCli, config: &mut Config) {
|
|||||||
config.ui.features.disable(FeatureFlags::StatusBar);
|
config.ui.features.disable(FeatureFlags::StatusBar);
|
||||||
remove_action_bindings(
|
remove_action_bindings(
|
||||||
&mut config.keybindings,
|
&mut config.keybindings,
|
||||||
&Action::ToggleStatusBar,
|
&Action::ToggleStatusBar.into(),
|
||||||
);
|
);
|
||||||
} else if args.hide_status_bar {
|
} else if args.hide_status_bar {
|
||||||
config.ui.features.hide(FeatureFlags::StatusBar);
|
config.ui.features.hide(FeatureFlags::StatusBar);
|
||||||
@ -184,7 +184,7 @@ fn apply_cli_overrides(args: &PostProcessedCli, config: &mut Config) {
|
|||||||
config.ui.features.disable(FeatureFlags::RemoteControl);
|
config.ui.features.disable(FeatureFlags::RemoteControl);
|
||||||
remove_action_bindings(
|
remove_action_bindings(
|
||||||
&mut config.keybindings,
|
&mut config.keybindings,
|
||||||
&Action::ToggleRemoteControl,
|
&Action::ToggleRemoteControl.into(),
|
||||||
);
|
);
|
||||||
} else if args.hide_remote {
|
} else if args.hide_remote {
|
||||||
config.ui.features.hide(FeatureFlags::RemoteControl);
|
config.ui.features.hide(FeatureFlags::RemoteControl);
|
||||||
@ -195,7 +195,10 @@ fn apply_cli_overrides(args: &PostProcessedCli, config: &mut Config) {
|
|||||||
// Handle help panel flags
|
// Handle help panel flags
|
||||||
if args.no_help_panel {
|
if args.no_help_panel {
|
||||||
config.ui.features.disable(FeatureFlags::HelpPanel);
|
config.ui.features.disable(FeatureFlags::HelpPanel);
|
||||||
remove_action_bindings(&mut config.keybindings, &Action::ToggleHelp);
|
remove_action_bindings(
|
||||||
|
&mut config.keybindings,
|
||||||
|
&Action::ToggleHelp.into(),
|
||||||
|
);
|
||||||
} else if args.hide_help_panel {
|
} else if args.hide_help_panel {
|
||||||
config.ui.features.hide(FeatureFlags::HelpPanel);
|
config.ui.features.hide(FeatureFlags::HelpPanel);
|
||||||
} else if args.show_help_panel {
|
} else if args.show_help_panel {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::KeyBindings,
|
config::KeyBindings,
|
||||||
screen::colors::Colorscheme,
|
screen::colors::Colorscheme,
|
||||||
screen::keybindings::{ActionMapping, find_keys_for_action},
|
screen::keybindings::{ActionMapping, find_keys_for_single_action},
|
||||||
television::Mode,
|
television::Mode,
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
@ -62,7 +62,7 @@ fn add_keybinding_lines_for_mappings(
|
|||||||
) {
|
) {
|
||||||
for mapping in mappings {
|
for mapping in mappings {
|
||||||
for (action, description) in &mapping.actions {
|
for (action, description) in &mapping.actions {
|
||||||
let keys = find_keys_for_action(keybindings, action);
|
let keys = find_keys_for_single_action(keybindings, action);
|
||||||
for key in keys {
|
for key in keys {
|
||||||
lines.push(create_compact_keybinding_line(
|
lines.push(create_compact_keybinding_line(
|
||||||
&key,
|
&key,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
action::Action,
|
action::{Action, Actions},
|
||||||
config::{Binding, KeyBindings},
|
config::{Binding, KeyBindings},
|
||||||
television::Mode,
|
television::Mode,
|
||||||
};
|
};
|
||||||
@ -166,7 +166,7 @@ pub fn extract_keys_from_binding(binding: &Binding) -> Vec<String> {
|
|||||||
/// Extract keys for a single action from the new Key->Action keybindings format
|
/// Extract keys for a single action from the new Key->Action keybindings format
|
||||||
pub fn find_keys_for_action(
|
pub fn find_keys_for_action(
|
||||||
keybindings: &KeyBindings,
|
keybindings: &KeyBindings,
|
||||||
target_action: &Action,
|
target_action: &Actions,
|
||||||
) -> Vec<String> {
|
) -> Vec<String> {
|
||||||
keybindings
|
keybindings
|
||||||
.bindings
|
.bindings
|
||||||
@ -181,10 +181,35 @@ pub fn find_keys_for_action(
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract keys for a single action (convenience function)
|
||||||
|
pub fn find_keys_for_single_action(
|
||||||
|
keybindings: &KeyBindings,
|
||||||
|
target_action: &Action,
|
||||||
|
) -> Vec<String> {
|
||||||
|
keybindings
|
||||||
|
.bindings
|
||||||
|
.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,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Extract keys for multiple actions and return them as a flat vector
|
/// Extract keys for multiple actions and return them as a flat vector
|
||||||
pub fn extract_keys_for_actions(
|
pub fn extract_keys_for_actions(
|
||||||
keybindings: &KeyBindings,
|
keybindings: &KeyBindings,
|
||||||
actions: &[Action],
|
actions: &[Actions],
|
||||||
) -> Vec<String> {
|
) -> Vec<String> {
|
||||||
actions
|
actions
|
||||||
.iter()
|
.iter()
|
||||||
@ -195,7 +220,7 @@ pub fn extract_keys_for_actions(
|
|||||||
/// Remove all keybindings for a specific action from `KeyBindings`
|
/// Remove all keybindings for a specific action from `KeyBindings`
|
||||||
pub fn remove_action_bindings(
|
pub fn remove_action_bindings(
|
||||||
keybindings: &mut KeyBindings,
|
keybindings: &mut KeyBindings,
|
||||||
target_action: &Action,
|
target_action: &Actions,
|
||||||
) {
|
) {
|
||||||
keybindings
|
keybindings
|
||||||
.bindings
|
.bindings
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
action::Action, draw::Ctx, features::FeatureFlags,
|
action::Action, draw::Ctx, features::FeatureFlags,
|
||||||
screen::keybindings::find_keys_for_action, television::Mode,
|
screen::keybindings::find_keys_for_single_action, television::Mode,
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
Frame,
|
Frame,
|
||||||
@ -133,7 +133,7 @@ pub fn draw_status_bar(f: &mut Frame<'_>, area: Rect, ctx: &Ctx) {
|
|||||||
.features
|
.features
|
||||||
.is_enabled(FeatureFlags::RemoteControl)
|
.is_enabled(FeatureFlags::RemoteControl)
|
||||||
{
|
{
|
||||||
let keys = find_keys_for_action(
|
let keys = find_keys_for_single_action(
|
||||||
&ctx.config.keybindings,
|
&ctx.config.keybindings,
|
||||||
&Action::ToggleRemoteControl,
|
&Action::ToggleRemoteControl,
|
||||||
);
|
);
|
||||||
@ -154,7 +154,7 @@ pub fn draw_status_bar(f: &mut Frame<'_>, area: Rect, ctx: &Ctx) {
|
|||||||
.features
|
.features
|
||||||
.is_enabled(FeatureFlags::PreviewPanel)
|
.is_enabled(FeatureFlags::PreviewPanel)
|
||||||
{
|
{
|
||||||
let keys = find_keys_for_action(
|
let keys = find_keys_for_single_action(
|
||||||
&ctx.config.keybindings,
|
&ctx.config.keybindings,
|
||||||
&Action::TogglePreview,
|
&Action::TogglePreview,
|
||||||
);
|
);
|
||||||
@ -174,8 +174,10 @@ pub fn draw_status_bar(f: &mut Frame<'_>, area: Rect, ctx: &Ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add keybinding help hint (available in both modes)
|
// Add keybinding help hint (available in both modes)
|
||||||
let keys =
|
let keys = find_keys_for_single_action(
|
||||||
find_keys_for_action(&ctx.config.keybindings, &Action::ToggleHelp);
|
&ctx.config.keybindings,
|
||||||
|
&Action::ToggleHelp,
|
||||||
|
);
|
||||||
if let Some(key) = keys.first() {
|
if let Some(key) = keys.first() {
|
||||||
add_hint("Help", key);
|
add_hint("Help", key);
|
||||||
}
|
}
|
||||||
|
@ -241,7 +241,7 @@ impl Television {
|
|||||||
config.ui.features.disable(FeatureFlags::PreviewPanel);
|
config.ui.features.disable(FeatureFlags::PreviewPanel);
|
||||||
remove_action_bindings(
|
remove_action_bindings(
|
||||||
&mut config.keybindings,
|
&mut config.keybindings,
|
||||||
&Action::TogglePreview,
|
&Action::TogglePreview.into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -968,7 +968,7 @@ mod test {
|
|||||||
let mut config = crate::config::Config::default();
|
let mut config = crate::config::Config::default();
|
||||||
config
|
config
|
||||||
.keybindings
|
.keybindings
|
||||||
.insert(Key::Ctrl('n'), Action::SelectNextEntry);
|
.insert(Key::Ctrl('n'), Action::SelectNextEntry.into());
|
||||||
|
|
||||||
let prototype =
|
let prototype =
|
||||||
toml::from_str::<crate::channels::prototypes::ChannelPrototype>(
|
toml::from_str::<crate::channels::prototypes::ChannelPrototype>(
|
||||||
@ -999,7 +999,7 @@ mod test {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tv.config.keybindings.get(&Key::Ctrl('j')),
|
tv.config.keybindings.get(&Key::Ctrl('j')),
|
||||||
Some(&Action::SelectNextEntry),
|
Some(&Action::SelectNextEntry.into()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user