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 pub full_metadata: bool,
422}
423
424impl Default for ListOptions {
425 fn default() -> Self {
426 Self {
427 all: false,
428 almost_all: false,
429 sort_by: SortBy::Name,
430 reverse: false,
431 dirs_first: false,
432 recursive: false,
433 max_depth: None,
434 gnu_mode: false,
435 follow_links: false,
436 directory: false,
437 ignore_backups: false,
438 ignore_patterns: Vec::new(),
439 hide_patterns: Vec::new(),
440 use_ignore_files: false,
441 list_archives: true,
442 time_field: TimeField::Modified,
443 cli_symlink: CliSymlinkMode::Never,
444 parallel: true,
445 threads: 0,
446 collect_timing: false,
447 resolve_owner_group: false,
449 read_selinux: false,
450 linux_statx: true,
451 io_uring: cfg!(feature = "io-uring"),
453 emit_dir_headers: true,
454 full_metadata: true,
457 }
458 }
459}
460
461impl ListOptions {
462 pub fn use_parallel_stat(&self, entry_count: usize) -> bool {
464 self.parallel && self.threads != 1 && entry_count > PARALLEL_STAT_THRESHOLD
465 }
466
467 pub fn use_kind_only(&self) -> bool {
470 if self.full_metadata || self.follow_links || self.resolve_owner_group || self.read_selinux
471 {
472 return false;
473 }
474 matches!(
475 self.sort_by,
476 SortBy::Name | SortBy::None | SortBy::Extension | SortBy::Version | SortBy::Width
477 )
478 }
479}
480
481#[derive(Debug, Clone)]
483pub struct Config {
484 pub list: ListOptions,
485 pub output: OutputMode,
486 pub color: ColorWhen,
487 pub human_sizes: bool,
488 pub si_sizes: bool,
490 pub icons: bool,
491 pub classify: bool,
492 pub indicator: IndicatorStyle,
493 pub terminal_width: usize,
495 pub is_stdout_tty: bool,
496 pub show_owner: bool,
497 pub show_group: bool,
498 pub numeric_uid_gid: bool,
499 pub show_inode: bool,
500 pub show_blocks: bool,
501 pub full_time: bool,
502 pub show_git: bool,
504 pub quoting_style: QuotingStyle,
506 pub control_chars: ControlChars,
508 pub show_author: bool,
510 pub block_size: BlockSize,
512 pub kibibytes: bool,
514 pub tabsize: usize,
516 pub hyperlink: HyperlinkWhen,
518 pub show_context: bool,
520 pub zero: bool,
522 pub dired: bool,
524 pub time_style: TimeStyle,
526 pub emit_block_total: bool,
529 pub json_full: bool,
531}
532
533impl Default for Config {
534 fn default() -> Self {
535 Self {
536 list: ListOptions::default(),
537 output: OutputMode::Default,
538 color: ColorWhen::Auto,
539 human_sizes: false,
540 si_sizes: false,
541 icons: false,
542 classify: false,
543 indicator: IndicatorStyle::None,
544 terminal_width: 80,
545 is_stdout_tty: false,
546 show_owner: true,
547 show_group: true,
548 numeric_uid_gid: false,
549 show_inode: false,
550 show_blocks: false,
551 full_time: false,
552 show_git: false,
553 quoting_style: QuotingStyle::Literal,
554 control_chars: ControlChars::Auto,
555 show_author: false,
556 block_size: BlockSize::default(),
557 kibibytes: false,
558 tabsize: 8,
559 hyperlink: HyperlinkWhen::Never,
560 show_context: false,
561 zero: false,
562 dired: false,
563 time_style: TimeStyle::Locale,
564 emit_block_total: true,
565 json_full: false,
566 }
567 }
568}
569
570impl Config {
571 pub fn color_enabled(&self) -> bool {
572 self.color.enabled(self.is_stdout_tty)
573 }
574
575 pub fn hyperlink_enabled(&self) -> bool {
576 self.hyperlink.enabled(self.is_stdout_tty)
577 }
578
579 pub fn effective_output(&self) -> OutputMode {
581 match self.output {
582 OutputMode::Default if !self.is_stdout_tty => OutputMode::OnePerLine,
583 OutputMode::Default if self.is_stdout_tty => OutputMode::Columns,
584 other => other,
585 }
586 }
587
588 pub fn indicator_style(&self) -> IndicatorStyle {
589 if self.classify {
590 IndicatorStyle::Classify
591 } else {
592 self.indicator
593 }
594 }
595
596 pub fn blocks_unit(&self) -> u64 {
603 if self.kibibytes {
604 return 1024;
605 }
606 match self.block_size {
607 BlockSize::Bytes(0 | 1) => {
608 if std::env::var_os("POSIXLY_CORRECT").is_some() {
609 512
610 } else {
611 1024
612 }
613 }
614 other => other.block_divisor(),
615 }
616 }
617
618 pub fn line_ending(&self) -> &'static str {
620 if self.zero {
621 "\0"
622 } else {
623 "\n"
624 }
625 }
626
627 pub fn hide_control_chars(&self) -> bool {
629 match self.control_chars {
630 ControlChars::Hide => true,
631 ControlChars::Show => false,
632 ControlChars::Auto => {
633 self.is_stdout_tty
634 && matches!(
635 self.quoting_style,
636 QuotingStyle::Literal | QuotingStyle::Locale
637 )
638 }
639 }
640 }
641}
642
643#[cfg(test)]
644mod tests {
645 use super::*;
646
647 #[test]
648 fn parse_block_size_binary_and_si() {
649 assert_eq!(BlockSize::parse("K"), Some(BlockSize::Bytes(1024)));
650 assert_eq!(BlockSize::parse("1K"), Some(BlockSize::Bytes(1024)));
651 assert_eq!(BlockSize::parse("M"), Some(BlockSize::Bytes(1024 * 1024)));
652 assert_eq!(BlockSize::parse("KB"), Some(BlockSize::Bytes(1000)));
653 assert_eq!(BlockSize::parse("MB"), Some(BlockSize::Bytes(1_000_000)));
654 assert_eq!(BlockSize::parse("512"), Some(BlockSize::Bytes(512)));
655 assert_eq!(
656 BlockSize::parse("G"),
657 Some(BlockSize::Bytes(1024u64.pow(3)))
658 );
659 assert_eq!(
660 BlockSize::parse("T"),
661 Some(BlockSize::Bytes(1024u64.pow(4)))
662 );
663 assert_eq!(
664 BlockSize::parse("human-readable"),
665 Some(BlockSize::HumanBinary)
666 );
667 assert_eq!(BlockSize::parse("si"), Some(BlockSize::HumanSi));
668 assert_eq!(BlockSize::parse("bogus"), None);
669 }
670
671 #[test]
672 fn parse_quoting_styles() {
673 assert_eq!(QuotingStyle::parse("escape"), Some(QuotingStyle::Escape));
674 assert_eq!(QuotingStyle::parse("c"), Some(QuotingStyle::C));
675 assert_eq!(
676 QuotingStyle::parse("shell-always"),
677 Some(QuotingStyle::ShellAlways)
678 );
679 assert_eq!(QuotingStyle::parse("literal"), Some(QuotingStyle::Literal));
680 assert_eq!(QuotingStyle::parse("nope"), None);
681 }
682
683 #[test]
684 fn parse_time_styles() {
685 assert_eq!(TimeStyle::parse("long-iso"), Some(TimeStyle::LongIso));
686 assert_eq!(TimeStyle::parse("full-iso"), Some(TimeStyle::FullIso));
687 assert_eq!(TimeStyle::parse("iso"), Some(TimeStyle::Iso));
688 assert_eq!(
689 TimeStyle::parse("+%Y"),
690 Some(TimeStyle::Format("%Y".into()))
691 );
692 }
693
694 #[test]
695 fn parse_indicator_style() {
696 assert_eq!(
697 IndicatorStyle::parse("classify"),
698 Some(IndicatorStyle::Classify)
699 );
700 assert_eq!(IndicatorStyle::parse("slash"), Some(IndicatorStyle::Slash));
701 assert_eq!(
702 IndicatorStyle::parse("file-type"),
703 Some(IndicatorStyle::FileType)
704 );
705 assert_eq!(IndicatorStyle::parse("none"), Some(IndicatorStyle::None));
706 }
707}