Skip to main content

coding_tools/cli/
ct_tree.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Jonathan Shook
3
4//! The `ct-tree` command grammar (see [`crate::cli`]); the `ct-tree` bin is a
5//! thin parse-and-dispatch wrapper over this `Cli`.
6
7use std::path::PathBuf;
8
9use clap::Parser;
10
11use crate::explain::Format;
12use crate::pattern;
13use crate::pulse::HeartbeatOpts;
14
15#[derive(Parser, Debug)]
16#[command(
17    name = "ct-tree",
18    version,
19    about = "Report a file tree with per-file line/word/char counts, filtered, sorted, and summarised.",
20    long_about = "ct-tree walks a directory for chosen file types and reports the effective tree with \
21                  per-file line, word, and character counts (also reachable as `ct tree`). Filter by \
22                  metric predicates (--min-lines etc.) and per-folder counts, sort by any column, and \
23                  choose a summarisation level (--tree, --flat, --summary). See `ct-tree --explain` \
24                  for agent-oriented documentation."
25)]
26#[command(group = clap::ArgGroup::new("output_mode")
27    .args(["tree", "flat", "summary"])
28    .multiple(false))]
29pub struct Cli {
30    /// Root to walk (relative or absolute), independent of the current directory.
31    #[arg(long, default_value = ".")]
32    pub base: PathBuf,
33
34    /// File-name pattern; '|'-separated alternatives, each substring->glob->regex promoted and anchored.
35    #[arg(long)]
36    pub name: Option<String>,
37
38    /// Pin how --name/--ext patterns are interpreted (promotion off): literal, glob, or regex.
39    #[arg(long, value_enum)]
40    pub mode: Option<pattern::Mode>,
41
42    /// Restrict to these extensions (comma-separated, no dots), e.g. --ext rs,toml. Combined with --name as alternatives.
43    #[arg(long, value_delimiter = ',')]
44    pub ext: Vec<String>,
45
46    /// Include dot-entries (names starting with '.'); default skips them.
47    #[arg(long)]
48    pub hidden: bool,
49
50    /// Follow symlinks while traversing.
51    #[arg(long)]
52    pub follow: bool,
53
54    /// Walk gitignored / .ignore files too (the .git directory is always skipped); by default the walk skips what git would.
55    #[arg(long)]
56    pub no_ignore: bool,
57
58    /// Only include files with at least N lines.
59    #[arg(long)]
60    pub min_lines: Option<u64>,
61    /// Only include files with at most N lines.
62    #[arg(long)]
63    pub max_lines: Option<u64>,
64    /// Only include files with at least N words.
65    #[arg(long)]
66    pub min_words: Option<u64>,
67    /// Only include files with at most N words.
68    #[arg(long)]
69    pub max_words: Option<u64>,
70    /// Only include files with at least N characters.
71    #[arg(long)]
72    pub min_chars: Option<u64>,
73    /// Only include files with at most N characters.
74    #[arg(long)]
75    pub max_chars: Option<u64>,
76
77    /// Only include folders that directly contain at least N matching files.
78    #[arg(long)]
79    pub min_files_per_folder: Option<usize>,
80    /// Only include folders that directly contain at most N matching files.
81    #[arg(long)]
82    pub max_files_per_folder: Option<usize>,
83
84    /// Sort key: path, name, lines, words, chars, or ext.
85    #[arg(long, value_enum, default_value_t = SortKey::Path)]
86    pub sort: SortKey,
87    /// Sort descending instead of ascending.
88    #[arg(long)]
89    pub desc: bool,
90
91    /// Output mode: an indented file tree with per-file and per-folder counts (default).
92    #[arg(long)]
93    pub tree: bool,
94    /// Output mode: one matching file per line with its counts.
95    #[arg(long)]
96    pub flat: bool,
97    /// Output mode: aggregate counts only, grouped by --group.
98    #[arg(long)]
99    pub summary: bool,
100
101    /// Grouping for --summary: ext, dir, or none (grand total only).
102    #[arg(long, value_enum, default_value_t = GroupBy::Ext)]
103    pub group: GroupBy,
104
105    /// Emit a structured JSON result instead of text.
106    #[arg(long)]
107    pub json: bool,
108
109    /// Like `--json`, but pretty-printed (indented).
110    #[arg(long)]
111    pub json_pretty: bool,
112
113    /// Abort with exit 2 if the report exceeds SECS seconds (fractional allowed).
114    #[arg(long, value_name = "SECS")]
115    pub timeout: Option<f64>,
116
117    #[command(flatten)]
118    pub heartbeat: HeartbeatOpts,
119
120    /// Print agent usage docs (md or json) and exit.
121    #[arg(long, value_enum, num_args = 0..=1, default_missing_value = "md")]
122    pub explain: Option<Format>,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
126pub enum SortKey {
127    Path,
128    Name,
129    Lines,
130    Words,
131    Chars,
132    Ext,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
136pub enum GroupBy {
137    Ext,
138    Dir,
139    None,
140}