use std::fmt::Display;
use tabled::Table;
use tabled::builder::Builder;
use tabled::settings::Format;
use tabled::settings::Modify;
use tabled::settings::Panel;
use tabled::settings::Remove;
use tabled::settings::Width;
use tabled::settings::object::{Columns, Rows, Segment};
use tabled::settings::style::Style;
use terminal_size::Width as TermWidth;
use terminal_size::terminal_size;
pub use tabled::settings::Padding;
pub const DEFAULT_TERMINAL_WIDTH: usize = 120;
#[must_use]
pub fn terminal_width() -> usize {
terminal_size()
.map(|(TermWidth(w), _)| w as usize)
.unwrap_or(DEFAULT_TERMINAL_WIDTH)
}
#[must_use]
pub fn responsive_width(utilization: f64) -> usize {
let width = terminal_width();
(width as f64 * utilization.clamp(0.0, 1.0)) as usize
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TableStyle {
#[default]
Modern,
Borderless,
Markdown,
Sharp,
Ascii,
Psql,
Dots,
}
#[cfg(feature = "clap")]
impl clap::ValueEnum for TableStyle {
fn value_variants<'a>() -> &'a [Self] {
&[
Self::Modern,
Self::Borderless,
Self::Markdown,
Self::Sharp,
Self::Ascii,
Self::Psql,
Self::Dots,
]
}
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
Some(clap::builder::PossibleValue::new(match self {
Self::Modern => "modern",
Self::Borderless => "borderless",
Self::Markdown => "markdown",
Self::Sharp => "sharp",
Self::Ascii => "ascii",
Self::Psql => "psql",
Self::Dots => "dots",
}))
}
}
impl TableStyle {
pub fn apply(self, table: &mut Table) {
match self {
TableStyle::Modern => {
table.with(Style::rounded());
}
TableStyle::Borderless => {
table.with(Style::blank());
}
TableStyle::Markdown => {
table.with(Style::markdown());
}
TableStyle::Sharp => {
table.with(Style::sharp());
}
TableStyle::Ascii => {
table.with(Style::ascii());
}
TableStyle::Psql => {
table.with(Style::psql());
}
TableStyle::Dots => {
table.with(Style::dots());
}
}
}
}
pub struct StyledTable {
style: TableStyle,
header: Option<String>,
remove_header_row: bool,
padding: Option<Padding>,
newline_replacement: Option<String>,
max_width: Option<usize>,
wrap_column: Option<(usize, usize)>,
}
impl Default for StyledTable {
fn default() -> Self {
Self::new()
}
}
impl StyledTable {
#[must_use]
pub fn new() -> Self {
Self {
style: TableStyle::default(),
header: None,
remove_header_row: false,
padding: None,
newline_replacement: None,
max_width: None,
wrap_column: None,
}
}
#[must_use]
pub fn max_width(mut self, width: usize) -> Self {
self.max_width = Some(width);
self
}
#[must_use]
pub fn wrap_column(mut self, column_index: usize, width: usize) -> Self {
self.wrap_column = Some((column_index, width));
self
}
pub fn style(mut self, style: TableStyle) -> Self {
self.style = style;
self
}
pub fn header(mut self, header: impl Into<String>) -> Self {
self.header = Some(header.into());
self
}
pub fn remove_header_row(mut self) -> Self {
self.remove_header_row = true;
self
}
pub fn padding(mut self, padding: Padding) -> Self {
self.padding = Some(padding);
self
}
pub fn replace_newlines(mut self, replacement: impl Into<String>) -> Self {
self.newline_replacement = Some(replacement.into());
self
}
pub fn build<T: tabled::Tabled>(self, data: Vec<T>) -> Table {
let mut table = Table::new(data);
self.style.apply(&mut table);
if let Some(padding) = self.padding {
table.with(padding);
}
if self.remove_header_row {
table.with(Remove::row(Rows::first()));
}
if let Some(header) = self.header {
table.with(Panel::header(header));
}
if let Some(replacement) = self.newline_replacement {
table.with(
Modify::new(Segment::all())
.with(Format::content(move |s| s.replace('\n', &replacement))),
);
}
if let Some((col_idx, width)) = self.wrap_column {
table.with(Modify::new(Columns::new(col_idx..=col_idx)).with(Width::wrap(width)));
}
if let Some(width) = self.max_width {
table.with(Width::truncate(width));
}
table
}
}
#[must_use]
pub fn display_option<T: Display>(opt: &Option<T>) -> String {
opt.as_ref().map_or_else(String::new, |val| val.to_string())
}
#[must_use]
pub fn display_option_or<T: Display>(opt: &Option<T>, default: &str) -> String {
opt.as_ref()
.map_or_else(|| default.to_string(), |val| val.to_string())
}
#[must_use]
pub fn parse_columns(columns_arg: &str) -> Vec<String> {
columns_arg
.split(',')
.map(|s| s.trim().to_lowercase())
.filter(|s| !s.is_empty())
.collect()
}
#[must_use]
pub fn build_table_with_columns<T: tabled::Tabled>(data: &[T], columns: &[String]) -> Table {
let mut builder = Builder::default();
let headers: Vec<String> = T::headers()
.into_iter()
.map(|c| c.to_string().to_lowercase())
.collect();
let valid_columns: Vec<(usize, &String)> = columns
.iter()
.filter_map(|col| headers.iter().position(|h| h == col).map(|idx| (idx, col)))
.collect();
builder.push_record(valid_columns.iter().map(|(_, col)| col.as_str()));
for item in data {
let fields: Vec<String> = item.fields().into_iter().map(|c| c.to_string()).collect();
let row: Vec<&str> = valid_columns
.iter()
.map(|(idx, _)| fields[*idx].as_str())
.collect();
builder.push_record(row);
}
builder.build()
}