From df3087f992b32bb74fcfdbb649267f2a49d731a7 Mon Sep 17 00:00:00 2001 From: veeso Date: Sat, 12 Oct 2024 19:24:34 +0200 Subject: [PATCH 01/18] feat: ratatui 0.28; dropped support for tui-rs --- .github/workflows/coverage.yml | 7 +--- .github/workflows/crossterm-windows.yml | 14 ++----- .github/workflows/ratatui_crossterm.yml | 38 +++++++++--------- .github/workflows/ratatui_termion.yml | 13 +++--- .github/workflows/tui_crossterm.yml | 28 ------------- .github/workflows/tui_termion.yml | 28 ------------- CHANGELOG.md | 15 +++++++ Cargo.toml | 26 +++++------- README.md | 36 ++++------------- examples/demo/app/model.rs | 4 +- examples/demo/components/clock.rs | 2 +- examples/demo/components/counter.rs | 4 +- examples/demo/components/label.rs | 4 +- examples/demo/components/mod.rs | 2 +- examples/user_events/components/label.rs | 4 +- examples/user_events/model.rs | 4 +- src/adapter/crossterm/mod.rs | 8 +--- src/adapter/crossterm/terminal.rs | 2 +- src/adapter/termion/mod.rs | 5 --- src/core/application.rs | 2 +- src/core/component.rs | 2 +- src/core/props/borders.rs | 2 +- src/core/props/dataset.rs | 6 +-- src/core/props/layout.rs | 29 ++++---------- src/core/props/mod.rs | 4 +- src/core/props/shape.rs | 2 +- src/core/props/texts.rs | 2 +- src/core/props/value.rs | 2 +- src/core/view.rs | 2 +- src/lib.rs | 2 +- src/mock/components.rs | 2 +- src/ratatui.rs | 5 +++ src/terminal.rs | 51 +++++++++++++++++++++++- src/tui.rs | 8 ---- src/utils/parser.rs | 2 +- 35 files changed, 154 insertions(+), 213 deletions(-) delete mode 100644 .github/workflows/tui_crossterm.yml delete mode 100644 .github/workflows/tui_termion.yml create mode 100644 src/ratatui.rs delete mode 100644 src/tui.rs diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index e4b3b9f..8f5b2a0 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -13,15 +13,12 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 - name: Setup rust toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@stable with: toolchain: nightly override: true - name: Run tests - uses: actions-rs/cargo@v1 - with: - command: test - args: --no-fail-fast --lib --no-default-features --features derive,serialize,with-crossterm + run: cargo test --no-fail-fast --lib --no-default-features --features derive,serialize,with-crossterm env: CARGO_INCREMENTAL: "0" RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests" diff --git a/.github/workflows/crossterm-windows.yml b/.github/workflows/crossterm-windows.yml index 188bd2c..455c52a 100644 --- a/.github/workflows/crossterm-windows.yml +++ b/.github/workflows/crossterm-windows.yml @@ -11,24 +11,16 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 + - uses: dtolnay/rust-toolchain@stable with: toolchain: stable override: true components: rustfmt, clippy - - uses: actions-rs/cargo@v1 - with: - command: test - args: --no-fail-fast --no-default-features --features derive,serialize,tui,crossterm - - uses: actions-rs/cargo@v1 - with: - command: test - args: --no-fail-fast --no-default-features --features derive,serialize,ratatui,crossterm + - name: Test + run: cargo test --no-fail-fast --no-default-features --features derive,serialize,crossterm - name: Examples run: cargo build --all-targets --examples - name: Format run: cargo fmt --all -- --check - - name: Clippy - run: cargo clippy --lib --no-default-features --features derive,serialize,tui,crossterm -- -Dwarnings - name: Clippy run: cargo clippy --lib --no-default-features --features derive,serialize,ratatui,crossterm -- -Dwarnings diff --git a/.github/workflows/ratatui_crossterm.yml b/.github/workflows/ratatui_crossterm.yml index beaccca..ace8b17 100644 --- a/.github/workflows/ratatui_crossterm.yml +++ b/.github/workflows/ratatui_crossterm.yml @@ -3,26 +3,24 @@ name: Ratatui-Crossterm on: [push, pull_request] env: - CARGO_TERM_COLOR: always + CARGO_TERM_COLOR: always jobs: - build: - runs-on: ubuntu-latest + build: + runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - components: rustfmt, clippy - - uses: actions-rs/cargo@v1 - with: - command: test - args: --no-fail-fast --features derive,serialize,ratatui,crossterm --no-default-features - - name: Examples - run: cargo build --all-targets --examples - - name: Format - run: cargo fmt --all -- --check - - name: Clippy - run: cargo clippy --lib --features derive,serialize,ratatui,crossterm --no-default-features -- -Dwarnings + steps: + - uses: actions/checkout@v2 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + override: true + components: rustfmt, clippy + - name: Test + run: cargo test --no-fail-fast --features derive,serialize,crossterm --no-default-features + - name: Examples + run: cargo build --all-targets --examples + - name: Format + run: cargo fmt --all -- --check + - name: Clippy + run: cargo clippy --lib --features derive,serialize,crossterm --no-default-features -- -Dwarnings diff --git a/.github/workflows/ratatui_termion.yml b/.github/workflows/ratatui_termion.yml index 429a2e4..038ea63 100644 --- a/.github/workflows/ratatui_termion.yml +++ b/.github/workflows/ratatui_termion.yml @@ -11,18 +11,15 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 + - uses: dtolnay/rust-toolchain@stable with: toolchain: stable - override: true components: rustfmt, clippy - - uses: actions-rs/cargo@v1 - with: - command: test - args: --no-fail-fast --no-default-features --features derive,serialize,ratatui,termion + - name: Test + run: cargo test --no-fail-fast --no-default-features --features derive,serialize,termion - name: Examples - run: cargo build --all-targets --no-default-features --features derive,serialize,ratatui,termion --examples + run: cargo build --all-targets --no-default-features --features derive,serialize,termion --examples - name: Format run: cargo fmt --all -- --check - name: Clippy - run: cargo clippy --lib --no-default-features --features derive,serialize,ratatui,termion -- -Dwarnings + run: cargo clippy --lib --no-default-features --features derive,serialize,termion -- -Dwarnings diff --git a/.github/workflows/tui_crossterm.yml b/.github/workflows/tui_crossterm.yml deleted file mode 100644 index e24c55d..0000000 --- a/.github/workflows/tui_crossterm.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Tui-Crossterm - -on: [push, pull_request] - -env: - CARGO_TERM_COLOR: always - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - components: rustfmt, clippy - - uses: actions-rs/cargo@v1 - with: - command: test - args: --no-fail-fast --features derive,serialize,tui,crossterm --no-default-features - - name: Examples - run: cargo build --all-targets --examples - - name: Format - run: cargo fmt --all -- --check - - name: Clippy - run: cargo clippy --lib --features derive,serialize,tui,crossterm --no-default-features -- -Dwarnings diff --git a/.github/workflows/tui_termion.yml b/.github/workflows/tui_termion.yml deleted file mode 100644 index 6a45f68..0000000 --- a/.github/workflows/tui_termion.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Tui-Termion - -on: [push, pull_request] - -env: - CARGO_TERM_COLOR: always - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - components: rustfmt, clippy - - uses: actions-rs/cargo@v1 - with: - command: test - args: --no-fail-fast --no-default-features --features derive,serialize,tui,termion - - name: Examples - run: cargo build --all-targets --no-default-features --features derive,serialize,tui,termion --examples - - name: Format - run: cargo fmt --all -- --check - - name: Clippy - run: cargo clippy --lib --no-default-features --features derive,serialize,tui,termion -- -Dwarnings diff --git a/CHANGELOG.md b/CHANGELOG.md index c21c61e..2f97cf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog - [Changelog](#changelog) + - [2.0.0](#200) - [1.9.2](#192) - [1.9.1](#191) - [1.9.0](#190) @@ -35,6 +36,20 @@ --- +## 2.0.0 + +Released on ??/10/2024 + +- Dropped support for `tui-rs`. Tui-rs was deprecated a long time ago, so it doesn't really makes sense to keep supporting it. +- Added new methods for `TerminalBridge` + - `init`: Initialize a terminal with reasonable defaults for most applications. + - Raw mode is enabled + - Alternate screen buffer enabled + - A panic hook is installed that restores the terminal before panicking. Ensure that this method is called after any other panic hooks that may be installed to ensure that the terminal is. + - `restore`: Restore the terminal to its original state + - `set_panic_hook`: Sets a panic hook that restores the terminal before panicking. +- Bump `ratatui` version to `0.28` + ## 1.9.2 Released on 04/03/2023 diff --git a/Cargo.toml b/Cargo.toml index 17cd154..a56f7d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tuirealm" -version = "1.9.2" +version = "2.0.0" authors = ["Christian Visintin"] edition = "2021" categories = ["command-line-utilities"] @@ -14,15 +14,14 @@ readme = "README.md" repository = "https://github.com/veeso/tui-realm" [dependencies] -bitflags = "2.4" -crossterm = { version = "0.27", optional = true } +bitflags = "2" +crossterm = { version = "0.28", optional = true } lazy-regex = "3" -ratatui = { version = "0.26", default-features = false, optional = true } +ratatui = { version = "0.28", default-features = false } serde = { version = "^1", features = ["derive"], optional = true } -termion = { version = "^2", optional = true } -thiserror = "^1.0.0" -tui = { version = "0.19", default-features = false, optional = true } -tuirealm_derive = { version = "^1.0.0", optional = true } +termion = { version = "^4", optional = true } +thiserror = "1" +tuirealm_derive = { version = "2", optional = true } [dev-dependencies] pretty_assertions = "^1" @@ -30,16 +29,11 @@ toml = "^0.8" tempfile = "^3" [features] -default = ["derive", "ratatui", "crossterm"] +default = ["derive", "crossterm"] derive = ["tuirealm_derive"] serialize = ["serde", "bitflags/serde"] -tui = ["dep:tui"] -crossterm = ["dep:crossterm", "tui?/crossterm", "ratatui?/crossterm"] -termion = ["dep:termion", "tui?/termion", "ratatui?/termion"] -ratatui = ["dep:ratatui"] -# deprecated aliases for broken out backend and UI library features -with-crossterm = ["tui", "crossterm"] -with-termion = ["tui", "termion"] +crossterm = ["dep:crossterm", "ratatui/crossterm"] +termion = ["dep:termion", "ratatui/termion"] [[example]] name = "demo" diff --git a/README.md b/README.md index a7d559a..f9e3f4a 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@

Developed by @veeso

-

Current version: 1.9.2 (04/03/2024)

+

Current version: 2.0.0 (12/10/2024)

