diff --git a/Cargo.lock b/Cargo.lock index ca4886b5..ba2b1c3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -593,6 +593,22 @@ dependencies = [ "winapi", ] +[[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" @@ -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", ] @@ -1563,6 +1579,12 @@ dependencies = [ "vcpkg", ] +[[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" @@ -1797,12 +1819,6 @@ version = "0.1.0" 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" @@ -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", @@ -2564,6 +2579,19 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" 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" @@ -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]] diff --git a/Cargo.toml b/Cargo.toml index e3dc326b..3cb58c9e 100644 --- a/Cargo.toml +++ b/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" } diff --git a/bin/commands/inbox/list.rs b/bin/commands/inbox/list.rs index bfa82eb3..b0a73197 100644 --- a/bin/commands/inbox/list.rs +++ b/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; const HELP: &str = r#"# Generic keybindings @@ -301,22 +300,6 @@ impl Show 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) { @@ -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 }); } } diff --git a/bin/commands/issue/list.rs b/bin/commands/issue/list.rs index e0fd63bc..31277189 100644 --- a/bin/commands/issue/list.rs +++ b/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) -> Widget { ) .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::()) .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) -> Widget { .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()) diff --git a/bin/commands/issue/list/ui.rs b/bin/commands/issue/list/ui.rs index f79e4aeb..79561877 100644 --- a/bin/commands/issue/list/ui.rs +++ b/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 { + fn handle_event(&mut self, props: Option<&ViewProps>, event: Event) -> Option { let default = BrowserProps::default(); let props = props .and_then(|props| props.inner_ref::()) .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 } } diff --git a/bin/commands/patch/list.rs b/bin/commands/patch/list.rs index 3463a684..6e9b6792 100644 --- a/bin/commands/patch/list.rs +++ b/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 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) { @@ -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 }); } } diff --git a/bin/commands/patch/review.rs b/bin/commands/patch/review.rs index 39850d4b..48ca49c8 100644 --- a/bin/commands/patch/review.rs +++ b/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}; diff --git a/bin/ui/im.rs b/bin/ui/im.rs index 67d42d63..1dbbd635 100644 --- a/bin/ui/im.rs +++ b/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(); } diff --git a/examples/basic_rmui.rs b/examples/basic_rmui.rs index afcb1aac..e4d2e3b3 100644 --- a/examples/basic_rmui.rs +++ b/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()); diff --git a/examples/hello.rs b/examples/hello.rs index 79bcd310..0c2a2082 100644 --- a/examples/hello.rs +++ b/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; diff --git a/examples/hello_rrmui.rs b/examples/hello_rrmui.rs index 687c39c5..bb2fc6e2 100644 --- a/examples/hello_rrmui.rs +++ b/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| { diff --git a/examples/selection.rs b/examples/selection.rs index 73cc9555..65dd6da5 100644 --- a/examples/selection.rs +++ b/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 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); } }, diff --git a/src/event.rs b/src/event.rs index 67c7ec54..99bff80c 100644 --- a/src/event.rs +++ b/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 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, + } + } } diff --git a/src/terminal.rs b/src/terminal.rs index c5d5ae34..845120e4 100644 --- a/src/terminal.rs +++ b/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 = TermionBackendExt; -pub type InlineTerminal = ratatui::Terminal>>; -pub type FullscreenTerminal = ratatui::Terminal>>>; +pub type Backend = CrosstermBackend; +pub type InlineTerminal = ratatui::Terminal>; +pub type FullscreenTerminal = ratatui::Terminal>; 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 for Terminal { fn try_from(viewport: Viewport) -> Result { 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 for Terminal { } } -/// FIXME Remove workaround after a new `ratatui` version with -/// included was released. -pub struct TermionBackendExt -where - W: Write, -{ - cursor: Option, - inner: TermionBackend, -} - -impl TermionBackendExt -where - W: Write, -{ - pub fn new(writer: W) -> Self { - Self { - cursor: None, - inner: TermionBackend::new(writer), - } - } -} - -impl ratatui::backend::Backend for TermionBackendExt { - fn draw<'a, I>(&mut self, content: I) -> io::Result<()> - where - I: Iterator, - { - 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 { - 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>(&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 { - self.inner.size() - } - - fn window_size(&mut self) -> io::Result { - self.inner.window_size() - } - - fn flush(&mut self) -> io::Result<()> { - ratatui::backend::Backend::flush(&mut self.inner) - } -} - #[derive(Default)] pub struct StdinReader {} @@ -179,35 +101,31 @@ impl StdinReader { event_tx: UnboundedSender, mut interrupt_rx: broadcast::Receiver>, ) -> anyhow::Result> { + 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; + 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::select! { - _ = token_clone.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 } diff --git a/src/ui/im.rs b/src/ui/im.rs index a1c2771a..2ff611dd 100644 --- a/src/ui/im.rs +++ b/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 InnerResponse { pub struct Context { /// 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, + inputs: VecDeque, /// 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 Context { } } - pub fn with_inputs(mut self, inputs: VecDeque) -> Self { + pub fn with_inputs(mut self, inputs: VecDeque) -> Self { self.inputs = inputs; self } @@ -168,8 +172,8 @@ impl Context { 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 { impl Ui { 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 { 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 } diff --git a/src/ui/im/widget.rs b/src/ui/im/widget.rs index a69163a4..219f39a7 100644 --- a/src/ui/im/widget.rs +++ b/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; } diff --git a/src/ui/rm.rs b/src/ui/rm.rs index 482d8d90..bda2a966 100644 --- a/src/ui/rm.rs +++ b/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))?; diff --git a/src/ui/rm/widget.rs b/src/ui/rm/widget.rs index b9251006..e10b6b32 100644 --- a/src/ui/rm/widget.rs +++ b/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 = Box>; pub type UpdateCallback = fn(&S) -> ViewProps; -pub type EventCallback = fn(Key, Option<&ViewState>, Option<&ViewProps>) -> Option; +pub type EventCallback = fn(Event, Option<&ViewState>, Option<&ViewProps>) -> Option; pub type RenderCallback = fn(Option<&ViewProps>, &RenderProps) -> Option; pub type InitCallback = fn() -> Option; @@ -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 { + fn handle_event(&mut self, _props: Option<&ViewProps>, _event: Event) -> Option { None } @@ -288,14 +287,14 @@ impl Widget { /// 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); } diff --git a/src/ui/rm/widget/container.rs b/src/ui/rm/widget/container.rs index 7c7d39d5..db90e4a5 100644 --- a/src/ui/rm/widget/container.rs +++ b/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 { + fn handle_event(&mut self, _props: Option<&ViewProps>, event: Event) -> Option { 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 { + fn handle_event(&mut self, props: Option<&ViewProps>, event: Event) -> Option { let default = SplitContainerProps::default(); let props = props .and_then(|props| props.inner_ref::()) @@ -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 { + fn handle_event(&mut self, props: Option<&ViewProps>, event: Event) -> Option { let default = SectionGroupProps::default(); let props = props .and_then(|props| props.inner_ref::()) @@ -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(); + if let Event::Key(key) = event { + match key { + Key::BackTab => { + self.prev(); + } + Key::Tab => { + self.next(self.sections.len()); + } + _ => {} } - Key::Char('\t') => { - self.next(self.sections.len()); - } - _ => {} } } diff --git a/src/ui/rm/widget/list.rs b/src/ui/rm/widget/list.rs index bf8667b4..9e97410b 100644 --- a/src/ui/rm/widget/list.rs +++ b/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 { + fn handle_event(&mut self, props: Option<&ViewProps>, event: Event) -> Option { let default = TableProps::default(); let props = props .and_then(|props| props.inner_ref::>()) @@ -178,26 +177,28 @@ where let page_size = self.height; - match key { - Key::Up | Key::Char('k') => { - self.prev(); + 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()); + } + _ => {} } - 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 { - match key { - Key::Up | Key::Char('k') => { - self.state.key_up(); + fn handle_event(&mut self, _props: Option<&ViewProps>, event: Event) -> Option { + 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(); + } + _ => {} } - 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 diff --git a/src/ui/rm/widget/text.rs b/src/ui/rm/widget/text.rs index d84042bc..bf476436 100644 --- a/src/ui/rm/widget/text.rs +++ b/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 { - match key { - Key::Char(to_insert) - if (key != Key::Alt('\n')) - && (key != Key::Char('\n')) - && (key != Key::Ctrl('\n')) => - { - self.enter_char(to_insert); + fn handle_event(&mut self, _props: Option<&ViewProps>, event: Event) -> Option { + 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(); + } + _ => {} } - 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 View for TextArea<'_, S, M> { type State = S; type Message = M; - fn handle_event(&mut self, props: Option<&ViewProps>, key: Key) -> Option { + fn handle_event(&mut self, props: Option<&ViewProps>, event: Event) -> Option { use tui_textarea::Input; let default = TextAreaProps::default(); @@ -473,32 +474,34 @@ impl 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 { + fn handle_event(&mut self, props: Option<&ViewProps>, event: Event) -> Option { let default = TextViewProps::default(); let props = props .and_then(|props| props.inner_ref::()) @@ -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(); + 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); + } + _ => {} } - 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); - } - _ => {} } } diff --git a/src/ui/rm/widget/window.rs b/src/ui/rm/widget/window.rs index 1f59e1e9..d6e1e2ac 100644 --- a/src/ui/rm/widget/window.rs +++ b/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 { + fn handle_event(&mut self, props: Option<&ViewProps>, event: Event) -> Option { let default = WindowProps::default(); let props = props .and_then(|props| props.inner_ref::>()) @@ -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 { + fn handle_event(&mut self, _props: Option<&ViewProps>, event: Event) -> Option { if let Some(content) = self.content.as_mut() { - content.handle_event(key); + content.handle_event(event); } None