Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
list/patch: Various improvements
Merged did:key:z6MkgFq6...nBGz opened 3 months ago
  • patches load without git stats on startup, and are loaded with stats in the background
  • git diff does not result in an application error anymore
  • inbox loading popup area is cleared before being drawn to
5 files changed +197 -85 4eb065c1 810bdcf4
modified bin/commands/inbox/list.rs
@@ -6,6 +6,7 @@ use std::vec;

use anyhow::Result;

+
use ratatui::widgets::Clear;
use serde::Serialize;

use radicle::node::notifications::NotificationId;
@@ -609,16 +610,19 @@ impl App {
                        None,
                        |ui| {
                            ui.label(frame, "");
-
                            ui.column_bar(
-
                                frame,
-
                                [Column::new(
-
                                    Span::raw(" Loading ").magenta().slow_blink(),
-
                                    Constraint::Fill(1),
-
                                )]
-
                                .to_vec(),
-
                                Spacing::from(0),
-
                                Some(Borders::All),
-
                            );
+
                            ui.layout(Layout::vertical([Constraint::Min(1)]), None, |ui| {
+
                                frame.render_widget(Clear, ui.area());
+
                                ui.column_bar(
+
                                    frame,
+
                                    [Column::new(
+
                                        Span::raw(" Loading ").magenta().rapid_blink(),
+
                                        Constraint::Fill(1),
+
                                    )]
+
                                    .to_vec(),
+
                                    Spacing::from(0),
+
                                    Some(Borders::All),
+
                                );
+
                            });
                        },
                    );
                },
