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
58pub fn format_one_per_line(
60 entries: &[Entry],
61 colorizer: &Colorizer,
62 icons: bool,
63 indicator: IndicatorStyle,
64) -> String {
65 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
78pub 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 let write_total = |out: &mut String, section: &[Entry]| {
89 if !config.show_blocks || config.zero || !config.emit_block_total {
91 return;
92 }
93 let total: u64 = section
94 .iter()
95 .filter(|e| !e.is_dir_header)
96 .map(|e| crate::human::block_display_with_unit(e.blocks, config.blocks_unit()))
97 .fold(0u64, u64::saturating_add);
98 out.push_str(&format!("total {total}"));
99 out.push_str(ending);
100 };
101
102 let write_entry = |out: &mut String, dired: &mut Vec<(usize, usize)>, entry: &Entry| {
103 let prepared = prepare_entries(std::slice::from_ref(entry), colorizer, config);
104 let p = &prepared[0];
105 if config.dired {
106 let start = out.len();
107 out.push_str(&p.painted);
108 let end = out.len();
109 dired.push((start, end));
110 } else {
111 out.push_str(&p.painted);
112 }
113 out.push_str(ending);
114 };
115
116 let has_headers = entries.iter().any(|e| e.is_dir_header);
117 if !has_headers {
118 write_total(&mut out, entries);
119 for entry in entries {
120 write_entry(&mut out, &mut dired, entry);
121 }
122 } else {
123 let mut i = 0;
124 while i < entries.len() {
125 if entries[i].is_dir_header {
126 if !out.is_empty() && !config.zero {
127 out.push('\n');
128 }
129 out.push_str(&format!("{}:", entries[i].path.display()));
130 out.push_str(ending);
131 i += 1;
132 }
133 let start = i;
134 while i < entries.len() && !entries[i].is_dir_header {
135 i += 1;
136 }
137 let section = &entries[start..i];
138 write_total(&mut out, section);
139 for entry in section {
140 write_entry(&mut out, &mut dired, entry);
141 }
142 }
143 }
144
145 if config.dired && !config.zero {
146 out.push_str("//DIRED//");
147 for (s, e) in &dired {
148 out.push_str(&format!(" {s} {e}"));
149 }
150 out.push('\n');
151 }
152 out
153}
154
155pub fn format_columns(
157 entries: &[Entry],
158 colorizer: &Colorizer,
159 icons: bool,
160 indicator: IndicatorStyle,
161 terminal_width: usize,
162) -> String {
163 let mut config = Config {
164 icons,
165 indicator,
166 terminal_width,
167 classify: matches!(indicator, IndicatorStyle::Classify),
168 ..Config::default()
169 };
170 if matches!(indicator, IndicatorStyle::Classify) {
171 config.classify = true;
172 }
173 format_columns_cfg(entries, colorizer, &config, false)
174}
175
176pub fn format_columns_cfg(
178 entries: &[Entry],
179 colorizer: &Colorizer,
180 config: &Config,
181 across: bool,
182) -> String {
183 let mut out = String::new();
184 let mut section: Vec<&Entry> = Vec::new();
185 let ending = config.line_ending();
186
187 let flush = |section: &mut Vec<&Entry>, out: &mut String| {
188 if section.is_empty() {
189 return;
190 }
191 let owned: Vec<Entry> = section.iter().map(|e| (*e).clone()).collect();
192 out.push_str(&format_columns_section(&owned, colorizer, config, across));
193 section.clear();
194 };
195
196 for entry in entries {
197 if entry.is_dir_header {
198 flush(&mut section, &mut out);
199 if !out.is_empty() && !config.zero {
200 out.push('\n');
201 }
202 out.push_str(&format!("{}:", entry.path.display()));
203 out.push_str(ending);
204 } else {
205 section.push(entry);
206 }
207 }
208 flush(&mut section, &mut out);
209 out
210}
211
212fn format_columns_section(
213 entries: &[Entry],
214 colorizer: &Colorizer,
215 config: &Config,
216 across: bool,
217) -> String {
218 let prepared = prepare_entries(entries, colorizer, config);
219 if prepared.is_empty() {
220 return String::new();
221 }
222
223 let term_w = if config.terminal_width == 0 {
225 usize::MAX / 4
226 } else {
227 config.terminal_width.max(1)
228 };
229 let n = prepared.len();
230 let max_w = prepared.iter().map(|p| p.width).max().unwrap_or(1);
231 let col_gap = 2;
232 let ending = config.line_ending();
233
234 if config.zero {
236 let mut out = String::new();
237 for p in &prepared {
238 out.push_str(&p.painted);
239 out.push_str(ending);
240 }
241 return out;
242 }
243
244 let max_cols = ((term_w + col_gap) / (1 + col_gap)).max(1).min(n);
245
246 let mut best_cols = 1;
247 let mut best_rows = n;
248 let mut col_widths = vec![max_w];
249
250 for cols in (1..=max_cols).rev() {
251 let rows = n.div_ceil(cols);
252 let mut widths = vec![0usize; cols];
253 for (i, p) in prepared.iter().enumerate() {
254 let col = if across { i % cols } else { i / rows };
255 if col < cols {
256 widths[col] = widths[col].max(p.width);
257 }
258 }
259 let total: usize = widths.iter().sum::<usize>() + col_gap * cols.saturating_sub(1);
260 if total <= term_w {
261 best_cols = cols;
262 best_rows = rows;
263 col_widths = widths;
264 break;
265 }
266 }
267
268 let mut out = String::new();
269 for row in 0..best_rows {
270 for (col, col_width) in col_widths.iter().copied().enumerate().take(best_cols) {
271 let idx = if across {
272 row * best_cols + col
273 } else {
274 col * best_rows + row
275 };
276 if idx >= n {
277 continue;
278 }
279 let p = &prepared[idx];
280 out.push_str(&p.painted);
281 if col + 1 < best_cols {
282 let next_idx = if across {
283 row * best_cols + col + 1
284 } else {
285 (col + 1) * best_rows + row
286 };
287 if next_idx < n {
288 let pad = col_width + col_gap - p.width;
289 out.push_str(&" ".repeat(pad));
290 }
291 }
292 }
293 out.push_str(ending);
294 }
295 let _ = &prepared.iter().map(|p| &p.plain);
296 out
297}