term: Add border support for tables

This commit is contained in:
Alexis Sellier 2023-03-26 11:31:43 +02:00
parent af99d6f47a
commit 6fb6cff708
No known key found for this signature in database
9 changed files with 267 additions and 73 deletions

View File

@ -45,9 +45,11 @@ repository that was already created:
``` ```
$ rad node tracking $ rad node tracking
RID Scope Policy ╭──────────────────────────────────────────────────────╮
--- ----- ------ │ RID Scope Policy │
rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji trusted track ├──────────────────────────────────────────────────────┤
│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji trusted track │
╰──────────────────────────────────────────────────────╯
``` ```
This is the same as using the `--repos` flag, but if we wish to see This is the same as using the `--repos` flag, but if we wish to see
@ -56,9 +58,11 @@ flag:
``` ```
$ rad node tracking --nodes $ rad node tracking --nodes
DID Alias Policy ╭───────────────────────────────────────────────────────────────────────────╮
--- ----- ------ │ DID Alias Policy │
did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk Bob track ├───────────────────────────────────────────────────────────────────────────┤
│ did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk Bob track │
╰───────────────────────────────────────────────────────────────────────────╯
``` ```
To see the routing table we can use the `rad node routing` command and To see the routing table we can use the `rad node routing` command and
@ -68,9 +72,11 @@ created.
``` ```
$ rad node routing $ rad node routing
RID NID ╭─────────────────────────────────────────────────────╮
--- --- │ RID NID │
rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji z6MknSL…StBU8Vi ├─────────────────────────────────────────────────────┤
│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji z6MknSL…StBU8Vi │
╰─────────────────────────────────────────────────────╯
``` ```
<details> <details>

View File

@ -51,14 +51,14 @@ $ rad patch
``` ```
``` ```
$ rad patch show 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 $ rad patch show 191a14e520f2eeff7c0e3ee0a5523c5217eecb89
╭──────────────────────────────────────────────────────────────────── ╭──────────────────────────────────────────────────────────────────╮
│ Title Define power requirements │ Title Define power requirements │
│ Patch 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 │ Patch 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 │
│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │
│ Status open │ Status open │
│ │
│ See details. │ See details. │
╰──────────────────────────────────────────────────────────────────── ╰──────────────────────────────────────────────────────────────────╯
commit 3e674d1a1df90807e934f9ae5da2591dd6848a33 commit 3e674d1a1df90807e934f9ae5da2591dd6848a33
Author: radicle <radicle@localhost> Author: radicle <radicle@localhost>

View File

@ -49,14 +49,14 @@ $ rad patch
│ ● opened by did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (you) 3 months ago │ │ ● opened by did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (you) 3 months ago │
╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╰─────────────────────────────────────────────────────────────────────────────────────────╯
$ rad patch show a07ef7743a32a2e902672ea3526d1db6ee08108a $ rad patch show a07ef7743a32a2e902672ea3526d1db6ee08108a
╭──────────────────────────────────────────────────────────────────── ╭──────────────────────────────────────────────────────────────────╮
│ Title Define power requirements │ Title Define power requirements │
│ Patch a07ef7743a32a2e902672ea3526d1db6ee08108a │ Patch a07ef7743a32a2e902672ea3526d1db6ee08108a │
│ Author did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │ Author did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │
│ Status open │ Status open │
│ │
│ See details. │ See details. │
╰──────────────────────────────────────────────────────────────────── ╰──────────────────────────────────────────────────────────────────╯
commit 3e674d1a1df90807e934f9ae5da2591dd6848a33 commit 3e674d1a1df90807e934f9ae5da2591dd6848a33
Author: radicle <radicle@localhost> Author: radicle <radicle@localhost>

View File

