1#![forbid(unsafe_code)]
5#![warn(clippy::pedantic)]
6#![allow(clippy::items_after_statements)]
7#![allow(clippy::missing_panics_doc)]
8#![allow(clippy::uninlined_format_args)]
9#![cfg_attr(docsrs, feature(doc_auto_cfg))]
10#![cfg_attr(docsrs, feature(doc_cfg))]
11
12use std::fmt::Write as FmtWrite;
26use std::fmt::{self, Display};
27use std::io::{self, BufWriter, Write};
28use std::num::NonZeroUsize;
29
30#[cfg(feature = "fallible-iterator")]
31use std::fmt::Debug;
32
33use unicode_segmentation::UnicodeSegmentation;
34use unicode_width::UnicodeWidthStr;
35
36#[cfg(feature = "fallible-iterator")]
37use fallible_iterator::FallibleIterator;
38
39const HORIZONTAL_LINE: &str = "─";
40const VERTICAL_LINE: &str = "│";
41const TOP_LEFT: &str = "╭";
42const TOP_RIGHT: &str = "╮";
43const BOTTOM_LEFT: &str = "╰";
44const BOTTOM_RIGHT: &str = "╯";
45const INTERSECTION: &str = "┼";
46const TOP_INTERSECTION: &str = "┬";
47const BOTTOM_INTERSECTION: &str = "┴";
48const LEFT_INTERSECTION: &str = "├";
49const RIGHT_INTERSECTION: &str = "┤";
50
51pub fn write_table<Cell: Display, Row: IntoIterator<Item = Cell>, const COLUMN_COUNT: usize>(
117 to: impl Write,
118 iter: impl Iterator<Item = Row>,
119 column_names: &[&str; COLUMN_COUNT],
120 column_widths: &[NonZeroUsize; COLUMN_COUNT],
121) -> io::Result<()> {
122 let mut writer = write_table_start(to, column_names, column_widths)?;
123
124 let mut value = String::new();
125 for row in iter {
126 writer.write_all(VERTICAL_LINE.as_bytes())?;
127
128 let mut row_iter = row.into_iter();
129 for space in column_widths.iter().copied().map(NonZeroUsize::get) {
130 if let Some(col) = row_iter.next() {
131 write!(&mut value, "{}", col).expect("formatting to a string shouldn't fail");
132 }
133 draw_cell(&mut writer, &value, space)?;
134 value.clear();
135 }
136
137 writer.write_all("\n".as_bytes())?;
138 }
139
140 write_table_end(writer, column_widths)
141}
142
143pub fn write_table_with_fmt<Row, const COLUMN_COUNT: usize>(
190 to: impl Write,
191 iter: impl Iterator<Item = Row>,
192 formatters: &[impl Fn(&Row, &mut String) -> fmt::Result; COLUMN_COUNT],
193 column_names: &[&str; COLUMN_COUNT],
194 column_widths: &[NonZeroUsize; COLUMN_COUNT],
195) -> io::Result<()> {
196 let mut writer = write_table_start(to, column_names, column_widths)?;
197
198 let mut value = String::new();
199 for row in iter {
200 writer.write_all(VERTICAL_LINE.as_bytes())?;
201
202 let mut formatters = formatters.iter();
203 for space in column_widths.iter().copied().map(NonZeroUsize::get) {
204 if let Some(formatter) = formatters.next() {
205 formatter(&row, &mut value).expect("formatting to a string shouldn't fail");
206 }
207 draw_cell(&mut writer, &value, space)?;
208 value.clear();
209 }
210
211 writer.write_all("\n".as_bytes())?;
212 }
213
214 write_table_end(writer, column_widths)
215}
216
217fn write_table_start<W: Write, const COLUMN_COUNT: usize>(
218 to: W,
219 column_names: &[&str; COLUMN_COUNT],
220 column_widths: &[NonZeroUsize; COLUMN_COUNT],
221) -> Result<BufWriter<W>, io::Error> {
222 let _: () = const { assert!(COLUMN_COUNT > 0, "table must have columns") };
223
224 let mut writer = BufWriter::new(to);
225 draw_horizontal_line(&mut writer, column_widths, TOP_LEFT, TOP_RIGHT, TOP_INTERSECTION)?;
226
227 writer.write_all(VERTICAL_LINE.as_bytes())?;
228 for (space, name) in column_widths.iter().copied().map(NonZeroUsize::get).zip(column_names) {
229 draw_cell(&mut writer, name, space)?;
230 }
231 writer.write_all("\n".as_bytes())?;
232
233 draw_horizontal_line(
234 &mut writer,
235 column_widths,
236 LEFT_INTERSECTION,
237 RIGHT_INTERSECTION,
238 INTERSECTION,
239 )?;
240
241 Ok(writer)
242}
243
244fn write_table_end<W: Write, const COLUMN_COUNT: usize>(
245 mut writer: BufWriter<W>,
246 column_widths: &[NonZeroUsize; COLUMN_COUNT],
247) -> Result<(), io::Error> {
248 draw_horizontal_line(
249 &mut writer,
250 column_widths,
251 BOTTOM_LEFT,
252 BOTTOM_RIGHT,
253 BOTTOM_INTERSECTION,
254 )?;
255 writer.flush()
256}
257
258fn draw_horizontal_line<const COLUMN_COUNT: usize, W: Write>(
259 writer: &mut BufWriter<W>,
260 column_widths: &[NonZeroUsize; COLUMN_COUNT],
261 left: &str,
262 right: &str,
263 intersection: &str,
264) -> io::Result<()> {
265 writer.write_all(left.as_bytes())?;
266 for (i, width) in column_widths.iter().enumerate() {
267 for _ in 0..width.get() {
268 writer.write_all(HORIZONTAL_LINE.as_bytes())?;
269 }
270 writer.write_all((if i == COLUMN_COUNT - 1 { right } else { intersection }).as_bytes())?;
271 }
272 writer.write_all("\n".as_bytes())
273}
274
275fn draw_cell<W: Write>(writer: &mut BufWriter<W>, value: &str, space: usize) -> io::Result<()> {
276 let value_width = value.width();
277 let padding = if unlikely(value_width > space) {
278 let mut remaining = space - 1;
279 for grapheme in value.graphemes(true) {
280 remaining = match remaining.checked_sub(grapheme.width()) {
281 Some(r) => r,
282 None => break,
283 };
284 writer.write_all(grapheme.as_bytes())?;
285 }
286 writer.write_all("…".as_bytes())?;
287 remaining
288 } else {
289 if value_width < space {
290 writer.write_all(" ".as_bytes())?;
291 }
292 writer.write_all(value.as_bytes())?;
293 (space - value_width).saturating_sub(1)
294 };
295 for _ in 0..padding {
296 writer.write_all(" ".as_bytes())?;
297 }
298 writer.write_all(VERTICAL_LINE.as_bytes())
299}
300
301#[cfg(feature = "fallible-iterator")]
314pub fn write_table_fallible<Cell: Display, Row: IntoIterator<Item = Cell>, IteratorError, const COLUMN_COUNT: usize>(
315 to: impl Write,
316 mut iter: impl FallibleIterator<Item = Row, Error = IteratorError>,
317 column_names: &[&str; COLUMN_COUNT],
318 column_widths: &[NonZeroUsize; COLUMN_COUNT],
319) -> Result<(), FallibleIteratorTableWriteError<IteratorError>> {
320 let mut writer = write_table_start(to, column_names, column_widths)?;
321
322 let mut value = String::new();
323 let ret = loop {
324 match iter.next() {
325 Ok(Some(row)) => {
326 writer.write_all(VERTICAL_LINE.as_bytes())?;
327
328 let mut row_iter = row.into_iter();
329 for space in column_widths.iter().copied().map(NonZeroUsize::get) {
330 if let Some(col) = row_iter.next() {
331 write!(&mut value, "{}", col).expect("formatting to a string shouldn't fail");
332 }
333 draw_cell(&mut writer, &value, space)?;
334 value.clear();
335 }
336
337 writer.write_all("\n".as_bytes())?;
338 }
339 Ok(None) => break Ok(()),
340 Err(err) => break Err(FallibleIteratorTableWriteError::Iterator(err)),
341 }
342 };
343
344 write_table_end(writer, column_widths)?;
345 ret
346}
347
348#[cfg(feature = "fallible-iterator")]
362pub fn write_table_with_fmt_fallible<Row, IteratorError, const COLUMN_COUNT: usize>(
363 to: impl Write,
364 mut iter: impl FallibleIterator<Item = Row, Error = IteratorError>,
365 formatters: &[impl Fn(&Row, &mut String) -> fmt::Result; COLUMN_COUNT],
366 column_names: &[&str; COLUMN_COUNT],
367 column_widths: &[NonZeroUsize; COLUMN_COUNT],
368) -> Result<(), FallibleIteratorTableWriteError<IteratorError>> {
369 let mut writer = write_table_start(to, column_names, column_widths)?;
370
371 let mut value = String::new();
372 let ret = loop {
373 match iter.next() {
374 Ok(Some(row)) => {
375 writer.write_all(VERTICAL_LINE.as_bytes())?;
376
377 let mut formatters = formatters.iter();
378 for space in column_widths.iter().copied().map(NonZeroUsize::get) {
379 if let Some(formatter) = formatters.next() {
380 formatter(&row, &mut value).expect("formatting to a string shouldn't fail");
381 }
382 draw_cell(&mut writer, &value, space)?;
383 value.clear();
384 }
385
386 writer.write_all("\n".as_bytes())?;
387 }
388 Ok(None) => break Ok(()),
389 Err(err) => break Err(FallibleIteratorTableWriteError::Iterator(err)),
390 }
391 };
392
393 write_table_end(writer, column_widths)?;
394 ret
395}
396
397#[cfg(feature = "fallible-iterator")]
399#[derive(Debug)]
400pub enum FallibleIteratorTableWriteError<IteratorError> {
401 Io(io::Error),
402 Iterator(IteratorError),
403}
404
405#[cfg(feature = "fallible-iterator")]
406impl<E> From<io::Error> for FallibleIteratorTableWriteError<E> {
407 fn from(error: io::Error) -> Self {
408 Self::Io(error)
409 }
410}
411
412#[cfg(feature = "fallible-iterator")]
413impl<E: Display> Display for FallibleIteratorTableWriteError<E> {
414 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
415 match self {
416 FallibleIteratorTableWriteError::Io(err) => write!(f, "failed to write table: {}", err),
417 FallibleIteratorTableWriteError::Iterator(err) => write!(f, "failed to get next table row: {}", err),
418 }
419 }
420}
421
422#[cfg(feature = "fallible-iterator")]
423impl<E: Debug + Display> std::error::Error for FallibleIteratorTableWriteError<E> {}
424
425#[allow(clippy::inline_always)]
426#[inline(always)]
427const fn unlikely(b: bool) -> bool {
428 if b {
429 cold();
430 }
431 b
432}
433
434#[inline(always)]
435#[cold]
436const fn cold() {}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 use unicode_width::UnicodeWidthStr;
442
443 macro_rules! nz {
444 ($val:expr) => {
445 ::core::num::NonZeroUsize::new($val).unwrap()
446 };
447 }
448
449 fn assert_consistent_width(output: &str) {
450 let mut width = None;
451 for line in output.lines() {
452 if let Some(width) = width {
453 assert_eq!(line.width(), width);
454 } else {
455 width = Some(line.width());
456 }
457 }
458 }
459
460 #[test]
461 fn simple() {
462 let data = [["q3rrq", "qfqh843f9", "qa"], ["123", "", "aaaaaa"]];
463 let mut output = Vec::new();
464 write_table(&mut output, data.iter(), &["A", "B", "C"], &[nz!(5), nz!(10), nz!(4)])
465 .expect("write_table failed");
466 let output = String::from_utf8(output).expect("valid UTF-8");
467 assert_eq!(
468 output,
469 "╭─────┬──────────┬────╮
470│ A │ B │ C │
471├─────┼──────────┼────┤
472│q3rrq│ qfqh843f9│ qa │
473│ 123 │ │aaa…│
474╰─────┴──────────┴────╯
475"
476 );
477 assert_consistent_width(&output);
478 }
479
480 #[test]
481 fn iter() {
482 use std::borrow::ToOwned;
483 use std::io::{BufRead, BufReader};
484
485 let data = "asdf eraf r34r23r
486awhfde 93ry af3f98
487awefz 234 23
4883442342 1 4";
489
490 let mut output = Vec::new();
491 let file = BufReader::new(data.as_bytes());
492 write_table(
493 &mut output,
494 file.lines()
495 .map(|line| line.unwrap().split(' ').map(ToOwned::to_owned).collect::<Vec<_>>()),
496 &["col1", "col2", "col3"],
497 &[nz!(5), nz!(7), nz!(10)],
498 )
499 .expect("write_table failed");
500
501 let output = String::from_utf8(output).expect("valid UTF-8");
502 assert_eq!(
503 output,
504 "╭─────┬───────┬──────────╮
505│ col1│ col2 │ col3 │
506├─────┼───────┼──────────┤
507│ asdf│ eraf │ r34r23r │
508│awhf…│ 93ry │ af3f98 │
509│awefz│ 234 │ 23 │
510│3442…│ 1 │ 4 │
511╰─────┴───────┴──────────╯
512"
513 );
514 assert_consistent_width(&output);
515 }
516
517 #[test]
518 fn empty() {
519 let data: [[&str; 0]; 0] = [];
520 let mut output = Vec::new();
521 write_table(&mut output, data.iter(), &["A", "B"], &[nz!(1), nz!(1)]).expect("write_table failed");
522
523 let output = String::from_utf8(output).expect("valid UTF-8");
524 assert_eq!(
525 output,
526 "╭─┬─╮
527│A│B│
528├─┼─┤
529╰─┴─╯
530"
531 );
532 assert_consistent_width(&output);
533 }
534
535 #[test]
536 fn not_enough_data() {
537 let data = [["A"], ["B"], ["C"]];
538 let mut output = Vec::new();
539 write_table(&mut output, data.iter(), &["1", "2"], &[nz!(3), nz!(5)]).expect("write_table failed");
540
541 let output = String::from_utf8(output).expect("valid UTF-8");
542 assert_eq!(
543 output,
544 "╭───┬─────╮
545│ 1 │ 2 │
546├───┼─────┤
547│ A │ │
548│ B │ │
549│ C │ │
550╰───┴─────╯
551"
552 );
553 assert_consistent_width(&output);
554 }
555
556 #[test]
557 fn too_much_data() {
558 let data = [["A", "B", "C"], ["D", "E", "F"], ["G", "H", "I"]];
559 let mut output = Vec::new();
560 write_table(&mut output, data.iter(), &["1", "2"], &[nz!(3), nz!(3)]).expect("write_table failed");
561
562 let output = String::from_utf8(output).expect("valid UTF-8");
563 assert_eq!(
564 output,
565 "╭───┬───╮
566│ 1 │ 2 │
567├───┼───┤
568│ A │ B │
569│ D │ E │
570│ G │ H │
571╰───┴───╯
572"
573 );
574 assert_consistent_width(&output);
575 }
576
577 #[test]
578 fn unicode() {
579 let data = [["あいうえお", "スペース"], ["🦀🦀🦀🦀🦀🦀", "🗿🗿🗿"]];
580 let mut output = Vec::new();
581 write_table(&mut output, data.iter(), &["A", "B"], &[nz!(12), nz!(7)]).expect("write_table failed");
582
583 let output = String::from_utf8(output).expect("valid UTF-8");
584 assert_eq!(
585 output,
586 "╭────────────┬───────╮
587│ A │ B │
588├────────────┼───────┤
589│ あいうえお │スペー…│
590│🦀🦀🦀🦀🦀🦀│ 🗿🗿🗿│
591╰────────────┴───────╯
592"
593 );
594 assert_consistent_width(&output);
595 }
596
597 mod custom_fmt {
598 use super::*;
599 use std::net::Ipv4Addr;
600
601 #[test]
602 fn addr() {
603 let addrs = [
604 Ipv4Addr::new(192, 168, 0, 1),
605 Ipv4Addr::new(1, 1, 1, 1),
606 Ipv4Addr::new(255, 127, 63, 31),
607 ];
608 let column_names = ["Full address", "BE bits", "Private"];
609 let column_widths = [nz!(17), nz!(12), nz!(7)];
610
611 let formatters: [fn(&Ipv4Addr, &mut String) -> fmt::Result; 3] = [
612 |a, f| write!(f, "{}", a),
613 |a, f| write!(f, "0x{:x}", a.to_bits().to_be()),
614 |a, f| write!(f, "{}", if a.is_private() { "yes" } else { "no" }),
615 ];
616
617 let mut output = Vec::new();
618 write_table_with_fmt(
619 &mut output,
620 addrs.iter().copied(),
621 &formatters,
622 &column_names,
623 &column_widths,
624 )
625 .expect("write_table failed");
626
627 let output = String::from_utf8(output).expect("valid UTF-8");
628 assert_eq!(
629 output,
630 "╭─────────────────┬────────────┬───────╮
631│ Full address │ BE bits │Private│
632├─────────────────┼────────────┼───────┤
633│ 192.168.0.1 │ 0x100a8c0 │ yes │
634│ 1.1.1.1 │ 0x1010101 │ no │
635│ 255.127.63.31 │ 0x1f3f7fff │ no │
636╰─────────────────┴────────────┴───────╯
637"
638 );
639 assert_consistent_width(&output);
640 }
641
642 #[test]
643 fn uppercase() {
644 let data = [["aaa", "bbb", "ccc", "ddd", "eee"], ["fff", "ggg", "hhh", "iii", "jjj"]];
645 let write_upper =
646 |index: usize| move |row: &&[&str; 5], f: &mut String| write!(f, "{}", row[index].to_ascii_uppercase());
647
648 let mut output = Vec::new();
649 write_table_with_fmt(
650 &mut output,
651 data.iter(),
652 &[
653 write_upper(0),
654 write_upper(1),
655 write_upper(2),
656 write_upper(3),
657 write_upper(4),
658 ],
659 &["1", "2", "3", "4", "5"],
660 &[nz!(3); 5],
661 )
662 .expect("write_table failed");
663
664 let output = String::from_utf8(output).expect("valid UTF-8");
665 assert_eq!(
666 output,
667 "╭───┬───┬───┬───┬───╮
668│ 1 │ 2 │ 3 │ 4 │ 5 │
669├───┼───┼───┼───┼───┤
670│AAA│BBB│CCC│DDD│EEE│
671│FFF│GGG│HHH│III│JJJ│
672╰───┴───┴───┴───┴───╯
673"
674 );
675 assert_consistent_width(&output);
676 }
677 }
678
679 #[cfg(feature = "fallible-iterator")]
680 mod fallible_iterator {
681 use super::*;
682 use ::fallible_iterator::FallibleIterator;
683
684 #[test]
685 fn fallible_ok() {
686 let data = [["q3rrq", "qfqh843f9", "qa"], ["123", "", "aaaaaa"]];
687 let mut output = Vec::new();
688 write_table_fallible(
689 &mut output,
690 ::fallible_iterator::convert(data.iter().map(Ok::<_, ()>)),
691 &["A", "B", "C"],
692 &[nz!(5), nz!(10), nz!(4)],
693 )
694 .expect("write_table failed");
695
696 let output = String::from_utf8(output).expect("valid UTF-8");
697 assert_eq!(
698 output,
699 "╭─────┬──────────┬────╮
700│ A │ B │ C │
701├─────┼──────────┼────┤
702│q3rrq│ qfqh843f9│ qa │
703│ 123 │ │aaa…│
704╰─────┴──────────┴────╯
705"
706 );
707 assert_consistent_width(&output);
708 }
709
710 #[test]
711 fn fallible_err() {
712 let data = [["q3rrq", "qfqh843f9", "qa"], ["123", "", "aaaaaa"]];
713 let mut output = Vec::new();
714 let result = write_table_fallible(
715 &mut output,
716 ::fallible_iterator::convert(data.iter().map(Ok::<_, &str>))
717 .take(1)
718 .chain(::fallible_iterator::once_err("error")),
719 &["A", "B", "C"],
720 &[nz!(5), nz!(10), nz!(4)],
721 );
722 assert!(matches!(result, Err(FallibleIteratorTableWriteError::Iterator(_))));
723
724 let output = String::from_utf8(output).expect("valid UTF-8");
725 assert_eq!(
726 output,
727 "╭─────┬──────────┬────╮
728│ A │ B │ C │
729├─────┼──────────┼────┤
730│q3rrq│ qfqh843f9│ qa │
731╰─────┴──────────┴────╯
732"
733 );
734 assert_consistent_width(&output);
735 }
736
737 #[test]
738 fn fallible_fmt() {
739 let data = [["q3rrq", "qfqh843f9", "qa"], ["123", "", "aaaaaa"]];
740 let len = |index: usize| move |row: &&[&str; 3], f: &mut String| write!(f, "{}", row[index].len());
741
742 let mut output = Vec::new();
743 write_table_with_fmt_fallible(
744 &mut output,
745 ::fallible_iterator::convert(data.iter().map(Ok::<_, ()>)),
746 &[len(0), len(1), len(2)],
747 &["A", "B", "C"],
748 &[nz!(3); 3],
749 )
750 .expect("write_table failed");
751
752 let output = String::from_utf8(output).expect("valid UTF-8");
753 assert_eq!(
754 output,
755 "╭───┬───┬───╮
756│ A │ B │ C │
757├───┼───┼───┤
758│ 5 │ 9 │ 2 │
759│ 3 │ 0 │ 6 │
760╰───┴───┴───╯
761"
762 );
763 assert_consistent_width(&output);
764 }
765 }
766}
767
768#[cfg(doctest)]
773fn no_columns() {}