Skip to main content

ite_cli/
cli.rs

1//! The declarative command-line surface: clap argument definitions and parsing
2//! for expansion policy, filesystem/JSON source selection, ignore behavior,
3//! and configuration paths.
4//!
5//! This module describes user input but performs no I/O and does not decide the
6//! implicit source. The executable interprets `Cli`, loads files/stdin, and
7//! passes the resulting settings into the application.
8
9use std::path::PathBuf;
10use std::str::FromStr;
11
12use clap::Parser;
13
14/// Argument to `--expand`: a depth or `all`.
15#[derive(Clone, Copy, PartialEq, Eq, Debug)]
16pub enum ExpandSpec {
17    Depth(usize),
18    All,
19}
20
21impl FromStr for ExpandSpec {
22    type Err = String;
23
24    fn from_str(s: &str) -> Result<Self, Self::Err> {
25        if s.eq_ignore_ascii_case("all") {
26            return Ok(Self::All);
27        }
28        s.parse()
29            .map(Self::Depth)
30            .map_err(|_| format!("expected a depth or `all`, got {s:?}"))
31    }
32}
33
34/// ite — interactive tree explorer.
35#[derive(Parser, Debug)]
36#[command(name = "ite", version, about)]
37pub struct Cli {
38    /// Directory to explore (defaults to the current directory).
39    pub path: Option<PathBuf>,
40
41    /// JSON file to explore instead of a directory; `-` reads stdin.
42    /// Piped stdin with no PATH is read as JSON automatically. JSONL
43    /// content is detected from the content itself.
44    #[arg(short, long, value_name = "PATH", conflicts_with = "path")]
45    pub json: Option<PathBuf>,
46
47    /// JSON Lines file to explore; `-` reads stdin. Forces the JSONL
48    /// reading when detection can't be sure (e.g. a single record).
49    #[arg(short = 'l', long, value_name = "PATH", conflicts_with_all = ["path", "json"])]
50    pub jsonl: Option<PathBuf>,
51
52    /// Do not respect ignore files (.gitignore etc.).
53    #[arg(short = 'I', long)]
54    pub no_ignore: bool,
55
56    /// Expand all non-leaves at or below depth N, or `all` for everything.
57    #[arg(short, long)]
58    pub expand: Option<ExpandSpec>,
59
60    /// Config file to load instead of the user config; repeatable.
61    #[arg(short, long)]
62    pub config: Vec<PathBuf>,
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    fn parse(args: &[&str]) -> Cli {
70        Cli::try_parse_from(std::iter::once("ite").chain(args.iter().copied())).unwrap()
71    }
72
73    #[test]
74    fn defaults() {
75        let cli = parse(&[]);
76        assert_eq!(cli.path, None);
77        assert_eq!(cli.json, None);
78        assert_eq!(cli.jsonl, None);
79        assert!(!cli.no_ignore);
80        assert_eq!(cli.expand, None);
81        assert!(cli.config.is_empty());
82    }
83
84    #[test]
85    fn positional_path() {
86        assert_eq!(parse(&["/some/dir"]).path, Some(PathBuf::from("/some/dir")));
87    }
88
89    #[test]
90    fn json_path_flag_and_alias() {
91        assert_eq!(
92            parse(&["--json", "data.json"]).json,
93            Some(PathBuf::from("data.json"))
94        );
95        assert_eq!(
96            parse(&["-j", "other.json"]).json,
97            Some(PathBuf::from("other.json"))
98        );
99    }
100
101    #[test]
102    fn json_and_directory_paths_are_mutually_exclusive() {
103        assert!(Cli::try_parse_from(["ite", "--json", "data.json", "/some/dir"]).is_err());
104    }
105
106    #[test]
107    fn jsonl_path_flag_and_alias() {
108        assert_eq!(
109            parse(&["--jsonl", "log.jsonl"]).jsonl,
110            Some(PathBuf::from("log.jsonl"))
111        );
112        assert_eq!(
113            parse(&["-l", "log.jsonl"]).jsonl,
114            Some(PathBuf::from("log.jsonl"))
115        );
116    }
117
118    #[test]
119    fn jsonl_conflicts_with_json_and_directory_paths() {
120        assert!(Cli::try_parse_from(["ite", "--jsonl", "a.jsonl", "--json", "b.json"]).is_err());
121        assert!(Cli::try_parse_from(["ite", "--jsonl", "a.jsonl", "/some/dir"]).is_err());
122    }
123
124    #[test]
125    fn no_ignore_flag_and_alias() {
126        assert!(parse(&["--no-ignore"]).no_ignore);
127        assert!(parse(&["-I"]).no_ignore);
128    }
129
130    #[test]
131    fn expand_depth_and_all() {
132        assert_eq!(parse(&["--expand", "2"]).expand, Some(ExpandSpec::Depth(2)));
133        assert_eq!(parse(&["-e", "all"]).expand, Some(ExpandSpec::All));
134    }
135
136    #[test]
137    fn expand_rejects_garbage() {
138        assert!(Cli::try_parse_from(["ite", "--expand", "banana"]).is_err());
139    }
140
141    #[test]
142    fn repeated_config_flags() {
143        let cli = parse(&["-c", "a.toml", "--config", "b.toml"]);
144        assert_eq!(
145            cli.config,
146            vec![PathBuf::from("a.toml"), PathBuf::from("b.toml")]
147        );
148    }
149}