Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
bin: Adjust to new view props
Erik Kundt committed 1 year ago
commit e5cd71450b5cc70988d72fd5c70ac1ebe4273ca6
parent 517ba30300537902c6d5333170bae07b72e74501
4 files changed +215 -252
modified bin/commands/inbox/select/ui.rs
@@ -1,6 +1,7 @@
use std::collections::HashMap;
use std::str::FromStr;

+
use ratatui::Frame;
use tokio::sync::mpsc::UnboundedSender;

use termion::event::Key;
@@ -108,38 +109,33 @@ impl<'a> From<&State> for BrowserProps<'a> {
    }
}

-
pub struct Browser<'a> {
-
    /// Internal props
-
    props: BrowserProps<'a>,
+
pub struct Browser {
    /// Notification widget
    notifications: Widget,
    /// Search widget
    search: Widget,
}

-
impl<'a: 'static> Browser<'a> {
+
impl Browser {
    fn new(tx: UnboundedSender<Message>) -> Self {
-
        let props = BrowserProps::default();
-

        Self {
-
            props: props.clone(),
            notifications: Container::default()
                .header(
                    Header::default()
-
                        .columns(
-
                            [
-
                                Column::new("", Constraint::Length(0)),
-
                                Column::new(Text::from(props.header), Constraint::Fill(1)),
-
                            ]
-
                            .to_vec(),
-
                        )
-
                        .cutoff(props.cutoff, props.cutoff_after)
+
                        // .columns(
+
                        //     [
+
                        //         Column::new("", Constraint::Length(0)),
+
                        //         Column::new(Text::from(props.header), Constraint::Fill(1)),
+
                        //     ]
+
                        //     .to_vec(),
+
                        // )
+
                        // .cutoff(props.cutoff, props.cutoff_after)
                        .to_widget(tx.clone()),
                )
                .content(
                    Table::<State, Message, NotificationItem, 9>::default()
                        .to_widget(tx.clone())
-
                        .on_event(|s, _| {
+
                        .on_event(|_, s, _| {
                            Some(Message::Select {
                                selected: s.and_then(|s| s.unwrap_usize()),
                            })
@@ -178,23 +174,27 @@ impl<'a: 'static> Browser<'a> {
    }
}

-
impl<'a: 'static> View for Browser<'a> {
+
impl View for Browser {
    type Message = Message;
    type State = State;

-
    fn handle_event(&mut self, key: Key) -> Option<Message> {
-
        if self.props.show_search {
+
    fn handle_event(&mut self, props: Option<&ViewProps>, key: Key) -> Option<Message> {
+
        let default = BrowserProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<BrowserProps>())
+
            .unwrap_or(&default);
+

+
        if props.show_search {
            self.search.handle_event(key);
            None
        } else {
            match key {
                Key::Char('/') => Some(Message::OpenSearch),
-
                Key::Char('\n') => self
-
                    .props
+
                Key::Char('\n') => props
                    .selected
-
                    .and_then(|selected| self.props.notifications.get(selected))
+
                    .and_then(|selected| props.notifications.get(selected))
                    .map(|notif| {
-
                        let selection = match self.props.mode.selection() {
+
                        let selection = match props.mode.selection() {
                            SelectionMode::Operation => Selection::default()
                                .with_operation(InboxOperation::Show.to_string())
                                .with_id(notif.id),
@@ -205,10 +205,9 @@ impl<'a: 'static> View for Browser<'a> {
                            selection: Some(selection),
                        }
                    }),
-
                Key::Char('c') => self
-
                    .props
+
                Key::Char('c') => props
                    .selected
-
                    .and_then(|selected| self.props.notifications.get(selected))
+
                    .and_then(|selected| props.notifications.get(selected))
                    .map(|notif| Message::Exit {
                        selection: Some(
                            Selection::default()
@@ -224,28 +223,27 @@ impl<'a: 'static> View for Browser<'a> {
        }
    }

-
    fn update(&mut self, state: &Self::State, props: Option<ViewProps>) {
-
        if let Some(props) = props.and_then(|props| props.inner::<BrowserProps>()) {
-
            self.props = props;
-
        } else {
-
            self.props = BrowserProps::from(state);
-
        }
-

+
    fn update(&mut self, _props: Option<&ViewProps>, state: &Self::State) {
        self.notifications.update(state);
        self.search.update(state);
    }

-
    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
-
        if self.props.show_search {
+
    fn render(&self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
+
        let default = BrowserProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<BrowserProps>())
+
            .unwrap_or(&default);
+

+
        if props.show_search {
            let [table_area, search_area] =
-
                Layout::vertical([Constraint::Min(1), Constraint::Length(2)]).areas(props.area);
+
                Layout::vertical([Constraint::Min(1), Constraint::Length(2)]).areas(render.area);

            self.notifications
-
                .render(frame, RenderProps::from(table_area));
+
                .render(RenderProps::from(table_area), frame);
            self.search
-
                .render(frame, RenderProps::from(search_area).focus(props.focus));
+
                .render(RenderProps::from(search_area).focus(render.focus), frame);
        } else {
-
            self.notifications.render(frame, props);
+
            self.notifications.render(render, frame);
        }
    }
}
@@ -321,7 +319,7 @@ impl<'a: 'static> View for BrowserPage<'a> {
    type Message = Message;
    type State = State;

-
    fn handle_event(&mut self, key: Key) -> Option<Self::Message> {
+
    fn handle_event(&mut self, _props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
        self.sections.handle_event(key);

        if self.props.handle_keys {
@@ -335,31 +333,25 @@ impl<'a: 'static> View for BrowserPage<'a> {
        None
    }

-
    fn update(&mut self, state: &Self::State, props: Option<ViewProps>) {
-
        if let Some(props) = props.and_then(|props| props.inner::<BrowserPageProps>()) {
-
            self.props = props;
-
        } else {
-
            self.props = BrowserPageProps::from(state);
-
        }
-

+
    fn update(&mut self, _props: Option<&ViewProps>, state: &Self::State) {
        self.sections.update(state);
        self.shortcuts.update(state);
    }

-
    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
-
        let page_size = props.area.height.saturating_sub(6) as usize;
+
    fn render(&self, _props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
+
        let page_size = render.area.height.saturating_sub(6) as usize;

        let [content_area, shortcuts_area] =
-
            Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(props.area);
+
            Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(render.area);

        self.sections.render(
-
            frame,
            RenderProps::from(content_area)
                .layout(Layout::horizontal([Constraint::Min(1)]))
                .focus(true),
+
            frame,
        );
        self.shortcuts
-
            .render(frame, RenderProps::from(shortcuts_area));
+
            .render(RenderProps::from(shortcuts_area), frame);

        // TODO: Find better solution
        if page_size != self.props.page_size {
@@ -387,7 +379,7 @@ impl Search {
            props: SearchProps {},
            input: TextField::default()
                .to_widget(tx.clone())
-
                .on_event(|s, _| {
+
                .on_event(|_, s, _| {
                    Some(Message::UpdateSearch {
                        value: s.and_then(|i| i.unwrap_string()).unwrap_or_default(),
                    })
@@ -408,7 +400,7 @@ impl View for Search {
    type Message = Message;
    type State = State;

-
    fn handle_event(&mut self, key: termion::event::Key) -> Option<Self::Message> {
+
    fn handle_event(&mut self, _props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
        match key {
            Key::Esc => Some(Message::CloseSearch),
            Key::Char('\n') => Some(Message::ApplySearch),
@@ -419,20 +411,16 @@ impl View for Search {
        }
    }

-
    fn update(&mut self, state: &Self::State, props: Option<ViewProps>) {
-
        if let Some(props) = props.and_then(|props| props.inner::<SearchProps>()) {
-
            self.props = props;
-
        }
-

+
    fn update(&mut self, _props: Option<&ViewProps>, state: &Self::State) {
        self.input.update(state);
    }

-
    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
+
    fn render(&self, _props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
        let layout = Layout::horizontal(Constraint::from_mins([0]))
            .horizontal_margin(1)
-
            .split(props.area);
+
            .split(render.area);

-
        self.input.render(frame, RenderProps::from(layout[0]));
+
        self.input.render(RenderProps::from(layout[0]), frame);
    }
}

@@ -456,22 +444,19 @@ impl<'a> From<&State> for HelpPageProps<'a> {
    }
}

-
pub struct HelpPage<'a> {
-
    /// Internal props
-
    props: HelpPageProps<'a>,
+
pub struct HelpPage {
    /// Content widget
    content: Widget,
    /// Shortcut widget
    shortcuts: Widget,
}

-
impl<'a: 'static> HelpPage<'a> {
+
impl HelpPage {
    pub fn new(tx: UnboundedSender<Message>) -> Self
    where
        Self: Sized,
    {
        Self {
-
            props: HelpPageProps::default(),
            content: Container::default()
                .header(Header::default().to_widget(tx.clone()).on_update(|_| {
                    HeaderProps::default()
@@ -482,7 +467,7 @@ impl<'a: 'static> HelpPage<'a> {
                .content(
                    Paragraph::default()
                        .to_widget(tx.clone())
-
                        .on_event(|s, _| {
+
                        .on_event(|_, s, _| {
                            Some(Message::ScrollHelp {
                                progress: s.and_then(|p| p.unwrap_usize()).unwrap_or_default(),
                            })
@@ -527,11 +512,11 @@ impl<'a: 'static> HelpPage<'a> {
    }
}

-
impl<'a: 'static> View for HelpPage<'a> {
+
impl View for HelpPage {
    type Message = Message;
    type State = State;

-
    fn handle_event(&mut self, key: termion::event::Key) -> Option<Self::Message> {
+
    fn handle_event(&mut self, _props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
        match key {
            Key::Esc | Key::Ctrl('c') => Some(Message::Exit { selection: None }),
            Key::Char('?') => Some(Message::LeavePage),
@@ -542,30 +527,29 @@ impl<'a: 'static> View for HelpPage<'a> {
        }
    }

-
    fn update(&mut self, state: &Self::State, props: Option<ViewProps>) {
-
        if let Some(props) = props.and_then(|props| props.inner::<HelpPageProps>()) {
-
            self.props = props;
-
        } else {
-
            self.props = HelpPageProps::from(state);
-
        }
-

+
    fn update(&mut self, _props: Option<&ViewProps>, state: &Self::State) {
        self.content.update(state);
        self.shortcuts.update(state);
    }

-
    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
-
        let page_size = props.area.height.saturating_sub(6) as usize;
+
    fn render(&self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
+
        let default = HelpPageProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<HelpPageProps>())
+
            .unwrap_or(&default);
+

+
        let page_size = render.area.height.saturating_sub(6) as usize;

        let [content_area, shortcuts_area] =
-
            Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(props.area);
+
            Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(render.area);

        self.content
-
            .render(frame, RenderProps::from(content_area).focus(true));
+
            .render(RenderProps::from(content_area).focus(true), frame);
        self.shortcuts
-
            .render(frame, RenderProps::from(shortcuts_area));
+
            .render(RenderProps::from(content_area).focus(true), frame);

        // TODO: Find better solution
-
        if page_size != self.props.page_size {
+
        if page_size != props.page_size {
            self.content.send(Message::HelpPageSize(page_size));
        }
    }
modified bin/commands/issue/select/ui.rs
@@ -3,6 +3,7 @@ use std::str::FromStr;
use std::vec;

use radicle::issue::{self, CloseReason};
+
use ratatui::Frame;
use tokio::sync::mpsc::UnboundedSender;

use termion::event::Key;
@@ -128,33 +129,29 @@ impl<'a> From<&State> for BrowserProps<'a> {
    }
}

-
pub struct Browser<'a> {
-
    /// Internal props
-
    props: BrowserProps<'a>,
+
pub struct Browser {
    /// Notifications widget
    issues: Widget,
    /// Search widget
    search: Widget,
}

-
impl<'a: 'static> Browser<'a> {
+
impl Browser {
    fn new(tx: UnboundedSender<Message>) -> Self {
-
        let props = BrowserProps::default();
        Self {
-
            props: props.clone(),
            issues: Container::default()
                .header(
                    Header::default()
-
                        .columns(props.header.clone())
-
                        .cutoff(props.cutoff, props.cutoff_after)
+
                        // .columns(props.header.clone())
+
                        // .cutoff(props.cutoff, props.cutoff_after)
                        .to_widget(tx.clone()),
                )
                .content(
                    Table::<State, Message, IssueItem, 8>::default()
                        .to_widget(tx.clone())
-
                        .on_event(|state, _| {
+
                        .on_event(|_, s, _| {
                            Some(Message::Select {
-
                                selected: state.and_then(|s| s.unwrap_usize()),
+
                                selected: s.and_then(|s| s.unwrap_usize()),
                            })
                        })
                        .on_update(|state| {
@@ -191,26 +188,31 @@ impl<'a: 'static> Browser<'a> {
    }
}

-
impl<'a: 'static> View for Browser<'a> {
+
impl View for Browser {
    type Message = Message;
    type State = State;

-
    fn handle_event(&mut self, key: Key) -> Option<Self::Message> {
-
        if self.props.show_search {
+
    fn handle_event(&mut self, props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
+
        let default = BrowserProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<BrowserProps>())
+
            .unwrap_or(&default);
+

+
        if props.show_search {
            self.search.handle_event(key);
            None
        } else {
            match key {
                Key::Char('/') => Some(Message::OpenSearch),
                Key::Char('\n') => {
-
                    let operation = match self.props.mode {
+
                    let operation = match props.mode {
                        Mode::Operation => Some(IssueOperation::Show.to_string()),
                        Mode::Id => None,
                    };

-
                    self.props
+
                    props
                        .selected
-
                        .and_then(|selected| self.props.issues.get(selected))
+
                        .and_then(|selected| props.issues.get(selected))
                        .map(|issue| Message::Exit {
                            selection: Some(Selection {
                                operation,
@@ -219,10 +221,9 @@ impl<'a: 'static> View for Browser<'a> {
                            }),
                        })
                }
-
                Key::Char('e') => self
-
                    .props
+
                Key::Char('e') => props
                    .selected
-
                    .and_then(|selected| self.props.issues.get(selected))
+
                    .and_then(|selected| props.issues.get(selected))
                    .map(|issue| Message::Exit {
                        selection: Some(Selection {
                            operation: Some(IssueOperation::Edit.to_string()),
@@ -238,27 +239,26 @@ impl<'a: 'static> View for Browser<'a> {
        }
    }

-
    fn update(&mut self, state: &Self::State, props: Option<ViewProps>) {
-
        if let Some(props) = props.and_then(|props| props.inner::<BrowserProps>()) {
-
            self.props = props;
-
        } else {
-
            self.props = BrowserProps::from(state);
-
        }
-

+
    fn update(&mut self, _props: Option<&ViewProps>, state: &Self::State) {
        self.issues.update(state);
        self.search.update(state);
    }

-
    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
-
        if self.props.show_search {
+
    fn render(&self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
+
        let default = BrowserProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<BrowserProps>())
+
            .unwrap_or(&default);
+

+
        if props.show_search {
            let [table_area, search_area] =
-
                Layout::vertical([Constraint::Min(1), Constraint::Length(2)]).areas(props.area);
+
                Layout::vertical([Constraint::Min(1), Constraint::Length(2)]).areas(render.area);

-
            self.issues.render(frame, RenderProps::from(table_area));
+
            self.issues.render(RenderProps::from(table_area), frame);
            self.search
-
                .render(frame, RenderProps::from(search_area).focus(props.focus));
+
                .render(RenderProps::from(search_area).focus(render.focus), frame);
        } else {
-
            self.issues.render(frame, props);
+
            self.issues.render(render, frame);
        }
    }
}
@@ -334,7 +334,7 @@ impl<'a: 'static> View for BrowserPage<'a> {
    type Message = Message;
    type State = State;

-
    fn handle_event(&mut self, key: Key) -> Option<Self::Message> {
+
    fn handle_event(&mut self, _props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
        self.sections.handle_event(key);

        if self.props.handle_keys {
@@ -348,31 +348,25 @@ impl<'a: 'static> View for BrowserPage<'a> {
        None
    }

-
    fn update(&mut self, state: &Self::State, props: Option<ViewProps>) {
-
        if let Some(props) = props.and_then(|props| props.inner::<BrowserPageProps>()) {
-
            self.props = props;
-
        } else {
-
            self.props = BrowserPageProps::from(state);
-
        }
-

+
    fn update(&mut self, _props: Option<&ViewProps>, state: &Self::State) {
        self.sections.update(state);
        self.shortcuts.update(state);
    }

-
    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
-
        let page_size = props.area.height.saturating_sub(6) as usize;
+
    fn render(&self, _props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
+
        let page_size = render.area.height.saturating_sub(6) as usize;

        let [content_area, shortcuts_area] =
-
            Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(props.area);
+
            Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(render.area);

        self.sections.render(
-
            frame,
            RenderProps::from(content_area)
                .layout(Layout::horizontal([Constraint::Min(1)]))
                .focus(true),
+
            frame,
        );
        self.shortcuts
-
            .render(frame, RenderProps::from(shortcuts_area));
+
            .render(RenderProps::from(shortcuts_area), frame);

        // TODO: Find better solution
        if page_size != self.props.page_size {
@@ -400,7 +394,7 @@ impl Search {
            props: SearchProps {},
            input: TextField::default()
                .to_widget(tx.clone())
-
                .on_event(|s, _| {
+
                .on_event(|_, s, _| {
                    Some(Message::UpdateSearch {
                        value: s.and_then(|i| i.unwrap_string()).unwrap_or_default(),
                    })
@@ -421,7 +415,7 @@ impl View for Search {
    type Message = Message;
    type State = State;

-
    fn handle_event(&mut self, key: termion::event::Key) -> Option<Self::Message> {
+
    fn handle_event(&mut self, _props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
        match key {
            Key::Esc => Some(Message::CloseSearch),
            Key::Char('\n') => Some(Message::ApplySearch),
@@ -432,20 +426,16 @@ impl View for Search {
        }
    }

-
    fn update(&mut self, state: &Self::State, props: Option<ViewProps>) {
-
        if let Some(props) = props.and_then(|props| props.inner::<SearchProps>()) {
-
            self.props = props;
-
        }
-

+
    fn update(&mut self, _props: Option<&ViewProps>, state: &Self::State) {
        self.input.update(state);
    }

-
    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
+
    fn render(&self, _props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
        let layout = Layout::horizontal(Constraint::from_mins([0]))
            .horizontal_margin(1)
-
            .split(props.area);
+
            .split(render.area);

-
        self.input.render(frame, RenderProps::from(layout[0]));
+
        self.input.render(RenderProps::from(layout[0]), frame);
    }
}

@@ -469,22 +459,19 @@ impl<'a> From<&State> for HelpPageProps<'a> {
    }
}

-
pub struct HelpPage<'a> {
-
    /// Internal props
-
    props: HelpPageProps<'a>,
+
pub struct HelpPage {
    /// Content widget
    content: Widget,
    /// Shortcut widget
    shortcuts: Widget,
}

-
impl<'a: 'static> HelpPage<'a> {
+
impl HelpPage {
    pub fn new(tx: UnboundedSender<Message>) -> Self
    where
        Self: Sized,
    {
        Self {
-
            props: HelpPageProps::default(),
            content: Container::default()
                .header(Header::default().to_widget(tx.clone()).on_update(|_| {
                    HeaderProps::default()
@@ -495,7 +482,7 @@ impl<'a: 'static> HelpPage<'a> {
                .content(
                    Paragraph::default()
                        .to_widget(tx.clone())
-
                        .on_event(|s, _| {
+
                        .on_event(|_, s, _| {
                            Some(Message::ScrollHelp {
                                progress: s.and_then(|p| p.unwrap_usize()).unwrap_or_default(),
                            })
@@ -540,11 +527,11 @@ impl<'a: 'static> HelpPage<'a> {
    }
}

-
impl<'a: 'static> View for HelpPage<'a> {
+
impl View for HelpPage {
    type Message = Message;
    type State = State;

-
    fn handle_event(&mut self, key: termion::event::Key) -> Option<Self::Message> {
+
    fn handle_event(&mut self, _props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
        match key {
            Key::Esc | Key::Ctrl('c') => Some(Message::Exit { selection: None }),
            Key::Char('?') => Some(Message::LeavePage),
@@ -555,30 +542,29 @@ impl<'a: 'static> View for HelpPage<'a> {
        }
    }

-
    fn update(&mut self, state: &Self::State, props: Option<ViewProps>) {
-
        if let Some(props) = props.and_then(|props| props.inner::<HelpPageProps>()) {
-
            self.props = props;
-
        } else {
-
            self.props = HelpPageProps::from(state);
-
        }
-

+
    fn update(&mut self, _props: Option<&ViewProps>, state: &Self::State) {
        self.content.update(state);
        self.shortcuts.update(state);
    }

-
    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
-
        let page_size = props.area.height.saturating_sub(6) as usize;
+
    fn render(&self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
+
        let default = HelpPageProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<HelpPageProps>())
+
            .unwrap_or(&default);
+

+
        let page_size = render.area.height.saturating_sub(6) as usize;

        let [content_area, shortcuts_area] =
-
            Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(props.area);
+
            Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(render.area);

        self.content
-
            .render(frame, RenderProps::from(content_area).focus(true));
+
            .render(RenderProps::from(content_area).focus(true), frame);
        self.shortcuts
-
            .render(frame, RenderProps::from(shortcuts_area));
+
            .render(RenderProps::from(content_area).focus(true), frame);

        // TODO: Find better solution
-
        if page_size != self.props.page_size {
+
        if page_size != props.page_size {
            self.content.send(Message::HelpPageSize(page_size));
        }
    }
modified bin/commands/patch/select.rs
@@ -19,8 +19,8 @@ use tui::ui::widget::ToWidget;

use tui::{BoxedAny, Channel, Exit, PageStack};

-
use self::ui::BrowserPage;
use self::ui::HelpPage;
+
use self::ui::{BrowserPage, BrowserPageProps};

use super::common::Mode;

@@ -206,7 +206,9 @@ impl App {
        let window = Window::default()
            .page(
                Page::Browse,
-
                BrowserPage::new(tx.clone()).to_widget(tx.clone()),
+
                BrowserPage::new(tx.clone())
+
                    .to_widget(tx.clone())
+
                    .on_update(|state| BrowserPageProps::from(state).to_boxed_any().into()),
            )
            .page(Page::Help, HelpPage::new(tx.clone()).to_widget(tx.clone()))
            .to_widget(tx.clone())
modified bin/commands/patch/select/ui.rs
@@ -2,6 +2,7 @@ use std::collections::HashMap;
use std::str::FromStr;
use std::vec;

+
use ratatui::Frame;
use tokio::sync::mpsc::UnboundedSender;

use termion::event::Key;
@@ -10,12 +11,14 @@ use ratatui::layout::{Constraint, Layout};
use ratatui::style::Stylize;
use ratatui::text::{Line, Span, Text};

-
use radicle::patch::{self, Status};
+
use radicle::patch;
+
use radicle::patch::Status;

use radicle_tui as tui;

use tui::ui::items::{PatchItem, PatchItemFilter};
use tui::ui::span;
+
use tui::ui::widget;
use tui::ui::widget::container::{
    Column, Container, ContainerProps, Footer, FooterProps, Header, HeaderProps, SectionGroup,
    SectionGroupProps,
@@ -24,7 +27,7 @@ use tui::ui::widget::input::{TextField, TextFieldProps};
use tui::ui::widget::list::{Table, TableProps, TableUtils};
use tui::ui::widget::text::{Paragraph, ParagraphProps};
use tui::ui::widget::window::{Shortcuts, ShortcutsProps};
-
use tui::ui::widget::{self, ViewProps};
+
use tui::ui::widget::ViewProps;
use tui::ui::widget::{RenderProps, ToWidget, View};

use tui::{BoxedAny, Selection};
@@ -128,31 +131,27 @@ impl<'a> From<&State> for BrowserProps<'a> {
    }
}

-
pub struct Browser<'a> {
-
    /// Internal props
-
    props: BrowserProps<'a>,
+
pub struct Browser {
    /// Patches widget
    patches: Widget,
    /// Search widget
    search: Widget,
}

-
impl<'a: 'static> Browser<'a> {
+
impl Browser {
    fn new(tx: UnboundedSender<Message>) -> Self {
-
        let props = BrowserProps::default();
        Self {
-
            props: props.clone(),
            patches: Container::default()
                .header(
                    Header::default()
-
                        .columns(props.header.clone())
-
                        .cutoff(props.cutoff, props.cutoff_after)
+
                        // .columns(props.header.clone())
+
                        // .cutoff(props.cutoff, props.cutoff_after)
                        .to_widget(tx.clone()),
                )
                .content(
                    Table::<State, Message, PatchItem, 9>::default()
                        .to_widget(tx.clone())
-
                        .on_event(|s, _| {
+
                        .on_event(|_, s, _| {
                            Some(Message::Select {
                                selected: s.and_then(|s| s.unwrap_usize()),
                            })
@@ -191,12 +190,17 @@ impl<'a: 'static> Browser<'a> {
    }
}

-
impl<'a: 'static> View for Browser<'a> {
+
impl View for Browser {
    type Message = Message;
    type State = State;

-
    fn handle_event(&mut self, key: Key) -> Option<Self::Message> {
-
        if self.props.show_search {
+
    fn handle_event(&mut self, props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
+
        let default = BrowserProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<BrowserProps>())
+
            .unwrap_or(&default);
+

+
        if props.show_search {
            self.search.handle_event(key);
            None
        } else {
@@ -205,14 +209,14 @@ impl<'a: 'static> View for Browser<'a> {
                Key::Char('?') => Some(Message::OpenHelp),
                Key::Char('/') => Some(Message::OpenSearch),
                Key::Char('\n') => {
-
                    let operation = match self.props.mode {
+
                    let operation = match props.mode {
                        Mode::Operation => Some(PatchOperation::Show.to_string()),
                        Mode::Id => None,
                    };

-
                    self.props
+
                    props
                        .selected
-
                        .and_then(|selected| self.props.patches.get(selected))
+
                        .and_then(|selected| props.patches.get(selected))
                        .map(|patch| Message::Exit {
                            selection: Some(Selection {
                                operation,
@@ -221,10 +225,9 @@ impl<'a: 'static> View for Browser<'a> {
                            }),
                        })
                }
-
                Key::Char('c') => self
-
                    .props
+
                Key::Char('c') => props
                    .selected
-
                    .and_then(|selected| self.props.patches.get(selected))
+
                    .and_then(|selected| props.patches.get(selected))
                    .map(|patch| Message::Exit {
                        selection: Some(Selection {
                            operation: Some(PatchOperation::Checkout.to_string()),
@@ -232,10 +235,9 @@ impl<'a: 'static> View for Browser<'a> {
                            args: vec![],
                        }),
                    }),
-
                Key::Char('d') => self
-
                    .props
+
                Key::Char('d') => props
                    .selected
-
                    .and_then(|selected| self.props.patches.get(selected))
+
                    .and_then(|selected| props.patches.get(selected))
                    .map(|patch| Message::Exit {
                        selection: Some(Selection {
                            operation: Some(PatchOperation::Diff.to_string()),
@@ -251,33 +253,32 @@ impl<'a: 'static> View for Browser<'a> {
        }
    }

-
    fn update(&mut self, state: &Self::State, props: Option<ViewProps>) {
-
        if let Some(props) = props.and_then(|props| props.inner::<BrowserProps>()) {
-
            self.props = props;
-
        } else {
-
            self.props = BrowserProps::from(state);
-
        }
-

+
    fn update(&mut self, _props: Option<&ViewProps>, state: &Self::State) {
        self.patches.update(state);
        self.search.update(state);
    }

-
    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
-
        if self.props.show_search {
+
    fn render(&self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
+
        let default = BrowserProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<BrowserProps>())
+
            .unwrap_or(&default);
+

+
        if props.show_search {
            let [table_area, search_area] =
-
                Layout::vertical([Constraint::Min(1), Constraint::Length(2)]).areas(props.area);
+
                Layout::vertical([Constraint::Min(1), Constraint::Length(2)]).areas(render.area);

-
            self.patches.render(frame, RenderProps::from(table_area));
+
            self.patches.render(RenderProps::from(table_area), frame);
            self.search
-
                .render(frame, RenderProps::from(search_area).focus(props.focus));
+
                .render(RenderProps::from(search_area).focus(render.focus), frame);
        } else {
-
            self.patches.render(frame, props);
+
            self.patches.render(render, frame);
        }
    }
}

#[derive(Clone, Default)]
-
struct BrowserPageProps<'a> {
+
pub struct BrowserPageProps<'a> {
    /// Current page size (height of table content).
    page_size: usize,
    /// If this pages' keys should be handled (`false` if search is shown).
@@ -309,19 +310,16 @@ impl<'a> From<&State> for BrowserPageProps<'a> {
    }
}

-
pub struct BrowserPage<'a> {
-
    /// Internal props
-
    props: BrowserPageProps<'a>,
+
pub struct BrowserPage {
    /// Sections widget
    sections: Widget,
    /// Shortcut widget
    shortcuts: Widget,
}

-
impl<'a: 'static> BrowserPage<'a> {
+
impl BrowserPage {
    pub fn new(tx: UnboundedSender<Message>) -> Self {
        Self {
-
            props: BrowserPageProps::default(),
            sections: SectionGroup::default()
                .section(Browser::new(tx.clone()).to_widget(tx.clone()))
                .to_widget(tx.clone())
@@ -344,14 +342,19 @@ impl<'a: 'static> BrowserPage<'a> {
    }
}

-
impl<'a: 'static> View for BrowserPage<'a> {
+
impl View for BrowserPage {
    type Message = Message;
    type State = State;

-
    fn handle_event(&mut self, key: Key) -> Option<Self::Message> {
+
    fn handle_event(&mut self, props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
+
        let default = BrowserPageProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<BrowserPageProps>())
+
            .unwrap_or(&default);
+

        self.sections.handle_event(key);

-
        if self.props.handle_keys {
+
        if props.handle_keys {
            return match key {
                Key::Esc | Key::Ctrl('c') => Some(Message::Exit { selection: None }),
                Key::Char('?') => Some(Message::OpenHelp),
@@ -362,34 +365,33 @@ impl<'a: 'static> View for BrowserPage<'a> {
        None
    }

-
    fn update(&mut self, state: &Self::State, props: Option<ViewProps>) {
-
        if let Some(props) = props.and_then(|props| props.inner::<BrowserPageProps>()) {
-
            self.props = props;
-
        } else {
-
            self.props = BrowserPageProps::from(state);
-
        }
-

+
    fn update(&mut self, _props: Option<&ViewProps>, state: &Self::State) {
        self.sections.update(state);
        self.shortcuts.update(state);
    }

-
    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
-
        let page_size = props.area.height.saturating_sub(6) as usize;
+
    fn render(&self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
+
        let default = BrowserPageProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<BrowserPageProps>())
+
            .unwrap_or(&default);
+

+
        let page_size = render.area.height.saturating_sub(6) as usize;

        let [content_area, shortcuts_area] =
-
            Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(props.area);
+
            Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(render.area);

        self.sections.render(
-
            frame,
            RenderProps::from(content_area)
                .layout(Layout::horizontal([Constraint::Min(1)]))
                .focus(true),
+
            frame,
        );
        self.shortcuts
-
            .render(frame, RenderProps::from(shortcuts_area));
+
            .render(RenderProps::from(shortcuts_area), frame);

        // TODO: Find better solution
-
        if page_size != self.props.page_size {
+
        if page_size != props.page_size {
            self.sections.send(Message::BrowserPageSize(page_size));
        }
    }
@@ -399,8 +401,6 @@ impl<'a: 'static> View for BrowserPage<'a> {
pub struct SearchProps {}

pub struct Search {
-
    /// Internal props
-
    props: SearchProps,
    /// Search input field
    input: Widget,
}
@@ -411,7 +411,6 @@ impl Search {
        Self: Sized,
    {
        Self {
-
            props: SearchProps {},
            input: TextField::default()
                .to_widget(tx.clone())
                .on_event(|s, _| {
@@ -435,7 +434,7 @@ impl View for Search {
    type Message = Message;
    type State = State;

-
    fn handle_event(&mut self, key: termion::event::Key) -> Option<Self::Message> {
+
    fn handle_event(&mut self, _props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
        match key {
            Key::Esc => Some(Message::CloseSearch),
            Key::Char('\n') => Some(Message::ApplySearch),
@@ -446,20 +445,16 @@ impl View for Search {
        }
    }

-
    fn update(&mut self, state: &Self::State, props: Option<ViewProps>) {
-
        if let Some(props) = props.and_then(|props| props.inner::<SearchProps>()) {
-
            self.props = props;
-
        }
-

+
    fn update(&mut self, _props: Option<&ViewProps>, state: &Self::State) {
        self.input.update(state);
    }

-
    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
+
    fn render(&self, _props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
        let layout = Layout::horizontal(Constraint::from_mins([0]))
            .horizontal_margin(1)
-
            .split(props.area);
+
            .split(render.area);

-
        self.input.render(frame, RenderProps::from(layout[0]));
+
        self.input.render(RenderProps::from(layout[0]), frame);
    }
}

@@ -483,22 +478,19 @@ impl<'a> From<&State> for HelpPageProps<'a> {
    }
}

-
pub struct HelpPage<'a> {
-
    /// Internal props
-
    props: HelpPageProps<'a>,
+
pub struct HelpPage {
    /// Content widget
    content: Widget,
    /// Shortcut widget
    shortcuts: Widget,
}

-
impl<'a: 'static> HelpPage<'a> {
+
impl HelpPage {
    pub fn new(tx: UnboundedSender<Message>) -> Self
    where
        Self: Sized,
    {
        Self {
-
            props: HelpPageProps::default(),
            content: Container::default()
                .header(Header::default().to_widget(tx.clone()).on_update(|_| {
                    HeaderProps::default()
@@ -509,7 +501,7 @@ impl<'a: 'static> HelpPage<'a> {
                .content(
                    Paragraph::default()
                        .to_widget(tx.clone())
-
                        .on_event(|s, _| {
+
                        .on_event(|_, s, _| {
                            Some(Message::ScrollHelp {
                                progress: s.and_then(|p| p.unwrap_usize()).unwrap_or_default(),
                            })
@@ -554,11 +546,11 @@ impl<'a: 'static> HelpPage<'a> {
    }
}

-
impl<'a: 'static> View for HelpPage<'a> {
+
impl View for HelpPage {
    type Message = Message;
    type State = State;

-
    fn handle_event(&mut self, key: termion::event::Key) -> Option<Self::Message> {
+
    fn handle_event(&mut self, _props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
        match key {
            Key::Esc | Key::Ctrl('c') => Some(Message::Exit { selection: None }),
            Key::Char('?') => Some(Message::LeavePage),
@@ -569,30 +561,29 @@ impl<'a: 'static> View for HelpPage<'a> {
        }
    }

-
    fn update(&mut self, state: &Self::State, props: Option<ViewProps>) {
-
        if let Some(props) = props.and_then(|props| props.inner::<HelpPageProps>()) {
-
            self.props = props;
-
        } else {
-
            self.props = HelpPageProps::from(state);
-
        }
-

+
    fn update(&mut self, _props: Option<&ViewProps>, state: &Self::State) {
        self.content.update(state);
        self.shortcuts.update(state);
    }

-
    fn render(&self, frame: &mut ratatui::Frame, props: RenderProps) {
-
        let page_size = props.area.height.saturating_sub(6) as usize;
+
    fn render(&self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
+
        let default = HelpPageProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<HelpPageProps>())
+
            .unwrap_or(&default);
+

+
        let page_size = render.area.height.saturating_sub(6) as usize;

        let [content_area, shortcuts_area] =
-
            Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(props.area);
+
            Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(render.area);

        self.content
-
            .render(frame, RenderProps::from(content_area).focus(true));
+
            .render(RenderProps::from(content_area).focus(true), frame);
        self.shortcuts
-
            .render(frame, RenderProps::from(shortcuts_area));
+
            .render(RenderProps::from(content_area).focus(true), frame);

        // TODO: Find better solution
-
        if page_size != self.props.page_size {
+
        if page_size != props.page_size {
            self.content.send(Message::HelpPageSize(page_size));
        }
    }