Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 190 additions & 52 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#![forbid(unsafe_code)]
#![warn(clippy::pedantic)]
#![allow(clippy::items_after_statements)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::uninlined_format_args)]

//! A tiny text table drawing library.
Expand All @@ -18,8 +19,8 @@
//!
//! See [`write_table`] for examples and usage details.

use std::fmt::Display;
use std::fmt::Write as FmtWrite;
use std::fmt::{self, Display};
use std::io::{self, BufWriter, Write};
use std::num::NonZeroUsize;

Expand Down Expand Up @@ -111,51 +112,108 @@ pub fn write_table<
column_names: &[&str; COLUMN_COUNT],
column_widths: &[NonZeroUsize; COLUMN_COUNT],
) -> io::Result<()> {
let _: () = const { assert!(COLUMN_COUNT > 0, "table must have columns") };
let mut writer = write_table_start(to, column_names, column_widths)?;

let mut value = String::new();
for row in iter {
writer.write_all(VERTICAL_LINE.as_bytes())?;

fn draw_horizontal_line<const COLUMN_COUNT: usize, W: Write>(
writer: &mut BufWriter<W>,
column_widths: &[NonZeroUsize; COLUMN_COUNT],
left: &str,
right: &str,
intersection: &str,
) -> io::Result<()> {
writer.write_all(left.as_bytes())?;
for (i, width) in column_widths.iter().enumerate() {
for _ in 0..width.get() {
writer.write_all(HORIZONTAL_LINE.as_bytes())?;
let mut row_iter = row.into_iter();
for space in column_widths.iter().copied().map(NonZeroUsize::get) {
if let Some(col) = row_iter.next() {
write!(&mut value, "{}", col).expect("formatting to a string shouldn't fail");
}
writer.write_all((if i == COLUMN_COUNT - 1 { right } else { intersection }).as_bytes())?;
draw_cell(&mut writer, &value, space)?;
value.clear();
}
writer.write_all("\n".as_bytes())

writer.write_all("\n".as_bytes())?;
}

fn draw_cell<W: Write>(writer: &mut BufWriter<W>, value: &str, space: usize) -> io::Result<()> {
let value_width = value.width();
let padding = if unlikely(value_width > space) {
let mut remaining = space - 1;
for grapheme in value.graphemes(true) {
remaining = match remaining.checked_sub(grapheme.width()) {
Some(r) => r,
None => break,
};
writer.write_all(grapheme.as_bytes())?;
}
writer.write_all("…".as_bytes())?;
remaining
} else {
if value_width < space {
writer.write_all(" ".as_bytes())?;
write_table_end(writer, column_widths)
}

/// Render a table using custom formatters.
///
/// Variant of [`write_table`] that converts values to text using the provided `formatters` instead of the [`Display`]
/// trait. Besides being able to use a different representation than the one provided by a type's [`Display`]
/// implementation, it is also useful for displaying values that do not implement [`Display`].
///
/// # Examples
///
/// ```
/// # use std::net::Ipv4Addr;
/// # use std::num::NonZeroUsize;
/// # use tinytable::write_table_with_fmt;
/// # let stdout = std::io::stdout();
/// use std::fmt::Write;
///
/// let addrs = [
/// Ipv4Addr::new(192, 168, 0, 1),
/// Ipv4Addr::new(1, 1, 1, 1),
/// Ipv4Addr::new(255, 127, 63, 31),
/// ];
/// let column_names = ["Full address", "BE bits", "Private"];
/// let column_widths = [17, 12, 7].map(|n| NonZeroUsize::new(n).expect("non zero"));
///
/// let formatters: [fn(&Ipv4Addr, &mut String) -> std::fmt::Result; 3] = [
/// |addr, f| write!(f, "{}", addr),
/// |addr, f| write!(f, "0x{:x}", addr.to_bits().to_be()),
/// |addr, f| write!(f, "{}", if addr.is_private() { "yes" } else { "no" }),
/// ];
///
/// write_table_with_fmt(stdout.lock(), addrs.iter().copied(), &formatters, &column_names, &column_widths)?;
/// # Ok::<(), std::io::Error>(())
/// ```
///
/// ```non_rust
/// ╭─────────────────┬────────────┬───────╮
/// │ Full address │ BE bits │Private│
/// ├─────────────────┼────────────┼───────┤
/// │ 192.168.0.1 │ 0x100a8c0 │ yes │
/// │ 1.1.1.1 │ 0x1010101 │ no │
/// │ 255.127.63.31 │ 0x1f3f7fff │ no │
/// ╰─────────────────┴────────────┴───────╯
/// ```
///
/// # Errors
///
/// If an I/O error is encountered while writing to the `to` writer, that error will be returned.
pub fn write_table_with_fmt<Row, const COLUMN_COUNT: usize>(
to: impl Write,
iter: impl Iterator<Item = Row>,
formatters: &[fn(&Row, &mut String) -> fmt::Result; COLUMN_COUNT],
column_names: &[&str; COLUMN_COUNT],
column_widths: &[NonZeroUsize; COLUMN_COUNT],
) -> io::Result<()> {
let mut writer = write_table_start(to, column_names, column_widths)?;

let mut value = String::new();
for row in iter {
writer.write_all(VERTICAL_LINE.as_bytes())?;

let mut formatters = formatters.iter();
for space in column_widths.iter().copied().map(NonZeroUsize::get) {
if let Some(formatter) = formatters.next() {
formatter(&row, &mut value).expect("formatting to a string shouldn't fail");
}
writer.write_all(value.as_bytes())?;
(space - value_width).saturating_sub(1)
};
for _ in 0..padding {
writer.write_all(" ".as_bytes())?;
draw_cell(&mut writer, &value, space)?;
value.clear();
}
writer.write_all(VERTICAL_LINE.as_bytes())

writer.write_all("\n".as_bytes())?;
}

write_table_end(writer, column_widths)
}

fn write_table_start<W: Write, const COLUMN_COUNT: usize>(
to: W,
column_names: &[&str; COLUMN_COUNT],
column_widths: &[NonZeroUsize; COLUMN_COUNT],
) -> Result<BufWriter<W>, io::Error> {
let _: () = const { assert!(COLUMN_COUNT > 0, "table must have columns") };

let mut writer = BufWriter::new(to);
draw_horizontal_line(&mut writer, column_widths, TOP_LEFT, TOP_RIGHT, TOP_INTERSECTION)?;

Expand All @@ -173,22 +231,13 @@ pub fn write_table<
INTERSECTION,
)?;

let mut value = String::new();
for row in iter {
writer.write_all(VERTICAL_LINE.as_bytes())?;

let mut row_iter = row.into_iter();
for space in column_widths.iter().copied().map(NonZeroUsize::get) {
if let Some(col) = row_iter.next() {
write!(&mut value, "{}", col).expect("formatting to a string shouldn't fail");
}
draw_cell(&mut writer, &value, space)?;
value.clear();
}

writer.write_all("\n".as_bytes())?;
}
Ok(writer)
}

fn write_table_end<W: Write, const COLUMN_COUNT: usize>(
mut writer: BufWriter<W>,
column_widths: &[NonZeroUsize; COLUMN_COUNT],
) -> Result<(), io::Error> {
draw_horizontal_line(
&mut writer,
column_widths,
Expand All @@ -199,6 +248,49 @@ pub fn write_table<
writer.flush()
}

fn draw_horizontal_line<const COLUMN_COUNT: usize, W: Write>(
writer: &mut BufWriter<W>,
column_widths: &[NonZeroUsize; COLUMN_COUNT],
left: &str,
right: &str,
intersection: &str,
) -> io::Result<()> {
writer.write_all(left.as_bytes())?;
for (i, width) in column_widths.iter().enumerate() {
for _ in 0..width.get() {
writer.write_all(HORIZONTAL_LINE.as_bytes())?;
}
writer.write_all((if i == COLUMN_COUNT - 1 { right } else { intersection }).as_bytes())?;
}
writer.write_all("\n".as_bytes())
}

fn draw_cell<W: Write>(writer: &mut BufWriter<W>, value: &str, space: usize) -> io::Result<()> {
let value_width = value.width();
let padding = if unlikely(value_width > space) {
let mut remaining = space - 1;
for grapheme in value.graphemes(true) {
remaining = match remaining.checked_sub(grapheme.width()) {
Some(r) => r,
None => break,
};
writer.write_all(grapheme.as_bytes())?;
}
writer.write_all("…".as_bytes())?;
remaining
} else {
if value_width < space {
writer.write_all(" ".as_bytes())?;
}
writer.write_all(value.as_bytes())?;
(space - value_width).saturating_sub(1)
};
for _ in 0..padding {
writer.write_all(" ".as_bytes())?;
}
writer.write_all(VERTICAL_LINE.as_bytes())
}

#[allow(clippy::inline_always)]
#[inline(always)]
const fn unlikely(b: bool) -> bool {
Expand Down Expand Up @@ -370,6 +462,52 @@ awefz 234 23
);
assert_consistent_width(&output);
}

mod custom_fmt {
use super::*;
use std::net::Ipv4Addr;

#[test]
fn test() {
let addrs = [
Ipv4Addr::new(192, 168, 0, 1),
Ipv4Addr::new(1, 1, 1, 1),
Ipv4Addr::new(255, 127, 63, 31),
];
let column_names = ["Full address", "BE bits", "Private"];
let column_widths = [nz!(17), nz!(12), nz!(7)];

let formatters: [fn(&Ipv4Addr, &mut String) -> fmt::Result; 3] = [
|a, f| write!(f, "{}", a),
|a, f| write!(f, "0x{:x}", a.to_bits().to_be()),
|a, f| write!(f, "{}", if a.is_private() { "yes" } else { "no" }),
];

let mut output = Vec::new();
write_table_with_fmt(
&mut output,
addrs.iter().copied(),
&formatters,
&column_names,
&column_widths,
)
.expect("write_table failed");

let output = String::from_utf8(output).expect("valid UTF-8");
assert_eq!(
output,
"╭─────────────────┬────────────┬───────╮
│ Full address │ BE bits │Private│
├─────────────────┼────────────┼───────┤
│ 192.168.0.1 │ 0x100a8c0 │ yes │
│ 1.1.1.1 │ 0x1010101 │ no │
│ 255.127.63.31 │ 0x1f3f7fff │ no │
╰─────────────────┴────────────┴───────╯
"
);
assert_consistent_width(&output);
}
}
}

/// ```compile_fail
Expand Down