diff --git a/Cargo.lock b/Cargo.lock index 9908dfb..58d5e86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,12 +8,156 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "castaway" version = "0.2.4" @@ -43,6 +187,15 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "convert_case" version = "0.10.0" @@ -52,6 +205,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crossterm" version = "0.29.0" @@ -176,20 +335,63 @@ dependencies = [ ] [[package]] -name = "flume" -version = "0.12.0" +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "spin", + "event-listener", + "pin-project-lite", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "foldhash" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "generational-box" version = "0.7.3" @@ -217,6 +419,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "ident_case" version = "1.0.1" @@ -355,6 +563,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -384,6 +598,31 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -583,6 +822,12 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "slotmap" version = "1.1.1" @@ -599,12 +844,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] -name = "spin" -version = "0.9.8" +name = "smol" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" dependencies = [ - "lock_api", + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", ] [[package]] @@ -762,11 +1015,12 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" name = "vtui" version = "0.4.2" dependencies = [ + "async-channel", "crossterm", - "flume", "generational-box", "ratatui", "slotmap", + "smol", "thiserror", "unicode-segmentation", "unicode-width", diff --git a/Cargo.toml b/Cargo.toml index 14c42a0..40fced9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,15 +13,17 @@ keywords = ["terminal", "tui"] categories = ["command-line-interface"] [features] -default = ["crossterm"] +default = ["crossterm", "smol"] crossterm = ["dep:crossterm", "ratatui/crossterm"] +smol = ["dep:smol"] [dependencies] +async-channel = "2.5.0" crossterm = { version = "0.29.0", optional = true } -flume = { version = "0.12.0", default-features = false } generational-box = "0.7.3" ratatui = { version = "0.30.0", default-features = false } slotmap = "1.1.1" +smol = { version = "2.0.2", default-features = false, optional = true } thiserror = "2.0.18" unicode-segmentation = "1.12.0" unicode-width = "0.2.2" diff --git a/src/errors.rs b/src/errors.rs index a8bad2d..af1afac 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -11,8 +11,8 @@ pub enum RuntimeError { #[derive(Debug)] pub struct SendError; -impl From> for SendError { - fn from(_: flume::SendError) -> Self { +impl From> for SendError { + fn from(_: async_channel::SendError) -> Self { Self } } diff --git a/src/launch.rs b/src/launch.rs index 01a272c..6b064e5 100644 --- a/src/launch.rs +++ b/src/launch.rs @@ -19,7 +19,7 @@ impl LaunchBuilder { } /// Launches the application with the given root component. - pub fn launch(self, app: Factory) -> Result<(), RuntimeError> { + pub async fn launch(self, app: Factory) -> Result<(), RuntimeError> { let node = app(Component::new(), ()); let bus = MessageBus::new(); @@ -27,13 +27,13 @@ impl LaunchBuilder { let mut driver = CrosstermDriver::new(io::stdout())?; driver.setup()?; - driver.spawn_event_handler(handle.clone()); + // driver.spawn_event_handler(handle.clone()); let mut runtime = Runtime::new(node, bus); loop { runtime.draw(&mut driver)?; - runtime.update(); + runtime.update().await; if runtime.should_exit() { break; @@ -52,7 +52,10 @@ impl LaunchBuilder { /// /// Panics if the runtime encounters an error. Use [`LaunchBuilder`] for controlled error handling. pub fn launch(app: Factory) { - LaunchBuilder::new() - .launch(app) - .expect("app panicked unexpectedly"); + smol::block_on(async { + LaunchBuilder::new() + .launch(app) + .await + .expect("app panicked unexpectedly"); + }) } diff --git a/src/lib.rs b/src/lib.rs index e2dac3f..b682213 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,6 +46,12 @@ //! listening component. vtui offers batteries-included systems such as a focus system to help the //! developer route events to components. +#[cfg(not(feature = "crossterm"))] +compile_error!("vtui requires a driver: enable 'crossterm' feature"); + +#[cfg(not(feature = "smol"))] +compile_error!("vtui requires an executor: enable 'smol' feature"); + extern crate alloc; pub use crate::{ diff --git a/src/runtime.rs b/src/runtime.rs index 792f7df..37a66e2 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -42,18 +42,21 @@ impl Runtime { Ok(()) } - pub fn update(&mut self) { + pub async fn update(&mut self) { if self.context.tick_requested() { let _ = self.bus.handle().send(Tick {}); self.context.clear_tick_request(); } let deadline = Instant::now() + Duration::from_millis(16); - let msg = self.bus.recv(); + let msg = self.bus.recv().await; self.dispatch(msg); - while let Some(msg) = self.bus.recv_timeout(deadline - Instant::now()) { + while let Some(msg) = { + let timeout = deadline.saturating_duration_since(Instant::now()); + self.bus.recv_timeout(timeout).await + } { self.dispatch(msg); } } diff --git a/src/transport.rs b/src/transport.rs index a76cdc3..c2793f5 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -76,20 +76,18 @@ impl<'d> Dispatch<'d> { pub struct MessageBus { tx: MessageSender, - rx: flume::Receiver, + rx: async_channel::Receiver, } impl Default for MessageBus { fn default() -> Self { - let (tx, rx) = flume::bounded(Self::DEFAULT_CAPACITY); + let (tx, rx) = async_channel::unbounded(); let tx = MessageSender::from(tx); Self { tx, rx } } } impl MessageBus { - const DEFAULT_CAPACITY: usize = 128; - pub fn new() -> Self { Self::default() } @@ -98,32 +96,34 @@ impl MessageBus { &self.tx } - pub fn recv(&self) -> Message { - self.rx.recv().expect("bus closed unexpectedly") + pub async fn recv(&self) -> Message { + self.rx.recv().await.expect("bus closed unexpectedly") } - pub fn recv_timeout(&self, timeout: Duration) -> Option { - match self.rx.recv_timeout(timeout) { - Ok(msg) => Some(msg), - Err(flume::RecvTimeoutError::Timeout) => None, - Err(flume::RecvTimeoutError::Disconnected) => panic!("bus closed unexpectedly"), - } + pub async fn recv_timeout(&self, timeout: Duration) -> Option { + // Cancel-safe according to: https://github.com/smol-rs/async-channel/issues/111 + // In other words, data loss will not occur if the timer wins the race. + smol::future::or(async { self.rx.recv().await.ok() }, async { + smol::Timer::after(timeout).await; + None + }) + .await } } #[derive(Clone)] pub struct MessageSender { - tx: flume::Sender, + tx: async_channel::Sender, } -impl From> for MessageSender { - fn from(tx: flume::Sender) -> Self { +impl From> for MessageSender { + fn from(tx: async_channel::Sender) -> Self { Self { tx } } } impl MessageSender { pub fn send(&self, msg: impl Into) -> Result<(), SendError> { - self.tx.send(msg.into()).map_err(|_| SendError) + self.tx.try_send(msg.into()).map_err(|_| SendError) } }