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