patch-select: Add support for patch filter option

This commit is contained in:
Erik Kundt
2024-01-29 17:30:41 +01:00
parent 5e9e6140ed
commit 8909cb668f
9 changed files with 177 additions and 59 deletions
+54 -36
View File
@@ -10,7 +10,8 @@ use std::ffi::OsString;
use anyhow::anyhow;
use radicle_tui as tui;
use tui::cob::patch;
use tui::cob::patch::{self, State};
use tui::{context, log, Window};
use crate::terminal;
@@ -24,15 +25,23 @@ pub const HELP: Help = Help {
usage: r#"
Usage
rad-tui patch [<option>...]
rad-tui patch select [--operation | --id] [<option>...]
rad-tui patch select [<option>...]
Select options
--operation Select patch id and operation (default)
--id Select patch id only
--mode <MODE> Set selection mode; see MODE below (default: operation)
--all Show all patches, including merged and archived patches
--archived Show only archived patches
--merged Show only merged patches
--open Show only open patches (default)
--draft Show only draft patches
--authored Show only patches that you have authored
--author <did> Show only patched where the given user is an author
(may be specified multiple times)
The MODE argument can be 'operation' or 'id'. 'operation' selects a patch id and
an operation, whereas 'id' selects a patch id only.
Other options
@@ -57,25 +66,18 @@ pub enum OperationName {
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct SelectOptions {
subject: select::Subject,
mode: select::Mode,
filter: patch::Filter,
}
impl SelectOptions {
pub fn new(subject: select::Subject, filter: patch::Filter) -> Self {
Self { subject, filter }
}
}
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<OperationName> = None;
let filter = patch::Filter::default();
let mut json = false;
let mut select_opts: Option<SelectOptions> = None;
let mut select_opts = SelectOptions::default();
#[allow(clippy::never_loop)]
while let Some(arg) = parser.next()? {
@@ -87,21 +89,39 @@ impl Args for Options {
json = true;
}
// Select options.
Long("operation") | Short('o') if op == Some(OperationName::Select) => {
if select_opts.is_some() {
anyhow::bail!("select option already given")
}
select_opts = Some(SelectOptions::new(
select::Subject::Operation,
filter.clone(),
));
// select options.
Long("mode") | Short('m') if op == Some(OperationName::Select) => {
let val = parser.value()?;
let val = val.to_str().unwrap_or_default();
select_opts.mode = match val {
"operation" => select::Mode::Operation,
"id" => select::Mode::Id,
unknown => anyhow::bail!("unknown mode '{}'", unknown),
};
}
Long("id") | Short('i') if op == Some(OperationName::Select) => {
if select_opts.is_some() {
anyhow::bail!("select option already given")
}
select_opts = Some(SelectOptions::new(select::Subject::Id, filter.clone()));
Long("all") if op == Some(OperationName::Select) => {
select_opts.filter = select_opts.filter.with_state(None);
}
Long("draft") if op == Some(OperationName::Select) => {
select_opts.filter = select_opts.filter.with_state(Some(State::Draft));
}
Long("archived") if op == Some(OperationName::Select) => {
select_opts.filter = select_opts.filter.with_state(Some(State::Archived));
}
Long("merged") if op == Some(OperationName::Select) => {
select_opts.filter = select_opts.filter.with_state(Some(State::Merged));
}
Long("open") if op == Some(OperationName::Select) => {
select_opts.filter = select_opts.filter.with_state(Some(State::Open));
}
Long("authored") if op == Some(OperationName::Select) => {
select_opts.filter = select_opts.filter.with_authored(true);
}
Long("author") if op == Some(OperationName::Select) => {
select_opts.filter = select_opts
.filter
.with_author(terminal::args::did(&parser.value()?)?);
}
Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
@@ -113,9 +133,7 @@ impl Args for Options {
}
let op = match op.ok_or_else(|| anyhow!("an operation must be provided"))? {
OperationName::Select => Operation::Select {
opts: select_opts.unwrap_or_default(),
},
OperationName::Select => Operation::Select { opts: select_opts },
};
Ok((Options { op, json }, vec![]))
}
@@ -132,7 +150,7 @@ pub fn run(options: Options, _ctx: impl terminal::Context) -> anyhow::Result<()>
log::enable(context.profile(), "patch", "select")?;
let mut app = select::App::new(context, opts.subject.clone(), opts.filter.clone());
let mut app = select::App::new(context, opts.mode.clone(), opts.filter.clone());
let output = Window::default().run(&mut app, 1000 / FPS)?;
let output = if options.json {
@@ -141,9 +159,9 @@ pub fn run(options: Options, _ctx: impl terminal::Context) -> anyhow::Result<()>
.unwrap_or_default()
} else {
match options.op {
Operation::Select { ref opts } => match &opts.subject {
select::Subject::Id => output.map(|o| format!("{}", o)).unwrap_or_default(),
select::Subject::Operation => output
Operation::Select { ref opts } => match &opts.mode {
select::Mode::Id => output.map(|o| format!("{}", o)).unwrap_or_default(),
select::Mode::Operation => output
.map(|o| format!("rad patch {}", o))
.unwrap_or_default(),
},
+19 -6
View File
@@ -56,7 +56,7 @@ impl PatchBrowser {
.as_ref()
.unwrap()
.iter()
.filter(|(_, patch)| filter.matches(patch));
.filter(|(_, patch)| filter.matches(context.profile(), patch));
let mut items = vec![];
for (id, patch) in patches {
@@ -104,7 +104,12 @@ impl WidgetComponent for PatchBrowser {
}
}
pub fn browse_context(context: &Context, _theme: &Theme, progress: Progress) -> Widget<ContextBar> {
pub fn browse_context(
context: &Context,
_theme: &Theme,
filter: Filter,
progress: Progress,
) -> Widget<ContextBar> {
use radicle::cob::patch::State;
let mut draft = 0;
@@ -112,7 +117,13 @@ pub fn browse_context(context: &Context, _theme: &Theme, progress: Progress) ->
let mut archived = 0;
let mut merged = 0;
let patches = context.patches().as_ref().unwrap();
let patches = context
.patches()
.as_ref()
.unwrap()
.iter()
.filter(|(_, patch)| filter.matches(context.profile(), patch));
for (_, patch) in patches {
match patch.state() {
State::Draft => draft += 1,
@@ -125,8 +136,8 @@ pub fn browse_context(context: &Context, _theme: &Theme, progress: Progress) ->
}
}
let context = label::default("");
let divider = label::default(" | ");
let context = label::reversable("/").style(style::magenta_reversed());
let filter = label::default(&filter.to_string()).style(style::magenta_dim());
let draft_n = label::default(&format!("{draft}")).style(style::gray_dim());
let draft = label::default(" Draft");
@@ -141,11 +152,13 @@ pub fn browse_context(context: &Context, _theme: &Theme, progress: Progress) ->
let merged = label::default(" Merged ");
let progress = label::reversable(&progress.to_string()).style(style::magenta_reversed());
let spacer = label::default("");
let divider = label::default(" | ");
let context_bar = ContextBar::new(
label::group(&[context]),
label::group(&[spacer.clone()]),
label::group(&[filter]),
label::group(&[spacer]),
label::group(&[
draft_n,
+5 -5
View File
@@ -57,7 +57,7 @@ impl Serialize for PatchId {
///
/// Depends on CLI arguments given by the user.
#[derive(Clone, Default, Debug, Eq, PartialEq)]
pub enum Subject {
pub enum Mode {
#[default]
Operation,
Id,
@@ -152,7 +152,7 @@ pub struct App {
pages: PageStack<Cid, Message>,
theme: Theme,
quit: bool,
subject: Subject,
mode: Mode,
filter: Filter,
output: Option<Output>,
}
@@ -160,13 +160,13 @@ pub struct App {
/// Creates a new application using a tui-realm-application, mounts all
/// components and sets focus to a default one.
impl App {
pub fn new(context: Context, subject: Subject, filter: Filter) -> Self {
pub fn new(context: Context, mode: Mode, filter: Filter) -> Self {
Self {
context,
pages: PageStack::default(),
theme: Theme::default(),
quit: false,
subject,
mode,
filter,
output: None,
}
@@ -177,7 +177,7 @@ impl App {
app: &mut Application<Cid, Message, NoUserEvent>,
theme: &Theme,
) -> Result<()> {
let home = Box::new(ListView::new(self.subject.clone(), self.filter.clone()));
let home = Box::new(ListView::new(self.mode.clone(), self.filter.clone()));
self.pages.push(home, app, &self.context, theme)?;
Ok(())
+6 -6
View File
@@ -15,20 +15,20 @@ use tui::ui::{layout, subscription};
use tui::ViewPage;
use super::super::common;
use super::{ui, Application, Cid, ListCid, Message, Subject};
use super::{ui, Application, Cid, ListCid, Message, Mode};
///
/// Home
///
pub struct ListView {
active_component: ListCid,
subject: Subject,
subject: Mode,
filter: Filter,
shortcuts: HashMap<ListCid, Widget<Shortcuts>>,
}
impl ListView {
pub fn new(subject: Subject, filter: Filter) -> Self {
pub fn new(subject: Mode, filter: Filter) -> Self {
Self {
active_component: ListCid::PatchBrowser,
subject,
@@ -50,7 +50,7 @@ impl ListView {
}
_ => Progress::None,
};
let context = common::ui::browse_context(context, theme, progress);
let context = common::ui::browse_context(context, theme, self.filter.clone(), progress);
app.remount(Cid::List(ListCid::Context), context.to_boxed(), vec![])?;
@@ -87,14 +87,14 @@ impl ViewPage<Cid, Message> for ListView {
app.remount(Cid::List(ListCid::Header), header, vec![])?;
match self.subject {
Subject::Id => {
Mode::Id => {
let patch_browser =
ui::id_select(theme, context, self.filter.clone(), None).to_boxed();
self.shortcuts = patch_browser.as_ref().shortcuts();
app.remount(Cid::List(ListCid::PatchBrowser), patch_browser, vec![])?;
}
Subject::Operation => {
Mode::Operation => {
let patch_browser =
ui::operation_select(theme, context, self.filter.clone(), None).to_boxed();
self.shortcuts = patch_browser.as_ref().shortcuts();
+1
View File
@@ -5,6 +5,7 @@ use anyhow::Result;
use radicle::cob::Label;
use radicle::prelude::Did;
pub mod format;
pub mod issue;
pub mod patch;
+7
View File
@@ -0,0 +1,7 @@
use radicle::identity::Did;
/// Format a DID.
pub fn did(did: &Did) -> String {
let nid = did.as_key().to_human();
format!("{}…{}", &nid[..7], &nid[nid.len() - 7..])
}
+74 -4
View File
@@ -1,8 +1,13 @@
use std::fmt::Display;
use anyhow::Result;
use radicle::cob::patch::{Patch, PatchId, Patches};
use radicle::identity::Did;
use radicle::storage::git::Repository;
use radicle::{patch, Profile};
use super::format;
#[derive(Clone, Default, Debug, Eq, PartialEq)]
pub enum State {
@@ -13,6 +18,18 @@ pub enum State {
Archived,
}
impl Display for State {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let state = match self {
State::Draft => "draft",
State::Open => "open",
State::Merged => "merged",
State::Archived => "archived",
};
f.write_str(state)
}
}
#[derive(Clone, Default, Debug, Eq, PartialEq)]
pub struct Filter {
state: Option<State>,
@@ -21,8 +38,8 @@ pub struct Filter {
}
impl Filter {
pub fn with_state(mut self, state: State) -> Self {
self.state = Some(state);
pub fn with_state(mut self, state: Option<State>) -> Self {
self.state = state;
self
}
@@ -36,8 +53,61 @@ impl Filter {
self
}
pub fn matches(&self, _patch: &Patch) -> bool {
true
pub fn matches(&self, profile: &Profile, patch: &Patch) -> bool {
let matches_state = match self.state {
Some(State::Draft) => matches!(patch.state(), patch::State::Draft),
Some(State::Open) => matches!(patch.state(), patch::State::Open { .. }),
Some(State::Merged) => matches!(patch.state(), patch::State::Merged { .. }),
Some(State::Archived) => matches!(patch.state(), patch::State::Archived),
None => true,
};
let matches_authored = self
.authored
.then(|| *patch.author().id() == profile.did())
.unwrap_or(true);
let matches_authors = (!self.authors.is_empty())
.then(|| {
self.authors
.iter()
.any(|other| *patch.author().id() == *other)
})
.unwrap_or(true);
matches_state && matches_authored && matches_authors
}
}
impl ToString for Filter {
fn to_string(&self) -> String {
let mut filter = String::new();
filter.push(' ');
if let Some(state) = &self.state {
filter.push_str(&format!("is:{}", state));
filter.push(' ');
}
if self.authored {
filter.push_str("is:authored");
filter.push(' ');
}
if !self.authors.is_empty() {
filter.push_str("authors:");
filter.push('[');
let mut authors = self.authors.iter().peekable();
while let Some(author) = authors.next() {
filter.push_str(&format::did(author));
if authors.peek().is_some() {
filter.push(',');
}
}
filter.push(']');
}
filter
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ use radicle::cob::issue::{Issue, IssueId};
use radicle::cob::patch::{Patch, PatchId};
use radicle::crypto::ssh::keystore::{Keystore, MemorySigner};
use radicle::crypto::Signer;
use radicle::prelude::{Id, Project};
use radicle::identity::{Id, Project};
use radicle::profile::env::RAD_PASSPHRASE;
use radicle::storage::git::Repository;
use radicle::storage::{ReadRepository, ReadStorage};
+10 -1
View File
@@ -66,6 +66,14 @@ pub mod style {
Style::default().fg(Color::Yellow)
}
pub fn yellow_dim() -> Style {
yellow().add_modifier(TextModifiers::DIM)
}
pub fn yellow_dim_reversed() -> Style {
yellow_dim().add_modifier(TextModifiers::REVERSED)
}
pub fn blue() -> Style {
Style::default().fg(Color::Blue)
}
@@ -108,7 +116,8 @@ pub mod style {
pub fn default_reversed() -> Style {
Style::default()
.add_modifier(TextModifiers::DIM)
.fg(Color::DarkGray)
// .add_modifier(TextModifiers::DIM)
.add_modifier(TextModifiers::REVERSED)
}