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 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
120pub 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
141pub 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 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 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}