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 for entry in entries {
232 if entry.is_dir_header {
233 if !out.is_empty() {
234 out.push_str(if config.zero { "\0" } else { "\n" });
235 }
236 out.push_str(&format!("{}:", entry.path.display()));
237 out.push_str(ending);
238 continue;
239 }
240 if config.dired {
241 let base = out.len();
242 let mut local = Vec::new();
243 let line = format_long_line_dired(entry, colorizer, config, &widths, Some(&mut local));
244 for (s, e) in local {
245 dired_global.push((base + s, base + e));
246 }
247 out.push_str(&line);
248 out.push_str(ending);
249 } else {
250 out.push_str(&format_long_line(entry, colorizer, config, &widths));
251 out.push_str(ending);
252 }
253 }
254
255 if config.dired && !config.zero {
256 out.push_str("//DIRED//");
258 for (s, e) in &dired_global {
259 out.push_str(&format!(" {s} {e}"));
260 }
261 out.push('\n');
262 out.push_str(&format!(
263 "//DIRED-OPTIONS// --quoting-style={}\n",
264 quoting_style_word(config.quoting_style)
265 ));
266 }
267 out
268}
269
270fn quoting_style_word(style: f00_core::QuotingStyle) -> &'static str {
271 use f00_core::QuotingStyle::*;
272 match style {
273 Literal => "literal",
274 Locale => "locale",
275 Shell => "shell",
276 ShellAlways => "shell-always",
277 ShellEscape => "shell-escape",
278 ShellEscapeAlways => "shell-escape-always",
279 C => "c",
280 Escape => "escape",
281 }
282}
283
284pub fn format_long_simple(
286 entries: &[Entry],
287 colorizer: &Colorizer,
288 human: bool,
289 icons: bool,
290 classify: bool,
291) -> String {
292 let mut config = Config {
293 human_sizes: human,
294 icons,
295 show_git: false,
296 show_owner: true,
297 show_group: true,
298 ..Config::default()
299 };
300 if classify {
301 config.classify = true;
302 config.indicator = IndicatorStyle::Classify;
303 }
304 format_long(entries, colorizer, &config)
305}
306
307pub fn format_long_line_simple(
309 entry: &Entry,
310 colorizer: &Colorizer,
311 human: bool,
312 icons: bool,
313 classify: bool,
314 size_width: usize,
315) -> String {
316 let mut config = Config {
317 human_sizes: human,
318 icons,
319 show_git: false,
320 ..Config::default()
321 };
322 if classify {
323 config.classify = true;
324 }
325 let widths = LongWidths {
326 size: size_width,
327 nlink: 1,
328 owner: 1,
329 group: 1,
330 author: 1,
331 inode: 1,
332 blocks: 1,
333 context: 1,
334 };
335 format_long_line(entry, colorizer, &config, &widths)
336}
337
338fn format_entry_time(entry: &Entry, config: &Config) -> String {
339 let style = if config.full_time {
340 TimeStyle::FullIso
341 } else {
342 config.time_style.clone()
343 };
344 let field = config.list.time_field;
345 match entry.datetime_for(field) {
346 Some(dt) => format_time_style(dt, &style),
347 None => " ".to_string(),
348 }
349}
350
351pub fn format_time_style(dt: DateTime<Local>, style: &TimeStyle) -> String {
352 match style {
353 TimeStyle::FullIso => dt.format("%Y-%m-%d %H:%M:%S.%f %z").to_string(),
354 TimeStyle::LongIso => dt.format("%Y-%m-%d %H:%M").to_string(),
355 TimeStyle::Iso => {
356 let now = Local::now();
357 let six_months = chrono::Duration::days(365 / 2);
358 if (now - dt).abs() > six_months {
359 dt.format("%Y-%m-%d").to_string()
360 } else {
361 dt.format("%m-%d %H:%M").to_string()
362 }
363 }
364 TimeStyle::Locale => format_ls_time(dt),
365 TimeStyle::Format(fmt) => dt.format(fmt).to_string(),
366 }
367}
368
369fn format_ls_time(dt: DateTime<Local>) -> String {
371 let now = Local::now();
372 let six_months = chrono::Duration::days(365 / 2);
373 if (now - dt).abs() > six_months {
374 dt.format("%b %e %Y").to_string()
375 } else {
376 dt.format("%b %e %H:%M").to_string()
377 }
378}