Skip to main content

todo_tree/
cli.rs

1use clap::{Args, Parser, Subcommand, ValueHint};
2use std::path::PathBuf;
3
4#[derive(Parser, Debug)]
5#[command(
6    name = "todo-tree",
7    author,
8    version,
9    about,
10    long_about = None,
11)]
12pub struct Cli {
13    #[command(flatten)]
14    pub global: GlobalOptions,
15
16    #[command(subcommand)]
17    pub command: Option<Commands>,
18}
19
20#[derive(Args, Debug, Clone)]
21pub struct GlobalOptions {
22    #[arg(long, global = true, env = "NO_COLOR", help = "Disable colored output")]
23    pub no_color: bool,
24
25    #[arg(short, long, global = true, help = "Enable verbose logging")]
26    pub verbose: bool,
27
28    #[arg(
29        long,
30        global = true,
31        value_hint = ValueHint::FilePath,
32        help = "Path to config file"
33    )]
34    pub config: Option<PathBuf>,
35}
36
37#[derive(Subcommand, Debug, Clone)]
38pub enum Commands {
39    #[command(visible_alias = "s", about = "Scan files and print TODO matches")]
40    Scan(ScanArgs),
41    #[command(visible_alias = "l", visible_alias = "ls", about = "List TODO matches")]
42    List(ListArgs),
43    #[command(visible_alias = "t", about = "Manage configured TODO tags")]
44    Tags(TagsArgs),
45    #[command(about = "Create a default configuration file")]
46    Init(InitArgs),
47    #[command(about = "Manage GitHub Actions workflow templates")]
48    Workflow(WorkflowArgs),
49    #[command(about = "Show summary stats for TODO matches")]
50    Stats(StatsArgs),
51}
52
53#[derive(Args, Debug, Clone)]
54pub struct ScanArgs {
55    #[arg(value_hint = ValueHint::AnyPath, help = "Path to scan (defaults to current directory)")]
56    pub path: Option<PathBuf>,
57    #[arg(
58        short,
59        long,
60        value_delimiter = ',',
61        help = "Tags to search for (comma-separated)"
62    )]
63    pub tags: Option<Vec<String>>,
64    #[arg(
65        short,
66        long,
67        value_delimiter = ',',
68        help = "File patterns to include (glob patterns, comma-separated)"
69    )]
70    pub include: Option<Vec<String>>,
71    #[arg(
72        short,
73        long,
74        value_delimiter = ',',
75        help = "File patterns to exclude (glob patterns, comma-separated)"
76    )]
77    pub exclude: Option<Vec<String>>,
78    #[arg(long, help = "Output results in JSON format")]
79    pub json: bool,
80    #[arg(long, help = "Print flat output without grouping by file")]
81    pub flat: bool,
82    #[arg(
83        short,
84        long,
85        default_value = "0",
86        help = "Limit directory traversal depth"
87    )]
88    pub depth: usize,
89    #[arg(long, help = "Follow symlinks when scanning")]
90    pub follow_links: bool,
91    #[arg(long, help = "Include hidden files and directories")]
92    pub hidden: bool,
93    #[arg(long, help = "Ignore case when matching tags")]
94    pub ignore_case: bool,
95    #[arg(long, help = "Allow tags without a trailing colon")]
96    pub no_require_colon: bool,
97    #[arg(long, default_value = "file", help = "Sort order for results")]
98    pub sort: SortOrder,
99    #[arg(long, help = "Group output by tag")]
100    pub group_by_tag: bool,
101}
102
103impl Default for ScanArgs {
104    fn default() -> Self {
105        Self {
106            path: None,
107            tags: None,
108            include: None,
109            exclude: None,
110            json: false,
111            flat: false,
112            depth: 0,
113            follow_links: false,
114            hidden: false,
115            ignore_case: false,
116            no_require_colon: false,
117            sort: SortOrder::File,
118            group_by_tag: false,
119        }
120    }
121}
122
123#[derive(Args, Debug, Clone, Default)]
124pub struct ListArgs {
125    #[arg(value_hint = ValueHint::AnyPath, help = "Path to scan (defaults to current directory)")]
126    pub path: Option<PathBuf>,
127    #[arg(
128        short,
129        long,
130        value_delimiter = ',',
131        help = "Tags to search for (comma-separated)"
132    )]
133    pub tags: Option<Vec<String>>,
134    #[arg(
135        short,
136        long,
137        value_delimiter = ',',
138        help = "File patterns to include (glob patterns, comma-separated)"
139    )]
140    pub include: Option<Vec<String>>,
141    #[arg(
142        short,
143        long,
144        value_delimiter = ',',
145        help = "File patterns to exclude (glob patterns, comma-separated)"
146    )]
147    pub exclude: Option<Vec<String>>,
148    #[arg(long, help = "Output results in JSON format")]
149    pub json: bool,
150    #[arg(long, help = "Filter results by a specific tag")]
151    pub filter: Option<String>,
152    #[arg(long, help = "Ignore case when matching tags")]
153    pub ignore_case: bool,
154    #[arg(long, help = "Allow tags without a trailing colon")]
155    pub no_require_colon: bool,
156}
157
158#[derive(Args, Debug, Clone)]
159pub struct TagsArgs {
160    #[arg(long, help = "Show tags in JSON format")]
161    pub json: bool,
162    #[arg(long, help = "Add a new tag to the configuration")]
163    pub add: Option<String>,
164    #[arg(long, help = "Remove a tag from the configuration")]
165    pub remove: Option<String>,
166    #[arg(long, help = "Reset tags to defaults")]
167    pub reset: bool,
168}
169
170#[derive(Args, Debug, Clone)]
171pub struct InitArgs {
172    #[arg(
173        long,
174        default_value = "json",
175        help = "Configuration format: json or yaml"
176    )]
177    pub format: ConfigFormat,
178    #[arg(short, long, help = "Overwrite the config file if it exists")]
179    pub force: bool,
180}
181
182#[derive(Args, Debug, Clone)]
183pub struct WorkflowArgs {
184    #[command(subcommand)]
185    pub command: WorkflowCommands,
186}
187
188#[derive(Subcommand, Debug, Clone)]
189pub enum WorkflowCommands {
190    #[command(about = "Create a GitHub Actions workflow for todo-tree-action")]
191    Init(WorkflowInitArgs),
192}
193
194#[derive(Args, Debug, Clone)]
195pub struct WorkflowInitArgs {
196    #[arg(short, long, help = "Overwrite the workflow file if it exists")]
197    pub force: bool,
198
199    #[arg(
200        long,
201        value_hint = ValueHint::FilePath,
202        help = "Path to the workflow file"
203    )]
204    pub path: Option<PathBuf>,
205
206    #[arg(
207        long,
208        help = "GitHub Action reference to use in the generated workflow"
209    )]
210    pub action: Option<String>,
211}
212
213#[derive(Args, Debug, Clone)]
214pub struct StatsArgs {
215    #[arg(value_hint = ValueHint::AnyPath, help = "Path to scan (defaults to current directory)")]
216    pub path: Option<PathBuf>,
217    #[arg(
218        short,
219        long,
220        value_delimiter = ',',
221        help = "Tags to search for (comma-separated)"
222    )]
223    pub tags: Option<Vec<String>>,
224    #[arg(long, help = "Output results in JSON format")]
225    pub json: bool,
226}
227
228#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, clap::ValueEnum)]
229pub enum SortOrder {
230    #[value(name = "file", help = "Sort by file path")]
231    #[default]
232    File,
233    #[value(name = "line", help = "Sort by line number")]
234    Line,
235    #[value(name = "priority", help = "Sort by tag priority")]
236    Priority,
237}
238
239#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, clap::ValueEnum)]
240pub enum ConfigFormat {
241    #[default]
242    #[value(name = "json", help = "Generate JSON config")]
243    Json,
244    #[value(name = "yaml", help = "Generate YAML config")]
245    Yaml,
246}
247
248impl Cli {
249    pub fn get_command(&self) -> Commands {
250        self.command
251            .clone()
252            .unwrap_or_else(|| Commands::Scan(ScanArgs::default()))
253    }
254}
255
256impl From<ScanArgs> for ListArgs {
257    fn from(scan: ScanArgs) -> Self {
258        Self {
259            path: scan.path,
260            tags: scan.tags,
261            include: scan.include,
262            exclude: scan.exclude,
263            json: scan.json,
264            filter: None,
265            ignore_case: scan.ignore_case,
266            no_require_colon: scan.no_require_colon,
267        }
268    }
269}