This commit is contained in:
JIe
2024-11-20 11:44:36 +08:00
commit 1c8e2579b9
32 changed files with 3116 additions and 0 deletions

77
src/cli.rs Normal file
View File

@@ -0,0 +1,77 @@
use std::path::PathBuf;
use clap::{Parser, ValueEnum, ValueHint};
#[derive(Parser)]
#[command(about, version)]
#[command(disable_help_subcommand = true)]
#[command(next_line_help = true)]
pub struct App {
/// File to view.
#[arg(name = "FILE", value_hint = ValueHint::FilePath)]
pub file: Option<PathBuf>,
/// Specify that the input has no header row.
#[arg(short = 'H', long = "no-headers")]
pub no_headers: bool,
/// Prepend a column of line numbers to the table.
#[arg(short, long, alias = "seq")]
pub number: bool,
/// Use '\t' as delimiter for tsv.
#[arg(short, long, conflicts_with = "delimiter")]
pub tsv: bool,
/// Specify the field delimiter.
#[arg(short, long, default_value_t = ',')]
pub delimiter: char,
/// Specify the border style.
#[arg(short, long, value_enum, default_value_t = TableStyle::Sharp, ignore_case = true)]
pub style: TableStyle,
/// Specify padding for table cell.
#[arg(short, long, default_value_t = 1)]
pub padding: usize,
/// Specify global indent for table.
#[arg(short, long, default_value_t = 0)]
pub indent: usize,
/// Limit column widths sniffing to the specified number of rows. Specify "0" to cancel limit.
#[arg(long, default_value_t = 1000, name = "LIMIT")]
pub sniff: usize,
/// Specify the alignment of the table header.
#[arg(long, value_enum, default_value_t = Alignment::Center, ignore_case = true)]
pub header_align: Alignment,
/// Specify the alignment of the table body.
#[arg(long, value_enum, default_value_t = Alignment::Left, ignore_case = true)]
pub body_align: Alignment,
#[cfg(all(feature = "pager", unix))]
/// Disable pager.
#[arg(long, short = 'P')]
pub disable_pager: bool,
}
#[derive(Copy, Clone, ValueEnum)]
pub enum TableStyle {
None,
Ascii,
Ascii2,
Sharp,
Rounded,
Reinforced,
Markdown,
Grid,
}
#[derive(Copy, Clone, ValueEnum)]
pub enum Alignment {
Left,
Center,
Right,
}

102
src/main.rs Normal file
View File

@@ -0,0 +1,102 @@
mod cli;
mod table;
mod util;
use anyhow::bail;
use clap::Parser;
use cli::App;
use csv::{ErrorKind, ReaderBuilder};
use std::{
fs::File,
io::{self, BufWriter, IsTerminal, Read},
process,
};
use table::TablePrinter;
use util::table_style;
#[cfg(all(feature = "pager", unix))]
use pager::Pager;
fn main() {
if let Err(e) = try_main() {
if let Some(ioerr) = e.root_cause().downcast_ref::<io::Error>() {
if ioerr.kind() == io::ErrorKind::BrokenPipe {
process::exit(exitcode::OK);
}
}
if let Some(csverr) = e.root_cause().downcast_ref::<csv::Error>() {
match csverr.kind() {
ErrorKind::Utf8 { .. } => {
eprintln!("[error] input is not utf8 encoded");
process::exit(exitcode::DATAERR)
}
ErrorKind::UnequalLengths { pos, expected_len, len } => {
let pos_info = pos
.as_ref()
.map(|p| format!(" at (byte: {}, line: {}, record: {})", p.byte(), p.line(), p.record()))
.unwrap_or_else(|| "".to_string());
eprintln!(
"[error] unequal lengths{}: expected length is {}, but got {}",
pos_info, expected_len, len
);
process::exit(exitcode::DATAERR)
}
ErrorKind::Io(e) => {
eprintln!("[error] io error: {}", e);
process::exit(exitcode::IOERR)
}
e => {
eprintln!("[error] failed to process input: {:?}", e);
process::exit(exitcode::DATAERR)
}
}
}
eprintln!("{}: {}", env!("CARGO_PKG_NAME"), e);
std::process::exit(1)
}
}
fn try_main() -> anyhow::Result<()> {
let App {
file,
no_headers,
number,
tsv,
delimiter,
style,
padding,
indent,
sniff,
header_align,
body_align,
#[cfg(all(feature = "pager", unix))]
disable_pager,
} = App::parse();
#[cfg(all(feature = "pager", unix))]
if !disable_pager && io::stdout().is_terminal() {
match std::env::var("CSVIEW_PAGER") {
Ok(pager) => Pager::with_pager(&pager).setup(),
// XXX: the extra null byte can be removed once https://gitlab.com/imp/pager-rs/-/merge_requests/8 is merged
Err(_) => Pager::with_pager("less").pager_envs(["LESS=-SF\0"]).setup(),
}
}
let stdout = io::stdout();
let wtr = &mut BufWriter::new(stdout.lock());
let rdr = ReaderBuilder::new()
.delimiter(if tsv { b'\t' } else { delimiter as u8 })
.has_headers(!no_headers)
.from_reader(match file {
Some(path) => Box::new(File::open(path)?) as Box<dyn Read>,
None if io::stdin().is_terminal() => bail!("no input file specified (use -h for help)"),
None => Box::new(io::stdin()),
});
let sniff = if sniff == 0 { usize::MAX } else { sniff };
let table = TablePrinter::new(rdr, sniff, number)?;
table.writeln(wtr, &table_style(style, padding, indent, header_align, body_align))?;
Ok(())
}

