diff --git a/.config/config.toml b/.config/config.toml index 19a389d..e8b9c1b 100644 --- a/.config/config.toml +++ b/.config/config.toml @@ -72,7 +72,7 @@ theme = "TwoDark" # # Channel mode # ------------------------ -[keybindings.Channel] +[keybindings] # Quit the application quit = ["esc", "ctrl-c"] # Scrolling through entries @@ -101,46 +101,6 @@ toggle_help = "ctrl-g" toggle_preview = "ctrl-o" -# Remote control mode -# ------------------------------- -[keybindings.RemoteControl] -# Quit the application -quit = "esc" -# Scrolling through entries -select_next_entry = ["down", "ctrl-n", "ctrl-j"] -select_prev_entry = ["up", "ctrl-p", "ctrl-k"] -select_next_page = "pagedown" -select_prev_page = "pageup" -# Select an entry -select_entry = "enter" -# Toggle the remote control mode -toggle_remote_control = "ctrl-r" -# Toggle the help bar -toggle_help = "ctrl-g" -# Toggle the preview panel -toggle_preview = "ctrl-o" - - -# Send to channel mode -# -------------------------------- -[keybindings.SendToChannel] -# Quit the application -quit = "esc" -# Scrolling through entries -select_next_entry = ["down", "ctrl-n", "ctrl-j"] -select_prev_entry = ["up", "ctrl-p", "ctrl-k"] -select_next_page = "pagedown" -select_prev_page = "pageup" -# Select an entry -select_entry = "enter" -# Toggle the send to channel mode -toggle_send_to_channel = "ctrl-s" -# Toggle the help bar -toggle_help = "ctrl-g" -# Toggle the preview panel -toggle_preview = "ctrl-o" - - # Shell integration # ---------------------------------------------------------------------------- # diff --git a/television/app.rs b/television/app.rs index daad908..55e26c6 100644 --- a/television/app.rs +++ b/television/app.rs @@ -6,7 +6,9 @@ use tracing::{debug, info, trace}; use crate::channels::entry::Entry; use crate::channels::TelevisionChannel; -use crate::config::{parse_key, Config}; +use crate::config::{ + merge_keybindings, parse_key, Binding, Config, KeyBindings, +}; use crate::keymap::Keymap; use crate::render::UiState; use crate::television::{Mode, Television}; @@ -109,16 +111,22 @@ impl App { let (_, event_rx) = mpsc::unbounded_channel(); let (event_abort_tx, _) = mpsc::unbounded_channel(); let tick_rate = config.config.tick_rate; - let keymap = Keymap::from(&config.keybindings).with_mode_mappings( - Mode::Channel, - passthrough_keybindings - .iter() - .flat_map(|s| match parse_key(s) { - Ok(key) => Ok((key, Action::SelectPassthrough(s.clone()))), - Err(e) => Err(e), - }) - .collect(), - ); + let keybindings = merge_keybindings(config.keybindings.clone(), { + &KeyBindings::from(passthrough_keybindings.iter().filter_map( + |s| match parse_key(s) { + Ok(key) => Some(( + Action::SelectPassthrough(s.to_string()), + Binding::SingleKey(key), + )), + Err(e) => { + debug!("Failed to parse keybinding: {}", e); + None + } + }, + )) + }); + let keymap = Keymap::from(&keybindings); + debug!("{:?}", keymap); let (ui_state_tx, ui_state_rx) = mpsc::unbounded_channel(); let television = @@ -258,14 +266,13 @@ impl App { _ => {} } // get action based on keybindings - self.keymap - .get(&self.television.mode) - .and_then(|keymap| keymap.get(&keycode).cloned()) - .unwrap_or(if let Key::Char(c) = keycode { + self.keymap.get(&keycode).cloned().unwrap_or( + if let Key::Char(c) = keycode { Action::AddInputChar(c) } else { Action::NoOp - }) + }, + ) } // terminal events Event::Tick => Action::Tick, diff --git a/television/config/keybindings.rs b/television/config/keybindings.rs index ed5cd59..2615509 100644 --- a/television/config/keybindings.rs +++ b/television/config/keybindings.rs @@ -1,6 +1,5 @@ use crate::action::Action; use crate::event::{convert_raw_event_to_key, Key}; -use crate::television::Mode; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use rustc_hash::FxHashMap; use serde::{Deserialize, Deserializer}; @@ -30,7 +29,16 @@ impl Display for Binding { } #[derive(Clone, Debug, Default, PartialEq)] -pub struct KeyBindings(pub FxHashMap>); +pub struct KeyBindings(pub FxHashMap); + +impl From for KeyBindings +where + I: IntoIterator, +{ + fn from(iter: I) -> Self { + KeyBindings(iter.into_iter().collect()) + } +} impl Hash for KeyBindings { fn hash(&self, state: &mut H) { @@ -40,7 +48,7 @@ impl Hash for KeyBindings { } impl Deref for KeyBindings { - type Target = FxHashMap>; + type Target = FxHashMap; fn deref(&self) -> &Self::Target { &self.0 } @@ -52,27 +60,20 @@ impl DerefMut for KeyBindings { } } +/// Merge two sets of keybindings together. +/// +/// Note that this function won't "meld", for a given action, the bindings from the first set +/// with the bindings from the second set. Instead, it will simply overwrite them with the second +/// set's keys. +/// This is because it is assumed that the second set will be the user's custom keybindings, and +/// they should take precedence over the default ones, effectively replacing them to avoid +/// conflicts. pub fn merge_keybindings( mut keybindings: KeyBindings, new_keybindings: &KeyBindings, ) -> KeyBindings { - for (mode, bindings) in new_keybindings.iter() { - for (action, binding) in bindings { - match keybindings.get_mut(mode) { - Some(mode_bindings) => { - mode_bindings.insert(action.clone(), binding.clone()); - } - None => { - keybindings.insert( - *mode, - [(action.clone(), binding.clone())] - .iter() - .cloned() - .collect(), - ); - } - } - } + for (action, binding) in new_keybindings.iter() { + keybindings.insert(action.clone(), binding.clone()); } keybindings } @@ -89,43 +90,30 @@ impl<'de> Deserialize<'de> for KeyBindings { where D: Deserializer<'de>, { - let parsed_map = FxHashMap::< - Mode, - FxHashMap, - >::deserialize(deserializer)?; + let parsed_map = + FxHashMap::::deserialize(deserializer)?; - let keybindings: FxHashMap> = - parsed_map - .into_iter() - .map(|(mode, inner_map)| { - let converted_inner_map = inner_map - .into_iter() - .map(|(cmd, binding)| { - ( - cmd, - match binding { - SerializedBinding::SingleKey(key_str) => { - Binding::SingleKey( - parse_key(&key_str).unwrap(), - ) - } - SerializedBinding::MultipleKeys( - keys_str, - ) => Binding::MultipleKeys( - keys_str - .iter() - .map(|key_str| { - parse_key(key_str).unwrap() - }) - .collect(), - ), - }, + let keybindings: FxHashMap = parsed_map + .into_iter() + .map(|(cmd, binding)| { + ( + cmd, + match binding { + SerializedBinding::SingleKey(key_str) => { + Binding::SingleKey(parse_key(&key_str).unwrap()) + } + SerializedBinding::MultipleKeys(keys_str) => { + Binding::MultipleKeys( + keys_str + .iter() + .map(|key_str| parse_key(key_str).unwrap()) + .collect(), ) - }) - .collect(); - (mode, converted_inner_map) - }) - .collect(); + } + }, + ) + }) + .collect(); Ok(KeyBindings(keybindings)) } @@ -380,4 +368,127 @@ mod tests { KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT) ); } + + #[test] + fn test_deserialize_keybindings() { + let keybindings: KeyBindings = toml::from_str( + r#" + # Quit the application + quit = ["esc", "ctrl-c"] + # Scrolling through entries + select_next_entry = ["down", "ctrl-n", "ctrl-j"] + select_prev_entry = ["up", "ctrl-p", "ctrl-k"] + select_next_page = "pagedown" + select_prev_page = "pageup" + # Scrolling the preview pane + scroll_preview_half_page_down = "ctrl-d" + scroll_preview_half_page_up = "ctrl-u" + # Add entry to selection and move to the next entry + toggle_selection_down = "tab" + # Add entry to selection and move to the previous entry + toggle_selection_up = "backtab" + # Confirm selection + confirm_selection = "enter" + # Copy the selected entry to the clipboard + copy_entry_to_clipboard = "ctrl-y" + # Toggle the remote control mode + toggle_remote_control = "ctrl-r" + # Toggle the send to channel mode + toggle_send_to_channel = "ctrl-s" + # Toggle the help bar + toggle_help = "ctrl-g" + # Toggle the preview panel + toggle_preview = "ctrl-o" + "#, + ) + .unwrap(); + + assert_eq!( + keybindings, + KeyBindings::from(vec![ + ( + Action::Quit, + Binding::MultipleKeys(vec![Key::Esc, Key::Ctrl('c'),]) + ), + ( + Action::SelectNextEntry, + Binding::MultipleKeys(vec![ + Key::Down, + Key::Ctrl('n'), + Key::Ctrl('j'), + ]) + ), + ( + Action::SelectPrevEntry, + Binding::MultipleKeys(vec![ + Key::Up, + Key::Ctrl('p'), + Key::Ctrl('k'), + ]) + ), + (Action::SelectNextPage, Binding::SingleKey(Key::PageDown)), + (Action::SelectPrevPage, Binding::SingleKey(Key::PageUp)), + ( + Action::ScrollPreviewHalfPageDown, + Binding::SingleKey(Key::Ctrl('d')) + ), + ( + Action::ScrollPreviewHalfPageUp, + Binding::SingleKey(Key::Ctrl('u')) + ), + (Action::ToggleSelectionDown, Binding::SingleKey(Key::Tab)), + (Action::ToggleSelectionUp, Binding::SingleKey(Key::BackTab)), + (Action::ConfirmSelection, Binding::SingleKey(Key::Enter)), + ( + Action::CopyEntryToClipboard, + Binding::SingleKey(Key::Ctrl('y')) + ), + ( + Action::ToggleRemoteControl, + Binding::SingleKey(Key::Ctrl('r')) + ), + ( + Action::ToggleSendToChannel, + Binding::SingleKey(Key::Ctrl('s')) + ), + (Action::ToggleHelp, Binding::SingleKey(Key::Ctrl('g'))), + (Action::TogglePreview, Binding::SingleKey(Key::Ctrl('o'))), + ]) + ); + } + + #[test] + fn test_merge_keybindings() { + let base_keybindings = KeyBindings::from(vec![ + (Action::Quit, Binding::SingleKey(Key::Esc)), + ( + Action::SelectNextEntry, + Binding::MultipleKeys(vec![Key::Down, Key::Ctrl('n')]), + ), + (Action::SelectPrevEntry, Binding::SingleKey(Key::Up)), + ]); + let custom_keybindings = KeyBindings::from(vec![ + (Action::SelectNextEntry, Binding::SingleKey(Key::Ctrl('j'))), + ( + Action::SelectPrevEntry, + Binding::MultipleKeys(vec![Key::Up, Key::Ctrl('k')]), + ), + (Action::SelectNextPage, Binding::SingleKey(Key::PageDown)), + ]); + + let merged = merge_keybindings(base_keybindings, &custom_keybindings); + + assert_eq!( + merged, + KeyBindings::from(vec![ + (Action::Quit, Binding::SingleKey(Key::Esc)), + (Action::SelectNextEntry, Binding::SingleKey(Key::Ctrl('j'))), + ( + Action::SelectPrevEntry, + Binding::MultipleKeys(vec![Key::Up, Key::Ctrl('k')]), + ), + (Action::SelectNextPage, Binding::SingleKey(Key::PageDown)), + ]) + ); + } } diff --git a/television/config/mod.rs b/television/config/mod.rs index e80dc1f..fc372df 100644 --- a/television/config/mod.rs +++ b/television/config/mod.rs @@ -7,7 +7,7 @@ use std::{ use anyhow::{Context, Result}; use directories::ProjectDirs; -use keybindings::merge_keybindings; +pub use keybindings::merge_keybindings; pub use keybindings::{parse_key, Binding, KeyBindings}; use previewers::PreviewersConfig; use serde::Deserialize; @@ -100,6 +100,17 @@ pub fn default_config_from_file() -> Result { Ok(default_config) } +const USER_CONFIG_ERROR_MSG: &str = " +╔══════════════════════════════════════════════════════════════════════════════╗ +║ ║ +║ If this follows a recent update, it is likely due to a breaking change in ║ +║ the configuration format. ║ +║ ║ +║ Check https://github.com/alexpasmantier/television/releases/latest for the ║ +║ latest release notes. ║ +║ ║ +╚══════════════════════════════════════════════════════════════════════════════╝"; + impl Config { #[allow(clippy::missing_panics_doc, clippy::missing_errors_doc)] pub fn new(config_env: &ConfigEnv) -> Result { @@ -134,8 +145,11 @@ impl Config { fn load_user_config(config_dir: &Path) -> Result { let path = config_dir.join(CONFIG_FILE_NAME); let contents = std::fs::read_to_string(&path)?; - let user_cfg: Config = toml::from_str(&contents) - .context("Error parsing configuration file.")?; + let user_cfg: Config = toml::from_str(&contents).context(format!( + "Error parsing configuration file: {}\n{}", + path.display(), + USER_CONFIG_ERROR_MSG, + ))?; Ok(user_cfg) } @@ -246,7 +260,6 @@ fn default_tick_rate() -> f64 { mod tests { use crate::action::Action; use crate::event::Key; - use crate::television::Mode; use super::*; use rustc_hash::FxHashMap; @@ -322,11 +335,9 @@ mod tests { [previewers.file] theme = "Visual Studio Dark" - [keybindings.Channel] + [keybindings] toggle_help = ["ctrl-a", "ctrl-b"] - - [keybindings.RemoteControl] - toggle_help = ["ctrl-c", "ctrl-d"] + confirm_selection = "ctrl-enter" [shell_integration.commands] "git add" = "git-diff" @@ -358,36 +369,18 @@ mod tests { default_config.ui.theme = "television".to_string(); default_config.previewers.file.theme = "Visual Studio Dark".to_string(); - default_config - .keybindings - .get_mut(&Mode::Channel) - .unwrap() - .extend({ - let mut map = FxHashMap::default(); - map.insert( - Action::ToggleHelp, - Binding::MultipleKeys(vec![ - Key::Ctrl('a'), - Key::Ctrl('b'), - ]), - ); - map - }); - default_config - .keybindings - .get_mut(&Mode::RemoteControl) - .unwrap() - .extend({ - let mut map = FxHashMap::default(); - map.insert( - Action::ToggleHelp, - Binding::MultipleKeys(vec![ - Key::Ctrl('c'), - Key::Ctrl('d'), - ]), - ); - map - }); + default_config.keybindings.extend({ + let mut map = FxHashMap::default(); + map.insert( + Action::ToggleHelp, + Binding::MultipleKeys(vec![Key::Ctrl('a'), Key::Ctrl('b')]), + ); + map.insert( + Action::ConfirmSelection, + Binding::SingleKey(Key::CtrlEnter), + ); + map + }); default_config .shell_integration diff --git a/television/draw.rs b/television/draw.rs index 5c93690..82c88ed 100644 --- a/television/draw.rs +++ b/television/draw.rs @@ -193,16 +193,12 @@ pub fn draw(ctx: &Ctx, f: &mut Frame<'_>, area: Rect) -> Result { &ctx.colorscheme, &ctx.config .keybindings - .get(&ctx.tv_state.mode) - .unwrap() .get(&Action::ToggleHelp) // just display the first keybinding .unwrap() .to_string(), &ctx.config .keybindings - .get(&ctx.tv_state.mode) - .unwrap() .get(&Action::TogglePreview) // just display the first keybinding .unwrap() diff --git a/television/keymap.rs b/television/keymap.rs index 9cb9253..2a60de3 100644 --- a/television/keymap.rs +++ b/television/keymap.rs @@ -1,8 +1,6 @@ use rustc_hash::FxHashMap; use std::ops::Deref; -use crate::television::Mode; - use crate::action::Action; use crate::config::{Binding, KeyBindings}; use crate::event::Key; @@ -13,20 +11,15 @@ use crate::event::Key; /// # Example: /// ```ignore /// Keymap { -/// Mode::Channel => { -/// Key::Char('j') => Action::MoveDown, -/// Key::Char('k') => Action::MoveUp, -/// Key::Char('q') => Action::Quit, -/// }, -/// Mode::Insert => { -/// Key::Ctrl('a') => Action::MoveToStart, -/// }, +/// Key::Char('j') => Action::MoveDown, +/// Key::Char('k') => Action::MoveUp, +/// Key::Char('q') => Action::Quit, /// } /// ``` -pub struct Keymap(pub FxHashMap>); +pub struct Keymap(pub FxHashMap); impl Deref for Keymap { - type Target = FxHashMap>; + type Target = FxHashMap; fn deref(&self) -> &Self::Target { &self.0 } @@ -40,38 +33,18 @@ impl From<&KeyBindings> for Keymap { /// key events. fn from(keybindings: &KeyBindings) -> Self { let mut keymap = FxHashMap::default(); - for (mode, bindings) in keybindings.iter() { - let mut mode_keymap = FxHashMap::default(); - for (action, binding) in bindings { - match binding { - Binding::SingleKey(key) => { - mode_keymap.insert(*key, action.clone()); - } - Binding::MultipleKeys(keys) => { - for key in keys { - mode_keymap.insert(*key, action.clone()); - } + for (action, binding) in keybindings.iter() { + match binding { + Binding::SingleKey(key) => { + keymap.insert(*key, action.clone()); + } + Binding::MultipleKeys(keys) => { + for key in keys { + keymap.insert(*key, action.clone()); } } } - keymap.insert(*mode, mode_keymap); } Self(keymap) } } - -impl Keymap { - /// For a provided `Mode`, merge the given `mappings` into the keymap. - pub fn with_mode_mappings( - mut self, - mode: Mode, - mappings: Vec<(Key, Action)>, - ) -> Self { - let mode_keymap = self.0.entry(mode).or_default(); - - for (key, action) in mappings { - mode_keymap.insert(key, action); - } - self - } -} diff --git a/television/screen/keybindings.rs b/television/screen/keybindings.rs index 3f1ea46..0b362a4 100644 --- a/television/screen/keybindings.rs +++ b/television/screen/keybindings.rs @@ -146,15 +146,7 @@ fn serialized_keys_for_actions( ) -> Vec { actions .iter() - .map(|a| { - keybindings - .get(&Mode::Channel) - .unwrap() - .get(a) - .unwrap() - .clone() - .to_string() - }) + .map(|a| keybindings.get(a).unwrap().clone().to_string()) .collect() }