Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: Display patches like issues, in `rad patch`
Alexis Sellier committed 3 years ago
commit 3b20d45d80e6dbcc1649f9628d32270586530122
parent b809bd3d736d51c517afdaeb867a69668d76f864
11 files changed +238 -140
modified radicle-cli/examples/rad-patch.md
@@ -41,19 +41,22 @@ It will now be listed as one of the project's open patches.

```
$ rad patch
-
╭─────────────────────────────────────────────────────────────────────────────────────────╮
-
│ Define power requirements 191a14e R0 3e674d1 (flux-capacitor-power) ahead 1, behind 0   │
-
├─────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ● opened by did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [    ...    ]│
-
╰─────────────────────────────────────────────────────────────────────────────────────────╯
+
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
+
│ ●  ID       Title                      Author                  Head     +   -   Opened       │
+
├──────────────────────────────────────────────────────────────────────────────────────────────┤
+
│ ●  191a14e  Define power requirements  z6MknSL…StBU8Vi  (you)  3e674d1  +0  -0  4 months ago │
+
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
```
```
$ rad patch show 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 -p
╭─────────────────────────────────────────────────────────────────────────────────────────╮
-
│ Title   Define power requirements                                                       │
-
│ Patch   191a14e520f2eeff7c0e3ee0a5523c5217eecb89                                        │
-
│ Author  did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi                        │
-
│ Status  open                                                                            │
+
│ Title     Define power requirements                                                     │
+
│ Patch     191a14e520f2eeff7c0e3ee0a5523c5217eecb89                                      │
+
│ Author    did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi                      │
+
│ Head      3e674d1a1df90807e934f9ae5da2591dd6848a33                                      │
+
│ Branches  flux-capacitor-power                                                          │
+
│ Commits   ahead 1, behind 0                                                             │
+
│ Status    open                                                                          │
│                                                                                         │
│ See details.                                                                            │
├─────────────────────────────────────────────────────────────────────────────────────────┤
@@ -120,12 +123,20 @@ $ rad review 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 --accept --no-message --no
Showing the patch list now will reveal the favorable verdict:

```
-
$ rad patch
-
╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮
-
│ Define power requirements 191a14e R1 27857ec (flux-capacitor-power, patch/191a14e) ahead 2, behind 0 │
-
├──────────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ● opened by did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [         ...            ]│
-
│ ↑ updated to b8f7bfbbb3c6a207b349e9f45bf535c706805871 (27857ec) [                    ...            ]│
-
│ ✓ accepted by z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [               ...            ]│
-
╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯
+
$ rad patch show 191a14e
+
╭─────────────────────────────────────────────────────────────────────────────────────────╮
+
│ Title     Define power requirements                                                     │
+
│ Patch     191a14e520f2eeff7c0e3ee0a5523c5217eecb89                                      │
+
│ Author    did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi                      │
+
│ Head      27857ec9eb04c69cacab516e8bf4b5fd36090f66                                      │
+
│ Branches  flux-capacitor-power, patch/191a14e                                           │
+
│ Commits   ahead 2, behind 0                                                             │
+
│ Status    open                                                                          │
+
│                                                                                         │
+
│ See details.                                                                            │
+
├─────────────────────────────────────────────────────────────────────────────────────────┤
+
│ ● opened by did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [    ...    ]│
+
│ ↑ updated to b8f7bfbbb3c6a207b349e9f45bf535c706805871 (27857ec) [               ...    ]│
+
│ ✓ accepted by z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [          ...    ]│
+
╰─────────────────────────────────────────────────────────────────────────────────────────╯
```
modified radicle-cli/examples/workflow/4-patching-contributor.md
@@ -41,17 +41,20 @@ It will now be listed as one of the project's open patches.

```
$ rad patch
-
╭─────────────────────────────────────────────────────────────────────────────────────────╮
-
│ Define power requirements a07ef77 R0 3e674d1 (flux-capacitor-power) ahead 1, behind 0   │
-
├─────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ● opened by did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (you) [    ...    ]│
-
╰─────────────────────────────────────────────────────────────────────────────────────────╯
+
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
+
│ ●  ID       Title                      Author                  Head     +   -   Opened       │
+
├──────────────────────────────────────────────────────────────────────────────────────────────┤
+
│ ●  a07ef77  Define power requirements  z6Mkt67…v4N1tRk  (you)  3e674d1  +0  -0  4 months ago │
+
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
$ rad patch show a07ef7743a32a2e902672ea3526d1db6ee08108a
╭─────────────────────────────────────────────────────────────────────────────────────────╮
-
│ Title   Define power requirements                                                       │
-
│ Patch   a07ef7743a32a2e902672ea3526d1db6ee08108a                                        │
-
│ Author  did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk                        │
-
│ Status  open                                                                            │
+
│ Title     Define power requirements                                                     │
+
│ Patch     a07ef7743a32a2e902672ea3526d1db6ee08108a                                      │
+
│ Author    did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk                      │
+
│ Head      3e674d1a1df90807e934f9ae5da2591dd6848a33                                      │
+
│ Branches  flux-capacitor-power                                                          │
+
│ Commits   ahead 1, behind 0                                                             │
+
│ Status    open                                                                          │
│                                                                                         │
│ See details.                                                                            │
├─────────────────────────────────────────────────────────────────────────────────────────┤
modified radicle-cli/examples/workflow/5-patching-maintainer.md
@@ -26,12 +26,19 @@ $ git branch -r
  bob/flux-capacitor-power
  bob/master
  rad/master