6
src/table/mod.rs Normal file
View File

@@ -0,0 +1,6 @@
mod printer;
mod row;
mod style;
pub use printer::TablePrinter;
pub use style::{RowSep, Style, StyleBuilder};

260
src/table/printer.rs Normal file
View File

@@ -0,0 +1,260 @@
use super::{row::Row, style::Style};
use csv::{Reader, StringRecord};
use std::io::{self, Result, Write};
use unicode_width::UnicodeWidthStr;
pub struct TablePrinter {
header: Option<StringRecord>,
widths: Vec<usize>,
records: Box<dyn Iterator<Item = csv::Result<StringRecord>>>,
with_seq: bool,
}
impl TablePrinter {
pub(crate) fn new<R: 'static + io::Read>(mut rdr: Reader<R>, sniff_rows: usize, with_seq: bool) -> Result<Self> {
let header = rdr.has_headers().then(|| rdr.headers()).transpose()?.cloned();
let (widths, buf) = sniff_widths(&mut rdr, header.as_ref(), sniff_rows, with_seq)?;
let records = Box::new(buf.into_iter().map(Ok).chain(rdr.into_records()));
Ok(Self { header, widths, records, with_seq })
}
pub(crate) fn writeln<W: Write>(self, wtr: &mut W, fmt: &Style) -> Result<()> {
let widths = &self.widths;
fmt.rowseps
.top
.map(|sep| fmt.write_row_sep(wtr, widths, &sep))
.transpose()?;
let mut iter = self.records.peekable();
if let Some(header) = self.header {
let row: Row = self.with_seq.then_some("#").into_iter().chain(header.iter()).collect();
row.writeln(wtr, fmt, widths, fmt.header_align)?;
if iter.peek().is_some() {
fmt.rowseps
.snd
.map(|sep| fmt.write_row_sep(wtr, widths, &sep))
.transpose()?;
}
}
let mut seq = 1;
while let Some(record) = iter.next().transpose()? {
let seq_str = self.with_seq.then(|| seq.to_string());
let row: Row = seq_str.iter().map(|s| s.as_str()).chain(record.into_iter()).collect();
row.writeln(wtr, fmt, widths, fmt.body_align)?;
if let Some(mid) = &fmt.rowseps.mid {
if iter.peek().is_some() {
fmt.write_row_sep(wtr, widths, mid)?;
}
}
seq += 1;
}
fmt.rowseps
.bot
.map(|sep| fmt.write_row_sep(wtr, widths, &sep))
.transpose()?;
wtr.flush()
}
}
fn sniff_widths<R: io::Read>(
rdr: &mut Reader<R>,
header: Option<&StringRecord>,
sniff_rows: usize,
with_seq: bool,
) -> Result<(Vec<usize>, Vec<StringRecord>)> {
let mut widths = Vec::new();
let mut buf = Vec::new();
fn update_widths(record: &StringRecord, widths: &mut Vec<usize>) {
widths.resize(record.len(), 0);
record
.into_iter()
.map(UnicodeWidthStr::width_cjk)
.enumerate()
.for_each(|(i, width)| widths[i] = widths[i].max(width))
}
let mut record = header.cloned().unwrap_or_default();
update_widths(&record, &mut widths);
let mut seq = 1;
while seq <= sniff_rows && rdr.read_record(&mut record)? {
update_widths(&record, &mut widths);
seq += 1;
buf.push(record.clone());
}
if with_seq {
widths.insert(0, seq.to_string().width());
}
Ok((widths, buf))
}
#[cfg(test)]
mod test {
use super::*;
use crate::table::{RowSep, StyleBuilder};
use anyhow::Result;
use csv::ReaderBuilder;
macro_rules! gen_table {
($($line:expr)*) => {
concat!(
$($line, "\n",)*
)
};
}
#[test]
fn test_write() -> Result<()> {
let text = "a,b,c\n1,2,3\n4,5,6";
let rdr = ReaderBuilder::new().has_headers(true).from_reader(text.as_bytes());
let wtr = TablePrinter::new(rdr, 3, false)?;
let mut buf = Vec::new();
wtr.writeln(&mut buf, &Style::default())?;
assert_eq!(
gen_table!(
"+---+---+---+"
"| a | b | c |"
"+---+---+---+"
"| 1 | 2 | 3 |"
"+---+---+---+"
"| 4 | 5 | 6 |"
"+---+---+---+"
),
std::str::from_utf8(&buf)?
);
Ok(())
}
#[test]
fn test_write_without_padding() -> Result<()> {
let text = "a,b,c\n1,2,3\n4,5,6";
let rdr = ReaderBuilder::new().has_headers(true).from_reader(text.as_bytes());
let wtr = TablePrinter::new(rdr, 3, false)?;
let fmt = StyleBuilder::default().padding(0).build();
let mut buf = Vec::new();
wtr.writeln(&mut buf, &fmt)?;
assert_eq!(
gen_table!(
"+-+-+-+"
"|a|b|c|"
"+-+-+-+"
"|1|2|3|"
"+-+-+-+"
"|4|5|6|"
"+-+-+-+"
),
std::str::from_utf8(&buf)?
);
Ok(())
}
#[test]
fn test_write_with_indent() -> Result<()> {
let text = "a,b,c\n1,2,3\n4,5,6";
let rdr = ReaderBuilder::new().has_headers(true).from_reader(text.as_bytes());
let wtr = TablePrinter::new(rdr, 3, false)?;
let fmt = StyleBuilder::default().indent(4).build();
let mut buf = Vec::new();
wtr.writeln(&mut buf, &fmt)?;
assert_eq!(
gen_table!(
" +---+---+---+"
" | a | b | c |"
" +---+---+---+"
" | 1 | 2 | 3 |"
" +---+---+---+"
" | 4 | 5 | 6 |"
" +---+---+---+"
),
std::str::from_utf8(&buf)?
);
Ok(())
}
#[test]
fn test_only_header() -> Result<()> {
let text = "a,ab,abc";
let rdr = ReaderBuilder::new().has_headers(true).from_reader(text.as_bytes());
let wtr = TablePrinter::new(rdr, 3, false)?;
let fmt = Style::default();
let mut buf = Vec::new();
wtr.writeln(&mut buf, &fmt)?;
assert_eq!(
gen_table!(
"+---+----+-----+"
"| a | ab | abc |"
"+---+----+-----+"
),
std::str::from_utf8(&buf)?
);
Ok(())
}
#[test]
fn test_without_header() -> Result<()> {
let text = "1,123,35\n383,2, 17";
let rdr = ReaderBuilder::new().has_headers(false).from_reader(text.as_bytes());
let wtr = TablePrinter::new(rdr, 3, false)?;
let fmt = StyleBuilder::new()
.col_sep('│')
.row_seps(
RowSep::new('─', '╭', '┬', '╮'),
RowSep::new('─', '├', '┼', '┤'),
None,
RowSep::new('─', '╰', '┴', '╯'),
)
.build();
let mut buf = Vec::new();
wtr.writeln(&mut buf, &fmt)?;
assert_eq!(
gen_table!(
"╭─────┬─────┬─────╮"
"│ 1 │ 123 │ 35 │"
"│ 383 │ 2 │ 17 │"
"╰─────┴─────┴─────╯"
),
std::str::from_utf8(&buf)?
);
Ok(())
}
#[test]
fn test_with_seq() -> Result<()> {
let text = "a,b,c\n1,2,3\n4,5,6";
let rdr = ReaderBuilder::new().has_headers(true).from_reader(text.as_bytes());
let wtr = TablePrinter::new(rdr, 3, true)?;
let mut buf = Vec::new();
wtr.writeln(&mut buf, &Style::default())?;
assert_eq!(
gen_table!(
"+---+---+---+---+"
"| # | a | b | c |"
"+---+---+---+---+"
"| 1 | 1 | 2 | 3 |"
"+---+---+---+---+"
"| 2 | 4 | 5 | 6 |"
"+---+---+---+---+"
),
std::str::from_utf8(&buf)?
);
Ok(())
}
}

