From 892b4495d321db3c49fe414848a61893087a3806 Mon Sep 17 00:00:00 2001 From: rhysd Date: Sat, 21 Oct 2023 01:37:01 +0900 Subject: [PATCH 1/3] support `termwiz` crate for `ratatui` --- .github/workflows/ci.yml | 5 ++ Cargo.toml | 2 + src/highlight.rs | 2 + src/input.rs | 112 +++++++++++++++++++++++++++++++++++++-- src/lib.rs | 3 ++ src/textarea.rs | 2 + 6 files changed, 122 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10f5b96..470132a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,9 @@ jobs: - uses: Swatinem/rust-cache@v2 - run: cargo test --features=search - run: cargo test --no-default-features --features=ratatui-crossterm,search -- --skip .rs + - run: cargo test --no-default-features --features=ratatui-termion,search -- --skip .rs + if: ${{ matrix.os != 'windows-latest' }} + - run: cargo test --no-default-features --features=ratatui-termwiz,search -- --skip .rs - run: cargo test --no-default-features --features=your-backend,search -- --skip .rs - run: cargo test --no-default-features --features=ratatui-your-backend,search -- --skip .rs lint: @@ -33,6 +36,8 @@ jobs: - run: cargo clippy --examples --no-default-features --features ratatui-crossterm,search -- -D warnings - run: cargo clippy --examples --no-default-features --features ratatui-termion -- -D warnings - run: cargo clippy --examples --no-default-features --features ratatui-termion,search -- -D warnings + - run: cargo clippy --examples --no-default-features --features ratatui-termwiz -- -D warnings + - run: cargo clippy --examples --no-default-features --features ratatui-termwiz,search -- -D warnings - run: cargo clippy --examples --no-default-features --features termion -- -D warnings - run: cargo clippy --examples --no-default-features --features termion,search -- -D warnings - run: cargo clippy --examples --no-default-features --features your-backend -- -D warnings diff --git a/Cargo.toml b/Cargo.toml index b7607e3..aacb9af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ your-backend = ["tui"] # Features to use ratatui ratatui-crossterm = ["dep:crossterm-027", "ratatui/crossterm"] ratatui-termion = ["dep:termion", "ratatui/termion"] +ratatui-termwiz = ["dep:termwiz", "ratatui/termwiz"] ratatui-your-backend = ["ratatui"] # Other optional features search = ["dep:regex"] @@ -44,6 +45,7 @@ arbitrary = { version = "1", features = ["derive"], optional = true } crossterm-027 = { package = "crossterm", version = "0.27", optional = true } ratatui = { version = "0.23.0", default-features = false, optional = true } unicode-width = "0.1.11" +termwiz = { version = "0.20.0", optional = true } [[example]] name = "minimal" diff --git a/src/highlight.rs b/src/highlight.rs index 98ac75b..cec8f2d 100644 --- a/src/highlight.rs +++ b/src/highlight.rs @@ -2,6 +2,7 @@ use crate::tui::style::Style; #[cfg(any( feature = "ratatui-crossterm", feature = "ratatui-termion", + feature = "ratatui-termwiz", feature = "ratatui-your-backend", ))] use crate::tui::text::Line as Spans; @@ -9,6 +10,7 @@ use crate::tui::text::Span; #[cfg(not(any( feature = "ratatui-crossterm", feature = "ratatui-termion", + feature = "ratatui-termwiz", feature = "ratatui-your-backend", )))] use crate::tui::text::Spans; diff --git a/src/input.rs b/src/input.rs index 8d6a184..439e5a3 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,12 +1,17 @@ #[cfg(any(feature = "crossterm", feature = "ratatui-crossterm"))] use crate::crossterm::event::{ - Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, - MouseEvent as CrosstermMouseEvent, MouseEventKind as CrosstermMouseEventKind, + Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEvent, + MouseEventKind as CrosstermMouseEventKind, }; #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; #[cfg(any(feature = "termion", feature = "ratatui-termion"))] use termion::event::{Event as TermionEvent, Key as TermionKey, MouseEvent as TermionMouseEvent}; +#[cfg(feature = "ratatui-termwiz")] +use termwiz::input::{ + InputEvent as TermwizInputEvent, KeyEvent as TermwizKeyEvent, MouseEvent as TermwizMouseEvent, + PixelMouseEvent, +}; /// Backend-agnostic key input kind. /// @@ -136,9 +141,9 @@ impl From for Input { } #[cfg(any(feature = "crossterm", feature = "ratatui-crossterm"))] -impl From for Input { +impl From for Input { /// Convert [`crossterm::event::MouseEvent`] to [`Input`]. - fn from(mouse: CrosstermMouseEvent) -> Self { + fn from(mouse: MouseEvent) -> Self { let key = match mouse.kind { CrosstermMouseEventKind::ScrollDown => Key::MouseScrollDown, CrosstermMouseEventKind::ScrollUp => Key::MouseScrollUp, @@ -218,3 +223,102 @@ impl From for Input { } } } + +#[cfg(feature = "ratatui-termwiz")] +impl From for Input { + /// Convert [`termwiz::input::InputEvent`] to [`Input`]. + fn from(input: TermwizInputEvent) -> Self { + match input { + TermwizInputEvent::Key(key) => Self::from(key), + TermwizInputEvent::Mouse(mouse) => Self::from(mouse), + TermwizInputEvent::PixelMouse(mouse) => Self::from(mouse), + _ => Self::default(), + } + } +} + +#[cfg(feature = "ratatui-termwiz")] +impl From for Input { + /// Convert [`termwiz::input::KeyEvent`] to [`Input`]. + fn from(key: TermwizKeyEvent) -> Self { + use termwiz::input::{KeyCode, Modifiers}; + + let TermwizKeyEvent { key, modifiers } = key; + let key = match key { + KeyCode::Char(c) => Key::Char(c), + KeyCode::Backspace => Key::Backspace, + KeyCode::Tab => Key::Tab, + KeyCode::Enter => Key::Enter, + KeyCode::Escape => Key::Esc, + KeyCode::PageUp => Key::PageUp, + KeyCode::PageDown => Key::PageDown, + KeyCode::End => Key::End, + KeyCode::Home => Key::Home, + KeyCode::LeftArrow => Key::Left, + KeyCode::RightArrow => Key::Right, + KeyCode::UpArrow => Key::Up, + KeyCode::DownArrow => Key::Down, + KeyCode::Delete => Key::Delete, + KeyCode::Function(x) => Key::F(x), + _ => Key::Null, + }; + let ctrl = modifiers.contains(Modifiers::CTRL); + let alt = modifiers.contains(Modifiers::ALT); + + Self { key, ctrl, alt } + } +} + +#[cfg(feature = "ratatui-termwiz")] +impl From for Input { + /// Convert [`termwiz::input::MouseEvent`] to [`Input`]. + fn from(mouse: TermwizMouseEvent) -> Self { + use termwiz::input::{Modifiers, MouseButtons}; + + let TermwizMouseEvent { + mouse_buttons, + modifiers, + .. + } = mouse; + let key = if mouse_buttons.contains(MouseButtons::VERT_WHEEL) { + if mouse_buttons.contains(MouseButtons::WHEEL_POSITIVE) { + Key::MouseScrollDown + } else { + Key::MouseScrollUp + } + } else { + Key::Null + }; + let ctrl = modifiers.contains(Modifiers::CTRL); + let alt = modifiers.contains(Modifiers::ALT); + + Self { key, ctrl, alt } + } +} + +#[cfg(feature = "ratatui-termwiz")] +impl From for Input { + /// Convert [`termwiz::input::PixelMouseEvent`] to [`Input`]. + fn from(mouse: PixelMouseEvent) -> Self { + use termwiz::input::{Modifiers, MouseButtons}; + + let PixelMouseEvent { + mouse_buttons, + modifiers, + .. + } = mouse; + let key = if mouse_buttons.contains(MouseButtons::VERT_WHEEL) { + if mouse_buttons.contains(MouseButtons::WHEEL_POSITIVE) { + Key::MouseScrollDown + } else { + Key::MouseScrollUp + } + } else { + Key::Null + }; + let ctrl = modifiers.contains(Modifiers::CTRL); + let alt = modifiers.contains(Modifiers::ALT); + + Self { key, ctrl, alt } + } +} diff --git a/src/lib.rs b/src/lib.rs index f7e3197..ecbe412 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ any( feature = "ratatui-crossterm", feature = "ratatui-termion", + feature = "ratatui-termwiz", feature = "ratatui-your-backend" ), ))] @@ -29,12 +30,14 @@ mod word; #[cfg(any( feature = "ratatui-crossterm", feature = "ratatui-termion", + feature = "ratatui-termwiz", feature = "ratatui-your-backend", ))] use ratatui as tui; #[cfg(not(any( feature = "ratatui-crossterm", feature = "ratatui-termion", + feature = "ratatui-termwiz", feature = "ratatui-your-backend", )))] #[allow(clippy::single_component_path_imports)] diff --git a/src/textarea.rs b/src/textarea.rs index f689fc1..db40a9c 100644 --- a/src/textarea.rs +++ b/src/textarea.rs @@ -10,12 +10,14 @@ use crate::tui::style::{Color, Modifier, Style}; #[cfg(any( feature = "ratatui-crossterm", feature = "ratatui-termion", + feature = "ratatui-termwiz", feature = "ratatui-your-backend", ))] use crate::tui::text::Line as Spans; #[cfg(not(any( feature = "ratatui-crossterm", feature = "ratatui-termion", + feature = "ratatui-termwiz", feature = "ratatui-your-backend", )))] use crate::tui::text::Spans; From 257c8bcbf02d2a16f85d05badc7b38011511696f Mon Sep 17 00:00:00 2001 From: rhysd Date: Sat, 21 Oct 2023 02:22:15 +0900 Subject: [PATCH 2/3] add example for usage with `termwiz` crate --- Cargo.toml | 4 +++ examples/termwiz.rs | 67 +++++++++++++++++++++++++++++++++++++++++++++ src/input.rs | 44 ++++++++++++++--------------- 3 files changed, 93 insertions(+), 22 deletions(-) create mode 100644 examples/termwiz.rs diff --git a/Cargo.toml b/Cargo.toml index aacb9af..42b62e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,6 +99,10 @@ required-features = ["ratatui-crossterm", "search"] name = "ratatui_termion" required-features = ["ratatui-termion"] +[[example]] +name = "termwiz" +required-features = ["ratatui-termwiz"] + [workspace] members = [ "bench", diff --git a/examples/termwiz.rs b/examples/termwiz.rs new file mode 100644 index 0000000..eade9a3 --- /dev/null +++ b/examples/termwiz.rs @@ -0,0 +1,67 @@ +use ratatui::backend::TermwizBackend; +use ratatui::widgets::{Block, Borders}; +use ratatui::Terminal; +use std::error::Error; +use std::time::{Duration, Instant}; +use termwiz::input::InputEvent; +use termwiz::terminal::Terminal as TermwizTerminal; +use tui_textarea::{Input, Key, TextArea}; + +const TICK_RATE: Duration = Duration::from_millis(100); + +fn main() -> Result<(), Box> { + let backend = TermwizBackend::new()?; + let mut term = Terminal::new(backend)?; + term.hide_cursor()?; + + let mut textarea = TextArea::default(); + textarea.set_block( + Block::default() + .borders(Borders::ALL) + .title("Termwiz Minimal Example"), + ); + let mut last_tick = Instant::now(); + + // The event loop + loop { + term.draw(|f| { + let widget = textarea.widget(); + f.render_widget(widget, f.size()); + })?; + + let timeout = TICK_RATE + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + + if let Some(input) = term + .backend_mut() + .buffered_terminal_mut() + .terminal() + .poll_input(Some(timeout))? + { + if let InputEvent::Resized { cols, rows } = input { + term.backend_mut() + .buffered_terminal_mut() + .resize(cols, rows); + } else { + match input.into() { + Input { key: Key::Esc, .. } => break, + input => { + textarea.input(input); + } + } + } + } + + if last_tick.elapsed() >= TICK_RATE { + last_tick = Instant::now(); + } + } + + term.show_cursor()?; + term.flush()?; + drop(term); // Leave terminal raw mode to print the following line + + println!("Lines: {:?}", textarea.lines()); + Ok(()) +} diff --git a/src/input.rs b/src/input.rs index 439e5a3..b858fc5 100644 --- a/src/input.rs +++ b/src/input.rs @@ -9,8 +9,8 @@ use arbitrary::Arbitrary; use termion::event::{Event as TermionEvent, Key as TermionKey, MouseEvent as TermionMouseEvent}; #[cfg(feature = "ratatui-termwiz")] use termwiz::input::{ - InputEvent as TermwizInputEvent, KeyEvent as TermwizKeyEvent, MouseEvent as TermwizMouseEvent, - PixelMouseEvent, + InputEvent as TermwizInputEvent, KeyEvent as TermwizKeyEvent, + MouseButtons as TermwizMouseButtons, MouseEvent as TermwizMouseEvent, PixelMouseEvent, }; /// Backend-agnostic key input kind. @@ -269,26 +269,34 @@ impl From for Input { } } +#[cfg(feature = "ratatui-termwiz")] +impl From for Key { + /// Convert [`termwiz::input::MouseButtons`] to [`Key`]. + fn from(buttons: TermwizMouseButtons) -> Self { + if buttons.contains(TermwizMouseButtons::VERT_WHEEL) { + if buttons.contains(TermwizMouseButtons::WHEEL_POSITIVE) { + Key::MouseScrollUp + } else { + Key::MouseScrollDown + } + } else { + Key::Null + } + } +} + #[cfg(feature = "ratatui-termwiz")] impl From for Input { /// Convert [`termwiz::input::MouseEvent`] to [`Input`]. fn from(mouse: TermwizMouseEvent) -> Self { - use termwiz::input::{Modifiers, MouseButtons}; + use termwiz::input::Modifiers; let TermwizMouseEvent { mouse_buttons, modifiers, .. } = mouse; - let key = if mouse_buttons.contains(MouseButtons::VERT_WHEEL) { - if mouse_buttons.contains(MouseButtons::WHEEL_POSITIVE) { - Key::MouseScrollDown - } else { - Key::MouseScrollUp - } - } else { - Key::Null - }; + let key = Key::from(mouse_buttons); let ctrl = modifiers.contains(Modifiers::CTRL); let alt = modifiers.contains(Modifiers::ALT); @@ -300,22 +308,14 @@ impl From for Input { impl From for Input { /// Convert [`termwiz::input::PixelMouseEvent`] to [`Input`]. fn from(mouse: PixelMouseEvent) -> Self { - use termwiz::input::{Modifiers, MouseButtons}; + use termwiz::input::Modifiers; let PixelMouseEvent { mouse_buttons, modifiers, .. } = mouse; - let key = if mouse_buttons.contains(MouseButtons::VERT_WHEEL) { - if mouse_buttons.contains(MouseButtons::WHEEL_POSITIVE) { - Key::MouseScrollDown - } else { - Key::MouseScrollUp - } - } else { - Key::Null - }; + let key = Key::from(mouse_buttons); let ctrl = modifiers.contains(Modifiers::CTRL); let alt = modifiers.contains(Modifiers::ALT); From 1e781950d6dc884a591ddd2c25a0090c952c8a0a Mon Sep 17 00:00:00 2001 From: rhysd Date: Sat, 21 Oct 2023 02:38:18 +0900 Subject: [PATCH 3/3] describe `termwiz` support in documents --- README.md | 31 ++++++++++++++++++++----------- src/input.rs | 3 ++- src/textarea.rs | 11 +++++++++-- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 95adb0f..cd8b669 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Multi-line text editor can be easily put as part of your TUI application. - Search with regular expressions - Mouse scrolling - Yank support. Paste text deleted with `C-k`, `C-j`, ... -- Backend agnostic. [crossterm][], [termion][], and your own backend are all supported +- Backend agnostic. [crossterm][], [termion][], [termwiz][], and your own backend are all supported - Multiple textarea widgets in the same screen - Support both [tui-rs][] (the original) and [ratatui][] (the fork by community) @@ -130,6 +130,14 @@ cargo run --example ratatui_termion --no-default-features --features=ratatui-ter cargo run --example ratatui_popup_placeholder --no-default-features --features=ratatui-crossterm ``` +### [`termwiz`](./examples/termwiz.rs) + +```sh +cargo run --example termwiz --no-default-features --features=ratatui-termwiz +``` + +Minimal usage with [termwiz][] support. + ## Installation Add `tui-textarea` crate to dependencies in your `Cargo.toml`. This enables crossterm backend support by default. @@ -160,10 +168,10 @@ tui-textarea = { version = "*", default-features = false, features = ["termion"] If you're using [ratatui][] instead of [tui-rs][], you need to enable features for using ratatui crate. The following table shows feature names corresponding to the dependencies. -| | crossterm | termion | Your own backend | -|---------|----------------------------------|-------------------|------------------------| -| tui-rs | `crossterm` (enabled by default) | `termion` | `your-backend` | -| ratatui | `ratatui-crossterm` | `ratatui-termion` | `ratatui-your-backend` | +| | crossterm | termion | termwiz | Your own backend | +|---------|----------------------------------|-------------------|-------------------|------------------------| +| tui-rs | `crossterm` (enabled by default) | `termion` | N/A | `your-backend` | +| ratatui | `ratatui-crossterm` | `ratatui-termion` | `ratatui-termwiz` | `ratatui-your-backend` | For example, when you want to use the combination of [ratatui][] and [crossterm][], @@ -176,9 +184,9 @@ tui-textarea = { version = "*", features = ["ratatui-crossterm"], default-featur Note that [tui-rs][] support and [ratatui][] support are exclusive. When you use [ratatui][] support, you must disable [tui-rs][] support by `default-features = false`. -In addition to above dependencies, you also need to install [crossterm][] or [termion][] to initialize your application -and to receive key inputs. Note that version of [crossterm][] crate is different between [tui-rs][] and [ratatui][]. -Please select the correct version. +In addition to above dependencies, you also need to install [crossterm][] or [termion][] or [termwiz][] to initialize +your application and to receive key inputs. Note that version of [crossterm][] crate is different between [tui-rs][] +and [ratatui][]. Please select the correct version. ## Minimal Usage @@ -553,7 +561,7 @@ match read()?.into() { tui-rs allows to make your own backend by implementing [`tui::backend::Backend`][tui-backend] trait. tui-textarea also supports it. In this case, please use `your-backend` feature for [tui-rs][] or `ratatui-your-backend` feature for -[ratatui][]. They avoid adding backend crates (crossterm and termion) since you're using your own backend. +[ratatui][]. They avoid adding backend crates (crossterm, termion, and termwiz) since you're using your own backend. ```toml [dependencies] @@ -696,9 +704,10 @@ tui-textarea is distributed under [The MIT License](./LICENSE.txt). [ci-badge]: https://github.com/rhysd/tui-textarea/actions/workflows/ci.yml/badge.svg?event=push [ci]: https://github.com/rhysd/tui-textarea/actions/workflows/ci.yml [tui-rs]: https://github.com/fdehau/tui-rs -[ratatui]: https://github.com/tui-rs-revival/ratatui -[termion]: https://docs.rs/termion/latest/termion/ +[ratatui]: https://github.com/ratatui-org/ratatui [crossterm]: https://docs.rs/crossterm/latest/crossterm/ +[termion]: https://docs.rs/termion/latest/termion/ +[termwiz]: https://docs.rs/termwiz/latest/termwiz/ [tui-backend]: https://docs.rs/tui/latest/tui/backend/trait.Backend.html [repo]: https://github.com/rhysd/tui-textarea [new-issue]: https://github.com/rhysd/tui-textarea/issues/new diff --git a/src/input.rs b/src/input.rs index b858fc5..ed62996 100644 --- a/src/input.rs +++ b/src/input.rs @@ -47,7 +47,8 @@ pub enum Key { /// Backend-agnostic key input type. /// -/// When `crossterm` and/or `termion` features are enabled, converting their key input types into this `Input` type is defined. +/// When `crossterm`, `termion`, `ratatui-termwiz` features are enabled, converting respective key input types into this +/// `Input` type is defined. /// ```no_run /// use tui_textarea::{TextArea, Input, Key}; /// use crossterm::event::{Event, read}; diff --git a/src/textarea.rs b/src/textarea.rs index db40a9c..213ced1 100644 --- a/src/textarea.rs +++ b/src/textarea.rs @@ -172,8 +172,8 @@ impl<'a> TextArea<'a> { /// Handle a key input with default key mappings. For default key mappings, see the table in /// [the module document](./index.html). - /// `crossterm` and `termion` features enable conversion from their own key event types into [`Input`] so this - /// method can take the event values directly. + /// `crossterm`, `termion`, and `ratatui-termwiz` features enable conversion from their own key event types into + /// [`Input`] so this method can take the event values directly. /// This method returns if the input modified text contents or not in the textarea. /// ```ignore /// use tui_textarea::{TextArea, Key, Input}; @@ -194,6 +194,13 @@ impl<'a> TextArea<'a> { /// textarea.input(key); /// } /// + /// // Handle termwiz key events + /// let event: termwiz::input::InputEvent = ...; + /// textarea.input(event); + /// if let termwiz::input::InputEvent::Key(key) = event { + /// textarea.input(key); + /// } + /// /// // Handle backend-agnostic key input /// let input = Input { key: Key::Char('a'), ctrl: false, alt: false }; /// let modified = textarea.input(input);