Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli/inbox: Use clap
Sebastian Martinez committed 6 months ago
commit a76dd927bdfe66f28264ac150e01cc73fa71250c
parent 6cfed884bf37cba1e0d8e97fa8b0e94df4a04b1f
4 files changed +280 -207
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,75 @@ 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,
-
}
-

-
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![],
-
        ))
-
    }
-
}
+
use args::{ClearMode, Command, ListArgs, ListMode, SortBy};

-
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 command = args
+
        .clone()
+
        .command
+
        .unwrap_or_else(|| Command::List(args.empty.into()));
+

+
    match command {
+
        Command::List(ListArgs {
            sort_by,
+
            reverse,
            show_unknown,
+
        }) => list(
            &notifs.read_only(),
+
            args.list_mode(),
+
            sort_by,
+
            reverse,
+
            show_unknown,
            storage,
            &profile,
        ),
-
        Operation::Clear => clear(mode, &mut notifs),
-
        Operation::Show => show(mode, &mut notifs, storage, &profile),
+
        Command::Clear { ids } => clear(&mut notifs, args.clear_mode(ids)),
+
        Command::Show { id } => show(&mut notifs, id, storage, &profile),
    }
}

fn list(
-
    mode: Mode,
+
    notifs: &notifications::StoreReader,
+
    mode: ListMode,
    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 => {
+
        ListMode::Contextual => {
            if let Ok((_, rid)) = radicle::rad::cwd() {
-
                list_repo(rid, sort_by, show_unknown, notifs, storage, profile)?
-
                    .into_iter()
-
                    .collect()
+
                list_repo(
+
                    notifs,
+
                    rid,
+
                    sort_by,
+
                    reverse,
+
                    show_unknown,
+
                    storage,
+
                    profile,
+
                )?
+
                .into_iter()
+
                .collect()
            } else {
-
                list_all(sort_by, show_unknown, notifs, storage, profile)?
+
                list_all(notifs, 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"),
+
        ListMode::All => list_all(notifs, sort_by, reverse, show_unknown, storage, profile)?,
+
        ListMode::ByRepo(rid) => list_repo(
+
            notifs,
+
            rid,
+
            sort_by,
+
            reverse,
+
            show_unknown,
+
            storage,
+
            profile,
+
        )?
+
        .into_iter()
+
        .collect(),
    };

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

fn list_all<'a>(
+
    notifs: &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 +118,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(
+
            notifs,
+
            repo.rid,
+
            sort_by,
+
            reverse,
+
            show_unknown,
+
            storage,
+
            profile,
+
        )?;
        vstacks.extend(vstack.into_iter());
    }
    Ok(vstacks)
}

fn list_repo<'a, R: ReadStorage>(
+
    notifs: &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,8 +151,10 @@ 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 notifs = notifs
+
        .by_repo(&rid, &sort_by.to_string())?
+
        .collect::<Vec<_>>();
+
    if !reverse {
        // Notifications are returned in descendant order by default.
        notifs.reverse();
    }
@@ -503,20 +384,16 @@ impl NotificationRow {
    }
}

-
fn clear(mode: Mode, notifs: &mut notifications::StoreWriter) -> anyhow::Result<()> {
+
fn clear(notifs: &mut notifications::StoreWriter, mode: ClearMode) -> 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 => {
+
        ClearMode::ByNotifications(ids) => notifs.clear(&ids)?,
+
        ClearMode::ByRepo(rid) => notifs.clear_by_repo(&rid)?,
+
        ClearMode::All => notifs.clear_all()?,
+
        ClearMode::Contextual => {
            if let Ok((_, rid)) = radicle::rad::cwd() {
                notifs.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,19 +406,11 @@ fn clear(mode: Mode, notifs: &mut notifications::StoreWriter) -> anyhow::Result<
}

fn show(
-
    mode: Mode,
    notifs: &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 repo = storage.repository(n.repo)?;

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

+
use clap::{Parser, Subcommand, ValueEnum};
+
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 items
+
belonging to this repository, unless `--all` is used.
+

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

+
The `clear` subcommand will clear all notifications with given IDs,
+
or all notifications if no IDs are given. Cleared notifications are
+
deleted and cannot be restored.
+
"#;
+

+
#[derive(Clone, 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: cwd]
+
    #[arg(value_name = "RID")]
+
    #[arg(long)]
+
    #[clap(global = true)]
+
    pub(crate) repo: Option<RepoId>,
+

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

+
    #[clap(flatten)]
+
    pub(crate) empty: EmptyArgs,
+
}
+

+
impl Args {
+
    pub(super) fn list_mode(&self) -> ListMode {
+
        if self.all {
+
            assert!(self.repo.is_none());
+
            return ListMode::All;
+
        }
+

+
        if let Some(repo) = self.repo {
+
            return ListMode::ByRepo(repo);
+
        }
+

+
        ListMode::Contextual
+
    }
+

+
    pub(super) fn clear_mode(&self, ids: Option<Vec<NotificationId>>) -> ClearMode {
+
        if let Some(ids) = ids {
+
            return ClearMode::ByNotifications(ids);
+
        }
+

+
        if self.all {
+
            assert!(self.repo.is_none());
+
            return ClearMode::All;
+
        }
+

+
        if let Some(repo) = self.repo {
+
            return ClearMode::ByRepo(repo);
+
        }
+

+
        ClearMode::Contextual
+
    }
+
}
+

+
#[derive(Subcommand, Clone, Debug)]
+
pub(crate) enum Command {
+
    /// List all items in your inbox
+
    List(ListArgs),
+
    /// Show a notification
+
    ///
+
    /// The NOTIFICATION_ID can be found by listing the items in your inbox
+
    ///
+
    /// Showing a notification will mark that notification as read
+
    Show {
+
        /// The notification to display
+
        #[arg(value_name = "NOTIFICATION_ID")]
+
        id: NotificationId,
+
    },
+
    /// Clear notifications
+
    ///
+
    /// This will clear all given notifications
+
    ///
+
    /// If no notifications are specified then all notifications are cleared
+
    Clear {
+
        /// A list of notifications to clear
+
        #[arg(value_name = "NOTIFICATION_ID")]
+
        ids: Option<Vec<NotificationId>>,
+
    },
+
}
+

+
#[derive(Parser, Clone, Copy, Debug)]
+
pub struct EmptyArgs {
+
    /// Sort by column
+
    #[arg(long, value_enum, default_value_t, hide = true)]
+
    sort_by: SortBy,
+

+
    /// Reverse the list
+
    #[arg(short, long, hide = true)]
+
    reverse: bool,
+

+
    /// Show any updates that were not recognized
+
    #[arg(long, hide = true)]
+
    show_unknown: bool,
+
}
+

+
#[derive(Parser, Clone, Copy, Debug)]
+
pub struct ListArgs {
+
    /// Sort by column
+
    #[arg(long, value_enum, default_value_t)]
+
    pub(super) sort_by: SortBy,
+

+
    /// Reverse the list
+
    #[arg(short, long)]
+
    pub(super) reverse: bool,
+

+
    /// Show any updates that were not recognized
+
    #[arg(long)]
+
    pub(super) show_unknown: bool,
+
}
+

+
impl From<EmptyArgs> for ListArgs {
+
    fn from(
+
        EmptyArgs {
+
            sort_by,
+
            reverse,
+
            show_unknown,
+
        }: EmptyArgs,
+
    ) -> Self {
+
        Self {
+
            sort_by,
+
            reverse,
+
            show_unknown,
+
        }
+
    }
+
}
+

+
#[derive(ValueEnum, Clone, Copy, 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 = String;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        match s {
+
            "id" => Ok(Self::Id),
+
            "timestamp" => Ok(Self::Timestamp),
+
            _ => Err(format!("'{s}' is not a valid sort by column")),
+
        }
+
    }
+
}
+

+
pub(super) enum ListMode {
+
    /// List the notifications of the current repository, if in a working
+
    /// directory, otherwise all the repositories.
+
    Contextual,
+
    /// List the notifications for a all repositories.
+
    All,
+
    /// List the notifications for a specific repository.
+
    ByRepo(RepoId),
+
}
+

+
pub(super) enum ClearMode {
+
    /// Clear the specified notifications.
+
    ///
+
    /// Note that this does not require a `RepoId` since the IDs are globally
+
    /// unique due to the use of a single sqlite table.
+
    ByNotifications(Vec<NotificationId>),
+
    /// Clear the notifications of a specific repository.
+
    ByRepo(RepoId),
+
    /// Clear all notifications of all repositories.
+
    All,
+
    /// Clear the notifications of the current repository, only if in a working
+
    /// directory.
+
    Contextual,
+
}
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 {