use crate::data::Palette;
use miette::{Diagnostic, LabeledSpan, NamedSource, SourceCode};
use std::fs::File;
use std::io::{self, Read};
use std::path::PathBuf;
use thiserror::Error;
use std::convert::TryFrom;
use super::keybinds::Keybinds;
use super::options::Options;
use super::plugins::{PluginsConfig, PluginsConfigError};
use super::theme::{Themes, UiConfig};
use crate::cli::{CliArgs, Command};
use crate::envs::EnvironmentVariables;
use crate::setup;
const DEFAULT_CONFIG_FILE_NAME: &str = "config.kdl";
type ConfigResult = Result<Config, ConfigError>;
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Config {
pub keybinds: Keybinds,
pub options: Options,
pub themes: Themes,
pub plugins: PluginsConfig,
pub ui: UiConfig,
pub env: EnvironmentVariables,
}
#[derive(Error, Debug)]
pub struct KdlError {
pub error_message: String,
pub src: Option<NamedSource>,
pub offset: Option<usize>,
pub len: Option<usize>,
pub help_message: Option<String>,
}
impl KdlError {
pub fn add_src(mut self, src_name: String, src_input: String) -> Self {
self.src = Some(NamedSource::new(src_name, src_input));
self
}
}
impl std::fmt::Display for KdlError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "Failed to parse Zellij configuration")
}
}
use std::fmt::Display;
impl Diagnostic for KdlError {
fn source_code(&self) -> Option<&dyn SourceCode> {
match self.src.as_ref() {
Some(src) => Some(src),
None => None,
}
}
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
match &self.help_message {
Some(help_message) => Some(Box::new(help_message)),
None => Some(Box::new(format!("For more information, please see our configuration guide: https://zellij.dev/documentation/configuration.html")))
}
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
if let (Some(offset), Some(len)) = (self.offset, self.len) {
let label = LabeledSpan::new(Some(self.error_message.clone()), offset, len);
Some(Box::new(std::iter::once(label)))
} else {
None
}
}
}
#[derive(Error, Debug, Diagnostic)]
pub enum ConfigError {
#[error("Deserialization error: {0}")]
KdlDeserializationError(#[from] kdl::KdlError),
#[error("KdlDeserialization error: {0}")]
KdlError(KdlError), #[error("IoError: {0}")]
Io(#[from] io::Error),
#[error("Config error: {0}")]
Std(#[from] Box<dyn std::error::Error>),
#[error("IoError: {0}, File: {1}")]
IoPath(io::Error, PathBuf),
#[error("FromUtf8Error: {0}")]
FromUtf8(#[from] std::string::FromUtf8Error),
#[error("PluginsError: {0}")]
PluginsError(#[from] PluginsConfigError),
#[error("{0}")]
ConversionError(#[from] ConversionError),
}
impl ConfigError {
pub fn new_kdl_error(error_message: String, offset: usize, len: usize) -> Self {
ConfigError::KdlError(KdlError {
error_message,
src: None,
offset: Some(offset),
len: Some(len),
help_message: None,
})
}
pub fn new_layout_kdl_error(error_message: String, offset: usize, len: usize) -> Self {
ConfigError::KdlError(KdlError {
error_message,
src: None,
offset: Some(offset),
len: Some(len),
help_message: Some(format!("For more information, please see our layout guide: https://zellij.dev/documentation/creating-a-layout.html")),
})
}
}
#[derive(Debug, Error)]
pub enum ConversionError {
#[error("{0}")]
UnknownInputMode(String),
}
impl TryFrom<&CliArgs> for Config {
type Error = ConfigError;
fn try_from(opts: &CliArgs) -> ConfigResult {
if let Some(ref path) = opts.config {
let default_config = Config::from_default_assets()?;
return Config::from_path(path, Some(default_config));
}
if let Some(Command::Setup(ref setup)) = opts.command {
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() {
let default_config = Config::from_default_assets()?;
Config::from_path(&path, Some(default_config))
} else {
Config::from_default_assets()
}
} else {
Config::from_default_assets()
}
}
}
impl Config {
pub fn theme_config(&self, opts: &Options) -> Option<Palette> {
match &opts.theme {
Some(theme_name) => self.themes.get_theme(theme_name).map(|theme| theme.palette),
None => self.themes.get_theme("default").map(|theme| theme.palette),
}
}
pub fn from_default_assets() -> ConfigResult {
let cfg = String::from_utf8(setup::DEFAULT_CONFIG.to_vec())?;
match Self::from_kdl(&cfg, None) {
Ok(config) => Ok(config),
Err(ConfigError::KdlError(kdl_error)) => Err(ConfigError::KdlError(
kdl_error.add_src("Default built-in-configuration".into(), cfg),
)),
Err(e) => Err(e),
}
}
pub fn from_path(path: &PathBuf, default_config: Option<Config>) -> ConfigResult {
match File::open(path) {
Ok(mut file) => {
let mut kdl_config = String::new();
file.read_to_string(&mut kdl_config)
.map_err(|e| ConfigError::IoPath(e, path.to_path_buf()))?;
match Config::from_kdl(&kdl_config, default_config) {
Ok(config) => Ok(config),
Err(ConfigError::KdlDeserializationError(kdl_error)) => {
let error_message = match kdl_error.kind {
kdl::KdlErrorKind::Context("valid node terminator") => {
format!("Failed to deserialize KDL node. \nPossible reasons:\n{}\n{}\n{}\n{}",
"- Missing `;` after a node name, eg. { node; another_node; }",
"- Missing quotations (\") around an argument node eg. { first_node \"argument_node\"; }",
"- Missing an equal sign (=) between node arguments on a title line. eg. argument=\"value\"",
"- Found an extraneous equal sign (=) between node child arguments and their values. eg. { argument=\"value\" }")
},
_ => {
String::from(kdl_error.help.unwrap_or("Kdl Deserialization Error"))
},
};
let kdl_error = KdlError {
error_message,
src: Some(NamedSource::new(
path.as_path().as_os_str().to_string_lossy(),
kdl_config,
)),
offset: Some(kdl_error.span.offset()),
len: Some(kdl_error.span.len()),
help_message: None,
};
Err(ConfigError::KdlError(kdl_error))
},
Err(ConfigError::KdlError(kdl_error)) => {
Err(ConfigError::KdlError(kdl_error.add_src(
path.as_path().as_os_str().to_string_lossy().to_string(),
kdl_config,
)))
},
Err(e) => Err(e),
}
},
Err(e) => Err(ConfigError::IoPath(e, path.into())),
}
}
}
#[cfg(test)]
mod config_test {
use super::*;
use crate::data::{InputMode, Palette, PaletteColor, PluginTag};
use crate::input::layout::RunPluginLocation;
use crate::input::options::{Clipboard, OnForceClose};
use crate::input::plugins::{PluginConfig, PluginType, PluginsConfig};
use crate::input::theme::{FrameConfig, Theme, Themes, UiConfig};
use std::collections::HashMap;
use std::io::Write;
use tempfile::tempdir;
#[test]
fn try_from_cli_args_with_config() {
let arbitrary_config = PathBuf::from("nonexistent.yaml");
let opts = CliArgs {
config: Some(arbitrary_config),
..Default::default()
};
println!("OPTS= {:?}", opts);
let result = Config::try_from(&opts);
assert!(result.is_err());
}
#[test]
fn try_from_cli_args_with_option_clean() {
use crate::setup::Setup;
let opts = CliArgs {
command: Some(Command::Setup(Setup {
clean: true,
..Setup::default()
})),
..Default::default()
};
let result = Config::try_from(&opts);
assert!(result.is_ok());
}
#[test]
fn try_from_cli_args_with_config_dir() {
let mut opts = CliArgs::default();
let tmp = tempdir().unwrap();
File::create(tmp.path().join(DEFAULT_CONFIG_FILE_NAME))
.unwrap()
.write_all(b"keybinds: invalid\n")
.unwrap();
opts.config_dir = Some(tmp.path().to_path_buf());
let result = Config::try_from(&opts);
assert!(result.is_err());
}
#[test]
fn try_from_cli_args_with_config_dir_without_config() {
let mut opts = CliArgs::default();
let tmp = tempdir().unwrap();
opts.config_dir = Some(tmp.path().to_path_buf());
let result = Config::try_from(&opts);
assert_eq!(result.unwrap(), Config::from_default_assets().unwrap());
}
#[test]
fn try_from_cli_args_default() {
let opts = CliArgs::default();
let result = Config::try_from(&opts);
assert_eq!(result.unwrap(), Config::from_default_assets().unwrap());
}
#[test]
fn can_define_options_in_configfile() {
let config_contents = r#"
simplified_ui true
theme "my cool theme"
default_mode "locked"
default_shell "/path/to/my/shell"
default_layout "/path/to/my/layout.kdl"
layout_dir "/path/to/my/layout-dir"
theme_dir "/path/to/my/theme-dir"
mouse_mode false
pane_frames false
mirror_session true
on_force_close "quit"
scroll_buffer_size 100000
copy_command "/path/to/my/copy-command"
copy_clipboard "primary"
copy_on_select false
scrollback_editor "/path/to/my/scrollback-editor"
session_name "my awesome session"
attach_to_session true
"#;
let config = Config::from_kdl(config_contents, None).unwrap();
assert_eq!(
config.options.simplified_ui,
Some(true),
"Option set in config"
);
assert_eq!(
config.options.theme,
Some(String::from("my cool theme")),
"Option set in config"
);
assert_eq!(
config.options.default_mode,
Some(InputMode::Locked),
"Option set in config"
);
assert_eq!(
config.options.default_shell,
Some(PathBuf::from("/path/to/my/shell")),
"Option set in config"
);
assert_eq!(
config.options.default_layout,
Some(PathBuf::from("/path/to/my/layout.kdl")),
"Option set in config"
);
assert_eq!(
config.options.layout_dir,
Some(PathBuf::from("/path/to/my/layout-dir")),
"Option set in config"
);
assert_eq!(
config.options.theme_dir,
Some(PathBuf::from("/path/to/my/theme-dir")),
"Option set in config"
);
assert_eq!(
config.options.mouse_mode,
Some(false),
"Option set in config"
);
assert_eq!(
config.options.pane_frames,
Some(false),
"Option set in config"
);
assert_eq!(
config.options.mirror_session,
Some(true),
"Option set in config"
);
assert_eq!(
config.options.on_force_close,
Some(OnForceClose::Quit),
"Option set in config"
);
assert_eq!(
config.options.scroll_buffer_size,
Some(100000),
"Option set in config"
);
assert_eq!(
config.options.copy_command,
Some(String::from("/path/to/my/copy-command")),
"Option set in config"
);
assert_eq!(
config.options.copy_clipboard,
Some(Clipboard::Primary),
"Option set in config"
);
assert_eq!(
config.options.copy_on_select,
Some(false),
"Option set in config"
);
assert_eq!(
config.options.scrollback_editor,
Some(PathBuf::from("/path/to/my/scrollback-editor")),
"Option set in config"
);
assert_eq!(
config.options.session_name,
Some(String::from("my awesome session")),
"Option set in config"
);
assert_eq!(
config.options.attach_to_session,
Some(true),
"Option set in config"
);
}
#[test]
fn can_define_themes_in_configfile() {
let config_contents = r#"
themes {
dracula {
fg 248 248 242
bg 40 42 54
red 255 85 85
green 80 250 123
yellow 241 250 140
blue 98 114 164
magenta 255 121 198
orange 255 184 108
cyan 139 233 253
black 0 0 0
white 255 255 255
}
}
"#;
let config = Config::from_kdl(config_contents, None).unwrap();
let mut expected_themes = HashMap::new();
expected_themes.insert(
"dracula".into(),
Theme {
palette: Palette {
fg: PaletteColor::Rgb((248, 248, 242)),
bg: PaletteColor::Rgb((40, 42, 54)),
red: PaletteColor::Rgb((255, 85, 85)),
green: PaletteColor::Rgb((80, 250, 123)),
yellow: PaletteColor::Rgb((241, 250, 140)),
blue: PaletteColor::Rgb((98, 114, 164)),
magenta: PaletteColor::Rgb((255, 121, 198)),
orange: PaletteColor::Rgb((255, 184, 108)),
cyan: PaletteColor::Rgb((139, 233, 253)),
black: PaletteColor::Rgb((0, 0, 0)),
white: PaletteColor::Rgb((255, 255, 255)),
..Default::default()
},
},
);
let expected_themes = Themes::from_data(expected_themes);
assert_eq!(config.themes, expected_themes, "Theme defined in config");
}
#[test]
fn can_define_multiple_themes_including_hex_themes_in_configfile() {
let config_contents = r##"
themes {
dracula {
fg 248 248 242
bg 40 42 54
red 255 85 85
green 80 250 123
yellow 241 250 140
blue 98 114 164
magenta 255 121 198
orange 255 184 108
cyan 139 233 253
black 0 0 0
white 255 255 255
}
nord {
fg "#D8DEE9"
bg "#2E3440"
black "#3B4252"
red "#BF616A"
green "#A3BE8C"
yellow "#EBCB8B"
blue "#81A1C1"
magenta "#B48EAD"
cyan "#88C0D0"
white "#E5E9F0"
orange "#D08770"
}
}
"##;
let config = Config::from_kdl(config_contents, None).unwrap();
let mut expected_themes = HashMap::new();
expected_themes.insert(
"dracula".into(),
Theme {
palette: Palette {
fg: PaletteColor::Rgb((248, 248, 242)),
bg: PaletteColor::Rgb((40, 42, 54)),
red: PaletteColor::Rgb((255, 85, 85)),
green: PaletteColor::Rgb((80, 250, 123)),
yellow: PaletteColor::Rgb((241, 250, 140)),
blue: PaletteColor::Rgb((98, 114, 164)),
magenta: PaletteColor::Rgb((255, 121, 198)),
orange: PaletteColor::Rgb((255, 184, 108)),
cyan: PaletteColor::Rgb((139, 233, 253)),
black: PaletteColor::Rgb((0, 0, 0)),
white: PaletteColor::Rgb((255, 255, 255)),
..Default::default()
},
},
);
expected_themes.insert(
"nord".into(),
Theme {
palette: Palette {
fg: PaletteColor::Rgb((216, 222, 233)),
bg: PaletteColor::Rgb((46, 52, 64)),
black: PaletteColor::Rgb((59, 66, 82)),
red: PaletteColor::Rgb((191, 97, 106)),
green: PaletteColor::Rgb((163, 190, 140)),
yellow: PaletteColor::Rgb((235, 203, 139)),
blue: PaletteColor::Rgb((129, 161, 193)),
magenta: PaletteColor::Rgb((180, 142, 173)),
cyan: PaletteColor::Rgb((136, 192, 208)),
white: PaletteColor::Rgb((229, 233, 240)),
orange: PaletteColor::Rgb((208, 135, 112)),
..Default::default()
},
},
);
let expected_themes = Themes::from_data(expected_themes);
assert_eq!(config.themes, expected_themes, "Theme defined in config");
}
#[test]
fn can_define_eight_bit_themes() {
let config_contents = r#"
themes {
eight_bit_theme {
fg 248
bg 40
red 255
green 80
yellow 241
blue 98
magenta 255
orange 255
cyan 139
black 1
white 255
}
}
"#;
let config = Config::from_kdl(config_contents, None).unwrap();
let mut expected_themes = HashMap::new();
expected_themes.insert(
"eight_bit_theme".into(),
Theme {
palette: Palette {
fg: PaletteColor::EightBit(248),
bg: PaletteColor::EightBit(40),
red: PaletteColor::EightBit(255),
green: PaletteColor::EightBit(80),
yellow: PaletteColor::EightBit(241),
blue: PaletteColor::EightBit(98),
magenta: PaletteColor::EightBit(255),
orange: PaletteColor::EightBit(255),
cyan: PaletteColor::EightBit(139),
black: PaletteColor::EightBit(1),
white: PaletteColor::EightBit(255),
..Default::default()
},
},
);
let expected_themes = Themes::from_data(expected_themes);
assert_eq!(config.themes, expected_themes, "Theme defined in config");
}
#[test]
fn can_define_plugin_configuration_in_configfile() {
let config_contents = r#"
plugins {
tab-bar { path "tab-bar"; }
status-bar { path "status-bar"; }
strider {
path "strider"
_allow_exec_host_cmd true
}
compact-bar { path "compact-bar"; }
}
"#;
let config = Config::from_kdl(config_contents, None).unwrap();
let mut expected_plugin_configuration = HashMap::new();
expected_plugin_configuration.insert(
PluginTag::new("tab-bar"),
PluginConfig {
path: PathBuf::from("tab-bar"),
run: PluginType::Pane(None),
location: RunPluginLocation::Zellij(PluginTag::new("tab-bar")),
_allow_exec_host_cmd: false,
},
);
expected_plugin_configuration.insert(
PluginTag::new("status-bar"),
PluginConfig {
path: PathBuf::from("status-bar"),
run: PluginType::Pane(None),
location: RunPluginLocation::Zellij(PluginTag::new("status-bar")),
_allow_exec_host_cmd: false,
},
);
expected_plugin_configuration.insert(
PluginTag::new("strider"),
PluginConfig {
path: PathBuf::from("strider"),
run: PluginType::Pane(None),
location: RunPluginLocation::Zellij(PluginTag::new("strider")),
_allow_exec_host_cmd: true,
},
);
expected_plugin_configuration.insert(
PluginTag::new("compact-bar"),
PluginConfig {
path: PathBuf::from("compact-bar"),
run: PluginType::Pane(None),
location: RunPluginLocation::Zellij(PluginTag::new("compact-bar")),
_allow_exec_host_cmd: false,
},
);
assert_eq!(
config.plugins,
PluginsConfig::from_data(expected_plugin_configuration),
"Plugins defined in config"
);
}
#[test]
fn can_define_ui_configuration_in_configfile() {
let config_contents = r#"
ui {
pane_frames {
rounded_corners true
}
}
"#;
let config = Config::from_kdl(config_contents, None).unwrap();
let expected_ui_config = UiConfig {
pane_frames: FrameConfig {
rounded_corners: true,
},
};
assert_eq!(config.ui, expected_ui_config, "Ui config defined in config");
}
#[test]
fn can_define_env_variables_in_config_file() {
let config_contents = r#"
env {
RUST_BACKTRACE 1
SOME_OTHER_VAR "foo"
}
"#;
let config = Config::from_kdl(config_contents, None).unwrap();
let mut expected_env_config = HashMap::new();
expected_env_config.insert("RUST_BACKTRACE".into(), "1".into());
expected_env_config.insert("SOME_OTHER_VAR".into(), "foo".into());
assert_eq!(
config.env,
EnvironmentVariables::from_data(expected_env_config),
"Env variables defined in config"
);
}
}