- Tui-Crossterm CI - Termion CI ⚠️ You can enable only one backend at the time and at least one must be enabled in order to build. -> ❗ You don't need tui as a dependency, since you can access to tui types via `use tuirealm::tui::` - #### Enabling other backends ⚠️ This library supports two backends: `crossterm` and `termion`, and two high @@ -174,13 +154,13 @@ crate's default features. Example using the termion backend: ```toml -tuirealm = { version = "^1.9.0", default-features = false, features = [ "termion", "derive", "tui" ] } +tuirealm = { version = "^1.9.0", default-features = false, features = [ "termion", "derive" ] } ``` -Example using the ratatui UI library: +Example using crossterm: ```toml -tuirealm = { version = "^1.9.0", default-features = false, features = [ "ratatui", "derive", "crossterm" ]} +tuirealm = { version = "^1.9.0", default-features = false, features = [ "derive", "crossterm" ]} ``` ### Create a tui-realm application 🪂 diff --git a/examples/demo/app/model.rs b/examples/demo/app/model.rs index 26b7905..7511373 100644 --- a/examples/demo/app/model.rs +++ b/examples/demo/app/model.rs @@ -6,8 +6,8 @@ use std::time::{Duration, SystemTime}; use tuirealm::event::NoUserEvent; use tuirealm::props::{Alignment, Color, TextModifiers}; +use tuirealm::ratatui::layout::{Constraint, Direction, Layout}; use tuirealm::terminal::TerminalBridge; -use tuirealm::tui::layout::{Constraint, Direction, Layout}; use tuirealm::{ Application, AttrValue, Attribute, EventListenerCfg, Sub, SubClause, SubEventClause, Update, }; @@ -55,7 +55,7 @@ impl Model { ] .as_ref(), ) - .split(f.size()); + .split(f.area()); self.app.view(&Id::Clock, f, chunks[0]); self.app.view(&Id::LetterCounter, f, chunks[1]); self.app.view(&Id::DigitCounter, f, chunks[2]); diff --git a/examples/demo/components/clock.rs b/examples/demo/components/clock.rs index 6d14e3d..86d6a8f 100644 --- a/examples/demo/components/clock.rs +++ b/examples/demo/components/clock.rs @@ -7,7 +7,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tuirealm::command::{Cmd, CmdResult}; use tuirealm::props::{Alignment, Color, TextModifiers}; -use tuirealm::tui::layout::Rect; +use tuirealm::ratatui::layout::Rect; use tuirealm::{ AttrValue, Attribute, Component, Event, Frame, MockComponent, NoUserEvent, State, StateValue, }; diff --git a/examples/demo/components/counter.rs b/examples/demo/components/counter.rs index f5a5eda..e9a218d 100644 --- a/examples/demo/components/counter.rs +++ b/examples/demo/components/counter.rs @@ -5,8 +5,8 @@ use tuirealm::command::{Cmd, CmdResult}; use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::props::{Alignment, Borders, Color, Style, TextModifiers}; -use tuirealm::tui::layout::Rect; -use tuirealm::tui::widgets::{BorderType, Paragraph}; +use tuirealm::ratatui::layout::Rect; +use tuirealm::ratatui::widgets::{BorderType, Paragraph}; use tuirealm::{ AttrValue, Attribute, Component, Event, Frame, MockComponent, NoUserEvent, Props, State, StateValue, diff --git a/examples/demo/components/label.rs b/examples/demo/components/label.rs index c21ad7e..8bab2cd 100644 --- a/examples/demo/components/label.rs +++ b/examples/demo/components/label.rs @@ -4,8 +4,8 @@ use tuirealm::command::{Cmd, CmdResult}; use tuirealm::props::{Alignment, Color, Style, TextModifiers}; -use tuirealm::tui::layout::Rect; -use tuirealm::tui::widgets::Paragraph; +use tuirealm::ratatui::layout::Rect; +use tuirealm::ratatui::widgets::Paragraph; use tuirealm::{ AttrValue, Attribute, Component, Event, Frame, MockComponent, NoUserEvent, Props, State, }; diff --git a/examples/demo/components/mod.rs b/examples/demo/components/mod.rs index a45c807..794c432 100644 --- a/examples/demo/components/mod.rs +++ b/examples/demo/components/mod.rs @@ -3,7 +3,7 @@ //! demo example components use tuirealm::props::{Alignment, Borders, Color, Style}; -use tuirealm::tui::widgets::Block; +use tuirealm::ratatui::widgets::Block; use super::Msg; diff --git a/examples/user_events/components/label.rs b/examples/user_events/components/label.rs index b0a7d07..25fb8f4 100644 --- a/examples/user_events/components/label.rs +++ b/examples/user_events/components/label.rs @@ -7,8 +7,8 @@ use std::time::UNIX_EPOCH; use tuirealm::command::{Cmd, CmdResult}; use tuirealm::event::{Key, KeyEvent}; use tuirealm::props::{Alignment, Color, Style, TextModifiers}; -use tuirealm::tui::layout::Rect; -use tuirealm::tui::widgets::Paragraph; +use tuirealm::ratatui::layout::Rect; +use tuirealm::ratatui::widgets::Paragraph; use tuirealm::{AttrValue, Attribute, Component, Event, Frame, MockComponent, Props, State}; use super::{Msg, UserEvent}; diff --git a/examples/user_events/model.rs b/examples/user_events/model.rs index afcfba6..41a6722 100644 --- a/examples/user_events/model.rs +++ b/examples/user_events/model.rs @@ -2,8 +2,8 @@ //! //! app model +use tuirealm::ratatui::layout::{Constraint, Direction, Layout}; use tuirealm::terminal::TerminalBridge; -use tuirealm::tui::layout::{Constraint, Direction, Layout}; use tuirealm::{Application, Update}; use super::{Id, Msg, UserEvent}; @@ -44,7 +44,7 @@ impl Model { ] .as_ref(), ) - .split(f.size()); + .split(f.area()); self.app.view(&Id::Label, f, chunks[0]); self.app.view(&Id::Other, f, chunks[1]); }) diff --git a/src/adapter/crossterm/mod.rs b/src/adapter/crossterm/mod.rs index 06c8ac9..5e74bf1 100644 --- a/src/adapter/crossterm/mod.rs +++ b/src/adapter/crossterm/mod.rs @@ -14,17 +14,13 @@ use std::io::Stdout; pub use listener::CrosstermInputListener; use super::{Event, Key, KeyEvent, KeyModifiers, MediaKeyCode}; -use crate::tui::backend::CrosstermBackend; -use crate::tui::{Frame as TuiFrame, Terminal as TuiTerminal}; +use crate::ratatui::backend::CrosstermBackend; +use crate::ratatui::{Frame as TuiFrame, Terminal as TuiTerminal}; // -- Frame /// Frame represents the Frame where the view will be displayed in -#[cfg(feature = "ratatui")] pub type Frame<'a> = TuiFrame<'a>; -#[cfg(feature = "tui")] -pub type Frame<'a> = TuiFrame<'a, CrosstermBackend>; - /// Terminal must be used to interact with the terminal in tui applications pub type Terminal = TuiTerminal>; diff --git a/src/adapter/crossterm/terminal.rs b/src/adapter/crossterm/terminal.rs index 20c6d7d..4669598 100644 --- a/src/adapter/crossterm/terminal.rs +++ b/src/adapter/crossterm/terminal.rs @@ -10,8 +10,8 @@ use crossterm::terminal::{ disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, }; +use crate::ratatui::backend::CrosstermBackend; use crate::terminal::{TerminalBridge, TerminalError, TerminalResult}; -use crate::tui::backend::CrosstermBackend; use crate::Terminal; impl TerminalBridge { diff --git a/src/adapter/termion/mod.rs b/src/adapter/termion/mod.rs index 1d71968..39f5eb4 100644 --- a/src/adapter/termion/mod.rs +++ b/src/adapter/termion/mod.rs @@ -23,13 +23,8 @@ use crate::tui::{Frame as TuiFrame, Terminal as TuiTerminal}; // -- Frame /// Frame represents the Frame where the view will be displayed in -#[cfg(feature = "ratatui")] pub type Frame<'a> = TuiFrame<'a>; -#[cfg(feature = "tui")] -pub type Frame<'a> = - TuiFrame<'a, TermionBackend>>>>; - /// Terminal must be used to interact with the terminal in tui applications pub type Terminal = TuiTerminal>>>>; diff --git a/src/core/application.rs b/src/core/application.rs index c37fad8..a60d29e 100644 --- a/src/core/application.rs +++ b/src/core/application.rs @@ -9,7 +9,7 @@ use thiserror::Error; use super::{Subscription, View, WrappedComponent}; use crate::listener::{EventListener, EventListenerCfg, ListenerError}; -use crate::tui::layout::Rect; +use crate::ratatui::layout::Rect; use crate::{AttrValue, Attribute, Event, Frame, Injector, State, Sub, SubEventClause, ViewError}; /// Result retuned by `Application`. diff --git a/src/core/component.rs b/src/core/component.rs index bb1f7bf..39cfc0b 100644 --- a/src/core/component.rs +++ b/src/core/component.rs @@ -3,7 +3,7 @@ //! This module exposes the component traits use crate::command::{Cmd, CmdResult}; -use crate::tui::layout::Rect; +use crate::ratatui::layout::Rect; use crate::{AttrValue, Attribute, Event, Frame, State}; /// A Mock Component represents a component which defines all the properties and states it can handle and represent diff --git a/src/core/props/borders.rs b/src/core/props/borders.rs index a97caf9..eb05791 100644 --- a/src/core/props/borders.rs +++ b/src/core/props/borders.rs @@ -4,7 +4,7 @@ use super::{Color, Style}; // Exports -pub use crate::tui::widgets::{BorderType, Borders as BorderSides}; +pub use crate::ratatui::widgets::{BorderType, Borders as BorderSides}; // -- Border diff --git a/src/core/props/dataset.rs b/src/core/props/dataset.rs index a742f72..b71d1b0 100644 --- a/src/core/props/dataset.rs +++ b/src/core/props/dataset.rs @@ -3,8 +3,8 @@ //! `Dataset` is a wrapper for tui dataset use super::Style; -use crate::tui::symbols::Marker; -use crate::tui::widgets::{Dataset as TuiDataset, GraphType}; +use crate::ratatui::symbols::Marker; +use crate::ratatui::widgets::{Dataset as TuiDataset, GraphType}; /// Dataset describes a set of data for a chart #[derive(Clone, Debug)] @@ -105,7 +105,7 @@ mod test { use pretty_assertions::assert_eq; use super::*; - use crate::tui::style::Color; + use crate::ratatui::style::Color; #[test] fn dataset() { diff --git a/src/core/props/layout.rs b/src/core/props/layout.rs index abe3bf6..8db3ef4 100644 --- a/src/core/props/layout.rs +++ b/src/core/props/layout.rs @@ -2,7 +2,7 @@ //! //! This module exposes the layout type -use crate::tui::layout::{Constraint, Direction, Layout as TuiLayout, Margin, Rect}; +use crate::ratatui::layout::{Constraint, Direction, Layout as TuiLayout, Margin, Rect}; /// Defines how a layout has to be rendered #[derive(Debug, PartialEq, Clone, Eq)] @@ -60,26 +60,13 @@ impl Layout { /// Split an `Area` into chunks using the current layout configuration pub fn chunks(&self, area: Rect) -> Vec { - #[cfg(feature = "tui")] - { - TuiLayout::default() - .direction(self.direction.clone()) - .horizontal_margin(self.margin.horizontal) - .vertical_margin(self.margin.vertical) - .constraints::<&[Constraint]>(self.constraints.as_ref()) - .split(area) - .to_vec() - } - #[cfg(feature = "ratatui")] - { - TuiLayout::default() - .direction(self.direction) - .horizontal_margin(self.margin.horizontal) - .vertical_margin(self.margin.vertical) - .constraints::<&[Constraint]>(self.constraints.as_ref()) - .split(area) - .to_vec() - } + TuiLayout::default() + .direction(self.direction) + .horizontal_margin(self.margin.horizontal) + .vertical_margin(self.margin.vertical) + .constraints::<&[Constraint]>(self.constraints.as_ref()) + .split(area) + .to_vec() } } diff --git a/src/core/props/mod.rs b/src/core/props/mod.rs index 5a38ee2..86029b5 100644 --- a/src/core/props/mod.rs +++ b/src/core/props/mod.rs @@ -24,8 +24,8 @@ pub use shape::Shape; pub use texts::{Table, TableBuilder, TextSpan}; pub use value::{PropPayload, PropValue}; -pub use crate::tui::layout::Alignment; -pub use crate::tui::style::{Color, Modifier as TextModifiers, Style}; +pub use crate::ratatui::layout::Alignment; +pub use crate::ratatui::style::{Color, Modifier as TextModifiers, Style}; /// The props struct holds all the attributes associated to the component. /// Properties have been designed to be versatile for all kind of components, but without introducing diff --git a/src/core/props/shape.rs b/src/core/props/shape.rs index b0667ca..c6ea41a 100644 --- a/src/core/props/shape.rs +++ b/src/core/props/shape.rs @@ -3,7 +3,7 @@ //! This module exposes the shape attribute type use super::Color; -use crate::tui::widgets::canvas::{Line, Map, Rectangle}; +use crate::ratatui::widgets::canvas::{Line, Map, Rectangle}; /// Describes the shape to draw on the canvas #[derive(Clone, Debug)] diff --git a/src/core/props/texts.rs b/src/core/props/texts.rs index b3035f8..d6b0f14 100644 --- a/src/core/props/texts.rs +++ b/src/core/props/texts.rs @@ -3,7 +3,7 @@ //! `Texts` is the module which defines the texts properties for components. //! It also provides some helpers and builders to facilitate the use of builders. -use crate::tui::style::{Color, Modifier}; +use crate::ratatui::style::{Color, Modifier}; // -- Text parts diff --git a/src/core/props/value.rs b/src/core/props/value.rs index 35f5d6e..fc21f78 100644 --- a/src/core/props/value.rs +++ b/src/core/props/value.rs @@ -315,7 +315,7 @@ mod tests { use std::collections::HashMap; use super::*; - use crate::tui::widgets::canvas::Map; + use crate::ratatui::widgets::canvas::Map; #[test] fn prop_values() { diff --git a/src/core/view.rs b/src/core/view.rs index f925b12..80089cb 100644 --- a/src/core/view.rs +++ b/src/core/view.rs @@ -8,7 +8,7 @@ use std::hash::Hash; use thiserror::Error; -use crate::tui::layout::Rect; +use crate::ratatui::layout::Rect; use crate::{AttrValue, Attribute, Component, Event, Frame, Injector, State}; /// A boxed component. Shorthand for View components map diff --git a/src/lib.rs b/src/lib.rs index add4379..be4cdda 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,8 +74,8 @@ mod core; pub mod listener; #[cfg(test)] pub mod mock; +pub mod ratatui; pub mod terminal; -pub mod tui; pub mod utils; // -- export pub use adapter::{Frame, Terminal}; diff --git a/src/mock/components.rs b/src/mock/components.rs index 58543eb..ce0e84c 100644 --- a/src/mock/components.rs +++ b/src/mock/components.rs @@ -23,7 +23,7 @@ impl Default for MockInput { } impl MockComponent for MockInput { - fn view(&mut self, _: &mut crate::Frame, _: crate::tui::layout::Rect) {} + fn view(&mut self, _: &mut crate::Frame, _: crate::ratatui::layout::Rect) {} fn query(&self, attr: Attribute) -> Option { self.props.get(attr) diff --git a/src/ratatui.rs b/src/ratatui.rs new file mode 100644 index 0000000..1cd250e --- /dev/null +++ b/src/ratatui.rs @@ -0,0 +1,5 @@ +//! ## ratatui +//! +//! `ratatui` just exposes the ratatui modules, in order to include the entire library inside realm + +pub use ratatui::*; diff --git a/src/terminal.rs b/src/terminal.rs index 637e83c..96e66de 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -25,7 +25,7 @@ pub enum TerminalError { Unsupported, } -/// An helper around `Terminal` to quickly setup and perform on terminal. +/// An helper around [`Terminal`] to quickly setup and perform on terminal. /// You can opt whether to use or not this structure to interact with the terminal /// Anyway this structure is 100% cross-backend compatible and is really easy to use, so I suggest you to use it. /// If you need more advance terminal command, you can get a reference to it using the `raw()` and `raw_mut()` methods. @@ -41,6 +41,55 @@ impl TerminalBridge { }) } + /// Initialize a terminal with reasonable defaults for most applications. + /// + /// This will create a new [`TerminalBridge`] and initialize it with the following defaults: + /// + /// - Raw mode is enabled + /// - Alternate screen buffer enabled + /// - A panic hook is installed that restores the terminal before panicking. Ensure that this method + /// is called after any other panic hooks that may be installed to ensure that the terminal is + /// restored before those hooks are called. + /// + /// For more control over the terminal initialization, use [`TerminalBridge::new`]. + pub fn init() -> TerminalResult { + let mut terminal = Self::new()?; + terminal.enable_raw_mode()?; + terminal.enter_alternate_screen()?; + Self::set_panic_hook(); + + Ok(terminal) + } + + /// Restore the terminal to its original state. + /// + /// This function will attempt to restore the terminal to its original state by leaving the alternate screen + /// and disabling raw mode. If either of these operations fail, the error will be returned. + pub fn restore(&mut self) -> TerminalResult<()> { + self.leave_alternate_screen()?; + self.disable_raw_mode() + } + + /// Sets a panic hook that restores the terminal before panicking. + /// + /// Replaces the panic hook with a one that will restore the terminal state before calling the + /// original panic hook. This ensures that the terminal is left in a good state when a panic occurs. + pub fn set_panic_hook() { + let hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + #[cfg(feature = "crossterm")] + { + let _ = crossterm::terminal::disable_raw_mode(); + let _ = crossterm::execute!( + std::io::stdout(), + crossterm::terminal::LeaveAlternateScreen + ); + } + + hook(info); + })); + } + /// Enter in alternate screen using the terminal adapter pub fn enter_alternate_screen(&mut self) -> TerminalResult<()> { self.adapt_enter_alternate_screen() diff --git a/src/tui.rs b/src/tui.rs deleted file mode 100644 index c5ae484..0000000 --- a/src/tui.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! ## tui -//! -//! `tui` just exposes the ratatui modules, in order to include the entire library inside realm - -#[cfg(feature = "ratatui")] -pub use ratatui::*; -#[cfg(feature = "tui")] -pub use tui::*; diff --git a/src/utils/parser.rs b/src/utils/parser.rs index 4fa0eb6..4e436f9 100644 --- a/src/utils/parser.rs +++ b/src/utils/parser.rs @@ -7,7 +7,7 @@ use std::str::FromStr; use lazy_regex::{Lazy, Regex}; use super::{Email, PhoneNumber}; -use crate::tui::style::Color; +use crate::ratatui::style::Color; /** * Regex matches: * - group 1: Red From b25e62d44705de9464b3e7c2c565188fdc3fd377 Mon Sep 17 00:00:00 2001 From: veeso Date: Sat, 12 Oct 2024 20:19:59 +0200 Subject: [PATCH 02/18] fix: ci --- .github/workflows/coverage.yml | 2 +- .github/workflows/crossterm-windows.yml | 2 +- src/lib.rs | 17 +---------------- 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 8f5b2a0..71f30c6 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -18,7 +18,7 @@ jobs: toolchain: nightly override: true - name: Run tests - run: cargo test --no-fail-fast --lib --no-default-features --features derive,serialize,with-crossterm + run: cargo test --no-fail-fast --lib --no-default-features --features derive,serialize env: CARGO_INCREMENTAL: "0" RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests" diff --git a/.github/workflows/crossterm-windows.yml b/.github/workflows/crossterm-windows.yml index 455c52a..04f6a58 100644 --- a/.github/workflows/crossterm-windows.yml +++ b/.github/workflows/crossterm-windows.yml @@ -23,4 +23,4 @@ jobs: - name: Format run: cargo fmt --all -- --check - name: Clippy - run: cargo clippy --lib --no-default-features --features derive,serialize,ratatui,crossterm -- -Dwarnings + run: cargo clippy --lib --no-default-features --features derive,serialize,crossterm -- -Dwarnings diff --git a/src/lib.rs b/src/lib.rs index be4cdda..f84caa0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,24 +20,9 @@ //! If you want the default features, just add tuirealm 1.x version: //! //! ```toml -//! tuirealm = "^1.9.0" +//! tuirealm = "^2" //! ``` //! -//! otherwise you can specify the features you want to add: -//! -//! ```toml -//! tuirealm = { version = "^1.9.0", default-features = false, features = [ "derive", "with-termion" ] } -//! ``` -//! -//! Supported features are: -//! -//! - `derive` (*default*): add the `#[derive(MockComponent)]` proc macro to automatically implement `MockComponent` for `Component`. [Read more](https://github.com/veeso/tuirealm_derive). -//! - `with-crossterm` (*default*): use [crossterm](https://github.com/crossterm-rs/crossterm) as backend for tui. -//! - `with-termion` (*default*): use [termion](https://github.com/redox-os/termion) as backend for tui. -//! -//! > ⚠️ You can enable only one backend at the time and at least one must be enabled in order to build. -//! > ❗ You don't need tui as a dependency, since you can access to tui types via `use tuirealm::tui::` -//! //! ### Create a tui-realm application 🪂 //! //! You can read the guide to get started with tui-realm on [Github](https://github.com/veeso/tui-realm/blob/main/docs/en/get-started.md) From 5e0d1f8ffb4d78925a888237357d61c9a0714f04 Mon Sep 17 00:00:00 2001 From: veeso Date: Sun, 13 Oct 2024 12:45:35 +0200 Subject: [PATCH 03/18] feat!: TerminalAdapter as a trait; draw as a method of TerminalAdapter --- CHANGELOG.md | 2 + Cargo.toml | 2 + examples/demo/app/model.rs | 26 ++-- examples/user_events/model.rs | 24 ++- examples/user_events/user_events.rs | 33 ++-- src/adapter/crossterm/listener.rs | 49 ------ src/adapter/crossterm/mod.rs | 26 ---- src/adapter/crossterm/terminal.rs | 54 ------- src/adapter/mod.rs | 23 --- src/adapter/termion/listener.rs | 44 ------ src/adapter/termion/mod.rs | 30 ---- src/adapter/termion/terminal.rs | 47 ------ src/core/application.rs | 3 +- src/core/component.rs | 4 +- src/core/event.rs | 28 ++++ src/core/view.rs | 3 +- src/lib.rs | 5 +- src/listener/builder.rs | 45 +++++- src/listener/mod.rs | 7 +- src/mock/components.rs | 4 +- src/terminal.rs | 146 +++++++++++++++--- src/terminal/adapter.rs | 57 +++++++ src/terminal/adapter/crossterm.rs | 78 ++++++++++ src/terminal/adapter/termion.rs | 77 +++++++++ src/terminal/event_listener.rs | 11 ++ .../event_listener/crossterm.rs} | 52 ++++++- .../event_listener/termion.rs} | 56 ++++++- 27 files changed, 580 insertions(+), 356 deletions(-) delete mode 100644 src/adapter/crossterm/listener.rs delete mode 100644 src/adapter/crossterm/mod.rs delete mode 100644 src/adapter/crossterm/terminal.rs delete mode 100644 src/adapter/mod.rs delete mode 100644 src/adapter/termion/listener.rs delete mode 100644 src/adapter/termion/mod.rs delete mode 100644 src/adapter/termion/terminal.rs create mode 100644 src/terminal/adapter.rs create mode 100644 src/terminal/adapter/crossterm.rs create mode 100644 src/terminal/adapter/termion.rs create mode 100644 src/terminal/event_listener.rs rename src/{adapter/crossterm/event.rs => terminal/event_listener/crossterm.rs} (85%) rename src/{adapter/termion/event.rs => terminal/event_listener/termion.rs} (74%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f97cf6..2a33dd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,8 @@ Released on ??/10/2024 - A panic hook is installed that restores the terminal before panicking. Ensure that this method is called after any other panic hooks that may be installed to ensure that the terminal is. - `restore`: Restore the terminal to its original state - `set_panic_hook`: Sets a panic hook that restores the terminal before panicking. + - Added `draw` to `TerminalBridge` +- Removed `InputListener`. Now use `CrosstermInputListener` or `TermionInputListener`. - Bump `ratatui` version to `0.28` ## 1.9.2 diff --git a/Cargo.toml b/Cargo.toml index a56f7d3..ffc4627 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,9 @@ termion = ["dep:termion", "ratatui/termion"] [[example]] name = "demo" path = "examples/demo/demo.rs" +required-features = ["crossterm"] [[example]] name = "user-events" path = "examples/user_events/user_events.rs" +required-features = ["crossterm"] diff --git a/examples/demo/app/model.rs b/examples/demo/app/model.rs index 7511373..cb32478 100644 --- a/examples/demo/app/model.rs +++ b/examples/demo/app/model.rs @@ -7,7 +7,7 @@ use std::time::{Duration, SystemTime}; use tuirealm::event::NoUserEvent; use tuirealm::props::{Alignment, Color, TextModifiers}; use tuirealm::ratatui::layout::{Constraint, Direction, Layout}; -use tuirealm::terminal::TerminalBridge; +use tuirealm::terminal::{CrosstermTerminalAdapter, TerminalAdapter, TerminalBridge}; use tuirealm::{ Application, AttrValue, Attribute, EventListenerCfg, Sub, SubClause, SubEventClause, Update, }; @@ -15,7 +15,10 @@ use tuirealm::{ use super::components::{Clock, DigitCounter, Label, LetterCounter}; use super::{Id, Msg}; -pub struct Model { +pub struct Model +where + T: TerminalAdapter, +{ /// Application pub app: Application, /// Indicates that the application must quit @@ -23,25 +26,27 @@ pub struct Model { /// Tells whether to redraw interface pub redraw: bool, /// Used to draw to terminal - pub terminal: TerminalBridge, + pub terminal: TerminalBridge, } -impl Default for Model { +impl Default for Model { fn default() -> Self { Self { app: Self::init_app(), quit: false, redraw: true, - terminal: TerminalBridge::new().expect("Cannot initialize terminal"), + terminal: TerminalBridge::init_crossterm().expect("Cannot initialize terminal"), } } } -impl Model { +impl Model +where + T: TerminalAdapter, +{ pub fn view(&mut self) { assert!(self .terminal - .raw_mut() .draw(|f| { let chunks = Layout::default() .direction(Direction::Vertical) @@ -72,7 +77,7 @@ impl Model { let mut app: Application = Application::init( EventListenerCfg::default() - .default_input_listener(Duration::from_millis(20)) + .crossterm_input_listener(Duration::from_millis(20)) .poll_timeout(Duration::from_millis(10)) .tick_interval(Duration::from_secs(1)), ); @@ -128,7 +133,10 @@ impl Model { // Let's implement Update for model -impl Update for Model { +impl Update for Model +where + T: TerminalAdapter, +{ fn update(&mut self, msg: Option) -> Option { if let Some(msg) = msg { // Set redraw diff --git a/examples/user_events/model.rs b/examples/user_events/model.rs index 41a6722..fd90f84 100644 --- a/examples/user_events/model.rs +++ b/examples/user_events/model.rs @@ -3,12 +3,15 @@ //! app model use tuirealm::ratatui::layout::{Constraint, Direction, Layout}; -use tuirealm::terminal::TerminalBridge; +use tuirealm::terminal::{TerminalAdapter, TerminalBridge}; use tuirealm::{Application, Update}; use super::{Id, Msg, UserEvent}; -pub struct Model { +pub struct Model +where + T: TerminalAdapter, +{ /// Application pub app: Application, /// Indicates that the application must quit @@ -16,23 +19,25 @@ pub struct Model { /// Tells whether to redraw interface pub redraw: bool, /// Used to draw to terminal - pub terminal: TerminalBridge, + pub terminal: TerminalBridge, } -impl Model { - pub fn new(app: Application) -> Self { +impl Model +where + T: TerminalAdapter, +{ + pub fn new(app: Application, adapter: T) -> Self { Self { app, quit: false, redraw: true, - terminal: TerminalBridge::new().expect("Cannot initialize terminal"), + terminal: TerminalBridge::init(adapter).expect("Cannot initialize terminal"), } } pub fn view(&mut self) { assert!(self .terminal - .raw_mut() .draw(|f| { let chunks = Layout::default() .direction(Direction::Vertical) @@ -54,7 +59,10 @@ impl Model { // Let's implement Update for model -impl Update for Model { +impl Update for Model +where + T: TerminalAdapter, +{ fn update(&mut self, msg: Option) -> Option { if let Some(msg) = msg { // Set redraw diff --git a/examples/user_events/user_events.rs b/examples/user_events/user_events.rs index 6a4ed21..5eb3be7 100644 --- a/examples/user_events/user_events.rs +++ b/examples/user_events/user_events.rs @@ -5,6 +5,7 @@ use std::time::{Duration, SystemTime}; use components::Label; use tuirealm::listener::{ListenerResult, Poll}; +use tuirealm::terminal::CrosstermTerminalAdapter; use tuirealm::{ Application, Event, EventListenerCfg, PollStrategy, Sub, SubClause, SubEventClause, Update, }; @@ -49,14 +50,14 @@ impl Poll for UserDataPort { } fn main() { - let mut app: Application = Application::init( - EventListenerCfg::default() - .default_input_listener(Duration::from_millis(10)) - .port( - Box::new(UserDataPort::default()), - Duration::from_millis(1000), - ), - ); + let event_listener = EventListenerCfg::default() + .crossterm_input_listener(Duration::from_millis(10)) + .port( + Box::new(UserDataPort::default()), + Duration::from_millis(1000), + ); + + let mut app: Application = Application::init(event_listener); // subscribe component to clause app.mount( @@ -80,9 +81,10 @@ fn main() { app.active(&Id::Label).expect("failed to active"); - let mut model = Model::new(app); - let _ = model.terminal.enter_alternate_screen(); - let _ = model.terminal.enable_raw_mode(); + let mut model = Model::new( + app, + CrosstermTerminalAdapter::new().expect("failed to create terminal"), + ); // Main loop // NOTE: loop until quit; quit is set in update if AppClose is received from counter while !model.quit { @@ -109,8 +111,9 @@ fn main() { model.redraw = false; } } - // Terminate terminal - let _ = model.terminal.leave_alternate_screen(); - let _ = model.terminal.disable_raw_mode(); - let _ = model.terminal.clear_screen(); + + model + .terminal + .restore() + .expect("failed to restore terminal"); } diff --git a/src/adapter/crossterm/listener.rs b/src/adapter/crossterm/listener.rs deleted file mode 100644 index f3f7068..0000000 --- a/src/adapter/crossterm/listener.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! ## Listener -//! -//! input listener adapter for crossterm - -use std::marker::PhantomData; -use std::time::Duration; - -use crossterm::event as xterm; - -use super::Event; -use crate::listener::{ListenerError, ListenerResult, Poll}; - -/// The input listener for crossterm. -/// If crossterm is enabled, this will already be exported as `InputEventListener` in the `adapter` module -/// or you can use it directly in the event listener, calling `default_input_listener()` in the `EventListenerCfg` -pub struct CrosstermInputListener -where - U: Eq + PartialEq + Clone + PartialOrd + Send, -{ - ghost: PhantomData, - interval: Duration, -} - -impl CrosstermInputListener -where - U: Eq + PartialEq + Clone + PartialOrd + Send, -{ - pub fn new(interval: Duration) -> Self { - Self { - ghost: PhantomData, - interval: interval / 2, - } - } -} - -impl Poll for CrosstermInputListener -where - U: Eq + PartialEq + Clone + PartialOrd + Send + 'static, -{ - fn poll(&mut self) -> ListenerResult>> { - match xterm::poll(self.interval) { - Ok(true) => xterm::read() - .map(|x| Some(Event::from(x))) - .map_err(|_| ListenerError::PollFailed), - Ok(false) => Ok(None), - Err(_) => Err(ListenerError::PollFailed), - } - } -} diff --git a/src/adapter/crossterm/mod.rs b/src/adapter/crossterm/mod.rs deleted file mode 100644 index 5e74bf1..0000000 --- a/src/adapter/crossterm/mod.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! ## crossterm -//! -//! this module contains the adapters for crossterm - -extern crate crossterm; - -mod event; -mod listener; -mod terminal; - -// -- export -use std::io::Stdout; - -pub use listener::CrosstermInputListener; - -use super::{Event, Key, KeyEvent, KeyModifiers, MediaKeyCode}; -use crate::ratatui::backend::CrosstermBackend; -use crate::ratatui::{Frame as TuiFrame, Terminal as TuiTerminal}; - -// -- Frame - -/// Frame represents the Frame where the view will be displayed in -pub type Frame<'a> = TuiFrame<'a>; - -/// Terminal must be used to interact with the terminal in tui applications -pub type Terminal = TuiTerminal>; diff --git a/src/adapter/crossterm/terminal.rs b/src/adapter/crossterm/terminal.rs deleted file mode 100644 index 4669598..0000000 --- a/src/adapter/crossterm/terminal.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! ## Terminal -//! -//! terminal bridge adapter for crossterm - -use std::io::stdout; - -use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; -use crossterm::execute; -use crossterm::terminal::{ - disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, -}; - -use crate::ratatui::backend::CrosstermBackend; -use crate::terminal::{TerminalBridge, TerminalError, TerminalResult}; -use crate::Terminal; - -impl TerminalBridge { - pub(crate) fn adapt_new_terminal() -> TerminalResult { - Terminal::new(CrosstermBackend::new(stdout())) - .map_err(|_| TerminalError::CannotConnectStdout) - } - - pub(crate) fn adapt_enter_alternate_screen(&mut self) -> TerminalResult<()> { - execute!( - self.raw_mut().backend_mut(), - EnterAlternateScreen, - EnableMouseCapture - ) - .map_err(|_| TerminalError::CannotEnterAlternateMode) - } - - pub(crate) fn adapt_leave_alternate_screen(&mut self) -> TerminalResult<()> { - execute!( - self.raw_mut().backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - ) - .map_err(|_| TerminalError::CannotLeaveAlternateMode) - } - - pub(crate) fn adapt_clear_screen(&mut self) -> TerminalResult<()> { - self.raw_mut() - .clear() - .map_err(|_| TerminalError::CannotClear) - } - - pub(crate) fn adapt_enable_raw_mode(&mut self) -> TerminalResult<()> { - enable_raw_mode().map_err(|_| TerminalError::CannotToggleRawMode) - } - - pub(crate) fn adapt_disable_raw_mode(&mut self) -> TerminalResult<()> { - disable_raw_mode().map_err(|_| TerminalError::CannotToggleRawMode) - } -} diff --git a/src/adapter/mod.rs b/src/adapter/mod.rs deleted file mode 100644 index 33c479a..0000000 --- a/src/adapter/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! ## adapters -//! -//! this module contains the event converter for the different backends - -#[cfg(feature = "crossterm")] -use crate::core::event::MediaKeyCode; -use crate::core::event::{Event, Key, KeyEvent, KeyModifiers}; - -// -- crossterm -#[cfg(feature = "crossterm")] -pub mod crossterm; -#[cfg(feature = "crossterm")] -pub use self::crossterm::CrosstermInputListener as InputEventListener; -#[cfg(feature = "crossterm")] -pub use self::crossterm::{Frame, Terminal}; - -// -- termion -#[cfg(feature = "termion")] -pub mod termion; -#[cfg(feature = "termion")] -pub use self::termion::TermionInputListener as InputEventListener; -#[cfg(feature = "termion")] -pub use self::termion::{Frame, Terminal}; diff --git a/src/adapter/termion/listener.rs b/src/adapter/termion/listener.rs deleted file mode 100644 index 117b930..0000000 --- a/src/adapter/termion/listener.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! ## Listener -//! -//! input listener adapter for termion - -use std::io::stdin; -use std::marker::PhantomData; -use std::time::Duration; - -use termion::input::TermRead; - -use super::Event; -use crate::listener::{ListenerError, ListenerResult, Poll}; - -/// The input listener for termion. -/// If termion is enabled, this will already be exported as `InputEventListener` in the `adapter` module -/// or you can use it directly in the event listener, calling `default_input_listener()` in the `EventListenerCfg` -pub struct TermionInputListener -where - U: Eq + PartialEq + Clone + PartialOrd + Send, -{ - ghost: PhantomData, -} - -impl TermionInputListener -where - U: Eq + PartialEq + Clone + PartialOrd + Send, -{ - pub fn new(_interval: Duration) -> Self { - Self { ghost: PhantomData } - } -} - -impl Poll for TermionInputListener -where - U: Eq + PartialEq + Clone + PartialOrd + Send + 'static, -{ - fn poll(&mut self) -> ListenerResult>> { - match stdin().events().next() { - Some(Ok(ev)) => Ok(Some(Event::from(ev))), - Some(Err(_)) => Err(ListenerError::PollFailed), - None => Ok(None), - } - } -} diff --git a/src/adapter/termion/mod.rs b/src/adapter/termion/mod.rs deleted file mode 100644 index 39f5eb4..0000000 --- a/src/adapter/termion/mod.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! ## termion -//! -//! this module contains the adapters for termion - -extern crate termion; - -mod event; -mod listener; -mod terminal; - -// -- export -use std::io::Stdout; - -pub use listener::TermionInputListener; -use termion::input::MouseTerminal; -use termion::raw::RawTerminal; -use termion::screen::AlternateScreen; - -use super::{Event, Key, KeyEvent, KeyModifiers}; -use crate::tui::backend::TermionBackend; -use crate::tui::{Frame as TuiFrame, Terminal as TuiTerminal}; - -// -- Frame - -/// Frame represents the Frame where the view will be displayed in -pub type Frame<'a> = TuiFrame<'a>; - -/// Terminal must be used to interact with the terminal in tui applications -pub type Terminal = - TuiTerminal>>>>; diff --git a/src/adapter/termion/terminal.rs b/src/adapter/termion/terminal.rs deleted file mode 100644 index f3439a8..0000000 --- a/src/adapter/termion/terminal.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! ## Terminal -//! -//! terminal bridge adapter for termion - -use std::io::stdout; - -use termion::input::MouseTerminal; -use termion::raw::IntoRawMode; -use termion::screen::IntoAlternateScreen; - -use crate::terminal::{TerminalBridge, TerminalError, TerminalResult}; -use crate::tui::backend::TermionBackend; -use crate::Terminal; - -impl TerminalBridge { - pub(crate) fn adapt_new_terminal() -> TerminalResult { - let stdout = stdout() - .into_raw_mode() - .map_err(|_| TerminalError::CannotConnectStdout)? - .into_alternate_screen() - .map_err(|_| TerminalError::CannotConnectStdout)?; - let stdout = MouseTerminal::from(stdout); - Terminal::new(TermionBackend::new(stdout)).map_err(|_| TerminalError::CannotConnectStdout) - } - - pub(crate) fn adapt_enter_alternate_screen(&mut self) -> TerminalResult<()> { - Err(TerminalError::Unsupported) - } - - pub(crate) fn adapt_leave_alternate_screen(&mut self) -> TerminalResult<()> { - Err(TerminalError::Unsupported) - } - - pub(crate) fn adapt_clear_screen(&mut self) -> TerminalResult<()> { - self.raw_mut() - .clear() - .map_err(|_| TerminalError::CannotClear) - } - - pub(crate) fn adapt_enable_raw_mode(&mut self) -> TerminalResult<()> { - Err(TerminalError::Unsupported) - } - - pub(crate) fn adapt_disable_raw_mode(&mut self) -> TerminalResult<()> { - Err(TerminalError::Unsupported) - } -} diff --git a/src/core/application.rs b/src/core/application.rs index a60d29e..7314fe3 100644 --- a/src/core/application.rs +++ b/src/core/application.rs @@ -5,12 +5,13 @@ use std::hash::Hash; use std::time::{Duration, Instant}; +use ratatui::Frame; use thiserror::Error; use super::{Subscription, View, WrappedComponent}; use crate::listener::{EventListener, EventListenerCfg, ListenerError}; use crate::ratatui::layout::Rect; -use crate::{AttrValue, Attribute, Event, Frame, Injector, State, Sub, SubEventClause, ViewError}; +use crate::{AttrValue, Attribute, Event, Injector, State, Sub, SubEventClause, ViewError}; /// Result retuned by `Application`. /// Ok depends on method diff --git a/src/core/component.rs b/src/core/component.rs index 39cfc0b..cea8322 100644 --- a/src/core/component.rs +++ b/src/core/component.rs @@ -2,9 +2,11 @@ //! //! This module exposes the component traits +use ratatui::Frame; + use crate::command::{Cmd, CmdResult}; use crate::ratatui::layout::Rect; -use crate::{AttrValue, Attribute, Event, Frame, State}; +use crate::{AttrValue, Attribute, Event, State}; /// A Mock Component represents a component which defines all the properties and states it can handle and represent /// and the way it should be rendered. It must also define how to behave in case of a `Cmd` (command). diff --git a/src/core/event.rs b/src/core/event.rs index 4fab6b4..c3319a7 100644 --- a/src/core/event.rs +++ b/src/core/event.rs @@ -142,6 +142,34 @@ pub enum Key { Media(MediaKeyCode), /// Escape key. Esc, + /// Shift left + ShiftLeft, + /// Alt left; warning: it is supported only on termion + AltLeft, + /// warning: it is supported only on termion + CtrlLeft, + /// warning: it is supported only on termion + ShiftRight, + /// warning: it is supported only on termion + AltRight, + /// warning: it is supported only on termion + CtrlRight, + /// warning: it is supported only on termion + ShiftUp, + /// warning: it is supported only on termion + AltUp, + /// warning: it is supported only on termion + CtrlUp, + /// warning: it is supported only on termion + ShiftDown, + /// warning: it is supported only on termion + AltDown, + /// warning: it is supported only on termion + CtrlDown, + /// warning: it is supported only on termion + CtrlHome, + /// warning: it is supported only on termion + CtrlEnd, } /// Defines special key states, such as shift, control, alt... diff --git a/src/core/view.rs b/src/core/view.rs index 80089cb..d499809 100644 --- a/src/core/view.rs +++ b/src/core/view.rs @@ -6,10 +6,11 @@ use std::collections::HashMap; use std::hash::Hash; +use ratatui::Frame; use thiserror::Error; use crate::ratatui::layout::Rect; -use crate::{AttrValue, Attribute, Component, Event, Frame, Injector, State}; +use crate::{AttrValue, Attribute, Component, Event, Injector, State}; /// A boxed component. Shorthand for View components map pub(crate) type WrappedComponent = Box>; diff --git a/src/lib.rs b/src/lib.rs index f84caa0..734613a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,8 +53,6 @@ extern crate self as tuirealm; #[macro_use] extern crate tuirealm_derive; -// -- modules -pub mod adapter; mod core; pub mod listener; #[cfg(test)] @@ -62,8 +60,6 @@ pub mod mock; pub mod ratatui; pub mod terminal; pub mod utils; -// -- export -pub use adapter::{Frame, Terminal}; pub use listener::{EventListenerCfg, ListenerError}; // -- derive #[cfg(feature = "derive")] @@ -76,3 +72,4 @@ pub use self::core::injector::Injector; pub use self::core::props::{self, AttrValue, Attribute, Props}; pub use self::core::subscription::{EventClause as SubEventClause, Sub, SubClause}; pub use self::core::{command, Component, MockComponent, State, StateValue, Update, ViewError}; +pub use self::ratatui::Frame; diff --git a/src/listener/builder.rs b/src/listener/builder.rs index e175421..84ee5ff 100644 --- a/src/listener/builder.rs +++ b/src/listener/builder.rs @@ -2,7 +2,7 @@ //! //! This module exposes the EventListenerCfg which is used to build the event listener -use super::{Duration, EventListener, InputEventListener, Poll, Port}; +use super::{Duration, EventListener, Poll, Port}; /// The event listener configurator is used to setup an event listener. /// Once you're done with configuration just call `start()` and the event listener will start and the listener @@ -65,9 +65,22 @@ where self } - /// Add to the event listener the default input event listener for the backend configured. - pub fn default_input_listener(self, interval: Duration) -> Self { - self.port(Box::new(InputEventListener::::new(interval)), interval) + #[cfg(feature = "crossterm")] + /// Add to the event listener the default crossterm input listener [`crate::terminal::CrosstermInputListener`] + pub fn crossterm_input_listener(self, interval: Duration) -> Self { + self.port( + Box::new(crate::terminal::CrosstermInputListener::::new(interval)), + interval, + ) + } + + #[cfg(feature = "termion")] + /// Add to the event listener the default termion input listener [`crate::terminal::TermionInputListener`] + pub fn termion_input_listener(self, interval: Duration) -> Self { + self.port( + Box::new(crate::terminal::TermionInputListener::::new(interval)), + interval, + ) } } @@ -80,7 +93,27 @@ mod test { use crate::mock::{MockEvent, MockPoll}; #[test] - fn should_configure_and_start_event_listener() { + #[cfg(feature = "crossterm")] + fn should_configure_and_start_event_listener_crossterm() { + let builder = EventListenerCfg::::default(); + assert!(builder.ports.is_empty()); + assert!(builder.tick_interval.is_none()); + assert_eq!(builder.poll_timeout, Duration::from_millis(10)); + let builder = builder.tick_interval(Duration::from_secs(10)); + assert_eq!(builder.tick_interval.unwrap(), Duration::from_secs(10)); + let builder = builder.poll_timeout(Duration::from_millis(50)); + assert_eq!(builder.poll_timeout, Duration::from_millis(50)); + let builder = builder + .crossterm_input_listener(Duration::from_millis(200)) + .port(Box::new(MockPoll::default()), Duration::from_secs(300)); + assert_eq!(builder.ports.len(), 2); + let mut listener = builder.start(); + assert!(listener.stop().is_ok()); + } + + #[test] + #[cfg(feature = "termion")] + fn should_configure_and_start_event_listener_termion() { let builder = EventListenerCfg::::default(); assert!(builder.ports.is_empty()); assert!(builder.tick_interval.is_none()); @@ -90,7 +123,7 @@ mod test { let builder = builder.poll_timeout(Duration::from_millis(50)); assert_eq!(builder.poll_timeout, Duration::from_millis(50)); let builder = builder - .default_input_listener(Duration::from_millis(200)) + .termion_input_listener(Duration::from_millis(200)) .port(Box::new(MockPoll::default()), Duration::from_secs(300)); assert_eq!(builder.ports.len(), 2); let mut listener = builder.start(); diff --git a/src/listener/mod.rs b/src/listener/mod.rs index 2433d37..e356eeb 100644 --- a/src/listener/mod.rs +++ b/src/listener/mod.rs @@ -20,10 +20,9 @@ use worker::EventListenerWorker; // -- internal use super::Event; -pub use crate::adapter::InputEventListener; /// Result returned by `EventListener`. Ok value depends on the method, while the -/// Err value is always `ListenerError`. +/// Err value is always [`ListenerError`]. pub type ListenerResult = Result; #[derive(Debug, Error)] @@ -38,7 +37,7 @@ pub enum ListenerError { PollFailed, } -/// The poll trait defines the function `poll`, which will be called by the event listener +/// The poll trait defines the function [`poll`], which will be called by the event listener /// dedicated thread to poll for events. pub trait Poll: Send where @@ -74,7 +73,7 @@ impl EventListener where U: Eq + PartialEq + Clone + PartialOrd + Send + 'static, { - /// Create a new `EventListener` and start it. + /// Create a new [`EventListener`] and start it. /// - `poll` is the trait object which polls for input events /// - `poll_interval` is the interval to poll for input events. It should always be at least a poll time used by `poll` /// - `tick_interval` is the interval used to send the `Tick` event. If `None`, no tick will be sent. diff --git a/src/mock/components.rs b/src/mock/components.rs index ce0e84c..6c23061 100644 --- a/src/mock/components.rs +++ b/src/mock/components.rs @@ -2,6 +2,8 @@ //! //! mock components +use ratatui::Frame; + use super::{MockEvent, MockMsg}; use crate::command::{Cmd, CmdResult, Direction}; use crate::event::{Event, Key, KeyEvent, KeyModifiers}; @@ -23,7 +25,7 @@ impl Default for MockInput { } impl MockComponent for MockInput { - fn view(&mut self, _: &mut crate::Frame, _: crate::ratatui::layout::Rect) {} + fn view(&mut self, _: &mut Frame, _: crate::ratatui::layout::Rect) {} fn query(&self, attr: Attribute) -> Option { self.props.get(attr) diff --git a/src/terminal.rs b/src/terminal.rs index 96e66de..ec867f5 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -2,15 +2,29 @@ //! //! Cross platform Terminal helper -use thiserror::Error; +mod adapter; +mod event_listener; -use crate::Terminal; +use ratatui::{CompletedFrame, Frame, Terminal}; +use thiserror::Error; -// -- types +#[cfg(feature = "crossterm")] +pub use self::adapter::CrosstermTerminalAdapter; +pub use self::adapter::TerminalAdapter; +#[cfg(feature = "termion")] +pub use self::adapter::TermionTerminalAdapter; +#[cfg(feature = "crossterm")] +pub use self::event_listener::CrosstermInputListener; +#[cfg(feature = "termion")] +pub use self::event_listener::TermionInputListener; + +/// TerminalResult is a type alias for a Result that uses [`TerminalError`] as the error type. pub type TerminalResult = Result; #[derive(Debug, Error)] pub enum TerminalError { + #[error("cannot draw frame")] + CannotDrawFrame, #[error("cannot connect to stdout")] CannotConnectStdout, #[error("cannot enter alternate mode")] @@ -29,16 +43,31 @@ pub enum TerminalError { /// You can opt whether to use or not this structure to interact with the terminal /// Anyway this structure is 100% cross-backend compatible and is really easy to use, so I suggest you to use it. /// If you need more advance terminal command, you can get a reference to it using the `raw()` and `raw_mut()` methods. -pub struct TerminalBridge { - terminal: Terminal, +/// +/// To quickly setup a terminal with default settings, you can use the [`TerminalBridge::init()`] method. +/// +/// ```rust +/// use tuirealm::terminal::TerminalBridge; +/// +/// #[cfg(feature = "crossterm")] +/// let mut terminal = TerminalBridge::init_crossterm().unwrap(); +/// #[cfg(feature = "termion")] +/// let mut terminal = TerminalBridge::init_termion().unwrap(); +/// ``` +pub struct TerminalBridge +where + T: TerminalAdapter, +{ + terminal: T, } -impl TerminalBridge { - /// Instantiates a new Terminal bridge - pub fn new() -> TerminalResult { - Ok(Self { - terminal: Self::adapt_new_terminal()?, - }) +impl TerminalBridge +where + T: TerminalAdapter, +{ + /// Instantiates a new Terminal bridge from a [`TerminalAdapter`] + pub fn new(terminal: T) -> Self { + Self { terminal } } /// Initialize a terminal with reasonable defaults for most applications. @@ -52,8 +81,8 @@ impl TerminalBridge { /// restored before those hooks are called. /// /// For more control over the terminal initialization, use [`TerminalBridge::new`]. - pub fn init() -> TerminalResult { - let mut terminal = Self::new()?; + pub fn init(terminal: T) -> TerminalResult { + let mut terminal = Self::new(terminal); terminal.enable_raw_mode()?; terminal.enter_alternate_screen()?; Self::set_panic_hook(); @@ -92,36 +121,105 @@ impl TerminalBridge { /// Enter in alternate screen using the terminal adapter pub fn enter_alternate_screen(&mut self) -> TerminalResult<()> { - self.adapt_enter_alternate_screen() + self.terminal.enter_alternate_screen() } /// Leave the alternate screen using the terminal adapter pub fn leave_alternate_screen(&mut self) -> TerminalResult<()> { - self.adapt_leave_alternate_screen() + self.terminal.leave_alternate_screen() } /// Clear the screen pub fn clear_screen(&mut self) -> TerminalResult<()> { - self.adapt_clear_screen() + self.terminal.clear_screen() } /// Enable terminal raw mode pub fn enable_raw_mode(&mut self) -> TerminalResult<()> { - self.adapt_enable_raw_mode() + self.terminal.enable_raw_mode() } /// Disable terminal raw mode pub fn disable_raw_mode(&mut self) -> TerminalResult<()> { - self.adapt_disable_raw_mode() + self.terminal.disable_raw_mode() + } + + /// Draws a single frame to the terminal. + /// + /// Returns a [`CompletedFrame`] if successful, otherwise a [`TerminalError`]. + /// + /// This method will: + /// + /// - autoresize the terminal if necessary + /// - call the render callback, passing it a [`Frame`] reference to render to + /// - flush the current internal state by copying the current buffer to the backend + /// - move the cursor to the last known position if it was set during the rendering closure + /// - return a [`CompletedFrame`] with the current buffer and the area of the terminal + /// + /// The [`CompletedFrame`] returned by this method can be useful for debugging or testing + /// purposes, but it is often not used in regular applicationss. + /// + /// The render callback should fully render the entire frame when called, including areas that + /// are unchanged from the previous frame. This is because each frame is compared to the + /// previous frame to determine what has changed, and only the changes are written to the + /// terminal. If the render callback does not fully render the frame, the terminal will not be + /// in a consistent state. + pub fn draw(&mut self, render_callback: F) -> TerminalResult + where + F: FnOnce(&mut Frame<'_>), + { + self.terminal.draw(render_callback) + } +} + +#[cfg(feature = "crossterm")] +impl TerminalBridge { + /// Create a new instance of the [`TerminalBridge`] using [`crossterm`] as backend + pub fn new_crossterm() -> TerminalResult { + Ok(Self::new(adapter::CrosstermTerminalAdapter::new()?)) + } + + /// Initialize a terminal with reasonable defaults for most applications using [`crossterm`] as backend. + /// + /// See [`TerminalBridge::init`] for more information. + pub fn init_crossterm() -> TerminalResult { + Self::init(adapter::CrosstermTerminalAdapter::new()?) + } + + /// Returns a reference to the underlying [`Terminal`] + pub fn raw(&self) -> &Terminal> { + self.terminal.raw() + } + + /// Returns a mutable reference the underlying [`Terminal`] + pub fn raw_mut( + &mut self, + ) -> &mut Terminal> { + self.terminal.raw_mut() + } +} + +#[cfg(feature = "termion")] +impl TerminalBridge { + /// Create a new instance of the [`TerminalBridge`] using [`termion`] as backend + pub fn new_termion() -> Self { + Self::new(adapter::TermionTerminalAdapter::new().unwrap()) + } + + /// Initialize a terminal with reasonable defaults for most applications using [`termion`] as backend. + /// + /// See [`TerminalBridge::init`] for more information. + pub fn init_termion() -> TerminalResult { + Self::init(adapter::TermionTerminalAdapter::new().unwrap()) } - /// Returna an immutable reference to the raw `Terminal` structure - pub fn raw(&self) -> &Terminal { - &self.terminal + /// Returns a reference to the underlying [`Terminal`] + pub fn raw(&self) -> &adapter::TermionBackend { + self.terminal.raw() } - /// Return a mutable reference to the raw `Terminal` structure - pub fn raw_mut(&mut self) -> &mut Terminal { - &mut self.terminal + /// Returns a mutable reference to the underlying [`Terminal`] + pub fn raw_mut(&mut self) -> &mut adapter::TermionBackend { + self.terminal.raw_mut() } } diff --git a/src/terminal/adapter.rs b/src/terminal/adapter.rs new file mode 100644 index 0000000..87db013 --- /dev/null +++ b/src/terminal/adapter.rs @@ -0,0 +1,57 @@ +#[cfg(feature = "crossterm")] +mod crossterm; +#[cfg(feature = "termion")] +mod termion; + +#[cfg(feature = "crossterm")] +pub use crossterm::CrosstermTerminalAdapter; +use ratatui::{CompletedFrame, Frame}; +#[cfg(feature = "termion")] +pub use termion::{TermionBackend, TermionTerminalAdapter}; + +use super::TerminalResult; + +/// TerminalAdapter is a trait that defines the methods that a terminal adapter should implement. +/// +/// This trait is used to abstract the terminal implementation from the rest of the application. +/// This allows tui-realm to be used with different terminal libraries, such as crossterm, termion, etc. +pub trait TerminalAdapter { + /// Draws a single frame to the terminal. + /// + /// Returns a [`CompletedFrame`] if successful, otherwise a [`super::TerminalError`]. + /// + /// This method will: + /// + /// - autoresize the terminal if necessary + /// - call the render callback, passing it a [`Frame`] reference to render to + /// - flush the current internal state by copying the current buffer to the backend + /// - move the cursor to the last known position if it was set during the rendering closure + /// - return a [`CompletedFrame`] with the current buffer and the area of the terminal + /// + /// The [`CompletedFrame`] returned by this method can be useful for debugging or testing + /// purposes, but it is often not used in regular applicationss. + /// + /// The render callback should fully render the entire frame when called, including areas that + /// are unchanged from the previous frame. This is because each frame is compared to the + /// previous frame to determine what has changed, and only the changes are written to the + /// terminal. If the render callback does not fully render the frame, the terminal will not be + /// in a consistent state. + fn draw(&mut self, render_callback: F) -> TerminalResult + where + F: FnOnce(&mut Frame<'_>); + + /// Clear the screen + fn clear_screen(&mut self) -> TerminalResult<()>; + + /// Enable terminal raw mode + fn enable_raw_mode(&mut self) -> TerminalResult<()>; + + /// Disable terminal raw mode + fn disable_raw_mode(&mut self) -> TerminalResult<()>; + + /// Enter in alternate screen using the terminal adapter + fn enter_alternate_screen(&mut self) -> TerminalResult<()>; + + /// Leave the alternate screen using the terminal adapter + fn leave_alternate_screen(&mut self) -> TerminalResult<()>; +} diff --git a/src/terminal/adapter/crossterm.rs b/src/terminal/adapter/crossterm.rs new file mode 100644 index 0000000..4012fc0 --- /dev/null +++ b/src/terminal/adapter/crossterm.rs @@ -0,0 +1,78 @@ +use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; +use crossterm::execute; +use crossterm::terminal::{ + disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, +}; +use ratatui::Terminal; + +use super::{TerminalAdapter, TerminalResult}; +use crate::ratatui::backend::CrosstermBackend; +use crate::terminal::TerminalError; + +/// CrosstermTerminalAdapter is the adapter for the [`crossterm`] terminal +/// +/// It implements the [`TerminalAdapter`] trait +pub struct CrosstermTerminalAdapter { + terminal: Terminal>, +} + +impl CrosstermTerminalAdapter { + /// Create a new instance of the CrosstermTerminalAdapter + pub fn new() -> TerminalResult { + let backend = CrosstermBackend::new(std::io::stdout()); + let terminal = Terminal::new(backend).map_err(|_| TerminalError::CannotConnectStdout)?; + + Ok(Self { terminal }) + } + + pub fn raw(&self) -> &Terminal> { + &self.terminal + } + + pub fn raw_mut(&mut self) -> &mut Terminal> { + &mut self.terminal + } +} + +impl TerminalAdapter for CrosstermTerminalAdapter { + fn draw(&mut self, render_callback: F) -> TerminalResult + where + F: FnOnce(&mut ratatui::Frame<'_>), + { + self.raw_mut() + .draw(render_callback) + .map_err(|_| TerminalError::CannotDrawFrame) + } + + fn clear_screen(&mut self) -> TerminalResult<()> { + self.terminal + .clear() + .map_err(|_| TerminalError::CannotClear) + } + + fn enable_raw_mode(&mut self) -> TerminalResult<()> { + enable_raw_mode().map_err(|_| TerminalError::CannotToggleRawMode) + } + + fn disable_raw_mode(&mut self) -> TerminalResult<()> { + disable_raw_mode().map_err(|_| TerminalError::CannotToggleRawMode) + } + + fn enter_alternate_screen(&mut self) -> TerminalResult<()> { + execute!( + self.terminal.backend_mut(), + EnterAlternateScreen, + EnableMouseCapture + ) + .map_err(|_| TerminalError::CannotEnterAlternateMode) + } + + fn leave_alternate_screen(&mut self) -> TerminalResult<()> { + execute!( + self.terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + ) + .map_err(|_| TerminalError::CannotLeaveAlternateMode) + } +} diff --git a/src/terminal/adapter/termion.rs b/src/terminal/adapter/termion.rs new file mode 100644 index 0000000..1e49547 --- /dev/null +++ b/src/terminal/adapter/termion.rs @@ -0,0 +1,77 @@ +use std::io::Stdout; + +use ratatui::prelude::TermionBackend as TermionLibBackend; +use ratatui::Terminal; +use termion::input::MouseTerminal; +use termion::raw::{IntoRawMode as _, RawTerminal}; +use termion::screen::{AlternateScreen, IntoAlternateScreen as _}; + +use super::{TerminalAdapter, TerminalResult}; +use crate::terminal::TerminalError; + +pub type TermionBackend = + Terminal>>>>; + +/// TermionTerminalAdapter is the adapter for the [`termion`] terminal +/// +/// It implements the [`TerminalAdapter`] trait +pub struct TermionTerminalAdapter { + terminal: TermionBackend, +} + +impl TermionTerminalAdapter { + pub fn new() -> TerminalResult { + let stdout = std::io::stdout() + .into_raw_mode() + .map_err(|_| TerminalError::CannotConnectStdout)? + .into_alternate_screen() + .map_err(|_| TerminalError::CannotConnectStdout)?; + let stdout = MouseTerminal::from(stdout); + + let terminal = Terminal::new(TermionLibBackend::new(stdout)) + .map_err(|_| TerminalError::CannotConnectStdout)?; + + Ok(Self { terminal }) + } + + pub fn raw(&self) -> &TermionBackend { + &self.terminal + } + + pub fn raw_mut(&mut self) -> &mut TermionBackend { + &mut self.terminal + } +} + +impl TerminalAdapter for TermionTerminalAdapter { + fn draw(&mut self, render_callback: F) -> TerminalResult + where + F: FnOnce(&mut ratatui::Frame<'_>), + { + self.raw_mut() + .draw(render_callback) + .map_err(|_| TerminalError::CannotDrawFrame) + } + + fn clear_screen(&mut self) -> TerminalResult<()> { + self.terminal + .clear() + .map_err(|_| TerminalError::CannotClear) + } + + fn disable_raw_mode(&mut self) -> TerminalResult<()> { + Err(TerminalError::Unsupported) + } + + fn enable_raw_mode(&mut self) -> TerminalResult<()> { + Err(TerminalError::Unsupported) + } + + fn enter_alternate_screen(&mut self) -> TerminalResult<()> { + Err(TerminalError::Unsupported) + } + + fn leave_alternate_screen(&mut self) -> TerminalResult<()> { + Err(TerminalError::Unsupported) + } +} diff --git a/src/terminal/event_listener.rs b/src/terminal/event_listener.rs new file mode 100644 index 0000000..f7fa3d1 --- /dev/null +++ b/src/terminal/event_listener.rs @@ -0,0 +1,11 @@ +#[cfg(feature = "crossterm")] +mod crossterm; +#[cfg(feature = "termion")] +mod termion; + +#[cfg(feature = "crossterm")] +pub use crossterm::CrosstermInputListener; +#[cfg(feature = "termion")] +pub use termion::TermionInputListener; + +use crate::Event; diff --git a/src/adapter/crossterm/event.rs b/src/terminal/event_listener/crossterm.rs similarity index 85% rename from src/adapter/crossterm/event.rs rename to src/terminal/event_listener/crossterm.rs index 2f80f57..b8b6f0e 100644 --- a/src/adapter/crossterm/event.rs +++ b/src/terminal/event_listener/crossterm.rs @@ -1,14 +1,55 @@ -//! ## Event -//! -//! event adapter for crossterm +use std::marker::PhantomData; +use std::time::Duration; use crossterm::event::{ - Event as XtermEvent, KeyCode as XtermKeyCode, KeyEvent as XtermKeyEvent, + self as xterm, Event as XtermEvent, KeyCode as XtermKeyCode, KeyEvent as XtermKeyEvent, KeyEventKind as XtermEventKind, KeyModifiers as XtermKeyModifiers, MediaKeyCode as XtermMediaKeyCode, }; -use super::{Event, Key, KeyEvent, KeyModifiers, MediaKeyCode}; +use super::Event; +use crate::event::{Key, KeyEvent, KeyModifiers, MediaKeyCode}; +use crate::listener::{ListenerResult, Poll}; +use crate::ListenerError; + +/// The input listener for crossterm. +/// If crossterm is enabled, this will already be exported as `InputEventListener` in the `adapter` module +/// or you can use it directly in the event listener, calling `default_input_listener()` in the `EventListenerCfg` +#[doc(alias = "InputEventListener")] +pub struct CrosstermInputListener +where + U: Eq + PartialEq + Clone + PartialOrd + Send, +{ + ghost: PhantomData, + interval: Duration, +} + +impl CrosstermInputListener +where + U: Eq + PartialEq + Clone + PartialOrd + Send, +{ + pub fn new(interval: Duration) -> Self { + Self { + ghost: PhantomData, + interval: interval / 2, + } + } +} + +impl Poll for CrosstermInputListener +where + U: Eq + PartialEq + Clone + PartialOrd + Send + 'static, +{ + fn poll(&mut self) -> ListenerResult>> { + match xterm::poll(self.interval) { + Ok(true) => xterm::read() + .map(|x| Some(Event::from(x))) + .map_err(|_| ListenerError::PollFailed), + Ok(false) => Ok(None), + Err(_) => Err(ListenerError::PollFailed), + } + } +} impl From for Event where @@ -112,6 +153,7 @@ mod test { use pretty_assertions::assert_eq; use super::*; + use crate::event::{Key, MediaKeyCode}; use crate::mock::MockEvent; #[test] diff --git a/src/adapter/termion/event.rs b/src/terminal/event_listener/termion.rs similarity index 74% rename from src/adapter/termion/event.rs rename to src/terminal/event_listener/termion.rs index ed4edcf..5821d09 100644 --- a/src/adapter/termion/event.rs +++ b/src/terminal/event_listener/termion.rs @@ -1,10 +1,44 @@ -//! ## Event -//! -//! event adapter for termion +use std::marker::PhantomData; +use std::time::Duration; use termion::event::{Event as TonEvent, Key as TonKey}; +use termion::input::TermRead as _; -use super::{Event, Key, KeyEvent, KeyModifiers}; +use super::Event; +use crate::event::{Key, KeyEvent, KeyModifiers}; +use crate::listener::{ListenerResult, Poll}; +use crate::ListenerError; + +/// The input listener for [`termion`]. +#[doc(alias = "InputEventListener")] +pub struct TermionInputListener +where + U: Eq + PartialEq + Clone + PartialOrd + Send, +{ + ghost: PhantomData, +} + +impl TermionInputListener +where + U: Eq + PartialEq + Clone + PartialOrd + Send, +{ + pub fn new(_interval: Duration) -> Self { + Self { ghost: PhantomData } + } +} + +impl Poll for TermionInputListener +where + U: Eq + PartialEq + Clone + PartialOrd + Send + 'static, +{ + fn poll(&mut self) -> ListenerResult>> { + match std::io::stdin().events().next() { + Some(Ok(ev)) => Ok(Some(Event::from(ev))), + Some(Err(_)) => Err(ListenerError::PollFailed), + None => Ok(None), + } + } +} impl From for Event where @@ -49,6 +83,20 @@ impl From for KeyEvent { TonKey::Null => Key::Null, TonKey::Esc => Key::Esc, TonKey::__IsNotComplete => Key::Null, + TonKey::ShiftLeft => Key::ShiftLeft, + TonKey::AltLeft => Key::AltLeft, + TonKey::CtrlLeft => Key::CtrlLeft, + TonKey::ShiftRight => Key::ShiftRight, + TonKey::AltRight => Key::AltRight, + TonKey::CtrlRight => Key::CtrlRight, + TonKey::ShiftUp => Key::ShiftUp, + TonKey::AltUp => Key::AltUp, + TonKey::CtrlUp => Key::CtrlUp, + TonKey::ShiftDown => Key::ShiftDown, + TonKey::AltDown => Key::AltDown, + TonKey::CtrlDown => Key::CtrlDown, + TonKey::CtrlHome => Key::CtrlHome, + TonKey::CtrlEnd => Key::CtrlEnd, }; Self { code, modifiers } } From 25aba53ec10494baff59cce8bd69b5b54623fd3d Mon Sep 17 00:00:00 2001 From: veeso Date: Sun, 13 Oct 2024 12:46:44 +0200 Subject: [PATCH 04/18] feat!: `CmdResult::Custom(&'static str)` changed to `CmdResult::Custom(&'static str, State)` --- CHANGELOG.md | 1 + src/core/command.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a33dd2..f1c6c4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ Released on ??/10/2024 - `restore`: Restore the terminal to its original state - `set_panic_hook`: Sets a panic hook that restores the terminal before panicking. - Added `draw` to `TerminalBridge` +- `CmdResult::Custom(&'static str)` changed to `CmdResult::Custom(&'static str, State)` - Removed `InputListener`. Now use `CrosstermInputListener` or `TermionInputListener`. - Bump `ratatui` version to `0.28` diff --git a/src/core/command.rs b/src/core/command.rs index 4421445..9a08896 100644 --- a/src/core/command.rs +++ b/src/core/command.rs @@ -75,7 +75,7 @@ pub enum CmdResult { /// The command could not be applied. Useful to report errors Invalid(Cmd), /// Custom cmd result - Custom(&'static str), + Custom(&'static str, State), /// An array of Command result Batch(Vec), /// No result to report From a035b7a29d954c5dd2ad4d15a7aaf1785a0c33b9 Mon Sep 17 00:00:00 2001 From: veeso Date: Sun, 13 Oct 2024 12:52:02 +0200 Subject: [PATCH 05/18] fix: docs --- src/core/application.rs | 24 ++++++++++++------------ src/core/command.rs | 4 ++-- src/core/component.rs | 14 +++++++------- src/core/injector.rs | 25 +------------------------ 4 files changed, 22 insertions(+), 45 deletions(-) diff --git a/src/core/application.rs b/src/core/application.rs index 7314fe3..b0f1477 100644 --- a/src/core/application.rs +++ b/src/core/application.rs @@ -13,15 +13,15 @@ use crate::listener::{EventListener, EventListenerCfg, ListenerError}; use crate::ratatui::layout::Rect; use crate::{AttrValue, Attribute, Event, Injector, State, Sub, SubEventClause, ViewError}; -/// Result retuned by `Application`. +/// Result retuned by [`Application`]. /// Ok depends on method -/// Err is always `ApplicationError` +/// Err is always [`ApplicationError`] pub type ApplicationResult = Result; /// The application defines a tui-realm application. /// It will handle events, subscriptions and the view too. /// It provides functions to interact with the view (mount, umount, query, etc), but also -/// the main function: `tick()`. See [tick](#tick) +/// the main function: [`Application::tick`]. pub struct Application where ComponentId: Eq + PartialEq + Clone + Hash, @@ -41,7 +41,7 @@ where Msg: PartialEq, UserEvent: Eq + PartialEq + Clone + PartialOrd + Send + 'static, { - /// Initialize a new `Application`. + /// Initialize a new [`Application`]. /// The event listener is immediately created and started. pub fn init(listener_cfg: EventListenerCfg) -> Self { Self { @@ -54,7 +54,7 @@ where /// Restart listener in case the previous listener has died or if you want to start a new one with a new configuration. /// - /// > The listener has died if you received a `ApplicationError::Listener(ListenerError::ListenerDied))` + /// > The listener has died if you received a [`ApplicationError::Listener(ListenerError::ListenerDied))`] pub fn restart_listener( &mut self, listener_cfg: EventListenerCfg, @@ -78,14 +78,14 @@ where /// The tick method makes the application to run once. /// The workflow of the tick method is the following one: /// - /// 1. The event listener is fetched according to the provided `PollStrategy` + /// 1. The event listener is fetched according to the provided [`PollStrategy`] /// 2. All the received events are sent to the current active component /// 3. All the received events are forwarded to the subscribed components which satisfy the received events and conditions. /// 4. Returns messages to process /// - /// As soon as function returns, you should call the `view()` method. + /// As soon as function returns, you should call the [`Application::view`] method. /// - /// > You can also call `view` from the `update()` if you need it + /// > You can also call [`Application::view`] from the [`crate::Update`] if you need it pub fn tick(&mut self, strategy: PollStrategy) -> ApplicationResult> { // Poll event listener let events = self.poll(strategy)?; @@ -355,19 +355,19 @@ where } } -/// Poll strategy defines how to call `poll` on the event listener. +/// Poll strategy defines how to call [`Application::poll`] on the event listener. pub enum PollStrategy { - /// The poll() function will be called once + /// [`Application::poll`] function will be called once Once, /// The application will keep waiting for events for the provided duration TryFor(Duration), - /// The poll() function will be called up to `n` times, until it will return `None`. + /// [`Application::poll`] function will be called up to `n` times, until it will return [`Option::None`]. UpTo(usize), } // -- error -/// Error variants returned by `Application` +/// Error variants returned by [`Application`] #[derive(Debug, Error)] pub enum ApplicationError { #[error("already subscribed")] diff --git a/src/core/command.rs b/src/core/command.rs index 9a08896..2ebcc83 100644 --- a/src/core/command.rs +++ b/src/core/command.rs @@ -61,9 +61,9 @@ pub enum Position { // -- Command result -/// A command result describes the output of a `Cmd` performed on a Component. +/// A command result describes the output of a [`Cmd`] performed on a Component. /// It reports a "logical" change on the `MockComponent`. -/// The `Component` then, must return a certain user defined `Msg` based on the value of the `CmdResult`. +/// The `Component` then, must return a certain user defined `Msg` based on the value of the [`CmdResult`]. #[derive(Debug, PartialEq, Clone)] #[allow(clippy::large_enum_variant)] pub enum CmdResult { diff --git a/src/core/component.rs b/src/core/component.rs index cea8322..883ea8c 100644 --- a/src/core/component.rs +++ b/src/core/component.rs @@ -9,8 +9,8 @@ use crate::ratatui::layout::Rect; use crate::{AttrValue, Attribute, Event, State}; /// A Mock Component represents a component which defines all the properties and states it can handle and represent -/// and the way it should be rendered. It must also define how to behave in case of a `Cmd` (command). -/// Despite that, it won't define how to behave after an `Event` and it won't send any `Msg`. +/// and the way it should be rendered. It must also define how to behave in case of a [`Cmd`] (command). +/// Despite that, it won't define how to behave after an [`Event`] and it won't send any `Msg`. /// The MockComponent is intended to be used as a reusable component to implement your application component. /// /// ### In practice @@ -19,7 +19,7 @@ use crate::{AttrValue, Attribute, Event, State}; /// The mock component is represented by the `Input`, which will define the properties (e.g. max input length, input type, ...) /// and by its behaviour (e.g. when the user types 'a', 'a' char is added to input state). /// -/// In your application though, you may use a `IpAddressInput` which is the `Component` using the `Input` mock component. +/// In your application though, you may use a `IpAddressInput` which is the [`Component`] using the `Input` mock component. /// If you want more example, just dive into the `examples/` folder in the project root. pub trait MockComponent { /// Based on the current properties and states, renders the component in the provided area frame. @@ -42,12 +42,12 @@ pub trait MockComponent { fn perform(&mut self, cmd: Cmd) -> CmdResult; } -/// The component describes the application level component, which is a wrapper around the `MockComponent`, +/// The component describes the application level component, which is a wrapper around the [`MockComponent`], /// which, in addition to all the methods exposed by the mock, it will handle the event coming from the `View`. /// The Event are passed to the `on` method, which will eventually return a `Msg`, -/// which is defined in your application as an enum. (Don't forget to derive `PartialEq` for your enum). +/// which is defined in your application as an enum. (Don't forget to derive [`PartialEq`] for your enum). /// In your application you should have a Component for each element on your UI, but the logic to implement -/// is very tiny, since the most of the work should already be done into the `MockComponent` +/// is very tiny, since the most of the work should already be done into the [`MockComponent`] /// and many of them are available in the standard library at . /// /// Don't forget you can find an example in the `examples/` directory and you can discover many more information @@ -59,6 +59,6 @@ where { /// Handle input event and update internal states. /// Returns a Msg to the view. - /// If `None` is returned it means there's no message to return for the provided event. + /// If [`None`] is returned it means there's no message to return for the provided event. fn on(&mut self, ev: Event) -> Option; } diff --git a/src/core/injector.rs b/src/core/injector.rs index f8730db..48c940e 100644 --- a/src/core/injector.rs +++ b/src/core/injector.rs @@ -4,34 +4,11 @@ use std::hash::Hash; -/** - * MIT License - * - * tui-realm - Copyright (C) 2022 Christian Visintin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ use super::props::{AttrValue, Attribute}; /// An injector is a trait object which can provide properties to inject to a certain component. /// The injector is called each time a component is mounted, providing the id of the mounted -/// component and may return a list of (`Attribute`, `AttrValue`) to inject. +/// component and may return a list of ([`Attribute`], [`AttrValue`]) to inject. pub trait Injector where ComponentId: Eq + PartialEq + Clone + Hash, From 07fa8f0ea386d89e6ce7f1168e174dca1ebe0a92 Mon Sep 17 00:00:00 2001 From: veeso Date: Sun, 13 Oct 2024 15:43:53 +0200 Subject: [PATCH 06/18] feat: Added new `subclause_and!(Id::Foo, Id::Bar, Id::Baz)` and `subclause_or!(Id::Foo, Id::Bar, Id::Baz)` macros. --- CHANGELOG.md | 1 + src/lib.rs | 1 + src/macros.rs | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 src/macros.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f1c6c4b..8553c75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ Released on ??/10/2024 - `set_panic_hook`: Sets a panic hook that restores the terminal before panicking. - Added `draw` to `TerminalBridge` - `CmdResult::Custom(&'static str)` changed to `CmdResult::Custom(&'static str, State)` +- Added new `subclause_and!(Id::Foo, Id::Bar, Id::Baz)` and `subclause_or!(Id::Foo, Id::Bar, Id::Baz)` macros. - Removed `InputListener`. Now use `CrosstermInputListener` or `TermionInputListener`. - Bump `ratatui` version to `0.28` diff --git a/src/lib.rs b/src/lib.rs index 734613a..2333160 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,6 +55,7 @@ extern crate tuirealm_derive; mod core; pub mod listener; +pub mod macros; #[cfg(test)] pub mod mock; pub mod ratatui; diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..3afbf9b --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,91 @@ +/// A macro to generate a chain of [`crate::SubClause::And`] from a list of +/// Ids with the case [`crate::SubClause::IsMounted`] for every id. +/// +/// ### example +/// +/// ```rust +/// use tuirealm::{SubClause, subclause_and}; +/// +/// #[derive(Debug, Eq, PartialEq, Clone, Hash)] +/// pub enum Id { +/// InputBar, +/// InputFoo, +/// InputOmar, +/// } +/// +/// let sub_clause = subclause_and!( +/// Id::InputBar, +/// Id::InputFoo, +/// Id::InputOmar +/// ); +/// +/// assert_eq!( +/// sub_clause, +/// SubClause::And( +/// Box::new(SubClause::IsMounted(Id::InputBar)), +/// Box::new(SubClause::And( +/// Box::new(SubClause::IsMounted(Id::InputFoo)), +/// Box::new(SubClause::IsMounted(Id::InputOmar)) +/// )) +/// ) +/// ); +/// ``` +/// +#[macro_export] +macro_rules! subclause_and { + ($id:expr) => { + SubClause::IsMounted($id) + }; + ($id:expr, $($rest:expr),+) => { + SubClause::And( + Box::new(SubClause::IsMounted($id)), + Box::new(subclause_and!($($rest),+)) + ) + }; +} + +/// A macro to generate a chain of [`crate::SubClause::Or`] from a list of +/// Ids with the case [`crate::SubClause::IsMounted`] for every id. +/// +/// ### example +/// +/// ```rust +/// use tuirealm::{SubClause, subclause_or}; +/// +/// #[derive(Debug, Eq, PartialEq, Clone, Hash)] +/// pub enum Id { +/// InputBar, +/// InputFoo, +/// InputOmar, +/// } +/// +/// let sub_clause = subclause_or!( +/// Id::InputBar, +/// Id::InputFoo, +/// Id::InputOmar +/// ); +/// +/// assert_eq!( +/// sub_clause, +/// SubClause::Or( +/// Box::new(SubClause::IsMounted(Id::InputBar)), +/// Box::new(SubClause::Or( +/// Box::new(SubClause::IsMounted(Id::InputFoo)), +/// Box::new(SubClause::IsMounted(Id::InputOmar)) +/// )) +/// ) +/// ); +/// ``` +/// +#[macro_export] +macro_rules! subclause_or { + ($id:expr) => { + SubClause::IsMounted($id) + }; + ($id:expr, $($rest:expr),+) => { + SubClause::Or( + Box::new(SubClause::IsMounted($id)), + Box::new(subclause_or!($($rest),+)) + ) + }; +} From d693c0c411db02a99d2e986046605d6e29a7bad4 Mon Sep 17 00:00:00 2001 From: veeso Date: Sun, 13 Oct 2024 15:48:15 +0200 Subject: [PATCH 07/18] fix: docs --- src/core/subscription.rs | 18 +++++++++--------- src/macros.rs | 24 ++++++++++++------------ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/core/subscription.rs b/src/core/subscription.rs index c43113d..0f77761 100644 --- a/src/core/subscription.rs +++ b/src/core/subscription.rs @@ -55,7 +55,7 @@ where K: Eq + PartialEq + Clone + Hash, U: Eq + PartialEq + Clone + PartialOrd + Send, { - /// Instantiates a new `Subscription` + /// Instantiates a new [`Subscription`] pub fn new(target: K, sub: Sub) -> Self { Self { target, @@ -107,7 +107,7 @@ where /// The event will be forwarded on a tick Tick, /// Event will be forwarded on this specific user event. - /// The way user event is matched, depends on its partialEq implementation + /// The way user event is matched, depends on its [`PartialEq`] implementation User(UserEvent), } @@ -124,7 +124,7 @@ where /// - WindowResize: matches only event type, not sizes /// - Tick: matches tick event /// - None: matches None event - /// - UserEvent: depends on UserEvent PartialEq + /// - UserEvent: depends on UserEvent [`PartialEq`] fn forward(&self, ev: &Event) -> bool { match self { EventClause::Any => true, @@ -139,9 +139,9 @@ where /// A subclause indicates the condition that must be satisfied in order to forward `ev` to `target`. /// Usually clauses are single conditions, but there are also some special condition, to create "ligatures", which are: /// -/// - `Not(SubClause)`: Negates inner condition -/// - `And(SubClause, SubClause)`: the AND of the two clauses must be `true` -/// - `Or(SubClause, SubClause)`: the OR of the two clauses must be `true` +/// - [`SubClause::Not`]: Negates inner condition +/// - [`SubClause::And`]: the AND of the two clauses must be `true` +/// - [`SubClause::Or`]: the OR of the two clauses must be `true` #[derive(Debug, PartialEq)] #[allow(clippy::large_enum_variant)] pub enum SubClause @@ -169,18 +169,18 @@ impl SubClause where Id: Eq + PartialEq + Clone + Hash, { - /// Shortcut for `SubClause::Not` without specifying `Box::new(...)` + /// Shortcut for [`SubClause::Not`] without specifying `Box::new(...)` #[allow(clippy::should_implement_trait)] pub fn not(clause: Self) -> Self { Self::Not(Box::new(clause)) } - /// Shortcut for `SubClause::And` without specifying `Box::new(...)` + /// Shortcut for [`SubClause::And`] without specifying `Box::new(...)` pub fn and(a: Self, b: Self) -> Self { Self::And(Box::new(a), Box::new(b)) } - /// Shortcut for `SubClause::Or` without specifying `Box::new(...)` + /// Shortcut for [`SubClause::Or`] without specifying `Box::new(...)` pub fn or(a: Self, b: Self) -> Self { Self::Or(Box::new(a), Box::new(b)) } diff --git a/src/macros.rs b/src/macros.rs index 3afbf9b..82dd756 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -37,9 +37,9 @@ macro_rules! subclause_and { SubClause::IsMounted($id) }; ($id:expr, $($rest:expr),+) => { - SubClause::And( - Box::new(SubClause::IsMounted($id)), - Box::new(subclause_and!($($rest),+)) + SubClause::and( + SubClause::IsMounted($id), + subclause_and!($($rest),+) ) }; } @@ -67,12 +67,12 @@ macro_rules! subclause_and { /// /// assert_eq!( /// sub_clause, -/// SubClause::Or( -/// Box::new(SubClause::IsMounted(Id::InputBar)), -/// Box::new(SubClause::Or( -/// Box::new(SubClause::IsMounted(Id::InputFoo)), -/// Box::new(SubClause::IsMounted(Id::InputOmar)) -/// )) +/// SubClause::or( +/// SubClause::IsMounted(Id::InputBar), +/// SubClause::or( +/// SubClause::IsMounted(Id::InputFoo), +/// SubClause::IsMounted(Id::InputOmar) +/// ) /// ) /// ); /// ``` @@ -83,9 +83,9 @@ macro_rules! subclause_or { SubClause::IsMounted($id) }; ($id:expr, $($rest:expr),+) => { - SubClause::Or( - Box::new(SubClause::IsMounted($id)), - Box::new(subclause_or!($($rest),+)) + SubClause::or( + SubClause::IsMounted($id), + subclause_or!($($rest),+) ) }; } From 55ca4989b98d4872060ace2877ad6a3be112e58d Mon Sep 17 00:00:00 2001 From: veeso Date: Sun, 13 Oct 2024 15:50:04 +0200 Subject: [PATCH 08/18] fix: docs --- src/listener/builder.rs | 2 +- src/listener/mod.rs | 4 ++-- src/listener/port.rs | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/listener/builder.rs b/src/listener/builder.rs index 84ee5ff..5ebb8a8 100644 --- a/src/listener/builder.rs +++ b/src/listener/builder.rs @@ -5,7 +5,7 @@ use super::{Duration, EventListener, Poll, Port}; /// The event listener configurator is used to setup an event listener. -/// Once you're done with configuration just call `start()` and the event listener will start and the listener +/// Once you're done with configuration just call [`EventListenerCfg::start`] and the event listener will start and the listener /// will be returned. pub struct EventListenerCfg where diff --git a/src/listener/mod.rs b/src/listener/mod.rs index e356eeb..bd51214 100644 --- a/src/listener/mod.rs +++ b/src/listener/mod.rs @@ -21,7 +21,7 @@ use worker::EventListenerWorker; // -- internal use super::Event; -/// Result returned by `EventListener`. Ok value depends on the method, while the +/// Result returned by [`EventListener`]. Ok value depends on the method, while the /// Err value is always [`ListenerError`]. pub type ListenerResult = Result; @@ -37,7 +37,7 @@ pub enum ListenerError { PollFailed, } -/// The poll trait defines the function [`poll`], which will be called by the event listener +/// The poll trait defines the function [`Poll::poll`], which will be called by the event listener /// dedicated thread to poll for events. pub trait Poll: Send where diff --git a/src/listener/port.rs b/src/listener/port.rs index ec22844..2b8f1bd 100644 --- a/src/listener/port.rs +++ b/src/listener/port.rs @@ -8,7 +8,7 @@ use std::time::{Duration, Instant}; use super::{Event, ListenerResult, Poll}; /// A port is a wrapper around the poll trait object, which also defines an interval, which defines -/// the amount of time between each poll() call. +/// the amount of time between each [`Poll::poll`] call. /// Its purpose is to listen for incoming events of a user-defined type pub struct Port where @@ -32,7 +32,7 @@ where } } - /// Returns the interval for the current `Port` + /// Returns the interval for the current [`Port`] pub fn interval(&self) -> &Duration { &self.interval } @@ -47,7 +47,7 @@ where self.next_poll <= Instant::now() } - /// Calls poll on the inner `Poll` trait object. + /// Calls [`Poll::poll`] on the inner [`Poll`] trait object. pub fn poll(&mut self) -> ListenerResult>> { self.poll.poll() } From bf24a02418616b56846d71781c9632bcc56f2749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=B8=83=E4=B8=81?= Date: Mon, 14 Oct 2024 00:21:33 +0800 Subject: [PATCH 09/18] Update README.md demo file link (#81) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f9e3f4a..db0f606 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ View how to implement a tui-realm application in the [related guide](/docs/en/ge Still confused about how tui-realm works? Don't worry, try with the examples: -- [demo](/examples/demo.rs): a simple application which shows how tui-realm works +- [demo](/examples/demo/demo.rs): a simple application which shows how tui-realm works ```sh cargo run --example demo From f090dfe5129fe9e5f0675e28dbbe47786ddfbac3 Mon Sep 17 00:00:00 2001 From: veeso Date: Sun, 13 Oct 2024 18:27:24 +0200 Subject: [PATCH 10/18] fix: ci --- .github/workflows/crossterm-windows.yml | 2 +- .github/workflows/ratatui_crossterm.yml | 2 +- .github/workflows/ratatui_termion.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/crossterm-windows.yml b/.github/workflows/crossterm-windows.yml index 04f6a58..c72804f 100644 --- a/.github/workflows/crossterm-windows.yml +++ b/.github/workflows/crossterm-windows.yml @@ -17,7 +17,7 @@ jobs: override: true components: rustfmt, clippy - name: Test - run: cargo test --no-fail-fast --no-default-features --features derive,serialize,crossterm + run: cargo test --no-fail-fast --lib --no-default-features --features derive,serialize,crossterm - name: Examples run: cargo build --all-targets --examples - name: Format diff --git a/.github/workflows/ratatui_crossterm.yml b/.github/workflows/ratatui_crossterm.yml index ace8b17..cadd4f5 100644 --- a/.github/workflows/ratatui_crossterm.yml +++ b/.github/workflows/ratatui_crossterm.yml @@ -17,7 +17,7 @@ jobs: override: true components: rustfmt, clippy - name: Test - run: cargo test --no-fail-fast --features derive,serialize,crossterm --no-default-features + run: cargo test --no-fail-fast --lib --features derive,serialize,crossterm --no-default-features - name: Examples run: cargo build --all-targets --examples - name: Format diff --git a/.github/workflows/ratatui_termion.yml b/.github/workflows/ratatui_termion.yml index 038ea63..b5591c1 100644 --- a/.github/workflows/ratatui_termion.yml +++ b/.github/workflows/ratatui_termion.yml @@ -16,7 +16,7 @@ jobs: toolchain: stable components: rustfmt, clippy - name: Test - run: cargo test --no-fail-fast --no-default-features --features derive,serialize,termion + run: cargo test --no-fail-fast --lib --no-default-features --features derive,serialize,termion - name: Examples run: cargo build --all-targets --no-default-features --features derive,serialize,termion --examples - name: Format From 45bead0112db9c88c39fd1cc4134514cfe75541a Mon Sep 17 00:00:00 2001 From: veeso Date: Sun, 13 Oct 2024 18:29:45 +0200 Subject: [PATCH 11/18] fix: ci --- README.md | 10 +++++----- src/terminal.rs | 9 ++++++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index db0f606..4e12ab8 100644 --- a/README.md +++ b/README.md @@ -149,18 +149,18 @@ level terminal TUI libraries: `tui` and `ratatui`. Whenever you explicitly declare any of the TUI library or backend feature sets you should disable the crate's default features. -> ❗ You can never have more than one backend and one UI library enabled at the same time +> ❗ The two features can co-exist, even if it doesn't make too much sense. -Example using the termion backend: +Example using crossterm: ```toml -tuirealm = { version = "^1.9.0", default-features = false, features = [ "termion", "derive" ] } +tuirealm = { version = "^1.9.0", default-features = false, features = [ "derive", "crossterm" ]} ``` -Example using crossterm: +Example using the termion backend: ```toml -tuirealm = { version = "^1.9.0", default-features = false, features = [ "derive", "crossterm" ]} +tuirealm = { version = "^1.9.0", default-features = false, features = [ "derive", "termion" ] } ``` ### Create a tui-realm application 🪂 diff --git a/src/terminal.rs b/src/terminal.rs index ec867f5..5c72ae9 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -5,7 +5,7 @@ mod adapter; mod event_listener; -use ratatui::{CompletedFrame, Frame, Terminal}; +use ratatui::{CompletedFrame, Frame}; use thiserror::Error; #[cfg(feature = "crossterm")] @@ -187,14 +187,17 @@ impl TerminalBridge { } /// Returns a reference to the underlying [`Terminal`] - pub fn raw(&self) -> &Terminal> { + pub fn raw( + &self, + ) -> &crate::ratatui::Terminal> { self.terminal.raw() } /// Returns a mutable reference the underlying [`Terminal`] pub fn raw_mut( &mut self, - ) -> &mut Terminal> { + ) -> &mut crate::ratatui::Terminal> + { self.terminal.raw_mut() } } From 3c96b35ea412d233569f87ec6352e9936af7d429 Mon Sep 17 00:00:00 2001 From: veeso Date: Sun, 13 Oct 2024 18:34:30 +0200 Subject: [PATCH 12/18] fix: docs --- CHANGELOG.md | 2 +- Cargo.toml | 4 ++++ README.md | 6 +++--- src/lib.rs | 29 ++++++++++++++++++++++------- src/terminal.rs | 4 ++++ 5 files changed, 34 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8553c75..9369adb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,7 @@ ## 2.0.0 -Released on ??/10/2024 +Released on 13/10/2024 - Dropped support for `tui-rs`. Tui-rs was deprecated a long time ago, so it doesn't really makes sense to keep supporting it. - Added new methods for `TerminalBridge` diff --git a/Cargo.toml b/Cargo.toml index ffc4627..fb1d9ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,3 +44,7 @@ required-features = ["crossterm"] name = "user-events" path = "examples/user_events/user_events.rs" required-features = ["crossterm"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] diff --git a/README.md b/README.md index 4e12ab8..cacd669 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@

