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 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 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 let mut widths = original_widths.clone();
69
70 let width_budget = self
72 .width
73 .saturating_sub(TABLE_COL_GAP * (self.header.len() - 1));
74
75 while widths.iter().sum::<usize>() > width_budget {
77 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 let mut all_rows = vec![self.header.clone()];
89 all_rows.extend(self.rows.clone());
90
91 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 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
135pub fn fix_str(text: &str, width: usize) -> String {
137 let text = text.split('\n').next().unwrap_or("");
139
140 let current_width = UnicodeWidthStr::width(text);
141
142 if current_width <= width {
143 format!("{}{}", text, " ".repeat(width - current_width))
145 } else {
146 truncate_with_ellipsis(text, width)
148 }
149}
150
151fn 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 result.push(' ');
170 break;
171 }
172
173 result.push(ch);
174 current_width += char_width;
175 }
176
177 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}