diff --git a/Cargo.lock b/Cargo.lock index 0448a4b..322deed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,6 +191,7 @@ dependencies = [ "clap", "crossterm", "futures", + "parking_lot", "thiserror", "tokio", "toss", @@ -579,9 +580,9 @@ checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" [[package]] name = "parking_lot" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core", @@ -1077,7 +1078,7 @@ dependencies = [ [[package]] name = "toss" version = "0.1.0" -source = "git+https://github.com/Garmelon/toss.git?rev=33264b4aec27066e6abb7cc7d15bd680b43fcd5a#33264b4aec27066e6abb7cc7d15bd680b43fcd5a" +source = "git+https://github.com/Garmelon/toss.git?rev=333cf74fba56080043a13b9f55c0b62695e2fa4a#333cf74fba56080043a13b9f55c0b62695e2fa4a" dependencies = [ "crossterm", "unicode-linebreak", diff --git a/cove-tui/Cargo.toml b/cove-tui/Cargo.toml index 3115fee..457ae26 100644 --- a/cove-tui/Cargo.toml +++ b/cove-tui/Cargo.toml @@ -8,6 +8,7 @@ anyhow = "1.0.57" clap = { version = "3.1.18", features = ["derive"] } crossterm = { version = "0.23.2", features = ["event-stream"] } futures = "0.3.21" +parking_lot = "0.12.1" thiserror = "1.0.31" tokio = { version = "1.18.2", features = ["full"] } -toss = { git = "https://github.com/Garmelon/toss.git", rev = "33264b4aec27066e6abb7cc7d15bd680b43fcd5a" } +toss = { git = "https://github.com/Garmelon/toss.git", rev = "333cf74fba56080043a13b9f55c0b62695e2fa4a" } diff --git a/cove-tui/src/main.rs b/cove-tui/src/main.rs index 1162517..f3e5aa9 100644 --- a/cove-tui/src/main.rs +++ b/cove-tui/src/main.rs @@ -8,6 +8,7 @@ use ui::Ui; #[tokio::main] async fn main() -> anyhow::Result<()> { let mut terminal = Terminal::new()?; + terminal.set_measuring(true); Ui::run(&mut terminal).await?; Ok(()) } diff --git a/cove-tui/src/ui.rs b/cove-tui/src/ui.rs index 17f8dc2..297a7e3 100644 --- a/cove-tui/src/ui.rs +++ b/cove-tui/src/ui.rs @@ -1,12 +1,16 @@ use std::collections::hash_map::Entry; +use std::sync::{Arc, Weak}; +use std::time::Duration; use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, MouseEvent}; use crossterm::style::ContentStyle; use futures::StreamExt; +use parking_lot::FairMutex; use tokio::sync::mpsc::error::TryRecvError; use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; +use tokio::task; use toss::frame::{Frame, Pos}; -use toss::terminal::{Redraw, Terminal}; +use toss::terminal::Terminal; #[derive(Debug)] pub enum UiEvent { @@ -24,26 +28,46 @@ pub struct Ui { } impl Ui { - fn new(event_tx: UnboundedSender) -> Self { - Self { event_tx } - } + const POLL_DURATION: Duration = Duration::from_millis(100); pub async fn run(terminal: &mut Terminal) -> anyhow::Result<()> { let (event_tx, event_rx) = mpsc::unbounded_channel(); - let mut ui = Self::new(event_tx.clone()); + let crossterm_lock = Arc::new(FairMutex::new(())); + // Prepare and start crossterm event polling task + let weak_crossterm_lock = Arc::downgrade(&crossterm_lock); + let event_tx_clone = event_tx.clone(); + let crossterm_event_task = task::spawn_blocking(|| { + Self::poll_crossterm_events(event_tx_clone, weak_crossterm_lock) + }); + + // Run main UI. + // + // If the run_main method exits at any point or if this `run` method is + // not awaited any more, the crossterm_lock Arc should be deallocated, + // meaning the crossterm_event_task will also stop after at most + // `Self::POLL_DURATION`. + // + // On the other hand, if the crossterm_event_task stops for any reason, + // the rest of the UI is also shut down and the client stops. + let mut ui = Self { event_tx }; let result = tokio::select! { - e = ui.run_main(terminal, event_tx.clone(), event_rx) => e, - e = Self::shovel_crossterm_events(event_tx) => e, + e = ui.run_main(terminal, event_rx, crossterm_lock) => e, + Ok(e) = crossterm_event_task => e, }; result } - async fn shovel_crossterm_events(tx: UnboundedSender) -> anyhow::Result<()> { - // Implemented manually because UnboundedSender doesn't implement the Sink trait - let mut stream = EventStream::new(); - while let Some(event) = stream.next().await { - tx.send(UiEvent::Term(event?))?; + fn poll_crossterm_events( + tx: UnboundedSender, + lock: Weak>, + ) -> anyhow::Result<()> { + while let Some(lock) = lock.upgrade() { + let _guard = lock.lock(); + if crossterm::event::poll(Self::POLL_DURATION)? { + let event = crossterm::event::read()?; + tx.send(UiEvent::Term(event))?; + } } Ok(()) } @@ -51,18 +75,23 @@ impl Ui { async fn run_main( &mut self, terminal: &mut Terminal, - event_tx: UnboundedSender, mut event_rx: UnboundedReceiver, + crossterm_lock: Arc>, ) -> anyhow::Result<()> { loop { // 1. Render current state terminal.autoresize()?; self.render(terminal.frame()).await?; - if terminal.present()? == Redraw::Required { - event_tx.send(UiEvent::Redraw); + terminal.present()?; + + // 2. Measure widths if required + if terminal.measuring_required() { + let _guard = crossterm_lock.lock(); + terminal.measure_widths()?; + self.event_tx.send(UiEvent::Redraw)?; } - // 2. Handle events (in batches) + // 3. Handle events (in batches) let mut event = match event_rx.recv().await { Some(event) => event, None => return Ok(()),