Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Switch to published Radicle crates
Erik Kundt committed 2 years ago
commit 7b0692df517ea44f8507a2c4c0c3f03fa6232047
parent 682400f7cb4102431962a87fb8946d53ab103586
8 files changed +121 -329
modified Cargo.lock
@@ -85,36 +85,6 @@ dependencies = [
]

[[package]]
-
name = "anstream"
-
version = "0.3.2"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163"
-
dependencies = [
-
 "anstyle",
-
 "anstyle-parse",
-
 "anstyle-query",
-
 "anstyle-wincon",
-
 "colorchoice",
-
 "is-terminal",
-
 "utf8parse",
-
]
-

-
[[package]]
-
name = "anstyle"
-
version = "1.0.1"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd"
-

-
[[package]]
-
name = "anstyle-parse"
-
version = "0.2.1"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
-
dependencies = [
-
 "utf8parse",
-
]
-

-
[[package]]
name = "anstyle-query"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -124,16 +94,6 @@ dependencies = [
]

[[package]]
-
name = "anstyle-wincon"
-
version = "1.0.1"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
-
dependencies = [
-
 "anstyle",
-
 "windows-sys",
-
]
-

-
[[package]]
name = "anyhow"
version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -176,12 +136,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"

[[package]]
-
name = "base64"
-
version = "0.21.2"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
-

-
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -298,23 +252,6 @@ dependencies = [
]

[[package]]
-
name = "colorchoice"
-
version = "1.0.0"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
-

-
[[package]]
-
name = "colored"
-
version = "1.9.4"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "5a5f741c91823341bebf717d4c71bda820630ce065443b58bd1b7451af008355"
-
dependencies = [
-
 "is-terminal",
-
 "lazy_static",
-
 "winapi",
-
]
-

-
[[package]]
name = "const-oid"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -485,12 +422,6 @@ dependencies = [
]

[[package]]
-
name = "diff"
-
version = "0.1.13"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
-

-
[[package]]
name = "digest"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -620,18 +551,6 @@ dependencies = [
]

[[package]]
-
name = "escargot"
-
version = "0.5.8"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "768064bd3a0e2bedcba91dc87ace90beea91acc41b6a01a3ca8e9aa8827461bf"
-
dependencies = [
-
 "log",
-
 "once_cell",
-
 "serde",
-
 "serde_json",
-
]
-

