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