radicle-cli 0.20.0

Radicle CLI
Documentation
mod args;

use radicle::node::{policy, Alias, AliasStore, Handle, NodeId};
use radicle::{prelude::*, Node};
use radicle_term::{Element as _, Paint, Table};

use crate::terminal as term;

pub use args::Args;
use args::Operation;

pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
    let profile = ctx.profile()?;
    let mut node = radicle::Node::new(profile.socket());

    match Operation::from(args) {
        Operation::Follow { nid, alias, .. } => follow(nid, alias, &mut node, &profile)?,
        Operation::List { alias, .. } => following(&profile, alias)?,
    }

    Ok(())
}

pub fn follow(
    nid: NodeId,
    alias: Option<Alias>,
    node: &mut Node,
    profile: &Profile,
) -> Result<(), anyhow::Error> {
    let followed = match node.follow(nid, alias.clone()) {
        Ok(updated) => updated,
        Err(e) if e.is_connection_err() => {
            let mut config = profile.policies_mut()?;
            config.follow(&nid, alias.as_ref())?
        }
        Err(e) => return Err(e.into()),
    };
    let outcome = if followed { "updated" } else { "exists" };

    if let Some(alias) = alias {
        term::success!(
            "Follow policy {outcome} for {} ({alias})",
            term::format::tertiary(nid),
        );
    } else {
        term::success!(
            "Follow policy {outcome} for {}",
            term::format::tertiary(nid),
        );
    }

    Ok(())
}

pub fn following(profile: &Profile, alias: Option<Alias>) -> anyhow::Result<()> {
    let store = profile.policies()?;
    let aliases = profile.aliases();
    let mut t = term::Table::new(term::table::TableOptions::bordered());
    t.header([
        term::format::default(String::from("DID")),
        term::format::default(String::from("Alias")),
        term::format::default(String::from("Policy")),
    ]);
    t.divider();
    push_policies(&mut t, &aliases, store.follow_policies()?, &alias);
    t.print();
    Ok(())
}

fn push_policies(
    t: &mut Table<3, Paint<String>>,
    aliases: &impl AliasStore,
    policies: impl Iterator<Item = Result<policy::FollowPolicy, policy::store::Error>>,
    filter: &Option<Alias>,
) {
    for policy in policies {
        match policy {
            Ok(policy::FollowPolicy {
                nid: id,
                alias,
                policy,
            }) => {
                if match (filter, &alias) {
                    (None, _) => false,
                    (Some(filter), Some(alias)) => *filter != *alias,
                    (Some(_), None) => true,
                } {
                    continue;
                }

                t.push([
                    term::format::highlight(Did::from(id).to_string()),
                    match alias {
                        None => term::format::secondary(fallback_alias(&id, aliases)),
                        Some(alias) => term::format::secondary(alias.to_string()),
                    },
                    term::format::secondary(policy.to_string()),
                ]);
            }
            Err(err) => {
                term::error(format!("Failed to read a follow policy: {err}"));
            }
        }
    }
}

fn fallback_alias(nid: &PublicKey, aliases: &impl AliasStore) -> String {
    aliases
        .alias(nid)
        .map_or("n/a".to_string(), |alias| alias.to_string())
}