make use of termcolor crate to disable colors (#236)

* table: replace custom color by termcolor

* table: deactivate colors if not tty

* table: rename printable to print, add more comments

* table: make use of writters, fix tests

* doc: update changelog

* doc: add page to wiki
This commit is contained in:
Clément DOUIN 2021-10-24 00:17:12 +02:00 committed by GitHub
parent 09d3de5e6f
commit 192445d7e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 303 additions and 247 deletions

View file

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added
- Disable color support [#185]
### Fixed ### Fixed
- Error when receiving notification from `notify` command [#228] - Error when receiving notification from `notify` command [#228]
@ -334,6 +338,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#160]: https://github.com/soywod/himalaya/issues/160 [#160]: https://github.com/soywod/himalaya/issues/160
[#162]: https://github.com/soywod/himalaya/issues/162 [#162]: https://github.com/soywod/himalaya/issues/162
[#176]: https://github.com/soywod/himalaya/issues/176 [#176]: https://github.com/soywod/himalaya/issues/176
[#185]: https://github.com/soywod/himalaya/issues/185
[#186]: https://github.com/soywod/himalaya/issues/186 [#186]: https://github.com/soywod/himalaya/issues/186
[#190]: https://github.com/soywod/himalaya/issues/190 [#190]: https://github.com/soywod/himalaya/issues/190
[#193]: https://github.com/soywod/himalaya/issues/193 [#193]: https://github.com/soywod/himalaya/issues/193

1
Cargo.lock generated
View file

@ -381,6 +381,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"shellexpand", "shellexpand",
"termcolor",
"terminal_size", "terminal_size",
"toml", "toml",
"tree_magic", "tree_magic",

View file

@ -26,6 +26,7 @@ rfc2047-decoder = "0.1.2"
serde = { version = "1.0.118", features = ["derive"] } serde = { version = "1.0.118", features = ["derive"] }
serde_json = "1.0.61" serde_json = "1.0.61"
shellexpand = "2.1.0" shellexpand = "2.1.0"
termcolor = "1.1"
terminal_size = "0.1.15" terminal_size = "0.1.15"
toml = "0.5.8" toml = "0.5.8"
tree_magic = "0.2.3" tree_magic = "0.2.3"

View file

@ -20,13 +20,12 @@ pub fn list<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceI
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use serde::Serialize; use serde::Serialize;
use std::fmt::Display;
use super::*; use super::*;
use crate::{ use crate::{
config::Config, config::Config,
domain::{AttrRemote, Attrs, Envelopes, Flags, Mbox, Mboxes, Msg}, domain::{AttrRemote, Attrs, Envelopes, Flags, Mbox, Mboxes, Msg},
output::OutputJson, output::{OutputJson, Print},
}; };
#[test] #[test]
@ -34,7 +33,7 @@ mod tests {
struct OutputServiceTest; struct OutputServiceTest;
impl OutputServiceInterface for OutputServiceTest { impl OutputServiceInterface for OutputServiceTest {
fn print<T: Serialize + Display>(&self, data: T) -> Result<()> { fn print<T: Serialize + Print>(&self, data: T) -> Result<()> {
let data = serde_json::to_string(&OutputJson::new(data))?; let data = serde_json::to_string(&OutputJson::new(data))?;
assert_eq!( assert_eq!(
data, data,

View file

@ -2,14 +2,13 @@
//! //!
//! This module contains the definition of the mailboxes and its traits implementations. //! This module contains the definition of the mailboxes and its traits implementations.
use anyhow::Result;
use serde::Serialize; use serde::Serialize;
use std::{ use std::ops::Deref;
fmt::{self, Display},
ops::Deref,
};
use crate::{ use crate::{
domain::{Mbox, RawMbox}, domain::{Mbox, RawMbox},
output::{Print, WriteWithColor},
ui::Table, ui::Table,
}; };
@ -29,10 +28,11 @@ impl<'a> Deref for Mboxes<'a> {
} }
} }
/// Makes the mailboxes displayable. /// Makes the mailboxes printable.
impl<'a> Display for Mboxes<'a> { impl<'a> Print for Mboxes<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn print<W: WriteWithColor>(&self, writter: &mut W) -> Result<()> {
writeln!(f, "\n{}", Table::render(&self)) writeln!(writter)?;
Table::println(writter, &self)
} }
} }

View file

@ -1,13 +1,10 @@
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use serde::Serialize; use serde::Serialize;
use std::{ use std::{convert::TryFrom, ops::Deref};
convert::TryFrom,
fmt::{self, Display},
ops::Deref,
};
use crate::{ use crate::{
domain::{msg::Envelope, RawEnvelope}, domain::{msg::Envelope, RawEnvelope},
output::{Print, WriteWithColor},
ui::Table, ui::Table,
}; };
@ -39,8 +36,9 @@ impl<'a> TryFrom<&'a RawEnvelopes> for Envelopes<'a> {
} }
} }
impl<'a> Display for Envelopes<'a> { impl<'a> Print for Envelopes<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn print<W: WriteWithColor>(&self, writter: &mut W) -> Result<()> {
writeln!(f, "\n{}", Table::render(&self)) println!();
Table::println(writter, &self)
} }
} }

View file

@ -7,3 +7,6 @@ pub use output_utils::*;
pub mod output_service; pub mod output_service;
pub use output_service::*; pub use output_service::*;
pub mod print;
pub use print::*;

View file

@ -1,10 +1,14 @@
use anyhow::{anyhow, Error, Result}; use anyhow::{anyhow, Error, Result};
use atty::Stream;
use log::debug; use log::debug;
use serde::Serialize; use serde::Serialize;
use std::{ use std::{
convert::{TryFrom, TryInto}, convert::{TryFrom, TryInto},
fmt, fmt,
}; };
use termcolor::{ColorChoice, StandardStream};
use crate::output::Print;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum OutputFmt { pub enum OutputFmt {
@ -58,7 +62,7 @@ impl<T: Serialize> OutputJson<T> {
} }
pub trait OutputServiceInterface { pub trait OutputServiceInterface {
fn print<T: Serialize + fmt::Display>(&self, data: T) -> Result<()>; fn print<T: Serialize + Print>(&self, data: T) -> Result<()>;
fn is_json(&self) -> bool; fn is_json(&self) -> bool;
} }
@ -70,10 +74,21 @@ pub struct OutputService {
impl OutputServiceInterface for OutputService { impl OutputServiceInterface for OutputService {
/// Print the provided item out according to the formatting setting when you created this /// Print the provided item out according to the formatting setting when you created this
/// struct. /// struct.
fn print<T: Serialize + fmt::Display>(&self, data: T) -> Result<()> { fn print<T: Serialize + Print>(&self, data: T) -> Result<()> {
match self.fmt { match self.fmt {
OutputFmt::Plain => { OutputFmt::Plain => {
println!("{}", data) data.print(&mut StandardStream::stdout(if atty::isnt(Stream::Stdin) {
// Colors should be deactivated if the terminal is not a tty.
ColorChoice::Never
} else {
// Otherwise let's `termcolor` decide by inspecting the environment. From the [doc]:
// - If `NO_COLOR` is set to any value, then colors will be suppressed.
// - If `TERM` is set to dumb, then colors will be suppressed.
// - In non-Windows environments, if `TERM` is not set, then colors will be suppressed.
//
// [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection
ColorChoice::Auto
}))?;
} }
OutputFmt::Json => { OutputFmt::Json => {
print!("{}", serde_json::to_string(&OutputJson::new(data))?) print!("{}", serde_json::to_string(&OutputJson::new(data))?)

28
src/output/print.rs Normal file
View file

@ -0,0 +1,28 @@
use anyhow::{Context, Result};
use std::io;
use termcolor::{StandardStream, WriteColor};
pub trait WriteWithColor: io::Write + WriteColor {}
impl WriteWithColor for StandardStream {}
pub trait Print {
fn print<W: WriteWithColor>(&self, writter: &mut W) -> Result<()>;
fn println<W: WriteWithColor>(&self, writter: &mut W) -> Result<()> {
println!();
self.print(writter)
}
}
impl Print for &str {
fn print<W: WriteWithColor>(&self, writter: &mut W) -> Result<()> {
write!(writter, "{}", self).context(format!(r#"cannot print string "{}""#, self))
}
}
impl Print for String {
fn print<W: WriteWithColor>(&self, writter: &mut W) -> Result<()> {
self.as_str().print(writter)
}
}

View file

@ -4,100 +4,66 @@
//! //!
//! [builder design pattern]: https://refactoring.guru/design-patterns/builder //! [builder design pattern]: https://refactoring.guru/design-patterns/builder
use anyhow::{Context, Result};
use log::trace; use log::trace;
use std::fmt; use termcolor::{Color, ColorSpec};
use terminal_size; use terminal_size;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
/// Define the default terminal size. use crate::output::{Print, WriteWithColor};
/// It is used when the size cannot be determined by the `terminal_size` crate.
/// Defines the default terminal size.
/// This is used when the size cannot be determined by the `terminal_size` crate.
/// TODO: make this customizable.
pub const DEFAULT_TERM_WIDTH: usize = 80; pub const DEFAULT_TERM_WIDTH: usize = 80;
/// Define the minimum size of a shrinked cell. /// Defines the minimum size of a shrinked cell.
/// TODO: make this customizable. /// TODO: make this customizable.
pub const MAX_SHRINK_WIDTH: usize = 5; pub const MAX_SHRINK_WIDTH: usize = 5;
/// Wrapper around [ANSI escape codes] for styling cells. /// Represents a cell in a table.
/// #[derive(Debug, Default)]
/// [ANSI escape codes]: https://en.wikipedia.org/wiki/ANSI_escape_code
#[derive(Debug)]
pub struct Style(
/// The style/color code.
u8,
/// The brightness code.
u8,
/// The shade code.
u8,
);
impl fmt::Display for Style {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Style(color, bright, shade) = self;
let mut style = String::new();
// Push first the style/color code.
style.push_str(&color.to_string());
// Then push the brightness code if exist.
if *bright > 0 {
style.push_str(";");
style.push_str(&bright.to_string());
};
// Then push the shade code if exist.
if *shade > 0 {
style.push_str(";");
style.push_str(&shade.to_string());
};
write!(f, "\x1b[{}m", style)
}
}
/// Representation of a table cell.
#[derive(Debug)]
pub struct Cell { pub struct Cell {
/// The list of style applied to the cell. /// Represents the style of the cell.
styles: Vec<Style>, style: ColorSpec,
/// The content of the cell. /// Represents the content of the cell.
value: String, value: String,
/// Allow/disallow the cell to shrink when the table exceeds the container width. /// (Dis)allowes the cell to shrink when the table exceeds the container width.
shrinkable: bool, shrinkable: bool,
} }
impl Cell { impl Cell {
pub fn new<T: AsRef<str>>(value: T) -> Self { pub fn new<T: AsRef<str>>(value: T) -> Self {
Self { Self {
styles: Vec::new(),
value: String::from(value.as_ref()).replace(&['\r', '\n', '\t'][..], ""), value: String::from(value.as_ref()).replace(&['\r', '\n', '\t'][..], ""),
shrinkable: false, ..Self::default()
} }
} }
/// Return the unicode width of the cell's value. /// Returns the unicode width of the cell's value.
pub fn unicode_width(&self) -> usize { pub fn unicode_width(&self) -> usize {
UnicodeWidthStr::width(self.value.as_str()) UnicodeWidthStr::width(self.value.as_str())
} }
/// Make the cell shrinkable. If the table exceeds the terminal width, this cell will be the /// Makes the cell shrinkable. If the table exceeds the terminal width, this cell will be the
/// one to shrink in order to prevent the table to overflow. /// one to shrink in order to prevent the table to overflow.
pub fn shrinkable(mut self) -> Self { pub fn shrinkable(mut self) -> Self {
self.shrinkable = true; self.shrinkable = true;
self self
} }
/// Return the shrinkable state of a cell. /// Returns the shrinkable state of a cell.
pub fn is_shrinkable(&self) -> bool { pub fn is_shrinkable(&self) -> bool {
self.shrinkable self.shrinkable
} }
/// Apply the bold style to the cell. /// Applies the bold style to the cell.
pub fn bold(mut self) -> Self { pub fn bold(mut self) -> Self {
self.styles.push(Style(1, 0, 0)); self.style.set_bold(true);
self self
} }
/// Apply the bold style to the cell conditionally. /// Applies the bold style to the cell conditionally.
pub fn bold_if(self, predicate: bool) -> Self { pub fn bold_if(self, predicate: bool) -> Self {
if predicate { if predicate {
self.bold() self.bold()
@ -106,76 +72,85 @@ impl Cell {
} }
} }
/// Apply the underline style to the cell. /// Applies the underline style to the cell.
pub fn underline(mut self) -> Self { pub fn underline(mut self) -> Self {
self.styles.push(Style(4, 0, 0)); self.style.set_underline(true);
self self
} }
/// Apply the red color to the cell. /// Applies the red color to the cell.
pub fn red(mut self) -> Self { pub fn red(mut self) -> Self {
self.styles.push(Style(31, 0, 0)); self.style.set_fg(Some(Color::Red));
self self
} }
/// Apply the green color to the cell. /// Applies the green color to the cell.
pub fn green(mut self) -> Self { pub fn green(mut self) -> Self {
self.styles.push(Style(32, 0, 0)); self.style.set_fg(Some(Color::Green));
self self
} }
/// Apply the yellow color to the cell. /// Applies the yellow color to the cell.
pub fn yellow(mut self) -> Self { pub fn yellow(mut self) -> Self {
self.styles.push(Style(33, 0, 0)); self.style.set_fg(Some(Color::Yellow));
self self
} }
/// Apply the blue color to the cell. /// Applies the blue color to the cell.
pub fn blue(mut self) -> Self { pub fn blue(mut self) -> Self {
self.styles.push(Style(34, 0, 0)); self.style.set_fg(Some(Color::Blue));
self self
} }
/// Apply the white color to the cell. /// Applies the white color to the cell.
pub fn white(mut self) -> Self { pub fn white(mut self) -> Self {
self.styles.push(Style(37, 0, 0)); self.style.set_fg(Some(Color::White));
self self
} }
/// Apply the custom shade color to the cell. /// Applies the custom ansi color to the cell.
pub fn ext(mut self, shade: u8) -> Self { pub fn ansi_256(mut self, code: u8) -> Self {
self.styles.push(Style(38, 5, shade)); self.style.set_fg(Some(Color::Ansi256(code)));
self self
} }
} }
impl fmt::Display for Cell { /// Makes the cell printable.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { impl Print for Cell {
if self.styles.is_empty() { fn print<W: WriteWithColor>(&self, writter: &mut W) -> Result<()> {
write!(f, "{}", self.value)?; //let color_choice = if atty::isnt(Stream::Stdin) {
} else { // // Colors should be deactivated if the terminal is not a tty.
for style in &self.styles { // ColorChoice::Never
write!(f, "{}", style)?; //} else {
} // // Otherwise let's `termcolor` decide by inspecting the environment. From the [doc]:
write!(f, "{}", self.value)?; // // - If `NO_COLOR` is set to any value, then colors will be suppressed.
// Apply the reset style in order to avoid style overlapping between cells. // // - If `TERM` is set to dumb, then colors will be suppressed.
write!(f, "{}", Style(0, 0, 0))?; // // - In non-Windows environments, if `TERM` is not set, then colors will be suppressed.
} // //
// // [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection
// ColorChoice::Auto
//};
Ok(()) // Applies colors to the cell
writter
.set_color(&self.style)
.context(format!(r#"cannot apply colors to cell "{}""#, self.value))?;
// Writes the colorized cell to stdout
write!(writter, "{}", self.value).context(format!(r#"cannot print cell "{}""#, self.value))
} }
} }
/// Representation of a table row. /// Represents a row in a table.
#[derive(Debug)] #[derive(Debug, Default)]
pub struct Row( pub struct Row(
/// A row contains a list of cells. /// Represents a list of cells.
pub Vec<Cell>, pub Vec<Cell>,
); );
impl Row { impl Row {
pub fn new() -> Self { pub fn new() -> Self {
Self(Vec::new()) Self::default()
} }
pub fn cell(mut self, cell: Cell) -> Self { pub fn cell(mut self, cell: Cell) -> Self {
@ -184,26 +159,27 @@ impl Row {
} }
} }
/// Abstract representation of a table. /// Represents a table abstraction.
pub trait Table pub trait Table
where where
Self: Sized, Self: Sized,
{ {
/// Defines the header row.
fn head() -> Row; fn head() -> Row;
/// Defines the row template.
fn row(&self) -> Row; fn row(&self) -> Row;
/// Determine the max width of the table. /// Determines the max width of the table.
/// The default implementation takes the terminal width as /// The default implementation takes the terminal width as the maximum width of the table.
/// the maximum width of the table.
fn max_width() -> usize { fn max_width() -> usize {
terminal_size::terminal_size() terminal_size::terminal_size()
.map(|(w, _)| w.0 as usize) .map(|(w, _)| w.0 as usize)
.unwrap_or(DEFAULT_TERM_WIDTH) .unwrap_or(DEFAULT_TERM_WIDTH)
} }
/// Apply styles to cells and return a list of list of printable styled cells. /// Prints the table.
/// TODO: find a way to build an unstyled version of cells. fn println<W: WriteWithColor>(writter: &mut W, items: &[Self]) -> Result<()> {
fn build(items: &[Self]) -> Vec<Vec<String>> {
let mut table = vec![Self::head()]; let mut table = vec![Self::head()];
let mut cell_widths: Vec<usize> = let mut cell_widths: Vec<usize> =
table[0].0.iter().map(|cell| cell.unicode_width()).collect(); table[0].0.iter().map(|cell| cell.unicode_width()).collect();
@ -219,104 +195,123 @@ where
}) })
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
); );
trace!("cell_widths: {:?}", cell_widths); trace!("cell widths: {:?}", cell_widths);
let spaces_plus_separators_len = cell_widths.len() * 2 - 1; let spaces_plus_separators_len = cell_widths.len() * 2 - 1;
let table_width = cell_widths.iter().sum::<usize>() + spaces_plus_separators_len; let table_width = cell_widths.iter().sum::<usize>() + spaces_plus_separators_len;
trace!("table_width: {}", table_width); trace!("table width: {}", table_width);
table for row in table.iter_mut() {
.iter_mut() let mut glue = Cell::default();
.map(|row| { for (i, cell) in row.0.iter_mut().enumerate() {
trace!("processing row: {:?}", row); glue.print(writter)?;
row.0
.iter_mut()
.enumerate()
.map(|(i, cell)| {
trace!("processing cell: {:?}", cell);
trace!("table_width: {}", table_width);
trace!("max_width: {}", Self::max_width());
let table_is_overflowing = table_width > Self::max_width(); let table_is_overflowing = table_width > Self::max_width();
if table_is_overflowing && cell.is_shrinkable() { if table_is_overflowing && cell.is_shrinkable() {
trace!("table is overflowing and cell is shrinkable"); trace!("table is overflowing and cell is shrinkable");
let shrink_width = table_width - Self::max_width(); let shrink_width = table_width - Self::max_width();
trace!("shrink_width: {}", shrink_width); trace!("shrink width: {}", shrink_width);
let cell_width = if shrink_width + MAX_SHRINK_WIDTH < cell_widths[i] { let cell_width = if shrink_width + MAX_SHRINK_WIDTH < cell_widths[i] {
cell_widths[i] - shrink_width cell_widths[i] - shrink_width
} else { } else {
MAX_SHRINK_WIDTH MAX_SHRINK_WIDTH
}; };
trace!("cell_width: {}", cell_width); trace!("cell width: {}", cell_width);
trace!("cell unicode_width: {}", cell.unicode_width()); trace!("cell unicode width: {}", cell.unicode_width());
let cell_is_overflowing = cell.unicode_width() > cell_width; let cell_is_overflowing = cell.unicode_width() > cell_width;
if cell_is_overflowing { if cell_is_overflowing {
trace!("cell is overflowing"); trace!("cell is overflowing");
let mut value = String::new(); let mut value = String::new();
let mut chars_width = 0; let mut chars_width = 0;
for c in cell.value.chars() { for c in cell.value.chars() {
let char_width = UnicodeWidthStr::width(c.to_string().as_str()); let char_width = UnicodeWidthStr::width(c.to_string().as_str());
if chars_width + char_width >= cell_width { if chars_width + char_width >= cell_width {
break; break;
}
chars_width += char_width;
value.push(c);
}
value.push_str("");
trace!("chars_width: {}", chars_width);
trace!("shrinked value: {}", value);
let spaces_count = cell_width - chars_width - 1;
trace!(
"number of spaces added to shrinked value: {}",
spaces_count
);
value.push_str(&" ".repeat(spaces_count));
cell.value = value;
cell.to_string()
} else {
trace!("cell is not overflowing");
let spaces_count = cell_width - cell.unicode_width() + 1;
trace!("number of spaces added to value: {}", spaces_count);
cell.value.push_str(&" ".repeat(spaces_count));
cell.to_string()
} }
} else {
trace!("table is not overflowing or cell is not shrinkable");
trace!("cell_width: {}", cell_widths[i]);
trace!("cell unicode_width: {}", cell.unicode_width());
let spaces_count = cell_widths[i] - cell.unicode_width() + 1;
trace!("number of spaces added to value: {}", spaces_count);
cell.value.push_str(&" ".repeat(spaces_count));
cell.to_string()
}
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
}
/// Render the final printable table as a string. chars_width += char_width;
fn render(items: &[Self]) -> String { value.push(c);
Self::build(items) }
.iter()
// Join cells with grey pipes. value.push_str("");
// TODO: make this customizable trace!("chars width: {}", chars_width);
.map(|row| row.join(&Cell::new("").ext(8).to_string())) trace!("shrinked value: {}", value);
.collect::<Vec<_>>() let spaces_count = cell_width - chars_width - 1;
.join("\n") trace!("number of spaces added to shrinked value: {}", spaces_count);
value.push_str(&" ".repeat(spaces_count));
cell.value = value;
cell.print(writter)?;
} else {
trace!("cell is not overflowing");
let spaces_count = cell_width - cell.unicode_width() + 1;
trace!("number of spaces added to value: {}", spaces_count);
cell.value.push_str(&" ".repeat(spaces_count));
cell.print(writter)?;
}
} else {
trace!("table is not overflowing or cell is not shrinkable");
trace!("cell width: {}", cell_widths[i]);
trace!("cell unicode width: {}", cell.unicode_width());
let spaces_count = cell_widths[i] - cell.unicode_width() + 1;
trace!("number of spaces added to value: {}", spaces_count);
cell.value.push_str(&" ".repeat(spaces_count));
cell.print(writter)?;
}
glue = Cell::new("").ansi_256(8);
}
writeln!(writter)?;
}
writeln!(writter)?;
Ok(())
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::io;
use termcolor::WriteColor;
use super::*; use super::*;
#[derive(Debug, Default)]
struct StringWritter {
content: String,
}
impl io::Write for StringWritter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.content
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.content = String::default();
Ok(())
}
}
impl WriteColor for StringWritter {
fn supports_color(&self) -> bool {
false
}
fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
io::Result::Ok(())
}
fn reset(&mut self) -> io::Result<()> {
io::Result::Ok(())
}
}
impl WriteWithColor for StringWritter {}
struct Item { struct Item {
id: u16, id: u16,
name: String, name: String,
@ -348,66 +343,77 @@ mod tests {
.cell(Cell::new(self.desc.as_str())) .cell(Cell::new(self.desc.as_str()))
} }
// Defines a fixed max width instead of terminal size for testing.
fn max_width() -> usize { fn max_width() -> usize {
// Use a fixed max width instead of terminal size for testing.
20 20
} }
} }
macro_rules! write_items {
($writter:expr, $($item:expr),*) => {
Table::println($writter, &[$($item,)*]).unwrap();
};
}
#[test] #[test]
fn row_smaller_than_head() { fn row_smaller_than_head() {
let items = vec![ let mut writter = StringWritter::default();
write_items![
&mut writter,
Item::new(1, "a", "aa"), Item::new(1, "a", "aa"),
Item::new(2, "b", "bb"), Item::new(2, "b", "bb"),
Item::new(3, "c", "cc"), Item::new(3, "c", "cc")
]; ];
let table = vec![ let expected = concat![
vec!["ID ", "NAME ", "DESC "], "ID │NAME │DESC \n",
vec!["1 ", "a ", "aa "], "1 │a │aa \n",
vec!["2 ", "b ", "bb "], "2 │b │bb \n",
vec!["3 ", "c ", "cc "], "3 │c │cc \n\n"
]; ];
assert_eq!(expected, writter.content);
assert_eq!(table, Table::build(&items));
} }
#[test] #[test]
fn row_bigger_than_head() { fn row_bigger_than_head() {
let items = vec![ let mut writter = StringWritter::default();
write_items![
&mut writter,
Item::new(1, "a", "aa"), Item::new(1, "a", "aa"),
Item::new(2222, "bbbbb", "bbbbb"), Item::new(2222, "bbbbb", "bbbbb"),
Item::new(3, "c", "cc"), Item::new(3, "c", "cc")
]; ];
let table = vec![ let expected = concat![
vec!["ID ", "NAME ", "DESC "], "ID │NAME │DESC \n",
vec!["1 ", "a ", "aa "], "1 │a │aa \n",
vec!["2222 ", "bbbbb ", "bbbbb "], "2222 │bbbbb │bbbbb \n",
vec!["3 ", "c ", "cc "], "3 │c │cc \n\n",
]; ];
assert_eq!(expected, writter.content);
assert_eq!(table, Table::build(&items)); let mut writter = StringWritter::default();
write_items![
let items = vec![ &mut writter,
Item::new(1, "a", "aa"), Item::new(1, "a", "aa"),
Item::new(2222, "bbbbb", "bbbbb"), Item::new(2222, "bbbbb", "bbbbb"),
Item::new(3, "cccccc", "cc"), Item::new(3, "cccccc", "cc")
]; ];
let table = vec![ let expected = concat![
vec!["ID ", "NAME ", "DESC "], "ID │NAME │DESC \n",
vec!["1 ", "a ", "aa "], "1 │a │aa \n",
vec!["2222 ", "bbbbb ", "bbbbb "], "2222 │bbbbb │bbbbb \n",
vec!["3 ", "cccccc ", "cc "], "3 │cccccc │cc \n\n",
]; ];
assert_eq!(expected, writter.content);
assert_eq!(table, Table::build(&items));
} }
#[test] #[test]
fn basic_shrink() { fn basic_shrink() {
let items = vec![ let mut writter = StringWritter::default();
write_items![
&mut writter,
Item::new(1, "", "desc"), Item::new(1, "", "desc"),
Item::new(2, "short", "desc"), Item::new(2, "short", "desc"),
Item::new(3, "loooooong", "desc"), Item::new(3, "loooooong", "desc"),
@ -415,37 +421,37 @@ mod tests {
Item::new(5, "shriiiiiiiiiink", "desc"), Item::new(5, "shriiiiiiiiiink", "desc"),
Item::new(6, "😍😍😍😍", "desc"), Item::new(6, "😍😍😍😍", "desc"),
Item::new(7, "😍😍😍😍😍", "desc"), Item::new(7, "😍😍😍😍😍", "desc"),
Item::new(8, "!😍😍😍😍😍", "desc"), Item::new(8, "!😍😍😍😍😍", "desc")
]; ];
let table = vec![ let expected = concat![
vec!["ID ", "NAME ", "DESC "], "ID │NAME │DESC \n",
vec!["1 ", " ", "desc "], "1 │ │desc \n",
vec!["2 ", "short ", "desc "], "2 │short │desc \n",
vec!["3 ", "loooooong ", "desc "], "3 │loooooong │desc \n",
vec!["4 ", "shriiiii… ", "desc "], "4 │shriiiii… │desc \n",
vec!["5 ", "shriiiii… ", "desc "], "5 │shriiiii… │desc \n",
vec!["6 ", "😍😍😍😍 ", "desc "], "6 │😍😍😍😍 │desc \n",
vec!["7 ", "😍😍😍😍… ", "desc "], "7 │😍😍😍😍… │desc \n",
vec!["8 ", "!😍😍😍… ", "desc "], "8 │!😍😍😍… │desc \n\n",
]; ];
assert_eq!(expected, writter.content);
assert_eq!(table, Table::build(&items));
} }
#[test] #[test]
fn max_shrink_width() { fn max_shrink_width() {
let items = vec![ let mut writter = StringWritter::default();
write_items![
&mut writter,
Item::new(1111, "shriiiiiiiink", "desc very looong"), Item::new(1111, "shriiiiiiiink", "desc very looong"),
Item::new(2222, "shriiiiiiiink", "desc very loooooooooong"), Item::new(2222, "shriiiiiiiink", "desc very loooooooooong")
]; ];
let table = vec![ let expected = concat![
vec!["ID ", "NAME ", "DESC "], "ID │NAME │DESC \n",
vec!["1111 ", "shri… ", "desc very looong "], "1111 │shri… │desc very looong \n",
vec!["2222 ", "shri… ", "desc very loooooooooong "], "2222 │shri… │desc very loooooooooong \n\n",
]; ];
assert_eq!(expected, writter.content);
assert_eq!(table, Table::build(&items));
} }
} }

2
wiki

@ -1 +1 @@
Subproject commit 061a644f0ebf00ebba76e7db37a7bedfe8d898f3 Subproject commit 8cf79989facecaf4210db6d1eaa9f090975f5e25