96
src/table/row.rs Normal file
View File

@@ -0,0 +1,96 @@
use std::io::{Result, Write};
use itertools::Itertools;
use unicode_truncate::{Alignment, UnicodeTruncateStr};
use crate::table::Style;
/// Represent a table row made of cells
#[derive(Clone, Debug)]
pub struct Row<'a> {
cells: Vec<&'a str>,
}
impl<'a> FromIterator<&'a str> for Row<'a> {
fn from_iter<I: IntoIterator<Item = &'a str>>(iter: I) -> Self {
Self { cells: iter.into_iter().collect() }
}
}
impl<'a> Row<'a> {
pub fn write<T: Write>(&self, wtr: &mut T, fmt: &Style, widths: &[usize], align: Alignment) -> Result<()> {
let sep = fmt.colseps.mid.map(|c| c.to_string()).unwrap_or_default();
write!(wtr, "{:indent$}", "", indent = fmt.indent)?;
fmt.colseps.lhs.map(|sep| fmt.write_col_sep(wtr, sep)).transpose()?;
Itertools::intersperse(
self.cells
.iter()
.zip(widths)
.map(|(cell, &width)| cell.unicode_pad(width, align, true))
.map(|s| format!("{:pad$}{}{:pad$}", "", s, "", pad = fmt.padding)),
sep,
)
.try_for_each(|s| write!(wtr, "{}", s))?;
fmt.colseps.rhs.map(|sep| fmt.write_col_sep(wtr, sep)).transpose()?;
Ok(())
}
pub fn writeln<T: Write>(&self, wtr: &mut T, fmt: &Style, widths: &[usize], align: Alignment) -> Result<()> {
self.write(wtr, fmt, widths, align).and_then(|_| writeln!(wtr))
}
}
#[cfg(test)]
mod test {
use super::*;
use anyhow::Result;
use std::str;
#[test]
fn write_ascii_row() -> Result<()> {
let row = Row::from_iter(["a", "b"]);
let buf = &mut Vec::new();
let fmt = Style::default();
let widths = [3, 4];
row.writeln(buf, &fmt, &widths, Alignment::Left)?;
assert_eq!("| a | b |\n", str::from_utf8(buf)?);
Ok(())
}
#[test]
fn write_cjk_row() -> Result<()> {
let row = Row::from_iter(["李磊(Jack)", "四川省成都市", "💍"]);
let buf = &mut Vec::new();
let fmt = Style::default();
let widths = [10, 8, 2];
row.writeln(buf, &fmt, &widths, Alignment::Left)?;
assert_eq!("| 李磊(Jack) | 四川省成 | 💍 |\n", str::from_utf8(buf)?);
Ok(())
}
#[test]
fn write_align_center() -> Result<()> {
let row = Row::from_iter(["a", "b"]);
let buf = &mut Vec::new();
let fmt = Style::default();
let widths = [3, 4];
row.writeln(buf, &fmt, &widths, Alignment::Center)?;
assert_eq!("| a | b |\n", str::from_utf8(buf)?);
Ok(())
}
#[test]
fn write_align_right() -> Result<()> {
let row = Row::from_iter(["a", "b"]);
let buf = &mut Vec::new();
let fmt = Style::default();
let widths = [3, 4];
row.writeln(buf, &fmt, &widths, Alignment::Right)?;
assert_eq!("| a | b |\n", str::from_utf8(buf)?);
Ok(())
}
}

