Skip to main content

f00_format/
csv.rs

1//! CSV / TSV output modes.
2
3use f00_core::Entry;
4
5use crate::perms::format_permissions;
6
7/// Format entries as CSV (header + rows).
8pub fn format_csv(entries: &[Entry]) -> String {
9    format_delimited(entries, ',')
10}
11
12/// Format entries as TSV (header + rows).
13pub fn format_tsv(entries: &[Entry]) -> String {
14    format_delimited(entries, '\t')
15}
16
17fn format_delimited(entries: &[Entry], sep: char) -> String {
18    let mut out = String::new();
19    // header — aligned with rich JSON fields (flat columns)
20    push_row(
21        &mut out,
22        sep,
23        &[
24            "name",
25            "path",
26            "kind",
27            "size",
28            "mode",
29            "permissions",
30            "readonly",
31            "modified",
32            "accessed",
33            "changed",
34            "created",
35            "inode",
36            "nlink",
37            "blocks",
38            "uid",
39            "gid",
40            "owner",
41            "group",
42            "author",
43            "symlink_target",
44            "context",
45            "extension",
46            "git_status",
47            "depth",
48        ],
49    );
50    for e in entries.iter().filter(|e| !e.is_dir_header) {
51        let ts = |d: Option<chrono::DateTime<chrono::Local>>| {
52            d.map(|x| x.to_rfc3339()).unwrap_or_default()
53        };
54        let target = e
55            .symlink_target
56            .as_ref()
57            .map(|p| p.display().to_string())
58            .unwrap_or_default();
59        let ext = e.extension().unwrap_or("").to_string();
60        let fields = [
61            e.name.clone(),
62            e.path.display().to_string(),
63            e.kind.as_str().to_string(),
64            e.size.to_string(),
65            format!("{:o}", e.mode),
66            format_permissions(e),
67            e.readonly.to_string(),
68            ts(e.modified_datetime()),
69            ts(e.accessed_datetime()),
70            ts(e.changed_datetime()),
71            ts(e.created_datetime()),
72            e.inode.to_string(),
73            e.nlink.to_string(),
74            e.blocks.to_string(),
75            e.uid.to_string(),
76            e.gid.to_string(),
77            e.owner.clone(),
78            e.group.clone(),
79            e.author.clone(),
80            target,
81            e.context.clone(),
82            ext,
83            e.git_status.as_str().to_string(),
84            e.depth.to_string(),
85        ];
86        let refs: Vec<&str> = fields.iter().map(|s| s.as_str()).collect();
87        push_row(&mut out, sep, &refs);
88    }
89    out
90}
91
92fn push_row(out: &mut String, sep: char, fields: &[&str]) {
93    for (i, f) in fields.iter().enumerate() {
94        if i > 0 {
95            out.push(sep);
96        }
97        if sep == ',' {
98            push_csv_field(out, f);
99        } else {
100            // TSV: escape tabs/newlines lightly
101            for c in f.chars() {
102                match c {
103                    '\t' => out.push_str("\\t"),
104                    '\n' => out.push_str("\\n"),
105                    '\r' => out.push_str("\\r"),
106                    _ => out.push(c),
107                }
108            }
109        }
110    }
111    out.push('\n');
112}
113
114fn push_csv_field(out: &mut String, f: &str) {
115    if f.contains([',', '"', '\n', '\r']) {
116        out.push('"');
117        for c in f.chars() {
118            if c == '"' {
119                out.push('"');
120            }
121            out.push(c);
122        }
123        out.push('"');
124    } else {
125        out.push_str(f);
126    }
127}