1use colored::{ColoredString, Colorize};
2use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
3
4use crate::TableError;
5
6pub const TOP_LEFT: &str = "╭";
7pub const TOP_RIGHT: &str = "╮";
8pub const BOTTOM_LEFT: &str = "╰";
9pub const BOTTOM_RIGHT: &str = "╯";
10pub const HORIZONTAL: &str = "─";
11pub const VERTICAL: &str = "│";
12pub const CROSS: &str = "┼";
13pub const TOP_T: &str = "┬";
14pub const BOTTOM_T: &str = "┴";
15pub const LEFT_T: &str = "├";
16pub const RIGHT_T: &str = "┤";
17
18pub const SPACES: &str = " ";
19
20pub enum TablePartial<const N: usize> {
21 Table(Table<N>),
22 TableError(TableError)
23}
24
25impl<const N: usize> TablePartial<N> {
26 pub fn title(self, title: ColoredString) -> Self {
29 match self {
30 Self::Table(mut table) => {
31 table.title = title;
32 Self::Table(table)
33 }
34 Self::TableError(err) => Self::TableError(err),
35 }
36 }
37
38 pub fn headers(self, headers: [ColoredString; N]) -> TableResult<N> {
39 match self {
40 Self::Table(mut table) => {
41 for (i, header) in headers.into_iter().enumerate() {
42 if header.width() > table.col_widths[i] {
43 table.col_widths[i] = header.width();
44 }
45 table.headers[i] = header;
46 }
47 TableResult::Table(table)
48 }
49 Self::TableError(err) => TableResult::TableError(err),
50 }
51 }
52}
53
54pub enum TableResult<const N: usize> {
55 Table(Table<N>),
56 TableError(TableError),
57}
58
59
60impl<const N: usize> TableResult<N> {
61 pub fn title(self, title: ColoredString) -> Self {
64 match self {
65 Self::Table(mut table) => {
66 table.title = title;
67 Self::Table(table)
68 }
69 Self::TableError(err) => Self::TableError(err),
70 }
71 }
72 pub fn row(self, row: [ColoredString; N]) -> Self {
73 match self {
74 Self::Table(mut table) => {
75 for (i, cell) in row.into_iter().enumerate() {
76 if cell.width() > table.col_widths[i] {
77 table.col_widths[i] = cell.width();
78 }
79 table.cells.push(cell);
80 }
81 table.rows += 1;
82 TableResult::Table(table)
83 }
84 err => err,
85 }
86 }
87
88 pub fn collect(self) -> Result<Table<N>, TableError> {
89 match self {
90 Self::Table(table) => Ok(table),
91 Self::TableError(err) => Err(err),
92 }
93 }
94}
95
96pub struct Table<const N: usize> {
97 title: ColoredString,
98 rows: usize,
99 col_widths: [usize; N],
100 headers: [ColoredString; N],
101 cells: Vec<ColoredString>,
102}
103
104impl<const N: usize> Table<N> {
105 #[allow(clippy::new_ret_no_self)]
107 pub fn new() -> TablePartial<N> {
108 TablePartial::Table(Self {
109 title: "".white(),
110 rows: 0,
111 col_widths: [0; N],
112 headers: std::array::from_fn(|_| "".white()),
113 cells: Vec::new(),
114 })
115 }
116 pub fn headers(&mut self, headers: [ColoredString; N]) -> Result<(), TableError> {
117 for (i, header) in headers.into_iter().enumerate() {
118 if header.width() > self.col_widths[i] {
119 self.col_widths[i] = header.width();
120 }
121 self.headers[i] = header;
122 }
123 Ok(())
124 }
125
126 pub fn push_row(&mut self, row: [ColoredString; N]) -> Result<(), TableError> {
127 for (i, cell) in row.into_iter().enumerate() {
128 if cell.width() > self.col_widths[i] {
129 self.col_widths[i] = cell.width();
130 }
131 self.cells.push(cell);
132 }
133 self.rows += 1;
134 Ok(())
135 }
136
137 pub fn get_title(&self) -> Option<&ColoredString> {
138 if self.title.is_empty() {
139 None
140 } else {
141 Some(&self.title)
142 }
143 }
144
145 pub fn get_row_count(&self) -> usize {
146 self.rows
147 }
148
149 pub fn get_column_count(&self) -> usize {
150 N
151 }
152
153 pub fn get_headers(&self) -> &[ColoredString] {
154 &self.headers
155 }
156
157 pub fn get_col_widths(&self) -> &[usize] {
158 &self.col_widths
159 }
160
161 pub fn get_row(&self, index: usize) -> Option<&[ColoredString]> {
162 if index >= self.rows {
163 return None;
164 }
165 Some(&self.cells[index * N..(index * N) + N])
166 }
167
168 pub fn padding(&self, spacing: usize) -> &str {
169 &SPACES[..std::cmp::min(spacing, SPACES.len())]
170 }
171
172 pub fn content_width(&self) -> usize {
173 self.get_col_widths().iter().sum()
174 }
175
176 fn safe_truncate(&self, input: &str, max_width: usize) -> String {
177 let mut width = 0;
178 let mut result = String::new();
179
180 for c in input.chars() {
181 let w = c.width().unwrap_or(0);
182 if width + w > max_width {
183 break;
184 }
185 width += w;
186 result.push(c);
187 }
188 result
189 }
190
191 fn _render_top<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
192 let table_width = self.content_width() + self.get_column_count() - 1;
193
194 writeln!(
195 w,
196 "\n{}{}{}",
197 TOP_LEFT.blue(),
198 HORIZONTAL.repeat(table_width).blue(),
199 TOP_RIGHT.blue()
200 )?;
201
202 Ok(())
203 }
204
205 fn _render_title<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
206 let table_width = self.content_width() + self.get_column_count() - 1;
207
208 if let Some(title) = self.get_title() {
209 write!(w, "{}", VERTICAL.blue())?;
210 if title.width() > table_width - 2 {
213 writeln!(
214 w,
215 "{}...{}",
216 self.safe_truncate(title, table_width - 3)
218 .color(self.title.fgcolor.unwrap_or(colored::Color::White)),
219 VERTICAL.blue(),
220 )?;
221 } else {
222 let pad_l = self.padding((table_width - title.width()) / 2);
223 let pad_r =
224 self.padding((table_width - title.width()) - (table_width - title.width()) / 2);
225
226 writeln!(w, "{}{}{}{}", pad_l, self.title, pad_r, VERTICAL.blue(),)?;
227 }
228 }
229 Ok(())
230 }
231
232 fn _render_headers<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
233 write!(
234 w,
235 "{}{}",
236 LEFT_T.blue(),
237 HORIZONTAL.repeat(self.get_col_widths()[0]).blue()
238 )?;
239 for i in 1..self.get_column_count() {
240 write!(
241 w,
242 "{}{}",
243 TOP_T.blue(),
244 HORIZONTAL.repeat(self.get_col_widths()[i]).blue()
245 )?;
246 }
247 writeln!(w, "{}", RIGHT_T.blue())?;
248 write!(w, "{}", VERTICAL.blue())?;
249 for (i, header) in self.get_headers().iter().enumerate() {
250 write!(
251 w,
252 "{}{}{}",
253 header,
254 self.padding(self.get_col_widths()[i] - header.width()),
255 VERTICAL.blue()
256 )?;
257 }
258 write!(
259 w,
260 "\n{}{}",
261 LEFT_T.blue(),
262 HORIZONTAL.repeat(self.get_col_widths()[0]).blue()
263 )?;
264 for i in 1..self.get_column_count() {
265 write!(
266 w,
267 "{}{}",
268 CROSS.blue(),
269 HORIZONTAL.repeat(self.get_col_widths()[i]).blue()
270 )?;
271 }
272 writeln!(w, "{}", RIGHT_T.blue())?;
273
274 Ok(())
275 }
276
277 fn _render_row<W: std::io::Write>(&self, w: &mut W, row: usize) -> Result<(), TableError> {
278 write!(w, "{}", VERTICAL.blue())?;
279 let Some(row_data) = self.get_row(row) else {
280 return Err(TableError::InputError(
281 "invalid row index given".to_string(),
282 ));
283 };
284 for (i, cell) in row_data.iter().enumerate() {
285 write!(
286 w,
287 "{}{}{}",
288 cell,
289 self.padding(self.get_col_widths()[i] - cell.width()),
290 VERTICAL.blue()
291 )?;
292 }
293 writeln!(w)?;
294 Ok(())
295 }
296
297 fn _render_bottom<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
298 write!(
299 w,
300 "{}{}",
301 BOTTOM_LEFT.blue(),
302 HORIZONTAL.repeat(self.get_col_widths()[0]).blue()
303 )?;
304 for i in 1..self.get_column_count() {
305 write!(
306 w,
307 "{}{}",
308 BOTTOM_T.blue(),
309 HORIZONTAL.repeat(self.get_col_widths()[i]).blue()
310 )?;
311 }
312 writeln!(w, "{}", BOTTOM_RIGHT.blue())?;
313 Ok(())
314 }
315
316 pub fn render<W: std::io::Write>(&self, w: &mut W) -> Result<(), TableError> {
317 self._render_top(w)?;
318 self._render_title(w)?;
319 self._render_headers(w)?;
320 for row in 0..self.get_row_count() {
321 self._render_row(w, row)?;
322 }
323 self._render_bottom(w)?;
324 Ok(())
325 }
326}