mirror of
https://codeberg.org/JasterV/teatui.git
synced 2026-04-26 18:10:03 +00:00
feat: provide a new tokio feature for async effects support via tokio
This commit is contained in:
parent
2825688c30
commit
f32c6e9dd9
5 changed files with 239 additions and 29 deletions
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
|
|
@ -1,5 +1,4 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
|
|
@ -7,16 +6,13 @@ on:
|
|||
- main
|
||||
- master
|
||||
- develop
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
# ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel
|
||||
# and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
fmt:
|
||||
name: fmt
|
||||
|
|
@ -85,6 +81,6 @@ jobs:
|
|||
if: hashFiles('Cargo.lock') == ''
|
||||
run: cargo generate-lockfile
|
||||
- name: cargo test --locked
|
||||
run: cargo test --locked --all-features --all-targets
|
||||
run: cargo test --locked --all-targets
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
|
|
|||
131
Cargo.lock
generated
131
Cargo.lock
generated
|
|
@ -92,6 +92,12 @@ version = "1.24.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.4"
|
||||
|
|
@ -310,7 +316,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -585,7 +591,7 @@ dependencies = [
|
|||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -779,6 +785,12 @@ dependencies = [
|
|||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.13.0"
|
||||
|
|
@ -972,7 +984,7 @@ dependencies = [
|
|||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1083,6 +1095,16 @@ version = "1.15.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
|
|
@ -1145,6 +1167,7 @@ dependencies = [
|
|||
"crossterm",
|
||||
"ratatui",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1271,6 +1294,34 @@ version = "0.1.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.49.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.19.0"
|
||||
|
|
@ -1505,6 +1556,15 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
|
|
@ -1514,6 +1574,71 @@ dependencies = [
|
|||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
[package]
|
||||
name = "teatui"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
description = "An elm-like abstraction over Ratatui"
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
tokio = ["dep:tokio"]
|
||||
|
||||
[dependencies]
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
thiserror = "2"
|
||||
tokio = { version = "1", features = ["full"], optional = true }
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
//! Actor responsible of processing side effects sent by the update actor.
|
||||
use std::sync::mpsc::{Receiver, SendError, Sender};
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
use std::future::Future;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum EffectsError<M> {
|
||||
#[error("Failed to send message to update process")]
|
||||
MessageSend(#[from] SendError<M>),
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "tokio"))]
|
||||
pub(crate) fn run<M, Msg, Eff, F>(
|
||||
effects_fn: F,
|
||||
rx: Receiver<(M, Eff)>,
|
||||
|
|
@ -26,3 +30,39 @@ where
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
pub(crate) fn run_async<M, Msg, Eff, F, Fut>(
|
||||
effects_fn: F,
|
||||
rx: Receiver<(M, Eff)>,
|
||||
tx: Sender<Msg>,
|
||||
) -> Result<(), EffectsError<Msg>>
|
||||
where
|
||||
M: Send + Sync + 'static,
|
||||
Msg: Send + Sync + 'static,
|
||||
Eff: Send + Sync + 'static,
|
||||
Fut: Future<Output = Option<Msg>> + Send,
|
||||
F: Fn(&M, Eff) -> Fut + Send + Sync + 'static,
|
||||
{
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Failed to build Tokio reactor for side-effects");
|
||||
|
||||
rt.block_on(async {
|
||||
loop {
|
||||
let Ok((model, effect)) = rx.recv() else {
|
||||
break;
|
||||
};
|
||||
|
||||
// We spawn the effect in the tokio reactor so they can run concurrently
|
||||
let fut = effects_fn(&model, effect);
|
||||
|
||||
if let Some(msg) = fut.await {
|
||||
let _ = tx.send(msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ where
|
|||
/// - A `view` function, responsible for constructing the view from the model.
|
||||
///
|
||||
/// - An `effects` function responsible for handling side effects.
|
||||
#[cfg(not(feature = "tokio"))]
|
||||
pub fn start<M, Msg, Eff, W, IF, UF, VF, EF>(
|
||||
init_fn: IF,
|
||||
update_fn: UF,
|
||||
|
|
@ -88,67 +89,110 @@ where
|
|||
UF: Fn(M, Msg) -> Update<M, Eff> + Send + Sync + 'static,
|
||||
VF: Fn(&M) -> W + Send + Sync + 'static,
|
||||
EF: Fn(&M, Eff) -> Option<Msg> + Send + Sync + 'static,
|
||||
{
|
||||
run_program(init_fn, update_fn, view_fn, move |effects_rx, update_tx| {
|
||||
effects::run(effects_fn, effects_rx, update_tx)
|
||||
})
|
||||
}
|
||||
|
||||
/// Starts the runtime with asynchronous (Tokio) side effects.
|
||||
#[cfg(feature = "tokio")]
|
||||
pub fn start<M, Msg, Eff, W, IF, UF, VF, EF, Fut>(
|
||||
init_fn: IF,
|
||||
update_fn: UF,
|
||||
view_fn: VF,
|
||||
effects_fn: EF,
|
||||
) -> Result<(), ProgramError<M, Msg, Eff>>
|
||||
where
|
||||
M: Clone + Send + Sync + 'static,
|
||||
Eff: Debug + Send + Sync + 'static,
|
||||
Msg: From<crossterm::event::Event> + Sync + Send + 'static,
|
||||
W: Widget,
|
||||
IF: Fn() -> (M, Option<Eff>) + Send + Sync + 'static,
|
||||
UF: Fn(M, Msg) -> Update<M, Eff> + Send + Sync + 'static,
|
||||
VF: Fn(&M) -> W + Send + Sync + 'static,
|
||||
EF: Fn(&M, Eff) -> Fut + Send + Sync + 'static,
|
||||
Fut: std::future::Future<Output = Option<Msg>> + Send,
|
||||
{
|
||||
run_program(init_fn, update_fn, view_fn, move |effects_rx, update_tx| {
|
||||
effects::run_async(effects_fn, effects_rx, update_tx)
|
||||
})
|
||||
}
|
||||
|
||||
/// Internal helper to abstract the common actor-spawning logic.
|
||||
fn run_program<M, Msg, Eff, W, IF, UF, VF, SF>(
|
||||
init_fn: IF,
|
||||
update_fn: UF,
|
||||
view_fn: VF,
|
||||
effects_fn: SF,
|
||||
) -> Result<(), ProgramError<M, Msg, Eff>>
|
||||
where
|
||||
M: Clone + Send + Sync + 'static,
|
||||
Eff: Debug + Send + Sync + 'static,
|
||||
Msg: From<crossterm::event::Event> + Sync + Send + 'static,
|
||||
W: Widget,
|
||||
IF: Fn() -> (M, Option<Eff>) + Send + Sync + 'static,
|
||||
UF: Fn(M, Msg) -> Update<M, Eff> + Send + Sync + 'static,
|
||||
VF: Fn(&M) -> W + Send + Sync + 'static,
|
||||
SF: FnOnce(
|
||||
std::sync::mpsc::Receiver<(M, Eff)>,
|
||||
std::sync::mpsc::Sender<Msg>,
|
||||
) -> Result<(), EffectsError<Msg>>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
{
|
||||
let terminal = ratatui::init();
|
||||
|
||||
// Channel for signaling when a task completes
|
||||
let (shutdown_tx, shutdown_rx) = channel::<Result<(), ProgramError<M, Msg, Eff>>>();
|
||||
|
||||
// Channels for inter-thread communication
|
||||
let (update_tx, update_rx) = channel::<Msg>();
|
||||
let (view_tx, view_rx) = channel::<M>();
|
||||
let (effects_tx, effects_rx) = channel::<(M, Eff)>();
|
||||
|
||||
// Spawn order is important.
|
||||
// If the view actor is started after the update actor, it could happen
|
||||
// that both actors have an out of sync version of the model for a bit.
|
||||
// Spawn View Actor
|
||||
thread::spawn({
|
||||
let (model, _) = init_fn();
|
||||
|
||||
let shutdown_tx = shutdown_tx.clone();
|
||||
|
||||
move || {
|
||||
let result = view::run(model, terminal, view_fn, view_rx)
|
||||
.map_err(|err| ProgramError::ViewError(err));
|
||||
|
||||
let result =
|
||||
view::run(model, terminal, view_fn, view_rx).map_err(ProgramError::ViewError);
|
||||
let _ = shutdown_tx.send(result);
|
||||
}
|
||||
});
|
||||
|
||||
// Spawn Update Actor
|
||||
thread::spawn({
|
||||
let shutdown_tx = shutdown_tx.clone();
|
||||
let (model, effect) = init_fn();
|
||||
|
||||
move || {
|
||||
let result = update::run(model, effect, update_fn, update_rx, view_tx, effects_tx)
|
||||
.map_err(|err| ProgramError::UpdateError(err));
|
||||
|
||||
.map_err(ProgramError::UpdateError);
|
||||
let _ = shutdown_tx.send(result);
|
||||
}
|
||||
});
|
||||
|
||||
// Spawn Effects Actor
|
||||
thread::spawn({
|
||||
let shutdown_tx = shutdown_tx.clone();
|
||||
let update_tx = update_tx.clone();
|
||||
let shutdown_tx = shutdown_tx.clone();
|
||||
|
||||
move || {
|
||||
let result = effects::run(effects_fn, effects_rx, update_tx)
|
||||
.map_err(|err| ProgramError::EffectsError(err));
|
||||
let result = effects_fn(effects_rx, update_tx).map_err(ProgramError::EffectsError);
|
||||
|
||||
let _ = shutdown_tx.send(result);
|
||||
}
|
||||
});
|
||||
|
||||
// Spawn Events Actor
|
||||
thread::spawn({
|
||||
let shutdown_tx = shutdown_tx.clone();
|
||||
|
||||
move || {
|
||||
let result = events::run(update_tx).map_err(|err| ProgramError::EventLoopError(err));
|
||||
let result = events::run(update_tx).map_err(ProgramError::EventLoopError);
|
||||
let _ = shutdown_tx.send(result);
|
||||
}
|
||||
});
|
||||
|
||||
let result = shutdown_rx.recv().ok();
|
||||
|
||||
ratatui::restore();
|
||||
|
||||
match result {
|
||||
|
|
|
|||
Loading…
Reference in a new issue