Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
cli/inbox: Use clap
Merged did:key:z6MkkfM3...sVz5 opened 6 months ago
4 files changed +227 -218 5741bafa 9bcdd353
modified crates/radicle-cli/src/commands/help.rs
@@ -74,7 +74,10 @@ const COMMANDS: &[CommandItem] = &[
        name: "init",
        about: crate::commands::init::ABOUT,
    },
-
    CommandItem::Lexopt(crate::commands::inbox::HELP),
+
    CommandItem::Clap {
+
        name: "inbox",
+
        about: crate::commands::inbox::ABOUT,
+
    },
    CommandItem::Lexopt(crate::commands::inspect::HELP),
    CommandItem::Clap {
        name: "issue",
modified crates/radicle-cli/src/commands/inbox.rs
@@ -1,4 +1,8 @@
-
use std::ffi::OsString;
+
mod args;
+

+
pub use args::Args;
+
pub(crate) use args::ABOUT;
+

use std::path::Path;
use std::process;

@@ -20,210 +24,67 @@ use radicle::{cob, git, Storage};
use term::Element as _;

use crate::terminal as term;
-
use crate::terminal::args;
-
use crate::terminal::args::{Args, Error, Help};
-

-
pub const HELP: Help = Help {
-
    name: "inbox",
-
    description: "Manage your Radicle notifications",
-
    version: env!("RADICLE_VERSION"),
-
    usage: r#"
-
Usage
-

-
    rad inbox [<option>...]
-
    rad inbox list [<option>...]
-
    rad inbox show <id> [<option>...]
-
    rad inbox clear <id...> [<option>...]
-

-
    By default, this command lists all items in your inbox.
-
    If your working directory is a Radicle repository, it only shows item
-
    belonging to this repository, unless `--all` is used.
-

-
    The `rad inbox show` command takes a notification ID (which can be found in
-
    the `list` command) and displays the information related to that
-
    notification. This will mark the notification as read.
-

-
    The `rad inbox clear` command will delete all notifications by their passed id
-
    or all notifications if no ids were passed.
-

-
Options
-

-
    --all                Operate on all repositories
-
    --repo <rid>         Operate on the given repository (default: rad .)
-
    --sort-by <field>    Sort by `id` or `timestamp` (default: timestamp)
-
    --reverse, -r        Reverse the list
-
    --show-unknown       Show any updates that were not recognized
-
    --help               Print help
-
"#,
-
};
-

-
#[derive(Debug, Default, PartialEq, Eq)]
-
enum Operation {
-
    #[default]
-
    List,
-
    Show,
-
    Clear,
-
}
-

-
#[derive(Default, Debug)]
-
enum Mode {
-
    #[default]
-
    Contextual,
-
    All,
-
    ById(Vec<NotificationId>),
-
    ByRepo(RepoId),
-
}
-

-
#[derive(Clone, Copy, Debug)]
-
struct SortBy {
-
    reverse: bool,
-
    field: &'static str,
-
}
+
use args::{Command, SortBy};

-
pub struct Options {
-
    op: Operation,
-
    mode: Mode,
-
    sort_by: SortBy,
-
    show_unknown: bool,
-
}
-

-
impl Args for Options {
-
    fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
-
        use lexopt::prelude::*;
-

-
        let mut parser = lexopt::Parser::from_args(args);
-
        let mut op: Option<Operation> = None;
-
        let mut mode = None;
-
        let mut ids = Vec::new();
-
        let mut reverse = None;
-
        let mut field = None;
-
        let mut show_unknown = false;
-

-
        while let Some(arg) = parser.next()? {
-
            match arg {
-
                Long("help") | Short('h') => {
-
                    return Err(Error::Help.into());
-
                }
-
                Long("all") | Short('a') if mode.is_none() => {
-
                    mode = Some(Mode::All);
-
                }
-
                Long("reverse") | Short('r') => {
-
                    reverse = Some(true);
-
                }
-
                Long("show-unknown") => {
-
                    show_unknown = true;
-
                }
-
                Long("sort-by") => {
-
                    let val = parser.value()?;
-

-
                    match term::args::string(&val).as_str() {
-
                        "timestamp" => field = Some("timestamp"),
-
                        "id" => field = Some("rowid"),
-
                        other => {
-
                            return Err(anyhow!(
-
                                "unknown sorting field `{other}`, see `rad inbox --help`"
-
                            ))
-
                        }
-
                    }
-
                }
-
                Long("repo") if mode.is_none() => {
-
                    let val = parser.value()?;
-
                    let repo = args::rid(&val)?;
-

-
                    mode = Some(Mode::ByRepo(repo));
-
                }
-
                Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
-
                    "list" => op = Some(Operation::List),
-
                    "show" => op = Some(Operation::Show),
-
                    "clear" => op = Some(Operation::Clear),
-
                    cmd => return Err(anyhow!("unknown command `{cmd}`, see `rad inbox --help`")),
-
                },
-
                Value(val) if op.is_some() && mode.is_none() => {
-
                    let id = term::args::number(&val)? as NotificationId;
-
                    ids.push(id);
-
                }
-
                _ => anyhow::bail!(arg.unexpected()),
-
            }
-
        }
-
        let mode = if ids.is_empty() {
-
            mode.unwrap_or_default()
-
        } else {
-
            Mode::ById(ids)
-
        };
-
        let op = op.unwrap_or_default();
-

-
        let sort_by = if let Some(field) = field {
-
            SortBy {
-
                field,
-
                reverse: reverse.unwrap_or(false),
-
            }
-
        } else {
-
            SortBy {
-
                field: "timestamp",
-
                reverse: true,
-
            }
-
        };
-

-
        Ok((
-
            Options {
-
                op,
-
                mode,
-
                sort_by,
-
                show_unknown,
-
            },
-
            vec![],
-
        ))
-
    }
