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}