From c97c553870380956904ab7d2daa36e5ce0aef010 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Fri, 5 Mar 2021 22:40:42 +0100 Subject: [PATCH] feature(input): add config file * use a simple platform dependent config location `ProjectDir` * deserialise from yaml iterate more on config format, maybe be more verbose? * make keybinds a tuple struct same size as newtype, but makes impls easier to add * merge optionally multiple keys for one keybinding easier configuration --- config.yaml | 14 +- src/common/config.rs | 116 +++-- src/common/input/actions.rs | 6 +- src/common/input/handler.rs | 16 +- src/common/input/keybinds.rs | 450 +++++++++++------- src/common/input/macros.rs | 12 - src/common/input/mod.rs | 3 +- src/common/input/ut/config_test.rs | 51 ++ src/common/input/ut/keybinds_test.rs | 0 src/common/mod.rs | 8 +- .../config/functioning_simple_config.yaml | 0 .../config/multiple_keys_for_one_action.yaml | 8 + 12 files changed, 430 insertions(+), 254 deletions(-) delete mode 100644 src/common/input/macros.rs create mode 100644 src/common/input/ut/config_test.rs create mode 100644 src/common/input/ut/keybinds_test.rs create mode 100644 src/tests/fixtures/config/functioning_simple_config.yaml create mode 100644 src/tests/fixtures/config/multiple_keys_for_one_action.yaml diff --git a/config.yaml b/config.yaml index b1657775..719d15a2 100644 --- a/config.yaml +++ b/config.yaml @@ -1,8 +1,12 @@ --- -macro: - {name:"closePane", sequence: [NewPane: Right,]} - keybinds: Normal: - Backspace: [NewPane:, NewPane:,] - {F: 1}: [NewPane:,] + - {F: 1}: [GoToTab: 1,] + - {F: 2}: [GoToTab: 2,] + - {F: 3}: [GoToTab: 3,] + - {F: 4}: [GoToTab: 4,] + - {F: 5}: [NewTab: ,] + - ? - F: 6 + - F: 7 + : - {GoToTab: 5} + - Backspace: [NewPane: Right, NewPane: Right,] diff --git a/src/common/config.rs b/src/common/config.rs index 705c1592..bf8a9a9f 100644 --- a/src/common/config.rs +++ b/src/common/config.rs @@ -1,104 +1,101 @@ //! Deserializes configuration options. use std; -//use std::collections::HashMap; use std::error; use std::fmt::{self, Display}; use std::fs::File; use std::io::{self, Read}; use std::path::PathBuf; -use super::input::{keybinds, macros}; +use super::input::keybinds::{Keybinds, MultipleKeybinds}; +use crate::utils::logging::*; use directories_next::ProjectDirs; use serde::Deserialize; -/// Intermediate struct -//pub struct KeybingsFromYaml { - -//} - -/// Intermediate struct +/// Intermediate deserialisation config struct #[derive(Debug, Deserialize)] pub struct ConfigFromYaml { - keybinds: Option, - macros: Option>, + pub keybinds: Option, } -///// Deserialized config state -#[derive(Debug, Clone, Default, Deserialize)] +/// Main configuration. +#[derive(Debug, Clone, PartialEq)] pub struct Config { - pub keybinds: keybinds::Keybinds, + pub keybinds: Keybinds, } #[derive(Debug)] pub enum ConfigError { - // from the serde documentation - // https://serde.rs/error-handling.html - // One or more variants that can be created by data structures through the - // `ser::Error` and `de::Error` traits. For example the Serialize impl for - // Mutex might return an error because the mutex is poisoned, or the - // Deserialize impl for a struct may return an error because a required - // field is missing. - //Message(String), - // serde_yaml error + // Deserialisation error Serde(serde_yaml::Error), - //Eof, - // io::Error + // Io error Io(io::Error), + // Io error with path context + IoPath(io::Error, PathBuf), +} + +impl Default for Config { + fn default() -> Self { + let keybinds = Keybinds::default(); + Config { keybinds } + } } impl Config { - /// Deserializes from given path - pub fn new(path: &PathBuf) -> Result { - let config: Config; - let config_deserialized: ConfigFromYaml; - let mut config_string = String::new(); + /// Uses defaults, but lets config override them. + pub fn from_yaml(yaml_config: &str) -> Result { + let config_from_yaml: ConfigFromYaml = serde_yaml::from_str(&yaml_config)?; + let keybinds = Keybinds::get_default_keybinds_with_config(config_from_yaml.keybinds); + Ok(Config { keybinds }) + } - // TODO fix this unwrap + /// Deserializes from given path. + /// The allow is here, because rust assumes there is no + /// error handling when logging the error to the log file. + #[allow(unused_must_use)] + pub fn new(path: &PathBuf) -> Result { match File::open(path) { Ok(mut file) => { - file.read_to_string(&mut config_string)?; - config_deserialized = serde_yaml::from_str(&config_string)?; - config = Config { - keybinds: config_deserialized - .keybinds - .unwrap_or_else(|| keybinds::get_default_keybinds().unwrap()), - } + let mut yaml_config = String::new(); + file.read_to_string(&mut yaml_config) + .map_err(|e| ConfigError::IoPath(e, path.to_path_buf()))?; + Ok(Config::from_yaml(&yaml_config)?) } Err(e) => { // TODO logging, if a file is not found // at an expected position - should not - // panic @a-kenji - eprintln!("{}", e); - config = Config::default(); + // panic, but log to file @a-kenji + debug_log_to_file(format!( + "{}\nUsing the default configuration!", + ConfigError::IoPath(e, path.to_path_buf()) + )); + Ok(Config::default()) } } - Ok(config) } + /// Deserializes the config from an optional path, or a platform specific path, + /// merges the default configuration - options take precedence. pub fn from_option_or_default(option: Option) -> Result { - let config; if let Some(config_path) = option { - config = Config::new(&config_path)?; + Ok(Config::new(&config_path)?) } else { - let project_dirs = ProjectDirs::from("org", "Zellij Contributors", "Zellij").unwrap(); - //let config_path = PathBuf::from(project_dirs.config_dir().as_os_str()); - let config_path = project_dirs.config_dir().to_owned().into(); - config = Config::new(&config_path)?; + let project_dirs = ProjectDirs::from("org", "Zellij Contributors", "Zellij").unwrap(); + let mut config_path: PathBuf = project_dirs.config_dir().to_owned().into(); + config_path.push("config.yaml"); + Ok(Config::new(&config_path)?) } - return Ok(config); } } impl Display for ConfigError { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { match self { - //ConfigError::Message(msg) => formatter.write_str(msg), - //ConfigError::Eof => formatter.write_str("unexpected end of input"), - // - ConfigError::Io(ref err) => write!(formatter, "Io error: {}", err), - ConfigError::Serde(ref err) => write!(formatter, "Serde error: {}", err), - /* and so forth */ + ConfigError::Io(ref err) => write!(formatter, "IoError: {}", err), + ConfigError::IoPath(ref err, ref path) => { + write!(formatter, "IoError: {}, File: {}", err, path.display(),) + } + ConfigError::Serde(ref err) => write!(formatter, "Deserialisation error: {}", err), } } } @@ -106,12 +103,8 @@ impl Display for ConfigError { impl std::error::Error for ConfigError { fn cause(&self) -> Option<&dyn error::Error> { match *self { - // N.B. Both of these implicitly cast `err` from their concrete - // types (either `&io::Error` or `&num::ParseIntError`) - // to a trait object `&Error`. This works because both error types - // implement `Error`. ConfigError::Io(ref err) => Some(err), - //ConfigError::Message(ref err) => Some(err), + ConfigError::IoPath(ref err, _) => Some(err), ConfigError::Serde(ref err) => Some(err), } } @@ -128,3 +121,8 @@ impl From for ConfigError { ConfigError::Serde(err) } } + +// The unit test location. +#[cfg(test)] +#[path = "./input/ut/config_test.rs"] +mod config_test; diff --git a/src/common/input/actions.rs b/src/common/input/actions.rs index d03ca859..6d5c5892 100644 --- a/src/common/input/actions.rs +++ b/src/common/input/actions.rs @@ -2,10 +2,10 @@ use super::handler; //use super::macros; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; /// The four directions (left, right, up, down). -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub enum Direction { Left, Right, @@ -14,7 +14,7 @@ pub enum Direction { } /// Actions that can be bound to keys. -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub enum Action { /// Quit Zellij. Quit, diff --git a/src/common/input/handler.rs b/src/common/input/handler.rs index 9d8b2cf2..9b08491b 100644 --- a/src/common/input/handler.rs +++ b/src/common/input/handler.rs @@ -1,7 +1,7 @@ //! Main input logic. use super::actions::Action; -use super::keybinds::get_default_keybinds; +use super::keybinds::Keybinds; use crate::common::config::Config; use crate::common::{update_state, AppInstruction, AppState, SenderWithContext, OPENCALLS}; use crate::errors::ContextType; @@ -15,8 +15,6 @@ use serde::{Deserialize, Serialize}; use strum_macros::EnumIter; use termion::input::TermReadEventsAndRaw; -use super::keybinds::key_to_actions; - /// Handles the dispatching of [`Action`]s according to the current /// [`InputMode`], and keep tracks of the current [`InputMode`]. struct InputHandler { @@ -62,9 +60,8 @@ impl InputHandler { self.send_pty_instructions.update(err_ctx); self.send_app_instructions.update(err_ctx); self.send_screen_instructions.update(err_ctx); - if let Ok(keybinds) = get_default_keybinds() { - let mut merged_keybinds = keybinds; - merged_keybinds.extend(self.config.keybinds.clone().into_iter()); + if let Ok(_keybinds) = Keybinds::get_default_keybinds() { + let keybinds = self.config.keybinds.clone(); 'input_loop: loop { //@@@ I think this should actually just iterate over stdin directly let stdin_buffer = self.os_input.read_from_stdin(); @@ -84,11 +81,8 @@ impl InputHandler { let mut should_break = false; // Hacked on way to have a means of testing Macros, needs to // get properly integrated - for action in key_to_actions( - &key, - raw_bytes, - &self.mode, - &merged_keybinds, + for action in Keybinds::key_to_actions( + &key, raw_bytes, &self.mode, &keybinds, ) { should_break |= self.dispatch_action(action); } diff --git a/src/common/input/keybinds.rs b/src/common/input/keybinds.rs index 8799649d..8b089ecf 100644 --- a/src/common/input/keybinds.rs +++ b/src/common/input/keybinds.rs @@ -1,183 +1,311 @@ //! Mapping of inputs to sequences of actions. +use std::collections::HashMap; use super::actions::{Action, Direction}; use super::handler::InputMode; -use std::collections::HashMap; - +use serde::Deserialize; use strum::IntoEnumIterator; use termion::event::Key; -pub type Keybinds = HashMap; -type ModeKeybinds = HashMap>; +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct Keybinds(HashMap); +#[derive(Clone, Debug, Default, PartialEq, Deserialize)] +pub struct ModeKeybinds(HashMap>); -/// Populates the default hashmap of keybinds. -/// @@@khs26 What about an input config file? -pub fn get_default_keybinds() -> Result { - let mut defaults = Keybinds::new(); +/// Intermediate struct for configuration. +#[derive(Clone, Debug, Default, PartialEq, Deserialize)] +pub struct MultipleKeybinds(HashMap>); +/// Intermediate struct for configuration. +#[derive(Clone, Debug, Default, PartialEq, Deserialize)] +struct MultipleModeKeybinds(HashMap, Vec>); - for mode in InputMode::iter() { - defaults.insert(mode, get_defaults_for_mode(&mode)?); - } - - Ok(defaults) +/// Intermediate enum for configuration. +#[derive(Debug, Clone, PartialEq, Deserialize)] +#[serde(untagged)] +enum ModeKeybind { + ModeKeybinds(ModeKeybinds), + MultipleModeKeybinds(MultipleModeKeybinds), } -/// Returns the default keybinds for a given [`InputMode`]. -fn get_defaults_for_mode(mode: &InputMode) -> Result { - let mut defaults = ModeKeybinds::new(); +impl Default for Keybinds { + fn default() -> Keybinds { + Keybinds::get_default_keybinds().expect("Something is wrong with the default Keybinds") + } +} - match *mode { - InputMode::Normal => { - defaults.insert( - Key::Ctrl('g'), - vec![Action::SwitchToMode(InputMode::Command)], - ); +impl Keybinds { + pub fn new() -> Keybinds { + Keybinds::from(HashMap::::new()) + } + + pub fn get_default_keybinds() -> Result { + let mut defaults = Keybinds::new(); + + for mode in InputMode::iter() { + defaults + .0 + .insert(mode, Keybinds::get_defaults_for_mode(&mode)?); } - InputMode::Command => { - defaults.insert( - Key::Char('r'), - vec![Action::SwitchToMode(InputMode::Resize)], - ); - defaults.insert(Key::Char('p'), vec![Action::SwitchToMode(InputMode::Pane)]); - defaults.insert(Key::Char('t'), vec![Action::SwitchToMode(InputMode::Tab)]); - defaults.insert( - Key::Char('s'), - vec![Action::SwitchToMode(InputMode::Scroll)], - ); - defaults.insert( - Key::Ctrl('g'), - vec![Action::SwitchToMode(InputMode::Normal)], - ); - defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Normal)]); - defaults.insert(Key::Char('q'), vec![Action::Quit]); + + Ok(defaults) + } + + pub fn get_default_keybinds_with_config(keybinds: Option) -> Keybinds { + let default_keybinds = Keybinds::default(); + if let Some(keybinds) = keybinds { + default_keybinds.merge_keybinds(Keybinds::from(keybinds)) + } else { + default_keybinds } - InputMode::Resize => { - defaults.insert(Key::Char('h'), vec![Action::Resize(Direction::Left)]); - defaults.insert(Key::Char('j'), vec![Action::Resize(Direction::Down)]); - defaults.insert(Key::Char('k'), vec![Action::Resize(Direction::Up)]); - defaults.insert(Key::Char('l'), vec![Action::Resize(Direction::Right)]); + } - defaults.insert(Key::Left, vec![Action::Resize(Direction::Left)]); - defaults.insert(Key::Down, vec![Action::Resize(Direction::Down)]); - defaults.insert(Key::Up, vec![Action::Resize(Direction::Up)]); - defaults.insert(Key::Right, vec![Action::Resize(Direction::Right)]); + /// Merges two Keybinds structs into one Keybinds struct + /// `other` overrides the ModeKeybinds of `self`. + fn merge_keybinds(&self, other: Keybinds) -> Keybinds { + let mut keybinds = Keybinds::default(); - defaults.insert(Key::Ctrl('b'), vec![Action::Resize(Direction::Left)]); - defaults.insert(Key::Ctrl('n'), vec![Action::Resize(Direction::Down)]); - defaults.insert(Key::Ctrl('p'), vec![Action::Resize(Direction::Up)]); - defaults.insert(Key::Ctrl('f'), vec![Action::Resize(Direction::Right)]); - - defaults.insert(Key::Char('q'), vec![Action::Quit]); - defaults.insert( - Key::Ctrl('g'), - vec![Action::SwitchToMode(InputMode::Normal)], - ); - defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Command)]); - } - InputMode::Pane => { - defaults.insert(Key::Char('h'), vec![Action::MoveFocus(Direction::Left)]); - defaults.insert(Key::Char('j'), vec![Action::MoveFocus(Direction::Down)]); - defaults.insert(Key::Char('k'), vec![Action::MoveFocus(Direction::Up)]); - defaults.insert(Key::Char('l'), vec![Action::MoveFocus(Direction::Right)]); - - defaults.insert(Key::Left, vec![Action::MoveFocus(Direction::Left)]); - defaults.insert(Key::Down, vec![Action::MoveFocus(Direction::Down)]); - defaults.insert(Key::Up, vec![Action::MoveFocus(Direction::Up)]); - defaults.insert(Key::Right, vec![Action::MoveFocus(Direction::Right)]); - - defaults.insert(Key::Ctrl('b'), vec![Action::MoveFocus(Direction::Left)]); - defaults.insert(Key::Ctrl('n'), vec![Action::MoveFocus(Direction::Down)]); - defaults.insert(Key::Ctrl('p'), vec![Action::MoveFocus(Direction::Up)]); - defaults.insert(Key::Ctrl('f'), vec![Action::MoveFocus(Direction::Right)]); - - defaults.insert(Key::Char('p'), vec![Action::SwitchFocus(Direction::Right)]); - defaults.insert(Key::Char('n'), vec![Action::NewPane(None)]); - defaults.insert(Key::Char('d'), vec![Action::NewPane(Some(Direction::Down))]); - defaults.insert( - Key::Char('r'), - vec![Action::NewPane(Some(Direction::Right))], - ); - defaults.insert(Key::Char('x'), vec![Action::CloseFocus]); - - defaults.insert(Key::Char('f'), vec![Action::ToggleFocusFullscreen]); - - defaults.insert(Key::Char('q'), vec![Action::Quit]); - defaults.insert( - Key::Ctrl('g'), - vec![Action::SwitchToMode(InputMode::Normal)], - ); - defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Command)]); - } - InputMode::Tab => { - defaults.insert(Key::Char('h'), vec![Action::GoToPreviousTab]); - defaults.insert(Key::Char('j'), vec![Action::GoToNextTab]); - defaults.insert(Key::Char('k'), vec![Action::GoToPreviousTab]); - defaults.insert(Key::Char('l'), vec![Action::GoToNextTab]); - - defaults.insert(Key::Left, vec![Action::GoToPreviousTab]); - defaults.insert(Key::Down, vec![Action::GoToNextTab]); - defaults.insert(Key::Up, vec![Action::GoToPreviousTab]); - defaults.insert(Key::Right, vec![Action::GoToNextTab]); - - defaults.insert(Key::Ctrl('b'), vec![Action::GoToPreviousTab]); - defaults.insert(Key::Ctrl('n'), vec![Action::GoToNextTab]); - defaults.insert(Key::Ctrl('p'), vec![Action::GoToPreviousTab]); - defaults.insert(Key::Ctrl('f'), vec![Action::GoToNextTab]); - - defaults.insert(Key::Char('n'), vec![Action::NewTab]); - defaults.insert(Key::Char('x'), vec![Action::CloseTab]); - - defaults.insert(Key::Char('q'), vec![Action::Quit]); - defaults.insert( - Key::Ctrl('g'), - vec![Action::SwitchToMode(InputMode::Normal)], - ); - for i in '1'..='9' { - defaults.insert(Key::Char(i), vec![Action::GoToTab(i.to_digit(10).unwrap())]); + for mode in InputMode::iter() { + let mut mode_keybinds: ModeKeybinds = if let Some(keybind) = self.0.get(&mode) { + keybind.clone() + } else { + ModeKeybinds::default() + }; + if let Some(keybind) = other.0.get(&mode) { + mode_keybinds.0.extend(keybind.0.clone()); } - defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Command)]); + keybinds.0.insert(mode, mode_keybinds); } - InputMode::Scroll => { - defaults.insert(Key::Char('j'), vec![Action::ScrollDown]); - defaults.insert(Key::Char('k'), vec![Action::ScrollUp]); - - defaults.insert(Key::Down, vec![Action::ScrollDown]); - defaults.insert(Key::Up, vec![Action::ScrollUp]); - - defaults.insert(Key::Ctrl('n'), vec![Action::ScrollDown]); - defaults.insert(Key::Ctrl('p'), vec![Action::ScrollUp]); - - defaults.insert(Key::Char('q'), vec![Action::Quit]); - defaults.insert( - Key::Ctrl('g'), - vec![Action::SwitchToMode(InputMode::Normal)], - ); - defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Command)]); - } - } - - Ok(defaults) -} - -/// Converts a [`Key`] terminal event to a sequence of [`Action`]s according to the current -/// [`InputMode`] and [`Keybinds`]. -pub fn key_to_actions( - key: &Key, - input: Vec, - mode: &InputMode, - keybinds: &Keybinds, -) -> Vec { - let mode_keybind_or_action = |action: Action| { keybinds - .get(mode) - .unwrap_or_else(|| unreachable!("Unrecognized mode: {:?}", mode)) - .get(key) - .cloned() - .unwrap_or_else(|| vec![action]) - }; - match *mode { - InputMode::Normal => mode_keybind_or_action(Action::Write(input)), - _ => mode_keybind_or_action(Action::NoOp), + } + + /// Returns the default keybinds for a given [`InputMode`]. + fn get_defaults_for_mode(mode: &InputMode) -> Result { + let mut defaults = HashMap::new(); + + match *mode { + InputMode::Normal => { + defaults.insert( + Key::Ctrl('g'), + vec![Action::SwitchToMode(InputMode::Command)], + ); + } + InputMode::Command => { + defaults.insert( + Key::Char('r'), + vec![Action::SwitchToMode(InputMode::Resize)], + ); + defaults.insert(Key::Char('p'), vec![Action::SwitchToMode(InputMode::Pane)]); + defaults.insert(Key::Char('t'), vec![Action::SwitchToMode(InputMode::Tab)]); + defaults.insert( + Key::Char('s'), + vec![Action::SwitchToMode(InputMode::Scroll)], + ); + defaults.insert( + Key::Ctrl('g'), + vec![Action::SwitchToMode(InputMode::Normal)], + ); + defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Normal)]); + defaults.insert(Key::Char('q'), vec![Action::Quit]); + } + InputMode::Resize => { + defaults.insert(Key::Char('h'), vec![Action::Resize(Direction::Left)]); + defaults.insert(Key::Char('j'), vec![Action::Resize(Direction::Down)]); + defaults.insert(Key::Char('k'), vec![Action::Resize(Direction::Up)]); + defaults.insert(Key::Char('l'), vec![Action::Resize(Direction::Right)]); + + defaults.insert(Key::Left, vec![Action::Resize(Direction::Left)]); + defaults.insert(Key::Down, vec![Action::Resize(Direction::Down)]); + defaults.insert(Key::Up, vec![Action::Resize(Direction::Up)]); + defaults.insert(Key::Right, vec![Action::Resize(Direction::Right)]); + + defaults.insert(Key::Ctrl('b'), vec![Action::Resize(Direction::Left)]); + defaults.insert(Key::Ctrl('n'), vec![Action::Resize(Direction::Down)]); + defaults.insert(Key::Ctrl('p'), vec![Action::Resize(Direction::Up)]); + defaults.insert(Key::Ctrl('f'), vec![Action::Resize(Direction::Right)]); + + defaults.insert(Key::Char('q'), vec![Action::Quit]); + defaults.insert( + Key::Ctrl('g'), + vec![Action::SwitchToMode(InputMode::Normal)], + ); + defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Command)]); + } + InputMode::Pane => { + defaults.insert(Key::Char('h'), vec![Action::MoveFocus(Direction::Left)]); + defaults.insert(Key::Char('j'), vec![Action::MoveFocus(Direction::Down)]); + defaults.insert(Key::Char('k'), vec![Action::MoveFocus(Direction::Up)]); + defaults.insert(Key::Char('l'), vec![Action::MoveFocus(Direction::Right)]); + + defaults.insert(Key::Left, vec![Action::MoveFocus(Direction::Left)]); + defaults.insert(Key::Down, vec![Action::MoveFocus(Direction::Down)]); + defaults.insert(Key::Up, vec![Action::MoveFocus(Direction::Up)]); + defaults.insert(Key::Right, vec![Action::MoveFocus(Direction::Right)]); + + defaults.insert(Key::Ctrl('b'), vec![Action::MoveFocus(Direction::Left)]); + defaults.insert(Key::Ctrl('n'), vec![Action::MoveFocus(Direction::Down)]); + defaults.insert(Key::Ctrl('p'), vec![Action::MoveFocus(Direction::Up)]); + defaults.insert(Key::Ctrl('f'), vec![Action::MoveFocus(Direction::Right)]); + + defaults.insert(Key::Char('p'), vec![Action::SwitchFocus(Direction::Right)]); + defaults.insert(Key::Char('n'), vec![Action::NewPane(None)]); + defaults.insert(Key::Char('d'), vec![Action::NewPane(Some(Direction::Down))]); + defaults.insert( + Key::Char('r'), + vec![Action::NewPane(Some(Direction::Right))], + ); + defaults.insert(Key::Char('x'), vec![Action::CloseFocus]); + + defaults.insert(Key::Char('f'), vec![Action::ToggleFocusFullscreen]); + + defaults.insert(Key::Char('q'), vec![Action::Quit]); + defaults.insert( + Key::Ctrl('g'), + vec![Action::SwitchToMode(InputMode::Normal)], + ); + defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Command)]); + } + InputMode::Tab => { + defaults.insert(Key::Char('h'), vec![Action::GoToPreviousTab]); + defaults.insert(Key::Char('j'), vec![Action::GoToNextTab]); + defaults.insert(Key::Char('k'), vec![Action::GoToPreviousTab]); + defaults.insert(Key::Char('l'), vec![Action::GoToNextTab]); + + defaults.insert(Key::Left, vec![Action::GoToPreviousTab]); + defaults.insert(Key::Down, vec![Action::GoToNextTab]); + defaults.insert(Key::Up, vec![Action::GoToPreviousTab]); + defaults.insert(Key::Right, vec![Action::GoToNextTab]); + + defaults.insert(Key::Ctrl('b'), vec![Action::GoToPreviousTab]); + defaults.insert(Key::Ctrl('n'), vec![Action::GoToNextTab]); + defaults.insert(Key::Ctrl('p'), vec![Action::GoToPreviousTab]); + defaults.insert(Key::Ctrl('f'), vec![Action::GoToNextTab]); + + defaults.insert(Key::Char('n'), vec![Action::NewTab]); + defaults.insert(Key::Char('x'), vec![Action::CloseTab]); + + defaults.insert(Key::Char('q'), vec![Action::Quit]); + defaults.insert( + Key::Ctrl('g'), + vec![Action::SwitchToMode(InputMode::Normal)], + ); + for i in '1'..='9' { + defaults.insert(Key::Char(i), vec![Action::GoToTab(i.to_digit(10).unwrap())]); + } + defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Command)]); + } + InputMode::Scroll => { + defaults.insert(Key::Char('j'), vec![Action::ScrollDown]); + defaults.insert(Key::Char('k'), vec![Action::ScrollUp]); + + defaults.insert(Key::Down, vec![Action::ScrollDown]); + defaults.insert(Key::Up, vec![Action::ScrollUp]); + + defaults.insert(Key::Ctrl('n'), vec![Action::ScrollDown]); + defaults.insert(Key::Ctrl('p'), vec![Action::ScrollUp]); + + defaults.insert(Key::Char('q'), vec![Action::Quit]); + defaults.insert( + Key::Ctrl('g'), + vec![Action::SwitchToMode(InputMode::Normal)], + ); + defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Command)]); + } + } + + Ok(ModeKeybinds::from(defaults)) + } + + /// Converts a [`Key`] terminal event to a sequence of [`Action`]s according to the current + /// [`InputMode`] and [`Keybinds`]. + pub fn key_to_actions( + key: &Key, + input: Vec, + mode: &InputMode, + keybinds: &Keybinds, + ) -> Vec { + let mode_keybind_or_action = |action: Action| { + keybinds + .0 + .get(mode) + .unwrap_or_else(|| unreachable!("Unrecognized mode: {:?}", mode)) + .0 + .get(key) + .cloned() + .unwrap_or_else(|| vec![action]) + }; + match *mode { + InputMode::Normal => mode_keybind_or_action(Action::Write(input)), + _ => mode_keybind_or_action(Action::NoOp), + } } } + +impl ModeKeybinds { + pub fn new() -> ModeKeybinds { + ModeKeybinds::from(HashMap::>::new()) + } + + /// Merges `self` with `other`, if keys are the same, `other` overwrites. + pub fn merge(self, other: ModeKeybinds) -> ModeKeybinds { + let mut merged = self; + merged.0.extend(other.0.clone()); + merged + } +} + +/// Converts from the `MultipleKeybinds` struct to a `Keybinds` struct. +/// TODO @a-kenji Error on conflicting keybinds? +/// Atm the Lower Keybind will take precedence and will overwrite the +/// one further up. +impl From for Keybinds { + fn from(multiple: MultipleKeybinds) -> Keybinds { + let mut keybinds = Keybinds::new(); + + for mode in InputMode::iter() { + let mut mode_keybinds = ModeKeybinds::new(); + for mode_keybind in multiple.0.get(&mode).iter() { + for keybind in mode_keybind.iter() { + match keybind { + ModeKeybind::ModeKeybinds(k) => { + mode_keybinds = mode_keybinds.clone().merge(k.clone()); + } + ModeKeybind::MultipleModeKeybinds(k) => { + mode_keybinds = + mode_keybinds.clone().merge(ModeKeybinds::from(k.clone())); + } + } + } + } + keybinds.0.insert(mode, mode_keybinds); + } + keybinds + } +} + +impl From> for Keybinds { + fn from(map: HashMap) -> Keybinds { + Keybinds(map) + } +} +impl From>> for ModeKeybinds { + fn from(map: HashMap>) -> ModeKeybinds { + ModeKeybinds(map) + } +} + +impl From for ModeKeybinds { + fn from(multiple: MultipleModeKeybinds) -> ModeKeybinds { + let single: HashMap> = multiple + .0 + .into_iter() + .flat_map(|(k, v)| (k.into_iter().map(move |k| (k, v.clone())))) + .collect(); + ModeKeybinds::from(single) + } +} + +// The unit test location. +#[cfg(test)] +#[path = "./ut/keybinds_test.rs"] +mod keybinds_test; diff --git a/src/common/input/macros.rs b/src/common/input/macros.rs deleted file mode 100644 index 4b7c6df1..00000000 --- a/src/common/input/macros.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! Use a list of commands and execute them in a -//! defined predictable order. - -use super::actions::Action; - -use serde::Deserialize; - -#[derive(Debug, Clone, Deserialize)] -pub struct Macro { - name: Option, - sequence: Vec, -} diff --git a/src/common/input/mod.rs b/src/common/input/mod.rs index 438df224..a7dca7ed 100644 --- a/src/common/input/mod.rs +++ b/src/common/input/mod.rs @@ -1,6 +1,5 @@ -//! The way terminal iput is handled. +//! The way terminal input is handled. pub mod actions; pub mod handler; pub mod keybinds; -pub mod macros; diff --git a/src/common/input/ut/config_test.rs b/src/common/input/ut/config_test.rs new file mode 100644 index 00000000..001aab15 --- /dev/null +++ b/src/common/input/ut/config_test.rs @@ -0,0 +1,51 @@ +//! For Configuration Options + +use super::super::config::*; +use crate::common::input::keybinds::*; +use crate::common::input::actions::*; +use std::path::PathBuf; + +use termion::event::Key; + + +#[test] +fn no_config_file_equals_default_config() { + let no_file = PathBuf::from(r"../fixtures/config/config.yamlll"); + let config = Config::from_option_or_default(Some(no_file)).unwrap(); + let default = Config::default(); + assert_eq!(config, default); +} + +#[test] +fn no_config_option_file_equals_default_config() { + let config = Config::from_option_or_default(None).unwrap(); + let default = Config::default(); + assert_eq!(config, default); +} + +#[test] +fn multiple_keys_mapped_to_one_action() { + let options = r" + --- +keybindings: + Normal: + - ? - F: 6 + - F: 7 + - F: 8 + : - {GoToTab: 5} + "; + + let config_options = Config::from_yaml(&options).unwrap(); + + assert_eq!(config_options, config_options) +} + +//#[test] +//fn merge_keybinds_merges(){ + //let mut self_keybinds = Keybinds::new(); + //let mut self_mode_keybinds = ModeKeybinds::new(); + //self_mode_keybinds.0.insert(Key::F(1), vec![Action::GoToTab(5)]); + //let mut other_keybinds = Keybinds::new(); + //let mut self_mode_keybinds = ModeKeybinds::new(); + //let mut expected = Keybinds::new(); +//} diff --git a/src/common/input/ut/keybinds_test.rs b/src/common/input/ut/keybinds_test.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/common/mod.rs b/src/common/mod.rs index c8b2ec98..fdd66649 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -29,6 +29,7 @@ use wasmer_wasi::{Pipe, WasiState}; use crate::cli::CliArgs; use crate::common::config::Config; use crate::layout::Layout; +use crate::utils::logging::*; use command_is_executing::CommandIsExecuting; use errors::{AppContext, ContextType, ErrorContext, PluginContext, PtyContext, ScreenContext}; use input::handler::input_loop; @@ -168,8 +169,13 @@ pub fn start(mut os_input: Box, opts: CliArgs) { let mut app_state = AppState::default(); + #[allow(unused_must_use)] let config = Config::from_option_or_default(opts.config) - .map_err(|e| eprintln!{"Config Error: {}", e}) + .map_err(|e| { + debug_log_to_file(format!("There was an error in the config file:\n{}", e)); + eprintln!("There was an error in the config file:\n{}", e); + std::process::exit(1); + }) .unwrap(); let command_is_executing = CommandIsExecuting::new(); diff --git a/src/tests/fixtures/config/functioning_simple_config.yaml b/src/tests/fixtures/config/functioning_simple_config.yaml new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/fixtures/config/multiple_keys_for_one_action.yaml b/src/tests/fixtures/config/multiple_keys_for_one_action.yaml new file mode 100644 index 00000000..7da62541 --- /dev/null +++ b/src/tests/fixtures/config/multiple_keys_for_one_action.yaml @@ -0,0 +1,8 @@ +--- +keybindings: + Normal: + - ? - F: 6 + - F: 7 + - F: 8 + : - {GoToTab: 5} +