Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
lib: Fix realm apps w/ new common module
Erik Kundt committed 2 years ago
commit e8f3179494c31d1a277bfb26f30a70c2a26f0db1
parent a1775f79e254d42c4c1b5a2eabc9656947fd1cd5
12 files changed +123 -475
modified Cargo.toml
@@ -11,7 +11,7 @@ name = "rad-tui"
path = "bin/main.rs"

[features]
-
default = ["flux"]
+
default = ["realm"]
realm = ["dep:tuirealm", "dep:tui-realm-stdlib"]
flux = ["dep:tokio", "dep:tokio-stream"]

modified bin/commands/inbox.rs
@@ -1,6 +1,3 @@
-
#[cfg(feature = "flux")]
-
#[path = "inbox/flux.rs"]
-
mod flux;
#[cfg(feature = "realm")]
#[path = "inbox/realm.rs"]
mod realm;
@@ -15,8 +12,6 @@ use anyhow::anyhow;
use radicle_tui as tui;

use tui::common::cob::inbox::{self};
-
use tui::common::context;
-
use tui::common::log;

use crate::terminal;
use crate::terminal::args::{Args, Error, Help};
@@ -131,6 +126,8 @@ impl Args for Options {
#[cfg(feature = "realm")]
pub fn run(options: Options, _ctx: impl terminal::Context) -> anyhow::Result<()> {
    use tui::realm::Window;
+
    use tui::common::context;
+
    use tui::common::log;

    pub const FPS: u64 = 60;
    let (_, id) = radicle::rad::cwd()
@@ -163,23 +160,10 @@ pub fn run(options: Options, _ctx: impl terminal::Context) -> anyhow::Result<()>
}

#[cfg(feature = "flux")]
-
#[tokio::main]
-
pub async fn run(options: Options, _ctx: impl terminal::Context) -> anyhow::Result<()> {
-
    let (_, id) = radicle::rad::cwd()
-
        .map_err(|_| anyhow!("this command must be run in the context of a project"))?;
-

+
pub fn run(options: Options, _ctx: impl terminal::Context) -> anyhow::Result<()> {
    match options.op {
-
        Operation::Select { opts } => {
-
            let profile = terminal::profile()?;
-
            let context = context::Context::new(profile, id)?;
-

-
            log::enable(context.profile(), "inbox", "select")?;
-

-
            let _ = flux::select::App::new(context, opts.filter.clone())
-
                .run()
-
                .await;
+
        Operation::Select { opts: _ } => {
+
            anyhow::bail!("operation not yet implemented with flux")
        }
    }
-

-
    Ok(())
}
deleted bin/commands/inbox/flux.rs
@@ -1,2 +0,0 @@
-
#[path = "flux/select.rs"]
-
pub mod select;
deleted bin/commands/inbox/flux/select.rs
@@ -1,102 +0,0 @@
-
#[path = "select/ui.rs"]
-
mod ui;
-

-
use anyhow::Result;
-

-
use radicle::node::notifications::NotificationId;
-

-
use radicle_tui as tui;
-

-
use tui::common::cob::inbox::{self, Filter};
-
use tui::common::context::Context;
-
use tui::flux::store::{State, Store};
-
use tui::flux::termination::{self, Interrupted};
-
use tui::flux::ui::cob::NotificationItem;
-
use tui::flux::ui::Frontend;
-
use tui::Exit;
-

-
use ui::ListPage;
-

-
pub struct App {
-
    context: Context,
-
    _filter: Filter,
-
}
-

-
#[derive(Clone, Debug)]
-
pub struct InboxState {
-
    notifications: Vec<NotificationItem>,
-
    selected: Option<NotificationId>,
-
}
-

-
impl TryFrom<&Context> for InboxState {
-
    type Error = anyhow::Error;
-

-
    fn try_from(context: &Context) -> Result<Self, Self::Error> {
-
        let notifications = inbox::all(context.repository(), context.profile())?;
-
        let mut items = vec![];
-

-
        for notif in &notifications {
-
            if let Ok(notif) = NotificationItem::try_from((context.repository(), notif)) {
-
                items.push(notif);
-
            }
-
        }
-

-
        Ok(Self {
-
            notifications: items,
-
            selected: None,
-
        })
-
    }
-
}
-

-
pub enum Action {
-
    Exit,
-
    Select(NotificationId),
-
}
-

-
impl State<Action> for InboxState {
-
    type Exit = Exit<String>;
-

-
    fn tick(&self) {}
-

-
    fn handle_action(&mut self, action: Action) -> Option<Exit<String>> {
-
        match action {
-
            Action::Select(id) => {
-
                self.selected = Some(id);
-
                None
-
            }
-
            Action::Exit => Some(Exit { value: None }),
-
        }
-
    }
-
}
-

-
impl App {
-
    pub fn new(context: Context, filter: Filter) -> Self {
-
        Self {
-
            context,
-
            _filter: filter,
-
        }
-
    }
-

-
    pub async fn run(&self) -> Result<()> {
-
        let (terminator, mut interrupt_rx) = termination::create_termination();
-
        let (store, state_rx) = Store::<Action, InboxState>::new();
-
        let (frontend, action_rx) = Frontend::<Action>::new();
-
        let state = InboxState::try_from(&self.context)?;
-

-
        tokio::try_join!(
-
            store.main_loop(state, terminator, action_rx, interrupt_rx.resubscribe()),
-
            frontend.main_loop::<InboxState, ListPage>(state_rx, interrupt_rx.resubscribe()),
-
        )?;
-

-
        if let Ok(reason) = interrupt_rx.recv().await {
-
            match reason {
-
                Interrupted::UserInt => {}
-
                Interrupted::OsSigInt => println!("exited because of an os sig int"),
-
            }
-
        } else {
-
            println!("exited because of an unexpected error");
-
        }
-

-
        Ok(())
-
    }
-
}
deleted bin/commands/inbox/flux/select/ui.rs
@@ -1,228 +0,0 @@
-
use tokio::sync::mpsc::UnboundedSender;
-

-
use termion::event::Key;
-

-
use ratatui::backend::Backend;
-
use ratatui::layout::{Constraint, Rect};
-
use ratatui::widgets::Cell;
-

-
use radicle_tui as tui;
-

-
use tui::flux::ui::cob::NotificationItem;
-
use tui::flux::ui::span;
-
use tui::flux::ui::widget::{
-
    FooterProps, Render, Shortcut, Shortcuts, ShortcutsProps, Table, TableProps, Widget,
-
};
-

-
use super::{Action, InboxState};
-

-
pub struct ListPage {
-
    /// Action sender
-
    pub action_tx: UnboundedSender<Action>,
-
    /// notification widget
-
    notifications: Notifications,
-
    /// Shortcut widget
-
    shortcuts: Shortcuts<Action>,
-
}
-

-
impl Widget<InboxState, Action> for ListPage {
-
    fn new(state: &InboxState, action_tx: UnboundedSender<Action>) -> Self
-
    where
-
        Self: Sized,
-
    {
-
        Self {
-
            action_tx: action_tx.clone(),
-
            notifications: Notifications::new(state, action_tx.clone()),
-
            shortcuts: Shortcuts::new(state, action_tx.clone()),
-
        }
-
        .move_with_state(state)
-
    }
-

-
    fn move_with_state(self, state: &InboxState) -> Self
-
    where
-
        Self: Sized,
-
    {
-
        ListPage {
-
            notifications: self.notifications.move_with_state(state),
-
            shortcuts: self.shortcuts.move_with_state(state),
-
            ..self
-
        }
-
    }
-

-
    fn name(&self) -> &str {
-
        "list-page"
-
    }
-

-
    fn handle_key_event(&mut self, key: termion::event::Key) {
-
        match key {
-
            Key::Char('q') => {
-
                let _ = self.action_tx.send(Action::Exit);
-
            }
-
            _ => {
-
                <Notifications as Widget<InboxState, Action>>::handle_key_event(
-
                    &mut self.notifications,
-
                    key,
-
                );
-
            }
-
        }
-
    }
-
}
-

-
impl Render<()> for ListPage {
-
    fn render<B: Backend>(&mut self, frame: &mut ratatui::Frame, _area: Rect, _props: ()) {
-
        let area = frame.size();
-
        let layout = tui::flux::ui::layout::default_page(area, 0u16, 1u16);
-

-
        self.notifications.render::<B>(frame, layout.component, ());
-
        self.shortcuts.render::<B>(
-
            frame,
-
            layout.shortcuts,
-
            ShortcutsProps {
-
                shortcuts: vec![Shortcut::new("enter", "select"), Shortcut::new("q", "quit")],
-
                divider: '∙',
-
            },
-
        );
-
    }
-
}
-

-
struct NotificationsProps {
-
    notifications: Vec<NotificationItem>,
-
}
-

-
impl From<&InboxState> for NotificationsProps {
-
    fn from(state: &InboxState) -> Self {
-
        Self {
-
            notifications: state.notifications.clone(),
-
        }
-
    }
-
}
-

-
struct Notifications {
-
    /// Sending actions to the state store
-
    action_tx: UnboundedSender<Action>,
-
    /// State Mapped RoomList Props
-
    props: NotificationsProps,
-
    /// Notification table
-
    table: Table<Action>,
-
}
-

-
impl Widget<InboxState, Action> for Notifications {
-
    fn new(state: &InboxState, action_tx: UnboundedSender<Action>) -> Self {
-
        Self {
-
            action_tx: action_tx.clone(),
-
            props: NotificationsProps::from(state),
-
            table: Table::new(state, action_tx.clone()),
-
        }
-
    }
-

-
    fn move_with_state(self, state: &InboxState) -> Self
-
    where
-
        Self: Sized,
-
    {
-
        Self {
-
            props: NotificationsProps::from(state),
-
            table: self.table.move_with_state(state),
-
            ..self
-
        }
-
    }
-

-
    fn name(&self) -> &str {
-
        "notification-list"
-
    }
-

-
    fn handle_key_event(&mut self, key: Key) {
-
        match key {
-
            Key::Up => {
-
                self.table.prev();
-

-
                let selected = self
-
                    .table
-
                    .selected()
-
                    .and_then(|selected| self.props.notifications.get(selected));
-

-
                // TODO: propagate error
-
                if let Some(notif) = selected {
-
                    let _ = self.action_tx.send(Action::Select(notif.id));
-
                }
-
            }
-
            Key::Down => {
-
                self.table.next(self.props.notifications.len());
-

-
                let selected = self
-
                    .table
-
                    .selected()
-
                    .and_then(|selected| self.props.notifications.get(selected));
-

-
                // TODO: propagate error
-
                if let Some(notif) = selected {
-
                    let _ = self.action_tx.send(Action::Select(notif.id));
-
                }
-
            }
-
            _ => {}
-
        }
-
    }
-
}
-

-
impl Render<()> for Notifications {
-
    fn render<B: Backend>(&mut self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
-
        let header: [Cell; 7] = [
-
            String::from("").into(),
-
            String::from(" ● ").into(),
-
            String::from("Type").into(),
-
            String::from("Summary").into(),
-
            String::from("ID").into(),
-
            String::from("Status").into(),
-
            String::from("Updated").into(),
-
        ];
-

-
        let widths = [
-
            Constraint::Length(5),
-
            Constraint::Length(3),
-
            Constraint::Length(6),
-
            Constraint::Fill(1),
-
            Constraint::Length(15),
-
            Constraint::Length(10),
-
            Constraint::Length(18),
-
        ];
-

-
        let progress = {
-
            let step = self
-
                .table
-
                .selected()
-
                .map(|selected| selected.saturating_add(1).to_string())
-
                .unwrap_or("-".to_string());
-
            let length = self.props.notifications.len().to_string();
-

-
            span::badge(format!("{}/{}", step, length))
-
        };
-

-
        let footer = FooterProps {
-
            cells: [
-
                span::badge("/".to_string()),
-
                String::from("").into(),
-
                String::from("").into(),
-
                progress.clone(),
-
            ]
-
            .to_vec(),
-
            widths: [
-
                Constraint::Length(3),
-
                Constraint::Fill(1),
-
                Constraint::Fill(1),
-
                Constraint::Length(progress.width() as u16),
-
            ]
-
            .to_vec(),
-
        };
-

-
        self.table.render::<B>(
-
            frame,
-
            area,
-
            TableProps {
-
                items: self.props.notifications.to_vec(),
-
                focus: false,
-
                widths,
-
                header,
-
                footer: Some(footer),
-
            },
-
        );
-
    }
-
}
modified bin/commands/inbox/realm/select.rs
@@ -10,7 +10,8 @@ use std::hash::Hash;

use anyhow::Result;

-
use serde::{Serialize, Serializer};
+
use radicle::node::notifications::NotificationId;
+
use serde::Serialize;

use tuirealm::application::PollStrategy;
use tuirealm::{Application, Frame, NoUserEvent, Sub, SubClause};
@@ -22,37 +23,13 @@ use tui::common::context::Context;
use tui::realm::ui::subscription;
use tui::realm::ui::theme::Theme;
use tui::realm::{PageStack, Tui};
-
use tui::{Exit, SelectionExit};
+
use tui::Exit;

use page::ListView;

use super::super::common::Mode;

-
/// Wrapper around radicle's `PatchId` that serializes
-
/// to a human-readable string.
-
#[derive(Clone, Debug, Eq, PartialEq)]
-
pub struct PatchId(radicle::cob::patch::PatchId);
-

-
impl From<radicle::cob::patch::PatchId> for PatchId {
-
    fn from(value: radicle::cob::patch::PatchId) -> Self {
-
        PatchId(value)
-
    }
-
}
-

-
impl Display for PatchId {
-
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-
        write!(f, "{}", self.0)
-
    }
-
}
-

-
impl Serialize for PatchId {
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        serializer.serialize_str(&format!("{}", *self.0))
-
    }
-
}
+
type Selection = tui::Selection<NotificationId>;

/// The selected issue operation returned by the operation
/// selection widget.
@@ -94,7 +71,7 @@ pub enum Cid {
pub enum Message {
    #[default]
    Tick,
-
    Quit(Option<SelectionExit>),
+
    Quit(Option<Selection>),
    Batch(Vec<Message>),
}

@@ -106,7 +83,7 @@ pub struct App {
    filter: Filter,
    sort_by: SortBy,
    quit: bool,
-
    output: Option<SelectionExit>,
+
    output: Option<Selection>,
}

/// Creates a new application using a tui-realm-application, mounts all
@@ -174,7 +151,7 @@ impl App {
    }
}

-
impl Tui<Cid, Message, SelectionExit> for App {
+
impl Tui<Cid, Message, Selection> for App {
    fn init(&mut self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
        self.view_list(app, &self.theme.clone())?;

@@ -213,7 +190,7 @@ impl Tui<Cid, Message, SelectionExit> for App {
        }
    }

-
    fn exit(&self) -> Option<Exit<SelectionExit>> {
+
    fn exit(&self) -> Option<Exit<Selection>> {
        if self.quit {
            return Some(Exit {
                value: self.output.clone(),
modified bin/commands/inbox/realm/select/event.rs
@@ -11,11 +11,12 @@ use tui::realm::ui::widget::container::{AppHeader, GlobalListener, LabeledContai
use tui::realm::ui::widget::context::{ContextBar, Shortcuts};
use tui::realm::ui::widget::list::PropertyList;
use tui::realm::ui::widget::Widget;
-
use tui::{Id, SelectionExit};

use super::ui::{IdSelect, OperationSelect};
use super::{InboxOperation, Message};

+
type Selection = tui::Selection<NotificationId>;
+

/// Since the framework does not know the type of messages that are being
/// passed around in the app, the following handlers need to be implemented for
/// each component used.
@@ -68,8 +69,12 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<IdSelect> {
            Event::Keyboard(KeyEvent {
                code: Key::Enter, ..
            }) => submit().map(|id| {
-
                let output = SelectionExit::default().with_id(Id::Notification(id));
-
                Message::Quit(Some(output))
+
                let selection = Selection {
+
                    operation: None,
+
                    ids: vec![id],
+
                    args: vec![],
+
                };
+
                Message::Quit(Some(selection))
            }),
            _ => None,
        }
@@ -111,19 +116,23 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<OperationSelect> {
            Event::Keyboard(KeyEvent {
                code: Key::Enter, ..
            }) => submit().map(|id| {
-
                let exit = SelectionExit::default()
-
                    .with_operation(InboxOperation::Show.to_string())
-
                    .with_id(Id::Notification(id));
-
                Message::Quit(Some(exit))
+
                let selection = Selection {
+
                    operation: Some(InboxOperation::Show.to_string()),
+
                    ids: vec![id],
+
                    args: vec![],
+
                };
+
                Message::Quit(Some(selection))
            }),
            Event::Keyboard(KeyEvent {
                code: Key::Char('c'),
                ..
            }) => submit().map(|id| {
-
                let exit = SelectionExit::default()
-
                    .with_operation(InboxOperation::Clear.to_string())
-
                    .with_id(Id::Notification(id));
-
                Message::Quit(Some(exit))
+
                let selection = Selection {
+
                    operation: Some(InboxOperation::Clear.to_string()),
+
                    ids: vec![id],
+
                    args: vec![],
+
                };
+
                Message::Quit(Some(selection))
            }),
            _ => None,
        }
modified bin/commands/issue/realm/select.rs
@@ -9,7 +9,8 @@ use std::fmt::Display;
use std::hash::Hash;

use anyhow::Result;
-
use serde::{Serialize, Serializer};
+
use radicle::issue::IssueId;
+
use serde::Serialize;

use tuirealm::application::PollStrategy;
use tuirealm::event::Key;
@@ -23,37 +24,13 @@ use tui::common::context::Context;
use tui::realm::ui::subscription;
use tui::realm::ui::theme::Theme;
use tui::realm::{PageStack, Tui};
-
use tui::{Exit, SelectionExit};
+
use tui::Exit;

use page::ListView;

use super::super::common::Mode;

-
/// Wrapper around radicle's `PatchId` that serializes
-
/// to a human-readable string.
-
#[derive(Clone, Debug, Eq, PartialEq)]
-
pub struct PatchId(radicle::cob::patch::PatchId);
-

-
impl From<radicle::cob::patch::PatchId> for PatchId {
-
    fn from(value: radicle::cob::patch::PatchId) -> Self {
-
        PatchId(value)
-
    }
-
}
-

-
impl Display for PatchId {
-
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-
        write!(f, "{}", self.0)
-
    }
-
}
-

-
impl Serialize for PatchId {
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        serializer.serialize_str(&format!("{}", *self.0))
-
    }
-
}
+
type Selection = tui::Selection<IssueId>;

/// The selected issue operation returned by the operation
/// selection widget.
@@ -104,7 +81,7 @@ pub enum Cid {
pub enum Message {
    #[default]
    Tick,
-
    Quit(Option<SelectionExit>),
+
    Quit(Option<Selection>),
    Batch(Vec<Message>),
}

@@ -115,7 +92,7 @@ pub struct App {
    quit: bool,
    mode: Mode,
    filter: Filter,
-
    output: Option<SelectionExit>,
+
    output: Option<Selection>,
}

/// Creates a new application using a tui-realm-application, mounts all
@@ -177,7 +154,7 @@ impl App {
    }
}

-
impl Tui<Cid, Message, SelectionExit> for App {
+
impl Tui<Cid, Message, Selection> for App {
    fn init(&mut self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
        self.view_list(app, &self.theme.clone())?;

@@ -216,7 +193,7 @@ impl Tui<Cid, Message, SelectionExit> for App {
        }
    }

-
    fn exit(&self) -> Option<Exit<SelectionExit>> {
+
    fn exit(&self) -> Option<Exit<Selection>> {
        if self.quit {
            return Some(Exit {
                value: self.output.clone(),
modified bin/commands/issue/realm/select/event.rs
@@ -1,3 +1,4 @@
+
use radicle::issue::IssueId;
use tuirealm::command::{Cmd, CmdResult, Direction as MoveDirection};
use tuirealm::event::{Event, Key, KeyEvent};
use tuirealm::{MockComponent, NoUserEvent};
@@ -9,11 +10,12 @@ use tui::realm::ui::widget::container::{AppHeader, GlobalListener, LabeledContai
use tui::realm::ui::widget::context::{ContextBar, Shortcuts};
use tui::realm::ui::widget::list::PropertyList;
use tui::realm::ui::widget::Widget;
-
use tui::{Id, SelectionExit};

use super::ui::{IdSelect, OperationSelect};
use super::{IssueOperation, Message};

+
type Selection = tui::Selection<IssueId>;
+

/// Since the framework does not know the type of messages that are being
/// passed around in the app, the following handlers need to be implemented for
/// each component used.
@@ -66,8 +68,12 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<IdSelect> {
            Event::Keyboard(KeyEvent {
                code: Key::Enter, ..
            }) => submit().map(|id| {
-
                let output = SelectionExit::default().with_id(Id::Object(id));
-
                Message::Quit(Some(output))
+
                let selection = Selection {
+
                    operation: None,
+
                    ids: vec![id],
+
                    args: vec![],
+
                };
+
                Message::Quit(Some(selection))
            }),
            _ => None,
        }
@@ -109,37 +115,45 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<OperationSelect> {
            Event::Keyboard(KeyEvent {
                code: Key::Enter, ..
            }) => submit().map(|id| {
-
                let exit = SelectionExit::default()
-
                    .with_operation(IssueOperation::Show.to_string())
-
                    .with_id(Id::Object(id));
-
                Message::Quit(Some(exit))
+
                let selection = Selection {
+
                    operation: Some(IssueOperation::Show.to_string()),
+
                    ids: vec![id],
+
                    args: vec![],
+
                };
+
                Message::Quit(Some(selection))
            }),
            Event::Keyboard(KeyEvent {
                code: Key::Char('d'),
                ..
            }) => submit().map(|id| {
-
                let exit = SelectionExit::default()
-
                    .with_operation(IssueOperation::Delete.to_string())
-
                    .with_id(Id::Object(id));
-
                Message::Quit(Some(exit))
+
                let selection = Selection {
+
                    operation: Some(IssueOperation::Delete.to_string()),
+
                    ids: vec![id],
+
                    args: vec![],
+
                };
+
                Message::Quit(Some(selection))
            }),
            Event::Keyboard(KeyEvent {
                code: Key::Char('e'),
                ..
            }) => submit().map(|id| {
-
                let exit = SelectionExit::default()
-
                    .with_operation(IssueOperation::Edit.to_string())
-
                    .with_id(Id::Object(id));
-
                Message::Quit(Some(exit))
+
                let selection = Selection {
+
                    operation: Some(IssueOperation::Edit.to_string()),
+
                    ids: vec![id],
+
                    args: vec![],
+
                };
+
                Message::Quit(Some(selection))
            }),
            Event::Keyboard(KeyEvent {
                code: Key::Char('m'),
                ..
            }) => submit().map(|id| {
-
                let exit = SelectionExit::default()
-
                    .with_operation(IssueOperation::Comment.to_string())
-
                    .with_id(Id::Object(id));
-
                Message::Quit(Some(exit))
+
                let selection = Selection {
+
                    operation: Some(IssueOperation::Comment.to_string()),
+
                    ids: vec![id],
+
                    args: vec![],
+
                };
+
                Message::Quit(Some(selection))
            }),
            _ => None,
        }
modified bin/commands/patch/realm/select.rs
@@ -9,6 +9,7 @@ use std::fmt::Display;
use std::hash::Hash;

use anyhow::Result;
+
use radicle::patch::PatchId;
use serde::Serialize;

use tuirealm::application::PollStrategy;
@@ -23,12 +24,14 @@ use tui::common::context::Context;
use tui::realm::ui::subscription;
use tui::realm::ui::theme::Theme;
use tui::realm::{PageStack, Tui};
-
use tui::{Exit, SelectionExit};
+
use tui::Exit;

use page::ListView;

use super::super::common::Mode;

+
type Selection = tui::Selection<PatchId>;
+

/// The selected patch operation returned by the operation
/// selection widget.
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
@@ -82,7 +85,7 @@ pub enum Cid {
pub enum Message {
    #[default]
    Tick,
-
    Quit(Option<SelectionExit>),
+
    Quit(Option<Selection>),
    Batch(Vec<Message>),
}

@@ -93,7 +96,7 @@ pub struct App {
    quit: bool,
    mode: Mode,
    filter: Filter,
-
    output: Option<SelectionExit>,
+
    output: Option<Selection>,
}

/// Creates a new application using a tui-realm-application, mounts all
@@ -155,7 +158,7 @@ impl App {
    }
}

-
impl Tui<Cid, Message, SelectionExit> for App {
+
impl Tui<Cid, Message, Selection> for App {
    fn init(&mut self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
        self.view_list(app, &self.theme.clone())?;

@@ -194,7 +197,7 @@ impl Tui<Cid, Message, SelectionExit> for App {
        }
    }

-
    fn exit(&self) -> Option<Exit<SelectionExit>> {
+
    fn exit(&self) -> Option<Exit<Selection>> {
        if self.quit {
            return Some(Exit {
                value: self.output.clone(),
modified bin/commands/patch/realm/select/event.rs
@@ -1,3 +1,4 @@
+
use radicle::patch::PatchId;
use tuirealm::command::{Cmd, CmdResult, Direction as MoveDirection};
use tuirealm::event::{Event, Key, KeyEvent};
use tuirealm::{MockComponent, NoUserEvent};
@@ -9,11 +10,12 @@ use tui::realm::ui::widget::container::{AppHeader, GlobalListener, LabeledContai
use tui::realm::ui::widget::context::{ContextBar, Shortcuts};
use tui::realm::ui::widget::list::PropertyList;
use tui::realm::ui::widget::Widget;
-
use tui::{Id, SelectionExit};

use super::ui::{IdSelect, OperationSelect};
use super::{Message, PatchOperation};

+
type Selection = tui::Selection<PatchId>;
+

/// Since the framework does not know the type of messages that are being
/// passed around in the app, the following handlers need to be implemented for
/// each component used.
@@ -66,8 +68,12 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<IdSelect> {
            Event::Keyboard(KeyEvent {
                code: Key::Enter, ..
            }) => submit().map(|id| {
-
                let output = SelectionExit::default().with_id(Id::Object(id));
-
                Message::Quit(Some(output))
+
                let selection = Selection {
+
                    operation: None,
+
                    ids: vec![id],
+
                    args: vec![],
+
                };
+
                Message::Quit(Some(selection))
            }),
            _ => None,
        }
@@ -109,46 +115,56 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<OperationSelect> {
            Event::Keyboard(KeyEvent {
                code: Key::Enter, ..
            }) => submit().map(|id| {
-
                let exit = SelectionExit::default()
-
                    .with_operation(PatchOperation::Show.to_string())
-
                    .with_id(Id::Object(id));
-
                Message::Quit(Some(exit))
+
                let selection = Selection {
+
                    operation: Some(PatchOperation::Show.to_string()),
+
                    ids: vec![id],
+
                    args: vec![],
+
                };
+
                Message::Quit(Some(selection))
            }),
            Event::Keyboard(KeyEvent {
                code: Key::Char('c'),
                ..
            }) => submit().map(|id| {
-
                let exit = SelectionExit::default()
-
                    .with_operation(PatchOperation::Checkout.to_string())
-
                    .with_id(Id::Object(id));
-
                Message::Quit(Some(exit))
+
                let selection = Selection {
+
                    operation: Some(PatchOperation::Checkout.to_string()),
+
                    ids: vec![id],
+
                    args: vec![],
+
                };
+
                Message::Quit(Some(selection))
            }),
            Event::Keyboard(KeyEvent {
                code: Key::Char('d'),
                ..
            }) => submit().map(|id| {
-
                let exit = SelectionExit::default()
-
                    .with_operation(PatchOperation::Delete.to_string())
-
                    .with_id(Id::Object(id));
-
                Message::Quit(Some(exit))
+
                let selection = Selection {
+
                    operation: Some(PatchOperation::Delete.to_string()),
+
                    ids: vec![id],
+
                    args: vec![],
+
                };
+
                Message::Quit(Some(selection))
            }),
            Event::Keyboard(KeyEvent {
                code: Key::Char('e'),
                ..
            }) => submit().map(|id| {
-
                let exit = SelectionExit::default()
-
                    .with_operation(PatchOperation::Edit.to_string())
-
                    .with_id(Id::Object(id));
-
                Message::Quit(Some(exit))
+
                let selection = Selection {
+
                    operation: Some(PatchOperation::Edit.to_string()),
+
                    ids: vec![id],
+
                    args: vec![],
+
                };
+
                Message::Quit(Some(selection))
            }),
            Event::Keyboard(KeyEvent {
                code: Key::Char('m'),
                ..
            }) => submit().map(|id| {
-
                let exit = SelectionExit::default()
-
                    .with_operation(PatchOperation::Comment.to_string())
-
                    .with_id(Id::Object(id));
-
                Message::Quit(Some(exit))
+
                let selection = Selection {
+
                    operation: Some(PatchOperation::Comment.to_string()),
+
                    ids: vec![id],
+
                    args: vec![],
+
                };
+
                Message::Quit(Some(selection))
            }),
            _ => None,
        }
modified src/flux/ui.rs
@@ -52,7 +52,7 @@ impl<A> Frontend<A> {
        let mut terminal = setup_terminal()?;
        let mut ticker = tokio::time::interval(RENDERING_TICK_RATE);
        let mut events_rx = events();
-
        
+

        let mut root = {
            let state = state_rx.recv().await.unwrap();