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"
@ -16,10 +16,10 @@ log = "0.4.14"
mailparse = "0.13.1" mailparse = "0.13.1"
native-tls = "0.2" native-tls = "0.2"
rfc2047-decoder = "0.1.2" 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"
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"
unicode-width = "0.1.7" unicode-width = "0.1.7"
uuid = { version = "0.8", features = ["v4"] } uuid = {version = "0.8", features = ["v4"]}

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 { pub fn is_shrinkable(&self) -> bool {
String::from(style_begin + &self.value[0..=col_size - 2] + "" + style_end) 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 { } else {
let padding = if col_size == 0 { self
"".to_string()
} else {
" ".repeat(col_size - unicode_width + 1)
};
String::from(style_begin + &self.value + &padding + style_end)
} }
} }
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 fn cell(mut self, cell: Cell) -> Self {
self.0.push(cell);
self
}
}
pub trait Table
where
Self: Sized,
{
fn head() -> Row;
fn row(&self) -> Row;
fn max_width() -> usize {
terminal_size::terminal_size()
.map(|(w, _)| w.0 as usize)
.unwrap_or_default()
}
fn build(items: &[Self]) -> Vec<Vec<String>> {
let mut table = vec![Self::head()];
let mut cell_widths: Vec<usize> =
table[0].0.iter().map(|cell| cell.unicode_width()).collect();
table.extend(
items
.iter()
.map(|item| {
let row = item.row();
row.0.iter().enumerate().for_each(|(i, cell)| {
cell_widths[i] = cell_widths[i].max(cell.unicode_width());
});
row
})
.collect::<Vec<_>>(),
);
let spaces_plus_separators_len = cell_widths.len() * 2 - 1;
let table_width = cell_widths.iter().sum::<usize>() + spaces_plus_separators_len;
table
.iter_mut()
.map(|row| {
row.0
.iter_mut()
.enumerate()
.map(|(i, cell)| {
let table_is_overflowing = table_width > Self::max_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 {
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<_>>()
}
fn render(items: &[Self]) -> String {
Self::build(items)
.iter()
.map(|row| row.join(&Cell::new("|").ext(8).to_string()))
.collect::<Vec<_>>()
.join("\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Item {
id: u16,
name: String,
desc: String,
}
impl<'a> Item {
pub fn new(id: u16, name: &'a str, desc: &'a str) -> Self {
Self {
id,
name: String::from(name),
desc: String::from(desc),
}
} }
} }
}
pub trait DisplayRow { impl Table for Item {
fn to_row(&self) -> Vec<Cell>; fn head() -> Row {
} Row::new()
.cell(Cell::new("ID"))
.cell(Cell::new("NAME").shrinkable())
.cell(Cell::new("DESC"))
}
pub trait DisplayTable<'a, T: DisplayRow + 'a> { fn row(&self) -> Row {
fn header_row() -> Vec<Cell>; Row::new()
fn rows(&self) -> &Vec<T>; .cell(Cell::new(self.id.to_string()))
.cell(Cell::new(self.name.as_str()).shrinkable())
.cell(Cell::new(self.desc.as_str()))
}
fn to_table(&self) -> String { fn max_width() -> usize {
let mut col_sizes = vec![]; 20
let head = Self::header_row(); }
}
head.iter().for_each(|cell| { #[test]
col_sizes.push(cell.unicode_width()); fn row_smaller_than_head() {
}); let items = vec![
Item::new(1, "a", "aa"),
Item::new(2, "b", "bb"),
Item::new(3, "c", "cc"),
];
let mut table = self let table = vec![
.rows() vec!["ID ", "NAME ", "DESC "],
.iter() vec!["1 ", "a ", "aa "],
.map(|item| { vec!["2 ", "b ", "bb "],
let row = item.to_row(); vec!["3 ", "c ", "cc "],
row.iter() ];
.enumerate()
.for_each(|(i, cell)| col_sizes[i] = col_sizes[i].max(cell.unicode_width()));
row
})
.collect::<Vec<_>>();
table.insert(0, head); assert_eq!(table, Table::build(&items));
}
let term_width = terminal_size().map(|size| size.0 .0).unwrap_or(0) as usize; #[test]
let seps_width = 2 * col_sizes.len() - 1; fn row_bigger_than_head() {
let table_width = col_sizes.iter().sum::<usize>() + seps_width; let items = vec![
let diff_width = if table_width < term_width { Item::new(1, "a", "aa"),
0 Item::new(2222, "bbbbb", "bbbbb"),
} else { Item::new(3, "c", "cc"),
table_width - term_width ];
};
table.iter().fold(String::new(), |output, row| { let table = vec![
let row_str = row vec!["ID ", "NAME ", "DESC "],
.iter() vec!["1 ", "a ", "aa "],
.enumerate() vec!["2222 ", "bbbbb ", "bbbbb "],
.map(|(i, cell)| { vec!["3 ", "c ", "cc "],
if cell.flex && col_sizes[i] > diff_width { ];
cell.render(col_sizes[i] - diff_width)
} else {
cell.render(col_sizes[i])
}
})
.collect::<Vec<_>>()
.join(&Cell::new(&[ext(8)], "|").render(0));
output + &row_str + "\n" assert_eq!(table, Table::build(&items));
})
let items = vec![
Item::new(1, "a", "aa"),
Item::new(2222, "bbbbb", "bbbbb"),
Item::new(3, "cccccc", "cc"),
];
let table = vec![
vec!["ID ", "NAME ", "DESC "],
vec!["1 ", "a ", "aa "],
vec!["2222 ", "bbbbb ", "bbbbb "],
vec!["3 ", "cccccc ", "cc "],
];
assert_eq!(table, Table::build(&items));
}
#[test]
fn shrink() {
let items = vec![
Item::new(1, "short", "desc"),
Item::new(2, "loooooong", "desc"),
Item::new(3, "shriiiiink", "desc"),
Item::new(4, "shriiiiiiiiiink", "desc"),
Item::new(5, "😍😍😍😍", "desc"),
Item::new(6, "😍😍😍😍😍", "desc"),
Item::new(7, "!😍😍😍😍😍", "desc"),
];
let table = vec![
vec!["ID ", "NAME ", "DESC "],
vec!["1 ", "short ", "desc "],
vec!["2 ", "loooooong ", "desc "],
vec!["3 ", "shriiiii… ", "desc "],
vec!["4 ", "shriiiii… ", "desc "],
vec!["5 ", "😍😍😍😍 ", "desc "],
vec!["6 ", "😍😍😍😍… ", "desc "],
vec!["7 ", "!😍😍😍… ", "desc "],
];
assert_eq!(table, Table::build(&items));
} }
} }
#[allow(dead_code)]
pub const RESET: Style = Style(0, 0, 0);
#[allow(dead_code)]
pub const BOLD: Style = Style(1, 0, 0);
#[allow(dead_code)]
pub const UNDERLINE: Style = Style(4, 0, 0);
#[allow(dead_code)]
pub const REVERSED: Style = Style(7, 0, 0);
#[allow(dead_code)]
pub const BLACK: Style = Style(30, 0, 0);
#[allow(dead_code)]
pub const RED: Style = Style(31, 0, 0);
#[allow(dead_code)]
pub const GREEN: Style = Style(32, 0, 0);
#[allow(dead_code)]
pub const YELLOW: Style = Style(33, 0, 0);
#[allow(dead_code)]
pub const BLUE: Style = Style(34, 0, 0);
#[allow(dead_code)]
pub const MAGENTA: Style = Style(35, 0, 0);
#[allow(dead_code)]
pub const CYAN: Style = Style(36, 0, 0);
#[allow(dead_code)]
pub const WHITE: Style = Style(37, 0, 0);
#[allow(dead_code)]
pub const BRIGHT_BLACK: Style = Style(30, 1, 0);
#[allow(dead_code)]
pub const BRIGHT_RED: Style = Style(31, 1, 0);
#[allow(dead_code)]
pub const BRIGHT_GREEN: Style = Style(32, 1, 0);
#[allow(dead_code)]
pub const BRIGHT_YELLOW: Style = Style(33, 1, 0);
#[allow(dead_code)]
pub const BRIGHT_BLUE: Style = Style(34, 1, 0);
#[allow(dead_code)]
pub const BRIGHT_MAGENTA: Style = Style(35, 1, 0);
#[allow(dead_code)]
pub const BRIGHT_CYAN: Style = Style(36, 1, 0);
#[allow(dead_code)]
pub const BRIGHT_WHITE: Style = Style(37, 1, 0);
#[allow(dead_code)]
pub const BG_BLACK: Style = Style(40, 0, 0);
#[allow(dead_code)]
pub const BG_RED: Style = Style(41, 0, 0);
#[allow(dead_code)]
pub const BG_GREEN: Style = Style(42, 0, 0);
#[allow(dead_code)]
pub const BG_YELLOW: Style = Style(43, 0, 0);
#[allow(dead_code)]
pub const BG_BLUE: Style = Style(44, 0, 0);
#[allow(dead_code)]
pub const BG_MAGENTA: Style = Style(45, 0, 0);
#[allow(dead_code)]
pub const BG_CYAN: Style = Style(46, 0, 0);
#[allow(dead_code)]
pub const BG_WHITE: Style = Style(47, 0, 0);
#[allow(dead_code)]
pub const BG_BRIGHT_BLACK: Style = Style(40, 1, 0);
#[allow(dead_code)]
pub const BG_BRIGHT_RED: Style = Style(41, 1, 0);
#[allow(dead_code)]
pub const BG_BRIGHT_GREEN: Style = Style(42, 1, 0);
#[allow(dead_code)]
pub const BG_BRIGHT_YELLOW: Style = Style(43, 1, 0);
#[allow(dead_code)]
pub const BG_BRIGHT_BLUE: Style = Style(44, 1, 0);
#[allow(dead_code)]
pub const BG_BRIGHT_MAGENTA: Style = Style(45, 1, 0);
#[allow(dead_code)]
pub const BG_BRIGHT_CYAN: Style = Style(46, 1, 0);
#[allow(dead_code)]
pub const BG_BRIGHT_WHITE: Style = Style(47, 1, 0);
#[allow(dead_code)]
fn ext(n: u8) -> Style {
Style(38, 5, n)
}