aboutsummaryrefslogtreecommitdiffstats
path: root/zellij-utils
diff options
context:
space:
mode:
authora-kenji <aks.kenji@protonmail.com>2021-05-18 10:05:15 +0200
committera-kenji <aks.kenji@protonmail.com>2021-05-18 10:05:15 +0200
commitbcbde9fbb5bffe4e1334acc1270314517938cac5 (patch)
tree8a18820a32392fb4a8a132a27740299ac4849f84 /zellij-utils
parentFix Clippy Suggestions (diff)
parentMerge pull request #510 from zellij-org/speedup-ci (diff)
downloadsl0ppij-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.toml36
-rw-r--r--zellij-utils/src/channels.rs65
-rw-r--r--zellij-utils/src/cli.rs55
-rw-r--r--zellij-utils/src/consts.rs54
-rw-r--r--zellij-utils/src/errors.rs263
-rw-r--r--zellij-utils/src/input/actions.rs64
-rw-r--r--zellij-utils/src/input/config.rs218
-rw-r--r--zellij-utils/src/input/keybinds.rs301
-rw-r--r--zellij-utils/src/input/mod.rs87
-rw-r--r--zellij-utils/src/input/options.rs45
-rw-r--r--zellij-utils/src/input/unit/keybinds_test.rs801
-rw-r--r--zellij-utils/src/ipc.rs140
-rw-r--r--zellij-utils/src/lib.rs10
-rw-r--r--zellij-utils/src/logging.rs85
-rw-r--r--zellij-utils/src/pane_size.rs24
-rw-r--r--zellij-utils/src/setup.rs240
-rw-r--r--zellij-utils/src/shared.rs119
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
+}