Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
Various library improvements
Merged did:key:z6MkswQE...2C1V opened 1 year ago
15 files changed +593 -571 0d43859b a16c82f2
modified bin/commands/inbox/select.rs
@@ -14,15 +14,13 @@ use radicle::storage::git::Repository;
use radicle::Profile;
use radicle_tui as tui;

-
use tui::cob::inbox::{self};
+
use tui::cob::inbox;
use tui::store;
use tui::store::StateValue;
-
use tui::task;
-
use tui::task::Interrupted;
use tui::ui::items::{Filter, NotificationItem, NotificationItemFilter};
use tui::ui::widget::window::{Window, WindowProps};
use tui::ui::widget::{Properties, Widget};
-
use tui::ui::Frontend;
+
use tui::Channel;
use tui::Exit;

use tui::PageStack;
@@ -197,7 +195,7 @@ impl TryFrom<&Context> for State {
    }
}

-
pub enum Action {
+
pub enum Message {
    Exit { selection: Option<Selection> },
    Select { selected: Option<usize> },
    BrowserPageSize(usize),
@@ -212,42 +210,42 @@ pub enum Action {
}

impl store::State<Selection> for State {
-
    type Action = Action;
+
    type Message = Message;

    fn tick(&self) {}

-
    fn handle_action(&mut self, action: Action) -> Option<Exit<Selection>> {
-
        match action {
-
            Action::Exit { selection } => Some(Exit { value: selection }),
-
            Action::Select { selected } => {
+
    fn update(&mut self, message: Message) -> Option<Exit<Selection>> {
+
        match message {
+
            Message::Exit { selection } => Some(Exit { value: selection }),
+
            Message::Select { selected } => {
                self.browser.selected = selected;
                None
            }
-
            Action::BrowserPageSize(size) => {
+
            Message::BrowserPageSize(size) => {
                self.browser.page_size = size;
                None
            }
-
            Action::HelpPageSize(size) => {
+
            Message::HelpPageSize(size) => {
                self.help.page_size = size;
                None
            }
-
            Action::OpenSearch => {
+
            Message::OpenSearch => {
                self.browser.show_search = true;
                None
            }
-
            Action::UpdateSearch { value } => {
+
            Message::UpdateSearch { value } => {
                self.browser.search.write(value);
                self.browser.filter = NotificationItemFilter::from_str(&self.browser.search.read())
                    .unwrap_or_default();

                None
            }
-
            Action::ApplySearch => {
+
            Message::ApplySearch => {
                self.browser.search.apply();
                self.browser.show_search = false;
                None
            }
-
            Action::CloseSearch => {
+
            Message::CloseSearch => {
                self.browser.search.reset();
                self.browser.show_search = false;
                self.browser.filter = NotificationItemFilter::from_str(&self.browser.search.read())
@@ -255,15 +253,15 @@ impl store::State<Selection> for State {

                None
            }
-
            Action::OpenHelp => {
+
            Message::OpenHelp => {
                self.pages.push(Page::Help);
                None
            }
-
            Action::LeavePage => {
+
            Message::LeavePage => {
                self.pages.pop();
                None
            }
-
            Action::ScrollHelp { progress } => {
+
            Message::ScrollHelp { progress } => {
                self.help.progress = progress;
                None
            }
@@ -277,19 +275,16 @@ impl App {
    }

    pub async fn run(&self) -> Result<Option<Selection>> {
-
        let (terminator, mut interrupt_rx) = task::create_termination();
-
        let (store, state_rx) = store::Store::<Action, State, Selection>::new();
-
        let (frontend, action_tx, action_rx) = Frontend::new();
+
        let channel = Channel::default();
        let state = State::try_from(&self.context)?;
-

-
        let window: Window<State, Action, Page> = Window::new(&state, action_tx.clone())
+
        let window: Window<State, Message, Page> = Window::new(&state, channel.tx.clone())
            .page(
                Page::Browse,
-
                BrowserPage::new(&state, action_tx.clone()).to_boxed(),
+
                BrowserPage::new(&state, channel.tx.clone()).to_boxed(),
            )
            .page(
                Page::Help,
-
                HelpPage::new(&state, action_tx.clone()).to_boxed(),
+
                HelpPage::new(&state, channel.tx.clone()).to_boxed(),
            )
            .on_update(|state| {
                WindowProps::default()
@@ -297,18 +292,6 @@ impl App {
                    .to_boxed()
            });

-
        tokio::try_join!(
-
            store.main_loop(state, terminator, action_rx, interrupt_rx.resubscribe()),
-
            frontend.main_loop(Some(window), state_rx, interrupt_rx.resubscribe()),
-
        )?;
-

-
        if let Ok(reason) = interrupt_rx.recv().await {
-
            match reason {
-
                Interrupted::User { payload } => Ok(payload),
-
                Interrupted::OsSignal => anyhow::bail!("exited because of an os sig int"),
-
            }
-
        } else {
-
            anyhow::bail!("exited because of an unexpected error");
-
        }
+
        tui::run(channel, state, window).await
    }
}
modified bin/commands/inbox/select/ui.rs
@@ -22,15 +22,15 @@ 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::{BaseView, BoxedAny, Properties, RenderProps, Widget};
+
use tui::ui::widget::{BoxedAny, Properties, RenderProps, Widget, WidgetBase};

use tui::Selection;

use crate::tui_inbox::common::{InboxOperation, Mode, RepositoryMode, SelectionMode};

-
use super::{Action, State};
+
use super::{Message, State};

-
type BoxedWidget = widget::BoxedWidget<State, Action>;
+
type BoxedWidget = widget::BoxedWidget<State, Message>;

#[derive(Clone)]
pub struct BrowserProps<'a> {
@@ -113,7 +113,7 @@ impl<'a> BoxedAny for BrowserProps<'a> {}

pub struct Browser<'a> {
    /// Internal base
-
    base: BaseView<State, Action>,
+
    base: WidgetBase<State, Message>,
    /// Internal props
    props: BrowserProps<'a>,
    /// Notification widget
@@ -123,22 +123,18 @@ pub struct Browser<'a> {
}

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

-
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self {
+
    fn new(state: &State, tx: UnboundedSender<Message>) -> Self {
        let props = BrowserProps::from(state);

        Self {
-
            base: BaseView {
-
                action_tx: action_tx.clone(),
-
                on_update: None,
-
                on_event: None,
-
            },
+
            base: WidgetBase::new(tx.clone()),
            props: props.clone(),
-
            notifications: Container::new(state, action_tx.clone())
+
            notifications: Container::new(state, tx.clone())
                .header(
-
                    Header::new(state, action_tx.clone())
+
                    Header::new(state, tx.clone())
                        .columns(
                            [
                                Column::new("", Constraint::Length(0)),
@@ -149,14 +145,17 @@ impl<'a: 'static> Widget for Browser<'a> {
                        .cutoff(props.cutoff, props.cutoff_after)
                        .to_boxed(),
                )
-
                .content(Box::<Table<State, Action, NotificationItem, 9>>::new(
-
                    Table::new(state, action_tx.clone())
+
                .content(Box::<Table<State, Message, NotificationItem, 9>>::new(
+
                    Table::new(state, tx.clone())
                        .on_event(|table| {
                            table
-
                                .downcast_mut::<Table<State, Action, NotificationItem, 9>>()
+
                                .downcast_mut::<Table<State, Message, NotificationItem, 9>>()
                                .and_then(|table| {
-
                                    let selected = table.selected();
-
                                    table.base_mut().send(Action::Select { selected }).ok()
+
                                    table
+
                                        .send(Message::Select {
+
                                            selected: table.selected(),
+
                                        })
+
                                        .ok()
                                });
                        })
                        .on_update(|state| {
@@ -172,7 +171,7 @@ impl<'a: 'static> Widget for Browser<'a> {
                        }),
                ))
                .footer(
-
                    Footer::new(state, action_tx.clone())
+
                    Footer::new(state, tx.clone())
                        .on_update(|state| {
                            FooterProps::default()
                                .columns(browse_footer(&BrowserProps::from(state)))
@@ -186,7 +185,7 @@ impl<'a: 'static> Widget for Browser<'a> {
                        .to_boxed()
                })
                .to_boxed(),
-
            search: Search::new(state, action_tx.clone()).to_boxed(),
+
            search: Search::new(state, tx.clone()).to_boxed(),
        }
    }

@@ -196,7 +195,7 @@ impl<'a: 'static> Widget for Browser<'a> {
        } else {
            match key {
                Key::Char('/') => {
-
                    let _ = self.base.send(Action::OpenSearch);
+
                    let _ = self.send(Message::OpenSearch);
                }
                Key::Char('\n') => {
                    self.props
@@ -211,7 +210,7 @@ impl<'a: 'static> Widget for Browser<'a> {
                            };

                            self.base
-
                                .send(Action::Exit {
+
                                .send(Message::Exit {
                                    selection: Some(selection),
                                })
                                .ok()
@@ -223,7 +222,7 @@ impl<'a: 'static> Widget for Browser<'a> {
                        .and_then(|selected| self.props.notifications.get(selected))
                        .and_then(|notif| {
                            self.base
-
                                .send(Action::Exit {
+
                                .send(Message::Exit {
                                    selection: Some(
                                        Selection::default()
                                            .with_operation(InboxOperation::Clear.to_string())
@@ -260,7 +259,11 @@ impl<'a: 'static> Widget for Browser<'a> {
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
    fn base(&self) -> &WidgetBase<State, Message> {
+
        &self.base
+
    }
+

+
    fn base_mut(&mut self) -> &mut WidgetBase<State, Message> {
        &mut self.base
    }
}
@@ -302,7 +305,7 @@ impl<'a> BoxedAny for BrowserPageProps<'a> {}

pub struct BrowserPage<'a> {
    /// Internal base
-
    base: BaseView<State, Action>,
+
    base: WidgetBase<State, Message>,
    /// Internal props
    props: BrowserPageProps<'a>,
    /// Sections widget
@@ -312,21 +315,17 @@ pub struct BrowserPage<'a> {
}

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

-
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self {
+
    fn new(state: &State, tx: UnboundedSender<Message>) -> Self {
        let props = BrowserPageProps::from(state);

        Self {
-
            base: BaseView {
-
                action_tx: action_tx.clone(),
-
                on_update: None,
-
                on_event: None,
-
            },
+
            base: WidgetBase::new(tx.clone()),
            props: props.clone(),
-
            sections: SectionGroup::new(state, action_tx.clone())
-
                .section(Browser::new(state, action_tx.clone()).to_boxed())
+
            sections: SectionGroup::new(state, tx.clone())
+
                .section(Browser::new(state, tx.clone()).to_boxed())
                .on_update(|state| {
                    let props = BrowserPageProps::from(state);
                    SectionGroupProps::default()
@@ -334,7 +333,7 @@ impl<'a: 'static> Widget for BrowserPage<'a> {
                        .to_boxed()
                })
                .to_boxed(),
-
            shortcuts: Shortcuts::new(state, action_tx.clone())
+
            shortcuts: Shortcuts::new(state, tx.clone())
                .on_update(|state| {
                    ShortcutsProps::default()
                        .shortcuts(&BrowserPageProps::from(state).shortcuts)
@@ -350,10 +349,10 @@ impl<'a: 'static> Widget for BrowserPage<'a> {
        if self.props.handle_keys {
            match key {
                Key::Esc | Key::Ctrl('c') => {
-
                    let _ = self.base.send(Action::Exit { selection: None });
+
                    let _ = self.send(Message::Exit { selection: None });
                }
                Key::Char('?') => {
-
                    let _ = self.base.send(Action::OpenHelp);
+
                    let _ = self.send(Message::OpenHelp);
                }
                _ => {}
            }
@@ -384,11 +383,15 @@ impl<'a: 'static> Widget for BrowserPage<'a> {
            .render(frame, RenderProps::from(shortcuts_area));

        if page_size != self.props.page_size {
-
            let _ = self.base.send(Action::BrowserPageSize(page_size));
+
            let _ = self.send(Message::BrowserPageSize(page_size));
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
    fn base(&self) -> &WidgetBase<State, Message> {
+
        &self.base
+
    }
+

+
    fn base_mut(&mut self) -> &mut WidgetBase<State, Message> {
        &mut self.base
    }
}
@@ -399,7 +402,7 @@ impl Properties for SearchProps {}

pub struct Search {
    /// Internal base
-
    base: BaseView<State, Action>,
+
    base: WidgetBase<State, Message>,
    /// Internal props
    _props: SearchProps,
    /// Search input field
@@ -407,51 +410,46 @@ pub struct Search {
}

impl Widget for Search {
-
    type Action = Action;
+
    type Message = Message;
    type State = State;

-
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
+
    fn new(state: &State, tx: UnboundedSender<Message>) -> Self
    where
        Self: Sized,
    {
-
        let input = TextField::new(state, action_tx.clone())
-
            .on_event(|field| {
-
                field
-
                    .downcast_mut::<TextField<State, Action>>()
-
                    .and_then(|field| {
-
                        let text = field.text().unwrap_or(&String::new()).to_string();
-
                        field
-
                            .base_mut()
-
                            .send(Action::UpdateSearch { value: text })
-
                            .ok()
-
                    });
-
            })
-
            .on_update(|state| {
-
                TextFieldProps::default()
-
                    .text(&state.browser.search.read().to_string())
-
                    .title("Search")
-
                    .inline(true)
-
                    .to_boxed()
-
            })
-
            .to_boxed();
        Self {
-
            base: BaseView {
-
                action_tx: action_tx.clone(),
-
                on_update: None,
-
                on_event: None,
-
            },
+
            base: WidgetBase::new(tx.clone()),
            _props: SearchProps {},
-
            input,
+
            input: TextField::new(state, tx.clone())
+
                .on_event(|widget| {
+
                    widget
+
                        .downcast_mut::<TextField<State, Message>>()
+
                        .and_then(|field| {
+
                            field
+
                                .send(Message::UpdateSearch {
+
                                    value: field.text().unwrap_or(&String::new()).to_string(),
+
                                })
+
                                .ok()
+
                        });
+
                })
+
                .on_update(|state| {
+
                    TextFieldProps::default()
+
                        .text(&state.browser.search.read().to_string())
+
                        .title("Search")
+
                        .inline(true)
+
                        .to_boxed()
+
                })
+
                .to_boxed(),
        }
    }

    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc => {
-
                let _ = self.base.send(Action::CloseSearch);
+
                let _ = self.send(Message::CloseSearch);
            }
            Key::Char('\n') => {
-
                let _ = self.base.send(Action::ApplySearch);
+
                let _ = self.send(Message::ApplySearch);
            }
            _ => {
                self.input.handle_event(key);
@@ -471,7 +469,11 @@ impl Widget for Search {
        self.input.render(frame, RenderProps::from(layout[0]));
    }

-
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
    fn base(&self) -> &WidgetBase<State, Message> {
+
        &self.base
+
    }
+

+
    fn base_mut(&mut self) -> &mut WidgetBase<State, Message> {
        &mut self.base
    }
}
@@ -501,7 +503,7 @@ impl<'a> BoxedAny for HelpPageProps<'a> {}

pub struct HelpPage<'a> {
    /// Internal base
-
    base: BaseView<State, Action>,
+
    base: WidgetBase<State, Message>,
    /// Internal props
    props: HelpPageProps<'a>,
    /// Content widget
@@ -511,23 +513,19 @@ pub struct HelpPage<'a> {
}

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

-
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
+
    fn new(state: &State, tx: UnboundedSender<Message>) -> Self
    where
        Self: Sized,
    {
        Self {
-
            base: BaseView {
-
                action_tx: action_tx.clone(),
-
                on_update: None,
-
                on_event: None,
-
            },
+
            base: WidgetBase::new(tx.clone()),
            props: HelpPageProps::from(state),
-
            content: Container::new(state, action_tx.clone())
+
            content: Container::new(state, tx.clone())
                .header(
-
                    Header::new(state, action_tx.clone())
+
                    Header::new(state, tx.clone())
                        .on_update(|_| {
                            HeaderProps::default()
                                .columns([Column::new(" Help ", Constraint::Fill(1))].to_vec())
@@ -536,15 +534,15 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                        .to_boxed(),
                )
                .content(
-
                    Paragraph::new(state, action_tx.clone())
+
                    Paragraph::new(state, tx.clone())
                        .on_event(|paragraph| {
                            paragraph
-
                                .downcast_mut::<Paragraph<'_, State, Action>>()
+
                                .downcast_mut::<Paragraph<'_, State, Message>>()
                                .and_then(|paragraph| {
-
                                    let progress = paragraph.progress();
                                    paragraph
-
                                        .base_mut()
-
                                        .send(Action::ScrollHelp { progress })
+
                                        .send(Message::ScrollHelp {
+
                                            progress: paragraph.progress(),
+
                                        })
                                        .ok()
                                });
                        })
@@ -559,7 +557,7 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                        .to_boxed(),
                )
                .footer(
-
                    Footer::new(state, action_tx.clone())
+
                    Footer::new(state, tx.clone())
                        .on_update(|state| {
                            let props = HelpPageProps::from(state);

@@ -580,7 +578,7 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                        .to_boxed(),
                )
                .to_boxed(),
-
            shortcuts: Shortcuts::new(state, action_tx.clone())
+
            shortcuts: Shortcuts::new(state, tx.clone())
                .on_update(|state| {
                    ShortcutsProps::default()
                        .shortcuts(&HelpPageProps::from(state).shortcuts)
@@ -593,10 +591,10 @@ impl<'a: 'static> Widget for HelpPage<'a> {
    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc | Key::Ctrl('c') => {
-
                let _ = self.base.send(Action::Exit { selection: None });
+
                let _ = self.send(Message::Exit { selection: None });
            }
            Key::Char('?') => {
-
                let _ = self.base.send(Action::LeavePage);
+
                let _ = self.send(Message::LeavePage);
            }
            _ => {
                self.content.handle_event(key);
@@ -624,11 +622,15 @@ impl<'a: 'static> Widget for HelpPage<'a> {
            .render(frame, RenderProps::from(shortcuts_area));

        if page_size != self.props.page_size {
-
            let _ = self.base.send(Action::HelpPageSize(page_size));
+
            let _ = self.send(Message::HelpPageSize(page_size));
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
    fn base(&self) -> &WidgetBase<State, Message> {
+
        &self.base
+
    }
+

+
    fn base_mut(&mut self) -> &mut WidgetBase<State, Message> {
        &mut self.base
    }
}
modified bin/commands/issue/select.rs
@@ -13,12 +13,11 @@ use radicle_tui as tui;

use tui::cob::issue;
use tui::store::StateValue;
-
use tui::task;
-
use tui::task::Interrupted;
use tui::ui::items::{Filter, IssueItem, IssueItemFilter};
use tui::ui::widget::window::{Window, WindowProps};
use tui::ui::widget::{Properties, Widget};
-
use tui::ui::Frontend;
+
use tui::Channel;
+

use tui::Exit;
use tui::{store, PageStack};

@@ -115,7 +114,7 @@ impl TryFrom<&Context> for State {
    }
}

-
pub enum Action {
+
pub enum Message {
    Exit { selection: Option<Selection> },
    Select { selected: Option<usize> },
    BrowserPageSize(usize),
@@ -130,40 +129,40 @@ pub enum Action {
}

impl store::State<Selection> for State {
-
    type Action = Action;
+
    type Message = Message;

-
    fn handle_action(&mut self, action: Action) -> Option<Exit<Selection>> {
-
        match action {
-
            Action::Exit { selection } => Some(Exit { value: selection }),
-
            Action::Select { selected } => {
+
    fn update(&mut self, message: Message) -> Option<Exit<Selection>> {
+
        match message {
+
            Message::Exit { selection } => Some(Exit { value: selection }),
+
            Message::Select { selected } => {
                self.browser.selected = selected;
                None
            }
-
            Action::BrowserPageSize(size) => {
+
            Message::BrowserPageSize(size) => {
                self.browser.page_size = size;
                None
            }
-
            Action::HelpPageSize(size) => {
+
            Message::HelpPageSize(size) => {
                self.help.page_size = size;
                None
            }
-
            Action::OpenSearch => {
+
            Message::OpenSearch => {
                self.browser.show_search = true;
                None
            }
-
            Action::UpdateSearch { value } => {
+
            Message::UpdateSearch { value } => {
                self.browser.search.write(value);
                self.browser.filter =
                    IssueItemFilter::from_str(&self.browser.search.read()).unwrap_or_default();

                None
            }
-
            Action::ApplySearch => {
+
            Message::ApplySearch => {
                self.browser.search.apply();
                self.browser.show_search = false;
                None
            }
-
            Action::CloseSearch => {
+
            Message::CloseSearch => {
                self.browser.search.reset();
                self.browser.show_search = false;
                self.browser.filter =
@@ -171,15 +170,15 @@ impl store::State<Selection> for State {

                None
            }
-
            Action::OpenHelp => {
+
            Message::OpenHelp => {
                self.pages.push(Page::Help);
                None
            }
-
            Action::LeavePage => {
+
            Message::LeavePage => {
                self.pages.pop();
                None
            }
-
            Action::ScrollHelp { progress } => {
+
            Message::ScrollHelp { progress } => {
                self.help.progress = progress;
                None
            }
@@ -195,19 +194,16 @@ impl App {
    }

    pub async fn run(&self) -> Result<Option<Selection>> {
-
        let (terminator, mut interrupt_rx) = task::create_termination();
-
        let (store, state_rx) = store::Store::<Action, State, Selection>::new();
-
        let (frontend, action_tx, action_rx) = Frontend::new();
+
        let channel = Channel::default();
        let state = State::try_from(&self.context)?;
-

-
        let window: Window<State, Action, Page> = Window::new(&state, action_tx.clone())
+
        let window: Window<State, Message, Page> = Window::new(&state, channel.tx.clone())
            .page(
                Page::Browse,
-
                BrowserPage::new(&state, action_tx.clone()).to_boxed(),
+
                BrowserPage::new(&state, channel.tx.clone()).to_boxed(),
            )
            .page(
                Page::Help,
-
                HelpPage::new(&state, action_tx.clone()).to_boxed(),
+
                HelpPage::new(&state, channel.tx.clone()).to_boxed(),
            )
            .on_update(|state| {
                WindowProps::default()
@@ -215,18 +211,6 @@ impl App {
                    .to_boxed()
            });

-
        tokio::try_join!(
-
            store.main_loop(state, terminator, action_rx, interrupt_rx.resubscribe()),
-
            frontend.main_loop(Some(window), state_rx, interrupt_rx.resubscribe()),
-
        )?;
-

-
        if let Ok(reason) = interrupt_rx.recv().await {
-
            match reason {
-
                Interrupted::User { payload } => Ok(payload),
-
                Interrupted::OsSignal => anyhow::bail!("exited because of an os sig int"),
-
            }
-
        } else {
-
            anyhow::bail!("exited because of an unexpected error");
-
        }
+
        tui::run(channel, state, window).await
    }
}
modified bin/commands/issue/select/ui.rs
@@ -24,16 +24,16 @@ 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::{BaseView, BoxedAny, Properties, RenderProps, Widget};
+
use tui::ui::widget::{BoxedAny, Properties, RenderProps, Widget, WidgetBase};

use tui::Selection;

use crate::tui_issue::common::IssueOperation;
use crate::tui_issue::common::Mode;

-
use super::{Action, State};
+
use super::{Message, State};

-
type BoxedWidget = widget::BoxedWidget<State, Action>;
+
type BoxedWidget = widget::BoxedWidget<State, Message>;

#[derive(Clone)]
struct BrowserProps<'a> {
@@ -133,7 +133,7 @@ impl<'a> BoxedAny for BrowserProps<'a> {}

pub struct Browser<'a> {
    /// Internal base
-
    base: BaseView<State, Action>,
+
    base: WidgetBase<State, Message>,
    /// Internal props
    props: BrowserProps<'a>,
    /// Notifications widget
@@ -143,34 +143,33 @@ pub struct Browser<'a> {
}

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

-
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self {
+
    fn new(state: &State, tx: UnboundedSender<Message>) -> Self {
        let props = BrowserProps::from(state);

        Self {
-
            base: BaseView {
-
                action_tx: action_tx.clone(),
-
                on_update: None,
-
                on_event: None,
-
            },
+
            base: WidgetBase::new(tx.clone()),
            props: BrowserProps::from(state),
-
            issues: Container::new(state, action_tx.clone())
+
            issues: Container::new(state, tx.clone())
                .header(
-
                    Header::new(state, action_tx.clone())
+
                    Header::new(state, tx.clone())
                        .columns(props.header.clone())
                        .cutoff(props.cutoff, props.cutoff_after)
                        .to_boxed(),
                )
-
                .content(Box::<Table<State, Action, IssueItem, 8>>::new(
-
                    Table::new(state, action_tx.clone())
+
                .content(Box::<Table<State, Message, IssueItem, 8>>::new(
+
                    Table::new(state, tx.clone())
                        .on_event(|table| {
                            table
-
                                .downcast_mut::<Table<State, Action, IssueItem, 8>>()
+
                                .downcast_mut::<Table<State, Message, IssueItem, 8>>()
                                .and_then(|table| {
-
                                    let selected = table.selected();
-
                                    table.base_mut().send(Action::Select { selected }).ok()
+
                                    table
+
                                        .send(Message::Select {
+
                                            selected: table.selected(),
+
                                        })
+
                                        .ok()
                                });
                        })
                        .on_update(|state| {
@@ -186,7 +185,7 @@ impl<'a: 'static> Widget for Browser<'a> {
                        }),
                ))
                .footer(
-
                    Footer::new(state, action_tx.clone())
+
                    Footer::new(state, tx.clone())
                        .on_update(|state| {
                            let props = BrowserProps::from(state);

@@ -202,7 +201,7 @@ impl<'a: 'static> Widget for Browser<'a> {
                        .to_boxed()
                })
                .to_boxed(),
-
            search: Search::new(state, action_tx.clone()).to_boxed(),
+
            search: Search::new(state, tx.clone()).to_boxed(),
        }
    }

@@ -212,7 +211,7 @@ impl<'a: 'static> Widget for Browser<'a> {
        } else {
            match key {
                Key::Char('/') => {
-
                    let _ = self.base.send(Action::OpenSearch);
+
                    let _ = self.send(Message::OpenSearch);
                }
                Key::Char('\n') => {
                    let operation = match self.props.mode {
@@ -225,7 +224,7 @@ impl<'a: 'static> Widget for Browser<'a> {
                        .and_then(|selected| self.props.issues.get(selected))
                        .and_then(|issue| {
                            self.base
-
                                .send(Action::Exit {
+
                                .send(Message::Exit {
                                    selection: Some(Selection {
                                        operation,
                                        ids: vec![issue.id],
@@ -241,7 +240,7 @@ impl<'a: 'static> Widget for Browser<'a> {
                        .and_then(|selected| self.props.issues.get(selected))
                        .and_then(|issue| {
                            self.base
-
                                .send(Action::Exit {
+
                                .send(Message::Exit {
                                    selection: Some(Selection {
                                        operation: Some(IssueOperation::Edit.to_string()),
                                        ids: vec![issue.id],
@@ -279,7 +278,11 @@ impl<'a: 'static> Widget for Browser<'a> {
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
    fn base(&self) -> &WidgetBase<State, Message> {
+
        &self.base
+
    }
+

+
    fn base_mut(&mut self) -> &mut WidgetBase<State, Message> {
        &mut self.base
    }
}
@@ -321,7 +324,7 @@ impl<'a> BoxedAny for BrowserPageProps<'a> {}

pub struct BrowserPage<'a> {
    /// Internal base
-
    base: BaseView<State, Action>,
+
    base: WidgetBase<State, Message>,
    /// Internal props
    props: BrowserPageProps<'a>,
    /// Sections widget
@@ -331,21 +334,17 @@ pub struct BrowserPage<'a> {
}

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

-
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self {
+
    fn new(state: &State, tx: UnboundedSender<Message>) -> Self {
        let props = BrowserPageProps::from(state);

        Self {
-
            base: BaseView {
-
                action_tx: action_tx.clone(),
-
                on_update: None,
-
                on_event: None,
-
            },
+
            base: WidgetBase::new(tx.clone()),
            props: props.clone(),
-
            sections: SectionGroup::new(state, action_tx.clone())
-
                .section(Browser::new(state, action_tx.clone()).to_boxed())
+
            sections: SectionGroup::new(state, tx.clone())
+
                .section(Browser::new(state, tx.clone()).to_boxed())
                .on_update(|state| {
                    let props = BrowserPageProps::from(state);
                    SectionGroupProps::default()
@@ -353,7 +352,7 @@ impl<'a: 'static> Widget for BrowserPage<'a> {
                        .to_boxed()
                })
                .to_boxed(),
-
            shortcuts: Shortcuts::new(state, action_tx.clone())
+
            shortcuts: Shortcuts::new(state, tx.clone())
                .on_update(|state| {
                    ShortcutsProps::default()
                        .shortcuts(&BrowserPageProps::from(state).shortcuts)
@@ -369,10 +368,10 @@ impl<'a: 'static> Widget for BrowserPage<'a> {
        if self.props.handle_keys {
            match key {
                Key::Esc | Key::Ctrl('c') => {
-
                    let _ = self.base.send(Action::Exit { selection: None });
+
                    let _ = self.send(Message::Exit { selection: None });
                }
                Key::Char('?') => {
-
                    let _ = self.base.send(Action::OpenHelp);
+
                    let _ = self.send(Message::OpenHelp);
                }
                _ => {}
            }
@@ -403,11 +402,15 @@ impl<'a: 'static> Widget for BrowserPage<'a> {
            .render(frame, RenderProps::from(shortcuts_area));

        if page_size != self.props.page_size {
-
            let _ = self.base.send(Action::BrowserPageSize(page_size));
+
            let _ = self.send(Message::BrowserPageSize(page_size));
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
    fn base(&self) -> &WidgetBase<State, Message> {
+
        &self.base
+
    }
+

+
    fn base_mut(&mut self) -> &mut WidgetBase<State, Message> {
        &mut self.base
    }
}
@@ -418,7 +421,7 @@ impl Properties for SearchProps {}

pub struct Search {
    /// Internal base
-
    base: BaseView<State, Action>,
+
    base: WidgetBase<State, Message>,
    /// Internal props
    _props: SearchProps,
    /// Search input field
@@ -426,51 +429,46 @@ pub struct Search {
}

impl Widget for Search {
-
    type Action = Action;
+
    type Message = Message;
    type State = State;

-
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
+
    fn new(state: &State, tx: UnboundedSender<Message>) -> Self
    where
        Self: Sized,
    {
-
        let input = TextField::new(state, action_tx.clone())
-
            .on_event(|field| {
-
                field
-
                    .downcast_mut::<TextField<State, Action>>()
-
                    .and_then(|field| {
-
                        let text = field.text().unwrap_or(&String::new()).to_string();
-
                        field
-
                            .base_mut()
-
                            .send(Action::UpdateSearch { value: text })
-
                            .ok()
-
                    });
-
            })
-
            .on_update(|state| {
-
                TextFieldProps::default()
-
                    .text(&state.browser.search.read().to_string())
-
                    .title("Search")
-
                    .inline(true)
-
                    .to_boxed()
-
            })
-
            .to_boxed();
        Self {
-
            base: BaseView {
-
                action_tx: action_tx.clone(),
-
                on_update: None,
-
                on_event: None,
-
            },
+
            base: WidgetBase::new(tx.clone()),
            _props: SearchProps {},
-
            input,
+
            input: TextField::new(state, tx.clone())
+
                .on_event(|widget| {
+
                    widget
+
                        .downcast_mut::<TextField<State, Message>>()
+
                        .and_then(|field| {
+
                            field
+
                                .send(Message::UpdateSearch {
+
                                    value: field.text().unwrap_or(&String::new()).to_string(),
+
                                })
+
                                .ok()
+
                        });
+
                })
+
                .on_update(|state| {
+
                    TextFieldProps::default()
+
                        .text(&state.browser.search.read().to_string())
+
                        .title("Search")
+
                        .inline(true)
+
                        .to_boxed()
+
                })
+
                .to_boxed(),
        }
    }

    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc => {
-
                let _ = self.base.send(Action::CloseSearch);
+
                let _ = self.send(Message::CloseSearch);
            }
            Key::Char('\n') => {
-
                let _ = self.base.send(Action::ApplySearch);
+
                let _ = self.send(Message::ApplySearch);
            }
            _ => {
                self.input.handle_event(key);
@@ -490,7 +488,11 @@ impl Widget for Search {
        self.input.render(frame, RenderProps::from(layout[0]));
    }

-
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
    fn base(&self) -> &WidgetBase<State, Message> {
+
        &self.base
+
    }
+

+
    fn base_mut(&mut self) -> &mut WidgetBase<State, Message> {
        &mut self.base
    }
}
@@ -517,7 +519,7 @@ impl<'a> BoxedAny for HelpPageProps<'a> {}

pub struct HelpPage<'a> {
    /// Internal base
-
    base: BaseView<State, Action>,
+
    base: WidgetBase<State, Message>,
    /// Internal props
    props: HelpPageProps<'a>,
    /// Content widget
@@ -527,23 +529,19 @@ pub struct HelpPage<'a> {
}

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

-
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
+
    fn new(state: &State, tx: UnboundedSender<Message>) -> Self
    where
        Self: Sized,
    {
        Self {
-
            base: BaseView {
-
                action_tx: action_tx.clone(),
-
                on_update: None,
-
                on_event: None,
-
            },
+
            base: WidgetBase::new(tx.clone()),
            props: HelpPageProps::from(state),
-
            content: Container::new(state, action_tx.clone())
+
            content: Container::new(state, tx.clone())
                .header(
-
                    Header::new(state, action_tx.clone())
+
                    Header::new(state, tx.clone())
                        .on_update(|_| {
                            HeaderProps::default()
                                .columns([Column::new(" Help ", Constraint::Fill(1))].to_vec())
@@ -552,7 +550,18 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                        .to_boxed(),
                )
                .content(
-
                    Paragraph::new(state, action_tx.clone())
+
                    Paragraph::new(state, tx.clone())
+
                        .on_event(|paragraph| {
+
                            paragraph
+
                                .downcast_mut::<Paragraph<'_, State, Message>>()
+
                                .and_then(|paragraph| {
+
                                    paragraph
+
                                        .send(Message::ScrollHelp {
+
                                            progress: paragraph.progress(),
+
                                        })
+
                                        .ok()
+
                                });
+
                        })
                        .on_update(|state| {
                            let props = HelpPageProps::from(state);

@@ -561,21 +570,10 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                                .page_size(props.page_size)
                                .to_boxed()
                        })
-
                        .on_event(|paragraph| {
-
                            paragraph
-
                                .downcast_mut::<Paragraph<'_, State, Action>>()
-
                                .and_then(|paragraph| {
-
                                    let progress = paragraph.progress();
-
                                    paragraph
-
                                        .base_mut()
-
                                        .send(Action::ScrollHelp { progress })
-
                                        .ok()
-
                                });
-
                        })
                        .to_boxed(),
                )
                .footer(
-
                    Footer::new(state, action_tx.clone())
+
                    Footer::new(state, tx.clone())
                        .on_update(|state| {
                            let props = HelpPageProps::from(state);

@@ -596,7 +594,7 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                        .to_boxed(),
                )
                .to_boxed(),
-
            shortcuts: Shortcuts::new(state, action_tx.clone())
+
            shortcuts: Shortcuts::new(state, tx.clone())
                .on_update(|state| {
                    ShortcutsProps::default()
                        .shortcuts(&HelpPageProps::from(state).shortcuts)
@@ -609,10 +607,10 @@ impl<'a: 'static> Widget for HelpPage<'a> {
    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc | Key::Ctrl('c') => {
-
                let _ = self.base.send(Action::Exit { selection: None });
+
                let _ = self.send(Message::Exit { selection: None });
            }
            Key::Char('?') => {
-
                let _ = self.base.send(Action::LeavePage);
+
                let _ = self.send(Message::LeavePage);
            }
            _ => {
                self.content.handle_event(key);
@@ -640,11 +638,15 @@ impl<'a: 'static> Widget for HelpPage<'a> {
            .render(frame, RenderProps::from(shortcuts_area));

        if page_size != self.props.page_size {
-
            let _ = self.base.send(Action::HelpPageSize(page_size));
+
            let _ = self.send(Message::HelpPageSize(page_size));
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
    fn base(&self) -> &WidgetBase<State, Message> {
+
        &self.base
+
    }
+

+
    fn base_mut(&mut self) -> &mut WidgetBase<State, Message> {
        &mut self.base
    }
}
modified bin/commands/patch/select.rs
@@ -13,12 +13,10 @@ use radicle_tui as tui;

use tui::cob::patch;
use tui::store;
-
use tui::task;
-
use tui::task::Interrupted;
use tui::ui::items::{Filter, PatchItem, PatchItemFilter};
use tui::ui::widget::window::{Window, WindowProps};
use tui::ui::widget::{Properties, Widget};
-
use tui::ui::Frontend;
+
use tui::Channel;
use tui::Exit;

use tui::PageStack;
@@ -117,7 +115,7 @@ impl TryFrom<&Context> for State {
    }
}

-
pub enum Action {
+
pub enum Message {
    Exit { selection: Option<Selection> },
    Select { selected: Option<usize> },
    BrowserPageSize(usize),
@@ -132,40 +130,40 @@ pub enum Action {
}

impl store::State<Selection> for State {
-
    type Action = Action;
+
    type Message = Message;

-
    fn handle_action(&mut self, action: Action) -> Option<Exit<Selection>> {
-
        match action {
-
            Action::Exit { selection } => Some(Exit { value: selection }),
-
            Action::Select { selected } => {
+
    fn update(&mut self, message: Message) -> Option<Exit<Selection>> {
+
        match message {
+
            Message::Exit { selection } => Some(Exit { value: selection }),
+
            Message::Select { selected } => {
                self.browser.selected = selected;
                None
            }
-
            Action::BrowserPageSize(size) => {
+
            Message::BrowserPageSize(size) => {
                self.browser.page_size = size;
                None
            }
-
            Action::HelpPageSize(size) => {
+
            Message::HelpPageSize(size) => {
                self.help.page_size = size;
                None
            }
-
            Action::OpenSearch => {
+
            Message::OpenSearch => {
                self.browser.show_search = true;
                None
            }
-
            Action::UpdateSearch { value } => {
+
            Message::UpdateSearch { value } => {
                self.browser.search.write(value);
                self.browser.filter =
                    PatchItemFilter::from_str(&self.browser.search.read()).unwrap_or_default();

                None
            }
-
            Action::ApplySearch => {
+
            Message::ApplySearch => {
                self.browser.search.apply();
                self.browser.show_search = false;
                None
            }
-
            Action::CloseSearch => {
+
            Message::CloseSearch => {
                self.browser.search.reset();
                self.browser.show_search = false;
                self.browser.filter =
@@ -173,15 +171,15 @@ impl store::State<Selection> for State {

                None
            }
-
            Action::OpenHelp => {
+
            Message::OpenHelp => {
                self.pages.push(Page::Help);
                None
            }
-
            Action::LeavePage => {
+
            Message::LeavePage => {
                self.pages.pop();
                None
            }
-
            Action::ScrollHelp { progress } => {
+
            Message::ScrollHelp { progress } => {
                self.help.progress = progress;
                None
            }
@@ -197,19 +195,16 @@ impl App {
    }

    pub async fn run(&self) -> Result<Option<Selection>> {
-
        let (terminator, mut interrupt_rx) = task::create_termination();
-
        let (store, state_rx) = store::Store::<Action, State, Selection>::new();
-
        let (frontend, action_tx, action_rx) = Frontend::<Action>::new();
+
        let channel = Channel::default();
        let state = State::try_from(&self.context)?;
-

-
        let window: Window<State, Action, Page> = Window::new(&state, action_tx.clone())
+
        let window: Window<State, Message, Page> = Window::new(&state, channel.tx.clone())
            .page(
                Page::Browse,
-
                BrowserPage::new(&state, action_tx.clone()).to_boxed(),
+
                BrowserPage::new(&state, channel.tx.clone()).to_boxed(),
            )
            .page(
                Page::Help,
-
                HelpPage::new(&state, action_tx.clone()).to_boxed(),
+
                HelpPage::new(&state, channel.tx.clone()).to_boxed(),
            )
            .on_update(|state| {
                WindowProps::default()
@@ -217,18 +212,6 @@ impl App {
                    .to_boxed()
            });

-
        tokio::try_join!(
-
            store.main_loop(state, terminator, action_rx, interrupt_rx.resubscribe()),
-
            frontend.main_loop(Some(window), state_rx, interrupt_rx.resubscribe()),
-
        )?;
-

-
        if let Ok(reason) = interrupt_rx.recv().await {
-
            match reason {
-
                Interrupted::User { payload } => Ok(payload),
-
                Interrupted::OsSignal => anyhow::bail!("exited because of an os sig int"),
-
            }
-
        } else {
-
            anyhow::bail!("exited because of an unexpected error");
-
        }
+
        tui::run(channel, state, window).await
    }
}
modified bin/commands/patch/select/ui.rs
@@ -25,16 +25,16 @@ 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::{BaseView, BoxedAny, Properties, RenderProps, Widget};
+
use tui::ui::widget::{BoxedAny, Properties, RenderProps, Widget, WidgetBase};

use tui::Selection;

use crate::tui_patch::common::Mode;
use crate::tui_patch::common::PatchOperation;

-
use super::{Action, State};
+
use super::{Message, State};

-
type BoxedWidget = widget::BoxedWidget<State, Action>;
+
type BoxedWidget = widget::BoxedWidget<State, Message>;

#[derive(Clone)]
pub struct BrowserProps<'a> {
@@ -133,7 +133,7 @@ impl<'a: 'static> BoxedAny for BrowserProps<'a> {}

pub struct Browser<'a> {
    /// Internal base
-
    base: BaseView<State, Action>,
+
    base: WidgetBase<State, Message>,
    /// Internal props
    props: BrowserProps<'a>,
    /// Patches widget
@@ -143,34 +143,33 @@ pub struct Browser<'a> {
}

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

-
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self {
+
    fn new(state: &State, tx: UnboundedSender<Message>) -> Self {
        let props = BrowserProps::from(state);

        Self {
-
            base: BaseView {
-
                action_tx: action_tx.clone(),
-
                on_update: None,
-
                on_event: None,
-
            },
+
            base: WidgetBase::new(tx.clone()),
            props: props.clone(),
-
            patches: Container::new(state, action_tx.clone())
+
            patches: Container::new(state, tx.clone())
                .header(
-
                    Header::new(state, action_tx.clone())
+
                    Header::new(state, tx.clone())
                        .columns(props.header.clone())
                        .cutoff(props.cutoff, props.cutoff_after)
                        .to_boxed(),
                )
-
                .content(Box::<Table<State, Action, PatchItem, 9>>::new(
-
                    Table::new(state, action_tx.clone())
+
                .content(Box::<Table<State, Message, PatchItem, 9>>::new(
+
                    Table::new(state, tx.clone())
                        .on_event(|table| {
                            table
-
                                .downcast_mut::<Table<State, Action, PatchItem, 9>>()
+
                                .downcast_mut::<Table<State, Message, PatchItem, 9>>()
                                .and_then(|table| {
-
                                    let selected = table.selected();
-
                                    table.base_mut().send(Action::Select { selected }).ok()
+
                                    table
+
                                        .send(Message::Select {
+
                                            selected: table.selected(),
+
                                        })
+
                                        .ok()
                                });
                        })
                        .on_update(|state| {
@@ -186,7 +185,7 @@ impl<'a: 'static> Widget for Browser<'a> {
                        }),
                ))
                .footer(
-
                    Footer::new(state, action_tx.clone())
+
                    Footer::new(state, tx.clone())
                        .on_update(|state| {
                            let props = BrowserProps::from(state);

@@ -202,7 +201,7 @@ impl<'a: 'static> Widget for Browser<'a> {
                        .to_boxed()
                })
                .to_boxed(),
-
            search: Search::new(state, action_tx.clone()).to_boxed(),
+
            search: Search::new(state, tx.clone()).to_boxed(),
        }
    }

@@ -212,13 +211,13 @@ impl<'a: 'static> Widget for Browser<'a> {
        } else {
            match key {
                Key::Esc | Key::Ctrl('c') => {
-
                    let _ = self.base.send(Action::Exit { selection: None });
+
                    let _ = self.send(Message::Exit { selection: None });
                }
                Key::Char('?') => {
-
                    let _ = self.base.send(Action::OpenHelp);
+
                    let _ = self.send(Message::OpenHelp);
                }
                Key::Char('/') => {
-
                    let _ = self.base.send(Action::OpenSearch);
+
                    let _ = self.send(Message::OpenSearch);
                }
                Key::Char('\n') => {
                    let operation = match self.props.mode {
@@ -231,7 +230,7 @@ impl<'a: 'static> Widget for Browser<'a> {
                        .and_then(|selected| self.props.patches.get(selected))
                        .and_then(|patch| {
                            self.base
-
                                .send(Action::Exit {
+
                                .send(Message::Exit {
                                    selection: Some(Selection {
                                        operation,
                                        ids: vec![patch.id],
@@ -247,7 +246,7 @@ impl<'a: 'static> Widget for Browser<'a> {
                        .and_then(|selected| self.props.patches.get(selected))
                        .and_then(|patch| {
                            self.base
-
                                .send(Action::Exit {
+
                                .send(Message::Exit {
                                    selection: Some(Selection {
                                        operation: Some(PatchOperation::Checkout.to_string()),
                                        ids: vec![patch.id],
@@ -263,7 +262,7 @@ impl<'a: 'static> Widget for Browser<'a> {
                        .and_then(|selected| self.props.patches.get(selected))
                        .and_then(|patch| {
                            self.base
-
                                .send(Action::Exit {
+
                                .send(Message::Exit {
                                    selection: Some(Selection {
                                        operation: Some(PatchOperation::Diff.to_string()),
                                        ids: vec![patch.id],
@@ -301,7 +300,11 @@ impl<'a: 'static> Widget for Browser<'a> {
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
    fn base(&self) -> &WidgetBase<State, Message> {
+
        &self.base
+
    }
+

+
    fn base_mut(&mut self) -> &mut WidgetBase<State, Message> {
        &mut self.base
    }
}
@@ -344,7 +347,7 @@ impl<'a> BoxedAny for BrowserPageProps<'a> {}

pub struct BrowserPage<'a> {
    /// Internal base
-
    base: BaseView<State, Action>,
+
    base: WidgetBase<State, Message>,
    /// Internal props
    props: BrowserPageProps<'a>,
    /// Sections widget
@@ -354,21 +357,17 @@ pub struct BrowserPage<'a> {
}

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

-
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self {
+
    fn new(state: &State, tx: UnboundedSender<Message>) -> Self {
        let props = BrowserPageProps::from(state);

        Self {
-
            base: BaseView {
-
                action_tx: action_tx.clone(),
-
                on_update: None,
-
                on_event: None,
-
            },
+
            base: WidgetBase::new(tx.clone()),
            props: props.clone(),
-
            sections: SectionGroup::new(state, action_tx.clone())
-
                .section(Browser::new(state, action_tx.clone()).to_boxed())
+
            sections: SectionGroup::new(state, tx.clone())
+
                .section(Browser::new(state, tx.clone()).to_boxed())
                .on_update(|state| {
                    let props = BrowserPageProps::from(state);
                    SectionGroupProps::default()
@@ -376,7 +375,7 @@ impl<'a: 'static> Widget for BrowserPage<'a> {
                        .to_boxed()
                })
                .to_boxed(),
-
            shortcuts: Shortcuts::new(state, action_tx.clone())
+
            shortcuts: Shortcuts::new(state, tx.clone())
                .on_update(|state| {
                    ShortcutsProps::default()
                        .shortcuts(&BrowserPageProps::from(state).shortcuts)
@@ -392,10 +391,10 @@ impl<'a: 'static> Widget for BrowserPage<'a> {
        if self.props.handle_keys {
            match key {
                Key::Esc | Key::Ctrl('c') => {
-
                    let _ = self.base.send(Action::Exit { selection: None });
+
                    let _ = self.send(Message::Exit { selection: None });
                }
                Key::Char('?') => {
-
                    let _ = self.base.send(Action::OpenHelp);
+
                    let _ = self.send(Message::OpenHelp);
                }
                _ => {}
            }
@@ -426,11 +425,15 @@ impl<'a: 'static> Widget for BrowserPage<'a> {
            .render(frame, RenderProps::from(shortcuts_area));

        if page_size != self.props.page_size {
-
            let _ = self.base.send(Action::BrowserPageSize(page_size));
+
            let _ = self.send(Message::BrowserPageSize(page_size));
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
    fn base(&self) -> &WidgetBase<State, Message> {
+
        &self.base
+
    }
+

+
    fn base_mut(&mut self) -> &mut WidgetBase<State, Message> {
        &mut self.base
    }
}
@@ -441,7 +444,7 @@ impl Properties for SearchProps {}

pub struct Search {
    /// Internal base
-
    base: BaseView<State, Action>,
+
    base: WidgetBase<State, Message>,
    /// Internal props
    _props: SearchProps,
    /// Search input field
@@ -449,51 +452,46 @@ pub struct Search {
}

impl Widget for Search {
-
    type Action = Action;
+
    type Message = Message;
    type State = State;

-
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
+
    fn new(state: &State, tx: UnboundedSender<Message>) -> Self
    where
        Self: Sized,
    {
-
        let input = TextField::new(state, action_tx.clone())
-
            .on_event(|field| {
-
                field
-
                    .downcast_mut::<TextField<State, Action>>()
-
                    .and_then(|field| {
-
                        let text = field.text().unwrap_or(&String::new()).to_string();
-
                        field
-
                            .base_mut()
-
                            .send(Action::UpdateSearch { value: text })
-
                            .ok()
-
                    });
-
            })
-
            .on_update(|state| {
-
                TextFieldProps::default()
-
                    .text(&state.browser.search.read().to_string())
-
                    .title("Search")
-
                    .inline(true)
-
                    .to_boxed()
-
            })
-
            .to_boxed();
        Self {
-
            base: BaseView {
-
                action_tx: action_tx.clone(),
-
                on_update: None,
-
                on_event: None,
-
            },
+
            base: WidgetBase::new(tx.clone()),
            _props: SearchProps {},
-
            input,
+
            input: TextField::new(state, tx.clone())
+
                .on_event(|widget| {
+
                    widget
+
                        .downcast_mut::<TextField<State, Message>>()
+
                        .and_then(|field| {
+
                            field
+
                                .send(Message::UpdateSearch {
+
                                    value: field.text().unwrap_or(&String::new()).to_string(),
+
                                })
+
                                .ok()
+
                        });
+
                })
+
                .on_update(|state| {
+
                    TextFieldProps::default()
+
                        .text(&state.browser.search.read().to_string())
+
                        .title("Search")
+
                        .inline(true)
+
                        .to_boxed()
+
                })
+
                .to_boxed(),
        }
    }

    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc => {
-
                let _ = self.base.send(Action::CloseSearch);
+
                let _ = self.send(Message::CloseSearch);
            }
            Key::Char('\n') => {
-
                let _ = self.base.send(Action::ApplySearch);
+
                let _ = self.send(Message::ApplySearch);
            }
            _ => {
                self.input.handle_event(key);
@@ -513,7 +511,11 @@ impl Widget for Search {
        self.input.render(frame, RenderProps::from(layout[0]));
    }

-
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
    fn base(&self) -> &WidgetBase<State, Message> {
+
        &self.base
+
    }
+

+
    fn base_mut(&mut self) -> &mut WidgetBase<State, Message> {
        &mut self.base
    }
}
@@ -540,7 +542,7 @@ impl<'a> BoxedAny for HelpPageProps<'a> {}

pub struct HelpPage<'a> {
    /// Internal base
-
    base: BaseView<State, Action>,
+
    base: WidgetBase<State, Message>,
    /// Internal props
    props: HelpPageProps<'a>,
    /// Content widget
@@ -550,23 +552,19 @@ pub struct HelpPage<'a> {
}

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

-
    fn new(state: &State, action_tx: UnboundedSender<Action>) -> Self
+
    fn new(state: &State, tx: UnboundedSender<Message>) -> Self
    where
        Self: Sized,
    {
        Self {
-
            base: BaseView {
-
                action_tx: action_tx.clone(),
-
                on_update: None,
-
                on_event: None,
-
            },
+
            base: WidgetBase::new(tx.clone()),
            props: HelpPageProps::from(state),
-
            content: Container::new(state, action_tx.clone())
+
            content: Container::new(state, tx.clone())
                .header(
-
                    Header::new(state, action_tx.clone())
+
                    Header::new(state, tx.clone())
                        .on_update(|_| {
                            HeaderProps::default()
                                .columns([Column::new(" Help ", Constraint::Fill(1))].to_vec())
@@ -575,15 +573,15 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                        .to_boxed(),
                )
                .content(
-
                    Paragraph::new(state, action_tx.clone())
+
                    Paragraph::new(state, tx.clone())
                        .on_event(|paragraph| {
                            paragraph
-
                                .downcast_mut::<Paragraph<'_, State, Action>>()
+
                                .downcast_mut::<Paragraph<'_, State, Message>>()
                                .and_then(|paragraph| {
-
                                    let progress = paragraph.progress();
                                    paragraph
-
                                        .base_mut()
-
                                        .send(Action::ScrollHelp { progress })
+
                                        .send(Message::ScrollHelp {
+
                                            progress: paragraph.progress(),
+
                                        })
                                        .ok()
                                });
                        })
@@ -598,7 +596,7 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                        .to_boxed(),
                )
                .footer(
-
                    Footer::new(state, action_tx.clone())
+
                    Footer::new(state, tx.clone())
                        .on_update(|state| {
                            let props = HelpPageProps::from(state);

@@ -619,7 +617,7 @@ impl<'a: 'static> Widget for HelpPage<'a> {
                        .to_boxed(),
                )
                .to_boxed(),
-
            shortcuts: Shortcuts::new(state, action_tx.clone())
+
            shortcuts: Shortcuts::new(state, tx.clone())
                .on_update(|state| {
                    ShortcutsProps::default()
                        .shortcuts(&HelpPageProps::from(state).shortcuts)
@@ -632,10 +630,10 @@ impl<'a: 'static> Widget for HelpPage<'a> {
    fn handle_event(&mut self, key: termion::event::Key) {
        match key {
            Key::Esc | Key::Ctrl('c') => {
-
                let _ = self.base.send(Action::Exit { selection: None });
+
                let _ = self.send(Message::Exit { selection: None });
            }
            Key::Char('?') => {
-
                let _ = self.base.send(Action::LeavePage);
+
                let _ = self.send(Message::LeavePage);
            }
            _ => {
                self.content.handle_event(key);
@@ -662,11 +660,15 @@ impl<'a: 'static> Widget for HelpPage<'a> {
            .render(frame, RenderProps::from(shortcuts_area));

        if page_size != self.props.page_size {
-
            let _ = self.base.send(Action::HelpPageSize(page_size));
+
            let _ = self.send(Message::HelpPageSize(page_size));
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<State, Action> {
+
    fn base(&self) -> &WidgetBase<State, Message> {
+
        &self.base
+
    }
+

+
    fn base_mut(&mut self) -> &mut WidgetBase<State, Message> {
        &mut self.base
    }
}
modified src/lib.rs
@@ -8,9 +8,15 @@ pub mod task;
pub mod terminal;
pub mod ui;

+
use std::fmt::Debug;
+

use anyhow::Result;

use serde::ser::{Serialize, SerializeStruct, Serializer};
+
use store::State;
+
use task::Interrupted;
+
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
+
use ui::{widget::Widget, Frontend};

/// An optional return value.
#[derive(Clone, Debug)]
@@ -97,3 +103,45 @@ impl<T> PageStack<T> {
        }
    }
}
+

+
/// A multi-producer, single-consumer message channel.
+
pub struct Channel<M> {
+
    pub tx: UnboundedSender<M>,
+
    pub rx: UnboundedReceiver<M>,
+
}
+

+
impl<A> Default for Channel<A> {
+
    fn default() -> Self {
+
        let (tx, rx) = mpsc::unbounded_channel();
+
        Self { tx: tx.clone(), rx }
+
    }
+
}
+

+
/// Initialize a `Store` with the `State` given and a `Frontend` with the `Widget` given,
+
/// and run their main loops concurrently. Connect them to the `Channel` and also to
+
/// an interrupt broadcast channel also initialized in this function.
+
pub async fn run<S, M, W, P>(channel: Channel<M>, state: S, root: W) -> Result<Option<P>>
+
where
+
    S: State<P, Message = M> + Clone + Debug + Send + Sync + 'static,
+
    W: Widget<State = S, Message = M>,
+
    P: Clone + Debug + Send + Sync + 'static,
+
{
+
    let (terminator, mut interrupt_rx) = task::create_termination();
+

+
    let (store, state_rx) = store::Store::<S, M, P>::new();
+
    let frontend = Frontend::<M>::new(channel.tx.clone());
+

+
    tokio::try_join!(
+
        store.main_loop(state, terminator, channel.rx, interrupt_rx.resubscribe()),
+
        frontend.main_loop(Some(root), state_rx, interrupt_rx.resubscribe()),
+
    )?;
+

+
    if let Ok(reason) = interrupt_rx.recv().await {
+
        match reason {
+
            Interrupted::User { payload } => Ok(payload),
+
            Interrupted::OsSignal => anyhow::bail!("exited because of an os sig int"),
+
        }
+
    } else {
+
        anyhow::bail!("exited because of an unexpected error");
+
    }
+
}
modified src/store.rs
@@ -17,11 +17,11 @@ pub trait State<P>
where
    P: Clone + Debug + Send + Sync,
{
-
    type Action;
+
    type Message;

    /// Handle a user-defined application message and return an `Exit` object
    /// in case the received message requested the application to also quit.
-
    fn handle_action(&mut self, action: Self::Action) -> Option<Exit<P>>;
+
    fn update(&mut self, message: Self::Message) -> Option<Exit<P>>;

    /// Handle recurring tick.
    fn tick(&self);
@@ -29,16 +29,16 @@ where

/// The `Store` updates the applications' state concurrently. It handles
/// messages coming from the frontend and updates the state accordingly.
-
pub struct Store<A, S, P>
+
pub struct Store<S, M, P>
where
    S: State<P> + Clone + Send + Sync,
    P: Clone + Debug + Send + Sync,
{
    state_tx: UnboundedSender<S>,
-
    _phantom: PhantomData<(A, P)>,
+
    _phantom: PhantomData<(M, P)>,
}

-
impl<A, S, P> Store<A, S, P>
+
impl<S, M, P> Store<S, M, P>
where
    S: State<P> + Clone + Send + Sync,
    P: Clone + Debug + Send + Sync,
@@ -56,9 +56,9 @@ where
    }
}

-
impl<A, S, P> Store<A, S, P>
+
impl<S, M, P> Store<S, M, P>
where
-
    S: State<P, Action = A> + Clone + Debug + Send + Sync + 'static,
+
    S: State<P, Message = M> + Clone + Debug + Send + Sync + 'static,
    P: Clone + Debug + Send + Sync + 'static,
{
    /// By calling `main_loop`, the store will wait for new messages coming
@@ -69,7 +69,7 @@ where
        self,
        mut state: S,
        mut terminator: Terminator<P>,
-
        mut action_rx: UnboundedReceiver<A>,
+
        mut message_rx: UnboundedReceiver<M>,
        mut interrupt_rx: broadcast::Receiver<Interrupted<P>>,
    ) -> anyhow::Result<Interrupted<P>> {
        // Send the initial state once
@@ -79,10 +79,10 @@ where

        let result = loop {
            tokio::select! {
-
                // Handle the actions coming from the frontend
+
                // Handle the messages coming from the frontend
                // and process them to do async operations
-
                Some(action) = action_rx.recv() => {
-
                    if let Some(exit) = state.handle_action(action) {
+
                Some(message) = message_rx.recv() => {
+
                    if let Some(exit) = state.update(message) {
                        let interrupted = Interrupted::User { payload: exit.value };
                        let _ = terminator.terminate(interrupted.clone());

modified src/ui.rs
@@ -10,7 +10,8 @@ use std::fmt::Debug;
use std::time::Duration;

use tokio::sync::broadcast;
-
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
+
use tokio::sync::mpsc;
+
use tokio::sync::mpsc::UnboundedReceiver;

use crate::ui::widget::RenderProps;

@@ -28,22 +29,14 @@ const INLINE_HEIGHT: usize = 20;
///
/// Once created and run with `main_loop`, the `Frontend` will wait for new messages
/// being sent on either the terminal event, the state or the interrupt message channel.
-
pub struct Frontend<A> {
-
    action_tx: mpsc::UnboundedSender<A>,
+
pub struct Frontend<M> {
+
    tx: mpsc::UnboundedSender<M>,
}

-
impl<A> Frontend<A> {
+
impl<M> Frontend<M> {
    /// Create a new `Frontend` storing the sending end of a message channel.
-
    pub fn new() -> (Self, UnboundedSender<A>, UnboundedReceiver<A>) {
-
        let (action_tx, action_rx) = mpsc::unbounded_channel();
-

-
        (
-
            Self {
-
                action_tx: action_tx.clone(),
-
            },
-
            action_tx,
-
            action_rx,
-
        )
+
    pub fn new(tx: mpsc::UnboundedSender<M>) -> Self {
+
        Self { tx: tx.clone() }
    }

    /// By calling `main_loop`, the `Frontend` will wait for new messages being sent
@@ -68,7 +61,7 @@ impl<A> Frontend<A> {
    ) -> anyhow::Result<Interrupted<P>>
    where
        S: State<P>,
-
        W: Widget<State = S, Action = A>,
+
        W: Widget<State = S, Message = M>,
        P: Clone + Send + Sync + Debug,
    {
        let mut ticker = tokio::time::interval(RENDERING_TICK_RATE);
@@ -84,7 +77,7 @@ impl<A> Frontend<A> {
                    root.update(&state);
                    root
                }
-
                None => W::new(&state, self.action_tx.clone()),
+
                None => W::new(&state, self.tx.clone()),
            }
        };

modified src/ui/widget.rs
@@ -14,24 +14,35 @@ use termion::event::Key;
use ratatui::prelude::*;
use ratatui::widgets::Cell;

-
pub type BoxedWidget<S, A> = Box<dyn Widget<State = S, Action = A>>;
+
pub type BoxedWidget<S, M> = Box<dyn Widget<State = S, Message = M>>;

pub type UpdateCallback<S> = fn(&S) -> Box<dyn Any>;
pub type EventCallback = fn(&mut dyn Any);

-
/// A `View`s common fields.
-
pub struct BaseView<S, A> {
+
/// A `WidgetBase` provides common functionality to a `Widget`. It's used to store
+
/// event and update callbacks as well sending messages to the UI's message channel.
+
pub struct WidgetBase<S, M> {
    /// Message sender
-
    pub action_tx: UnboundedSender<A>,
+
    pub tx: UnboundedSender<M>,
    /// Custom update handler
    pub on_update: Option<UpdateCallback<S>>,
    /// Additional custom event handler
    pub on_event: Option<EventCallback>,
}

-
impl<S, A> BaseView<S, A> {
-
    pub fn send(&self, action: A) -> Result<(), SendError<A>> {
-
        self.action_tx.send(action)
+
impl<S, M> WidgetBase<S, M> {
+
    /// Create a new `WidgetBase` with no callbacks set.
+
    pub fn new(tx: UnboundedSender<M>) -> Self {
+
        Self {
+
            tx: tx.clone(),
+
            on_update: None,
+
            on_event: None,
+
        }
+
    }
+

+
    /// Send a message to the internal channel.
+
    pub fn send(&self, message: M) -> Result<(), SendError<M>> {
+
        self.tx.send(message)
    }
}

@@ -76,11 +87,11 @@ impl From<Rect> for RenderProps {
/// This is the trait that you should implement to define a custom `Widget`.
pub trait Widget {
    type State;
-
    type Action;
+
    type Message;

    /// Should return a new view with props build from state (if type is known) and a
    /// message sender set.
-
    fn new(state: &Self::State, action_tx: UnboundedSender<Self::Action>) -> Self
+
    fn new(state: &Self::State, tx: UnboundedSender<Self::Message>) -> Self
    where
        Self: Sized;

@@ -107,8 +118,16 @@ pub trait Widget {
    /// Optional render props can be given.
    fn render(&self, frame: &mut Frame, props: RenderProps);

-
    /// Return a mutable reference to this widgets' base view.
-
    fn base_mut(&mut self) -> &mut BaseView<Self::State, Self::Action>;
+
    /// Return a reference to this widgets' base.
+
    fn base(&self) -> &WidgetBase<Self::State, Self::Message>;
+

+
    /// Return a mutable reference to this widgets' base.
+
    fn base_mut(&mut self) -> &mut WidgetBase<Self::State, Self::Message>;
+

+
    /// Send a message to the widgets' base channel.
+
    fn send(&self, message: Self::Message) -> Result<(), SendError<Self::Message>> {
+
        self.base().send(message)
+
    }

    /// Should set the optional custom event handler.
    fn on_event(mut self, callback: EventCallback) -> Self
@@ -135,13 +154,6 @@ pub trait Widget {
    {
        Box::new(self)
    }
-

-
    fn downcast_ref(any: &dyn Any) -> Option<&Self>
-
    where
-
        Self: Sized + 'static,
-
    {
-
        any.downcast_ref::<Self>()
-
    }
}

/// Needs to be implemented for items that are supposed to be rendered in tables.
modified src/ui/widget/container.rs
@@ -10,7 +10,7 @@ use ratatui::widgets::{Block, BorderType, Borders, Row};
use crate::ui::ext::{FooterBlock, FooterBlockType, HeaderBlock};
use crate::ui::theme::style;

-
use super::{BaseView, BoxedAny, BoxedWidget, Properties, RenderProps, Widget};
+
use super::{BoxedAny, BoxedWidget, Properties, RenderProps, Widget, WidgetBase};

#[derive(Clone, Debug)]
pub struct Column<'a> {
@@ -67,11 +67,11 @@ impl<'a> Default for HeaderProps<'a> {
impl<'a: 'static> Properties for HeaderProps<'a> {}
impl<'a: 'static> BoxedAny for HeaderProps<'a> {}

-
pub struct Header<'a: 'static, S, A> {
+
pub struct Header<'a: 'static, S, M> {
    /// Internal props
    props: HeaderProps<'a>,
    /// Internal base
-
    base: BaseView<S, A>,
+
    base: WidgetBase<S, M>,
}

impl<'a, S, A> Header<'a, S, A> {
@@ -87,17 +87,13 @@ impl<'a, S, A> Header<'a, S, A> {
    }
}

-
impl<'a: 'static, S, A> Widget for Header<'a, S, A> {
-
    type Action = A;
+
impl<'a: 'static, S, M> Widget for Header<'a, S, M> {
+
    type Message = M;
    type State = S;

-
    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self {
+
    fn new(_state: &S, tx: UnboundedSender<M>) -> Self {
        Self {
-
            base: BaseView {
-
                action_tx: action_tx.clone(),
-
                on_update: None,
-
                on_event: None,
-
            },
+
            base: WidgetBase::new(tx.clone()),
            props: HeaderProps::default(),
        }
    }
@@ -170,7 +166,11 @@ impl<'a: 'static, S, A> Widget for Header<'a, S, A> {
        frame.render_widget(header, header_layout[0]);
    }

-
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
    fn base(&self) -> &WidgetBase<S, M> {
+
        &self.base
+
    }
+

+
    fn base_mut(&mut self) -> &mut WidgetBase<S, M> {
        &mut self.base
    }
}
@@ -208,14 +208,14 @@ impl<'a> Default for FooterProps<'a> {
impl<'a: 'static> Properties for FooterProps<'a> {}
impl<'a: 'static> BoxedAny for FooterProps<'a> {}

-
pub struct Footer<'a, S, A> {
+
pub struct Footer<'a, S, M> {
    /// Internal props
    props: FooterProps<'a>,
    /// Internal base
-
    base: BaseView<S, A>,
+
    base: WidgetBase<S, M>,
}

-
impl<'a, S, A> Footer<'a, S, A> {
+
impl<'a, S, M> Footer<'a, S, M> {
    pub fn columns(mut self, columns: Vec<Column<'a>>) -> Self {
        self.props.columns = columns;
        self
@@ -250,17 +250,13 @@ impl<'a, S, A> Footer<'a, S, A> {
    }
}

-
impl<'a: 'static, S, A> Widget for Footer<'a, S, A> {
-
    type Action = A;
+
impl<'a: 'static, S, M> Widget for Footer<'a, S, M> {
+
    type Message = M;
    type State = S;

-
    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self {
+
    fn new(_state: &S, tx: UnboundedSender<M>) -> Self {
        Self {
-
            base: BaseView {
-
                action_tx: action_tx.clone(),
-
                on_update: None,
-
                on_event: None,
-
            },
+
            base: WidgetBase::new(tx.clone()),
            props: FooterProps::default(),
        }
    }
@@ -309,7 +305,11 @@ impl<'a: 'static, S, A> Widget for Footer<'a, S, A> {
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
    fn base(&self) -> &WidgetBase<S, M> {
+
        &self.base
+
    }
+

+
    fn base_mut(&mut self) -> &mut WidgetBase<S, M> {
        &mut self.base
    }
}
@@ -329,51 +329,46 @@ impl ContainerProps {
impl Properties for ContainerProps {}
impl BoxedAny for ContainerProps {}

-
pub struct Container<S, A> {
+
pub struct Container<S, M> {
    /// Internal base
-
    base: BaseView<S, A>,
+
    base: WidgetBase<S, M>,
    /// Internal props
    props: ContainerProps,
    /// Container header
-
    header: Option<BoxedWidget<S, A>>,
+
    header: Option<BoxedWidget<S, M>>,
    /// Content widget
-
    content: Option<BoxedWidget<S, A>>,
+
    content: Option<BoxedWidget<S, M>>,
    /// Container footer
-
    footer: Option<BoxedWidget<S, A>>,
+
    footer: Option<BoxedWidget<S, M>>,
}

-
impl<S, A> Container<S, A> {
-
    pub fn header(mut self, header: BoxedWidget<S, A>) -> Self {
+
impl<S, M> Container<S, M> {
+
    pub fn header(mut self, header: BoxedWidget<S, M>) -> Self {
        self.header = Some(header);
        self
    }

-
    pub fn content(mut self, content: BoxedWidget<S, A>) -> Self {
+
    pub fn content(mut self, content: BoxedWidget<S, M>) -> Self {
        self.content = Some(content);
        self
    }

-
    pub fn footer(mut self, footer: BoxedWidget<S, A>) -> Self {
+
    pub fn footer(mut self, footer: BoxedWidget<S, M>) -> Self {
        self.footer = Some(footer);
        self
    }
}

-
impl<S, A> Widget for Container<S, A> {
-
    type Action = A;
+
impl<S, M> Widget for Container<S, M> {
+
    type Message = M;
    type State = S;

-
    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self
+
    fn new(_state: &S, tx: UnboundedSender<M>) -> Self
    where
        Self: Sized,
    {
        Self {
-
            base: BaseView {
-
                action_tx: action_tx.clone(),
-

-
                on_update: None,
-
                on_event: None,
-
            },
+
            base: WidgetBase::new(tx.clone()),
            props: ContainerProps::default(),
            header: None,
            content: None,
@@ -451,7 +446,11 @@ impl<S, A> Widget for Container<S, A> {
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
    fn base(&self) -> &WidgetBase<S, M> {
+
        &self.base
+
    }
+

+
    fn base_mut(&mut self) -> &mut WidgetBase<S, M> {
        &mut self.base
    }
}
@@ -480,19 +479,19 @@ impl SectionGroupProps {
impl Properties for SectionGroupProps {}
impl BoxedAny for SectionGroupProps {}

-
pub struct SectionGroup<S, A> {
+
pub struct SectionGroup<S, M> {
    /// Internal base
-
    base: BaseView<S, A>,
+
    base: WidgetBase<S, M>,
    /// Internal table properties
    props: SectionGroupProps,
    /// All sections
-
    sections: Vec<BoxedWidget<S, A>>,
+
    sections: Vec<BoxedWidget<S, M>>,
    /// Internal selection and offset state
    state: SectionGroupState,
}

-
impl<S, A> SectionGroup<S, A> {
-
    pub fn section(mut self, section: BoxedWidget<S, A>) -> Self {
+
impl<S, M> SectionGroup<S, M> {
+
    pub fn section(mut self, section: BoxedWidget<S, M>) -> Self {
        self.sections.push(section);
        self
    }
@@ -516,17 +515,17 @@ impl<S, A> SectionGroup<S, A> {
    }
}

-
impl<S: 'static, A: 'static> Widget for SectionGroup<S, A> {
+
impl<S, M> Widget for SectionGroup<S, M>
+
where
+
    S: 'static,
+
    M: 'static,
+
{
    type State = S;
-
    type Action = A;
+
    type Message = M;

-
    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self {
+
    fn new(_state: &S, tx: UnboundedSender<M>) -> Self {
        Self {
-
            base: BaseView {
-
                action_tx: action_tx.clone(),
-
                on_update: None,
-
                on_event: None,
-
            },
+
            base: WidgetBase::new(tx.clone()),
            props: SectionGroupProps::default(),
            sections: vec![],
            state: SectionGroupState { focus: Some(0) },
@@ -584,7 +583,11 @@ impl<S: 'static, A: 'static> Widget for SectionGroup<S, A> {
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
    fn base(&self) -> &WidgetBase<S, M> {
+
        &self.base
+
    }
+

+
    fn base_mut(&mut self) -> &mut WidgetBase<S, M> {
        &mut self.base
    }
}
modified src/ui/widget/input.rs
@@ -6,7 +6,7 @@ use ratatui::layout::{Constraint, Layout};
use ratatui::style::Stylize;
use ratatui::text::{Line, Span};

-
use super::{BaseView, BoxedAny, Properties, RenderProps, Widget};
+
use super::{BoxedAny, Properties, RenderProps, Widget, WidgetBase};

#[derive(Clone)]
pub struct TextFieldProps {
@@ -56,16 +56,16 @@ struct TextFieldState {

impl BoxedAny for TextFieldState {}

-
pub struct TextField<S, A> {
+
pub struct TextField<S, M> {
    /// Internal base
-
    base: BaseView<S, A>,
+
    base: WidgetBase<S, M>,
    /// Internal props
    props: TextFieldProps,
    /// Internal state
    state: TextFieldState,
}

-
impl<S, A> TextField<S, A> {
+
impl<S, M> TextField<S, M> {
    pub fn text(&self) -> Option<&String> {
        self.state.text.as_ref()
    }
@@ -131,17 +131,17 @@ impl<S, A> TextField<S, A> {
    }
}

-
impl<S: 'static, A: 'static> Widget for TextField<S, A> {
-
    type Action = A;
+
impl<S, M> Widget for TextField<S, M>
+
where
+
    S: 'static,
+
    M: 'static,
+
{
+
    type Message = M;
    type State = S;

-
    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self {
+
    fn new(_state: &S, tx: UnboundedSender<M>) -> Self {
        Self {
-
            base: BaseView {
-
                action_tx: action_tx.clone(),
-
                on_update: None,
-
                on_event: None,
-
            },
+
            base: WidgetBase::new(tx.clone()),
            props: TextFieldProps::default(),
            state: TextFieldState {
                text: None,
@@ -238,9 +238,13 @@ impl<S: 'static, A: 'static> Widget for TextField<S, A> {
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
    fn base(&self) -> &WidgetBase<S, M> {
+
        &self.base
+
    }
+

+
    fn base_mut(&mut self) -> &mut WidgetBase<S, M> {
        &mut self.base
    }
}

-
impl<S, A> BoxedAny for TextField<S, A> {}
+
impl<S, M> BoxedAny for TextField<S, M> {}
modified src/ui/widget/list.rs
@@ -14,7 +14,7 @@ use crate::ui::theme::style;
use crate::ui::{layout, span};

use super::BoxedAny;
-
use super::{container::Column, BaseView, Properties, RenderProps, ToRow, Widget};
+
use super::{container::Column, Properties, RenderProps, ToRow, Widget, WidgetBase};

#[derive(Clone, Debug)]
pub struct TableProps<'a, R, const W: usize>
@@ -88,19 +88,19 @@ impl<'a: 'static, R, const W: usize> BoxedAny for TableProps<'a, R, W> where R:

impl BoxedAny for TableState {}

-
pub struct Table<'a, S, A, R, const W: usize>
+
pub struct Table<'a, S, M, R, const W: usize>
where
    R: ToRow<W>,
{
    /// Internal base
-
    base: BaseView<S, A>,
+
    base: WidgetBase<S, M>,
    /// Internal table properties
    props: TableProps<'a, R, W>,
    /// Internal selection and offset state
    state: TableState,
}

-
impl<'a, S, A, R, const W: usize> Table<'a, S, A, R, W>
+
impl<'a, S, M, R, const W: usize> Table<'a, S, M, R, W>
where
    R: ToRow<W>,
{
@@ -159,20 +159,16 @@ where
    }
}

-
impl<'a: 'static, S: 'static, A: 'static, R, const W: usize> Widget for Table<'a, S, A, R, W>
+
impl<'a: 'static, S: 'a, M: 'a, R, const W: usize> Widget for Table<'a, S, M, R, W>
where
    R: ToRow<W> + Clone + 'static,
{
-
    type Action = A;
+
    type Message = M;
    type State = S;

-
    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self {
+
    fn new(_state: &S, tx: UnboundedSender<M>) -> Self {
        Self {
-
            base: BaseView {
-
                action_tx: action_tx.clone(),
-
                on_update: None,
-
                on_event: None,
-
            },
+
            base: WidgetBase::new(tx.clone()),
            props: TableProps::default(),
            state: TableState::default().with_selected(Some(0)),
        }
@@ -277,7 +273,11 @@ where
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
    fn base(&self) -> &WidgetBase<S, M> {
+
        &self.base
+
    }
+

+
    fn base_mut(&mut self) -> &mut WidgetBase<S, M> {
        &mut self.base
    }
}
modified src/ui/widget/text.rs
@@ -5,7 +5,7 @@ use termion::event::Key;
use ratatui::layout::{Constraint, Layout};
use ratatui::text::Text;

-
use super::{BaseView, BoxedAny, Properties, RenderProps, Widget};
+
use super::{BoxedAny, Properties, RenderProps, Widget, WidgetBase};

#[derive(Clone)]
pub struct ParagraphProps<'a> {
@@ -53,16 +53,16 @@ struct ParagraphState {

impl BoxedAny for ParagraphState {}

-
pub struct Paragraph<'a, S, A> {
+
pub struct Paragraph<'a, S, M> {
    /// Internal base
-
    base: BaseView<S, A>,
+
    base: WidgetBase<S, M>,
    /// Internal props
    props: ParagraphProps<'a>,
    /// Internal state
    state: ParagraphState,
}

-
impl<'a, S, A> Paragraph<'a, S, A> {
+
impl<'a, S, M> Paragraph<'a, S, M> {
    pub fn scroll(&self) -> (u16, u16) {
        (self.state.offset as u16, 0)
    }
@@ -136,20 +136,21 @@ impl<'a, S, A> Paragraph<'a, S, A> {
    }
}

-
impl<'a: 'static, S: 'static, A: 'static> Widget for Paragraph<'a, S, A> {
-
    type Action = A;
+
impl<'a, S, M> Widget for Paragraph<'a, S, M>
+
where
+
    'a: 'static,
+
    S: 'static,
+
    M: 'static,
+
{
+
    type Message = M;
    type State = S;

-
    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self
+
    fn new(_state: &S, tx: UnboundedSender<M>) -> Self
    where
        Self: Sized,
    {
        Self {
-
            base: BaseView {
-
                action_tx: action_tx.clone(),
-
                on_update: None,
-
                on_event: None,
-
            },
+
            base: WidgetBase::new(tx.clone()),
            props: ParagraphProps::default(),
            state: ParagraphState {
                offset: 0,
@@ -204,7 +205,11 @@ impl<'a: 'static, S: 'static, A: 'static> Widget for Paragraph<'a, S, A> {
        frame.render_widget(content, content_area);
    }

-
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
    fn base(&self) -> &WidgetBase<S, M> {
+
        &self.base
+
    }
+

+
    fn base_mut(&mut self) -> &mut WidgetBase<S, M> {
        &mut self.base
    }
}
modified src/ui/widget/window.rs
@@ -12,7 +12,7 @@ use ratatui::widgets::Row;

use crate::ui::theme::style;

-
use super::{BaseView, BoxedAny, BoxedWidget, Properties, RenderProps, Widget};
+
use super::{BoxedAny, BoxedWidget, Properties, RenderProps, Widget, WidgetBase};

#[derive(Clone)]
pub struct WindowProps<Id> {
@@ -35,42 +35,39 @@ impl<Id> Default for WindowProps<Id> {
impl<Id> Properties for WindowProps<Id> {}
impl<Id> BoxedAny for WindowProps<Id> {}

-
pub struct Window<S, A, Id> {
+
pub struct Window<S, M, Id> {
    /// Internal base
-
    base: BaseView<S, A>,
+
    base: WidgetBase<S, M>,
    /// Internal properties
    props: WindowProps<Id>,
    /// All pages known
-
    pages: HashMap<Id, BoxedWidget<S, A>>,
+
    pages: HashMap<Id, BoxedWidget<S, M>>,
}

-
impl<S, A, Id> Window<S, A, Id>
+
impl<S, M, Id> Window<S, M, Id>
where
    Id: Clone + Hash + Eq + PartialEq,
{
-
    pub fn page(mut self, id: Id, page: BoxedWidget<S, A>) -> Self {
+
    pub fn page(mut self, id: Id, page: BoxedWidget<S, M>) -> Self {
        self.pages.insert(id, page);
        self
    }
}

-
impl<'a: 'static, S, A, Id> Widget for Window<S, A, Id>
+
impl<'a, S, M, Id> Widget for Window<S, M, Id>
where
-
    Id: Clone + Hash + Eq + PartialEq + 'a,
+
    'a: 'static,
+
    Id: Clone + Hash + Eq + PartialEq + 'static,
{
-
    type Action = A;
+
    type Message = M;
    type State = S;

-
    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self
+
    fn new(_state: &S, tx: UnboundedSender<M>) -> Self
    where
        Self: Sized,
    {
        Self {
-
            base: BaseView {
-
                action_tx: action_tx.clone(),
-
                on_update: None,
-
                on_event: None,
-
            },
+
            base: WidgetBase::new(tx.clone()),
            props: WindowProps::default(),
            pages: HashMap::new(),
        }
@@ -117,7 +114,11 @@ where
        }
    }

-
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
    fn base(&self) -> &WidgetBase<S, M> {
+
        &self.base
+
    }
+

+
    fn base_mut(&mut self) -> &mut WidgetBase<S, M> {
        &mut self.base
    }
}
@@ -155,14 +156,14 @@ impl Default for ShortcutsProps {
impl Properties for ShortcutsProps {}
impl BoxedAny for ShortcutsProps {}

-
pub struct Shortcuts<S, A> {
+
pub struct Shortcuts<S, M> {
    /// Internal properties
    props: ShortcutsProps,
    /// Internal base
-
    base: BaseView<S, A>,
+
    base: WidgetBase<S, M>,
}

-
impl<S, A> Shortcuts<S, A> {
+
impl<S, M> Shortcuts<S, M> {
    pub fn divider(mut self, divider: char) -> Self {
        self.props.divider = divider;
        self
@@ -179,17 +180,13 @@ impl<S, A> Shortcuts<S, A> {
    }
}

-
impl<S, A> Widget for Shortcuts<S, A> {
-
    type Action = A;
+
impl<S, M> Widget for Shortcuts<S, M> {
+
    type Message = M;
    type State = S;

-
    fn new(_state: &S, action_tx: UnboundedSender<A>) -> Self {
+
    fn new(_state: &S, tx: UnboundedSender<M>) -> Self {
        Self {
-
            base: BaseView {
-
                action_tx: action_tx.clone(),
-
                on_update: None,
-
                on_event: None,
-
            },
+
            base: WidgetBase::new(tx.clone()),
            props: ShortcutsProps::default(),
        }
    }
@@ -239,7 +236,11 @@ impl<S, A> Widget for Shortcuts<S, A> {
        frame.render_widget(table, props.area);
    }

-
    fn base_mut(&mut self) -> &mut BaseView<S, A> {
+
    fn base(&self) -> &WidgetBase<S, M> {
+
        &self.base
+
    }
+

+
    fn base_mut(&mut self) -> &mut WidgetBase<S, M> {
        &mut self.base
    }
}