277
src/table/style.rs Normal file
View File

@@ -0,0 +1,277 @@
use std::io::{Result, Write};
use unicode_truncate::Alignment;
#[derive(Debug, Clone, Copy)]
pub struct RowSeps {
/// Top separator row (top border)
/// ```
/// >┌───┬───┐
/// │ a │ b │
/// ```
pub top: Option<RowSep>,
/// Second separator row (between the header row and the first data row)
/// ```
/// ┌───┬───┐
/// │ a │ b │
/// >├───┼───┤
/// ```
pub snd: Option<RowSep>,
/// Middle separator row (between data rows)
/// ```
/// >├───┼───┤
/// │ 2 │ 2 │
/// >├───┼───┤
/// ```
pub mid: Option<RowSep>,
/// Bottom separator row (bottom border)
/// ```
/// │ 3 │ 3 │
/// >└───┴───┘
/// ```
pub bot: Option<RowSep>,
}
/// The characters used for printing a row separator
#[derive(Debug, Clone, Copy)]
pub struct RowSep {
/// Inner row separator
/// ```
/// ┌───┬───┐
/// ^
/// ```
inner: char,
/// Left junction separator
/// ```
/// ┌───┬───┐
/// ^
/// ```
ljunc: char,
/// Crossing junction separator
/// ```
/// ┌───┬───┐
/// ^
/// ```
cjunc: char,
/// Right junction separator
/// ```
/// ┌───┬───┐
/// ^
/// ```
rjunc: char,
}
#[derive(Debug, Clone, Copy)]
pub struct ColSeps {
/// Left separator column (left border)
/// ```
/// │ 1 │ 2 │
/// ^
/// ```
pub lhs: Option<char>,
/// Middle column separators
/// ```
/// │ 1 │ 2 │
/// ^
/// ```
pub mid: Option<char>,
/// Right separator column (right border)
/// ```
/// │ 1 │ 2 │
/// ^
/// ```
pub rhs: Option<char>,
}
impl RowSep {
pub fn new(sep: char, ljunc: char, cjunc: char, rjunc: char) -> RowSep {
RowSep { inner: sep, ljunc, cjunc, rjunc }
}
}
impl Default for RowSep {
fn default() -> Self {
RowSep::new('-', '+', '+', '+')
}
}
impl Default for RowSeps {
fn default() -> Self {
Self {
top: Some(RowSep::default()),
snd: Some(RowSep::default()),
mid: Some(RowSep::default()),
bot: Some(RowSep::default()),
}
}
}
impl Default for ColSeps {
fn default() -> Self {
Self { lhs: Some('|'), mid: Some('|'), rhs: Some('|') }
}
}
#[derive(Debug, Clone, Copy)]
pub struct Style {
/// Column style
pub colseps: ColSeps,
/// Row style
pub rowseps: RowSeps,
/// Left and right padding
pub padding: usize,
/// Global indentation
pub indent: usize,
/// Header alignment
pub header_align: Alignment,
/// Data alignment
pub body_align: Alignment,
}
impl Default for Style {
fn default() -> Self {
Self {
indent: 0,
padding: 1,
colseps: ColSeps::default(),
rowseps: RowSeps::default(),
header_align: Alignment::Center,
body_align: Alignment::Left,
}
}
}
impl Style {
pub fn write_row_sep<W: Write>(&self, wtr: &mut W, widths: &[usize], sep: &RowSep) -> Result<()> {
write!(wtr, "{:indent$}", "", indent = self.indent)?;
if self.colseps.lhs.is_some() {
write!(wtr, "{}", sep.ljunc)?;
}
let mut iter = widths.iter().peekable();
while let Some(width) = iter.next() {
for _ in 0..width + self.padding * 2 {
write!(wtr, "{}", sep.inner)?;
}
if self.colseps.mid.is_some() && iter.peek().is_some() {
write!(wtr, "{}", sep.cjunc)?;
}
}
if self.colseps.rhs.is_some() {
write!(wtr, "{}", sep.rjunc)?;
}
writeln!(wtr)
}
#[inline]
pub fn write_col_sep<W: Write>(&self, wtr: &mut W, sep: char) -> Result<()> {
write!(wtr, "{}", sep)
}
}
#[derive(Default, Debug, Clone)]
pub struct StyleBuilder {
format: Box<Style>,
}
impl StyleBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn padding(mut self, padding: usize) -> Self {
self.format.padding = padding;
self
}
pub fn col_sep(self, sep: impl Into<Option<char>>) -> Self {
let sep = sep.into();
self.col_seps(sep, sep, sep)
}
pub fn col_seps<L, M, R>(mut self, lhs: L, mid: M, rhs: R) -> Self
where
L: Into<Option<char>>,
M: Into<Option<char>>,
R: Into<Option<char>>,
{
self.format.colseps = ColSeps { lhs: lhs.into(), mid: mid.into(), rhs: rhs.into() };
self
}
pub fn row_seps<S1, S2, S3, S4>(mut self, top: S1, snd: S2, mid: S3, bot: S4) -> Self
where
S1: Into<Option<RowSep>>,
S2: Into<Option<RowSep>>,
S3: Into<Option<RowSep>>,
S4: Into<Option<RowSep>>,
{
self.format.rowseps = RowSeps {
top: top.into(),
snd: snd.into(),
mid: mid.into(),
bot: bot.into(),
};
self
}
pub fn clear_seps(self) -> Self {
self.col_seps(None, None, None).row_seps(None, None, None, None)
}
pub fn indent(mut self, indent: usize) -> Self {
self.format.indent = indent;
self
}
pub fn header_align(mut self, align: Alignment) -> Self {
self.format.header_align = align;
self
}
pub fn body_align(mut self, align: Alignment) -> Self {
self.format.body_align = align;
self
}
pub fn build(&self) -> Style {
*self.format
}
}
#[cfg(test)]
mod test {
use super::*;
use anyhow::Result;
use std::str;
#[test]
fn test_write_column_separator() -> Result<()> {
let fmt = StyleBuilder::new().col_seps('|', '|', '|').padding(1).build();
let buf = &mut Vec::new();
fmt.colseps.lhs.map(|sep| fmt.write_col_sep(buf, sep)).transpose()?;
assert_eq!("|", str::from_utf8(buf)?);
Ok(())
}
#[test]
fn test_write_row_separator() -> Result<()> {
let fmt = StyleBuilder::new().indent(4).build();
let buf = &mut Vec::new();
let widths = &[2, 4, 6];
fmt.rowseps
.top
.map(|sep| fmt.write_row_sep(buf, widths, &sep))
.transpose()?;
assert_eq!(" +----+------+--------+\n", str::from_utf8(buf)?);
Ok(())
}
}

