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}
411
412impl Default for ListOptions {
413    fn default() -> Self {
414        Self {
415            all: false,
416            almost_all: false,
417            sort_by: SortBy::Name,
418            reverse: false,
419            dirs_first: false,
420            recursive: false,
421            max_depth: None,
422            gnu_mode: false,
423            follow_links: false,
424            directory: false,
425            ignore_backups: false,
426            ignore_patterns: Vec::new(),
427            hide_patterns: Vec::new(),
428            use_ignore_files: false,
429            list_archives: true,
430            time_field: TimeField::Modified,
431            cli_symlink: CliSymlinkMode::Never,
432            parallel: true,
433            threads: 0,
434            collect_timing: false,
435            // Default: cheap path — names filled when long format needs them via CLI.
436            resolve_owner_group: false,
437            read_selinux: false,
438            linux_statx: true,
439            // Default on when the feature is compiled; runtime still falls back.
440            io_uring: cfg!(feature = "io-uring"),
441        }
442    }
443}
444
445impl ListOptions {
446    /// Whether metadata should use rayon for this listing size.
447    pub fn use_parallel_stat(&self, entry_count: usize) -> bool {
448        self.parallel && self.threads != 1 && entry_count > PARALLEL_STAT_THRESHOLD
449    }
450}
451
452/// Full runtime configuration combining listing and presentation.
453#[derive(Debug, Clone)]
454pub struct Config {
455    pub list: ListOptions,
456    pub output: OutputMode,
457    pub color: ColorWhen,
458    pub human_sizes: bool,
459    /// SI units (powers of 1000) instead of 1024 (`--si`).
460    pub si_sizes: bool,
461    pub icons: bool,
462    pub classify: bool,
463    pub indicator: IndicatorStyle,
464    /// Terminal width used for multi-column layout (`-w`); `0` means unlimited.
465    pub terminal_width: usize,
466    pub is_stdout_tty: bool,
467    pub show_owner: bool,
468    pub show_group: bool,
469    pub numeric_uid_gid: bool,
470    pub show_inode: bool,
471    pub show_blocks: bool,
472    pub full_time: bool,
473    /// When true, suppress git decorations in format (also controlled by CLI).
474    pub show_git: bool,
475    /// Filename quoting style.
476    pub quoting_style: QuotingStyle,
477    /// Control-character handling for names.
478    pub control_chars: ControlChars,
479    /// Show author column in long format (`--author`); usually same as owner.
480    pub show_author: bool,
481    /// Block size for size/`-s` display.
482    pub block_size: BlockSize,
483    /// Force 1024-byte blocks for `-s` (`-k` / `--kibibytes`).
484    pub kibibytes: bool,
485    /// Tab stop size (`-T` / `--tabsize`); stored for future column layout.
486    pub tabsize: usize,
487    /// OSC 8 hyperlinks for file names.
488    pub hyperlink: HyperlinkWhen,
489    /// Show SELinux security context (`-Z`).
490    pub show_context: bool,
491    /// Use NUL as line terminator (`--zero`).
492    pub zero: bool,
493    /// Emacs dired mode (`-D` / `--dired`).
494    pub dired: bool,
495    /// Long-listing time style.
496    pub time_style: TimeStyle,
497}
498
499impl Default for Config {
500    fn default() -> Self {
501        Self {
502            list: ListOptions::default(),
503            output: OutputMode::Default,
504            color: ColorWhen::Auto,
505            human_sizes: false,
506            si_sizes: false,
507            icons: false,
508            classify: false,
509            indicator: IndicatorStyle::None,
510            terminal_width: 80,
511            is_stdout_tty: false,
512            show_owner: true,
513            show_group: true,
514            numeric_uid_gid: false,
515            show_inode: false,
516            show_blocks: false,
517            full_time: false,
518            show_git: false,
519            quoting_style: QuotingStyle::Literal,
520            control_chars: ControlChars::Auto,
521            show_author: false,
522            block_size: BlockSize::default(),
523            kibibytes: false,
524            tabsize: 8,
525            hyperlink: HyperlinkWhen::Never,
526            show_context: false,
527            zero: false,
528            dired: false,
529            time_style: TimeStyle::Locale,
530        }
531    }
532}
533
534impl Config {
535    pub fn color_enabled(&self) -> bool {
536        self.color.enabled(self.is_stdout_tty)
537    }
538
539    pub fn hyperlink_enabled(&self) -> bool {
540        self.hyperlink.enabled(self.is_stdout_tty)
541    }
542
543    /// Resolve default output mode: multi-column on TTY, one-per-line otherwise.
544    pub fn effective_output(&self) -> OutputMode {
545        match self.output {
546            OutputMode::Default if !self.is_stdout_tty => OutputMode::OnePerLine,
547            OutputMode::Default if self.is_stdout_tty => OutputMode::Columns,
548            other => other,
549        }
550    }
551
552    pub fn indicator_style(&self) -> IndicatorStyle {
553        if self.classify {
554            IndicatorStyle::Classify
555        } else {
556            self.indicator
557        }
558    }
559
560    /// Effective block size for allocated-size display (`-s`).
561    pub fn blocks_unit(&self) -> u64 {
562        if self.kibibytes {
563            return 1024;
564        }
565        self.block_size.block_divisor()
566    }
567
568    /// Line terminator (newline or NUL).
569    pub fn line_ending(&self) -> &'static str {
570        if self.zero {
571            "\0"
572        } else {
573            "\n"
574        }
575    }
576
577    /// Whether control characters should be replaced with `?`.
578    pub fn hide_control_chars(&self) -> bool {
579        match self.control_chars {
580            ControlChars::Hide => true,
581            ControlChars::Show => false,
582            ControlChars::Auto => {
583                self.is_stdout_tty
584                    && matches!(
585                        self.quoting_style,
586                        QuotingStyle::Literal | QuotingStyle::Locale
587                    )
588            }
589        }
590    }
591}
592
593#[cfg(test)]
594mod tests {
595    use super::*;
596
597    #[test]
598    fn parse_block_size_binary_and_si() {
599        assert_eq!(BlockSize::parse("K"), Some(BlockSize::Bytes(1024)));
600        assert_eq!(BlockSize::parse("1K"), Some(BlockSize::Bytes(1024)));
601        assert_eq!(BlockSize::parse("M"), Some(BlockSize::Bytes(1024 * 1024)));
602        assert_eq!(BlockSize::parse("KB"), Some(BlockSize::Bytes(1000)));
603        assert_eq!(BlockSize::parse("MB"), Some(BlockSize::Bytes(1_000_000)));
604        assert_eq!(BlockSize::parse("512"), Some(BlockSize::Bytes(512)));
605        assert_eq!(
606            BlockSize::parse("G"),
607            Some(BlockSize::Bytes(1024u64.pow(3)))
608        );
609        assert_eq!(
610            BlockSize::parse("T"),
611            Some(BlockSize::Bytes(1024u64.pow(4)))
612        );
613        assert_eq!(
614            BlockSize::parse("human-readable"),
615            Some(BlockSize::HumanBinary)
616        );
617        assert_eq!(BlockSize::parse("si"), Some(BlockSize::HumanSi));
618        assert_eq!(BlockSize::parse("bogus"), None);
619    }
620
621    #[test]
622    fn parse_quoting_styles() {
623        assert_eq!(QuotingStyle::parse("escape"), Some(QuotingStyle::Escape));
624        assert_eq!(QuotingStyle::parse("c"), Some(QuotingStyle::C));
625        assert_eq!(
626            QuotingStyle::parse("shell-always"),
627            Some(QuotingStyle::ShellAlways)
628        );
629        assert_eq!(QuotingStyle::parse("literal"), Some(QuotingStyle::Literal));
630        assert_eq!(QuotingStyle::parse("nope"), None);
631    }
632
633    #[test]
634    fn parse_time_styles() {
635        assert_eq!(TimeStyle::parse("long-iso"), Some(TimeStyle::LongIso));
636        assert_eq!(TimeStyle::parse("full-iso"), Some(TimeStyle::FullIso));
637        assert_eq!(TimeStyle::parse("iso"), Some(TimeStyle::Iso));
638        assert_eq!(
639            TimeStyle::parse("+%Y"),
640            Some(TimeStyle::Format("%Y".into()))
641        );
642    }
643
644    #[test]
645    fn parse_indicator_style() {
646        assert_eq!(
647            IndicatorStyle::parse("classify"),
648            Some(IndicatorStyle::Classify)
649        );
650        assert_eq!(IndicatorStyle::parse("slash"), Some(IndicatorStyle::Slash));
651        assert_eq!(
652            IndicatorStyle::parse("file-type"),
653            Some(IndicatorStyle::FileType)
654        );
655        assert_eq!(IndicatorStyle::parse("none"), Some(IndicatorStyle::None));
656    }
657}