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(e.inode.to_string().len());
164 w.blocks = w.blocks.max(
165 block_display_with_unit(e.blocks, config.blocks_unit())
166 .to_string()
167 .len(),
168 );
169 if config.show_context {
170 let ctx = if e.context.is_empty() {
171 "?"
172 } else {
173 e.context.as_str()
174 };
175 w.context = w.context.max(ctx.len());
176 }
177 w.nlink = w.nlink.max(e.nlink.to_string().len());
178 if config.show_owner {
179 w.owner = w.owner.max(e.owner_display(config.numeric_uid_gid).len());
180 }
181 if config.show_group {
182 w.group = w.group.max(e.group_display(config.numeric_uid_gid).len());
183 }
184 if config.show_author {
185 w.author = w.author.max(e.author_display(config.numeric_uid_gid).len());
186 }
187 w.size = w.size.max(format_size_field(e, config).len());
188 }
189 w
190 }
191}
192
193pub fn format_long(entries: &[Entry], colorizer: &Colorizer, config: &Config) -> String {
195 let widths = LongWidths::compute(entries, config);
196 let mut out = String::new();
197 let mut dired_global: Vec<(usize, usize)> = Vec::new();
198 let ending = config.line_ending();
199
200 for entry in entries {
201 if entry.is_dir_header {
202 if !out.is_empty() {
203 out.push_str(if config.zero { "\0" } else { "\n" });
204 }
205 out.push_str(&format!("{}:", entry.path.display()));
206 out.push_str(ending);
207 continue;
208 }
209 if config.dired {
210 let base = out.len();
211 let mut local = Vec::new();
212 let line = format_long_line_dired(entry, colorizer, config, &widths, Some(&mut local));
213 for (s, e) in local {
214 dired_global.push((base + s, base + e));
215 }
216 out.push_str(&line);
217 out.push_str(ending);
218 } else {
219 out.push_str(&format_long_line(entry, colorizer, config, &widths));
220 out.push_str(ending);
221 }
222 }
223
224 if config.dired && !config.zero {
225 out.push_str("//DIRED//");
227 for (s, e) in &dired_global {
228 out.push_str(&format!(" {s} {e}"));
229 }
230 out.push('\n');
231 out.push_str(&format!(
232 "//DIRED-OPTIONS// --quoting-style={}\n",
233 quoting_style_word(config.quoting_style)
234 ));
235 }
236 out
237}
238
239fn quoting_style_word(style: f00_core::QuotingStyle) -> &'static str {
240 use f00_core::QuotingStyle::*;
241 match style {
242 Literal => "literal",
243 Locale => "locale",
244 Shell => "shell",
245 ShellAlways => "shell-always",
246 ShellEscape => "shell-escape",
247 ShellEscapeAlways => "shell-escape-always",
248 C => "c",
249 Escape => "escape",
250 }
251}
252
253pub fn format_long_simple(
255 entries: &[Entry],
256 colorizer: &Colorizer,
257 human: bool,
258 icons: bool,
259 classify: bool,
260) -> String {
261 let mut config = Config {
262 human_sizes: human,
263 icons,
264 show_git: false,
265 show_owner: true,
266 show_group: true,
267 ..Config::default()
268 };
269 if classify {
270 config.classify = true;
271 config.indicator = IndicatorStyle::Classify;
272 }
273 format_long(entries, colorizer, &config)
274}
275
276pub fn format_long_line_simple(
278 entry: &Entry,
279 colorizer: &Colorizer,
280 human: bool,
281 icons: bool,
282 classify: bool,
283 size_width: usize,
284) -> String {
285 let mut config = Config {
286 human_sizes: human,
287 icons,
288 show_git: false,
289 ..Config::default()
290 };
291 if classify {
292 config.classify = true;
293 }
294 let widths = LongWidths {
295 size: size_width,
296 nlink: 1,
297 owner: 1,
298 group: 1,
299 author: 1,
300 inode: 1,
301 blocks: 1,
302 context: 1,
303 };
304 format_long_line(entry, colorizer, &config, &widths)
305}
306
307fn format_entry_time(entry: &Entry, config: &Config) -> String {
308 let style = if config.full_time {
309 TimeStyle::FullIso
310 } else {
311 config.time_style.clone()
312 };
313 let field = config.list.time_field;
314 match entry.datetime_for(field) {
315 Some(dt) => format_time_style(dt, &style),
316 None => " ".to_string(),
317 }
318}
319
320pub fn format_time_style(dt: DateTime<Local>, style: &TimeStyle) -> String {
321 match style {
322 TimeStyle::FullIso => dt.format("%Y-%m-%d %H:%M:%S.%f %z").to_string(),
323 TimeStyle::LongIso => dt.format("%Y-%m-%d %H:%M").to_string(),
324 TimeStyle::Iso => {
325 let now = Local::now();
326 let six_months = chrono::Duration::days(365 / 2);
327 if (now - dt).abs() > six_months {
328 dt.format("%Y-%m-%d").to_string()
329 } else {
330 dt.format("%m-%d %H:%M").to_string()
331 }
332 }
333 TimeStyle::Locale => format_ls_time(dt),
334 TimeStyle::Format(fmt) => dt.format(fmt).to_string(),
335 }
336}
337
338fn format_ls_time(dt: DateTime<Local>) -> String {
340 let now = Local::now();
341 let six_months = chrono::Duration::days(365 / 2);
342 if (now - dt).abs() > six_months {
343 dt.format("%b %e %Y").to_string()
344 } else {
345 dt.format("%b %e %H:%M").to_string()
346 }
347}