-
}
-

-
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
+
pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
    let profile = ctx.profile()?;
    let storage = &profile.storage;
-
    let mut notifs = profile.notifications_mut()?;
-
    let Options {
-
        op,
-
        mode,
-
        sort_by,
-
        show_unknown,
-
    } = options;
-

-
    match op {
-
        Operation::List => list(
-
            mode,
+
    let mut cache = profile.notifications_mut()?;
+
    let command = args.command.unwrap_or_default();
+

+
    match command {
+
        Command::List {
            sort_by,
+
            reverse,
            show_unknown,
-
            &notifs.read_only(),
-
            storage,
-
            &profile,
-
        ),
-
        Operation::Clear => clear(mode, &mut notifs),
-
        Operation::Show => show(mode, &mut notifs, storage, &profile),
+
        } => {
+
            let sort_by = sort_by.unwrap_or_default();
+
            let reverse = reverse.unwrap_or(false);
+
            let show_unknown = show_unknown.unwrap_or(false);
+

+
            list(
+
                &cache.read_only(),
+
                args.all,
+
                args.repo,
+
                sort_by,
+
                reverse,
+
                show_unknown,
+
                storage,
+
                &profile,
+
            )
+
        }
+
        Command::Clear { ids } => clear(&mut cache, args.all, args.repo, ids),
+
        Command::Show { id } => show(&mut cache, id, storage, &profile),
    }
}

fn list(
-
    mode: Mode,
+
    cache: &notifications::StoreReader,
+
    all: bool,
+
    repo: Option<RepoId>,
    sort_by: SortBy,
+
    reverse: bool,
    show_unknown: bool,
-
    notifs: &notifications::StoreReader,
    storage: &Storage,
    profile: &Profile,
) -> anyhow::Result<()> {
-
    let repos: Vec<term::VStack<'_>> = match mode {
-
        Mode::Contextual => {
+
    let repos: Vec<term::VStack<'_>> = match (all, repo) {
+
        (true, None) => list_all(cache, sort_by, reverse, show_unknown, storage, profile)?,
+
        (false, None) => {
            if let Ok((_, rid)) = radicle::rad::cwd() {
-
                list_repo(rid, sort_by, show_unknown, notifs, storage, profile)?
+
                list_repo(cache, rid, sort_by, reverse, show_unknown, storage, profile)?
                    .into_iter()
                    .collect()
            } else {
-
                list_all(sort_by, show_unknown, notifs, storage, profile)?
+
                list_all(cache, sort_by, reverse, show_unknown, storage, profile)?
            }
        }
-
        Mode::ByRepo(rid) => list_repo(rid, sort_by, show_unknown, notifs, storage, profile)?
-
            .into_iter()
-
            .collect(),
-
        Mode::All => list_all(sort_by, show_unknown, notifs, storage, profile)?,
-
        Mode::ById(_) => anyhow::bail!("the `list` command does not take IDs"),
+
        (false, Some(rid)) => {
+
            list_repo(cache, rid, sort_by, reverse, show_unknown, storage, profile)?
+
                .into_iter()
+
                .collect()
+
        }
+
        (true, Some(_)) => list_all(cache, sort_by, reverse, show_unknown, storage, profile)?,
    };

    if repos.is_empty() {
@@ -237,9 +98,10 @@ fn list(
}

fn list_all<'a>(
+
    cache: &notifications::StoreReader,
    sort_by: SortBy,
+
    reverse: bool,
    show_unknown: bool,
-
    notifs: &notifications::StoreReader,
    storage: &Storage,
    profile: &Profile,
) -> anyhow::Result<Vec<term::VStack<'a>>> {
@@ -248,17 +110,26 @@ fn list_all<'a>(

    let mut vstacks = Vec::new();
    for repo in repos {
-
        let vstack = list_repo(repo.rid, sort_by, show_unknown, notifs, storage, profile)?;
+
        let vstack = list_repo(
+
            cache,
+
            repo.rid,
+
            sort_by.clone(),
+
            reverse,
+
            show_unknown,
+
            storage,
+
            profile,
+
        )?;
        vstacks.extend(vstack.into_iter());
    }
    Ok(vstacks)
}

