summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authora-kenji <aks.kenji@protonmail.com>2021-11-14 22:58:20 +0100
committerGitHub <noreply@github.com>2021-11-14 22:58:20 +0100
commit347e02ea35793ba7e898ca0693f569a21525d459 (patch)
treebeab3ed6f4102b96cffa8c7c7cfbaaa33d794943
parent96315ed332438374743d73d3e3b38c3cdbdc3331 (diff)
feature(layout): add layout config (#866)
feature(layout): add layout config (#866) * It is now possible to configure zellij through a layout: The config file and the layout file will be merged, on conflicting options the order is as follows: 1. config options `zellij options` 2. layout 3. config Example: ``` --- template: direction: Horizontal parts: - direction: Vertical body: true - direction: Vertical borderless: true split_size: Fixed: 1 run: plugin: location: "zellij:tab-bar" default_shell: fish ```
-rw-r--r--zellij-utils/src/input/config.rs16
-rw-r--r--zellij-utils/src/input/keybinds.rs12
-rw-r--r--zellij-utils/src/input/layout.rs173
-rw-r--r--zellij-utils/src/input/plugins.rs8
-rw-r--r--zellij-utils/src/input/theme.rs8
-rw-r--r--zellij-utils/src/setup.rs134
6 files changed, 336 insertions, 15 deletions
diff --git a/zellij-utils/src/input/config.rs b/zellij-utils/src/input/config.rs
index 752e63e35..4c3765a27 100644
--- a/zellij-utils/src/input/config.rs
+++ b/zellij-utils/src/input/config.rs
@@ -5,7 +5,7 @@ use std::io::{self, Read};
use std::path::{Path, PathBuf};
use thiserror::Error;
-use serde::Deserialize;
+use serde::{Deserialize, Serialize};
use std::convert::{TryFrom, TryInto};
use super::keybinds::{Keybinds, KeybindsFromYaml};
@@ -20,7 +20,7 @@ const DEFAULT_CONFIG_FILE_NAME: &str = "config.yaml";
type ConfigResult = Result<Config, ConfigError>;
/// Intermediate deserialization config struct
-#[derive(Clone, Debug, Deserialize)]
+#[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq)]
pub struct ConfigFromYaml {
#[serde(flatten)]
pub options: Option<Options>,
@@ -151,6 +151,18 @@ impl Config {
let cfg = String::from_utf8(setup::DEFAULT_CONFIG.to_vec())?;
Self::from_yaml(cfg.as_str())
}
+
+ /// Merges two Config structs into one Config struct
+ /// `other` overrides `self`.
+ pub fn merge(&self, other: Self) -> Self {
+ //let themes = if let Some()
+ Self {
+ keybinds: self.keybinds.merge_keybinds(other.keybinds),
+ options: self.options.merge(other.options),
+ themes: None,
+ plugins: self.plugins.merge(other.plugins),
+ }
+ }
}
impl TryFrom<ConfigFromYaml> for Config {
diff --git a/zellij-utils/src/input/keybinds.rs b/zellij-utils/src/input/keybinds.rs
index d3d0c7051..e3f88b1a0 100644
--- a/zellij-utils/src/input/keybinds.rs
+++ b/zellij-utils/src/input/keybinds.rs
@@ -16,7 +16,7 @@ pub struct ModeKeybinds(HashMap<Key, Vec<Action>>);
/// Intermediate struct used for deserialisation
/// Used in the config file.
-#[derive(Clone, Debug, PartialEq, Deserialize)]
+#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct KeybindsFromYaml {
#[serde(flatten)]
keybinds: HashMap<InputMode, Vec<KeyActionUnbind>>,
@@ -25,7 +25,7 @@ pub struct KeybindsFromYaml {
}
/// Intermediate enum used for deserialisation
-#[derive(Clone, Debug, PartialEq, Deserialize)]
+#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(untagged)]
enum KeyActionUnbind {
KeyAction(KeyActionFromYaml),
@@ -40,21 +40,21 @@ struct KeyActionUnbindFromYaml {
}
/// Intermediate struct used for deserialisation
-#[derive(Clone, Debug, PartialEq, Deserialize)]
+#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct KeyActionFromYaml {
action: Vec<Action>,
key: Vec<Key>,
}
/// Intermediate struct used for deserialisation
-#[derive(Clone, Debug, PartialEq, Deserialize)]
+#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
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)]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(untagged)]
enum Unbind {
// This is the correct order, don't rearrange!
@@ -168,7 +168,7 @@ impl Keybinds {
/// Merges two Keybinds structs into one Keybinds struct
/// `other` overrides the ModeKeybinds of `self`.
- fn merge_keybinds(&self, other: Keybinds) -> Keybinds {
+ pub fn merge_keybinds(&self, other: Keybinds) -> Keybinds {
let mut keybinds = Keybinds::new();
for mode in InputMode::iter() {
diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs
index df976b8e1..e06f20018 100644
--- a/zellij-utils/src/input/layout.rs
+++ b/zellij-utils/src/input/layout.rs
@@ -18,7 +18,10 @@ use crate::{
};
use crate::{serde, serde_yaml};
-use super::plugins::{PluginTag, PluginsConfigError};
+use super::{
+ config::ConfigFromYaml,
+ plugins::{PluginTag, PluginsConfigError},
+};
use serde::{Deserialize, Serialize};
use std::convert::{TryFrom, TryInto};
use std::vec::Vec;
@@ -138,6 +141,26 @@ pub struct Layout {
}
// The struct that is used to deserialize the layout from
+// a yaml configuration file, is needed because of:
+// https://github.com/bincode-org/bincode/issues/245
+// flattened fields don't retain size information.
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
+#[serde(crate = "self::serde")]
+#[serde(default)]
+pub struct LayoutFromYamlIntermediate {
+ #[serde(default)]
+ pub template: LayoutTemplate,
+ #[serde(default)]
+ pub borderless: bool,
+ #[serde(default)]
+ pub tabs: Vec<TabLayout>,
+ #[serde(default)]
+ pub session: SessionFromYaml,
+ #[serde(flatten)]
+ pub config: Option<ConfigFromYaml>,
+}
+
+// The struct that is used to deserialize the layout from
// a yaml configuration file
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(crate = "self::serde")]
@@ -153,6 +176,124 @@ pub struct LayoutFromYaml {
pub tabs: Vec<TabLayout>,
}
+type LayoutFromYamlIntermediateResult = Result<LayoutFromYamlIntermediate, ConfigError>;
+
+impl LayoutFromYamlIntermediate {
+ pub fn from_path(layout_path: &Path) -> LayoutFromYamlIntermediateResult {
+ let mut layout_file = File::open(&layout_path)
+ .or_else(|_| File::open(&layout_path.with_extension("yaml")))
+ .map_err(|e| ConfigError::IoPath(e, layout_path.into()))?;
+
+ let mut layout = String::new();
+ layout_file.read_to_string(&mut layout)?;
+ let layout: Option<LayoutFromYamlIntermediate> = match serde_yaml::from_str(&layout) {
+ Err(e) => {
+ // needs direct check, as `[ErrorImpl]` is private
+ // https://github.com/dtolnay/serde-yaml/issues/121
+ if layout.is_empty() {
+ return Ok(LayoutFromYamlIntermediate::default());
+ }
+ return Err(ConfigError::Serde(e));
+ }
+ Ok(config) => config,
+ };
+
+ match layout {
+ Some(layout) => {
+ for tab in layout.tabs.clone() {
+ tab.check()?;
+ }
+ Ok(layout)
+ }
+ None => Ok(LayoutFromYamlIntermediate::default()),
+ }
+ }
+
+ pub fn from_yaml(yaml: &str) -> LayoutFromYamlIntermediateResult {
+ let layout: LayoutFromYamlIntermediate = match serde_yaml::from_str(yaml) {
+ Err(e) => {
+ // needs direct check, as `[ErrorImpl]` is private
+ // https://github.com/dtolnay/serde-yaml/issues/121
+ if yaml.is_empty() {
+ return Ok(LayoutFromYamlIntermediate::default());
+ }
+ return Err(ConfigError::Serde(e));
+ }
+ Ok(config) => config,
+ };
+ Ok(layout)
+ }
+
+ pub fn to_layout_and_config(&self) -> (LayoutFromYaml, Option<ConfigFromYaml>) {
+ let config = self.config.clone();
+ let layout = self.clone().into();
+ (layout, config)
+ }
+
+ pub fn from_path_or_default(
+ layout: Option<&PathBuf>,
+ layout_path: Option<&PathBuf>,
+ layout_dir: Option<PathBuf>,
+ ) -> Option<LayoutFromYamlIntermediateResult> {
+ layout
+ .map(|p| LayoutFromYamlIntermediate::from_dir(p, layout_dir.as_ref()))
+ .or_else(|| layout_path.map(|p| LayoutFromYamlIntermediate::from_path(p)))
+ .or_else(|| {
+ Some(LayoutFromYamlIntermediate::from_dir(
+ &std::path::PathBuf::from("default"),
+ layout_dir.as_ref(),
+ ))
+ })
+ }
+
+ // It wants to use Path here, but that doesn't compile.
+ #[allow(clippy::ptr_arg)]
+ pub fn from_dir(
+ layout: &PathBuf,
+ layout_dir: Option<&PathBuf>,
+ ) -> LayoutFromYamlIntermediateResult {
+ match layout_dir {
+ Some(dir) => Self::from_path(&dir.join(layout))
+ .or_else(|_| LayoutFromYamlIntermediate::from_default_assets(layout.as_path())),
+ None => LayoutFromYamlIntermediate::from_default_assets(layout.as_path()),
+ }
+ }
+ // Currently still needed but on nightly
+ // this is already possible:
+ // HashMap<&'static str, Vec<u8>>
+ pub fn from_default_assets(path: &Path) -> LayoutFromYamlIntermediateResult {
+ match path.to_str() {
+ Some("default") => Self::default_from_assets(),
+ Some("strider") => Self::strider_from_assets(),
+ Some("disable-status-bar") => Self::disable_status_from_assets(),
+ None | Some(_) => Err(ConfigError::IoPath(
+ std::io::Error::new(std::io::ErrorKind::Other, "The layout was not found"),
+ path.into(),
+ )),
+ }
+ }
+
+ // TODO Deserialize the assets from bytes &[u8],
+ // once serde-yaml supports zero-copy
+ pub fn default_from_assets() -> LayoutFromYamlIntermediateResult {
+ let layout: LayoutFromYamlIntermediate =
+ serde_yaml::from_str(String::from_utf8(setup::DEFAULT_LAYOUT.to_vec())?.as_str())?;
+ Ok(layout)
+ }
+
+ pub fn strider_from_assets() -> LayoutFromYamlIntermediateResult {
+ let layout: LayoutFromYamlIntermediate =
+ serde_yaml::from_str(String::from_utf8(setup::STRIDER_LAYOUT.to_vec())?.as_str())?;
+ Ok(layout)
+ }
+
+ pub fn disable_status_from_assets() -> LayoutFromYamlIntermediateResult {
+ let layout: LayoutFromYamlIntermediate =
+ serde_yaml::from_str(String::from_utf8(setup::NO_STATUS_LAYOUT.to_vec())?.as_str())?;
+ Ok(layout)
+ }
+}
+
type LayoutFromYamlResult = Result<LayoutFromYaml, ConfigError>;
impl LayoutFromYaml {
@@ -211,6 +352,7 @@ impl LayoutFromYaml {
))
})
}
+
// Currently still needed but on nightly
// this is already possible:
// HashMap<&'static str, Vec<u8>>
@@ -525,6 +667,35 @@ impl TryFrom<RunFromYaml> for Run {
}
}
+impl From<LayoutFromYamlIntermediate> for LayoutFromYaml {
+ fn from(layout_from_yaml_intermediate: LayoutFromYamlIntermediate) -> Self {
+ Self {
+ template: layout_from_yaml_intermediate.template,
+ borderless: layout_from_yaml_intermediate.borderless,
+ tabs: layout_from_yaml_intermediate.tabs,
+ session: layout_from_yaml_intermediate.session,
+ }
+ }
+}
+
+impl From<LayoutFromYaml> for LayoutFromYamlIntermediate {
+ fn from(layout_from_yaml: LayoutFromYaml) -> Self {
+ Self {
+ template: layout_from_yaml.template,
+ borderless: layout_from_yaml.borderless,
+ tabs: layout_from_yaml.tabs,
+ config: None,
+ session: layout_from_yaml.session,
+ }
+ }
+}
+
+impl Default for LayoutFromYamlIntermediate {
+ fn default() -> Self {
+ LayoutFromYaml::default().into()
+ }
+}
+
impl TryFrom<TabLayout> for Layout {
type Error = ConfigError;
diff --git a/zellij-utils/src/input/plugins.rs b/zellij-utils/src/input/plugins.rs
index e97a8ad03..6be975e4d 100644
--- a/zellij-utils/src/input/plugins.rs
+++ b/zellij-utils/src/input/plugins.rs
@@ -62,6 +62,14 @@ impl PluginsConfig {
pub fn iter(&self) -> impl Iterator<Item = &PluginConfig> {
self.0.values()
}
+
+ /// Merges two PluginConfig structs into one PluginConfig struct
+ /// `other` overrides the PluginConfig of `self`.
+ pub fn merge(&self, other: Self) -> Self {
+ let mut plugin_config = self.0.clone();
+ plugin_config.extend(other.0);
+ Self(plugin_config)
+ }
}
impl Default for PluginsConfig {
diff --git a/zellij-utils/src/input/theme.rs b/zellij-utils/src/input/theme.rs
index 35bd3d87b..da71b6334 100644
--- a/zellij-utils/src/input/theme.rs
+++ b/zellij-utils/src/input/theme.rs
@@ -63,6 +63,14 @@ impl ThemesFromYaml {
.get_theme(theme)
.map(|t| Palette::from(t.palette))
}
+
+ /// Merges two Theme structs into one Theme struct
+ /// `other` overrides the Theme of `self`.
+ pub fn merge(&self, other: Self) -> Self {
+ let mut theme = self.0.clone();
+ theme.extend(other.0);
+ Self(theme)
+ }
}
impl From<PaletteFromYaml> for Palette {
diff --git a/zellij-utils/src/setup.rs b/zellij-utils/src/setup.rs
index fcd90d1c3..902d593f2 100644
--- a/zellij-utils/src/setup.rs
+++ b/zellij-utils/src/setup.rs
@@ -6,7 +6,7 @@ use crate::{
},
input::{
config::{Config, ConfigError},
- layout::LayoutFromYaml,
+ layout::{LayoutFromYaml, LayoutFromYamlIntermediate},
options::Options,
},
};
@@ -147,10 +147,12 @@ pub struct Setup {
impl Setup {
/// Entrypoint from main
/// Merges options from the config file and the command line options
- /// into `[Options]`, the command line options superceding the config
- /// file options:
+ /// into `[Options]`, the command line options superceeding the layout
+ /// file options, superceeding the config file options:
/// 1. command line options (`zellij options`)
- /// 2. config options (`config.yaml`)
+ /// 2. layout options
+ /// (`layout.yaml` / `zellij --layout` / `zellij --layout-path`)
+ /// 3. config options (`config.yaml`)
pub fn from_options(
opts: &CliArgs,
) -> Result<(Config, Option<LayoutFromYaml>, Options), ConfigError> {
@@ -187,7 +189,7 @@ impl Setup {
.layout_dir
.clone()
.or_else(|| get_layout_dir(opts.config_dir.clone().or_else(find_default_config_dir)));
- let layout_result = LayoutFromYaml::from_path_or_default(
+ let layout_result = LayoutFromYamlIntermediate::from_path_or_default(
opts.layout.as_ref(),
opts.layout_path.as_ref(),
layout_dir,
@@ -212,7 +214,7 @@ impl Setup {
);
};
- Ok((config, layout, config_options))
+ Setup::merge_config_with_layout(config, layout, config_options)
}
/// General setup helpers
@@ -252,6 +254,30 @@ impl Setup {
Ok(())
}
+ fn merge_config_with_layout(
+ config: Config,
+ layout: Option<LayoutFromYamlIntermediate>,
+ config_options: Options,
+ ) -> Result<(Config, Option<LayoutFromYaml>, Options), ConfigError> {
+ let (layout, layout_config) = match layout.map(|l| l.to_layout_and_config()) {
+ None => (None, None),
+ Some((layout, layout_config)) => (Some(layout), layout_config),
+ };
+
+ let (config, config_options) = if let Some(layout_config) = layout_config {
+ let config_options = if let Some(options) = layout_config.options.clone() {
+ config_options.merge(options)
+ } else {
+ config_options
+ };
+ let config = config.merge(layout_config.try_into()?);
+ (config, config_options)
+ } else {
+ (config, config_options)
+ };
+ Ok((config, layout, config_options))
+ }
+
pub fn check_defaults_config(opts: &CliArgs, config_options: &Options) -> std::io::Result<()> {
let data_dir = opts.data_dir.clone().unwrap_or_else(get_default_data_dir);
let config_dir = opts.config_dir.clone().or_else(find_default_config_dir);
@@ -360,3 +386,99 @@ impl Setup {
CliArgs::clap().gen_completions_to("zellij", shell, &mut out);
}
}
+
+#[cfg(test)]
+mod setup_test {
+ use super::Setup;
+ use crate::input::{
+ config::{Config, ConfigError},
+ keybinds::Keybinds,
+ layout::{LayoutFromYaml, LayoutFromYamlIntermediate},
+ options::Options,
+ };
+
+ fn deserialise_config_and_layout(
+ config: &str,
+ layout: &str,
+ ) -> Result<(Config, LayoutFromYamlIntermediate), ConfigError> {
+ let config = Config::from_yaml(&config)?;
+ let layout = LayoutFromYamlIntermediate::from_yaml(&layout)?;
+ Ok((config, layout))
+ }
+
+ #[test]
+ fn empty_config_empty_layout() {
+ let goal = Config::default();
+ let config = r"";
+ let layout = r"";
+ let config_layout_result = deserialise_config_and_layout(config, layout);
+ let (config, layout) = config_layout_result.unwrap();
+ let config_options = Options::default();
+ let (config, _layout, _config_options) =
+ Setup::merge_config_with_layout(config, Some(layout), config_options).unwrap();
+ assert_eq!(config, goal);
+ }
+
+ #[test]
+ fn config_empty_layout() {
+ let mut goal = Config::default();
+ goal.options.default_shell = Some(std::path::PathBuf::from("fish"));
+ let config = r"---
+ default_shell: fish";
+ let layout = r"";
+ let config_layout_result = deserialise_config_and_layout(config, layout);
+ let (config, layout) = config_layout_result.unwrap();
+ let config_options = Options::default();
+ let (config, _layout, _config_options) =
+ Setup::merge_config_with_layout(config, Some(layout), config_options).unwrap();
+ assert_eq!(config, goal);
+ }
+
+ #[test]
+ fn layout_overwrites_config() {
+ let mut goal = Config::default();
+ goal.options.default_shell = Some(std::path::PathBuf::from("bash"));
+ let config = r"---
+ default_shell: fish";
+ let layout = r"---
+ default_shell: bash";
+ let config_layout_result = deserialise_config_and_layout(config, layout);
+ let (config, layout) = config_layout_result.unwrap();
+ let config_options = Options::default();
+ let (config, _layout, _config_options) =
+ Setup::merge_config_with_layout(config, Some(layout), config_options).unwrap();
+ assert_eq!(config, goal);
+ }
+
+ #[test]
+ fn empty_config_nonempty_layout() {
+ let mut goal = Config::default();
+ goal.options.default_shell = Some(std::path::PathBuf::from("bash"));
+ let config = r"";
+ let layout = r"---
+ default_shell: bash";
+ let config_layout_result = deserialise_config_and_layout(config, layout);
+ let (config, layout) = config_layout_result.unwrap();
+ let config_options = Options::default();
+ let (config, _layout, _config_options) =
+ Setup::merge_config_with_layout(config, Some(layout), config_options).unwrap();
+ assert_eq!(config, goal);
+ }
+
+ #[test]
+ fn nonempty_config_nonempty_layout() {
+ let mut goal = Config::default();
+ goal.options.default_shell = Some(std::path::PathBuf::from("bash"));
+ goal.options.default_mode = Some(zellij_tile::prelude::InputMode::Locked);
+ let config = r"---
+ default_mode: locked";
+ let layout = r"---
+ default_shell: bash";
+ let config_layout_result = deserialise_config_and_layout(config, layout);
+ let (config, layout) = config_layout_result.unwrap();
+ let config_options = Options::default();
+ let (config, _layout, _config_options) =
+ Setup::merge_config_with_layout(config, Some(layout), config_options).unwrap();
+ assert_eq!(config, goal);
+ }
+}