Skip to main content

f00_format/
columns.rs

1use f00_core::{Config, Entry, IndicatorStyle};
2use unicode_width::UnicodeWidthStr;
3
4use crate::color::Colorizer;
5use crate::human::block_display_with_unit;
6use crate::hyperlink::hyperlink_name;
7use crate::icons::icon_prefix;
8use crate::perms::classify_suffix;
9use crate::quoting::display_name;
10
11struct Prepared {
12    plain: String,
13    painted: String,
14    width: usize,
15}
16
17fn prepare_entries(entries: &[Entry], colorizer: &Colorizer, config: &Config) -> Vec<Prepared> {
18    let icons = config.icons;
19    let indicator = config.indicator_style();
20    let hide_ctrl = config.hide_control_chars();
21    let hyper = config.hyperlink_enabled();
22
23    entries
24        .iter()
25        .filter(|e| !e.is_dir_header)
26        .map(|e| {
27            let icon = icon_prefix(e, icons);
28            let suffix = classify_suffix(e, indicator);
29            let quoted = display_name(&e.name, config.quoting_style, hide_ctrl);
30            let mut plain = format!("{icon}{quoted}{suffix}");
31            if config.show_inode {
32                plain = format!("{} {plain}", e.inode);
33            }
34            if config.show_blocks {
35                let b = block_display_with_unit(e.blocks, config.blocks_unit());
36                plain = format!("{b} {plain}");
37            }
38            if config.show_context {
39                let ctx = if e.context.is_empty() {
40                    "?"
41                } else {
42                    e.context.as_str()
43                };
44                plain = format!("{ctx} {plain}");
45            }
46            let width = UnicodeWidthStr::width(plain.as_str());
47            let painted = colorizer.paint_name(e, &plain);
48            let painted = hyperlink_name(&e.path, &painted, hyper);
49            Prepared {
50                plain,
51                painted,
52                width,
53            }
54        })
55        .collect()
56}
57
58/// One entry per line.
59pub fn format_one_per_line(
60    entries: &[Entry],
61    colorizer: &Colorizer,
62    icons: bool,
63    indicator: IndicatorStyle,
64) -> String {
65    // Backward-compatible path without full Config.
66    let mut config = Config {
67        icons,
68        indicator,
69        classify: matches!(indicator, IndicatorStyle::Classify),
70        ..Config::default()
71    };
72    if matches!(indicator, IndicatorStyle::Classify) {
73        config.classify = true;
74    }
75    format_one_per_line_cfg(entries, colorizer, &config)
76}
77
78/// One entry per line with full config (quoting, zero, inode, etc.).
79pub fn format_one_per_line_cfg(
80    entries: &[Entry],
81    colorizer: &Colorizer,
82    config: &Config,
83) -> String {
84    let ending = config.line_ending();
85    let mut out = String::new();
86    let mut dired: Vec<(usize, usize)> = Vec::new();
87
88    for entry in entries {
89        if entry.is_dir_header {
90            if !out.is_empty() && !config.zero {
91                out.push('\n');
92            }
93            out.push_str(&format!("{}:", entry.path.display()));
94            out.push_str(ending);
95            continue;
96        }
97        let prepared = prepare_entries(std::slice::from_ref(entry), colorizer, config);
98        let p = &prepared[0];
99        if config.dired {
100            let start = out.len();
101            out.push_str(&p.painted);
102            let end = out.len();
103            dired.push((start, end));
104        } else {
105            out.push_str(&p.painted);
106        }
107        out.push_str(ending);
108    }
109
110    if config.dired && !config.zero {
111        out.push_str("//DIRED//");
112        for (s, e) in &dired {
113            out.push_str(&format!(" {s} {e}"));
114        }
115        out.push('\n');
116    }
117    out
118}
119
120/// Multi-column layout similar to `ls` default (column-major).
121pub fn format_columns(
122    entries: &[Entry],
123    colorizer: &Colorizer,
124    icons: bool,
125    indicator: IndicatorStyle,
126    terminal_width: usize,
127) -> String {
128    let mut config = Config {
129        icons,
130        indicator,
131        terminal_width,
132        classify: matches!(indicator, IndicatorStyle::Classify),
133        ..Config::default()
134    };
135    if matches!(indicator, IndicatorStyle::Classify) {
136        config.classify = true;
137    }
138    format_columns_cfg(entries, colorizer, &config, false)
139}
140
141/// Multi-column with full config. `across` selects row-major (`-x`).
142pub fn format_columns_cfg(
143    entries: &[Entry],
144    colorizer: &Colorizer,
145    config: &Config,
146    across: bool,
147) -> String {
148    let mut out = String::new();
149    let mut section: Vec<&Entry> = Vec::new();
150    let ending = config.line_ending();
151
152    let flush = |section: &mut Vec<&Entry>, out: &mut String| {
153        if section.is_empty() {
154            return;
155        }
156        let owned: Vec<Entry> = section.iter().map(|e| (*e).clone()).collect();
157        out.push_str(&format_columns_section(&owned, colorizer, config, across));
158        section.clear();
159    };
160
161    for entry in entries {
162        if entry.is_dir_header {
163            flush(&mut section, &mut out);
164            if !out.is_empty() && !config.zero {
165                out.push('\n');
166            }
167            out.push_str(&format!("{}:", entry.path.display()));
168            out.push_str(ending);
169        } else {
170            section.push(entry);
171        }
172    }
173    flush(&mut section, &mut out);
174    out
175}
176
177fn format_columns_section(
178    entries: &[Entry],
179    colorizer: &Colorizer,
180    config: &Config,
181    across: bool,
182) -> String {
183    let prepared = prepare_entries(entries, colorizer, config);
184    if prepared.is_empty() {
185        return String::new();
186    }
187
188    // Width 0 ⇒ unlimited (use a huge value).
189    let term_w = if config.terminal_width == 0 {
190        usize::MAX / 4
191    } else {
192        config.terminal_width.max(1)
193    };
194    let n = prepared.len();
195    let max_w = prepared.iter().map(|p| p.width).max().unwrap_or(1);
196    let col_gap = 2;
197    let ending = config.line_ending();
198
199    // With --zero, GNU falls back to one-per-line-ish; keep simple.
200    if config.zero {
201        let mut out = String::new();
202        for p in &prepared {
203            out.push_str(&p.painted);
204            out.push_str(ending);
205        }
206        return out;
207    }
208
209    let max_cols = ((term_w + col_gap) / (1 + col_gap)).max(1).min(n);
210
211    let mut best_cols = 1;
212    let mut best_rows = n;
213    let mut col_widths = vec![max_w];
214
215    for cols in (1..=max_cols).rev() {
216        let rows = n.div_ceil(cols);
217        let mut widths = vec![0usize; cols];
218        for (i, p) in prepared.iter().enumerate() {
219            let col = if across { i % cols } else { i / rows };
220            if col < cols {
221                widths[col] = widths[col].max(p.width);
222            }
223        }
224        let total: usize = widths.iter().sum::<usize>() + col_gap * cols.saturating_sub(1);
225        if total <= term_w {
226            best_cols = cols;
227            best_rows = rows;
228            col_widths = widths;
229            break;
230        }
231    }
232
233    let mut out = String::new();
234    for row in 0..best_rows {
235        for (col, col_width) in col_widths.iter().copied().enumerate().take(best_cols) {
236            let idx = if across {
237                row * best_cols + col
238            } else {
239                col * best_rows + row
240            };
241            if idx >= n {
242                continue;
243            }
244            let p = &prepared[idx];
245            out.push_str(&p.painted);
246            if col + 1 < best_cols {
247                let next_idx = if across {
248                    row * best_cols + col + 1
249                } else {
250                    (col + 1) * best_rows + row
251                };
252                if next_idx < n {
253                    let pad = col_width + col_gap - p.width;
254                    out.push_str(&" ".repeat(pad));
255                }
256            }
257        }
258        out.push_str(ending);
259    }
260    let _ = &prepared.iter().map(|p| &p.plain);
261    out
262}