Skip to main content

f00_cli/
cli.rs

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