refactor table system (#132)

* init table trait

* add shrink_col_index and max_width method to Table trait

* make unicodes work when shrinking

* improve readability of the table

* replace old table system with new one

* update changelog
This commit is contained in:
Clément DOUIN 2021-04-27 14:54:53 +02:00 committed by GitHub
parent cb296a5d98
commit cddb7bde37
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 348 additions and 278 deletions

View file

@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Vim table containing emoji [#122] - Vim table containing emoji [#122]
- IDLE mode after network interruption [#123] - IDLE mode after network interruption [#123]
- Output redirected to `stderr` [#130] - Output redirected to `stderr` [#130]
- Refactor table system [#132]
## [0.2.6] - 2021-04-17 ## [0.2.6] - 2021-04-17
@ -209,3 +210,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#125]: https://github.com/soywod/himalaya/issues/125 [#125]: https://github.com/soywod/himalaya/issues/125
[#126]: https://github.com/soywod/himalaya/issues/126 [#126]: https://github.com/soywod/himalaya/issues/126
[#130]: https://github.com/soywod/himalaya/issues/130 [#130]: https://github.com/soywod/himalaya/issues/130
[#132]: https://github.com/soywod/himalaya/issues/132

View file

@ -1,6 +1,6 @@
[package] [package]
name = "himalaya" name = "himalaya"
description = "📫 The CLI email client." description = "📫 CLI email client"
version = "0.2.7" version = "0.2.7"
authors = ["soywod <clement.douin@posteo.net>"] authors = ["soywod <clement.douin@posteo.net>"]
edition = "2018" edition = "2018"

View file

@ -1,6 +1,6 @@
# 📫 Himalaya [![gh-actions](https://github.com/soywod/himalaya/workflows/deployment/badge.svg)](https://github.com/soywod/himalaya/actions?query=workflow%3Adeployment) # 📫 Himalaya [![gh-actions](https://github.com/soywod/himalaya/workflows/deployment/badge.svg)](https://github.com/soywod/himalaya/actions?query=workflow%3Adeployment)
The CLI email client. CLI email client written in Rust.
*The project is under active development. Do not use in production before the *The project is under active development. Do not use in production before the
`v1.0.0` (see the [roadmap](https://github.com/soywod/himalaya/milestone/5)).* `v1.0.0` (see the [roadmap](https://github.com/soywod/himalaya/milestone/5)).*

View file

@ -1,5 +1,5 @@
{ {
description = "The CLI email client."; description = "📫 CLI email client";
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";

View file

@ -43,11 +43,11 @@ impl<'f> ToString for Flags<'f> {
flags.push_str(if self.0.contains(&Flag::Seen) { flags.push_str(if self.0.contains(&Flag::Seen) {
" " " "
} else { } else {
"🟓" ""
}); });
flags.push_str(if self.0.contains(&Flag::Answered) { flags.push_str(if self.0.contains(&Flag::Answered) {
"" ""
} else { } else {
" " " "
}); });

View file

@ -4,7 +4,7 @@ use std::fmt;
use crate::{ use crate::{
output::fmt::{get_output_fmt, OutputFmt, Response}, output::fmt::{get_output_fmt, OutputFmt, Response},
table::{self, DisplayRow, DisplayTable}, table::{Cell, Row, Table},
}; };
// Mbox // Mbox
@ -26,15 +26,25 @@ impl Mbox {
} }
} }
impl DisplayRow for Mbox { impl Table for Mbox {
fn to_row(&self) -> Vec<table::Cell> { fn head() -> Row {
use crate::table::*; Row::new()
.cell(Cell::new("DELIM").bold().underline().white())
.cell(Cell::new("NAME").bold().underline().white())
.cell(
Cell::new("ATTRIBUTES")
.shrinkable()
.bold()
.underline()
.white(),
)
}
vec![ fn row(&self) -> Row {
Cell::new(&[BLUE], &self.delim), Row::new()
Cell::new(&[GREEN], &self.name), .cell(Cell::new(&self.delim).red())
FlexCell::new(&[YELLOW], &self.attributes.join(", ")), .cell(Cell::new(&self.name).green())
] .cell(Cell::new(&self.attributes.join(", ")).shrinkable().yellow())
} }
} }
@ -43,28 +53,12 @@ impl DisplayRow for Mbox {
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct Mboxes(pub Vec<Mbox>); pub struct Mboxes(pub Vec<Mbox>);
impl<'a> DisplayTable<'a, Mbox> for Mboxes {
fn header_row() -> Vec<table::Cell> {
use crate::table::*;
vec![
Cell::new(&[BOLD, UNDERLINE, WHITE], "DELIM"),
Cell::new(&[BOLD, UNDERLINE, WHITE], "NAME"),
FlexCell::new(&[BOLD, UNDERLINE, WHITE], "ATTRIBUTES"),
]
}
fn rows(&self) -> &Vec<Mbox> {
&self.0
}
}
impl fmt::Display for Mboxes { impl fmt::Display for Mboxes {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
unsafe { unsafe {
match get_output_fmt() { match get_output_fmt() {
&OutputFmt::Plain => { &OutputFmt::Plain => {
writeln!(f, "\n{}", self.to_table()) writeln!(f, "\n{}", Table::render(&self.0))
} }
&OutputFmt::Json => { &OutputFmt::Json => {
let res = serde_json::to_string(&Response::new(self)).unwrap(); let res = serde_json::to_string(&Response::new(self)).unwrap();

View file

@ -16,7 +16,7 @@ use crate::{
config::model::{Account, Config}, config::model::{Account, Config},
flag::model::{Flag, Flags}, flag::model::{Flag, Flags},
output::fmt::{get_output_fmt, OutputFmt, Response}, output::fmt::{get_output_fmt, OutputFmt, Response},
table::{self, DisplayRow, DisplayTable}, table::{Cell, Row, Table},
}; };
error_chain! { error_chain! {
@ -624,23 +624,29 @@ struct MsgSpec {
default_content: Option<Vec<String>>, default_content: Option<Vec<String>>,
} }
impl<'m> DisplayRow for Msg<'m> { impl<'m> Table for Msg<'m> {
fn to_row(&self) -> Vec<table::Cell> { fn head() -> Row {
use crate::table::*; Row::new()
.cell(Cell::new("UID").bold().underline().white())
.cell(Cell::new("FLAGS").bold().underline().white())
.cell(Cell::new("SUBJECT").shrinkable().bold().underline().white())
.cell(Cell::new("SENDER").bold().underline().white())
.cell(Cell::new("DATE").bold().underline().white())
}
let unseen = if self.flags.contains(&Flag::Seen) { fn row(&self) -> Row {
RESET let is_seen = !self.flags.contains(&Flag::Seen);
} else { Row::new()
BOLD .cell(Cell::new(&self.uid.to_string()).bold_if(is_seen).red())
}; .cell(Cell::new(&self.flags.to_string()).bold_if(is_seen).white())
.cell(
vec![ Cell::new(&self.subject)
Cell::new(&[unseen.to_owned(), RED], &self.uid.to_string()), .shrinkable()
Cell::new(&[unseen.to_owned(), WHITE], &self.flags.to_string()), .bold_if(is_seen)
FlexCell::new(&[unseen.to_owned(), GREEN], &self.subject), .green(),
Cell::new(&[unseen.to_owned(), BLUE], &self.sender), )
Cell::new(&[unseen.to_owned(), YELLOW], &self.date), .cell(Cell::new(&self.sender).bold_if(is_seen).blue())
] .cell(Cell::new(&self.date).bold_if(is_seen).yellow())
} }
} }
@ -649,24 +655,6 @@ impl<'m> DisplayRow for Msg<'m> {
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct Msgs<'m>(pub Vec<Msg<'m>>); pub struct Msgs<'m>(pub Vec<Msg<'m>>);
impl<'m> DisplayTable<'m, Msg<'m>> for Msgs<'m> {
fn header_row() -> Vec<table::Cell> {
use crate::table::*;
vec![
Cell::new(&[BOLD, UNDERLINE, WHITE], "UID"),
Cell::new(&[BOLD, UNDERLINE, WHITE], "FLAGS"),
FlexCell::new(&[BOLD, UNDERLINE, WHITE], "SUBJECT"),
Cell::new(&[BOLD, UNDERLINE, WHITE], "SENDER"),
Cell::new(&[BOLD, UNDERLINE, WHITE], "DATE"),
]
}
fn rows(&self) -> &Vec<Msg<'m>> {
&self.0
}
}
impl<'m> From<&'m imap::types::ZeroCopy<Vec<imap::types::Fetch>>> for Msgs<'m> { impl<'m> From<&'m imap::types::ZeroCopy<Vec<imap::types::Fetch>>> for Msgs<'m> {
fn from(fetches: &'m imap::types::ZeroCopy<Vec<imap::types::Fetch>>) -> Self { fn from(fetches: &'m imap::types::ZeroCopy<Vec<imap::types::Fetch>>) -> Self {
Self(fetches.iter().rev().map(Msg::from).collect::<Vec<_>>()) Self(fetches.iter().rev().map(Msg::from).collect::<Vec<_>>())
@ -678,7 +666,7 @@ impl<'m> fmt::Display for Msgs<'m> {
unsafe { unsafe {
match get_output_fmt() { match get_output_fmt() {
&OutputFmt::Plain => { &OutputFmt::Plain => {
writeln!(f, "\n{}", self.to_table()) writeln!(f, "\n{}", Table::render(&self.0))
} }
&OutputFmt::Json => { &OutputFmt::Json => {
let res = serde_json::to_string(&Response::new(self)).unwrap(); let res = serde_json::to_string(&Response::new(self)).unwrap();

View file

@ -1,30 +1,26 @@
use std::fmt; use std::fmt;
use terminal_size::terminal_size;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
#[derive(Clone, Debug)] #[derive(Debug)]
pub struct Style(u8, u8, u8); pub struct Style(u8, u8, u8);
impl fmt::Display for Style { impl fmt::Display for Style {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Style(color, bright, shade) = self; let Style(color, bright, shade) = self;
let bright_str: String = if *bright > 0 {
String::from(";") + &bright.to_string()
} else {
String::from("")
};
let shade_str: String = if *shade > 0 {
String::from(";") + &shade.to_string()
} else {
String::from("")
};
let mut style = String::from("\x1b["); let mut style = String::from("\x1b[");
style.push_str(&color.to_string()); style.push_str(&color.to_string());
style.push_str(&bright_str);
style.push_str(&shade_str); if *bright > 0 {
style.push_str(";");
style.push_str(&bright.to_string());
};
if *shade > 0 {
style.push_str(";");
style.push_str(&shade.to_string());
};
style.push_str("m"); style.push_str("m");
write!(f, "{}", style) write!(f, "{}", style)
@ -33,17 +29,17 @@ impl fmt::Display for Style {
#[derive(Debug)] #[derive(Debug)]
pub struct Cell { pub struct Cell {
pub styles: Vec<Style>, styles: Vec<Style>,
pub value: String, value: String,
pub flex: bool, shrinkable: bool,
} }
impl Cell { impl Cell {
pub fn new(styles: &[Style], value: &str) -> Self { pub fn new<T: AsRef<str>>(value: T) -> Self {
Self { Self {
styles: styles.to_vec(), styles: Vec::new(),
value: value.trim().to_string(), value: String::from(value.as_ref()),
flex: false, shrinkable: false,
} }
} }
@ -51,209 +47,299 @@ impl Cell {
UnicodeWidthStr::width(self.value.as_str()) UnicodeWidthStr::width(self.value.as_str())
} }
pub fn render(&self, col_size: usize) -> String { pub fn shrinkable(mut self) -> Self {
let style_begin = self self.shrinkable = true;
.styles self
.iter()
.map(|style| style.to_string())
.collect::<Vec<_>>()
.concat();
let style_end = "\x1b[0m";
let unicode_width = self.unicode_width();
if col_size > 0 && unicode_width > col_size {
String::from(style_begin + &self.value[0..=col_size - 2] + "" + style_end)
} else {
let padding = if col_size == 0 {
"".to_string()
} else {
" ".repeat(col_size - unicode_width + 1)
};
String::from(style_begin + &self.value + &padding + style_end)
} }
pub fn is_shrinkable(&self) -> bool {
self.shrinkable
}
pub fn bold(mut self) -> Self {
self.styles.push(Style(1, 0, 0));
self
}
pub fn bold_if(self, predicate: bool) -> Self {
if predicate {
self.bold()
} else {
self
}
}
pub fn underline(mut self) -> Self {
self.styles.push(Style(4, 0, 0));
self
}
pub fn red(mut self) -> Self {
self.styles.push(Style(31, 0, 0));
self
}
pub fn green(mut self) -> Self {
self.styles.push(Style(32, 0, 0));
self
}
pub fn yellow(mut self) -> Self {
self.styles.push(Style(33, 0, 0));
self
}
pub fn blue(mut self) -> Self {
self.styles.push(Style(34, 0, 0));
self
}
pub fn white(mut self) -> Self {
self.styles.push(Style(37, 0, 0));
self
}
pub fn ext(mut self, shade: u8) -> Self {
self.styles.push(Style(38, 5, shade));
self
}
}
impl fmt::Display for Cell {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for style in &self.styles {
write!(f, "{}", style)?;
}
write!(f, "{}", self.value)?;
if !self.styles.is_empty() {
write!(f, "{}", Style(0, 0, 0))?;
}
Ok(())
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub struct FlexCell; pub struct Row(pub Vec<Cell>);
impl FlexCell { impl Row {
pub fn new(styles: &[Style], value: &str) -> Cell { pub fn new() -> Self {
Cell { Self(Vec::new())
flex: true,
..Cell::new(styles, value)
}
}
} }
pub trait DisplayRow { pub fn cell(mut self, cell: Cell) -> Self {
fn to_row(&self) -> Vec<Cell>; self.0.push(cell);
self
}
} }
pub trait DisplayTable<'a, T: DisplayRow + 'a> { pub trait Table
fn header_row() -> Vec<Cell>; where
fn rows(&self) -> &Vec<T>; Self: Sized,
{
fn head() -> Row;
fn row(&self) -> Row;
fn to_table(&self) -> String { fn max_width() -> usize {
let mut col_sizes = vec![]; terminal_size::terminal_size()
let head = Self::header_row(); .map(|(w, _)| w.0 as usize)
.unwrap_or_default()
}
head.iter().for_each(|cell| { fn build(items: &[Self]) -> Vec<Vec<String>> {
col_sizes.push(cell.unicode_width()); let mut table = vec![Self::head()];
}); let mut cell_widths: Vec<usize> =
table[0].0.iter().map(|cell| cell.unicode_width()).collect();
let mut table = self table.extend(
.rows() items
.iter() .iter()
.map(|item| { .map(|item| {
let row = item.to_row(); let row = item.row();
row.iter() row.0.iter().enumerate().for_each(|(i, cell)| {
.enumerate() cell_widths[i] = cell_widths[i].max(cell.unicode_width());
.for_each(|(i, cell)| col_sizes[i] = col_sizes[i].max(cell.unicode_width())); });
row row
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>(),
);
table.insert(0, head); let spaces_plus_separators_len = cell_widths.len() * 2 - 1;
let table_width = cell_widths.iter().sum::<usize>() + spaces_plus_separators_len;
let term_width = terminal_size().map(|size| size.0 .0).unwrap_or(0) as usize; table
let seps_width = 2 * col_sizes.len() - 1; .iter_mut()
let table_width = col_sizes.iter().sum::<usize>() + seps_width; .map(|row| {
let diff_width = if table_width < term_width { row.0
0 .iter_mut()
} else {
table_width - term_width
};
table.iter().fold(String::new(), |output, row| {
let row_str = row
.iter()
.enumerate() .enumerate()
.map(|(i, cell)| { .map(|(i, cell)| {
if cell.flex && col_sizes[i] > diff_width { let table_is_overflowing = table_width > Self::max_width();
cell.render(col_sizes[i] - diff_width)
if table_is_overflowing && cell.is_shrinkable() {
let shrink_width = table_width - Self::max_width();
let cell_width = cell_widths[i] - shrink_width;
let cell_is_overflowing = cell.unicode_width() > cell_width;
if cell_is_overflowing {
let mut value = String::new();
let mut chars_width = 0;
for c in cell.value.chars() {
let char_width = UnicodeWidthStr::width(c.to_string().as_str());
if chars_width + char_width >= cell_width {
break;
}
chars_width += char_width;
value.push(c);
}
value.push_str("");
let repeat_count = cell_width - chars_width - 1;
value.push_str(&" ".repeat(repeat_count));
cell.value = value;
cell.to_string()
} else { } else {
cell.render(col_sizes[i]) let repeat_len = cell_width - cell.unicode_width() + 1;
cell.value.push_str(&" ".repeat(repeat_len));
cell.to_string()
}
} else {
let repeat_count = cell_widths[i] - cell.unicode_width() + 1;
cell.value.push_str(&" ".repeat(repeat_count));
cell.to_string()
} }
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(&Cell::new(&[ext(8)], "|").render(0));
output + &row_str + "\n"
}) })
.collect::<Vec<_>>()
}
fn render(items: &[Self]) -> String {
Self::build(items)
.iter()
.map(|row| row.join(&Cell::new("|").ext(8).to_string()))
.collect::<Vec<_>>()
.join("\n")
} }
} }
#[allow(dead_code)] #[cfg(test)]
pub const RESET: Style = Style(0, 0, 0); mod tests {
use super::*;
#[allow(dead_code)] struct Item {
pub const BOLD: Style = Style(1, 0, 0); id: u16,
name: String,
#[allow(dead_code)] desc: String,
pub const UNDERLINE: Style = Style(4, 0, 0); }
#[allow(dead_code)] impl<'a> Item {
pub const REVERSED: Style = Style(7, 0, 0); pub fn new(id: u16, name: &'a str, desc: &'a str) -> Self {
Self {
#[allow(dead_code)] id,
pub const BLACK: Style = Style(30, 0, 0); name: String::from(name),
desc: String::from(desc),
#[allow(dead_code)] }
pub const RED: Style = Style(31, 0, 0); }
}
#[allow(dead_code)]
pub const GREEN: Style = Style(32, 0, 0); impl Table for Item {
fn head() -> Row {
#[allow(dead_code)] Row::new()
pub const YELLOW: Style = Style(33, 0, 0); .cell(Cell::new("ID"))
.cell(Cell::new("NAME").shrinkable())
#[allow(dead_code)] .cell(Cell::new("DESC"))
pub const BLUE: Style = Style(34, 0, 0); }
#[allow(dead_code)] fn row(&self) -> Row {
pub const MAGENTA: Style = Style(35, 0, 0); Row::new()
.cell(Cell::new(self.id.to_string()))
#[allow(dead_code)] .cell(Cell::new(self.name.as_str()).shrinkable())
pub const CYAN: Style = Style(36, 0, 0); .cell(Cell::new(self.desc.as_str()))
}
#[allow(dead_code)]
pub const WHITE: Style = Style(37, 0, 0); fn max_width() -> usize {
20
#[allow(dead_code)] }
pub const BRIGHT_BLACK: Style = Style(30, 1, 0); }
#[allow(dead_code)] #[test]
pub const BRIGHT_RED: Style = Style(31, 1, 0); fn row_smaller_than_head() {
let items = vec![
#[allow(dead_code)] Item::new(1, "a", "aa"),
pub const BRIGHT_GREEN: Style = Style(32, 1, 0); Item::new(2, "b", "bb"),
Item::new(3, "c", "cc"),
#[allow(dead_code)] ];
pub const BRIGHT_YELLOW: Style = Style(33, 1, 0);
let table = vec![
#[allow(dead_code)] vec!["ID ", "NAME ", "DESC "],
pub const BRIGHT_BLUE: Style = Style(34, 1, 0); vec!["1 ", "a ", "aa "],
vec!["2 ", "b ", "bb "],
#[allow(dead_code)] vec!["3 ", "c ", "cc "],
pub const BRIGHT_MAGENTA: Style = Style(35, 1, 0); ];
#[allow(dead_code)] assert_eq!(table, Table::build(&items));
pub const BRIGHT_CYAN: Style = Style(36, 1, 0); }
#[allow(dead_code)] #[test]
pub const BRIGHT_WHITE: Style = Style(37, 1, 0); fn row_bigger_than_head() {
let items = vec![
#[allow(dead_code)] Item::new(1, "a", "aa"),
pub const BG_BLACK: Style = Style(40, 0, 0); Item::new(2222, "bbbbb", "bbbbb"),
Item::new(3, "c", "cc"),
#[allow(dead_code)] ];
pub const BG_RED: Style = Style(41, 0, 0);
let table = vec![
#[allow(dead_code)] vec!["ID ", "NAME ", "DESC "],
pub const BG_GREEN: Style = Style(42, 0, 0); vec!["1 ", "a ", "aa "],
vec!["2222 ", "bbbbb ", "bbbbb "],
#[allow(dead_code)] vec!["3 ", "c ", "cc "],
pub const BG_YELLOW: Style = Style(43, 0, 0); ];
#[allow(dead_code)] assert_eq!(table, Table::build(&items));
pub const BG_BLUE: Style = Style(44, 0, 0);
let items = vec![
#[allow(dead_code)] Item::new(1, "a", "aa"),
pub const BG_MAGENTA: Style = Style(45, 0, 0); Item::new(2222, "bbbbb", "bbbbb"),
Item::new(3, "cccccc", "cc"),
#[allow(dead_code)] ];
pub const BG_CYAN: Style = Style(46, 0, 0);
let table = vec![
#[allow(dead_code)] vec!["ID ", "NAME ", "DESC "],
pub const BG_WHITE: Style = Style(47, 0, 0); vec!["1 ", "a ", "aa "],
vec!["2222 ", "bbbbb ", "bbbbb "],
#[allow(dead_code)] vec!["3 ", "cccccc ", "cc "],
pub const BG_BRIGHT_BLACK: Style = Style(40, 1, 0); ];
#[allow(dead_code)] assert_eq!(table, Table::build(&items));
pub const BG_BRIGHT_RED: Style = Style(41, 1, 0); }
#[allow(dead_code)] #[test]
pub const BG_BRIGHT_GREEN: Style = Style(42, 1, 0); fn shrink() {
let items = vec![
#[allow(dead_code)] Item::new(1, "short", "desc"),
pub const BG_BRIGHT_YELLOW: Style = Style(43, 1, 0); Item::new(2, "loooooong", "desc"),
Item::new(3, "shriiiiink", "desc"),
#[allow(dead_code)] Item::new(4, "shriiiiiiiiiink", "desc"),
pub const BG_BRIGHT_BLUE: Style = Style(44, 1, 0); Item::new(5, "😍😍😍😍", "desc"),
Item::new(6, "😍😍😍😍😍", "desc"),
#[allow(dead_code)] Item::new(7, "!😍😍😍😍😍", "desc"),
pub const BG_BRIGHT_MAGENTA: Style = Style(45, 1, 0); ];
#[allow(dead_code)] let table = vec![
pub const BG_BRIGHT_CYAN: Style = Style(46, 1, 0); vec!["ID ", "NAME ", "DESC "],
vec!["1 ", "short ", "desc "],
#[allow(dead_code)] vec!["2 ", "loooooong ", "desc "],
pub const BG_BRIGHT_WHITE: Style = Style(47, 1, 0); vec!["3 ", "shriiiii… ", "desc "],
vec!["4 ", "shriiiii… ", "desc "],
#[allow(dead_code)] vec!["5 ", "😍😍😍😍 ", "desc "],
fn ext(n: u8) -> Style { vec!["6 ", "😍😍😍😍… ", "desc "],
Style(38, 5, n) vec!["7 ", "!😍😍😍… ", "desc "],
];
assert_eq!(table, Table::build(&items));
}
} }