Skip to main content

zellij_utils/
setup.rs

1#[cfg(not(target_family = "wasm"))]
2use crate::consts::ASSET_MAP;
3use crate::input::theme::Themes;
4#[allow(unused_imports)]
5use crate::{
6    cli::{CliArgs, Command, SessionCommand, Sessions},
7    consts::{FEATURES, VERSION, ZELLIJ_CACHE_DIR, ZELLIJ_DEFAULT_THEMES},
8    data::LayoutInfo,
9    errors::prelude::*,
10    home::*,
11    input::{
12        config::{Config, ConfigError},
13        layout::Layout,
14        options::Options,
15    },
16};
17use clap::{Args, IntoApp};
18use clap_complete::Shell;
19use log::info;
20use serde::{Deserialize, Serialize};
21use std::{convert::TryFrom, fmt::Write as FmtWrite, fs, io::Write, path::PathBuf, process};
22
23const CONFIG_NAME: &str = "config.kdl";
24static ARROW_SEPARATOR: &str = "";
25
26#[cfg(not(test))]
27pub fn get_default_themes() -> Themes {
28    let mut themes = Themes::default();
29    for file in ZELLIJ_DEFAULT_THEMES.files() {
30        if let Some(content) = file.contents_utf8() {
31            let sourced_from_external_file = true;
32            match Themes::from_string(&content.to_string(), sourced_from_external_file) {
33                Ok(theme) => themes = themes.merge(theme),
34                Err(_) => {},
35            }
36        }
37    }
38    themes
39}
40
41#[cfg(test)]
42pub fn get_default_themes() -> Themes {
43    Themes::default()
44}
45
46pub fn dump_asset(asset: &[u8]) -> std::io::Result<()> {
47    std::io::stdout().write_all(asset)?;
48    Ok(())
49}
50
51pub const DEFAULT_CONFIG: &[u8] = include_bytes!(concat!(
52    env!("CARGO_MANIFEST_DIR"),
53    "/",
54    "assets/config/default.kdl"
55));
56
57pub const DEFAULT_LAYOUT: &[u8] = include_bytes!(concat!(
58    env!("CARGO_MANIFEST_DIR"),
59    "/",
60    "assets/layouts/default.kdl"
61));
62
63pub const DEFAULT_SWAP_LAYOUT: &[u8] = include_bytes!(concat!(
64    env!("CARGO_MANIFEST_DIR"),
65    "/",
66    "assets/layouts/default.swap.kdl"
67));
68
69pub const STRIDER_LAYOUT: &[u8] = include_bytes!(concat!(
70    env!("CARGO_MANIFEST_DIR"),
71    "/",
72    "assets/layouts/strider.kdl"
73));
74
75pub const STRIDER_SWAP_LAYOUT: &[u8] = include_bytes!(concat!(
76    env!("CARGO_MANIFEST_DIR"),
77    "/",
78    "assets/layouts/strider.swap.kdl"
79));
80
81pub const NO_STATUS_LAYOUT: &[u8] = include_bytes!(concat!(
82    env!("CARGO_MANIFEST_DIR"),
83    "/",
84    "assets/layouts/disable-status-bar.kdl"
85));
86
87pub const COMPACT_BAR_LAYOUT: &[u8] = include_bytes!(concat!(
88    env!("CARGO_MANIFEST_DIR"),
89    "/",
90    "assets/layouts/compact.kdl"
91));
92
93pub const COMPACT_BAR_SWAP_LAYOUT: &[u8] = include_bytes!(concat!(
94    env!("CARGO_MANIFEST_DIR"),
95    "/",
96    "assets/layouts/compact.swap.kdl"
97));
98
99pub const CLASSIC_LAYOUT: &[u8] = include_bytes!(concat!(
100    env!("CARGO_MANIFEST_DIR"),
101    "/",
102    "assets/layouts/classic.kdl"
103));
104
105pub const CLASSIC_SWAP_LAYOUT: &[u8] = include_bytes!(concat!(
106    env!("CARGO_MANIFEST_DIR"),
107    "/",
108    "assets/layouts/classic.swap.kdl"
109));
110
111pub const WELCOME_LAYOUT: &[u8] = include_bytes!(concat!(
112    env!("CARGO_MANIFEST_DIR"),
113    "/",
114    "assets/layouts/welcome.kdl"
115));
116
117pub const FISH_EXTRA_COMPLETION: &[u8] = include_bytes!(concat!(
118    env!("CARGO_MANIFEST_DIR"),
119    "/",
120    "assets/completions/comp.fish"
121));
122
123pub const BASH_EXTRA_COMPLETION: &[u8] = include_bytes!(concat!(
124    env!("CARGO_MANIFEST_DIR"),
125    "/",
126    "assets/completions/comp.bash"
127));
128
129pub const ZSH_EXTRA_COMPLETION: &[u8] = include_bytes!(concat!(
130    env!("CARGO_MANIFEST_DIR"),
131    "/",
132    "assets/completions/comp.zsh"
133));
134
135pub const BASH_AUTO_START_SCRIPT: &[u8] = include_bytes!(concat!(
136    env!("CARGO_MANIFEST_DIR"),
137    "/",
138    "assets/shell/auto-start.bash"
139));
140
141pub const FISH_AUTO_START_SCRIPT: &[u8] = include_bytes!(concat!(
142    env!("CARGO_MANIFEST_DIR"),
143    "/",
144    "assets/shell/auto-start.fish"
145));
146
147pub const ZSH_AUTO_START_SCRIPT: &[u8] = include_bytes!(concat!(
148    env!("CARGO_MANIFEST_DIR"),
149    "/",
150    "assets/shell/auto-start.zsh"
151));
152
153pub fn add_layout_ext(s: &str) -> String {
154    match s {
155        c if s.ends_with(".kdl") => c.to_owned(),
156        _ => {
157            let mut s = s.to_owned();
158            s.push_str(".kdl");
159            s
160        },
161    }
162}
163
164pub fn dump_default_config() -> std::io::Result<()> {
165    dump_asset(DEFAULT_CONFIG)
166}
167
168pub fn dump_specified_layout(layout: &str) -> std::io::Result<()> {
169    match layout {
170        "strider" => dump_asset(STRIDER_LAYOUT),
171        "default" => dump_asset(DEFAULT_LAYOUT),
172        "compact" => dump_asset(COMPACT_BAR_LAYOUT),
173        "disable-status" => dump_asset(NO_STATUS_LAYOUT),
174        "classic" => dump_asset(CLASSIC_LAYOUT),
175        custom => {
176            info!("Dump {custom} layout");
177            let custom = add_layout_ext(custom);
178            let home = default_layout_dir();
179            let path = home.map(|h| h.join(&custom));
180            let layout_exists = path.as_ref().map(|p| p.exists()).unwrap_or_default();
181
182            match (path, layout_exists) {
183                (Some(path), true) => {
184                    let content = fs::read_to_string(path)?;
185                    std::io::stdout().write_all(content.as_bytes())
186                },
187                _ => {
188                    log::error!("No layout named {custom} found");
189                    return Ok(());
190                },
191            }
192        },
193    }
194}
195
196pub fn dump_specified_swap_layout(swap_layout: &str) -> std::io::Result<()> {
197    match swap_layout {
198        "strider" => dump_asset(STRIDER_SWAP_LAYOUT),
199        "default" => dump_asset(DEFAULT_SWAP_LAYOUT),
200        "compact" => dump_asset(COMPACT_BAR_SWAP_LAYOUT),
201        "classic" => dump_asset(CLASSIC_SWAP_LAYOUT),
202        not_found => Err(std::io::Error::new(
203            std::io::ErrorKind::Other,
204            format!("Swap Layout not found for: {}", not_found),
205        )),
206    }
207}
208
209#[cfg(not(target_family = "wasm"))]
210pub fn dump_builtin_plugins(path: &PathBuf) -> Result<()> {
211    for (asset_path, bytes) in ASSET_MAP.iter() {
212        let plugin_path = path.join(asset_path);
213        plugin_path
214            .parent()
215            .with_context(|| {
216                format!(
217                    "failed to acquire parent path of '{}'",
218                    plugin_path.display()
219                )
220            })
221            .and_then(|parent_path| {
222                std::fs::create_dir_all(parent_path).context("failed to create parent path")
223            })
224            .with_context(|| {
225                format!(
226                    "failed to create folder '{}' to dump plugin '{}' to",
227                    path.display(),
228                    plugin_path.display()
229                )
230            })?;
231
232        std::fs::write(plugin_path, bytes)
233            .with_context(|| format!("failed to dump builtin plugin '{}'", asset_path.display()))?;
234    }
235
236    Ok(())
237}
238
239#[cfg(target_family = "wasm")]
240pub fn dump_builtin_plugins(_path: &PathBuf) -> Result<()> {
241    Ok(())
242}
243
244#[derive(Debug, Default, Clone, Args, Serialize, Deserialize)]
245pub struct Setup {
246    /// Dump the default configuration file to stdout
247    #[clap(long, value_parser)]
248    pub dump_config: bool,
249
250    /// Disables loading of configuration file at default location,
251    /// loads the defaults that zellij ships with
252    #[clap(long, value_parser)]
253    pub clean: bool,
254
255    /// Checks the configuration of zellij and displays
256    /// currently used directories
257    #[clap(long, value_parser)]
258    pub check: bool,
259
260    /// Dump specified layout to stdout
261    #[clap(long, value_parser)]
262    pub dump_layout: Option<String>,
263
264    /// Dump the specified swap layout file to stdout
265    #[clap(long, value_parser)]
266    pub dump_swap_layout: Option<String>,
267
268    /// Dump the builtin plugins to DIR or "DATA DIR" if unspecified
269    #[clap(
270        long,
271        value_name = "DIR",
272        value_parser,
273        exclusive = true,
274        min_values = 0,
275        max_values = 1
276    )]
277    pub dump_plugins: Option<Option<PathBuf>>,
278
279    /// Generates completion for the specified shell
280    #[clap(long, value_name = "SHELL", value_parser)]
281    pub generate_completion: Option<String>,
282
283    /// Generates auto-start script for the specified shell
284    #[clap(long, value_name = "SHELL", value_parser)]
285    pub generate_auto_start: Option<String>,
286}
287
288impl Setup {
289    /// Entrypoint from main
290    /// Merges options from the config file and the command line options
291    /// into `[Options]`, the command line options superceeding the layout
292    /// file options, superceeding the config file options:
293    /// 1. command line options (`zellij options`)
294    /// 2. layout options
295    ///    (`layout.kdl` / `zellij --layout`)
296    /// 3. config options (`config.kdl`)
297    pub fn from_cli_args(
298        cli_args: &CliArgs,
299    ) -> Result<(Config, Option<LayoutInfo>, Options, Config, Options), ConfigError> {
300        // note that this can potentially exit the process
301        Setup::handle_setup_commands(cli_args);
302        let config = Config::try_from(cli_args)?;
303        let cli_config_options: Option<Options> =
304            if let Some(Command::Options(options)) = cli_args.command.clone() {
305                Some(options.into())
306            } else {
307                None
308            };
309
310        // the attach CLI command can also have its own Options, we need to merge them if they
311        // exist
312        let cli_config_options = merge_attach_command_options(cli_config_options, &cli_args);
313
314        let mut config_without_layout = config.clone();
315        let (layout_info, mut config) =
316            Setup::parse_layout_and_override_config(cli_config_options.as_ref(), config, cli_args)?;
317
318        let config_options =
319            apply_themes_to_config(&mut config, cli_config_options.clone(), cli_args)?;
320        let config_options_without_layout =
321            apply_themes_to_config(&mut config_without_layout, cli_config_options, cli_args)?;
322        fn apply_themes_to_config(
323            config: &mut Config,
324            cli_config_options: Option<Options>,
325            cli_args: &CliArgs,
326        ) -> Result<Options, ConfigError> {
327            let config_options = match cli_config_options {
328                Some(cli_config_options) => config.options.merge(cli_config_options),
329                None => config.options.clone(),
330            };
331
332            config.themes = config.themes.merge(get_default_themes());
333
334            let user_theme_dir = config_options.theme_dir.clone().or_else(|| {
335                get_theme_dir(cli_args.config_dir.clone().or_else(find_default_config_dir))
336                    .filter(|dir| dir.exists())
337            });
338            if let Some(user_theme_dir) = user_theme_dir {
339                config.themes = config.themes.merge(Themes::from_dir(user_theme_dir)?);
340            }
341            Ok(config_options)
342        }
343
344        if let Some(Command::Setup(ref setup)) = &cli_args.command {
345            setup
346                .from_cli_with_options(cli_args, &config_options)
347                .map_or_else(
348                    |e| {
349                        eprintln!("{:?}", e);
350                        process::exit(1);
351                    },
352                    |_| {},
353                );
354        };
355        Ok((
356            config,
357            layout_info,
358            config_options,
359            config_without_layout,
360            config_options_without_layout,
361        ))
362    }
363
364    /// General setup helpers
365    pub fn from_cli(&self) -> Result<()> {
366        if self.clean {
367            return Ok(());
368        }
369
370        if self.dump_config {
371            dump_default_config()?;
372            std::process::exit(0);
373        }
374
375        if let Some(shell) = &self.generate_completion {
376            Self::generate_completion(shell);
377            std::process::exit(0);
378        }
379
380        if let Some(shell) = &self.generate_auto_start {
381            Self::generate_auto_start(shell);
382            std::process::exit(0);
383        }
384
385        if let Some(layout) = &self.dump_layout {
386            dump_specified_layout(&layout)?;
387            std::process::exit(0);
388        }
389
390        if let Some(swap_layout) = &self.dump_swap_layout {
391            dump_specified_swap_layout(swap_layout)?;
392            std::process::exit(0);
393        }
394
395        Ok(())
396    }
397
398    /// Checks the merged configuration
399    pub fn from_cli_with_options(&self, opts: &CliArgs, config_options: &Options) -> Result<()> {
400        if self.check {
401            Setup::check_defaults_config(opts, config_options)?;
402            std::process::exit(0);
403        }
404
405        if let Some(maybe_path) = &self.dump_plugins {
406            let data_dir = &opts.data_dir.clone().unwrap_or_else(get_default_data_dir);
407            let dir = match maybe_path {
408                Some(path) => path,
409                None => data_dir,
410            };
411
412            println!("Dumping plugins to '{}'", dir.display());
413            dump_builtin_plugins(&dir)?;
414            std::process::exit(0);
415        }
416
417        Ok(())
418    }
419
420    pub fn check_defaults_config(opts: &CliArgs, config_options: &Options) -> std::io::Result<()> {
421        let data_dir = opts.data_dir.clone().unwrap_or_else(get_default_data_dir);
422        let config_dir = opts.config_dir.clone().or_else(find_default_config_dir);
423        let plugin_dir = data_dir.join("plugins");
424        let layout_dir = config_options
425            .layout_dir
426            .clone()
427            .or_else(|| get_layout_dir(config_dir.clone()));
428        let system_data_dir = system_data_dir();
429        let config_file = opts
430            .config
431            .clone()
432            .or_else(|| config_dir.clone().map(|p| p.join(CONFIG_NAME)));
433
434        // according to
435        // https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
436        let hyperlink_start = "\u{1b}]8;;";
437        let hyperlink_mid = "\u{1b}\\";
438        let hyperlink_end = "\u{1b}]8;;\u{1b}\\";
439
440        let mut message = String::new();
441
442        writeln!(&mut message, "[Version]: {:?}", VERSION).unwrap();
443        if let Some(config_dir) = config_dir {
444            writeln!(&mut message, "[CONFIG DIR]: \"{}\"", config_dir.display()).unwrap();
445        } else {
446            message.push_str("[CONFIG DIR]: Not Found\n");
447            let mut default_config_dirs = default_config_dirs()
448                .iter()
449                .filter_map(|p| p.clone())
450                .collect::<Vec<PathBuf>>();
451            default_config_dirs.dedup();
452            message.push_str(
453                " On your system zellij looks in the following config directories by default:\n",
454            );
455            for dir in default_config_dirs {
456                writeln!(&mut message, " \"{}\"", dir.display()).unwrap();
457            }
458        }
459        if let Some(config_file) = config_file {
460            writeln!(
461                &mut message,
462                "[LOOKING FOR CONFIG FILE FROM]: \"{}\"",
463                config_file.display()
464            )
465            .unwrap();
466            match Config::from_path(&config_file, None) {
467                Ok(_) => message.push_str("[CONFIG FILE]: Well defined.\n"),
468                Err(e) => writeln!(
469                    &mut message,
470                    "[CONFIG ERROR]: {}. \n By default, zellij loads default configuration",
471                    e
472                )
473                .unwrap(),
474            }
475        } else {
476            message.push_str("[CONFIG FILE]: Not Found\n");
477            writeln!(
478                &mut message,
479                " By default zellij looks for a file called [{}] in the configuration directory",
480                CONFIG_NAME
481            )
482            .unwrap();
483        }
484        writeln!(&mut message, "[CACHE DIR]: {}", ZELLIJ_CACHE_DIR.display()).unwrap();
485        writeln!(&mut message, "[DATA DIR]: \"{}\"", data_dir.display()).unwrap();
486        writeln!(&mut message, "[PLUGIN DIR]: \"{}\"", plugin_dir.display()).unwrap();
487        if !cfg!(feature = "disable_automatic_asset_installation") {
488            writeln!(
489                &mut message,
490                " Builtin, default plugins will not be loaded from disk."
491            )
492            .unwrap();
493            writeln!(
494                &mut message,
495                " Create a custom layout if you require this behavior."
496            )
497            .unwrap();
498        }
499        if let Some(layout_dir) = layout_dir {
500            writeln!(&mut message, "[LAYOUT DIR]: \"{}\"", layout_dir.display()).unwrap();
501        } else {
502            message.push_str("[LAYOUT DIR]: Not Found\n");
503        }
504        writeln!(
505            &mut message,
506            "[SYSTEM DATA DIR]: \"{}\"",
507            system_data_dir.display()
508        )
509        .unwrap();
510
511        writeln!(&mut message, "[ARROW SEPARATOR]: {}", ARROW_SEPARATOR).unwrap();
512        message.push_str(" Is the [ARROW_SEPARATOR] displayed correctly?\n");
513        message.push_str(" If not you may want to either start zellij with a compatible mode: 'zellij options --simplified-ui true'\n");
514        let mut hyperlink_compat = String::new();
515        hyperlink_compat.push_str(hyperlink_start);
516        hyperlink_compat.push_str("https://zellij.dev/documentation/compatibility.html#the-status-bar-fonts-dont-render-correctly");
517        hyperlink_compat.push_str(hyperlink_mid);
518        hyperlink_compat.push_str("https://zellij.dev/documentation/compatibility.html#the-status-bar-fonts-dont-render-correctly");
519        hyperlink_compat.push_str(hyperlink_end);
520        write!(
521            &mut message,
522            " Or check the font that is in use:\n {}\n",
523            hyperlink_compat
524        )
525        .unwrap();
526        message.push_str("[MOUSE INTERACTION]: \n");
527        message.push_str(" Can be temporarily disabled through pressing the [SHIFT] key.\n");
528        message.push_str(" If that doesn't fix any issues consider to disable the mouse handling of zellij: 'zellij options --disable-mouse-mode'\n");
529
530        let default_editor = std::env::var("EDITOR")
531            .or_else(|_| std::env::var("VISUAL"))
532            .unwrap_or_else(|_| String::from("Not set, checked $EDITOR and $VISUAL"));
533        writeln!(&mut message, "[DEFAULT EDITOR]: {}", default_editor).unwrap();
534        writeln!(&mut message, "[FEATURES]: {:?}", FEATURES).unwrap();
535        let mut hyperlink = String::new();
536        hyperlink.push_str(hyperlink_start);
537        hyperlink.push_str("https://www.zellij.dev/documentation/");
538        hyperlink.push_str(hyperlink_mid);
539        hyperlink.push_str("zellij.dev/documentation");
540        hyperlink.push_str(hyperlink_end);
541        writeln!(&mut message, "[DOCUMENTATION]: {}", hyperlink).unwrap();
542        //printf '\e]8;;http://example.com\e\\This is a link\e]8;;\e\\\n'
543
544        std::io::stdout().write_all(message.as_bytes())?;
545
546        Ok(())
547    }
548    fn generate_completion(shell: &str) {
549        let shell: Shell = match shell.to_lowercase().parse() {
550            Ok(shell) => shell,
551            _ => {
552                eprintln!("Unsupported shell: {}", shell);
553                std::process::exit(1);
554            },
555        };
556        let mut out = std::io::stdout();
557        clap_complete::generate(shell, &mut CliArgs::command(), "zellij", &mut out);
558        // add shell dependent extra completion
559        match shell {
560            Shell::Bash => {
561                let _ = out.write_all(BASH_EXTRA_COMPLETION);
562            },
563            Shell::Elvish => {},
564            Shell::Fish => {
565                let _ = out.write_all(FISH_EXTRA_COMPLETION);
566            },
567            Shell::PowerShell => {},
568            Shell::Zsh => {
569                let _ = out.write_all(ZSH_EXTRA_COMPLETION);
570            },
571            _ => {},
572        };
573    }
574
575    fn generate_auto_start(shell: &str) {
576        let shell: Shell = match shell.to_lowercase().parse() {
577            Ok(shell) => shell,
578            _ => {
579                eprintln!("Unsupported shell: {}", shell);
580                std::process::exit(1);
581            },
582        };
583
584        let mut out = std::io::stdout();
585        match shell {
586            Shell::Bash => {
587                let _ = out.write_all(BASH_AUTO_START_SCRIPT);
588            },
589            Shell::Fish => {
590                let _ = out.write_all(FISH_AUTO_START_SCRIPT);
591            },
592            Shell::Zsh => {
593                let _ = out.write_all(ZSH_AUTO_START_SCRIPT);
594            },
595            _ => {},
596        }
597    }
598    fn parse_layout_and_override_config(
599        cli_config_options: Option<&Options>,
600        config: Config,
601        cli_args: &CliArgs,
602    ) -> Result<(Option<LayoutInfo>, Config), ConfigError> {
603        // find the layout folder relative to which we'll look for our layout
604        let layout_dir = cli_config_options
605            .as_ref()
606            .and_then(|cli_options| cli_options.layout_dir.clone())
607            .or_else(|| config.options.layout_dir.clone())
608            .or_else(|| get_layout_dir(cli_args.config_dir.clone()))
609            .or_else(|| get_layout_dir(find_default_config_dir()))
610            // Try to get an absolute path, else let the resolution code figure this out.
611            .map(|d| d.canonicalize().unwrap_or(d));
612        // the chosen layout can either be a path relative to the layout_dir or a name of one
613        // of our assets, this distinction is made when parsing the layout - TODO: ideally, this
614        // logic should not be split up and all the decisions should happen here
615        let (layout_info, chosen_layout) = if let Some(ref layout_string) = cli_args.layout_string {
616            (Some(LayoutInfo::Stringified(layout_string.clone())), None)
617        } else if let Some(chosen_layout) = cli_args.layout.clone() {
618            let layout_info = LayoutInfo::from_cli(
619                &layout_dir,
620                &Some(chosen_layout.clone()),
621                std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
622            );
623            (layout_info, Some(chosen_layout))
624        } else {
625            let chosen_layout = cli_config_options
626                .as_ref()
627                .and_then(|cli_options| cli_options.default_layout.clone())
628                .or_else(|| config.options.default_layout.clone());
629            let layout_info = LayoutInfo::from_config(&layout_dir, &chosen_layout);
630            (layout_info, chosen_layout)
631        };
632        match layout_info {
633            Some(LayoutInfo::Url(ref layout_url)) => {
634                Layout::from_url(layout_url, config).map(|(_layout, config)| (layout_info, config))
635            },
636            Some(LayoutInfo::Stringified(ref raw_layout)) => {
637                Layout::from_stringified_layout(raw_layout, config)
638                    .map(|(_layout, config)| (layout_info, config))
639            },
640            _ => Layout::from_path_or_default(chosen_layout.as_ref(), layout_dir.clone(), config)
641                .map(|(_layout, config)| (layout_info, config)),
642        }
643    }
644    fn handle_setup_commands(cli_args: &CliArgs) {
645        if let Some(Command::Setup(ref setup)) = &cli_args.command {
646            setup.from_cli().map_or_else(
647                |e| {
648                    eprintln!("{:?}", e);
649                    process::exit(1);
650                },
651                |_| {},
652            );
653        };
654    }
655}
656
657fn merge_attach_command_options(
658    cli_config_options: Option<Options>,
659    cli_args: &CliArgs,
660) -> Option<Options> {
661    let cli_config_options = if let Some(Command::Sessions(Sessions::Attach { options, .. })) =
662        cli_args.command.clone()
663    {
664        match options.clone().as_deref() {
665            Some(SessionCommand::Options(options)) => match cli_config_options {
666                Some(cli_config_options) => {
667                    Some(cli_config_options.merge_from_cli(options.to_owned().into()))
668                },
669                None => Some(options.to_owned().into()),
670            },
671            _ => cli_config_options,
672        }
673    } else {
674        cli_config_options
675    };
676    cli_config_options
677}
678
679#[cfg(test)]
680mod setup_test {
681    use super::Setup;
682    use crate::cli::{CliArgs, Command};
683    use crate::data::LayoutInfo;
684    use crate::input::options::Options;
685    use insta::assert_snapshot;
686    use std::path::PathBuf;
687
688    #[test]
689    fn default_config_with_no_cli_arguments() {
690        let cli_args = CliArgs::default();
691        let (config, layout_info, options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
692        assert_snapshot!(format!("{:#?}", config));
693        assert_snapshot!(format!("{:#?}", layout_info));
694        assert_snapshot!(format!("{:#?}", options));
695    }
696    #[test]
697    fn cli_arguments_override_config_options() {
698        let mut cli_args = CliArgs::default();
699        cli_args.command = Some(Command::Options(Options {
700            simplified_ui: Some(true),
701            ..Default::default()
702        }));
703        let (_config, _layout_info, options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
704        assert_snapshot!(format!("{:#?}", options));
705    }
706    #[test]
707    fn layout_options_override_config_options() {
708        let mut cli_args = CliArgs::default();
709        cli_args.layout = Some(PathBuf::from(format!(
710            "{}/src/test-fixtures/layout-with-options.kdl",
711            env!("CARGO_MANIFEST_DIR")
712        )));
713        let (_config, layout_info, options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
714        assert_snapshot!(format!("{:#?}", options));
715        let Some(LayoutInfo::File(layout_path, _)) = layout_info else {
716            panic!("layout info doesn't have expected format");
717        };
718        assert_eq!(
719            layout_path,
720            format!(
721                "{}/src/test-fixtures/layout-with-options.kdl",
722                env!("CARGO_MANIFEST_DIR")
723            )
724        );
725    }
726    #[test]
727    fn cli_arguments_override_layout_options() {
728        let mut cli_args = CliArgs::default();
729        cli_args.layout = Some(PathBuf::from(format!(
730            "{}/src/test-fixtures/layout-with-options.kdl",
731            env!("CARGO_MANIFEST_DIR")
732        )));
733        cli_args.command = Some(Command::Options(Options {
734            pane_frames: Some(true),
735            ..Default::default()
736        }));
737        let (_config, layout_info, options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
738        assert_snapshot!(format!("{:#?}", options));
739        let Some(LayoutInfo::File(layout_path, _)) = layout_info else {
740            panic!("layout info doesn't have expected format");
741        };
742        assert_eq!(
743            layout_path,
744            format!(
745                "{}/src/test-fixtures/layout-with-options.kdl",
746                env!("CARGO_MANIFEST_DIR")
747            )
748        );
749    }
750    #[test]
751    fn layout_env_vars_override_config_env_vars() {
752        let mut cli_args = CliArgs::default();
753        cli_args.config = Some(PathBuf::from(format!(
754            "{}/src/test-fixtures/config-with-env-vars.kdl",
755            env!("CARGO_MANIFEST_DIR")
756        )));
757        cli_args.layout = Some(PathBuf::from(format!(
758            "{}/src/test-fixtures/layout-with-env-vars.kdl",
759            env!("CARGO_MANIFEST_DIR")
760        )));
761        let (config, _layout_info, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
762        assert_snapshot!(format!("{:#?}", config));
763    }
764    #[test]
765    fn layout_ui_config_overrides_config_ui_config() {
766        let mut cli_args = CliArgs::default();
767        cli_args.config = Some(PathBuf::from(format!(
768            "{}/src/test-fixtures/config-with-ui-config.kdl",
769            env!("CARGO_MANIFEST_DIR")
770        )));
771        cli_args.layout = Some(PathBuf::from(format!(
772            "{}/src/test-fixtures/layout-with-ui-config.kdl",
773            env!("CARGO_MANIFEST_DIR")
774        )));
775        let (config, _layout_info, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
776        assert_snapshot!(format!("{:#?}", config));
777    }
778    #[test]
779    fn layout_themes_override_config_themes() {
780        let mut cli_args = CliArgs::default();
781        cli_args.config = Some(PathBuf::from(format!(
782            "{}/src/test-fixtures/config-with-themes-config.kdl",
783            env!("CARGO_MANIFEST_DIR")
784        )));
785        cli_args.layout = Some(PathBuf::from(format!(
786            "{}/src/test-fixtures/layout-with-themes-config.kdl",
787            env!("CARGO_MANIFEST_DIR")
788        )));
789        let (config, _layout_info, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
790        assert_snapshot!(format!("{:#?}", config));
791    }
792    #[test]
793    fn layout_keybinds_override_config_keybinds() {
794        let mut cli_args = CliArgs::default();
795        cli_args.config = Some(PathBuf::from(format!(
796            "{}/src/test-fixtures/config-with-keybindings-config.kdl",
797            env!("CARGO_MANIFEST_DIR")
798        )));
799        cli_args.layout = Some(PathBuf::from(format!(
800            "{}/src/test-fixtures/layout-with-keybindings-config.kdl",
801            env!("CARGO_MANIFEST_DIR")
802        )));
803        let (config, _layout_info, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
804        assert_snapshot!(format!("{:#?}", config));
805    }
806    #[test]
807    fn cli_config_dir_overrides_defaults() {
808        let config_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
809            .join("src")
810            .join("test-fixtures")
811            .join("config-dirs")
812            .join("layout-upside-down");
813        let cli_args = CliArgs {
814            config_dir: Some(config_dir.clone()),
815            ..Default::default()
816        };
817        let (_, layout_info, _, _, _) = Setup::from_cli_args(&cli_args).unwrap();
818        let Some(LayoutInfo::File(layout_path, _)) = layout_info else {
819            panic!("layout info has unexpected format");
820        };
821        let expected = config_dir
822            .join("layouts")
823            .join("upside-down.kdl")
824            .canonicalize()
825            .unwrap();
826        assert_eq!(layout_path, expected.display().to_string());
827    }
828    #[test]
829    fn cli_config_dir_finds_custom_default() {
830        let config_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
831            .join("src")
832            .join("test-fixtures")
833            .join("config-dirs")
834            .join("custom-default-layout");
835        let cli_args = CliArgs {
836            config_dir: Some(config_dir.clone()),
837            ..Default::default()
838        };
839        let (_, layout_info, _, _, _) = Setup::from_cli_args(&cli_args).unwrap();
840        let Some(LayoutInfo::File(layout_path, _)) = layout_info else {
841            panic!("layout info has unexpected format");
842        };
843        let expected = config_dir
844            .join("layouts")
845            .join("default.kdl")
846            .canonicalize()
847            .unwrap();
848        assert_eq!(layout_path, expected.display().to_string());
849    }
850
851    #[test]
852    fn cli_with_relative_layout_and_extension() {
853        // NOTE: We assume to be in `zellij-utils` root directory. If this doesn't hold, path
854        // resolution cannot work (as it actually reads path to ensure they exist).
855        let cwd = std::env::current_dir().unwrap();
856        assert_eq!(cwd, PathBuf::from(env!("CARGO_MANIFEST_DIR")));
857
858        let cli_args = CliArgs {
859            layout: Some(PathBuf::from("assets/layouts/compact.kdl")),
860            ..Default::default()
861        };
862        let (_, layout_info, _, _, _) = Setup::from_cli_args(&cli_args).unwrap();
863        let Some(LayoutInfo::File(layout_path, _)) = layout_info else {
864            panic!("layout info has unexpected format: {:?}", &layout_info);
865        };
866        let expected = cwd.join("assets/layouts/compact.kdl");
867        assert_eq!(layout_path, expected.display().to_string());
868    }
869
870    #[test]
871    fn cli_with_relative_layout_and_separator() {
872        // NOTE: We assume to be in `zellij-utils` root directory. If this doesn't hold, path
873        // resolution cannot work (as it actually reads path to ensure they exist).
874        let cwd = std::env::current_dir().unwrap();
875        assert_eq!(cwd, PathBuf::from(env!("CARGO_MANIFEST_DIR")));
876
877        let cli_args = CliArgs {
878            layout: Some(PathBuf::from("assets/layouts/compact")),
879            ..Default::default()
880        };
881        let (_, layout_info, _, _, _) = Setup::from_cli_args(&cli_args).unwrap();
882        let Some(LayoutInfo::File(layout_path, _)) = layout_info else {
883            panic!("layout info has unexpected format");
884        };
885        let expected = cwd.join("assets/layouts/compact");
886        assert_eq!(layout_path, expected.display().to_string());
887    }
888
889    #[test]
890    fn layout_string_cli_argument() {
891        let layout_kdl = "layout {\n    pane\n    pane\n}\n".to_string();
892        let cli_args = CliArgs {
893            layout_string: Some(layout_kdl.clone()),
894            ..Default::default()
895        };
896        let (_, layout_info, _, _, _) = Setup::from_cli_args(&cli_args).unwrap();
897        let Some(LayoutInfo::Stringified(content)) = layout_info else {
898            panic!(
899                "layout info should be Stringified variant, got: {:#?}",
900                layout_info
901            );
902        };
903        assert_eq!(content, layout_kdl);
904    }
905}