term: Add border support for tables
This commit is contained in:
parent
af99d6f47a
commit
6fb6cff708
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue