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