Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
Switch to crossterm backend
Merged did:key:z6MkgFq6...nBGz opened 4 months ago
22 files changed +434 -415 d99bcd24 e414502f
modified Cargo.lock
@@ -595,6 +595,22 @@ dependencies = [

[[package]]
name = "crossterm"
+
version = "0.28.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
+
dependencies = [
+
 "bitflags 2.9.3",
+
 "crossterm_winapi",
+
 "mio 1.0.4",
+
 "parking_lot",
+
 "rustix 0.38.44",
+
 "signal-hook",
+
 "signal-hook-mio",
+
 "winapi",
+
]
+

+
[[package]]
+
name = "crossterm"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
@@ -605,7 +621,7 @@ dependencies = [
 "document-features",
 "mio 1.0.4",
 "parking_lot",
-
 "rustix",
+
 "rustix 1.0.8",
 "signal-hook",
 "signal-hook-mio",
 "winapi",
@@ -1393,7 +1409,7 @@ dependencies = [
 "newline-converter",
 "once_cell",
 "tempfile",
-
 "termion 2.0.3",
+
 "termion",
 "unicode-segmentation",
 "unicode-width 0.1.14",
]
@@ -1565,6 +1581,12 @@ dependencies = [

[[package]]
name = "linux-raw-sys"
+
version = "0.4.15"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+

+
[[package]]
+
name = "linux-raw-sys"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
@@ -1798,12 +1820,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"

[[package]]
-
name = "numtoa"
-
version = "0.2.4"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f"
-

-
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2384,7 +2400,6 @@ dependencies = [
 "signal-hook",
 "simple-logging",
 "terminal-light",
-
 "termion 4.0.5",
 "textwrap",
 "thiserror 2.0.16",
 "timeago 0.5.0",
@@ -2434,6 +2449,7 @@ dependencies = [
 "bitflags 2.9.3",
 "cassowary",
 "compact_str",
+
 "crossterm 0.28.1",
 "indoc",
 "instability",
 "itertools 0.13.0",
@@ -2441,7 +2457,6 @@ dependencies = [
 "paste",
 "serde",
 "strum",
-
 "termion 4.0.5",
 "time",
 "unicode-segmentation",
 "unicode-truncate",
@@ -2566,6 +2581,19 @@ checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"

[[package]]
name = "rustix"
+
version = "0.38.44"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+
dependencies = [
+
 "bitflags 2.9.3",
+
 "errno",
+
 "libc",
+
 "linux-raw-sys 0.4.15",
+
 "windows-sys 0.52.0",
+
]
+

+
[[package]]
+
name = "rustix"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
@@ -2573,7 +2601,7 @@ dependencies = [
 "bitflags 2.9.3",
 "errno",
 "libc",
-
 "linux-raw-sys",
+
 "linux-raw-sys 0.9.4",
 "windows-sys 0.60.2",
]

@@ -3073,7 +3101,7 @@ dependencies = [
 "fastrand",
 "getrandom 0.3.3",
 "once_cell",
-
 "rustix",
+
 "rustix 1.0.8",
 "windows-sys 0.60.2",
]

@@ -3097,19 +3125,7 @@ checksum = "c4648c7def6f2043b2568617b9f9b75eae88ca185dbc1f1fda30e95a85d49d7d"
dependencies = [
 "libc",
 "libredox 0.0.2",
-
 "numtoa 0.1.0",
-
 "redox_termios",
-
]
-

-
[[package]]
-
name = "termion"
-
version = "4.0.5"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "3669a69de26799d6321a5aa713f55f7e2cd37bd47be044b50f2acafc42c122bb"
-
dependencies = [
-
 "libc",
-
 "libredox 0.1.9",
-
 "numtoa 0.2.4",
+
 "numtoa",
 "redox_termios",
]

@@ -3497,8 +3513,8 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae"
dependencies = [
+
 "crossterm 0.28.1",
 "ratatui",
-
 "termion 4.0.5",
 "unicode-width 0.2.0",
]

@@ -4199,7 +4215,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909"
dependencies = [
 "libc",
-
 "rustix",
+
 "rustix 1.0.8",
]

[[package]]
modified Cargo.toml
@@ -43,7 +43,7 @@ radicle-surf = { version = "0.22.0" }
radicle-signals = { version = "0.11.0" }
ratatui = { version = "0.29.0", default-features = false, features = [
    "all-widgets",
-
    "termion",
+
    "crossterm",
    "serde",
] }
md5 = { version = "0.8.0" }
@@ -52,7 +52,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0" }
signal-hook = { version = "0.3.17" }
timeago = { version = "0.5.0" }
-
termion = { version = "4.0.5" }
+
# termion = { version = "4.0.5" }
terminal-light = { version = "1.4.0" }
textwrap = { version = "0.16.0" }
thiserror = { version = "2.0.16" }
@@ -60,7 +60,7 @@ tokio = { version = "1.32.0", features = ["full"] }
tokio-util = { version = "0.7.17" }
tokio-stream = { version = "0.1.14" }
tui-textarea = { version = "0.7.0", default-features = false, features = [
-
    "termion",
+
    "crossterm",
] }
tui-tree-widget = { version = "0.23.0" }

modified bin/commands/inbox/list.rs
@@ -3,8 +3,6 @@ use std::sync::{Arc, Mutex};

use anyhow::Result;

-
use termion::event::Key;
-

use ratatui::layout::Constraint;
use ratatui::layout::Layout;
use ratatui::prelude::*;
@@ -19,6 +17,7 @@ use radicle::Profile;

use radicle_tui as tui;

+
use tui::event::Key;
use tui::store;
use tui::task::{Process, Task};
use tui::ui::im;
@@ -27,12 +26,12 @@ use tui::ui::im::{Borders, Show};
use tui::ui::{BufferedValue, Column, Spacing};
use tui::{Channel, Exit};

+
use super::common::{Mode, RepositoryMode};
+
use crate::commands::tui_inbox::common::InboxOperation;
use crate::ui::items::filter::Filter;
use crate::ui::items::notification::filter::{NotificationFilter, SortBy};
use crate::ui::items::notification::Notification;

-
use super::common::{InboxOperation, Mode, RepositoryMode};
-

type Selection = tui::Selection<NotificationId>;

const HELP: &str = r#"# Generic keybindings
@@ -301,22 +300,6 @@ impl Show<Message> for App {
                            }
                        },
                    );
-
                    if ui.has_input(|key| key == Key::Char('?')) {
-
                        ui.send_message(Message::Changed(Change::Page { page: Page::Help }));
-
                    }
-
                    if ui.has_input(|key| key == Key::Char('\n')) {
-
                        ui.send_message(Message::Exit {
-
                            operation: Some(InboxOperation::Show),
-
                        });
-
                    }
-
                    if ui.has_input(|key| key == Key::Char('c')) {
-
                        ui.send_message(Message::Exit {
-
                            operation: Some(InboxOperation::Clear),
-
                        });
-
                    }
-
                    if ui.has_input(|key| key == Key::Char('r')) {
-
                        ui.send_message(Message::Reload);
-
                    }
                }

                Page::Help => {
@@ -417,6 +400,23 @@ impl App {
            self.show_browser_context(frame, ui);
            self.show_browser_shortcuts(frame, ui);
        });
+

+
        if ui.has_input(|key| key == Key::Char('?')) {
+
            ui.send_message(Message::Changed(Change::Page { page: Page::Help }));
+
        }
+
        if ui.has_input(|key| key == Key::Enter) {
+
            ui.send_message(Message::Exit {
+
                operation: Some(InboxOperation::Show),
+
            });
+
        }
+
        if ui.has_input(|key| key == Key::Char('c')) {
+
            ui.send_message(Message::Exit {
+
                operation: Some(InboxOperation::Clear),
+
            });
+
        }
+
        if ui.has_input(|key| key == Key::Char('r')) {
+
            ui.send_message(Message::Reload);
+
        }
    }

    pub fn show_browser_search(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
@@ -445,7 +445,7 @@ impl App {
        if ui.has_input(|key| key == Key::Esc) {
            ui.send_message(Message::HideSearch { apply: false });
        }
-
        if ui.has_input(|key| key == Key::Char('\n')) {
+
        if ui.has_input(|key| key == Key::Enter) {
            ui.send_message(Message::HideSearch { apply: true });
        }
    }
modified bin/commands/issue/list.rs
@@ -6,8 +6,6 @@ use std::str::FromStr;

use anyhow::{bail, Result};

-
use termion::event::Key;
-

use ratatui::layout::Constraint;
use ratatui::style::Stylize;
use ratatui::text::Text;
@@ -21,6 +19,7 @@ use radicle::Profile;

use radicle_tui as tui;

+
use tui::event::{Event, Key};
use tui::store;
use tui::task::EmptyProcessors;
use tui::ui::rm::widget::container::{
@@ -433,22 +432,26 @@ fn browser_page(channel: &Channel<Message>) -> Widget<State, Message> {
        )
        .shortcuts(shortcuts)
        .to_widget(tx.clone())
-
        .on_event(|key, _, props| {
+
        .on_event(|event, _, props| {
            let default = PageProps::default();
            let props = props
                .and_then(|props| props.inner_ref::<PageProps>())
                .unwrap_or(&default);

            if props.handle_keys {
-
                match key {
-
                    Key::Char('q') | Key::Ctrl('c') => Some(Message::Quit),
-
                    Key::Char('p') => Some(Message::TogglePreview),
-
                    Key::Char('?') => Some(Message::OpenHelp),
-
                    Key::Char('\n') => Some(Message::ExitFromMode),
-
                    Key::Char('e') => Some(Message::Exit {
-
                        operation: Some(IssueOperation::Edit),
-
                    }),
-
                    _ => None,
+
                if let Event::Key(key) = event {
+
                    match key {
+
                        Key::Char('q') | Key::Ctrl('c') => Some(Message::Quit),
+
                        Key::Char('p') => Some(Message::TogglePreview),
+
                        Key::Char('?') => Some(Message::OpenHelp),
+
                        Key::Enter => Some(Message::ExitFromMode),
+
                        Key::Char('e') => Some(Message::Exit {
+
                            operation: Some(IssueOperation::Edit),
+
                        }),
+
                        _ => None,
+
                    }
+
                } else {
+
                    None
                }
            } else {
                None
@@ -646,9 +649,9 @@ fn help_page(channel: &Channel<Message>) -> Widget<State, Message> {
        .content(content)
        .shortcuts(shortcuts)
        .to_widget(tx.clone())
-
        .on_event(|key, _, _| match key {
-
            Key::Char('q') | Key::Ctrl('c') => Some(Message::Quit),
-
            Key::Char('?') => Some(Message::LeavePage),
+
        .on_event(|event, _, _| match event {
+
            Event::Key(Key::Char('q')) | Event::Key(Key::Ctrl('c')) => Some(Message::Quit),
+
            Event::Key(Key::Char('?')) => Some(Message::LeavePage),
            _ => None,
        })
        .on_update(|_| PageProps::default().handle_keys(true).to_boxed_any().into())
modified bin/commands/issue/list/ui.rs
@@ -3,17 +3,17 @@ use std::str::FromStr;
use std::vec;

use radicle::issue::{self, CloseReason};
+

use ratatui::Frame;
use tokio::sync::broadcast;

-
use termion::event::Key;
-

use ratatui::layout::{Constraint, Layout};
use ratatui::style::Stylize;
use ratatui::text::{Line, Text};

use radicle_tui as tui;

+
use tui::event::{Event, Key};
use tui::ui::rm::widget;
use tui::ui::rm::widget::container::{
    Container, ContainerProps, Footer, FooterProps, Header, HeaderProps,
@@ -197,29 +197,29 @@ impl View for Browser {
    type Message = Message;
    type State = State;

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

        if props.show_search {
-
            match key {
-
                Key::Esc => {
+
            match event {
+
                Event::Key(Key::Esc) => {
                    self.search.reset();
                    Some(Message::CloseSearch)
                }
-
                Key::Char('\n') => Some(Message::ApplySearch),
+
                Event::Key(Key::Enter) => Some(Message::ApplySearch),
                _ => {
-
                    self.search.handle_event(key);
+
                    self.search.handle_event(event);
                    None
                }
            }
        } else {
-
            match key {
-
                Key::Char('/') => Some(Message::OpenSearch),
+
            match event {
+
                Event::Key(Key::Char('/')) => Some(Message::OpenSearch),
                _ => {
-
                    self.issues.handle_event(key);
+
                    self.issues.handle_event(event);
                    None
                }
            }
modified bin/commands/patch/list.rs
@@ -3,8 +3,6 @@ use std::sync::{Arc, Mutex};

use anyhow::Result;

-
use termion::event::Key;
-

use radicle::patch::PatchId;
use radicle::storage::git::Repository;
use radicle::Profile;
@@ -16,6 +14,7 @@ use ratatui::{Frame, Viewport};

use radicle_tui as tui;

+
use tui::event::Key;
use tui::store;
use tui::task::EmptyProcessors;
use tui::ui::im;
@@ -285,27 +284,6 @@ impl Show<Message> for App {
                            }
                        },
                    );
-
                    if ui.has_input(|key| key == Key::Char('?')) {
-
                        ui.send_message(Message::Changed(Change::Page { page: Page::Help }));
-
                    }
-
                    if ui.has_input(|key| key == Key::Char('\n')) {
-
                        ui.send_message(Message::ExitFromMode);
-
                    }
-
                    if ui.has_input(|key| key == Key::Char('d')) {
-
                        ui.send_message(Message::Exit {
-
                            operation: Some(PatchOperation::Diff),
-
                        });
-
                    }
-
                    if ui.has_input(|key| key == Key::Char('r')) {
-
                        ui.send_message(Message::Exit {
-
                            operation: Some(PatchOperation::Review),
-
                        });
-
                    }
-
                    if ui.has_input(|key| key == Key::Char('c')) {
-
                        ui.send_message(Message::Exit {
-
                            operation: Some(PatchOperation::Checkout),
-
                        });
-
                    }
                }

                Page::Help => {
@@ -400,6 +378,28 @@ impl App {
            self.show_browser_context(frame, ui);
            self.show_browser_shortcuts(frame, ui);
        });
+

+
        if ui.has_input(|key| key == Key::Char('?')) {
+
            ui.send_message(Message::Changed(Change::Page { page: Page::Help }));
+
        }
+
        if ui.has_input(|key| key == Key::Enter) {
+
            ui.send_message(Message::ExitFromMode);
+
        }
+
        if ui.has_input(|key| key == Key::Char('d')) {
+
            ui.send_message(Message::Exit {
+
                operation: Some(PatchOperation::Diff),
+
            });
+
        }
+
        if ui.has_input(|key| key == Key::Char('r')) {
+
            ui.send_message(Message::Exit {
+
                operation: Some(PatchOperation::Review),
+
            });
+
        }
+
        if ui.has_input(|key| key == Key::Char('c')) {
+
            ui.send_message(Message::Exit {
+
                operation: Some(PatchOperation::Checkout),
+
            });
+
        }
    }

    pub fn show_browser_search(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
@@ -428,7 +428,7 @@ impl App {
        if ui.has_input(|key| key == Key::Esc) {
            ui.send_message(Message::HideSearch { apply: false });
        }
-
        if ui.has_input(|key| key == Key::Char('\n')) {
+
        if ui.has_input(|key| key == Key::Enter) {
            ui.send_message(Message::HideSearch { apply: true });
        }
    }
modified bin/commands/patch/review.rs
@@ -8,8 +8,6 @@ use anyhow::Result;

use serde::{Deserialize, Serialize};

-
use termion::event::Key;
-

use ratatui::layout::{Constraint, Layout, Position};
use ratatui::style::{Style, Stylize};
use ratatui::text::Text;
@@ -22,6 +20,7 @@ use radicle::Storage;

use radicle_tui as tui;

+
use tui::event::Key;
use tui::store;
use tui::task::EmptyProcessors;
use tui::ui::im::widget::{ContainerState, TableState, TextViewState, Window};
modified bin/ui/im.rs
@@ -1,11 +1,11 @@
use radicle_tui::ui::Spacing;
-
use termion::event::Key;

use ratatui::layout::{Constraint, Layout};
use ratatui::Frame;

use radicle_tui as tui;

+
use tui::event::Key;
use tui::ui::im::widget::{TableState, TextEditState, Widget};
use tui::ui::im::{Borders, Response, Ui};
use tui::ui::{BufferedValue, Column, ToRow};
@@ -176,7 +176,7 @@ where
                *self.show_search = false;
                self.search.reset();
            }
-
            if ui.has_input(|key| key == Key::Char('\n')) {
+
            if ui.has_input(|key| key == Key::Enter) {
                *self.show_search = false;
                self.search.apply();
            }
modified examples/basic_rmui.rs
@@ -1,12 +1,11 @@
use anyhow::Result;

-
use termion::event::Key;
-

use ratatui::layout::Constraint;
use ratatui::Viewport;

use radicle_tui as tui;

+
use tui::event::{Event, Key};
use tui::store;
use tui::task::EmptyProcessors;
use tui::ui::rm::widget::container::{Container, Header, HeaderProps};
@@ -102,9 +101,9 @@ pub async fn main() -> Result<()> {
    let window = Window::default()
        .page(0, page)
        .to_widget(sender.clone())
-
        .on_event(|key, _, _| match key {
-
            Key::Char('r') => Some(Message::ReverseContent),
-
            Key::Char('q') => Some(Message::Quit),
+
        .on_event(|event, _, _| match event {
+
            Event::Key(Key::Char('r')) => Some(Message::ReverseContent),
+
            Event::Key(Key::Char('q')) => Some(Message::Quit),
            _ => None,
        })
        .on_update(|_| WindowProps::default().current_page(0).to_boxed_any().into());
modified examples/hello.rs
@@ -1,12 +1,12 @@
use anyhow::Result;

use ratatui::layout::Position;
-
use termion::event::Key;

use ratatui::{Frame, Viewport};

use radicle_tui as tui;

+
use tui::event::Key;
use tui::store;
use tui::task::EmptyProcessors;
use tui::ui::im::widget::Window;
modified examples/hello_rrmui.rs
@@ -1,13 +1,13 @@
use anyhow::Result;

use ratatui::Viewport;
-
use termion::event::Key;

use ratatui::style::Color;
use ratatui::text::Text;

use radicle_tui as tui;

+
use tui::event::{Event, Key};
use tui::store;
use tui::task::EmptyProcessors;
use tui::ui::rm::widget::text::{TextArea, TextAreaProps};
@@ -59,8 +59,8 @@ pub async fn main() -> Result<()> {

    let scene = TextArea::default()
        .to_widget(sender.clone())
-
        .on_event(|key, _, _| match key {
-
            Key::Char('q') => Some(Message::Quit),
+
        .on_event(|event, _, _| match event {
+
            Event::Key(Key::Char('q')) => Some(Message::Quit),
            _ => None,
        })
        .on_update(|app: &App| {
modified examples/selection.rs
@@ -2,9 +2,6 @@ use std::time::{SystemTime, UNIX_EPOCH};

use anyhow::Result;

-
use radicle_tui::ui::Spacing;
-
use termion::event::Key;
-

use ratatui::layout::{Constraint, Layout};
use ratatui::style::Stylize;
use ratatui::text::Span;
@@ -13,9 +10,11 @@ use ratatui::{Frame, Viewport};

use radicle_tui as tui;

+
use tui::event::Key;
use tui::task::EmptyProcessors;
use tui::ui::im::widget::Window;
use tui::ui::im::{Borders, Context};
+
use tui::ui::Spacing;
use tui::ui::{Column, ToRow};
use tui::Channel;
use tui::{
@@ -120,7 +119,7 @@ impl Show<Message> for App {

                    ui.shortcuts(frame, &[("q", "quit")], '|');

-
                    if ui.has_input(|key| key == Key::Char('\n')) {
+
                    if ui.has_input(|key| key == Key::Enter) {
                        ui.send_message(Message::Return);
                    }
                },
modified src/event.rs
@@ -1,5 +1,61 @@
+
use ratatui::crossterm::{self, event::KeyModifiers};
+

+
#[derive(Clone, Copy, Debug, PartialEq)]
+
pub enum Key {
+
    Char(char),
+
    Alt(char),
+
    Ctrl(char),
+
    Enter,
+
    Backspace,
+
    Tab,
+
    BackTab,
+
    Delete,
+
    Up,
+
    Down,
+
    Left,
+
    Right,
+
    PageUp,
+
    PageDown,
+
    Home,
+
    End,
+
    Esc,
+
    Unknown,
+
}
+

#[derive(Clone, Copy, Debug)]
pub enum Event {
-
    Key(termion::event::Key),
-
    Resize,
+
    Key(Key),
+
    Resize(u16, u16),
+
    Unknown,
+
}
+

+
impl From<crossterm::event::Event> for Event {
+
    fn from(event: crossterm::event::Event) -> Self {
+
        use crossterm::event::KeyCode;
+

+
        match event {
+
            crossterm::event::Event::Key(key) => match (key.code, key.modifiers) {
+
                (KeyCode::Char(c), KeyModifiers::CONTROL) => Event::Key(Key::Ctrl(c)),
+
                (KeyCode::Char(c), KeyModifiers::ALT) => Event::Key(Key::Alt(c)),
+
                (KeyCode::Char(c), _) => Event::Key(Key::Char(c)),
+
                (KeyCode::Enter, _) => Event::Key(Key::Enter),
+
                (KeyCode::Backspace, _) => Event::Key(Key::Backspace),
+
                (KeyCode::Tab, _) => Event::Key(Key::Tab),
+
                (KeyCode::BackTab, _) => Event::Key(Key::BackTab),
+
                (KeyCode::Delete, _) => Event::Key(Key::Delete),
+
                (KeyCode::Up, _) => Event::Key(Key::Up),
+
                (KeyCode::Down, _) => Event::Key(Key::Down),
+
                (KeyCode::Left, _) => Event::Key(Key::Left),
+
                (KeyCode::Right, _) => Event::Key(Key::Right),
+
                (KeyCode::PageUp, _) => Event::Key(Key::PageUp),
+
                (KeyCode::PageDown, _) => Event::Key(Key::PageDown),
+
                (KeyCode::Home, _) => Event::Key(Key::Home),
+
                (KeyCode::Esc, _) => Event::Key(Key::Esc),
+
                (KeyCode::End, _) => Event::Key(Key::End),
+
                _ => Event::Key(Key::Unknown),
+
            },
+
            crossterm::event::Event::Resize(x, y) => Event::Resize(x, y),
+
            _ => Event::Unknown,
+
        }
+
    }
}
modified src/terminal.rs
@@ -1,31 +1,25 @@
use std::io;
-
use std::io::Write;
-
use std::thread;
use std::time::Duration;

-
use signal_hook::iterator::Signals;
-

use tokio::sync::broadcast;
use tokio::sync::mpsc::UnboundedSender;

use tokio_util::sync::CancellationToken;

-
use termion::async_stdin;
-
use termion::input::TermRead;
-
use termion::raw::{IntoRawMode, RawTerminal};
-

use ratatui::prelude::*;
-
use ratatui::termion::screen::{AlternateScreen, IntoAlternateScreen};
use ratatui::{CompletedFrame, TerminalOptions, Viewport};

-
use crate::Share;
+
use ratatui::crossterm::execute;
+
use ratatui::crossterm::terminal::{
+
    disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
+
};

use super::event::Event;
-
use super::Interrupted;
+
use super::{Interrupted, Share};

-
pub type Backend<S> = TermionBackendExt<S>;
-
pub type InlineTerminal = ratatui::Terminal<Backend<RawTerminal<io::Stdout>>>;
-
pub type FullscreenTerminal = ratatui::Terminal<Backend<AlternateScreen<RawTerminal<io::Stdout>>>>;
+
pub type Backend<S> = CrosstermBackend<S>;
+
pub type InlineTerminal = ratatui::Terminal<Backend<io::Stdout>>;
+
pub type FullscreenTerminal = ratatui::Terminal<Backend<io::Stdout>>;

const STDIN_TICK_RATE: Duration = Duration::from_millis(20);

@@ -38,18 +32,24 @@ impl Terminal {
    pub fn restore(&mut self) -> io::Result<()> {
        match self {
            Terminal::Fullscreen(inner) => {
+
                disable_raw_mode()?;
+
                execute!(io::stdout(), LeaveAlternateScreen)?;
                inner.clear()?;
            }
            Terminal::Inline(inner) => {
-
                // TODO(erikli): Check if still needed.
-
                let area = inner.get_frame().area();
-
                let position = Position::new(area.x, area.y);
-
                inner.set_cursor_position(position)?;
-

+
                disable_raw_mode()?;
                inner.clear()?;
            }
        }
+
        Ok(())
+
    }

+
    pub fn clear(&mut self) -> io::Result<()> {
+
        match self {
+
            Terminal::Fullscreen(inner) | Terminal::Inline(inner) => {
+
                inner.clear()?;
+
            }
+
        }
        Ok(())
    }

@@ -70,20 +70,21 @@ impl TryFrom<Viewport> for Terminal {
    fn try_from(viewport: Viewport) -> Result<Self, Self::Error> {
        match viewport {
            Viewport::Fullscreen => {
-
                let stdout = io::stdout().into_raw_mode()?.into_alternate_screen()?;
+
                enable_raw_mode()?;
+
                execute!(io::stdout(), EnterAlternateScreen)?;
                let options = TerminalOptions { viewport };
                let mut terminal =
-
                    ratatui::Terminal::with_options(TermionBackendExt::new(stdout), options)?;
+
                    ratatui::Terminal::with_options(CrosstermBackend::new(io::stdout()), options)?;

                terminal.clear()?;

                Ok(Terminal::Fullscreen(terminal))
            }
            _ => {
-
                let stdout = io::stdout().into_raw_mode()?;
+
                enable_raw_mode()?;
                let options = TerminalOptions { viewport };
                let terminal =
-
                    ratatui::Terminal::with_options(TermionBackendExt::new(stdout), options)?;
+
                    ratatui::Terminal::with_options(CrosstermBackend::new(io::stdout()), options)?;

                Ok(Terminal::Inline(terminal))
            }
@@ -91,85 +92,6 @@ impl TryFrom<Viewport> for Terminal {
    }
}

-
/// FIXME Remove workaround after a new `ratatui` version with
-
/// <https://github.com/ratatui-org/ratatui/pull/981/> included was released.
-
pub struct TermionBackendExt<W>
-
where
-
    W: Write,
-
{
-
    cursor: Option<Position>,
-
    inner: TermionBackend<W>,
-
}
-

-
impl<W> TermionBackendExt<W>
-
where
-
    W: Write,
-
{
-
    pub fn new(writer: W) -> Self {
-
        Self {
-
            cursor: None,
-
            inner: TermionBackend::new(writer),
-
        }
-
    }
-
}
-

-
impl<W: Write> ratatui::backend::Backend for TermionBackendExt<W> {
-
    fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
-
    where
-
        I: Iterator<Item = (u16, u16, &'a buffer::Cell)>,
-
    {
-
        self.inner.draw(content)
-
    }
-

-
    fn append_lines(&mut self, n: u16) -> io::Result<()> {
-
        self.inner.append_lines(n)
-
    }
-

-
    fn hide_cursor(&mut self) -> io::Result<()> {
-
        self.inner.hide_cursor()
-
    }
-

-
    fn show_cursor(&mut self) -> io::Result<()> {
-
        self.inner.show_cursor()
-
    }
-

-
    fn get_cursor_position(&mut self) -> io::Result<Position> {
-
        match self.inner.get_cursor_position() {
-
            Ok(position) => {
-
                self.cursor = Some(position);
-
                Ok(position)
-
            }
-
            Err(_) => Ok(self.cursor.unwrap_or_default()),
-
        }
-
    }
-

-
    fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
-
        self.cursor = Some(position.into());
-
        self.inner
-
            .set_cursor_position(self.cursor.unwrap_or_default())
-
    }
-

-
    fn clear(&mut self) -> io::Result<()> {
-
        self.inner.clear()
-
    }
-

-
    fn clear_region(&mut self, clear_type: backend::ClearType) -> io::Result<()> {
-
        self.inner.clear_region(clear_type)
-
    }
-

-
    fn size(&self) -> io::Result<Size> {
-
        self.inner.size()
-
    }
-

-
    fn window_size(&mut self) -> io::Result<backend::WindowSize> {
-
        self.inner.window_size()
-
    }
-

-
    fn flush(&mut self) -> io::Result<()> {
-
        ratatui::backend::Backend::flush(&mut self.inner)
-
    }
-
}
-

#[derive(Default)]
pub struct StdinReader {}

@@ -179,34 +101,30 @@ impl StdinReader {
        event_tx: UnboundedSender<Event>,
        mut interrupt_rx: broadcast::Receiver<Interrupted<P>>,
    ) -> anyhow::Result<Interrupted<P>> {
+
        use ratatui::crossterm::event;
+

        let token = CancellationToken::new();
        let token_clone = token.clone();

        let key_event_tx = event_tx.clone();
        let key_listener = tokio::spawn(async move {
-
            let mut stdin = async_stdin().keys();
            loop {
-
                if let Some(Ok(key)) = stdin.next() {
-
                    if key_event_tx.send(Event::Key(key)).is_err() {
-
                        return;
-
                    }
-
                }
-
                tokio::select! {
-
                    _ = token_clone.cancelled() => {
-
                        break;
+
                match event::poll(STDIN_TICK_RATE) {
+
                    Ok(true) => match event::read() {
+
                        Ok(event) => {
+
                            if key_event_tx.send(event.into()).is_err() {
+
                                return;
+
                            }
+
                        }
+
                        Err(err) => {
+
                            log::error!(target: "terminal", "Could not read from stdin: {err}");
+
                        }
+
                    },
+
                    _ => {
+
                        if token_clone.is_cancelled() {
+
                            break;
+
                        }
                    }
-
                    _ = tokio::time::sleep(STDIN_TICK_RATE) => {}
-
                }
-
            }
-
        });
-

-
        let mut signals = Signals::new([libc::SIGWINCH])?;
-
        let signal_handle = signals.handle();
-
        let signal_event_tx = event_tx.clone();
-
        thread::spawn(move || {
-
            for _ in signals.forever() {
-
                if signal_event_tx.send(Event::Resize).is_err() {
-
                    return;
                }
            }
        });
@@ -217,9 +135,7 @@ impl StdinReader {
                Ok(interrupted)
            }
        };
-

        key_listener.await?;
-
        signal_handle.close();

        result
    }
modified src/ui/im.rs
@@ -12,12 +12,10 @@ use ratatui::text::{Span, Text};
use tokio::sync::broadcast;
use tokio::sync::mpsc::UnboundedReceiver;

-
use termion::event::Key;
-

use ratatui::layout::{Constraint, Flex, Position, Rect};
use ratatui::{Frame, Viewport};

-
use crate::event::Event;
+
use crate::event::{Event, Key};
use crate::store::Update;
use crate::terminal::Terminal;
use crate::ui::theme::Theme;
@@ -67,8 +65,17 @@ impl Frontend {
                // Handle input events
                Some(event) = event_rx.recv() => {
                    match event {
-
                        Event::Key(key) => ctx.store_input(key),
-
                        Event::Resize => (),
+
                        Event::Key(key) => {
+
                            log::debug!(target: "frontend", "Received key event: {key:?}");
+
                            ctx.store_input(event)
+
                        }
+
                        Event::Resize(x, y) => {
+
                            log::debug!(target: "frontend", "Received resize event: {x},{y}");
+
                            terminal.clear()?;
+
                        },
+
                        Event::Unknown => {
+
                            log::debug!(target: "frontend", "Received unknown event")
+
                        }
                    }
                },
                // Handle state updates
@@ -77,9 +84,6 @@ impl Frontend {
                },
                // Catch and handle interrupt signal to gracefully shutdown
                Ok(interrupted) = interrupt_rx.recv() => {
-
                    log::info!("Received interrupt: {interrupted:?}");
-
                    terminal.restore()?;
-

                    break Ok(interrupted);
                }
            }
@@ -124,7 +128,7 @@ impl<R> InnerResponse<R> {
pub struct Context<M> {
    /// Currently captured user inputs. Inputs that where stored via `store_input`
    /// need to be cleared manually via `clear_inputs` (usually for each frame drawn).
-
    inputs: VecDeque<Key>,
+
    inputs: VecDeque<Event>,
    /// Current frame of the application.
    pub(crate) frame_size: Rect,
    /// The message sender used by the `Ui` to send application messages.
@@ -149,7 +153,7 @@ impl<M> Context<M> {
        }
    }

-
    pub fn with_inputs(mut self, inputs: VecDeque<Key>) -> Self {
+
    pub fn with_inputs(mut self, inputs: VecDeque<Event>) -> Self {
        self.inputs = inputs;
        self
    }
@@ -168,8 +172,8 @@ impl<M> Context<M> {
        self.frame_size
    }

-
    pub fn store_input(&mut self, key: Key) {
-
        self.inputs.push_back(key);
+
    pub fn store_input(&mut self, event: Event) {
+
        self.inputs.push_back(event);
    }

    pub fn clear_inputs(&mut self) {
@@ -308,12 +312,31 @@ pub struct Ui<M> {

impl<M> Ui<M> {
    pub fn has_input(&mut self, f: impl Fn(Key) -> bool) -> bool {
-
        self.has_focus && self.is_area_focused() && self.ctx.inputs.iter().any(|key| f(*key))
+
        self.has_focus
+
            && self.is_area_focused()
+
            && self.ctx.inputs.iter().any(|event| {
+
                if let Event::Key(key) = event {
+
                    return f(*key);
+
                }
+
                false
+
            })
    }

    pub fn get_input(&mut self, f: impl Fn(Key) -> bool) -> Option<Key> {
        if self.has_focus && self.is_area_focused() {
-
            self.ctx.inputs.iter().find(|key| f(**key)).copied()
+
            let matches = |&event| {
+
                if let Event::Key(key) = event {
+
                    return f(key);
+
                }
+
                false
+
            };
+

+
            if let Some(Event::Key(key)) =
+
                self.ctx.inputs.iter().find(|event| matches(event)).copied()
+
            {
+
                return Some(key);
+
            }
+
            None
        } else {
            None
        }
modified src/ui/im/widget.rs
@@ -1,14 +1,15 @@
use std::cmp;

+
use serde::{Deserialize, Serialize};
+

use ratatui::layout::{Direction, Layout, Position, Rect};
use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{Block, BorderType, Row, Scrollbar, ScrollbarState};
use ratatui::Frame;
use ratatui::{layout::Constraint, widgets::Paragraph};
-
use serde::{Deserialize, Serialize};
-
use termion::event::Key;

+
use crate::event::Key;
use crate::ui::ext::{FooterBlock, FooterBlockType, HeaderBlock};
use crate::ui::theme::style;
use crate::ui::{layout, span, Spacing};
@@ -131,7 +132,7 @@ impl<'a> Container<'a> {
            len: self.len,
        };

-
        if ui.has_input(|key| key == Key::Char('\t')) {
+
        if ui.has_input(|key| key == Key::Tab) {
            state.focus_next();
            response.changed = true;
        }
modified src/ui/rm.rs
@@ -30,11 +30,6 @@ impl Frontend {
    /// on either the terminal event, the state or the interrupt message channel.
    /// After all, it will draw the (potentially) updated root widget.
    ///
-
    /// Terminal event messages are being sent by a thread polling `stdin` for new user input
-
    /// and another thread polling UNIX signals, e.g. `SIGWINCH` when the terminal
-
    /// window size is being changed. Terminal events are then passed to the root widget
-
    /// of the application.
-
    ///
    /// State messages are being sent by the applications' `Store`. Received state updates
    /// will be passed to the root widget as well.
    ///
@@ -69,10 +64,17 @@ impl Frontend {
                _ = ticker.tick() => (),
                // Handle input events
                Some(event) = events_rx.recv() => match event {
-
                    Event::Key(key) => root.handle_event(key),
-
                    Event::Resize => {
-
                        log::info!("Resizing frontend...");
+
                    Event::Key(key) => {
+
                        log::debug!(target: "frontend", "Received key event: {key:?}");
+
                        root.handle_event(event);
+
                    }
+
                    Event::Resize(x, y) => {
+
                        log::debug!(target: "frontend", "Received resize event: {x},{y}");
+
                        terminal.clear()?;
                    },
+
                    Event::Unknown => {
+
                        log::debug!(target: "frontend", "Received unknown event")
+
                    }
                },
                // Handle state updates
                Some(state) = state_rx.recv() => {
@@ -80,9 +82,7 @@ impl Frontend {
                },
                // Catch and handle interrupt signal to gracefully shutdown
                Ok(interrupted) = interrupt_rx.recv() => {
-
                    terminal.restore()?;
-

-
                    break Ok(interrupted);
+
                   break Ok(interrupted);
                }
            }
            terminal.draw(|frame| root.render(RenderProps::from(frame.area()), frame))?;
modified src/ui/rm/widget.rs
@@ -9,18 +9,17 @@ use std::rc::Rc;

use tokio::sync::broadcast;

-
use termion::event::Key;
-

use ratatui::prelude::*;

use self::{
    container::SectionGroupState,
    text::{TextAreaState, TextViewState},
};
+
use crate::event::Event;

pub type BoxedView<S, M> = Box<dyn View<State = S, Message = M>>;
pub type UpdateCallback<S> = fn(&S) -> ViewProps;
-
pub type EventCallback<M> = fn(Key, Option<&ViewState>, Option<&ViewProps>) -> Option<M>;
+
pub type EventCallback<M> = fn(Event, Option<&ViewState>, Option<&ViewProps>) -> Option<M>;
pub type RenderCallback<M> = fn(Option<&ViewProps>, &RenderProps) -> Option<M>;
pub type InitCallback<M> = fn() -> Option<M>;

@@ -225,7 +224,7 @@ pub trait View {
    fn reset(&mut self) {}

    /// Should handle key events and call `handle_event` on all children.
-
    fn handle_event(&mut self, _props: Option<&ViewProps>, _key: Key) -> Option<Self::Message> {
+
    fn handle_event(&mut self, _props: Option<&ViewProps>, _event: Event) -> Option<Self::Message> {
        None
    }

@@ -288,14 +287,14 @@ impl<S: 'static, M: 'static> Widget<S, M> {

    /// Calls `handle_event` on the wrapped view as well as the `on_event` callback.
    /// Sends any message returned by either the view or the callback.
-
    pub fn handle_event(&mut self, key: Key) {
-
        if let Some(message) = self.view.handle_event(self.props.as_ref(), key) {
+
    pub fn handle_event(&mut self, event: Event) {
+
        if let Some(message) = self.view.handle_event(self.props.as_ref(), event) {
            let _ = self.sender.send(message);
        }

        if let Some(on_event) = self.on_event {
            if let Some(message) =
-
                (on_event)(key, self.view.view_state().as_ref(), self.props.as_ref())
+
                (on_event)(event, self.view.view_state().as_ref(), self.props.as_ref())
            {
                let _ = self.sender.send(message);
            }
modified src/ui/rm/widget/container.rs
@@ -1,11 +1,10 @@
use std::fmt::Debug;
use std::marker::PhantomData;

-
use termion::event::Key;
-

use ratatui::prelude::*;
use ratatui::widgets::{Block, BorderType, Borders, Row};

+
use crate::event::{Event, Key};
use crate::ui::ext::{FooterBlock, FooterBlockType, HeaderBlock};
use crate::ui::theme::{style, Theme};
use crate::ui::Column;
@@ -354,9 +353,9 @@ where
    type Message = M;
    type State = S;

-
    fn handle_event(&mut self, _props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
+
    fn handle_event(&mut self, _props: Option<&ViewProps>, event: Event) -> Option<Self::Message> {
        if let Some(content) = &mut self.content {
-
            content.handle_event(key);
+
            content.handle_event(event);
        }

        None
@@ -521,7 +520,7 @@ where
    type Message = M;
    type State = S;

-
    fn handle_event(&mut self, props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
+
    fn handle_event(&mut self, props: Option<&ViewProps>, event: Event) -> Option<Self::Message> {
        let default = SplitContainerProps::default();
        let props = props
            .and_then(|props| props.inner_ref::<SplitContainerProps>())
@@ -530,12 +529,12 @@ where
        match props.split_focus {
            SplitContainerFocus::Top => {
                if let Some(top) = self.top.as_mut() {
-
                    top.handle_event(key);
+
                    top.handle_event(event);
                }
            }
            SplitContainerFocus::Bottom => {
                if let Some(bottom) = self.bottom.as_mut() {
-
                    bottom.handle_event(key);
+
                    bottom.handle_event(event);
                }
            }
        }
@@ -697,7 +696,7 @@ where
    type State = S;
    type Message = M;

-
    fn handle_event(&mut self, props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
+
    fn handle_event(&mut self, props: Option<&ViewProps>, event: Event) -> Option<Self::Message> {
        let default = SectionGroupProps::default();
        let props = props
            .and_then(|props| props.inner_ref::<SectionGroupProps>())
@@ -708,18 +707,20 @@ where
            .focus
            .and_then(|focus| self.sections.get_mut(focus))
        {
-
            section.handle_event(key);
+
            section.handle_event(event);
        }

        if props.handle_keys {
-
            match key {
-
                Key::BackTab => {
-
                    self.prev();
-
                }
-
                Key::Char('\t') => {
-
                    self.next(self.sections.len());
+
            if let Event::Key(key) = event {
+
                match key {
+
                    Key::BackTab => {
+
                        self.prev();
+
                    }
+
                    Key::Tab => {
+
                        self.next(self.sections.len());
+
                    }
+
                    _ => {}
                }
-
                _ => {}
            }
        }

modified src/ui/rm/widget/list.rs
@@ -3,8 +3,6 @@ use std::hash::Hash;
use std::marker::PhantomData;
use std::{cmp, vec};

-
use termion::event::Key;
-

use ratatui::layout::{Constraint, Layout};
use ratatui::style::{Style, Stylize};
use ratatui::symbols::border;
@@ -15,6 +13,7 @@ use ratatui::Frame;

use tui_tree_widget::TreeState;

+
use crate::event::{Event, Key};
use crate::ui::theme::style;
use crate::ui::{layout, span};
use crate::ui::{Column, ToRow, ToTree};
@@ -170,7 +169,7 @@ where
    type Message = M;
    type State = S;

-
    fn handle_event(&mut self, props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
+
    fn handle_event(&mut self, props: Option<&ViewProps>, event: Event) -> Option<Self::Message> {
        let default = TableProps::default();
        let props = props
            .and_then(|props| props.inner_ref::<TableProps<R, W>>())
@@ -178,26 +177,28 @@ where

        let page_size = self.height;

-
        match key {
-
            Key::Up | Key::Char('k') => {
-
                self.prev();
-
            }
-
            Key::Down | Key::Char('j') => {
-
                self.next(props.items.len());
-
            }
-
            Key::PageUp => {
-
                self.prev_page(page_size as usize);
-
            }
-
            Key::PageDown => {
-
                self.next_page(props.items.len(), page_size as usize);
-
            }
-
            Key::Home => {
-
                self.begin();
-
            }
-
            Key::End => {
-
                self.end(props.items.len());
+
        if let Event::Key(key) = event {
+
            match key {
+
                Key::Up | Key::Char('k') => {
+
                    self.prev();
+
                }
+
                Key::Down | Key::Char('j') => {
+
                    self.next(props.items.len());
+
                }
+
                Key::PageUp => {
+
                    self.prev_page(page_size as usize);
+
                }
+
                Key::PageDown => {
+
                    self.next_page(props.items.len(), page_size as usize);
+
                }
+
                Key::Home => {
+
                    self.begin();
+
                }
+
                Key::End => {
+
                    self.end(props.items.len());
+
                }
+
                _ => {}
            }
-
            _ => {}
        }

        None
@@ -452,23 +453,25 @@ where
        }
    }

-
    fn handle_event(&mut self, _props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
-
        match key {
-
            Key::Up | Key::Char('k') => {
-
                self.state.key_up();
-
            }
-
            Key::Down | Key::Char('j') => {
-
                self.state.key_down();
-
            }
-
            Key::Left | Key::Char('h')
-
                if !self.state.selected().is_empty() && !self.state.opened().is_empty() =>
-
            {
-
                self.state.key_left();
-
            }
-
            Key::Right | Key::Char('l') => {
-
                self.state.key_right();
+
    fn handle_event(&mut self, _props: Option<&ViewProps>, event: Event) -> Option<Self::Message> {
+
        if let Event::Key(key) = event {
+
            match key {
+
                Key::Up | Key::Char('k') => {
+
                    self.state.key_up();
+
                }
+
                Key::Down | Key::Char('j') => {
+
                    self.state.key_down();
+
                }
+
                Key::Left | Key::Char('h')
+
                    if !self.state.selected().is_empty() && !self.state.opened().is_empty() =>
+
                {
+
                    self.state.key_left();
+
                }
+
                Key::Right | Key::Char('l') => {
+
                    self.state.key_right();
+
                }
+
                _ => {}
            }
-
            _ => {}
        }

        None
modified src/ui/rm/widget/text.rs
@@ -1,13 +1,12 @@
use std::marker::PhantomData;

-
use termion::event::Key;
-

use ratatui::layout::{Alignment, Constraint, Layout, Position, Rect};
use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Span, Text};
use ratatui::widgets::Paragraph;
use ratatui::Frame;

+
use crate::event::{Event, Key};
use crate::ui::theme::{self, Theme};

use super::{utils, RenderProps, View, ViewProps, ViewState};
@@ -261,28 +260,30 @@ where
        };
    }

-
    fn handle_event(&mut self, _props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
-
        match key {
-
            Key::Char(to_insert)
-
                if (key != Key::Alt('\n'))
-
                    && (key != Key::Char('\n'))
-
                    && (key != Key::Ctrl('\n')) =>
-
            {
-
                self.enter_char(to_insert);
-
            }
-
            Key::Backspace => {
-
                self.delete_char_left();
-
            }
-
            Key::Delete => {
-
                self.delete_char_right();
-
            }
-
            Key::Left => {
-
                self.move_cursor_left();
-
            }
-
            Key::Right => {
-
                self.move_cursor_right();
+
    fn handle_event(&mut self, _props: Option<&ViewProps>, event: Event) -> Option<Self::Message> {
+
        if let Event::Key(key) = event {
+
            match key {
+
                Key::Char(to_insert)
+
                    if (key != Key::Alt('\n'))
+
                        && (key != Key::Char('\n'))
+
                        && (key != Key::Ctrl('\n')) =>
+
                {
+
                    self.enter_char(to_insert);
+
                }
+
                Key::Backspace => {
+
                    self.delete_char_left();
+
                }
+
                Key::Delete => {
+
                    self.delete_char_right();
+
                }
+
                Key::Left => {
+
                    self.move_cursor_left();
+
                }
+
                Key::Right => {
+
                    self.move_cursor_right();
+
                }
+
                _ => {}
            }
-
            _ => {}
        }

        None
@@ -463,7 +464,7 @@ impl<S, M> View for TextArea<'_, S, M> {
    type State = S;
    type Message = M;

-
    fn handle_event(&mut self, props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
+
    fn handle_event(&mut self, props: Option<&ViewProps>, event: Event) -> Option<Self::Message> {
        use tui_textarea::Input;

        let default = TextAreaProps::default();
@@ -473,32 +474,34 @@ impl<S, M> View for TextArea<'_, S, M> {

        if props.handle_keys {
            if !props.insert_mode {
-
                match key {
-
                    Key::Left | Key::Char('h') => {
-
                        self.textarea.input(Input {
-
                            key: tui_textarea::Key::Left,
-
                            ..Default::default()
-
                        });
+
                if let Event::Key(key) = event {
+
                    match key {
+
                        Key::Left | Key::Char('h') => {
+
                            self.textarea.input(Input {
+
                                key: tui_textarea::Key::Left,
+
                                ..Default::default()
+
                            });
+
                        }
+
                        Key::Right | Key::Char('l') => {
+
                            self.textarea.input(Input {
+
                                key: tui_textarea::Key::Right,
+
                                ..Default::default()
+
                            });
+
                        }
+
                        Key::Up | Key::Char('k') => {
+
                            self.textarea.input(Input {
+
                                key: tui_textarea::Key::Up,
+
                                ..Default::default()
+
                            });
+
                        }
+
                        Key::Down | Key::Char('j') => {
+
                            self.textarea.input(Input {
+
                                key: tui_textarea::Key::Down,
+
                                ..Default::default()
+
                            });
+
                        }
+
                        _ => {}
                    }
-
                    Key::Right | Key::Char('l') => {
-
                        self.textarea.input(Input {
-
                            key: tui_textarea::Key::Right,
-
                            ..Default::default()
-
                        });
-
                    }
-
                    Key::Up | Key::Char('k') => {
-
                        self.textarea.input(Input {
-
                            key: tui_textarea::Key::Up,
-
                            ..Default::default()
-
                        });
-
                    }
-
                    Key::Down | Key::Char('j') => {
-
                        self.textarea.input(Input {
-
                            key: tui_textarea::Key::Down,
-
                            ..Default::default()
-
                        });
-
                    }
-
                    _ => {}
                }
            } else {
                // TODO: Implement insert mode.
@@ -867,7 +870,7 @@ where
    type Message = M;
    type State = S;

-
    fn handle_event(&mut self, props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
+
    fn handle_event(&mut self, props: Option<&ViewProps>, event: Event) -> Option<Self::Message> {
        let default = TextViewProps::default();
        let props = props
            .and_then(|props| props.inner_ref::<TextViewProps>())
@@ -879,32 +882,34 @@ where
        let page_size = self.area.0 as usize;

        if props.handle_keys {
-
            match key {
-
                Key::Up | Key::Char('k') => {
-
                    self.scroll_up();
-
                }
-
                Key::Down | Key::Char('j') => {
-
                    self.scroll_down(len, page_size);
-
                }
-
                Key::Left | Key::Char('h') => {
-
                    self.scroll_left();
-
                }
-
                Key::Right | Key::Char('l') => {
-
                    self.scroll_right(max_line_len.saturating_sub(self.area.1.into()));
-
                }
-
                Key::PageUp => {
-
                    self.prev_page(page_size);
-
                }
-
                Key::PageDown => {
-
                    self.next_page(len, page_size);
-
                }
-
                Key::Home => {
-
                    self.begin();
-
                }
-
                Key::End => {
-
                    self.end(len, page_size);
+
            if let Event::Key(key) = event {
+
                match key {
+
                    Key::Up | Key::Char('k') => {
+
                        self.scroll_up();
+
                    }
+
                    Key::Down | Key::Char('j') => {
+
                        self.scroll_down(len, page_size);
+
                    }
+
                    Key::Left | Key::Char('h') => {
+
                        self.scroll_left();
+
                    }
+
                    Key::Right | Key::Char('l') => {
+
                        self.scroll_right(max_line_len.saturating_sub(self.area.1.into()));
+
                    }
+
                    Key::PageUp => {
+
                        self.prev_page(page_size);
+
                    }
+
                    Key::PageDown => {
+
                        self.next_page(len, page_size);
+
                    }
+
                    Key::Home => {
+
                        self.begin();
+
                    }
+
                    Key::End => {
+
                        self.end(len, page_size);
+
                    }
+
                    _ => {}
                }
-
                _ => {}
            }
        }

modified src/ui/rm/widget/window.rs
@@ -1,14 +1,13 @@
use std::hash::Hash;
use std::{collections::HashMap, marker::PhantomData};

-
use ratatui::Frame;
-
use termion::event::Key;
-

use ratatui::layout::{Constraint, Layout};
use ratatui::style::{Style, Stylize};
use ratatui::text::Text;
use ratatui::widgets::Row;
+
use ratatui::Frame;

+
use crate::event::Event;
use crate::ui::theme::{style, Theme};

use super::{RenderProps, View, ViewProps, Widget};
@@ -63,7 +62,7 @@ where
    type Message = M;
    type State = S;

-
    fn handle_event(&mut self, props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
+
    fn handle_event(&mut self, props: Option<&ViewProps>, event: Event) -> Option<Self::Message> {
        let default = WindowProps::default();
        let props = props
            .and_then(|props| props.inner_ref::<WindowProps<Id>>())
@@ -75,7 +74,7 @@ where
            .and_then(|id| self.pages.get_mut(id));

        if let Some(page) = page {
-
            page.handle_event(key);
+
            page.handle_event(event);
        }

        None
@@ -165,9 +164,9 @@ where
    type State = S;
    type Message = M;

-
    fn handle_event(&mut self, _props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
+
    fn handle_event(&mut self, _props: Option<&ViewProps>, event: Event) -> Option<Self::Message> {
        if let Some(content) = self.content.as_mut() {
-
            content.handle_event(key);
+
            content.handle_event(event);
        }

        None