diff options
| author | a-kenji <aks.kenji@protonmail.com> | 2021-05-18 10:05:15 +0200 |
|---|---|---|
| committer | a-kenji <aks.kenji@protonmail.com> | 2021-05-18 10:05:15 +0200 |
| commit | bcbde9fbb5bffe4e1334acc1270314517938cac5 (patch) | |
| tree | 8a18820a32392fb4a8a132a27740299ac4849f84 /zellij-utils | |
| parent | Fix Clippy Suggestions (diff) | |
| parent | Merge pull request #510 from zellij-org/speedup-ci (diff) | |
| download | sl0ppij-bcbde9fbb5bffe4e1334acc1270314517938cac5.tar.gz sl0ppij-bcbde9fbb5bffe4e1334acc1270314517938cac5.tar.xz sl0ppij-bcbde9fbb5bffe4e1334acc1270314517938cac5.zip | |
Merge branch 'main' of https://github.com/zellij-org/zellij into layout-path-506
Diffstat (limited to 'zellij-utils')
| -rw-r--r-- | zellij-utils/Cargo.toml | 36 | ||||
| -rw-r--r-- | zellij-utils/src/channels.rs | 65 | ||||
| -rw-r--r-- | zellij-utils/src/cli.rs | 55 | ||||
| -rw-r--r-- | zellij-utils/src/consts.rs | 54 | ||||
| -rw-r--r-- | zellij-utils/src/errors.rs | 263 | ||||
| -rw-r--r-- | zellij-utils/src/input/actions.rs | 64 | ||||
| -rw-r--r-- | zellij-utils/src/input/config.rs | 218 | ||||
| -rw-r--r-- | zellij-utils/src/input/keybinds.rs | 301 | ||||
| -rw-r--r-- | zellij-utils/src/input/mod.rs | 87 | ||||
| -rw-r--r-- | zellij-utils/src/input/options.rs | 45 | ||||
| -rw-r--r-- | zellij-utils/src/input/unit/keybinds_test.rs | 801 | ||||
| -rw-r--r-- | zellij-utils/src/ipc.rs | 140 | ||||
| -rw-r--r-- | zellij-utils/src/lib.rs | 10 | ||||
| -rw-r--r-- | zellij-utils/src/logging.rs | 85 | ||||
| -rw-r--r-- | zellij-utils/src/pane_size.rs | 24 | ||||
| -rw-r--r-- | zellij-utils/src/setup.rs | 240 | ||||
| -rw-r--r-- | zellij-utils/src/shared.rs | 119 |
17 files changed, 2607 insertions, 0 deletions
diff --git a/zellij-utils/Cargo.toml b/zellij-utils/Cargo.toml new file mode 100644 index 00000000..d33ac659 --- /dev/null +++ b/zellij-utils/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "zellij-utils" +version = "0.12.0" +authors = ["Kunal Mohan <kunalmohan99@gmail.com>"] +edition = "2018" +description = "A utility library for Zellij client and server" +license = "MIT" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +backtrace = "0.3.55" +bincode = "1.3.1" +interprocess = "1.1.1" +structopt = "0.3" +serde = { version = "1.0", features = ["derive"] } +zellij-tile = { path = "../zellij-tile/", version = "0.12.0" } +names = "0.11.0" +colors-transform = "0.2.5" +strip-ansi-escapes = "0.1.0" +strum = "0.20.0" +serde_yaml = "0.8" +nix = "0.19.1" +lazy_static = "1.4.0" +directories-next = "2.0" +termion = "1.5.0" + +[dependencies.async-std] +version = "1.3.0" +features = ["unstable"] + +[dev-dependencies] +tempfile = "3.2.0" + +[features] +test = [] diff --git a/zellij-utils/src/channels.rs b/zellij-utils/src/channels.rs new file mode 100644 index 00000000..8a97fa7d --- /dev/null +++ b/zellij-utils/src/channels.rs @@ -0,0 +1,65 @@ +//! 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::errors::{get_current_ctx, ErrorContext}; + +/// 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() +} diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs new file mode 100644 index 00000000..56e93edb --- /dev/null +++ b/zellij-utils/src/cli.rs @@ -0,0 +1,55 @@ +use crate::consts::{ZELLIJ_CONFIG_DIR_ENV, ZELLIJ_CONFIG_FILE_ENV}; +use crate::input::options::Options; +use crate::setup::Setup; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use structopt::StructOpt; + +#[derive(StructOpt, Default, Debug, Clone, Serialize, Deserialize)] +#[structopt(name = "zellij")] +pub struct CliArgs { + /// Maximum panes on screen, caution: opening more panes will close old ones + #[structopt(long)] + pub max_panes: Option<usize>, + + /// Change where zellij looks for layouts and plugins + #[structopt(long, parse(from_os_str))] + pub data_dir: Option<PathBuf>, + + /// Run server listening at the specified socket path + #[structopt(long, parse(from_os_str))] + pub server: Option<PathBuf>, + + /// Path to a layout yaml file + #[structopt(short, long, parse(from_os_str))] + pub layout: Option<PathBuf>, + + /// Path to a layout yaml file + #[structopt(long, parse(from_os_str))] + pub layout_path: Option<PathBuf>, + + /// Change where zellij looks for the configuration + #[structopt(short, long, env=ZELLIJ_CONFIG_FILE_ENV, parse(from_os_str))] + pub config: Option<PathBuf>, + + /// Change where zellij looks for the configuration + #[structopt(long, env=ZELLIJ_CONFIG_DIR_ENV, parse(from_os_str))] + pub config_dir: Option<PathBuf>, + + #[structopt(subcommand)] + pub option: Option<ConfigCli>, + + #[structopt(short, long)] + pub debug: bool, +} + +#[derive(Debug, StructOpt, Clone, Serialize, Deserialize)] +pub enum ConfigCli { + /// Change the behaviour of zellij + #[structopt(name = "options")] + Options(Options), + + /// Setup zellij and check its configuration + #[structopt(name = "setup")] + Setup(Setup), +} diff --git a/zellij-utils/src/consts.rs b/zellij-utils/src/consts.rs new file mode 100644 index 00000000..799e2913 --- /dev/null +++ b/zellij-utils/src/consts.rs @@ -0,0 +1,54 @@ +//! Zellij program-wide constants. + +use crate::shared::set_permissions; +use directories_next::ProjectDirs; +use lazy_static::lazy_static; +use nix::unistd::Uid; +use std::path::PathBuf; +use std::{env, fs}; + +pub const ZELLIJ_CONFIG_FILE_ENV: &str = "ZELLIJ_CONFIG_FILE"; +pub const ZELLIJ_CONFIG_DIR_ENV: &str = "ZELLIJ_CONFIG_DIR"; +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub const SYSTEM_DEFAULT_CONFIG_DIR: &str = "/etc/zellij"; +pub const SYSTEM_DEFAULT_DATA_DIR_PREFIX: &str = system_default_data_dir(); + +const fn system_default_data_dir() -> &'static str { + if let Some(data_dir) = std::option_env!("PREFIX") { + data_dir + } else { + &"/usr" + } +} + +lazy_static! { + static ref UID: Uid = Uid::current(); + pub static ref SESSION_NAME: String = names::Generator::default().next().unwrap(); + pub static ref ZELLIJ_PROJ_DIR: ProjectDirs = + ProjectDirs::from("org", "Zellij Contributors", "Zellij").unwrap(); + pub static ref ZELLIJ_IPC_PIPE: PathBuf = { + let mut ipc_dir = env::var("ZELLIJ_SOCKET_DIR").map_or_else( + |_| { + ZELLIJ_PROJ_DIR + .runtime_dir() + .map_or_else(|| ZELLIJ_TMP_DIR.clone(), |p| p.to_owned()) + }, + PathBuf::from, + ); + ipc_dir.push(VERSION); + fs::create_dir_all(&ipc_dir).unwrap(); + set_permissions(&ipc_dir).unwrap(); + ipc_dir.push(&*SESSION_NAME); + ipc_dir + }; + pub static ref ZELLIJ_TMP_DIR: PathBuf = + PathBuf::from("/tmp/zellij-".to_string() + &format!("{}", *UID)); + pub static ref ZELLIJ_TMP_LOG_DIR: PathBuf = ZELLIJ_TMP_DIR.join("zellij-log"); + pub static ref ZELLIJ_TMP_LOG_FILE: PathBuf = ZELLIJ_TMP_LOG_DIR.join("log.txt"); +} + +pub const FEATURES: &[&str] = &[ + #[cfg(feature = "disable_automatic_asset_installation")] + "disable_automatic_asset_installation", +]; diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs new file mode 100644 index 00000000..94a894c4 --- /dev/null +++ b/zellij-utils/src/errors.rs @@ -0,0 +1,263 @@ +//! Error context system based on a thread-local representation of the call stack, itself based on +//! the instructions that are sent between threads. + +use crate::channels::{SenderWithContext, ASYNCOPENCALLS, OPENCALLS}; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Error, Formatter}; +use std::panic::PanicInfo; + +/// 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; + +pub trait ErrorInstruction { + fn error(err: String) -> Self; +} + +/// Custom panic handler/hook. Prints the [`ErrorContext`]. +pub fn handle_panic<T>(info: &PanicInfo<'_>, sender: &SenderWithContext<T>) +where + T: ErrorInstruction + Clone, +{ + use backtrace::Backtrace; + use std::{process, thread}; + let backtrace = Backtrace::new(); + let thread = thread::current(); + let thread = thread.name().unwrap_or("unnamed"); + + let msg = match info.payload().downcast_ref::<&'static str>() { + Some(s) => Some(*s), + None => info.payload().downcast_ref::<String>().map(|s| &**s), + }; + + let err_ctx = OPENCALLS.with(|ctx| *ctx.borrow()); + + let backtrace = match (info.location(), msg) { + (Some(location), Some(msg)) => format!( + "{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked at '{}': {}:{}\n\u{1b}[0;0m{:?}", + err_ctx, + thread, + msg, + location.file(), + location.line(), + backtrace, + ), + (Some(location), None) => format!( + "{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked: {}:{}\n\u{1b}[0;0m{:?}", + err_ctx, + thread, + location.file(), + location.line(), + backtrace + ), + (None, Some(msg)) => format!( + "{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked at '{}'\n\u{1b}[0;0m{:?}", + err_ctx, thread, msg, backtrace + ), + (None, None) => format!( + "{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked\n\u{1b}[0;0m{:?}", + err_ctx, thread, backtrace + ), + }; + + if thread == "main" { + println!("{}", backtrace); + process::exit(1); + } else { + let _ = sender.send(T::error(backtrace)); + } +} + +pub fn get_current_ctx() -> ErrorContext { + ASYNCOPENCALLS + .try_with(|ctx| *ctx.borrow()) + .unwrap_or_else(|_| OPENCALLS.with(|ctx| *ctx.borrow())) +} + +/// A representation of the call stack. +#[derive(Clone, Copy, Serialize, Deserialize)] +pub struct ErrorContext { + calls: [ContextType; MAX_THREAD_CALL_STACK], +} + +impl ErrorContext { + /// Returns a new, blank [`ErrorContext`] containing only [`Empty`](ContextType::Empty) + /// calls. + pub fn new() -> Self { + Self { + calls: [ContextType::Empty; MAX_THREAD_CALL_STACK], + } + } + + /// Adds a call to this [`ErrorContext`]'s call stack representation. + pub fn add_call(&mut self, call: ContextType) { + for ctx in self.calls.iter_mut() { + if *ctx == ContextType::Empty { + *ctx = call; + break; + } + } + self.update_thread_ctx() + } + + /// Updates the thread local [`ErrorContext`]. + pub fn update_thread_ctx(&self) { + ASYNCOPENCALLS + .try_with(|ctx| *ctx.borrow_mut() = *self) + .unwrap_or_else(|_| OPENCALLS.with(|ctx| *ctx.borrow_mut() = *self)); + } +} + +impl Default for ErrorContext { + fn default() -> Self { + Self::new() + } +} + +impl Display for ErrorContext { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + writeln!(f, "Originating Thread(s):")?; + for (index, ctx) in self.calls.iter().enumerate() { + if *ctx == ContextType::Empty { + break; + } + writeln!(f, "\u{1b}[0;0m{}. {}", index + 1, ctx)?; + } + Ok(()) + } +} + +/// Different types of calls that form an [`ErrorContext`] call stack. +/// +/// Complex variants store a variant of a related enum, whose variants can be built from +/// the corresponding Zellij MSPC instruction enum variants ([`ScreenInstruction`], +/// [`PtyInstruction`], [`ClientInstruction`], etc). +#[derive(Copy, Clone, PartialEq, Serialize, Deserialize)] +pub enum ContextType { + /// A screen-related call. + Screen(ScreenContext), + /// A PTY-related call. + Pty(PtyContext), + /// A plugin-related call. + Plugin(PluginContext), + /// An app-related call. + Client(ClientContext), + /// A server-related call. + IPCServer(ServerContext), + StdinHandler, + AsyncTask, + /// An empty, placeholder call. This should be thought of as representing no call at all. + /// A call stack representation filled with these is the representation of an empty call stack. + Empty, +} + +// TODO use the `colored` crate for color formatting +impl Display for ContextType { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + let purple = "\u{1b}[1;35m"; + let green = "\u{1b}[0;32m"; + match *self { + ContextType::Screen(c) => write!(f, "{}screen_thread: {}{:?}", purple, green, c), + ContextType::Pty(c) => write!(f, "{}pty_thread: {}{:?}", purple, green, c), + ContextType::Plugin(c) => write!(f, "{}plugin_thread: {}{:?}", purple, green, c), + ContextType::Client(c) => write!(f, "{}main_thread: {}{:?}", purple, green, c), + ContextType::IPCServer(c) => write!(f, "{}ipc_server: {}{:?}", purple, green, c), + ContextType::StdinHandler => { + write!(f, "{}stdin_handler_thread: {}AcceptInput", purple, green) + } + ContextType::AsyncTask => { + write!(f, "{}stream_terminal_bytes: {}AsyncTask", purple, green) + } + ContextType::Empty => write!(f, ""), + } + } +} + +// FIXME: Just deriving EnumDiscriminants from strum will remove the need for any of this!!! +/// Stack call representations corresponding to the different types of [`ScreenInstruction`]s. +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum ScreenContext { + HandlePtyBytes, + Render, + NewPane, + HorizontalSplit, + VerticalSplit, + WriteCharacter, + ResizeLeft, + ResizeRight, + ResizeDown, + ResizeUp, + SwitchFocus, + FocusNextPane, + FocusPreviousPane, + MoveFocusLeft, + MoveFocusLeftOrPreviousTab, + MoveFocusDown, + MoveFocusUp, + MoveFocusRight, + MoveFocusRightOrNextTab, + Exit, + ScrollUp, + ScrollDown, + PageScrollUp, + PageScrollDown, + ClearScroll, + CloseFocusedPane, + ToggleActiveSyncTab, + ToggleActiveTerminalFullscreen, + SetSelectable, + SetInvisibleBorders, + SetMaxHeight, + ClosePane, + ApplyLayout, + NewTab, + SwitchTabNext, + SwitchTabPrev, + CloseTab, + GoToTab, + UpdateTabName, + TerminalResize, + ChangeMode, +} + +/// Stack call representations corresponding to the different types of [`PtyInstruction`]s. +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum PtyContext { + SpawnTerminal, + SpawnTerminalVertically, + SpawnTerminalHorizontally, + NewTab, + ClosePane, + CloseTab, + Exit, +} + +/// Stack call representations corresponding to the different types of [`PluginInstruction`]s. +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum PluginContext { + Load, + Update, + Render, + Unload, + Exit, +} + +/// Stack call representations corresponding to the different types of [`ClientInstruction`]s. +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum ClientContext { + Exit, + Error, + UnblockInputThread, + Render, + ServerError, +} + +/// Stack call representations corresponding to the different types of [`ServerInstruction`]s. +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum ServerContext { + NewClient, + Render, + UnblockInputThread, + ClientExit, + Error, +} diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs new file mode 100644 index 00000000..b806aa3f --- /dev/null +++ b/zellij-utils/src/input/actions.rs @@ -0,0 +1,64 @@ +//! Definition of the actions that can be bound to keys. + +use serde::{Deserialize, Serialize}; +use zellij_tile::data::InputMode; + +/// The four directions (left, right, up, down). +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub enum Direction { + Left, + Right, + Up, + Down, +} + +/// Actions that can be bound to keys. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub enum Action { + /// Quit Zellij. + Quit, + /// Write to the terminal. + Write(Vec<u8>), + /// Switch to the specified input mode. + SwitchToMode(InputMode), + /// Resize focus pane in specified direction. + Resize(Direction), + /// Switch focus to next pane in specified direction. + FocusNextPane, + FocusPreviousPane, + /// 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. + ScrollDown, + /// Scroll up one page in focus pane. + PageScrollUp, + /// Scroll down one page in focus pane. + PageScrollDown, + /// Toggle between fullscreen focus pane and normal layout. + ToggleFocusFullscreen, + /// 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>), + /// Close the focus pane. + CloseFocus, + /// Create a new tab. + NewTab, + /// Do nothing. + NoOp, + /// Go to the next tab. + GoToNextTab, + /// Go to the previous tab. + GoToPreviousTab, + /// Close the current tab. + CloseTab, + GoToTab(u32), + TabNameInput(Vec<u8>), +} diff --git a/zellij-utils/src/input/config.rs b/zellij-utils/src/input/config.rs new file mode 100644 index 00000000..a0341689 --- /dev/null +++ b/zellij-utils/src/input/config.rs @@ -0,0 +1,218 @@ +//! Deserializes configuration options. +use std::error; +use std::fmt::{self, Display}; +use std::fs::File; +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::setup; + +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; + +const DEFAULT_CONFIG_FILE_NAME: &str = "config.yaml"; + +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, Deserialize, Serialize)] +pub struct Config { + pub keybinds: Keybinds, + pub options: Options, +} + +#[derive(Debug)] +pub enum ConfigError { + // Deserialization error + Serde(serde_yaml::Error), + // Io error + Io(io::Error), + // Io error with path context + IoPath(io::Error, PathBuf), + // Internal Deserialization Error + FromUtf8(std::string::FromUtf8Error), +} + +impl Default for Config { + fn default() -> Self { + let keybinds = Keybinds::default(); + let options = Options::default(); + Config { keybinds, options } + } +} + +impl TryFrom<&CliArgs> for Config { + type Error = ConfigError; + + fn try_from(opts: &CliArgs) -> ConfigResult { + if let Some(ref path) = opts.config { + return Config::new(&path); + } + + if let Some(ConfigCli::Setup(setup)) = opts.option.clone() { + if setup.clean { + return Config::from_default_assets(); + } + } + + let config_dir = opts + .config_dir + .clone() + .or_else(setup::find_default_config_dir); + + if let Some(ref config) = config_dir { + let path = config.join(DEFAULT_CONFIG_FILE_NAME); + if path.exists() { + Config::new(&path) + } else { + Config::from_default_assets() + } + } else { + Config::from_default_assets() + } + } +} + +impl Config { + /// Uses defaults, but lets config override them. + 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); + let options = Options::from_yaml(config_from_yaml.options); + Ok(Config { keybinds, options }) + } + + /// Deserializes from given path. + pub fn new(path: &Path) -> ConfigResult { + match File::open(path) { + Ok(mut file) => { + 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) => Err(ConfigError::IoPath(e, path.into())), + } + } + + /// Gets default configuration from assets + // TODO Deserialize the Configuration from bytes &[u8], + // once serde-yaml supports zero-copy + pub fn from_default_assets() -> ConfigResult { + Self::from_yaml(String::from_utf8(setup::DEFAULT_CONFIG.to_vec())?.as_str()) + } +} + +impl Display for ConfigError { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + match self { + 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, "Deserialization error: {}", err), + ConfigError::FromUtf8(ref err) => write!(formatter, "FromUtf8Error: {}", err), + } + } +} + +impl std::error::Error for ConfigError { + fn cause(&self) -> Option<&dyn error::Error> { + match *self { + ConfigError::Io(ref err) => Some(err), + ConfigError::IoPath(ref err, _) => Some(err), + ConfigError::Serde(ref err) => Some(err), + ConfigError::FromUtf8(ref err) => Some(err), + } + } +} + +impl From<io::Error> for ConfigError { + fn from(err: io::Error) -> ConfigError { + ConfigError::Io(err) + } +} + +impl From<serde_yaml::Error> for ConfigError { + fn from(err: serde_yaml::Error) -> ConfigError { + ConfigError::Serde(err) + } +} + +impl From<std::string::FromUtf8Error> for ConfigError { + fn from(err: std::string::FromUtf8Error) -> ConfigError { + ConfigError::FromUtf8(err) + } +} + +// The unit test location. +#[cfg(test)] +mod config_test { + use std::io::Write; + + use tempfile::tempdir; + + use super::*; + + #[test] + fn try_from_cli_args_with_config() { + let arbitrary_config = PathBuf::from("nonexistent.yaml"); + let mut opts = CliArgs::default(); + opts.config = Some(arbitrary_config); + println!("OPTS= {:?}", opts); + let result = Config::try_from(&opts); + assert!(result.is_err()); + } + + #[test] + fn try_from_cli_args_with_option_clean() { + use crate::setup::Setup; + let mut opts = CliArgs::default(); + opts.option = Some(ConfigCli::Setup(Setup { + clean: true, + ..Setup::default() + })); + let result = Config::try_from(&opts); + assert!(result.is_ok()); + } + + #[test] + fn try_from_cli_args_with_config_dir() { + let mut opts = CliArgs::default(); + let tmp = tempdir().unwrap(); + File::create(tmp.path().join(DEFAULT_CONFIG_FILE_NAME)) + .unwrap() + .write_all(b"keybinds: invalid\n") + .unwrap(); + opts.config_dir = Some(tmp.path().to_path_buf()); + let result = Config::try_from(&opts); + assert!(result.is_err()); + } + + #[test] + fn try_from_cli_args_with_config_dir_without_config() { + let mut opts = CliArgs::default(); + let tmp = tempdir().unwrap(); + opts.config_dir = Some(tmp.path().to_path_buf()); + let result = Config::try_from(&opts); + assert_eq!(result.unwrap(), Config::default()); + } + + #[test] + fn try_from_cli_args_default() { + let opts = CliArgs::default(); + let result = Config::try_from(&opts); + assert_eq!(result.unwrap(), Config::default()); + } +} diff --git a/zellij-utils/src/input/keybinds.rs b/zellij-utils/src/input/keybinds.rs new file mode 100644 index 00000000..8bef47fd --- /dev/null +++ b/zellij-utils/src/input/keybinds.rs @@ -0,0 +1,301 @@ +//! Mapping of inputs to sequences of actions. +use std::collections::HashMap; + +use super::actions::Action; +use super::config; + +use serde::{Deserialize, Serialize}; +use strum::IntoEnumIterator; +use zellij_tile::data::*; + +/// Used in the config struct +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct Keybinds(HashMap<InputMode, ModeKeybinds>); +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct ModeKeybinds(HashMap<Key, Vec<Action>>); + +/// Intermediate struct used for deserialisation +/// Used in the config file. +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct KeybindsFromYaml { + #[serde(flatten)] + keybinds: HashMap<InputMode, Vec<KeyActionUnbind>>, + #[serde(default)] + unbind: Unbind, +} + +/// Intermediate enum used for deserialisation +#[derive(Clone, Debug, PartialEq, Deserialize)] +#[serde(untagged)] +enum KeyActionUnbind { + KeyAction(KeyActionFromYaml), + Unbind(UnbindFromYaml), +} + +/// Intermediate struct used for deserialisation +#[derive(Clone, Debug, PartialEq, Deserialize)] +struct KeyActionUnbindFromYaml { + keybinds: Vec<KeyActionFromYaml>, + unbind: Unbind, +} + +/// Intermediate struct used for deserialisation +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct KeyActionFromYaml { + action: Vec<Action>, + key: Vec<Key>, +} + +/// Intermediate struct used for deserialisation +#[derive(Clone, Debug, PartialEq, Deserialize)] +struct UnbindFromYaml { + unbind: Unbind, +} + +/// List of keys, for which to disable their respective default actions +/// `All` is a catch all, and will disable the default actions for all keys. +#[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), +} + +impl Default for Keybinds { + // Use once per codepath + // TODO investigate why + fn default() -> Keybinds { + Self::from_default_assets() + } +} + +impl Keybinds { + pub fn new() -> 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) | Unbind::Keys(_) => Keybinds::unbind(keybinds), + }, + None => Keybinds::default(), + }; + + if let Some(keybinds) = from_yaml { + default_keybinds.merge_keybinds(Keybinds::from(keybinds)) + } else { + default_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 { + let mut keybinds = Keybinds::new(); + + 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()); + }; + if let Some(keybind) = other.0.get(&mode) { + mode_keybinds.0.extend(keybind.0.clone()); + } + if !mode_keybinds.0.is_empty() { + keybinds.0.insert(mode, mode_keybinds); + } + } + keybinds + } + + /// Converts a [`Key`] terminal event to a sequence of [`Action`]s according to the current + /// [`InputMode`] and [`Keybinds`]. + pub fn key_to_actions( + key: &Key, + input: Vec<u8>, + mode: &InputMode, + keybinds: &Keybinds, + ) -> Vec<Action> { + 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 | InputMode::Locked => mode_keybind_or_action(Action::Write(input)), + InputMode::RenameTab => mode_keybind_or_action(Action::TabNameInput(input)), + _ => mode_keybind_or_action(Action::NoOp), + } + } +} + +impl ModeKeybinds { + fn new() -> ModeKeybinds { + ModeKeybinds(HashMap::<Key, Vec<Action>>::new()) + } + + /// Merges `self` with `other`, if keys are the same, `other` overwrites. + fn merge(self, other: ModeKeybinds) -> ModeKeybinds { + let mut merged = self; + 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 { + fn from(keybinds_from_yaml: KeybindsFromYaml) -> Keybinds { + let mut keybinds = Keybinds::new(); + + for mode in InputMode::iter() { + let mut mode_keybinds = ModeKeybinds::new(); + for key_action in keybinds_from_yaml.keybinds.get(&mode).iter() { + for keybind in key_action.iter() { + mode_keybinds = mode_keybinds.merge(ModeKeybinds::from(keybind.clone())); + } + } + keybinds.0.insert(mode, mode_keybinds); + } + keybinds + } +} + +/// 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 actions = key_action.action; + + ModeKeybinds( + key_action + .key + .into_iter() + .map(|k| (k, actions.clone())) + .collect::<HashMap<Key, Vec<Action>>>(), + ) + } +} + +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 + } +} + +impl Default for Unbind { + fn default() -> Unbind { + Unbind::All(false) + } +} + +// The unit test location. +#[cfg(test)] +#[path = "./unit/keybinds_test.rs"] +mod keybinds_test; diff --git a/zellij-utils/src/input/mod.rs b/zellij-utils/src/input/mod.rs new file mode 100644 index 00000000..2747205b --- /dev/null +++ b/zellij-utils/src/input/mod.rs @@ -0,0 +1,87 @@ +//! The way terminal input is handled. + +pub mod actions; +pub mod config; +pub mod keybinds; +pub mod options; + +use termion::input::TermRead; +use zellij_tile::data::{InputMode, Key, ModeInfo, Palette, PluginCapabilities}; + +/// 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, + capabilities: PluginCapabilities, +) -> ModeInfo { + let mut keybinds: Vec<(String, String)> = vec![]; + match mode { + InputMode::Normal | InputMode::Locked => {} + InputMode::Resize => { + keybinds.push(("←↓↑→".to_string(), "Resize".to_string())); + } + InputMode::Pane => { + keybinds.push(("←↓↑→".to_string(), "Move focus".to_string())); + keybinds.push(("p".to_string(), "Next".to_string())); + keybinds.push(("n".to_string(), "New".to_string())); + 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(("f".to_string(), "Fullscreen".to_string())); + } + InputMode::Tab => { + keybinds.push(("←↓↑→".to_string(), "Move focus".to_string())); + 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())); + keybinds.push(("PgUp/PgDn".to_string(), "Scroll Page".to_string())); + } + InputMode::RenameTab => { + keybinds.push(("Enter".to_string(), "when done".to_string())); + } + } + ModeInfo { + mode, + keybinds, + palette, + capabilities, + } +} + +pub fn parse_keys(input_bytes: &[u8]) -> Vec<Key> { + input_bytes.keys().flatten().map(cast_termion_key).collect() +} + +// FIXME: This is an absolutely cursed function that should be destroyed as soon +// as an alternative that doesn't touch zellij-tile can be developed... +pub fn cast_termion_key(event: termion::event::Key) -> Key { + match event { + termion::event::Key::Backspace => Key::Backspace, + termion::event::Key::Left => Key::Left, + termion::event::Key::Right => Key::Right, + termion::event::Key::Up => Key::Up, + termion::event::Key::Down => Key::Down, + termion::event::Key::Home => Key::Home, + termion::event::Key::End => Key::End, + termion::event::Key::PageUp => Key::PageUp, + termion::event::Key::PageDown => Key::PageDown, + termion::event::Key::BackTab => Key::BackTab, + termion::event::Key::Delete => Key::Delete, + termion::event::Key::Insert => Key::Insert, + termion::event::Key::F(n) => Key::F(n), + termion::event::Key::Char(c) => Key::Char(c), + termion::event::Key::Alt(c) => Key::Alt(c), + termion::event::Key::Ctrl(c) => Key::Ctrl(c), + termion::event::Key::Null => Key::Null, + termion::event::Key::Esc => Key::Esc, + _ => { + unimplemented!("Encountered an unknown key!") + } + } +} diff --git a/zellij-utils/src/input/options.rs b/zellij-utils/src/input/options.rs new file mode 100644 index 00000000..33625b65 --- /dev/null +++ b/zellij-utils/src/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/zellij-utils/src/input/unit/keybinds_test.rs b/zellij-utils/src/input/unit/keybinds_test.rs new file mode 100644 index 00000000..4767fc15 --- /dev/null +++ b/zellij-utils/src/input/unit/keybinds_test.rs @@ -0,0 +1,801 @@ +use super::super::actions::*; +use super::super::keybinds::*; +use zellij_tile::data::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) + ) +} + +#[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 toplevel_unbind_unbinds_all() { + let from_yaml = KeybindsFromYaml { + unbind: Unbind::All(true), + keybinds: HashMap::new(), + }; + + let keybinds_from_yaml = Keybinds::get_default_keybinds_with_config(Some(from_yaml)); + + assert_eq!(keybinds_from_yaml, Keybinds::new()); +} + +#[test] +fn no_unbind_unbinds_none() { + let from_yaml = KeybindsFromYaml { + unbind: Unbind::All(false), + keybinds: HashMap::new(), + }; + + 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/zellij-utils/src/ipc.rs b/zellij-utils/src/ipc.rs new file mode 100644 index 00000000..725bf16b --- /dev/null +++ b/zellij-utils/src/ipc.rs @@ -0,0 +1,140 @@ +//! IPC stuff for starting to split things into a client and server model. + +use crate::cli::CliArgs; +use crate::pane_size::PositionAndSize; +use crate::{ + errors::{get_current_ctx, ErrorContext}, + input::{actions::Action, options::Options}, +}; +use interprocess::local_socket::LocalSocketStream; +use nix::unistd::dup; +use serde::{Deserialize, Serialize}; +use std::io::{self, Write}; +use std::marker::PhantomData; +use std::os::unix::io::{AsRawFd, FromRawFd}; + +use zellij_tile::data::Palette; + +type SessionId = u64; + +#[derive(PartialEq, Eq, Serialize, Deserialize, Hash)] +pub struct Session { + // Unique ID for this session + id: SessionId, + // Identifier for the underlying IPC primitive (socket, pipe) + conn_name: String, + // User configured alias for the session + alias: String, +} + +// How do we want to connect to a session? +#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ClientType { + Reader, + Writer, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ClientAttributes { + pub position_and_size: PositionAndSize, + pub palette: Palette, +} + +// Types of messages sent from the client to the server +#[allow(clippy::large_enum_variant)] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum ClientToServerMsg { + /*// List which sessions are available + ListSessions, + // Create a new session + CreateSession, + // Attach to a running session + AttachToSession(SessionId, ClientType), + // Force detach + DetachSession(SessionId), + // Disconnect from the session we're connected to + DisconnectFromSession,*/ + ClientExit, + TerminalResize(PositionAndSize), + NewClient(ClientAttributes, Box<CliArgs>, Box<Options>), + Action(Action), +} + +// Types of messages sent from the server to the client +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum ServerToClientMsg { + /*// Info about a particular session + SessionInfo(Session), + // A list of sessions + SessionList(HashSet<Session>),*/ + Render(Option<String>), + UnblockInputThread, + Exit, + ServerError(String), +} + +/// Sends messages on a stream socket, along with an [`ErrorContext`]. +pub struct IpcSenderWithContext<T: Serialize> { + sender: io::BufWriter<LocalSocketStream>, + _phantom: PhantomData<T>, +} + +impl<T: Serialize> IpcSenderWithContext<T> { + /// Returns a sender to the given [LocalSocketStream](interprocess::local_socket::LocalSocketStream). + pub fn new(sender: LocalSocketStream) -> Self { + Self { + sender: io::BufWriter::new(sender), + _phantom: PhantomData, + } + } + + /// Sends an event, along with the current [`ErrorContext`], on this [`IpcSenderWithContext`]'s socket. + pub fn send(&mut self, msg: T) { + let err_ctx = get_current_ctx(); + bincode::serialize_into(&mut self.sender, &(msg, err_ctx)).unwrap(); + self.sender.flush().unwrap(); + } + + /// Returns an [`IpcReceiverWithContext`] with the same socket as this sender. + pub fn get_receiver<F>(&self) -> IpcReceiverWithContext<F> + where + F: for<'de> Deserialize<'de> + Serialize, + { + let sock_fd = self.sender.get_ref().as_raw_fd(); + let dup_sock = dup(sock_fd).unwrap(); + let socket = unsafe { LocalSocketStream::from_raw_fd(dup_sock) }; + IpcReceiverWithContext::new(socket) + } +} + +/// Receives messages on a stream socket, along with an [`ErrorContext`]. +pub struct IpcReceiverWithContext<T> { + receiver: io::BufReader<LocalSocketStream>, + _phantom: PhantomData<T>, +} + +impl<T> IpcReceiverWithContext<T> +where + T: for<'de> Deserialize<'de> + Serialize, +{ + /// Returns a receiver to the given [LocalSocketStream](interprocess::local_socket::LocalSocketStream). + pub fn new(receiver: LocalSocketStream) -> Self { + Self { + receiver: io::BufReader::new(receiver), + _phantom: PhantomData, + } + } + + /// Receives an event, along with the current [`ErrorContext`], on this [`IpcReceiverWithContext`]'s socket. + pub fn recv(&mut self) -> (T, ErrorContext) { + bincode::deserialize_from(&mut self.receiver).unwrap() + } + + /// Returns an [`IpcSenderWithContext`] with the same socket as this receiver. + pub fn get_sender<F: Serialize>(&self) -> IpcSenderWithContext<F> { + let sock_fd = self.receiver.get_ref().as_raw_fd(); + let dup_sock = dup(sock_fd).unwrap(); + let socket = unsafe { LocalSocketStream::from_raw_fd(dup_sock) }; + IpcSenderWithContext::new(socket) + } +} diff --git a/zellij-utils/src/lib.rs b/zellij-utils/src/lib.rs new file mode 100644 index 00000000..35f27244 --- /dev/null +++ b/zellij-utils/src/lib.rs @@ -0,0 +1,10 @@ +pub mod channels; +pub mod cli; +pub mod consts; +pub mod errors; +pub mod input; +pub mod ipc; +pub mod logging; +pub mod pane_size; +pub mod setup; +pub mod shared; diff --git a/zellij-utils/src/logging.rs b/zellij-utils/src/logging.rs new file mode 100644 index 00000000..d2c57612 --- /dev/null +++ b/zellij-utils/src/logging.rs @@ -0,0 +1,85 @@ +//! Zellij logging utility functions. + +use std::{ + fs, + io::{self, prelude::*}, + os::unix::io::RawFd, + path::{Path, PathBuf}, +}; + +use crate::consts::{ZELLIJ_TMP_LOG_DIR, ZELLIJ_TMP_LOG_FILE}; +use crate::shared::set_permissions; + +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<()> { + let result = if let Err(e) = fs::create_dir(dir_name) { + if e.kind() == std::io::ErrorKind::AlreadyExists { + Ok(()) + } else { + Err(e) + } + } else { + Ok(()) + }; + if result.is_ok() { + set_permissions(dir_name)?; + } + result +} + +pub fn debug_log_to_file(mut message: String) -> io::Result<()> { + message.push('\n'); + debug_log_to_file_without_newline(message) +} + +pub fn debug_log_to_file_without_newline(message: String) -> io::Result<()> { + atomic_create_file(&*ZELLIJ_TMP_LOG_FILE)?; + let mut file = fs::OpenOptions::new() + .append(true) + .open(&*ZELLIJ_TMP_LOG_FILE)?; + file.write_all(message.as_bytes()) +} + +pub fn _debug_log_to_file_pid_3(message: String, pid: RawFd) -> io::Result<()> { + if pid == 3 { + debug_log_to_file(message) + } else { + Ok(()) + } +} + +pub fn _delete_log_file() -> io::Result<()> { + if fs::metadata(&*ZELLIJ_TMP_LOG_FILE).is_ok() { + fs::remove_file(&*ZELLIJ_TMP_LOG_FILE) + } else { + Ok(()) + } +} + +pub fn _delete_log_dir() -> io::Result<()> { + if fs::metadata(&*ZELLIJ_TMP_LOG_DIR).is_ok() { + fs::remove_dir_all(&*ZELLIJ_TMP_LOG_DIR) + } else { + Ok(()) + } +} + +pub fn debug_to_file(message: u8, pid: RawFd) -> io::Result<()> { + let mut path = PathBuf::new(); + path.push(&*ZELLIJ_TMP_LOG_DIR); + path.push(format!("zellij-{}.log", pid.to_string())); + + let mut file = fs::OpenOptions::new() + .append(true) + .create(true) + .open(&path)?; + set_permissions(&path)?; + file.write_all(&[message]) +} diff --git a/zellij-utils/src/pane_size.rs b/zellij-utils/src/pane_size.rs new file mode 100644 index 00000000..2744c43f --- /dev/null +++ b/zellij-utils/src/pane_size.rs @@ -0,0 +1,24 @@ +use nix::pty::Winsize; +use serde::{Deserialize, Serialize}; + +/// Contains the position and size of a [`Pane`], or more generally of any terminal, measured +/// in character rows and columns. +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)] +pub struct PositionAndSize { + pub x: usize, + pub y: usize, + pub rows: usize, + pub columns: usize, + pub max_rows: Option<usize>, + pub max_columns: Option<usize>, +} + +impl From<Winsize> for PositionAndSize { + fn from(winsize: Winsize) -> PositionAndSize { + PositionAndSize { + columns: winsize.ws_col as usize, + rows: winsize.ws_row as usize, + ..Default::default() + } + } +} diff --git a/zellij-utils/src/setup.rs b/zellij-utils/src/setup.rs new file mode 100644 index 00000000..ef51b2e5 --- /dev/null +++ b/zellij-utils/src/setup.rs @@ -0,0 +1,240 @@ +use crate::cli::CliArgs; +use crate::consts::{ + FEATURES, SYSTEM_DEFAULT_CONFIG_DIR, SYSTEM_DEFAULT_DATA_DIR_PREFIX, VERSION, ZELLIJ_PROJ_DIR, +}; +use crate::shared::set_permissions; +use directories_next::BaseDirs; +use serde::{Deserialize, Serialize}; +use std::io::Write; +use std::{fs, path::Path, path::PathBuf}; +use structopt::StructOpt; + +const CONFIG_LOCATION: &str = ".config/zellij"; +const CONFIG_NAME: &str = "config.yaml"; +static ARROW_SEPARATOR: &str = ""; + +#[macro_export] +macro_rules! asset_map { + ($($src:literal => $dst:literal),+ $(,)?) => { + { + let mut assets = std::collections::HashMap::new(); + $( + assets.insert($dst, include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/", $src)).to_vec()); + )+ + assets + } + } +} + +pub mod install { + use super::*; + + pub fn populate_data_dir(data_dir: &Path) { + // First run installation of default plugins & layouts + let mut assets = asset_map! { + "../assets/layouts/default.yaml" => "layouts/default.yaml", + "../assets/layouts/strider.yaml" => "layouts/strider.yaml", + }; + assets.extend(asset_map! { + "../assets/plugins/status-bar.wasm" => "plugins/status-bar.wasm", + "../assets/plugins/tab-bar.wasm" => "plugins/tab-bar.wasm", + "../assets/plugins/strider.wasm" => "plugins/strider.wasm", + }); + assets.insert("VERSION", VERSION.as_bytes().to_vec()); + + let last_version = fs::read_to_string(data_dir.join("VERSION")).unwrap_or_default(); + let out_of_date = VERSION != last_version; + + for (path, bytes) in assets { + let path = data_dir.join(path); + let parent_path = path.parent().unwrap(); + fs::create_dir_all(parent_path).unwrap(); + set_permissions(parent_path).unwrap(); + if out_of_date || !path.exists() { + fs::write(path, bytes).expect("Failed to install default assets!"); + } + } + } +} + +#[cfg(not(any(feature = "test", test)))] +/// Goes through a predefined list and checks for an already +/// existing config directory, returns the first match +pub fn find_default_config_dir() -> Option<PathBuf> { + default_config_dirs() + .into_iter() + .filter(|p| p.is_some()) + .find(|p| p.clone().unwrap().exists()) + .flatten() +} + +#[cfg(any(feature = "test", test))] +pub fn find_default_config_dir() -> Option<PathBuf> { + None +} + +/// Order in which config directories are checked +fn default_config_dirs() -> Vec<Option<PathBuf>> { + vec![ + home_config_dir(), + Some(xdg_config_dir()), + Some(Path::new(SYSTEM_DEFAULT_CONFIG_DIR).to_path_buf()), + ] +} + +/// Looks for an existing dir, uses that, else returns a +/// dir matching the config spec. +pub fn get_default_data_dir() -> PathBuf { + vec![ + xdg_data_dir(), + Path::new(SYSTEM_DEFAULT_DATA_DIR_PREFIX).join("share/zellij"), + ] + .into_iter() + .find(|p| p.exists()) + .unwrap_or_else(xdg_data_dir) +} + +pub fn xdg_config_dir() -> PathBuf { + ZELLIJ_PROJ_DIR.config_dir().to_owned() +} + +pub fn xdg_data_dir() -> PathBuf { + ZELLIJ_PROJ_DIR.data_dir().to_owned() +} + +pub fn home_config_dir() -> Option<PathBuf> { + if let Some(user_dirs) = BaseDirs::new() { + let config_dir = user_dirs.home_dir().join(CONFIG_LOCATION); + Some(config_dir) + } else { + None + } +} + +pub fn dump_asset(asset: &[u8]) -> std::io::Result<()> { + std::io::stdout().write_all(&asset)?; + Ok(()) +} + +pub const DEFAULT_CONFIG: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/", + "../assets/config/default.yaml" +)); + +pub fn dump_default_config() -> std::io::Result<()> { + dump_asset(DEFAULT_CONFIG) +} + +#[derive(Debug, Default, Clone, StructOpt, Serialize, Deserialize)] +pub struct Setup { + /// Dump the default configuration file to stdout + #[structopt(long)] + pub dump_config: bool, + /// Disables loading of configuration file at default location, + /// loads the defaults that zellij ships with + #[structopt(long)] + pub clean: bool, + /// Checks the configuration of zellij and displays + /// currently used directories + #[structopt(long)] + pub check: bool, + + #[structopt(long)] + pub generate_completion: Option<String>, +} + +impl Setup { + /// Entrypoint from main + pub fn from_cli(&self, opts: CliArgs) -> std::io::Result<()> { + if self.dump_config { + dump_default_config()?; + } + + if self.check { + Setup::check_defaults_config(opts)?; + } + + if let Some(shell) = &self.generate_completion { + Self::generate_completion(shell.into()); + } + + Ok(()) + } + + pub fn check_defaults_config(opts: CliArgs) -> std::io::Result<()> { + let data_dir = opts.data_dir.unwrap_or_else(get_default_data_dir); + let config_dir = opts.config_dir.or_else(find_default_config_dir); + let plugin_dir = data_dir.join("plugins"); + let layout_dir = data_dir.join("layouts"); + let system_data_dir = PathBuf::from(SYSTEM_DEFAULT_DATA_DIR_PREFIX).join("share/zellij"); + let config_file = opts + .config + .or_else(|| config_dir.clone().map(|p| p.join(CONFIG_NAME))); + + let mut message = String::new(); + + message.push_str(&format!("[Version]: {:?}\n", VERSION)); + if let Some(config_dir) = config_dir { + message.push_str(&format!("[CONFIG DIR]: {:?}\n", config_dir)); + } else { + message.push_str(&"[CONFIG DIR]: Not Found\n"); + let mut default_config_dirs = default_config_dirs() + .iter() + .filter_map(|p| p.clone()) + .collect::<Vec<PathBuf>>(); + default_config_dirs.dedup(); + message.push_str( + &" On your system zellij looks in the following config directories by default:\n", + ); + for dir in default_config_dirs { + message.push_str(&format!(" {:?}\n", dir)); + } + } + if let Some(config_file) = config_file { + use crate::input::config::Config; + message.push_str(&format!("[CONFIG FILE]: {:?}\n", config_file)); + match Config::new(&config_file) { + Ok(_) => message.push_str(&"[CONFIG FILE]: Well defined.\n"), + Err(e) => message.push_str(&format!("[CONFIG ERROR]: {}\n", e)), + } + } else { + message.push_str(&"[CONFIG FILE]: Not Found\n"); + message.push_str(&format!( + " By default zellij looks for a file called [{}] in the configuration directory\n", + CONFIG_NAME + )); + } + message.push_str(&format!("[DATA DIR]: {:?}\n", data_dir)); + message.push_str(&format!("[PLUGIN DIR]: {:?}\n", plugin_dir)); + message.push_str(&format!("[LAYOUT DIR]: {:?}\n", layout_dir)); + message.push_str(&format!("[SYSTEM DATA DIR]: {:?}\n", system_data_dir)); + + message.push_str(&format!("[ARROW SEPARATOR]: {}\n", ARROW_SEPARATOR)); + message.push_str(&" Is the [ARROW_SEPARATOR] displayed correctly?\n"); + message.push_str(&" If not you may want to either start zellij with a compatible mode 'zellij options --simple-ui'\n"); + message.push_str(&" Or check the font that is in use:\n https://zellij.dev/documentation/compatibility.html#the-status-bar-fonts-dont-render-correctly\n"); + + message.push_str(&format!("[FEATURES]: {:?}\n", FEATURES)); + message.push_str(&"[DOCUMENTATION]: zellij.dev/documentation/\n"); + + std::io::stdout().write_all(message.as_bytes())?; + + Ok(()) + } + fn generate_completion(shell: String) { + let shell = match shell.as_ref() { + "bash" => structopt::clap::Shell::Bash, + "fish" => structopt::clap::Shell::Fish, + "zsh" => structopt::clap::Shell::Zsh, + "powerShell" => structopt::clap::Shell::PowerShell, + "elvish" => structopt::clap::Shell::Elvish, + other => { + eprintln!("Unsupported shell: {}", other); + std::process::exit(1); + } + }; + let mut out = std::io::stdout(); + CliArgs::clap().gen_completions_to("zellij", shell, &mut out); + } +} diff --git a/zellij-utils/src/shared.rs b/zellij-utils/src/shared.rs new file mode 100644 index 00000000..bc8b6771 --- /dev/null +++ b/zellij-utils/src/shared.rs @@ -0,0 +1,119 @@ +//! Some general utility functions. + +use std::{iter, str::from_utf8}; + +use strip_ansi_escapes::strip; + +use colors_transform::{Color, Rgb}; +use std::os::unix::fs::PermissionsExt; +use std::path::Path; +use std::{fs, io}; +use zellij_tile::data::{Palette, PaletteColor, PaletteSource, Theme}; + +const UNIX_PERMISSIONS: u32 = 0o700; + +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) +} + +fn ansi_len(s: &str) -> usize { + from_utf8(&strip(s.as_bytes()).unwrap()) + .unwrap() + .chars() + .count() +} + +pub fn adjust_to_size(s: &str, rows: usize, columns: usize) -> String { + s.lines() + .map(|l| { + let actual_len = ansi_len(l); + if actual_len > columns { + let mut line = String::from(l); + line.truncate(columns); + line + } else { + [l, &str::repeat(" ", columns - ansi_len(l))].concat() + } + }) + .chain(iter::repeat(str::repeat(" ", columns))) + .take(rows) + .collect::<Vec<_>>() + .join("\n\r") +} + +// Colors +pub mod colors { + pub const WHITE: u8 = 255; + pub const GREEN: u8 = 154; + pub const GRAY: u8 = 238; + pub const BRIGHT_GRAY: u8 = 245; + pub const RED: u8 = 88; + pub const ORANGE: u8 = 166; + pub const BLACK: u8 = 16; +} + +pub fn _hex_to_rgb(hex: &str) -> (u8, u8, u8) { + let rgb = Rgb::from_hex_str(hex) + .expect("The passed argument must be a valid hex color") + .as_tuple(); + (rgb.0 as u8, rgb.1 as u8, rgb.2 as u8) +} + +pub fn default_palette() -> Palette { + Palette { + source: PaletteSource::Default, + theme: Theme::Dark, + fg: PaletteColor::EightBit(colors::BRIGHT_GRAY), + bg: PaletteColor::EightBit(colors::GRAY), + black: PaletteColor::EightBit(colors::BLACK), + red: PaletteColor::EightBit(colors::RED), + green: PaletteColor::EightBit(colors::GREEN), + yellow: PaletteColor::EightBit(colors::GRAY), + blue: PaletteColor::EightBit(colors::GRAY), + magenta: PaletteColor::EightBit(colors::GRAY), + cyan: PaletteColor::EightBit(colors::GRAY), + white: PaletteColor::EightBit(colors::WHITE), + orange: PaletteColor::EightBit(colors::ORANGE), + } +} + +// Dark magic +pub fn _detect_theme(bg: PaletteColor) -> Theme { + match bg { + PaletteColor::Rgb((r, g, b)) => { + // HSP, P stands for perceived brightness + let hsp: f64 = (0.299 * (r as f64 * r as f64) + + 0.587 * (g as f64 * g as f64) + + 0.114 * (b as f64 * b as f64)) + .sqrt(); + match hsp > 127.5 { + true => Theme::Light, + false => Theme::Dark, + } + } + _ => Theme::Dark, + } +} + +// (this was shamelessly copied from alacritty) +// +// This returns the current terminal version as a unique number based on the +// semver version. The different versions are padded to ensure that a higher semver version will +// always report a higher version number. +pub fn version_number(mut version: &str) -> usize { + if let Some(separator) = version.rfind('-') { + version = &version[..separator]; + } + + let mut version_number = 0; + + let semver_versions = version.split('.'); + for (i, semver_version) in semver_versions.rev().enumerate() { + let semver_number = semver_version.parse::<usize>().unwrap_or(0); + version_number += usize::pow(100, i as u32) * semver_number; + } + + version_number +} |
