1#[cfg(test)]
52mod tests;
53
54#[cfg(feature = "color_codes")]
55use ::regex::Regex;
56#[cfg(feature = "wide_characters")]
57use ::unicode_width::UnicodeWidthStr;
58
59use ::std::collections::BTreeMap;
60use ::std::fmt::Display;
61#[cfg(feature = "color_codes")]
62use ::std::sync::LazyLock;
63
64const DEFAULT_WIDTH: usize = 100;
65const SE: &str = "┌";
66const NW: &str = "┘";
67const SW: &str = "┐";
68const NS: &str = "│";
69const NE: &str = "└";
70const EWS: &str = "┬";
71const NES: &str = "├";
72const NWS: &str = "┤";
73const NEW: &str = "┴";
74const NEWS: &str = "┼";
75const EW: &str = "─";
76
77#[cfg(feature = "color_codes")]
78static COLOR_CODE_PARSER: LazyLock<Regex> =
79 LazyLock::new(|| Regex::new("\u{1b}\\[([0-9]+;)*[0-9]+m").expect("Regex compilation error"));
80
81#[derive(Clone, Debug, Default, Eq, PartialEq)]
82pub struct AsciiTable {
83 max_width: Option<usize>,
84 columns: BTreeMap<usize, Column>,
85}
86
87impl AsciiTable {
88 pub fn set_max_width(&mut self, max_width: usize) -> &mut Self {
94 self.max_width = Some(max_width);
95 self
96 }
97
98 pub fn max_width(&self) -> usize {
101 match self.max_width {
102 Some(width) => width,
103 None => default_table_width(),
104 }
105 }
106
107 pub fn column(&mut self, index: usize) -> &mut Column {
108 self.columns.entry(index).or_default()
109 }
110}
111
112#[cfg(feature = "auto_table_width")]
113fn default_table_width() -> usize {
114 ::termsize::get()
115 .map(|size| size.cols.into())
116 .unwrap_or(DEFAULT_WIDTH)
117}
118
119#[cfg(not(feature = "auto_table_width"))]
120fn default_table_width() -> usize {
121 DEFAULT_WIDTH
122}
123
124#[derive(Clone, Debug, Eq, PartialEq)]
125pub struct Column {
126 header: String,
127 align: Align,
128 max_width: usize,
129}
130
131impl Column {
132 pub fn set_header<T>(&mut self, header: T) -> &mut Self
135 where
136 T: Into<String>,
137 {
138 self.header = header.into();
139 self
140 }
141
142 pub fn header(&self) -> &str {
143 &self.header
144 }
145
146 pub fn set_align(&mut self, align: Align) -> &mut Self {
147 self.align = align;
148 self
149 }
150
151 pub fn align(&self) -> Align {
152 self.align
153 }
154
155 pub fn set_max_width(&mut self, max_width: usize) -> &mut Self {
160 self.max_width = max_width;
161 self
162 }
163
164 pub fn max_width(&self) -> usize {
165 self.max_width
166 }
167}
168
169impl Default for Column {
170 fn default() -> Self {
171 Column {
172 header: Default::default(),
173 align: Default::default(),
174 max_width: usize::max_value(),
175 }
176 }
177}
178
179#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Ord, PartialOrd)]
181pub enum Align {
182 #[default]
183 Left,
184 Center,
185 Right,
186}
187
188impl AsciiTable {
189 pub fn print<L1, L2, T>(&self, data: L1)
190 where
191 L1: IntoIterator<Item = L2>,
192 L2: IntoIterator<Item = T>,
193 T: Display,
194 {
195 print!("{}", self.format(data))
196 }
197
198 pub fn format<L1, L2, T>(&self, data: L1) -> String
199 where
200 L1: IntoIterator<Item = L2>,
201 L2: IntoIterator<Item = T>,
202 T: Display,
203 {
204 self.format_inner(self.stringify(data))
205 }
206
207 fn format_inner(&self, data: Vec<Vec<SmartString>>) -> String {
208 let num_cols = data.iter().map(|row| row.len()).max().unwrap_or(0);
209 let table_max_width = self.max_width();
210 if !self.valid(&data, num_cols, table_max_width) {
211 return self.format_empty();
212 }
213
214 let header = self.stringify_header(num_cols);
215 let data = self.square_data(data, num_cols);
216 let has_header = header.iter().any(|text| !text.is_empty());
217 let widths = self.column_widths(&header, &data, table_max_width);
218
219 let mut result = String::new();
220 result.push_str(&self.format_first(&widths));
221 if has_header {
222 result.push_str(&self.format_header_row(&header, &widths));
223 result.push_str(&self.format_middle(&widths));
224 }
225 for row in data {
226 result.push_str(&self.format_row(&row, &widths));
227 }
228 result.push_str(&self.format_last(&widths));
229 result
230 }
231
232 fn valid(&self, data: &Vec<Vec<SmartString>>, num_cols: usize, table_max_width: usize) -> bool {
233 if data.len() == 0 {
234 false
235 } else if num_cols == 0 {
236 false
237 } else if table_max_width < Self::smallest_width(num_cols) {
238 false
239 } else {
240 true
241 }
242 }
243
244 fn smallest_width(num_cols: usize) -> usize {
245 ((num_cols - 1) * 3) + 4
246 }
247
248 fn stringify<L1, L2, T>(&self, data: L1) -> Vec<Vec<SmartString>>
249 where
250 L1: IntoIterator<Item = L2>,
251 L2: IntoIterator<Item = T>,
252 T: Display,
253 {
254 data.into_iter()
255 .map(|row| {
256 row.into_iter()
257 .map(|cell| SmartString::from(cell.to_string()))
258 .collect()
259 })
260 .collect()
261 }
262
263 fn stringify_header(&self, num_cols: usize) -> Vec<SmartString> {
264 (0..num_cols)
265 .map(|n| {
266 let value = self
267 .columns
268 .get(&n)
269 .map(|column| column.header.as_str())
270 .unwrap_or("");
271 SmartString::from(value)
272 })
273 .collect()
274 }
275
276 fn square_data(
277 &self,
278 mut data: Vec<Vec<SmartString>>,
279 num_cols: usize,
280 ) -> Vec<Vec<SmartString>> {
281 for row in &mut data {
282 while row.len() < num_cols {
283 row.push(SmartString::new());
284 }
285 }
286 data
287 }
288
289 fn column_widths(
290 &self,
291 header: &[SmartString],
292 data: &[Vec<SmartString>],
293 table_max_width: usize,
294 ) -> Vec<usize> {
295 let default_conf = &Default::default();
296 let result: Vec<_> = (0..header.len())
297 .map(|n| {
298 let conf = self.columns.get(&n).unwrap_or(default_conf);
299 let column_width = data.iter().map(|row| row[n].width()).max().unwrap();
300 let header_width = header[n].width();
301 column_width.max(header_width).min(conf.max_width)
302 })
303 .collect();
304 self.truncate_widths(result, table_max_width)
305 }
306
307 fn truncate_widths(&self, mut widths: Vec<usize>, table_max_width: usize) -> Vec<usize> {
308 let table_padding = Self::smallest_width(widths.len());
309 while widths.iter().sum::<usize>() + table_padding > table_max_width
310 && *widths.iter().max().unwrap() > 0
311 {
312 let max = widths.iter().max().unwrap();
313 let idx = widths.iter().rposition(|x| x == max).unwrap();
314 widths[idx] -= 1;
315 }
316 widths
317 }
318
319 fn format_line(&self, row: &[SmartString], head: &str, delim: &str, tail: &str) -> String {
320 let mut result = String::new();
321 result.push_str(head);
322 for cell in row {
323 result.push_str(&format!("{}{}", cell, delim));
324 }
325 for _ in 0..delim.chars().count() {
326 result.pop();
327 }
328 result.push_str(tail);
329 result.push('\n');
330 result
331 }
332
333 fn format_empty(&self) -> String {
334 self.format_first(&vec![0])
335 + &self.format_line(
336 &[SmartString::new()],
337 &format!("{}{}", NS, ' '),
338 &format!("{}{}{}", ' ', NS, ' '),
339 &format!("{}{}", ' ', NS),
340 )
341 + &self.format_last(&[0])
342 }
343
344 fn format_first(&self, widths: &[usize]) -> String {
345 let row: Vec<_> = widths
346 .iter()
347 .map(|&x| SmartString::from_visible(EW.repeat(x)))
348 .collect();
349 self.format_line(
350 &row,
351 &format!("{}{}", SE, EW),
352 &format!("{}{}{}", EW, EWS, EW),
353 &format!("{}{}", EW, SW),
354 )
355 }
356
357 fn format_middle(&self, widths: &[usize]) -> String {
358 let row: Vec<_> = widths
359 .iter()
360 .map(|&x| SmartString::from_visible(EW.repeat(x)))
361 .collect();
362 self.format_line(
363 &row,
364 &format!("{}{}", NES, EW),
365 &format!("{}{}{}", EW, NEWS, EW),
366 &format!("{}{}", EW, NWS),
367 )
368 }
369
370 fn format_row(&self, row: &[SmartString], widths: &[usize]) -> String {
371 let default_conf = &Default::default();
372 let row: Vec<_> = (0..widths.len())
373 .map(|a| {
374 let cell = &row[a];
375 let width = widths[a];
376 let conf = self.columns.get(&a).unwrap_or(default_conf);
377 self.format_cell(cell, width, ' ', conf.align)
378 })
379 .collect();
380 self.format_line(
381 &row,
382 &format!("{}{}", NS, ' '),
383 &format!("{}{}{}", ' ', NS, ' '),
384 &format!("{}{}", ' ', NS),
385 )
386 }
387
388 fn format_header_row(&self, row: &[SmartString], widths: &[usize]) -> String {
389 let row: Vec<_> = row
390 .iter()
391 .zip(widths.iter())
392 .map(|(cell, &width)| self.format_cell(cell, width, ' ', Align::Left))
393 .collect();
394 self.format_line(
395 &row,
396 &format!("{}{}", NS, ' '),
397 &format!("{}{}{}", ' ', NS, ' '),
398 &format!("{}{}", ' ', NS),
399 )
400 }
401
402 fn format_last(&self, widths: &[usize]) -> String {
403 let row: Vec<_> = widths
404 .iter()
405 .map(|&x| SmartString::from_visible(EW.repeat(x)))
406 .collect();
407 self.format_line(
408 &row,
409 &format!("{}{}", NE, EW),
410 &format!("{}{}{}", EW, NEW, EW),
411 &format!("{}{}", EW, NW),
412 )
413 }
414
415 fn format_cell(&self, text: &SmartString, len: usize, pad: char, align: Align) -> SmartString {
416 if text.width() > len {
417 let mut result = text.clone();
418 while result.width() > len {
419 result.pop();
420 }
421 if result.pop().is_some() {
422 result.push_visible('+')
423 }
424 result
425 } else {
426 let mut result = text.clone();
427 match align {
428 Align::Left => {
429 while result.width() < len {
430 result.push_visible(pad)
431 }
432 }
433 Align::Right => {
434 while result.width() < len {
435 result.lpush_visible(pad)
436 }
437 }
438 Align::Center => {
439 while result.width() < len {
440 result.push_visible(pad);
441 if result.width() < len {
442 result.lpush_visible(pad)
443 }
444 }
445 }
446 }
447 result
448 }
449 }
450}
451
452#[derive(Clone, Debug)]
453struct SmartString {
454 fragments: Vec<SmartStringFragment>,
455}
456
457#[derive(Clone, Debug)]
458struct SmartStringFragment {
459 string: String,
460 visible: bool,
461}
462
463impl SmartString {
464 fn new() -> Self {
465 Self {
466 fragments: Vec::new(),
467 }
468 }
469
470 #[cfg(feature = "color_codes")]
471 fn from<T>(string: T) -> Self
472 where
473 T: AsRef<str>,
474 {
475 let string = string.as_ref();
476 let mut fragments = Vec::new();
477 let mut last = 0;
478 for r#match in COLOR_CODE_PARSER.find_iter(string) {
479 let start = r#match.start();
480 let end = r#match.end();
481
482 if last < start {
483 fragments.push(SmartStringFragment::new(&string[last..start], true));
484 }
485 fragments.push(SmartStringFragment::new(&string[start..end], false));
486
487 last = end;
488 }
489
490 if last < string.len() {
491 fragments.push(SmartStringFragment::new(&string[last..], true));
492 }
493
494 Self { fragments }
495 }
496
497 #[cfg(not(feature = "color_codes"))]
498 fn from<T>(string: T) -> Self
499 where
500 T: Into<String>,
501 {
502 Self {
503 fragments: vec![SmartStringFragment::new(string, true)],
504 }
505 }
506
507 fn from_visible<T>(string: T) -> Self
508 where
509 T: Into<String>,
510 {
511 Self {
512 fragments: vec![SmartStringFragment::new(string, true)],
513 }
514 }
515
516 fn width(&self) -> usize {
517 self.fragments
518 .iter()
519 .filter(|fragment| fragment.visible)
520 .map(|fragment| fragment.width())
521 .sum()
522 }
523
524 fn is_empty(&self) -> bool {
525 self.fragments
526 .iter()
527 .filter(|fragment| fragment.visible)
528 .all(|fragment| fragment.string.is_empty())
529 }
530
531 fn pop(&mut self) -> Option<char> {
532 self.fragments
533 .iter_mut()
534 .filter(|fragment| fragment.visible && !fragment.string.is_empty())
535 .last()
536 .and_then(|fragment| fragment.string.pop())
537 }
538
539 fn push_visible(&mut self, ch: char) {
540 let last_fragment = self
541 .fragments
542 .iter_mut()
543 .filter(|fragment| fragment.visible)
544 .map(|fragment| &mut fragment.string)
545 .last();
546 if let Some(fragment) = last_fragment {
547 fragment.push(ch);
548 } else {
549 self.fragments.push(SmartStringFragment::new(ch, true));
550 }
551 }
552
553 fn lpush_visible(&mut self, ch: char) {
554 let first_fragment = self
555 .fragments
556 .iter_mut()
557 .filter(|fragment| fragment.visible)
558 .map(|fragment| &mut fragment.string)
559 .next();
560 if let Some(fragment) = first_fragment {
561 fragment.insert(0, ch);
562 } else {
563 self.fragments.insert(0, SmartStringFragment::new(ch, true));
564 }
565 }
566}
567
568impl Display for SmartString {
569 fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> {
570 self.fragments
571 .iter()
572 .try_for_each(|fragment| fragment.string.fmt(fmt))
573 }
574}
575
576impl SmartStringFragment {
577 fn new<T>(string: T, visible: bool) -> Self
578 where
579 T: Into<String>,
580 {
581 Self {
582 string: string.into(),
583 visible,
584 }
585 }
586
587 #[cfg(feature = "wide_characters")]
588 fn width(&self) -> usize {
589 UnicodeWidthStr::width(self.string.as_str())
590 }
591
592 #[cfg(not(feature = "wide_characters"))]
593 fn width(&self) -> usize {
594 self.string.chars().count()
595 }
596}