Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
term: Add border support for tables
Alexis Sellier committed 3 years ago
commit 6fb6cff70825fbac2aef72722181d39c9cd4113e
parent af99d6f47aa4d6bbc0bf1e48c588397e5f1594db
9 files changed +267 -73
modified radicle-cli/examples/rad-node.md
@@ -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>
modified radicle-cli/examples/rad-patch.md
@@ -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>
modified radicle-cli/examples/workflow/4-patching-contributor.md
@@ -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>
modified radicle-cli/src/commands/node/routing.rs
@@ -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()),
modified radicle-cli/src/commands/node/tracking.rs
@@ -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()),
modified radicle-term/src/cell.rs
@@ -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()
        }
modified radicle-term/src/element.rs
@@ -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)]
modified radicle-term/src/lib.rs
@@ -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::*;
modified radicle-term/src/table.rs
@@ -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
    }
}

@@ -165,6 +260,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 {
            max: Max {