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, CommandFactory};
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        num_args(0..=1)
275    )]
276    pub dump_plugins: Option<Option<PathBuf>>,
277
278    /// Generates completion for the specified shell
279    #[clap(long, value_name = "SHELL", value_parser)]
280    pub generate_completion: Option<String>,
281
282    /// Generates auto-start script for the specified shell
283    #[clap(long, value_name = "SHELL", value_parser)]
284    pub generate_auto_start: Option<String>,
285}
286
287impl Setup {
288    /// Entrypoint from main
289    /// Merges options from the config file and the command line options
290    /// into `[Options]`, the command line options superseding the layout
291    /// file options, superseding the config file options:
292    /// 1. command line options (`zellij options`)
293    /// 2. layout options
294    ///    (`layout.kdl` / `zellij --layout`)
295    /// 3. config options (`config.kdl`)
296    pub fn from_cli_args(
297        cli_args: &CliArgs,
298    ) -> Result<(Config, Option<LayoutInfo>, Options, Config, Options), ConfigError> {
299        // note that this can potentially exit the process
300        Setup::handle_setup_commands(cli_args);
301        let config = Config::try_from(cli_args)?;
302        let cli_config_options: Option<Options> =
303            if let Some(Command::Options(options)) = cli_args.command.clone() {
304                Some(options.into())
305            } else {
306                None
307            };
308
309        // the attach CLI command can also have its own Options, we need to merge them if they
310        // exist
311        let cli_config_options = merge_attach_command_options(cli_config_options, &cli_args);
312
313        let mut config_without_layout = config.clone();
314        let (layout_info, mut config) =
315            Setup::parse_layout_and_override_config(cli_config_options.as_ref(), config, cli_args)?;
316
317        let config_options =
318            apply_themes_to_config(&mut config, cli_config_options.clone(), cli_args)?;
319        let config_options_without_layout =
320            apply_themes_to_config(&mut config_without_layout, cli_config_options, cli_args)?;
321        fn apply_themes_to_config(
322            config: &mut Config,
323            cli_config_options: Option<Options>,
324            cli_args: &CliArgs,
325        ) -> Result<Options, ConfigError> {
326            let config_options = match cli_config_options {
327                Some(cli_config_options) => config.options.merge(cli_config_options),
328                None => config.options.clone(),
329            };
330
331            config.themes = config.themes.merge(get_default_themes());
332
333            let user_theme_dir = config_options.theme_dir.clone().or_else(|| {
334                get_theme_dir(cli_args.config_dir.clone().or_else(find_default_config_dir))
335                    .filter(|dir| dir.exists())
336            });
337            if let Some(user_theme_dir) = user_theme_dir {
338                config.themes = config.themes.merge(Themes::from_dir(user_theme_dir)?);
339            }
340            Ok(config_options)
341        }
342
343        if let Some(Command::Setup(ref setup)) = &cli_args.command {
344            setup
345                .from_cli_with_options(cli_args, &config_options)
346                .map_or_else(
347                    |e| {
348                        eprintln!("{:?}", e);
349                        process::exit(1);
350                    },
351                    |_| {},
352                );
353        };
354        Ok((
355            config,
356            layout_info,
357            config_options,
358            config_without_layout,
359            config_options_without_layout,
360        ))
361    }
362
363    /// General setup helpers
364    pub fn from_cli(&self) -> Result<()> {
365        if self.clean {
366            return Ok(());
367        }
368
369        if self.dump_config {
370            dump_default_config()?;
371            std::process::exit(0);
372        }
373
374        if let Some(shell) = &self.generate_completion {
375            Self::generate_completion(shell);
376            std::process::exit(0);
377        }
378
379        if let Some(shell) = &self.generate_auto_start {
380            Self::generate_auto_start(shell);
381            std::process::exit(0);
382        }
383
384        if let Some(layout) = &self.dump_layout {
385            dump_specified_layout(&layout)?;
386            std::process::exit(0);
387        }
388
389        if let Some(swap_layout) = &self.dump_swap_layout {
390            dump_specified_swap_layout(swap_layout)?;
391            std::process::exit(0);
392        }
393
394        Ok(())
395    }
396
397    /// Checks the merged configuration
398    pub fn from_cli_with_options(&self, opts: &CliArgs, config_options: &Options) -> Result<()> {
399        if self.check {
400            Setup::check_defaults_config(opts, config_options)?;
401            std::process::exit(0);
402        }
403
404        if let Some(maybe_path) = &self.dump_plugins {
405            if cfg!(feature = "disable_automatic_asset_installation") {
406                return Err(anyhow!(
407                    "This zellij was built without bundled plugins (feature \
408                     'disable_automatic_asset_installation'). Builtin plugins are provided by the \
409                     distributor of this build and must be placed in the plugin directory, \
410                     visible in the output of `zellij setup --check`."
411                ))
412                .context("failed to dump plugins");
413            }
414            let data_dir = &opts.data_dir.clone().unwrap_or_else(get_default_data_dir);
415            let dir = match maybe_path {
416                Some(path) => path,
417                None => data_dir,
418            };
419
420            println!("Dumping plugins to '{}'", dir.display());
421            dump_builtin_plugins(&dir)?;
422            std::process::exit(0);
423        }
424
425        Ok(())
426    }
427
428    pub fn check_defaults_config(opts: &CliArgs, config_options: &Options) -> std::io::Result<()> {
429        let data_dir = opts.data_dir.clone().unwrap_or_else(get_default_data_dir);
430        let config_dir = opts.config_dir.clone().or_else(find_default_config_dir);
431        let plugin_dir = data_dir.join("plugins");
432        let layout_dir = config_options
433            .layout_dir
434            .clone()
435            .or_else(|| get_layout_dir(config_dir.clone()));
436        let system_data_dir = system_data_dir();
437        let config_file = opts
438            .config
439            .clone()
440            .or_else(|| config_dir.clone().map(|p| p.join(CONFIG_NAME)));
441
442        // according to
443        // https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
444        let hyperlink_start = "\u{1b}]8;;";
445        let hyperlink_mid = "\u{1b}\\";
446        let hyperlink_end = "\u{1b}]8;;\u{1b}\\";
447
448        let mut message = String::new();
449
450        writeln!(&mut message, "[Version]: {:?}", VERSION).unwrap();
451        if let Some(config_dir) = config_dir {
452            writeln!(&mut message, "[CONFIG DIR]: \"{}\"", config_dir.display()).unwrap();
453        } else {
454            message.push_str("[CONFIG DIR]: Not Found\n");
455            let mut default_config_dirs = default_config_dirs()
456                .iter()
457                .filter_map(|p| p.clone())
458                .collect::<Vec<PathBuf>>();
459            default_config_dirs.dedup();
460            message.push_str(
461                " On your system zellij looks in the following config directories by default:\n",
462            );
463            for dir in default_config_dirs {
464                writeln!(&mut message, " \"{}\"", dir.display()).unwrap();
465            }
466        }
467        if let Some(config_file) = config_file {
468            writeln!(
469                &mut message,
470                "[LOOKING FOR CONFIG FILE FROM]: \"{}\"",
471                config_file.display()
472            )
473            .unwrap();
474            match Config::from_path(&config_file, None) {
475                Ok(_) => message.push_str("[CONFIG FILE]: Well defined.\n"),
476                Err(e) => writeln!(
477                    &mut message,
478                    "[CONFIG ERROR]: {}. \n By default, zellij loads default configuration",
479                    e
480                )
481                .unwrap(),
482            }
483        } else {
484            message.push_str("[CONFIG FILE]: Not Found\n");
485            writeln!(
486                &mut message,
487                " By default zellij looks for a file called [{}] in the configuration directory",
488                CONFIG_NAME
489            )
490            .unwrap();
491        }
492        writeln!(&mut message, "[CACHE DIR]: {}", ZELLIJ_CACHE_DIR.display()).unwrap();
493        writeln!(&mut message, "[DATA DIR]: \"{}\"", data_dir.display()).unwrap();
494        writeln!(&mut message, "[PLUGIN DIR]: \"{}\"", plugin_dir.display()).unwrap();
495        if !cfg!(feature = "disable_automatic_asset_installation") {
496            writeln!(
497                &mut message,
498                " Builtin, default plugins will not be loaded from disk."
499            )
500            .unwrap();
501            writeln!(
502                &mut message,
503                " Create a custom layout if you require this behavior."
504            )
505            .unwrap();
506        } else {
507            writeln!(
508                &mut message,
509                " This zellij was built without bundled plugins."
510            )
511            .unwrap();
512            writeln!(
513                &mut message,
514                " Builtin plugins are loaded from the 'PLUGIN DIR' above, or from '{}'.",
515                system_data_dir.join("plugins").display()
516            )
517            .unwrap();
518        }
519        if let Some(layout_dir) = layout_dir {
520            writeln!(&mut message, "[LAYOUT DIR]: \"{}\"", layout_dir.display()).unwrap();
521        } else {
522            message.push_str("[LAYOUT DIR]: Not Found\n");
523        }
524        writeln!(
525            &mut message,
526            "[SYSTEM DATA DIR]: \"{}\"",
527            system_data_dir.display()
528        )
529        .unwrap();
530
531        writeln!(&mut message, "[ARROW SEPARATOR]: {}", ARROW_SEPARATOR).unwrap();
532        message.push_str(" Is the [ARROW_SEPARATOR] displayed correctly?\n");
533        message.push_str(" If not you may want to either start zellij with a compatible mode: 'zellij options --simplified-ui true'\n");
534        let mut hyperlink_compat = String::new();
535        hyperlink_compat.push_str(hyperlink_start);
536        hyperlink_compat.push_str("https://zellij.dev/documentation/compatibility.html#the-status-bar-fonts-dont-render-correctly");
537        hyperlink_compat.push_str(hyperlink_mid);
538        hyperlink_compat.push_str("https://zellij.dev/documentation/compatibility.html#the-status-bar-fonts-dont-render-correctly");
539        hyperlink_compat.push_str(hyperlink_end);
540        write!(
541            &mut message,
542            " Or check the font that is in use:\n {}\n",
543            hyperlink_compat
544        )
545        .unwrap();
546        message.push_str("[MOUSE INTERACTION]: \n");
547        message.push_str(" Can be temporarily disabled through pressing the [SHIFT] key.\n");
548        message.push_str(" If that doesn't fix any issues consider to disable the mouse handling of zellij: 'zellij options --disable-mouse-mode'\n");
549
550        let default_editor = std::env::var("EDITOR")
551            .or_else(|_| std::env::var("VISUAL"))
552            .unwrap_or_else(|_| String::from("Not set, checked $EDITOR and $VISUAL"));
553        writeln!(&mut message, "[DEFAULT EDITOR]: {}", default_editor).unwrap();
554        writeln!(&mut message, "[FEATURES]: {:?}", FEATURES).unwrap();
555        let mut hyperlink = String::new();
556        hyperlink.push_str(hyperlink_start);
557        hyperlink.push_str("https://www.zellij.dev/documentation/");
558        hyperlink.push_str(hyperlink_mid);
559        hyperlink.push_str("zellij.dev/documentation");
560        hyperlink.push_str(hyperlink_end);
561        writeln!(&mut message, "[DOCUMENTATION]: {}", hyperlink).unwrap();
562        //printf '\e]8;;http://example.com\e\\This is a link\e]8;;\e\\\n'
563
564        std::io::stdout().write_all(message.as_bytes())?;
565
566        Ok(())
567    }
568    fn generate_completion(shell: &str) {
569        let shell: Shell = match shell.to_lowercase().parse() {
570            Ok(shell) => shell,
571            _ => {
572                eprintln!("Unsupported shell: {}", shell);
573                std::process::exit(1);
574            },
575        };
576        let mut out = std::io::stdout();
577        clap_complete::generate(shell, &mut CliArgs::command(), "zellij", &mut out);
578        // add shell dependent extra completion
579        match shell {
580            Shell::Bash => {
581                let _ = out.write_all(BASH_EXTRA_COMPLETION);
582            },
583            Shell::Elvish => {},
584            Shell::Fish => {
585                let _ = out.write_all(FISH_EXTRA_COMPLETION);
586            },
587            Shell::PowerShell => {},
588            Shell::Zsh => {
589                let _ = out.write_all(ZSH_EXTRA_COMPLETION);
590            },
591            _ => {},
592        };
593    }
594
595    fn generate_auto_start(shell: &str) {
596        let shell: Shell = match shell.to_lowercase().parse() {
597            Ok(shell) => shell,
598            _ => {
599                eprintln!("Unsupported shell: {}", shell);
600                std::process::exit(1);
601            },
602        };
603
604        let mut out = std::io::stdout();
605        match shell {
606            Shell::Bash => {
607                let _ = out.write_all(BASH_AUTO_START_SCRIPT);
608            },
609            Shell::Fish => {
610                let _ = out.write_all(FISH_AUTO_START_SCRIPT);
611            },
612            Shell::Zsh => {
613                let _ = out.write_all(ZSH_AUTO_START_SCRIPT);
614            },
615            _ => {},
616        }
617    }
618    fn parse_layout_and_override_config(
619        cli_config_options: Option<&Options>,
620        config: Config,
621        cli_args: &CliArgs,
622    ) -> Result<(Option<LayoutInfo>, Config), ConfigError> {
623        // find the layout folder relative to which we'll look for our layout
624        let layout_dir = cli_config_options
625            .as_ref()
626            .and_then(|cli_options| cli_options.layout_dir.clone())
627            .or_else(|| config.options.layout_dir.clone())
628            .or_else(|| get_layout_dir(cli_args.config_dir.clone()))
629            .or_else(|| get_layout_dir(find_default_config_dir()))
630            // Try to get an absolute path, else let the resolution code figure this out.
631            .map(|d| d.canonicalize().unwrap_or(d));
632        // the chosen layout can either be a path relative to the layout_dir or a name of one
633        // of our assets, this distinction is made when parsing the layout - TODO: ideally, this
634        // logic should not be split up and all the decisions should happen here
635        let (layout_info, chosen_layout) = if let Some(ref layout_string) = cli_args.layout_string {
636            (Some(LayoutInfo::Stringified(layout_string.clone())), None)
637        } else if let Some(chosen_layout) = cli_args.layout.clone() {
638            let layout_info = LayoutInfo::from_cli(
639                &layout_dir,
640                &Some(chosen_layout.clone()),
641                std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
642            );
643            (layout_info, Some(chosen_layout))
644        } else {
645            let chosen_layout = cli_config_options
646                .as_ref()
647                .and_then(|cli_options| cli_options.default_layout.clone())
648                .or_else(|| config.options.default_layout.clone());
649            let layout_info = LayoutInfo::from_config(&layout_dir, &chosen_layout);
650            (layout_info, chosen_layout)
651        };
652        match layout_info {
653            Some(LayoutInfo::Url(ref layout_url)) => {
654                Layout::from_url(layout_url, config).map(|(_layout, config)| (layout_info, config))
655            },
656            Some(LayoutInfo::Stringified(ref raw_layout)) => {
657                Layout::from_stringified_layout(raw_layout, config)
658                    .map(|(_layout, config)| (layout_info, config))
659            },
660            _ => Layout::from_path_or_default(chosen_layout.as_ref(), layout_dir.clone(), config)
661                .map(|(_layout, config)| (layout_info, config)),
662        }
663    }
664    fn handle_setup_commands(cli_args: &CliArgs) {
665        if let Some(Command::Setup(ref setup)) = &cli_args.command {
666            setup.from_cli().map_or_else(
667                |e| {
668                    eprintln!("{:?}", e);
669                    process::exit(1);
670                },
671                |_| {},
672            );
673        };
674    }
675}
676
677fn merge_attach_command_options(
678    cli_config_options: Option<Options>,
679    cli_args: &CliArgs,
680) -> Option<Options> {
681    let cli_config_options = if let Some(Command::Sessions(Sessions::Attach { options, .. })) =
682        cli_args.command.clone()
683    {
684        match options.clone().as_deref() {
685            Some(SessionCommand::Options(options)) => match cli_config_options {
686                Some(cli_config_options) => {
687                    Some(cli_config_options.merge_from_cli(options.to_owned().into()))
688                },
689                None => Some(options.to_owned().into()),
690            },
691            _ => cli_config_options,
692        }
693    } else {
694        cli_config_options
695    };
696    cli_config_options
697}
698
699#[cfg(test)]
700mod setup_test {
701    use super::Setup;
702    use crate::cli::{CliArgs, Command};
703    use crate::data::LayoutInfo;
704    use crate::input::options::Options;
705    use insta::assert_snapshot;
706    use std::path::PathBuf;
707
708    #[test]
709    fn default_config_with_no_cli_arguments() {
710        let cli_args = CliArgs::default();
711        let (config, layout_info, options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
712        assert_snapshot!(format!("{:#?}", config));
713        assert_snapshot!(format!("{:#?}", layout_info));
714        assert_snapshot!(format!("{:#?}", options));
715    }
716    #[test]
717    fn cli_arguments_override_config_options() {
718        let mut cli_args = CliArgs::default();
719        cli_args.command = Some(Command::Options(Options {
720            simplified_ui: Some(true),
721            ..Default::default()
722        }));
723        let (_config, _layout_info, options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
724        assert_snapshot!(format!("{:#?}", options));
725    }
726    #[test]
727    fn layout_options_override_config_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        let (_config, layout_info, options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
734        assert_snapshot!(format!("{:#?}", options));
735        let Some(LayoutInfo::File(layout_path, _)) = layout_info else {
736            panic!("layout info doesn't have expected format");
737        };
738        assert_eq!(
739            layout_path,
740            format!(
741                "{}/src/test-fixtures/layout-with-options.kdl",
742                env!("CARGO_MANIFEST_DIR")
743            )
744        );
745    }
746    #[test]
747    fn cli_arguments_override_layout_options() {
748        let mut cli_args = CliArgs::default();
749        cli_args.layout = Some(PathBuf::from(format!(
750            "{}/src/test-fixtures/layout-with-options.kdl",
751            env!("CARGO_MANIFEST_DIR")
752        )));
753        cli_args.command = Some(Command::Options(Options {
754            pane_frames: Some(true),
755            ..Default::default()
756        }));
757        let (_config, layout_info, options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
758        assert_snapshot!(format!("{:#?}", options));
759        let Some(LayoutInfo::File(layout_path, _)) = layout_info else {
760            panic!("layout info doesn't have expected format");
761        };
762        assert_eq!(
763            layout_path,
764            format!(
765                "{}/src/test-fixtures/layout-with-options.kdl",
766                env!("CARGO_MANIFEST_DIR")
767            )
768        );
769    }
770    #[test]
771    fn layout_env_vars_override_config_env_vars() {
772        let mut cli_args = CliArgs::default();
773        cli_args.config = Some(PathBuf::from(format!(
774            "{}/src/test-fixtures/config-with-env-vars.kdl",
775            env!("CARGO_MANIFEST_DIR")
776        )));
777        cli_args.layout = Some(PathBuf::from(format!(
778            "{}/src/test-fixtures/layout-with-env-vars.kdl",
779            env!("CARGO_MANIFEST_DIR")
780        )));
781        let (config, _layout_info, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
782        assert_snapshot!(format!("{:#?}", config));
783    }
784    #[test]
785    fn layout_ui_config_overrides_config_ui_config() {
786        let mut cli_args = CliArgs::default();
787        cli_args.config = Some(PathBuf::from(format!(
788            "{}/src/test-fixtures/config-with-ui-config.kdl",
789            env!("CARGO_MANIFEST_DIR")
790        )));
791        cli_args.layout = Some(PathBuf::from(format!(
792            "{}/src/test-fixtures/layout-with-ui-config.kdl",
793            env!("CARGO_MANIFEST_DIR")
794        )));
795        let (config, _layout_info, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
796        assert_snapshot!(format!("{:#?}", config));
797    }
798    #[test]
799    fn layout_themes_override_config_themes() {
800        let mut cli_args = CliArgs::default();
801        cli_args.config = Some(PathBuf::from(format!(
802            "{}/src/test-fixtures/config-with-themes-config.kdl",
803            env!("CARGO_MANIFEST_DIR")
804        )));
805        cli_args.layout = Some(PathBuf::from(format!(
806            "{}/src/test-fixtures/layout-with-themes-config.kdl",
807            env!("CARGO_MANIFEST_DIR")
808        )));
809        let (config, _layout_info, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
810        assert_snapshot!(format!("{:#?}", config));
811    }
812    #[test]
813    fn layout_keybinds_override_config_keybinds() {
814        let mut cli_args = CliArgs::default();
815        cli_args.config = Some(PathBuf::from(format!(
816            "{}/src/test-fixtures/config-with-keybindings-config.kdl",
817            env!("CARGO_MANIFEST_DIR")
818        )));
819        cli_args.layout = Some(PathBuf::from(format!(
820            "{}/src/test-fixtures/layout-with-keybindings-config.kdl",
821            env!("CARGO_MANIFEST_DIR")
822        )));
823        let (config, _layout_info, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
824        assert_snapshot!(format!("{:#?}", config));
825    }
826    #[test]
827    fn cli_config_dir_overrides_defaults() {
828        let config_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
829            .join("src")
830            .join("test-fixtures")
831            .join("config-dirs")
832            .join("layout-upside-down");
833        let cli_args = CliArgs {
834            config_dir: Some(config_dir.clone()),
835            ..Default::default()
836        };
837        let (_, layout_info, _, _, _) = Setup::from_cli_args(&cli_args).unwrap();
838        let Some(LayoutInfo::File(layout_path, _)) = layout_info else {
839            panic!("layout info has unexpected format");
840        };
841        let expected = config_dir
842            .join("layouts")
843            .join("upside-down.kdl")
844            .canonicalize()
845            .unwrap();
846        assert_eq!(layout_path, expected.display().to_string());
847    }
848    #[test]
849    fn cli_config_dir_finds_custom_default() {
850        let config_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
851            .join("src")
852            .join("test-fixtures")
853            .join("config-dirs")
854            .join("custom-default-layout");
855        let cli_args = CliArgs {
856            config_dir: Some(config_dir.clone()),
857            ..Default::default()
858        };
859        let (_, layout_info, _, _, _) = Setup::from_cli_args(&cli_args).unwrap();
860        let Some(LayoutInfo::File(layout_path, _)) = layout_info else {
861            panic!("layout info has unexpected format");
862        };
863        let expected = config_dir
864            .join("layouts")
865            .join("default.kdl")
866            .canonicalize()
867            .unwrap();
868        assert_eq!(layout_path, expected.display().to_string());
869    }
870
871    #[test]
872    fn cli_with_relative_layout_and_extension() {
873        // NOTE: We assume to be in `zellij-utils` root directory. If this doesn't hold, path
874        // resolution cannot work (as it actually reads path to ensure they exist).
875        let cwd = std::env::current_dir().unwrap();
876        assert_eq!(cwd, PathBuf::from(env!("CARGO_MANIFEST_DIR")));
877
878        let cli_args = CliArgs {
879            layout: Some(PathBuf::from("assets/layouts/compact.kdl")),
880            ..Default::default()
881        };
882        let (_, layout_info, _, _, _) = Setup::from_cli_args(&cli_args).unwrap();
883        let Some(LayoutInfo::File(layout_path, _)) = layout_info else {
884            panic!("layout info has unexpected format: {:?}", &layout_info);
885        };
886        let expected = cwd.join("assets/layouts/compact.kdl");
887        assert_eq!(layout_path, expected.display().to_string());
888    }
889
890    #[test]
891    fn cli_with_relative_layout_and_separator() {
892        // NOTE: We assume to be in `zellij-utils` root directory. If this doesn't hold, path
893        // resolution cannot work (as it actually reads path to ensure they exist).
894        let cwd = std::env::current_dir().unwrap();
895        assert_eq!(cwd, PathBuf::from(env!("CARGO_MANIFEST_DIR")));
896
897        let cli_args = CliArgs {
898            layout: Some(PathBuf::from("assets/layouts/compact")),
899            ..Default::default()
900        };
901        let (_, layout_info, _, _, _) = Setup::from_cli_args(&cli_args).unwrap();
902        let Some(LayoutInfo::File(layout_path, _)) = layout_info else {
903            panic!("layout info has unexpected format");
904        };
905        let expected = cwd.join("assets/layouts/compact");
906        assert_eq!(layout_path, expected.display().to_string());
907    }
908
909    #[test]
910    fn layout_string_cli_argument() {
911        let layout_kdl = "layout {\n    pane\n    pane\n}\n".to_string();
912        let cli_args = CliArgs {
913            layout_string: Some(layout_kdl.clone()),
914            ..Default::default()
915        };
916        let (_, layout_info, _, _, _) = Setup::from_cli_args(&cli_args).unwrap();
917        let Some(LayoutInfo::Stringified(content)) = layout_info else {
918            panic!(
919                "layout info should be Stringified variant, got: {:#?}",
920                layout_info
921            );
922        };
923        assert_eq!(content, layout_kdl);
924    }
925}