74
src/util.rs Normal file
View File

@@ -0,0 +1,74 @@
use cli::Alignment;
use crate::{
cli::{self, TableStyle},
table::{RowSep, Style, StyleBuilder},
};
pub fn table_style(
style: TableStyle,
padding: usize,
indent: usize,
header_align: Alignment,
body_align: Alignment,
) -> Style {
let builder = match style {
TableStyle::None => StyleBuilder::new().clear_seps(),
TableStyle::Ascii => StyleBuilder::new().col_sep('|').row_seps(
RowSep::new('-', '+', '+', '+'),
RowSep::new('-', '+', '+', '+'),
None,
RowSep::new('-', '+', '+', '+'),
),
TableStyle::Ascii2 => {
StyleBuilder::new()
.col_seps(' ', '|', ' ')
.row_seps(None, RowSep::new('-', ' ', '+', ' '), None, None)
}
TableStyle::Sharp => StyleBuilder::new().col_sep('│').row_seps(
RowSep::new('─', '┌', '┬', '┐'),
RowSep::new('─', '├', '┼', '┤'),
None,
RowSep::new('─', '└', '┴', '┘'),
),
TableStyle::Rounded => StyleBuilder::new().col_sep('│').row_seps(
RowSep::new('─', '╭', '┬', '╮'),
RowSep::new('─', '├', '┼', '┤'),
None,
RowSep::new('─', '╰', '┴', '╯'),
),
TableStyle::Reinforced => StyleBuilder::new().col_sep('│').row_seps(
RowSep::new('─', '┏', '┬', '┓'),
RowSep::new('─', '├', '┼', '┤'),
None,
RowSep::new('─', '┗', '┴', '┛'),
),
TableStyle::Markdown => {
StyleBuilder::new()
.col_sep('|')
.row_seps(None, RowSep::new('-', '|', '|', '|'), None, None)
}
TableStyle::Grid => StyleBuilder::new().col_sep('│').row_seps(
RowSep::new('─', '┌', '┬', '┐'),
RowSep::new('─', '├', '┼', '┤'),
RowSep::new('─', '├', '┼', '┤'),
RowSep::new('─', '└', '┴', '┘'),
),
};
builder
.padding(padding)
.indent(indent)
.header_align(header_align.into())
.body_align(body_align.into())
.build()
}
impl From<Alignment> for unicode_truncate::Alignment {
fn from(a: Alignment) -> Self {
match a {
Alignment::Left => unicode_truncate::Alignment::Left,
Alignment::Center => unicode_truncate::Alignment::Center,
Alignment::Right => unicode_truncate::Alignment::Right,
}
}
}