use std::{
io::{Error, ErrorKind, Write},
iter::repeat,
};
use encode_unicode::Utf8Char;
#[cfg(any(not(windows), not(feature = "win_crlf")))]
pub static NEWLINE: &[u8] = b"\n";
#[cfg(all(windows, feature = "win_crlf"))]
pub static NEWLINE: &[u8] = b"\r\n";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Alignment {
Left,
Right,
Center,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TableConfig {
pub col_width: Vec<usize>,
pub padding: (usize, usize),
pub align: Alignment,
pub colsep: bool,
pub lborder: bool,
pub rborder: bool,
}
impl TableConfig {
pub fn new(
col_width: Vec<usize>,
padding: (usize, usize),
align: Alignment,
colsep: bool,
lborder: bool,
rborder: bool,
) -> TableConfig {
TableConfig {
col_width,
padding,
align,
colsep,
lborder,
rborder,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TableLogger {
line: char,
junc: char,
ljunc: char,
rjunc: char,
}
impl TableLogger {
pub fn new(line: char, junc: char, ljunc: char, rjunc: char) -> TableLogger {
TableLogger {
line,
junc,
ljunc,
rjunc,
}
}
pub fn display_width(text: &str) -> usize {
let width = text.len();
let mut state = 0;
let mut hidden = 0;
for c in text.chars() {
state = match (state, c) {
(0, '\u{1b}') => 1,
(1, '[') => 2,
(1, _) => 0,
(2, 'm') => 3,
_ => state,
};
if state > 1 {
hidden += 1;
}
if state == 3 {
state = 0;
}
}
width - hidden
}
fn align<T: Write + ?Sized>(
out: &mut T,
align: Alignment,
text: &str,
fill: char,
size: usize,
) -> Result<(), Error> {
let text_len = TableLogger::display_width(text);
let mut nfill = if text_len < size { size - text_len } else { 0 };
let n = match align {
Alignment::Left => 0,
Alignment::Right => nfill,
Alignment::Center => nfill / 2,
};
if n > 0 {
out.write_all(repeat(fill).take(n).collect::<String>().as_bytes())?;
nfill -= n;
}
out.write_all(text.as_bytes())?;
if nfill > 0 {
out.write_all(repeat(fill).take(nfill).collect::<String>().as_bytes())?;
}
Ok(())
}
pub fn log_separator<T: Write + ?Sized>(
&self,
out: &mut T,
col_width: &[usize],
padding: (usize, usize),
colsep: bool,
lborder: bool,
rborder: bool,
newline: bool,
) -> Result<usize, Error> {
if lborder {
out.write_all(Utf8Char::from(self.ljunc).as_bytes())?;
}
let mut iter = col_width.iter().peekable();
while let Some(w) = iter.next() {
for _ in 0..w + padding.0 + padding.1 {
out.write_all(Utf8Char::from(self.line).as_bytes())?;
}
if colsep && iter.peek().is_some() {
out.write_all(Utf8Char::from(self.junc).as_bytes())?;
}
}
if rborder {
out.write_all(Utf8Char::from(self.rjunc).as_bytes())?;
}
if newline {
out.write_all(NEWLINE)?;
}
Ok(1)
}
pub fn log_separator_with_config<T: Write + ?Sized>(
&self,
out: &mut T,
config: &TableConfig,
newline: bool,
) -> Result<usize, Error> {
self.log_separator(
out,
&config.col_width,
config.padding,
config.colsep,
config.lborder,
config.rborder,
newline,
)
}
pub fn log_value<T: Write + ?Sized, F: ToString>(
&self,
out: &mut T,
value: Vec<F>,
col_width: &[usize],
padding: (usize, usize),
align: Alignment,
colsep: bool,
lborder: bool,
rborder: bool,
) -> Result<usize, Error> {
if lborder {
out.write_all(Utf8Char::from(self.ljunc).as_bytes())?;
}
assert_eq!(value.len(), col_width.len());
let mut iter = value.iter().zip(col_width.iter()).peekable();
while let Some((v, w)) = iter.next() {
let has_next: bool = iter.peek().is_some();
TableLogger::align(out, align, &v.to_string(), ' ', *w + padding.0 + padding.1)?;
if colsep && has_next {
out.write_all(Utf8Char::from(self.junc).as_bytes())?;
}
}
if rborder {
out.write_all(Utf8Char::from(self.rjunc).as_bytes())?;
}
out.write_all(NEWLINE)?;
Ok(1)
}
pub fn log_value_with_config<T: Write + ?Sized, F: ToString>(
&self,
out: &mut T,
value: Vec<F>,
config: &TableConfig,
) -> Result<usize, Error> {
self.log_value(
out,
value,
&config.col_width,
config.padding,
config.align,
config.colsep,
config.lborder,
config.rborder,
)
}
}
impl Default for TableLogger {
fn default() -> Self {
TableLogger::new('-', '+', '+', '+')
}
}
pub struct StringWriter {
string: String,
}
impl StringWriter {
pub fn new() -> StringWriter {
StringWriter {
string: String::new(),
}
}
pub fn as_string(&self) -> &str {
&self.string
}
}
impl Write for StringWriter {
fn write(&mut self, data: &[u8]) -> Result<usize, Error> {
let string = match std::str::from_utf8(data) {
Ok(s) => s,
Err(e) => {
return Err(Error::new(
ErrorKind::Other,
format!("Cannot decode utf8 string : {}", e),
))
}
};
self.string.push_str(string);
Ok(data.len())
}
fn flush(&mut self) -> Result<(), Error> {
Ok(())
}
}
pub mod consts {
lazy_static! {
pub static ref DEFAULT_TABLE_LOGGER: super::TableLogger = super::TableLogger::default();
pub static ref DEFAULT_INNER_TABLE_LOGGER: super::TableLogger = super::TableLogger::new('-', '|', '+', '+');
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_table_formatter() {
let mut writer = StringWriter::new();
let sep = TableLogger::default();
sep.log_separator(&mut writer, &[10, 8, 5], (2, 2), true, true, true, true)
.unwrap();
assert_eq!(
writer.as_string(),
"+--------------+------------+---------+\n"
);
sep.log_value(
&mut writer,
vec!["foo", "bar", "baz"],
&[10, 8, 5],
(2, 2),
Alignment::Left,
true,
true,
true,
)
.unwrap();
assert_eq!(
writer.as_string(),
"+--------------+------------+---------+\n+foo +bar +baz +\n"
);
}
}