1use std::io::{self, IsTerminal, Write};
2use std::path::PathBuf;
3use std::time::Instant;
4
5use anyhow::{Context, Result};
6use f00_compat::{apply_gnu_list_options, apply_gnu_output, parse_format_word, parse_sort_word};
7#[cfg(feature = "archives")]
8use f00_core::list_path;
9use f00_core::{
10 list_paths_with_errors, BlockSize, CliSymlinkMode, Config, ControlChars, HyperlinkWhen,
11 IndicatorStyle, ListOptions, ListOutcome, ListTiming, OutputMode, QuotingStyle, SortBy,
12 TimeField, TimeStyle,
13};
14use f00_format::format_listings;
15
16use crate::cli::Args;
17use crate::config::{load_user_config, resolve_args};
18
19fn terminal_width() -> usize {
21 std::env::var("COLUMNS")
22 .ok()
23 .and_then(|s| s.parse().ok())
24 .filter(|&n: &usize| n > 0)
25 .or_else(|| terminal_size::terminal_size().map(|(terminal_size::Width(w), _)| w as usize))
26 .unwrap_or(80)
27}
28
29fn resolve_sort(args: &Args) -> SortBy {
30 if let Some(ref word) = args.sort {
31 if let Some(s) = parse_sort_word(word) {
32 return s;
33 }
34 }
35 if args.sort_none || args.unsorted_all {
36 SortBy::None
37 } else if args.sort_version {
38 SortBy::Version
39 } else if args.sort_time || args.access_time || args.change_time {
40 SortBy::Time
41 } else if args.sort_size {
42 SortBy::Size
43 } else if args.sort_extension {
44 SortBy::Extension
45 } else {
46 SortBy::Name
47 }
48}
49
50fn resolve_time_field(args: &Args) -> TimeField {
51 if let Some(ref word) = args.time {
52 return match word.to_ascii_lowercase().as_str() {
53 "atime" | "access" | "use" => TimeField::Accessed,
54 "ctime" | "status" | "change" => TimeField::Changed,
55 "birth" | "creation" => TimeField::Birth,
56 _ => TimeField::Modified,
57 };
58 }
59 if args.access_time {
60 TimeField::Accessed
61 } else if args.change_time {
62 TimeField::Changed
63 } else {
64 TimeField::Modified
65 }
66}
67
68fn resolve_output(args: &Args) -> OutputMode {
69 if let Some(ref word) = args.format {
70 if let Some(m) = parse_format_word(word) {
71 return m;
72 }
73 }
74 if args.csv {
75 OutputMode::Csv
76 } else if args.tsv {
77 OutputMode::Tsv
78 } else if args.json || args.json_full {
79 OutputMode::Json
80 } else if args.tree {
81 OutputMode::Tree
82 } else if args.long
83 || args.no_owner
84 || args.no_group_long
85 || args.numeric_uid_gid
86 || args.author
87 {
88 OutputMode::Long
90 } else if args.one_per_line || args.zero {
91 OutputMode::OnePerLine
93 } else if args.commas {
94 OutputMode::Commas
95 } else if args.across {
96 OutputMode::Across
97 } else if args.columns {
98 OutputMode::Columns
99 } else {
100 OutputMode::Default
101 }
102}
103
104fn resolve_indicator(args: &Args, is_tty: bool) -> IndicatorStyle {
105 if let Some(ref word) = args.indicator_style {
106 if let Some(s) = IndicatorStyle::parse(word) {
107 return s;
108 }
109 }
110 if let Some(when) = args.classify {
112 let when = f00_core::ColorWhen::from(when);
113 if when.enabled(is_tty) {
114 return IndicatorStyle::Classify;
115 }
116 }
118 if args.file_type {
119 IndicatorStyle::FileType
120 } else if args.indicator_slash {
121 IndicatorStyle::Slash
122 } else {
123 IndicatorStyle::None
124 }
125}
126
127fn resolve_quoting(args: &Args) -> QuotingStyle {
128 if args.literal {
129 return QuotingStyle::Literal;
130 }
131 if args.escape {
132 return QuotingStyle::Escape;
133 }
134 if args.quote_name {
135 return QuotingStyle::C;
136 }
137 if let Some(ref word) = args.quoting_style {
138 if let Some(s) = QuotingStyle::parse(word) {
139 return s;
140 }
141 }
142 QuotingStyle::from_env().unwrap_or(QuotingStyle::Literal)
143}
144
145fn resolve_control_chars(args: &Args) -> ControlChars {
146 if args.hide_control_chars {
147 ControlChars::Hide
148 } else if args.show_control_chars {
149 ControlChars::Show
150 } else {
151 ControlChars::Auto
152 }
153}
154
155fn resolve_time_style(args: &Args) -> TimeStyle {
156 if args.full_time {
157 return TimeStyle::FullIso;
158 }
159 if let Some(ref s) = args.time_style {
160 if let Some(ts) = TimeStyle::parse(s) {
161 return ts;
162 }
163 }
164 TimeStyle::from_env().unwrap_or(TimeStyle::Locale)
165}
166
167fn resolve_block_size(args: &Args) -> BlockSize {
168 if let Some(ref s) = args.block_size {
169 if let Some(bs) = BlockSize::parse(s) {
170 return bs;
171 }
172 }
173 if args.human_readable {
174 BlockSize::HumanBinary
175 } else if args.si {
176 BlockSize::HumanSi
177 } else {
178 BlockSize::Bytes(1)
179 }
180}
181
182fn resolve_hyperlink(args: &Args) -> HyperlinkWhen {
183 match args.hyperlink {
184 None => HyperlinkWhen::Never,
185 Some(crate::cli::ColorArg::Auto) => HyperlinkWhen::Auto,
186 Some(crate::cli::ColorArg::Always) => HyperlinkWhen::Always,
187 Some(crate::cli::ColorArg::Never) => HyperlinkWhen::Never,
188 }
189}
190
191fn resolve_cli_symlink(args: &Args) -> CliSymlinkMode {
192 if args.dereference {
193 return CliSymlinkMode::Never; }
196 if args.dereference_command_line {
197 CliSymlinkMode::Always
198 } else if args.dereference_command_line_symlink_to_dir {
199 CliSymlinkMode::DirOnly
200 } else {
201 CliSymlinkMode::Never
202 }
203}
204
205fn resolve_width(args: &Args) -> usize {
206 if let Some(w) = args.width {
207 return w;
208 }
209 terminal_width()
210}
211
212pub fn build_config(args: &Args) -> Config {
213 let is_stdout_tty = io::stdout().is_terminal();
214 let sort_by = resolve_sort(args);
215 let mut output = resolve_output(args);
216 output = apply_gnu_output(output, is_stdout_tty, args.gnu);
217
218 let mut all = args.all || args.unsorted_all;
219 let mut almost_all = args.almost_all;
220
221 let mut list = ListOptions {
222 all,
223 almost_all: almost_all || all,
224 sort_by,
225 reverse: args.reverse,
226 dirs_first: args.dirs_first,
228 recursive: (args.recursive || args.tree) && !args.directory,
229 max_depth: args.max_depth,
230 gnu_mode: args.gnu,
231 follow_links: args.dereference,
232 directory: args.directory,
233 ignore_backups: args.ignore_backups,
234 ignore_patterns: args.ignore.clone(),
235 hide_patterns: args.hide.clone(),
236 use_ignore_files: args.ignore_files && !args.no_ignore && !args.gnu,
237 list_archives: args.archive && !args.gnu && !args.directory,
238 time_field: resolve_time_field(args),
239 cli_symlink: resolve_cli_symlink(args),
240 parallel: args.threads != 1,
242 threads: args.threads,
243 collect_timing: args.profile,
244 resolve_owner_group: false,
246 read_selinux: false,
247 linux_statx: true,
248 io_uring: cfg!(feature = "io-uring"),
249 emit_dir_headers: true,
251 full_metadata: true,
253 };
254
255 apply_gnu_list_options(&mut list, args.gnu);
256
257 if all {
259 list.all = true;
260 list.almost_all = false;
261 } else if almost_all {
262 list.almost_all = true;
263 list.all = false;
264 }
265
266 let icons = if args.gnu || args.unsorted_all {
268 false
269 } else {
270 f00_core::IconsWhen::from(args.icons).enabled(is_stdout_tty)
271 };
272 let show_git = args.git && !args.gnu && !args.unsorted_all;
273
274 let show_owner = !args.no_owner;
276 let show_group = !(args.no_group || args.no_group_long);
277
278 if args.full_time && !matches!(output, OutputMode::Long) {
279 output = OutputMode::Long;
280 }
281 if args.dired && !matches!(output, OutputMode::Long | OutputMode::OnePerLine) {
283 }
285
286 let machine = matches!(output, OutputMode::Json | OutputMode::Csv | OutputMode::Tsv);
289 let needs_names = (matches!(output, OutputMode::Long)
290 && ((show_owner && !args.numeric_uid_gid) || (show_group && !args.numeric_uid_gid)))
291 || (machine && !args.numeric_uid_gid);
292 list.resolve_owner_group = needs_names || args.author || args.json_full;
293 list.read_selinux = args.context || args.json_full;
295 list.linux_statx = true;
296 list.io_uring = args.io_uring && cfg!(feature = "io-uring");
297 list.emit_dir_headers = !matches!(output, OutputMode::Tree);
299
300 let color_on = if args.zero {
303 false
304 } else {
305 f00_core::ColorWhen::from(args.color).enabled(is_stdout_tty)
306 };
307 let indicator = resolve_indicator(args, is_stdout_tty);
308 let needs_mode = color_on
309 || !matches!(indicator, f00_core::IndicatorStyle::None)
310 || args.classify.is_some()
311 || args.file_type
312 || args.indicator_slash;
313 let needs_size_or_time = matches!(sort_by, f00_core::SortBy::Size | f00_core::SortBy::Time)
314 || matches!(
315 output,
316 OutputMode::Long | OutputMode::Json | OutputMode::Csv | OutputMode::Tsv
317 )
318 || args.size_blocks
319 || args.inode
320 || args.human_readable
321 || args.si;
322 list.full_metadata = list.resolve_owner_group
323 || list.read_selinux
324 || needs_mode
325 || needs_size_or_time
326 || list.follow_links
327 || args.recursive
328 || args.tree;
329
330 let _ = (&mut all, &mut almost_all);
331
332 let color = if args.zero {
334 f00_core::ColorWhen::Never
335 } else {
336 args.color.into()
337 };
338
339 let mut hyperlink = resolve_hyperlink(args);
340 if args.zero {
341 hyperlink = HyperlinkWhen::Never;
342 }
343
344 Config {
345 list,
346 output,
347 color,
348 human_sizes: args.human_readable || args.si,
349 si_sizes: args.si,
350 icons,
351 classify: matches!(indicator, IndicatorStyle::Classify),
352 indicator,
353 terminal_width: resolve_width(args),
354 is_stdout_tty,
355 show_owner,
356 show_group,
357 numeric_uid_gid: args.numeric_uid_gid,
358 show_inode: args.inode,
359 show_blocks: args.size_blocks,
360 full_time: args.full_time,
361 show_git,
362 quoting_style: resolve_quoting(args),
363 control_chars: resolve_control_chars(args),
364 show_author: args.author,
365 block_size: resolve_block_size(args),
366 kibibytes: args.kibibytes,
367 tabsize: args.tabsize.unwrap_or(8),
368 hyperlink,
369 show_context: args.context,
370 zero: args.zero,
371 dired: args.dired,
372 time_style: resolve_time_style(args),
373 emit_block_total: true,
375 json_full: args.json_full,
376 }
377}
378
379pub fn prepare_args(mut args: Args, as_ls: bool) -> Result<Args> {
381 let file = load_user_config(args.config.as_deref())?;
382 resolve_args(&mut args, file.as_ref(), as_ls);
383 if args.gnu {
386 args.git = false;
387 args.icons = crate::cli::IconsArg::Never;
388 }
391 Ok(args)
392}
393
394pub fn run(args: Args) -> Result<i32> {
396 run_with_argv0(args, false)
397}
398
399pub fn run_with_argv0(args: Args, as_ls: bool) -> Result<i32> {
401 let args = prepare_args(args, as_ls)?;
402
403 if args.browse {
406 #[cfg(feature = "tui")]
407 {
408 let start = args
409 .paths
410 .first()
411 .map(PathBuf::as_path)
412 .unwrap_or_else(|| std::path::Path::new("."));
413 let is_tty = io::stdout().is_terminal();
414 let icons = !args.gnu && f00_core::IconsWhen::from(args.icons).enabled(is_tty);
415 let code = f00_tui::run_browser(
416 start,
417 f00_tui::BrowserOptions {
418 show_hidden: args.almost_all || args.all,
419 icons,
420 git: args.git && !args.gnu,
421 },
422 )?;
423 return Ok(code);
424 }
425 #[cfg(not(feature = "tui"))]
426 {
427 anyhow::bail!(
428 "interactive browser is the separate `f00-tui` binary \
429 (install from the same release, or: cargo install f00-tui). \
430 Embed with: cargo build -p f00 --features tui"
431 );
432 }
433 }
434
435 let config = build_config(&args);
436 let paths: Vec<PathBuf> = if args.paths.is_empty() {
437 vec![PathBuf::from(".")]
438 } else {
439 args.paths.clone()
440 };
441
442 let t_total = Instant::now();
443 let outcome = list_paths_with_archives(&paths, &config.list);
444 let core_timing = outcome.total_timing();
445
446 for err in &outcome.path_errors {
447 eprintln!("f00: {err}");
448 }
449
450 let exit_code = outcome.exit_code();
451 #[cfg(feature = "git")]
452 let listings = {
453 let mut listings = outcome.listings;
454 if config.show_git && args.git {
455 f00_git::annotate_listings(&mut listings);
456 }
457 #[cfg(feature = "plugins")]
458 {
459 crate::plugins_cmd::decorate_listings(listings)
460 }
461 #[cfg(not(feature = "plugins"))]
462 {
463 listings
464 }
465 };
466 #[cfg(all(not(feature = "git"), feature = "plugins"))]
467 let listings = crate::plugins_cmd::decorate_listings(outcome.listings);
468 #[cfg(all(not(feature = "git"), not(feature = "plugins")))]
469 let listings = outcome.listings;
470
471 let mut format_ms = 0u128;
472 if !listings.is_empty() {
473 let t_fmt = Instant::now();
474 let rendered = format_listings(&listings, &config).map_err(|e| anyhow::anyhow!(e))?;
475 format_ms = t_fmt.elapsed().as_millis();
476
477 let mut stdout = io::stdout().lock();
478 stdout
479 .write_all(rendered.as_bytes())
480 .context("writing output")?;
481 } else if exit_code == 0 {
482 eprintln!("f00: no listings produced");
483 return Ok(2);
484 }
485
486 if args.profile {
487 print_profile(&core_timing, format_ms, t_total.elapsed().as_millis());
488 }
489
490 Ok(exit_code)
491}
492
493fn print_profile(core: &ListTiming, format_ms: u128, total_ms: u128) {
494 eprintln!(
495 "f00 profile: readdir_ms={} stat_ms={} sort_ms={} format_ms={} total_ms={}",
496 core.readdir_ms, core.stat_ms, core.sort_ms, format_ms, total_ms
497 );
498}
499
500fn list_paths_with_archives(paths: &[PathBuf], opts: &ListOptions) -> ListOutcome {
502 #[cfg(feature = "archives")]
503 {
504 if opts.list_archives {
505 let mut listings = Vec::new();
506 let mut path_errors = Vec::new();
507 let mut minor = 0usize;
508 for p in paths {
509 if f00_archive::is_archive(p) {
510 match f00_archive::list_archive_as_listing(p) {
511 Ok(l) => {
512 minor += l.minor_errors;
513 listings.push(l);
514 }
515 Err(e) => path_errors.push(f00_core::Error::Io(std::io::Error::other(
516 format!("{}: {e}", p.display()),
517 ))),
518 }
519 } else {
520 match list_path(p, opts) {
521 Ok(l) => {
522 minor += l.minor_errors;
523 listings.push(l);
524 }
525 Err(e) => path_errors.push(e),
526 }
527 }
528 }
529 return ListOutcome {
530 listings,
531 path_errors,
532 minor_errors: minor,
533 };
534 }
535 }
536 let _ = opts.list_archives;
537 list_paths_with_errors(paths, opts)
538}