Skip to main content

f00_core/
options.rs

1use crate::entry::TimeField;
2
3/// How entries should be ordered.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5pub enum SortBy {
6    #[default]
7    Name,
8    /// Largest first (GNU `ls -S`).
9    Size,
10    /// Newest first (GNU `ls -t`).
11    Time,
12    Extension,
13    /// Natural / version sort (`-v`, strverscmp-like).
14    Version,
15    /// Shortest display width first (GNU `--sort=width`).
16    Width,
17    /// Directory order / no sort (`-U`).
18    None,
19}
20
21/// Color when to emit ANSI sequences.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum ColorWhen {
24    #[default]
25    Auto,
26    Always,
27    Never,
28}
29
30impl ColorWhen {
31    /// Parse GNU `ls` WHEN tokens used by `--color`, `--classify`, `--hyperlink`.
32    pub fn parse(s: &str) -> Option<Self> {
33        match s.to_ascii_lowercase().as_str() {
34            "auto" | "tty" | "if-tty" => Some(Self::Auto),
35            "always" | "yes" | "force" | "true" | "on" => Some(Self::Always),
36            "never" | "no" | "none" | "false" | "off" => Some(Self::Never),
37            _ => None,
38        }
39    }
40
41    pub fn enabled(self, is_tty: bool) -> bool {
42        match self {
43            Self::Always => true,
44            Self::Never => false,
45            Self::Auto => is_tty,
46        }
47    }
48}
49
50/// When to show file-type icons (`--icons=WHEN`).
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
52pub enum IconsWhen {
53    #[default]
54    Auto,
55    Always,
56    Never,
57}
58
59impl IconsWhen {
60    pub fn parse(s: &str) -> Option<Self> {
61        match s.to_ascii_lowercase().as_str() {
62            "auto" | "tty" | "if-tty" => Some(Self::Auto),
63            "always" | "yes" | "force" | "true" | "on" => Some(Self::Always),
64            "never" | "no" | "none" | "false" | "off" => Some(Self::Never),
65            _ => None,
66        }
67    }
68
69    /// Icons on for `Always`, off for `Never`, and for `Auto` only when `is_tty`.
70    pub fn enabled(self, is_tty: bool) -> bool {
71        match self {
72            Self::Always => true,
73            Self::Never => false,
74            Self::Auto => is_tty,
75        }
76    }
77}
78
79/// How to present the listing.
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
81pub enum OutputMode {
82    /// Multi-column when TTY, otherwise one-per-line.
83    #[default]
84    Default,
85    /// Force multi-column column-major (`-C`).
86    Columns,
87    /// Multi-column row-major (`-x` / `--format=across`).
88    Across,
89    /// Force one entry per line (`-1`).
90    OnePerLine,
91    /// Long listing (`-l`).
92    Long,
93    /// Comma-separated (`-m`).
94    Commas,
95    /// JSON array of entries.
96    Json,
97    /// Tree view.
98    Tree,
99    /// CSV rows.
100    Csv,
101    /// TSV rows.
102    Tsv,
103}
104
105/// Indicator style (`ls -F` / `-p` / `--indicator-style`).
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
107pub enum IndicatorStyle {
108    #[default]
109    None,
110    /// Append `/` to directories only (`-p`).
111    Slash,
112    /// Classify (`-F`): `*/=@|`.
113    Classify,
114    /// Like classify without `*` (`--file-type`).
115    FileType,
116}
117
118impl IndicatorStyle {
119    pub fn parse(s: &str) -> Option<Self> {
120        match s.to_ascii_lowercase().as_str() {
121            "none" => Some(Self::None),
122            "slash" => Some(Self::Slash),
123            "file-type" | "filetype" => Some(Self::FileType),
124            "classify" => Some(Self::Classify),
125            _ => None,
126        }
127    }
128}
129
130/// Filename quoting style (GNU `--quoting-style`).
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
132pub enum QuotingStyle {
133    /// No quoting (`-N` / `literal`).
134    #[default]
135    Literal,
136    /// Locale-aware (treated like shell-escape for nongraphic).
137    Locale,
138    /// Quote when needed for the shell.
139    Shell,
140    /// Always single-quote.
141    ShellAlways,
142    /// Shell quoting with `$''` escapes for nongraphic.
143    ShellEscape,
144    /// Always use shell-escape style.
145    ShellEscapeAlways,
146    /// Double quotes with C escapes (`-Q` / `c`).
147    C,
148    /// C escapes without surrounding quotes (`-b` / `escape`).
149    Escape,
150}
151
152impl QuotingStyle {
153    pub fn parse(s: &str) -> Option<Self> {
154        match s.to_ascii_lowercase().as_str() {
155            "literal" => Some(Self::Literal),
156            "locale" => Some(Self::Locale),
157            "shell" => Some(Self::Shell),
158            "shell-always" => Some(Self::ShellAlways),
159            "shell-escape" => Some(Self::ShellEscape),
160            "shell-escape-always" => Some(Self::ShellEscapeAlways),
161            "c" => Some(Self::C),
162            "escape" => Some(Self::Escape),
163            _ => None,
164        }
165    }
166
167    /// From `QUOTING_STYLE` env var.
168    pub fn from_env() -> Option<Self> {
169        std::env::var("QUOTING_STYLE")
170            .ok()
171            .as_deref()
172            .and_then(Self::parse)
173    }
174}
175
176/// Whether to hide nongraphic characters as `?` (`-q` / `--show-control-chars`).
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
178pub enum ControlChars {
179    /// Default: hide on TTY, show otherwise (GNU-ish).
180    #[default]
181    Auto,
182    /// Replace nongraphic with `?` (`-q`).
183    Hide,
184    /// Show control chars as-is (`--show-control-chars`).
185    Show,
186}
187
188/// Long-listing time style (`--time-style`).
189#[derive(Debug, Clone, PartialEq, Eq, Default)]
190pub enum TimeStyle {
191    /// Locale default (`Mon DD HH:MM` / `Mon DD  YYYY`).
192    #[default]
193    Locale,
194    /// `%Y-%m-%d %H:%M:%S.%N %z` (full-iso / `--full-time`).
195    FullIso,
196    /// `%Y-%m-%d %H:%M`.
197    LongIso,
198    /// Recent: `%m-%d %H:%M`; older: `%Y-%m-%d`.
199    Iso,
200    /// Custom strftime format (`+FORMAT`).
201    Format(String),
202}
203
204impl TimeStyle {
205    pub fn parse(s: &str) -> Option<Self> {
206        if let Some(fmt) = s.strip_prefix('+') {
207            return Some(Self::Format(fmt.to_string()));
208        }
209        match s.to_ascii_lowercase().as_str() {
210            "full-iso" | "full_iso" => Some(Self::FullIso),
211            "long-iso" | "long_iso" => Some(Self::LongIso),
212            "iso" => Some(Self::Iso),
213            "locale" => Some(Self::Locale),
214            _ => None,
215        }
216    }
217
218    pub fn from_env() -> Option<Self> {
219        std::env::var("TIME_STYLE")
220            .ok()
221            .as_deref()
222            .and_then(Self::parse)
223    }
224}
225
226/// Hyperlink (OSC 8) emission (`--hyperlink`).
227#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
228pub enum HyperlinkWhen {
229    #[default]
230    Never,
231    Auto,
232    Always,
233}
234
235impl HyperlinkWhen {
236    /// Same WHEN vocabulary as GNU `--color` / `--classify` / `--hyperlink`.
237    pub fn parse(s: &str) -> Option<Self> {
238        match s.to_ascii_lowercase().as_str() {
239            "auto" | "tty" | "if-tty" => Some(Self::Auto),
240            "always" | "yes" | "force" | "true" | "on" => Some(Self::Always),
241            "never" | "no" | "none" | "false" | "off" => Some(Self::Never),
242            _ => None,
243        }
244    }
245
246    pub fn enabled(self, is_tty: bool) -> bool {
247        match self {
248            Self::Always => true,
249            Self::Never => false,
250            Self::Auto => is_tty,
251        }
252    }
253}
254
255/// How to scale sizes / block counts (`--block-size`).
256#[derive(Debug, Clone, Copy, PartialEq, Eq)]
257pub enum BlockSize {
258    /// Human binary (`-h`): powers of 1024 with unit suffix.
259    HumanBinary,
260    /// Human SI (`--si`): powers of 1000.
261    HumanSi,
262    /// Fixed divisor in bytes (e.g. 1024 for 1K, 1000 for KB).
263    Bytes(u64),
264}
265
266impl Default for BlockSize {
267    fn default() -> Self {
268        // GNU default for long size field: 1-byte units.
269        Self::Bytes(1)
270    }
271}
272
273impl BlockSize {
274    /// Parse GNU-style SIZE: `K`, `M`, `G`, `T`, `KB`, `MB`, `1K`, `1024`, etc.
275    ///
276    /// - Binary suffixes without `B` (`K`/`M`/`G`/`T`/`P`/`E`) are powers of 1024.
277    /// - SI suffixes (`KB`/`MB`/…) are powers of 1000.
278    /// - Bare number is bytes.
279    pub fn parse(s: &str) -> Option<Self> {
280        let s = s.trim();
281        if s.is_empty() {
282            return None;
283        }
284        let lower = s.to_ascii_lowercase();
285        match lower.as_str() {
286            "human-readable" | "human" => return Some(Self::HumanBinary),
287            "si" => return Some(Self::HumanSi),
288            _ => {}
289        }
290
291        // Optional leading number, then optional unit.
292        let (num_part, unit_part) = split_size_parts(s)?;
293        let mult = if unit_part.is_empty() {
294            1u64
295        } else {
296            parse_size_unit(unit_part)?
297        };
298        let n: u64 = if num_part.is_empty() {
299            1
300        } else {
301            num_part.parse().ok()?
302        };
303        n.checked_mul(mult).map(Self::Bytes)
304    }
305
306    /// Divisor used when displaying allocated blocks (`-s`).
307    /// Human modes fall back to 1024 (kibibytes).
308    pub fn block_divisor(self) -> u64 {
309        match self {
310            Self::HumanBinary | Self::HumanSi => 1024,
311            Self::Bytes(n) => n.max(1),
312        }
313    }
314}
315
316fn split_size_parts(s: &str) -> Option<(&str, &str)> {
317    let bytes = s.as_bytes();
318    let mut i = 0;
319    while i < bytes.len() && bytes[i].is_ascii_digit() {
320        i += 1;
321    }
322    // Allow a leading number or a bare unit.
323    if i == 0 && !bytes[0].is_ascii_alphabetic() {
324        return None;
325    }
326    Some((&s[..i], &s[i..]))
327}
328
329fn parse_size_unit(unit: &str) -> Option<u64> {
330    let u = unit.to_ascii_lowercase();
331    // Strip optional trailing `ib` / `b` for forms like KiB, MiB, KB.
332    let (base, power_of_1000) = match u.as_str() {
333        "k" | "ki" | "kib" => (1024u64, false),
334        "m" | "mi" | "mib" => (1024u64.pow(2), false),
335        "g" | "gi" | "gib" => (1024u64.pow(3), false),
336        "t" | "ti" | "tib" => (1024u64.pow(4), false),
337        "p" | "pi" | "pib" => (1024u64.pow(5), false),
338        "e" | "ei" | "eib" => (1024u64.pow(6), false),
339        "kb" => (1000u64, true),
340        "mb" => (1000u64.pow(2), true),
341        "gb" => (1000u64.pow(3), true),
342        "tb" => (1000u64.pow(4), true),
343        "pb" => (1000u64.pow(5), true),
344        "eb" => (1000u64.pow(6), true),
345        "b" => (1u64, true),
346        _ => return None,
347    };
348    let _ = power_of_1000;
349    Some(base)
350}
351
352/// How to follow symlinks for command-line arguments.
353#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
354pub enum CliSymlinkMode {
355    /// Never follow CLI symlinks specially (use `follow_links` for all).
356    #[default]
357    Never,
358    /// Follow all CLI path symlinks (`-H`).
359    Always,
360    /// Follow CLI symlinks only when they resolve to a directory.
361    DirOnly,
362}
363
364/// Minimum number of directory children before parallel metadata is used.
365pub const PARALLEL_STAT_THRESHOLD: usize = 32;
366
367/// Options controlling which entries appear and how they are ordered.
368#[derive(Debug, Clone)]
369pub struct ListOptions {
370    pub all: bool,
371    pub almost_all: bool,
372    pub sort_by: SortBy,
373    pub reverse: bool,
374    pub dirs_first: bool,
375    pub recursive: bool,
376    pub max_depth: Option<usize>,
377    /// Stricter GNU-compatible behavior.
378    pub gnu_mode: bool,
379    /// Follow symlinks when stating (`-L`).
380    pub follow_links: bool,
381    /// List directories themselves, not contents (`-d`).
382    pub directory: bool,
383    /// Hide `*~` backup names (`-B`).
384    pub ignore_backups: bool,
385    /// Shell-style ignore patterns (`-I` / `--ignore`); always hidden.
386    pub ignore_patterns: Vec<String>,
387    /// Shell-style hide patterns (`--hide`); hidden unless `-a`/`-A`.
388    pub hide_patterns: Vec<String>,
389    /// When true, honor `.gitignore` / `.f00ignore` in listed directories.
390    pub use_ignore_files: bool,
391    /// When true, list inside zip/tar archives when a path is an archive file.
392    /// (Applied by callers that integrate `f00-archive`; core itself does not open archives.)
393    pub list_archives: bool,
394    /// Which timestamp to sort/display by.
395    pub time_field: TimeField,
396    /// How CLI path arguments that are symlinks are handled.
397    pub cli_symlink: CliSymlinkMode,
398    /// Parallelize metadata (`stat`) for large directories (rayon).
399    /// Forced off when [`Self::threads`] is `1`.
400    pub parallel: bool,
401    /// Rayon worker count: `0` = auto, `1` = force serial, `N>1` = fixed pool size.
402    pub threads: usize,
403    /// When true, fill [`crate::Listing::timing`] with phase durations.
404    pub collect_timing: bool,
405    /// Resolve uid/gid to owner/group names (expensive NSS). Off for short listings.
406    pub resolve_owner_group: bool,
407    /// Read SELinux context xattr (`-Z`). Off unless requested.
408    pub read_selinux: bool,
409    /// Prefer Linux `statx` for metadata when available (ignored on other OSes).
410    pub linux_statx: bool,
411    /// Prefer io_uring batch `statx` for large directories when the `io-uring`
412    /// cargo feature is enabled (Linux only; ignored otherwise).
413    pub io_uring: bool,
414    /// Emit synthetic directory section headers in recursive listings (`-R`).
415    /// Off for `--tree` (headers are not used and cost extra work).
416    pub emit_dir_headers: bool,
417}
418
419impl Default for ListOptions {
420    fn default() -> Self {
421        Self {
422            all: false,
423            almost_all: false,
424            sort_by: SortBy::Name,
425            reverse: false,
426            dirs_first: false,
427            recursive: false,
428            max_depth: None,
429            gnu_mode: false,
430            follow_links: false,
431            directory: false,
432            ignore_backups: false,
433            ignore_patterns: Vec::new(),
434            hide_patterns: Vec::new(),
435            use_ignore_files: false,
436            list_archives: true,
437            time_field: TimeField::Modified,
438            cli_symlink: CliSymlinkMode::Never,
439            parallel: true,
440            threads: 0,
441            collect_timing: false,
442            // Default: cheap path — names filled when long format needs them via CLI.
443            resolve_owner_group: false,
444            read_selinux: false,
445            linux_statx: true,
446            // Default on when the feature is compiled; runtime still falls back.
447            io_uring: cfg!(feature = "io-uring"),
448            emit_dir_headers: true,
449        }
450    }
451}
452
453impl ListOptions {
454    /// Whether metadata should use rayon for this listing size.
455    pub fn use_parallel_stat(&self, entry_count: usize) -> bool {
456        self.parallel && self.threads != 1 && entry_count > PARALLEL_STAT_THRESHOLD
457    }
458}
459
460/// Full runtime configuration combining listing and presentation.
461#[derive(Debug, Clone)]
462pub struct Config {
463    pub list: ListOptions,
464    pub output: OutputMode,
465    pub color: ColorWhen,
466    pub human_sizes: bool,
467    /// SI units (powers of 1000) instead of 1024 (`--si`).
468    pub si_sizes: bool,
469    pub icons: bool,
470    pub classify: bool,
471    pub indicator: IndicatorStyle,
472    /// Terminal width used for multi-column layout (`-w`); `0` means unlimited.
473    pub terminal_width: usize,
474    pub is_stdout_tty: bool,
475    pub show_owner: bool,
476    pub show_group: bool,
477    pub numeric_uid_gid: bool,
478    pub show_inode: bool,
479    pub show_blocks: bool,
480    pub full_time: bool,
481    /// When true, suppress git decorations in format (also controlled by CLI).
482    pub show_git: bool,
483    /// Filename quoting style.
484    pub quoting_style: QuotingStyle,
485    /// Control-character handling for names.
486    pub control_chars: ControlChars,
487    /// Show author column in long format (`--author`); usually same as owner.
488    pub show_author: bool,
489    /// Block size for size/`-s` display.
490    pub block_size: BlockSize,
491    /// Force 1024-byte blocks for `-s` (`-k` / `--kibibytes`).
492    pub kibibytes: bool,
493    /// Tab stop size (`-T` / `--tabsize`); stored for future column layout.
494    pub tabsize: usize,
495    /// OSC 8 hyperlinks for file names.
496    pub hyperlink: HyperlinkWhen,
497    /// Show SELinux security context (`-Z`).
498    pub show_context: bool,
499    /// Use NUL as line terminator (`--zero`).
500    pub zero: bool,
501    /// Emacs dired mode (`-D` / `--dired`).
502    pub dired: bool,
503    /// Long-listing time style.
504    pub time_style: TimeStyle,
505    /// Print GNU `total N` before directory content listings (`-l` / `-s`).
506    /// False when listing a single non-directory operand (or `-d` on a dir).
507    pub emit_block_total: bool,
508}
509
510impl Default for Config {
511    fn default() -> Self {
512        Self {
513            list: ListOptions::default(),
514            output: OutputMode::Default,
515            color: ColorWhen::Auto,
516            human_sizes: false,
517            si_sizes: false,
518            icons: false,
519            classify: false,
520            indicator: IndicatorStyle::None,
521            terminal_width: 80,
522            is_stdout_tty: false,
523            show_owner: true,
524            show_group: true,
525            numeric_uid_gid: false,
526            show_inode: false,
527            show_blocks: false,
528            full_time: false,
529            show_git: false,
530            quoting_style: QuotingStyle::Literal,
531            control_chars: ControlChars::Auto,
532            show_author: false,
533            block_size: BlockSize::default(),
534            kibibytes: false,
535            tabsize: 8,
536            hyperlink: HyperlinkWhen::Never,
537            show_context: false,
538            zero: false,
539            dired: false,
540            time_style: TimeStyle::Locale,
541            emit_block_total: true,
542        }
543    }
544}
545
546impl Config {
547    pub fn color_enabled(&self) -> bool {
548        self.color.enabled(self.is_stdout_tty)
549    }
550
551    pub fn hyperlink_enabled(&self) -> bool {
552        self.hyperlink.enabled(self.is_stdout_tty)
553    }
554
555    /// Resolve default output mode: multi-column on TTY, one-per-line otherwise.
556    pub fn effective_output(&self) -> OutputMode {
557        match self.output {
558            OutputMode::Default if !self.is_stdout_tty => OutputMode::OnePerLine,
559            OutputMode::Default if self.is_stdout_tty => OutputMode::Columns,
560            other => other,
561        }
562    }
563
564    pub fn indicator_style(&self) -> IndicatorStyle {
565        if self.classify {
566            IndicatorStyle::Classify
567        } else {
568            self.indicator
569        }
570    }
571
572    /// Effective block size for allocated-size display (`-s`).
573    ///
574    /// GNU coreutils defaults to **1024**-byte units for `-s` (and `-k` forces
575    /// that). A bare `--block-size=1` / default long-size unit of 1 byte must
576    /// **not** be reused for `-s` (that produced wild totals like `4096` for a
577    /// 4KiB file). `POSIXLY_CORRECT` selects 512-byte units.
578    pub fn blocks_unit(&self) -> u64 {
579        if self.kibibytes {
580            return 1024;
581        }
582        match self.block_size {
583            BlockSize::Bytes(0 | 1) => {
584                if std::env::var_os("POSIXLY_CORRECT").is_some() {
585                    512
586                } else {
587                    1024
588                }
589            }
590            other => other.block_divisor(),
591        }
592    }
593
594    /// Line terminator (newline or NUL).
595    pub fn line_ending(&self) -> &'static str {
596        if self.zero {
597            "\0"
598        } else {
599            "\n"
600        }
601    }
602
603    /// Whether control characters should be replaced with `?`.
604    pub fn hide_control_chars(&self) -> bool {
605        match self.control_chars {
606            ControlChars::Hide => true,
607            ControlChars::Show => false,
608            ControlChars::Auto => {
609                self.is_stdout_tty
610                    && matches!(
611                        self.quoting_style,
612                        QuotingStyle::Literal | QuotingStyle::Locale
613                    )
614            }
615        }
616    }
617}
618
619#[cfg(test)]
620mod tests {
621    use super::*;
622
623    #[test]
624    fn parse_block_size_binary_and_si() {
625        assert_eq!(BlockSize::parse("K"), Some(BlockSize::Bytes(1024)));
626        assert_eq!(BlockSize::parse("1K"), Some(BlockSize::Bytes(1024)));
627        assert_eq!(BlockSize::parse("M"), Some(BlockSize::Bytes(1024 * 1024)));
628        assert_eq!(BlockSize::parse("KB"), Some(BlockSize::Bytes(1000)));
629        assert_eq!(BlockSize::parse("MB"), Some(BlockSize::Bytes(1_000_000)));
630        assert_eq!(BlockSize::parse("512"), Some(BlockSize::Bytes(512)));
631        assert_eq!(
632            BlockSize::parse("G"),
633            Some(BlockSize::Bytes(1024u64.pow(3)))
634        );
635        assert_eq!(
636            BlockSize::parse("T"),
637            Some(BlockSize::Bytes(1024u64.pow(4)))
638        );
639        assert_eq!(
640            BlockSize::parse("human-readable"),
641            Some(BlockSize::HumanBinary)
642        );
643        assert_eq!(BlockSize::parse("si"), Some(BlockSize::HumanSi));
644        assert_eq!(BlockSize::parse("bogus"), None);
645    }
646
647    #[test]
648    fn parse_quoting_styles() {
649        assert_eq!(QuotingStyle::parse("escape"), Some(QuotingStyle::Escape));
650        assert_eq!(QuotingStyle::parse("c"), Some(QuotingStyle::C));
651        assert_eq!(
652            QuotingStyle::parse("shell-always"),
653            Some(QuotingStyle::ShellAlways)
654        );
655        assert_eq!(QuotingStyle::parse("literal"), Some(QuotingStyle::Literal));
656        assert_eq!(QuotingStyle::parse("nope"), None);
657    }
658
659    #[test]
660    fn parse_time_styles() {
661        assert_eq!(TimeStyle::parse("long-iso"), Some(TimeStyle::LongIso));
662        assert_eq!(TimeStyle::parse("full-iso"), Some(TimeStyle::FullIso));
663        assert_eq!(TimeStyle::parse("iso"), Some(TimeStyle::Iso));
664        assert_eq!(
665            TimeStyle::parse("+%Y"),
666            Some(TimeStyle::Format("%Y".into()))
667        );
668    }
669
670    #[test]
671    fn parse_indicator_style() {
672        assert_eq!(
673            IndicatorStyle::parse("classify"),
674            Some(IndicatorStyle::Classify)
675        );
676        assert_eq!(IndicatorStyle::parse("slash"), Some(IndicatorStyle::Slash));
677        assert_eq!(
678            IndicatorStyle::parse("file-type"),
679            Some(IndicatorStyle::FileType)
680        );
681        assert_eq!(IndicatorStyle::parse("none"), Some(IndicatorStyle::None));
682    }
683}