use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::picker::PickerId;
use gtm_core::state::Tab;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyContext {
Global,
Normal,
List,
}
#[derive(Debug, Clone)]
pub enum KeyboardAction {
NextTab,
PrevTab,
SwitchTab(Tab),
MoveUp,
MoveDown,
PageUp,
PageDown,
Top,
Bottom,
Select,
Delete,
EnterFilter,
PlayPause,
Next,
Prev,
Stop,
VolumeUp,
VolumeDown,
SeekForward,
SeekBackward,
ToggleShuffle,
CycleRepeat,
ToggleMute,
ToggleFavourite,
ClearQueue,
OpenOverlay(PickerId),
Back,
FocusLeft,
FocusRight,
FetchLyrics,
ToggleMultiselect,
AddToQueue,
AddToPlaylist,
DeleteFromList,
JumpToEnd,
EditMetadata,
Quit,
QuitDaemon,
ToggleHelp,
HideHelpBar,
CycleFooterPreset,
CycleProgressStyle,
CycleDesign,
ToggleVisualizer,
CycleVisualizerPreset,
CheckHealth,
}
#[derive(Debug, Clone)]
pub struct BoundCommand {
pub action: KeyboardAction,
pub contexts: Vec<KeyContext>,
}
#[derive(Debug, Clone)]
pub struct Keybindings {
pub bindings: Vec<(KeyEvent, BoundCommand)>,
}
impl Keybindings {
pub fn dispatch(&self, key: KeyEvent, context: KeyContext) -> Option<KeyboardAction> {
for (binding_key, cmd) in &self.bindings {
if key_matches(&key, binding_key) && cmd.contexts.contains(&context) {
return Some(cmd.action.clone());
}
}
None
}
}
fn key_matches(event: &KeyEvent, binding: &KeyEvent) -> bool {
event.code == binding.code && event.modifiers == binding.modifiers
}
pub fn default_keybindings() -> Keybindings {
Keybindings {
bindings: vec![
(
KeyCode::Char('q').into(),
BoundCommand {
action: KeyboardAction::Quit,
contexts: vec![KeyContext::Global, KeyContext::Normal],
},
),
(
KeyCode::Char('?').into(),
BoundCommand {
action: KeyboardAction::ToggleHelp,
contexts: vec![KeyContext::Global, KeyContext::Normal],
},
),
(
KeyEvent::new(KeyCode::Char('h'), KeyModifiers::CONTROL),
BoundCommand {
action: KeyboardAction::HideHelpBar,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char(':').into(),
BoundCommand {
action: KeyboardAction::OpenOverlay(PickerId::CommandPalette),
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Tab.into(),
BoundCommand {
action: KeyboardAction::NextTab,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::BackTab.into(),
BoundCommand {
action: KeyboardAction::PrevTab,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char('1').into(),
BoundCommand {
action: KeyboardAction::SwitchTab(Tab::Library),
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char('2').into(),
BoundCommand {
action: KeyboardAction::SwitchTab(Tab::Settings),
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Up.into(),
BoundCommand {
action: KeyboardAction::MoveUp,
contexts: vec![KeyContext::List, KeyContext::Normal],
},
),
(
KeyCode::Down.into(),
BoundCommand {
action: KeyboardAction::MoveDown,
contexts: vec![KeyContext::List, KeyContext::Normal],
},
),
(
KeyCode::Char('k').into(),
BoundCommand {
action: KeyboardAction::MoveUp,
contexts: vec![KeyContext::List, KeyContext::Normal],
},
),
(
KeyCode::Char('j').into(),
BoundCommand {
action: KeyboardAction::MoveDown,
contexts: vec![KeyContext::List, KeyContext::Normal],
},
),
(
KeyCode::PageUp.into(),
BoundCommand {
action: KeyboardAction::PageUp,
contexts: vec![KeyContext::List, KeyContext::Normal],
},
),
(
KeyCode::PageDown.into(),
BoundCommand {
action: KeyboardAction::PageDown,
contexts: vec![KeyContext::List, KeyContext::Normal],
},
),
(
KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL),
BoundCommand {
action: KeyboardAction::PageUp,
contexts: vec![KeyContext::Normal],
},
),
(
KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL),
BoundCommand {
action: KeyboardAction::PageDown,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Home.into(),
BoundCommand {
action: KeyboardAction::Top,
contexts: vec![KeyContext::List, KeyContext::Normal],
},
),
(
KeyCode::End.into(),
BoundCommand {
action: KeyboardAction::Bottom,
contexts: vec![KeyContext::List, KeyContext::Normal],
},
),
(
KeyCode::Char(' ').into(),
BoundCommand {
action: KeyboardAction::PlayPause,
contexts: vec![KeyContext::Global, KeyContext::Normal],
},
),
(
KeyCode::Char('n').into(),
BoundCommand {
action: KeyboardAction::Next,
contexts: vec![KeyContext::Global, KeyContext::Normal],
},
),
(
KeyCode::Char('p').into(),
BoundCommand {
action: KeyboardAction::Prev,
contexts: vec![KeyContext::Global, KeyContext::Normal],
},
),
(
KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL),
BoundCommand {
action: KeyboardAction::Next,
contexts: vec![KeyContext::Global, KeyContext::Normal],
},
),
(
KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL),
BoundCommand {
action: KeyboardAction::Prev,
contexts: vec![KeyContext::Global, KeyContext::Normal],
},
),
(
KeyCode::Char('s').into(),
BoundCommand {
action: KeyboardAction::Stop,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char('+').into(),
BoundCommand {
action: KeyboardAction::VolumeUp,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char('=').into(),
BoundCommand {
action: KeyboardAction::VolumeUp,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char('-').into(),
BoundCommand {
action: KeyboardAction::VolumeDown,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char('m').into(),
BoundCommand {
action: KeyboardAction::ToggleMute,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char('Q').into(),
BoundCommand {
action: KeyboardAction::QuitDaemon,
contexts: vec![KeyContext::Global, KeyContext::Normal],
},
),
(
KeyEvent::new(KeyCode::Char('q'), KeyModifiers::CONTROL),
BoundCommand {
action: KeyboardAction::QuitDaemon,
contexts: vec![KeyContext::Global, KeyContext::Normal],
},
),
(
KeyCode::Char('F').into(),
BoundCommand {
action: KeyboardAction::ToggleFavourite,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char('D').into(),
BoundCommand {
action: KeyboardAction::ClearQueue,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char('[').into(),
BoundCommand {
action: KeyboardAction::FocusLeft,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char(']').into(),
BoundCommand {
action: KeyboardAction::FocusRight,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char('l').into(),
BoundCommand {
action: KeyboardAction::FetchLyrics,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char('r').into(),
BoundCommand {
action: KeyboardAction::CycleRepeat,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char('R').into(),
BoundCommand {
action: KeyboardAction::CycleRepeat,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char('S').into(),
BoundCommand {
action: KeyboardAction::ToggleShuffle,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char('.').into(),
BoundCommand {
action: KeyboardAction::SeekForward,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char(',').into(),
BoundCommand {
action: KeyboardAction::SeekBackward,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char('/').into(),
BoundCommand {
action: KeyboardAction::EnterFilter,
contexts: vec![KeyContext::Normal],
},
),
(
KeyEvent::new(KeyCode::Char('f'), KeyModifiers::CONTROL),
BoundCommand {
action: KeyboardAction::EnterFilter,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Enter.into(),
BoundCommand {
action: KeyboardAction::Select,
contexts: vec![KeyContext::List, KeyContext::Normal],
},
),
(
KeyCode::Backspace.into(),
BoundCommand {
action: KeyboardAction::Back,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Delete.into(),
BoundCommand {
action: KeyboardAction::Delete,
contexts: vec![KeyContext::List, KeyContext::Normal],
},
),
(
KeyCode::Char('d').into(),
BoundCommand {
action: KeyboardAction::Delete,
contexts: vec![KeyContext::List, KeyContext::Normal],
},
),
(
KeyEvent::new(KeyCode::Char('q'), KeyModifiers::ALT),
BoundCommand {
action: KeyboardAction::OpenOverlay(PickerId::Queue),
contexts: vec![KeyContext::Normal],
},
),
(
KeyEvent::new(KeyCode::Char('y'), KeyModifiers::ALT),
BoundCommand {
action: KeyboardAction::OpenOverlay(PickerId::YTSearch),
contexts: vec![KeyContext::Normal],
},
),
(
KeyEvent::new(KeyCode::Char('f'), KeyModifiers::ALT),
BoundCommand {
action: KeyboardAction::OpenOverlay(PickerId::SearchLibrary),
contexts: vec![KeyContext::Normal],
},
),
(
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::ALT),
BoundCommand {
action: KeyboardAction::OpenOverlay(PickerId::About),
contexts: vec![KeyContext::Normal],
},
),
(
KeyEvent::new(KeyCode::Char('z'), KeyModifiers::ALT),
BoundCommand {
action: KeyboardAction::OpenOverlay(PickerId::SleepTimer),
contexts: vec![KeyContext::Normal],
},
),
(
KeyEvent::new(KeyCode::Char('c'), KeyModifiers::ALT),
BoundCommand {
action: KeyboardAction::OpenOverlay(PickerId::ThemePicker),
contexts: vec![KeyContext::Normal],
},
),
(
KeyEvent::new(KeyCode::Char('e'), KeyModifiers::ALT),
BoundCommand {
action: KeyboardAction::OpenOverlay(PickerId::Equalizer),
contexts: vec![KeyContext::Normal],
},
),
(
KeyEvent::new(KeyCode::Char('p'), KeyModifiers::ALT),
BoundCommand {
action: KeyboardAction::OpenOverlay(PickerId::CommandPalette),
contexts: vec![KeyContext::Normal],
},
),
(
KeyEvent::new(KeyCode::Char('s'), KeyModifiers::ALT),
BoundCommand {
action: KeyboardAction::OpenOverlay(PickerId::SpotifySearch),
contexts: vec![KeyContext::Normal],
},
),
(
KeyEvent::new(KeyCode::Char('x'), KeyModifiers::ALT),
BoundCommand {
action: KeyboardAction::OpenOverlay(PickerId::SoundEffects),
contexts: vec![KeyContext::Normal],
},
),
(
KeyEvent::new(KeyCode::Char('F'), KeyModifiers::ALT),
BoundCommand {
action: KeyboardAction::CycleFooterPreset,
contexts: vec![KeyContext::Normal],
},
),
(
KeyEvent::new(KeyCode::Char('D'), KeyModifiers::ALT),
BoundCommand {
action: KeyboardAction::CycleDesign,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char('v').into(),
BoundCommand {
action: KeyboardAction::ToggleMultiselect,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char('a').into(),
BoundCommand {
action: KeyboardAction::AddToQueue,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char('A').into(),
BoundCommand {
action: KeyboardAction::AddToPlaylist,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char('x').into(),
BoundCommand {
action: KeyboardAction::DeleteFromList,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char('G').into(),
BoundCommand {
action: KeyboardAction::JumpToEnd,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char('e').into(),
BoundCommand {
action: KeyboardAction::EditMetadata,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char('P').into(),
BoundCommand {
action: KeyboardAction::CycleProgressStyle,
contexts: vec![KeyContext::Normal],
},
),
(
KeyEvent::new(KeyCode::Char('v'), KeyModifiers::CONTROL),
BoundCommand {
action: KeyboardAction::ToggleVisualizer,
contexts: vec![KeyContext::Normal],
},
),
(
KeyEvent::new(KeyCode::Char('V'), KeyModifiers::ALT),
BoundCommand {
action: KeyboardAction::CycleVisualizerPreset,
contexts: vec![KeyContext::Normal],
},
),
(
KeyCode::Char(':').into(),
BoundCommand {
action: KeyboardAction::CheckHealth,
contexts: vec![KeyContext::Normal],
},
),
],
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dispatch(key: KeyEvent, ctx: KeyContext) -> Option<KeyboardAction> {
default_keybindings().dispatch(key, ctx)
}
#[test]
fn delete_uses_lowercase_d_and_del() {
assert!(matches!(
dispatch(KeyCode::Char('d').into(), KeyContext::Normal),
Some(KeyboardAction::Delete)
));
assert!(matches!(
dispatch(KeyCode::Delete.into(), KeyContext::Normal),
Some(KeyboardAction::Delete)
));
assert!(matches!(
dispatch(KeyCode::Char('D').into(), KeyContext::Normal),
Some(KeyboardAction::ClearQueue)
));
}
#[test]
fn numbered_tabs_switch_views() {
assert!(matches!(
dispatch(KeyCode::Char('1').into(), KeyContext::Normal),
Some(KeyboardAction::SwitchTab(Tab::Library))
));
assert!(matches!(
dispatch(KeyCode::Char('2').into(), KeyContext::Normal),
Some(KeyboardAction::SwitchTab(Tab::Settings))
));
}
#[test]
fn quit_keyboard_actions() {
assert!(matches!(
dispatch(KeyCode::Char('q').into(), KeyContext::Global),
Some(KeyboardAction::Quit)
));
assert!(matches!(
dispatch(KeyCode::Char('Q').into(), KeyContext::Global),
Some(KeyboardAction::QuitDaemon)
));
}
}