init
This commit is contained in:
77
src/cli.rs
Normal file
77
src/cli.rs
Normal 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
102
src/main.rs
Normal 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
6
src/table/mod.rs
Normal 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
260
src/table/printer.rs
Normal 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
96
src/table/row.rs
Normal 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
277
src/table/style.rs
Normal 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
74
src/util.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user