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 {
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) -> 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 args.classify {
111 IndicatorStyle::Classify
112 } else if args.file_type {
113 IndicatorStyle::FileType
114 } else if args.indicator_slash {
115 IndicatorStyle::Slash
116 } else {
117 IndicatorStyle::None
118 }
119}
120
121fn resolve_quoting(args: &Args) -> QuotingStyle {
122 if args.literal {
123 return QuotingStyle::Literal;
124 }
125 if args.escape {
126 return QuotingStyle::Escape;
127 }
128 if args.quote_name {
129 return QuotingStyle::C;
130 }
131 if let Some(ref word) = args.quoting_style {
132 if let Some(s) = QuotingStyle::parse(word) {
133 return s;
134 }
135 }
136 QuotingStyle::from_env().unwrap_or(QuotingStyle::Literal)
137}
138
139fn resolve_control_chars(args: &Args) -> ControlChars {
140 if args.hide_control_chars {
141 ControlChars::Hide
142 } else if args.show_control_chars {
143 ControlChars::Show
144 } else {
145 ControlChars::Auto
146 }
147}
148
149fn resolve_time_style(args: &Args) -> TimeStyle {
150 if args.full_time {
151 return TimeStyle::FullIso;
152 }
153 if let Some(ref s) = args.time_style {
154 if let Some(ts) = TimeStyle::parse(s) {
155 return ts;
156 }
157 }
158 TimeStyle::from_env().unwrap_or(TimeStyle::Locale)
159}
160
161fn resolve_block_size(args: &Args) -> BlockSize {
162 if let Some(ref s) = args.block_size {
163 if let Some(bs) = BlockSize::parse(s) {
164 return bs;
165 }
166 }
167 if args.human_readable {
168 BlockSize::HumanBinary
169 } else if args.si {
170 BlockSize::HumanSi
171 } else {
172 BlockSize::Bytes(1)
173 }
174}
175
176fn resolve_hyperlink(args: &Args) -> HyperlinkWhen {
177 match args.hyperlink.as_deref() {
178 None => HyperlinkWhen::Never,
179 Some(s) => HyperlinkWhen::parse(s).unwrap_or(HyperlinkWhen::Always),
180 }
181}
182
183fn resolve_cli_symlink(args: &Args) -> CliSymlinkMode {
184 if args.dereference {
185 return CliSymlinkMode::Never; }
188 if args.dereference_command_line {
189 CliSymlinkMode::Always
190 } else if args.dereference_command_line_symlink_to_dir {
191 CliSymlinkMode::DirOnly
192 } else {
193 CliSymlinkMode::Never
194 }
195}
196
197fn resolve_width(args: &Args) -> usize {
198 if let Some(w) = args.width {
199 return w;
200 }
201 terminal_width()
202}
203
204pub fn build_config(args: &Args) -> Config {
205 let is_stdout_tty = io::stdout().is_terminal();
206 let sort_by = resolve_sort(args);
207 let mut output = resolve_output(args);
208 output = apply_gnu_output(output, is_stdout_tty, args.gnu);
209
210 let mut all = args.all || args.unsorted_all;
211 let mut almost_all = args.almost_all;
212
213 let mut list = ListOptions {
214 all,
215 almost_all: almost_all || all,
216 sort_by,
217 reverse: args.reverse,
218 dirs_first: args.dirs_first && !args.gnu,
219 recursive: (args.recursive || args.tree) && !args.directory,
220 max_depth: args.max_depth,
221 gnu_mode: args.gnu,
222 follow_links: args.dereference,
223 directory: args.directory,
224 ignore_backups: args.ignore_backups,
225 ignore_patterns: args.ignore.clone(),
226 hide_patterns: args.hide.clone(),
227 use_ignore_files: args.ignore_files && !args.no_ignore && !args.gnu,
228 list_archives: args.archive && !args.gnu && !args.directory,
229 time_field: resolve_time_field(args),
230 cli_symlink: resolve_cli_symlink(args),
231 parallel: args.threads != 1,
233 threads: args.threads,
234 collect_timing: args.profile,
235 resolve_owner_group: false,
237 read_selinux: false,
238 linux_statx: true,
239 io_uring: cfg!(feature = "io-uring"),
240 };
241
242 apply_gnu_list_options(&mut list, args.gnu);
243
244 if all {
246 list.all = true;
247 list.almost_all = false;
248 } else if almost_all {
249 list.almost_all = true;
250 list.all = false;
251 }
252
253 let icons = if args.gnu || args.unsorted_all {
255 false
256 } else {
257 f00_core::IconsWhen::from(args.icons).enabled(is_stdout_tty)
258 };
259 let show_git = args.git && !args.gnu && !args.unsorted_all;
260
261 let show_owner = !args.no_owner;
263 let show_group = !(args.no_group || args.no_group_long);
264
265 if args.full_time && !matches!(output, OutputMode::Long) {
266 output = OutputMode::Long;
267 }
268 if args.dired && !matches!(output, OutputMode::Long | OutputMode::OnePerLine) {
270 }
272
273 let machine = matches!(output, OutputMode::Json | OutputMode::Csv | OutputMode::Tsv);
276 let needs_names = (matches!(output, OutputMode::Long)
277 && ((show_owner && !args.numeric_uid_gid) || (show_group && !args.numeric_uid_gid)))
278 || (machine && !args.numeric_uid_gid);
279 list.resolve_owner_group = needs_names || args.author;
280 list.read_selinux = args.context;
281 list.linux_statx = true;
282 list.io_uring = args.io_uring && cfg!(feature = "io-uring");
283
284 let _ = (&mut all, &mut almost_all);
285
286 let color = if args.zero {
288 f00_core::ColorWhen::Never
289 } else {
290 args.color.into()
291 };
292
293 let mut hyperlink = resolve_hyperlink(args);
294 if args.zero {
295 hyperlink = HyperlinkWhen::Never;
296 }
297
298 Config {
299 list,
300 output,
301 color,
302 human_sizes: args.human_readable || args.si,
303 si_sizes: args.si,
304 icons,
305 classify: args.classify,
306 indicator: resolve_indicator(args),
307 terminal_width: resolve_width(args),
308 is_stdout_tty,
309 show_owner,
310 show_group,
311 numeric_uid_gid: args.numeric_uid_gid,
312 show_inode: args.inode,
313 show_blocks: args.size_blocks,
314 full_time: args.full_time,
315 show_git,
316 quoting_style: resolve_quoting(args),
317 control_chars: resolve_control_chars(args),
318 show_author: args.author,
319 block_size: resolve_block_size(args),
320 kibibytes: args.kibibytes,
321 tabsize: args.tabsize.unwrap_or(8),
322 hyperlink,
323 show_context: args.context,
324 zero: args.zero,
325 dired: args.dired,
326 time_style: resolve_time_style(args),
327 }
328}
329
330pub fn prepare_args(mut args: Args, as_ls: bool) -> Result<Args> {
332 let file = load_user_config(args.config.as_deref())?;
333 resolve_args(&mut args, file.as_ref(), as_ls);
334 if args.gnu {
337 args.git = false;
338 args.icons = crate::cli::IconsArg::Never;
339 args.dirs_first = false;
340 }
341 Ok(args)
342}
343
344pub fn run(args: Args) -> Result<i32> {
346 run_with_argv0(args, false)
347}
348
349pub fn run_with_argv0(args: Args, as_ls: bool) -> Result<i32> {
351 let args = prepare_args(args, as_ls)?;
352
353 if args.browse {
355 #[cfg(feature = "tui")]
356 {
357 let start = args
358 .paths
359 .first()
360 .map(PathBuf::as_path)
361 .unwrap_or_else(|| std::path::Path::new("."));
362 let is_tty = io::stdout().is_terminal();
363 let icons = !args.gnu && f00_core::IconsWhen::from(args.icons).enabled(is_tty);
364 let code = f00_tui::run_browser(
365 start,
366 f00_tui::BrowserOptions {
367 show_hidden: args.almost_all || args.all,
368 icons,
369 git: args.git && !args.gnu,
370 },
371 )?;
372 return Ok(code);
373 }
374 #[cfg(not(feature = "tui"))]
375 {
376 anyhow::bail!("TUI browser requires building with --features tui");
377 }
378 }
379
380 let config = build_config(&args);
381 let paths: Vec<PathBuf> = if args.paths.is_empty() {
382 vec![PathBuf::from(".")]
383 } else {
384 args.paths.clone()
385 };
386
387 let t_total = Instant::now();
388 let outcome = list_paths_with_archives(&paths, &config.list);
389 let core_timing = outcome.total_timing();
390
391 for err in &outcome.path_errors {
392 eprintln!("f00: {err}");
393 }
394
395 let exit_code = outcome.exit_code();
396 #[cfg(feature = "git")]
397 let listings = {
398 let mut listings = outcome.listings;
399 if config.show_git && args.git {
400 f00_git::annotate_listings(&mut listings);
401 }
402 #[cfg(feature = "plugins")]
403 {
404 crate::plugins_cmd::decorate_listings(listings)
405 }
406 #[cfg(not(feature = "plugins"))]
407 {
408 listings
409 }
410 };
411 #[cfg(all(not(feature = "git"), feature = "plugins"))]
412 let listings = crate::plugins_cmd::decorate_listings(outcome.listings);
413 #[cfg(all(not(feature = "git"), not(feature = "plugins")))]
414 let listings = outcome.listings;
415
416 let mut format_ms = 0u128;
417 if !listings.is_empty() {
418 let t_fmt = Instant::now();
419 let rendered = format_listings(&listings, &config).map_err(|e| anyhow::anyhow!(e))?;
420 format_ms = t_fmt.elapsed().as_millis();
421
422 let mut stdout = io::stdout().lock();
423 stdout
424 .write_all(rendered.as_bytes())
425 .context("writing output")?;
426 } else if exit_code == 0 {
427 eprintln!("f00: no listings produced");
428 return Ok(2);
429 }
430
431 if args.profile {
432 print_profile(&core_timing, format_ms, t_total.elapsed().as_millis());
433 }
434
435 Ok(exit_code)
436}
437
438fn print_profile(core: &ListTiming, format_ms: u128, total_ms: u128) {
439 eprintln!(
440 "f00 profile: readdir_ms={} stat_ms={} sort_ms={} format_ms={} total_ms={}",
441 core.readdir_ms, core.stat_ms, core.sort_ms, format_ms, total_ms
442 );
443}
444
445fn list_paths_with_archives(paths: &[PathBuf], opts: &ListOptions) -> ListOutcome {
447 #[cfg(feature = "archives")]
448 {
449 if opts.list_archives {
450 let mut listings = Vec::new();
451 let mut path_errors = Vec::new();
452 let mut minor = 0usize;
453 for p in paths {
454 if f00_archive::is_archive(p) {
455 match f00_archive::list_archive_as_listing(p) {
456 Ok(l) => {
457 minor += l.minor_errors;
458 listings.push(l);
459 }
460 Err(e) => path_errors.push(f00_core::Error::Io(std::io::Error::other(
461 format!("{}: {e}", p.display()),
462 ))),
463 }
464 } else {
465 match list_path(p, opts) {
466 Ok(l) => {
467 minor += l.minor_errors;
468 listings.push(l);
469 }
470 Err(e) => path_errors.push(e),
471 }
472 }
473 }
474 return ListOutcome {
475 listings,
476 path_errors,
477 minor_errors: minor,
478 };
479 }
480 }
481 let _ = opts.list_archives;
482 list_paths_with_errors(paths, opts)
483}