Skip to content

Commit

Permalink
feature(layout): add layout config (#866)
Browse files Browse the repository at this point in the history
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
```
  • Loading branch information
a-kenji committed Nov 14, 2021
1 parent 96315ed commit 347e02e
Show file tree
Hide file tree
Showing 6 changed files with 336 additions and 15 deletions.
16 changes: 14 additions & 2 deletions zellij-utils/src/input/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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>,
Expand Down Expand Up @@ -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 {
Expand Down
12 changes: 6 additions & 6 deletions zellij-utils/src/input/keybinds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>>,
Expand All @@ -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),
Expand All @@ -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!
Expand Down Expand Up @@ -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() {
Expand Down
173 changes: 172 additions & 1 deletion zellij-utils/src/input/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -137,6 +140,26 @@ pub struct Layout {
pub borderless: bool,
}

// 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)]
Expand All @@ -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 {
Expand Down Expand Up @@ -211,6 +352,7 @@ impl LayoutFromYaml {
))
})
}

// Currently still needed but on nightly
// this is already possible:
// HashMap<&'static str, Vec<u8>>
Expand Down Expand Up @@ -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;

Expand Down
8 changes: 8 additions & 0 deletions zellij-utils/src/input/plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions zellij-utils/src/input/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 347e02e

Please sign in to comment.