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

View File

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

View File

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

View File

@ -4,15 +4,13 @@ use crate::terminal as term;
use crate::terminal::Element;
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([
term::format::default(String::from("RID")),
term::format::default(String::from("NID")),
]);
t.push([
term::format::default(String::from("---")),
term::format::default(String::from("---")),
]);
t.divider();
for (id, node) in routing.entries()? {
t.push([
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<()> {
let mut t = term::Table::new(term::table::TableOptions::default());
let mut t = term::Table::new(term::table::TableOptions::bordered());
t.push([
term::format::default(String::from("RID")),
term::format::default(String::from("Scope")),
term::format::default(String::from("Policy")),
]);
t.push([
term::format::default(String::from("---")),
term::format::default(String::from("-----")),
term::format::default(String::from("------")),
]);
t.divider();
for tracking::Repo { id, scope, policy } in store.repo_policies()? {
let id = id.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<()> {
let mut t = term::Table::new(term::table::TableOptions::default());
let mut t = term::Table::new(term::table::TableOptions::bordered());
t.push([
term::format::default(String::from("DID")),
term::format::default(String::from("Alias")),
term::format::default(String::from("Policy")),
]);
t.push([
term::format::default(String::from("---")),
term::format::default(String::from("-----")),
term::format::default(String::from("------")),
]);
t.divider();
for tracking::Node { id, alias, policy } in store.node_policies()? {
t.push([
term::format::highlight(Did::from(id).to_string()),

View File

@ -111,7 +111,12 @@ impl Cell for str {
boundary += g.len();
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 {
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.
#[derive(Clone, Default, Debug)]
pub struct Line {
@ -189,6 +196,17 @@ impl Size {
pub fn new(cols: usize, rows: usize) -> Self {
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)]

View File

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

View File

@ -20,17 +20,10 @@ use std::fmt;
use crate as term;
use crate::cell::Cell;
use crate::{Label, Line, Size};
use crate::{Color, Label, Line, Max, Paint, Size};
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)]
pub struct TableOptions {
/// Whether the table should be allowed to overflow.
@ -39,6 +32,8 @@ pub struct TableOptions {
pub max: Max,
/// Horizontal spacing between table cells.
pub spacing: usize,
/// Table border.
pub border: Option<Color>,
}
impl Default for TableOptions {
@ -47,15 +42,31 @@ impl Default for TableOptions {
overflow: false,
max: Max::default(),
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)]
pub struct Table<const W: usize, T> {
rows: Vec<[T; W]>,
rows: Vec<Row<W, T>>,
widths: [usize; W],
width: usize,
opts: TableOptions,
}
@ -64,7 +75,6 @@ impl<const W: usize, T> Default for Table<W, T> {
Self {
rows: Vec::new(),
widths: [0; W],
width: 0,
opts: TableOptions::default(),
}
}
@ -80,24 +90,71 @@ where
fn render(&self) -> Vec<Line> {
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 {
let mut line = Line::default();
for (i, cell) in row.iter().enumerate() {
let pad = if i == row.len() - 1 {
0
} else {
self.widths[i] + self.opts.spacing
};
line.push(cell.pad(pad));
}
match row {
Row::Data(cells) => {
if let Some(color) = border {
line.push(Paint::new("").fg(color));
}
for (i, cell) in cells.iter().enumerate() {
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 {
line.truncate(width, "");
};
lines.push(line);
if let Some(width) = width {
line.truncate(width, "");
}
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
}
@ -108,22 +165,60 @@ impl<const W: usize, T: Cell> Table<W, T> {
Self {
rows: Vec::new(),
widths: [0; W],
width: 0,
opts,
}
}
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]) {
for (i, cell) in row.iter().enumerate() {
self.widths[i] = self.widths[i].max(cell.width());
}
self.width =
self.width.max(row.iter().map(Cell::width).sum()) + (W - 1) * self.opts.spacing;
self.rows.push(row);
self.rows.push(Row::Data(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]
fn test_table_truncate() {
let mut t = Table::new(TableOptions {