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
This commit is contained in:
a-kenji 2021-03-05 22:40:42 +01:00
parent c644a1c243
commit c97c553870
12 changed files with 430 additions and 254 deletions

View file

@ -1,8 +1,12 @@
--- ---
macro:
{name:"closePane", sequence: [NewPane: Right,]}
keybinds: keybinds:
Normal: Normal:
Backspace: [NewPane:, NewPane:,] - {F: 1}: [GoToTab: 1,]
{F: 1}: [NewPane:,] - {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,]

View file

@ -1,104 +1,101 @@
//! Deserializes configuration options. //! Deserializes configuration options.
use std; use std;
//use std::collections::HashMap;
use std::error; use std::error;
use std::fmt::{self, Display}; use std::fmt::{self, Display};
use std::fs::File; use std::fs::File;
use std::io::{self, Read}; use std::io::{self, Read};
use std::path::PathBuf; use std::path::PathBuf;
use super::input::{keybinds, macros}; use super::input::keybinds::{Keybinds, MultipleKeybinds};
use crate::utils::logging::*;
use directories_next::ProjectDirs; use directories_next::ProjectDirs;
use serde::Deserialize; use serde::Deserialize;
/// Intermediate struct /// Intermediate deserialisation config struct
//pub struct KeybingsFromYaml {
//}
/// Intermediate struct
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct ConfigFromYaml { pub struct ConfigFromYaml {
keybinds: Option<keybinds::Keybinds>, pub keybinds: Option<MultipleKeybinds>,
macros: Option<Vec<macros::Macro>>,
} }
///// Deserialized config state /// Main configuration.
#[derive(Debug, Clone, Default, Deserialize)] #[derive(Debug, Clone, PartialEq)]
pub struct Config { pub struct Config {
pub keybinds: keybinds::Keybinds, pub keybinds: Keybinds,
} }
#[derive(Debug)] #[derive(Debug)]
pub enum ConfigError { pub enum ConfigError {
// from the serde documentation // Deserialisation error
// 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<T> 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
Serde(serde_yaml::Error), Serde(serde_yaml::Error),
//Eof, // Io error
// io::Error
Io(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 { impl Config {
/// Deserializes from given path /// Uses defaults, but lets config override them.
pub fn new(path: &PathBuf) -> Result<Config, ConfigError> { pub fn from_yaml(yaml_config: &str) -> Result<Config, ConfigError> {
let config: Config; let config_from_yaml: ConfigFromYaml = serde_yaml::from_str(&yaml_config)?;
let config_deserialized: ConfigFromYaml; let keybinds = Keybinds::get_default_keybinds_with_config(config_from_yaml.keybinds);
let mut config_string = String::new(); 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<Config, ConfigError> {
match File::open(path) { match File::open(path) {
Ok(mut file) => { Ok(mut file) => {
file.read_to_string(&mut config_string)?; let mut yaml_config = String::new();
config_deserialized = serde_yaml::from_str(&config_string)?; file.read_to_string(&mut yaml_config)
config = Config { .map_err(|e| ConfigError::IoPath(e, path.to_path_buf()))?;
keybinds: config_deserialized Ok(Config::from_yaml(&yaml_config)?)
.keybinds
.unwrap_or_else(|| keybinds::get_default_keybinds().unwrap()),
}
} }
Err(e) => { Err(e) => {
// TODO logging, if a file is not found // TODO logging, if a file is not found
// at an expected position - should not // at an expected position - should not
// panic @a-kenji // panic, but log to file @a-kenji
eprintln!("{}", e); debug_log_to_file(format!(
config = Config::default(); "{}\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<PathBuf>) -> Result<Config, ConfigError> { pub fn from_option_or_default(option: Option<PathBuf>) -> Result<Config, ConfigError> {
let config;
if let Some(config_path) = option { if let Some(config_path) = option {
config = Config::new(&config_path)?; Ok(Config::new(&config_path)?)
} else { } else {
let project_dirs = ProjectDirs::from("org", "Zellij Contributors", "Zellij").unwrap(); let project_dirs = ProjectDirs::from("org", "Zellij Contributors", "Zellij").unwrap();
//let config_path = PathBuf::from(project_dirs.config_dir().as_os_str()); let mut config_path: PathBuf = project_dirs.config_dir().to_owned().into();
let config_path = project_dirs.config_dir().to_owned().into(); config_path.push("config.yaml");
config = Config::new(&config_path)?; Ok(Config::new(&config_path)?)
} }
return Ok(config);
} }
} }
impl Display for ConfigError { impl Display for ConfigError {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
match self { match self {
//ConfigError::Message(msg) => formatter.write_str(msg), ConfigError::Io(ref err) => write!(formatter, "IoError: {}", err),
//ConfigError::Eof => formatter.write_str("unexpected end of input"), ConfigError::IoPath(ref err, ref path) => {
// write!(formatter, "IoError: {}, File: {}", err, path.display(),)
ConfigError::Io(ref err) => write!(formatter, "Io error: {}", err), }
ConfigError::Serde(ref err) => write!(formatter, "Serde error: {}", err), ConfigError::Serde(ref err) => write!(formatter, "Deserialisation error: {}", err),
/* and so forth */
} }
} }
} }
@ -106,12 +103,8 @@ impl Display for ConfigError {
impl std::error::Error for ConfigError { impl std::error::Error for ConfigError {
fn cause(&self) -> Option<&dyn error::Error> { fn cause(&self) -> Option<&dyn error::Error> {
match *self { 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::Io(ref err) => Some(err),
//ConfigError::Message(ref err) => Some(err), ConfigError::IoPath(ref err, _) => Some(err),
ConfigError::Serde(ref err) => Some(err), ConfigError::Serde(ref err) => Some(err),
} }
} }
@ -128,3 +121,8 @@ impl From<serde_yaml::Error> for ConfigError {
ConfigError::Serde(err) ConfigError::Serde(err)
} }
} }
// The unit test location.
#[cfg(test)]
#[path = "./input/ut/config_test.rs"]
mod config_test;

View file

@ -2,10 +2,10 @@
use super::handler; use super::handler;
//use super::macros; //use super::macros;
use serde::Deserialize; use serde::{Deserialize, Serialize};
/// The four directions (left, right, up, down). /// The four directions (left, right, up, down).
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub enum Direction { pub enum Direction {
Left, Left,
Right, Right,
@ -14,7 +14,7 @@ pub enum Direction {
} }
/// Actions that can be bound to keys. /// Actions that can be bound to keys.
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub enum Action { pub enum Action {
/// Quit Zellij. /// Quit Zellij.
Quit, Quit,

View file

@ -1,7 +1,7 @@
//! Main input logic. //! Main input logic.
use super::actions::Action; use super::actions::Action;
use super::keybinds::get_default_keybinds; use super::keybinds::Keybinds;
use crate::common::config::Config; use crate::common::config::Config;
use crate::common::{update_state, AppInstruction, AppState, SenderWithContext, OPENCALLS}; use crate::common::{update_state, AppInstruction, AppState, SenderWithContext, OPENCALLS};
use crate::errors::ContextType; use crate::errors::ContextType;
@ -15,8 +15,6 @@ use serde::{Deserialize, Serialize};
use strum_macros::EnumIter; use strum_macros::EnumIter;
use termion::input::TermReadEventsAndRaw; use termion::input::TermReadEventsAndRaw;
use super::keybinds::key_to_actions;
/// Handles the dispatching of [`Action`]s according to the current /// Handles the dispatching of [`Action`]s according to the current
/// [`InputMode`], and keep tracks of the current [`InputMode`]. /// [`InputMode`], and keep tracks of the current [`InputMode`].
struct InputHandler { struct InputHandler {
@ -62,9 +60,8 @@ impl InputHandler {
self.send_pty_instructions.update(err_ctx); self.send_pty_instructions.update(err_ctx);
self.send_app_instructions.update(err_ctx); self.send_app_instructions.update(err_ctx);
self.send_screen_instructions.update(err_ctx); self.send_screen_instructions.update(err_ctx);
if let Ok(keybinds) = get_default_keybinds() { if let Ok(_keybinds) = Keybinds::get_default_keybinds() {
let mut merged_keybinds = keybinds; let keybinds = self.config.keybinds.clone();
merged_keybinds.extend(self.config.keybinds.clone().into_iter());
'input_loop: loop { 'input_loop: loop {
//@@@ I think this should actually just iterate over stdin directly //@@@ I think this should actually just iterate over stdin directly
let stdin_buffer = self.os_input.read_from_stdin(); let stdin_buffer = self.os_input.read_from_stdin();
@ -84,11 +81,8 @@ impl InputHandler {
let mut should_break = false; let mut should_break = false;
// Hacked on way to have a means of testing Macros, needs to // Hacked on way to have a means of testing Macros, needs to
// get properly integrated // get properly integrated
for action in key_to_actions( for action in Keybinds::key_to_actions(
&key, &key, raw_bytes, &self.mode, &keybinds,
raw_bytes,
&self.mode,
&merged_keybinds,
) { ) {
should_break |= self.dispatch_action(action); should_break |= self.dispatch_action(action);
} }

View file

@ -1,31 +1,87 @@
//! Mapping of inputs to sequences of actions. //! Mapping of inputs to sequences of actions.
use std::collections::HashMap;
use super::actions::{Action, Direction}; use super::actions::{Action, Direction};
use super::handler::InputMode; use super::handler::InputMode;
use std::collections::HashMap; use serde::Deserialize;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use termion::event::Key; use termion::event::Key;
pub type Keybinds = HashMap<InputMode, ModeKeybinds>; #[derive(Clone, Debug, PartialEq, Deserialize)]
type ModeKeybinds = HashMap<Key, Vec<Action>>; pub struct Keybinds(HashMap<InputMode, ModeKeybinds>);
#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub struct ModeKeybinds(HashMap<Key, Vec<Action>>);
/// Populates the default hashmap of keybinds. /// Intermediate struct for configuration.
/// @@@khs26 What about an input config file? #[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub fn get_default_keybinds() -> Result<Keybinds, String> { pub struct MultipleKeybinds(HashMap<InputMode, Vec<ModeKeybind>>);
/// Intermediate struct for configuration.
#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
struct MultipleModeKeybinds(HashMap<Vec<Key>, Vec<Action>>);
/// Intermediate enum for configuration.
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(untagged)]
enum ModeKeybind {
ModeKeybinds(ModeKeybinds),
MultipleModeKeybinds(MultipleModeKeybinds),
}
impl Default for Keybinds {
fn default() -> Keybinds {
Keybinds::get_default_keybinds().expect("Something is wrong with the default Keybinds")
}
}
impl Keybinds {
pub fn new() -> Keybinds {
Keybinds::from(HashMap::<InputMode, ModeKeybinds>::new())
}
pub fn get_default_keybinds() -> Result<Keybinds, String> {
let mut defaults = Keybinds::new(); let mut defaults = Keybinds::new();
for mode in InputMode::iter() { for mode in InputMode::iter() {
defaults.insert(mode, get_defaults_for_mode(&mode)?); defaults
.0
.insert(mode, Keybinds::get_defaults_for_mode(&mode)?);
} }
Ok(defaults) Ok(defaults)
} }
/// Returns the default keybinds for a given [`InputMode`]. pub fn get_default_keybinds_with_config(keybinds: Option<MultipleKeybinds>) -> Keybinds {
fn get_defaults_for_mode(mode: &InputMode) -> Result<ModeKeybinds, String> { let default_keybinds = Keybinds::default();
let mut defaults = ModeKeybinds::new(); if let Some(keybinds) = keybinds {
default_keybinds.merge_keybinds(Keybinds::from(keybinds))
} else {
default_keybinds
}
}
/// 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();
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());
}
keybinds.0.insert(mode, mode_keybinds);
}
keybinds
}
/// Returns the default keybinds for a given [`InputMode`].
fn get_defaults_for_mode(mode: &InputMode) -> Result<ModeKeybinds, String> {
let mut defaults = HashMap::new();
match *mode { match *mode {
InputMode::Normal => { InputMode::Normal => {
@ -157,21 +213,23 @@ fn get_defaults_for_mode(mode: &InputMode) -> Result<ModeKeybinds, String> {
} }
} }
Ok(defaults) Ok(ModeKeybinds::from(defaults))
} }
/// Converts a [`Key`] terminal event to a sequence of [`Action`]s according to the current /// Converts a [`Key`] terminal event to a sequence of [`Action`]s according to the current
/// [`InputMode`] and [`Keybinds`]. /// [`InputMode`] and [`Keybinds`].
pub fn key_to_actions( pub fn key_to_actions(
key: &Key, key: &Key,
input: Vec<u8>, input: Vec<u8>,
mode: &InputMode, mode: &InputMode,
keybinds: &Keybinds, keybinds: &Keybinds,
) -> Vec<Action> { ) -> Vec<Action> {
let mode_keybind_or_action = |action: Action| { let mode_keybind_or_action = |action: Action| {
keybinds keybinds
.0
.get(mode) .get(mode)
.unwrap_or_else(|| unreachable!("Unrecognized mode: {:?}", mode)) .unwrap_or_else(|| unreachable!("Unrecognized mode: {:?}", mode))
.0
.get(key) .get(key)
.cloned() .cloned()
.unwrap_or_else(|| vec![action]) .unwrap_or_else(|| vec![action])
@ -180,4 +238,74 @@ pub fn key_to_actions(
InputMode::Normal => mode_keybind_or_action(Action::Write(input)), InputMode::Normal => mode_keybind_or_action(Action::Write(input)),
_ => mode_keybind_or_action(Action::NoOp), _ => mode_keybind_or_action(Action::NoOp),
} }
}
} }
impl ModeKeybinds {
pub fn new() -> ModeKeybinds {
ModeKeybinds::from(HashMap::<Key, Vec<Action>>::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<MultipleKeybinds> 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<HashMap<InputMode, ModeKeybinds>> for Keybinds {
fn from(map: HashMap<InputMode, ModeKeybinds>) -> Keybinds {
Keybinds(map)
}
}
impl From<HashMap<Key, Vec<Action>>> for ModeKeybinds {
fn from(map: HashMap<Key, Vec<Action>>) -> ModeKeybinds {
ModeKeybinds(map)
}
}
impl From<MultipleModeKeybinds> for ModeKeybinds {
fn from(multiple: MultipleModeKeybinds) -> ModeKeybinds {
let single: HashMap<Key, Vec<Action>> = 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;

View file

@ -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<String>,
sequence: Vec<Action>,
}

View file

@ -1,6 +1,5 @@
//! The way terminal iput is handled. //! The way terminal input is handled.
pub mod actions; pub mod actions;
pub mod handler; pub mod handler;
pub mod keybinds; pub mod keybinds;
pub mod macros;

View file

@ -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();
//}

View file

View file

@ -29,6 +29,7 @@ use wasmer_wasi::{Pipe, WasiState};
use crate::cli::CliArgs; use crate::cli::CliArgs;
use crate::common::config::Config; use crate::common::config::Config;
use crate::layout::Layout; use crate::layout::Layout;
use crate::utils::logging::*;
use command_is_executing::CommandIsExecuting; use command_is_executing::CommandIsExecuting;
use errors::{AppContext, ContextType, ErrorContext, PluginContext, PtyContext, ScreenContext}; use errors::{AppContext, ContextType, ErrorContext, PluginContext, PtyContext, ScreenContext};
use input::handler::input_loop; use input::handler::input_loop;
@ -168,8 +169,13 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) {
let mut app_state = AppState::default(); let mut app_state = AppState::default();
#[allow(unused_must_use)]
let config = Config::from_option_or_default(opts.config) 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(); .unwrap();
let command_is_executing = CommandIsExecuting::new(); let command_is_executing = CommandIsExecuting::new();

View file

@ -0,0 +1,8 @@
---
keybindings:
Normal:
- ? - F: 6
- F: 7
- F: 8
: - {GoToTab: 5}