diff options
author | a-kenji <aks.kenji@protonmail.com> | 2021-11-14 22:58:20 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-11-14 22:58:20 +0100 |
commit | 347e02ea35793ba7e898ca0693f569a21525d459 (patch) | |
tree | beab3ed6f4102b96cffa8c7c7cfbaaa33d794943 /zellij-utils/src | |
parent | 96315ed332438374743d73d3e3b38c3cdbdc3331 (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
```
Diffstat (limited to 'zellij-utils/src')
-rw-r--r-- | zellij-utils/src/input/config.rs | 16 | ||||
-rw-r--r-- | zellij-utils/src/input/keybinds.rs | 12 | ||||
-rw-r--r-- | zellij-utils/src/input/layout.rs | 173 | ||||
-rw-r--r-- | zellij-utils/src/input/plugins.rs | 8 | ||||
-rw-r--r-- | zellij-utils/src/input/theme.rs | 8 | ||||
-rw-r--r-- | zellij-utils/src/setup.rs | 134 |
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); + } +} |