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)
-
-
⚠️ 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
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);