1use crate::errors::{FsError, Result};
2use crate::models::{Column, EntryKind, OutputFormat, SortKey, SortOrder};
3use clap::{Parser, Subcommand, ValueEnum};
4use std::path::PathBuf;
5
6#[derive(Parser, Debug)]
7#[command(name = "fexplorer")]
8#[command(author, version, about = "A fast, modern file system explorer and toolkit", long_about = None)]
9pub struct Cli {
10 #[command(subcommand)]
11 pub command: Commands,
12
13 #[arg(long, global = true)]
15 pub no_color: bool,
16
17 #[arg(long, short = 'q', global = true)]
19 pub quiet: bool,
20
21 #[arg(long, short = 'v', global = true)]
23 pub verbose: bool,
24}
25
26#[derive(Subcommand, Debug)]
27pub enum Commands {
28 #[command(visible_alias = "ls")]
30 List {
31 #[arg(default_value = ".")]
33 path: PathBuf,
34
35 #[arg(long, value_name = "KEY")]
37 sort: Option<String>,
38
39 #[arg(long, default_value = "asc")]
41 order: String,
42
43 #[arg(long)]
45 dirs_first: bool,
46
47 #[command(flatten)]
48 common: CommonArgs,
49 },
50
51 Tree {
53 #[arg(default_value = ".")]
55 path: PathBuf,
56
57 #[arg(long)]
59 dirs_first: bool,
60
61 #[command(flatten)]
62 common: CommonArgs,
63 },
64
65 Find {
67 #[arg(default_value = ".")]
69 path: PathBuf,
70
71 #[arg(long = "name")]
73 names: Vec<String>,
74
75 #[arg(long)]
77 regex: Option<String>,
78
79 #[arg(long, value_delimiter = ',')]
81 ext: Vec<String>,
82
83 #[arg(long)]
85 min_size: Option<String>,
86
87 #[arg(long)]
89 max_size: Option<String>,
90
91 #[arg(long)]
93 after: Option<String>,
94
95 #[arg(long)]
97 before: Option<String>,
98
99 #[arg(long, value_delimiter = ',')]
101 kind: Vec<String>,
102
103 #[arg(long)]
105 category: Option<String>,
106
107 #[command(flatten)]
108 common: CommonArgs,
109 },
110
111 Size {
113 #[arg(default_value = ".")]
115 path: PathBuf,
116
117 #[arg(long)]
119 top: Option<usize>,
120
121 #[arg(long)]
123 aggregate: bool,
124
125 #[arg(long)]
127 du: bool,
128
129 #[command(flatten)]
130 common: CommonArgs,
131 },
132
133 #[cfg(feature = "grep")]
135 Grep {
136 #[arg(default_value = ".")]
138 path: PathBuf,
139
140 #[arg(value_name = "PATTERN")]
142 pattern: String,
143
144 #[arg(long, short = 'e')]
146 regex: bool,
147
148 #[arg(long, short = 'i')]
150 case_insensitive: bool,
151
152 #[arg(long, value_delimiter = ',')]
154 ext: Vec<String>,
155
156 #[arg(long, short = 'C', default_value = "0")]
158 context: usize,
159
160 #[arg(long, short = 'n')]
162 line_numbers: bool,
163
164 #[command(flatten)]
165 common: CommonArgs,
166 },
167
168 #[cfg(feature = "dedup")]
170 Duplicates {
171 #[arg(default_value = ".")]
173 path: PathBuf,
174
175 #[arg(long, default_value = "0")]
177 min_size: String,
178
179 #[arg(long)]
181 summary: bool,
182
183 #[command(flatten)]
184 common: CommonArgs,
185 },
186
187 #[cfg(feature = "git")]
189 Git {
190 #[arg(default_value = ".")]
192 path: PathBuf,
193
194 #[arg(long, value_enum)]
196 status: Option<GitStatusFilter>,
197
198 #[arg(long)]
200 since: Option<String>,
201
202 #[command(flatten)]
203 common: CommonArgs,
204 },
205
206 #[cfg(feature = "tui")]
208 #[command(visible_alias = "tui")]
209 Interactive {
210 #[arg(default_value = ".")]
212 path: PathBuf,
213 },
214
215 #[cfg(feature = "trends")]
217 Snapshot {
218 #[arg(default_value = ".")]
220 path: PathBuf,
221
222 #[arg(long)]
224 description: Option<String>,
225 },
226
227 #[cfg(feature = "trends")]
229 Trends {
230 #[arg(default_value = ".")]
232 path: PathBuf,
233
234 #[arg(long)]
236 since: Option<String>,
237
238 #[arg(long)]
240 chart: bool,
241 },
242
243 Completions {
245 #[arg(value_enum)]
247 shell: Shell,
248 },
249
250 Profiles {
252 #[command(subcommand)]
253 command: ProfileCommand,
254 },
255
256 Run {
258 profile: String,
260
261 #[arg(long)]
263 path: Option<PathBuf>,
264
265 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
267 args: Vec<String>,
268 },
269
270 #[cfg(feature = "watch")]
272 Watch {
273 #[arg(default_value = ".")]
275 path: PathBuf,
276
277 #[arg(long, value_delimiter = ',')]
279 events: Vec<String>,
280
281 #[arg(long, default_value = "ndjson")]
283 format: String,
284 },
285
286 #[cfg(feature = "plugins")]
288 Plugins {
289 #[command(subcommand)]
290 command: PluginCommand,
291 },
292}
293
294#[derive(Subcommand, Debug)]
296pub enum ProfileCommand {
297 List,
299
300 Show {
302 name: String,
304 },
305
306 Init,
308}
309
310#[derive(Subcommand, Debug)]
312#[cfg(feature = "plugins")]
313pub enum PluginCommand {
314 List,
316
317 Enable {
319 name: String,
321 },
322
323 Disable {
325 name: String,
327 },
328}
329
330#[derive(Debug, Clone, Copy, ValueEnum)]
332#[cfg(feature = "git")]
333pub enum GitStatusFilter {
334 Untracked,
335 Modified,
336 Staged,
337 Conflict,
338 Clean,
339 Ignored,
340}
341
342#[derive(Debug, Clone, Copy, ValueEnum)]
344pub enum Shell {
345 Bash,
346 Zsh,
347 Fish,
348 Powershell,
349 Elvish,
350}
351
352#[derive(Parser, Debug, Clone)]
354pub struct CommonArgs {
355 #[arg(long)]
357 pub max_depth: Option<usize>,
358
359 #[arg(long)]
361 pub hidden: bool,
362
363 #[arg(long)]
365 pub no_gitignore: bool,
366
367 #[arg(long)]
369 pub follow_symlinks: bool,
370
371 #[arg(long, default_value = "pretty")]
373 pub format: String,
374
375 #[arg(long, value_delimiter = ',')]
377 pub columns: Vec<String>,
378
379 #[cfg(feature = "parallel")]
381 #[arg(long, default_value = "4")]
382 pub threads: usize,
383
384 #[cfg(feature = "progress")]
386 #[arg(long)]
387 pub progress: bool,
388
389 #[cfg(feature = "templates")]
391 #[arg(long)]
392 pub template: Option<String>,
393}
394
395impl Default for CommonArgs {
396 fn default() -> Self {
397 Self {
398 max_depth: None,
399 hidden: false,
400 no_gitignore: false,
401 follow_symlinks: false,
402 format: "pretty".to_string(),
403 columns: Vec::new(),
404 #[cfg(feature = "parallel")]
405 threads: 4,
406 #[cfg(feature = "progress")]
407 progress: false,
408 #[cfg(feature = "templates")]
409 template: None,
410 }
411 }
412}
413
414impl CommonArgs {
415 pub fn output_format(&self) -> Result<OutputFormat> {
416 OutputFormat::from_str(&self.format).ok_or_else(|| FsError::InvalidFormat {
417 format: self.format.clone(),
418 })
419 }
420
421 pub fn columns(&self) -> Result<Vec<Column>> {
422 if self.columns.is_empty() {
423 return Ok(vec![
425 Column::Path,
426 Column::Size,
427 Column::Mtime,
428 Column::Kind,
429 ]);
430 }
431
432 self.columns
433 .iter()
434 .map(|s| {
435 Column::from_str(s).ok_or_else(|| FsError::InvalidFormat {
436 format: format!("Invalid column: {}", s),
437 })
438 })
439 .collect()
440 }
441}
442
443pub fn parse_sort_key(s: &str) -> Result<SortKey> {
446 match s.to_lowercase().as_str() {
447 "name" => Ok(SortKey::Name),
448 "size" => Ok(SortKey::Size),
449 "mtime" => Ok(SortKey::Mtime),
450 "kind" => Ok(SortKey::Kind),
451 _ => Err(FsError::InvalidFormat {
452 format: format!("Invalid sort key: {}", s),
453 }),
454 }
455}
456
457pub fn parse_sort_order(s: &str) -> Result<SortOrder> {
458 match s.to_lowercase().as_str() {
459 "asc" | "ascending" => Ok(SortOrder::Asc),
460 "desc" | "descending" => Ok(SortOrder::Desc),
461 _ => Err(FsError::InvalidFormat {
462 format: format!("Invalid sort order: {}", s),
463 }),
464 }
465}
466
467pub fn parse_entry_kinds(kinds: &[String]) -> Result<Vec<EntryKind>> {
468 kinds
469 .iter()
470 .map(|s| match s.to_lowercase().as_str() {
471 "file" => Ok(EntryKind::File),
472 "dir" | "directory" => Ok(EntryKind::Dir),
473 "symlink" | "link" => Ok(EntryKind::Symlink),
474 _ => Err(FsError::InvalidFormat {
475 format: format!("Invalid kind: {}", s),
476 }),
477 })
478 .collect()
479}
480
481pub fn determine_sort_order(_asc: bool, desc: bool) -> SortOrder {
482 if desc {
483 SortOrder::Desc
484 } else {
485 SortOrder::Asc
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492
493 #[test]
494 fn test_parse_sort_key() {
495 assert!(matches!(parse_sort_key("name").unwrap(), SortKey::Name));
496 assert!(matches!(parse_sort_key("size").unwrap(), SortKey::Size));
497 assert!(matches!(parse_sort_key("mtime").unwrap(), SortKey::Mtime));
498 assert!(parse_sort_key("invalid").is_err());
499 }
500
501 #[test]
502 fn test_parse_sort_order() {
503 assert!(matches!(parse_sort_order("asc").unwrap(), SortOrder::Asc));
504 assert!(matches!(parse_sort_order("desc").unwrap(), SortOrder::Desc));
505 assert!(matches!(
506 parse_sort_order("ascending").unwrap(),
507 SortOrder::Asc
508 ));
509 assert!(parse_sort_order("invalid").is_err());
510 }
511
512 #[test]
513 fn test_parse_entry_kinds() {
514 let kinds = parse_entry_kinds(&["file".to_string(), "dir".to_string()]).unwrap();
515 assert_eq!(kinds.len(), 2);
516 assert!(kinds.contains(&EntryKind::File));
517 assert!(kinds.contains(&EntryKind::Dir));
518 }
519
520 #[test]
521 fn test_determine_sort_order() {
522 assert!(matches!(determine_sort_order(false, false), SortOrder::Asc));
523 assert!(matches!(determine_sort_order(false, true), SortOrder::Desc));
524 assert!(matches!(determine_sort_order(true, false), SortOrder::Asc));
525 }
526}