zellij_utils/input/
config.rs

1use crate::data::Palette;
2use miette::{Diagnostic, LabeledSpan, NamedSource, SourceCode};
3use serde::{Deserialize, Serialize};
4use std::collections::HashSet;
5use std::fs::File;
6use std::io::{self, Read};
7use std::path::PathBuf;
8use thiserror::Error;
9
10use std::convert::TryFrom;
11
12use super::keybinds::Keybinds;
13use super::layout::RunPluginOrAlias;
14use super::options::Options;
15use super::plugins::{PluginAliases, PluginsConfigError};
16use super::theme::{Themes, UiConfig};
17use crate::cli::{CliArgs, Command};
18use crate::envs::EnvironmentVariables;
19use crate::{home, setup};
20
21const DEFAULT_CONFIG_FILE_NAME: &str = "config.kdl";
22
23type ConfigResult = Result<Config, ConfigError>;
24
25/// Main configuration.
26#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
27pub struct Config {
28    pub keybinds: Keybinds,
29    pub options: Options,
30    pub themes: Themes,
31    pub plugins: PluginAliases,
32    pub ui: UiConfig,
33    pub env: EnvironmentVariables,
34    pub background_plugins: HashSet<RunPluginOrAlias>,
35}
36
37#[derive(Error, Debug)]
38pub struct KdlError {
39    pub error_message: String,
40    pub src: Option<NamedSource>,
41    pub offset: Option<usize>,
42    pub len: Option<usize>,
43    pub help_message: Option<String>,
44}
45
46impl KdlError {
47    pub fn add_src(mut self, src_name: String, src_input: String) -> Self {
48        self.src = Some(NamedSource::new(src_name, src_input));
49        self
50    }
51}
52
53impl std::fmt::Display for KdlError {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
55        write!(f, "Failed to parse Zellij configuration")
56    }
57}
58use std::fmt::Display;
59
60impl Diagnostic for KdlError {
61    fn source_code(&self) -> Option<&dyn SourceCode> {
62        match self.src.as_ref() {
63            Some(src) => Some(src),
64            None => None,
65        }
66    }
67    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
68        match &self.help_message {
69            Some(help_message) => Some(Box::new(help_message)),
70            None => Some(Box::new(format!("For more information, please see our configuration guide: https://zellij.dev/documentation/configuration.html")))
71        }
72    }
73    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
74        if let (Some(offset), Some(len)) = (self.offset, self.len) {
75            let label = LabeledSpan::new(Some(self.error_message.clone()), offset, len);
76            Some(Box::new(std::iter::once(label)))
77        } else {
78            None
79        }
80    }
81}
82
83#[derive(Error, Debug, Diagnostic)]
84pub enum ConfigError {
85    // Deserialization error
86    #[error("Deserialization error: {0}")]
87    KdlDeserializationError(#[from] kdl::KdlError),
88    #[error("KdlDeserialization error: {0}")]
89    KdlError(KdlError), // TODO: consolidate these
90    #[error("Config error: {0}")]
91    Std(#[from] Box<dyn std::error::Error>),
92    // Io error with path context
93    #[error("IoError: {0}, File: {1}")]
94    IoPath(io::Error, PathBuf),
95    // Internal Deserialization Error
96    #[error("FromUtf8Error: {0}")]
97    FromUtf8(#[from] std::string::FromUtf8Error),
98    // Plugins have a semantic error, usually trying to parse two of the same tag
99    #[error("PluginsError: {0}")]
100    PluginsError(#[from] PluginsConfigError),
101    #[error("{0}")]
102    ConversionError(#[from] ConversionError),
103    #[error("{0}")]
104    DownloadError(String),
105}
106
107impl ConfigError {
108    pub fn new_kdl_error(error_message: String, offset: usize, len: usize) -> Self {
109        ConfigError::KdlError(KdlError {
110            error_message,
111            src: None,
112            offset: Some(offset),
113            len: Some(len),
114            help_message: None,
115        })
116    }
117    pub fn new_layout_kdl_error(error_message: String, offset: usize, len: usize) -> Self {
118        ConfigError::KdlError(KdlError {
119            error_message,
120            src: None,
121            offset: Some(offset),
122            len: Some(len),
123            help_message: Some(format!("For more information, please see our layout guide: https://zellij.dev/documentation/creating-a-layout.html")),
124        })
125    }
126}
127
128#[derive(Debug, Error)]
129pub enum ConversionError {
130    #[error("{0}")]
131    UnknownInputMode(String),
132}
133
134impl TryFrom<&CliArgs> for Config {
135    type Error = ConfigError;
136
137    fn try_from(opts: &CliArgs) -> ConfigResult {
138        if let Some(ref path) = opts.config {
139            let default_config = Config::from_default_assets()?;
140            return Config::from_path(path, Some(default_config));
141        }
142
143        if let Some(Command::Setup(ref setup)) = opts.command {
144            if setup.clean {
145                return Config::from_default_assets();
146            }
147        }
148
149        let config_dir = opts
150            .config_dir
151            .clone()
152            .or_else(home::find_default_config_dir);
153
154        if let Some(ref config) = config_dir {
155            let path = config.join(DEFAULT_CONFIG_FILE_NAME);
156            if path.exists() {
157                let default_config = Config::from_default_assets()?;
158                Config::from_path(&path, Some(default_config))
159            } else {
160                Config::from_default_assets()
161            }
162        } else {
163            Config::from_default_assets()
164        }
165    }
166}
167
168impl Config {
169    pub fn theme_config(&self, theme_name: Option<&String>) -> Option<Palette> {
170        match &theme_name {
171            Some(theme_name) => self.themes.get_theme(theme_name).map(|theme| theme.palette),
172            None => self.themes.get_theme("default").map(|theme| theme.palette),
173        }
174    }
175    /// Gets default configuration from assets
176    pub fn from_default_assets() -> ConfigResult {
177        let cfg = String::from_utf8(setup::DEFAULT_CONFIG.to_vec())?;
178        match Self::from_kdl(&cfg, None) {
179            Ok(config) => Ok(config),
180            Err(ConfigError::KdlError(kdl_error)) => Err(ConfigError::KdlError(
181                kdl_error.add_src("Default built-in-configuration".into(), cfg),
182            )),
183            Err(e) => Err(e),
184        }
185    }
186    pub fn from_path(path: &PathBuf, default_config: Option<Config>) -> ConfigResult {
187        match File::open(path) {
188            Ok(mut file) => {
189                let mut kdl_config = String::new();
190                file.read_to_string(&mut kdl_config)
191                    .map_err(|e| ConfigError::IoPath(e, path.to_path_buf()))?;
192                match Config::from_kdl(&kdl_config, default_config) {
193                    Ok(config) => Ok(config),
194                    Err(ConfigError::KdlDeserializationError(kdl_error)) => {
195                        let error_message = match kdl_error.kind {
196                            kdl::KdlErrorKind::Context("valid node terminator") => {
197                                format!("Failed to deserialize KDL node. \nPossible reasons:\n{}\n{}\n{}\n{}",
198                                "- Missing `;` after a node name, eg. { node; another_node; }",
199                                "- Missing quotations (\") around an argument node eg. { first_node \"argument_node\"; }",
200                                "- Missing an equal sign (=) between node arguments on a title line. eg. argument=\"value\"",
201                                "- Found an extraneous equal sign (=) between node child arguments and their values. eg. { argument=\"value\" }")
202                            },
203                            _ => {
204                                String::from(kdl_error.help.unwrap_or("Kdl Deserialization Error"))
205                            },
206                        };
207                        let kdl_error = KdlError {
208                            error_message,
209                            src: Some(NamedSource::new(
210                                path.as_path().as_os_str().to_string_lossy(),
211                                kdl_config,
212                            )),
213                            offset: Some(kdl_error.span.offset()),
214                            len: Some(kdl_error.span.len()),
215                            help_message: None,
216                        };
217                        Err(ConfigError::KdlError(kdl_error))
218                    },
219                    Err(ConfigError::KdlError(kdl_error)) => {
220                        Err(ConfigError::KdlError(kdl_error.add_src(
221                            path.as_path().as_os_str().to_string_lossy().to_string(),
222                            kdl_config,
223                        )))
224                    },
225                    Err(e) => Err(e),
226                }
227            },
228            Err(e) => Err(ConfigError::IoPath(e, path.into())),
229        }
230    }
231    pub fn merge(&mut self, other: Config) -> Result<(), ConfigError> {
232        self.options = self.options.merge(other.options);
233        self.keybinds.merge(other.keybinds.clone());
234        self.themes = self.themes.merge(other.themes);
235        self.plugins.merge(other.plugins);
236        self.ui = self.ui.merge(other.ui);
237        self.env = self.env.merge(other.env);
238        Ok(())
239    }
240    pub fn config_file_path(opts: &CliArgs) -> Option<PathBuf> {
241        opts.config.clone().or_else(|| {
242            opts.config_dir
243                .clone()
244                .or_else(home::find_default_config_dir)
245                .map(|config_dir| config_dir.join(DEFAULT_CONFIG_FILE_NAME))
246        })
247    }
248    pub fn write_config_to_disk(config: String, opts: &CliArgs) -> Result<Config, Option<PathBuf>> {
249        // if we fail, try to return the PathBuf of the file we were not able to write to
250        Config::from_kdl(&config, None)
251            .map_err(|e| {
252                log::error!("Failed to parse config: {}", e);
253                None
254            })
255            .and_then(|parsed_config| {
256                let backed_up_file_name = Config::backup_current_config(&opts)?;
257                let config_file_path = Config::config_file_path(&opts).ok_or_else(|| {
258                    log::error!("Config file path not found");
259                    None
260                })?;
261                let config = match backed_up_file_name {
262                    Some(backed_up_file_name) => {
263                        format!(
264                            "{}{}",
265                            Config::autogen_config_message(backed_up_file_name),
266                            config
267                        )
268                    },
269                    None => config,
270                };
271                std::fs::write(&config_file_path, config.as_bytes()).map_err(|e| {
272                    log::error!("Failed to write config: {}", e);
273                    Some(config_file_path.clone())
274                })?;
275                let written_config = std::fs::read_to_string(&config_file_path).map_err(|e| {
276                    log::error!("Failed to read written config: {}", e);
277                    Some(config_file_path.clone())
278                })?;
279                let parsed_written_config =
280                    Config::from_kdl(&written_config, None).map_err(|e| {
281                        log::error!("Failed to parse written config: {}", e);
282                        None
283                    })?;
284                if parsed_written_config == parsed_config {
285                    Ok(parsed_config)
286                } else {
287                    log::error!("Configuration corrupted when writing to disk");
288                    Err(Some(config_file_path))
289                }
290            })
291    }
292    // returns true if the config was not previouly written to disk and we successfully wrote it
293    pub fn write_config_to_disk_if_it_does_not_exist(config: String, opts: &CliArgs) -> bool {
294        if opts.config.is_none() {
295            // if a config file path wasn't explicitly specified, we try to create the default
296            // config folder
297            home::try_create_home_config_dir();
298        }
299        match Config::config_file_path(opts) {
300            Some(config_file_path) => {
301                if config_file_path.exists() {
302                    false
303                } else {
304                    if let Err(e) = std::fs::write(&config_file_path, config.as_bytes()) {
305                        log::error!("Failed to write config to disk: {}", e);
306                        return false;
307                    }
308                    match std::fs::read_to_string(&config_file_path) {
309                        Ok(written_config) => written_config == config,
310                        Err(e) => {
311                            log::error!("Failed to read written config: {}", e);
312                            false
313                        },
314                    }
315                }
316            },
317            None => false,
318        }
319    }
320    fn find_free_backup_file_name(config_file_path: &PathBuf) -> Option<PathBuf> {
321        let mut backup_config_path = None;
322        let config_file_name = config_file_path
323            .file_name()
324            .and_then(|f| f.to_str())
325            .unwrap_or_else(|| DEFAULT_CONFIG_FILE_NAME);
326        for i in 0..100 {
327            let new_file_name = if i == 0 {
328                format!("{}.bak", config_file_name)
329            } else {
330                format!("{}.bak.{}", config_file_name, i)
331            };
332            let mut potential_config_path = config_file_path.clone();
333            potential_config_path.set_file_name(new_file_name);
334            if !potential_config_path.exists() {
335                backup_config_path = Some(potential_config_path);
336                break;
337            }
338        }
339        backup_config_path
340    }
341    fn backup_config_with_written_content_confirmation(
342        current_config: &str,
343        current_config_file_path: &PathBuf,
344        backup_config_path: &PathBuf,
345    ) -> bool {
346        let _ = std::fs::copy(current_config_file_path, &backup_config_path);
347        match std::fs::read_to_string(&backup_config_path) {
348            Ok(backed_up_config) => current_config == &backed_up_config,
349            Err(e) => {
350                log::error!(
351                    "Failed to back up config file {}: {:?}",
352                    backup_config_path.display(),
353                    e
354                );
355                false
356            },
357        }
358    }
359    fn backup_current_config(opts: &CliArgs) -> Result<Option<PathBuf>, Option<PathBuf>> {
360        // if we fail, try to return the PathBuf of the file we were not able to write to
361        if let Some(config_file_path) = Config::config_file_path(&opts) {
362            match std::fs::read_to_string(&config_file_path) {
363                Ok(current_config) => {
364                    let Some(backup_config_path) =
365                        Config::find_free_backup_file_name(&config_file_path)
366                    else {
367                        log::error!("Failed to find a file name to back up the configuration to, ran out of files.");
368                        return Err(None);
369                    };
370                    if Config::backup_config_with_written_content_confirmation(
371                        &current_config,
372                        &config_file_path,
373                        &backup_config_path,
374                    ) {
375                        Ok(Some(backup_config_path))
376                    } else {
377                        log::error!(
378                            "Failed to back up config file: {}",
379                            backup_config_path.display()
380                        );
381                        Err(Some(backup_config_path))
382                    }
383                },
384                Err(e) => {
385                    if e.kind() == std::io::ErrorKind::NotFound {
386                        Ok(None)
387                    } else {
388                        log::error!(
389                            "Failed to read current config {}: {}",
390                            config_file_path.display(),
391                            e
392                        );
393                        Err(Some(config_file_path))
394                    }
395                },
396            }
397        } else {
398            log::error!("No config file path found?");
399            Err(None)
400        }
401    }
402    fn autogen_config_message(backed_up_file_name: PathBuf) -> String {
403        format!("//\n// THIS FILE WAS AUTOGENERATED BY ZELLIJ, THE PREVIOUS FILE AT THIS LOCATION WAS COPIED TO: {}\n//\n\n", backed_up_file_name.display())
404    }
405}
406
407#[cfg(test)]
408mod config_test {
409    use super::*;
410    use crate::data::{InputMode, Palette, PaletteColor};
411    use crate::input::layout::RunPlugin;
412    use crate::input::options::{Clipboard, OnForceClose};
413    use crate::input::theme::{FrameConfig, Theme, Themes, UiConfig};
414    use std::collections::{BTreeMap, HashMap};
415    use std::io::Write;
416    use tempfile::tempdir;
417
418    #[test]
419    fn try_from_cli_args_with_config() {
420        // makes sure loading a config file with --config tries to load the config
421        let arbitrary_config = PathBuf::from("nonexistent.yaml");
422        let opts = CliArgs {
423            config: Some(arbitrary_config),
424            ..Default::default()
425        };
426        println!("OPTS= {:?}", opts);
427        let result = Config::try_from(&opts);
428        assert!(result.is_err());
429    }
430
431    #[test]
432    fn try_from_cli_args_with_option_clean() {
433        // makes sure --clean works... TODO: how can this actually fail now?
434        use crate::setup::Setup;
435        let opts = CliArgs {
436            command: Some(Command::Setup(Setup {
437                clean: true,
438                ..Setup::default()
439            })),
440            ..Default::default()
441        };
442        let result = Config::try_from(&opts);
443        assert!(result.is_ok());
444    }
445
446    #[test]
447    fn try_from_cli_args_with_config_dir() {
448        let mut opts = CliArgs::default();
449        let tmp = tempdir().unwrap();
450        File::create(tmp.path().join(DEFAULT_CONFIG_FILE_NAME))
451            .unwrap()
452            .write_all(b"keybinds: invalid\n")
453            .unwrap();
454        opts.config_dir = Some(tmp.path().to_path_buf());
455        let result = Config::try_from(&opts);
456        assert!(result.is_err());
457    }
458
459    #[test]
460    fn try_from_cli_args_with_config_dir_without_config() {
461        let mut opts = CliArgs::default();
462        let tmp = tempdir().unwrap();
463        opts.config_dir = Some(tmp.path().to_path_buf());
464        let result = Config::try_from(&opts);
465        assert_eq!(result.unwrap(), Config::from_default_assets().unwrap());
466    }
467
468    #[test]
469    fn try_from_cli_args_default() {
470        let opts = CliArgs::default();
471        let result = Config::try_from(&opts);
472        assert_eq!(result.unwrap(), Config::from_default_assets().unwrap());
473    }
474
475    #[test]
476    fn can_define_options_in_configfile() {
477        let config_contents = r#"
478            simplified_ui true
479            theme "my cool theme"
480            default_mode "locked"
481            default_shell "/path/to/my/shell"
482            default_cwd "/path"
483            default_layout "/path/to/my/layout.kdl"
484            layout_dir "/path/to/my/layout-dir"
485            theme_dir "/path/to/my/theme-dir"
486            mouse_mode false
487            pane_frames false
488            mirror_session true
489            on_force_close "quit"
490            scroll_buffer_size 100000
491            copy_command "/path/to/my/copy-command"
492            copy_clipboard "primary"
493            copy_on_select false
494            scrollback_editor "/path/to/my/scrollback-editor"
495            session_name "my awesome session"
496            attach_to_session true
497        "#;
498        let config = Config::from_kdl(config_contents, None).unwrap();
499        assert_eq!(
500            config.options.simplified_ui,
501            Some(true),
502            "Option set in config"
503        );
504        assert_eq!(
505            config.options.theme,
506            Some(String::from("my cool theme")),
507            "Option set in config"
508        );
509        assert_eq!(
510            config.options.default_mode,
511            Some(InputMode::Locked),
512            "Option set in config"
513        );
514        assert_eq!(
515            config.options.default_shell,
516            Some(PathBuf::from("/path/to/my/shell")),
517            "Option set in config"
518        );
519        assert_eq!(
520            config.options.default_cwd,
521            Some(PathBuf::from("/path")),
522            "Option set in config"
523        );
524        assert_eq!(
525            config.options.default_layout,
526            Some(PathBuf::from("/path/to/my/layout.kdl")),
527            "Option set in config"
528        );
529        assert_eq!(
530            config.options.layout_dir,
531            Some(PathBuf::from("/path/to/my/layout-dir")),
532            "Option set in config"
533        );
534        assert_eq!(
535            config.options.theme_dir,
536            Some(PathBuf::from("/path/to/my/theme-dir")),
537            "Option set in config"
538        );
539        assert_eq!(
540            config.options.mouse_mode,
541            Some(false),
542            "Option set in config"
543        );
544        assert_eq!(
545            config.options.pane_frames,
546            Some(false),
547            "Option set in config"
548        );
549        assert_eq!(
550            config.options.mirror_session,
551            Some(true),
552            "Option set in config"
553        );
554        assert_eq!(
555            config.options.on_force_close,
556            Some(OnForceClose::Quit),
557            "Option set in config"
558        );
559        assert_eq!(
560            config.options.scroll_buffer_size,
561            Some(100000),
562            "Option set in config"
563        );
564        assert_eq!(
565            config.options.copy_command,
566            Some(String::from("/path/to/my/copy-command")),
567            "Option set in config"
568        );
569        assert_eq!(
570            config.options.copy_clipboard,
571            Some(Clipboard::Primary),
572            "Option set in config"
573        );
574        assert_eq!(
575            config.options.copy_on_select,
576            Some(false),
577            "Option set in config"
578        );
579        assert_eq!(
580            config.options.scrollback_editor,
581            Some(PathBuf::from("/path/to/my/scrollback-editor")),
582            "Option set in config"
583        );
584        assert_eq!(
585            config.options.session_name,
586            Some(String::from("my awesome session")),
587            "Option set in config"
588        );
589        assert_eq!(
590            config.options.attach_to_session,
591            Some(true),
592            "Option set in config"
593        );
594    }
595
596    #[test]
597    fn can_define_themes_in_configfile() {
598        let config_contents = r#"
599            themes {
600                dracula {
601                    fg 248 248 242
602                    bg 40 42 54
603                    red 255 85 85
604                    green 80 250 123
605                    yellow 241 250 140
606                    blue 98 114 164
607                    magenta 255 121 198
608                    orange 255 184 108
609                    cyan 139 233 253
610                    black 0 0 0
611                    white 255 255 255
612                }
613            }
614        "#;
615        let config = Config::from_kdl(config_contents, None).unwrap();
616        let mut expected_themes = HashMap::new();
617        expected_themes.insert(
618            "dracula".into(),
619            Theme {
620                palette: Palette {
621                    fg: PaletteColor::Rgb((248, 248, 242)),
622                    bg: PaletteColor::Rgb((40, 42, 54)),
623                    red: PaletteColor::Rgb((255, 85, 85)),
624                    green: PaletteColor::Rgb((80, 250, 123)),
625                    yellow: PaletteColor::Rgb((241, 250, 140)),
626                    blue: PaletteColor::Rgb((98, 114, 164)),
627                    magenta: PaletteColor::Rgb((255, 121, 198)),
628                    orange: PaletteColor::Rgb((255, 184, 108)),
629                    cyan: PaletteColor::Rgb((139, 233, 253)),
630                    black: PaletteColor::Rgb((0, 0, 0)),
631                    white: PaletteColor::Rgb((255, 255, 255)),
632                    ..Default::default()
633                },
634                sourced_from_external_file: false,
635            },
636        );
637        let expected_themes = Themes::from_data(expected_themes);
638        assert_eq!(config.themes, expected_themes, "Theme defined in config");
639    }
640
641    #[test]
642    fn can_define_multiple_themes_including_hex_themes_in_configfile() {
643        let config_contents = r##"
644            themes {
645                dracula {
646                    fg 248 248 242
647                    bg 40 42 54
648                    red 255 85 85
649                    green 80 250 123
650                    yellow 241 250 140
651                    blue 98 114 164
652                    magenta 255 121 198
653                    orange 255 184 108
654                    cyan 139 233 253
655                    black 0 0 0
656                    white 255 255 255
657                }
658                nord {
659                    fg "#D8DEE9"
660                    bg "#2E3440"
661                    black "#3B4252"
662                    red "#BF616A"
663                    green "#A3BE8C"
664                    yellow "#EBCB8B"
665                    blue "#81A1C1"
666                    magenta "#B48EAD"
667                    cyan "#88C0D0"
668                    white "#E5E9F0"
669                    orange "#D08770"
670                }
671            }
672        "##;
673        let config = Config::from_kdl(config_contents, None).unwrap();
674        let mut expected_themes = HashMap::new();
675        expected_themes.insert(
676            "dracula".into(),
677            Theme {
678                palette: Palette {
679                    fg: PaletteColor::Rgb((248, 248, 242)),
680                    bg: PaletteColor::Rgb((40, 42, 54)),
681                    red: PaletteColor::Rgb((255, 85, 85)),
682                    green: PaletteColor::Rgb((80, 250, 123)),
683                    yellow: PaletteColor::Rgb((241, 250, 140)),
684                    blue: PaletteColor::Rgb((98, 114, 164)),
685                    magenta: PaletteColor::Rgb((255, 121, 198)),
686                    orange: PaletteColor::Rgb((255, 184, 108)),
687                    cyan: PaletteColor::Rgb((139, 233, 253)),
688                    black: PaletteColor::Rgb((0, 0, 0)),
689                    white: PaletteColor::Rgb((255, 255, 255)),
690                    ..Default::default()
691                },
692                sourced_from_external_file: false,
693            },
694        );
695        expected_themes.insert(
696            "nord".into(),
697            Theme {
698                palette: Palette {
699                    fg: PaletteColor::Rgb((216, 222, 233)),
700                    bg: PaletteColor::Rgb((46, 52, 64)),
701                    black: PaletteColor::Rgb((59, 66, 82)),
702                    red: PaletteColor::Rgb((191, 97, 106)),
703                    green: PaletteColor::Rgb((163, 190, 140)),
704                    yellow: PaletteColor::Rgb((235, 203, 139)),
705                    blue: PaletteColor::Rgb((129, 161, 193)),
706                    magenta: PaletteColor::Rgb((180, 142, 173)),
707                    cyan: PaletteColor::Rgb((136, 192, 208)),
708                    white: PaletteColor::Rgb((229, 233, 240)),
709                    orange: PaletteColor::Rgb((208, 135, 112)),
710                    ..Default::default()
711                },
712                sourced_from_external_file: false,
713            },
714        );
715        let expected_themes = Themes::from_data(expected_themes);
716        assert_eq!(config.themes, expected_themes, "Theme defined in config");
717    }
718
719    #[test]
720    fn can_define_eight_bit_themes() {
721        let config_contents = r#"
722            themes {
723                eight_bit_theme {
724                    fg 248
725                    bg 40
726                    red 255
727                    green 80
728                    yellow 241
729                    blue 98
730                    magenta 255
731                    orange 255
732                    cyan 139
733                    black 1
734                    white 255
735                }
736            }
737        "#;
738        let config = Config::from_kdl(config_contents, None).unwrap();
739        let mut expected_themes = HashMap::new();
740        expected_themes.insert(
741            "eight_bit_theme".into(),
742            Theme {
743                palette: Palette {
744                    fg: PaletteColor::EightBit(248),
745                    bg: PaletteColor::EightBit(40),
746                    red: PaletteColor::EightBit(255),
747                    green: PaletteColor::EightBit(80),
748                    yellow: PaletteColor::EightBit(241),
749                    blue: PaletteColor::EightBit(98),
750                    magenta: PaletteColor::EightBit(255),
751                    orange: PaletteColor::EightBit(255),
752                    cyan: PaletteColor::EightBit(139),
753                    black: PaletteColor::EightBit(1),
754                    white: PaletteColor::EightBit(255),
755                    ..Default::default()
756                },
757                sourced_from_external_file: false,
758            },
759        );
760        let expected_themes = Themes::from_data(expected_themes);
761        assert_eq!(config.themes, expected_themes, "Theme defined in config");
762    }
763
764    #[test]
765    fn can_define_plugin_configuration_in_configfile() {
766        let config_contents = r#"
767            plugins {
768                tab-bar location="zellij:tab-bar"
769                status-bar location="zellij:status-bar"
770                strider location="zellij:strider"
771                compact-bar location="zellij:compact-bar"
772                session-manager location="zellij:session-manager"
773                welcome-screen location="zellij:session-manager" {
774                    welcome_screen true
775                }
776                filepicker location="zellij:strider"
777            }
778        "#;
779        let config = Config::from_kdl(config_contents, None).unwrap();
780        let mut expected_plugin_configuration = BTreeMap::new();
781        expected_plugin_configuration.insert(
782            "tab-bar".to_owned(),
783            RunPlugin::from_url("zellij:tab-bar").unwrap(),
784        );
785        expected_plugin_configuration.insert(
786            "status-bar".to_owned(),
787            RunPlugin::from_url("zellij:status-bar").unwrap(),
788        );
789        expected_plugin_configuration.insert(
790            "strider".to_owned(),
791            RunPlugin::from_url("zellij:strider").unwrap(),
792        );
793        expected_plugin_configuration.insert(
794            "compact-bar".to_owned(),
795            RunPlugin::from_url("zellij:compact-bar").unwrap(),
796        );
797        expected_plugin_configuration.insert(
798            "session-manager".to_owned(),
799            RunPlugin::from_url("zellij:session-manager").unwrap(),
800        );
801        let mut welcome_screen_configuration = BTreeMap::new();
802        welcome_screen_configuration.insert("welcome_screen".to_owned(), "true".to_owned());
803        expected_plugin_configuration.insert(
804            "welcome-screen".to_owned(),
805            RunPlugin::from_url("zellij:session-manager")
806                .unwrap()
807                .with_configuration(welcome_screen_configuration),
808        );
809        expected_plugin_configuration.insert(
810            "filepicker".to_owned(),
811            RunPlugin::from_url("zellij:strider").unwrap(),
812        );
813        assert_eq!(
814            config.plugins,
815            PluginAliases::from_data(expected_plugin_configuration),
816            "Plugins defined in config"
817        );
818    }
819
820    #[test]
821    fn can_define_ui_configuration_in_configfile() {
822        let config_contents = r#"
823            ui {
824                pane_frames {
825                    rounded_corners true
826                    hide_session_name true
827                }
828            }
829        "#;
830        let config = Config::from_kdl(config_contents, None).unwrap();
831        let expected_ui_config = UiConfig {
832            pane_frames: FrameConfig {
833                rounded_corners: true,
834                hide_session_name: true,
835            },
836        };
837        assert_eq!(config.ui, expected_ui_config, "Ui config defined in config");
838    }
839
840    #[test]
841    fn can_define_env_variables_in_config_file() {
842        let config_contents = r#"
843            env {
844                RUST_BACKTRACE 1
845                SOME_OTHER_VAR "foo"
846            }
847        "#;
848        let config = Config::from_kdl(config_contents, None).unwrap();
849        let mut expected_env_config = HashMap::new();
850        expected_env_config.insert("RUST_BACKTRACE".into(), "1".into());
851        expected_env_config.insert("SOME_OTHER_VAR".into(), "foo".into());
852        assert_eq!(
853            config.env,
854            EnvironmentVariables::from_data(expected_env_config),
855            "Env variables defined in config"
856        );
857    }
858}