1use crate::entry::TimeField;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5pub enum SortBy {
6 #[default]
7 Name,
8 Size,
10 Time,
12 Extension,
13 Version,
15 Width,
17 None,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum ColorWhen {
24 #[default]
25 Auto,
26 Always,
27 Never,
28}
29
30impl ColorWhen {
31 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#[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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
81pub enum OutputMode {
82 #[default]
84 Default,
85 Columns,
87 Across,
89 OnePerLine,
91 Long,
93 Commas,
95 Json,
97 Tree,
99 Csv,
101 Tsv,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
107pub enum IndicatorStyle {
108 #[default]
109 None,
110 Slash,
112 Classify,
114 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
132pub enum QuotingStyle {
133 #[default]
135 Literal,
136 Locale,
138 Shell,
140 ShellAlways,
142 ShellEscape,
144 ShellEscapeAlways,
146 C,
148 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
178pub enum ControlChars {
179 #[default]
181 Auto,
182 Hide,
184 Show,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Default)]
190pub enum TimeStyle {
191 #[default]
193 Locale,
194 FullIso,
196 LongIso,
198 Iso,
200 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
228pub enum HyperlinkWhen {
229 #[default]
230 Never,
231 Auto,
232 Always,
233}
234
235impl HyperlinkWhen {
236 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
257pub enum BlockSize {
258 HumanBinary,
260 HumanSi,
262 Bytes(u64),
264}
265
266impl Default for BlockSize {
267 fn default() -> Self {
268 Self::Bytes(1)
270 }
271}
272
273impl BlockSize {
274 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 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
354pub enum CliSymlinkMode {
355 #[default]
357 Never,
358 Always,
360 DirOnly,
362}
363
364pub const PARALLEL_STAT_THRESHOLD: usize = 32;
366
367#[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 pub gnu_mode: bool,
379 pub follow_links: bool,
381 pub directory: bool,
383 pub ignore_backups: bool,
385 pub ignore_patterns: Vec<String>,
387 pub hide_patterns: Vec<String>,
389 pub use_ignore_files: bool,
391 pub list_archives: bool,
394 pub time_field: TimeField,
396 pub cli_symlink: CliSymlinkMode,
398 pub parallel: bool,
401 pub threads: usize,
403 pub collect_timing: bool,
405 pub resolve_owner_group: bool,
407 pub read_selinux: bool,
409 pub linux_statx: bool,
411 pub io_uring: bool,
414 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 resolve_owner_group: false,
444 read_selinux: false,
445 linux_statx: true,
446 io_uring: cfg!(feature = "io-uring"),
448 emit_dir_headers: true,
449 }
450 }
451}
452
453impl ListOptions {
454 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#[derive(Debug, Clone)]
462pub struct Config {
463 pub list: ListOptions,
464 pub output: OutputMode,
465 pub color: ColorWhen,
466 pub human_sizes: bool,
467 pub si_sizes: bool,
469 pub icons: bool,
470 pub classify: bool,
471 pub indicator: IndicatorStyle,
472 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 pub show_git: bool,
483 pub quoting_style: QuotingStyle,
485 pub control_chars: ControlChars,
487 pub show_author: bool,
489 pub block_size: BlockSize,
491 pub kibibytes: bool,
493 pub tabsize: usize,
495 pub hyperlink: HyperlinkWhen,
497 pub show_context: bool,
499 pub zero: bool,
501 pub dired: bool,
503 pub time_style: TimeStyle,
505 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 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 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 pub fn line_ending(&self) -> &'static str {
596 if self.zero {
597 "\0"
598 } else {
599 "\n"
600 }
601 }
602
603 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}