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 None,
17}
18
19#[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#[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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
78pub enum OutputMode {
79 #[default]
81 Default,
82 Columns,
84 Across,
86 OnePerLine,
88 Long,
90 Commas,
92 Json,
94 Tree,
96 Csv,
98 Tsv,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
104pub enum IndicatorStyle {
105 #[default]
106 None,
107 Slash,
109 Classify,
111 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
129pub enum QuotingStyle {
130 #[default]
132 Literal,
133 Locale,
135 Shell,
137 ShellAlways,
139 ShellEscape,
141 ShellEscapeAlways,
143 C,
145 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
175pub enum ControlChars {
176 #[default]
178 Auto,
179 Hide,
181 Show,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Default)]
187pub enum TimeStyle {
188 #[default]
190 Locale,
191 FullIso,
193 LongIso,
195 Iso,
197 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253pub enum BlockSize {
254 HumanBinary,
256 HumanSi,
258 Bytes(u64),
260}
261
262impl Default for BlockSize {
263 fn default() -> Self {
264 Self::Bytes(1)
266 }
267}
268
269impl BlockSize {
270 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 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
350pub enum CliSymlinkMode {
351 #[default]
353 Never,
354 Always,
356 DirOnly,
358}
359
360pub const PARALLEL_STAT_THRESHOLD: usize = 32;
362
363#[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 pub gnu_mode: bool,
375 pub follow_links: bool,
377 pub directory: bool,
379 pub ignore_backups: bool,
381 pub ignore_patterns: Vec<String>,
383 pub hide_patterns: Vec<String>,
385 pub use_ignore_files: bool,
387 pub list_archives: bool,
390 pub time_field: TimeField,
392 pub cli_symlink: CliSymlinkMode,
394 pub parallel: bool,
397 pub threads: usize,
399 pub collect_timing: bool,
401 pub resolve_owner_group: bool,
403 pub read_selinux: bool,
405 pub linux_statx: bool,
407 pub io_uring: bool,
410 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 resolve_owner_group: false,
440 read_selinux: false,
441 linux_statx: true,
442 io_uring: cfg!(feature = "io-uring"),
444 emit_dir_headers: true,
445 }
446 }
447}
448
449impl ListOptions {
450 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#[derive(Debug, Clone)]
458pub struct Config {
459 pub list: ListOptions,
460 pub output: OutputMode,
461 pub color: ColorWhen,
462 pub human_sizes: bool,
463 pub si_sizes: bool,
465 pub icons: bool,
466 pub classify: bool,
467 pub indicator: IndicatorStyle,
468 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 pub show_git: bool,
479 pub quoting_style: QuotingStyle,
481 pub control_chars: ControlChars,
483 pub show_author: bool,
485 pub block_size: BlockSize,
487 pub kibibytes: bool,
489 pub tabsize: usize,
491 pub hyperlink: HyperlinkWhen,
493 pub show_context: bool,
495 pub zero: bool,
497 pub dired: bool,
499 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 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 pub fn blocks_unit(&self) -> u64 {
566 if self.kibibytes {
567 return 1024;
568 }
569 self.block_size.block_divisor()
570 }
571
572 pub fn line_ending(&self) -> &'static str {
574 if self.zero {
575 "\0"
576 } else {
577 "\n"
578 }
579 }
580
581 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}