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 | dc067580f3df50bff17aee48fb330c164084c6bd (diff) | |
parent | 8c3bf215b7c56fb1254e55ac182233ac488f3113 (diff) |
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 000000000..d33ac659d --- /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 000000000..8a97fa7d8 --- /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 000000000..56e93edb3 --- /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 000000000..799e29132 --- /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 000000000..94a894c46 --- /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 000000000..b806aa3fc --- /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 000000000..a03416890 --- /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 { |