diff --git a/radicle-cli/examples/rad-issue.md b/radicle-cli/examples/rad-issue.md index 191f9399..6ac27cea 100644 --- a/radicle-cli/examples/rad-issue.md +++ b/radicle-cli/examples/rad-issue.md @@ -17,7 +17,7 @@ The issue is now listed under our project. ``` $ rad issue list -2e8c1bf3fe0532a314778357c886608a966a34bd "flux capacitor underpowered" +2e8c1bf3fe0532a314778357c886608a966a34bd "flux capacitor underpowered" ❲unassigned❳ ``` Great! Now we've documented the issue for ourselves and others. diff --git a/radicle-cli/examples/rad-patch.md b/radicle-cli/examples/rad-patch.md index 104abe89..d55ed55e 100644 --- a/radicle-cli/examples/rad-patch.md +++ b/radicle-cli/examples/rad-patch.md @@ -58,10 +58,14 @@ Define power requirements 191a14e520f R0 3e674d1 (flux-capacitor-power) ahead 1, Nothing to show. $ rad patch show 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 - -Define power requirements - -See details. +╭────────────────────────────────────────────────────────────────────╮ +│ Title Define power requirements │ +│ Patch 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 │ +│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │ +│ Status open │ +│ │ +│ See details. │ +╰────────────────────────────────────────────────────────────────────╯ commit 3e674d1a1df90807e934f9ae5da2591dd6848a33 Author: radicle diff --git a/radicle-cli/examples/workflow/3-issues.md b/radicle-cli/examples/workflow/3-issues.md index c58b2e98..65c85f4b 100644 --- a/radicle-cli/examples/workflow/3-issues.md +++ b/radicle-cli/examples/workflow/3-issues.md @@ -17,7 +17,7 @@ The issue is now listed under our project. ``` $ rad issue list -b05e945bb63c11bf80320f4e26ad1d1f7c51f755 "flux capacitor underpowered" +b05e945bb63c11bf80320f4e26ad1d1f7c51f755 "flux capacitor underpowered" ❲unassigned❳ ``` Great! Now we've documented the issue for ourselves and others. diff --git a/radicle-cli/examples/workflow/4-patching-contributor.md b/radicle-cli/examples/workflow/4-patching-contributor.md index 97926857..dac34859 100644 --- a/radicle-cli/examples/workflow/4-patching-contributor.md +++ b/radicle-cli/examples/workflow/4-patching-contributor.md @@ -58,10 +58,14 @@ Define power requirements a07ef7743a3 R0 3e674d1 (flux-capacitor-power) ahead 1, Nothing to show. $ rad patch show a07ef7743a32a2e902672ea3526d1db6ee08108a - -Define power requirements - -See details. +╭────────────────────────────────────────────────────────────────────╮ +│ Title Define power requirements │ +│ Patch a07ef7743a32a2e902672ea3526d1db6ee08108a │ +│ Author did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │ +│ Status open │ +│ │ +│ See details. │ +╰────────────────────────────────────────────────────────────────────╯ commit 3e674d1a1df90807e934f9ae5da2591dd6848a33 Author: radicle diff --git a/radicle-cli/src/commands/id.rs b/radicle-cli/src/commands/id.rs index a5e450ee..d1914d45 100644 --- a/radicle-cli/src/commands/id.rs +++ b/radicle-cli/src/commands/id.rs @@ -10,6 +10,7 @@ use radicle_crypto::Verified; use crate::terminal as term; use crate::terminal::args::{Args, Error, Help}; +use crate::terminal::Element; use crate::terminal::Interactive; pub const HELP: Help = Help { @@ -403,7 +404,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { state, ]); } - t.render(); + t.print(); } Operation::Commit { id, rev } => { let mut proposal = proposals.get_mut(&id)?; diff --git a/radicle-cli/src/commands/issue.rs b/radicle-cli/src/commands/issue.rs index 4b1efca6..d80760e9 100644 --- a/radicle-cli/src/commands/issue.rs +++ b/radicle-cli/src/commands/issue.rs @@ -8,6 +8,7 @@ use radicle::prelude::Did; use crate::terminal as term; use crate::terminal::args::{Args, Error, Help}; +use crate::terminal::Element; use radicle::cob::common::{Reaction, Tag}; use radicle::cob::issue; @@ -314,10 +315,14 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { t.push([ id.to_string(), format!("{:?}", issue.title()), - assigned.to_string(), + if assigned.is_empty() { + String::from("❲unassigned❳") + } else { + assigned.to_string() + }, ]); } - t.render(); + t.print(); } Operation::Delete { id } => { issues.remove(&id, &signer)?; diff --git a/radicle-cli/src/commands/ls.rs b/radicle-cli/src/commands/ls.rs index 70c7c027..32a35fc9 100644 --- a/radicle-cli/src/commands/ls.rs +++ b/radicle-cli/src/commands/ls.rs @@ -4,6 +4,7 @@ use crate::terminal as term; use crate::terminal::args::{Args, Error, Help}; use radicle::storage::{ReadRepository, ReadStorage}; +use term::Element; pub const HELP: Help = Help { name: "ls", @@ -52,13 +53,13 @@ pub fn run(_options: Options, ctx: impl term::Context) -> anyhow::Result<()> { let Ok(proj) = repo.project_of(profile.id()) else { return }; let head = term::format::oid(head); table.push([ - term::format::bold(proj.name()), - term::format::tertiary(id.urn().as_str()), - term::format::secondary(head.as_str()), - term::format::italic(proj.description()), + term::format::bold(proj.name().to_owned()), + term::format::tertiary(id.urn()), + term::format::secondary(head), + term::format::italic(proj.description().to_owned()), ]); }); - table.render(); + table.print(); Ok(()) } diff --git a/radicle-cli/src/commands/node/routing.rs b/radicle-cli/src/commands/node/routing.rs index 8f6b2bce..d012f83b 100644 --- a/radicle-cli/src/commands/node/routing.rs +++ b/radicle-cli/src/commands/node/routing.rs @@ -1,17 +1,25 @@ use radicle::node; use crate::terminal as term; +use crate::terminal::Element; pub fn run(routing: &S) -> anyhow::Result<()> { let mut t = term::Table::new(term::table::TableOptions::default()); - t.push(["RID", "NID"]); - t.push(["---", "---"]); + 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("---")), + ]); for (id, node) in routing.entries()? { t.push([ - term::format::highlight(id).to_string(), - term::format::node(&node), + term::format::highlight(id.to_string()), + term::format::default(term::format::node(&node)), ]); } - t.render(); + t.print(); + Ok(()) } diff --git a/radicle-cli/src/commands/node/tracking.rs b/radicle-cli/src/commands/node/tracking.rs index d1f7272e..d2b93b1e 100644 --- a/radicle-cli/src/commands/node/tracking.rs +++ b/radicle-cli/src/commands/node/tracking.rs @@ -2,6 +2,7 @@ use radicle::node::tracking; use radicle::prelude::Did; use crate::terminal as term; +use term::Element; use super::TrackingMode; @@ -15,33 +16,55 @@ 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()); - t.push(["RID", "Scope", "Policy"]); - t.push(["---", "-----", "------"]); + 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("------")), + ]); for tracking::Repo { id, scope, policy } in store.repo_policies()? { + let id = id.to_string(); + let scope = scope.to_string(); + let policy = policy.to_string(); + t.push([ - term::format::highlight(id.to_string()), - term::format::secondary(scope.to_string()), - term::format::secondary(policy.to_string()), + term::format::highlight(id), + term::format::secondary(scope), + term::format::secondary(policy), ]) } - t.render(); + t.print(); + Ok(()) } fn print_nodes(store: &tracking::store::Config) -> anyhow::Result<()> { let mut t = term::Table::new(term::table::TableOptions::default()); - t.push(["DID", "Alias", "Policy"]); - t.push(["---", "-----", "------"]); + 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("------")), + ]); for tracking::Node { id, alias, policy } in store.node_policies()? { t.push([ term::format::highlight(Did::from(id).to_string()), match alias { - None => term::format::secondary("n/a".to_string()), + None => term::format::secondary(String::from("n/a")), Some(alias) => term::format::secondary(alias), }, term::format::secondary(policy.to_string()), ]); } - t.render(); + t.print(); + Ok(()) } diff --git a/radicle-cli/src/commands/patch/show.rs b/radicle-cli/src/commands/patch/show.rs index d700c50e..13cf2e19 100644 --- a/radicle-cli/src/commands/patch/show.rs +++ b/radicle-cli/src/commands/patch/show.rs @@ -6,6 +6,11 @@ use super::*; use radicle::cob::patch; use radicle::git; use radicle::storage::git::Repository; +use radicle_term::{ + label, + table::{Table, TableOptions}, + Element, Paint, VStack, +}; use crate::terminal as term; @@ -40,15 +45,40 @@ pub fn run( anyhow::bail!("Patch `{patch_id}` not found"); }; - term::blank(); - term::info!("{}", term::format::bold(patch.title())); - term::blank(); + let mut attrs = Table::<2, Paint>::new(TableOptions { + spacing: 2, + ..TableOptions::default() + }); + attrs.push([ + term::format::tertiary("Title".to_owned()), + term::format::bold(patch.title().to_owned()), + ]); + attrs.push([ + term::format::tertiary("Patch".to_owned()), + term::format::default(patch_id.to_string()), + ]); + attrs.push([ + term::format::tertiary("Author".to_owned()), + term::format::default(patch.author().id().to_string()), + ]); + attrs.push([ + term::format::tertiary("Status".to_owned()), + term::format::default(patch.state().to_string()), + ]); let description = patch.description().trim(); - if !description.is_empty() { - term::blob(description); - term::blank(); - } + let meta = VStack::default() + .border(Some(term::ansi::Color::Blue)) + .child(attrs) + .blank() + .children(if !description.is_empty() { + Some(label(term::format::dim(description))) + } else { + None + }); + + meta.print(); + term::blank(); show_patch_diff(&patch, storage, workdir)?; term::blank(); diff --git a/radicle-cli/src/commands/self.rs b/radicle-cli/src/commands/self.rs index 057c6923..e15e9ab2 100644 --- a/radicle-cli/src/commands/self.rs +++ b/radicle-cli/src/commands/self.rs @@ -5,6 +5,7 @@ use radicle::Profile; use crate::terminal as term; use crate::terminal::args::{Args, Error, Help}; +use crate::terminal::Element as _; pub const HELP: Help = Help { name: "self", @@ -143,7 +144,7 @@ fn all(profile: &Profile) -> anyhow::Result<()> { term::format::tertiary(node_path.join("radicle.sock").display()).to_string(), ]); - table.render(); + table.print(); Ok(()) } diff --git a/radicle-cli/src/terminal/patch.rs b/radicle-cli/src/terminal/patch.rs index 5af6c9f3..e1c2e464 100644 --- a/radicle-cli/src/terminal/patch.rs +++ b/radicle-cli/src/terminal/patch.rs @@ -1,6 +1,7 @@ use radicle::git; use crate::terminal as term; +use crate::terminal::Element; /// The user supplied `Patch` description. #[derive(Clone, Debug, PartialEq, Eq)] @@ -59,7 +60,7 @@ pub fn list_commits(commits: &[git::raw::Commit]) -> anyhow::Result<()> { term::format::italic(String::from_utf8_lossy(message).to_string()), ]); } - table.render(); + table.print(); Ok(()) } diff --git a/radicle-term/src/ansi/paint.rs b/radicle-term/src/ansi/paint.rs index ec4f0a96..ad78e9ed 100644 --- a/radicle-term/src/ansi/paint.rs +++ b/radicle-term/src/ansi/paint.rs @@ -38,6 +38,15 @@ impl From<&str> for Paint { } } +impl From> for Paint { + fn from(paint: Paint<&str>) -> Self { + Self { + item: paint.item.to_owned(), + style: paint.style, + } + } +} + impl Paint { /// Constructs a new `Paint` structure encapsulating `item` with no set /// styling. diff --git a/radicle-term/src/element.rs b/radicle-term/src/element.rs new file mode 100644 index 00000000..88cea2b7 --- /dev/null +++ b/radicle-term/src/element.rs @@ -0,0 +1,187 @@ +use std::fmt; +use std::ops::Deref; +use std::vec; + +use crate::cell::Cell; +use crate::Label; + +/// A text element that has a size and can be rendered to the terminal. +pub trait Element: fmt::Debug { + /// Get the size of the element, in rows and columns. + fn size(&self) -> Size; + + #[must_use] + /// Render the element as lines of text that can be printed. + fn render(&self) -> Vec; + + /// Get the number of columns occupied by this element. + fn columns(&self) -> usize { + self.size().cols + } + + /// Get the number of rows occupied by this element. + fn rows(&self) -> usize { + self.size().rows + } + + /// Print this element to stdout. + fn print(&self) { + for line in self.render() { + println!("{line}"); + } + } + + #[must_use] + /// Return a string representation of this element. + fn display(&self) -> String { + let mut out = String::new(); + for line in self.render() { + out.extend(line.into_iter().map(|l| l.to_string())); + out.push('\n'); + } + out + } +} + +impl Element for &T { + fn size(&self) -> Size { + self.deref().size() + } + + fn render(&self) -> Vec { + self.deref().render() + } + + fn print(&self) { + self.deref().print() + } +} + +/// A line of text that has styling and can be displayed. +#[derive(Clone, Default, Debug)] +pub struct Line { + items: Vec