Skip to main content

ite_cli/
cli.rs

1//! Command-line interface.
2
3use std::path::PathBuf;
4use std::str::FromStr;
5
6use clap::Parser;
7
8/// Argument to `--expand`: a depth or `all`.
9#[derive(Clone, Copy, PartialEq, Eq, Debug)]
10pub enum ExpandSpec {
11    Depth(usize),
12    All,
13}
14
15impl FromStr for ExpandSpec {
16    type Err = String;
17
18    fn from_str(s: &str) -> Result<Self, Self::Err> {
19        if s.eq_ignore_ascii_case("all") {
20            return Ok(Self::All);
21        }
22        s.parse()
23            .map(Self::Depth)
24            .map_err(|_| format!("expected a depth or `all`, got {s:?}"))
25    }
26}
27
28/// ite — interactive tree explorer.
29#[derive(Parser, Debug)]
30#[command(name = "ite", version, about)]
31pub struct Cli {
32    /// Directory to explore (defaults to the current directory).
33    pub path: Option<PathBuf>,
34
35    /// JSON file to explore instead of a directory; `-` reads stdin.
36    /// Piped stdin with no PATH is read as JSON automatically.
37    #[arg(short, long, value_name = "PATH", conflicts_with = "path")]
38    pub json: Option<PathBuf>,
39
40    /// Do not respect ignore files (.gitignore etc.).
41    #[arg(short = 'I', long)]
42    pub no_ignore: bool,
43
44    /// Expand all non-leaves at or below depth N, or `all` for everything.
45    #[arg(short, long)]
46    pub expand: Option<ExpandSpec>,
47
48    /// Config file to load instead of the user config; repeatable.
49    #[arg(short, long)]
50    pub config: Vec<PathBuf>,
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    fn parse(args: &[&str]) -> Cli {
58        Cli::try_parse_from(std::iter::once("ite").chain(args.iter().copied())).unwrap()
59    }
60
61    #[test]
62    fn defaults() {
63        let cli = parse(&[]);
64        assert_eq!(cli.path, None);
65        assert_eq!(cli.json, None);
66        assert!(!cli.no_ignore);
67        assert_eq!(cli.expand, None);
68        assert!(cli.config.is_empty());
69    }
70
71    #[test]
72    fn positional_path() {
73        assert_eq!(parse(&["/some/dir"]).path, Some(PathBuf::from("/some/dir")));
74    }
75
76    #[test]
77    fn json_path_flag_and_alias() {
78        assert_eq!(
79            parse(&["--json", "data.json"]).json,
80            Some(PathBuf::from("data.json"))
81        );
82        assert_eq!(
83            parse(&["-j", "other.json"]).json,
84            Some(PathBuf::from("other.json"))
85        );
86    }
87
88    #[test]
89    fn json_and_directory_paths_are_mutually_exclusive() {
90        assert!(Cli::try_parse_from(["ite", "--json", "data.json", "/some/dir"]).is_err());
91    }
92
93    #[test]
94    fn no_ignore_flag_and_alias() {
95        assert!(parse(&["--no-ignore"]).no_ignore);
96        assert!(parse(&["-I"]).no_ignore);
97    }
98
99    #[test]
100    fn expand_depth_and_all() {
101        assert_eq!(parse(&["--expand", "2"]).expand, Some(ExpandSpec::Depth(2)));
102        assert_eq!(parse(&["-e", "all"]).expand, Some(ExpandSpec::All));
103    }
104
105    #[test]
106    fn expand_rejects_garbage() {
107        assert!(Cli::try_parse_from(["ite", "--expand", "banana"]).is_err());
108    }
109
110    #[test]
111    fn repeated_config_flags() {
112        let cli = parse(&["-c", "a.toml", "--config", "b.toml"]);
113        assert_eq!(
114            cli.config,
115            vec![PathBuf::from("a.toml"), PathBuf::from("b.toml")]
116        );
117    }
118}