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:
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,]

View file

@ -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<keybinds::Keybinds>,
macros: Option<Vec<macros::Macro>>,
pub keybinds: Option<MultipleKeybinds>,
}
///// 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<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
// 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<Config, ConfigError> {
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<Config, ConfigError> {
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<Config, ConfigError> {
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<PathBuf>) -> Result<Config, ConfigError> {
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 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<serde_yaml::Error> for ConfigError {
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::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,

View file

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

View file

@ -1,31 +1,87 @@
//! 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<InputMode, ModeKeybinds>;
type ModeKeybinds = HashMap<Key, Vec<Action>>;
#[derive(Clone, Debug, PartialEq, Deserialize)]
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.
/// @@@khs26 What about an input config file?
pub fn get_default_keybinds() -> Result<Keybinds, String> {
/// Intermediate struct for configuration.
#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
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();
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)
}
}
/// Returns the default keybinds for a given [`InputMode`].
fn get_defaults_for_mode(mode: &InputMode) -> Result<ModeKeybinds, String> {
let mut defaults = ModeKeybinds::new();
pub fn get_default_keybinds_with_config(keybinds: Option<MultipleKeybinds>) -> Keybinds {
let default_keybinds = Keybinds::default();
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 {
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
/// [`InputMode`] and [`Keybinds`].
pub fn key_to_actions(
/// 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<u8>,
mode: &InputMode,
keybinds: &Keybinds,
) -> Vec<Action> {
) -> Vec<Action> {
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])
@ -180,4 +238,74 @@ pub fn key_to_actions(
InputMode::Normal => mode_keybind_or_action(Action::Write(input)),
_ => 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 handler;
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::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<dyn OsApi>, 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();

View file

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