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 size = format_size_field(entry, config);
52 let mtime = format_entry_time(entry, config);
53 let icon = icon_prefix(entry, config.icons);
54 let suffix = classify_suffix(entry, config.indicator_style());
55
56 let quoted = display_name(
57 &entry.name,
58 config.quoting_style,
59 config.hide_control_chars(),
60 );
61 let mut name_plain = format!("{icon}{quoted}{suffix}");
62 if let Some(target) = &entry.symlink_target {
63 let tq = display_name(
64 &target.display().to_string(),
65 config.quoting_style,
66 config.hide_control_chars(),
67 );
68 name_plain = format!("{name_plain} -> {tq}");
69 }
70 let name_colored = colorizer.paint_name(entry, &name_plain);
71 let name = hyperlink_name(&entry.path, &name_colored, config.hyperlink_enabled());
72
73 let git = if config.show_git {
74 entry
75 .git_status
76 .as_char()
77 .map(|c| format!(" {} ", colorizer.paint_git_char(c)))
78 .unwrap_or_else(|| " ".to_string())
79 } else {
80 String::new()
81 };
82
83 let mut parts = Vec::new();
84 if config.show_inode {
85 parts.push(format!("{:>w$}", entry.inode, w = widths.inode));
86 }
87 if config.show_blocks {
88 parts.push(format!(
89 "{:>w$}",
90 block_display_with_unit(entry.blocks, config.blocks_unit()),
91 w = widths.blocks
92 ));
93 }
94 if config.show_context {
95 let ctx = if entry.context.is_empty() {
96 "?"
97 } else {
98 entry.context.as_str()
99 };
100 parts.push(format!("{:<w$}", ctx, w = widths.context));
101 }
102 parts.push(perms);
103 parts.push(format!("{:>w$}", nlink, w = widths.nlink));
104 if config.show_owner {
105 parts.push(format!("{:<w$}", owner, w = widths.owner));
106 }
107 if config.show_group {
108 parts.push(format!("{:<w$}", group, w = widths.group));
109 }
110 if config.show_author {
111 parts.push(format!("{:<w$}", author, w = widths.author));
112 }
113 parts.push(format!("{:>w$}", size, w = widths.size));
114 parts.push(mtime);
115 let prefix = parts.join(" ");
116 let line_without_name = format!("{prefix}{git} ");
118 if let Some(offsets) = dired_offsets {
119 let start = line_without_name.len();
120 let end = start + name.len();
124 offsets.push((start, end));
125 }
126 format!("{line_without_name}{name}")
127}
128
129fn format_size_field(entry: &Entry, config: &Config) -> String {
130 format_size_bytes(
131 entry.size,
132 config.block_size,
133 config.human_sizes,
134 config.si_sizes,
135 )
136}
137
138#[derive(Debug, Clone, Default)]
139pub struct LongWidths {
140 pub inode: usize,
141 pub blocks: usize,
142 pub context: usize,
143 pub nlink: usize,
144 pub owner: usize,
145 pub group: usize,
146 pub author: usize,
147 pub size: usize,
148}
149
150impl LongWidths {
151 pub fn compute(entries: &[Entry], config: &Config) -> Self {
152 let mut w = Self {
153 inode: 1,
154 blocks: 1,
155 context: 1,
156 nlink: 1,
157 owner: 1,
158 group: 1,
159 author: 1,
160 size: 1,
161 };
162 for e in entries.iter().filter(|e| !e.is_dir_header) {
163 w.inode = w.inode.max(decimal_digits(e.inode));
164 let blocks_n = block_display_with_unit(e.blocks, config.blocks_unit());
165 w.blocks = w.blocks.max(decimal_digits(blocks_n));
166 if config.show_context {
167 let ctx = if e.context.is_empty() {
168 "?"
169 } else {
170 e.context.as_str()
171 };
172 w.context = w.context.max(ctx.len());
173 }
174 w.nlink = w.nlink.max(decimal_digits(e.nlink));
175 if config.show_owner {
176 w.owner = w.owner.max(e.owner_display(config.numeric_uid_gid).len());
177 }
178 if config.show_group {
179 w.group = w.group.max(e.group_display(config.numeric_uid_gid).len());
180 }
181 if config.show_author {
182 w.author = w.author.max(e.author_display(config.numeric_uid_gid).len());
183 }
184 w.size = w.size.max(format_size_field(e, config).len());
185 }
186 w
187 }
188}
189
190fn decimal_digits(mut n: u64) -> usize {
192 if n == 0 {
193 return 1;
194 }
195 let mut d = 0;
196 while n > 0 {
197 n /= 10;
198 d += 1;
199 }
200 d
201}
202
203pub fn format_long(entries: &[Entry], colorizer: &Colorizer, config: &Config) -> String {
205 let widths = LongWidths::compute(entries, config);
206 let mut out = String::with_capacity(entries.len().saturating_mul(96));
208 let mut dired_global: Vec<(usize, usize)> = Vec::new();
209 let ending = config.line_ending();
210
211 for entry in entries {
212 if entry.is_dir_header {
213 if !out.is_empty() {
214 out.push_str(if config.zero { "\0" } else { "\n" });
215 }
216 out.push_str(&format!("{}:", entry.path.display()));
217 out.push_str(ending);
218 continue;
219 }
220 if config.dired {
221 let base = out.len();
222 let mut local = Vec::new();
223 let line = format_long_line_dired(entry, colorizer, config, &widths, Some(&mut local));
224 for (s, e) in local {
225 dired_global.push((base + s, base + e));
226 }
227 out.push_str(&line);
228 out.push_str(ending);
229 } else {
230 out.push_str(&format_long_line(entry, colorizer, config, &widths));
231 out.push_str(ending);
232 }
233 }
234
235 if config.dired && !config.zero {
236 out.push_str("//DIRED//");
238 for (s, e) in &dired_global {
239 out.push_str(&format!(" {s} {e}"));
240 }
241 out.push('\n');
242 out.push_str(&format!(
243 "//DIRED-OPTIONS// --quoting-style={}\n",
244 quoting_style_word(config.quoting_style)
245 ));
246 }
247 out
248}
249
250fn quoting_style_word(style: f00_core::QuotingStyle) -> &'static str {
251 use f00_core::QuotingStyle::*;
252 match style {
253 Literal => "literal",
254 Locale => "locale",
255 Shell => "shell",
256 ShellAlways => "shell-always",
257 ShellEscape => "shell-escape",
258 ShellEscapeAlways => "shell-escape-always",
259 C => "c",
260 Escape => "escape",
261 }
262}
263
264pub fn format_long_simple(
266 entries: &[Entry],
267 colorizer: &Colorizer,
268 human: bool,
269 icons: bool,
270 classify: bool,
271) -> String {
272 let mut config = Config {
273 human_sizes: human,
274 icons,
275 show_git: false,
276 show_owner: true,
277 show_group: true,
278 ..Config::default()
279 };
280 if classify {
281 config.classify = true;
282 config.indicator = IndicatorStyle::Classify;
283 }
284 format_long(entries, colorizer, &config)
285}
286
287pub fn format_long_line_simple(
289 entry: &Entry,
290 colorizer: &Colorizer,
291 human: bool,
292 icons: bool,
293 classify: bool,
294 size_width: usize,
295) -> String {
296 let mut config = Config {
297 human_sizes: human,
298 icons,
299 show_git: false,
300 ..Config::default()
301 };
302 if classify {
303 config.classify = true;
304 }
305 let widths = LongWidths {
306 size: size_width,
307 nlink: 1,
308 owner: 1,
309 group: 1,
310 author: 1,
311 inode: 1,
312 blocks: 1,
313 context: 1,
314 };
315 format_long_line(entry, colorizer, &config, &widths)
316}
317
318fn format_entry_time(entry: &Entry, config: &Config) -> String {
319 let style = if config.full_time {
320 TimeStyle::FullIso
321 } else {
322 config.time_style.clone()
323 };
324 let field = config.list.time_field;
325 match entry.datetime_for(field) {
326 Some(dt) => format_time_style(dt, &style),
327 None => " ".to_string(),
328 }
329}
330
331pub fn format_time_style(dt: DateTime<Local>, style: &TimeStyle) -> String {
332 match style {
333 TimeStyle::FullIso => dt.format("%Y-%m-%d %H:%M:%S.%f %z").to_string(),
334 TimeStyle::LongIso => dt.format("%Y-%m-%d %H:%M").to_string(),
335 TimeStyle::Iso => {
336 let now = Local::now();
337 let six_months = chrono::Duration::days(365 / 2);
338 if (now - dt).abs() > six_months {
339 dt.format("%Y-%m-%d").to_string()
340 } else {
341 dt.format("%m-%d %H:%M").to_string()
342 }
343 }
344 TimeStyle::Locale => format_ls_time(dt),
345 TimeStyle::Format(fmt) => dt.format(fmt).to_string(),
346 }
347}
348
349fn format_ls_time(dt: DateTime<Local>) -> String {
351 let now = Local::now();
352 let six_months = chrono::Duration::days(365 / 2);
353 if (now - dt).abs() > six_months {
354 dt.format("%b %e %Y").to_string()
355 } else {
356 dt.format("%b %e %H:%M").to_string()
357 }
358}