summaryrefslogtreecommitdiffstats
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
parentdc067580f3df50bff17aee48fb330c164084c6bd (diff)
parent8c3bf215b7c56fb1254e55ac182233ac488f3113 (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.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 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 {