Skip to main content

rstask_core/
table.rs

1use crate::constants::*;
2use unicode_width::UnicodeWidthStr;
3
4#[derive(Debug, Clone, Default)]
5pub struct RowStyle {
6    pub mode: u8,
7    pub fg: u8,
8    pub bg: u8,
9}
10
11pub struct Table {
12    pub header: Vec<String>,
13    pub rows: Vec<Vec<String>>,
14    pub row_styles: Vec<RowStyle>,
15    pub width: usize,
16}
17
18impl Table {
19    pub fn new(width: usize, header: Vec<String>) -> Self {
20        let w = width.min(TABLE_MAX_WIDTH);
21
22        Table {
23            header,
24            rows: Vec::new(),
25            row_styles: vec![RowStyle {
26                mode: MODE_HEADER,
27                fg: 0,
28                bg: 0,
29            }],
30            width: w,
31        }
32    }
33
34    pub fn add_row(&mut self, row: Vec<String>, style: RowStyle) {
35        if row.len() != self.header.len() {
36            panic!(
37                "Row length {} doesn't match header length {}",
38                row.len(),
39                self.header.len()
40            );
41        }
42        self.rows.push(row);
43        self.row_styles.push(style);
44    }
45
46    pub fn render(&self) {
47        let mut original_widths = vec![0; self.header.len()];
48
49        // Calculate widths from data rows
50        for row in &self.rows {
51            for (j, cell) in row.iter().enumerate() {
52                let width = UnicodeWidthStr::width(cell.as_str());
53                if original_widths[j] < width {
54                    original_widths[j] = width;
55                }
56            }
57        }
58
59        // Account for header cells
60        for (j, cell) in self.header.iter().enumerate() {
61            let width = UnicodeWidthStr::width(cell.as_str());
62            if original_widths[j] < width {
63                original_widths[j] = width;
64            }
65        }
66
67        // Initialize with original sizes
68        let mut widths = original_widths.clone();
69
70        // Account for gaps
71        let width_budget = self
72            .width
73            .saturating_sub(TABLE_COL_GAP * (self.header.len() - 1));
74
75        // Iteratively reduce widths to fit budget
76        while widths.iter().sum::<usize>() > width_budget {
77            // Find max width column
78            let (max_idx, &max_width) = widths.iter().enumerate().max_by_key(|(_, w)| *w).unwrap();
79
80            if max_width == 0 {
81                break;
82            }
83
84            widths[max_idx] -= 1;
85        }
86
87        // Combine header and rows
88        let mut all_rows = vec![self.header.clone()];
89        all_rows.extend(self.rows.clone());
90
91        // Render each row
92        for (i, row) in all_rows.iter().enumerate() {
93            let style = &self.row_styles[i];
94
95            let mode = if style.mode == 0 {
96                MODE_DEFAULT
97            } else {
98                style.mode
99            };
100            let fg = if style.fg == 0 { FG_DEFAULT } else { style.fg };
101            let bg = if style.bg == 0 {
102                if i % 2 != 0 {
103                    BG_DEFAULT_1
104                } else {
105                    BG_DEFAULT_2
106                }
107            } else {
108                style.bg
109            };
110
111            let mut cells = Vec::new();
112            for (j, cell) in row.iter().enumerate() {
113                let trimmed = fix_str(cell, widths[j]);
114
115                // Support ' / ' markup for notes
116                let final_cell = if trimmed.contains(&format!(" {} ", NOTE_MODE_KEYWORD)) {
117                    let with_note_color = trimmed.replace(
118                        &format!(" {} ", NOTE_MODE_KEYWORD),
119                        &format!("\x1b[38;5;{}m ", FG_NOTE),
120                    );
121                    format!("{}\x1b[38;5;{}m", with_note_color, fg)
122                } else {
123                    trimmed
124                };
125
126                cells.push(final_cell);
127            }
128
129            let line = cells.join(&" ".repeat(TABLE_COL_GAP));
130            println!("\x1b[{};38;5;{};48;5;{}m{}\x1b[0m", mode, fg, bg, line);
131        }
132    }
133}
134
135/// Fixes a string to a specific width, truncating or padding as needed
136pub fn fix_str(text: &str, width: usize) -> String {
137    // Remove anything after newline
138    let text = text.split('\n').next().unwrap_or("");
139
140    let current_width = UnicodeWidthStr::width(text);
141
142    if current_width <= width {
143        // Pad with spaces
144        format!("{}{}", text, " ".repeat(width - current_width))
145    } else {
146        // Truncate with ellipsis
147        truncate_with_ellipsis(text, width)
148    }
149}
150
151/// Truncates a string to fit within width, adding " " at the end
152fn truncate_with_ellipsis(text: &str, width: usize) -> String {
153    if width == 0 {
154        return String::new();
155    }
156
157    if width == 1 {
158        return " ".to_string();
159    }
160
161    let mut result = String::new();
162    let mut current_width = 0;
163
164    for ch in text.chars() {
165        let char_width = UnicodeWidthStr::width(ch.to_string().as_str());
166
167        if current_width + char_width + 1 > width {
168            // Need to add ellipsis
169            result.push(' ');
170            break;
171        }
172
173        result.push(ch);
174        current_width += char_width;
175    }
176
177    // Pad to exact width
178    while UnicodeWidthStr::width(result.as_str()) < width {
179        result.push(' ');
180    }
181
182    result
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_fix_str_padding() {
191        assert_eq!(fix_str("hello", 10), "hello     ");
192    }
193
194    #[test]
195    fn test_fix_str_truncation() {
196        let result = fix_str("hello world", 8);
197        assert_eq!(UnicodeWidthStr::width(result.as_str()), 8);
198        assert!(result.ends_with(' '));
199    }
200
201    #[test]
202    fn test_fix_str_newline() {
203        assert_eq!(fix_str("hello\nworld", 10), "hello     ");
204    }
205}