Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
Building of term::Table from iterator
Merged did:key:z6MkwcUR...q1kL opened 7 months ago

This patchset introduces a FromIterator for term::Table implementation and makes use of that to collect iterators into term::Table instances.

This reduces cognitive load on the (API-)consuming side as well as it might be more resource efficient as preallocation of internal Vecs can happen. I did not measure of course, but still.

8 files changed +89 -46 ff021d58 370ae364
modified Cargo.lock
@@ -1862,6 +1862,15 @@ dependencies = [
]

[[package]]
+
name = "itertools"
+
version = "0.14.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+
dependencies = [
+
 "either",
+
]
+

+
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2714,6 +2723,7 @@ dependencies = [
 "dunce",
 "git-ref-format",
 "human-panic",
+
 "itertools",
 "lexopt",
 "localtime",
 "log",
modified Cargo.toml
@@ -30,6 +30,7 @@ dunce = "1.0.5"
fastrand = { version = "2.0.0", default-features = false }
git2 = { version = "0.19.0", default-features = false }
human-panic = "2"
+
itertools = "0.14"
lexopt = "0.3.0"
libc = "0.2.137"
localtime = "1.2.0"
modified crates/radicle-cli/Cargo.toml
@@ -19,6 +19,7 @@ chrono = { workspace = true, features = ["clock", "std"] }
dunce = { workspace = true }
git-ref-format = { version = "0.3.0", features = ["macro"] }
human-panic.workspace = true
+
itertools.workspace = true
lexopt = { workspace = true }
localtime = { workspace = true }
log = { workspace = true, features = ["std"] }
modified crates/radicle-cli/src/commands/inbox.rs
@@ -264,10 +264,6 @@ fn list_repo<'a, R: ReadStorage>(
where
    <R as ReadStorage>::Repository: cob::Store<Namespace = NodeId>,
{
-
    let mut table = term::Table::new(term::TableOptions {
-
        spacing: 3,
-
        ..term::TableOptions::default()
-
    });
    let repo = storage.repository(rid)?;
    let (_, head) = repo.head()?;
    let doc = repo.identity_doc()?;
@@ -281,8 +277,11 @@ where
        notifs.reverse();
    }

-
    for n in notifs {
-
        let n: Notification = n?;
+
    let table = notifs.into_iter().flat_map(|n| {
+
        let n: Notification = match n {
+
            Err(e) => return Some(Err(anyhow::Error::from(e))),
+
            Ok(n) => n,
+
        };

        let seen = if n.status.is_read() {
            term::Label::blank()
@@ -305,26 +304,33 @@ where
            state,
            name,
        } = match &n.kind {
-
            NotificationKind::Branch { name } => NotificationRow::branch(name, head, &n, &repo)?,
+
            NotificationKind::Branch { name } => match NotificationRow::branch(name, head, &n, &repo) {
+
                Err(e) => return Some(Err(e)),
+
                Ok(b) => b,
+
            },
            NotificationKind::Cob { typed_id } => {
                match NotificationRow::cob(typed_id, &n, &issues, &patches, &repo) {
                    Ok(Some(row)) => row,
-
                    Ok(None) => continue,
+
                    Ok(None) => return None,
                    Err(e) => {
                        log::error!(target: "cli", "Error loading notification for {typed_id}: {e}");
-
                        continue;
+
                        return None
                    }
                }
            }
            NotificationKind::Unknown { refname } => {
                if show_unknown {
-
                    NotificationRow::unknown(refname, &n, &repo)?
+
                    match NotificationRow::unknown(refname, &n, &repo) {
+
                        Err(e) => return Some(Err(e)),
+
                        Ok(u) => u,
+
                    }
                } else {
-
                    continue;
+
                    return None
                }
            }
        };
-
        table.push([
+

+
        Some(Ok([
            notification_id,
            seen,
            name.into(),
@@ -333,8 +339,12 @@ where
            state.into(),
            author,
            timestamp,
-
        ]);
-
    }
+
        ]))
+
    }).collect::<Result<term::Table<8, _>, anyhow::Error>>()?
+
    .with_opts(term::TableOptions {
+
        spacing: 3,
+
        ..term::TableOptions::default()
+
    });

    if table.is_empty() {
        Ok(None)
modified crates/radicle-cli/src/commands/patch/list.rs
@@ -14,6 +14,8 @@ use term::Element as _;
use crate::terminal as term;
use crate::terminal::patch as common;

+
use itertools::Itertools;
+

/// List patches.
pub fn run(
    filter: Option<&patch::Status>,
@@ -79,13 +81,14 @@ pub fn run(
        is_me.then(by_rev_time).then(by_id)
    });

-
    let mut errors = Vec::new();
-
    for (id, patch) in &mut all {
-
        match row(id, patch, repository, profile) {
-
            Ok(r) => table.push(r),
-
            Err(e) => errors.push((patch.title(), id, e.to_string())),
-
        }
-
    }
+
    let (rows, errors): (Vec<_>, Vec<_>) = all
+
        .iter()
+
        .map(|(id, patch)| {
+
            row(id, patch, repository, profile).map_err(|e| (patch.title(), id, e.to_string()))
+
        })
+
        .partition_result();
+

+
    table.extend(rows);
    table.print();

    if !errors.is_empty() {
modified crates/radicle-cli/src/commands/remote/list.rs
@@ -81,10 +81,9 @@ pub fn untracked<'a>(
}

pub fn print_tracked<'a>(tracked: impl Iterator<Item = &'a Tracked>) {
-
    let mut table = Table::default();
-
    for Tracked { direction, name } in tracked {
+
    Table::from_iter(tracked.into_iter().flat_map(|Tracked { direction, name }| {
        let Some(direction) = direction else {
-
            continue;
+
            return None;
        };

        let (dir, url) = match direction {
@@ -95,25 +94,24 @@ pub fn print_tracked<'a>(tracked: impl Iterator<Item = &'a Tracked>) {
            term::format::dim("(canonical upstream)".to_string()).italic(),
            |namespace| term::format::tertiary(namespace.to_string()),
        );
-
        table.push([
+
        Some([
            term::format::bold(name.clone()),
            description,
            term::format::parens(term::format::secondary(dir.to_owned())),
-
        ]);
-
    }
-
    table.print();
+
        ])
+
    }))
+
    .print();
}

pub fn print_untracked<'a>(untracked: impl Iterator<Item = &'a Untracked>) {
-
    let mut t = Table::default();
-
    for Untracked { remote, alias } in untracked {
-
        t.push([
+
    Table::from_iter(untracked.into_iter().map(|Untracked { remote, alias }| {
+
        [
            match alias {
                None => term::format::secondary("n/a".to_string()),
                Some(alias) => term::format::secondary(alias.to_string()),
            },
            term::format::highlight(Did::from(remote).to_string()),
-
        ])
-
    }
-
    t.print();
+
        ]
+
    }))
+
    .print();
}
modified crates/radicle-cli/src/terminal/patch.rs
@@ -306,18 +306,20 @@ pub fn get_update_message(

/// List the given commits in a table.
pub fn list_commits(commits: &[git::raw::Commit]) -> anyhow::Result<()> {
-
    let mut table = term::Table::default();
-

-
    for commit in commits {
-
        let message = commit
-
            .summary_bytes()
-
            .unwrap_or_else(|| commit.message_bytes());
-
        table.push([
-
            term::format::secondary(term::format::oid(commit.id()).into()),
-
            term::format::italic(String::from_utf8_lossy(message).to_string()),
-
        ]);
-
    }
-
    table.print();
+
    commits
+
        .iter()
+
        .map(|commit| {
+
            let message = commit
+
                .summary_bytes()
+
                .unwrap_or_else(|| commit.message_bytes());
+

+
            [
+
                term::format::secondary(term::format::oid(commit.id()).into()),
+
                term::format::italic(String::from_utf8_lossy(message).to_string()),
+
            ]
+
        })
+
        .collect::<term::Table<2, _>>()
+
        .print();

    Ok(())
}
modified crates/radicle-term/src/table.rs
@@ -78,6 +78,19 @@ impl<const W: usize, T> Default for Table<W, T> {
    }
}

+
impl<const W: usize, T: Cell> FromIterator<[T; W]> for Table<W, T> {
+
    fn from_iter<I: IntoIterator<Item = [T; W]>>(iter: I) -> Self {
+
        let mut table = Self::default();
+
        table.rows.extend(iter.into_iter().map(|row| {
+
            for (i, cell) in row.iter().enumerate() {
+
                table.widths[i] = table.widths[i].max(cell.width());
+
            }
+
            Row::Data(row)
+
        }));
+
        table
+
    }
+
}
+

impl<const W: usize, T: Cell + fmt::Debug + Send + Sync> Element for Table<W, T>
where
    T::Padded: Into<Line>,
@@ -169,6 +182,11 @@ impl<const W: usize, T: Cell> Table<W, T> {
        }
    }

+
    pub fn with_opts(mut self, opts: TableOptions) -> Self {
+
        self.opts = opts;
+
        self
+
    }
+

    pub fn size(&self, parent: Constraint) -> Size {
        self.outer(parent)
    }