@ -4,15 +4,13 @@ use crate::terminal as term;
use crate::terminal::Element; use crate::terminal::Element;
pub fn run<S: node::routing::Store>(routing: &S) -> anyhow::Result<()> { pub fn run<S: node::routing::Store>(routing: &S) -> anyhow::Result<()> {
let mut t = term::Table::new(term::table::TableOptions::default()); let mut t = term::Table::new(term::table::TableOptions::bordered());
t.push([ t.push([
term::format::default(String::from("RID")), term::format::default(String::from("RID")),
term::format::default(String::from("NID")), term::format::default(String::from("NID")),
]); ]);
t.push([ t.divider();
term::format::default(String::from("---")),
term::format::default(String::from("---")),
]);
for (id, node) in routing.entries()? { for (id, node) in routing.entries()? {
t.push([ t.push([
term::format::highlight(id.to_string()), term::format::highlight(id.to_string()),

View File

@ -15,17 +15,14 @@ pub fn run(store: &tracking::store::Config, mode: TrackingMode) -> anyhow::Resul
} }
fn print_repos(store: &tracking::store::Config) -> anyhow::Result<()> { fn print_repos(store: &tracking::store::Config) -> anyhow::Result<()> {
let mut t = term::Table::new(term::table::TableOptions::default()); let mut t = term::Table::new(term::table::TableOptions::bordered());
t.push([ t.push([
term::format::default(String::from("RID")), term::format::default(String::from("RID")),
term::format::default(String::from("Scope")), term::format::default(String::from("Scope")),
term::format::default(String::from("Policy")), term::format::default(String::from("Policy")),
]); ]);
t.push([ t.divider();
term::format::default(String::from("---")),
term::format::default(String::from("-----")),
term::format::default(String::from("------")),
]);
for tracking::Repo { id, scope, policy } in store.repo_policies()? { for tracking::Repo { id, scope, policy } in store.repo_policies()? {
let id = id.to_string(); let id = id.to_string();
let scope = scope.to_string(); let scope = scope.to_string();
@ -43,17 +40,14 @@ fn print_repos(store: &tracking::store::Config) -> anyhow::Result<()> {
} }
fn print_nodes(store: &tracking::store::Config) -> anyhow::Result<()> { fn print_nodes(store: &tracking::store::Config) -> anyhow::Result<()> {
let mut t = term::Table::new(term::table::TableOptions::default()); let mut t = term::Table::new(term::table::TableOptions::bordered());
t.push([ t.push([
term::format::default(String::from("DID")), term::format::default(String::from("DID")),
term::format::default(String::from("Alias")), term::format::default(String::from("Alias")),
term::format::default(String::from("Policy")), term::format::default(String::from("Policy")),
]); ]);
t.push([ t.divider();
term::format::default(String::from("---")),
term::format::default(String::from("-----")),
term::format::default(String::from("------")),
]);
for tracking::Node { id, alias, policy } in store.node_policies()? { for tracking::Node { id, alias, policy } in store.node_policies()? {
t.push([ t.push([
term::format::highlight(Did::from(id).to_string()), term::format::highlight(Did::from(id).to_string()),

View File

@ -111,7 +111,12 @@ impl Cell for str {
boundary += g.len(); boundary += g.len();
cols += c; cols += c;
} }
format!("{}{delim}", &self[..boundary]) // Don't add the delimiter if we just trimmed whitespace.
if self[boundary..].trim().is_empty() {
self[..boundary + 1].to_owned()
} else {
format!("{}{delim}", &self[..boundary])
}
} else { } else {
self.to_owned() self.to_owned()
} }

View File

@ -71,6 +71,13 @@ impl<T: Element> Element for &T {
} }
} }
/// Used to specify maximum width or height.
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
pub struct Max {
pub width: Option<usize>,
pub height: Option<usize>,
}
/// A line of text that has styling and can be displayed. /// A line of text that has styling and can be displayed.
#[derive(Clone, Default, Debug)] #[derive(Clone, Default, Debug)]
pub struct Line { pub struct Line {
@ -189,6 +196,17 @@ impl Size {
pub fn new(cols: usize, rows: usize) -> Self { pub fn new(cols: usize, rows: usize) -> Self {
Self { cols, rows } Self { cols, rows }
} }
/// Limit size.
pub fn limit(mut self, lim: Max) -> Self {
if let Some(w) = lim.width {
self.cols = self.cols.min(w);
}
if let Some(h) = lim.height {
self.rows = self.rows.min(h);
}
self
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -16,7 +16,7 @@ pub mod vstack;
pub use ansi::Color; pub use ansi::Color;
pub use ansi::{paint, Paint}; pub use ansi::{paint, Paint};
pub use editor::Editor; pub use editor::Editor;
pub use element::{Element, Line, Size}; pub use element::{Element, Line, Max, Size};
pub use hstack::HStack; pub use hstack::HStack;
pub use inquire::ui::Styled; pub use inquire::ui::Styled;
pub use io::*; pub use io::*;

View File

@ -20,17 +20,10 @@ use std::fmt;
use crate as term; use crate as term;
use crate::cell::Cell; use crate::cell::Cell;
use crate::{Label, Line, Size}; use crate::{Color, Label, Line, Max, Paint, Size};
pub use crate::Element; pub use crate::Element;
/// Used to specify maximum width or height.
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
pub struct Max {
width: Option<usize>,
height: Option<usize>,
}
#[derive(Debug)] #[derive(Debug)]
pub struct TableOptions { pub struct TableOptions {
/// Whether the table should be allowed to overflow. /// Whether the table should be allowed to overflow.
@ -39,6 +32,8 @@ pub struct TableOptions {
pub max: Max, pub max: Max,
/// Horizontal spacing between table cells. /// Horizontal spacing between table cells.
pub spacing: usize, pub spacing: usize,
/// Table border.
pub border: Option<Color>,
} }
impl Default for TableOptions { impl Default for TableOptions {
@ -47,15 +42,31 @@ impl Default for TableOptions {
overflow: false, overflow: false,
max: Max::default(), max: Max::default(),
spacing: 1, spacing: 1,
border: None,
}
}
}
impl TableOptions {
pub fn bordered() -> Self {
Self {
border: Some(term::colors::FAINT),
spacing: 3,
..Self::default()
} }
} }
} }
#[derive(Debug)]
enum Row<const W: usize, T> {
Data([T; W]),
Divider,
}
#[derive(Debug)] #[derive(Debug)]
pub struct Table<const W: usize, T> { pub struct Table<const W: usize, T> {
rows: Vec<[T; W]>, rows: Vec<Row<W, T>>,
widths: [usize; W], widths: [usize; W],
width: usize,
opts: TableOptions, opts: TableOptions,
} }
@ -64,7 +75,6 @@ impl<const W: usize, T> Default for Table<W, T> {
Self { Self {
rows: Vec::new(), rows: Vec::new(),
widths: [0; W], widths: [0; W],
width: 0,
opts: TableOptions::default(), opts: TableOptions::default(),
} }
} }
@ -80,24 +90,71 @@ where
fn render(&self) -> Vec<Line> { fn render(&self) -> Vec<Line> {
let mut lines = Vec::new(); let mut lines = Vec::new();
let width = self.opts.max.width.or_else(term::columns); let limits = self.limits();
let width = limits.width;
let border = self.opts.border;
let inner = self.inner().limit(limits);
let cols = inner.cols;
if let Some(color) = border {
lines.push(
Line::default()
.item(Paint::new("").fg(color))
.item(Paint::new("".repeat(cols)).fg(color))
.item(Paint::new("").fg(color)),
);
}
for row in &self.rows { for row in &self.rows {
let mut line = Line::default(); let mut line = Line::default();
for (i, cell) in row.iter().enumerate() { match row {
let pad = if i == row.len() - 1 { Row::Data(cells) => {
0 if let Some(color) = border {
} else { line.push(Paint::new("").fg(color));
self.widths[i] + self.opts.spacing }
}; for (i, cell) in cells.iter().enumerate() {
line.push(cell.pad(pad)); let pad = if i == cells.len() - 1 {
} if border.is_some() {
self.widths[i]
} else {
0
}
} else {
self.widths[i] + self.opts.spacing
};
line.push(cell.pad(pad));
}
if let Some(width) = width { if let Some(width) = width {
line.truncate(width, ""); line.truncate(width, "");
}; }
lines.push(line); if let Some(color) = border {
line.push(Paint::new("").fg(color));
}
lines.push(line);
}
Row::Divider => {
if let Some(color) = border {
lines.push(
Line::default()
.item(Paint::new("").fg(color))
.item(Paint::new("".repeat(cols)).fg(color))
.item(Paint::new("").fg(color)),
);
} else {
lines.push(Line::default());
}
}
}
}
if let Some(color) = border {
lines.push(
Line::default()
.item(Paint::new("").fg(color))
.item(Paint::new("".repeat(cols)).fg(color))
.item(Paint::new("").fg(color)),
);
} }
lines lines
} }
@ -108,22 +165,60 @@ impl<const W: usize, T: Cell> Table<W, T> {
Self { Self {
rows: Vec::new(), rows: Vec::new(),
widths: [0; W], widths: [0; W],
width: 0,
opts, opts,
} }
} }
pub fn size(&self) -> Size { pub fn size(&self) -> Size {
Size::new(self.width, self.rows.len()) self.outer()
}
pub fn divider(&mut self) {
self.rows.push(Row::Divider);
}
pub fn limits(&self) -> Max {
let width = self.opts.max.width.or_else(term::columns).map(|w| {
if self.opts.border.is_some() {
w - 2
} else {
w
}
});
Max {
width,
height: None,
}
} }
pub fn push(&mut self, row: [T; W]) { pub fn push(&mut self, row: [T; W]) {
for (i, cell) in row.iter().enumerate() { for (i, cell) in row.iter().enumerate() {
self.widths[i] = self.widths[i].max(cell.width()); self.widths[i] = self.widths[i].max(cell.width());
} }
self.width = self.rows.push(Row::Data(row));
self.width.max(row.iter().map(Cell::width).sum()) + (W - 1) * self.opts.spacing; }
self.rows.push(row);
fn inner(&self) -> Size {
let mut cols = self.widths.iter().sum::<usize>() + (W - 1) * self.opts.spacing;
let rows = self.rows.len();
let limits = self.limits();
// Account for inner spacing.
if self.opts.border.is_some() {
cols += 2;
}
Size::new(cols, rows).limit(limits)
}
fn outer(&self) -> Size {
let mut inner = self.inner();
// Account for outer borders.
if self.opts.border.is_some() {
inner.cols += 2;
inner.rows += 2;
}
inner
} }
} }
@ -164,6 +259,84 @@ mod test {
); );
} }
#[test]
fn test_table_border() {
let mut t = Table::new(TableOptions {
border: Some(Color::Unset),
spacing: 3,
..TableOptions::default()
});
t.push(["Country", "Population", "Code"]);
t.divider();
t.push(["France", "60M", "FR"]);
t.push(["Switzerland", "7M", "CH"]);
t.push(["Germany", "80M", "DE"]);
let inner = t.inner();
assert_eq!(inner.cols, 33);
assert_eq!(inner.rows, 5);
let outer = t.outer();
assert_eq!(outer.cols, 35);
assert_eq!(outer.rows, 7);
assert_eq!(
t.display(),
r#"
Country Population Code
France 60M FR
Switzerland 7M CH
Germany 80M DE
"#
.trim_start()
);
}
#[test]
fn test_table_border_truncated() {
let mut t = Table::new(TableOptions {
border: Some(Color::Unset),
spacing: 3,
max: Max {
width: Some(19),
height: None,
},
..TableOptions::default()
});
t.push(["Code", "Name"]);
t.divider();
t.push(["FR", "France"]);
t.push(["CH", "Switzerland"]);
t.push(["DE", "Germany"]);
let inner = t.inner();
assert_eq!(inner.cols, 17);
assert_eq!(inner.rows, 5);
let outer = t.outer();
assert_eq!(outer.cols, 19);
assert_eq!(outer.rows, 7);
assert_eq!(
t.display(),
r#"
Code Name
FR France
CH Switzer
DE Germany
"#
.trim_start()
);
}
#[test] #[test]
fn test_table_truncate() { fn test_table_truncate() {
let mut t = Table::new(TableOptions { let mut t = Table::new(TableOptions {