fancy_tree/
cli.rs

1//! CLI utilities.
2use crate::color::ColorChoice;
3use crate::config::{self, ConfigDir, ConfigFile as _};
4use crate::git::Git;
5use crate::lua;
6use crate::tree;
7use clap::{Parser, ValueEnum};
8use std::fs;
9use std::path::PathBuf;
10
11/// Lists files in a directory.
12#[derive(Parser)]
13#[deny(missing_docs)]
14pub struct Cli {
15    /// The path to search in.
16    #[arg(default_value = ".")]
17    pub path: PathBuf,
18
19    /// Controls colorization.
20    #[arg(long = "color")]
21    pub color_choice: Option<ColorChoice>,
22
23    /// Go only this many levels deep.
24    #[arg(short = 'L', long)]
25    pub level: Option<usize>,
26
27    /// Edit the main configuration file and exit.
28    #[arg(long, num_args = 0..=1, default_missing_value = "config")]
29    pub edit_config: Option<EditConfig>,
30}
31
32/// Choices for which config file to edit.
33#[derive(ValueEnum, Clone, Copy)]
34pub enum EditConfig {
35    /// The main configuration file.
36    Config,
37    /// The custom icon configuration.
38    Icons,
39    /// The custom colors configuration.
40    Colors,
41}
42
43impl Cli {
44    /// An environment variable the user can set to specify which editor to use.
45    const EDITOR_ENV_VAR: &str = "FANCY_TREE_EDITOR";
46
47    /// Runs the CLI.
48    pub fn run(&self) -> crate::Result {
49        if self.should_edit_file() {
50            self.edit_file()?;
51            return Ok(());
52        }
53
54        let git = Git::new(&self.path).expect("Should be able to read the git repository");
55
56        // NOTE The Lua state must live as long as the configuration values.
57        let lua_state = {
58            let mut builder = lua::state::Builder::new();
59            if let Some(ref git) = git {
60                builder = builder.with_git(git);
61            }
62            builder.build().expect("The lua state should be valid")
63        };
64
65        // TODO Skip loading the config instead of panicking.
66        let config_dir = ConfigDir::new().expect("A config dir should be available");
67        let config = config_dir
68            .load_main(lua_state.to_inner())
69            .expect("The configuration should be valid");
70        let icons = config_dir
71            .load_icons(lua_state.to_inner())
72            .expect("The icon configuration should be valid");
73        let colors = config_dir
74            .load_colors(lua_state.to_inner())
75            .expect("The color configuration should be valid");
76
77        let color_choice = self
78            .color_choice
79            .or_else(|| config.as_ref().and_then(|config| config.color_choice()))
80            .unwrap_or_default();
81        let mut builder = tree::Builder::new(&self.path, color_choice);
82        if let Some(config) = config {
83            builder = builder.config(config);
84        }
85        if let Some(icons) = icons {
86            builder = builder.icons(icons);
87        }
88        if let Some(colors) = colors {
89            builder = builder.colors(colors);
90        }
91        if let Some(ref git) = git {
92            builder = builder.git(git);
93        }
94        if let Some(level) = self.level {
95            builder = builder.max_level(level);
96        }
97        let tree = builder.build();
98
99        lua_state.in_git_scope(|| tree.write_to_stdout().map_err(mlua::Error::external))?;
100
101        Ok(())
102    }
103
104    /// Returns `true` if the user wants to edit a file. Edit prompts should exit early
105    /// without running the main behavior.
106    #[inline]
107    fn should_edit_file(&self) -> bool {
108        self.edit_config.is_some()
109    }
110
111    /// Opens an editor for the file the user specified, creating the config directory
112    /// if needed.
113    fn edit_file(&self) -> crate::Result {
114        let config_dir = ConfigDir::new()?;
115        fs::create_dir_all(config_dir.path())?;
116
117        let edit_config = self.edit_config.expect("Should have checked if Some");
118
119        let (file_path, contents) = match edit_config {
120            EditConfig::Config => (config_dir.main_path(), config::Main::DEFAULT_MODULE),
121            EditConfig::Icons => (config_dir.icons_path(), config::Icons::DEFAULT_MODULE),
122            EditConfig::Colors => (config_dir.colors_path(), config::Colors::DEFAULT_MODULE),
123        };
124
125        // NOTE If we can't check if it exists, we'll be safe and skip overwriting it.
126        if !file_path.try_exists().unwrap_or(false) {
127            // NOTE Ignore error, because editing the file is a higher priority than
128            //      writing to it.
129            let _ = fs::write(&file_path, contents);
130        }
131
132        println!("Opening `{}`", file_path.display());
133
134        let finder = find_editor::Finder::with_extra_environment_variables([Self::EDITOR_ENV_VAR]);
135        /// Should the program wait for the editor to close before continuing?
136        const WAIT: bool = true;
137        finder.open_editor(file_path, WAIT)?;
138
139        Ok(())
140    }
141}
142
143// Runs the CLI. Can exit early without returning an error. For example, this will exit
144// early if the user passes `-h` as CLI argument.
145pub fn run() -> crate::Result {
146    let cli = Cli::parse();
147    cli.run()
148}