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 (also auto when stdout is not a TTY)
127    #[arg(long = "gnu", action = ArgAction::SetTrue)]
128    pub gnu: bool,
129
130    /// Force modern product behavior even when stdout is not a TTY
131    /// (disables auto script-safe / GNU mode for pipes)
132    #[arg(long = "no-gnu", action = ArgAction::SetTrue, conflicts_with = "gnu")]
133    pub no_gnu: bool,
134
135    /// Show file icons (auto/always/never; default: auto — TTY only, off under --gnu)
136    #[arg(
137        long = "icons",
138        value_name = "WHEN",
139        default_value = "auto",
140        num_args = 0..=1,
141        default_missing_value = "always",
142        require_equals = true
143    )]
144    pub icons: IconsArg,
145
146    /// Append indicator (one of */=@|) to entries WHEN (GNU: `-F`, `--classify[=WHEN]`)
147    #[arg(
148        short = 'F',
149        long = "classify",
150        value_name = "WHEN",
151        num_args = 0..=1,
152        default_missing_value = "always",
153        require_equals = true
154    )]
155    pub classify: Option<ColorArg>,
156
157    /// Append / indicator to directories
158    #[arg(short = 'p')]
159    pub indicator_slash: bool,
160
161    /// Like -F, except do not append '*'
162    #[arg(long = "file-type")]
163    pub file_type: bool,
164
165    /// Append indicator with style WORD: none, slash, file-type, classify
166    #[arg(long = "indicator-style", value_name = "WORD")]
167    pub indicator_style: Option<String>,
168
169    /// Group directories before files
170    #[arg(long = "group-directories-first", alias = "dirs-first")]
171    pub dirs_first: bool,
172
173    /// List directories themselves, not their contents
174    #[arg(short = 'd', long = "directory")]
175    pub directory: bool,
176
177    /// Do not list implied entries ending with ~
178    #[arg(short = 'B', long = "ignore-backups")]
179    pub ignore_backups: bool,
180
181    /// Do not list implied entries matching shell PATTERN (repeatable)
182    #[arg(short = 'I', long = "ignore", value_name = "PATTERN", action = ArgAction::Append)]
183    pub ignore: Vec<String>,
184
185    /// Do not list implied entries matching shell PATTERN (unless -a/-A)
186    #[arg(long = "hide", value_name = "PATTERN", action = ArgAction::Append)]
187    pub hide: Vec<String>,
188
189    /// When showing file information for a symbolic link, show info for the file it references
190    #[arg(short = 'L', long = "dereference")]
191    pub dereference: bool,
192
193    /// Follow symbolic links listed on the command line
194    #[arg(short = 'H', long = "dereference-command-line")]
195    pub dereference_command_line: bool,
196
197    /// Follow each command line symbolic link that points to a directory
198    #[arg(long = "dereference-command-line-symlink-to-dir")]
199    pub dereference_command_line_symlink_to_dir: bool,
200
201    /// Like -l, but do not list owner
202    #[arg(short = 'g')]
203    pub no_owner: bool,
204
205    /// Like -l, but do not list group information
206    #[arg(short = 'o')]
207    pub no_group_long: bool,
208
209    /// In a long listing, don't print group names
210    #[arg(short = 'G', long = "no-group")]
211    pub no_group: bool,
212
213    /// Like -l, but list numeric user and group IDs
214    #[arg(short = 'n', long = "numeric-uid-gid")]
215    pub numeric_uid_gid: bool,
216
217    /// Print the index number of each file
218    #[arg(short = 'i', long = "inode")]
219    pub inode: bool,
220
221    /// Print the allocated size of each file, in blocks
222    #[arg(short = 's', long = "size")]
223    pub size_blocks: bool,
224
225    /// Default to 1024-byte blocks for filesystem disk usage (`-s`)
226    #[arg(short = 'k', long = "kibibytes")]
227    pub kibibytes: bool,
228
229    /// Scale sizes by SIZE before printing (e.g. 1K, 1M, KB, MB)
230    #[arg(long = "block-size", value_name = "SIZE")]
231    pub block_size: Option<String>,
232
233    /// Like -l --time-style=full-iso
234    #[arg(long = "full-time")]
235    pub full_time: bool,
236
237    /// Time/date format with -l; also TIME_STYLE env
238    #[arg(long = "time-style", value_name = "TIME_STYLE")]
239    pub time_style: Option<String>,
240
241    /// Print ? instead of nongraphic characters
242    #[arg(short = 'q', long = "hide-control-chars")]
243    pub hide_control_chars: bool,
244
245    /// Show nongraphic characters as-is (the default unless -q)
246    #[arg(long = "show-control-chars")]
247    pub show_control_chars: bool,
248
249    /// Print C-style escapes for nongraphic characters
250    #[arg(short = 'b', long = "escape")]
251    pub escape: bool,
252
253    /// Enclose entry names in double quotes
254    #[arg(short = 'Q', long = "quote-name")]
255    pub quote_name: bool,
256
257    /// Print entry names without quoting
258    #[arg(short = 'N', long = "literal")]
259    pub literal: bool,
260
261    /// Use quoting style WORD for entry names
262    #[arg(long = "quoting-style", value_name = "WORD")]
263    pub quoting_style: Option<String>,
264
265    /// List entries by lines instead of by columns (row-major)
266    #[arg(short = 'x')]
267    pub across: bool,
268
269    /// Same as -a -U (and disable decorations in GNU mode)
270    #[arg(short = 'f')]
271    pub unsorted_all: bool,
272
273    /// Across/commas/long/single-column/vertical
274    #[arg(long = "format", value_name = "WORD")]
275    pub format: Option<String>,
276
277    /// Assume tab stops at each COLS instead of 8 (stored for layout)
278    #[arg(short = 'T', long = "tabsize", value_name = "COLS")]
279    pub tabsize: Option<usize>,
280
281    /// Set output width to COLS; 0 means no limit
282    #[arg(short = 'w', long = "width", value_name = "COLS")]
283    pub width: Option<usize>,
284
285    /// Print any security context of each file (SELinux)
286    #[arg(short = 'Z', long = "context")]
287    pub context: bool,
288
289    /// End each output line with NUL, not newline
290    #[arg(long = "zero")]
291    pub zero: bool,
292
293    /// Generate output designed for Emacs' dired mode
294    #[arg(short = 'D', long = "dired")]
295    pub dired: bool,
296
297    /// With -l, print the author of each file
298    #[arg(long = "author")]
299    pub author: bool,
300
301    /// Hyperlink file names WHEN (GNU: `--hyperlink[=WHEN]`; same synonyms as `--color`)
302    #[arg(
303        long = "hyperlink",
304        value_name = "WHEN",
305        num_args = 0..=1,
306        default_missing_value = "always",
307        require_equals = true
308    )]
309    pub hyperlink: Option<ColorArg>,
310
311    /// Maximum recursion depth (with -R / --tree)
312    #[arg(long = "max-depth", value_name = "N")]
313    pub max_depth: Option<usize>,
314
315    /// Annotate with git status (requires feature `git`)
316    #[arg(long = "git", default_value_t = true, action = clap::ArgAction::Set)]
317    pub git: bool,
318
319    /// Path to TOML config file (overrides default search path)
320    #[arg(long = "config", value_name = "PATH")]
321    pub config: Option<PathBuf>,
322
323    /// Interactive TUI directory browser (prefer `f00-tui`; embed with feature `tui`)
324    #[arg(long = "browse", visible_alias = "tui")]
325    pub browse: bool,
326
327    /// Honor `.gitignore` / `.f00ignore` when listing
328    #[arg(long = "ignore-files")]
329    pub ignore_files: bool,
330
331    /// Do not honor ignore files (overrides --ignore-files)
332    #[arg(long = "no-ignore")]
333    pub no_ignore: bool,
334
335    /// Auto-list zip/tar archives as directories (default: true; off under --gnu)
336    #[arg(long = "archive", default_value_t = true, action = clap::ArgAction::Set)]
337    pub archive: bool,
338
339    /// Parallel metadata threads: `0` = auto (rayon default), `1` = serial, `N>1` = fixed pool
340    #[arg(long = "threads", value_name = "N", default_value_t = 0)]
341    pub threads: usize,
342
343    /// Print phase timing breakdown to stderr (readdir/stat/sort/format/total)
344    #[arg(long = "profile")]
345    pub profile: bool,
346
347    /// Use io_uring batch metadata when built with `--features io-uring` (Linux)
348    #[arg(long = "io-uring", action = ArgAction::Set, default_value_t = true)]
349    pub io_uring: bool,
350
351    /// Generate shell completions to stdout and exit (`bash`, `zsh`, `fish`, `powershell`, `elvish`)
352    #[arg(long = "generate-completions", value_name = "SHELL", hide = true)]
353    pub generate_completions: Option<clap_complete::Shell>,
354
355    /// Generate a man page to stdout and exit
356    #[arg(long = "generate-man", hide = true, action = ArgAction::SetTrue)]
357    pub generate_man: bool,
358
359    /// Update f00 from the latest GitHub Release (checksum verified)
360    #[arg(long = "update", action = ArgAction::SetTrue)]
361    pub update: bool,
362
363    /// Check whether a newer release is available (no mutation); exit 1 if behind
364    #[arg(long = "check-update", action = ArgAction::SetTrue)]
365    pub check_update: bool,
366
367    /// List loaded plugins (requires feature `plugins`)
368    #[arg(long = "list-plugins", action = ArgAction::SetTrue)]
369    pub list_plugins: bool,
370}
371
372impl Args {
373    /// Minimal args for tests (all flags default/off).
374    pub fn test_default() -> Self {
375        Self {
376            paths: vec![],
377            help: None,
378            all: false,
379            almost_all: false,
380            long: false,
381            one_per_line: false,
382            columns: false,
383            commas: false,
384            human_readable: false,
385            si: false,
386            recursive: false,
387            reverse: false,
388            sort_time: false,
389            sort_size: false,
390            sort_extension: false,
391            sort_version: false,
392            sort_none: false,
393            sort: None,
394            time: None,
395            access_time: false,
396            change_time: false,
397            color: ColorArg::Auto,
398            json: false,
399            csv: false,
400            tsv: false,
401            tree: false,
402            gnu: false,
403            no_gnu: false,
404            icons: IconsArg::Auto,
405            classify: None,
406            indicator_slash: false,
407            file_type: false,
408            indicator_style: None,
409            dirs_first: false,
410            directory: false,
411            ignore_backups: false,
412            ignore: vec![],
413            hide: vec![],
414            dereference: false,
415            dereference_command_line: false,
416            dereference_command_line_symlink_to_dir: false,
417            no_owner: false,
418            no_group_long: false,
419            no_group: false,
420            numeric_uid_gid: false,
421            inode: false,
422            size_blocks: false,
423            kibibytes: false,
424            block_size: None,
425            full_time: false,
426            time_style: None,
427            hide_control_chars: false,
428            show_control_chars: false,
429            escape: false,
430            quote_name: false,
431            literal: false,
432            quoting_style: None,
433            across: false,
434            unsorted_all: false,
435            format: None,
436            tabsize: None,
437            width: None,
438            context: false,
439            zero: false,
440            dired: false,
441            author: false,
442            hyperlink: None,
443            max_depth: None,
444            git: false,
445            config: None,
446            browse: false,
447            ignore_files: false,
448            no_ignore: false,
449            archive: true,
450            threads: 0,
451            profile: false,
452            io_uring: true,
453            generate_completions: None,
454            generate_man: false,
455            update: false,
456            check_update: false,
457            list_plugins: false,
458        }
459    }
460}
461
462#[derive(Debug, Clone, Copy, Default, ValueEnum)]
463pub enum ColorArg {
464    /// Color when stdout is a TTY.
465    ///
466    /// GNU coreutils synonyms: `tty`, `if-tty` (NixOS injects `ls --color=tty`).
467    #[default]
468    #[value(alias = "tty", alias = "if-tty")]
469    Auto,
470    /// Always color (GNU: `always`, `yes`, `force`).
471    #[value(alias = "yes", alias = "force", alias = "on", alias = "true")]
472    Always,
473    /// Never color (GNU: `never`, `no`, `none`).
474    #[value(alias = "no", alias = "none", alias = "off", alias = "false")]
475    Never,
476}
477
478impl From<ColorArg> for f00_core::ColorWhen {
479    fn from(value: ColorArg) -> Self {
480        match value {
481            ColorArg::Auto => Self::Auto,
482            ColorArg::Always => Self::Always,
483            ColorArg::Never => Self::Never,
484        }
485    }
486}
487
488/// When to show file-type icons (mirrors [`f00_core::IconsWhen`]).
489#[derive(Debug, Clone, Copy, Default, ValueEnum, PartialEq, Eq)]
490pub enum IconsArg {
491    #[default]
492    #[value(alias = "tty", alias = "if-tty")]
493    Auto,
494    #[value(alias = "yes", alias = "force", alias = "on", alias = "true")]
495    Always,
496    #[value(alias = "no", alias = "none", alias = "off", alias = "false")]
497    Never,
498}
499
500impl From<IconsArg> for f00_core::IconsWhen {
501    fn from(value: IconsArg) -> Self {
502        match value {
503            IconsArg::Auto => Self::Auto,
504            IconsArg::Always => Self::Always,
505            IconsArg::Never => Self::Never,
506        }
507    }
508}
509
510impl From<f00_core::IconsWhen> for IconsArg {
511    fn from(value: f00_core::IconsWhen) -> Self {
512        match value {
513            f00_core::IconsWhen::Auto => Self::Auto,
514            f00_core::IconsWhen::Always => Self::Always,
515            f00_core::IconsWhen::Never => Self::Never,
516        }
517    }
518}