zellij_utils/input/
config.rs

1use crate::data::Styling;
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 super::web_client::WebClientConfig;
18use crate::cli::{CliArgs, Command};
19use crate::envs::EnvironmentVariables;
20use crate::{home, setup};
21
22const DEFAULT_CONFIG_FILE_NAME: &str = "config.kdl";
23
24type ConfigResult = Result<Config, ConfigError>;
25
26/// Main configuration.
27#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
28pub struct Config {
29    pub keybinds: Keybinds,
30    pub options: Options,
31    pub themes: Themes,
32    pub plugins: PluginAliases,
33    pub ui: UiConfig,
34    pub env: EnvironmentVariables,
35    pub background_plugins: HashSet<RunPluginOrAlias>,
36    pub web_client: WebClientConfig,
37}
38
39#[derive(Error, Debug)]
40pub struct KdlError {
41    pub error_message: String,
42    pub src: Option<NamedSource>,
43    pub offset: Option<usize>,
44    pub len: Option<usize>,
45    pub help_message: Option<String>,
46}
47
48impl KdlError {
49    pub fn add_src(mut self, src_name: String, src_input: String) -> Self {
50        self.src = Some(NamedSource::new(src_name, src_input));
51        self
52    }
53}
54
55impl std::fmt::Display for KdlError {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
57        write!(f, "Failed to parse Zellij configuration")
58    }
59}
60use std::fmt::Display;
61
62impl Diagnostic for KdlError {
63    fn source_code(&self) -> Option<&dyn SourceCode> {
64        match self.src.as_ref() {
65            Some(src) => Some(src),
66            None => None,
67        }
68    }
69    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
70        match &self.help_message {
71            Some(help_message) => Some(Box::new(help_message)),
72            None => Some(Box::new(format!("For more information, please see our configuration guide: https://zellij.dev/documentation/configuration.html")))
73        }
74    }
75    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
76        if let (Some(offset), Some(len)) = (self.offset, self.len) {
77            let label = LabeledSpan::new(Some(self.error_message.clone()), offset, len);
78            Some(Box::new(std::iter::once(label)))
79        } else {
80            None
81        }
82    }
83}
84
85#[derive(Error, Debug, Diagnostic)]
86pub enum ConfigError {
87    // Deserialization error
88    #[error("Deserialization error: {0}")]
89    KdlDeserializationError(#[from] kdl::KdlError),
90    #[error("KdlDeserialization error: {0}")]
91    KdlError(KdlError), // TODO: consolidate these
92    #[error("Config error: {0}")]
93    Std(#[from] Box<dyn std::error::Error>),
94    // Io error with path context
95    #[error("IoError: {0}, File: {1}")]
96    IoPath(io::Error, PathBuf),
97    // Internal Deserialization Error
98    #[error("FromUtf8Error: {0}")]
99    FromUtf8(#[from] std::string::FromUtf8Error),
100    // Plugins have a semantic error, usually trying to parse two of the same tag
101    #[error("PluginsError: {0}")]
102    PluginsError(#[from] PluginsConfigError),
103    #[error("{0}")]
104    ConversionError(#[from] ConversionError),
105    #[error("{0}")]
106    DownloadError(String),
107}
108
109impl ConfigError {
110    pub fn new_kdl_error(error_message: String, offset: usize, len: usize) -> Self {
111        ConfigError::KdlError(KdlError {
112            error_message,
113            src: None,
114            offset: Some(offset),
115            len: Some(len),
116            help_message: None,
117        })
118    }
119    pub fn new_layout_kdl_error(error_message: String, offset: usize, len: usize) -> Self {
120        ConfigError::KdlError(KdlError {
121            error_message,
122            src: None,
123            offset: Some(offset),
124            len: Some(len),
125            help_message: Some(format!("For more information, please see our layout guide: https://zellij.dev/documentation/creating-a-layout.html")),
126        })
127    }
128}
129
130#[derive(Debug, Error)]
131pub enum ConversionError {
132    #[error("{0}")]
133    UnknownInputMode(String),
134}
135
136impl TryFrom<&CliArgs> for Config {
137    type Error = ConfigError;
138
139    fn try_from(opts: &CliArgs) -> ConfigResult {
140        if let Some(ref path) = opts.config {
141            let default_config = Config::from_default_assets()?;
142            return Config::from_path(path, Some(default_config));
143        }
144
145        if let Some(Command::Setup(ref setup)) = opts.command {
146            if setup.clean {
147                return Config::from_default_assets();
148            }
149        }
150
151        let config_dir = opts
152            .config_dir
153            .clone()
154            .or_else(home::find_default_config_dir);
155
156        if let Some(ref config) = config_dir {
157            let path = config.join(DEFAULT_CONFIG_FILE_NAME);
158            if path.exists() {
159                let default_config = Config::from_default_assets()?;
160                Config::from_path(&path, Some(default_config))
161            } else {
162                Config::from_default_assets()
163            }
164        } else {
165            Config::from_default_assets()
166        }
167    }
168}
169
170impl Config {
171    pub fn theme_config(&self, theme_name: Option<&String>) -> Option<Styling> {
172        match &theme_name {
173            Some(theme_name) => self.themes.get_theme(theme_name).map(|theme| theme.palette),
174            None => self.themes.get_theme("default").map(|theme| theme.palette),
175        }
176    }
177    /// Gets default configuration from assets
178    pub fn from_default_assets() -> ConfigResult {
179        let cfg = String::from_utf8(setup::DEFAULT_CONFIG.to_vec())?;
180        match Self::from_kdl(&cfg, None) {
181            Ok(config) => Ok(config),
182            Err(ConfigError::KdlError(kdl_error)) => Err(ConfigError::KdlError(
183                kdl_error.add_src("Default built-in-configuration".into(), cfg),
184            )),
185            Err(e) => Err(e),
186        }
187    }
188    pub fn from_path(path: &PathBuf, default_config: Option<Config>) -> ConfigResult {
189        match File::open(path) {
190            Ok(mut file) => {
191                let mut kdl_config = String::new();
192                file.read_to_string(&mut kdl_config)
193                    .map_err(|e| ConfigError::IoPath(e, path.to_path_buf()))?;
194                match Config::from_kdl(&kdl_config, default_config) {
195                    Ok(config) => Ok(config),
196                    Err(ConfigError::KdlDeserializationError(kdl_error)) => {
197                        let error_message = match kdl_error.kind {
198                            kdl::KdlErrorKind::Context("valid node terminator") => {
199                                format!("Failed to deserialize KDL node. \nPossible reasons:\n{}\n{}\n{}\n{}",
200                                "- Missing `;` after a node name, eg. { node; another_node; }",
201                                "- Missing quotations (\") around an argument node eg. { first_node \"argument_node\"; }",
202                                "- Missing an equal sign (=) between node arguments on a title line. eg. argument=\"value\"",
203                                "- Found an extraneous equal sign (=) between node child arguments and their values. eg. { argument=\"value\" }")
204                            },
205                            _ => {
206                                String::from(kdl_error.help.unwrap_or("Kdl Deserialization Error"))
207                            },
208                        };
209                        let kdl_error = KdlError {
210                            error_message,
211                            src: Some(NamedSource::new(
212                                path.as_path().as_os_str().to_string_lossy(),
213                                kdl_config,
214                            )),
215                            offset: Some(kdl_error.span.offset()),
216                            len: Some(kdl_error.span.len()),
217                            help_message: None,
218                        };
219                        Err(ConfigError::KdlError(kdl_error))
220                    },
221                    Err(ConfigError::KdlError(kdl_error)) => {
222                        Err(ConfigError::KdlError(kdl_error.add_src(
223                            path.as_path().as_os_str().to_string_lossy().to_string(),
224                            kdl_config,
225                        )))
226                    },
227                    Err(e) => Err(e),
228                }
229            },
230            Err(e) => Err(ConfigError::IoPath(e, path.into())),
231        }
232    }
233    pub fn merge(&mut self, other: Config) -> Result<(), ConfigError> {
234        self.options = self.options.merge(other.options);
235        self.keybinds.merge(other.keybinds.clone());
236        self.themes = self.themes.merge(other.themes);
237        self.plugins.merge(other.plugins);
238        self.ui = self.ui.merge(other.ui);
239        self.env = self.env.merge(other.env);
240        Ok(())
241    }
242    pub fn config_file_path(opts: &CliArgs) -> Option<PathBuf> {
243        opts.config.clone().or_else(|| {
244            opts.config_dir
245                .clone()
246                .or_else(home::find_default_config_dir)
247                .map(|config_dir| config_dir.join(DEFAULT_CONFIG_FILE_NAME))
248        })
249    }
250    pub fn default_config_file_path() -> Option<PathBuf> {
251        home::find_default_config_dir().map(|config_dir| config_dir.join(DEFAULT_CONFIG_FILE_NAME))
252    }
253    pub fn write_config_to_disk(config: String, opts: &CliArgs) -> Result<Config, Option<PathBuf>> {
254        // if we fail, try to return the PathBuf of the file we were not able to write to
255        Config::from_kdl(&config, None)
256            .map_err(|e| {
257                log::error!("Failed to parse config: {}", e);
258                None
259            })
260            .and_then(|parsed_config| {
261                let backed_up_file_name = Config::backup_current_config(&opts)?;
262                let config_file_path = Config::config_file_path(&opts).ok_or_else(|| {
263                    log::error!("Config file path not found");
264                    None
265                })?;
266                let config = match backed_up_file_name {
267                    Some(backed_up_file_name) => {
268                        format!(
269                            "{}{}",
270                            Config::autogen_config_message(backed_up_file_name),
271                            config
272                        )
273                    },
274                    None => config,
275                };
276                std::fs::write(&config_file_path, config.as_bytes()).map_err(|e| {
277                    log::error!("Failed to write config: {}", e);
278                    Some(config_file_path.clone())
279                })?;
280                let written_config = std::fs::read_to_string(&config_file_path).map_err(|e| {
281                    log::error!("Failed to read written config: {}", e);
282                    Some(config_file_path.clone())
283                })?;
284                let parsed_written_config =
285                    Config::from_kdl(&written_config, None).map_err(|e| {
286                        log::error!("Failed to parse written config: {}", e);
287                        None
288                    })?;
289                if parsed_written_config == parsed_config {
290                    Ok(parsed_config)
291                } else {
292                    log::error!("Configuration corrupted when writing to disk");
293                    Err(Some(config_file_path))
294                }
295            })
296    }
297    // returns true if the config was not previouly written to disk and we successfully wrote it
298    pub fn write_config_to_disk_if_it_does_not_exist(config: String, opts: &CliArgs) -> bool {
299        if opts.config.is_none() {
300            // if a config file path wasn't explicitly specified, we try to create the default
301            // config folder
302            home::try_create_home_config_dir();
303        }
304        match Config::config_file_path(opts) {
305            Some(config_file_path) => {
306                if config_file_path.exists() {
307                    false
308                } else {
309                    if let Err(e) = std::fs::write(&config_file_path, config.as_bytes()) {
310                        log::error!("Failed to write config to disk: {}", e);
311                        return false;
312                    }
313                    match std::fs::read_to_string(&config_file_path) {
314                        Ok(written_config) => written_config == config,
315                        Err(e) => {
316                            log::error!("Failed to read written config: {}", e);
317                            false
318                        },
319                    }
320                }
321            },
322            None => false,
323        }
324    }
325    fn find_free_backup_file_name(config_file_path: &PathBuf) -> Option<PathBuf> {
326        let mut backup_config_path = None;
327        let config_file_name = config_file_path
328            .file_name()
329            .and_then(|f| f.to_str())
330            .unwrap_or_else(|| DEFAULT_CONFIG_FILE_NAME);
331        for i in 0..100 {
332            let new_file_name = if i == 0 {
333                format!("{}.bak", config_file_name)
334            } else {
335                format!("{}.bak.{}", config_file_name, i)
336            };
337            let mut potential_config_path = config_file_path.clone();
338            potential_config_path.set_file_name(new_file_name);
339            if !potential_config_path.exists() {
340                backup_config_path = Some(potential_config_path);
341                break;
342            }
343        }
344        backup_config_path
345    }
346    fn backup_config_with_written_content_confirmation(
347        current_config: &str,
348        current_config_file_path: &PathBuf,
349        backup_config_path: &PathBuf,
350    ) -> bool {
351        let _ = std::fs::copy(current_config_file_path, &backup_config_path);
352        match std::fs::read_to_string(&backup_config_path) {
353            Ok(backed_up_config) => current_config == &backed_up_config,
354            Err(e) => {
355                log::error!(
356                    "Failed to back up config file {}: {:?}",
357                    backup_config_path.display(),
358                    e
359                );
360                false
361            },
362        }
363    }
364    fn backup_current_config(opts: &CliArgs) -> Result<Option<PathBuf>, Option<PathBuf>> {
365        // if we fail, try to return the PathBuf of the file we were not able to write to
366        if let Some(config_file_path) = Config::config_file_path(&opts) {
367            match std::fs::read_to_string(&config_file_path) {
368                Ok(current_config) => {
369                    let Some(backup_config_path) =
370                        Config::find_free_backup_file_name(&config_file_path)
371                    else {
372                        log::error!("Failed to find a file name to back up the configuration to, ran out of files.");
373                        return Err(None);
374                    };
375                    if Config::backup_config_with_written_content_confirmation(
376                        &current_config,
377                        &config_file_path,
378                        &backup_config_path,
379                    ) {
380                        Ok(Some(backup_config_path))
381                    } else {
382                        log::error!(
383                            "Failed to back up config file: {}",
384                            backup_config_path.display()
385                        );
386                        Err(Some(backup_config_path))
387                    }
388                },
389                Err(e) => {
390                    if e.kind() == std::io::ErrorKind::NotFound {
391                        Ok(None)
392                    } else {
393                        log::error!(
394                            "Failed to read current config {}: {}",
395                            config_file_path.display(),
396                            e
397                        );
398                        Err(Some(config_file_path))
399                    }
400                },
401            }
402        } else {
403            log::error!("No config file path found?");
404            Err(None)
405        }
406    }
407    fn autogen_config_message(backed_up_file_name: PathBuf) -> String {
408        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())
409    }
410}
411
412#[cfg(not(target_family = "wasm"))]
413pub async fn watch_config_file_changes<F, Fut>(config_file_path: PathBuf, on_config_change: F)
414where
415    F: Fn(Config) -> Fut + Send + 'static,
416    Fut: std::future::Future<Output = ()> + Send,
417{
418    // in a gist, what we do here is fire the `on_config_change` function whenever there is a
419    // change in the config file, we do this by:
420    // 1. Trying to watch the provided config file for changes
421    // 2. If the file is deleted or does not exist, we periodically poll for it (manually, not
422    //    through filesystem events)
423    // 3. Once it exists, we start watching it for changes again
424    //
425    // we do this because the alternative is to watch its parent folder and this might cause the
426    // classic "too many open files" issue if there are a lot of files there and/or lots of Zellij
427    // instances
428    use crate::setup::Setup;
429    use notify::{self, Config as WatcherConfig, Event, PollWatcher, RecursiveMode, Watcher};
430    use std::time::Duration;
431    use tokio::sync::mpsc;
432    loop {
433        if config_file_path.exists() {
434            let (tx, mut rx) = mpsc::unbounded_channel();
435
436            let mut watcher = match PollWatcher::new(
437                move |res: Result<Event, notify::Error>| {
438                    let _ = tx.send(res);
439                },
440                WatcherConfig::default().with_poll_interval(Duration::from_secs(1)),
441            ) {
442                Ok(watcher) => watcher,
443                Err(_) => break,
444            };
445
446            if watcher
447                .watch(&config_file_path, RecursiveMode::NonRecursive)
448                .is_err()
449            {
450                break;
451            }
452
453            while let Some(event_result) = rx.recv().await {
454                match event_result {
455                    Ok(event) => {
456                        if event.paths.contains(&config_file_path) {
457                            if event.kind.is_remove() {
458                                break;
459                            } else if event.kind.is_create() || event.kind.is_modify() {
460                                tokio::time::sleep(Duration::from_millis(100)).await;
461
462                                if !config_file_path.exists() {
463                                    continue;
464                                }
465
466                                let mut cli_args_for_config = CliArgs::default();
467                                cli_args_for_config.config = Some(PathBuf::from(&config_file_path));
468                                if let Ok(new_config) = Setup::from_cli_args(&cli_args_for_config)
469                                    .map_err(|e| e.to_string())
470                                {
471                                    on_config_change(new_config.0).await;
472                                }
473                            }
474                        }
475                    },
476                    Err(_) => break,
477                }
478            }
479        }
480
481        while !config_file_path.exists() {
482            tokio::time::sleep(Duration::from_secs(3)).await;
483        }
484    }
485}
486
487#[cfg(test)]
488mod config_test {
489    use super::*;
490    use crate::data::{InputMode, Palette, PaletteColor, StyleDeclaration, Styling};
491    use crate::input::layout::RunPlugin;
492    use crate::input::options::{Clipboard, OnForceClose};
493    use crate::input::theme::{FrameConfig, Theme, Themes, UiConfig};
494    use std::collections::{BTreeMap, HashMap};
495    use std::io::Write;
496    use tempfile::tempdir;
497
498    #[test]
499    fn try_from_cli_args_with_config() {
500        // makes sure loading a config file with --config tries to load the config
501        let arbitrary_config = PathBuf::from("nonexistent.yaml");
502        let opts = CliArgs {
503            config: Some(arbitrary_config),
504            ..Default::default()
505        };
506        println!("OPTS= {:?}", opts);
507        let result = Config::try_from(&opts);
508        assert!(result.is_err());
509    }
510
511    #[test]
512    fn try_from_cli_args_with_option_clean() {
513        // makes sure --clean works... TODO: how can this actually fail now?
514        use crate::setup::Setup;
515        let opts = CliArgs {
516            command: Some(Command::Setup(Setup {
517                clean: true,
518                ..Setup::default()
519            })),
520            ..Default::default()
521        };
522        let result = Config::try_from(&opts);
523        assert!(result.is_ok());
524    }
525
526    #[test]
527    fn try_from_cli_args_with_config_dir() {
528        let mut opts = CliArgs::default();
529        let tmp = tempdir().unwrap();
530        File::create(tmp.path().join(DEFAULT_CONFIG_FILE_NAME))
531            .unwrap()
532            .write_all(b"keybinds: invalid\n")
533            .unwrap();
534        opts.config_dir = Some(tmp.path().to_path_buf());
535        let result = Config::try_from(&opts);
536        assert!(result.is_err());
537    }
538
539    #[test]
540    fn try_from_cli_args_with_config_dir_without_config() {
541        let mut opts = CliArgs::default();
542        let tmp = tempdir().unwrap();
543        opts.config_dir = Some(tmp.path().to_path_buf());
544        let result = Config::try_from(&opts);
545        assert_eq!(result.unwrap(), Config::from_default_assets().unwrap());
546    }
547
548    #[test]
549    fn try_from_cli_args_default() {
550        let opts = CliArgs::default();
551        let result = Config::try_from(&opts);
552        assert_eq!(result.unwrap(), Config::from_default_assets().unwrap());
553    }
554
555    #[test]
556    fn can_define_options_in_configfile() {
557        let config_contents = r#"
558            simplified_ui true
559            theme "my cool theme"
560            default_mode "locked"
561            default_shell "/path/to/my/shell"
562            default_cwd "/path"
563            default_layout "/path/to/my/layout.kdl"
564            layout_dir "/path/to/my/layout-dir"
565            theme_dir "/path/to/my/theme-dir"
566            mouse_mode false
567            pane_frames false
568            mirror_session true
569            on_force_close "quit"
570            scroll_buffer_size 100000
571            copy_command "/path/to/my/copy-command"
572            copy_clipboard "primary"
573            copy_on_select false
574            scrollback_editor "/path/to/my/scrollback-editor"
575            session_name "my awesome session"
576            attach_to_session true
577        "#;
578        let config = Config::from_kdl(config_contents, None).unwrap();
579        assert_eq!(
580            config.options.simplified_ui,
581            Some(true),
582            "Option set in config"
583        );
584        assert_eq!(
585            config.options.theme,
586            Some(String::from("my cool theme")),
587            "Option set in config"
588        );
589        assert_eq!(
590            config.options.default_mode,
591            Some(InputMode::Locked),
592            "Option set in config"
593        );
594        assert_eq!(
595            config.options.default_shell,
596            Some(PathBuf::from("/path/to/my/shell")),
597            "Option set in config"
598        );
599        assert_eq!(
600            config.options.default_cwd,
601            Some(PathBuf::from("/path")),
602            "Option set in config"
603        );
604        assert_eq!(
605            config.options.default_layout,
606            Some(PathBuf::from("/path/to/my/layout.kdl")),
607            "Option set in config"
608        );
609        assert_eq!(
610            config.options.layout_dir,
611            Some(PathBuf::from("/path/to/my/layout-dir")),
612            "Option set in config"
613        );
614        assert_eq!(
615            config.options.theme_dir,
616            Some(PathBuf::from("/path/to/my/theme-dir")),
617            "Option set in config"
618        );
619        assert_eq!(
620            config.options.mouse_mode,
621            Some(false),
622            "Option set in config"
623        );
624        assert_eq!(
625            config.options.pane_frames,
626            Some(false),
627            "Option set in config"
628        );
629        assert_eq!(
630            config.options.mirror_session,
631            Some(true),
632            "Option set in config"
633        );
634        assert_eq!(
635            config.options.on_force_close,
636            Some(OnForceClose::Quit),
637            "Option set in config"
638        );
639        assert_eq!(
640            config.options.scroll_buffer_size,
641            Some(100000),
642            "Option set in config"
643        );
644        assert_eq!(
645            config.options.copy_command,
646            Some(String::from("/path/to/my/copy-command")),
647            "Option set in config"
648        );
649        assert_eq!(
650            config.options.copy_clipboard,
651            Some(Clipboard::Primary),
652            "Option set in config"
653        );
654        assert_eq!(
655            config.options.copy_on_select,
656            Some(false),
657            "Option set in config"
658        );
659        assert_eq!(
660            config.options.scrollback_editor,
661            Some(PathBuf::from("/path/to/my/scrollback-editor")),
662            "Option set in config"
663        );
664        assert_eq!(
665            config.options.session_name,
666            Some(String::from("my awesome session")),
667            "Option set in config"
668        );
669        assert_eq!(
670            config.options.attach_to_session,
671            Some(true),
672            "Option set in config"
673        );
674    }
675
676    #[test]
677    fn can_define_themes_in_configfile() {
678        let config_contents = r#"
679            themes {
680                dracula {
681                    fg 248 248 242
682                    bg 40 42 54
683                    red 255 85 85
684                    green 80 250 123
685                    yellow 241 250 140
686                    blue 98 114 164
687                    magenta 255 121 198
688                    orange 255 184 108
689                    cyan 139 233 253
690                    black 0 0 0
691                    white 255 255 255
692                }
693            }
694        "#;
695        let config = Config::from_kdl(config_contents, None).unwrap();
696        let mut expected_themes = HashMap::new();
697        expected_themes.insert(
698            "dracula".into(),
699            Theme {
700                palette: Palette {
701                    fg: PaletteColor::Rgb((248, 248, 242)),
702                    bg: PaletteColor::Rgb((40, 42, 54)),
703                    red: PaletteColor::Rgb((255, 85, 85)),
704                    green: PaletteColor::Rgb((80, 250, 123)),
705                    yellow: PaletteColor::Rgb((241, 250, 140)),
706                    blue: PaletteColor::Rgb((98, 114, 164)),
707                    magenta: PaletteColor::Rgb((255, 121, 198)),
708                    orange: PaletteColor::Rgb((255, 184, 108)),
709                    cyan: PaletteColor::Rgb((139, 233, 253)),
710                    black: PaletteColor::Rgb((0, 0, 0)),
711                    white: PaletteColor::Rgb((255, 255, 255)),
712                    ..Default::default()
713                }
714                .into(),
715                sourced_from_external_file: false,
716            },
717        );
718        let expected_themes = Themes::from_data(expected_themes);
719        assert_eq!(config.themes, expected_themes, "Theme defined in config");
720    }
721
722    #[test]
723    fn can_define_multiple_themes_including_hex_themes_in_configfile() {
724        let config_contents = r##"
725            themes {
726                dracula {
727                    fg 248 248 242
728                    bg 40 42 54
729                    red 255 85 85
730                    green 80 250 123
731                    yellow 241 250 140
732                    blue 98 114 164
733                    magenta 255 121 198
734                    orange 255 184 108
735                    cyan 139 233 253
736                    black 0 0 0
737                    white 255 255 255
738                }
739                nord {
740                    fg "#D8DEE9"
741                    bg "#2E3440"
742                    black "#3B4252"
743                    red "#BF616A"
744                    green "#A3BE8C"
745                    yellow "#EBCB8B"
746                    blue "#81A1C1"
747                    magenta "#B48EAD"
748                    cyan "#88C0D0"
749                    white "#E5E9F0"
750                    orange "#D08770"
751                }
752            }
753        "##;
754        let config = Config::from_kdl(config_contents, None).unwrap();
755        let mut expected_themes = HashMap::new();
756        expected_themes.insert(
757            "dracula".into(),
758            Theme {
759                palette: Palette {
760                    fg: PaletteColor::Rgb((248, 248, 242)),
761                    bg: PaletteColor::Rgb((40, 42, 54)),
762                    red: PaletteColor::Rgb((255, 85, 85)),
763                    green: PaletteColor::Rgb((80, 250, 123)),
764                    yellow: PaletteColor::Rgb((241, 250, 140)),
765                    blue: PaletteColor::Rgb((98, 114, 164)),
766                    magenta: PaletteColor::Rgb((255, 121, 198)),
767                    orange: PaletteColor::Rgb((255, 184, 108)),
768                    cyan: PaletteColor::Rgb((139, 233, 253)),
769                    black: PaletteColor::Rgb((0, 0, 0)),
770                    white: PaletteColor::Rgb((255, 255, 255)),
771                    ..Default::default()
772                }
773                .into(),
774                sourced_from_external_file: false,
775            },
776        );
777        expected_themes.insert(
778            "nord".into(),
779            Theme {
780                palette: Palette {
781                    fg: PaletteColor::Rgb((216, 222, 233)),
782                    bg: PaletteColor::Rgb((46, 52, 64)),
783                    black: PaletteColor::Rgb((59, 66, 82)),
784                    red: PaletteColor::Rgb((191, 97, 106)),
785                    green: PaletteColor::Rgb((163, 190, 140)),
786                    yellow: PaletteColor::Rgb((235, 203, 139)),
787                    blue: PaletteColor::Rgb((129, 161, 193)),
788                    magenta: PaletteColor::Rgb((180, 142, 173)),
789                    cyan: PaletteColor::Rgb((136, 192, 208)),
790                    white: PaletteColor::Rgb((229, 233, 240)),
791                    orange: PaletteColor::Rgb((208, 135, 112)),
792                    ..Default::default()
793                }
794                .into(),
795                sourced_from_external_file: false,
796            },
797        );
798        let expected_themes = Themes::from_data(expected_themes);
799        assert_eq!(config.themes, expected_themes, "Theme defined in config");
800    }
801
802    #[test]
803    fn can_define_eight_bit_themes() {
804        let config_contents = r#"
805            themes {
806                eight_bit_theme {
807                    fg 248
808                    bg 40
809                    red 255
810                    green 80
811                    yellow 241
812                    blue 98
813                    magenta 255
814                    orange 255
815                    cyan 139
816                    black 1
817                    white 255
818                }
819            }
820        "#;
821        let config = Config::from_kdl(config_contents, None).unwrap();
822        let mut expected_themes = HashMap::new();
823        expected_themes.insert(
824            "eight_bit_theme".into(),
825            Theme {
826                palette: Palette {
827                    fg: PaletteColor::EightBit(248),
828                    bg: PaletteColor::EightBit(40),
829                    red: PaletteColor::EightBit(255),
830                    green: PaletteColor::EightBit(80),
831                    yellow: PaletteColor::EightBit(241),
832                    blue: PaletteColor::EightBit(98),
833                    magenta: PaletteColor::EightBit(255),
834                    orange: PaletteColor::EightBit(255),
835                    cyan: PaletteColor::EightBit(139),
836                    black: PaletteColor::EightBit(1),
837                    white: PaletteColor::EightBit(255),
838                    ..Default::default()
839                }
840                .into(),
841                sourced_from_external_file: false,
842            },
843        );
844        let expected_themes = Themes::from_data(expected_themes);
845        assert_eq!(config.themes, expected_themes, "Theme defined in config");
846    }
847
848    #[test]
849    fn can_define_style_for_theme_with_hex() {
850        let config_contents = r##"
851            themes {
852                named_theme {
853                    text_unselected {
854                        base "#DCD7BA"
855                        emphasis_0 "#DCD7CD"
856                        emphasis_1 "#DCD8DD"
857                        emphasis_2 "#DCD899"
858                        emphasis_3 "#ACD7CD"
859                        background   "#1F1F28"
860                    }
861                    text_selected {
862                        base "#16161D"
863                        emphasis_0 "#16161D"
864                        emphasis_1 "#16161D"
865                        emphasis_2 "#16161D"
866                        emphasis_3 "#16161D"
867                        background   "#9CABCA"
868                    }
869                    ribbon_unselected {
870                        base "#DCD7BA"
871                        emphasis_0 "#7FB4CA"
872                        emphasis_1 "#A3D4D5"
873                        emphasis_2 "#7AA89F"
874                        emphasis_3 "#DCD819"
875                        background   "#252535"
876                    }
877                    ribbon_selected {
878                        base "#16161D"
879                        emphasis_0 "#181820"
880                        emphasis_1 "#1A1A22"
881                        emphasis_2 "#2A2A37"
882                        emphasis_3 "#363646"
883                        background   "#76946A"
884                    }
885                    table_title {
886                        base "#DCD7BA"
887                        emphasis_0 "#7FB4CA"
888                        emphasis_1 "#A3D4D5"
889                        emphasis_2 "#7AA89F"
890                        emphasis_3 "#DCD819"
891                        background   "#252535"
892                    }
893                    table_cell_unselected {
894                        base "#DCD7BA"
895                        emphasis_0 "#DCD7CD"
896                        emphasis_1 "#DCD8DD"
897                        emphasis_2 "#DCD899"
898                        emphasis_3 "#ACD7CD"
899                        background   "#1F1F28"
900                    }
901                    table_cell_selected {
902                        base "#16161D"
903                        emphasis_0 "#181820"
904                        emphasis_1 "#1A1A22"
905                        emphasis_2 "#2A2A37"
906                        emphasis_3 "#363646"
907                        background   "#76946A"
908                    }
909                    list_unselected {
910                        base "#DCD7BA"
911                        emphasis_0 "#DCD7CD"
912                        emphasis_1 "#DCD8DD"
913                        emphasis_2 "#DCD899"
914                        emphasis_3 "#ACD7CD"
915                        background   "#1F1F28"
916                    }
917                    list_selected {
918                        base "#16161D"
919                        emphasis_0 "#181820"
920                        emphasis_1 "#1A1A22"
921                        emphasis_2 "#2A2A37"
922                        emphasis_3 "#363646"
923                        background   "#76946A"
924                    }
925                    frame_unselected {
926                        base "#DCD8DD"
927                        emphasis_0 "#7FB4CA"
928                        emphasis_1 "#A3D4D5"
929                        emphasis_2 "#7AA89F"
930                        emphasis_3 "#DCD819"
931                    }
932                    frame_selected {
933                        base "#76946A"
934                        emphasis_0 "#C34043"
935                        emphasis_1 "#C8C093"
936                        emphasis_2 "#ACD7CD"
937                        emphasis_3 "#DCD819"
938                    }
939                    exit_code_success {
940                        base "#76946A"
941                        emphasis_0 "#76946A"
942                        emphasis_1 "#76946A"
943                        emphasis_2 "#76946A"
944                        emphasis_3 "#76946A"
945                    }
946                    exit_code_error {
947                        base "#C34043"
948                        emphasis_0 "#C34043"
949                        emphasis_1 "#C34043"
950                        emphasis_2 "#C34043"
951                        emphasis_3 "#C34043"
952                    }
953                }
954            }
955            "##;
956
957        let config = Config::from_kdl(config_contents, None).unwrap();
958        let mut expected_themes = HashMap::new();
959        expected_themes.insert(
960            "named_theme".into(),
961            Theme {
962                sourced_from_external_file: false,
963                palette: Styling {
964                    text_unselected: StyleDeclaration {
965                        base: PaletteColor::Rgb((220, 215, 186)),
966                        emphasis_0: PaletteColor::Rgb((220, 215, 205)),
967                        emphasis_1: PaletteColor::Rgb((220, 216, 221)),
968                        emphasis_2: PaletteColor::Rgb((220, 216, 153)),
969                        emphasis_3: PaletteColor::Rgb((172, 215, 205)),
970                        background: PaletteColor::Rgb((31, 31, 40)),
971                    },
972                    text_selected: StyleDeclaration {
973                        base: PaletteColor::Rgb((22, 22, 29)),
974                        emphasis_0: PaletteColor::Rgb((22, 22, 29)),
975                        emphasis_1: PaletteColor::Rgb((22, 22, 29)),
976                        emphasis_2: PaletteColor::Rgb((22, 22, 29)),
977                        emphasis_3: PaletteColor::Rgb((22, 22, 29)),
978                        background: PaletteColor::Rgb((156, 171, 202)),
979                    },
980                    ribbon_unselected: StyleDeclaration {
981                        base: PaletteColor::Rgb((220, 215, 186)),
982                        emphasis_0: PaletteColor::Rgb((127, 180, 202)),
983                        emphasis_1: PaletteColor::Rgb((163, 212, 213)),
984                        emphasis_2: PaletteColor::Rgb((122, 168, 159)),
985                        emphasis_3: PaletteColor::Rgb((220, 216, 25)),
986                        background: PaletteColor::Rgb((37, 37, 53)),
987                    },
988                    ribbon_selected: StyleDeclaration {
989                        base: PaletteColor::Rgb((22, 22, 29)),
990                        emphasis_0: PaletteColor::Rgb((24, 24, 32)),
991                        emphasis_1: PaletteColor::Rgb((26, 26, 34)),
992                        emphasis_2: PaletteColor::Rgb((42, 42, 55)),
993                        emphasis_3: PaletteColor::Rgb((54, 54, 70)),
994                        background: PaletteColor::Rgb((118, 148, 106)),
995                    },
996                    table_title: StyleDeclaration {
997                        base: PaletteColor::Rgb((220, 215, 186)),
998                        emphasis_0: PaletteColor::Rgb((127, 180, 202)),
999                        emphasis_1: PaletteColor::Rgb((163, 212, 213)),
1000                        emphasis_2: PaletteColor::Rgb((122, 168, 159)),
1001                        emphasis_3: PaletteColor::Rgb((220, 216, 25)),
1002                        background: PaletteColor::Rgb((37, 37, 53)),
1003                    },
1004                    table_cell_unselected: StyleDeclaration {
1005                        base: PaletteColor::Rgb((220, 215, 186)),
1006                        emphasis_0: PaletteColor::Rgb((220, 215, 205)),
1007                        emphasis_1: PaletteColor::Rgb((220, 216, 221)),
1008                        emphasis_2: PaletteColor::Rgb((220, 216, 153)),
1009                        emphasis_3: PaletteColor::Rgb((172, 215, 205)),
1010                        background: PaletteColor::Rgb((31, 31, 40)),
1011                    },
1012                    table_cell_selected: StyleDeclaration {
1013                        base: PaletteColor::Rgb((22, 22, 29)),
1014                        emphasis_0: PaletteColor::Rgb((24, 24, 32)),
1015                        emphasis_1: PaletteColor::Rgb((26, 26, 34)),
1016                        emphasis_2: PaletteColor::Rgb((42, 42, 55)),
1017                        emphasis_3: PaletteColor::Rgb((54, 54, 70)),
1018                        background: PaletteColor::Rgb((118, 148, 106)),
1019                    },
1020                    list_unselected: StyleDeclaration {
1021                        base: PaletteColor::Rgb((220, 215, 186)),
1022                        emphasis_0: PaletteColor::Rgb((220, 215, 205)),
1023                        emphasis_1: PaletteColor::Rgb((220, 216, 221)),
1024                        emphasis_2: PaletteColor::Rgb((220, 216, 153)),
1025                        emphasis_3: PaletteColor::Rgb((172, 215, 205)),
1026                        background: PaletteColor::Rgb((31, 31, 40)),
1027                    },
1028                    list_selected: StyleDeclaration {
1029                        base: PaletteColor::Rgb((22, 22, 29)),
1030                        emphasis_0: PaletteColor::Rgb((24, 24, 32)),
1031                        emphasis_1: PaletteColor::Rgb((26, 26, 34)),
1032                        emphasis_2: PaletteColor::Rgb((42, 42, 55)),
1033                        emphasis_3: PaletteColor::Rgb((54, 54, 70)),
1034                        background: PaletteColor::Rgb((118, 148, 106)),
1035                    },
1036                    frame_unselected: Some(StyleDeclaration {
1037                        base: PaletteColor::Rgb((220, 216, 221)),
1038                        emphasis_0: PaletteColor::Rgb((127, 180, 202)),
1039                        emphasis_1: PaletteColor::Rgb((163, 212, 213)),
1040                        emphasis_2: PaletteColor::Rgb((122, 168, 159)),
1041                        emphasis_3: PaletteColor::Rgb((220, 216, 25)),
1042                        ..Default::default()
1043                    }),
1044                    frame_selected: StyleDeclaration {
1045                        base: PaletteColor::Rgb((118, 148, 106)),
1046                        emphasis_0: PaletteColor::Rgb((195, 64, 67)),
1047                        emphasis_1: PaletteColor::Rgb((200, 192, 147)),
1048                        emphasis_2: PaletteColor::Rgb((172, 215, 205)),
1049                        emphasis_3: PaletteColor::Rgb((220, 216, 25)),
1050                        ..Default::default()
1051                    },
1052                    exit_code_success: StyleDeclaration {
1053                        base: PaletteColor::Rgb((118, 148, 106)),
1054                        emphasis_0: PaletteColor::Rgb((118, 148, 106)),
1055                        emphasis_1: PaletteColor::Rgb((118, 148, 106)),
1056                        emphasis_2: PaletteColor::Rgb((118, 148, 106)),
1057                        emphasis_3: PaletteColor::Rgb((118, 148, 106)),
1058                        ..Default::default()
1059                    },
1060                    exit_code_error: StyleDeclaration {
1061                        base: PaletteColor::Rgb((195, 64, 67)),
1062                        emphasis_0: PaletteColor::Rgb((195, 64, 67)),
1063                        emphasis_1: PaletteColor::Rgb((195, 64, 67)),
1064                        emphasis_2: PaletteColor::Rgb((195, 64, 67)),
1065                        emphasis_3: PaletteColor::Rgb((195, 64, 67)),
1066                        ..Default::default()
1067                    },
1068                    ..Default::default()
1069                },
1070            },
1071        );
1072        let expected_themes = Themes::from_data(expected_themes);
1073        assert_eq!(config.themes, expected_themes, "Theme defined in config")
1074    }
1075
1076    #[test]
1077    fn omitting_required_style_errors() {
1078        let config_contents = r##"
1079            themes {
1080                named_theme {
1081                    text_unselected {
1082                        base "#DCD7BA"
1083                        emphasis_1 "#DCD8DD"
1084                        emphasis_2 "#DCD899"
1085                        emphasis_3 "#ACD7CD"
1086                        background   "#1F1F28"
1087                    }
1088                }
1089            }
1090            "##;
1091
1092        let config = Config::from_kdl(config_contents, None);
1093        assert!(config.is_err());
1094        if let Err(ConfigError::KdlError(KdlError {
1095            error_message,
1096            src: _,
1097            offset: _,
1098            len: _,
1099            help_message: _,
1100        })) = config
1101        {
1102            assert_eq!(error_message, "Missing theme color: emphasis_0")
1103        }
1104    }
1105
1106    #[test]
1107    fn partial_declaration_of_styles_defaults_omitted() {
1108        let config_contents = r##"
1109            themes {
1110                named_theme {
1111                    text_unselected {
1112                        base "#DCD7BA"
1113                        emphasis_0 "#DCD7CD"
1114                        emphasis_1 "#DCD8DD"
1115                        emphasis_2 "#DCD899"
1116                        emphasis_3 "#ACD7CD"
1117                        background   "#1F1F28"
1118                    }
1119                }
1120            }
1121            "##;
1122
1123        let config = Config::from_kdl(config_contents, None).unwrap();
1124        let mut expected_themes = HashMap::new();
1125        expected_themes.insert(
1126            "named_theme".into(),
1127            Theme {
1128                sourced_from_external_file: false,
1129                palette: Styling {
1130                    text_unselected: StyleDeclaration {
1131                        base: PaletteColor::Rgb((220, 215, 186)),
1132                        emphasis_0: PaletteColor::Rgb((220, 215, 205)),
1133                        emphasis_1: PaletteColor::Rgb((220, 216, 221)),
1134                        emphasis_2: PaletteColor::Rgb((220, 216, 153)),
1135                        emphasis_3: PaletteColor::Rgb((172, 215, 205)),
1136                        background: PaletteColor::Rgb((31, 31, 40)),
1137                    },
1138                    ..Default::default()
1139                },
1140            },
1141        );
1142        let expected_themes = Themes::from_data(expected_themes);
1143        assert_eq!(config.themes, expected_themes, "Theme defined in config")
1144    }
1145
1146    #[test]
1147    fn can_define_plugin_configuration_in_configfile() {
1148        let config_contents = r#"
1149            plugins {
1150                tab-bar location="zellij:tab-bar"
1151                status-bar location="zellij:status-bar"
1152                strider location="zellij:strider"
1153                compact-bar location="zellij:compact-bar"
1154                session-manager location="zellij:session-manager"
1155                welcome-screen location="zellij:session-manager" {
1156                    welcome_screen true
1157                }
1158                filepicker location="zellij:strider"
1159            }
1160        "#;
1161        let config = Config::from_kdl(config_contents, None).unwrap();
1162        let mut expected_plugin_configuration = BTreeMap::new();
1163        expected_plugin_configuration.insert(
1164            "tab-bar".to_owned(),
1165            RunPlugin::from_url("zellij:tab-bar").unwrap(),
1166        );
1167        expected_plugin_configuration.insert(
1168            "status-bar".to_owned(),
1169            RunPlugin::from_url("zellij:status-bar").unwrap(),
1170        );
1171        expected_plugin_configuration.insert(
1172            "strider".to_owned(),
1173            RunPlugin::from_url("zellij:strider").unwrap(),
1174        );
1175        expected_plugin_configuration.insert(
1176            "compact-bar".to_owned(),
1177            RunPlugin::from_url("zellij:compact-bar").unwrap(),
1178        );
1179        expected_plugin_configuration.insert(
1180            "session-manager".to_owned(),
1181            RunPlugin::from_url("zellij:session-manager").unwrap(),
1182        );
1183        let mut welcome_screen_configuration = BTreeMap::new();
1184        welcome_screen_configuration.insert("welcome_screen".to_owned(), "true".to_owned());
1185        expected_plugin_configuration.insert(
1186            "welcome-screen".to_owned(),
1187            RunPlugin::from_url("zellij:session-manager")
1188                .unwrap()
1189                .with_configuration(welcome_screen_configuration),
1190        );
1191        expected_plugin_configuration.insert(
1192            "filepicker".to_owned(),
1193            RunPlugin::from_url("zellij:strider").unwrap(),
1194        );
1195        assert_eq!(
1196            config.plugins,
1197            PluginAliases::from_data(expected_plugin_configuration),
1198            "Plugins defined in config"
1199        );
1200    }
1201
1202    #[test]
1203    fn can_define_ui_configuration_in_configfile() {
1204        let config_contents = r#"
1205            ui {
1206                pane_frames {
1207                    rounded_corners true
1208                    hide_session_name true
1209                }
1210            }
1211        "#;
1212        let config = Config::from_kdl(config_contents, None).unwrap();
1213        let expected_ui_config = UiConfig {
1214            pane_frames: FrameConfig {
1215                rounded_corners: true,
1216                hide_session_name: true,
1217            },
1218        };
1219        assert_eq!(config.ui, expected_ui_config, "Ui config defined in config");
1220    }
1221
1222    #[test]
1223    fn can_define_env_variables_in_config_file() {
1224        let config_contents = r#"
1225            env {
1226                RUST_BACKTRACE 1
1227                SOME_OTHER_VAR "foo"
1228            }
1229        "#;
1230        let config = Config::from_kdl(config_contents, None).unwrap();
1231        let mut expected_env_config = HashMap::new();
1232        expected_env_config.insert("RUST_BACKTRACE".into(), "1".into());
1233        expected_env_config.insert("SOME_OTHER_VAR".into(), "foo".into());
1234        assert_eq!(
1235            config.env,
1236            EnvironmentVariables::from_data(expected_env_config),
1237            "Env variables defined in config"
1238        );
1239    }
1240}