diff options
| author | a-kenji <aks.kenji@protonmail.com> | 2021-05-12 20:03:04 +0200 |
|---|---|---|
| committer | a-kenji <aks.kenji@protonmail.com> | 2021-05-12 20:03:04 +0200 |
| commit | 4134c952f65aa2951404991bcf525462e3482fe9 (patch) | |
| tree | cf809552c3f676c5dee56873052a5a584e377623 /src/common | |
| parent | Add Complile Time System Directories (diff) | |
| parent | Merge pull request #497 from a-kenji/fix-default-keybind (diff) | |
| download | sl0ppij-4134c952f65aa2951404991bcf525462e3482fe9.tar.gz sl0ppij-4134c952f65aa2951404991bcf525462e3482fe9.tar.xz sl0ppij-4134c952f65aa2951404991bcf525462e3482fe9.zip | |
Merge branch 'main' of https://github.com/zellij-org/zellij into set-data-dir
Diffstat (limited to 'src/common')
| -rw-r--r-- | src/common/errors.rs | 22 | ||||
| -rw-r--r-- | src/common/input/actions.rs | 7 | ||||
| -rw-r--r-- | src/common/input/config.rs | 21 | ||||
| -rw-r--r-- | src/common/input/handler.rs | 42 | ||||
| -rw-r--r-- | src/common/input/keybinds.rs | 138 | ||||
| -rw-r--r-- | src/common/input/mod.rs | 1 | ||||
| -rw-r--r-- | src/common/input/options.rs | 45 | ||||
| -rw-r--r-- | src/common/input/unit/keybinds_test.rs | 653 | ||||
| -rw-r--r-- | src/common/mod.rs | 65 | ||||
| -rw-r--r-- | src/common/os_input_output.rs | 6 | ||||
| -rw-r--r-- | src/common/pty.rs (renamed from src/common/pty_bus.rs) | 165 | ||||
| -rw-r--r-- | src/common/screen.rs | 341 | ||||
| -rw-r--r-- | src/common/setup.rs | 6 | ||||
| -rw-r--r-- | src/common/thread_bus.rs | 142 | ||||
| -rw-r--r-- | src/common/utils/consts.rs | 2 | ||||
| -rw-r--r-- | src/common/utils/logging.rs | 18 | ||||
| -rw-r--r-- | src/common/wasm_vm.rs | 138 |
17 files changed, 1565 insertions, 247 deletions
diff --git a/src/common/errors.rs b/src/common/errors.rs index 411e0225..c2caf19d 100644 --- a/src/common/errors.rs +++ b/src/common/errors.rs @@ -1,20 +1,22 @@ //! Error context system based on a thread-local representation of the call stack, itself based on //! the instructions that are sent between threads. -use super::{ServerInstruction, ASYNCOPENCALLS, OPENCALLS}; -use crate::client::ClientInstruction; -use crate::pty_bus::PtyInstruction; -use crate::screen::ScreenInstruction; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Error, Formatter}; +use crate::client::ClientInstruction; +use crate::common::thread_bus::{ASYNCOPENCALLS, OPENCALLS}; +use crate::pty::PtyInstruction; +use crate::screen::ScreenInstruction; +use crate::server::ServerInstruction; + /// The maximum amount of calls an [`ErrorContext`] will keep track /// of in its stack representation. This is a per-thread maximum. const MAX_THREAD_CALL_STACK: usize = 6; #[cfg(not(test))] -use super::SenderWithContext; +use super::thread_bus::SenderWithContext; #[cfg(not(test))] use std::panic::PanicInfo; /// Custom panic handler/hook. Prints the [`ErrorContext`]. @@ -190,9 +192,11 @@ pub enum ScreenContext { FocusNextPane, FocusPreviousPane, MoveFocusLeft, + MoveFocusLeftOrPreviousTab, MoveFocusDown, MoveFocusUp, MoveFocusRight, + MoveFocusRightOrNextTab, Exit, ScrollUp, ScrollDown, @@ -200,7 +204,7 @@ pub enum ScreenContext { PageScrollDown, ClearScroll, CloseFocusedPane, - ToggleActiveSyncPanes, + ToggleActiveSyncTab, ToggleActiveTerminalFullscreen, SetSelectable, SetInvisibleBorders, @@ -235,9 +239,13 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::FocusNextPane => ScreenContext::FocusNextPane, ScreenInstruction::FocusPreviousPane => ScreenContext::FocusPreviousPane, ScreenInstruction::MoveFocusLeft => ScreenContext::MoveFocusLeft, + ScreenInstruction::MoveFocusLeftOrPreviousTab => { + ScreenContext::MoveFocusLeftOrPreviousTab + } ScreenInstruction::MoveFocusDown => ScreenContext::MoveFocusDown, ScreenInstruction::MoveFocusUp => ScreenContext::MoveFocusUp, ScreenInstruction::MoveFocusRight => ScreenContext::MoveFocusRight, + ScreenInstruction::MoveFocusRightOrNextTab => ScreenContext::MoveFocusRightOrNextTab, ScreenInstruction::Exit => ScreenContext::Exit, ScreenInstruction::ScrollUp => ScreenContext::ScrollUp, ScreenInstruction::ScrollDown => ScreenContext::ScrollDown, @@ -261,7 +269,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::UpdateTabName(_) => ScreenContext::UpdateTabName, ScreenInstruction::TerminalResize(_) => ScreenContext::TerminalResize, ScreenInstruction::ChangeMode(_) => ScreenContext::ChangeMode, - ScreenInstruction::ToggleActiveSyncPanes => ScreenContext::ToggleActiveSyncPanes, + ScreenInstruction::ToggleActiveSyncTab => ScreenContext::ToggleActiveSyncTab, } } } diff --git a/src/common/input/actions.rs b/src/common/input/actions.rs index 46a8e1d6..b806aa3f 100644 --- a/src/common/input/actions.rs +++ b/src/common/input/actions.rs @@ -29,6 +29,9 @@ pub enum Action { /// Move the focus pane in specified direction. SwitchFocus, MoveFocus(Direction), + /// Tries to move the focus pane in specified direction. + /// If there is no pane in the direction, move to previous/next Tab. + MoveFocusOrTab(Direction), /// Scroll up in focus pane. ScrollUp, /// Scroll down in focus pane. @@ -39,8 +42,8 @@ pub enum Action { PageScrollDown, /// Toggle between fullscreen focus pane and normal layout. ToggleFocusFullscreen, - /// Toggle between sending text commands to all panes and normal mode. - ToggleActiveSyncPanes, + /// Toggle between sending text commands to all panes on the current tab and normal mode. + ToggleActiveSyncTab, /// Open a new pane in the specified direction (relative to focus). /// If no direction is specified, will try to use the biggest available space. NewPane(Option<Direction>), diff --git a/src/common/input/config.rs b/src/common/input/config.rs index 3263b571..5f2656f3 100644 --- a/src/common/input/config.rs +++ b/src/common/input/config.rs @@ -6,10 +6,11 @@ use std::io::{self, Read}; use std::path::{Path, PathBuf}; use super::keybinds::{Keybinds, KeybindsFromYaml}; +use super::options::Options; use crate::cli::{CliArgs, ConfigCli}; use crate::common::setup; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::convert::TryFrom; const DEFAULT_CONFIG_FILE_NAME: &str = "config.yaml"; @@ -19,13 +20,16 @@ type ConfigResult = Result<Config, ConfigError>; /// Intermediate deserialization config struct #[derive(Debug, Deserialize)] pub struct ConfigFromYaml { + #[serde(flatten)] + pub options: Option<Options>, pub keybinds: Option<KeybindsFromYaml>, } /// Main configuration. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct Config { pub keybinds: Keybinds, + pub options: Options, } #[derive(Debug)] @@ -43,7 +47,8 @@ pub enum ConfigError { impl Default for Config { fn default() -> Self { let keybinds = Keybinds::default(); - Config { keybinds } + let options = Options::default(); + Config { keybinds, options } } } @@ -55,7 +60,7 @@ impl TryFrom<&CliArgs> for Config { return Config::new(&path); } - if let Some(ConfigCli::Config { clean, .. }) = opts.option { + if let Some(ConfigCli::Setup { clean, .. }) = opts.option { if clean { return Config::from_default_assets(); } @@ -84,7 +89,8 @@ impl Config { 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 }) + let options = Options::from_yaml(config_from_yaml.options); + Ok(Config { keybinds, options }) } /// Deserializes from given path. @@ -172,7 +178,10 @@ mod config_test { #[test] fn try_from_cli_args_with_option_clean() { let mut opts = CliArgs::default(); - opts.option = Some(ConfigCli::Config { clean: true }); + opts.option = Some(ConfigCli::Setup { + clean: true, + dump_config: false, + }); let result = Config::try_from(&opts); assert!(result.is_ok()); } diff --git a/src/common/input/handler.rs b/src/common/input/handler.rs index 75f9f8b1..2dbec080 100644 --- a/src/common/input/handler.rs +++ b/src/common/input/handler.rs @@ -4,14 +4,14 @@ use super::actions::Action; use super::keybinds::Keybinds; use crate::client::ClientInstruction; use crate::common::input::config::Config; -use crate::common::{SenderWithContext, OPENCALLS}; +use crate::common::thread_bus::{SenderWithContext, OPENCALLS}; use crate::errors::ContextType; use crate::os_input_output::ClientOsApi; use crate::server::ServerInstruction; use crate::CommandIsExecuting; use termion::input::{TermRead, TermReadEventsAndRaw}; -use zellij_tile::data::{InputMode, Key, ModeInfo, Palette}; +use zellij_tile::data::{InputMode, Key, ModeInfo, Palette, PluginCapabilities}; /// Handles the dispatching of [`Action`]s according to the current /// [`InputMode`], and keep tracks of the current [`InputMode`]. @@ -23,6 +23,7 @@ struct InputHandler { command_is_executing: CommandIsExecuting, send_client_instructions: SenderWithContext<ClientInstruction>, should_exit: bool, + pasting: bool, } impl InputHandler { @@ -40,6 +41,7 @@ impl InputHandler { command_is_executing, send_client_instructions, should_exit: false, + pasting: false, } } @@ -49,6 +51,8 @@ impl InputHandler { let mut err_ctx = OPENCALLS.with(|ctx| *ctx.borrow()); err_ctx.add_call(ContextType::StdinHandler); let alt_left_bracket = vec![27, 91]; + let bracketed_paste_start = vec![27, 91, 50, 48, 48, 126]; // \u{1b}[200~ + let bracketed_paste_end = vec![27, 91, 50, 48, 49, 126]; // \u{1b}[201 loop { if self.should_exit { break; @@ -67,6 +71,10 @@ impl InputHandler { if unsupported_key == alt_left_bracket { let key = Key::Alt('['); self.handle_key(&key, raw_bytes); + } else if unsupported_key == bracketed_paste_start { + self.pasting = true; + } else if unsupported_key == bracketed_paste_end { + self.pasting = false; } } termion::event::Event::Mouse(_) => { @@ -81,10 +89,20 @@ impl InputHandler { } fn handle_key(&mut self, key: &Key, raw_bytes: Vec<u8>) { let keybinds = &self.config.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; + if self.pasting { + // we're inside a paste block, if we're in a mode that allows sending text to the + // terminal, send all text directly without interpreting it + // otherwise, just discard the input + if self.mode == InputMode::Normal || self.mode == InputMode::Locked { + let action = Action::Write(raw_bytes); + self.dispatch_action(action); + } + } else { + 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; + } } } } @@ -119,7 +137,8 @@ impl InputHandler { | Action::GoToNextTab | Action::GoToPreviousTab | Action::CloseTab - | Action::GoToTab(_) => { + | Action::GoToTab(_) + | Action::MoveFocusOrTab(_) => { self.command_is_executing.blocking_input_thread(); self.os_input .send_to_server(ServerInstruction::Action(action)); @@ -146,7 +165,11 @@ impl InputHandler { /// Creates a [`Help`] struct indicating the current [`InputMode`] and its keybinds /// (as pairs of [`String`]s). // TODO this should probably be automatically generated in some way -pub fn get_mode_info(mode: InputMode, palette: Palette) -> ModeInfo { +pub fn get_mode_info( + mode: InputMode, + palette: Palette, + capabilities: PluginCapabilities, +) -> ModeInfo { let mut keybinds: Vec<(String, String)> = vec![]; match mode { InputMode::Normal | InputMode::Locked => {} @@ -160,7 +183,6 @@ pub fn get_mode_info(mode: InputMode, palette: Palette) -> ModeInfo { keybinds.push(("d".to_string(), "Down split".to_string())); keybinds.push(("r".to_string(), "Right split".to_string())); keybinds.push(("x".to_string(), "Close".to_string())); - keybinds.push(("s".to_string(), "Sync".to_string())); keybinds.push(("f".to_string(), "Fullscreen".to_string())); } InputMode::Tab => { @@ -168,6 +190,7 @@ pub fn get_mode_info(mode: InputMode, palette: Palette) -> ModeInfo { keybinds.push(("n".to_string(), "New".to_string())); keybinds.push(("x".to_string(), "Close".to_string())); keybinds.push(("r".to_string(), "Rename".to_string())); + keybinds.push(("s".to_string(), "Sync".to_string())); } InputMode::Scroll => { keybinds.push(("↓↑".to_string(), "Scroll".to_string())); @@ -181,6 +204,7 @@ pub fn get_mode_info(mode: InputMode, palette: Palette) -> ModeInfo { mode, keybinds, palette, + capabilities, } } diff --git a/src/common/input/keybinds.rs b/src/common/input/keybinds.rs index 9842fbe2..8bef47fd 100644 --- a/src/common/input/keybinds.rs +++ b/src/common/input/keybinds.rs @@ -4,14 +4,14 @@ use std::collections::HashMap; use super::actions::Action; use super::config; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; use zellij_tile::data::*; /// Used in the config struct -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct Keybinds(HashMap<InputMode, ModeKeybinds>); -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct ModeKeybinds(HashMap<Key, Vec<Action>>); /// Intermediate struct used for deserialisation @@ -29,8 +29,14 @@ pub struct KeybindsFromYaml { #[serde(untagged)] enum KeyActionUnbind { KeyAction(KeyActionFromYaml), - // TODO: use the enum - //Unbind(UnbindFromYaml), + Unbind(UnbindFromYaml), +} + +/// Intermediate struct used for deserialisation +#[derive(Clone, Debug, PartialEq, Deserialize)] +struct KeyActionUnbindFromYaml { + keybinds: Vec<KeyActionFromYaml>, + unbind: Unbind, } /// Intermediate struct used for deserialisation @@ -41,7 +47,7 @@ pub struct KeyActionFromYaml { } /// Intermediate struct used for deserialisation -#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize)] +#[derive(Clone, Debug, PartialEq, Deserialize)] struct UnbindFromYaml { unbind: Unbind, } @@ -51,16 +57,19 @@ struct UnbindFromYaml { #[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize)] #[serde(untagged)] enum Unbind { + // This is the correct order, don't rearrange! + // Suspected Bug in the untagged macro. + // 1. Keys + Keys(Vec<Key>), + // 2. All All(bool), - // TODO@a-kenji: use the enum - //Keys(Vec<Key>), } impl Default for Keybinds { + // Use once per codepath + // TODO investigate why fn default() -> Keybinds { - config::Config::from_default_assets() - .expect("Keybinds from default assets Error") - .keybinds + Self::from_default_assets() } } @@ -69,11 +78,18 @@ impl Keybinds { Keybinds(HashMap::<InputMode, ModeKeybinds>::new()) } + fn from_default_assets() -> Keybinds { + config::Config::from_default_assets() + .expect("Keybinds from default assets Error!") + .keybinds + } + + /// Entrypoint from the config module pub fn get_default_keybinds_with_config(from_yaml: Option<KeybindsFromYaml>) -> Keybinds { let default_keybinds = match from_yaml.clone() { Some(keybinds) => match keybinds.unbind { Unbind::All(true) => Keybinds::new(), - Unbind::All(false) => Keybinds::default(), + Unbind::All(false) | Unbind::Keys(_) => Keybinds::unbind(keybinds), }, None => Keybinds::default(), }; @@ -85,6 +101,71 @@ impl Keybinds { } } + /// Unbinds the default keybindings in relation to their mode + fn unbind(from_yaml: KeybindsFromYaml) -> Keybinds { + let mut keybind_config = Self::new(); + let mut unbind_config: HashMap<InputMode, Unbind> = HashMap::new(); + let keybinds_from_yaml = from_yaml.keybinds; + + for mode in InputMode::iter() { + if let Some(keybinds) = keybinds_from_yaml.get(&mode) { + for keybind in keybinds.iter() { + match keybind { + KeyActionUnbind::Unbind(unbind) => { + unbind_config.insert(mode, unbind.unbind.clone()); + } + KeyActionUnbind::KeyAction(key_action_from_yaml) => { + keybind_config + .0 + .insert(mode, ModeKeybinds::from(key_action_from_yaml.clone())); + } + } + } + } + } + + let mut default = Self::default().unbind_mode(unbind_config); + + // Toplevel Unbinds + if let Unbind::Keys(_) = from_yaml.unbind { + let mut unbind_config: HashMap<InputMode, Unbind> = HashMap::new(); + for mode in InputMode::iter() { + unbind_config.insert(mode, from_yaml.unbind.clone()); + } + default = default.unbind_mode(unbind_config); + }; + + default.merge_keybinds(keybind_config) + } + + /// Unbind [`Key`] bindings respective to their mode + fn unbind_mode(&self, unbind: HashMap<InputMode, Unbind>) -> Keybinds { + let mut keybinds = Keybinds::new(); + + for mode in InputMode::iter() { + if let Some(unbind) = unbind.get(&mode) { + match unbind { + Unbind::All(true) => {} + Unbind::Keys(keys) => { + if let Some(defaults) = self.0.get(&mode) { + keybinds + .0 + .insert(mode, defaults.clone().unbind_keys(keys.to_vec())); + } + } + Unbind::All(false) => { + if let Some(defaults) = self.0.get(&mode) { + keybinds.0.insert(mode, defaults.clone()); + } + } + } + } else if let Some(defaults) = self.0.get(&mode) { + keybinds.0.insert(mode, defaults.clone()); + } + } + keybinds + } + /// Merges two Keybinds structs into one Keybinds struct /// `other` overrides the ModeKeybinds of `self`. fn merge_keybinds(&self, other: Keybinds) -> Keybinds { @@ -142,6 +223,15 @@ impl ModeKeybinds { merged.0.extend(other.0); merged } + + /// Remove [`Key`]'s from [`ModeKeybinds`] + fn unbind_keys(self, unbind: Vec<Key>) -> Self { + let mut keymap = self; + for key in unbind { + keymap.0.remove(&key); + } + keymap + } } impl From<KeybindsFromYaml> for Keybinds { @@ -161,27 +251,41 @@ impl From<KeybindsFromYaml> for Keybinds { } } -/// For each `Key` assigned to `Action`s, -/// map the `Action`s to the key +/// For each [`Key`] assigned to [`Action`]s, +/// map the [`Action`]s to the [`Key`] impl From<KeyActionFromYaml> for ModeKeybinds { fn from(key_action: KeyActionFromYaml) -> ModeKeybinds { - let keys = key_action.key; let actions = key_action.action; ModeKeybinds( - keys.into_iter() + key_action + .key + .into_iter() .map(|k| (k, actions.clone())) .collect::<HashMap<Key, Vec<Action>>>(), ) } } -// Currently an enum for future use impl From<KeyActionUnbind> for ModeKeybinds { fn from(key_action_unbind: KeyActionUnbind) -> ModeKeybinds { match key_action_unbind { KeyActionUnbind::KeyAction(key_action) => ModeKeybinds::from(key_action), + KeyActionUnbind::Unbind(_) => ModeKeybinds::new(), + } + } +} + +impl From<Vec<KeyActionFromYaml>> for ModeKeybinds { + fn from(key_action_from_yaml: Vec<KeyActionFromYaml>) -> ModeKeybinds { + let mut mode_keybinds = ModeKeybinds::new(); + + for keybind in key_action_from_yaml { + for key in keybind.key { + mode_keybinds.0.insert(key, keybind.action.clone()); + } } + mode_keybinds } } diff --git a/src/common/input/mod.rs b/src/common/input/mod.rs index 6589cac2..f30ccf23 100644 --- a/src/common/input/mod.rs +++ b/src/common/input/mod.rs @@ -4,3 +4,4 @@ pub mod actions; pub mod config; pub mod handler; pub mod keybinds; +pub mod options; diff --git a/src/common/input/options.rs b/src/common/input/options.rs new file mode 100644 index 00000000..33625b65 --- /dev/null +++ b/src/common/input/options.rs @@ -0,0 +1,45 @@ +//! Handles cli and configuration options +use crate::cli::ConfigCli; +use serde::{Deserialize, Serialize}; +use structopt::StructOpt; + +#[derive(Clone, Default, Debug, PartialEq, Deserialize, Serialize, StructOpt)] +/// Options that can be set either through the config file, +/// or cli flags +pub struct Options { + /// Allow plugins to use a more simplified layout + /// that is compatible with more fonts + #[structopt(long)] + pub simplified_ui: bool, +} + +impl Options { + pub fn from_yaml(from_yaml: Option<Options>) -> Options { + if let Some(opts) = from_yaml { + opts + } else { + Options::default() + } + } + + /// Merges two [`Options`] structs, a `Some` in `other` + /// will supercede a `Some` in `self` + // TODO: Maybe a good candidate for a macro? + pub fn merge(&self, other: Options) -> Options { + let simplified_ui = if other.simplified_ui { + true + } else { + self.simplified_ui + }; + + Options { simplified_ui } + } + + pub fn from_cli(&self, other: Option<ConfigCli>) -> Options { + if let Some(ConfigCli::Options(options)) = other { + Options::merge(&self, options) + } else { + self.to_owned() + } + } +} diff --git a/src/common/input/unit/keybinds_test.rs b/src/common/input/unit/keybinds_test.rs index e0d78b62..4767fc15 100644 --- a/src/common/input/unit/keybinds_test.rs +++ b/src/common/input/unit/keybinds_test.rs @@ -136,6 +136,7 @@ fn toplevel_unbind_unbinds_all() { assert_eq!(keybinds_from_yaml, Keybinds::new()); } +#[test] fn no_unbind_unbinds_none() { let from_yaml = KeybindsFromYaml { unbind: Unbind::All(false), @@ -144,5 +145,657 @@ fn no_unbind_unbinds_none() { let keybinds_from_yaml = Keybinds::get_default_keybinds_with_config(Some(from_yaml)); + assert_eq!(keybinds_from_yaml, Keybinds::default()); +} + +#[test] +fn last_keybind_is_taken() { + let actions_1 = vec![Action::NoOp, Action::NewTab]; + let keyaction_1 = KeyActionFromYaml { + action: actions_1.clone(), + key: vec![Key::F(1), Key::Backspace, Key::Char('t')], + }; + let actions_2 = vec![Action::GoToTab(1)]; + let keyaction_2 = KeyActionFromYaml { + action: actions_2.clone(), + key: vec![Key::F(1), Key::Backspace, Key::Char('t')], + }; + + let mut expected = ModeKeybinds::new(); + expected.0.insert(Key::F(1), actions_2.clone()); + expected.0.insert(Key::Backspace, actions_2.clone()); + expected.0.insert(Key::Char('t'), actions_2); + + assert_eq!(expected, ModeKeybinds::from(vec![keyaction_1, keyaction_2])); +} + +#[test] +fn last_keybind_overwrites() { + let actions_1 = vec![Action::NoOp, Action::NewTab]; + let keyaction_1 = KeyActionFromYaml { + action: actions_1.clone(), + key: vec![Key::F(1), Key::Backspace, Key::Char('t')], + }; + let actions_2 = vec![Action::GoToTab(1)]; + let keyaction_2 = KeyActionFromYaml { + action: actions_2.clone(), + key: vec![Key::F(1), Key::Char('t')], + }; + + let mut expected = ModeKeybinds::new(); + expected.0.insert(Key::F(1), actions_2.clone()); + expected.0.insert(Key::Backspace, actions_1.clone()); + expected.0.insert(Key::Char('t'), actions_2); + + assert_eq!(expected, ModeKeybinds::from(vec![keyaction_1, keyaction_2])); +} + +#[test] +fn unbind_single_mode() { + let unbind = Unbind::All(true); + let unbind_from_yaml = UnbindFromYaml { unbind }; + let key_action_unbinds = vec![KeyActionUnbind::Unbind(unbind_from_yaml)]; + + let mut keys = HashMap::<InputMode, Vec<KeyActionUnbind>>::new(); + keys.insert(InputMode::Normal, key_action_unbinds); + + let keybinds_from_yaml = KeybindsFromYaml { + keybinds: keys, + unbind: Unbind::All(false), + }; + + let keybinds = Keybinds::unbind(keybinds_from_yaml); + let result = keybinds.0.get(&InputMode::Normal); + assert!(result.is_none()); +} + +#[test] +fn unbind_multiple_modes() { + let unbind = Unbind::All(true); + let unbind_from_yaml = UnbindFromYaml { unbind }; + let key_action_unbinds = vec![KeyActionUnbind::Unbind(unbind_from_yaml)]; + + let mut keys = HashMap::<InputMode, Vec<KeyActionUnbind>>::new(); + keys.insert(InputMode::Normal, key_action_unbinds.clone()); + keys.insert(InputMode::Pane, key_action_unbinds); + + let keybinds_from_yaml = KeybindsFromYaml { + keybinds: keys, + unbind: Unbind::All(false), + }; + + let keybinds = Keybinds::unbind(keybinds_from_yaml); + let normal = keybinds.0.get(&InputMode::Normal); + let pane = keybinds.0.get(&InputMode::Pane); + assert!(normal.is_none()); + assert!(pane.is_none()); +} + +#[test] +fn unbind_single_keybind_single_mode() { + let unbind = Unbind::Keys(vec![Key::Alt('n')]); + let unbind_from_yaml = UnbindFromYaml { unbind }; + let key_action_unbinds = vec![KeyActionUnbind::Unbind(unbind_from_yaml)]; + + let mut keys = HashMap::<InputMode, Vec<KeyActionUnbind>>::new(); + keys.insert(InputMode::Normal, key_action_unbinds); + + let keybinds_from_yaml = KeybindsFromYaml { + keybinds: keys, + unbind: Unbind::All(false), + }; + + let keybinds = Keybinds::unbind(keybinds_from_yaml); + let mode_keybinds = keybinds.0.get(&InputMode::Normal); + let result = mode_keybinds + .expect("Mode shouldn't be empty") + .0 + .get(&Key::Alt('n')); + assert!(result.is_none()); +} + +#[test] +fn unbind_single_keybind_multiple_modes() { + let unbind_n = Unbind::Keys(vec![Key::Alt('n')]); + let unbind_h = Unbind::Keys(vec![Key::Alt('h')]); + let unbind_from_yaml_n = UnbindFromYaml { unbind: unbind_n }; + let unbind_from_yaml_h = UnbindFromYaml { unbind: unbind_h }; + let key_action_unbinds_n = vec![KeyActionUnbind::Unbind(unbind_from_yaml_n)]; + let key_action_unbinds_h = vec![KeyActionUnbind::Unbind(unbind_from_yaml_h)]; + + let mut keys = HashMap::<InputMode, Vec<KeyActionUnbind>>::new(); + keys.insert(InputMode::Normal, key_action_unbinds_n); + keys.insert(InputMode::Pane, key_action_unbinds_h); + + let keybinds_from_yaml = KeybindsFromYaml { + keybinds: keys, + unbind: Unbind::All(false), + }; + + let keybinds = Keybinds::unbind(keybinds_from_yaml); + let normal = keybinds.0.get(&InputMode::Normal); + let pane = keybinds.0.get(&InputMode::Pane); + let result_normal = normal + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('n')); + let result_pane = pane.expect("Mode shouldn't be empty").0.get(&Key::Alt('h')); + assert!(result_normal.is_none()); + assert!(result_pane.is_none()); +} + +#[test] +fn unbind_multiple_keybinds_single_mode() { + let unbind = Unbind::Keys(vec![Key::Alt('n'), Key::Ctrl('p')]); + let unbind_from_yaml = UnbindFromYaml { unbind }; + let key_action_unbinds = vec![KeyActionUnbind::Unbind(unbind_from_yaml)]; + + let mut keys = HashMap::<InputMode, Vec<KeyActionUnbind>>::new(); + keys.insert(InputMode::Normal, key_action_unbinds); + + let keybinds_from_yaml = KeybindsFromYaml { + keybinds: keys, + unbind: Unbind::All(false), + }; + + let keybinds = Keybinds::unbind(keybinds_from_yaml); + let mode_keybinds = keybinds.0.get(&InputMode::Normal); + let result_n = mode_keybinds + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('n')); + let result_p = mode_keybinds + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Ctrl('p')); + assert!(result_n.is_none()); + assert!(result_p.is_none()); +} + +#[test] +fn unbind_multiple_keybinds_multiple_modes() { + let unbind_normal = Unbind::Keys(vec![Key::Alt('n'), Key::Ctrl('p')]); + let unbind_resize = Unbind::Keys(vec![Key::Char('h'), Key::Ctrl('r')]); + let unbind_from_yaml_normal = UnbindFromYaml { + unbind: unbind_normal, + }; + let unbind_from_yaml_resize = UnbindFromYaml { + unbind: unbind_resize, + }; + let key_action_unbinds_normal = vec![KeyActionUnbind::Unbind(unbind_from_yaml_normal)]; + let key_action_unbinds_resize = vec![KeyActionUnbind::Unbind(unbind_from_yaml_resize)]; + + let mut keys = HashMap::<InputMode, Vec<KeyActionUnbind>>::new(); + keys.insert(InputMode::Normal, key_action_unbinds_normal); + keys.insert(InputMode::Resize, key_action_unbinds_resize); + + let keybinds_from_yaml = KeybindsFromYaml { + keybinds: keys, + unbind: Unbind::All(false), + }; + + let keybinds = Keybinds::unbind(keybinds_from_yaml); + let mode_keybinds_normal = keybinds.0.get(&InputMode::Normal); + let mode_keybinds_resize = keybinds.0.get(&InputMode::Resize); + let result_normal_1 = mode_keybinds_normal + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('n')); + let result_normal_2 = mode_keybinds_normal + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Ctrl('p')); + let result_resize_1 = mode_keybinds_resize + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Char('h')); + let result_resize_2 = mode_keybinds_resize + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Ctrl('r')); + assert!(result_normal_1.is_none()); + assert!(result_resize_1.is_none()); + assert!(result_normal_2.is_none()); + assert!(result_resize_2.is_none()); +} + +#[test] +fn unbind_multiple_keybinds_all_modes() { + let unbind = Unbind::Keys(vec![Key::Alt('n'), Key::Alt('h')]); + let keys = HashMap::<InputMode, Vec<KeyActionUnbind>>::new(); + let keybinds_from_yaml = KeybindsFromYaml { + keybinds: keys, + unbind, + }; + + let keybinds = Keybinds::unbind(keybinds_from_yaml); + let mode_keybinds_normal = keybinds.0.get(&InputMode::Normal); + let mode_keybinds_resize = keybinds.0.get(&InputMode::Resize); + let result_normal_1 = mode_keybinds_normal + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('n')); + let result_normal_2 = mode_keybinds_normal + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Ctrl('h')); + let result_resize_1 = mode_keybinds_resize + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Char('n')); + let result_resize_2 = mode_keybinds_resize + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Ctrl('h')); + assert!(result_normal_1.is_none()); + assert!(result_resize_1.is_none()); + assert!(result_normal_2.is_none()); + assert!(result_resize_2.is_none()); +} + +#[test] +fn unbind_all_toplevel_single_key_single_mode() { + let unbind = Unbind::Keys(vec![Key::Alt('h')]); + let unbind_from_yaml = UnbindFromYaml { unbind }; + let key_action_unbinds_normal = vec![KeyActionUnbind::Unbind(unbind_from_yaml)]; + let mut keys = HashMap::<InputMode, Vec<KeyActionUnbind>>::new(); + keys.insert(InputMode::Normal, key_action_unbinds_normal); + let from_yaml = KeybindsFromYaml { + keybinds: keys, + unbind: Unbind::All(true), + }; + + let keybinds_from_yaml = Keybinds::get_default_keybinds_with_config(Some(from_yaml)); + assert_eq!(keybinds_from_yaml, Keybinds::new()); +} + +#[test] +fn unbind_all_toplevel_single_key_multiple_modes() { + let unbind_n = Unbind::Keys(vec![Key::Alt('n')]); + let unbind_h = Unbind::Keys(vec![Key::Alt('h')]); + let unbind_from_yaml_n = UnbindFromYaml { unbind: unbind_n }; + let unbind_from_yaml_h = UnbindFromYaml { unbind: unbind_h }; + let key_action_unbinds_normal = vec![KeyActionUnbind::Unbind(unbind_from_yaml_n)]; + let key_action_unbinds_pane = vec![KeyActionUnbind::Unbind(unbind_from_yaml_h)]; + let mut keys = HashMap::<InputMode, Vec<KeyActionUnbind>>::new(); + keys.insert(InputMode::Normal, key_action_unbinds_normal); + keys.insert(InputMode::Pane, key_action_unbinds_pane); + let from_yaml = KeybindsFromYaml { + keybinds: keys, + unbind: Unbind::All(true), + }; + + let keybinds_from_yaml = Keybinds::get_default_keybinds_with_config(Some(from_yaml)); + assert_eq!(keybinds_from_yaml, Keybinds::new()); +} + +#[test] +fn unbind_all_toplevel_multiple_key_multiple_modes() { + let unbind_n = Unbind::Keys(vec![Key::Alt('n'), Key::Ctrl('p')]); + let unbind_h = Unbind::Keys(vec![Key::Alt('h'), Key::Ctrl('t')]); + let unbind_from_yaml_n = UnbindFromYaml { unbind: unbind_n }; + let unbind_from_yaml_h = UnbindFromYaml { unbind: unbind_h }; + let key_action_unbinds_normal = vec![KeyActionUnbind::Unbind(unbind_from_yaml_n)]; + let key_action_unbinds_pane = vec![KeyActionUnbind::Unbind(unbind_from_yaml_h)]; + let mut keys = HashMap::<InputMode, Vec<KeyActionUnbind>>::new(); + keys.insert(InputMode::Normal, key_action_unbinds_normal); + keys.insert(InputMode::Pane, key_action_unbinds_pane); + let from_yaml = KeybindsFromYaml { + keybinds: keys, + unbind: Unbind::All(true), + }; + + let keybinds_from_yaml = Keybinds::get_default_keybinds_with_config(Some(from_yaml)); assert_eq!(keybinds_from_yaml, Keybinds::new()); } + +#[test] +fn unbind_all_toplevel_all_key_multiple_modes() { + let unbind = Unbind::All(true); + let unbind_from_yaml = UnbindFromYaml { unbind }; + let key_action_unbinds_normal = vec![KeyActionUnbind::Unbind(unbind_from_yaml.clone())]; + let key_action_unbinds_pane = vec![KeyActionUnbind::Unbind(unbind_from_yaml)]; + let mut keys = HashMap::<InputMode, Vec<KeyActionUnbind>>::new(); + keys.insert(InputMode::Normal, key_action_unbinds_normal); + keys.insert(InputMode::Pane, key_action_unbinds_pane); + let from_yaml = KeybindsFromYaml { + keybinds: keys, + unbind: Unbind::All(true), + }; + + let keybinds_from_yaml = Keybinds::get_default_keybinds_with_config(Some(from_yaml)); + assert_eq!(keybinds_from_yaml, Keybinds::new()); +} + +#[test] +fn unbind_single_keybind_all_modes() { + let keys = HashMap::<InputMode, Vec<KeyActionUnbind>>::new(); + let from_yaml = KeybindsFromYaml { + keybinds: keys, + unbind: Unbind::Keys(vec![Key::Alt('n')]), + }; + + let keybinds_from_yaml = Keybinds::get_default_keybinds_with_config(Some(from_yaml)); + + let result_normal = keybinds_from_yaml + .0 + .get(&InputMode::Normal) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('n')); + let result_pane = keybinds_from_yaml + .0 + .get(&InputMode::Pane) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('n')); + let result_resize = keybinds_from_yaml + .0 + .get(&InputMode::Resize) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('n')); + let result_tab = keybinds_from_yaml + .0 + .get(&InputMode::Tab) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('n')); + + assert!(result_normal.is_none()); + assert!(result_pane.is_none()); + assert!(result_resize.is_none()); + assert!(result_tab.is_none()); +} + +#[test] +fn unbind_single_toplevel_single_key_single_mode_identical() { + let unbind = Unbind::Keys(vec![Key::Alt('n')]); + let unbind_from_yaml = UnbindFromYaml { unbind }; + let key_action_unbind = vec![KeyActionUnbind::Unbind(unbind_from_yaml)]; + let mut keys = HashMap::<InputMode, Vec<KeyActionUnbind>>::new(); + keys.insert(InputMode::Normal, key_action_unbind); + let from_yaml = KeybindsFromYaml { + keybinds: keys, + unbind: Unbind::Keys(vec![Key::Alt('n')]), + }; + + let keybinds_from_yaml = Keybinds::get_default_keybinds_with_config(Some(from_yaml)); + + let result_normal = keybinds_from_yaml + .0 + .get(&InputMode::Normal) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('n')); + let result_pane = keybinds_from_yaml + .0 + .get(&InputMode::Pane) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('n')); + let result_resize = keybinds_from_yaml + .0 + .get(&InputMode::Resize) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('n')); + let result_tab = keybinds_from_yaml + .0 + .get(&InputMode::Tab) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('n')); + + assert!(result_normal.is_none()); + assert!(result_pane.is_none()); + assert!(result_resize.is_none()); + assert!(result_tab.is_none()); +} + +#[test] +fn unbind_single_toplevel_single_key_single_mode_differing() { + let unbind = Unbind::Keys(vec![Key::Alt('l')]); + let unbind_from_yaml = UnbindFromYaml { unbind }; + let key_action_unbind = vec![KeyActionUnbind::Unbind(unbind_from_yaml)]; + let mut keys = HashMap::<InputMode, Vec<KeyActionUnbind>>::new(); + keys.insert(InputMode::Normal, key_action_unbind); + let from_yaml = KeybindsFromYaml { + keybinds: keys, + unbind: Unbind::Keys(vec![Key::Alt('n')]), + }; + + let keybinds_from_yaml = Keybinds::get_default_keybinds_with_config(Some(from_yaml)); + + let result_normal_n = keybinds_from_yaml + .0 + .get(&InputMode::Normal) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('n')); + let result_normal_l = keybinds_from_yaml + .0 + .get(&InputMode::Normal) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('l')); + let result_resize_n = keybinds_from_yaml + .0 + .get(&InputMode::Resize) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('n')); + let result_resize_l = keybinds_from_yaml + .0 + .get(&InputMode::Resize) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('l')); + + assert!(result_normal_n.is_none()); + assert!(result_normal_l.is_none()); + assert!(result_resize_n.is_none()); + assert!(result_resize_l.is_some()); +} + +#[test] +fn unbind_single_toplevel_single_key_multiple_modes() { + let unbind = Unbind::Keys(vec![Key::Alt('l')]); + let unbind_from_yaml = UnbindFromYaml { unbind }; + let key_action_unbind = vec![KeyActionUnbind::Unbind(unbind_from_yaml)]; + let mut keys = HashMap::<InputMode, Vec<KeyActionUnbind>>::new(); + keys.insert(InputMode::Normal, key_action_unbind.clone()); + keys.insert(InputMode::Pane, key_action_unbind); + let from_yaml = KeybindsFromYaml { + keybinds: keys, + unbind: Unbind::Keys(vec![Key::Alt('n')]), + }; + + let keybinds_from_yaml = Keybinds::get_default_keybinds_with_config(Some(from_yaml)); + + let result_normal_n = keybinds_from_yaml + .0 + .get(&InputMode::Normal) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('n')); + let result_normal_l = keybinds_from_yaml + .0 + .get(&InputMode::Normal) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('l')); + let result_pane_n = keybinds_from_yaml + .0 + .get(&InputMode::Pane) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('n')); + let result_pane_l = keybinds_from_yaml + .0 + .get(&InputMode::Pane) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('l')); + + assert!(result_normal_n.is_none()); + assert!(result_normal_l.is_none()); + assert!(result_pane_n.is_none()); + assert!(result_pane_l.is_none()); +} + +#[test] +fn unbind_single_toplevel_multiple_keys_single_mode() { + let unbind = Unbind::Keys(vec![ + Key::Alt('l'), + Key::Alt('h'), + Key::Alt('j'), + Key::Alt('k'), + ]); + let unbind_from_yaml = UnbindFromYaml { unbind }; + let key_action_unbind = vec![KeyActionUnbind::Unbind(unbind_from_yaml)]; + let mut keys = HashMap::<InputMode, Vec<KeyActionUnbind>>::new(); + keys.insert(InputMode::Normal, key_action_unbind.clone()); + keys.insert(InputMode::Pane, key_action_unbind); + let from_yaml = KeybindsFromYaml { + keybinds: keys, + unbind: Unbind::Keys(vec![Key::Alt('n')]), + }; + + let keybinds_from_yaml = Keybinds::get_default_keybinds_with_config(Some(from_yaml)); + + let result_normal_n = keybinds_from_yaml + .0 + .get(&InputMode::Normal) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('n')); + let result_normal_l = keybinds_from_yaml + .0 + .get(&InputMode::Normal) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('l')); + let result_normal_k = keybinds_from_yaml + .0 + .get(&InputMode::Pane) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('k')); + let result_normal_h = keybinds_from_yaml + .0 + .get(&InputMode::Pane) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('h')); + + assert!(result_normal_n.is_none()); + assert!(result_normal_l.is_none()); + assert!(result_normal_h.is_none()); + assert!(result_normal_k.is_none()); +} + +#[test] +fn unbind_single_toplevel_multiple_keys_multiple_modes() { + let unbind_normal = Unbind::Keys(vec![Key::Alt('l'), Key::Ctrl('p')]); + let unbind_from_yaml_normal = UnbindFromYaml { + unbind: unbind_normal, + }; + let key_action_unbind_normal = vec![KeyActionUnbind::Unbind(unbind_from_yaml_normal)]; + let unbind = Unbind::Keys(vec![Key::Alt('l'), Key::Alt('k')]); + let unbind_from_yaml = UnbindFromYaml { unbind }; + let key_action_unbind = vec![KeyActionUnbind::Unbind(unbind_from_yaml)]; + let mut keys = HashMap::<InputMode, Vec<KeyActionUnbind>>::new(); + keys.insert(InputMode::Normal, key_action_unbind_normal); + keys.insert(InputMode::Pane, key_action_unbind); + let from_yaml = KeybindsFromYaml { + keybinds: keys, + unbind: Unbind::Keys(vec![Key::Alt('n')]), + }; + + let keybinds_from_yaml = Keybinds::get_default_keybinds_with_config(Some(from_yaml)); + + let result_normal_n = keybinds_from_yaml + .0 + .get(&InputMode::Normal) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('n')); + let result_normal_p = keybinds_from_yaml + .0 + .get(&InputMode::Normal) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Ctrl('p')); + let result_normal_l = keybinds_from_yaml + .0 + .get(&InputMode::Normal) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('l')); + let result_pane_p = keybinds_from_yaml + .0 + .get(&InputMode::Pane) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Ctrl('p')); + let result_pane_n = keybinds_from_yaml + .0 + .get(&InputMode::Pane) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('n')); + let result_pane_l = keybinds_from_yaml + .0 + .get(&InputMode::Pane) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Alt('l')); + + assert!(result_normal_n.is_none()); + assert!(result_normal_l.is_none()); + assert!(result_normal_p.is_none()); + assert!(result_pane_n.is_none()); + assert!(result_pane_p.is_some()); + assert!(result_pane_l.is_none()); +} + +#[test] +fn uppercase_and_lowercase_are_distinct() { + let key_action_n = KeyActionFromYaml { + key: vec![Key::Char('n')], + action: vec![Action::NewTab], + }; + let key_action_large_n = KeyActionFromYaml { + key: vec![Key::Char('N')], + action: vec![Action::NewPane(None)], + }; + + let key_action_unbind = vec![ + KeyActionUnbind::KeyAction(key_action_n), + KeyActionUnbind::KeyAction(key_action_large_n), + ]; + let mut keys = HashMap::<InputMode, Vec<KeyActionUnbind>>::new(); + keys.insert(InputMode::Normal, key_action_unbind); + let from_yaml = KeybindsFromYaml { + keybinds: keys, + unbind: Unbind::All(false), + }; + + let keybinds_from_yaml = Keybinds::get_default_keybinds_with_config(Some(from_yaml)); + let result_n = keybinds_from_yaml + .0 + .get(&InputMode::Normal) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Char('n')); + let result_large_n = keybinds_from_yaml + .0 + .get(&InputMode::Normal) + .expect("ModeKeybinds shouldn't be empty") + .0 + .get(&Key::Char('N')); + + assert!(result_n.is_some()); + assert!(result_large_n.is_some()); +} diff --git a/src/common/mod.rs b/src/common/mod.rs index 6061f1a6..c91e1e05 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -3,73 +3,12 @@ pub mod errors; pub mod input; pub mod ipc; pub mod os_input_output; -pub mod pty_bus; +pub mod pty; pub mod screen; pub mod setup; +pub mod thread_bus; pub mod utils; pub mod wasm_vm; use crate::panes::PaneId; use crate::server::ServerInstruction; -use async_std::task_local; -use errors::{get_current_ctx, ErrorContext}; -use std::cell::RefCell; -use std::sync::mpsc; - -/// An [MPSC](mpsc) asynchronous channel with added error context. -pub type ChannelWithContext<T> = ( - mpsc::Sender<(T, ErrorContext)>, - mpsc::Receiver<(T, ErrorContext)>, -); -/// An [MPSC](mpsc) synchronous channel with added error context. -pub type SyncChannelWithContext<T> = ( - mpsc::SyncSender<(T, ErrorContext)>, - mpsc::Receiver<(T, ErrorContext)>, -); - -/// Wrappers around the two standard [MPSC](mpsc) sender types, [`mpsc::Sender`] and [`mpsc::SyncSender`], with an additional [`ErrorContext`]. -#[derive(Clone)] -pub enum SenderType<T: Clone> { - /// A wrapper around an [`mpsc::Sender`], adding an [`ErrorContext`]. - Sender(mpsc::Sender<(T, ErrorContext)>), - /// A wrapper around an [`mpsc::SyncSender`], adding an [`ErrorContext`]. - SyncSender(mpsc::SyncSender<(T, ErrorContext)>), -} - -/// Sends messages on an [MPSC](std::sync::mpsc) channel, along with an [`ErrorContext`], -/// synchronously or asynchronously depending on the underlying [`SenderType`]. -#[derive(Clone)] -pub struct SenderWithContext<T: Clone> { - sender: SenderType<T>, -} - -impl<T: Clone> SenderWithContext<T> { - pub fn new(sender: SenderType<T>) -> Self { - Self { sender } - } - - /// Sends an event, along with the current [`ErrorContext`], on this - /// [`SenderWithContext`]'s channel. - pub fn send(&self, event: T) -> Result<(), mpsc::SendError<(T, ErrorContext)>> { - let err_ctx = get_current_ctx(); - match self.sender { - SenderType::Sender(ref s) => s.send((event, err_ctx)), - SenderType::SyncSender(ref s) => s.send((event, err_ctx)), - } - } -} - -unsafe impl<T: Clone> Send for SenderWithContext<T> {} -unsafe impl<T: Clone> Sync for SenderWithContext<T> {} - -thread_local!( - /// A key to some thread local storage (TLS) that holds a representation of the thread's call - /// stack in the form of an [`ErrorContext`]. - pub static OPENCALLS: RefCell<ErrorContext> = RefCell::default() -); - -task_local! { - /// A key to some task local storage that holds a representation of the task's call - /// stack in the form of an [`ErrorContext`]. - static ASYNCOPENCALLS: RefCell<ErrorContext> = RefCell::default() -} diff --git a/src/common/os_input_output.rs b/src/common/os_input_output.rs index 5539ae70..6f891e0d 100644 --- a/src/common/os_input_output.rs +++ b/src/common/os_input_output.rs @@ -25,10 +25,10 @@ use zellij_tile::data::Palette; const UNIX_PERMISSIONS: u32 = 0o700; -pub fn set_permissions(path: &Path) { - let mut permissions = fs::metadata(path).unwrap().permissions(); +pub fn set_permissions(path: &Path) -> io::Result<()> { + let mut permissions = fs::metadata(path)?.permissions(); permissions.set_mode(UNIX_PERMISSIONS); - fs::set_permissions(path, permissions).unwrap(); + fs::set_permissions(path, permissions) } fn into_raw_mode(pid: RawFd) { diff --git a/src/common/pty_bus.rs b/src/common/pty.rs index 31696186..3c471d6a 100644 --- a/src/common/pty_bus.rs +++ b/src/common/pty.rs @@ -1,23 +1,21 @@ -use ::async_std::stream::*; -use ::async_std::task; -use ::async_std::task::*; -use ::std::collections::HashMap; -use ::std::os::unix::io::RawFd; -use ::std::pin::*; -use ::std::sync::mpsc::Receiver; -use ::std::time::{Duration, Instant}; +use async_std::stream::*; +use async_std::task; +use async_std::task::*; +use std::collections::HashMap; +use std::os::unix::io::RawFd; use std::path::PathBuf; +use std::pin::*; +use std::time::{Duration, Instant}; -use super::SenderWithContext; +use crate::client::panes::PaneId; +use crate::common::errors::{get_current_ctx, ContextType, PtyContext}; +use crate::common::screen::ScreenInstruction; +use crate::common::thread_bus::{Bus, ThreadSenders}; use crate::layout::Layout; use crate::os_input_output::ServerOsApi; +use crate::server::ServerInstruction; use crate::utils::logging::debug_to_file; -use crate::{ - errors::{get_current_ctx, ContextType, ErrorContext}, - panes::PaneId, - screen::ScreenInstruction, - wasm_vm::PluginInstruction, -}; +use crate::wasm_vm::PluginInstruction; pub struct ReadFromPid { pid: RawFd, @@ -79,19 +77,72 @@ pub enum PtyInstruction { Exit, } -pub struct PtyBus { - pub receive_pty_instructions: Receiver<(PtyInstruction, ErrorContext)>, +pub struct Pty { + pub bus: Bus<PtyInstruction>, pub id_to_child_pid: HashMap<RawFd, RawFd>, - pub send_screen_instructions: SenderWithContext<ScreenInstruction>, - send_plugin_instructions: SenderWithContext<PluginInstruction>, - os_input: Box<dyn ServerOsApi>, debug_to_file: bool, task_handles: HashMap<RawFd, JoinHandle<()>>, } +pub fn pty_thread_main(mut pty: Pty, maybe_layout: Option<Layout>) { + loop { + let (event, mut err_ctx) = pty.bus.recv().expect("failed to receive event on channel"); + err_ctx.add_call(ContextType::Pty(PtyContext::from(&event))); + match event { + PtyInstruction::SpawnTerminal(file_to_open) => { + let pid = pty.spawn_terminal(file_to_open); + pty.bus + .senders + .send_to_screen(ScreenInstruction::NewPane(PaneId::Terminal(pid))) + .unwrap(); + } + PtyInstruction::SpawnTerminalVertically(file_to_open) => { + let pid = pty.spawn_terminal(file_to_open); + pty.bus + .senders + .send_to_screen(ScreenInstruction::VerticalSplit(PaneId::Terminal(pid))) + .unwrap(); + } + PtyInstruction::SpawnTerminalHorizontally(file_to_open) => { + let pid = pty.spawn_terminal(file_to_open); + pty.bus + .senders + .send_to_screen(ScreenInstruction::HorizontalSplit(PaneId::Terminal(pid))) + .unwrap(); + } + PtyInstruction::NewTab => { + if let Some(layout) = maybe_layout.clone() { + pty.spawn_terminals_for_layout(layout); + } else { + let pid = pty.spawn_terminal(None); + pty.bus + .senders + .send_to_screen(ScreenInstruction::NewTab(pid)) + .unwrap(); + } + } + PtyInstruction::ClosePane(id) => { + pty.close_pane(id); + pty.bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + PtyInstruction::CloseTab(ids) => { + pty.close_tab(ids); + pty.bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + PtyInstruction::Exit => break, + } + } +} + fn stream_terminal_bytes( pid: RawFd, - send_screen_instructions: SenderWithContext<ScreenInstruction>, + senders: ThreadSenders, os_input: Box<dyn ServerOsApi>, debug: bool, ) -> JoinHandle<()> { @@ -113,7 +164,7 @@ fn stream_terminal_bytes( } } if !bytes_is_empty { - let _ = send_screen_instructions.send(ScreenInstruction::PtyBytes(pid, bytes)); + let _ = senders.send_to_screen(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 @@ -124,9 +175,7 @@ fn stream_terminal_bytes( Some(receive_time) => { if receive_time.elapsed() > max_render_pause { pending_render = false; - send_screen_instructions - .send(ScreenInstruction::Render) - .unwrap(); + let _ = senders.send_to_screen(ScreenInstruction::Render); last_byte_receive_time = Some(Instant::now()); } else { pending_render = true; @@ -140,53 +189,44 @@ fn stream_terminal_bytes( } else { if pending_render { pending_render = false; - send_screen_instructions - .send(ScreenInstruction::Render) - .unwrap(); + let _ = senders.send_to_screen(ScreenInstruction::Render); } last_byte_receive_time = None; task::sleep(::std::time::Duration::from_millis(10)).await; } } - send_screen_instructions - .send(ScreenInstruction::Render) - .unwrap(); + senders.send_to_screen(ScreenInstruction::Render).unwrap(); #[cfg(not(test))] // this is a little hacky, and is because the tests end the file as soon as // we read everything, rather than hanging until there is new data // a better solution would be to fix the test fakes, but this will do for now - send_screen_instructions - .send(ScreenInstruction::ClosePane(PaneId::Terminal(pid))) + senders + .send_to_screen(ScreenInstruction::ClosePane(PaneId::Terminal(pid))) .unwrap(); } }) } -impl PtyBus { - pub fn new( - receive_pty_instructions: Receiver<(PtyInstruction, ErrorContext)>, - send_screen_instructions: SenderWithContext<ScreenInstruction>, - send_plugin_instructions: SenderWithContext<PluginInstruction>, - os_input: Box<dyn ServerOsApi>, - debug_to_file: bool, - ) -> Self { - PtyBus { - receive_pty_instructions, - os_input, +impl Pty { + pub fn new(bus: Bus<PtyInstruction>, debug_to_file: bool) -> Self { + Pty { + bus, id_to_child_pid: HashMap::new(), - send_screen_instructions, - send_plugin_instructions, debug_to_file, task_handles: HashMap::new(), } } pub fn spawn_terminal(&mut self, file_to_open: Option<PathBuf>) -> RawFd { - let (pid_primary, pid_secondary): (RawFd, RawFd) = - self.os_input.spawn_terminal(file_to_open); + let (pid_primary, pid_secondary): (RawFd, RawFd) = self + .bus + .os_input + .as_mut() + .unwrap() + .spawn_terminal(file_to_open); let task_handle = stream_terminal_bytes( pid_primary, - self.send_screen_instructions.clone(), - self.os_input.clone(), + self.bus.senders.clone(), + self.bus.os_input.as_ref().unwrap().clone(), self.debug_to_file, ); self.task_handles.insert(pid_primary, task_handle); @@ -197,12 +237,14 @@ impl PtyBus { let total_panes = layout.total_terminal_panes(); let mut new_pane_pids = vec![]; for _ in 0..total_panes { - let (pid_primary, pid_secondary): (RawFd, RawFd) = self.os_input.spawn_terminal(None); + let (pid_primary, pid_secondary): (RawFd, RawFd) = + self.bus.os_input.as_mut().unwrap().spawn_terminal(None); self.id_to_child_pid.insert(pid_primary, pid_secondary); new_pane_pids.push(pid_primary); } - self.send_screen_instructions - .send(ScreenInstruction::ApplyLayout( + self.bus + .senders + .send_to_screen(ScreenInstruction::ApplyLayout( layout, new_pane_pids.clone(), )) @@ -210,8 +252,8 @@ impl PtyBus { for id in new_pane_pids { let task_handle = stream_terminal_bytes( id, - self.send_screen_instructions.clone(), - self.os_input.clone(), + self.bus.senders.clone(), + self.bus.os_input.as_ref().unwrap().clone(), self.debug_to_file, ); self.task_handles.insert(id, task_handle); @@ -222,15 +264,16 @@ impl PtyBus { PaneId::Terminal(id) => { let child_pid = self.id_to_child_pid.remove(&id).unwrap(); let handle = self.task_handles.remove(&id).unwrap(); - self.os_input.kill(child_pid).unwrap(); + self.bus.os_input.as_mut().unwrap().kill(child_pid).unwrap(); task::block_on(async { handle.cancel().await; }); } - PaneId::Plugin(pid) => self - .send_plugin_instructions - .send(PluginInstruction::Unload(pid)) - .unwrap(), + PaneId::Plugin(pid) => drop( + self.bus + .senders + .send_to_plugin(PluginInstruction::Unload(pid)), + ), } } pub fn close_tab(&mut self, ids: Vec<PaneId>) { @@ -240,7 +283,7 @@ impl PtyBus { } } -impl Drop for PtyBus { +impl Drop for Pty { fn drop(&mut self) { let child_ids: Vec<RawFd> = self.id_to_child_pid.keys().copied().collect(); for id in child_ids { diff --git a/src/common/screen.rs b/src/common/screen.rs index 562b0ffe..bc99f953 100644 --- a/src/common/screen.rs +++ b/src/common/screen.rs @@ -3,18 +3,19 @@ use std::collections::BTreeMap; use std::os::unix::io::RawFd; use std::str; -use std::sync::mpsc::Receiver; -use crate::common::SenderWithContext; -use crate::os_input_output::ServerOsApi; +use crate::common::input::options::Options; +use crate::common::pty::{PtyInstruction, VteBytes}; +use crate::common::thread_bus::Bus; +use crate::errors::{ContextType, ScreenContext}; +use crate::layout::Layout; +use crate::panes::PaneId; use crate::panes::PositionAndSize; -use crate::pty_bus::{PtyInstruction, VteBytes}; use crate::server::ServerInstruction; use crate::tab::Tab; -use crate::{errors::ErrorContext, wasm_vm::PluginInstruction}; -use crate::{layout::Layout, panes::PaneId}; +use crate::wasm_vm::PluginInstruction; -use zellij_tile::data::{Event, InputMode, ModeInfo, Palette, TabInfo}; +use zellij_tile::data::{Event, InputMode, ModeInfo, Palette, PluginCapabilities, TabInfo}; /// Instructions that can be sent to the [`Screen`]. #[derive(Debug, Clone)] @@ -33,9 +34,11 @@ pub enum ScreenInstruction { FocusNextPane, FocusPreviousPane, MoveFocusLeft, + MoveFocusLeftOrPreviousTab, MoveFocusDown, MoveFocusUp, MoveFocusRight, + MoveFocusRightOrNextTab, Exit, ScrollUp, ScrollDown, @@ -52,7 +55,7 @@ pub enum ScreenInstruction { NewTab(RawFd), SwitchTabNext, SwitchTabPrev, - ToggleActiveSyncPanes, + ToggleActiveSyncTab, CloseTab, GoToTab(u32), UpdateTabName(Vec<u8>), @@ -63,55 +66,37 @@ pub enum ScreenInstruction { /// A [`Screen`] holds multiple [`Tab`]s, each one holding multiple [`panes`](crate::client::panes). /// It only directly controls which tab is active, delegating the rest to the individual `Tab`. pub struct Screen { - /// A [`ScreenInstruction`] and [`ErrorContext`] receiver. - pub receiver: Receiver<(ScreenInstruction, ErrorContext)>, + /// A Bus for sending and receiving messages with the other threads. + pub bus: Bus<ScreenInstruction>, /// An optional maximal amount of panes allowed per [`Tab`] in this [`Screen`] instance. max_panes: Option<usize>, /// A map between this [`Screen`]'s tabs and their ID/key. tabs: BTreeMap<usize, Tab>, - /// A [`PluginInstruction`] and [`ErrorContext`] sender. - pub send_plugin_instructions: SenderWithContext<PluginInstruction>, - /// An [`PtyInstruction`] and [`ErrorContext`] sender. - pub send_pty_instructions: SenderWithContext<PtyInstruction>, - /// An [`ServerInstruction`] and [`ErrorContext`] sender. - pub send_server_instructions: SenderWithContext<ServerInstruction>, /// The full size of this [`Screen`]. full_screen_ws: PositionAndSize, /// The index of this [`Screen`]'s active [`Tab`]. active_tab_index: Option<usize>, - /// The [`ServerOsApi`] this [`Screen`] uses. - os_api: Box<dyn ServerOsApi>, mode_info: ModeInfo, input_mode: InputMode, colors: Palette, } impl Screen { - // FIXME: This lint needs actual fixing! Maybe by bundling the Senders /// Creates and returns a new [`Screen`]. - #[allow(clippy::too_many_arguments)] pub fn new( - receive_screen_instructions: Receiver<(ScreenInstruction, ErrorContext)>, - send_plugin_instructions: SenderWithContext<PluginInstruction>, - send_pty_instructions: SenderWithContext<PtyInstruction>, - send_server_instructions: SenderWithContext<ServerInstruction>, + bus: Bus<ScreenInstruction>, full_screen_ws: &PositionAndSize, - os_api: Box<dyn ServerOsApi>, max_panes: Option<usize>, mode_info: ModeInfo, input_mode: InputMode, colors: Palette, ) -> Self { Screen { - receiver: receive_screen_instructions, + bus, max_panes, - send_plugin_instructions, - send_pty_instructions, - send_server_instructions, full_screen_ws: *full_screen_ws, active_tab_index: None, tabs: BTreeMap::new(), - os_api, mode_info, input_mode, colors, @@ -128,10 +113,8 @@ impl Screen { position, String::new(), &self.full_screen_ws, - self.os_api.clone(), - self.send_plugin_instructions.clone(), - self.send_pty_instructions.clone(), - self.send_server_instructions.clone(), + self.bus.os_input.as_ref().unwrap().clone(), + self.bus.senders.clone(), self.max_panes, Some(PaneId::Terminal(pane_id)), self.mode_info.clone(), @@ -215,13 +198,15 @@ impl Screen { // below we don't check the result of sending the CloseTab instruction to the pty thread // because this might be happening when the app is closing, at which point the pty thread // has already closed and this would result in an error - self.send_pty_instructions - .send(PtyInstruction::CloseTab(pane_ids)) + self.bus + .senders + .send_to_pty(PtyInstruction::CloseTab(pane_ids)) .unwrap(); if self.tabs.is_empty() { self.active_tab_index = None; - self.send_server_instructions - .send(ServerInstruction::Render(None)) + self.bus + .senders + .send_to_server(ServerInstruction::Render(None)) .unwrap(); } else { for t in self.tabs.values_mut() { @@ -284,10 +269,8 @@ impl Screen { position, String::new(), &self.full_screen_ws, - self.os_api.clone(), - self.send_plugin_instructions.clone(), - self.send_pty_instructions.clone(), - self.send_server_instructions.clone(), + self.bus.os_input.as_ref().unwrap().clone(), + self.bus.senders.clone(), self.max_panes, None, self.mode_info.clone(), @@ -311,8 +294,9 @@ impl Screen { is_sync_panes_active: tab.is_sync_panes_active(), }); } - self.send_plugin_instructions - .send(PluginInstruction::Update(None, Event::TabUpdate(tab_data))) + self.bus + .senders + .send_to_plugin(PluginInstruction::Update(None, Event::TabUpdate(tab_data))) .unwrap(); } @@ -340,3 +324,272 @@ impl Screen { } } } + +pub fn screen_thread_main( + bus: Bus<ScreenInstruction>, + max_panes: Option<usize>, + full_screen_ws: PositionAndSize, + config_options: Options, +) { + let colors = bus.os_input.as_ref().unwrap().load_palette(); + let capabilities = config_options.simplified_ui; + + let mut screen = Screen::new( + bus, + &full_screen_ws, + max_panes, + ModeInfo { + palette: colors, + capabilities: PluginCapabilities { + arrow_fonts: capabilities, + }, + ..ModeInfo::default() + }, + InputMode::Normal, + colors, + ); + loop { + let (event, mut err_ctx) = screen + .bus + .recv() + .expect("failed to receive event on channel"); + err_ctx.add_call(ContextType::Screen(ScreenContext::from(&event))); + match 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_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_bytes(pid, vte_bytes); + break; + } + } + } + } + ScreenInstruction::Render => { + screen.render(); + } + ScreenInstruction::NewPane(pid) => { + screen.get_active_tab_mut().unwrap().new_pane(pid); + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::HorizontalSplit(pid) => { + screen.get_active_tab_mut().unwrap().horizontal_split(pid); + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::VerticalSplit(pid) => { + screen.get_active_tab_mut().unwrap().vertical_split(pid); + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::WriteCharacter(bytes) => { + let active_tab = screen.get_active_tab_mut().unwrap(); + match active_tab.is_sync_panes_active() { + true => active_tab.write_to_terminals_on_current_tab(bytes), + false => active_tab.write_to_active_terminal(bytes), + } + } + ScreenInstruction::ResizeLeft => { + screen.get_active_tab_mut().unwrap().resize_left(); + } + ScreenInstruction::ResizeRight => { + screen.get_active_tab_mut().unwrap().resize_right(); + } + ScreenInstruction::ResizeDown => { + screen.get_active_tab_mut().unwrap().resize_down(); + } + ScreenInstruction::ResizeUp => { + screen.get_active_tab_mut().unwrap().resize_up(); + } + 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(); + } + ScreenInstruction::MoveFocusLeftOrPreviousTab => { + if !screen.get_active_tab_mut().unwrap().move_focus_left() { + screen.switch_tab_prev(); + } + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::MoveFocusDown => { + screen.get_active_tab_mut().unwrap().move_focus_down(); + } + ScreenInstruction::MoveFocusRight => { + screen.get_active_tab_mut().unwrap().move_focus_right(); + } + ScreenInstruction::MoveFocusRightOrNextTab => { + if !screen.get_active_tab_mut().unwrap().move_focus_right() { + screen.switch_tab_next(); + } + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::MoveFocusUp => { + screen.get_active_tab_mut().unwrap().move_focus_up(); + } + ScreenInstruction::ScrollUp => { + screen + .get_active_tab_mut() + .unwrap() + .scroll_active_terminal_up(); + } + ScreenInstruction::ScrollDown => { + screen + .get_active_tab_mut() + .unwrap() + .scroll_active_terminal_down(); + } + ScreenInstruction::PageScrollUp => { + screen + .get_active_tab_mut() + .unwrap() + .scroll_active_terminal_up_page(); + } + ScreenInstruction::PageScrollDown => { + screen + .get_active_tab_mut() + .unwrap() + .scroll_active_terminal_down_page(); + } + ScreenInstruction::ClearScroll => { + screen + .get_active_tab_mut() + .unwrap() + .clear_active_terminal_scroll(); + } + ScreenInstruction::CloseFocusedPane => { + screen.get_active_tab_mut().unwrap().close_focused_pane(); + screen.render(); + } + ScreenInstruction::SetSelectable(id, selectable) => { + screen + .get_active_tab_mut() + .unwrap() + .set_pane_selectable(id, selectable); + } + ScreenInstruction::SetMaxHeight(id, max_height) => { + screen + .get_active_tab_mut() + .unwrap() + .set_pane_max_height(id, max_height); + } + ScreenInstruction::SetInvisibleBorders(id, invisible_borders) => { + screen + .get_active_tab_mut() + .unwrap() + .set_pane_invisible_borders(id, invisible_borders); + screen.render(); + } + ScreenInstruction::ClosePane(id) => { + screen.get_active_tab_mut().unwrap().close_pane(id); + screen.render(); + } + ScreenInstruction::ToggleActiveTerminalFullscreen => { + screen + .get_active_tab_mut() + .unwrap() + .toggle_active_pane_fullscreen(); + } + ScreenInstruction::NewTab(pane_id) => { + screen.new_tab(pane_id); + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::SwitchTabNext => { + screen.switch_tab_next(); + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::SwitchTabPrev => { + screen.switch_tab_prev(); + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::CloseTab => { + screen.close_tab(); + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::ApplyLayout(layout, new_pane_pids) => { + screen.apply_layout(layout, new_pane_pids); + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::GoToTab(tab_index) => { + screen.go_to_tab(tab_index as usize); + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::UpdateTabName(c) => { + screen.update_active_tab_name(c); + } + ScreenInstruction::TerminalResize(new_size) => { + screen.resize_to_screen(new_size); + } + ScreenInstruction::ChangeMode(mode_info) => { + screen.change_mode(mode_info); + } + ScreenInstruction::ToggleActiveSyncTab => { + screen + .get_active_tab_mut() + .unwrap() + .toggle_sync_panes_is_active(); + screen.update_tabs(); + } + ScreenInstruction::Exit => { + break; + } + } + } +} diff --git a/src/common/setup.rs b/src/common/setup.rs index bcda4589..5913211a 100644 --- a/src/common/setup.rs +++ b/src/common/setup.rs @@ -6,7 +6,7 @@ use directories_next::BaseDirs; use std::io::Write; use std::{fs, path::Path, path::PathBuf}; -const CONFIG_LOCATION: &str = "/.config/zellij"; +const CONFIG_LOCATION: &str = ".config/zellij"; #[macro_export] macro_rules! asset_map { @@ -44,7 +44,7 @@ pub mod install { let path = data_dir.join(path); let parent_path = path.parent().unwrap(); fs::create_dir_all(parent_path).unwrap(); - set_permissions(parent_path); + set_permissions(parent_path).unwrap(); if out_of_date || !path.exists() { fs::write(path, bytes).expect("Failed to install default assets!"); } @@ -57,8 +57,8 @@ pub mod install { /// existing config directory, returns the first match pub fn find_default_config_dir() -> Option<PathBuf> { vec![ - Some(xdg_config_dir()), home_config_dir(), + Some(xdg_config_dir()), Some(Path::new(SYSTEM_DEFAULT_CONFIG_DIR).to_path_buf()), ] .into_iter() diff --git a/src/common/thread_bus.rs b/src/common/thread_bus.rs new file mode 100644 index 00000000..8827d068 --- /dev/null +++ b/src/common/thread_bus.rs @@ -0,0 +1,142 @@ +//! Definitions and helpers for sending and receiving messages between threads. + +use async_std::task_local; +use std::cell::RefCell; +use std::sync::mpsc; + +use crate::common::pty::PtyInstruction; +use crate::common::ServerInstruction; +use crate::errors::{get_current_ctx, ErrorContext}; +use crate::os_input_output::ServerOsApi; +use crate::screen::ScreenInstruction; +use crate::wasm_vm::PluginInstruction; + +/// An [MPSC](mpsc) asynchronous channel with added error context. +pub type ChannelWithContext<T> = ( + mpsc::Sender<(T, ErrorContext)>, + mpsc::Receiver<(T, ErrorContext)>, +); +/// An [MPSC](mpsc) synchronous channel with added error context. +pub type SyncChannelWithContext<T> = ( + mpsc::SyncSender<(T, ErrorContext)>, + mpsc::Receiver<(T, ErrorContext)>, +); + +/// Wrappers around the two standard [MPSC](mpsc) sender types, [`mpsc::Sender`] and [`mpsc::SyncSender`], with an additional [`ErrorContext`]. +#[derive(Clone)] +pub enum SenderType<T: Clone> { + /// A wrapper around an [`mpsc::Sender`], adding an [`ErrorContext`]. + Sender(mpsc::Sender<(T, ErrorContext)>), + /// A wrapper around an [`mpsc::SyncSender`], adding an [`ErrorContext`]. + SyncSender(mpsc::SyncSender<(T, ErrorContext)>), +} + +/// Sends messages on an [MPSC](std::sync::mpsc) channel, along with an [`ErrorContext`], +/// synchronously or asynchronously depending on the underlying [`SenderType`]. +#[derive(Clone)] +pub struct SenderWithContext<T: Clone> { + sender: SenderType<T>, +} + +impl<T: Clone> SenderWithContext<T> { + pub fn new(sender: SenderType<T>) -> Self { + Self { sender } + } + + /// Sends an event, along with the current [`ErrorContext`], on this + /// [`SenderWithContext`]'s channel. + pub fn send(&self, event: T) -> Result<(), mpsc::SendError<(T, ErrorContext)>> { + let err_ctx = get_current_ctx(); + match self.sender { + SenderType::Sender(ref s) => s.send((event, err_ctx)), + SenderType::SyncSender(ref s) => s.send((event, err_ctx)), + } + } +} + +unsafe impl<T: Clone> Send for SenderWithContext<T> {} +unsafe impl<T: Clone> Sync for SenderWithContext<T> {} + +thread_local!( + /// A key to some thread local storage (TLS) that holds a representation of the thread's call + /// stack in the form of an [`ErrorContext`]. + pub static OPENCALLS: RefCell<ErrorContext> = RefCell::default() +); + +task_local! { + /// A key to some task local storage that holds a representation of the task's call + /// stack in the form of an [`ErrorContext`]. + pub static ASYNCOPENCALLS: RefCell<ErrorContext> = RefCell::default() +} + +/// A container for senders to the different threads in zellij on the server side +#[derive(Clone)] +pub struct ThreadSenders { + pub to_screen: Option<SenderWithContext<ScreenInstruction>>, + pub to_pty: Option<SenderWithContext<PtyInstruction>>, + pub to_plugin: Option<SenderWithContext<PluginInstruction>>, + pub to_server: Option<SenderWithContext<ServerInstruction>>, +} + +impl ThreadSenders { + pub fn send_to_screen( + &self, + instruction: ScreenInstruction, + ) -> Result<(), mpsc::SendError<(ScreenInstruction, ErrorContext)>> { + self.to_screen.as_ref().unwrap().send(instruction) + } + + pub fn send_to_pty( + &self, + instruction: PtyInstruction, + ) -> Result<(), mpsc::SendError<(PtyInstruction, ErrorContext)>> { + self.to_pty.as_ref().unwrap().send(instruction) + } + + pub fn send_to_plugin( + &self, + instruction: PluginInstruction, + ) -> Result<(), mpsc::SendError<(PluginInstruction, ErrorContext)>> { + self.to_plugin.as_ref().unwrap().send(instruction) + } + + pub fn send_to_server( + &self, + instruction: ServerInstruction, + ) -> Result<(), mpsc::SendError<(ServerInstruction, ErrorContext)>> { + self.to_server.as_ref().unwrap().send(instruction) + } +} + +/// A container for a receiver, OS input and the senders to a given thread +pub struct Bus<T> { + pub receiver: mpsc::Receiver<(T, ErrorContext)>, + pub senders: ThreadSenders, + pub os_input: Option<Box<dyn ServerOsApi>>, +} + +impl<T> Bus<T> { + pub fn new( + receiver: mpsc::Receiver<(T, ErrorContext)>, + to_screen: Option<&SenderWithContext<ScreenInstruction>>, + to_pty: Option<&SenderWithContext<PtyInstruction>>, + to_plugin: Option<&SenderWithContext<PluginInstruction>>, + to_server: Option<&SenderWithContext<ServerInstruction>>, + os_input: Option<Box<dyn ServerOsApi>>, + ) -> Self { + Bus { + receiver, + senders: ThreadSenders { + to_screen: to_screen.cloned(), + to_pty: to_pty.cloned(), + to_plugin: to_plugin.cloned(), + to_server: to_server.cloned(), + }, + os_input: os_input.clone(), + } + } + + pub fn recv(&self) -> Result<(T, ErrorContext), mpsc::RecvError> { + self.receiver.recv() + } +} diff --git a/src/common/utils/consts.rs b/src/common/utils/consts.rs index 29870d97..c77f1c9e 100644 --- a/src/common/utils/consts.rs +++ b/src/common/utils/consts.rs @@ -38,7 +38,7 @@ lazy_static! { ); ipc_dir.push(VERSION); fs::create_dir_all(&ipc_dir).unwrap(); - set_permissions(&ipc_dir); + set_permissions(&ipc_dir).unwrap(); ipc_dir.push(&*SESSION_NAME); ipc_dir }; diff --git a/src/common/utils/logging.rs b/src/common/utils/logging.rs index ff3fb44c..0dd382d6 100644 --- a/src/common/utils/logging.rs +++ b/src/common/utils/logging.rs @@ -10,10 +10,12 @@ use std::{ use crate::os_input_output::set_permissions; use crate::utils::consts::{ZELLIJ_TMP_LOG_DIR, ZELLIJ_TMP_LOG_FILE}; -pub fn atomic_create_file(file_name: &Path) { - let _ = fs::OpenOptions::new().create(true).open(file_name); - #[cfg(not(test))] - set_permissions(file_name); +pub fn atomic_create_file(file_name: &Path) -> io::Result<()> { + let _ = fs::OpenOptions::new() + .append(true) + .create(true) + .open(file_name)?; + set_permissions(file_name) } pub fn atomic_create_dir(dir_name: &Path) -> io::Result<()> { @@ -27,7 +29,7 @@ pub fn atomic_create_dir(dir_name: &Path) -> io::Result<()> { Ok(()) }; if result.is_ok() { - set_permissions(dir_name); + set_permissions(dir_name)?; } result } @@ -38,10 +40,9 @@ pub fn debug_log_to_file(mut message: String) -> io::Result<()> { } pub fn debug_log_to_file_without_newline(message: String) -> io::Result<()> { - atomic_create_file(&*ZELLIJ_TMP_LOG_FILE); + atomic_create_file(&*ZELLIJ_TMP_LOG_FILE)?; let mut file = fs::OpenOptions::new() .append(true) - .create(true) .open(&*ZELLIJ_TMP_LOG_FILE)?; file.write_all(message.as_bytes()) } @@ -78,6 +79,7 @@ pub fn debug_to_file(message: u8, pid: RawFd) -> io::Result<()> { let mut file = fs::OpenOptions::new() .append(true) .create(true) - .open(path)?; + .open(&path)?; + set_permissions(&path)?; file.write_all(&[message]) } diff --git a/src/common/wasm_vm.rs b/src/common/wasm_vm.rs index db52c2bb..e5567285 100644 --- a/src/common/wasm_vm.rs +++ b/src/common/wasm_vm.rs @@ -1,17 +1,25 @@ +use std::collections::{HashMap, HashSet}; +use std::fs; +use std::path::PathBuf; +use std::process; +use std::str::FromStr; +use std::sync::{mpsc::Sender, Arc, Mutex}; +use std::thread; +use std::time::{Duration, Instant}; + use serde::{de::DeserializeOwned, Serialize}; -use std::{ - collections::HashSet, - path::PathBuf, - process, - sync::{mpsc::Sender, Arc, Mutex}, - thread, - time::{Duration, Instant}, +use wasmer::{ + imports, ChainableNamedResolver, Function, ImportObject, Instance, Module, Store, Value, + WasmerEnv, }; -use wasmer::{imports, Function, ImportObject, Store, WasmerEnv}; -use wasmer_wasi::WasiEnv; +use wasmer_wasi::{Pipe, WasiEnv, WasiState}; use zellij_tile::data::{Event, EventType, PluginIds}; -use super::{pty_bus::PtyInstruction, screen::ScreenInstruction, PaneId, SenderWithContext}; +use crate::common::errors::{ContextType, PluginContext}; +use crate::common::pty::PtyInstruction; +use crate::common::screen::ScreenInstruction; +use crate::common::thread_bus::{Bus, ThreadSenders}; +use crate::common::PaneId; #[derive(Clone, Debug)] pub enum PluginInstruction { @@ -25,14 +33,97 @@ pub enum PluginInstruction { #[derive(WasmerEnv, Clone)] pub struct PluginEnv { pub plugin_id: u32, - // FIXME: This should be a big bundle of all of the channels - pub send_screen_instructions: SenderWithContext<ScreenInstruction>, - pub send_pty_instructions: SenderWithContext<PtyInstruction>, - pub send_plugin_instructions: SenderWithContext<PluginInstruction>, + pub senders: ThreadSenders, pub wasi_env: WasiEnv, pub subscriptions: Arc<Mutex<HashSet<EventType>>>, } +// Thread main -------------------------------------------------------------------------------------------------------- +pub fn wasm_thread_main(bus: Bus<PluginInstruction>, store: Store, data_dir: PathBuf) { + let mut plugin_id = 0; + let mut plugin_map = HashMap::new(); + loop { + let (event, mut err_ctx) = bus.recv().expect("failed to receive event on channel"); + err_ctx.add_call(ContextType::Plugin(PluginContext::from(&event))); + match event { + PluginInstruction::Load(pid_tx, path) => { + let plugin_dir = data_dir.join("plugins/"); + let wasm_bytes = fs::read(&path) + .or_else(|_| fs::read(&path.with_extension("wasm"))) + .or_else(|_| fs::read(&plugin_dir.join(&path).with_extension("wasm"))) + .unwrap_or_else(|_| panic!("cannot find plugin {}", &path.display())); + + // FIXME: Cache this compiled module on disk. I could use `(de)serialize_to_file()` for that + let module = Module::new(&store, &wasm_bytes).unwrap(); + + let output = Pipe::new(); + let input = Pipe::new(); + let mut wasi_env = WasiState::new("Zellij") + .env("CLICOLOR_FORCE", "1") + .preopen(|p| { + p.directory(".") // FIXME: Change this to a more meaningful dir + .alias(".") + .read(true) + .write(true) + .create(true) + }) + .unwrap() + .stdin(Box::new(input)) + .stdout(Box::new(output)) + .finalize() + .unwrap(); + + let wasi = wasi_env.import_object(&module).unwrap(); + + let plugin_env = PluginEnv { + plugin_id, + senders: bus.senders.clone(), + wasi_env, + subscriptions: Arc::new(Mutex::new(HashSet::new())), + }; + + let zellij = zellij_exports(&store, &plugin_env); + let instance = Instance::new(&module, &zellij.chain_back(wasi)).unwrap(); + + let start = instance.exports.get_function("_start").unwrap(); + + // This eventually calls the `.load()` method + start.call(&[]).unwrap(); + + plugin_map.insert(plugin_id, (instance, plugin_env)); + pid_tx.send(plugin_id).unwrap(); + plugin_id += 1; + } + PluginInstruction::Update(pid, event) => { + for (&i, (instance, plugin_env)) in &plugin_map { + let subs = plugin_env.subscriptions.lock().unwrap(); + // FIXME: This is very janky... Maybe I should write my own macro for Event -> EventType? + let event_type = EventType::from_str(&event.to_string()).unwrap(); + if (pid.is_none() || pid == Some(i)) && subs.contains(&event_type) { + let update = instance.exports.get_function("update").unwrap(); + wasi_write_object(&plugin_env.wasi_env, &event); + update.call(&[]).unwrap(); + } + } + drop(bus.senders.send_to_screen(ScreenInstruction::Render)); + } + PluginInstruction::Render(buf_tx, pid, rows, cols) => { + let (instance, plugin_env) = plugin_map.get(&pid).unwrap(); + + let render = instance.exports.get_function("render").unwrap(); + + render + .call(&[Value::I32(rows as i32), Value::I32(cols as i32)]) + .unwrap(); + + buf_tx.send(wasi_read_string(&plugin_env.wasi_env)).unwrap(); + } + PluginInstruction::Unload(pid) => drop(plugin_map.remove(&pid)), + PluginInstruction::Exit => break, + } + } +} + // Plugin API --------------------------------------------------------------------------------------------------------- pub fn zellij_exports(store: &Store, plugin_env: &PluginEnv) -> ImportObject { @@ -74,8 +165,8 @@ fn host_unsubscribe(plugin_env: &PluginEnv) { fn host_set_selectable(plugin_env: &PluginEnv, selectable: i32) { let selectable = selectable != 0; plugin_env - .send_screen_instructions - .send(ScreenInstruction::SetSelectable( + .senders + .send_to_screen(ScreenInstruction::SetSelectable( PaneId::Plugin(plugin_env.plugin_id), selectable, )) @@ -85,8 +176,8 @@ fn host_set_selectable(plugin_env: &PluginEnv, selectable: i32) { fn host_set_max_height(plugin_env: &PluginEnv, max_height: i32) { let max_height = max_height as usize; plugin_env - .send_screen_instructions - .send(ScreenInstruction::SetMaxHeight( + .senders + .send_to_screen(ScreenInstruction::SetMaxHeight( PaneId::Plugin(plugin_env.plugin_id), max_height, )) @@ -96,8 +187,8 @@ fn host_set_max_height(plugin_env: &PluginEnv, max_height: i32) { fn host_set_invisible_borders(plugin_env: &PluginEnv, invisible_borders: i32) { let invisible_borders = invisible_borders != 0; plugin_env - .send_screen_instructions - .send(ScreenInstruction::SetInvisibleBorders( + .senders + .send_to_screen(ScreenInstruction::SetInvisibleBorders( PaneId::Plugin(plugin_env.plugin_id), invisible_borders, )) @@ -115,8 +206,8 @@ fn host_get_plugin_ids(plugin_env: &PluginEnv) { fn host_open_file(plugin_env: &PluginEnv) { let path: PathBuf = wasi_read_object(&plugin_env.wasi_env); plugin_env - .send_pty_instructions - .send(PtyInstruction::SpawnTerminal(Some(path))) + .senders + .send_to_pty(PtyInstruction::SpawnTerminal(Some(path))) .unwrap(); } @@ -130,7 +221,7 @@ fn host_set_timeout(plugin_env: &PluginEnv, secs: f64) { // timers as we'd like. // // But that's a lot of code, and this is a few lines: - let send_plugin_instructions = plugin_env.send_plugin_instructions.clone(); + let send_plugin_instructions = plugin_env.senders.to_plugin.clone(); let update_target = Some(plugin_env.plugin_id); thread::spawn(move || { let start_time = Instant::now(); @@ -140,6 +231,7 @@ fn host_set_timeout(plugin_env: &PluginEnv, secs: f64) { let elapsed_time = Instant::now().duration_since(start_time).as_secs_f64(); send_plugin_instructions + .unwrap() .send(PluginInstruction::Update( update_target, Event::Timer(elapsed_time), |