modified bin/commands/patch.rs
@@ -377,7 +377,7 @@ mod interface {
        loop {
            let context = list::Context {
                profile: profile.clone(),
-
                repository: profile.storage.repository(rid).unwrap(),
+
                rid,
                filter: (me, opts.filter.clone()).into(),
                search: state.search.clone(),
                patch_id: state.patch_id,
@@ -423,7 +423,7 @@ mod interface {
                                search: Some(args.search()),
                            };

-
                            terminal::run_git(Some("diff"), &[range.into()])?;
+
                            let _ = terminal::run_git(Some("diff"), &[range.into()]);
                        }
                        list::PatchOperation::Checkout { args } => {
                            state = PreviousState {
modified bin/commands/patch/list.rs
@@ -3,11 +3,13 @@ use std::sync::{Arc, Mutex};

use anyhow::{anyhow, Result};

+
use radicle::prelude::RepoId;
+
use radicle::storage::ReadStorage;
+
use ratatui::widgets::Clear;
use serde::Serialize;

use radicle::patch::cache::Patches;
use radicle::patch::PatchId;
-
use radicle::storage::git::Repository;
use radicle::Profile;

use ratatui::layout::{Alignment, Constraint, Layout, Position};
@@ -19,7 +21,7 @@ use radicle_tui as tui;

use tui::event::Key;
use tui::store;
-
use tui::task::EmptyProcessors;
+
use tui::task::{Process, Task};
use tui::ui;
use tui::ui::layout::Spacing;
use tui::ui::widget::{
@@ -100,14 +102,68 @@ pub enum PatchOperation {

type Selection = tui::Selection<PatchOperation>;

+
#[derive(Clone, Debug)]
pub struct Context {
    pub profile: Profile,
-
    pub repository: Repository,
+
    pub rid: RepoId,
    pub filter: PatchFilter,
    pub patch_id: Option<PatchId>,
    pub search: Option<String>,
}

+
#[derive(Clone, Debug)]
+
pub struct Loader {
+
    context: Context,
+
}
+

+
impl Loader {
+
    fn new(context: Context) -> Self {
+
        Self { context }
+
    }
+
}
+

+
#[derive(Debug)]
+
pub struct PatchLoader {
+
    context: Context,
+
}
+

+
impl PatchLoader {
+
    fn new(context: Context) -> Self {
+
        PatchLoader { context }
+
    }
+
}
+

+
impl Task for PatchLoader {
+
    type Return = Message;
+

+
    fn run(&self) -> anyhow::Result<Vec<Self::Return>> {
+
        let context = &self.context;
+
        let profile = context.profile.clone();
+
        let repo = profile.storage.repository(context.rid)?;
+
        let cache = profile.patches(&repo)?;
+
        let patches = cache
+
            .list()?
+
            .filter_map(|patch| patch.ok())
+
            .flat_map(|patch| Patch::new(&context.profile, &repo, patch.clone()).ok())
+
            .collect::<Vec<_>>();
+

+
        Ok(vec![Message::Loaded(patches)])
+
    }
+
}
+

+
impl Process<Message> for Loader {
+
    async fn process(&mut self, message: Message) -> anyhow::Result<Vec<Message>> {
+
        match message {
+
            Message::Initialize | Message::Reload => {
+
                let loader = PatchLoader::new(self.context.clone());
+
                let messages = tokio::spawn(async move { loader.run() }).await.unwrap()?;
+
                Ok(messages)
+
            }
+
            _ => Ok(vec![]),
+
        }
+
    }
+
}
+

pub struct Tui {
    context: Context,
}
@@ -122,7 +178,13 @@ impl Tui {
        let channel = Channel::default();
        let state = App::try_from(&self.context)?;

-
        tui::im(state, viewport, channel, EmptyProcessors::new()).await
+
        tui::im(
+
            state,
+
            viewport,
+
            channel,
+
            vec![Loader::new(self.context.clone())],
+
        )
+
        .await
    }
}

@@ -147,9 +209,12 @@ pub enum Change {

#[derive(Clone, Debug)]
pub enum Message {
+
    Initialize,
    Changed(Change),
    ShowSearch,
    HideSearch { apply: bool },
+
    Reload,
+
    Loaded(Vec<Patch>),
    Exit { operation: Option<PatchOperation> },
    Quit,
}
@@ -169,6 +234,8 @@ pub struct AppState {
    show_search: bool,
    help: TextViewState,
    filter: PatchFilter,
+
    loading: bool,
+
    initialized: bool,
}

#[derive(Clone, Debug)]
@@ -181,11 +248,12 @@ impl TryFrom<&Context> for App {
    type Error = anyhow::Error;

    fn try_from(context: &Context) -> Result<Self, Self::Error> {
-
        let cache = &context.profile.patches(&context.repository)?;
+
        let repo = &context.profile.storage.repository(context.rid)?;
+
        let cache = &context.profile.patches(repo)?;
        let mut patches = cache
            .list()?
            .filter_map(|patch| patch.ok())
-
            .flat_map(|patch| Patch::new(&context.profile, &context.repository, patch.clone()).ok())
+
            .flat_map(|patch| Patch::without_stats(&context.profile, patch.clone()).ok())
            .collect::<Vec<_>>();
        patches.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));

@@ -224,6 +292,8 @@ impl TryFrom<&Context> for App {
                show_search: false,
                help: TextViewState::new(Position::default()),
                filter,
+
                loading: false,
+
                initialized: false,
            },
        })
    }
@@ -234,6 +304,21 @@ impl store::Update<Message> for App {

    fn update(&mut self, message: Message) -> Option<tui::Exit<Selection>> {
        match message {
+
            Message::Initialize => {
+
                self.state.loading = true;
+
                self.state.initialized = true;
+
                None
+
            }
+
            Message::Reload => {
+
                self.state.loading = true;
+
                None
+
            }
+
            Message::Loaded(patches) => {
+
                self.apply_patches(patches);
+
                self.apply_sorting();
+
                self.state.loading = false;
+
                None
+
            }
            Message::Quit => Some(Exit { value: None }),
            Message::Exit { operation } => Some(Exit {
                value: Some(Selection {
@@ -293,6 +378,11 @@ impl store::Update<Message> for App {
impl Show<Message> for App {
    fn show(&self, ctx: &ui::Context<Message>, frame: &mut Frame) -> Result<()> {
        Window::default().show(ctx, |ui| {
+
            // Initialize
+
            if !self.state.initialized {
+
                ui.send_message(Message::Initialize);
+
            }
+

            match self.state.page {
                Page::Main => {
                    let show_search = self.state.show_search;
@@ -404,6 +494,10 @@ impl App {
                        state: TableState::new(selected),
                    }));
                }
+

+
                if self.state.loading {
+
                    self.show_loading_popup(frame, ui);
+
                }
            },
        );

@@ -411,6 +505,9 @@ impl App {
        if ui.has_input(|key| key == Key::Char('/')) {
            ui.send_message(Message::ShowSearch);
        }
+
        if ui.has_input(|key| key == Key::Char('r')) {
+
            ui.send_message(Message::Reload);
+
        }

        if let Ok(args) = OperationArguments::try_from((&patches, &self.state)) {
            if ui.has_input(|key| key == Key::Enter) {
@@ -621,6 +718,7 @@ impl App {
                ("c", "checkout"),
                ("d", "diff"),
                ("/", "search"),
+
                ("r", "reload"),
                ("?", "help"),
            ],
            '∙',
@@ -628,6 +726,38 @@ impl App {
        );
    }

+
    fn show_loading_popup(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
+
        ui.popup(Layout::vertical([Constraint::Min(1)]), |ui| {
+
            ui.layout(
+
                Layout::vertical([Constraint::Min(1), Constraint::Length(3)]).margin(1),
+
                None,
+
                |ui| {
+
                    ui.label(frame, "");
+
                    ui.layout(
+
                        Layout::horizontal([Constraint::Min(1), Constraint::Length(11)]),
+
                        None,
+
                        |ui| {
+
                            ui.label(frame, "");
+
                            ui.layout(Layout::vertical([Constraint::Min(1)]), None, |ui| {
+
                                frame.render_widget(Clear, ui.area());
+
                                ui.column_bar(
+
                                    frame,
+
                                    [Column::new(
+
                                        Span::raw(" Loading ").magenta().rapid_blink(),
+
                                        Constraint::Fill(1),
+
                                    )]
+
                                    .to_vec(),
+
                                    Spacing::from(0),
+
                                    Some(Borders::All),
+
                                );
+
                            });
+
                        },
+
                    );
+
                },
+
            );
+
        });
+
    }
+

    fn show_help_text(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        ui.column_bar(
            frame,
@@ -675,3 +805,15 @@ impl App {
        );
    }
}
+

+
impl App {
+
    fn apply_patches(&mut self, patches: Vec<Patch>) {
+
        let mut items = self.patches.lock().unwrap();
+
        *items = patches;
+
    }
+

+
    fn apply_sorting(&mut self) {
+
        let mut items = self.patches.lock().unwrap();
+
        items.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
+
    }
+
}
modified bin/ui/items/patch.rs
@@ -52,9 +52,9 @@ pub struct Patch {
    /// Head of the latest revision.
    pub head: Oid,
    /// Lines added by the latest revision.
-
    pub added: u16,
+
    pub added: Option<usize>,
    /// Lines removed by the latest revision.
-
    pub removed: u16,
+
    pub removed: Option<usize>,
    /// Time when patch was opened.
    pub timestamp: Timestamp,
}
@@ -76,8 +76,27 @@ impl Patch {
            title: patch.title().into(),
            author: AuthorItem::new(Some(*patch.author().id), profile),
            head: revision.head(),
-
            added: stats.insertions() as u16,
-
            removed: stats.deletions() as u16,
+
            added: Some(stats.insertions()),
+
            removed: Some(stats.deletions()),
+
            timestamp: patch.updated_at(),
+
        })
+
    }
+

+
    pub fn without_stats(
+
        profile: &Profile,
+
        patch: (PatchId, radicle::patch::Patch),
+
    ) -> Result<Self, anyhow::Error> {
+
        let (id, patch) = patch;
+
        let (_, revision) = patch.latest();
+

+
        Ok(Self {
+
            id,
+
            state: patch.state().clone(),
+
            title: patch.title().into(),
+
            author: AuthorItem::new(Some(*patch.author().id), profile),
+
            head: revision.head(),
+
            added: None,
+
            removed: None,
            timestamp: patch.updated_at(),
        })
    }
@@ -110,8 +129,16 @@ impl ToRow<9> for Patch {
        };

        let head = span::ternary(&format::oid(self.head));
-
        let added = span::positive(&format!("+{}", self.added));
-
        let removed = span::negative(&format!("-{}", self.removed));
+
        let added = span::positive(&format!(
+
            "+{}",
+
            self.added.map(|a| a.to_string()).unwrap_or("?".to_string())
+
        ));
+
        let removed = span::negative(&format!(
+
            "+{}",
+
            self.removed
+
                .map(|r| r.to_string())
+
                .unwrap_or("?".to_string())
+
        ));
        let updated = span::timestamp(&format::timestamp(&self.timestamp));

        [
modified src/lib.rs
@@ -4,7 +4,6 @@ pub mod task;
pub mod terminal;
pub mod ui;

-
use std::any::Any;
use std::fmt::Debug;

use anyhow::Result;
@@ -78,66 +77,6 @@ pub trait Share: Clone + Debug + Send + Sync + 'static {}
/// traits.
impl<T: Clone + Debug + Send + Sync + 'static> Share for T {}

-
/// Provide implementations for conversions to and from `Box<dyn Any>`.
-
pub trait BoxedAny {
-
    fn from_boxed_any(any: Box<dyn Any>) -> Option<Self>
-
    where
-
        Self: Sized + Clone + 'static;
-

-
    fn to_boxed_any(self) -> Box<dyn Any>
-
    where
-
        Self: Sized + Clone + 'static;
-
}
-

-
impl<T> BoxedAny for T
-
where
-
    T: Sized + Clone + 'static,
-
{
-
    fn from_boxed_any(any: Box<dyn Any>) -> Option<Self>
-
    where
-
        Self: Sized + Clone + 'static,
-
    {
-
        any.downcast::<Self>().ok().map(|b| *b)
-
    }
-

-
    fn to_boxed_any(self) -> Box<dyn Any>
-
    where
-
        Self: Sized + Clone + 'static,
-
    {
-
        Box::new(self)
-
    }
-
}
-

-
/// A 'PageStack' for applications. Page identifier can be pushed to and
-
/// popped from the stack.
-
#[derive(Clone, Default, Debug)]
-
pub struct PageStack<T> {
-
    pages: Vec<T>,
-
}
-

-
impl<T> PageStack<T> {
-
    pub fn new(pages: Vec<T>) -> Self {
-
        Self { pages }
-
    }
-

-
    pub fn push(&mut self, page: T) {
-
        self.pages.push(page);
-
    }
-

-
    pub fn pop(&mut self) -> Option<T> {
-
        self.pages.pop()
-
    }
-

-
    pub fn peek(&self) -> Result<&T> {
-
        match self.pages.last() {
-
            Some(page) => Ok(page),
-
            None => Err(anyhow::anyhow!(
-
                "Could not peek active page. Page stack is empty."
-
            )),
-
        }
-
    }
-
}
-

/// A multi-producer, multi-consumer message channel.
pub struct Channel<M> {
    pub tx: broadcast::Sender<M>,