Skip to main content

f00_cli/
cli.rs

1use std::path::PathBuf;
2
3use clap::{ArgAction, Parser, ValueEnum};
4
5/// f00 — a modern, friendly directory lister
6#[derive(Debug, Clone, Parser)]
7#[command(
8    name = "f00",
9    version,
10    about = "List directory contents with a friendly default UI",
11    long_about = None,
12    // Free `-h` for human-readable (GNU ls style); keep `--help`.
13    disable_help_flag = true
14)]
15pub struct Args {
16    /// Paths to list (default: current directory)
17    pub paths: Vec<PathBuf>,
18
19    /// Print help
20    #[arg(long = "help", action = ArgAction::Help, help = "Print help")]
21    pub help: Option<bool>,
22
23    /// Do not ignore entries starting with `.`
24    #[arg(short = 'a', long = "all")]
25    pub all: bool,
26
27    /// Do not list implied `.` and `..` (show other hidden files)
28    #[arg(short = 'A', long = "almost-all")]
29    pub almost_all: bool,
30
31    /// Use a long listing format
32    #[arg(short = 'l')]
33    pub long: bool,
34
35    /// List one file per line
36    #[arg(short = '1')]
37    pub one_per_line: bool,
38
39    /// List entries by columns
40    #[arg(short = 'C')]
41    pub columns: bool,
42
43    /// Fill width with a comma separated list of entries
44    #[arg(short = 'm')]
45    pub commas: bool,
46
47    /// With -l and -s, print sizes like 1K 234M 2G etc.
48    #[arg(short = 'h', long = "human-readable")]
49    pub human_readable: bool,
50
51    /// Likewise, but use powers of 1000 not 1024
52    #[arg(long = "si")]
53    pub si: bool,
54
55    /// List subdirectories recursively
56    #[arg(short = 'R', long = "recursive")]
57    pub recursive: bool,
58
59    /// Reverse sort order
60    #[arg(short = 'r', long = "reverse")]
61    pub reverse: bool,
62
63    /// Sort by time, newest first
64    #[arg(short = 't')]
65    pub sort_time: bool,
66
67    /// Sort by file size, largest first
68    #[arg(short = 'S')]
69    pub sort_size: bool,
70
71    /// Sort alphabetically by entry extension
72    #[arg(short = 'X')]
73    pub sort_extension: bool,
74
75    /// Natural sort of (version) numbers within text (`strverscmp`)
76    #[arg(short = 'v')]
77    pub sort_version: bool,
78
79    /// Do not sort; list entries in directory order
80    #[arg(short = 'U')]
81    pub sort_none: bool,
82
83    /// Sort by WORD (name, size, time, extension, version, none)
84    #[arg(long = "sort", value_name = "WORD")]
85    pub sort: Option<String>,
86
87    /// Use time WORD for display/sort: mtime, atime, ctime, birth
88    #[arg(long = "time", value_name = "WORD")]
89    pub time: Option<String>,
90
91    /// Sort by, and show, access time
92    #[arg(short = 'u')]
93    pub access_time: bool,
94
95    /// Sort by, and show, ctime (status change) when possible
96    #[arg(short = 'c')]
97    pub change_time: bool,
98
99    /// Colorize output
100    #[arg(
101        long = "color",
102        value_name = "WHEN",
103        default_value = "auto",
104        num_args = 0..=1,
105        default_missing_value = "always",
106        require_equals = true
107    )]
108    pub color: ColorArg,
109
110    /// Emit structured JSON (`-j` is free: GNU ls has no short `-j`)
111    #[arg(short = 'j', long = "json")]
112    pub json: bool,
113
114    /// Emit CSV
115    #[arg(long = "csv")]
116    pub csv: bool,
117
118    /// Emit TSV
119    #[arg(long = "tsv")]
120    pub tsv: bool,
121
122    /// Show entries as a tree
123    #[arg(long = "tree")]
124    pub tree: bool,
125
126    /// Stricter GNU ls-compatible behavior
127    #[arg(long = "gnu")]
128    pub gnu: bool,
129
130    /// Show file icons (auto/always/never; default: auto — TTY only, off under --gnu)
131    #[arg(
132        long = "icons",
133        value_name = "WHEN",
134        default_value = "auto",
135        num_args = 0..=1,
136        default_missing_value = "always",
137        require_equals = true
138    )]
139    pub icons: IconsArg,
140
141    /// Append indicator (one of */=@|) to entries WHEN (GNU: `-F`, `--classify[=WHEN]`)
142    #[arg(
143        short = 'F',
144        long = "classify",
145        value_name = "WHEN",
146        num_args = 0..=1,
147        default_missing_value = "always",
148        require_equals = true
149    )]
150    pub classify: Option<ColorArg>,
151
152    /// Append / indicator to directories
153    #[arg(short = 'p')]
154    pub indicator_slash: bool,
155
156    /// Like -F, except do not append '*'
157    #[arg(long = "file-type")]
158    pub file_type: bool,
159
160    /// Append indicator with style WORD: none, slash, file-type, classify
161    #[arg(long = "indicator-style", value_name = "WORD")]
162    pub indicator_style: Option<String>,
163
164    /// Group directories before files
165    #[arg(long = "group-directories-first", alias = "dirs-first")]
166    pub dirs_first: bool,
167
168    /// List directories themselves, not their contents
169    #[arg(short = 'd', long = "directory")]
170    pub directory: bool,
171
172    /// Do not list implied entries ending with ~
173    #[arg(short = 'B', long = "ignore-backups")]
174    pub ignore_backups: bool,
175
176    /// Do not list implied entries matching shell PATTERN (repeatable)
177    #[arg(short = 'I', long = "ignore", value_name = "PATTERN", action = ArgAction::Append)]
178    pub ignore: Vec<String>,
179
180    /// Do not list implied entries matching shell PATTERN (unless -a/-A)
181    #[arg(long = "hide", value_name = "PATTERN", action = ArgAction::Append)]
182    pub hide: Vec<String>,
183
184    /// When showing file information for a symbolic link, show info for the file it references
185    #[arg(short = 'L', long = "dereference")]
186    pub dereference: bool,
187
188    /// Follow symbolic links listed on the command line
189    #[arg(short = 'H', long = "dereference-command-line")]
190    pub dereference_command_line: bool,
191
192    /// Follow each command line symbolic link that points to a directory
193    #[arg(long = "dereference-command-line-symlink-to-dir")]
194    pub dereference_command_line_symlink_to_dir: bool,
195
196    /// Like -l, but do not list owner
197    #[arg(short = 'g')]
198    pub no_owner: bool,
199
200    /// Like -l, but do not list group information
201    #[arg(short = 'o')]
202    pub no_group_long: bool,
203
204    /// In a long listing, don't print group names
205    #[arg(short = 'G', long = "no-group")]
206    pub no_group: bool,
207
208    /// Like -l, but list numeric user and group IDs
209    #[arg(short = 'n', long = "numeric-uid-gid")]
210    pub numeric_uid_gid: bool,
211
212    /// Print the index number of each file
213    #[arg(short = 'i', long = "inode")]
214    pub inode: bool,
215
216    /// Print the allocated size of each file, in blocks
217    #[arg(short = 's', long = "size")]
218    pub size_blocks: bool,
219
220    /// Default to 1024-byte blocks for filesystem disk usage (`-s`)
221    #[arg(short = 'k', long = "kibibytes")]
222    pub kibibytes: bool,
223
224    /// Scale sizes by SIZE before printing (e.g. 1K, 1M, KB, MB)
225    #[arg(long = "block-size", value_name = "SIZE")]
226    pub block_size: Option<String>,
227
228    /// Like -l --time-style=full-iso
229    #[arg(long = "full-time")]
230    pub full_time: bool,
231
232    /// Time/date format with -l; also TIME_STYLE env
233    #[arg(long = "time-style", value_name = "TIME_STYLE")]
234    pub time_style: Option<String>,
235
236    /// Print ? instead of nongraphic characters
237    #[arg(short = 'q', long = "hide-control-chars")]
238    pub hide_control_chars: bool,
239
240    /// Show nongraphic characters as-is (the default unless -q)
241    #[arg(long = "show-control-chars")]
242    pub show_control_chars: bool,
243
244    /// Print C-style escapes for nongraphic characters
245    #[arg(short = 'b', long = "escape")]
246    pub escape: bool,
247
248    /// Enclose entry names in double quotes
249    #[arg(short = 'Q', long = "quote-name")]
250    pub quote_name: bool,
251
252    /// Print entry names without quoting
253    #[arg(short = 'N', long = "literal")]
254    pub literal: bool,
255
256    /// Use quoting style WORD for entry names
257    #[arg(long = "quoting-style", value_name = "WORD")]
258    pub quoting_style: Option<String>,
259
260    /// List entries by lines instead of by columns (row-major)
261    #[arg(short = 'x')]
262    pub across: bool,
263
264    /// Same as -a -U (and disable decorations in GNU mode)
265    #[arg(short = 'f')]
266    pub unsorted_all: bool,
267
268    /// Across/commas/long/single-column/vertical
269    #[arg(long = "format", value_name = "WORD")]
270    pub format: Option<String>,
271
272    /// Assume tab stops at each COLS instead of 8 (stored for layout)
273    #[arg(short = 'T', long = "tabsize", value_name = "COLS")]
274    pub tabsize: Option<usize>,
275
276    /// Set output width to COLS; 0 means no limit
277    #[arg(short = 'w', long = "width", value_name = "COLS")]
278    pub width: Option<usize>,
279
280    /// Print any security context of each file (SELinux)
281    #[arg(short = 'Z', long = "context")]
282    pub context: bool,
283
284    /// End each output line with NUL, not newline
285    #[arg(long = "zero")]
286    pub zero: bool,
287
288    /// Generate output designed for Emacs' dired mode
289    #[arg(short = 'D', long = "dired")]
290    pub dired: bool,
291
292    /// With -l, print the author of each file
293    #[arg(long = "author")]
294    pub author: bool,
295
296    /// Hyperlink file names WHEN (GNU: `--hyperlink[=WHEN]`; same synonyms as `--color`)
297    #[arg(
298        long = "hyperlink",
299        value_name = "WHEN",
300        num_args = 0..=1,
301        default_missing_value = "always",
302        require_equals = true
303    )]
304    pub hyperlink: Option<ColorArg>,
305
306    /// Maximum recursion depth (with -R / --tree)
307    #[arg(long = "max-depth", value_name = "N")]
308    pub max_depth: Option<usize>,
309
310    /// Annotate with git status (requires feature `git`)
311    #[arg(long = "git", default_value_t = true, action = clap::ArgAction::Set)]
312    pub git: bool,
313
314    /// Path to TOML config file (overrides default search path)
315    #[arg(long = "config", value_name = "PATH")]
316    pub config: Option<PathBuf>,
317
318    /// Interactive TUI directory browser (requires feature `tui`)
319    #[arg(long = "browse", visible_alias = "tui")]
320    pub browse: bool,
321
322    /// Honor `.gitignore` / `.f00ignore` when listing
323    #[arg(long = "ignore-files")]
324    pub ignore_files: bool,
325
326    /// Do not honor ignore files (overrides --ignore-files)
327    #[arg(long = "no-ignore")]
328    pub no_ignore: bool,
329
330    /// Auto-list zip/tar archives as directories (default: true; off under --gnu)
331    #[arg(long = "archive", default_value_t = true, action = clap::ArgAction::Set)]
332    pub archive: bool,
333
334    /// Parallel metadata threads: `0` = auto (rayon default), `1` = serial, `N>1` = fixed pool
335    #[arg(long = "threads", value_name = "N", default_value_t = 0)]
336    pub threads: usize,
337
338    /// Print phase timing breakdown to stderr (readdir/stat/sort/format/total)
339    #[arg(long = "profile")]
340    pub profile: bool,
341
342    /// Use io_uring batch metadata when built with `--features io-uring` (Linux)
343    #[arg(long = "io-uring", action = ArgAction::Set, default_value_t = true)]
344    pub io_uring: bool,
345
346    /// Generate shell completions to stdout and exit (`bash`, `zsh`, `fish`, `powershell`, `elvish`)
347    #[arg(long = "generate-completions", value_name = "SHELL", hide = true)]
348    pub generate_completions: Option<clap_complete::Shell>,
349
350    /// Generate a man page to stdout and exit
351    #[arg(long = "generate-man", hide = true, action = ArgAction::SetTrue)]
352    pub generate_man: bool,
353
354    /// Update f00 from the latest GitHub Release (checksum verified)
355    #[arg(long = "update", action = ArgAction::SetTrue)]
356    pub update: bool,
357
358    /// Check whether a newer release is available (no mutation); exit 1 if behind
359    #[arg(long = "check-update", action = ArgAction::SetTrue)]
360    pub check_update: bool,
361
362    /// List loaded plugins (requires feature `plugins`)
363    #[arg(long = "list-plugins", action = ArgAction::SetTrue)]
364    pub list_plugins: bool,
365}
366
367impl Args {
368    /// Minimal args for tests (all flags default/off).
369    pub fn test_default() -> Self {
370        Self {
371            paths: vec![],
372            help: None,
373            all: false,
374            almost_all: false,
375            long: false,
376            one_per_line: false,
377            columns: false,
378            commas: false,
379            human_readable: false,
380            si: false,
381            recursive: false,
382            reverse: false,
383            sort_time: false,
384            sort_size: false,
385            sort_extension: false,
386            sort_version: false,
387            sort_none: false,
388            sort: None,
389            time: None,
390            access_time: false,
391            change_time: false,
392            color: ColorArg::Auto,
393            json: false,
394            csv: false,
395            tsv: false,
396            tree: false,
397            gnu: false,
398            icons: IconsArg::Auto,
399            classify: None,
400            indicator_slash: false,
401            file_type: false,
402            indicator_style: None,
403            dirs_first: false,
404            directory: false,
405            ignore_backups: false,
406            ignore: vec![],
407            hide: vec![],
408            dereference: false,
409            dereference_command_line: false,
410            dereference_command_line_symlink_to_dir: false,
411            no_owner: false,
412            no_group_long: false,
413            no_group: false,
414            numeric_uid_gid: false,
415            inode: false,
416            size_blocks: false,
417            kibibytes: false,
418            block_size: None,
419            full_time: false,
420            time_style: None,
421            hide_control_chars: false,
422            show_control_chars: false,
423            escape: false,
424            quote_name: false,
425            literal: false,
426            quoting_style: None,
427            across: false,
428            unsorted_all: false,
429            format: None,
430            tabsize: None,
431            width: None,
432            context: false,
433            zero: false,
434            dired: false,
435            author: false,
436            hyperlink: None,
437            max_depth: None,
438            git: false,
439            config: None,
440            browse: false,
441            ignore_files: false,
442            no_ignore: false,
443            archive: true,
444            threads: 0,
445            profile: false,
446            io_uring: true,
447            generate_completions: None,
448            generate_man: false,
449            update: false,
450            check_update: false,
451            list_plugins: false,
452        }
453    }
454}
455
456#[derive(Debug, Clone, Copy, Default, ValueEnum)]
457pub enum ColorArg {
458    /// Color when stdout is a TTY.
459    ///
460    /// GNU coreutils synonyms: `tty`, `if-tty` (NixOS injects `ls --color=tty`).
461    #[default]
462    #[value(alias = "tty", alias = "if-tty")]
463    Auto,
464    /// Always color (GNU: `always`, `yes`, `force`).
465    #[value(alias = "yes", alias = "force", alias = "on", alias = "true")]
466    Always,
467    /// Never color (GNU: `never`, `no`, `none`).
468    #[value(alias = "no", alias = "none", alias = "off", alias = "false")]
469    Never,
470}
471
472impl From<ColorArg> for f00_core::ColorWhen {
473    fn from(value: ColorArg) -> Self {
474        match value {
475            ColorArg::Auto => Self::Auto,
476            ColorArg::Always => Self::Always,
477            ColorArg::Never => Self::Never,
478        }
479    }
480}
481
482/// When to show file-type icons (mirrors [`f00_core::IconsWhen`]).
483#[derive(Debug, Clone, Copy, Default, ValueEnum, PartialEq, Eq)]
484pub enum IconsArg {
485    #[default]
486    #[value(alias = "tty", alias = "if-tty")]
487    Auto,
488    #[value(alias = "yes", alias = "force", alias = "on", alias = "true")]
489    Always,
490    #[value(alias = "no", alias = "none", alias = "off", alias = "false")]
491    Never,
492}
493
494impl From<IconsArg> for f00_core::IconsWhen {
495    fn from(value: IconsArg) -> Self {
496        match value {
497            IconsArg::Auto => Self::Auto,
498            IconsArg::Always => Self::Always,
499            IconsArg::Never => Self::Never,
500        }
501    }
502}
503
504impl From<f00_core::IconsWhen> for IconsArg {
505    fn from(value: f00_core::IconsWhen) -> Self {
506        match value {
507            f00_core::IconsWhen::Auto => Self::Auto,
508            f00_core::IconsWhen::Always => Self::Always,
509            f00_core::IconsWhen::Never => Self::Never,
510        }
511    }
512}