Developed by @veeso

-

Current version: 2.0.0 (12/10/2024)

+

Current version: 2.0.0 (13/10/2024)

Date: Sun, 13 Oct 2024 18:37:32 +0200 Subject: [PATCH 13/18] Add function to manually control mouse-capture (#80) * feat(terminal): dont enable "MouseCapture" by default * feat(terminal): add function "enable_mouse_capture" and "disable_mouse_capture" --------- Co-authored-by: veeso --- CHANGELOG.md | 2 ++ src/terminal.rs | 12 ++++++++++++ src/terminal/adapter.rs | 6 ++++++ src/terminal/adapter/crossterm.rs | 10 ++++++++++ src/terminal/adapter/termion.rs | 8 ++++++++ 5 files changed, 38 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9369adb..b6f42d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,8 @@ Released on 13/10/2024 - Added new `subclause_and!(Id::Foo, Id::Bar, Id::Baz)` and `subclause_or!(Id::Foo, Id::Bar, Id::Baz)` macros. - Removed `InputListener`. Now use `CrosstermInputListener` or `TermionInputListener`. - Bump `ratatui` version to `0.28` +- Dont enable `MouseCapture` by default +- Add function `enable_mouse_capture` and `disable_mouse_capture` to `TerminalBridge` ## 1.9.2 diff --git a/src/terminal.rs b/src/terminal.rs index 95d6feb..4b9ef28 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -41,6 +41,8 @@ pub enum TerminalError { CannotClear, #[error("backend doesn't support this command")] Unsupported, + #[error("cannot activate / deactivate mouse capture")] + CannotToggleMouseCapture, } /// An helper around [`Terminal`] to quickly setup and perform on terminal. @@ -148,6 +150,16 @@ where self.terminal.disable_raw_mode() } + /// Enable mouse-event capture, if the backend supports it + pub fn enable_mouse_capture(&mut self) -> TerminalResult<()> { + self.terminal.enable_mouse_capture() + } + + /// Disable mouse-event capture, if the backend supports it + pub fn disable_mouse_capture(&mut self) -> TerminalResult<()> { + self.terminal.disable_mouse_capture() + } + /// Draws a single frame to the terminal. /// /// Returns a [`CompletedFrame`] if successful, otherwise a [`TerminalError`]. diff --git a/src/terminal/adapter.rs b/src/terminal/adapter.rs index 87db013..131b3d3 100644 --- a/src/terminal/adapter.rs +++ b/src/terminal/adapter.rs @@ -54,4 +54,10 @@ pub trait TerminalAdapter { /// Leave the alternate screen using the terminal adapter fn leave_alternate_screen(&mut self) -> TerminalResult<()>; + + /// Enable mouse capture using the terminal adapter + fn enable_mouse_capture(&mut self) -> TerminalResult<()>; + + /// Disable mouse capture using the terminal adapter + fn disable_mouse_capture(&mut self) -> TerminalResult<()>; } diff --git a/src/terminal/adapter/crossterm.rs b/src/terminal/adapter/crossterm.rs index 4012fc0..26b1652 100644 --- a/src/terminal/adapter/crossterm.rs +++ b/src/terminal/adapter/crossterm.rs @@ -75,4 +75,14 @@ impl TerminalAdapter for CrosstermTerminalAdapter { ) .map_err(|_| TerminalError::CannotLeaveAlternateMode) } + + fn enable_mouse_capture(&mut self) -> TerminalResult<()> { + execute!(self.raw_mut().backend_mut(), EnableMouseCapture) + .map_err(|_| TerminalError::CannotToggleMouseCapture) + } + + fn disable_mouse_capture(&mut self) -> TerminalResult<()> { + execute!(self.raw_mut().backend_mut(), DisableMouseCapture) + .map_err(|_| TerminalError::CannotToggleMouseCapture) + } } diff --git a/src/terminal/adapter/termion.rs b/src/terminal/adapter/termion.rs index 1e49547..2357387 100644 --- a/src/terminal/adapter/termion.rs +++ b/src/terminal/adapter/termion.rs @@ -74,4 +74,12 @@ impl TerminalAdapter for TermionTerminalAdapter { fn leave_alternate_screen(&mut self) -> TerminalResult<()> { Err(TerminalError::Unsupported) } + + fn disable_mouse_capture(&mut self) -> TerminalResult<()> { + Err(TerminalError::Unsupported) + } + + fn enable_mouse_capture(&mut self) -> TerminalResult<()> { + Err(TerminalError::Unsupported) + } } From 3df4628e6d374dd7fe8c48334243dff359794289 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sun, 13 Oct 2024 18:46:24 +0200 Subject: [PATCH 14/18] feat(event): add event handling for mouse events (#79) crossterm only, as termion does not support mouse events fixes #64 Co-authored-by: veeso --- src/core/event.rs | 83 ++++++++++- src/core/subscription.rs | 93 ++++++++++++- src/terminal/event_listener/crossterm.rs | 168 ++++++++++++++++++++++- 3 files changed, 337 insertions(+), 7 deletions(-) diff --git a/src/core/event.rs b/src/core/event.rs index c3319a7..08fd569 100644 --- a/src/core/event.rs +++ b/src/core/event.rs @@ -16,6 +16,8 @@ where { /// A keyboard event Keyboard(KeyEvent), + /// A Mouse event + Mouse(MouseEvent), /// This event is raised after the terminal window is resized WindowResize(u16, u16), /// Window focus gained @@ -45,6 +47,14 @@ where } } + pub(crate) fn is_mouse(&self) -> Option<&MouseEvent> { + if let Event::Mouse(m) = self { + Some(m) + } else { + None + } + } + pub(crate) fn is_window_resize(&self) -> bool { matches!(self, Self::WindowResize(_, _)) } @@ -234,6 +244,66 @@ pub enum MediaKeyCode { MuteVolume, } +/// A keyboard event +#[derive(Debug, Eq, PartialEq, Copy, Clone, PartialOrd, Hash)] +#[cfg_attr( + feature = "serialize", + derive(Deserialize, Serialize), + serde(tag = "type") +)] +pub struct MouseEvent { + /// The kind of mouse event that was caused + pub kind: MouseEventKind, + /// The key modifiers active when the event occurred + pub modifiers: KeyModifiers, + /// The column that the event occurred on + pub column: u16, + /// The row that the event occurred on + pub row: u16, +} + +/// A Mouse event +#[derive(Debug, Eq, PartialEq, Copy, Clone, PartialOrd, Hash)] +#[cfg_attr( + feature = "serialize", + derive(Deserialize, Serialize), + serde(tag = "type", content = "args") +)] +pub enum MouseEventKind { + /// Pressed mouse button. Contains the button that was pressed + Down(MouseButton), + /// Released mouse button. Contains the button that was released + Up(MouseButton), + /// Moved the mouse cursor while pressing the contained mouse button + Drag(MouseButton), + /// Moved / Hover changed without pressing any buttons + Moved, + /// Scrolled mouse wheel downwards + ScrollDown, + /// Scrolled mouse wheel upwards + ScrollUp, + /// Scrolled mouse wheel left + ScrollLeft, + /// Scrolled mouse wheel right + ScrollRight, +} + +/// A keyboard event +#[derive(Debug, Eq, PartialEq, Copy, Clone, PartialOrd, Hash)] +#[cfg_attr( + feature = "serialize", + derive(Deserialize, Serialize), + serde(tag = "type", content = "args") +)] +pub enum MouseButton { + /// Left mouse button. + Left, + /// Right mouse button. + Right, + /// Middle mouse button. + Middle, +} + #[cfg(test)] mod test { @@ -262,7 +332,7 @@ mod test { assert!(e.is_keyboard().is_some()); assert_eq!(e.is_window_resize(), false); assert_eq!(e.is_tick(), false); - assert_eq!(e.is_tick(), false); + assert_eq!(e.is_mouse().is_some(), false); assert!(e.is_user().is_none()); let e: Event = Event::WindowResize(0, 24); assert!(e.is_window_resize()); @@ -271,6 +341,17 @@ mod test { assert!(e.is_tick()); let e: Event = Event::User(MockEvent::Bar); assert_eq!(e.is_user().unwrap(), &MockEvent::Bar); + + let e: Event = Event::Mouse(MouseEvent { + kind: MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + column: 0, + row: 0, + }); + assert!(e.is_mouse().is_some()); + assert_eq!(e.is_keyboard().is_some(), false); + assert_eq!(e.is_tick(), false); + assert_eq!(e.is_window_resize(), false); } // -- serde diff --git a/src/core/subscription.rs b/src/core/subscription.rs index 0f77761..4865129 100644 --- a/src/core/subscription.rs +++ b/src/core/subscription.rs @@ -3,8 +3,9 @@ //! This module defines the model for the Subscriptions use std::hash::Hash; +use std::ops::Range; -use crate::event::KeyEvent; +use crate::event::{KeyEvent, KeyModifiers, MouseEvent, MouseEventKind}; use crate::{AttrValue, Attribute, Event, State}; /// Public type to define a subscription. @@ -91,6 +92,25 @@ where } } +/// A event clause for [`MouseEvent`]s +#[derive(Debug, PartialEq, Eq)] +pub struct MouseEventClause { + /// The kind of mouse event that was caused + pub kind: MouseEventKind, + /// The key modifiers active when the event occurred + pub modifiers: KeyModifiers, + /// The column that the event occurred on + pub column: Range, + /// The row that the event occurred on + pub row: Range, +} + +impl MouseEventClause { + fn is_in_range(&self, ev: &MouseEvent) -> bool { + self.column.contains(&ev.column) && self.row.contains(&ev.row) + } +} + #[derive(Debug, PartialEq, Eq)] /// An event clause indicates on which kind of event the event must be forwarded to the `target` component. @@ -102,6 +122,8 @@ where Any, /// Check whether a certain key has been pressed Keyboard(KeyEvent), + /// Check whether a certain key has been pressed + Mouse(MouseEventClause), /// Check whether window has been resized WindowResize, /// The event will be forwarded on a tick @@ -121,6 +143,7 @@ where /// /// - Any: Forward, no matter what kind of event /// - Keyboard: everything must match + /// - Mouse: everything must match, column and row need to be within range /// - WindowResize: matches only event type, not sizes /// - Tick: matches tick event /// - None: matches None event @@ -129,6 +152,7 @@ where match self { EventClause::Any => true, EventClause::Keyboard(k) => Some(k) == ev.is_keyboard(), + EventClause::Mouse(m) => ev.is_mouse().map(|ev| m.is_in_range(ev)).unwrap_or(false), EventClause::WindowResize => ev.is_window_resize(), EventClause::Tick => ev.is_tick(), EventClause::User(u) => Some(u) == ev.is_user(), @@ -296,7 +320,7 @@ mod test { use super::*; use crate::command::Cmd; - use crate::event::Key; + use crate::event::{Key, KeyModifiers, MouseEventKind}; use crate::mock::{MockComponentId, MockEvent, MockFooInput}; use crate::{MockComponent, StateValue}; @@ -389,6 +413,71 @@ mod test { EventClause::::Keyboard(KeyEvent::from(Key::Enter)).forward(&Event::Tick), false ); + assert_eq!( + EventClause::::Keyboard(KeyEvent::from(Key::Enter)).forward(&Event::Mouse( + MouseEvent { + kind: MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + column: 0, + row: 0 + } + )), + false + ); + } + + #[test] + fn event_clause_mouse_should_forward() { + assert_eq!( + EventClause::::Mouse(MouseEventClause { + kind: MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + column: 0..10, + row: 0..10 + }) + .forward(&Event::Mouse(MouseEvent { + kind: MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + column: 0, + row: 0 + })), + true + ); + assert_eq!( + EventClause::::Mouse(MouseEventClause { + kind: MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + column: 0..10, + row: 0..10 + }) + .forward(&Event::Mouse(MouseEvent { + kind: MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + column: 20, + row: 20 + })), + false + ); + assert_eq!( + EventClause::::Mouse(MouseEventClause { + kind: MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + column: 0..10, + row: 0..10 + }) + .forward(&Event::Keyboard(KeyEvent::from(Key::Backspace))), + false + ); + assert_eq!( + EventClause::::Mouse(MouseEventClause { + kind: MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + column: 0..10, + row: 0..10 + }) + .forward(&Event::Tick), + false + ); } #[test] diff --git a/src/terminal/event_listener/crossterm.rs b/src/terminal/event_listener/crossterm.rs index b8b6f0e..1fdd2d8 100644 --- a/src/terminal/event_listener/crossterm.rs +++ b/src/terminal/event_listener/crossterm.rs @@ -4,11 +4,14 @@ use std::time::Duration; use crossterm::event::{ self as xterm, Event as XtermEvent, KeyCode as XtermKeyCode, KeyEvent as XtermKeyEvent, KeyEventKind as XtermEventKind, KeyModifiers as XtermKeyModifiers, - MediaKeyCode as XtermMediaKeyCode, + MediaKeyCode as XtermMediaKeyCode, MouseButton as XtermMouseButton, + MouseEvent as XtermMouseEvent, MouseEventKind as XtermMouseEventKind, }; use super::Event; -use crate::event::{Key, KeyEvent, KeyModifiers, MediaKeyCode}; +use crate::event::{ + Key, KeyEvent, KeyModifiers, MediaKeyCode, MouseButton, MouseEvent, MouseEventKind, +}; use crate::listener::{ListenerResult, Poll}; use crate::ListenerError; @@ -59,7 +62,7 @@ where match e { XtermEvent::Key(key) if key.kind == XtermEventKind::Press => Self::Keyboard(key.into()), XtermEvent::Key(_) => Self::None, - XtermEvent::Mouse(_) => Self::None, + XtermEvent::Mouse(ev) => Self::Mouse(ev.into()), XtermEvent::Resize(w, h) => Self::WindowResize(w, h), XtermEvent::FocusGained => Self::FocusGained, XtermEvent::FocusLost => Self::FocusLost, @@ -146,6 +149,42 @@ impl From for MediaKeyCode { } } +impl From for MouseEvent { + fn from(value: XtermMouseEvent) -> Self { + Self { + kind: value.kind.into(), + modifiers: value.modifiers.into(), + column: value.column, + row: value.row, + } + } +} + +impl From for MouseEventKind { + fn from(value: XtermMouseEventKind) -> Self { + match value { + XtermMouseEventKind::Down(b) => Self::Down(b.into()), + XtermMouseEventKind::Up(b) => Self::Up(b.into()), + XtermMouseEventKind::Drag(b) => Self::Drag(b.into()), + XtermMouseEventKind::Moved => Self::Moved, + XtermMouseEventKind::ScrollDown => Self::ScrollDown, + XtermMouseEventKind::ScrollUp => Self::ScrollUp, + XtermMouseEventKind::ScrollLeft => Self::ScrollLeft, + XtermMouseEventKind::ScrollRight => Self::ScrollRight, + } + } +} + +impl From for MouseButton { + fn from(value: XtermMouseButton) -> Self { + match value { + XtermMouseButton::Left => Self::Left, + XtermMouseButton::Right => Self::Right, + XtermMouseButton::Middle => Self::Middle, + } + } +} + #[cfg(test)] mod test { @@ -248,6 +287,122 @@ mod test { ); } + #[test] + fn should_adapt_mouse_event() { + assert_eq!( + MouseEvent::from(XtermMouseEvent { + kind: XtermMouseEventKind::Moved, + column: 1, + row: 1, + modifiers: XtermKeyModifiers::NONE + }), + MouseEvent { + kind: MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + column: 1, + row: 1 + } + ); + assert_eq!( + MouseEvent::from(XtermMouseEvent { + kind: XtermMouseEventKind::Down(XtermMouseButton::Left), + column: 1, + row: 1, + modifiers: XtermKeyModifiers::NONE + }), + MouseEvent { + kind: MouseEventKind::Down(MouseButton::Left), + modifiers: KeyModifiers::NONE, + column: 1, + row: 1 + } + ); + assert_eq!( + MouseEvent::from(XtermMouseEvent { + kind: XtermMouseEventKind::Up(XtermMouseButton::Right), + column: 1, + row: 1, + modifiers: XtermKeyModifiers::NONE + }), + MouseEvent { + kind: MouseEventKind::Up(MouseButton::Right), + modifiers: KeyModifiers::NONE, + column: 1, + row: 1 + } + ); + assert_eq!( + MouseEvent::from(XtermMouseEvent { + kind: XtermMouseEventKind::Drag(XtermMouseButton::Middle), + column: 1, + row: 1, + modifiers: XtermKeyModifiers::NONE + }), + MouseEvent { + kind: MouseEventKind::Drag(MouseButton::Middle), + modifiers: KeyModifiers::NONE, + column: 1, + row: 1 + } + ); + assert_eq!( + MouseEvent::from(XtermMouseEvent { + kind: XtermMouseEventKind::ScrollUp, + column: 1, + row: 1, + modifiers: XtermKeyModifiers::NONE + }), + MouseEvent { + kind: MouseEventKind::ScrollUp, + modifiers: KeyModifiers::NONE, + column: 1, + row: 1 + } + ); + assert_eq!( + MouseEvent::from(XtermMouseEvent { + kind: XtermMouseEventKind::ScrollDown, + column: 1, + row: 1, + modifiers: XtermKeyModifiers::NONE + }), + MouseEvent { + kind: MouseEventKind::ScrollDown, + modifiers: KeyModifiers::NONE, + column: 1, + row: 1 + } + ); + assert_eq!( + MouseEvent::from(XtermMouseEvent { + kind: XtermMouseEventKind::ScrollLeft, + column: 1, + row: 1, + modifiers: XtermKeyModifiers::NONE + }), + MouseEvent { + kind: MouseEventKind::ScrollLeft, + modifiers: KeyModifiers::NONE, + column: 1, + row: 1 + } + ); + assert_eq!( + MouseEvent::from(XtermMouseEvent { + kind: XtermMouseEventKind::ScrollRight, + column: 1, + row: 1, + modifiers: XtermKeyModifiers::NONE + }), + MouseEvent { + kind: MouseEventKind::ScrollRight, + modifiers: KeyModifiers::NONE, + column: 1, + row: 1 + } + ); + } + #[test] fn adapt_crossterm_key_event() { assert_eq!( @@ -279,7 +434,12 @@ mod test { row: 0, modifiers: XtermKeyModifiers::NONE, })), - Event::None + Event::Mouse(MouseEvent { + kind: MouseEventKind::Moved, + modifiers: KeyModifiers::NONE, + column: 0, + row: 0 + }) ); assert_eq!( AppEvent::from(XtermEvent::FocusGained), From 3d7325fb516115ff5873d0acbab92d83481b1d25 Mon Sep 17 00:00:00 2001 From: veeso Date: Sun, 13 Oct 2024 18:49:08 +0200 Subject: [PATCH 15/18] fix: changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6f42d2..9fbfb4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,10 +52,14 @@ Released on 13/10/2024 - `CmdResult::Custom(&'static str)` changed to `CmdResult::Custom(&'static str, State)` - Added new `subclause_and!(Id::Foo, Id::Bar, Id::Baz)` and `subclause_or!(Id::Foo, Id::Bar, Id::Baz)` macros. - Removed `InputListener`. Now use `CrosstermInputListener` or `TermionInputListener`. +- Added Event handling for Mouse Events + - Added `Mouse` in `SubEventClause`. - Bump `ratatui` version to `0.28` - Dont enable `MouseCapture` by default - Add function `enable_mouse_capture` and `disable_mouse_capture` to `TerminalBridge` +Huge thanks to [hasezoey](https://github.com/hasezoey) for the contributions. + ## 1.9.2 Released on 04/03/2023 From 3c8e556655cb06554b660183a60eacce66e2462d Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sun, 13 Oct 2024 18:59:55 +0200 Subject: [PATCH 16/18] feat(listener): poll a port multiple times until either "max_poll" or returned "None" (#78) * feat(listener): poll a port multiple times until either "max_poll" or returned "None" fixes #71 * feat(EventListenerCfg): add function "port_1" to add a Port directly to apply custom options to a port --------- Co-authored-by: veeso --- CHANGELOG.md | 4 ++ examples/demo/app/model.rs | 2 +- examples/user_events/user_events.rs | 5 +- src/core/application.rs | 3 +- src/listener/builder.rs | 52 +++++++++++++++---- src/listener/mod.rs | 1 + src/listener/port.rs | 19 +++++-- src/listener/worker.rs | 78 +++++++++++++++++++---------- 8 files changed, 119 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fbfb4a..48abcf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,10 @@ Released on 13/10/2024 - Bump `ratatui` version to `0.28` - Dont enable `MouseCapture` by default - Add function `enable_mouse_capture` and `disable_mouse_capture` to `TerminalBridge` +- **Max poll for ports**: + - Add `Port::set_max_poll` to set the amount a `Port` is polled in a single `Port::should_poll`. + - Add `EventListenerCfg::port` to add a manually constructed `Port` + - Previous `EventListenerCfg::port` has been renamed to `EventListenerCfg::add_port` Huge thanks to [hasezoey](https://github.com/hasezoey) for the contributions. diff --git a/examples/demo/app/model.rs b/examples/demo/app/model.rs index cb32478..aa9e317 100644 --- a/examples/demo/app/model.rs +++ b/examples/demo/app/model.rs @@ -77,7 +77,7 @@ where let mut app: Application = Application::init( EventListenerCfg::default() - .crossterm_input_listener(Duration::from_millis(20)) + .crossterm_input_listener(Duration::from_millis(20), 3) .poll_timeout(Duration::from_millis(10)) .tick_interval(Duration::from_secs(1)), ); diff --git a/examples/user_events/user_events.rs b/examples/user_events/user_events.rs index 5eb3be7..8b5aa20 100644 --- a/examples/user_events/user_events.rs +++ b/examples/user_events/user_events.rs @@ -51,10 +51,11 @@ impl Poll for UserDataPort { fn main() { let event_listener = EventListenerCfg::default() - .crossterm_input_listener(Duration::from_millis(10)) - .port( + .crossterm_input_listener(Duration::from_millis(10), 3) + .add_port( Box::new(UserDataPort::default()), Duration::from_millis(1000), + 1, ); let mut app: Application = Application::init(event_listener); diff --git a/src/core/application.rs b/src/core/application.rs index b0f1477..999eb9e 100644 --- a/src/core/application.rs +++ b/src/core/application.rs @@ -1035,9 +1035,10 @@ mod test { } fn listener_config() -> EventListenerCfg { - EventListenerCfg::default().port( + EventListenerCfg::default().add_port( Box::new(MockPoll::::default()), Duration::from_millis(100), + 1, ) } diff --git a/src/listener/builder.rs b/src/listener/builder.rs index 5ebb8a8..c84d748 100644 --- a/src/listener/builder.rs +++ b/src/listener/builder.rs @@ -59,27 +59,45 @@ where self } - /// Add a new Port (Poll, Interval) to the the event listener - pub fn port(mut self, poll: Box>, interval: Duration) -> Self { - self.ports.push(Port::new(poll, interval)); + /// Add a new [`Port`] (Poll, Interval) to the the event listener. + /// + /// The interval is the amount of time between each [`Poll::poll`] call. + /// The max_poll is the maximum amount of times the port should be polled in a single poll. + pub fn add_port(self, poll: Box>, interval: Duration, max_poll: usize) -> Self { + self.port(Port::new(poll, interval, max_poll)) + } + + /// Add a new [`Port`] to the the event listener + /// + /// The [`Port`] needs to be manually constructed, unlike [`Self::add_port`] + pub fn port(mut self, port: Port) -> Self { + self.ports.push(port); self } #[cfg(feature = "crossterm")] /// Add to the event listener the default crossterm input listener [`crate::terminal::CrosstermInputListener`] - pub fn crossterm_input_listener(self, interval: Duration) -> Self { - self.port( + /// + /// The interval is the amount of time between each [`Poll::poll`] call. + /// The max_poll is the maximum amount of times the port should be polled in a single poll. + pub fn crossterm_input_listener(self, interval: Duration, max_poll: usize) -> Self { + self.add_port( Box::new(crate::terminal::CrosstermInputListener::::new(interval)), interval, + max_poll, ) } #[cfg(feature = "termion")] /// Add to the event listener the default termion input listener [`crate::terminal::TermionInputListener`] - pub fn termion_input_listener(self, interval: Duration) -> Self { - self.port( + /// + /// The interval is the amount of time between each [`Poll::poll`] call. + /// The max_poll is the maximum amount of times the port should be polled in a single poll. + pub fn termion_input_listener(self, interval: Duration, max_poll: usize) -> Self { + self.add_port( Box::new(crate::terminal::TermionInputListener::::new(interval)), interval, + max_poll, ) } } @@ -104,8 +122,8 @@ mod test { let builder = builder.poll_timeout(Duration::from_millis(50)); assert_eq!(builder.poll_timeout, Duration::from_millis(50)); let builder = builder - .crossterm_input_listener(Duration::from_millis(200)) - .port(Box::new(MockPoll::default()), Duration::from_secs(300)); + .crossterm_input_listener(Duration::from_millis(200), 1) + .add_port(Box::new(MockPoll::default()), Duration::from_secs(300), 1); assert_eq!(builder.ports.len(), 2); let mut listener = builder.start(); assert!(listener.stop().is_ok()); @@ -123,8 +141,8 @@ mod test { let builder = builder.poll_timeout(Duration::from_millis(50)); assert_eq!(builder.poll_timeout, Duration::from_millis(50)); let builder = builder - .termion_input_listener(Duration::from_millis(200)) - .port(Box::new(MockPoll::default()), Duration::from_secs(300)); + .termion_input_listener(Duration::from_millis(200), 1) + .add_port(Box::new(MockPoll::default()), Duration::from_secs(300), 1); assert_eq!(builder.ports.len(), 2); let mut listener = builder.start(); assert!(listener.stop().is_ok()); @@ -137,4 +155,16 @@ mod test { .poll_timeout(Duration::from_secs(0)) .start(); } + + #[test] + fn should_add_port_via_port_1() { + let builder = EventListenerCfg::::default(); + assert!(builder.ports.is_empty()); + let builder = builder.port(Port::new( + Box::new(MockPoll::default()), + Duration::from_millis(1), + 1, + )); + assert_eq!(builder.ports.len(), 1); + } } diff --git a/src/listener/mod.rs b/src/listener/mod.rs index bd51214..9dc8106 100644 --- a/src/listener/mod.rs +++ b/src/listener/mod.rs @@ -247,6 +247,7 @@ mod test { vec![Port::new( Box::new(MockPoll::default()), Duration::from_secs(10), + 1, )], Duration::from_millis(10), Some(Duration::from_secs(3)), diff --git a/src/listener/port.rs b/src/listener/port.rs index 2b8f1bd..326eb3b 100644 --- a/src/listener/port.rs +++ b/src/listener/port.rs @@ -17,21 +17,34 @@ where poll: Box>, interval: Duration, next_poll: Instant, + max_poll: usize, } impl Port where U: Eq + PartialEq + Clone + PartialOrd + Send + 'static, { - /// Define a new `Port` - pub fn new(poll: Box>, interval: Duration) -> Self { + /// Define a new [`Port`] + /// + /// # Parameters + /// + /// * `poll` - The poll trait object + /// * `interval` - The interval between each poll + /// * `max_poll` - The maximum amount of times the port should be polled in a single poll + pub fn new(poll: Box>, interval: Duration, max_poll: usize) -> Self { Self { poll, interval, next_poll: Instant::now(), + max_poll, } } + /// Get how often a port should get polled in a single poll + pub fn max_poll(&self) -> usize { + self.max_poll + } + /// Returns the interval for the current [`Port`] pub fn interval(&self) -> &Duration { &self.interval @@ -69,7 +82,7 @@ mod test { #[test] fn test_single_listener() { let mut listener = - Port::::new(Box::new(MockPoll::default()), Duration::from_secs(5)); + Port::::new(Box::new(MockPoll::default()), Duration::from_secs(5), 1); assert!(listener.next_poll() <= Instant::now()); assert_eq!(listener.should_poll(), true); assert!(listener.poll().ok().unwrap().is_some()); diff --git a/src/listener/worker.rs b/src/listener/worker.rs index 165f76c..1f6ada9 100644 --- a/src/listener/worker.rs +++ b/src/listener/worker.rs @@ -118,35 +118,32 @@ where /// Returns only the messages, while the None returned by poll are discarded #[allow(clippy::needless_collect)] fn poll(&mut self) -> Result<(), mpsc::SendError>> { - let msg: Vec> = self - .ports - .iter_mut() - .filter_map(|x| { - if x.should_poll() { - let msg = match x.poll() { - Ok(Some(ev)) => Some(ListenerMsg::User(ev)), - Ok(None) => None, - Err(err) => Some(ListenerMsg::Error(err)), - }; - // Update next poll - x.calc_next_poll(); - msg - } else { - None + let port_iter = self.ports.iter_mut().filter(|port| port.should_poll()); + + for port in port_iter { + let mut times_remaining = port.max_poll(); + // poll a port until it has nothing anymore + loop { + let msg = match port.poll() { + Ok(Some(ev)) => ListenerMsg::User(ev), + Ok(None) => break, + Err(err) => ListenerMsg::Error(err), + }; + + self.sender.send(msg)?; + + // do this at the end to at least call it once + times_remaining = times_remaining.saturating_sub(1); + + if times_remaining == 0 { + break; } - }) - .collect(); - // Send messages - match msg - .into_iter() - .map(|x| self.sender.send(x)) - .filter(|x| x.is_err()) - .map(|x| x.err().unwrap()) - .next() - { - None => Ok(()), - Some(e) => Err(e), + } + // Update next poll + port.calc_next_poll(); } + + Ok(()) } /// thread run method @@ -186,6 +183,29 @@ mod test { use crate::mock::{MockEvent, MockPoll}; use crate::Event; + #[test] + fn worker_should_poll_multiple_times() { + let (tx, rx) = mpsc::channel(); + let paused = Arc::new(RwLock::new(false)); + let paused_t = Arc::clone(&paused); + let running = Arc::new(RwLock::new(true)); + let running_t = Arc::clone(&running); + + let mock_port = Port::new(Box::new(MockPoll::default()), Duration::from_secs(5), 10); + + let mut worker = + EventListenerWorker::::new(vec![mock_port], tx, paused_t, running_t, None); + assert!(worker.poll().is_ok()); + assert!(worker.next_event() <= Duration::from_secs(5)); + let mut recieved = Vec::new(); + + while let Ok(msg) = rx.try_recv() { + recieved.push(msg); + } + + assert_eq!(recieved.len(), 10); + } + #[test] fn worker_should_send_poll() { let (tx, rx) = mpsc::channel(); @@ -197,6 +217,7 @@ mod test { vec![Port::new( Box::new(MockPoll::default()), Duration::from_secs(5), + 1, )], tx, paused_t, @@ -223,6 +244,7 @@ mod test { vec![Port::new( Box::new(MockPoll::default()), Duration::from_secs(5), + 1, )], tx, paused_t, @@ -249,6 +271,7 @@ mod test { vec![Port::new( Box::new(MockPoll::default()), Duration::from_secs(5), + 1, )], tx, paused_t, @@ -293,6 +316,7 @@ mod test { vec![Port::new( Box::new(MockPoll::default()), Duration::from_secs(3), + 1, )], tx, paused_t, From cc92e9477d633b0e188f7f94a1ecc06f918f2cc7 Mon Sep 17 00:00:00 2001 From: veeso Date: Sun, 13 Oct 2024 19:01:58 +0200 Subject: [PATCH 17/18] fix: deps --- Cargo.toml | 4 ++-- src/core/event.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fb1d9ec..3b19e85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,8 +30,8 @@ tempfile = "^3" [features] default = ["derive", "crossterm"] -derive = ["tuirealm_derive"] -serialize = ["serde", "bitflags/serde"] +derive = ["dep:tuirealm_derive"] +serialize = ["dep:serde", "bitflags/serde"] crossterm = ["dep:crossterm", "ratatui/crossterm"] termion = ["dep:termion", "ratatui/termion"] diff --git a/src/core/event.rs b/src/core/event.rs index 08fd569..0d25c3c 100644 --- a/src/core/event.rs +++ b/src/core/event.rs @@ -3,7 +3,7 @@ //! `events` exposes the event raised by a user interaction or by the runtime use bitflags::bitflags; -#[cfg(feature = "serde")] +#[cfg(feature = "serialize")] use serde::{Deserialize, Serialize}; // -- event From e7ac3dfc0ecc0c0f549f19d6113dd455911f9618 Mon Sep 17 00:00:00 2001 From: veeso Date: Sun, 13 Oct 2024 19:09:09 +0200 Subject: [PATCH 18/18] fix: use AtomicBool instead of RwLock --- src/listener/mod.rs | 46 +++++++++++----------------- src/listener/worker.rs | 68 +++++++++++++++--------------------------- 2 files changed, 41 insertions(+), 73 deletions(-) diff --git a/src/listener/mod.rs b/src/listener/mod.rs index 9dc8106..bfa9fda 100644 --- a/src/listener/mod.rs +++ b/src/listener/mod.rs @@ -8,8 +8,9 @@ mod builder; mod port; mod worker; +use std::sync::atomic::AtomicBool; // -- export -use std::sync::{mpsc, Arc, RwLock}; +use std::sync::{mpsc, Arc}; use std::thread::{self, JoinHandle}; use std::time::Duration; @@ -60,9 +61,9 @@ where /// Max Time to wait when calling `recv()` on thread receiver poll_timeout: Duration, /// Indicates whether the worker should paused polling ports - paused: Arc>, + paused: Arc, /// Indicates whether the worker should keep running - running: Arc>, + running: Arc, /// Msg receiver from worker recv: mpsc::Receiver>, /// Join handle for worker @@ -104,14 +105,9 @@ where /// Stop event listener pub fn stop(&mut self) -> ListenerResult<()> { - { - // NOTE: keep these brackets to drop running after block - let mut running = match self.running.write() { - Ok(lock) => Ok(lock), - Err(_) => Err(ListenerError::CouldNotStop), - }?; - *running = false; - } + self.running + .store(false, std::sync::atomic::Ordering::Relaxed); + // Join thread match self.thread.take().map(|x| x.join()) { Some(Ok(_)) => Ok(()), @@ -122,23 +118,15 @@ where /// Pause event listener worker pub fn pause(&mut self) -> ListenerResult<()> { - // NOTE: keep these brackets to drop running after block - let mut paused = match self.paused.write() { - Ok(lock) => Ok(lock), - Err(_) => Err(ListenerError::CouldNotStop), - }?; - *paused = true; + self.paused + .store(true, std::sync::atomic::Ordering::Relaxed); Ok(()) } /// Unpause event listener worker pub fn unpause(&mut self) -> ListenerResult<()> { - // NOTE: keep these brackets to drop running after block - let mut paused = match self.paused.write() { - Ok(lock) => Ok(lock), - Err(_) => Err(ListenerError::CouldNotStop), - }?; - *paused = false; + self.paused + .store(false, std::sync::atomic::Ordering::Relaxed); Ok(()) } @@ -154,9 +142,9 @@ where /// Setup the thread and returns the structs necessary to interact with it fn setup_thread(ports: Vec>, tick_interval: Option) -> ThreadConfig { let (sender, recv) = mpsc::channel(); - let paused = Arc::new(RwLock::new(false)); + let paused = Arc::new(AtomicBool::new(false)); let paused_t = Arc::clone(&paused); - let running = Arc::new(RwLock::new(true)); + let running = Arc::new(AtomicBool::new(true)); let running_t = Arc::clone(&running); // Start thread let thread = thread::spawn(move || { @@ -183,8 +171,8 @@ where U: Eq + PartialEq + Clone + PartialOrd + Send + 'static, { rx: mpsc::Receiver>, - paused: Arc>, - running: Arc>, + paused: Arc, + running: Arc, thread: JoinHandle<()>, } @@ -194,8 +182,8 @@ where { pub fn new( rx: mpsc::Receiver>, - paused: Arc>, - running: Arc>, + paused: Arc, + running: Arc, thread: JoinHandle<()>, ) -> Self { Self { diff --git a/src/listener/worker.rs b/src/listener/worker.rs index 1f6ada9..11187db 100644 --- a/src/listener/worker.rs +++ b/src/listener/worker.rs @@ -3,7 +3,8 @@ //! This module implements the worker thread for the event listener use std::ops::{Add, Sub}; -use std::sync::{mpsc, Arc, RwLock}; +use std::sync::atomic::AtomicBool; +use std::sync::{mpsc, Arc}; use std::thread; use std::time::{Duration, Instant}; @@ -18,8 +19,8 @@ where { ports: Vec>, sender: mpsc::Sender>, - paused: Arc>, - running: Arc>, + paused: Arc, + running: Arc, next_tick: Instant, tick_interval: Option, } @@ -31,8 +32,8 @@ where pub(super) fn new( ports: Vec>, sender: mpsc::Sender>, - paused: Arc>, - running: Arc>, + paused: Arc, + running: Arc, tick_interval: Option, ) -> Self { Self { @@ -77,18 +78,12 @@ where /// Returns whether should keep running fn running(&self) -> bool { - if let Ok(lock) = self.running.read() { - return *lock; - } - true + self.running.load(std::sync::atomic::Ordering::Relaxed) } /// Returns whether worker is paused fn paused(&self) -> bool { - if let Ok(lock) = self.paused.read() { - return *lock; - } - false + self.paused.load(std::sync::atomic::Ordering::Relaxed) } /// Returns whether it's time to tick. @@ -177,7 +172,7 @@ mod test { use pretty_assertions::assert_eq; - use super::super::{ListenerError, ListenerResult}; + use super::super::ListenerResult; use super::*; use crate::core::event::{Key, KeyEvent}; use crate::mock::{MockEvent, MockPoll}; @@ -186,9 +181,9 @@ mod test { #[test] fn worker_should_poll_multiple_times() { let (tx, rx) = mpsc::channel(); - let paused = Arc::new(RwLock::new(false)); + let paused = Arc::new(AtomicBool::new(false)); let paused_t = Arc::clone(&paused); - let running = Arc::new(RwLock::new(true)); + let running = Arc::new(AtomicBool::new(true)); let running_t = Arc::clone(&running); let mock_port = Port::new(Box::new(MockPoll::default()), Duration::from_secs(5), 10); @@ -209,9 +204,9 @@ mod test { #[test] fn worker_should_send_poll() { let (tx, rx) = mpsc::channel(); - let paused = Arc::new(RwLock::new(false)); + let paused = Arc::new(AtomicBool::new(false)); let paused_t = Arc::clone(&paused); - let running = Arc::new(RwLock::new(true)); + let running = Arc::new(AtomicBool::new(true)); let running_t = Arc::clone(&running); let mut worker = EventListenerWorker::::new( vec![Port::new( @@ -236,9 +231,9 @@ mod test { #[test] fn worker_should_send_tick() { let (tx, rx) = mpsc::channel(); - let paused = Arc::new(RwLock::new(false)); + let paused = Arc::new(AtomicBool::new(false)); let paused_t = Arc::clone(&paused); - let running = Arc::new(RwLock::new(true)); + let running = Arc::new(AtomicBool::new(true)); let running_t = Arc::clone(&running); let mut worker = EventListenerWorker::::new( vec![Port::new( @@ -263,9 +258,9 @@ mod test { #[test] fn worker_should_calc_times_correctly_with_tick() { let (tx, rx) = mpsc::channel(); - let paused = Arc::new(RwLock::new(false)); + let paused = Arc::new(AtomicBool::new(false)); let paused_t = Arc::clone(&paused); - let running = Arc::new(RwLock::new(true)); + let running = Arc::new(AtomicBool::new(true)); let running_t = Arc::clone(&running); let mut worker = EventListenerWorker::::new( vec![Port::new( @@ -292,15 +287,7 @@ mod test { // Now should no more tick and poll assert_eq!(worker.should_tick(), false); // Stop - { - let mut running_flag = match running.write() { - Ok(lock) => Ok(lock), - Err(_) => Err(ListenerError::CouldNotStop), - } - .ok() - .unwrap(); - *running_flag = false; - } + running.store(false, std::sync::atomic::Ordering::Relaxed); assert_eq!(worker.running(), false); drop(rx); } @@ -308,9 +295,9 @@ mod test { #[test] fn worker_should_calc_times_correctly_without_tick() { let (tx, rx) = mpsc::channel(); - let paused = Arc::new(RwLock::new(false)); + let paused = Arc::new(AtomicBool::new(false)); let paused_t = Arc::clone(&paused); - let running = Arc::new(RwLock::new(true)); + let running = Arc::new(AtomicBool::new(true)); let running_t = Arc::clone(&running); let worker = EventListenerWorker::::new( vec![Port::new( @@ -332,15 +319,8 @@ mod test { // Next event should be in 3 second (poll) assert!(worker.next_event() <= Duration::from_secs(3)); // Stop - { - let mut running_flag = match running.write() { - Ok(lock) => Ok(lock), - Err(_) => Err(ListenerError::CouldNotStop), - } - .ok() - .unwrap(); - *running_flag = false; - } + running.store(false, std::sync::atomic::Ordering::Relaxed); + assert_eq!(worker.running(), false); drop(rx); } @@ -349,9 +329,9 @@ mod test { #[should_panic] fn worker_should_panic_when_trying_next_tick_without_it() { let (tx, _) = mpsc::channel(); - let paused = Arc::new(RwLock::new(false)); + let paused = Arc::new(AtomicBool::new(false)); let paused_t = Arc::clone(&paused); - let running = Arc::new(RwLock::new(true)); + let running = Arc::new(AtomicBool::new(true)); let running_t = Arc::clone(&running); let mut worker = EventListenerWorker::::new(vec![], tx, paused_t, running_t, None);