use crate::entry::TimeField;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SortBy {
#[default]
Name,
Size,
Time,
Extension,
Version,
Width,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ColorWhen {
#[default]
Auto,
Always,
Never,
}
impl ColorWhen {
pub fn parse(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"auto" | "tty" | "if-tty" => Some(Self::Auto),
"always" | "yes" | "force" | "true" | "on" => Some(Self::Always),
"never" | "no" | "none" | "false" | "off" => Some(Self::Never),
_ => None,
}
}
pub fn enabled(self, is_tty: bool) -> bool {
match self {
Self::Always => true,
Self::Never => false,
Self::Auto => is_tty,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IconsWhen {
#[default]
Auto,
Always,
Never,
}
impl IconsWhen {
pub fn parse(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"auto" | "tty" | "if-tty" => Some(Self::Auto),
"always" | "yes" | "force" | "true" | "on" => Some(Self::Always),
"never" | "no" | "none" | "false" | "off" => Some(Self::Never),
_ => None,
}
}
pub fn enabled(self, is_tty: bool) -> bool {
match self {
Self::Always => true,
Self::Never => false,
Self::Auto => is_tty,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OutputMode {
#[default]
Default,
Columns,
Across,
OnePerLine,
Long,
Commas,
Json,
Tree,
Csv,
Tsv,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IndicatorStyle {
#[default]
None,
Slash,
Classify,
FileType,
}
impl IndicatorStyle {
pub fn parse(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"none" => Some(Self::None),
"slash" => Some(Self::Slash),
"file-type" | "filetype" => Some(Self::FileType),
"classify" => Some(Self::Classify),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum QuotingStyle {
#[default]
Literal,
Locale,
Shell,
ShellAlways,
ShellEscape,
ShellEscapeAlways,
C,
Escape,
}
impl QuotingStyle {
pub fn parse(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"literal" => Some(Self::Literal),
"locale" => Some(Self::Locale),
"shell" => Some(Self::Shell),
"shell-always" => Some(Self::ShellAlways),
"shell-escape" => Some(Self::ShellEscape),
"shell-escape-always" => Some(Self::ShellEscapeAlways),
"c" => Some(Self::C),
"escape" => Some(Self::Escape),
_ => None,
}
}
pub fn from_env() -> Option<Self> {
std::env::var("QUOTING_STYLE")
.ok()
.as_deref()
.and_then(Self::parse)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ControlChars {
#[default]
Auto,
Hide,
Show,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum TimeStyle {
#[default]
Locale,
FullIso,
LongIso,
Iso,
Format(String),
}
impl TimeStyle {
pub fn parse(s: &str) -> Option<Self> {
if let Some(fmt) = s.strip_prefix('+') {
return Some(Self::Format(fmt.to_string()));
}
match s.to_ascii_lowercase().as_str() {
"full-iso" | "full_iso" => Some(Self::FullIso),
"long-iso" | "long_iso" => Some(Self::LongIso),
"iso" => Some(Self::Iso),
"locale" => Some(Self::Locale),
_ => None,
}
}
pub fn from_env() -> Option<Self> {
std::env::var("TIME_STYLE")
.ok()
.as_deref()
.and_then(Self::parse)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HyperlinkWhen {
#[default]
Never,
Auto,
Always,
}
impl HyperlinkWhen {
pub fn parse(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"auto" | "tty" | "if-tty" => Some(Self::Auto),
"always" | "yes" | "force" | "true" | "on" => Some(Self::Always),
"never" | "no" | "none" | "false" | "off" => Some(Self::Never),
_ => None,
}
}
pub fn enabled(self, is_tty: bool) -> bool {
match self {
Self::Always => true,
Self::Never => false,
Self::Auto => is_tty,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockSize {
HumanBinary,
HumanSi,
Bytes(u64),
}
impl Default for BlockSize {
fn default() -> Self {
Self::Bytes(1)
}
}
impl BlockSize {
pub fn parse(s: &str) -> Option<Self> {
let s = s.trim();
if s.is_empty() {
return None;
}
let lower = s.to_ascii_lowercase();
match lower.as_str() {
"human-readable" | "human" => return Some(Self::HumanBinary),
"si" => return Some(Self::HumanSi),
_ => {}
}
let (num_part, unit_part) = split_size_parts(s)?;
let mult = if unit_part.is_empty() {
1u64
} else {
parse_size_unit(unit_part)?
};
let n: u64 = if num_part.is_empty() {
1
} else {
num_part.parse().ok()?
};
n.checked_mul(mult).map(Self::Bytes)
}
pub fn block_divisor(self) -> u64 {
match self {
Self::HumanBinary | Self::HumanSi => 1024,
Self::Bytes(n) => n.max(1),
}
}
}
fn split_size_parts(s: &str) -> Option<(&str, &str)> {
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
if i == 0 && !bytes[0].is_ascii_alphabetic() {
return None;
}
Some((&s[..i], &s[i..]))
}
fn parse_size_unit(unit: &str) -> Option<u64> {
let u = unit.to_ascii_lowercase();
let (base, power_of_1000) = match u.as_str() {
"k" | "ki" | "kib" => (1024u64, false),
"m" | "mi" | "mib" => (1024u64.pow(2), false),
"g" | "gi" | "gib" => (1024u64.pow(3), false),
"t" | "ti" | "tib" => (1024u64.pow(4), false),
"p" | "pi" | "pib" => (1024u64.pow(5), false),
"e" | "ei" | "eib" => (1024u64.pow(6), false),
"kb" => (1000u64, true),
"mb" => (1000u64.pow(2), true),
"gb" => (1000u64.pow(3), true),
"tb" => (1000u64.pow(4), true),
"pb" => (1000u64.pow(5), true),
"eb" => (1000u64.pow(6), true),
"b" => (1u64, true),
_ => return None,
};
let _ = power_of_1000;
Some(base)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CliSymlinkMode {
#[default]
Never,
Always,
DirOnly,
}
pub const PARALLEL_STAT_THRESHOLD: usize = 32;
#[derive(Debug, Clone)]
pub struct ListOptions {
pub all: bool,
pub almost_all: bool,
pub sort_by: SortBy,
pub reverse: bool,
pub dirs_first: bool,
pub recursive: bool,
pub max_depth: Option<usize>,
pub gnu_mode: bool,
pub follow_links: bool,
pub directory: bool,
pub ignore_backups: bool,
pub ignore_patterns: Vec<String>,
pub hide_patterns: Vec<String>,
pub use_ignore_files: bool,
pub list_archives: bool,
pub time_field: TimeField,
pub cli_symlink: CliSymlinkMode,
pub parallel: bool,
pub threads: usize,
pub collect_timing: bool,
pub resolve_owner_group: bool,
pub read_selinux: bool,
pub linux_statx: bool,
pub io_uring: bool,
pub emit_dir_headers: bool,
}
impl Default for ListOptions {
fn default() -> Self {
Self {
all: false,
almost_all: false,
sort_by: SortBy::Name,
reverse: false,
dirs_first: false,
recursive: false,
max_depth: None,
gnu_mode: false,
follow_links: false,
directory: false,
ignore_backups: false,
ignore_patterns: Vec::new(),
hide_patterns: Vec::new(),
use_ignore_files: false,
list_archives: true,
time_field: TimeField::Modified,
cli_symlink: CliSymlinkMode::Never,
parallel: true,
threads: 0,
collect_timing: false,
resolve_owner_group: false,
read_selinux: false,
linux_statx: true,
io_uring: cfg!(feature = "io-uring"),
emit_dir_headers: true,
}
}
}
impl ListOptions {
pub fn use_parallel_stat(&self, entry_count: usize) -> bool {
self.parallel && self.threads != 1 && entry_count > PARALLEL_STAT_THRESHOLD
}
}
#[derive(Debug, Clone)]
pub struct Config {
pub list: ListOptions,
pub output: OutputMode,
pub color: ColorWhen,
pub human_sizes: bool,
pub si_sizes: bool,
pub icons: bool,
pub classify: bool,
pub indicator: IndicatorStyle,
pub terminal_width: usize,
pub is_stdout_tty: bool,
pub show_owner: bool,
pub show_group: bool,
pub numeric_uid_gid: bool,
pub show_inode: bool,
pub show_blocks: bool,
pub full_time: bool,
pub show_git: bool,
pub quoting_style: QuotingStyle,
pub control_chars: ControlChars,
pub show_author: bool,
pub block_size: BlockSize,
pub kibibytes: bool,
pub tabsize: usize,
pub hyperlink: HyperlinkWhen,
pub show_context: bool,
pub zero: bool,
pub dired: bool,
pub time_style: TimeStyle,
pub emit_block_total: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
list: ListOptions::default(),
output: OutputMode::Default,
color: ColorWhen::Auto,
human_sizes: false,
si_sizes: false,
icons: false,
classify: false,
indicator: IndicatorStyle::None,
terminal_width: 80,
is_stdout_tty: false,
show_owner: true,
show_group: true,
numeric_uid_gid: false,
show_inode: false,
show_blocks: false,
full_time: false,
show_git: false,
quoting_style: QuotingStyle::Literal,
control_chars: ControlChars::Auto,
show_author: false,
block_size: BlockSize::default(),
kibibytes: false,
tabsize: 8,
hyperlink: HyperlinkWhen::Never,
show_context: false,
zero: false,
dired: false,
time_style: TimeStyle::Locale,
emit_block_total: true,
}
}
}
impl Config {
pub fn color_enabled(&self) -> bool {
self.color.enabled(self.is_stdout_tty)
}
pub fn hyperlink_enabled(&self) -> bool {
self.hyperlink.enabled(self.is_stdout_tty)
}
pub fn effective_output(&self) -> OutputMode {
match self.output {
OutputMode::Default if !self.is_stdout_tty => OutputMode::OnePerLine,
OutputMode::Default if self.is_stdout_tty => OutputMode::Columns,
other => other,
}
}
pub fn indicator_style(&self) -> IndicatorStyle {
if self.classify {
IndicatorStyle::Classify
} else {
self.indicator
}
}
pub fn blocks_unit(&self) -> u64 {
if self.kibibytes {
return 1024;
}
match self.block_size {
BlockSize::Bytes(0 | 1) => {
if std::env::var_os("POSIXLY_CORRECT").is_some() {
512
} else {
1024
}
}
other => other.block_divisor(),
}
}
pub fn line_ending(&self) -> &'static str {
if self.zero {
"\0"
} else {
"\n"
}
}
pub fn hide_control_chars(&self) -> bool {
match self.control_chars {
ControlChars::Hide => true,
ControlChars::Show => false,
ControlChars::Auto => {
self.is_stdout_tty
&& matches!(
self.quoting_style,
QuotingStyle::Literal | QuotingStyle::Locale
)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_block_size_binary_and_si() {
assert_eq!(BlockSize::parse("K"), Some(BlockSize::Bytes(1024)));
assert_eq!(BlockSize::parse("1K"), Some(BlockSize::Bytes(1024)));
assert_eq!(BlockSize::parse("M"), Some(BlockSize::Bytes(1024 * 1024)));
assert_eq!(BlockSize::parse("KB"), Some(BlockSize::Bytes(1000)));
assert_eq!(BlockSize::parse("MB"), Some(BlockSize::Bytes(1_000_000)));
assert_eq!(BlockSize::parse("512"), Some(BlockSize::Bytes(512)));
assert_eq!(
BlockSize::parse("G"),
Some(BlockSize::Bytes(1024u64.pow(3)))
);
assert_eq!(
BlockSize::parse("T"),
Some(BlockSize::Bytes(1024u64.pow(4)))
);
assert_eq!(
BlockSize::parse("human-readable"),
Some(BlockSize::HumanBinary)
);
assert_eq!(BlockSize::parse("si"), Some(BlockSize::HumanSi));
assert_eq!(BlockSize::parse("bogus"), None);
}
#[test]
fn parse_quoting_styles() {
assert_eq!(QuotingStyle::parse("escape"), Some(QuotingStyle::Escape));
assert_eq!(QuotingStyle::parse("c"), Some(QuotingStyle::C));
assert_eq!(
QuotingStyle::parse("shell-always"),
Some(QuotingStyle::ShellAlways)
);
assert_eq!(QuotingStyle::parse("literal"), Some(QuotingStyle::Literal));
assert_eq!(QuotingStyle::parse("nope"), None);
}
#[test]
fn parse_time_styles() {
assert_eq!(TimeStyle::parse("long-iso"), Some(TimeStyle::LongIso));
assert_eq!(TimeStyle::parse("full-iso"), Some(TimeStyle::FullIso));
assert_eq!(TimeStyle::parse("iso"), Some(TimeStyle::Iso));
assert_eq!(
TimeStyle::parse("+%Y"),
Some(TimeStyle::Format("%Y".into()))
);
}
#[test]
fn parse_indicator_style() {
assert_eq!(
IndicatorStyle::parse("classify"),
Some(IndicatorStyle::Classify)
);
assert_eq!(IndicatorStyle::parse("slash"), Some(IndicatorStyle::Slash));
assert_eq!(
IndicatorStyle::parse("file-type"),
Some(IndicatorStyle::FileType)
);
assert_eq!(IndicatorStyle::parse("none"), Some(IndicatorStyle::None));
}
}