-
$ rad patch
+
$ rad patch show a07ef77
╭───────────────────────────────────────────────────────────────────────────────────╮
-
│ Define power requirements a07ef77 R1 27857ec ahead 2, behind 0                    │
+
│ Title    Define power requirements                                                │
+
│ Patch    a07ef7743a32a2e902672ea3526d1db6ee08108a                                 │
+
│ Author   did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk                 │
+
│ Head     27857ec9eb04c69cacab516e8bf4b5fd36090f66                                 │
+
│ Commits  ahead 2, behind 0                                                        │
+
│ Status   open                                                                     │
+
│                                                                                   │
+
│ See details.                                                                      │
├───────────────────────────────────────────────────────────────────────────────────┤
-
│ ● opened by did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk [    ...    ]│
-
│ ↑ updated to 11483929d8714a92992229f65433e06288f3b760 (27857ec) [         ...    ]│
+
│ ● opened by did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk [    ...   ] │
+
│ ↑ updated to 11483929d8714a92992229f65433e06288f3b760 (27857ec) [         ...   ] │
╰───────────────────────────────────────────────────────────────────────────────────╯
```

@@ -76,15 +83,23 @@ Fast-forward
The patch is now merged and closed :).

```
-
$ rad patch --merged
-
╭───────────────────────────────────────────────────────────────────────────────────────────────╮
-
│ Define power requirements a07ef77 R2 f6484e0 (flux-capacitor-power, master) ahead 3, behind 0 │
-
├───────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ● opened by did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk [            ...        ]│
-
│ ↑ updated to 11483929d8714a92992229f65433e06288f3b760 (27857ec) [                 ...        ]│
-
│ ↑ updated to 0795d619232479e910f95bb9c873ee1ec305c43c (f6484e0) [                 ...        ]│
-
│ ✓ merged by did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [      ...        ]│
-
╰───────────────────────────────────────────────────────────────────────────────────────────────╯
+
$ rad patch show a07ef77
+
╭─────────────────────────────────────────────────────────────────────────────────────────╮
+
│ Title     Define power requirements                                                     │
+
│ Patch     a07ef7743a32a2e902672ea3526d1db6ee08108a                                      │
+
│ Author    did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk                      │
+
│ Head      f6484e0f43e48a8983b9b39bf9bd4cd889f1d520                                      │
+
│ Branches  flux-capacitor-power, master                                                  │
+
│ Commits   ahead 3, behind 0                                                             │
+
│ Status    merged                                                                        │
+
│                                                                                         │
+
│ See details.                                                                            │
+
├─────────────────────────────────────────────────────────────────────────────────────────┤
+
│ ● opened by did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk [        ...     ] │
+
│ ↑ updated to 11483929d8714a92992229f65433e06288f3b760 (27857ec) [             ...     ] │
+
│ ↑ updated to 0795d619232479e910f95bb9c873ee1ec305c43c (f6484e0) [             ...     ] │
+
│ ✓ merged by did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [  ...     ] │
+
╰─────────────────────────────────────────────────────────────────────────────────────────╯
```

To publish our new state to the network, we simply push:
modified radicle-cli/src/commands/patch.rs
@@ -270,7 +270,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
            create::run(&repository, &profile, &workdir, message.clone(), options)?;
        }
        Operation::List { filter } => {
-
            list::run(&repository, &profile, Some(workdir), filter)?;
+
            list::run(&repository, &profile, filter)?;
        }
        Operation::Show { patch_id, diff } => {
            let patch_id = patch_id.resolve(&repository.backend)?;
modified radicle-cli/src/commands/patch/common.rs
@@ -68,8 +68,23 @@ pub fn patch_merge_target_oid(target: MergeTarget, repository: &Repository) -> a
    }
}

+
/// Get the diff stats between two commits.
+
pub fn diff_stats(
+
    repo: &git::raw::Repository,
+
    old: &Oid,
+
    new: &Oid,
+
) -> Result<git::raw::DiffStats, git::raw::Error> {
+
    let old = repo.find_commit(*old)?;
+
    let new = repo.find_commit(*new)?;
+
    let old_tree = old.tree()?;
+
    let new_tree = new.tree()?;
+
    let diff = repo.diff_tree_to_tree(Some(&old_tree), Some(&new_tree), None)?;
+

+
    diff.stats()
+
}
+

/// Create a human friendly message about git's sync status.
-
pub fn pretty_sync_status(
+
pub fn ahead_behind(
    repo: &git::raw::Repository,
    revision_oid: Oid,
    head_oid: Oid,
@@ -89,35 +104,21 @@ pub fn pretty_sync_status(
        .item(behind))
}

-
/// Make a human friendly string for commit version information.
-
///
-
/// For example '<oid> (branch1[, branch2])'.
-
pub fn pretty_commit_version(
-
    revision_oid: &Oid,
-
    repo: Option<&git::raw::Repository>,
-
) -> anyhow::Result<term::Line> {
-
    let oid = term::format::secondary(term::format::oid(*revision_oid));
-
    let mut line = term::Line::new(oid);
+
/// Get the branches that point to a commit.
+
pub fn branches(target: &Oid, repo: &git::raw::Repository) -> anyhow::Result<Vec<String>> {
    let mut branches: Vec<String> = vec![];

-
    if let Some(repo) = repo {
-
        for r in repo.references()?.flatten() {
-
            if !r.is_branch() {
-
                continue;
-
            }
-
            if let (Some(oid), Some(name)) = (&r.target(), &r.shorthand()) {
-
                if oid == revision_oid {
-
                    branches.push(name.to_string());
-
                };
-
            };
+
    for r in repo.references()?.flatten() {
+
        if !r.is_branch() {
+
            continue;
        }
-
    };
-
    if !branches.is_empty() {
-
        line.push(term::Label::space());
-
        line.push(term::format::yellow(format!("({})", branches.join(", "))));
+
        if let (Some(oid), Some(name)) = (&r.target(), &r.shorthand()) {
+
            if oid == target {
+
                branches.push(name.to_string());
+
            };
+
        };
    }
-

-
    Ok(line)
+
    Ok(branches)
}

#[inline]
modified radicle-cli/src/commands/patch/list.rs
@@ -1,13 +1,13 @@
use anyhow::anyhow;

use radicle::cob::patch;
-
use radicle::cob::patch::{Patch, PatchId, Patches, Revision, Verdict};
-
use radicle::git;
+
use radicle::cob::patch::{Patch, PatchId, Patches, Verdict};
use radicle::prelude::*;
use radicle::profile::Profile;
use radicle::storage::git::Repository;

use crate::terminal as term;
+
use term::table::{Table, TableOptions};
use term::Element as _;

use super::common;
@@ -16,7 +16,6 @@ use super::common;
pub fn run(
    repository: &Repository,
    profile: &Profile,
-
    workdir: Option<git::raw::Repository>,
    filter: Option<patch::State>,
) -> anyhow::Result<()> {
    let me = *profile.id();
@@ -48,62 +47,89 @@ pub fn run(
        return Ok(());
    }

+
    let mut table = Table::<9, term::Line>::new(TableOptions {
+
        spacing: 2,
+
        border: Some(term::colors::FAINT),
+
        ..TableOptions::default()
+
    });
+

+
    table.push([
+
        term::format::dim(String::from("●")).into(),
+
        term::format::bold(String::from("ID")).into(),
+
        term::format::bold(String::from("Title")).into(),
+
        term::format::bold(String::from("Author")).into(),
+
        term::format::bold(String::new()).into(),
+
        term::format::bold(String::from("Head")).into(),
+
        term::format::bold(String::from("+")).into(),
+
        term::format::bold(String::from("-")).into(),
+
        term::format::bold(String::from("Opened")).into(),
+
    ]);
+
    table.divider();
+

+
    let mut errors = Vec::new();
    for (id, patch) in &mut own {
-
        widget(&me, id, patch, workdir.as_ref(), repository)?.print();
+
        match row(&me, id, patch, repository) {
+
            Ok(r) => table.push(r),
+
            Err(e) => errors.push((patch.title(), id, e.to_string())),
+
        }
    }
    for (id, patch) in &mut other {
-
        widget(profile.id(), id, patch, workdir.as_ref(), repository)?.print();
+
        match row(&me, id, patch, repository) {
+
            Ok(r) => table.push(r),
+
            Err(e) => errors.push((patch.title(), id, e.to_string())),
+
        }
    }
+
    table.print();

-
    Ok(())
-
}
+
    if !errors.is_empty() {
+
        for (title, id, error) in errors {
+
            term::error(format!(
+
                "{} Patch {title:?} ({id}) failed to load: {error}",
+
                term::format::negative("Error:")
+
            ));
+
        }
+
    }

-
pub fn header(
-
    patch_id: &PatchId,
-
    patch: &Patch,
-
    workdir: Option<&git::raw::Repository>,
-
    repository: &Repository,
-
    revision: &Revision,
-
) -> anyhow::Result<term::Line> {
-
    let target_head = common::patch_merge_target_oid(patch.target(), repository)?;
-
    let header = term::Line::spaced([
-
        term::format::bold(patch.title()).into(),
-
        term::format::highlight(term::format::cob(patch_id)).into(),
-
        term::format::dim(format!("R{}", patch.version())).into(),
-
    ])
-
    .space()
-
    .extend(common::pretty_commit_version(&revision.head(), workdir)?)
-
    .space()
-
    .extend(common::pretty_sync_status(
-
        repository.raw(),
-
        revision.head().into(),
-
        target_head,
-
    )?);
-

-
    Ok(header)
+
    Ok(())
}

-
/// Patch widget.
-
pub fn widget<'a>(
+
/// Patch row.
+
pub fn row(
    whoami: &PublicKey,
-
    patch_id: &PatchId,
+
    id: &PatchId,
    patch: &Patch,
-
    workdir: Option<&git::raw::Repository>,
    repository: &Repository,
-
) -> anyhow::Result<term::VStack<'a>> {
+
) -> anyhow::Result<[term::Line; 9]> {
+
    let state = patch.state();
    let (_, revision) = patch
        .latest()
        .ok_or_else(|| anyhow!("patch is malformed: no revisions found"))?;
-
    let header = header(patch_id, patch, workdir, repository, revision)?;
-
    let mut widget = term::VStack::default()
-
        .child(header)
-
        .divider()
-
        .border(Some(term::colors::FAINT));
-

-
    for line in timeline(whoami, patch_id, patch, repository)? {
-
        widget.push(line);
-
    }
-
    Ok(widget)
+
    let stats = common::diff_stats(repository.raw(), revision.base(), &revision.head())?;
+
    let author = patch.author().id;
+

+
    Ok([
+
        match state {
+
            patch::State::Open => term::format::positive("●").into(),
+
            patch::State::Archived { .. } => term::format::yellow("●").into(),
+
            patch::State::Draft => term::format::dim("●").into(),
+
            patch::State::Merged { .. } => term::format::primary("✔").into(),
+
        },
+
        term::format::tertiary(term::format::cob(id)).into(),
+
        term::format::default(patch.title().to_owned()).into(),
+
        term::format::did(&author).dim().into(),
+
        if author.as_key() == whoami {
+
            term::format::primary("(you)".to_owned()).into()
+
        } else {
+
            term::format::default(String::new()).into()
+
        },
+
        term::format::secondary(term::format::oid(revision.head())).into(),
+
        term::format::positive(format!("+{}", stats.insertions())).into(),
+
        term::format::negative(format!("-{}", stats.deletions())).into(),
+
        term::format::timestamp(&patch.timestamp())
+
            .dim()
+
            .italic()
+
            .into(),
+
    ])
}

pub fn timeline(
modified radicle-cli/src/commands/patch/show.rs
@@ -5,7 +5,7 @@ use radicle::git;
use radicle::storage::git::Repository;
use radicle_term::{
    table::{Table, TableOptions},
-
    textarea, Element, Paint, VStack,
+
    textarea, Element, VStack,
};

use crate::terminal as term;
@@ -13,12 +13,7 @@ use crate::terminal as term;
use super::common::*;
use super::*;

-
fn show_patch_diff(
-
    patch: &patch::Patch,
-
    storage: &Repository,
-
    // TODO: Tell user which working copy branches point to the patch.
-
    _workdir: &git::raw::Repository,
-
) -> anyhow::Result<()> {
+
fn show_patch_diff(patch: &patch::Patch, storage: &Repository) -> anyhow::Result<()> {
    let target_head = patch_merge_target_oid(patch.target(), storage)?;
    let base_oid = storage.raw().merge_base(target_head, **patch.head())?;
    let diff = format!("{}..{}", base_oid, patch.head());
@@ -46,32 +41,53 @@ pub fn run(
    let Some(patch) = patches.get(patch_id)? else {
        anyhow::bail!("Patch `{patch_id}` not found");
    };
+
    let (_, revision) = patch
+
        .latest()
+
        .ok_or_else(|| anyhow!("patch is malformed: no revisions found"))?;
    let state = patch.state();
+
    let branches = common::branches(&revision.head(), workdir)?;
+
    let target_head = common::patch_merge_target_oid(patch.target(), stored)?;
+
    let ahead_behind = common::ahead_behind(stored.raw(), revision.head().into(), target_head)?;

-
    let mut attrs = Table::<2, Paint<String>>::new(TableOptions {
+
    let mut attrs = Table::<2, term::Line>::new(TableOptions {
        spacing: 2,
        ..TableOptions::default()
    });
    attrs.push([
-
        term::format::tertiary("Title".to_owned()),
-
        term::format::bold(patch.title().to_owned()),
+
        term::format::tertiary("Title".to_owned()).into(),
+
        term::format::bold(patch.title().to_owned()).into(),
+
    ]);
+
    attrs.push([
+
        term::format::tertiary("Patch".to_owned()).into(),
+
        term::format::default(patch_id.to_string()).into(),
    ]);
    attrs.push([
-
        term::format::tertiary("Patch".to_owned()),
-
        term::format::default(patch_id.to_string()),
+
        term::format::tertiary("Author".to_owned()).into(),
+
        term::format::default(patch.author().id().to_string()).into(),
    ]);
    attrs.push([
-
        term::format::tertiary("Author".to_owned()),
-
        term::format::default(patch.author().id().to_string()),
+
        term::format::tertiary("Head".to_owned()).into(),
+
        term::format::secondary(revision.head().to_string()).into(),
+
    ]);
+
    if !branches.is_empty() {
+
        attrs.push([
+
            term::format::tertiary("Branches".to_owned()).into(),
+
            term::format::yellow(branches.join(", ")).into(),
+
        ]);
+
    }
+
    attrs.push([
+
        term::format::tertiary("Commits".to_owned()).into(),
+
        ahead_behind,
    ]);
    attrs.push([
-
        term::format::tertiary("Status".to_owned()),
+
        term::format::tertiary("Status".to_owned()).into(),
        match state {
            patch::State::Open => term::format::positive(state.to_string()),
            patch::State::Draft => term::format::dim(state.to_string()),
            patch::State::Archived => term::format::yellow(state.to_string()),
            patch::State::Merged => term::format::primary(state.to_string()),
-
        },
+
        }
+
        .into(),
    ]);

    let description = patch.description().trim();
@@ -95,7 +111,7 @@ pub fn run(

    if diff {
        term::blank();
-
        show_patch_diff(&patch, stored, workdir)?;
+
        show_patch_diff(&patch, stored)?;
        term::blank();
    }
    Ok(())
modified radicle-term/src/cell.rs
@@ -1,6 +1,6 @@
use std::fmt::Display;

-
use super::Paint;
+
use super::{Element, Line, Paint};

use unicode_width::UnicodeWidthStr;

@@ -14,8 +14,10 @@ pub trait Cell: Display {
    /// Cell display width in number of terminal columns.
    fn width(&self) -> usize;
    /// Truncate cell if longer than given width. Shows the delimiter if truncated.
+
    #[must_use]
    fn truncate(&self, width: usize, delim: &str) -> Self::Truncated;
    /// Pad the cell so that it is the given width, while keeping the content left-aligned.
+
    #[must_use]
    fn pad(&self, width: usize) -> Self::Padded;
}

@@ -42,6 +44,27 @@ impl Cell for Paint<String> {
    }
}

+
impl Cell for Line {
+
    type Truncated = Line;
+
    type Padded = Line;
+

+
    fn width(&self) -> usize {
+
        <Self as Element>::size(self).cols
+
    }
+

+
    fn pad(&self, width: usize) -> Self::Padded {
+
        let mut line = self.clone();
+
        Line::pad(&mut line, width);
+
        line
+
    }
+

+
    fn truncate(&self, width: usize, delim: &str) -> Self::Truncated {
+
        let mut line = self.clone();
+
        Line::truncate(&mut line, width, delim);
+
        line
+
    }
+
}
+

impl Cell for Paint<&str> {
    type Truncated = Paint<String>;
    type Padded = Paint<String>;
modified radicle-term/src/colors.rs
@@ -2,3 +2,6 @@ use crate::ansi::Color;

/// The faintest color; useful for borders and such.
pub const FAINT: Color = Color::Fixed(236);
+

+
/// Negative color, useful for errors.
+
pub const NEGATIVE: Color = Color::Red;
modified radicle-term/src/element.rs
@@ -157,9 +157,9 @@ impl IntoIterator for Line {
    }
}

-
impl From<Label> for Line {
-
    fn from(label: Label) -> Self {
-
        Self { items: vec![label] }
+
impl<T: Into<Label>> From<T> for Line {
+
    fn from(value: T) -> Self {
+
        Self::new(value)
    }
}

@@ -218,19 +218,19 @@ mod test {
        let line = Line::default().item("banana").item("peach").item("apple");

        let mut actual = line.clone();
-
        actual.truncate(9, "…");
+
        actual = actual.truncate(9, "…");
        assert_eq!(actual.to_string(), "bananape…");

        let mut actual = line.clone();
-
        actual.truncate(7, "…");
+
        actual = actual.truncate(7, "…");
        assert_eq!(actual.to_string(), "banana…");

        let mut actual = line.clone();
-
        actual.truncate(1, "…");
+
        actual = actual.truncate(1, "…");
        assert_eq!(actual.to_string(), "…");

        let mut actual = line;
-
        actual.truncate(0, "…");
+
        actual = actual.truncate(0, "…");
        assert_eq!(actual.to_string(), "");
    }
}
modified radicle-term/src/table.rs
@@ -20,7 +20,7 @@ use std::fmt;

use crate as term;
use crate::cell::Cell;
-
use crate::{Color, Label, Line, Max, Paint, Size};
+
use crate::{Color, Line, Max, Paint, Size};

pub use crate::Element;

@@ -82,7 +82,7 @@ impl<const W: usize, T> Default for Table<W, T> {

impl<const W: usize, T: Cell + fmt::Debug> Element for Table<W, T>
where
-
    T::Padded: Into<Label>,
+
    T::Padded: Into<Line>,
{
    fn size(&self) -> Size {
        Table::size(self)
@@ -123,11 +123,11 @@ where
                        } else {
                            self.widths[i] + self.opts.spacing
                        };
-
                        line.push(cell.pad(pad));
+
                        line = line.extend(cell.pad(pad).into());
                    }

                    if let Some(width) = width {
-
                        line.truncate(width, "…");
+
                        line = line.truncate(width, "…");
                    }
                    if let Some(color) = border {
                        line.push(Paint::new(" │").fg(color));