/ lsf / src / grid.rs
Swadhin Biswas feat: add --git flag, overhaul CI/CD, rewrite website, prepare for crates.io · 3/7/2026
d0f56db
156 lines · 4.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
use unicode_width::UnicodeWidthStr;

/// Calculate the display width of a string, ignoring ANSI escape sequences.
pub fn display_width(s: &str) -> usize {
    let stripped = strip_ansi(s);
    UnicodeWidthStr::width(stripped.as_str())
}

/// Strip ANSI escape sequences from a string.
pub fn strip_ansi(s: &str) -> String {
    let mut result = String::with_capacity(s.len());
    let mut in_escape = false;

    for c in s.chars() {
        if c == '\x1b' {
            in_escape = true;
            continue;
        }
        if in_escape {
            if c == 'm' {
                in_escape = false;
            }
            continue;
        }
        result.push(c);
    }

    result
}

/// Format entries into a multi-column grid.
/// `items` - pre-formatted strings (with ANSI colors).
/// `term_width` - terminal width in columns.
/// `tabsize` - tab stop width.
/// `by_rows` - if true, fill rows first (like `ls -x`); if false, fill columns first.
pub fn format_grid(items: &[String], term_width: usize, tabsize: usize, by_rows: bool) -> String {
    if items.is_empty() {
        return String::new();
    }

    let tabsize = if tabsize == 0 { 8 } else { tabsize };

    // Find the maximum display width
    let max_len = items.iter().map(|s| display_width(s)).max().unwrap_or(0);

    // Calculate column width (align to tab stops)
    let col_width = if max_len == 0 {
        tabsize
    } else {
        let w = max_len.div_ceil(tabsize) * tabsize;
        if w <= max_len {
            max_len + tabsize
        } else {
            w
        }
    };

    let cols = (term_width / col_width).max(1);
    let rows = items.len().div_ceil(cols);

    let mut output = String::with_capacity(items.len() * (col_width + 2));

    for i in 0..rows {
        for j in 0..cols {
            let index = if by_rows { i * cols + j } else { i + j * rows };

            if index >= items.len() {
                continue;
            }

            let item = &items[index];
            let d_len = display_width(item);

            // Determine if there's a next item in this row
            let has_next = if by_rows {
                j != cols - 1 && (index + 1) < items.len()
            } else {
                j != cols - 1 && (index + rows) < items.len()
            };

            output.push_str(item);

            if has_next {
                let offset = d_len % col_width;
                let padding = if offset == 0 {
                    tabsize
                } else {
                    let p = col_width - offset;
                    if p < 1 {
                        tabsize
                    } else {
                        p
                    }
                };
                for _ in 0..padding {
                    output.push(' ');
                }
            }
        }
        output.push('\n');
    }

    output
}

/// Format entries as one-per-line.
pub fn format_single_column(items: &[String], terminator: char) -> String {
    let mut output = String::with_capacity(items.len() * 40);
    for item in items {
        output.push_str(item);
        output.push(terminator);
    }
    output
}

/// Format entries as comma-separated.
pub fn format_comma(items: &[String], term_width: usize) -> String {
    if items.is_empty() {
        return String::new();
    }

    let mut output = String::with_capacity(items.len() * 20);
    let mut line_len: usize = 0;

    for (i, item) in items.iter().enumerate() {
        let d_len = display_width(item);
        let separator = if i < items.len() - 1 { ", " } else { "" };
        let needed = d_len + separator.len();

        if line_len > 0 && line_len + needed > term_width {
            output.push('\n');
            line_len = 0;
        }

        output.push_str(item);
        output.push_str(separator);
        line_len += needed;
    }

    if !output.ends_with('\n') {
        output.push('\n');
    }

    output
}

/// Get terminal width, with optional override.
pub fn get_terminal_width(override_width: Option<usize>) -> usize {
    if let Some(w) = override_width {
        return w;
    }
    terminal_size::terminal_size()
        .map(|(w, _)| w.0 as usize)
        .unwrap_or(80)
}