Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
patch/list: Return to app and keep state
Erik Kundt committed 3 months ago
commit b95c5dfff3cbc4e7a7376ded206d07fb6a901a0e
parent dbb2970
2 files changed +157 -79
modified bin/commands/patch.rs
@@ -9,7 +9,6 @@ use anyhow::anyhow;

use radicle::cob::ObjectId;
use radicle::identity::RepoId;
-
use radicle::patch::cache::Patches;
use radicle::patch::{Patch, Revision, RevisionId, Status};
use radicle::prelude::Did;
use radicle::storage::git::Repository;
@@ -297,7 +296,6 @@ impl Args for Options {

#[tokio::main]
pub async fn run(options: Options, ctx: impl radicle_cli::terminal::Context) -> anyhow::Result<()> {
-
    use crate::tui_patch::list::PatchOperation;
    use radicle::storage::ReadStorage;

    let (_, rid) = radicle::rad::cwd()
@@ -309,53 +307,10 @@ pub async fn run(options: Options, ctx: impl radicle_cli::terminal::Context) ->

    match options.op {
        Operation::List { opts } => {
-
            let profile = ctx.profile()?;
-
            let rid = options.repo.unwrap_or(rid);
-

-
            // Run TUI with patch list interface
-
            let selection = interface::list(opts.clone(), profile.clone(), rid).await?;
-

-
            if opts.json {
-
                let selection = selection
-
                    .map(|o| serde_json::to_string(&o).unwrap_or_default())
-
                    .unwrap_or_default();
-

-
                log::info!("About to print to `stderr`: {selection}");
-
                log::info!("Exiting patch list interface..");
-

-
                eprint!("{selection}");
-
            } else if let Some(selection) = selection {
-
                if let Some(operation) = selection.operation.clone() {
-
                    match operation {
-
                        PatchOperation::Show { id } => {
-
                            terminal::run_rad(
-
                                Some("patch"),
-
                                &["show".into(), id.to_string().into()],
-
                            )?;
-
                        }
-
                        PatchOperation::Diff { id } => {
-
                            let repo = profile.storage.repository(rid)?;
-
                            let cache = profile.patches(&repo)?;
-
                            let patch = cache
-
                                .get(&id)?
-
                                .ok_or_else(|| anyhow!("unknown patch '{id}'"))?;
-
                            let range = format!("{}..{}", patch.base(), patch.head());
+
            log::info!("Starting patch selection interface in project {rid}..");

-
                            terminal::run_git(Some("diff"), &[range.into()])?;
-
                        }
-
                        PatchOperation::Checkout { id } => {
-
                            terminal::run_rad(
-
                                Some("patch"),
-
                                &["checkout".into(), id.to_string().into()],
-
                            )?;
-
                        }
-
                        PatchOperation::_Review { id } => {
-
                            let opts = ReviewOptions::default();
-
                            interface::review(opts, profile, rid, id).await?;
-
                        }
-
                    }
-
                }
-
            }
+
            let rid = options.repo.unwrap_or(rid);
+
            interface::list(opts.clone(), ctx.profile()?, rid).await?;
        }
        Operation::Review { ref opts } => {
            log::info!("Starting patch review interface in project {rid}..");
@@ -395,11 +350,8 @@ mod interface {
    use radicle::storage::ReadStorage;
    use radicle::Profile;

-
    use radicle_cli::terminal;
-

-
    use radicle_tui::Selection;
-

    use crate::cob;
+
    use crate::terminal;
    use crate::tui_patch::list;
    use crate::tui_patch::review::builder::CommentBuilder;
    use crate::tui_patch::review::ReviewAction;
@@ -409,23 +361,96 @@ mod interface {
    use super::review::builder::ReviewBuilder;
    use super::{ListOptions, ReviewOptions};

-
    pub async fn list(
-
        opts: ListOptions,
-
        profile: Profile,
-
        rid: RepoId,
-
    ) -> anyhow::Result<Option<Selection<list::PatchOperation>>> {
-
        let repository = profile.storage.repository(rid).unwrap();
+
    pub async fn list(opts: ListOptions, profile: Profile, rid: RepoId) -> anyhow::Result<()> {
        let me = profile.did();

-
        log::info!("Starting patch selection interface in project {rid}..");
+
        #[derive(Default)]
+
        struct PreviousState {
+
            patch_id: Option<PatchId>,
+
            search: Option<String>,
+
        }

-
        let context = list::Context {
-
            profile,
-
            repository,
-
            filter: (me, opts.filter.clone()).into(),
-
        };
+
        // Store issue and comment selection across app runs in order to
+
        // preselect them when re-running the app.
+
        let mut state = PreviousState::default();

-
        list::Tui::new(context).run().await
+
        loop {
+
            let context = list::Context {
+
                profile: profile.clone(),
+
                repository: profile.storage.repository(rid).unwrap(),
+
                filter: (me, opts.filter.clone()).into(),
+
                search: state.search.clone(),
+
                patch_id: state.patch_id,
+
            };
+

+
            // Run TUI with patch list interface
+
            let selection = list::Tui::new(context).run().await?;
+

+
            if opts.json {
+
                let selection = selection
+
                    .map(|o| serde_json::to_string(&o).unwrap_or_default())
+
                    .unwrap_or_default();
+

+
                log::info!("About to print to `stderr`: {selection}");
+
                log::info!("Exiting patch list interface..");
+

+
                eprint!("{selection}");
+

+
                break;
+
            } else if let Some(selection) = selection {
+
                if let Some(operation) = selection.operation.clone() {
+
                    match operation {
+
                        list::PatchOperation::Show { args } => {
+
                            state = PreviousState {
+
                                patch_id: Some(args.id()),
+
                                search: Some(args.search()),
+
                            };
+
                            terminal::run_rad(
+
                                Some("patch"),
+
                                &["show".into(), args.id().to_string().into()],
+
                            )?;
+
                        }
+
                        list::PatchOperation::Diff { args } => {
+
                            let repo = profile.clone().storage.repository(rid)?;
+
                            let cache = profile.patches(&repo)?;
+
                            let patch = cache
+
                                .get(&args.id())?
+
                                .ok_or_else(|| anyhow!("unknown patch '{}'", args.id()))?;
+
                            let range = format!("{}..{}", patch.base(), patch.head());
+

+
                            state = PreviousState {
+
                                patch_id: Some(args.id()),
+
                                search: Some(args.search()),
+
                            };
+

+
                            terminal::run_git(Some("diff"), &[range.into()])?;
+
                        }
+
                        list::PatchOperation::Checkout { args } => {
+
                            state = PreviousState {
+
                                patch_id: Some(args.id()),
+
                                search: Some(args.search()),
+
                            };
+
                            terminal::run_rad(
+
                                Some("patch"),
+
                                &["checkout".into(), args.id().to_string().into()],
+
                            )?;
+
                        }
+
                        list::PatchOperation::_Review { args } => {
+
                            state = PreviousState {
+
                                patch_id: Some(args.id()),
+
                                search: Some(args.search()),
+
                            };
+
                            let opts = ReviewOptions::default();
+
                            review(opts, profile.clone(), rid, args.id()).await?;
+
                        }
+
                    }
+
                }
+
            } else {
+
                break;
+
            }
+
        }
+

+
        Ok(())
    }

    pub async fn review(
@@ -434,6 +459,8 @@ mod interface {
        rid: RepoId,
        patch_id: PatchId,
    ) -> anyhow::Result<()> {
+
        use radicle_cli::terminal;
+

        let repo = profile.storage.repository(rid)?;
        let signer = terminal::signer(&profile)?;
        let cache = profile.patches(&repo)?;
modified bin/commands/patch/list.rs
@@ -1,7 +1,7 @@
use std::str::FromStr;
use std::sync::{Arc, Mutex};

-
use anyhow::Result;
+
use anyhow::{anyhow, Result};

use serde::Serialize;

@@ -56,14 +56,46 @@ const HELP: &str = r#"# Generic keybindings
Examples:   state=open bugfix
            state=merged author=(did:key:... or did:key:...)"#;

+
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
+
pub struct OperationArguments {
+
    id: PatchId,
+
    search: String,
+
}
+

+
impl OperationArguments {
+
    pub fn id(&self) -> PatchId {
+
        self.id
+
    }
+

+
    pub fn search(&self) -> String {
+
        self.search.clone()
+
    }
+
}
+

+
impl TryFrom<(&Vec<Patch>, &AppState)> for OperationArguments {
+
    type Error = anyhow::Error;
+

+
    fn try_from(value: (&Vec<Patch>, &AppState)) -> Result<Self> {
+
        let (patches, state) = value;
+
        let selected = state.patches.selected();
+
        let id = selected
+
            .and_then(|s| patches.get(s))
+
            .ok_or(anyhow!("No patch selected"))?
+
            .id;
+
        let search = state.search.read().text;
+

+
        Ok(Self { id, search })
+
    }
+
}
+

/// The selected patch operation returned by the operation
/// selection widget.
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub enum PatchOperation {
-
    Checkout { id: PatchId },
-
    Diff { id: PatchId },
-
    Show { id: PatchId },
-
    _Review { id: PatchId },
+
    Checkout { args: OperationArguments },
+
    Diff { args: OperationArguments },
+
    Show { args: OperationArguments },
+
    _Review { args: OperationArguments },
}

type Selection = tui::Selection<PatchOperation>;
@@ -72,6 +104,8 @@ pub struct Context {
    pub profile: Profile,
    pub repository: Repository,
    pub filter: PatchFilter,
+
    pub patch_id: Option<PatchId>,
+
    pub search: Option<String>,
}

pub struct Tui {
@@ -155,9 +189,16 @@ impl TryFrom<&Context> for App {
            .collect::<Vec<_>>();
        patches.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));

-
        let search = {
-
            let raw = context.filter.to_string();
-
            raw.trim().to_string()
+
        let search = context.search.as_ref().map(|s| s.trim().to_string());
+
        let (search, filter) = match search {
+
            Some(search) => (
+
                search.clone(),
+
                PatchFilter::from_str(search.trim()).unwrap_or(PatchFilter::Invalid),
+
            ),
+
            None => {
+
                let filter = context.filter.clone();
+
                (filter.to_string().trim().to_string(), filter)
+
            }
        };

        Ok(App {
@@ -165,14 +206,24 @@ impl TryFrom<&Context> for App {
            state: AppState {
                page: Page::Main,
                main_group: ContainerState::new(3, Some(0)),
-
                patches: TableState::new(Some(0)),
+
                patches: TableState::new(Some(
+
                    context
+
                        .patch_id
+
                        .and_then(|id| {
+
                            patches
+
                                .iter()
+
                                .filter(|item| filter.matches(item))
+
                                .position(|item| item.id == id)
+
                        })
+
                        .unwrap_or(0),
+
                )),
                search: BufferedValue::new(TextEditState {
                    text: search.clone(),
                    cursor: search.len(),
                }),
                show_search: false,
                help: TextViewState::new(Position::default()),
-
                filter: context.filter.clone(),
+
                filter,
            },
        })
    }
@@ -361,20 +412,20 @@ impl App {
            ui.send_message(Message::ShowSearch);
        }

-
        if let Some(patch) = selected.and_then(|s| patches.get(s)) {
+
        if let Ok(args) = OperationArguments::try_from((&patches, &self.state)) {
            if ui.has_input(|key| key == Key::Enter) {
                ui.send_message(Message::Exit {
-
                    operation: Some(PatchOperation::Show { id: patch.id }),
+
                    operation: Some(PatchOperation::Show { args: args.clone() }),
                });
            }
            if ui.has_input(|key| key == Key::Char('d')) {
                ui.send_message(Message::Exit {
-
                    operation: Some(PatchOperation::Diff { id: patch.id }),
+
                    operation: Some(PatchOperation::Diff { args: args.clone() }),
                });
            }
            if ui.has_input(|key| key == Key::Char('c')) {
                ui.send_message(Message::Exit {
-
                    operation: Some(PatchOperation::Checkout { id: patch.id }),
+
                    operation: Some(PatchOperation::Checkout { args }),
                });
            }
        }