1use chrono::{DateTime, Local};
2
3use f00_core::{Config, Entry, IndicatorStyle, TimeStyle};
4
5use crate::color::Colorizer;
6use crate::human::{block_display_with_unit, format_size_bytes};
7use crate::hyperlink::hyperlink_name;
8use crate::icons::icon_prefix;
9use crate::perms::{classify_suffix, format_permissions};
10use crate::quoting::display_name;
11
12pub fn format_long_line(
17 entry: &Entry,
18 colorizer: &Colorizer,
19 config: &Config,
20 widths: &LongWidths,
21) -> String {
22 format_long_line_dired(entry, colorizer, config, widths, None)
23}
24
25pub fn format_long_line_dired(
27 entry: &Entry,
28 colorizer: &Colorizer,
29 config: &Config,
30 widths: &LongWidths,
31 dired_offsets: Option<&mut Vec<(usize, usize)>>,
32) -> String {
33 let perms = format_permissions(entry);
34 let nlink = entry.nlink.to_string();
35 let owner = if config.show_owner {
36 entry.owner_display(config.numeric_uid_gid)
37 } else {
38 String::new()
39 };
40 let group = if config.show_group {
41 entry.group_display(config.numeric_uid_gid)
42 } else {
43 String::new()
44 };
45 let author = if config.show_author {
46 entry.author_display(config.numeric_uid_gid)
47 } else {
48 String::new()
49 };
50
51 let gnu = config.list.gnu_mode;
52 let size = format_size_field(entry, config);
53 let mtime = format_entry_time(entry, config);
54 let icon = icon_prefix(entry, config.icons);
55 let suffix = classify_suffix(entry, config.indicator_style());
56
57 let quoted = display_name(
58 &entry.name,
59 config.quoting_style,
60 config.hide_control_chars(),
61 );
62 let name_core = format!("{icon}{quoted}{suffix}");
63 let arrow_target = if let Some(target) = &entry.symlink_target {
64 let tq = display_name(
65 &target.display().to_string(),
66 config.quoting_style,
67 config.hide_control_chars(),
68 );
69 format!(" -> {tq}")
70 } else {
71 String::new()
72 };
73 let name_colored = if entry.symlink_target.is_some() {
74 colorizer.paint_symlink_name(entry, &name_core, &arrow_target, gnu)
75 } else {
76 colorizer.paint_name(entry, &name_core)
77 };
78 let name = hyperlink_name(&entry.path, &name_colored, config.hyperlink_enabled());
79
80 let git = if config.show_git {
81 entry
82 .git_status
83 .as_char()
84 .map(|c| format!(" {} ", colorizer.paint_git_char(c)))
85 .unwrap_or_else(|| " ".to_string())
86 } else {
87 String::new()
88 };
89
90 let mut parts = Vec::new();
92 if config.show_inode {
93 let s = format!("{:>w$}", entry.inode, w = widths.inode);
94 parts.push(colorizer.paint_meta(&s, gnu));
95 }
96 if config.show_blocks {
97 let s = format!(
98 "{:>w$}",
99 block_display_with_unit(entry.blocks, config.blocks_unit()),
100 w = widths.blocks
101 );
102 parts.push(colorizer.paint_meta(&s, gnu));
103 }
104 if config.show_context {
105 let ctx = if entry.context.is_empty() {
106 "?"
107 } else {
108 entry.context.as_str()
109 };
110 let s = format!("{:<w$}", ctx, w = widths.context);
111 parts.push(colorizer.paint_meta(&s, gnu));
112 }
113 parts.push(colorizer.paint_perms(&perms, gnu));
114 {
115 let s = format!("{:>w$}", nlink, w = widths.nlink);
116 parts.push(colorizer.paint_meta(&s, gnu));
117 }
118 if config.show_owner {
119 let s = format!("{:<w$}", owner, w = widths.owner);
120 parts.push(colorizer.paint_user(&s, gnu));
121 }
122 if config.show_group {
123 let s = format!("{:<w$}", group, w = widths.group);
124 parts.push(colorizer.paint_user(&s, gnu));
125 }
126 if config.show_author {
127 let s = format!("{:<w$}", author, w = widths.author);
128 parts.push(colorizer.paint_user(&s, gnu));
129 }
130 {
131 let s = format!("{:>w$}", size, w = widths.size);
132 parts.push(colorizer.paint_size(&s, entry.size, gnu));
133 }
134 parts.push(colorizer.paint_time(&mtime, gnu));
135 let prefix = parts.join(" ");
136 let line_without_name = format!("{prefix}{git} ");
138 if let Some(offsets) = dired_offsets {
139 let start = line_without_name.len();
140 let end = start + name.len();
144 offsets.push((start, end));
145 }
146 format!("{line_without_name}{name}")
147}
148
149fn format_size_field(entry: &Entry, config: &Config) -> String {
150 format_size_bytes(
151 entry.size,
152 config.block_size,
153 config.human_sizes,
154 config.si_sizes,
155 )
156}
157
158#[derive(Debug, Clone, Default)]
159pub struct LongWidths {
160 pub inode: usize,
161 pub blocks: usize,
162 pub context: usize,
163 pub nlink: usize,
164 pub owner: usize,
165 pub group: usize,
166 pub author: usize,
167 pub size: usize,
168}
169
170impl LongWidths {
171 pub fn compute(entries: &[Entry], config: &Config) -> Self {
172 let mut w = Self {
173 inode: 1,
174 blocks: 1,
175 context: 1,
176 nlink: 1,
177 owner: 1,
178 group: 1,
179 author: 1,
180 size: 1,
181 };
182 for e in entries.iter().filter(|e| !e.is_dir_header) {
183 w.inode = w.inode.max(decimal_digits(e.inode));
184 let blocks_n = block_display_with_unit(e.blocks, config.blocks_unit());
185 w.blocks = w.blocks.max(decimal_digits(blocks_n));
186 if config.show_context {
187 let ctx = if e.context.is_empty() {
188 "?"
189 } else {
190 e.context.as_str()
191 };
192 w.context = w.context.max(ctx.len());
193 }
194 w.nlink = w.nlink.max(decimal_digits(e.nlink));
195 if config.show_owner {
196 w.owner = w.owner.max(e.owner_display(config.numeric_uid_gid).len());
197 }
198 if config.show_group {
199 w.group = w.group.max(e.group_display(config.numeric_uid_gid).len());
200 }
201 if config.show_author {
202 w.author = w.author.max(e.author_display(config.numeric_uid_gid).len());
203 }
204 w.size = w.size.max(format_size_field(e, config).len());
205 }
206 w
207 }
208}
209
210fn decimal_digits(mut n: u64) -> usize {
212 if n == 0 {
213 return 1;
214 }
215 let mut d = 0;
216 while n > 0 {
217 n /= 10;
218 d += 1;
219 }
220 d
221}
222
223pub fn format_long(entries: &[Entry], colorizer: &Colorizer, config: &Config) -> String {
225 let widths = LongWidths::compute(entries, config);
226 let mut out = String::with_capacity(entries.len().saturating_mul(96));
228 let mut dired_global: Vec<(usize, usize)> = Vec::new();
229 let ending = config.line_ending();
230
231 let write_total = |out: &mut String, section: &[Entry]| {
232 if config.zero || !config.emit_block_total {
233 return;
234 }
235 let total: u64 = section
236 .iter()
237 .filter(|e| !e.is_dir_header)
238 .map(|e| crate::human::block_display_with_unit(e.blocks, config.blocks_unit()))
239 .fold(0u64, u64::saturating_add);
240 out.push_str(&format!("total {total}"));
241 out.push_str(ending);
242 };
243
244 let write_entry = |out: &mut String, dired_global: &mut Vec<(usize, usize)>, entry: &Entry| {
245 if config.dired {
246 let base = out.len();
247 let mut local = Vec::new();
248 let line = format_long_line_dired(entry, colorizer, config, &widths, Some(&mut local));
249 for (s, e) in local {
250 dired_global.push((base + s, base + e));
251 }
252 out.push_str(&line);
253 out.push_str(ending);
254 } else {
255 out.push_str(&format_long_line(entry, colorizer, config, &widths));
256 out.push_str(ending);
257 }
258 };
259
260 let mut i = 0;
263 let has_headers = entries.iter().any(|e| e.is_dir_header);
264 if !has_headers {
265 write_total(&mut out, entries);
266 for entry in entries {
267 write_entry(&mut out, &mut dired_global, entry);
268 }
269 } else {
270 while i < entries.len() {
271 if entries[i].is_dir_header {
272 if !out.is_empty() {
273 out.push_str(if config.zero { "\0" } else { "\n" });
274 }
275 out.push_str(&format!("{}:", entries[i].path.display()));
276 out.push_str(ending);
277 i += 1;
278 }
279 let start = i;
280 while i < entries.len() && !entries[i].is_dir_header {
281 i += 1;
282 }
283 let section = &entries[start..i];
284 write_total(&mut out, section);
285 for entry in section {
286 write_entry(&mut out, &mut dired_global, entry);
287 }
288 }
289 }
290
291 if config.dired && !config.zero {
292 out.push_str("//DIRED//");
294 for (s, e) in &dired_global {
295 out.push_str(&format!(" {s} {e}"));
296 }
297 out.push('\n');
298 out.push_str(&format!(
299 "//DIRED-OPTIONS// --quoting-style={}\n",
300 quoting_style_word(config.quoting_style)
301 ));
302 }
303 out
304}
305
306fn quoting_style_word(style: f00_core::QuotingStyle) -> &'static str {
307 use f00_core::QuotingStyle::*;
308 match style {
309 Literal => "literal",
310 Locale => "locale",
311 Shell => "shell",
312 ShellAlways => "shell-always",
313 ShellEscape => "shell-escape",
314 ShellEscapeAlways => "shell-escape-always",
315 C => "c",
316 Escape => "escape",
317 }
318}
319
320pub fn format_long_simple(
322 entries: &[Entry],
323 colorizer: &Colorizer,
324 human: bool,
325 icons: bool,
326 classify: bool,
327) -> String {
328 let mut config = Config {
329 human_sizes: human,
330 icons,
331 show_git: false,
332 show_owner: true,
333 show_group: true,
334 ..Config::default()
335 };
336 if classify {
337 config.classify = true;
338 config.indicator = IndicatorStyle::Classify;
339 }
340 format_long(entries, colorizer, &config)
341}
342
343pub fn format_long_line_simple(
345 entry: &Entry,
346 colorizer: &Colorizer,
347 human: bool,
348 icons: bool,
349 classify: bool,
350 size_width: usize,
351) -> String {
352 let mut config = Config {
353 human_sizes: human,
354 icons,
355 show_git: false,
356 ..Config::default()
357 };
358 if classify {
359 config.classify = true;
360 }
361 let widths = LongWidths {
362 size: size_width,
363 nlink: 1,
364 owner: 1,
365 group: 1,
366 author: 1,
367 inode: 1,
368 blocks: 1,
369 context: 1,
370 };
371 format_long_line(entry, colorizer, &config, &widths)
372}
373
374fn format_entry_time(entry: &Entry, config: &Config) -> String {
375 let style = if config.full_time {
376 TimeStyle::FullIso
377 } else {
378 config.time_style.clone()
379 };
380 let field = config.list.time_field;
381 match entry.datetime_for(field) {
382 Some(dt) => format_time_style(dt, &style),
383 None => " ".to_string(),
384 }
385}
386
387pub fn format_time_style(dt: DateTime<Local>, style: &TimeStyle) -> String {
388 match style {
389 TimeStyle::FullIso => dt.format("%Y-%m-%d %H:%M:%S.%f %z").to_string(),
390 TimeStyle::LongIso => dt.format("%Y-%m-%d %H:%M").to_string(),
391 TimeStyle::Iso => {
392 let now = Local::now();
393 let six_months = chrono::Duration::days(365 / 2);
394 if (now - dt).abs() > six_months {
395 dt.format("%Y-%m-%d").to_string()
396 } else {
397 dt.format("%m-%d %H:%M").to_string()
398 }
399 }
400 TimeStyle::Locale => format_ls_time(dt),
401 TimeStyle::Format(fmt) => dt.format(fmt).to_string(),
402 }
403}
404
405fn format_ls_time(dt: DateTime<Local>) -> String {
407 let now = Local::now();
408 let six_months = chrono::Duration::days(365 / 2);
409 if (now - dt).abs() > six_months {
410 dt.format("%b %e %Y").to_string()
411 } else {
412 dt.format("%b %e %H:%M").to_string()
413 }
414}