Skip to main content

aft/compress/
listing_fold.rs

1//! Shared outlier-preserving run folding for `ls` and `find` output.
2
3pub const FOLD_THRESHOLD: usize = 8;
4/// Line ceiling for folded listings. Distinct names are kept verbatim below this;
5/// middle-cap with an explicit note is only used above this as a last resort.
6pub const MAX_LINES: usize = 400;
7
8/// Mask digit runs in `name` to `#` for shape grouping.
9pub fn mask_digits_in_name(name: &str) -> String {
10    let mut out = String::with_capacity(name.len());
11    let mut chars = name.chars().peekable();
12    while let Some(c) = chars.next() {
13        if c.is_ascii_digit() {
14            while chars.peek().is_some_and(|p| p.is_ascii_digit()) {
15                chars.next();
16            }
17            out.push('#');
18        } else {
19            out.push(c);
20        }
21    }
22    out
23}
24
25/// Shape key: `directory|masked_basename` (directory may be empty for plain names).
26pub fn shape_key_for_basename(dir: &str, basename: &str) -> String {
27    let masked = mask_digits_in_name(basename);
28    if dir.is_empty() {
29        masked
30    } else {
31        format!("{dir}|{masked}")
32    }
33}
34
35/// Display pattern: masked name with `#` → `*`.
36pub fn shape_pattern(masked_basename: &str) -> String {
37    masked_basename.replace('#', "*")
38}
39
40#[derive(Clone, Debug)]
41pub struct FoldEntry {
42    pub line: String,
43    pub dir: String,
44    pub basename: String,
45    pub shape_key: String,
46}
47
48pub fn fold_consecutive_runs(entries: Vec<FoldEntry>) -> Vec<String> {
49    if entries.is_empty() {
50        return Vec::new();
51    }
52
53    let mut out = Vec::new();
54    let mut i = 0;
55    while i < entries.len() {
56        let key = entries[i].shape_key.clone();
57        let mut j = i + 1;
58        while j < entries.len() && entries[j].shape_key == key {
59            j += 1;
60        }
61        let run = &entries[i..j];
62        if run.len() >= FOLD_THRESHOLD {
63            let masked = mask_digits_in_name(&run[0].basename);
64            let pattern = if run[0].dir.is_empty() {
65                shape_pattern(&masked)
66            } else {
67                format!(
68                    "{}/{}",
69                    run[0].dir.trim_end_matches('/'),
70                    shape_pattern(&masked)
71                )
72            };
73            let first = display_name(&run[0]);
74            let last = display_name(run.last().expect("non-empty run"));
75            let count = run.len();
76            let noun = if count == 1 { "file" } else { "files" };
77            out.push(format!("{pattern} — {count} {noun} ({first} … {last})"));
78        } else {
79            for e in run {
80                out.push(e.line.clone());
81            }
82        }
83        i = j;
84    }
85    out
86}
87
88fn display_name(e: &FoldEntry) -> String {
89    if e.dir.is_empty() {
90        e.basename.clone()
91    } else {
92        format!("{}/{}", e.dir.trim_end_matches('/'), e.basename)
93    }
94}
95
96/// Fold bulk runs, keep every distinct name; only middle-cap as an absolute last
97/// resort when line count exceeds [`MAX_LINES`], and say so explicitly.
98pub fn finish_folded(lines: Vec<String>) -> String {
99    if lines.len() <= MAX_LINES {
100        return lines.join("\n");
101    }
102    let omitted = lines.len() - (MAX_LINES - 1);
103    let head_count = (MAX_LINES - 1) / 2;
104    let tail_count = (MAX_LINES - 1) - head_count;
105    let mut kept: Vec<String> = lines.iter().take(head_count).cloned().collect();
106    kept.push(format!(
107        "… +{omitted} entries omitted (listing too long; narrow with a path/glob)"
108    ));
109    kept.extend(lines.iter().skip(lines.len() - tail_count).cloned());
110    kept.join("\n")
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn masks_digit_runs_in_filename() {
119        assert_eq!(mask_digits_in_name("module_017.ts"), "module_#.ts");
120        assert_eq!(
121            mask_digits_in_name("module_100_NEEDLE_FILE_marker.ts"),
122            "module_#_NEEDLE_FILE_marker.ts"
123        );
124        assert_ne!(
125            shape_key_for_basename("", "module_100.ts"),
126            shape_key_for_basename("", "module_100_NEEDLE_FILE_marker.ts")
127        );
128        assert_eq!(
129            shape_pattern(&mask_digits_in_name("module_100_NEEDLE_FILE_marker.ts")),
130            "module_*_NEEDLE_FILE_marker.ts"
131        );
132    }
133}