fn list_repo<'a, R: ReadStorage>(
+
    cache: &notifications::StoreReader,
    rid: RepoId,
    sort_by: SortBy,
+
    reverse: bool,
    show_unknown: bool,
-
    notifs: &notifications::StoreReader,
    storage: &R,
    profile: &Profile,
) -> anyhow::Result<Option<term::VStack<'a>>>
@@ -272,13 +143,15 @@ where
    let issues = term::cob::issues(profile, &repo)?;
    let patches = term::cob::patches(profile, &repo)?;

-
    let mut notifs = notifs.by_repo(&rid, sort_by.field)?.collect::<Vec<_>>();
-
    if !sort_by.reverse {
+
    let mut cache = cache
+
        .by_repo(&rid, &sort_by.to_string())?
+
        .collect::<Vec<_>>();
+
    if !reverse {
        // Notifications are returned in descendant order by default.
-
        notifs.reverse();
+
        cache.reverse();
    }

-
    let table = notifs.into_iter().flat_map(|n| {
+
    let table = cache.into_iter().flat_map(|n| {
        let n: Notification = match n {
            Err(e) => return Some(Err(anyhow::Error::from(e))),
            Ok(n) => n,
@@ -503,20 +376,21 @@ impl NotificationRow {
    }
}

-
fn clear(mode: Mode, notifs: &mut notifications::StoreWriter) -> anyhow::Result<()> {
-
    let cleared = match mode {
-
        Mode::All => notifs.clear_all()?,
-
        Mode::ById(ids) => notifs.clear(&ids)?,
-
        Mode::ByRepo(rid) => notifs.clear_by_repo(&rid)?,
-
        Mode::Contextual => {
+
fn clear(
+
    cache: &mut notifications::StoreWriter,
+
    all: bool,
+
    rid: Option<RepoId>,
+
    ids: Option<Vec<NotificationId>>,
+
) -> anyhow::Result<()> {
+
    let cleared = match (all, rid, ids) {
+
        (true, _, _) => cache.clear_all()?,
+
        (_, _, Some(ids)) => cache.clear(&ids)?,
+
        (_, Some(rid), _) => cache.clear_by_repo(&rid)?,
+
        (_, None, _) => {
            if let Ok((_, rid)) = radicle::rad::cwd() {
-
                notifs.clear_by_repo(&rid)?
+
                cache.clear_by_repo(&rid)?
            } else {
-
                return Err(Error::WithHint {
-
                    err: anyhow!("not a radicle repository"),
-
                    hint: "to clear all repository notifications, use the `--all` flag",
-
                }
-
                .into());
+
                return Err(anyhow!("not a radicle repository"));
            }
        }
    };
@@ -529,20 +403,12 @@ fn clear(mode: Mode, notifs: &mut notifications::StoreWriter) -> anyhow::Result<
}

fn show(
-
    mode: Mode,
-
    notifs: &mut notifications::StoreWriter,
+
    cache: &mut notifications::StoreWriter,
+
    id: NotificationId,
    storage: &Storage,
    profile: &Profile,
) -> anyhow::Result<()> {
-
    let id = match mode {
-
        Mode::ById(ids) => match ids.as_slice() {
-
            [id] => *id,
-
            [] => anyhow::bail!("a Notification ID must be given"),
-
            _ => anyhow::bail!("too many Notification IDs given"),
-
        },
-
        _ => anyhow::bail!("a Notification ID must be given"),
-
    };
-
    let n = notifs.get(id)?;
+
    let n = cache.get(id)?;
    let repo = storage.repository(n.repo)?;

    match n.kind {
@@ -587,7 +453,7 @@ fn show(
            term::json::to_pretty(&notification, Path::new("notification.json"))?.print();
        }
    }
-
    notifs.set_status(NotificationStatus::ReadAt(LocalTime::now()), &[id])?;
+
    cache.set_status(NotificationStatus::ReadAt(LocalTime::now()), &[id])?;

    Ok(())
}
added crates/radicle-cli/src/commands/inbox/args.rs
@@ -0,0 +1,137 @@
+
use std::{fmt::Display, str::FromStr};
+

+
use clap::{Parser, Subcommand};
+
use radicle::{node::notifications::NotificationId, prelude::RepoId};
+

+
pub(crate) const ABOUT: &str = "Manage your Radicle notifications";
+
const LONG_ABOUT: &str = r#"
+
By default, this command lists all items in your inbox.
+
If your working directory is a Radicle repository, it only shows item
+
belonging to this repository, unless `--all` is used.
+

+
The `rad inbox show` command takes a notification ID (which can be found in
+
the `list` command) and displays the information related to that
+
notification. This will mark the notification as read.
+

+
The `rad inbox clear` command will delete all notifications by their passed id
+
or all notifications if no ids were passed.
+
"#;
+

+
#[derive(Debug, Parser)]
+
#[command(about = ABOUT, long_about = LONG_ABOUT, disable_version_flag = true)]
+
pub struct Args {
+
    #[command(subcommand)]
+
    pub(crate) command: Option<Command>,
+

+
    /// Operate on a given repository [default: current working directory]
+
    #[arg(value_name = "RID")]
+
    #[arg(long)]
+
    #[clap(global = true)]
+
    pub(crate) repo: Option<RepoId>,
+

+
    /// Operate on all repositories
+
    #[arg(short, long)]
+
    #[clap(global = true)]
+
    pub(crate) all: bool,
+
}
+

+
#[derive(Subcommand, Debug)]
+
pub(crate) enum Command {
+
    /// List notifications
+
    List {
+
        /// Sort by column
+
        #[arg(long)]
+
        #[arg(value_parser = SortByParser)]
+
        #[arg(default_value = "timestamp")]
+
        sort_by: Option<SortBy>,
+

+
        /// Reverse the list
+
        #[arg(short, long)]
+
        reverse: Option<bool>,
+

+
        /// Show any updates that were not recognized
+
        #[arg(long)]
+
        show_unknown: Option<bool>,
+
    },
+
    /// Show a notification
+
    Show {
+
        /// The notification to display
+
        #[arg(value_name = "NOTIFICATION_ID")]
+
        id: NotificationId,
+
    },
+
    /// Clear notifications
+
    Clear {
+
        /// A list of notification to clear
+
        #[arg(value_name = "NOTIFICATION_ID")]
+
        ids: Option<Vec<NotificationId>>,
+
    },
+
}
+

+
impl Default for Command {
+
    fn default() -> Self {
+
        Command::List {
+
            sort_by: None,
+
            reverse: None,
+
            show_unknown: None,
+
        }
+
    }
+
}
+

+
#[derive(Clone, Default, Debug)]
+
pub enum SortBy {
+
    Id,
+
    #[default]
+
    Timestamp,
+
}
+

+
impl Display for SortBy {
+
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+
        match self {
+
            Self::Id => write!(f, "rowid"),
+
            Self::Timestamp => write!(f, "timestamp"),
+
        }
+
    }
+
}
+

+
impl FromStr for SortBy {
+
    type Err = error::ParseSortBy;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        match s {
+
            "id" => Ok(Self::Id),
+
            "timestamp" => Ok(Self::Timestamp),
+
            _ => Err(error::ParseSortBy(s.to_owned())),
+
        }
+
    }
+
}
+

+
#[derive(Clone, Debug)]
+
struct SortByParser;
+

+
impl clap::builder::TypedValueParser for SortByParser {
+
    type Value = SortBy;
+

+
    fn parse_ref(
+
        &self,
+
        cmd: &clap::Command,
+
        arg: Option<&clap::Arg>,
+
        value: &std::ffi::OsStr,
+
    ) -> Result<Self::Value, clap::Error> {
+
        <SortBy as std::str::FromStr>::from_str.parse_ref(cmd, arg, value)
+
    }
+

+
    fn possible_values(
+
        &self,
+
    ) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
+
        use clap::builder::PossibleValue;
+
        Some(Box::new(
+
            [PossibleValue::new("id"), PossibleValue::new("timestamp")].into_iter(),
+
        ))
+
    }
+
}
+

+
mod error {
+
    #[derive(Debug, thiserror::Error)]
+
    #[error("'{0}' is not a valid sort by column")]
+
    pub struct ParseSortBy(pub(super) String);
+
}
modified crates/radicle-cli/src/main.rs
@@ -70,6 +70,7 @@ enum Commands {
    Fork(fork::Args),
    Id(id::Args),
    Init(init::Args),
+
    Inbox(inbox::Args),
    Issue(issue::Args),
    Ls(ls::Args),
    Path(path::Args),
@@ -267,7 +268,9 @@ pub(crate) fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyho
            }
        }
        "inbox" => {
-
            term::run_command_args::<inbox::Options, _>(inbox::HELP, inbox::run, args.to_vec())
+
            if let Some(Commands::Inbox(args)) = CliArgs::parse().command {
+
                term::run_command_fn(inbox::run, args)
+
            }
        }
        "init" => {
            if let Some(Commands::Init(args)) = CliArgs::parse().command {