Skip to main content

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