Skip to main content

f00_format/
lib.rs

1//! Display formatting for **f00**: columns, long listing, tree, JSON, icons, color, quoting.
2
3mod color;
4mod columns;
5mod csv;
6mod human;
7mod hyperlink;
8mod icons;
9mod json;
10mod long;
11mod perms;
12mod quoting;
13mod tree;
14
15pub use color::Colorizer;
16pub use columns::{
17    format_columns, format_columns_cfg, format_one_per_line, format_one_per_line_cfg,
18};
19pub use csv::{format_csv, format_tsv};
20pub use human::{
21    block_display, block_display_with_unit, format_size_bytes, human_size, human_size_si,
22};
23pub use hyperlink::hyperlink_name;
24pub use icons::{icon_for, icon_prefix};
25pub use json::format_json;
26pub use long::{
27    format_long, format_long_line, format_long_line_simple, format_long_simple, format_time_style,
28};
29pub use perms::{classify_suffix, classify_suffix_bool, format_permissions};
30pub use quoting::{display_name, quote_name};
31pub use tree::format_tree;
32
33use f00_core::{Config, Entry, Listing, OutputMode};
34
35/// Format a single listing according to config.
36pub fn format_listing(listing: &Listing, config: &Config) -> std::result::Result<String, String> {
37    let colorizer = Colorizer::new(config.color_enabled());
38    format_entries(&listing.entries, config, &colorizer)
39}
40
41/// Format multiple listings (multiple path arguments).
42pub fn format_listings(
43    listings: &[Listing],
44    config: &Config,
45) -> std::result::Result<String, String> {
46    let colorizer = Colorizer::new(config.color_enabled());
47    let multi = listings.len() > 1 || listings.iter().any(|l| l.root_is_dir && listings.len() > 1);
48
49    // JSON: combine all entries into one array.
50    if matches!(config.effective_output(), OutputMode::Json) {
51        let mut all = Vec::new();
52        for l in listings {
53            all.extend(l.entries.iter().cloned());
54        }
55        return format_json(&all).map_err(|e| e.to_string());
56    }
57
58    // CSV/TSV: combine entries.
59    if matches!(config.effective_output(), OutputMode::Csv | OutputMode::Tsv) {
60        let mut all = Vec::new();
61        for l in listings {
62            all.extend(l.entries.iter().cloned());
63        }
64        return Ok(match config.effective_output() {
65            OutputMode::Csv => format_csv(&all),
66            OutputMode::Tsv => format_tsv(&all),
67            _ => unreachable!(),
68        });
69    }
70
71    let mut out = String::new();
72    let ending = config.line_ending();
73    for (i, listing) in listings.iter().enumerate() {
74        if i > 0 {
75            if !config.zero {
76                out.push('\n');
77            } else {
78                // still separate with NUL if zero? GNU uses newline between dirs usually;
79                // with --zero, entries are NUL-separated; headers still appear.
80            }
81        }
82        // Print path header when multiple args list directory contents.
83        // Skip for `-d` (single entry that is the directory itself).
84        let is_dir_self = listing.root_is_dir
85            && listing.entries.len() == 1
86            && listing.entries[0].path == listing.root;
87        if (multi || listings.len() > 1) && listing.root_is_dir && !is_dir_self {
88            out.push_str(&format!("{}:", listing.root.display()));
89            out.push_str(ending);
90        }
91
92        // GNU prints `total N` only for directory *contents*, not file operands / `-d`.
93        let mut cfg = config.clone();
94        cfg.emit_block_total = listing.root_is_dir && !is_dir_self;
95        out.push_str(&format_entries(&listing.entries, &cfg, &colorizer)?);
96    }
97    Ok(out)
98}
99
100fn format_entries(
101    entries: &[Entry],
102    config: &Config,
103    colorizer: &Colorizer,
104) -> std::result::Result<String, String> {
105    let icons = config.icons;
106    let indicator = config.indicator_style();
107    match config.effective_output() {
108        OutputMode::Long => Ok(format_long(entries, colorizer, config)),
109        OutputMode::OnePerLine => Ok(format_one_per_line_cfg(entries, colorizer, config)),
110        OutputMode::Commas => Ok(format_commas(entries, colorizer, config)),
111        OutputMode::Json => format_json(entries).map_err(|e| e.to_string()),
112        OutputMode::Tree => Ok(format_tree(entries, colorizer, icons, indicator)),
113        OutputMode::Csv => Ok(format_csv(entries)),
114        OutputMode::Tsv => Ok(format_tsv(entries)),
115        OutputMode::Across => Ok(format_columns_cfg(entries, colorizer, config, true)),
116        OutputMode::Default | OutputMode::Columns => {
117            Ok(format_columns_cfg(entries, colorizer, config, false))
118        }
119    }
120}
121
122fn format_commas(entries: &[Entry], colorizer: &Colorizer, config: &Config) -> String {
123    let hide_ctrl = config.hide_control_chars();
124    let icons = config.icons;
125    let indicator = config.indicator_style();
126    let hyper = config.hyperlink_enabled();
127    let mut parts = Vec::new();
128    for entry in entries.iter().filter(|e| !e.is_dir_header) {
129        let icon = icon_prefix(entry, icons);
130        let suffix = classify_suffix(entry, indicator);
131        let quoted = display_name(&entry.name, config.quoting_style, hide_ctrl);
132        let plain = format!("{icon}{quoted}{suffix}");
133        let painted = colorizer.paint_name(entry, &plain);
134        parts.push(hyperlink_name(&entry.path, &painted, hyper));
135    }
136    let mut out = parts.join(", ");
137    if !out.is_empty() {
138        out.push_str(config.line_ending());
139    }
140    out
141}