pub mod data;
pub mod group;
pub mod sort;
pub mod stream;
pub mod transform;
use std::fmt;
use std::sync::Arc;
use rich::r#box::Box as BoxSet;
use rich::{ColumnOptions, Console, Justify, Style, Table, Text};
pub use data::TableData;
pub use group::{Aggregate, Group, GroupBy};
pub use sort::{Compare, Order, SortKey};
pub use stream::{RenderStats, StreamingTable, Window};
pub const STYLES: &[(&str, &str)] = &[
("table.group", "bold"),
("table.aggregate", "italic"),
("table.sort_indicator", "cyan"),
("table.more", "dim"),
];
pub(crate) fn style(console: &Console, key: &str) -> Style {
let fallback = STYLES
.iter()
.find(|(name, _)| *name == key)
.map_or("", |(_, spec)| *spec);
crate::event::theme_style(console, key, fallback)
}
#[derive(Clone, Debug, Default)]
pub enum Value {
#[default]
Null,
Int(i64),
Float(f64),
Str(String),
Text(Text),
}
impl Value {
pub fn is_empty(&self) -> bool {
match self {
Value::Null => true,
Value::Str(s) => s.is_empty(),
Value::Text(t) => t.plain().is_empty(),
Value::Int(_) | Value::Float(_) => false,
}
}
pub fn as_f64(&self) -> Option<f64> {
match self {
Value::Int(n) => Some(*n as f64),
Value::Float(f) => Some(*f),
_ => None,
}
}
pub fn plain(&self) -> String {
match self {
Value::Null => String::new(),
Value::Int(n) => n.to_string(),
Value::Float(f) => f.to_string(),
Value::Str(s) => s.clone(),
Value::Text(t) => t.plain().to_string(),
}
}
pub fn to_text(&self) -> Text {
match self {
Value::Text(t) => t.clone(),
other => Text::new(other.plain()),
}
}
}
impl PartialEq for Value {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Value::Null, Value::Null) => true,
(Value::Int(a), Value::Int(b)) => a == b,
(Value::Float(a), Value::Float(b)) => a.to_bits() == b.to_bits(),
(Value::Str(a), Value::Str(b)) => a == b,
_ => false,
}
}
}
impl From<&str> for Value {
fn from(s: &str) -> Self {
Value::Str(s.to_string())
}
}
impl From<String> for Value {
fn from(s: String) -> Self {
Value::Str(s)
}
}
impl From<Text> for Value {
fn from(t: Text) -> Self {
Value::Text(t)
}
}
impl From<f64> for Value {
fn from(f: f64) -> Self {
Value::Float(f)
}
}
macro_rules! int_value {
($($t:ty),*) => {$(
impl From<$t> for Value {
fn from(n: $t) -> Self {
i64::try_from(n).map_or(Value::Float(n as f64), Value::Int)
}
}
)*};
}
int_value!(i8, i16, i32, i64, u8, u16, u32, u64, usize, isize);
impl<T: Into<Value>> From<Option<T>> for Value {
fn from(v: Option<T>) -> Self {
v.map_or(Value::Null, Into::into)
}
}
pub type Formatter = Arc<dyn Fn(&Value) -> Text + Send + Sync>;
#[derive(Clone)]
pub struct Column {
header: String,
options: ColumnOptions,
formatter: Option<Formatter>,
}
impl fmt::Debug for Column {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Column")
.field("header", &self.header)
.field("options", &self.options)
.field("formatter", &self.formatter.is_some())
.finish()
}
}
impl Column {
pub fn new(header: impl Into<String>) -> Self {
Column {
header: header.into(),
options: ColumnOptions::default(),
formatter: None,
}
}
pub fn justify(mut self, justify: Justify) -> Self {
self.options.justify = justify;
self
}
pub fn options(mut self, options: ColumnOptions) -> Self {
self.options = options;
self
}
pub fn format(mut self, formatter: impl Fn(&Value) -> Text + Send + Sync + 'static) -> Self {
self.formatter = Some(Arc::new(formatter));
self
}
pub fn header(&self) -> &str {
&self.header
}
pub fn column_options(&self) -> &ColumnOptions {
&self.options
}
pub fn cell(&self, value: &Value) -> Text {
match &self.formatter {
Some(format) => format(value),
None => value.to_text(),
}
}
}
pub(crate) fn headers(console: &Console, columns: &[Column], keys: &[SortKey]) -> Vec<Text> {
let ascii = console.ascii_only();
columns
.iter()
.enumerate()
.map(|(index, column)| {
let mut text = Text::new(column.header.clone());
if let Some(priority) = keys.iter().position(|k| k.column == index) {
let mut mark = sort::indicator(keys[priority].order, ascii).to_string();
if keys.len() > 1 {
mark.push_str(&(priority + 1).to_string());
}
text.append(" ", None);
text.append(&mark, Some(style(console, "table.sort_indicator").into()));
}
text
})
.collect()
}
#[derive(Clone, Debug)]
pub(crate) struct Frame {
pub title: Option<String>,
pub caption: Option<String>,
pub box_set: Option<BoxSet>,
pub show_edge: bool,
pub expand: bool,
pub border_style: Style,
}
impl Default for Frame {
fn default() -> Self {
Frame {
title: None,
caption: None,
box_set: Some(rich::r#box::HEAVY_HEAD),
show_edge: true,
expand: false,
border_style: Style::new(),
}
}
}
impl Frame {
pub fn table(
&self,
columns: &[Column],
headers: &[Text],
show_header: bool,
annotations: bool,
) -> Table {
let mut table = Table::new()
.show_header(show_header)
.show_edge(self.show_edge)
.expand(self.expand)
.border_style(self.border_style.clone());
table = match self.box_set {
Some(box_set) => table.box_set(box_set),
None => table.without_box(),
};
if annotations {
if let Some(title) = &self.title {
table = table.title(title.clone());
}
if let Some(caption) = &self.caption {
table = table.caption(caption.clone());
}
}
for (column, header) in columns.iter().zip(headers) {
table.add_column_with(header.clone(), column.options.clone());
}
table
}
pub fn edge_lines(&self) -> usize {
usize::from(self.box_set.is_some() && self.show_edge)
}
}
macro_rules! frame_builders {
([$($g:ident)?] $ty:ty) => {
impl$(<$g>)? $ty {
pub fn title(mut self, title: impl Into<String>) -> Self {
self.frame.title = Some(title.into());
self
}
pub fn caption(mut self, caption: impl Into<String>) -> Self {
self.frame.caption = Some(caption.into());
self
}
pub fn box_set(mut self, box_set: rich::r#box::Box) -> Self {
self.frame.box_set = Some(box_set);
self
}
pub fn without_box(mut self) -> Self {
self.frame.box_set = None;
self
}
pub fn show_edge(mut self, show: bool) -> Self {
self.frame.show_edge = show;
self
}
pub fn expand(mut self, expand: bool) -> Self {
self.frame.expand = expand;
self
}
pub fn border_style(mut self, style: rich::Style) -> Self {
self.frame.border_style = style;
self
}
}
};
}
pub(crate) use frame_builders;