Skip to main content

zellij_utils/input/
config.rs

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