From 3e0174cc4a973b9f3f6324af66b1b9ed9b6a7361 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Thu, 4 Mar 2021 15:00:23 +0100 Subject: [PATCH 01/39] poke at creating Macros - create config struct - create config errors - create macro struct TODO create --config option for the file --- src/common/config.rs | 129 +++++++++++++++++++++++++++++++++++ src/common/input/actions.rs | 6 +- src/common/input/keybinds.rs | 2 +- src/common/input/macros.rs | 12 ++++ src/common/input/mod.rs | 1 + src/common/mod.rs | 3 + 6 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 src/common/config.rs create mode 100644 src/common/input/macros.rs diff --git a/src/common/config.rs b/src/common/config.rs new file mode 100644 index 00000000..2ce25bff --- /dev/null +++ b/src/common/config.rs @@ -0,0 +1,129 @@ +//! 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,handler}; + +use serde::Deserialize; + +/// Intermediate struct +//pub struct KeybingsFromYaml { + +//} + + +/// Intermediate struct +//#[derive(Debug, Deserialize)] +pub struct ConfigFromYaml { + keybinds: HashMap>, +} + +///// Deserialized config state +#[derive(Debug, Clone, Default, Deserialize)] +pub struct Config { + keybinds: Vec, +} + +#[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 + Serde(serde_yaml::Error), + //Eof, + // io::Error + Io(io::Error) +} + +impl Config { + /// Deserializes from given path + pub fn new(path: &PathBuf) -> Result { + let config_deserialized: Config; + let mut config_string = String::new(); + + match File::open(path) { + Ok(mut file) => { + file.read_to_string(&mut config_string)?; + config_deserialized = serde_yaml::from_str(&config_string)?; + } + Err(_) => { + // TODO logging, if a file is not found + // at an expected position - should not + // panic @a-kenji + config_deserialized = Config::default(); + } + } + Ok(config_deserialized) + } +} + +//impl de::Error for ConfigError { + //fn custom(msg: T) -> Self { + //ConfigError::Message(msg.to_string()) + //} +//} + +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 */ + } + } +} + +impl std::error::Error for ConfigError { + fn description(&self) -> &str { + match *self { + //ConfigError::Message(ref err) => err, + ConfigError::Io(ref err) => err.to_string().as_str(), + ConfigError::Serde(ref err) => err.to_string().as_str(), + } + } + + 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::Serde(ref err) => Some(err), + } +} +} + +impl From for ConfigError { + fn from(err: io::Error) -> ConfigError { + ConfigError::Io(err) + } +} + +impl From for ConfigError { + fn from(err: serde_yaml::Error) -> ConfigError { + ConfigError::Serde(err) + } +} + +//impl From for ConfigError { + //fn from(err: de::Error::Message) -> ConfigError { + //ConfigError::Message(err) + //} +//} + diff --git a/src/common/input/actions.rs b/src/common/input/actions.rs index fe5e2dad..d03ca859 100644 --- a/src/common/input/actions.rs +++ b/src/common/input/actions.rs @@ -1,9 +1,11 @@ //! Definition of the actions that can be bound to keys. use super::handler; +//use super::macros; +use serde::Deserialize; /// The four directions (left, right, up, down). -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize)] pub enum Direction { Left, Right, @@ -12,7 +14,7 @@ pub enum Direction { } /// Actions that can be bound to keys. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize)] pub enum Action { /// Quit Zellij. Quit, diff --git a/src/common/input/keybinds.rs b/src/common/input/keybinds.rs index e56488b4..0ea0695f 100644 --- a/src/common/input/keybinds.rs +++ b/src/common/input/keybinds.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; use strum::IntoEnumIterator; use termion::event::Key; -type Keybinds = HashMap; +pub type Keybinds = HashMap; type ModeKeybinds = HashMap>; /// Populates the default hashmap of keybinds. diff --git a/src/common/input/macros.rs b/src/common/input/macros.rs new file mode 100644 index 00000000..cbd14332 --- /dev/null +++ b/src/common/input/macros.rs @@ -0,0 +1,12 @@ +//! 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 20c30dae..438df224 100644 --- a/src/common/input/mod.rs +++ b/src/common/input/mod.rs @@ -3,3 +3,4 @@ pub mod actions; pub mod handler; pub mod keybinds; +pub mod macros; diff --git a/src/common/mod.rs b/src/common/mod.rs index f78303ea..5ba2e15d 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,3 +1,4 @@ +pub mod config; pub mod command_is_executing; pub mod errors; pub mod input; @@ -26,6 +27,7 @@ use wasmer::{ChainableNamedResolver, Instance, Module, Store, Value}; use wasmer_wasi::{Pipe, WasiState}; use crate::cli::CliArgs; +use crate::common::config::Config; use crate::layout::Layout; use command_is_executing::CommandIsExecuting; use errors::{AppContext, ContextType, ErrorContext, PluginContext, PtyContext, ScreenContext}; @@ -47,6 +49,7 @@ pub enum ApiCommand { #[derive(Debug, Clone, Default)] pub struct AppState { pub input_mode: InputMode, + pub config : Config, } // FIXME: Make this a method on the big `Communication` struct, so that app_tx can be extracted From c644a1c2437ac439425eaa185d4f69911cf5ee45 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Fri, 5 Mar 2021 19:23:06 +0100 Subject: [PATCH 02/39] poking --- assets/completions/_zellij | 2 + assets/completions/zellij.bash | 10 +++- assets/completions/zellij.fish | 1 + config.yaml | 8 ++++ src/cli.rs | 4 ++ src/common/config.rs | 85 +++++++++++++++++----------------- src/common/input/handler.rs | 19 ++++++-- src/common/input/keybinds.rs | 2 +- src/common/input/macros.rs | 4 +- src/common/mod.rs | 20 ++++++-- 10 files changed, 103 insertions(+), 52 deletions(-) create mode 100644 config.yaml diff --git a/assets/completions/_zellij b/assets/completions/_zellij index 64bd538c..cd8dce56 100644 --- a/assets/completions/_zellij +++ b/assets/completions/_zellij @@ -22,6 +22,8 @@ _zellij() { '--max-panes=[Maximum panes on screen, caution: opening more panes will close old ones]' \ '-l+[Path to a layout yaml file]' \ '--layout=[Path to a layout yaml file]' \ +'-c+[Path to the configuration yaml file]' \ +'--config=[Path to the configuration yaml file]' \ '-m[Send "move focused pane" to active zellij session]' \ '--move-focus[Send "move focused pane" to active zellij session]' \ '-d[]' \ diff --git a/assets/completions/zellij.bash b/assets/completions/zellij.bash index a921e7c9..0ede5cce 100644 --- a/assets/completions/zellij.bash +++ b/assets/completions/zellij.bash @@ -20,7 +20,7 @@ _zellij() { case "${cmd}" in zellij) - opts=" -m -d -h -V -s -o -l --move-focus --debug --help --version --split --open-file --max-panes --layout " + opts=" -m -d -h -V -s -o -l -c --move-focus --debug --help --version --split --open-file --max-panes --layout --config " if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -55,6 +55,14 @@ _zellij() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; + --config) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -c) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; diff --git a/assets/completions/zellij.fish b/assets/completions/zellij.fish index e902823f..eaba9fa4 100644 --- a/assets/completions/zellij.fish +++ b/assets/completions/zellij.fish @@ -2,6 +2,7 @@ complete -c zellij -n "__fish_use_subcommand" -s s -l split -d 'Send "split (dir complete -c zellij -n "__fish_use_subcommand" -s o -l open-file -d 'Send "open file in new pane" to active zellij session' complete -c zellij -n "__fish_use_subcommand" -l max-panes -d 'Maximum panes on screen, caution: opening more panes will close old ones' complete -c zellij -n "__fish_use_subcommand" -s l -l layout -d 'Path to a layout yaml file' +complete -c zellij -n "__fish_use_subcommand" -s c -l config -d 'Path to the configuration yaml file' complete -c zellij -n "__fish_use_subcommand" -s m -l move-focus -d 'Send "move focused pane" to active zellij session' complete -c zellij -n "__fish_use_subcommand" -s d -l debug complete -c zellij -n "__fish_use_subcommand" -s h -l help -d 'Prints help information' diff --git a/config.yaml b/config.yaml new file mode 100644 index 00000000..b1657775 --- /dev/null +++ b/config.yaml @@ -0,0 +1,8 @@ +--- +macro: + {name:"closePane", sequence: [NewPane: Right,]} + +keybinds: + Normal: + Backspace: [NewPane:, NewPane:,] + {F: 1}: [NewPane:,] diff --git a/src/cli.rs b/src/cli.rs index 6e384e16..67a92196 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -24,6 +24,10 @@ pub struct CliArgs { #[structopt(short, long)] pub layout: Option, + /// Path to the configuration yaml file + #[structopt(short, long)] + pub config: Option, + #[structopt(short, long)] pub debug: bool, } diff --git a/src/common/config.rs b/src/common/config.rs index 2ce25bff..705c1592 100644 --- a/src/common/config.rs +++ b/src/common/config.rs @@ -1,14 +1,15 @@ //! Deserializes configuration options. use std; -use std::collections::HashMap; +//use std::collections::HashMap; use std::error; use std::fmt::{self, Display}; use std::fs::File; -use std::io::{self,Read}; +use std::io::{self, Read}; use std::path::PathBuf; -use super::input::{keybinds,handler}; +use super::input::{keybinds, macros}; +use directories_next::ProjectDirs; use serde::Deserialize; /// Intermediate struct @@ -16,17 +17,17 @@ use serde::Deserialize; //} - /// Intermediate struct -//#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize)] pub struct ConfigFromYaml { - keybinds: HashMap>, + keybinds: Option, + macros: Option>, } ///// Deserialized config state #[derive(Debug, Clone, Default, Deserialize)] pub struct Config { - keybinds: Vec, + pub keybinds: keybinds::Keybinds, } #[derive(Debug)] @@ -43,37 +44,52 @@ pub enum ConfigError { Serde(serde_yaml::Error), //Eof, // io::Error - Io(io::Error) + Io(io::Error), } impl Config { /// Deserializes from given path - pub fn new(path: &PathBuf) -> Result { - let config_deserialized: Config; + pub fn new(path: &PathBuf) -> Result { + let config: Config; + let config_deserialized: ConfigFromYaml; let mut config_string = String::new(); + // TODO fix this unwrap 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()), + } } - Err(_) => { + Err(e) => { // TODO logging, if a file is not found // at an expected position - should not // panic @a-kenji - config_deserialized = Config::default(); + eprintln!("{}", e); + config = Config::default(); } } - Ok(config_deserialized) + Ok(config) + } + + pub fn from_option_or_default(option: Option) -> Result { + let config; + if let Some(config_path) = option { + config = 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)?; + } + return Ok(config); } } -//impl de::Error for ConfigError { - //fn custom(msg: T) -> Self { - //ConfigError::Message(msg.to_string()) - //} -//} - impl Display for ConfigError { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { match self { @@ -88,25 +104,17 @@ impl Display for ConfigError { } impl std::error::Error for ConfigError { - fn description(&self) -> &str { + fn cause(&self) -> Option<&dyn error::Error> { match *self { - //ConfigError::Message(ref err) => err, - ConfigError::Io(ref err) => err.to_string().as_str(), - ConfigError::Serde(ref err) => err.to_string().as_str(), + // 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::Serde(ref err) => Some(err), } } - - 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::Serde(ref err) => Some(err), - } -} } impl From for ConfigError { @@ -120,10 +128,3 @@ impl From for ConfigError { ConfigError::Serde(err) } } - -//impl From for ConfigError { - //fn from(err: de::Error::Message) -> ConfigError { - //ConfigError::Message(err) - //} -//} - diff --git a/src/common/input/handler.rs b/src/common/input/handler.rs index 5c4f1971..9d8b2cf2 100644 --- a/src/common/input/handler.rs +++ b/src/common/input/handler.rs @@ -2,6 +2,7 @@ use super::actions::Action; use super::keybinds::get_default_keybinds; +use crate::common::config::Config; use crate::common::{update_state, AppInstruction, AppState, SenderWithContext, OPENCALLS}; use crate::errors::ContextType; use crate::os_input_output::OsApi; @@ -22,6 +23,7 @@ struct InputHandler { /// The current input mode mode: InputMode, os_input: Box, + config: Config, command_is_executing: CommandIsExecuting, send_screen_instructions: SenderWithContext, send_pty_instructions: SenderWithContext, @@ -34,6 +36,7 @@ impl InputHandler { fn new( os_input: Box, command_is_executing: CommandIsExecuting, + config: Config, send_screen_instructions: SenderWithContext, send_pty_instructions: SenderWithContext, send_plugin_instructions: SenderWithContext, @@ -42,6 +45,7 @@ impl InputHandler { InputHandler { mode: InputMode::Normal, os_input, + config, command_is_executing, send_screen_instructions, send_pty_instructions, @@ -59,6 +63,8 @@ impl InputHandler { 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()); 'input_loop: loop { //@@@ I think this should actually just iterate over stdin directly let stdin_buffer = self.os_input.read_from_stdin(); @@ -76,9 +82,14 @@ impl InputHandler { // been revised. Sorry about this (@categorille) if { let mut should_break = false; - for action in - key_to_actions(&key, raw_bytes, &self.mode, &keybinds) - { + // 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, + ) { should_break |= self.dispatch_action(action); } should_break @@ -324,6 +335,7 @@ pub fn get_help(mode: InputMode) -> Help { /// its [`InputHandler::handle_input()`] loop. pub fn input_loop( os_input: Box, + config: Config, command_is_executing: CommandIsExecuting, send_screen_instructions: SenderWithContext, send_pty_instructions: SenderWithContext, @@ -333,6 +345,7 @@ pub fn input_loop( let _handler = InputHandler::new( os_input, command_is_executing, + config, send_screen_instructions, send_pty_instructions, send_plugin_instructions, diff --git a/src/common/input/keybinds.rs b/src/common/input/keybinds.rs index 0ea0695f..8799649d 100644 --- a/src/common/input/keybinds.rs +++ b/src/common/input/keybinds.rs @@ -23,7 +23,7 @@ pub fn get_default_keybinds() -> Result { Ok(defaults) } -/// Returns the default keybinds for a givent [`InputMode`]. +/// Returns the default keybinds for a given [`InputMode`]. fn get_defaults_for_mode(mode: &InputMode) -> Result { let mut defaults = ModeKeybinds::new(); diff --git a/src/common/input/macros.rs b/src/common/input/macros.rs index cbd14332..4b7c6df1 100644 --- a/src/common/input/macros.rs +++ b/src/common/input/macros.rs @@ -2,11 +2,11 @@ //! defined predictable order. use super::actions::Action; - + use serde::Deserialize; #[derive(Debug, Clone, Deserialize)] pub struct Macro { name: Option, - sequence: Vec + sequence: Vec, } diff --git a/src/common/mod.rs b/src/common/mod.rs index 5ba2e15d..c8b2ec98 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,5 +1,5 @@ -pub mod config; pub mod command_is_executing; +pub mod config; pub mod errors; pub mod input; pub mod install; @@ -46,10 +46,17 @@ pub enum ApiCommand { MoveFocus, } // FIXME: It would be good to add some more things to this over time -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct AppState { pub input_mode: InputMode, - pub config : Config, +} + +impl Default for AppState { + fn default() -> Self { + AppState { + input_mode: InputMode::default(), + } + } } // FIXME: Make this a method on the big `Communication` struct, so that app_tx can be extracted @@ -158,8 +165,13 @@ pub fn start(mut os_input: Box, opts: CliArgs) { .get_stdout_writer() .write(take_snapshot.as_bytes()) .unwrap(); + let mut app_state = AppState::default(); + let config = Config::from_option_or_default(opts.config) + .map_err(|e| eprintln!{"Config Error: {}", e}) + .unwrap(); + let command_is_executing = CommandIsExecuting::new(); let full_screen_ws = os_input.get_terminal_size_using_fd(0); @@ -638,9 +650,11 @@ pub fn start(mut os_input: Box, opts: CliArgs) { let send_pty_instructions = send_pty_instructions.clone(); let send_plugin_instructions = send_plugin_instructions.clone(); let os_input = os_input.clone(); + let config = config.clone(); move || { input_loop( os_input, + config, command_is_executing, send_screen_instructions, send_pty_instructions, From c97c553870380956904ab7d2daa36e5ce0aef010 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Fri, 5 Mar 2021 22:40:42 +0100 Subject: [PATCH 03/39] 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} + From 2b2b7325e7e6b6c3db6376cc0031148a3af1c086 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Thu, 11 Mar 2021 13:58:58 +0100 Subject: [PATCH 04/39] Poking around --- assets/completions/_zellij | 1 - assets/completions/zellij.bash | 6 +- assets/completions/zellij.fish | 2 +- config.yaml | 23 +++--- src/cli.rs | 2 +- src/common/config.rs | 11 ++- src/common/input/handler.rs | 72 ++++++++---------- src/common/input/keybinds.rs | 109 ++++++++++----------------- src/common/input/ut/config_test.rs | 43 ++++++----- src/common/input/ut/keybinds_test.rs | 76 +++++++++++++++++++ src/common/mod.rs | 2 +- 11 files changed, 193 insertions(+), 154 deletions(-) diff --git a/assets/completions/_zellij b/assets/completions/_zellij index cd8dce56..f292d560 100644 --- a/assets/completions/_zellij +++ b/assets/completions/_zellij @@ -22,7 +22,6 @@ _zellij() { '--max-panes=[Maximum panes on screen, caution: opening more panes will close old ones]' \ '-l+[Path to a layout yaml file]' \ '--layout=[Path to a layout yaml file]' \ -'-c+[Path to the configuration yaml file]' \ '--config=[Path to the configuration yaml file]' \ '-m[Send "move focused pane" to active zellij session]' \ '--move-focus[Send "move focused pane" to active zellij session]' \ diff --git a/assets/completions/zellij.bash b/assets/completions/zellij.bash index 0ede5cce..17afd610 100644 --- a/assets/completions/zellij.bash +++ b/assets/completions/zellij.bash @@ -20,7 +20,7 @@ _zellij() { case "${cmd}" in zellij) - opts=" -m -d -h -V -s -o -l -c --move-focus --debug --help --version --split --open-file --max-panes --layout --config " + opts=" -m -d -h -V -s -o -l --move-focus --debug --help --version --split --open-file --max-panes --layout --config " if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -59,10 +59,6 @@ _zellij() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - -c) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; *) COMPREPLY=() ;; diff --git a/assets/completions/zellij.fish b/assets/completions/zellij.fish index eaba9fa4..36425a25 100644 --- a/assets/completions/zellij.fish +++ b/assets/completions/zellij.fish @@ -2,7 +2,7 @@ complete -c zellij -n "__fish_use_subcommand" -s s -l split -d 'Send "split (dir complete -c zellij -n "__fish_use_subcommand" -s o -l open-file -d 'Send "open file in new pane" to active zellij session' complete -c zellij -n "__fish_use_subcommand" -l max-panes -d 'Maximum panes on screen, caution: opening more panes will close old ones' complete -c zellij -n "__fish_use_subcommand" -s l -l layout -d 'Path to a layout yaml file' -complete -c zellij -n "__fish_use_subcommand" -s c -l config -d 'Path to the configuration yaml file' +complete -c zellij -n "__fish_use_subcommand" -l config -d 'Path to the configuration yaml file' complete -c zellij -n "__fish_use_subcommand" -s m -l move-focus -d 'Send "move focused pane" to active zellij session' complete -c zellij -n "__fish_use_subcommand" -s d -l debug complete -c zellij -n "__fish_use_subcommand" -s h -l help -d 'Prints help information' diff --git a/config.yaml b/config.yaml index 719d15a2..eb227d4e 100644 --- a/config.yaml +++ b/config.yaml @@ -1,12 +1,17 @@ --- keybinds: Normal: - - {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,] + - action: [GoToTab: 1,] + key: [F: 1,] + - action: [GoToTab: 2,] + key: [F: 2,] + - action: [GoToTab: 3,] + key: [F: 3,] + - action: [GoToTab: 4,] + key: [F: 3,] + - action: [NewTab,] + key: [F: 5,] + - action: [NewPane: Right, NewPane: Right,] + key: [F: 6,] + - action: [NewPane: Right, NewPane: Right,] + key: [F: 6,] diff --git a/src/cli.rs b/src/cli.rs index 67a92196..d6d3057e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -25,7 +25,7 @@ pub struct CliArgs { pub layout: Option, /// Path to the configuration yaml file - #[structopt(short, long)] + #[structopt(long)] pub config: Option, #[structopt(short, long)] diff --git a/src/common/config.rs b/src/common/config.rs index bf8a9a9f..4a66acb0 100644 --- a/src/common/config.rs +++ b/src/common/config.rs @@ -1,12 +1,11 @@ //! Deserializes configuration options. -use std; use std::error; use std::fmt::{self, Display}; use std::fs::File; use std::io::{self, Read}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; -use super::input::keybinds::{Keybinds, MultipleKeybinds}; +use super::input::keybinds::{Keybinds, KeybindsFromYaml}; use crate::utils::logging::*; use directories_next::ProjectDirs; @@ -15,7 +14,7 @@ use serde::Deserialize; /// Intermediate deserialisation config struct #[derive(Debug, Deserialize)] pub struct ConfigFromYaml { - pub keybinds: Option, + pub keybinds: Option, } /// Main configuration. @@ -53,7 +52,7 @@ impl Config { /// 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 { + pub fn new(path: &Path) -> Result { match File::open(path) { Ok(mut file) => { let mut yaml_config = String::new(); @@ -81,7 +80,7 @@ impl Config { Ok(Config::new(&config_path)?) } else { let project_dirs = ProjectDirs::from("org", "Zellij Contributors", "Zellij").unwrap(); - let mut config_path: PathBuf = project_dirs.config_dir().to_owned().into(); + let mut config_path: PathBuf = project_dirs.config_dir().to_owned(); config_path.push("config.yaml"); Ok(Config::new(&config_path)?) } diff --git a/src/common/input/handler.rs b/src/common/input/handler.rs index 9b08491b..3438bba8 100644 --- a/src/common/input/handler.rs +++ b/src/common/input/handler.rs @@ -60,49 +60,43 @@ 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) = 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(); - drop( - self.send_plugin_instructions - .send(PluginInstruction::GlobalInput(stdin_buffer.clone())), - ); - for key_result in stdin_buffer.events_and_raw() { - match key_result { - Ok((event, raw_bytes)) => match event { - termion::event::Event::Key(key) => { - // FIXME this explicit break is needed because the current test - // framework relies on it to not create dead threads that loop - // and eat up CPUs. Do not remove until the test framework has - // been revised. Sorry about this (@categorille) - if { - let mut should_break = false; - // Hacked on way to have a means of testing Macros, needs to - // get properly integrated - for action in Keybinds::key_to_actions( - &key, raw_bytes, &self.mode, &keybinds, - ) { - should_break |= self.dispatch_action(action); - } - should_break - } { - break 'input_loop; + 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(); + drop( + self.send_plugin_instructions + .send(PluginInstruction::GlobalInput(stdin_buffer.clone())), + ); + for key_result in stdin_buffer.events_and_raw() { + match key_result { + Ok((event, raw_bytes)) => match event { + termion::event::Event::Key(key) => { + // FIXME this explicit break is needed because the current test + // framework relies on it to not create dead threads that loop + // and eat up CPUs. Do not remove until the test framework has + // been revised. Sorry about this (@categorille) + if { + let mut should_break = false; + // Hacked on way to have a means of testing Macros, needs to + // get properly integrated + for action in + Keybinds::key_to_actions(&key, raw_bytes, &self.mode, &keybinds) + { + should_break |= self.dispatch_action(action); } + should_break + } { + break 'input_loop; } - termion::event::Event::Mouse(_) - | termion::event::Event::Unsupported(_) => { - unimplemented!("Mouse and unsupported events aren't supported!"); - } - }, - Err(err) => panic!("Encountered read error: {:?}", err), - } + } + termion::event::Event::Mouse(_) | termion::event::Event::Unsupported(_) => { + unimplemented!("Mouse and unsupported events aren't supported!"); + } + }, + Err(err) => panic!("Encountered read error: {:?}", err), } } - } else { - //@@@ Error handling? - self.exit(); } } diff --git a/src/common/input/keybinds.rs b/src/common/input/keybinds.rs index 8b089ecf..2c395d37 100644 --- a/src/common/input/keybinds.rs +++ b/src/common/input/keybinds.rs @@ -8,50 +8,41 @@ use serde::Deserialize; use strum::IntoEnumIterator; use termion::event::Key; -#[derive(Clone, Debug, PartialEq, Deserialize)] +#[derive(Clone, Debug, PartialEq)] pub struct Keybinds(HashMap); -#[derive(Clone, Debug, Default, PartialEq, Deserialize)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct ModeKeybinds(HashMap>); -/// 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>); +/// Intermediate struct used for deserialisation +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct KeybindsFromYaml(HashMap>); -/// Intermediate enum for configuration. -#[derive(Debug, Clone, PartialEq, Deserialize)] -#[serde(untagged)] -enum ModeKeybind { - ModeKeybinds(ModeKeybinds), - MultipleModeKeybinds(MultipleModeKeybinds), +/// Intermediate struct used for deserialisation +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct KeyActionFromYaml { + action: Vec, + key: Vec, } 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::::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)?); + .insert(mode, Keybinds::get_defaults_for_mode(&mode)); } + defaults + } +} - Ok(defaults) +impl Keybinds { + pub fn new() -> Keybinds { + Keybinds(HashMap::::new()) } - pub fn get_default_keybinds_with_config(keybinds: Option) -> Keybinds { + 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)) @@ -80,7 +71,7 @@ impl Keybinds { } /// Returns the default keybinds for a given [`InputMode`]. - fn get_defaults_for_mode(mode: &InputMode) -> Result { + fn get_defaults_for_mode(mode: &InputMode) -> ModeKeybinds { let mut defaults = HashMap::new(); match *mode { @@ -212,8 +203,7 @@ impl Keybinds { defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Command)]); } } - - Ok(ModeKeybinds::from(defaults)) + ModeKeybinds(defaults) } /// Converts a [`Key`] terminal event to a sequence of [`Action`]s according to the current @@ -242,39 +232,27 @@ impl Keybinds { } impl ModeKeybinds { - pub fn new() -> ModeKeybinds { - ModeKeybinds::from(HashMap::>::new()) + fn new() -> ModeKeybinds { + ModeKeybinds(HashMap::>::new()) } /// Merges `self` with `other`, if keys are the same, `other` overwrites. - pub fn merge(self, other: ModeKeybinds) -> ModeKeybinds { + fn merge(self, other: ModeKeybinds) -> ModeKeybinds { let mut merged = self; - merged.0.extend(other.0.clone()); + merged.0.extend(other.0); 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 { +impl From for Keybinds { + fn from(keybinds_from_yaml: KeybindsFromYaml) -> 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())); - } - } + for key_action in keybinds_from_yaml.0.get(&mode).iter() { + for keybind in key_action.iter() { + mode_keybinds = mode_keybinds.merge(ModeKeybinds::from(keybind.clone())); } } keybinds.0.insert(mode, mode_keybinds); @@ -283,25 +261,18 @@ impl From for Keybinds { } } -impl From> for Keybinds { - fn from(map: HashMap) -> Keybinds { - Keybinds(map) - } -} -impl From>> for ModeKeybinds { - fn from(map: HashMap>) -> ModeKeybinds { - ModeKeybinds(map) - } -} +/// For each `Key` assigned to `Action`s, +/// map the `Action`s to the key +impl From for ModeKeybinds { + fn from(key_action: KeyActionFromYaml) -> ModeKeybinds { + let keys = key_action.key; + let actions = key_action.action; -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) + ModeKeybinds( + keys.into_iter() + .map(|k| (k, actions.clone())) + .collect::>>(), + ) } } diff --git a/src/common/input/ut/config_test.rs b/src/common/input/ut/config_test.rs index 001aab15..17649d14 100644 --- a/src/common/input/ut/config_test.rs +++ b/src/common/input/ut/config_test.rs @@ -1,13 +1,12 @@ //! For Configuration Options use super::super::config::*; -use crate::common::input::keybinds::*; use crate::common::input::actions::*; +use crate::common::input::keybinds::*; 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"); @@ -23,29 +22,29 @@ fn no_config_option_file_equals_default_config() { 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} - "; +//#[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(); +//let config_options = Config::from_yaml(&options).unwrap(); - assert_eq!(config_options, config_options) -} +//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(); +//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 index e69de29b..44472985 100644 --- a/src/common/input/ut/keybinds_test.rs +++ b/src/common/input/ut/keybinds_test.rs @@ -0,0 +1,76 @@ +use super::super::actions::*; +use super::super::keybinds::*; +use termion::event::Key; + +#[test] +fn merge_keybinds_merges_different_keys() { + let mut mode_keybinds_self = ModeKeybinds::new(); + mode_keybinds_self.0.insert(Key::F(1), vec![Action::NoOp]); + let mut mode_keybinds_other = ModeKeybinds::new(); + mode_keybinds_other + .0 + .insert(Key::Backspace, vec![Action::NoOp]); + + let mut mode_keybinds_expected = ModeKeybinds::new(); + mode_keybinds_expected + .0 + .insert(Key::F(1), vec![Action::NoOp]); + mode_keybinds_expected + .0 + .insert(Key::Backspace, vec![Action::NoOp]); + + let mode_keybinds_merged = mode_keybinds_self.merge(mode_keybinds_other); + + assert_eq!(mode_keybinds_expected, mode_keybinds_merged); +} + +#[test] +fn merge_mode_keybinds_overwrites_same_keys() { + let mut mode_keybinds_self = ModeKeybinds::new(); + mode_keybinds_self.0.insert(Key::F(1), vec![Action::NoOp]); + let mut mode_keybinds_other = ModeKeybinds::new(); + mode_keybinds_other + .0 + .insert(Key::F(1), vec![Action::GoToTab(1)]); + + let mut mode_keybinds_expected = ModeKeybinds::new(); + mode_keybinds_expected + .0 + .insert(Key::F(1), vec![Action::GoToTab(1)]); + + let mode_keybinds_merged = mode_keybinds_self.merge(mode_keybinds_other); + + assert_eq!(mode_keybinds_expected, mode_keybinds_merged); +} + +#[test] +fn merge_keybinds_merges() { + let mut mode_keybinds_self = ModeKeybinds::new(); + mode_keybinds_self.0.insert(Key::F(1), vec![Action::NoOp]); + let mut mode_keybinds_other = ModeKeybinds::new(); + mode_keybinds_other + .0 + .insert(Key::Backspace, vec![Action::NoOp]); + let mut keybinds_self = Keybinds::new(); + keybinds_self + .0 + .insert(InputMode::Normal, mode_keybinds_self.clone()); + let mut keybinds_other = Keybinds::new(); + keybinds_other + .0 + .insert(InputMode::Resize, mode_keybinds_other.clone()); + let mut keybinds_expected = Keybinds::new(); + keybinds_expected.0.insert( + InputMode::Normal, + mode_keybinds_self + ); + keybinds_expected.0.insert( + InputMode::Resize, + mode_keybinds_other + ); + + assert_eq!( + keybinds_expected, + keybinds_self.merge_keybinds(keybinds_other) + ) +} diff --git a/src/common/mod.rs b/src/common/mod.rs index fdd66649..3e2f370a 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -656,7 +656,7 @@ pub fn start(mut os_input: Box, opts: CliArgs) { let send_pty_instructions = send_pty_instructions.clone(); let send_plugin_instructions = send_plugin_instructions.clone(); let os_input = os_input.clone(); - let config = config.clone(); + let config = config; move || { input_loop( os_input, From b4fca93eb71ad15c39a539d0d511b57785d177ad Mon Sep 17 00:00:00 2001 From: a-kenji Date: Thu, 11 Mar 2021 16:38:22 +0100 Subject: [PATCH 05/39] Poking --- src/common/input/keybinds.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/common/input/keybinds.rs b/src/common/input/keybinds.rs index 2c395d37..de75c38f 100644 --- a/src/common/input/keybinds.rs +++ b/src/common/input/keybinds.rs @@ -54,18 +54,17 @@ impl 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(); + let mut keybinds = Keybinds::new(); - for mode in InputMode::iter() { - let mut mode_keybinds: ModeKeybinds = if let Some(keybind) = self.0.get(&mode) { - keybind.clone() - } else { - ModeKeybinds::default() + for mode in self.0.keys().chain(other.0.keys()) { + let mut mode_keybinds = ModeKeybinds::new(); + if let Some(keybind) = self.0.get(&mode) { + mode_keybinds.0.extend(keybind.0.clone()); }; if let Some(keybind) = other.0.get(&mode) { mode_keybinds.0.extend(keybind.0.clone()); } - keybinds.0.insert(mode, mode_keybinds); + keybinds.0.insert(mode.clone(), mode_keybinds); } keybinds } From a86d8c216186030754a42968768f2af6a131d90d Mon Sep 17 00:00:00 2001 From: a-kenji Date: Fri, 12 Mar 2021 22:05:41 +0100 Subject: [PATCH 06/39] Add example config file. --- config.yaml => example/config.yaml | 8 +- src/common/{ => input}/config.rs | 7 +- src/common/input/handler.rs | 9 ++- src/common/input/keybinds.rs | 6 +- src/common/input/mod.rs | 1 + src/common/input/ut/config_test.rs | 31 -------- src/common/input/ut/keybinds_test.rs | 75 +++++++++++++++++-- src/common/mod.rs | 3 +- .../config/functioning_simple_config.yaml | 0 .../config/multiple_keys_for_one_action.yaml | 8 -- 10 files changed, 85 insertions(+), 63 deletions(-) rename config.yaml => example/config.yaml (58%) rename src/common/{ => input}/config.rs (93%) delete mode 100644 src/tests/fixtures/config/functioning_simple_config.yaml delete mode 100644 src/tests/fixtures/config/multiple_keys_for_one_action.yaml diff --git a/config.yaml b/example/config.yaml similarity index 58% rename from config.yaml rename to example/config.yaml index eb227d4e..f19a2114 100644 --- a/config.yaml +++ b/example/config.yaml @@ -1,6 +1,6 @@ --- keybinds: - Normal: + normal: - action: [GoToTab: 1,] key: [F: 1,] - action: [GoToTab: 2,] @@ -8,10 +8,6 @@ keybinds: - action: [GoToTab: 3,] key: [F: 3,] - action: [GoToTab: 4,] - key: [F: 3,] + key: [F: 4,] - action: [NewTab,] key: [F: 5,] - - action: [NewPane: Right, NewPane: Right,] - key: [F: 6,] - - action: [NewPane: Right, NewPane: Right,] - key: [F: 6,] diff --git a/src/common/config.rs b/src/common/input/config.rs similarity index 93% rename from src/common/config.rs rename to src/common/input/config.rs index 4a66acb0..b63ccef1 100644 --- a/src/common/config.rs +++ b/src/common/input/config.rs @@ -5,7 +5,7 @@ use std::fs::File; use std::io::{self, Read}; use std::path::{Path, PathBuf}; -use super::input::keybinds::{Keybinds, KeybindsFromYaml}; +use super::keybinds::{Keybinds, KeybindsFromYaml}; use crate::utils::logging::*; use directories_next::ProjectDirs; @@ -61,9 +61,6 @@ impl Config { Ok(Config::from_yaml(&yaml_config)?) } Err(e) => { - // TODO logging, if a file is not found - // at an expected position - should not - // panic, but log to file @a-kenji debug_log_to_file(format!( "{}\nUsing the default configuration!", ConfigError::IoPath(e, path.to_path_buf()) @@ -123,5 +120,5 @@ impl From for ConfigError { // The unit test location. #[cfg(test)] -#[path = "./input/ut/config_test.rs"] +#[path = "./ut/config_test.rs"] mod config_test; diff --git a/src/common/input/handler.rs b/src/common/input/handler.rs index 862cc0d2..dc9fa382 100644 --- a/src/common/input/handler.rs +++ b/src/common/input/handler.rs @@ -2,7 +2,7 @@ use super::actions::Action; use super::keybinds::Keybinds; -use crate::common::config::Config; +use crate::common::input::config::Config; use crate::common::{update_state, AppInstruction, AppState, SenderWithContext, OPENCALLS}; use crate::errors::ContextType; use crate::os_input_output::OsApi; @@ -273,18 +273,25 @@ impl InputHandler { pub enum InputMode { /// In `Normal` mode, input is always written to the terminal, except for the shortcuts leading /// to other modes + #[serde(alias = "normal")] Normal, /// In `Locked` mode, input is always written to the terminal and all shortcuts are disabled /// except the one leading back to normal mode + #[serde(alias = "locked")] Locked, /// `Resize` mode allows resizing the different existing panes. + #[serde(alias = "resize")] Resize, /// `Pane` mode allows creating and closing panes, as well as moving between them. + #[serde(alias = "pane")] Pane, /// `Tab` mode allows creating and closing tabs, as well as moving between them. + #[serde(alias = "tab")] Tab, /// `Scroll` mode allows scrolling up and down within a pane. + #[serde(alias = "scroll")] Scroll, + #[serde(alias = "renametab")] RenameTab, } diff --git a/src/common/input/keybinds.rs b/src/common/input/keybinds.rs index d7f64652..0c37693c 100644 --- a/src/common/input/keybinds.rs +++ b/src/common/input/keybinds.rs @@ -56,7 +56,7 @@ impl Keybinds { fn merge_keybinds(&self, other: Keybinds) -> Keybinds { let mut keybinds = Keybinds::new(); - for mode in self.0.keys().chain(other.0.keys()) { + for mode in InputMode::iter() { let mut mode_keybinds = ModeKeybinds::new(); if let Some(keybind) = self.0.get(&mode) { mode_keybinds.0.extend(keybind.0.clone()); @@ -64,7 +64,9 @@ impl Keybinds { if let Some(keybind) = other.0.get(&mode) { mode_keybinds.0.extend(keybind.0.clone()); } - keybinds.0.insert(mode.clone(), mode_keybinds); + if !mode_keybinds.0.is_empty() { + keybinds.0.insert(mode, mode_keybinds); + } } keybinds } diff --git a/src/common/input/mod.rs b/src/common/input/mod.rs index a7dca7ed..6589cac2 100644 --- a/src/common/input/mod.rs +++ b/src/common/input/mod.rs @@ -1,5 +1,6 @@ //! The way terminal input is handled. pub mod actions; +pub mod config; pub mod handler; pub mod keybinds; diff --git a/src/common/input/ut/config_test.rs b/src/common/input/ut/config_test.rs index 17649d14..eb4bc960 100644 --- a/src/common/input/ut/config_test.rs +++ b/src/common/input/ut/config_test.rs @@ -1,12 +1,8 @@ //! For Configuration Options use super::super::config::*; -use crate::common::input::actions::*; -use crate::common::input::keybinds::*; 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"); @@ -21,30 +17,3 @@ fn no_config_option_file_equals_default_config() { 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 index 44472985..2b306bc3 100644 --- a/src/common/input/ut/keybinds_test.rs +++ b/src/common/input/ut/keybinds_test.rs @@ -60,17 +60,76 @@ fn merge_keybinds_merges() { .0 .insert(InputMode::Resize, mode_keybinds_other.clone()); let mut keybinds_expected = Keybinds::new(); - keybinds_expected.0.insert( - InputMode::Normal, - mode_keybinds_self - ); - keybinds_expected.0.insert( - InputMode::Resize, - mode_keybinds_other - ); + keybinds_expected + .0 + .insert(InputMode::Normal, mode_keybinds_self); + keybinds_expected + .0 + .insert(InputMode::Resize, mode_keybinds_other); assert_eq!( keybinds_expected, keybinds_self.merge_keybinds(keybinds_other) ) } + +#[test] +fn merge_keybinds_overwrites_same_keys() { + let mut mode_keybinds_self = ModeKeybinds::new(); + mode_keybinds_self.0.insert(Key::F(1), vec![Action::NoOp]); + mode_keybinds_self.0.insert(Key::F(2), vec![Action::NoOp]); + mode_keybinds_self.0.insert(Key::F(3), vec![Action::NoOp]); + let mut mode_keybinds_other = ModeKeybinds::new(); + mode_keybinds_other + .0 + .insert(Key::F(1), vec![Action::GoToTab(1)]); + mode_keybinds_other + .0 + .insert(Key::F(2), vec![Action::GoToTab(2)]); + mode_keybinds_other + .0 + .insert(Key::F(3), vec![Action::GoToTab(3)]); + let mut keybinds_self = Keybinds::new(); + keybinds_self + .0 + .insert(InputMode::Normal, mode_keybinds_self.clone()); + let mut keybinds_other = Keybinds::new(); + keybinds_other + .0 + .insert(InputMode::Normal, mode_keybinds_other.clone()); + let mut keybinds_expected = Keybinds::new(); + keybinds_expected + .0 + .insert(InputMode::Normal, mode_keybinds_other); + + assert_eq!( + keybinds_expected, + keybinds_self.merge_keybinds(keybinds_other) + ) +} + +#[test] +fn from_keyaction_from_yaml_to_mode_keybindings() { + let actions = vec![Action::NoOp, Action::GoToTab(1)]; + let keyaction = KeyActionFromYaml { + action: actions.clone(), + key: vec![Key::F(1), Key::Backspace, Key::Char('t')], + }; + + let mut expected = ModeKeybinds::new(); + expected.0.insert(Key::F(1), actions.clone()); + expected.0.insert(Key::Backspace, actions.clone()); + expected.0.insert(Key::Char('t'), actions); + + assert_eq!(expected, ModeKeybinds::from(keyaction)); +} + +//#[test] +//fn from_keybinds_from_yaml_to_keybinds(){ +//let mut keybinds_from_yaml = KeybindsFromYaml(HashMap>); +//let actions = vec![Action::NoOp, Action::GoToTab(1), ]; +//let keyaction = KeyActionFromYaml { +//action : actions.clone(), +//key : vec![ Key::F(1), Key::Backspace , Key::Char('t'), ], +//}; +//} diff --git a/src/common/mod.rs b/src/common/mod.rs index c9b4959a..8351bc4a 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,5 +1,4 @@ pub mod command_is_executing; -pub mod config; pub mod errors; pub mod input; pub mod install; @@ -27,7 +26,7 @@ use wasmer::{ChainableNamedResolver, Instance, Module, Store, Value}; use wasmer_wasi::{Pipe, WasiState}; use crate::cli::CliArgs; -use crate::common::config::Config; +use crate::common::input::config::Config; use crate::layout::Layout; use crate::utils::logging::*; use command_is_executing::CommandIsExecuting; diff --git a/src/tests/fixtures/config/functioning_simple_config.yaml b/src/tests/fixtures/config/functioning_simple_config.yaml deleted file mode 100644 index e69de29b..00000000 diff --git a/src/tests/fixtures/config/multiple_keys_for_one_action.yaml b/src/tests/fixtures/config/multiple_keys_for_one_action.yaml deleted file mode 100644 index 7da62541..00000000 --- a/src/tests/fixtures/config/multiple_keys_for_one_action.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -keybindings: - Normal: - - ? - F: 6 - - F: 7 - - F: 8 - : - {GoToTab: 5} - From 1da743b1c7eba86ff0178a043053978596583a50 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Sat, 13 Mar 2021 10:20:56 +0100 Subject: [PATCH 07/39] Add more options to example config file --- example/config.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/example/config.yaml b/example/config.yaml index f19a2114..fcd75a66 100644 --- a/example/config.yaml +++ b/example/config.yaml @@ -11,3 +11,11 @@ keybinds: key: [F: 4,] - action: [NewTab,] key: [F: 5,] + - action: [SwitchToMode: Pane , MoveFocus: Left, SwitchToMode: Normal,] + key: [ Alt: h,] + - action: [SwitchToMode: Pane , MoveFocus: Right, SwitchToMode: Normal,] + key: [ Alt: l,] + - action: [SwitchToMode: Pane , MoveFocus: Down, SwitchToMode: Normal,] + key: [ Alt: j,] + - action: [SwitchToMode: Pane , MoveFocus: Up, SwitchToMode: Normal,] + key: [ Alt: k,] From cac143db76344b90202762af029b8263cc651649 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Sat, 13 Mar 2021 10:37:38 +0100 Subject: [PATCH 08/39] Modify example config file --- example/config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/example/config.yaml b/example/config.yaml index fcd75a66..a91f24f9 100644 --- a/example/config.yaml +++ b/example/config.yaml @@ -11,11 +11,11 @@ keybinds: key: [F: 4,] - action: [NewTab,] key: [F: 5,] - - action: [SwitchToMode: Pane , MoveFocus: Left, SwitchToMode: Normal,] + - action: [MoveFocus: Left,] key: [ Alt: h,] - - action: [SwitchToMode: Pane , MoveFocus: Right, SwitchToMode: Normal,] + - action: [MoveFocus: Right,] key: [ Alt: l,] - - action: [SwitchToMode: Pane , MoveFocus: Down, SwitchToMode: Normal,] + - action: [MoveFocus: Down,] key: [ Alt: j,] - - action: [SwitchToMode: Pane , MoveFocus: Up, SwitchToMode: Normal,] + - action: [MoveFocus: Up,] key: [ Alt: k,] From 9c0762fc53b567a34d234b2f152e49add895a620 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Sat, 13 Mar 2021 12:25:28 +0100 Subject: [PATCH 09/39] Update default config file --- example/config.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/example/config.yaml b/example/config.yaml index a91f24f9..3bb4fbfa 100644 --- a/example/config.yaml +++ b/example/config.yaml @@ -19,3 +19,8 @@ keybinds: key: [ Alt: j,] - action: [MoveFocus: Up,] key: [ Alt: k,] + pane: + - action: [ NewPane:, SwitchToMode: Normal,] + key: [Char: 'n',] + - action: [ NewPane: , ] + key: [Char: 'N',] From e0d0502539a558b547df7206f621034605021463 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Sat, 13 Mar 2021 12:48:27 +0100 Subject: [PATCH 10/39] Add config to README --- README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/README.md b/README.md index f9bc9fe0..e1986d9c 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,42 @@ Zellij was initially called "Mosaic". The status bar on the bottom should guide you through the possible keyboard shortcuts in the app. +# Configuration +It is possible to configure keyboard shortcuts and their actions in a yaml file. +An example file can be found under `example/config.yaml`. + +To pass a config file to zellij run it either with: +`cargo run -- --config [FILE]` or `zellij --config [FILE]`. + +The structure is as follows: +``` +keybinds: + normal: + - action: [] + key: [] +``` +`normal` is one of the `modes` zellij can be in. +It is possible to bind a sequence of actions to numerous keys at the same time. + + +For example: +``` +keybinds: + normal: + - action: [ NewTab, GoToTab: 1,] + key: [ Char: 'c',] +``` +Will create a new tab and then switch to tab number 1 on pressing the +`c` key. +Whereas: +``` +keybinds: + normal: + - action: [ NewTab,] + key: [ Char: 'c', Char: 'd',] +``` +Will create a new tab on pressing either the `c` or the `d` key. + # What is the current status of the project? Zellij is in the last stages of being VT compatible. As much as modern terminals are. From 2ce034a66acb09a826b124f9d83e57f611ff9aed Mon Sep 17 00:00:00 2001 From: a-kenji Date: Sat, 13 Mar 2021 13:07:46 +0100 Subject: [PATCH 11/39] Remove unneccessary comment --- src/common/input/handler.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/common/input/handler.rs b/src/common/input/handler.rs index dc9fa382..49d87124 100644 --- a/src/common/input/handler.rs +++ b/src/common/input/handler.rs @@ -78,8 +78,6 @@ impl InputHandler { // been revised. Sorry about this (@categorille) if { let mut should_break = false; - // Hacked on way to have a means of testing Macros, needs to - // get properly integrated for action in Keybinds::key_to_actions(&key, raw_bytes, &self.mode, &keybinds) { From d3a72a27df5021d35600071b96edd8c0f79e9105 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Tue, 16 Mar 2021 21:14:10 +0100 Subject: [PATCH 12/39] Add key events documentation Add key events documentation from termion to the README --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e1986d9c..eea40060 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,9 @@ The status bar on the bottom should guide you through the possible keyboard shor It is possible to configure keyboard shortcuts and their actions in a yaml file. An example file can be found under `example/config.yaml`. -To pass a config file to zellij run it either with: +Zellij will look for a file `/zellij/config.yaml` in the default configuration location of your os. + +To pass a config file directly to zellij run it either with: `cargo run -- --config [FILE]` or `zellij --config [FILE]`. The structure is as follows: @@ -52,7 +54,7 @@ keybinds: ``` `normal` is one of the `modes` zellij can be in. It is possible to bind a sequence of actions to numerous keys at the same time. - +Here a reference to the [Key](https://docs.rs/termion/1.5.6/termion/event/enum.Key.html) format that is used. For example: ``` From 564ded0bb3a47ab0918e20d5505385f35d31906b Mon Sep 17 00:00:00 2001 From: a-kenji Date: Tue, 16 Mar 2021 21:22:59 +0100 Subject: [PATCH 13/39] Add short --config -c option --- assets/completions/_zellij | 1 + assets/completions/zellij.bash | 6 +++++- assets/completions/zellij.fish | 2 +- src/cli.rs | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/assets/completions/_zellij b/assets/completions/_zellij index f292d560..cd8dce56 100644 --- a/assets/completions/_zellij +++ b/assets/completions/_zellij @@ -22,6 +22,7 @@ _zellij() { '--max-panes=[Maximum panes on screen, caution: opening more panes will close old ones]' \ '-l+[Path to a layout yaml file]' \ '--layout=[Path to a layout yaml file]' \ +'-c+[Path to the configuration yaml file]' \ '--config=[Path to the configuration yaml file]' \ '-m[Send "move focused pane" to active zellij session]' \ '--move-focus[Send "move focused pane" to active zellij session]' \ diff --git a/assets/completions/zellij.bash b/assets/completions/zellij.bash index 17afd610..0ede5cce 100644 --- a/assets/completions/zellij.bash +++ b/assets/completions/zellij.bash @@ -20,7 +20,7 @@ _zellij() { case "${cmd}" in zellij) - opts=" -m -d -h -V -s -o -l --move-focus --debug --help --version --split --open-file --max-panes --layout --config " + opts=" -m -d -h -V -s -o -l -c --move-focus --debug --help --version --split --open-file --max-panes --layout --config " if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -59,6 +59,10 @@ _zellij() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; + -c) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; diff --git a/assets/completions/zellij.fish b/assets/completions/zellij.fish index 36425a25..eaba9fa4 100644 --- a/assets/completions/zellij.fish +++ b/assets/completions/zellij.fish @@ -2,7 +2,7 @@ complete -c zellij -n "__fish_use_subcommand" -s s -l split -d 'Send "split (dir complete -c zellij -n "__fish_use_subcommand" -s o -l open-file -d 'Send "open file in new pane" to active zellij session' complete -c zellij -n "__fish_use_subcommand" -l max-panes -d 'Maximum panes on screen, caution: opening more panes will close old ones' complete -c zellij -n "__fish_use_subcommand" -s l -l layout -d 'Path to a layout yaml file' -complete -c zellij -n "__fish_use_subcommand" -l config -d 'Path to the configuration yaml file' +complete -c zellij -n "__fish_use_subcommand" -s c -l config -d 'Path to the configuration yaml file' complete -c zellij -n "__fish_use_subcommand" -s m -l move-focus -d 'Send "move focused pane" to active zellij session' complete -c zellij -n "__fish_use_subcommand" -s d -l debug complete -c zellij -n "__fish_use_subcommand" -s h -l help -d 'Prints help information' diff --git a/src/cli.rs b/src/cli.rs index d6d3057e..67a92196 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -25,7 +25,7 @@ pub struct CliArgs { pub layout: Option, /// Path to the configuration yaml file - #[structopt(long)] + #[structopt(short, long)] pub config: Option, #[structopt(short, long)] From 224c606b4507c3a350a9bf50d01d1ddad380649a Mon Sep 17 00:00:00 2001 From: a-kenji Date: Tue, 16 Mar 2021 21:34:50 +0100 Subject: [PATCH 14/39] Remove logging to log.txt --- src/common/input/config.rs | 9 +-------- src/common/mod.rs | 2 -- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/common/input/config.rs b/src/common/input/config.rs index b63ccef1..d9d1f784 100644 --- a/src/common/input/config.rs +++ b/src/common/input/config.rs @@ -6,7 +6,6 @@ use std::io::{self, Read}; use std::path::{Path, PathBuf}; use super::keybinds::{Keybinds, KeybindsFromYaml}; -use crate::utils::logging::*; use directories_next::ProjectDirs; use serde::Deserialize; @@ -49,8 +48,6 @@ impl Config { } /// 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: &Path) -> Result { match File::open(path) { @@ -60,11 +57,7 @@ impl Config { .map_err(|e| ConfigError::IoPath(e, path.to_path_buf()))?; Ok(Config::from_yaml(&yaml_config)?) } - Err(e) => { - debug_log_to_file(format!( - "{}\nUsing the default configuration!", - ConfigError::IoPath(e, path.to_path_buf()) - )); + Err(_) => { Ok(Config::default()) } } diff --git a/src/common/mod.rs b/src/common/mod.rs index 8351bc4a..02e75f98 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -28,7 +28,6 @@ use wasmer_wasi::{Pipe, WasiState}; use crate::cli::CliArgs; use crate::common::input::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; @@ -173,7 +172,6 @@ pub fn start(mut os_input: Box, opts: CliArgs) { #[allow(unused_must_use)] let config = Config::from_option_or_default(opts.config) .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); }) From e64192b42e584663ac6a64a73cff1921f098d945 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Wed, 17 Mar 2021 16:56:02 +0100 Subject: [PATCH 15/39] Add a clean config option Add a clean config option, which makes zellij not look for a default configuration file. --- assets/completions/_zellij | 49 +++++++++++++++++++++++++++++++--- assets/completions/zellij.bash | 46 ++++++++++++++++++++++++------- assets/completions/zellij.fish | 8 +++++- src/cli.rs | 18 ++++++++++--- src/common/input/config.rs | 21 ++++++++++----- src/common/mod.rs | 3 +-- 6 files changed, 119 insertions(+), 26 deletions(-) diff --git a/assets/completions/_zellij b/assets/completions/_zellij index cd8dce56..8b89db14 100644 --- a/assets/completions/_zellij +++ b/assets/completions/_zellij @@ -22,8 +22,6 @@ _zellij() { '--max-panes=[Maximum panes on screen, caution: opening more panes will close old ones]' \ '-l+[Path to a layout yaml file]' \ '--layout=[Path to a layout yaml file]' \ -'-c+[Path to the configuration yaml file]' \ -'--config=[Path to the configuration yaml file]' \ '-m[Send "move focused pane" to active zellij session]' \ '--move-focus[Send "move focused pane" to active zellij session]' \ '-d[]' \ @@ -32,16 +30,59 @@ _zellij() { '--help[Prints help information]' \ '-V[Prints version information]' \ '--version[Prints version information]' \ +":: :_zellij_commands" \ +"*::: :->zellij" \ && ret=0 - + case $state in + (zellij) + words=($line[1] "${words[@]}") + (( CURRENT += 1 )) + curcontext="${curcontext%:*:*}:zellij-command-$line[1]:" + case $line[1] in + (config) +_arguments "${_arguments_options[@]}" \ +'--clean[Disables loading of configuration file at default location]' \ +'-h[Prints help information]' \ +'--help[Prints help information]' \ +'-V[Prints version information]' \ +'--version[Prints version information]' \ +'::path:_files' \ +&& ret=0 +;; +(help) +_arguments "${_arguments_options[@]}" \ +'-h[Prints help information]' \ +'--help[Prints help information]' \ +'-V[Prints version information]' \ +'--version[Prints version information]' \ +&& ret=0 +;; + esac + ;; +esac } (( $+functions[_zellij_commands] )) || _zellij_commands() { local commands; commands=( - + "config:Path to the configuration yaml file" \ +"help:Prints this message or the help of the given subcommand(s)" \ ) _describe -t commands 'zellij commands' commands "$@" } +(( $+functions[_zellij__config_commands] )) || +_zellij__config_commands() { + local commands; commands=( + + ) + _describe -t commands 'zellij config commands' commands "$@" +} +(( $+functions[_zellij__help_commands] )) || +_zellij__help_commands() { + local commands; commands=( + + ) + _describe -t commands 'zellij help commands' commands "$@" +} _zellij "$@" \ No newline at end of file diff --git a/assets/completions/zellij.bash b/assets/completions/zellij.bash index 0ede5cce..549b1e9f 100644 --- a/assets/completions/zellij.bash +++ b/assets/completions/zellij.bash @@ -13,6 +13,12 @@ _zellij() { cmd="zellij" ;; + config) + cmd+="__config" + ;; + help) + cmd+="__help" + ;; *) ;; esac @@ -20,7 +26,7 @@ _zellij() { case "${cmd}" in zellij) - opts=" -m -d -h -V -s -o -l -c --move-focus --debug --help --version --split --open-file --max-panes --layout --config " + opts=" -m -d -h -V -s -o -l --move-focus --debug --help --version --split --open-file --max-panes --layout config help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -55,14 +61,6 @@ _zellij() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --config) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - -c) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; *) COMPREPLY=() ;; @@ -71,6 +69,36 @@ _zellij() { return 0 ;; + zellij__config) + opts=" -h -V --clean --help --version " + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + zellij__help) + opts=" -h -V --help --version " + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; esac } diff --git a/assets/completions/zellij.fish b/assets/completions/zellij.fish index eaba9fa4..36bab192 100644 --- a/assets/completions/zellij.fish +++ b/assets/completions/zellij.fish @@ -2,8 +2,14 @@ complete -c zellij -n "__fish_use_subcommand" -s s -l split -d 'Send "split (dir complete -c zellij -n "__fish_use_subcommand" -s o -l open-file -d 'Send "open file in new pane" to active zellij session' complete -c zellij -n "__fish_use_subcommand" -l max-panes -d 'Maximum panes on screen, caution: opening more panes will close old ones' complete -c zellij -n "__fish_use_subcommand" -s l -l layout -d 'Path to a layout yaml file' -complete -c zellij -n "__fish_use_subcommand" -s c -l config -d 'Path to the configuration yaml file' complete -c zellij -n "__fish_use_subcommand" -s m -l move-focus -d 'Send "move focused pane" to active zellij session' complete -c zellij -n "__fish_use_subcommand" -s d -l debug complete -c zellij -n "__fish_use_subcommand" -s h -l help -d 'Prints help information' complete -c zellij -n "__fish_use_subcommand" -s V -l version -d 'Prints version information' +complete -c zellij -n "__fish_use_subcommand" -f -a "config" -d 'Path to the configuration yaml file' +complete -c zellij -n "__fish_use_subcommand" -f -a "help" -d 'Prints this message or the help of the given subcommand(s)' +complete -c zellij -n "__fish_seen_subcommand_from config" -l clean -d 'Disables loading of configuration file at default location' +complete -c zellij -n "__fish_seen_subcommand_from config" -s h -l help -d 'Prints help information' +complete -c zellij -n "__fish_seen_subcommand_from config" -s V -l version -d 'Prints version information' +complete -c zellij -n "__fish_seen_subcommand_from help" -s h -l help -d 'Prints help information' +complete -c zellij -n "__fish_seen_subcommand_from help" -s V -l version -d 'Prints version information' diff --git a/src/cli.rs b/src/cli.rs index 67a92196..740879f0 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use structopt::StructOpt; -#[derive(StructOpt, Debug, Default)] +#[derive(StructOpt, Debug)] #[structopt(name = "zellij")] pub struct CliArgs { /// Send "split (direction h == horizontal / v == vertical)" to active zellij session @@ -24,10 +24,20 @@ pub struct CliArgs { #[structopt(short, long)] pub layout: Option, - /// Path to the configuration yaml file - #[structopt(short, long)] - pub config: Option, + #[structopt(subcommand)] + pub config: Option, #[structopt(short, long)] pub debug: bool, } + +#[derive(Debug, StructOpt)] +pub enum ConfigCli { + /// Path to the configuration yaml file + Config { + path: Option, + #[structopt(long)] + /// Disables loading of configuration file at default location + clean: bool, + }, +} diff --git a/src/common/input/config.rs b/src/common/input/config.rs index d9d1f784..668b4106 100644 --- a/src/common/input/config.rs +++ b/src/common/input/config.rs @@ -6,10 +6,13 @@ use std::io::{self, Read}; use std::path::{Path, PathBuf}; use super::keybinds::{Keybinds, KeybindsFromYaml}; +use crate::cli::ConfigCli; use directories_next::ProjectDirs; use serde::Deserialize; +type ConfigResult = Result; + /// Intermediate deserialisation config struct #[derive(Debug, Deserialize)] pub struct ConfigFromYaml { @@ -41,7 +44,7 @@ impl Default for Config { impl Config { /// Uses defaults, but lets config override them. - pub fn from_yaml(yaml_config: &str) -> Result { + pub fn from_yaml(yaml_config: &str) -> ConfigResult { 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 }) @@ -49,7 +52,7 @@ impl Config { /// Deserializes from given path. #[allow(unused_must_use)] - pub fn new(path: &Path) -> Result { + pub fn new(path: &Path) -> ConfigResult { match File::open(path) { Ok(mut file) => { let mut yaml_config = String::new(); @@ -57,15 +60,13 @@ impl Config { .map_err(|e| ConfigError::IoPath(e, path.to_path_buf()))?; Ok(Config::from_yaml(&yaml_config)?) } - Err(_) => { - Ok(Config::default()) - } + Err(_) => Ok(Config::default()), } } /// 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 { + fn from_option_or_default(option: Option) -> ConfigResult { if let Some(config_path) = option { Ok(Config::new(&config_path)?) } else { @@ -75,6 +76,14 @@ impl Config { Ok(Config::new(&config_path)?) } } + + pub fn from_cli_config(cli_config: Option) -> ConfigResult { + match cli_config { + Some(ConfigCli::Config { clean, .. }) if clean => Ok(Config::default()), + Some(ConfigCli::Config { path, .. }) => Ok(Config::from_option_or_default(path)?), + None => Ok(Config::default()), + } + } } impl Display for ConfigError { diff --git a/src/common/mod.rs b/src/common/mod.rs index 02e75f98..777b615f 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -169,8 +169,7 @@ 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) + let config = Config::from_cli_config(opts.config) .map_err(|e| { eprintln!("There was an error in the config file:\n{}", e); std::process::exit(1); From 91c109a90245edb3791cd3234787a8a1f6a8f794 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Wed, 17 Mar 2021 22:50:03 +0100 Subject: [PATCH 16/39] Move test to /unit --- src/cli.rs | 2 +- src/common/input/config.rs | 20 +++++++++++++++++-- src/common/input/keybinds.rs | 2 +- .../input/{ut => unit}/keybinds_test.rs | 0 src/common/input/ut/config_test.rs | 19 ------------------ 5 files changed, 20 insertions(+), 23 deletions(-) rename src/common/input/{ut => unit}/keybinds_test.rs (100%) delete mode 100644 src/common/input/ut/config_test.rs diff --git a/src/cli.rs b/src/cli.rs index 740879f0..d4c0f78a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use structopt::StructOpt; -#[derive(StructOpt, Debug)] +#[derive(StructOpt, Default, Debug)] #[structopt(name = "zellij")] pub struct CliArgs { /// Send "split (direction h == horizontal / v == vertical)" to active zellij session diff --git a/src/common/input/config.rs b/src/common/input/config.rs index 668b4106..7d804d68 100644 --- a/src/common/input/config.rs +++ b/src/common/input/config.rs @@ -122,5 +122,21 @@ impl From for ConfigError { // The unit test location. #[cfg(test)] -#[path = "./ut/config_test.rs"] -mod config_test; +mod config_test { + use super::*; + + #[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); + } +} diff --git a/src/common/input/keybinds.rs b/src/common/input/keybinds.rs index 0c37693c..c70123b6 100644 --- a/src/common/input/keybinds.rs +++ b/src/common/input/keybinds.rs @@ -368,5 +368,5 @@ impl From for ModeKeybinds { // The unit test location. #[cfg(test)] -#[path = "./ut/keybinds_test.rs"] +#[path = "./unit/keybinds_test.rs"] mod keybinds_test; diff --git a/src/common/input/ut/keybinds_test.rs b/src/common/input/unit/keybinds_test.rs similarity index 100% rename from src/common/input/ut/keybinds_test.rs rename to src/common/input/unit/keybinds_test.rs diff --git a/src/common/input/ut/config_test.rs b/src/common/input/ut/config_test.rs deleted file mode 100644 index eb4bc960..00000000 --- a/src/common/input/ut/config_test.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! For Configuration Options - -use super::super::config::*; -use std::path::PathBuf; - -#[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); -} From b30400ac4746e3b40243700fdd4425f671864cae Mon Sep 17 00:00:00 2001 From: a-kenji Date: Fri, 26 Mar 2021 21:15:12 +0100 Subject: [PATCH 17/39] Fix config default location The default location is now correctly used again --- src/common/input/config.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/common/input/config.rs b/src/common/input/config.rs index 7d804d68..4678343e 100644 --- a/src/common/input/config.rs +++ b/src/common/input/config.rs @@ -60,28 +60,30 @@ impl Config { .map_err(|e| ConfigError::IoPath(e, path.to_path_buf()))?; Ok(Config::from_yaml(&yaml_config)?) } - Err(_) => Ok(Config::default()), + Err(e) => Err(ConfigError::IoPath(e, path.into())), } } - /// Deserializes the config from an optional path, or a platform specific path, + /// Deserializes the config from a default platform specific path, /// merges the default configuration - options take precedence. - fn from_option_or_default(option: Option) -> ConfigResult { - if let Some(config_path) = option { - Ok(Config::new(&config_path)?) - } else { - let project_dirs = ProjectDirs::from("org", "Zellij Contributors", "Zellij").unwrap(); - let mut config_path: PathBuf = project_dirs.config_dir().to_owned(); - config_path.push("config.yaml"); - Ok(Config::new(&config_path)?) + fn from_default_path() -> ConfigResult { + let project_dirs = ProjectDirs::from("org", "Zellij Contributors", "Zellij").unwrap(); + let mut config_path: PathBuf = project_dirs.config_dir().to_owned(); + config_path.push("config.yaml"); + + match Config::new(&config_path) { + Ok(config) => Ok(config), + Err(ConfigError::IoPath(_,_)) => Ok(Config::default()), + Err(e) => Err(e), } } + /// Entry point of the configuration pub fn from_cli_config(cli_config: Option) -> ConfigResult { match cli_config { Some(ConfigCli::Config { clean, .. }) if clean => Ok(Config::default()), - Some(ConfigCli::Config { path, .. }) => Ok(Config::from_option_or_default(path)?), - None => Ok(Config::default()), + Some(ConfigCli::Config { path, .. }) if path.is_some()=> Ok(Config::new(&path.unwrap())?), + Some(_) | None => Ok(Config::from_default_path()?), } } } From 98c4227c816bf2852aac7a638cf9f955451c5f35 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Fri, 26 Mar 2021 21:59:20 +0100 Subject: [PATCH 18/39] Fix tests --- src/common/input/config.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/common/input/config.rs b/src/common/input/config.rs index 4678343e..d7420f67 100644 --- a/src/common/input/config.rs +++ b/src/common/input/config.rs @@ -128,16 +128,17 @@ mod config_test { use super::*; #[test] - fn no_config_file_equals_default_config() { + fn clean_option_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 cli_config = ConfigCli::Config { path: Some(no_file) ,clean: true}; + let config = Config::from_cli_config(Some(cli_config)).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 config = Config::from_cli_config(None).unwrap(); let default = Config::default(); assert_eq!(config, default); } From a80d5f700b9d8b5544f560ce382ad86527453960 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Fri, 26 Mar 2021 21:59:38 +0100 Subject: [PATCH 19/39] (Chore): rustfmt --- src/common/input/config.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/common/input/config.rs b/src/common/input/config.rs index d7420f67..0713e54e 100644 --- a/src/common/input/config.rs +++ b/src/common/input/config.rs @@ -73,7 +73,7 @@ impl Config { match Config::new(&config_path) { Ok(config) => Ok(config), - Err(ConfigError::IoPath(_,_)) => Ok(Config::default()), + Err(ConfigError::IoPath(_, _)) => Ok(Config::default()), Err(e) => Err(e), } } @@ -82,7 +82,9 @@ impl Config { pub fn from_cli_config(cli_config: Option) -> ConfigResult { match cli_config { Some(ConfigCli::Config { clean, .. }) if clean => Ok(Config::default()), - Some(ConfigCli::Config { path, .. }) if path.is_some()=> Ok(Config::new(&path.unwrap())?), + Some(ConfigCli::Config { path, .. }) if path.is_some() => { + Ok(Config::new(&path.unwrap())?) + } Some(_) | None => Ok(Config::from_default_path()?), } } @@ -130,7 +132,10 @@ mod config_test { #[test] fn clean_option_equals_default_config() { let no_file = PathBuf::from(r"../fixtures/config/config.yamlll"); - let cli_config = ConfigCli::Config { path: Some(no_file) ,clean: true}; + let cli_config = ConfigCli::Config { + path: Some(no_file), + clean: true, + }; let config = Config::from_cli_config(Some(cli_config)).unwrap(); let default = Config::default(); assert_eq!(config, default); From 68737f78ac247814f2ad79e27d4f0da7c960b4a9 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Fri, 26 Mar 2021 22:12:01 +0100 Subject: [PATCH 20/39] Config: Fix README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eea40060..203de8cc 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ An example file can be found under `example/config.yaml`. Zellij will look for a file `/zellij/config.yaml` in the default configuration location of your os. To pass a config file directly to zellij run it either with: -`cargo run -- --config [FILE]` or `zellij --config [FILE]`. +`cargo run -- config [FILE]` or `zellij config [FILE]`. The structure is as follows: ``` From 9ba26e736713394b573b9ca6ac2de5a261f590cb Mon Sep 17 00:00:00 2001 From: a-kenji Date: Mon, 29 Mar 2021 20:42:16 +0200 Subject: [PATCH 21/39] Fix(strider): Ignore descending into empty dir Adds a guard to check if the directory is empty before trying to descend into it. Eg: Doesn't error anymore, if inside ./git/branches directory and pressing `l`. --- default-tiles/strider/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default-tiles/strider/src/main.rs b/default-tiles/strider/src/main.rs index c2867fe4..71e2e5b7 100644 --- a/default-tiles/strider/src/main.rs +++ b/default-tiles/strider/src/main.rs @@ -23,7 +23,7 @@ impl ZellijTile for State { let next = self.selected().saturating_add(1); *self.selected_mut() = min(self.files.len() - 1, next); } - Key::Right | Key::Char('\n') | Key::Char('l') => { + Key::Right | Key::Char('\n') | Key::Char('l') if !self.files.is_empty() => { match self.files[self.selected()].clone() { FsEntry::Dir(p, _) => { self.path = p; From 267f4b48de2ed6ecf5d21942c40955cb74790058 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Wed, 31 Mar 2021 12:09:14 +0200 Subject: [PATCH 22/39] Fix(Mouse): Remove unimplemented! Macro for Mouse Remove `unimplemented!` macro for mouse and unsupported events, essentialy mimicking the NoOp. Should not crash anymore when handling mouse events. Alternatives: Keep the `unimplemented!` macro for `termion::event::Event::Unsupported` ? closes #243 --- src/common/input/handler.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/common/input/handler.rs b/src/common/input/handler.rs index 33f4e5dc..8de1ed3c 100644 --- a/src/common/input/handler.rs +++ b/src/common/input/handler.rs @@ -81,7 +81,8 @@ impl InputHandler { } termion::event::Event::Mouse(_) | termion::event::Event::Unsupported(_) => { - unimplemented!("Mouse and unsupported events aren't supported!"); + // Mouse and unsupported events aren't implemented yet, + // use a NoOp untill then. } }, Err(err) => panic!("Encountered read error: {:?}", err), From 50f6dc1ea4a1f8fad73525c9916abd1279150b58 Mon Sep 17 00:00:00 2001 From: Roee Shapira <35409124+5c077m4n@users.noreply.github.com> Date: Wed, 31 Mar 2021 19:49:07 +0300 Subject: [PATCH 23/39] Added me to contributors --- GOVERNANCE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 2928cd60..86c32d5d 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -26,3 +26,4 @@ Once the organization reaches 10 members, a reasonable and achievable process mu * Denis Maximov * Kunal Mohan * Henil Dedania +* Roee Shapira From 1739f370f98923d8955e1478bcda564220294829 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Thu, 1 Apr 2021 17:38:13 +0200 Subject: [PATCH 24/39] * fix(ui): react to terminal window size changes (SIGWINCH) (#245) * fix(ui): react to terminal window size changes (SIGWINCH) * style(fmt): rustfmt * style(format): remove some warnings * style(fmt): rustfmt --- Cargo.lock | 14 +- Cargo.toml | 2 +- default-tiles/status-bar/src/first_line.rs | 4 +- src/client/mod.rs | 1 + src/client/pane_resizer.rs | 508 ++++++++++++++++++ src/client/panes/plugin_pane.rs | 12 + src/client/panes/terminal_pane.rs | 12 + src/client/tab.rs | 112 +++- src/common/errors.rs | 4 +- src/common/mod.rs | 16 + src/common/os_input_output.rs | 32 +- src/common/screen.rs | 10 + src/common/utils/shared.rs | 13 +- src/tests/fakes.rs | 38 +- src/tests/integration/close_pane.rs | 2 +- src/tests/integration/mod.rs | 1 + ..._window_height_increase_with_one_pane.snap | 25 + ...dth_and_height_decrease_with_one_pane.snap | 25 + ...__window_width_decrease_with_one_pane.snap | 25 + ...__window_width_increase_with_one_pane.snap | 25 + .../integration/terminal_window_resize.rs | 127 +++++ src/tests/possible_tty_inputs.rs | 9 +- src/tests/tty_inputs.rs | 46 ++ src/tests/utils.rs | 1 - 24 files changed, 1007 insertions(+), 57 deletions(-) create mode 100644 src/client/pane_resizer.rs create mode 100644 src/tests/integration/snapshots/zellij__tests__integration__terminal_window_resize__window_height_increase_with_one_pane.snap create mode 100644 src/tests/integration/snapshots/zellij__tests__integration__terminal_window_resize__window_width_and_height_decrease_with_one_pane.snap create mode 100644 src/tests/integration/snapshots/zellij__tests__integration__terminal_window_resize__window_width_decrease_with_one_pane.snap create mode 100644 src/tests/integration/snapshots/zellij__tests__integration__terminal_window_resize__window_width_increase_with_one_pane.snap create mode 100644 src/tests/integration/terminal_window_resize.rs diff --git a/Cargo.lock b/Cargo.lock index e446df9f..abc7504b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,7 +130,7 @@ dependencies = [ "event-listener", "futures-lite", "once_cell", - "signal-hook 0.3.6", + "signal-hook", "winapi", ] @@ -1414,16 +1414,6 @@ dependencies = [ "yaml-rust", ] -[[package]] -name = "signal-hook" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729" -dependencies = [ - "libc", - "signal-hook-registry", -] - [[package]] name = "signal-hook" version = "0.3.6" @@ -2259,7 +2249,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "signal-hook 0.1.17", + "signal-hook", "strip-ansi-escapes", "structopt", "strum", diff --git a/Cargo.toml b/Cargo.toml index 776eb3e9..ee226a9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ nom = "6.0.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.8" -signal-hook = "0.1.10" +signal-hook = "0.3" strip-ansi-escapes = "0.1.0" structopt = "0.3" termion = "1.5.0" diff --git a/default-tiles/status-bar/src/first_line.rs b/default-tiles/status-bar/src/first_line.rs index 0e4ec53b..ea48cb1b 100644 --- a/default-tiles/status-bar/src/first_line.rs +++ b/default-tiles/status-bar/src/first_line.rs @@ -99,7 +99,7 @@ fn unselected_mode_shortcut(letter: char, text: &str) -> LinePart { suffix_separator, ]) .to_string(), - len: text.chars().count() + 6, // 2 for the arrows, 3 for the char separators, 1 for the character + len: text.chars().count() + 7, // 2 for the arrows, 3 for the char separators, 1 for the character, 1 for the text padding } } @@ -129,7 +129,7 @@ fn selected_mode_shortcut(letter: char, text: &str) -> LinePart { suffix_separator, ]) .to_string(), - len: text.chars().count() + 6, // 2 for the arrows, 3 for the char separators, 1 for the character + len: text.chars().count() + 7, // 2 for the arrows, 3 for the char separators, 1 for the character, 1 for the text padding } } diff --git a/src/client/mod.rs b/src/client/mod.rs index 93f35e06..cba12a46 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,5 +1,6 @@ pub mod boundaries; pub mod layout; +pub mod pane_resizer; pub mod panes; pub mod tab; diff --git a/src/client/pane_resizer.rs b/src/client/pane_resizer.rs new file mode 100644 index 00000000..007dc33f --- /dev/null +++ b/src/client/pane_resizer.rs @@ -0,0 +1,508 @@ +use crate::os_input_output::OsApi; +use crate::panes::{PaneId, PositionAndSize}; +use crate::tab::Pane; +use std::collections::{BTreeMap, HashSet}; + +pub struct PaneResizer<'a> { + panes: &'a mut BTreeMap>, + os_api: &'a mut Box, +} + +// TODO: currently there are some functions here duplicated with Tab +// the reason for this is that we need to get rid of the expansion_boundary +// otherwise we'll have a big separation of concerns issue +// once that is done, all resizing functions should move here + +impl<'a> PaneResizer<'a> { + pub fn new( + panes: &'a mut BTreeMap>, + os_api: &'a mut Box, + ) -> Self { + PaneResizer { panes, os_api } + } + pub fn resize( + &mut self, + mut current_size: PositionAndSize, + new_size: PositionAndSize, + ) -> Option<(isize, isize)> { + // (column_difference, row_difference) + let mut successfully_resized = false; + let mut column_difference: isize = 0; + let mut row_difference: isize = 0; + if new_size.columns < current_size.columns { + let reduce_by = current_size.columns - new_size.columns; + find_reducible_vertical_chain( + &self.panes, + reduce_by, + current_size.columns, + current_size.rows, + ) + .map(|panes_to_resize| { + self.reduce_panes_left_and_pull_adjacents_left(panes_to_resize, reduce_by); + column_difference = new_size.columns as isize - current_size.columns as isize; + current_size.columns = (current_size.columns as isize + column_difference) as usize; + successfully_resized = true; + }); + } else if new_size.columns > current_size.columns { + let increase_by = new_size.columns - current_size.columns; + find_increasable_vertical_chain( + &self.panes, + increase_by, + current_size.columns, + current_size.rows, + ) + .map(|panes_to_resize| { + self.increase_panes_right_and_push_adjacents_right(panes_to_resize, increase_by); + column_difference = new_size.columns as isize - current_size.columns as isize; + current_size.columns = (current_size.columns as isize + column_difference) as usize; + successfully_resized = true; + }); + } + if new_size.rows < current_size.rows { + let reduce_by = current_size.rows - new_size.rows; + find_reducible_horizontal_chain( + &self.panes, + reduce_by, + current_size.columns, + current_size.rows, + ) + .map(|panes_to_resize| { + self.reduce_panes_up_and_pull_adjacents_up(panes_to_resize, reduce_by); + row_difference = new_size.rows as isize - current_size.rows as isize; + current_size.rows = (current_size.rows as isize + row_difference) as usize; + successfully_resized = true; + }); + } else if new_size.rows > current_size.rows { + let increase_by = new_size.rows - current_size.rows; + find_increasable_horizontal_chain( + &self.panes, + increase_by, + current_size.columns, + current_size.rows, + ) + .map(|panes_to_resize| { + self.increase_panes_down_and_push_down_adjacents(panes_to_resize, increase_by); + row_difference = new_size.rows as isize - current_size.rows as isize; + current_size.rows = (current_size.rows as isize + row_difference) as usize; + successfully_resized = true; + }); + } + if successfully_resized { + Some((column_difference, row_difference)) + } else { + None + } + } + fn reduce_panes_left_and_pull_adjacents_left( + &mut self, + panes_to_reduce: Vec, + reduce_by: usize, + ) { + let mut pulled_panes: HashSet = HashSet::new(); + for pane_id in panes_to_reduce { + let (pane_x, pane_y, pane_columns, pane_rows) = { + let pane = self.panes.get(&pane_id).unwrap(); + (pane.x(), pane.y(), pane.columns(), pane.rows()) + }; + let panes_to_pull = self.panes.values_mut().filter(|p| { + p.x() > pane_x + pane_columns + && (p.y() <= pane_y && p.y() + p.rows() >= pane_y + || p.y() >= pane_y && p.y() + p.rows() <= pane_y + pane_rows) + }); + for pane in panes_to_pull { + if !pulled_panes.contains(&pane.pid()) { + pane.pull_left(reduce_by); + pulled_panes.insert(pane.pid()); + } + } + self.reduce_pane_width_left(&pane_id, reduce_by); + } + } + fn reduce_panes_up_and_pull_adjacents_up( + &mut self, + panes_to_reduce: Vec, + reduce_by: usize, + ) { + let mut pulled_panes: HashSet = HashSet::new(); + for pane_id in panes_to_reduce { + let (pane_x, pane_y, pane_columns, pane_rows) = { + let pane = self.panes.get(&pane_id).unwrap(); + (pane.x(), pane.y(), pane.columns(), pane.rows()) + }; + let panes_to_pull = self.panes.values_mut().filter(|p| { + p.y() > pane_y + pane_rows + && (p.x() <= pane_x && p.x() + p.columns() >= pane_x + || p.x() >= pane_x && p.x() + p.columns() <= pane_x + pane_columns) + }); + for pane in panes_to_pull { + if !pulled_panes.contains(&pane.pid()) { + pane.pull_up(reduce_by); + pulled_panes.insert(pane.pid()); + } + } + self.reduce_pane_height_up(&pane_id, reduce_by); + } + } + fn increase_panes_down_and_push_down_adjacents( + &mut self, + panes_to_increase: Vec, + increase_by: usize, + ) { + let mut pushed_panes: HashSet = HashSet::new(); + for pane_id in panes_to_increase { + let (pane_x, pane_y, pane_columns, pane_rows) = { + let pane = self.panes.get(&pane_id).unwrap(); + (pane.x(), pane.y(), pane.columns(), pane.rows()) + }; + let panes_to_push = self.panes.values_mut().filter(|p| { + p.y() > pane_y + pane_rows + && (p.x() <= pane_x && p.x() + p.columns() >= pane_x + || p.x() >= pane_x && p.x() + p.columns() <= pane_x + pane_columns) + }); + for pane in panes_to_push { + if !pushed_panes.contains(&pane.pid()) { + pane.push_down(increase_by); + pushed_panes.insert(pane.pid()); + } + } + self.increase_pane_height_down(&pane_id, increase_by); + } + } + fn increase_panes_right_and_push_adjacents_right( + &mut self, + panes_to_increase: Vec, + increase_by: usize, + ) { + let mut pushed_panes: HashSet = HashSet::new(); + for pane_id in panes_to_increase { + let (pane_x, pane_y, pane_columns, pane_rows) = { + let pane = self.panes.get(&pane_id).unwrap(); + (pane.x(), pane.y(), pane.columns(), pane.rows()) + }; + let panes_to_push = self.panes.values_mut().filter(|p| { + p.x() > pane_x + pane_columns + && (p.y() <= pane_y && p.y() + p.rows() >= pane_y + || p.y() >= pane_y && p.y() + p.rows() <= pane_y + pane_rows) + }); + for pane in panes_to_push { + if !pushed_panes.contains(&pane.pid()) { + pane.push_right(increase_by); + pushed_panes.insert(pane.pid()); + } + } + self.increase_pane_width_right(&pane_id, increase_by); + } + } + fn reduce_pane_height_up(&mut self, id: &PaneId, count: usize) { + let pane = self.panes.get_mut(id).unwrap(); + pane.reduce_height_up(count); + if let PaneId::Terminal(pid) = id { + self.os_api + .set_terminal_size_using_fd(*pid, pane.columns() as u16, pane.rows() as u16); + } + } + fn increase_pane_height_down(&mut self, id: &PaneId, count: usize) { + let pane = self.panes.get_mut(id).unwrap(); + pane.increase_height_down(count); + if let PaneId::Terminal(pid) = pane.pid() { + self.os_api + .set_terminal_size_using_fd(pid, pane.columns() as u16, pane.rows() as u16); + } + } + fn increase_pane_width_right(&mut self, id: &PaneId, count: usize) { + let pane = self.panes.get_mut(id).unwrap(); + pane.increase_width_right(count); + if let PaneId::Terminal(pid) = pane.pid() { + self.os_api + .set_terminal_size_using_fd(pid, pane.columns() as u16, pane.rows() as u16); + } + } + fn reduce_pane_width_left(&mut self, id: &PaneId, count: usize) { + let pane = self.panes.get_mut(id).unwrap(); + pane.reduce_width_left(count); + if let PaneId::Terminal(pid) = pane.pid() { + self.os_api + .set_terminal_size_using_fd(pid, pane.columns() as u16, pane.rows() as u16); + } + } +} + +fn find_next_increasable_horizontal_pane( + panes: &BTreeMap>, + right_of: &Box, + increase_by: usize, +) -> Option { + let next_pane_candidates = panes.values().filter( + |p| { + p.x() == right_of.x() + right_of.columns() + 1 + && p.horizontally_overlaps_with(right_of.as_ref()) + }, // TODO: the name here is wrong, it should be vertically_overlaps_with + ); + let resizable_candidates = + next_pane_candidates.filter(|p| p.can_increase_height_by(increase_by)); + resizable_candidates.fold(None, |next_pane_id, p| match next_pane_id { + Some(next_pane) => { + let next_pane = panes.get(&next_pane).unwrap(); + if next_pane.y() < p.y() { + next_pane_id + } else { + Some(p.pid()) + } + } + None => Some(p.pid()), + }) +} + +fn find_next_increasable_vertical_pane( + panes: &BTreeMap>, + below: &Box, + increase_by: usize, +) -> Option { + let next_pane_candidates = panes.values().filter( + |p| p.y() == below.y() + below.rows() + 1 && p.vertically_overlaps_with(below.as_ref()), // TODO: the name here is wrong, it should be horizontally_overlaps_with + ); + let resizable_candidates = + next_pane_candidates.filter(|p| p.can_increase_width_by(increase_by)); + resizable_candidates.fold(None, |next_pane_id, p| match next_pane_id { + Some(next_pane) => { + let next_pane = panes.get(&next_pane).unwrap(); + if next_pane.x() < p.x() { + next_pane_id + } else { + Some(p.pid()) + } + } + None => Some(p.pid()), + }) +} + +fn find_next_reducible_vertical_pane( + panes: &BTreeMap>, + below: &Box, + reduce_by: usize, +) -> Option { + let next_pane_candidates = panes.values().filter( + |p| p.y() == below.y() + below.rows() + 1 && p.vertically_overlaps_with(below.as_ref()), // TODO: the name here is wrong, it should be horizontally_overlaps_with + ); + let resizable_candidates = next_pane_candidates.filter(|p| p.can_reduce_width_by(reduce_by)); + resizable_candidates.fold(None, |next_pane_id, p| match next_pane_id { + Some(next_pane) => { + let next_pane = panes.get(&next_pane).unwrap(); + if next_pane.x() < p.x() { + next_pane_id + } else { + Some(p.pid()) + } + } + None => Some(p.pid()), + }) +} + +fn find_next_reducible_horizontal_pane( + panes: &BTreeMap>, + right_of: &Box, + reduce_by: usize, +) -> Option { + let next_pane_candidates = panes.values().filter( + |p| { + p.x() == right_of.x() + right_of.columns() + 1 + && p.horizontally_overlaps_with(right_of.as_ref()) + }, // TODO: the name here is wrong, it should be vertically_overlaps_with + ); + let resizable_candidates = next_pane_candidates.filter(|p| p.can_reduce_height_by(reduce_by)); + resizable_candidates.fold(None, |next_pane_id, p| match next_pane_id { + Some(next_pane) => { + let next_pane = panes.get(&next_pane).unwrap(); + if next_pane.y() < p.y() { + next_pane_id + } else { + Some(p.pid()) + } + } + None => Some(p.pid()), + }) +} + +fn find_increasable_horizontal_chain( + panes: &BTreeMap>, + increase_by: usize, + screen_width: usize, + screen_height: usize, // TODO: this is the previous size (make this clearer) +) -> Option> { + let mut horizontal_coordinate = 0; + loop { + if horizontal_coordinate == screen_height { + return None; + } + + match panes + .values() + .find(|p| p.x() == 0 && p.y() == horizontal_coordinate) + { + Some(leftmost_pane) => { + if !leftmost_pane.can_increase_height_by(increase_by) { + horizontal_coordinate = leftmost_pane.y() + leftmost_pane.rows() + 1; + continue; + } + let mut panes_to_resize = vec![]; + let mut current_pane = leftmost_pane; + loop { + panes_to_resize.push(current_pane.pid()); + if current_pane.x() + current_pane.columns() == screen_width { + return Some(panes_to_resize); + } + match find_next_increasable_horizontal_pane(panes, ¤t_pane, increase_by) { + Some(next_pane_id) => { + current_pane = panes.get(&next_pane_id).unwrap(); + } + None => { + horizontal_coordinate = leftmost_pane.y() + leftmost_pane.rows() + 1; + break; + } + }; + } + } + None => { + return None; + } + } + } +} + +fn find_increasable_vertical_chain( + panes: &BTreeMap>, + increase_by: usize, + screen_width: usize, + screen_height: usize, // TODO: this is the previous size (make this clearer) +) -> Option> { + let mut vertical_coordinate = 0; + loop { + if vertical_coordinate == screen_width { + return None; + } + + match panes + .values() + .find(|p| p.y() == 0 && p.x() == vertical_coordinate) + { + Some(topmost_pane) => { + if !topmost_pane.can_increase_width_by(increase_by) { + vertical_coordinate = topmost_pane.x() + topmost_pane.columns() + 1; + continue; + } + let mut panes_to_resize = vec![]; + let mut current_pane = topmost_pane; + loop { + panes_to_resize.push(current_pane.pid()); + if current_pane.y() + current_pane.rows() == screen_height { + return Some(panes_to_resize); + } + match find_next_increasable_vertical_pane(panes, ¤t_pane, increase_by) { + Some(next_pane_id) => { + current_pane = panes.get(&next_pane_id).unwrap(); + } + None => { + vertical_coordinate = topmost_pane.x() + topmost_pane.columns() + 1; + break; + } + }; + } + } + None => { + return None; + } + } + } +} + +fn find_reducible_horizontal_chain( + panes: &BTreeMap>, + reduce_by: usize, + screen_width: usize, + screen_height: usize, // TODO: this is the previous size (make this clearer) +) -> Option> { + let mut horizontal_coordinate = 0; + loop { + if horizontal_coordinate == screen_height { + return None; + } + + match panes + .values() + .find(|p| p.x() == 0 && p.y() == horizontal_coordinate) + { + Some(leftmost_pane) => { + if !leftmost_pane.can_reduce_height_by(reduce_by) { + horizontal_coordinate = leftmost_pane.y() + leftmost_pane.rows() + 1; + continue; + } + let mut panes_to_resize = vec![]; + let mut current_pane = leftmost_pane; + loop { + panes_to_resize.push(current_pane.pid()); + if current_pane.x() + current_pane.columns() == screen_width { + return Some(panes_to_resize); + } + match find_next_reducible_horizontal_pane(panes, ¤t_pane, reduce_by) { + Some(next_pane_id) => { + current_pane = panes.get(&next_pane_id).unwrap(); + } + None => { + horizontal_coordinate = leftmost_pane.y() + leftmost_pane.rows() + 1; + break; + } + }; + } + } + None => { + return None; + } + } + } +} + +fn find_reducible_vertical_chain( + panes: &BTreeMap>, + increase_by: usize, + screen_width: usize, + screen_height: usize, // TODO: this is the previous size (make this clearer) +) -> Option> { + let mut vertical_coordinate = 0; + loop { + if vertical_coordinate == screen_width { + return None; + } + + match panes + .values() + .find(|p| p.y() == 0 && p.x() == vertical_coordinate) + { + Some(topmost_pane) => { + if !topmost_pane.can_reduce_width_by(increase_by) { + vertical_coordinate = topmost_pane.x() + topmost_pane.columns() + 1; + continue; + } + let mut panes_to_resize = vec![]; + let mut current_pane = topmost_pane; + loop { + panes_to_resize.push(current_pane.pid()); + if current_pane.y() + current_pane.rows() == screen_height { + return Some(panes_to_resize); + } + match find_next_reducible_vertical_pane(panes, ¤t_pane, increase_by) { + Some(next_pane_id) => { + current_pane = panes.get(&next_pane_id).unwrap(); + } + None => { + vertical_coordinate = topmost_pane.x() + topmost_pane.columns() + 1; + break; + } + }; + } + } + None => { + return None; + } + } + } +} diff --git a/src/client/panes/plugin_pane.rs b/src/client/panes/plugin_pane.rs index 4cffff5f..caf49acc 100644 --- a/src/client/panes/plugin_pane.rs +++ b/src/client/panes/plugin_pane.rs @@ -173,6 +173,18 @@ impl Pane for PluginPane { self.position_and_size.columns += count; self.should_render = true; } + fn push_down(&mut self, count: usize) { + self.position_and_size.y += count; + } + fn push_right(&mut self, count: usize) { + self.position_and_size.x += count; + } + fn pull_left(&mut self, count: usize) { + self.position_and_size.x -= count; + } + fn pull_up(&mut self, count: usize) { + self.position_and_size.y -= count; + } fn scroll_up(&mut self, _count: usize) { unimplemented!() } diff --git a/src/client/panes/terminal_pane.rs b/src/client/panes/terminal_pane.rs index f0b15e50..463a33e3 100644 --- a/src/client/panes/terminal_pane.rs +++ b/src/client/panes/terminal_pane.rs @@ -282,6 +282,18 @@ impl Pane for TerminalPane { self.position_and_size.columns += count; self.reflow_lines(); } + fn push_down(&mut self, count: usize) { + self.position_and_size.y += count; + } + fn push_right(&mut self, count: usize) { + self.position_and_size.x += count; + } + fn pull_left(&mut self, count: usize) { + self.position_and_size.x -= count; + } + fn pull_up(&mut self, count: usize) { + self.position_and_size.y -= count; + } fn scroll_up(&mut self, count: usize) { self.grid.move_viewport_up(count); self.mark_for_rerender(); diff --git a/src/client/tab.rs b/src/client/tab.rs index 6a0ec72f..695d42cb 100644 --- a/src/client/tab.rs +++ b/src/client/tab.rs @@ -2,13 +2,16 @@ //! as well as how they should be resized use crate::boundaries::colors; +use crate::client::pane_resizer::PaneResizer; use crate::common::{input::handler::parse_keys, AppInstruction, SenderWithContext}; use crate::layout::Layout; +use crate::os_input_output::OsApi; use crate::panes::{PaneId, PositionAndSize, TerminalPane}; use crate::pty_bus::{PtyInstruction, VteEvent}; +use crate::utils::shared::adjust_to_size; use crate::wasm_vm::PluginInstruction; use crate::{boundaries::Boundaries, panes::PluginPane}; -use crate::{os_input_output::OsApi, utils::shared::pad_to_size}; +use serde::{Deserialize, Serialize}; use std::os::unix::io::RawFd; use std::{ cmp::Reverse, @@ -66,6 +69,16 @@ pub struct Tab { pub send_plugin_instructions: SenderWithContext, pub send_app_instructions: SenderWithContext, expansion_boundary: Option, + should_clear_display_before_rendering: bool, + pub mode_info: ModeInfo, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct TabData { + /* subset of fields to publish to plugins */ + pub position: usize, + pub name: String, + pub active: bool, pub mode_info: ModeInfo, } @@ -99,6 +112,10 @@ pub trait Pane { fn reduce_width_right(&mut self, count: usize); fn reduce_width_left(&mut self, count: usize); fn increase_width_left(&mut self, count: usize); + fn push_down(&mut self, count: usize); + fn push_right(&mut self, count: usize); + fn pull_left(&mut self, count: usize); + fn pull_up(&mut self, count: usize); fn scroll_up(&mut self, count: usize); fn scroll_down(&mut self, count: usize); fn clear_scroll(&mut self); @@ -153,6 +170,22 @@ pub trait Pane { rows: self.rows(), } } + fn can_increase_height_by(&self, increase_by: usize) -> bool { + self.max_height() + .map(|max_height| self.rows() + increase_by <= max_height) + .unwrap_or(true) + } + fn can_increase_width_by(&self, increase_by: usize) -> bool { + self.max_width() + .map(|max_width| self.columns() + increase_by <= max_width) + .unwrap_or(true) + } + fn can_reduce_height_by(&self, reduce_by: usize) -> bool { + self.rows() > reduce_by && self.rows() - reduce_by >= self.min_height() + } + fn can_reduce_width_by(&self, reduce_by: usize) -> bool { + self.columns() > reduce_by && self.columns() - reduce_by >= self.min_width() + } fn min_width(&self) -> usize { MIN_TERMINAL_WIDTH } @@ -213,6 +246,7 @@ impl Tab { send_pty_instructions, send_plugin_instructions, expansion_boundary: None, + should_clear_display_before_rendering: false, mode_info, } } @@ -630,28 +664,33 @@ impl Tab { stdout .write_all(&hide_cursor.as_bytes()) .expect("cannot write to stdout"); - for (kind, terminal) in self.panes.iter_mut() { - if !self.panes_to_hide.contains(&terminal.pid()) { - match self.active_terminal.unwrap() == terminal.pid() { - true => boundaries.add_rect( - terminal.as_ref(), - self.mode_info.mode, - Some(colors::GREEN), - ), - false => boundaries.add_rect(terminal.as_ref(), self.mode_info.mode, None), + if self.should_clear_display_before_rendering { + let clear_display = "\u{1b}[2J"; + stdout + .write_all(&clear_display.as_bytes()) + .expect("cannot write to stdout"); + self.should_clear_display_before_rendering = false; + } + for (kind, pane) in self.panes.iter_mut() { + if !self.panes_to_hide.contains(&pane.pid()) { + match self.active_terminal.unwrap() == pane.pid() { + true => { + boundaries.add_rect(pane.as_ref(), self.mode_info.mode, Some(colors::GREEN)) + } + false => boundaries.add_rect(pane.as_ref(), self.mode_info.mode, None), } - if let Some(vte_output) = terminal.render() { + if let Some(vte_output) = pane.render() { let vte_output = if let PaneId::Terminal(_) = kind { vte_output } else { - pad_to_size(&vte_output, terminal.rows(), terminal.columns()) + adjust_to_size(&vte_output, pane.rows(), pane.columns()) }; // FIXME: Use Termion for cursor and style clearing? write!( stdout, "\u{1b}[{};{}H\u{1b}[m{}", - terminal.y() + 1, - terminal.x() + 1, + pane.y() + 1, + pane.x() + 1, vte_output ) .expect("cannot write to stdout"); @@ -1664,17 +1703,30 @@ impl Tab { false } } - pub fn resize_right(&mut self) { - // TODO: find out by how much we actually reduced and only reduce by that much - let count = 10; - if let Some(active_pane_id) = self.get_active_pane_id() { - if self.can_increase_pane_and_surroundings_right(&active_pane_id, count) { - self.increase_pane_and_surroundings_right(&active_pane_id, count); - } else if self.can_reduce_pane_and_surroundings_right(&active_pane_id, count) { - self.reduce_pane_and_surroundings_right(&active_pane_id, count); - } + pub fn resize_whole_tab(&mut self, new_screen_size: PositionAndSize) { + if self.fullscreen_is_active { + // this is not ideal but until we get rid of expansion_boundary, it's a necessity + self.toggle_active_pane_fullscreen(); } - self.render(); + match PaneResizer::new(&mut self.panes, &mut self.os_api) + .resize(self.full_screen_ws, new_screen_size) + { + Some((column_difference, row_difference)) => { + self.should_clear_display_before_rendering = true; + self.expansion_boundary.as_mut().map(|expansion_boundary| { + // TODO: this is not always accurate + expansion_boundary.columns = + (expansion_boundary.columns as isize + column_difference) as usize; + expansion_boundary.rows = + (expansion_boundary.rows as isize + row_difference) as usize; + }); + self.full_screen_ws.columns = + (self.full_screen_ws.columns as isize + column_difference) as usize; + self.full_screen_ws.rows = + (self.full_screen_ws.rows as isize + row_difference) as usize; + } + None => {} + }; } pub fn resize_left(&mut self) { // TODO: find out by how much we actually reduced and only reduce by that much @@ -1688,6 +1740,18 @@ impl Tab { } self.render(); } + pub fn resize_right(&mut self) { + // TODO: find out by how much we actually reduced and only reduce by that much + let count = 10; + if let Some(active_pane_id) = self.get_active_pane_id() { + if self.can_increase_pane_and_surroundings_right(&active_pane_id, count) { + self.increase_pane_and_surroundings_right(&active_pane_id, count); + } else if self.can_reduce_pane_and_surroundings_right(&active_pane_id, count) { + self.reduce_pane_and_surroundings_right(&active_pane_id, count); + } + } + self.render(); + } pub fn resize_down(&mut self) { // TODO: find out by how much we actually reduced and only reduce by that much let count = 2; diff --git a/src/common/errors.rs b/src/common/errors.rs index 56bf27f4..00284cd4 100644 --- a/src/common/errors.rs +++ b/src/common/errors.rs @@ -42,7 +42,7 @@ pub fn handle_panic( msg, location.file(), location.line(), - backtrace + backtrace, ), (Some(location), None) => format!( "{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked: {}:{}\n\u{1b}[0;0m{:?}", @@ -200,6 +200,7 @@ pub enum ScreenContext { CloseTab, GoToTab, UpdateTabName, + TerminalResize, ChangeMode, } @@ -241,6 +242,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::CloseTab => ScreenContext::CloseTab, ScreenInstruction::GoToTab(_) => ScreenContext::GoToTab, ScreenInstruction::UpdateTabName(_) => ScreenContext::UpdateTabName, + ScreenInstruction::TerminalResize => ScreenContext::TerminalResize, ScreenInstruction::ChangeMode(_) => ScreenContext::ChangeMode, } } diff --git a/src/common/mod.rs b/src/common/mod.rs index a566d655..87d8cd94 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -389,6 +389,9 @@ pub fn start(mut os_input: Box, opts: CliArgs) { ScreenInstruction::UpdateTabName(c) => { screen.update_active_tab_name(c); } + ScreenInstruction::TerminalResize => { + screen.resize_to_screen(); + } ScreenInstruction::ChangeMode(mode_info) => { screen.change_mode(mode_info); } @@ -506,6 +509,19 @@ pub fn start(mut os_input: Box, opts: CliArgs) { }) .unwrap(); + let _signal_thread = thread::Builder::new() + .name("signal_listener".to_string()) + .spawn({ + let os_input = os_input.clone(); + let send_screen_instructions = send_screen_instructions.clone(); + move || { + os_input.receive_sigwinch(Box::new(move || { + let _ = send_screen_instructions.send(ScreenInstruction::TerminalResize); + })); + } + }) + .unwrap(); + // TODO: currently we don't wait for this to quit // because otherwise the app will hang. Need to fix this so it both // listens to the ipc-bus and is able to quit cleanly diff --git a/src/common/os_input_output.rs b/src/common/os_input_output.rs index 043a9aea..de0c6a3b 100644 --- a/src/common/os_input_output.rs +++ b/src/common/os_input_output.rs @@ -13,6 +13,8 @@ use std::path::PathBuf; use std::process::{Child, Command}; use std::sync::{Arc, Mutex}; +use signal_hook::{consts::signal::*, iterator::Signals}; + use std::env; fn into_raw_mode(pid: RawFd) { @@ -65,7 +67,7 @@ pub fn set_terminal_size_using_fd(fd: RawFd, columns: u16, rows: u16) { /// process exits. fn handle_command_exit(mut child: Child) { // register the SIGINT signal (TODO handle more signals) - let signals = ::signal_hook::iterator::Signals::new(&[::signal_hook::SIGINT]).unwrap(); + let mut signals = ::signal_hook::iterator::Signals::new(&[SIGINT]).unwrap(); 'handle_exit: loop { // test whether the child process has exited match child.try_wait() { @@ -82,10 +84,15 @@ fn handle_command_exit(mut child: Child) { } for signal in signals.pending() { - if signal == signal_hook::SIGINT { - child.kill().unwrap(); - child.wait().unwrap(); - break 'handle_exit; + // FIXME: We need to handle more signals here! + #[allow(clippy::single_match)] + match signal { + SIGINT => { + child.kill().unwrap(); + child.wait().unwrap(); + break 'handle_exit; + } + _ => {} } } } @@ -188,6 +195,7 @@ pub trait OsApi: Send + Sync { fn get_stdout_writer(&self) -> Box; /// Returns a [`Box`] pointer to this [`OsApi`] struct. fn box_clone(&self) -> Box; + fn receive_sigwinch(&self, cb: Box); } impl OsApi for OsInputOutput { @@ -238,6 +246,20 @@ impl OsApi for OsInputOutput { waitpid(Pid::from_raw(pid), None).unwrap(); Ok(()) } + fn receive_sigwinch(&self, cb: Box) { + let mut signals = Signals::new(&[SIGWINCH, SIGTERM, SIGINT, SIGQUIT]).unwrap(); + for signal in signals.forever() { + match signal { + SIGWINCH => { + cb(); + } + SIGTERM | SIGINT | SIGQUIT => { + break; + } + _ => unreachable!(), + } + } + } } impl Clone for Box { diff --git a/src/common/screen.rs b/src/common/screen.rs index 178e8be9..121f21bd 100644 --- a/src/common/screen.rs +++ b/src/common/screen.rs @@ -50,6 +50,7 @@ pub enum ScreenInstruction { CloseTab, GoToTab(u32), UpdateTabName(Vec), + TerminalResize, ChangeMode(ModeInfo), } @@ -213,6 +214,15 @@ impl Screen { } } + pub fn resize_to_screen(&mut self) { + let new_screen_size = self.os_api.get_terminal_size_using_fd(0); + self.full_screen_ws = new_screen_size; + for (_, tab) in self.tabs.iter_mut() { + tab.resize_whole_tab(new_screen_size); + } + self.render(); + } + /// Renders this [`Screen`], which amounts to rendering its active [`Tab`]. pub fn render(&mut self) { if let Some(active_tab) = self.get_active_tab_mut() { diff --git a/src/common/utils/shared.rs b/src/common/utils/shared.rs index 9717837a..93892d4f 100644 --- a/src/common/utils/shared.rs +++ b/src/common/utils/shared.rs @@ -11,9 +11,18 @@ fn ansi_len(s: &str) -> usize { .count() } -pub fn pad_to_size(s: &str, rows: usize, columns: usize) -> String { +pub fn adjust_to_size(s: &str, rows: usize, columns: usize) -> String { s.lines() - .map(|l| [l, &str::repeat(" ", columns - ansi_len(l))].concat()) + .map(|l| { + let actual_len = ansi_len(l); + if actual_len > columns { + let mut line = String::from(l); + line.truncate(columns); + return line; + } else { + return [l, &str::repeat(" ", columns - ansi_len(l))].concat(); + } + }) .chain(iter::repeat(str::repeat(" ", columns))) .take(rows) .collect::>() diff --git a/src/tests/fakes.rs b/src/tests/fakes.rs index c800de97..c9504a98 100644 --- a/src/tests/fakes.rs +++ b/src/tests/fakes.rs @@ -3,14 +3,13 @@ use std::collections::{HashMap, VecDeque}; use std::io::Write; use std::os::unix::io::RawFd; use std::path::PathBuf; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Condvar, Mutex}; use std::time::{Duration, Instant}; use crate::os_input_output::OsApi; use crate::tests::possible_tty_inputs::{get_possible_tty_inputs, Bytes}; -use crate::tests::utils::commands::SLEEP; +use crate::tests::utils::commands::{QUIT, SLEEP}; const MIN_TIME_BETWEEN_SNAPSHOTS: Duration = Duration::from_millis(50); @@ -72,7 +71,8 @@ pub struct FakeInputOutput { win_sizes: Arc>>, possible_tty_inputs: HashMap, last_snapshot_time: Arc>, - started_reading_from_pty: Arc, + should_trigger_sigwinch: Arc<(Mutex, Condvar)>, + sigwinch_event: Option, } impl FakeInputOutput { @@ -91,7 +91,8 @@ impl FakeInputOutput { io_events: Arc::new(Mutex::new(vec![])), win_sizes: Arc::new(Mutex::new(win_sizes)), possible_tty_inputs: get_possible_tty_inputs(), - started_reading_from_pty: Arc::new(AtomicBool::new(false)), + should_trigger_sigwinch: Arc::new((Mutex::new(false), Condvar::new())), + sigwinch_event: None, } } pub fn with_tty_inputs(mut self, tty_inputs: HashMap) -> Self { @@ -108,10 +109,20 @@ impl FakeInputOutput { pub fn add_terminal(&mut self, fd: RawFd) { self.stdin_writes.lock().unwrap().insert(fd, vec![]); } + pub fn add_sigwinch_event(&mut self, new_position_and_size: PositionAndSize) { + self.sigwinch_event = Some(new_position_and_size); + } } impl OsApi for FakeInputOutput { fn get_terminal_size_using_fd(&self, pid: RawFd) -> PositionAndSize { + if let Some(new_position_and_size) = self.sigwinch_event { + let (lock, _cvar) = &*self.should_trigger_sigwinch; + let should_trigger_sigwinch = lock.lock().unwrap(); + if *should_trigger_sigwinch && pid == 0 { + return new_position_and_size; + } + } let win_sizes = self.win_sizes.lock().unwrap(); let winsize = win_sizes.get(&pid).unwrap(); *winsize @@ -159,7 +170,6 @@ impl OsApi for FakeInputOutput { if bytes_read > bytes.read_position { bytes.set_read_position(bytes_read); } - self.started_reading_from_pty.store(true, Ordering::Release); return Ok(bytes_read); } None => Err(nix::Error::Sys(nix::errno::Errno::EAGAIN)), @@ -199,6 +209,12 @@ impl OsApi for FakeInputOutput { .unwrap_or(vec![]); if command == SLEEP { std::thread::sleep(std::time::Duration::from_millis(200)); + } else if command == QUIT && self.sigwinch_event.is_some() { + let (lock, cvar) = &*self.should_trigger_sigwinch; + let mut should_trigger_sigwinch = lock.lock().unwrap(); + *should_trigger_sigwinch = true; + cvar.notify_one(); + ::std::thread::sleep(MIN_TIME_BETWEEN_SNAPSHOTS); // give some time for the app to resize before quitting } command } @@ -209,4 +225,14 @@ impl OsApi for FakeInputOutput { self.io_events.lock().unwrap().push(IoEvent::Kill(fd)); Ok(()) } + fn receive_sigwinch(&self, cb: Box) { + if self.sigwinch_event.is_some() { + let (lock, cvar) = &*self.should_trigger_sigwinch; + let mut should_trigger_sigwinch = lock.lock().unwrap(); + while !*should_trigger_sigwinch { + should_trigger_sigwinch = cvar.wait(should_trigger_sigwinch).unwrap(); + } + cb(); + } + } } diff --git a/src/tests/integration/close_pane.rs b/src/tests/integration/close_pane.rs index 0a7aea48..7421ba37 100644 --- a/src/tests/integration/close_pane.rs +++ b/src/tests/integration/close_pane.rs @@ -6,7 +6,7 @@ use crate::tests::utils::{get_next_to_last_snapshot, get_output_frame_snapshots} use crate::{start, CliArgs}; use crate::tests::utils::commands::{ - CLOSE_PANE_IN_PANE_MODE, COMMAND_TOGGLE, ESC, MOVE_FOCUS_IN_PANE_MODE, PANE_MODE, QUIT, + CLOSE_PANE_IN_PANE_MODE, ESC, MOVE_FOCUS_IN_PANE_MODE, PANE_MODE, QUIT, RESIZE_DOWN_IN_RESIZE_MODE, RESIZE_LEFT_IN_RESIZE_MODE, RESIZE_MODE, RESIZE_UP_IN_RESIZE_MODE, SPLIT_DOWN_IN_PANE_MODE, SPLIT_RIGHT_IN_PANE_MODE, }; diff --git a/src/tests/integration/mod.rs b/src/tests/integration/mod.rs index 9036ad6d..83cb7d22 100644 --- a/src/tests/integration/mod.rs +++ b/src/tests/integration/mod.rs @@ -12,4 +12,5 @@ pub mod resize_left; pub mod resize_right; pub mod resize_up; pub mod tabs; +pub mod terminal_window_resize; pub mod toggle_fullscreen; diff --git a/src/tests/integration/snapshots/zellij__tests__integration__terminal_window_resize__window_height_increase_with_one_pane.snap b/src/tests/integration/snapshots/zellij__tests__integration__terminal_window_resize__window_height_increase_with_one_pane.snap new file mode 100644 index 00000000..2b8efe26 --- /dev/null +++ b/src/tests/integration/snapshots/zellij__tests__integration__terminal_window_resize__window_height_increase_with_one_pane.snap @@ -0,0 +1,25 @@ +--- +source: src/tests/integration/terminal_window_resize.rs +expression: snapshot_before_quit + +--- +line1-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line2-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line3-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line4-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line5-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line6-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line7-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line8-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line9-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line10-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line11-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line12-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line13-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line14-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line15-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line16-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line17-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line18-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line19-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + â–ˆ diff --git a/src/tests/integration/snapshots/zellij__tests__integration__terminal_window_resize__window_width_and_height_decrease_with_one_pane.snap b/src/tests/integration/snapshots/zellij__tests__integration__terminal_window_resize__window_width_and_height_decrease_with_one_pane.snap new file mode 100644 index 00000000..55f32b5d --- /dev/null +++ b/src/tests/integration/snapshots/zellij__tests__integration__terminal_window_resize__window_width_and_height_decrease_with_one_pane.snap @@ -0,0 +1,25 @@ +--- +source: src/tests/integration/terminal_window_resize.rs +expression: snapshot_before_quit + +--- +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line16-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line17-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line18-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line19-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +prompt $ â–ˆ + + + + + + + + + + diff --git a/src/tests/integration/snapshots/zellij__tests__integration__terminal_window_resize__window_width_decrease_with_one_pane.snap b/src/tests/integration/snapshots/zellij__tests__integration__terminal_window_resize__window_width_decrease_with_one_pane.snap new file mode 100644 index 00000000..2f72d22a --- /dev/null +++ b/src/tests/integration/snapshots/zellij__tests__integration__terminal_window_resize__window_width_decrease_with_one_pane.snap @@ -0,0 +1,25 @@ +--- +source: src/tests/integration/terminal_window_resize.rs +expression: snapshot_before_quit + +--- +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line11-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line12-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line13-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line14-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line15-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line16-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line17-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line18-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line19-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +prompt $ â–ˆ diff --git a/src/tests/integration/snapshots/zellij__tests__integration__terminal_window_resize__window_width_increase_with_one_pane.snap b/src/tests/integration/snapshots/zellij__tests__integration__terminal_window_resize__window_width_increase_with_one_pane.snap new file mode 100644 index 00000000..95cc6c36 --- /dev/null +++ b/src/tests/integration/snapshots/zellij__tests__integration__terminal_window_resize__window_width_increase_with_one_pane.snap @@ -0,0 +1,25 @@ +--- +source: src/tests/integration/terminal_window_resize.rs +expression: snapshot_before_quit + +--- +line2-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line3-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line4-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line5-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line6-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line7-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line8-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line9-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line10-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line11-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line12-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line13-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line14-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line15-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line16-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line17-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line18-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +line19-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +prompt $ + â–ˆ diff --git a/src/tests/integration/terminal_window_resize.rs b/src/tests/integration/terminal_window_resize.rs new file mode 100644 index 00000000..ab73d24e --- /dev/null +++ b/src/tests/integration/terminal_window_resize.rs @@ -0,0 +1,127 @@ +use crate::panes::PositionAndSize; +use ::insta::assert_snapshot; + +use crate::tests::fakes::FakeInputOutput; +use crate::tests::utils::commands::QUIT; +use crate::tests::utils::{get_next_to_last_snapshot, get_output_frame_snapshots}; +use crate::{start, CliArgs}; + +fn get_fake_os_input(fake_win_size: &PositionAndSize) -> FakeInputOutput { + FakeInputOutput::new(fake_win_size.clone()) +} + +#[test] +pub fn window_width_decrease_with_one_pane() { + let fake_win_size = PositionAndSize { + columns: 121, + rows: 20, + x: 0, + y: 0, + }; + let mut fake_input_output = get_fake_os_input(&fake_win_size); + fake_input_output.add_terminal_input(&[&QUIT]); + fake_input_output.add_sigwinch_event(PositionAndSize { + columns: 90, + rows: 20, + x: 0, + y: 0, + }); + let opts = CliArgs::default(); + start(Box::new(fake_input_output.clone()), opts); + let output_frames = fake_input_output + .stdout_writer + .output_frames + .lock() + .unwrap(); + let snapshots = get_output_frame_snapshots(&output_frames, &fake_win_size); + let snapshot_before_quit = + get_next_to_last_snapshot(snapshots).expect("could not find snapshot"); + assert_snapshot!(snapshot_before_quit); +} + +#[test] +pub fn window_width_increase_with_one_pane() { + let fake_win_size = PositionAndSize { + columns: 121, + rows: 20, + x: 0, + y: 0, + }; + let mut fake_input_output = get_fake_os_input(&fake_win_size); + fake_input_output.add_terminal_input(&[&QUIT]); + fake_input_output.add_sigwinch_event(PositionAndSize { + columns: 141, + rows: 20, + x: 0, + y: 0, + }); + let opts = CliArgs::default(); + start(Box::new(fake_input_output.clone()), opts); + let output_frames = fake_input_output + .stdout_writer + .output_frames + .lock() + .unwrap(); + let snapshots = get_output_frame_snapshots(&output_frames, &fake_win_size); + let snapshot_before_quit = + get_next_to_last_snapshot(snapshots).expect("could not find snapshot"); + assert_snapshot!(snapshot_before_quit); +} + +#[test] +pub fn window_height_increase_with_one_pane() { + let fake_win_size = PositionAndSize { + columns: 121, + rows: 20, + x: 0, + y: 0, + }; + let mut fake_input_output = get_fake_os_input(&fake_win_size); + fake_input_output.add_terminal_input(&[&QUIT]); + fake_input_output.add_sigwinch_event(PositionAndSize { + columns: 121, + rows: 30, + x: 0, + y: 0, + }); + let opts = CliArgs::default(); + start(Box::new(fake_input_output.clone()), opts); + let output_frames = fake_input_output + .stdout_writer + .output_frames + .lock() + .unwrap(); + let snapshots = get_output_frame_snapshots(&output_frames, &fake_win_size); + let snapshot_before_quit = + get_next_to_last_snapshot(snapshots).expect("could not find snapshot"); + assert_snapshot!(snapshot_before_quit); +} + +#[test] +pub fn window_width_and_height_decrease_with_one_pane() { + let fake_win_size = PositionAndSize { + columns: 121, + rows: 20, + x: 0, + y: 0, + }; + let mut fake_input_output = get_fake_os_input(&fake_win_size); + fake_input_output.add_terminal_input(&[&QUIT]); + fake_input_output.add_sigwinch_event(PositionAndSize { + columns: 90, + rows: 10, + x: 0, + y: 0, + }); + let opts = CliArgs::default(); + start(Box::new(fake_input_output.clone()), opts); + let output_frames = fake_input_output + .stdout_writer + .output_frames + .lock() + .unwrap(); + let snapshots = get_output_frame_snapshots(&output_frames, &fake_win_size); + let snapshot_before_quit = + get_next_to_last_snapshot(snapshots).expect("could not find snapshot"); + assert_snapshot!(snapshot_before_quit); +} diff --git a/src/tests/possible_tty_inputs.rs b/src/tests/possible_tty_inputs.rs index fa918be0..30668360 100644 --- a/src/tests/possible_tty_inputs.rs +++ b/src/tests/possible_tty_inputs.rs @@ -1,6 +1,7 @@ use crate::tests::tty_inputs::{ - COL_10, COL_121, COL_14, COL_15, COL_19, COL_20, COL_24, COL_25, COL_29, COL_30, COL_34, - COL_39, COL_4, COL_40, COL_47, COL_50, COL_60, COL_70, COL_8, COL_9, COL_90, COL_96, + COL_10, COL_121, COL_14, COL_141, COL_15, COL_19, COL_20, COL_24, COL_25, COL_29, COL_30, + COL_34, COL_39, COL_4, COL_40, COL_47, COL_50, COL_60, COL_70, COL_8, COL_80, COL_9, COL_90, + COL_96, }; use std::collections::HashMap; use std::fs; @@ -69,9 +70,11 @@ pub fn get_possible_tty_inputs() -> HashMap { let col_50_bytes = Bytes::new().content_from_str(&COL_50); let col_60_bytes = Bytes::new().content_from_str(&COL_60); let col_70_bytes = Bytes::new().content_from_str(&COL_70); + let col_80_bytes = Bytes::new().content_from_str(&COL_80); let col_90_bytes = Bytes::new().content_from_str(&COL_90); let col_96_bytes = Bytes::new().content_from_str(&COL_96); let col_121_bytes = Bytes::new().content_from_str(&COL_121); + let col_141_bytes = Bytes::new().content_from_str(&COL_141); possible_inputs.insert(4, col_4_bytes); possible_inputs.insert(8, col_8_bytes); possible_inputs.insert(9, col_9_bytes); @@ -91,8 +94,10 @@ pub fn get_possible_tty_inputs() -> HashMap { possible_inputs.insert(50, col_50_bytes); possible_inputs.insert(60, col_60_bytes); possible_inputs.insert(70, col_70_bytes); + possible_inputs.insert(80, col_80_bytes); possible_inputs.insert(90, col_90_bytes); possible_inputs.insert(96, col_96_bytes); possible_inputs.insert(121, col_121_bytes); + possible_inputs.insert(141, col_141_bytes); possible_inputs } diff --git a/src/tests/tty_inputs.rs b/src/tests/tty_inputs.rs index cf49290e..43ac8abc 100644 --- a/src/tests/tty_inputs.rs +++ b/src/tests/tty_inputs.rs @@ -1,3 +1,26 @@ +pub const COL_141: [&str; 20] = [ + "line1-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", + "line2-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", + "line3-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", + "line4-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", + "line5-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", + "line6-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", + "line7-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", + "line8-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", + "line9-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", + "line10-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", + "line11-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", + "line12-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", + "line13-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", + "line14-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", + "line15-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", + "line16-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", + "line17-baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", + "line18-baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", + "line19-baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", + "prompt $ ", +]; + pub const COL_121: [&str; 20] = [ "line1-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", "line2-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n", @@ -457,6 +480,29 @@ pub const COL_70: [&str; 20] = [ "prompt $ ", ]; +pub const COL_80: [&str; 20] = [ + "line1-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r\n", + "line2-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r\n", + "line3-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r\n", + "line4-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r\n", + "line5-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r\n", + "line6-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r\n", + "line7-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r\n", + "line8-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r\n", + "line9-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r\n", + "line10-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r\n", + "line11-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r\n", + "line12-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r\n", + "line13-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r\n", + "line14-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r\n", + "line15-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r\n", + "line16-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r\n", + "line17-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r\n", + "line18-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r\n", + "line19-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r\n", + "prompt $ ", +]; + pub const COL_90: [&str; 20] = [ "line1-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r\n", "line2-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r\n", diff --git a/src/tests/utils.rs b/src/tests/utils.rs index 14f4cfa0..915adfb0 100644 --- a/src/tests/utils.rs +++ b/src/tests/utils.rs @@ -45,7 +45,6 @@ pub fn get_next_to_last_snapshot(mut snapshots: Vec) -> Option { } pub mod commands { - pub const COMMAND_TOGGLE: [u8; 1] = [7]; // ctrl-g pub const QUIT: [u8; 1] = [17]; // ctrl-q pub const ESC: [u8; 1] = [27]; From ecc106e23167695bf51430af79858b9ce42552b2 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Thu, 1 Apr 2021 20:08:44 +0200 Subject: [PATCH 25/39] Add myself to GOVERNANCE.md --- GOVERNANCE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 86c32d5d..ce3693af 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -27,3 +27,4 @@ Once the organization reaches 10 members, a reasonable and achievable process mu * Kunal Mohan * Henil Dedania * Roee Shapira +* Alex Kenji Berthold From d6a17f54fd3b5653357e624577dc69ba211dea2e Mon Sep 17 00:00:00 2001 From: Kyle Sutherland-Cash Date: Sat, 3 Apr 2021 01:42:13 -0700 Subject: [PATCH 26/39] Add Kyle to governance.md --- GOVERNANCE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index ce3693af..b405d29d 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -28,3 +28,4 @@ Once the organization reaches 10 members, a reasonable and achievable process mu * Henil Dedania * Roee Shapira * Alex Kenji Berthold +* Kyle Sutherland-Cash From a4430bf1580a0ab09706179867bb09ef1b19fc8e Mon Sep 17 00:00:00 2001 From: a-kenji Date: Sat, 3 Apr 2021 21:28:57 +0200 Subject: [PATCH 27/39] Chore(rustfmt) --- src/common/input/handler.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/common/input/handler.rs b/src/common/input/handler.rs index 7db14d64..2020fc20 100644 --- a/src/common/input/handler.rs +++ b/src/common/input/handler.rs @@ -82,11 +82,10 @@ impl InputHandler { break 'input_loop; } } - termion::event::Event::Mouse(_) - | termion::event::Event::Unsupported(_) => { - // Mouse and unsupported events aren't implemented yet, - // use a NoOp untill then. - } + termion::event::Event::Mouse(_) | termion::event::Event::Unsupported(_) => { + // Mouse and unsupported events aren't implemented yet, + // use a NoOp untill then. + } }, Err(err) => panic!("Encountered read error: {:?}", err), } From ca014d7aaced18c534c5a3577906922db0a58e75 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Sun, 4 Apr 2021 18:03:24 +0200 Subject: [PATCH 28/39] Use Default Configuration for Tests Uses the default configuration for tests, in order to not mess up test cases with different configurations in the config file. --- src/common/input/config.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/common/input/config.rs b/src/common/input/config.rs index 0713e54e..4bb9e10f 100644 --- a/src/common/input/config.rs +++ b/src/common/input/config.rs @@ -79,6 +79,7 @@ impl Config { } /// Entry point of the configuration + #[cfg(not(test))] pub fn from_cli_config(cli_config: Option) -> ConfigResult { match cli_config { Some(ConfigCli::Config { clean, .. }) if clean => Ok(Config::default()), @@ -88,6 +89,13 @@ impl Config { Some(_) | None => Ok(Config::from_default_path()?), } } + + //#[allow(unused_must_use)] + /// In order not to mess up tests from changing configurations + #[cfg(test)] + pub fn from_cli_config(_: Option) -> ConfigResult { + Ok(Config::default()) + } } impl Display for ConfigError { From d9a85e977d4c81f6efcf73785b1e17175d3edc59 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Sun, 4 Apr 2021 19:27:28 +0200 Subject: [PATCH 29/39] Readd short option for config --- assets/completions/_zellij | 36 +++++++++++++++++++++++++++++++++- assets/completions/zellij.bash | 20 ++++++++++++++++++- src/cli.rs | 1 + 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/assets/completions/_zellij b/assets/completions/_zellij index 8b89db14..fc1cd00f 100644 --- a/assets/completions/_zellij +++ b/assets/completions/_zellij @@ -39,7 +39,27 @@ _zellij() { (( CURRENT += 1 )) curcontext="${curcontext%:*:*}:zellij-command-$line[1]:" case $line[1] in - (config) + (c) +_arguments "${_arguments_options[@]}" \ +'--clean[Disables loading of configuration file at default location]' \ +'-h[Prints help information]' \ +'--help[Prints help information]' \ +'-V[Prints version information]' \ +'--version[Prints version information]' \ +'::path:_files' \ +&& ret=0 +;; +(c) +_arguments "${_arguments_options[@]}" \ +'--clean[Disables loading of configuration file at default location]' \ +'-h[Prints help information]' \ +'--help[Prints help information]' \ +'-V[Prints version information]' \ +'--version[Prints version information]' \ +'::path:_files' \ +&& ret=0 +;; +(config) _arguments "${_arguments_options[@]}" \ '--clean[Disables loading of configuration file at default location]' \ '-h[Prints help information]' \ @@ -70,6 +90,20 @@ _zellij_commands() { ) _describe -t commands 'zellij commands' commands "$@" } +(( $+functions[_c_commands] )) || +_c_commands() { + local commands; commands=( + + ) + _describe -t commands 'c commands' commands "$@" +} +(( $+functions[_zellij__c_commands] )) || +_zellij__c_commands() { + local commands; commands=( + + ) + _describe -t commands 'zellij c commands' commands "$@" +} (( $+functions[_zellij__config_commands] )) || _zellij__config_commands() { local commands; commands=( diff --git a/assets/completions/zellij.bash b/assets/completions/zellij.bash index 549b1e9f..10c18d53 100644 --- a/assets/completions/zellij.bash +++ b/assets/completions/zellij.bash @@ -13,6 +13,9 @@ _zellij() { cmd="zellij" ;; + c) + cmd+="__c" + ;; config) cmd+="__config" ;; @@ -26,7 +29,7 @@ _zellij() { case "${cmd}" in zellij) - opts=" -m -d -h -V -s -o -l --move-focus --debug --help --version --split --open-file --max-panes --layout config help" + opts=" -m -d -h -V -s -o -l --move-focus --debug --help --version --split --open-file --max-panes --layout config help c c" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -69,6 +72,21 @@ _zellij() { return 0 ;; + zellij__c) + opts=" -h -V --clean --help --version " + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; zellij__config) opts=" -h -V --clean --help --version " if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then diff --git a/src/cli.rs b/src/cli.rs index d4c0f78a..b1fb8f38 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -34,6 +34,7 @@ pub struct CliArgs { #[derive(Debug, StructOpt)] pub enum ConfigCli { /// Path to the configuration yaml file + #[structopt(alias = "c")] Config { path: Option, #[structopt(long)] From cdbd74f49b9d7e896fffd010dabede5164186278 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Mon, 5 Apr 2021 10:48:27 +0200 Subject: [PATCH 30/39] fix(tabs): do not resize title bar when closing pane under it (#252) * fix(tabs): closing a pane no longer resizes the tab bar * style(tab): add clarification comment --- src/client/tab.rs | 105 +++++++++++++++++++++++++++++----------------- 1 file changed, 67 insertions(+), 38 deletions(-) diff --git a/src/client/tab.rs b/src/client/tab.rs index 695d42cb..9e2b370f 100644 --- a/src/client/tab.rs +++ b/src/client/tab.rs @@ -2064,47 +2064,76 @@ impl Tab { } } pub fn close_pane_without_rerender(&mut self, id: PaneId) { - if let Some(terminal_to_close) = self.panes.get(&id) { - let terminal_to_close_width = terminal_to_close.columns(); - let terminal_to_close_height = terminal_to_close.rows(); - if let Some(terminals) = self.panes_to_the_left_between_aligning_borders(id) { - for terminal_id in terminals.iter() { - self.increase_pane_width_right(&terminal_id, terminal_to_close_width + 1); - // 1 for the border + if let Some(pane_to_close) = self.panes.get(&id) { + let pane_to_close_width = pane_to_close.columns(); + let pane_to_close_height = pane_to_close.rows(); + if let Some(panes) = self.panes_to_the_left_between_aligning_borders(id) { + if panes.iter().all(|p| { + let pane = self.panes.get(p).unwrap(); + pane.can_increase_width_by(pane_to_close_width + 1) + }) { + for pane_id in panes.iter() { + self.increase_pane_width_right(&pane_id, pane_to_close_width + 1); + // 1 for the border + } + self.panes.remove(&id); + if self.active_terminal == Some(id) { + self.active_terminal = self.next_active_pane(panes); + } + return; } - if self.active_terminal == Some(id) { - self.active_terminal = self.next_active_pane(terminals); - } - } else if let Some(terminals) = self.panes_to_the_right_between_aligning_borders(id) { - for terminal_id in terminals.iter() { - self.increase_pane_width_left(&terminal_id, terminal_to_close_width + 1); - // 1 for the border - } - if self.active_terminal == Some(id) { - self.active_terminal = self.next_active_pane(terminals); - } - } else if let Some(terminals) = self.panes_above_between_aligning_borders(id) { - for terminal_id in terminals.iter() { - self.increase_pane_height_down(&terminal_id, terminal_to_close_height + 1); - // 1 for the border - } - if self.active_terminal == Some(id) { - self.active_terminal = self.next_active_pane(terminals); - } - } else if let Some(terminals) = self.panes_below_between_aligning_borders(id) { - for terminal_id in terminals.iter() { - self.increase_pane_height_up(&terminal_id, terminal_to_close_height + 1); - // 1 for the border - } - if self.active_terminal == Some(id) { - self.active_terminal = self.next_active_pane(terminals); - } - } else { } + if let Some(panes) = self.panes_to_the_right_between_aligning_borders(id) { + if panes.iter().all(|p| { + let pane = self.panes.get(p).unwrap(); + pane.can_increase_width_by(pane_to_close_width + 1) + }) { + for pane_id in panes.iter() { + self.increase_pane_width_left(&pane_id, pane_to_close_width + 1); + // 1 for the border + } + self.panes.remove(&id); + if self.active_terminal == Some(id) { + self.active_terminal = self.next_active_pane(panes); + } + return; + } + } + if let Some(panes) = self.panes_above_between_aligning_borders(id) { + if panes.iter().all(|p| { + let pane = self.panes.get(p).unwrap(); + pane.can_increase_height_by(pane_to_close_height + 1) + }) { + for pane_id in panes.iter() { + self.increase_pane_height_down(&pane_id, pane_to_close_height + 1); + // 1 for the border + } + self.panes.remove(&id); + if self.active_terminal == Some(id) { + self.active_terminal = self.next_active_pane(panes); + } + return; + } + } + if let Some(panes) = self.panes_below_between_aligning_borders(id) { + if panes.iter().all(|p| { + let pane = self.panes.get(p).unwrap(); + pane.can_increase_height_by(pane_to_close_height + 1) + }) { + for pane_id in panes.iter() { + self.increase_pane_height_up(&pane_id, pane_to_close_height + 1); + // 1 for the border + } + self.panes.remove(&id); + if self.active_terminal == Some(id) { + self.active_terminal = self.next_active_pane(panes); + } + return; + } + } + // if we reached here, this is either the last pane or there's some sort of + // configuration error (eg. we're trying to close a pane surrounded by fixed panes) self.panes.remove(&id); - if self.active_terminal.is_none() { - self.active_terminal = self.next_active_pane(self.get_pane_ids()); - } } } pub fn close_focused_pane(&mut self) { From f84d293c1bbaf55229f788f8061d7f0f033f822f Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Mon, 5 Apr 2021 16:37:21 +0200 Subject: [PATCH 31/39] fix(terminal): reset styles on cursor movement (#254) --- src/client/panes/terminal_pane.rs | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/client/panes/terminal_pane.rs b/src/client/panes/terminal_pane.rs index 463a33e3..13575475 100644 --- a/src/client/panes/terminal_pane.rs +++ b/src/client/panes/terminal_pane.rs @@ -380,8 +380,7 @@ impl TerminalPane { self.mark_for_rerender(); } fn add_newline(&mut self) { - let mut pad_character = EMPTY_TERMINAL_CHARACTER; - pad_character.styles = self.pending_styles; + let pad_character = EMPTY_TERMINAL_CHARACTER; self.grid.add_canonical_line(pad_character); self.mark_for_rerender(); } @@ -497,8 +496,7 @@ impl vte::Perform for TerminalPane { } else { (params[0] as usize - 1, params[1] as usize - 1) }; - let mut pad_character = EMPTY_TERMINAL_CHARACTER; - pad_character.styles = self.pending_styles; + let pad_character = EMPTY_TERMINAL_CHARACTER; self.grid.move_cursor_to(col, row, pad_character); } else if c == 'A' { // move cursor up until edge of screen @@ -507,8 +505,7 @@ impl vte::Perform for TerminalPane { } else if c == 'B' { // move cursor down until edge of screen let move_down_count = if params[0] == 0 { 1 } else { params[0] }; - let mut pad_character = EMPTY_TERMINAL_CHARACTER; - pad_character.styles = self.pending_styles; + let pad_character = EMPTY_TERMINAL_CHARACTER; self.grid .move_cursor_down(move_down_count as usize, pad_character); } else if c == 'D' { @@ -600,8 +597,7 @@ impl vte::Perform for TerminalPane { } else { params[0] as usize }; - let mut pad_character = EMPTY_TERMINAL_CHARACTER; - pad_character.styles = self.pending_styles; + let pad_character = EMPTY_TERMINAL_CHARACTER; self.grid .delete_lines_in_scroll_region(line_count_to_delete, pad_character); } else if c == 'L' { @@ -611,8 +607,7 @@ impl vte::Perform for TerminalPane { } else { params[0] as usize }; - let mut pad_character = EMPTY_TERMINAL_CHARACTER; - pad_character.styles = self.pending_styles; + let pad_character = EMPTY_TERMINAL_CHARACTER; self.grid .add_empty_lines_in_scroll_region(line_count_to_add, pad_character); } else if c == 'q' { @@ -632,8 +627,7 @@ impl vte::Perform for TerminalPane { // minus 1 because this is 1 indexed params[0] as usize - 1 }; - let mut pad_character = EMPTY_TERMINAL_CHARACTER; - pad_character.styles = self.pending_styles; + let pad_character = EMPTY_TERMINAL_CHARACTER; self.grid.move_cursor_to_line(line, pad_character); } else if c == 'P' { // erase characters @@ -672,8 +666,7 @@ impl vte::Perform for TerminalPane { } else { params[0] as usize }; - let mut pad_character = EMPTY_TERMINAL_CHARACTER; - pad_character.styles = self.pending_styles; + let pad_character = EMPTY_TERMINAL_CHARACTER; self.grid .delete_lines_in_scroll_region(count, pad_character); // TODO: since delete_lines_in_scroll_region also adds lines, is the below redundant? From bb87cbdd28618ce510f83f58678c2f4592cb89b2 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Tue, 6 Apr 2021 11:09:13 +0200 Subject: [PATCH 32/39] fix(terminal): do not hang app if terminal refuses to quit (#255) --- src/common/command_is_executing.rs | 4 ++-- src/common/os_input_output.rs | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/common/command_is_executing.rs b/src/common/command_is_executing.rs index 93c44eb6..775a7bfc 100644 --- a/src/common/command_is_executing.rs +++ b/src/common/command_is_executing.rs @@ -23,7 +23,7 @@ impl CommandIsExecuting { let (lock, cvar) = &*self.closing_pane; let mut closing_pane = lock.lock().unwrap(); *closing_pane = false; - cvar.notify_one(); + cvar.notify_all(); } pub fn opening_new_pane(&mut self) { let (lock, _cvar) = &*self.opening_new_pane; @@ -34,7 +34,7 @@ impl CommandIsExecuting { let (lock, cvar) = &*self.opening_new_pane; let mut opening_new_pane = lock.lock().unwrap(); *opening_new_pane = false; - cvar.notify_one(); + cvar.notify_all(); } pub fn wait_until_pane_is_closed(&self) { let (lock, cvar) = &*self.closing_pane; diff --git a/src/common/os_input_output.rs b/src/common/os_input_output.rs index de0c6a3b..013b9f12 100644 --- a/src/common/os_input_output.rs +++ b/src/common/os_input_output.rs @@ -242,7 +242,13 @@ impl OsApi for OsInputOutput { Box::new(stdout) } fn kill(&mut self, pid: RawFd) -> Result<(), nix::Error> { - kill(Pid::from_raw(pid), Some(Signal::SIGINT)).unwrap(); + // TODO: + // Ideally, we should be using SIGINT rather than SIGKILL here, but there are cases in which + // the terminal we're trying to kill hangs on SIGINT and so all the app gets stuck + // that's why we're sending SIGKILL here + // A better solution would be to send SIGINT here and not wait for it, and then have + // a background thread do the waitpid stuff and send SIGKILL if the process is stuck + kill(Pid::from_raw(pid), Some(Signal::SIGKILL)).unwrap(); waitpid(Pid::from_raw(pid), None).unwrap(); Ok(()) } From 1c71d16eb534bb60fbaa0dd894a03cb9ef16a37a Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Tue, 6 Apr 2021 15:16:18 +0200 Subject: [PATCH 33/39] fix(terminal): support bright colors (#256) * fix(terminal): support bright colors * style(fmt): make it uglier because rustfmt --- src/client/panes/terminal_character.rs | 64 ++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/client/panes/terminal_character.rs b/src/client/panes/terminal_character.rs index 053ebd3f..ce967ef8 100644 --- a/src/client/panes/terminal_character.rs +++ b/src/client/panes/terminal_character.rs @@ -37,6 +37,14 @@ pub enum NamedColor { Magenta, Cyan, White, + BrightBlack, + BrightRed, + BrightGreen, + BrightYellow, + BrightBlue, + BrightMagenta, + BrightCyan, + BrightWhite, } impl NamedColor { @@ -50,6 +58,14 @@ impl NamedColor { NamedColor::Magenta => format!("{}", 35), NamedColor::Cyan => format!("{}", 36), NamedColor::White => format!("{}", 37), + NamedColor::BrightBlack => format!("{}", 90), + NamedColor::BrightRed => format!("{}", 91), + NamedColor::BrightGreen => format!("{}", 92), + NamedColor::BrightYellow => format!("{}", 93), + NamedColor::BrightBlue => format!("{}", 94), + NamedColor::BrightMagenta => format!("{}", 95), + NamedColor::BrightCyan => format!("{}", 96), + NamedColor::BrightWhite => format!("{}", 97), } } fn to_background_ansi_code(&self) -> String { @@ -62,6 +78,14 @@ impl NamedColor { NamedColor::Magenta => format!("{}", 45), NamedColor::Cyan => format!("{}", 46), NamedColor::White => format!("{}", 47), + NamedColor::BrightBlack => format!("{}", 100), + NamedColor::BrightRed => format!("{}", 101), + NamedColor::BrightGreen => format!("{}", 102), + NamedColor::BrightYellow => format!("{}", 103), + NamedColor::BrightBlue => format!("{}", 104), + NamedColor::BrightMagenta => format!("{}", 105), + NamedColor::BrightCyan => format!("{}", 106), + NamedColor::BrightWhite => format!("{}", 107), } } } @@ -383,6 +407,46 @@ impl CharacterStyles { params_used += 1; // even if it's a bug, let's not create an endless loop, eh? } [49, ..] => *self = self.background(Some(AnsiCode::Reset)), + [90, ..] => { + *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::BrightBlack))) + } + [91, ..] => *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::BrightRed))), + [92, ..] => { + *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::BrightGreen))) + } + [93, ..] => { + *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::BrightYellow))) + } + [94, ..] => *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::BrightBlue))), + [95, ..] => { + *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::BrightMagenta))) + } + [96, ..] => *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::BrightCyan))), + [97, ..] => { + *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::BrightWhite))) + } + [100, ..] => { + *self = self.background(Some(AnsiCode::NamedColor(NamedColor::BrightBlack))) + } + [101, ..] => *self = self.background(Some(AnsiCode::NamedColor(NamedColor::BrightRed))), + [102, ..] => { + *self = self.background(Some(AnsiCode::NamedColor(NamedColor::BrightGreen))) + } + [103, ..] => { + *self = self.background(Some(AnsiCode::NamedColor(NamedColor::BrightYellow))) + } + [104, ..] => { + *self = self.background(Some(AnsiCode::NamedColor(NamedColor::BrightBlue))) + } + [105, ..] => { + *self = self.background(Some(AnsiCode::NamedColor(NamedColor::BrightMagenta))) + } + [106, ..] => { + *self = self.background(Some(AnsiCode::NamedColor(NamedColor::BrightCyan))) + } + [107, ..] => { + *self = self.background(Some(AnsiCode::NamedColor(NamedColor::BrightWhite))) + } _ => { // if this happens, it's a bug let _ = debug_log_to_file(format!("unhandled csi m code {:?}", ansi_params)); From e551bec538fcfe709239fcbc6fc2678335729b8c Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Tue, 6 Apr 2021 16:55:32 +0200 Subject: [PATCH 34/39] fix(screen): handle events directed at other tabs (#257) --- src/client/tab.rs | 3 +++ src/common/mod.rs | 20 ++++++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/client/tab.rs b/src/client/tab.rs index 9e2b370f..db109480 100644 --- a/src/client/tab.rs +++ b/src/client/tab.rs @@ -561,6 +561,9 @@ impl Tab { None } } + pub fn has_terminal_pid(&self, pid: RawFd) -> bool { + self.panes.contains_key(&PaneId::Terminal(pid)) + } pub fn handle_pty_event(&mut self, pid: RawFd, event: VteEvent) { // if we don't have the terminal in self.terminals it's probably because // of a race condition where the terminal was created in pty_bus but has not diff --git a/src/common/mod.rs b/src/common/mod.rs index e9b74d58..8180a724 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -276,10 +276,22 @@ pub fn start(mut os_input: Box, opts: CliArgs) { screen.send_pty_instructions.update(err_ctx); match event { ScreenInstruction::Pty(pid, vte_event) => { - screen - .get_active_tab_mut() - .unwrap() - .handle_pty_event(pid, vte_event); + let active_tab = screen.get_active_tab_mut().unwrap(); + if active_tab.has_terminal_pid(pid) { + // it's most likely that this event is directed at the active tab + // look there first + active_tab.handle_pty_event(pid, vte_event); + } else { + // if this event wasn't directed at the active tab, start looking + // in other tabs + let all_tabs = screen.get_tabs_mut(); + for tab in all_tabs.values_mut() { + if tab.has_terminal_pid(pid) { + tab.handle_pty_event(pid, vte_event); + break; + } + } + } } ScreenInstruction::Render => { screen.render(); From c82767270850dd4292057040991a8452a1aff5d5 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Tue, 6 Apr 2021 17:38:00 +0200 Subject: [PATCH 35/39] fix(tabs): do not toggle fullscreen if there are no other panes (#258) * fix(tabs): do not toggle fullscreen if there are no other panes * style(fmt): rustfmt --- src/client/tab.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/client/tab.rs b/src/client/tab.rs index db109480..c573bdd4 100644 --- a/src/client/tab.rs +++ b/src/client/tab.rs @@ -634,8 +634,17 @@ impl Tab { } }); self.panes_to_hide = pane_ids_to_hide.collect(); - let active_terminal = self.panes.get_mut(&active_pane_id).unwrap(); - active_terminal.override_size_and_position(expand_to.x, expand_to.y, &expand_to); + if self.panes_to_hide.is_empty() { + // nothing to do, pane is already as fullscreen as it can be, let's bail + return; + } else { + let active_terminal = self.panes.get_mut(&active_pane_id).unwrap(); + active_terminal.override_size_and_position( + expand_to.x, + expand_to.y, + &expand_to, + ); + } } let active_terminal = self.panes.get(&active_pane_id).unwrap(); if let PaneId::Terminal(active_pid) = active_pane_id { From c25eb04de99fb2e9a722ddec0e7d2b1e67415a69 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Tue, 6 Apr 2021 17:55:48 +0200 Subject: [PATCH 36/39] fix(tabs): do not crash if closing pane in fullscreen (#259) --- src/client/tab.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/client/tab.rs b/src/client/tab.rs index c573bdd4..82c2639f 100644 --- a/src/client/tab.rs +++ b/src/client/tab.rs @@ -2076,6 +2076,9 @@ impl Tab { } } pub fn close_pane_without_rerender(&mut self, id: PaneId) { + if self.fullscreen_is_active { + self.toggle_active_pane_fullscreen(); + } if let Some(pane_to_close) = self.panes.get(&id) { let pane_to_close_width = pane_to_close.columns(); let pane_to_close_height = pane_to_close.rows(); From 65c75ebb9593dc2a3b2ae5a82db03dad41bd1b10 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Thu, 8 Apr 2021 11:36:49 +0200 Subject: [PATCH 37/39] feat(controls): add quick navigation (#260) * feat(input): quick navigation * feat(ui): quick navigation * style(fmt): rustfmt --- default-tiles/status-bar/src/main.rs | 4 +- default-tiles/status-bar/src/second_line.rs | 131 +++++++++++++++++++- src/client/boundaries.rs | 4 +- src/client/tab.rs | 56 +++++++++ src/common/errors.rs | 8 +- src/common/input/actions.rs | 4 +- src/common/input/handler.rs | 55 +++++--- src/common/input/keybinds.rs | 49 +++++++- src/common/mod.rs | 10 +- src/common/screen.rs | 4 +- 10 files changed, 292 insertions(+), 33 deletions(-) diff --git a/default-tiles/status-bar/src/main.rs b/default-tiles/status-bar/src/main.rs index 8daa158e..ee4d1763 100644 --- a/default-tiles/status-bar/src/main.rs +++ b/default-tiles/status-bar/src/main.rs @@ -63,8 +63,8 @@ impl ZellijTile for State { let second_line = keybinds(&self.mode_info, cols); // [48;5;238m is gray background, [0K is so that it fills the rest of the line - // [48;5;16m is black background, [0K is so that it fills the rest of the line + // [m is background reset, [0K is so that it clears the rest of the line println!("{}\u{1b}[48;5;238m\u{1b}[0K", first_line); - println!("{}\u{1b}[48;5;16m\u{1b}[0K", second_line); + println!("\u{1b}[m{}\u{1b}[0K", second_line); } } diff --git a/default-tiles/status-bar/src/second_line.rs b/default-tiles/status-bar/src/second_line.rs index ebcb93dd..48a82c6c 100644 --- a/default-tiles/status-bar/src/second_line.rs +++ b/default-tiles/status-bar/src/second_line.rs @@ -59,6 +59,122 @@ fn first_word_shortcut(is_first_shortcut: bool, letter: &str, description: &str) len, } } +fn quicknav_full() -> LinePart { + let text_first_part = " Tip: "; + let alt = "Alt"; + let text_second_part = " + "; + let new_pane_shortcut = "n"; + let text_third_part = " => open new pane. "; + let second_alt = "Alt"; + let text_fourth_part = " + "; + let brackets_navigation = "[]"; + let text_fifth_part = " or "; + let hjkl_navigation = "hjkl"; + let text_sixths_part = " => navigate between panes."; + let len = text_first_part.chars().count() + + alt.chars().count() + + text_second_part.chars().count() + + new_pane_shortcut.chars().count() + + text_third_part.chars().count() + + second_alt.chars().count() + + text_fourth_part.chars().count() + + brackets_navigation.chars().count() + + text_fifth_part.chars().count() + + hjkl_navigation.chars().count() + + text_sixths_part.chars().count(); + LinePart { + part: format!( + "{}{}{}{}{}{}{}{}{}{}{}", + text_first_part, + Style::new().fg(ORANGE).bold().paint(alt), + text_second_part, + Style::new().fg(GREEN).bold().paint(new_pane_shortcut), + text_third_part, + Style::new().fg(ORANGE).bold().paint(second_alt), + text_fourth_part, + Style::new().fg(GREEN).bold().paint(brackets_navigation), + text_fifth_part, + Style::new().fg(GREEN).bold().paint(hjkl_navigation), + text_sixths_part, + ), + len, + } +} + +fn quicknav_medium() -> LinePart { + let text_first_part = " Tip: "; + let alt = "Alt"; + let text_second_part = " + "; + let new_pane_shortcut = "n"; + let text_third_part = " => new pane. "; + let second_alt = "Alt"; + let text_fourth_part = " + "; + let brackets_navigation = "[]"; + let text_fifth_part = " or "; + let hjkl_navigation = "hjkl"; + let text_sixths_part = " => navigate."; + let len = text_first_part.chars().count() + + alt.chars().count() + + text_second_part.chars().count() + + new_pane_shortcut.chars().count() + + text_third_part.chars().count() + + second_alt.chars().count() + + text_fourth_part.chars().count() + + brackets_navigation.chars().count() + + text_fifth_part.chars().count() + + hjkl_navigation.chars().count() + + text_sixths_part.chars().count(); + LinePart { + part: format!( + "{}{}{}{}{}{}{}{}{}{}{}", + text_first_part, + Style::new().fg(ORANGE).bold().paint(alt), + text_second_part, + Style::new().fg(GREEN).bold().paint(new_pane_shortcut), + text_third_part, + Style::new().fg(ORANGE).bold().paint(second_alt), + text_fourth_part, + Style::new().fg(GREEN).bold().paint(brackets_navigation), + text_fifth_part, + Style::new().fg(GREEN).bold().paint(hjkl_navigation), + text_sixths_part, + ), + len, + } +} + +fn quicknav_short() -> LinePart { + let text_first_part = " QuickNav: "; + let alt = "Alt"; + let text_second_part = " + "; + let new_pane_shortcut = "n"; + let text_third_part = "/"; + let brackets_navigation = "[]"; + let text_fifth_part = "/"; + let hjkl_navigation = "hjkl"; + let len = text_first_part.chars().count() + + alt.chars().count() + + text_second_part.chars().count() + + new_pane_shortcut.chars().count() + + text_third_part.chars().count() + + brackets_navigation.chars().count() + + text_fifth_part.chars().count() + + hjkl_navigation.chars().count(); + LinePart { + part: format!( + "{}{}{}{}{}{}{}{}", + text_first_part, + Style::new().fg(ORANGE).bold().paint(alt), + text_second_part, + Style::new().fg(GREEN).bold().paint(new_pane_shortcut), + text_third_part, + Style::new().fg(GREEN).bold().paint(brackets_navigation), + text_fifth_part, + Style::new().fg(GREEN).bold().paint(hjkl_navigation), + ), + len, + } +} fn locked_interface_indication() -> LinePart { let locked_text = " -- INTERFACE LOCKED -- "; @@ -99,7 +215,7 @@ fn select_pane_shortcut(is_first_shortcut: bool) -> LinePart { fn full_shortcut_list(help: &ModeInfo) -> LinePart { match help.mode { - InputMode::Normal => LinePart::default(), + InputMode::Normal => quicknav_full(), InputMode::Locked => locked_interface_indication(), _ => { let mut line_part = LinePart::default(); @@ -118,7 +234,7 @@ fn full_shortcut_list(help: &ModeInfo) -> LinePart { fn shortened_shortcut_list(help: &ModeInfo) -> LinePart { match help.mode { - InputMode::Normal => LinePart::default(), + InputMode::Normal => quicknav_medium(), InputMode::Locked => locked_interface_indication(), _ => { let mut line_part = LinePart::default(); @@ -137,7 +253,14 @@ fn shortened_shortcut_list(help: &ModeInfo) -> LinePart { fn best_effort_shortcut_list(help: &ModeInfo, max_len: usize) -> LinePart { match help.mode { - InputMode::Normal => LinePart::default(), + InputMode::Normal => { + let line_part = quicknav_short(); + if line_part.len <= max_len { + line_part + } else { + LinePart::default() + } + } InputMode::Locked => { let line_part = locked_interface_indication(); if line_part.len <= max_len { @@ -157,7 +280,7 @@ fn best_effort_shortcut_list(help: &ModeInfo, max_len: usize) -> LinePart { break; } line_part.len += shortcut.len; - line_part.part = format!("{}{}", line_part.part, shortcut,); + line_part.part = format!("{}{}", line_part.part, shortcut); } let select_pane_shortcut = select_pane_shortcut(help.keybinds.is_empty()); if line_part.len + select_pane_shortcut.len <= max_len { diff --git a/src/client/boundaries.rs b/src/client/boundaries.rs index e498da89..28e01a11 100644 --- a/src/client/boundaries.rs +++ b/src/client/boundaries.rs @@ -21,9 +21,9 @@ pub mod boundary_type { pub mod colors { use ansi_term::Colour::{self, Fixed}; - pub const WHITE: Colour = Fixed(255); pub const GREEN: Colour = Fixed(154); pub const GRAY: Colour = Fixed(238); + pub const ORANGE: Colour = Fixed(166); } pub type BoundaryType = &'static str; // easy way to refer to boundary_type above @@ -768,7 +768,7 @@ impl Boundaries { let color = match color.is_some() { true => match input_mode { InputMode::Normal | InputMode::Locked => Some(colors::GREEN), - _ => Some(colors::WHITE), + _ => Some(colors::ORANGE), }, false => None, }; diff --git a/src/client/tab.rs b/src/client/tab.rs index 82c2639f..3a12656d 100644 --- a/src/client/tab.rs +++ b/src/client/tab.rs @@ -1809,6 +1809,62 @@ impl Tab { } self.render(); } + pub fn focus_next_pane(&mut self) { + if !self.has_selectable_panes() { + return; + } + if self.fullscreen_is_active { + return; + } + let active_pane_id = self.get_active_pane_id().unwrap(); + let mut panes: Vec<(&PaneId, &Box)> = self.get_selectable_panes().collect(); + panes.sort_by(|(_a_id, a_pane), (_b_id, b_pane)| { + if a_pane.y() == b_pane.y() { + a_pane.x().cmp(&b_pane.x()) + } else { + a_pane.y().cmp(&b_pane.y()) + } + }); + let first_pane = panes.get(0).unwrap(); + let active_pane_position = panes + .iter() + .position(|(id, _)| *id == &active_pane_id) // TODO: better + .unwrap(); + if let Some(next_pane) = panes.get(active_pane_position + 1) { + self.active_terminal = Some(*next_pane.0); + } else { + self.active_terminal = Some(*first_pane.0); + } + self.render(); + } + pub fn focus_previous_pane(&mut self) { + if !self.has_selectable_panes() { + return; + } + if self.fullscreen_is_active { + return; + } + let active_pane_id = self.get_active_pane_id().unwrap(); + let mut panes: Vec<(&PaneId, &Box)> = self.get_selectable_panes().collect(); + panes.sort_by(|(_a_id, a_pane), (_b_id, b_pane)| { + if a_pane.y() == b_pane.y() { + a_pane.x().cmp(&b_pane.x()) + } else { + a_pane.y().cmp(&b_pane.y()) + } + }); + let last_pane = panes.last().unwrap(); + let active_pane_position = panes + .iter() + .position(|(id, _)| *id == &active_pane_id) // TODO: better + .unwrap(); + if active_pane_position == 0 { + self.active_terminal = Some(*last_pane.0); + } else { + self.active_terminal = Some(*panes.get(active_pane_position - 1).unwrap().0); + } + self.render(); + } pub fn move_focus_left(&mut self) { if !self.has_selectable_panes() { return; diff --git a/src/common/errors.rs b/src/common/errors.rs index 00284cd4..1548aec0 100644 --- a/src/common/errors.rs +++ b/src/common/errors.rs @@ -178,7 +178,9 @@ pub enum ScreenContext { ResizeRight, ResizeDown, ResizeUp, - MoveFocus, + SwitchFocus, + FocusNextPane, + FocusPreviousPane, MoveFocusLeft, MoveFocusDown, MoveFocusUp, @@ -218,7 +220,9 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::ResizeRight => ScreenContext::ResizeRight, ScreenInstruction::ResizeDown => ScreenContext::ResizeDown, ScreenInstruction::ResizeUp => ScreenContext::ResizeUp, - ScreenInstruction::MoveFocus => ScreenContext::MoveFocus, + ScreenInstruction::SwitchFocus => ScreenContext::SwitchFocus, + ScreenInstruction::FocusNextPane => ScreenContext::FocusNextPane, + ScreenInstruction::FocusPreviousPane => ScreenContext::FocusPreviousPane, ScreenInstruction::MoveFocusLeft => ScreenContext::MoveFocusLeft, ScreenInstruction::MoveFocusDown => ScreenContext::MoveFocusDown, ScreenInstruction::MoveFocusUp => ScreenContext::MoveFocusUp, diff --git a/src/common/input/actions.rs b/src/common/input/actions.rs index ac923f7d..47a5f763 100644 --- a/src/common/input/actions.rs +++ b/src/common/input/actions.rs @@ -24,8 +24,10 @@ pub enum Action { /// Resize focus pane in specified direction. Resize(Direction), /// Switch focus to next pane in specified direction. - SwitchFocus(Direction), + FocusNextPane, + FocusPreviousPane, /// Move the focus pane in specified direction. + SwitchFocus, MoveFocus(Direction), /// Scroll up in focus pane. ScrollUp, diff --git a/src/common/input/handler.rs b/src/common/input/handler.rs index 2020fc20..44065c67 100644 --- a/src/common/input/handler.rs +++ b/src/common/input/handler.rs @@ -26,6 +26,7 @@ struct InputHandler { send_pty_instructions: SenderWithContext, send_plugin_instructions: SenderWithContext, send_app_instructions: SenderWithContext, + should_exit: bool, } impl InputHandler { @@ -48,6 +49,7 @@ impl InputHandler { send_pty_instructions, send_plugin_instructions, send_app_instructions, + should_exit: false, } } @@ -60,30 +62,29 @@ impl InputHandler { self.send_app_instructions.update(err_ctx); self.send_screen_instructions.update(err_ctx); let keybinds = self.config.keybinds.clone(); - 'input_loop: loop { - //@@@ I think this should actually just iterate over stdin directly + let alt_left_bracket = vec![27, 91]; + loop { + if self.should_exit { + break; + } let stdin_buffer = self.os_input.read_from_stdin(); for key_result in stdin_buffer.events_and_raw() { match key_result { Ok((event, raw_bytes)) => match event { termion::event::Event::Key(key) => { let key = cast_termion_key(key); - // FIXME this explicit break is needed because the current test - // framework relies on it to not create dead threads that loop - // and eat up CPUs. Do not remove until the test framework has - // been revised. Sorry about this (@categorille) - let mut should_break = false; - for action in - Keybinds::key_to_actions(&key, raw_bytes, &self.mode, &keybinds) - { - should_break |= self.dispatch_action(action); - } - if should_break { - break 'input_loop; + self.handle_key(&key, raw_bytes, &keybinds); + } + termion::event::Event::Unsupported(unsupported_key) => { + // we have to do this because of a bug in termion + // this should be a key event and not an unsupported event + if unsupported_key == alt_left_bracket { + let key = Key::Alt('['); + self.handle_key(&key, raw_bytes, &keybinds); } } - termion::event::Event::Mouse(_) | termion::event::Event::Unsupported(_) => { - // Mouse and unsupported events aren't implemented yet, + termion::event::Event::Mouse(_) => { + // Mouse events aren't implemented yet, // use a NoOp untill then. } }, @@ -92,6 +93,14 @@ impl InputHandler { } } } + fn handle_key(&mut self, key: &Key, raw_bytes: Vec, keybinds: &Keybinds) { + for action in Keybinds::key_to_actions(&key, raw_bytes, &self.mode, &keybinds) { + let should_exit = self.dispatch_action(action); + if should_exit { + self.should_exit = true; + } + } + } /// Dispatches an [`Action`]. /// @@ -144,9 +153,19 @@ impl InputHandler { }; self.send_screen_instructions.send(screen_instr).unwrap(); } - Action::SwitchFocus(_) => { + Action::SwitchFocus => { self.send_screen_instructions - .send(ScreenInstruction::MoveFocus) + .send(ScreenInstruction::SwitchFocus) + .unwrap(); + } + Action::FocusNextPane => { + self.send_screen_instructions + .send(ScreenInstruction::FocusNextPane) + .unwrap(); + } + Action::FocusPreviousPane => { + self.send_screen_instructions + .send(ScreenInstruction::FocusPreviousPane) .unwrap(); } Action::MoveFocus(direction) => { diff --git a/src/common/input/keybinds.rs b/src/common/input/keybinds.rs index 6a14a182..8a064970 100644 --- a/src/common/input/keybinds.rs +++ b/src/common/input/keybinds.rs @@ -91,6 +91,14 @@ impl Keybinds { vec![Action::SwitchToMode(InputMode::Scroll)], ); defaults.insert(Key::Ctrl('q'), vec![Action::Quit]); + + defaults.insert(Key::Alt('n'), vec![Action::NewPane(None)]); + defaults.insert(Key::Alt('h'), vec![Action::MoveFocus(Direction::Left)]); + defaults.insert(Key::Alt('j'), vec![Action::MoveFocus(Direction::Down)]); + defaults.insert(Key::Alt('k'), vec![Action::MoveFocus(Direction::Up)]); + defaults.insert(Key::Alt('l'), vec![Action::MoveFocus(Direction::Right)]); + defaults.insert(Key::Alt('['), vec![Action::FocusPreviousPane]); + defaults.insert(Key::Alt(']'), vec![Action::FocusNextPane]); } InputMode::Locked => { defaults.insert( @@ -133,6 +141,14 @@ impl Keybinds { 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::Alt('n'), vec![Action::NewPane(None)]); + defaults.insert(Key::Alt('h'), vec![Action::MoveFocus(Direction::Left)]); + defaults.insert(Key::Alt('j'), vec![Action::MoveFocus(Direction::Down)]); + defaults.insert(Key::Alt('k'), vec![Action::MoveFocus(Direction::Up)]); + defaults.insert(Key::Alt('l'), vec![Action::MoveFocus(Direction::Right)]); + defaults.insert(Key::Alt('['), vec![Action::FocusPreviousPane]); + defaults.insert(Key::Alt(']'), vec![Action::FocusNextPane]); } InputMode::Pane => { defaults.insert( @@ -173,7 +189,7 @@ impl Keybinds { defaults.insert(Key::Up, vec![Action::MoveFocus(Direction::Up)]); defaults.insert(Key::Right, vec![Action::MoveFocus(Direction::Right)]); - defaults.insert(Key::Char('p'), vec![Action::SwitchFocus(Direction::Right)]); + defaults.insert(Key::Char('p'), vec![Action::SwitchFocus]); defaults.insert(Key::Char('n'), vec![Action::NewPane(None)]); defaults.insert(Key::Char('d'), vec![Action::NewPane(Some(Direction::Down))]); defaults.insert( @@ -182,6 +198,14 @@ impl Keybinds { ); defaults.insert(Key::Char('x'), vec![Action::CloseFocus]); defaults.insert(Key::Char('f'), vec![Action::ToggleFocusFullscreen]); + + defaults.insert(Key::Alt('n'), vec![Action::NewPane(None)]); + defaults.insert(Key::Alt('h'), vec![Action::MoveFocus(Direction::Left)]); + defaults.insert(Key::Alt('j'), vec![Action::MoveFocus(Direction::Down)]); + defaults.insert(Key::Alt('k'), vec![Action::MoveFocus(Direction::Up)]); + defaults.insert(Key::Alt('l'), vec![Action::MoveFocus(Direction::Right)]); + defaults.insert(Key::Alt('['), vec![Action::FocusPreviousPane]); + defaults.insert(Key::Alt(']'), vec![Action::FocusNextPane]); } InputMode::Tab => { defaults.insert( @@ -240,6 +264,13 @@ impl Keybinds { for i in '1'..='9' { defaults.insert(Key::Char(i), vec![Action::GoToTab(i.to_digit(10).unwrap())]); } + defaults.insert(Key::Alt('n'), vec![Action::NewPane(None)]); + defaults.insert(Key::Alt('h'), vec![Action::MoveFocus(Direction::Left)]); + defaults.insert(Key::Alt('j'), vec![Action::MoveFocus(Direction::Down)]); + defaults.insert(Key::Alt('k'), vec![Action::MoveFocus(Direction::Up)]); + defaults.insert(Key::Alt('l'), vec![Action::MoveFocus(Direction::Right)]); + defaults.insert(Key::Alt('['), vec![Action::FocusPreviousPane]); + defaults.insert(Key::Alt(']'), vec![Action::FocusNextPane]); } InputMode::Scroll => { defaults.insert( @@ -272,6 +303,14 @@ impl Keybinds { defaults.insert(Key::Down, vec![Action::ScrollDown]); defaults.insert(Key::Up, vec![Action::ScrollUp]); + + defaults.insert(Key::Alt('n'), vec![Action::NewPane(None)]); + defaults.insert(Key::Alt('h'), vec![Action::MoveFocus(Direction::Left)]); + defaults.insert(Key::Alt('j'), vec![Action::MoveFocus(Direction::Down)]); + defaults.insert(Key::Alt('k'), vec![Action::MoveFocus(Direction::Up)]); + defaults.insert(Key::Alt('l'), vec![Action::MoveFocus(Direction::Right)]); + defaults.insert(Key::Alt('['), vec![Action::FocusPreviousPane]); + defaults.insert(Key::Alt(']'), vec![Action::FocusNextPane]); } InputMode::RenameTab => { defaults.insert(Key::Char('\n'), vec![Action::SwitchToMode(InputMode::Tab)]); @@ -286,6 +325,14 @@ impl Keybinds { Action::SwitchToMode(InputMode::Tab), ], ); + + defaults.insert(Key::Alt('n'), vec![Action::NewPane(None)]); + defaults.insert(Key::Alt('h'), vec![Action::MoveFocus(Direction::Left)]); + defaults.insert(Key::Alt('j'), vec![Action::MoveFocus(Direction::Down)]); + defaults.insert(Key::Alt('k'), vec![Action::MoveFocus(Direction::Up)]); + defaults.insert(Key::Alt('l'), vec![Action::MoveFocus(Direction::Right)]); + defaults.insert(Key::Alt('['), vec![Action::FocusPreviousPane]); + defaults.insert(Key::Alt(']'), vec![Action::FocusNextPane]); } } ModeKeybinds(defaults) diff --git a/src/common/mod.rs b/src/common/mod.rs index 8180a724..7651905c 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -326,9 +326,15 @@ pub fn start(mut os_input: Box, opts: CliArgs) { ScreenInstruction::ResizeUp => { screen.get_active_tab_mut().unwrap().resize_up(); } - ScreenInstruction::MoveFocus => { + ScreenInstruction::SwitchFocus => { screen.get_active_tab_mut().unwrap().move_focus(); } + ScreenInstruction::FocusNextPane => { + screen.get_active_tab_mut().unwrap().focus_next_pane(); + } + ScreenInstruction::FocusPreviousPane => { + screen.get_active_tab_mut().unwrap().focus_previous_pane(); + } ScreenInstruction::MoveFocusLeft => { screen.get_active_tab_mut().unwrap().move_focus_left(); } @@ -589,7 +595,7 @@ pub fn start(mut os_input: Box, opts: CliArgs) { } ApiCommand::MoveFocus => { send_screen_instructions - .send(ScreenInstruction::MoveFocus) + .send(ScreenInstruction::FocusNextPane) .unwrap(); } } diff --git a/src/common/screen.rs b/src/common/screen.rs index 121f21bd..41e934cb 100644 --- a/src/common/screen.rs +++ b/src/common/screen.rs @@ -28,7 +28,9 @@ pub enum ScreenInstruction { ResizeRight, ResizeDown, ResizeUp, - MoveFocus, + SwitchFocus, + FocusNextPane, + FocusPreviousPane, MoveFocusLeft, MoveFocusDown, MoveFocusUp, From f23118a20afc8fc289309ed71df2fbad26bae0e7 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Fri, 9 Apr 2021 15:00:18 +0200 Subject: [PATCH 38/39] fix(compatibility): reset (#261) --- src/client/panes/terminal_pane.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/client/panes/terminal_pane.rs b/src/client/panes/terminal_pane.rs index 13575475..0702863a 100644 --- a/src/client/panes/terminal_pane.rs +++ b/src/client/panes/terminal_pane.rs @@ -379,6 +379,13 @@ impl TerminalPane { self.grid.rotate_scroll_region_down(count); self.mark_for_rerender(); } + fn reset_terminal_state(&mut self) { + let rows = self.get_rows(); + let columns = self.get_columns(); + self.grid = Grid::new(rows, columns); + self.alternative_grid = None; + self.cursor_key_mode = false; + } fn add_newline(&mut self) { let pad_character = EMPTY_TERMINAL_CHARACTER; self.grid.add_canonical_line(pad_character); @@ -678,8 +685,14 @@ impl vte::Perform for TerminalPane { } fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) { - if let (b'M', None) = (byte, intermediates.get(0)) { - self.grid.move_cursor_up_with_scrolling(1); + match (byte, intermediates.get(0)) { + (b'M', None) => { + self.grid.move_cursor_up_with_scrolling(1); + } + (b'c', None) => { + self.reset_terminal_state(); + } + _ => {} } } } From 313ac9f41474222133b344c1c0d8827b4dc2fa50 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Mon, 12 Apr 2021 16:00:05 +0200 Subject: [PATCH 39/39] fix(performance): remove unnecessary allocations from pty (#264) * work * refactor(pty): removed unused code * style(comment): remove unused * style(fmt): rustfmt --- src/client/panes/plugin_pane.rs | 4 +- src/client/panes/terminal_pane.rs | 35 ++---------- src/client/tab.rs | 8 +-- src/common/errors.rs | 4 +- src/common/mod.rs | 6 +- src/common/pty_bus.rs | 92 ++----------------------------- src/common/screen.rs | 4 +- src/main.rs | 1 - 8 files changed, 23 insertions(+), 131 deletions(-) diff --git a/src/client/panes/plugin_pane.rs b/src/client/panes/plugin_pane.rs index caf49acc..49cf2aee 100644 --- a/src/client/panes/plugin_pane.rs +++ b/src/client/panes/plugin_pane.rs @@ -1,6 +1,6 @@ #![allow(clippy::clippy::if_same_then_else)] -use crate::{common::SenderWithContext, pty_bus::VteEvent, tab::Pane, wasm_vm::PluginInstruction}; +use crate::{common::SenderWithContext, pty_bus::VteBytes, tab::Pane, wasm_vm::PluginInstruction}; use std::{sync::mpsc::channel, unimplemented}; @@ -79,7 +79,7 @@ impl Pane for PluginPane { self.position_and_size_override = Some(position_and_size_override); self.should_render = true; } - fn handle_event(&mut self, _event: VteEvent) { + fn handle_pty_bytes(&mut self, _event: VteBytes) { unimplemented!() } fn cursor_coordinates(&self) -> Option<(usize, usize)> { diff --git a/src/client/panes/terminal_pane.rs b/src/client/panes/terminal_pane.rs index 0702863a..d2473ba3 100644 --- a/src/client/panes/terminal_pane.rs +++ b/src/client/panes/terminal_pane.rs @@ -3,15 +3,14 @@ use crate::tab::Pane; use ::nix::pty::Winsize; use ::std::os::unix::io::RawFd; -use ::vte::Perform; use std::fmt::Debug; use crate::panes::grid::Grid; use crate::panes::terminal_character::{ CharacterStyles, TerminalCharacter, EMPTY_TERMINAL_CHARACTER, }; +use crate::pty_bus::VteBytes; use crate::utils::logging::debug_log_to_file; -use crate::VteEvent; #[derive(PartialEq, Eq, Ord, PartialOrd, Hash, Clone, Copy, Debug)] pub enum PaneId { @@ -86,34 +85,10 @@ impl Pane for TerminalPane { self.position_and_size_override = Some(position_and_size_override); self.reflow_lines(); } - fn handle_event(&mut self, event: VteEvent) { - match event { - VteEvent::Print(c) => { - self.print(c); - self.mark_for_rerender(); - } - VteEvent::Execute(byte) => { - self.execute(byte); - } - VteEvent::Hook(params, intermediates, ignore, c) => { - self.hook(¶ms, &intermediates, ignore, c); - } - VteEvent::Put(byte) => { - self.put(byte); - } - VteEvent::Unhook => { - self.unhook(); - } - VteEvent::OscDispatch(params, bell_terminated) => { - let params: Vec<&[u8]> = params.iter().map(|p| &p[..]).collect(); - self.osc_dispatch(¶ms[..], bell_terminated); - } - VteEvent::CsiDispatch(params, intermediates, ignore, c) => { - self.csi_dispatch(¶ms, &intermediates, ignore, c); - } - VteEvent::EscDispatch(intermediates, ignore, byte) => { - self.esc_dispatch(&intermediates, ignore, byte); - } + fn handle_pty_bytes(&mut self, bytes: VteBytes) { + let mut vte_parser = vte::Parser::new(); + for byte in bytes.iter() { + vte_parser.advance(self, *byte); } } fn cursor_coordinates(&self) -> Option<(usize, usize)> { diff --git a/src/client/tab.rs b/src/client/tab.rs index 3a12656d..9cafdcec 100644 --- a/src/client/tab.rs +++ b/src/client/tab.rs @@ -7,7 +7,7 @@ use crate::common::{input::handler::parse_keys, AppInstruction, SenderWithContex use crate::layout::Layout; use crate::os_input_output::OsApi; use crate::panes::{PaneId, PositionAndSize, TerminalPane}; -use crate::pty_bus::{PtyInstruction, VteEvent}; +use crate::pty_bus::{PtyInstruction, VteBytes}; use crate::utils::shared::adjust_to_size; use crate::wasm_vm::PluginInstruction; use crate::{boundaries::Boundaries, panes::PluginPane}; @@ -91,7 +91,7 @@ pub trait Pane { fn reset_size_and_position_override(&mut self); fn change_pos_and_size(&mut self, position_and_size: &PositionAndSize); fn override_size_and_position(&mut self, x: usize, y: usize, size: &PositionAndSize); - fn handle_event(&mut self, event: VteEvent); + fn handle_pty_bytes(&mut self, bytes: VteBytes); fn cursor_coordinates(&self) -> Option<(usize, usize)>; fn adjust_input_to_terminal(&self, input_bytes: Vec) -> Vec; @@ -564,14 +564,14 @@ impl Tab { pub fn has_terminal_pid(&self, pid: RawFd) -> bool { self.panes.contains_key(&PaneId::Terminal(pid)) } - pub fn handle_pty_event(&mut self, pid: RawFd, event: VteEvent) { + pub fn handle_pty_bytes(&mut self, pid: RawFd, bytes: VteBytes) { // if we don't have the terminal in self.terminals it's probably because // of a race condition where the terminal was created in pty_bus but has not // yet been created in Screen. These events are currently not buffered, so // if you're debugging seemingly randomly missing stdout data, this is // the reason if let Some(terminal_output) = self.panes.get_mut(&PaneId::Terminal(pid)) { - terminal_output.handle_event(event); + terminal_output.handle_pty_bytes(bytes); } } pub fn write_to_active_terminal(&mut self, input_bytes: Vec) { diff --git a/src/common/errors.rs b/src/common/errors.rs index 1548aec0..9801428c 100644 --- a/src/common/errors.rs +++ b/src/common/errors.rs @@ -168,7 +168,7 @@ impl Display for ContextType { /// Stack call representations corresponding to the different types of [`ScreenInstruction`]s. #[derive(Debug, Clone, Copy, PartialEq)] pub enum ScreenContext { - HandlePtyEvent, + HandlePtyBytes, Render, NewPane, HorizontalSplit, @@ -210,7 +210,7 @@ pub enum ScreenContext { impl From<&ScreenInstruction> for ScreenContext { fn from(screen_instruction: &ScreenInstruction) -> Self { match *screen_instruction { - ScreenInstruction::Pty(..) => ScreenContext::HandlePtyEvent, + ScreenInstruction::PtyBytes(..) => ScreenContext::HandlePtyBytes, ScreenInstruction::Render => ScreenContext::Render, ScreenInstruction::NewPane(_) => ScreenContext::NewPane, ScreenInstruction::HorizontalSplit(_) => ScreenContext::HorizontalSplit, diff --git a/src/common/mod.rs b/src/common/mod.rs index 7651905c..2466aecf 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -275,19 +275,19 @@ pub fn start(mut os_input: Box, opts: CliArgs) { screen.send_app_instructions.update(err_ctx); screen.send_pty_instructions.update(err_ctx); match event { - ScreenInstruction::Pty(pid, vte_event) => { + ScreenInstruction::PtyBytes(pid, vte_bytes) => { let active_tab = screen.get_active_tab_mut().unwrap(); if active_tab.has_terminal_pid(pid) { // it's most likely that this event is directed at the active tab // look there first - active_tab.handle_pty_event(pid, vte_event); + active_tab.handle_pty_bytes(pid, vte_bytes); } else { // if this event wasn't directed at the active tab, start looking // in other tabs let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_terminal_pid(pid) { - tab.handle_pty_event(pid, vte_event); + tab.handle_pty_bytes(pid, vte_bytes); break; } } diff --git a/src/common/pty_bus.rs b/src/common/pty_bus.rs index 7e854257..2cab8ae8 100644 --- a/src/common/pty_bus.rs +++ b/src/common/pty_bus.rs @@ -6,7 +6,6 @@ use ::std::os::unix::io::RawFd; use ::std::pin::*; use ::std::sync::mpsc::Receiver; use ::std::time::{Duration, Instant}; -use ::vte; use std::path::PathBuf; use super::{ScreenInstruction, SenderWithContext, OPENCALLS}; @@ -64,86 +63,7 @@ impl Stream for ReadFromPid { } } -#[derive(Debug, Clone)] -pub enum VteEvent { - // TODO: try not to allocate Vecs - Print(char), - Execute(u8), // byte - Hook(Vec, Vec, bool, char), // params, intermediates, ignore, char - Put(u8), // byte - Unhook, - OscDispatch(Vec>, bool), // params, bell_terminated - CsiDispatch(Vec, Vec, bool, char), // params, intermediates, ignore, char - EscDispatch(Vec, bool, u8), // intermediates, ignore, byte -} - -struct VteEventSender { - id: RawFd, - sender: SenderWithContext, -} - -impl VteEventSender { - pub fn new(id: RawFd, sender: SenderWithContext) -> Self { - VteEventSender { id, sender } - } -} - -impl vte::Perform for VteEventSender { - fn print(&mut self, c: char) { - let _ = self - .sender - .send(ScreenInstruction::Pty(self.id, VteEvent::Print(c))); - } - fn execute(&mut self, byte: u8) { - let _ = self - .sender - .send(ScreenInstruction::Pty(self.id, VteEvent::Execute(byte))); - } - - fn hook(&mut self, params: &[i64], intermediates: &[u8], ignore: bool, c: char) { - let params = params.iter().copied().collect(); - let intermediates = intermediates.iter().copied().collect(); - let instruction = - ScreenInstruction::Pty(self.id, VteEvent::Hook(params, intermediates, ignore, c)); - let _ = self.sender.send(instruction); - } - - fn put(&mut self, byte: u8) { - let _ = self - .sender - .send(ScreenInstruction::Pty(self.id, VteEvent::Put(byte))); - } - - fn unhook(&mut self) { - let _ = self - .sender - .send(ScreenInstruction::Pty(self.id, VteEvent::Unhook)); - } - - fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) { - let params = params.iter().map(|p| p.to_vec()).collect(); - let instruction = - ScreenInstruction::Pty(self.id, VteEvent::OscDispatch(params, bell_terminated)); - let _ = self.sender.send(instruction); - } - - fn csi_dispatch(&mut self, params: &[i64], intermediates: &[u8], ignore: bool, c: char) { - let params = params.iter().copied().collect(); - let intermediates = intermediates.iter().copied().collect(); - let instruction = ScreenInstruction::Pty( - self.id, - VteEvent::CsiDispatch(params, intermediates, ignore, c), - ); - let _ = self.sender.send(instruction); - } - - fn esc_dispatch(&mut self, intermediates: &[u8], ignore: bool, byte: u8) { - let intermediates = intermediates.iter().copied().collect(); - let instruction = - ScreenInstruction::Pty(self.id, VteEvent::EscDispatch(intermediates, ignore, byte)); - let _ = self.sender.send(instruction); - } -} +pub type VteBytes = Vec; /// Instructions related to PTYs (pseudoterminals). #[derive(Clone, Debug)] @@ -178,8 +98,6 @@ fn stream_terminal_bytes( async move { err_ctx.add_call(ContextType::AsyncTask); send_screen_instructions.update(err_ctx); - let mut vte_parser = vte::Parser::new(); - let mut vte_event_sender = VteEventSender::new(pid, send_screen_instructions.clone()); let mut terminal_bytes = ReadFromPid::new(&pid, os_input); let mut last_byte_receive_time: Option = None; @@ -188,13 +106,13 @@ fn stream_terminal_bytes( while let Some(bytes) = terminal_bytes.next().await { let bytes_is_empty = bytes.is_empty(); - for byte in bytes { - if debug { - debug_to_file(byte, pid).unwrap(); + if debug { + for byte in bytes.iter() { + debug_to_file(*byte, pid).unwrap(); } - vte_parser.advance(&mut vte_event_sender, byte); } if !bytes_is_empty { + let _ = send_screen_instructions.send(ScreenInstruction::PtyBytes(pid, bytes)); // for UX reasons, if we got something on the wire, we only send the render notice if: // 1. there aren't any more bytes on the wire afterwards // 2. a certain period (currently 30ms) has elapsed since the last render diff --git a/src/common/screen.rs b/src/common/screen.rs index 41e934cb..1e37ca4f 100644 --- a/src/common/screen.rs +++ b/src/common/screen.rs @@ -8,7 +8,7 @@ use std::sync::mpsc::Receiver; use super::{AppInstruction, SenderWithContext}; use crate::os_input_output::OsApi; use crate::panes::PositionAndSize; -use crate::pty_bus::{PtyInstruction, VteEvent}; +use crate::pty_bus::{PtyInstruction, VteBytes}; use crate::tab::Tab; use crate::{errors::ErrorContext, wasm_vm::PluginInstruction}; use crate::{layout::Layout, panes::PaneId}; @@ -18,7 +18,7 @@ use zellij_tile::data::{Event, ModeInfo, TabInfo}; /// Instructions that can be sent to the [`Screen`]. #[derive(Debug, Clone)] pub enum ScreenInstruction { - Pty(RawFd, VteEvent), + PtyBytes(RawFd, VteBytes), Render, NewPane(PaneId), HorizontalSplit(PaneId), diff --git a/src/main.rs b/src/main.rs index 716acb91..7d8ca6d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,7 +21,6 @@ use structopt::StructOpt; use crate::cli::CliArgs; use crate::command_is_executing::CommandIsExecuting; use crate::os_input_output::get_os_input; -use crate::pty_bus::VteEvent; use crate::utils::{ consts::{ZELLIJ_IPC_PIPE, ZELLIJ_TMP_DIR, ZELLIJ_TMP_LOG_DIR}, logging::*,