-
[[package]]
name = "fastrand"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -777,23 +696,11 @@ dependencies = [

[[package]]
name = "hashbrown"
-
version = "0.12.3"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
-

-
[[package]]
-
name = "hashbrown"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"

[[package]]
-
name = "hermit-abi"
-
version = "0.3.2"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
-

-
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -837,22 +744,12 @@ dependencies = [

[[package]]
name = "indexmap"
-
version = "1.9.3"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
-
dependencies = [
-
 "autocfg",
-
 "hashbrown 0.12.3",
-
]
-

-
[[package]]
-
name = "indexmap"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
 "equivalent",
-
 "hashbrown 0.14.0",
+
 "hashbrown",
]

[[package]]
@@ -891,17 +788,6 @@ dependencies = [
]

[[package]]
-
name = "is-terminal"
-
version = "0.4.9"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
-
dependencies = [
-
 "hermit-abi",
-
 "rustix",
-
 "windows-sys",
-
]
-

-
[[package]]
name = "isolang"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -935,17 +821,6 @@ dependencies = [
]

[[package]]
-
name = "json-color"
-
version = "0.7.1"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "e6dc8c55175cad7234a98cc3e31ba3009e276800271692ed3ad2c2f1c574b6e8"
-
dependencies = [
-
 "colored",
-
 "serde",
-
 "serde_json",
-
]
-

-
[[package]]
name = "keccak"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1029,12 +904,6 @@ dependencies = [
]

[[package]]
-
name = "linked-hash-map"
-
version = "0.5.6"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
-

-
[[package]]
name = "linux-raw-sys"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1106,12 +975,6 @@ dependencies = [
]

[[package]]
-
name = "normalize-line-endings"
-
version = "0.3.0"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
-

-
[[package]]
name = "num-bigint-dig"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1276,16 +1139,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"

[[package]]
-
name = "pretty_assertions"
-
version = "1.4.0"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
-
dependencies = [
-
 "diff",
-
 "yansi",
-
]
-

-
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1330,6 +1183,8 @@ dependencies = [
[[package]]
name = "radicle"
version = "0.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b56a9f71126b44b3768009f40a4c3f1f75325e7d2eb8ba95c5432ff844df6af3"
dependencies = [
 "amplify",
 "crossbeam-channel",
@@ -1355,49 +1210,10 @@ dependencies = [
]

[[package]]
-
name = "radicle-cli"
-
version = "0.8.0"
-
dependencies = [
-
 "anyhow",
-
 "chrono",
-
 "git-ref-format",
-
 "json-color",
-
 "lexopt",
-
 "localtime",
-
 "log",
-
 "nonempty 0.8.1",
-
 "radicle",
-
 "radicle-cli-test",
-
 "radicle-cob",
-
 "radicle-crypto",
-
 "radicle-git-ext",
-
 "radicle-surf",
-
 "radicle-term",
-
 "serde",
-
 "serde_json",
-
 "serde_yaml",
-
 "similar",
-
 "thiserror",
-
 "timeago 0.3.1",
-
 "ureq",
-
 "zeroize",
-
]
-

-
[[package]]
-
name = "radicle-cli-test"
-
version = "0.1.1"
-
dependencies = [
-
 "escargot",
-
 "log",
-
 "pretty_assertions",
-
 "shlex",
-
 "snapbox",
-
 "thiserror",
-
]
-

-
[[package]]
name = "radicle-cob"
-
version = "0.1.0"
+
version = "0.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cf0ce7bd1c9f962813b0319faabfa19deae4dccac98154be050bfa21a1e121b6"
dependencies = [
 "fastrand 1.9.0",
 "git2",
@@ -1413,7 +1229,9 @@ dependencies = [

[[package]]
name = "radicle-crypto"
-
version = "0.1.0"
+
version = "0.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cc9f1a3bc7e5e27099a8c9a2306dc8286c34c7db4ab0dfabf88e1b0d04d56a03"
dependencies = [
 "amplify",
 "cyphernet",
@@ -1430,7 +1248,9 @@ dependencies = [

[[package]]
name = "radicle-dag"
-
version = "0.1.0"
+
version = "0.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cf2f9da8f20e47901022d5a58c0ed1953d0b1dcdfbbb1ac8eee28b93d888a258"
dependencies = [
 "fastrand 1.9.0",
]
@@ -1452,6 +1272,8 @@ dependencies = [
[[package]]
name = "radicle-ssh"
version = "0.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c4636c5c5d0610143ffbcf26008cdc1b7149644116acceadec62b749f8cebaaf"
dependencies = [
 "byteorder",
 "log",
@@ -1472,7 +1294,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b801a32980495a643bd380cc88f2074cf978862efbc8c085d5bd9f3be4caafd6"
dependencies = [
 "anyhow",
-
 "base64 0.13.1",
+
 "base64",
 "flate2",
 "git2",
 "log",
@@ -1486,6 +1308,8 @@ dependencies = [
[[package]]
name = "radicle-term"
version = "0.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "820fb6595467096019b19b10b0469b1b09d0acbe908a230bab8af7d381151fe7"
dependencies = [
 "anstyle-query",
 "anyhow",
@@ -1505,11 +1329,10 @@ dependencies = [
 "anyhow",
 "lexopt",
 "radicle",
-
 "radicle-cli",
 "radicle-surf",
 "radicle-term",
 "textwrap 0.16.0",
-
 "timeago 0.4.1",
+
 "timeago",
 "tui-realm-stdlib",
 "tuirealm",
]
@@ -1731,25 +1554,13 @@ version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c"
dependencies = [
-
 "indexmap 2.0.0",
+
 "indexmap",
 "itoa",
 "ryu",
 "serde",
]

[[package]]
-
name = "serde_yaml"
-
version = "0.8.26"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b"
-
dependencies = [
-
 "indexmap 1.9.3",
-
 "ryu",
-
 "serde",
-
 "yaml-rust",
-
]
-

-
[[package]]
name = "sha2"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1784,12 +1595,6 @@ dependencies = [
]

[[package]]
-
name = "shlex"
-
version = "1.1.0"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
-

-
[[package]]
name = "signature"
version = "1.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1800,12 +1605,6 @@ dependencies = [
]

[[package]]
-
name = "similar"
-
version = "2.2.1"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf"
-

-
[[package]]
name = "siphasher"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1824,28 +1623,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"

[[package]]
-
name = "snapbox"
-
version = "0.4.11"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "f6bccd62078347f89a914e3004d94582e13824d4e3d8a816317862884c423835"
-
dependencies = [
-
 "anstream",
-
 "anstyle",
-
 "normalize-line-endings",
-
 "similar",
-
 "snapbox-macros",
-
]
-

-
[[package]]
-
name = "snapbox-macros"
-
version = "0.3.4"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "eaaf09df9f0eeae82be96290918520214530e738a7fe5a351b0f24cf77c0ca31"
-
dependencies = [
-
 "anstream",
-
]
-

-
[[package]]
name = "socks5-client"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2064,12 +1841,6 @@ dependencies = [

[[package]]
name = "timeago"
-
version = "0.3.1"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "6ec32dde57efb15c035ac074118d7f32820451395f28cb0524a01d4e94983b26"
-

-
[[package]]
-
name = "timeago"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5082dc942361cdfb74eab98bf995762d6015e5bb3a20bf7c5c71213778b4fcb4"
@@ -2188,20 +1959,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"

[[package]]
-
name = "ureq"
-
version = "2.7.1"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "0b11c96ac7ee530603dcdf68ed1557050f374ce55a5a07193ebf8cbc9f8927e9"
-
dependencies = [
-
 "base64 0.21.2",
-
 "log",
-
 "once_cell",
-
 "serde",
-
 "serde_json",
-
 "url",
-
]
-

-
[[package]]
name = "url"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2213,12 +1970,6 @@ dependencies = [
]

[[package]]
-
name = "utf8parse"
-
version = "0.2.1"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
-

-
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2409,21 +2160,6 @@ dependencies = [
]

[[package]]
-
name = "yaml-rust"
-
version = "0.4.5"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
-
dependencies = [
-
 "linked-hash-map",
-
]
-

-
[[package]]
-
name = "yansi"
-
version = "0.5.1"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
-

-
[[package]]
name = "zeroize"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
modified Cargo.toml
@@ -13,20 +13,22 @@ path = "src/main.rs"
[dependencies]
anyhow = { version = "1" }
lexopt = { version = "0.2" }
+
radicle = { version = "0.2.0" }
+
radicle-term = { version = "0.1.0" }
radicle-surf = { version = "0.14.0" }
timeago = { version = "0.4.1" }
textwrap = { version = "0.16.0" }
tuirealm = { version = "1.8.0", default-features = false, features = [ "with-termion" ] }
tui-realm-stdlib = { version = "1.2.0", default-features = false, features = [ "with-termion" ] }

-
[dependencies.radicle]
-
version = "0"
-
path = "../heartwood/radicle"
+
# [dependencies.radicle]
+
# version = "0"
+
# path = "../heartwood/radicle"

-
[dependencies.radicle-cli]
-
version = "0"
-
path = "../heartwood/radicle-cli"
+
# [dependencies.radicle-cli]
+
# version = "0"
+
# path = "../heartwood/radicle-cli"

-
[dependencies.radicle-term]
-
version = "0"
-
path = "../heartwood/radicle-term"
+
# [dependencies.radicle-term]
+
# version = "0"
+
# path = "../heartwood/radicle-term"
modified src/main.rs
@@ -2,12 +2,15 @@ use std::process;

use anyhow::{anyhow, Context};

-
use radicle::storage::ReadStorage;
-

-
use radicle_cli as cli;
+
use radicle::{
+
    crypto::ssh::keystore::MemorySigner, prelude::Signer, profile::env::RAD_PASSPHRASE, Profile,
+
};
use radicle_term as term;
use radicle_tui::Window;

+
use radicle::storage::ReadStorage;
+
use term::{passphrase, spinner};
+

mod app;

pub const NAME: &str = "radicle-tui";
@@ -53,15 +56,39 @@ impl Options {
    }
}

+
/// Get the default profile. Fails if there is no profile.
+
pub fn profile() -> Result<Profile, anyhow::Error> {
+
    match Profile::load() {
+
        Ok(profile) => Ok(profile),
+
        Err(_) => Err(anyhow::anyhow!(
+
            "Could not load radicle profile. To setup your radicle profile, run `rad auth`."
+
        )),
+
    }
+
}
+

+
/// Get the signer. First we try getting it from ssh-agent, otherwise we prompt the user.
+
pub fn signer(profile: &Profile) -> anyhow::Result<Box<dyn Signer>> {
+
    if let Ok(signer) = profile.signer() {
+
        return Ok(signer);
+
    }
+
    let passphrase = passphrase(RAD_PASSPHRASE)?;
+
    let spinner = spinner("Unsealing key...");
+
    let signer = MemorySigner::load(&profile.keystore, Some(passphrase))?;
+

+
    spinner.finish();
+

+
    Ok(signer.boxed())
+
}
+

fn execute() -> anyhow::Result<()> {
    let _ = Options::from_env()?;

    let (_, id) = radicle::rad::cwd()
        .map_err(|_| anyhow!("this command must be run in the context of a project"))?;

-
    let profile = cli::terminal::profile()?;
+
    let profile = profile()?;

-
    let signer = cli::terminal::signer(&profile)?;
+
    let signer = signer(&profile)?;
    let storage = &profile.storage;

    let payload = storage
modified src/ui/cob.rs
@@ -1,7 +1,9 @@
+
pub mod format;
+

use radicle_surf;

-
use cli::terminal::format;
-
use radicle_cli as cli;
+
// use cli::terminal::format;
+
// use radicle_cli as cli;

use radicle::prelude::Did;
use radicle::storage::git::Repository;
@@ -10,7 +12,7 @@ use radicle::Profile;

use radicle::cob::issue::{Issue, IssueId, State as IssueState};
use radicle::cob::patch::{Patch, PatchId, State as PatchState};
-
use radicle::cob::{Tag, Timestamp};
+
use radicle::cob::{Label, Timestamp};

use tuirealm::props::{Color, Style};
use tuirealm::tui::text::{Span, Spans};
@@ -142,7 +144,7 @@ impl TableItem<8> for PatchItem {
        let author = Cell::from(format_author(&self.author.did, self.author.is_you))
            .style(Style::default().fg(theme.colors.browser_list_author));

-
        let head = Cell::from(format::oid(self.head).item)
+
        let head = Cell::from(format::oid(self.head))
            .style(Style::default().fg(theme.colors.browser_patch_list_head));

        let added = Cell::from(format!("{}", self.added))
@@ -151,7 +153,7 @@ impl TableItem<8> for PatchItem {
        let removed = Cell::from(format!("{}", self.removed))
            .style(Style::default().fg(theme.colors.browser_patch_list_removed));

-
        let updated = Cell::from(format::timestamp(&self.timestamp).to_string())
+
        let updated = Cell::from(format::timestamp(&self.timestamp))
            .style(Style::default().fg(theme.colors.browser_list_timestamp));

        [state, id, title, author, head, added, removed, updated]
@@ -172,8 +174,8 @@ pub struct IssueItem {
    title: String,
    /// Issue author.
    author: AuthorItem,
-
    /// Issue tags.
-
    tags: Vec<Tag>,
+
    /// Issue labels.
+
    labels: Vec<Label>,
    /// Issue assignees.
    assignees: Vec<AuthorItem>,
    /// Time when issue was opened.
@@ -197,8 +199,8 @@ impl IssueItem {
        &self.author
    }

-
    pub fn tags(&self) -> &Vec<Tag> {
-
        &self.tags
+
    pub fn labels(&self) -> &Vec<Label> {
+
        &self.labels
    }

    pub fn assignees(&self) -> &Vec<AuthorItem> {
@@ -222,12 +224,12 @@ impl From<(&Profile, &Repository, IssueId, Issue)> for IssueItem {
                did: issue.author().id,
                is_you: *issue.author().id == *profile.did(),
            },
-
            tags: issue.tags().cloned().collect(),
+
            labels: issue.labels().cloned().collect(),
            assignees: issue
                .assigned()
                .map(|did| AuthorItem {
-
                    did,
-
                    is_you: did == profile.did(),
+
                    did: *did,
+
                    is_you: *did == profile.did(),
                })
                .collect::<Vec<_>>(),
            timestamp: issue.timestamp(),
@@ -249,8 +251,8 @@ impl TableItem<7> for IssueItem {
        let author = Cell::from(format_author(&self.author.did, self.author.is_you))
            .style(Style::default().fg(theme.colors.browser_list_author));

-
        let tags = Cell::from(format_tags(&self.tags))
-
            .style(Style::default().fg(theme.colors.browser_list_tags));
+
        let labels = Cell::from(format_labels(&self.labels))
+
            .style(Style::default().fg(theme.colors.browser_list_labels));

        let assignees = self
            .assignees
@@ -260,10 +262,10 @@ impl TableItem<7> for IssueItem {
        let assignees = Cell::from(format_assignees(&assignees))
            .style(Style::default().fg(theme.colors.browser_list_author));

-
        let opened = Cell::from(format::timestamp(&self.timestamp).to_string())
+
        let opened = Cell::from(format::timestamp(&self.timestamp))
            .style(Style::default().fg(theme.colors.browser_list_timestamp));

-
        [state, id, title, author, tags, assignees, opened]
+
        [state, id, title, author, labels, assignees, opened]
    }
}

@@ -289,7 +291,7 @@ impl ListItem for IssueItem {
                    Style::default().fg(theme.colors.property_divider_fg),
                ),
                Span::styled(
-
                    format::timestamp(&self.timestamp).to_string(),
+
                    format::timestamp(&self.timestamp),
                    Style::default().fg(theme.colors.browser_list_timestamp),
                ),
            ]),
@@ -320,7 +322,7 @@ pub fn format_author(did: &Did, is_you: bool) -> String {
    if is_you {
        format!("{} (you)", format::did(did))
    } else {
-
        format!("{}", format::did(did))
+
        format::did(did)
    }
}

@@ -331,14 +333,14 @@ pub fn format_issue_state(state: &IssueState) -> (String, Color) {
    }
}

-
pub fn format_tags(tags: &[Tag]) -> String {
+
pub fn format_labels(labels: &[Label]) -> String {
    let mut output = String::new();
-
    let mut tags = tags.iter().peekable();
+
    let mut labels = labels.iter().peekable();

-
    while let Some(tag) = tags.next() {
-
        output.push_str(&tag.to_string());
+
    while let Some(label) = labels.next() {
+
        output.push_str(&label.to_string());

-
        if tags.peek().is_some() {
+
        if labels.peek().is_some() {
            output.push(',');
        }
    }
added src/ui/cob/format.rs
@@ -0,0 +1,27 @@
+
use radicle::cob::{ObjectId, Timestamp};
+
use radicle::prelude::Did;
+

+
/// Format a git Oid.
+
pub fn oid(oid: impl Into<radicle::git::Oid>) -> String {
+
    format!("{:.7}", oid.into())
+
}
+

+
/// Format a COB id.
+
pub fn cob(id: &ObjectId) -> String {
+
    format!("{:.7}", id.to_string())
+
}
+

+
/// Format a DID.
+
pub fn did(did: &Did) -> String {
+
    let nid = did.as_key().to_human();
+
    format!("{}…{}", &nid[..7], &nid[nid.len() - 7..])
+
}
+

+
/// Format a timestamp.
+
pub fn timestamp(time: &Timestamp) -> String {
+
    let fmt = timeago::Formatter::new();
+
    let now = Timestamp::now();
+
    let duration = std::time::Duration::from_secs(now.as_secs() - time.as_secs());
+

+
    fmt.convert(duration)
+
}
modified src/ui/theme.rs
@@ -24,7 +24,7 @@ pub struct Colors {
    pub browser_list_title: Color,
    pub browser_list_description: Color,
    pub browser_list_author: Color,
-
    pub browser_list_tags: Color,
+
    pub browser_list_labels: Color,
    pub browser_list_comments: Color,
    pub browser_list_timestamp: Color,
    pub browser_patch_list_head: Color,
@@ -94,7 +94,7 @@ pub fn default_dark() -> Theme {
            browser_list_title: COLOR_DEFAULT_FG,
            browser_list_description: COLOR_DEFAULT_DARK,
            browser_list_author: Color::Gray,
-
            browser_list_tags: Color::LightBlue,
+
            browser_list_labels: Color::LightBlue,
            browser_list_comments: COLOR_DEFAULT_DARK_FG,
            browser_list_timestamp: COLOR_DEFAULT_DARK,
            browser_patch_list_head: Color::LightBlue,
modified src/ui/widget/issue.rs
@@ -106,10 +106,10 @@ impl IssueHeader {
            common::label(&id.to_string()).foreground(theme.colors.browser_list_description),
        );

-
        let tags = Property::new(
+
        let labels = Property::new(
            common::label("Tags").foreground(theme.colors.property_name_fg),
-
            common::label(&cob::format_tags(item.tags()))
-
                .foreground(theme.colors.browser_list_tags),
+
            common::label(&cob::format_labels(item.labels()))
+
                .foreground(theme.colors.browser_list_labels),
        );

        let assignees = Property::new(
@@ -135,7 +135,7 @@ impl IssueHeader {
                Widget::new(title),
                Widget::new(issue_id),
                Widget::new(author),
-
                Widget::new(tags),
+
                Widget::new(labels),
                Widget::new(assignees),
                Widget::new(state),
            ],
modified src/ui/widget/patch.rs
@@ -1,7 +1,5 @@
use radicle::cob::patch::{Patch, PatchId};

-
use radicle_cli::terminal::format;
-

use tuirealm::command::{Cmd, CmdResult};
use tuirealm::tui::layout::Rect;
use tuirealm::{AttrValue, Attribute, Frame, MockComponent, Props, State};
@@ -108,7 +106,7 @@ pub fn context(context: &Context, theme: &Theme, patch: (PatchId, Patch)) -> Wid
    let (_, rev) = patch.latest();
    let is_you = *patch.author().id() == context.profile().did();

-
    let id = format::cob(&id);
+
    let id = cob::format::cob(&id);
    let title = patch.title();
    let author = cob::format_author(patch.author().id(), is_you);
    let comments = rev.discussion().len();