From 52c48bb21f166bd5d8b12854af1a6d80788b5a01 Mon Sep 17 00:00:00 2001 From: alexpasmantier Date: Wed, 18 Sep 2024 00:01:16 +0200 Subject: [PATCH] refactoring in progress --- Cargo.lock | 12 +++++ Cargo.toml | 1 + src/app.rs | 125 +++++++++++++++++++++++++--------------------- src/components.rs | 2 +- src/tui.rs | 10 ++++ 5 files changed, 92 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9989fd4..23a1fa9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2213,6 +2213,7 @@ dependencies = [ "strip-ansi-escapes", "strum", "tokio", + "tokio-stream", "tokio-util", "tracing", "tracing-error", @@ -2359,6 +2360,17 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "tokio-stream" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.12" diff --git a/Cargo.toml b/Cargo.toml index fda9075..d60ed06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ signal-hook = "0.3.17" strip-ansi-escapes = "0.2.0" strum = { version = "0.26.3", features = ["derive"] } tokio = { version = "1.39.3", features = ["full"] } +tokio-stream = "0.1.16" tokio-util = "0.7.11" tracing = "0.1.40" tracing-error = "0.2.0" diff --git a/src/app.rs b/src/app.rs index ad1034b..450073f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -25,13 +25,13 @@ /// │ Render component │ │ Update component │ /// └──────────────────┘ └──────────────────┘ /// -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use color_eyre::Result; use crossterm::event::KeyEvent; use ratatui::prelude::Rect; use serde::{Deserialize, Serialize}; -use tokio::sync::{mpsc, Mutex}; +use tokio::sync::mpsc; use tracing::{debug, info}; use crate::{ @@ -52,6 +52,8 @@ pub struct App { last_tick_key_events: Vec, action_tx: mpsc::UnboundedSender, action_rx: mpsc::UnboundedReceiver, + render_tx: mpsc::UnboundedSender, + render_rx: mpsc::UnboundedReceiver, } #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -63,6 +65,7 @@ pub enum Mode { impl App { pub fn new(tick_rate: f64, frame_rate: f64) -> Result { let (action_tx, action_rx) = mpsc::unbounded_channel(); + let (render_tx, mut render_rx) = mpsc::unbounded_channel(); Ok(Self { tick_rate, frame_rate, @@ -77,51 +80,23 @@ impl App { last_tick_key_events: Vec::new(), action_tx, action_rx, + render_tx, + render_rx, }) } pub async fn run(&mut self) -> Result<()> { - // Rendering loop - let (render_tx, mut render_rx) = mpsc::unbounded_channel(); + let mut tui = Tui::new()? + .tick_rate(self.tick_rate) + .frame_rate(self.frame_rate); + // this starts the event handling loop in Tui + tui.enter(); + // Rendering loop tokio::spawn(async move { - let tui = Tui::new()? - .tick_rate(self.tick_rate) - .frame_rate(self.frame_rate); - tui.enter(); - - let components = self.components.clone(); - for component in self.components.lock().await.iter_mut() { - component.register_action_handler(self.action_tx.clone())?; - } - for component in self.components.lock().await.iter_mut() { - component.register_config_handler(self.config.clone())?; - } - for component in self.components.lock().await.iter_mut() { - component.init(tui.size()?)?; - } - - loop { - // add - if let Some(_) = render_rx.recv().await { - let c = components.lock().await; - tui.terminal.draw(|frame| { - for component in c.iter() { - if let Err(err) = component.draw(frame, frame.area()) { - let _ = self - .action_tx - .send(Action::Error(format!("Failed to draw: {:?}", err))); - } - } - })?; - } - } - - tui.exit() + self.render(tui).await; }); - // Event handling loop - // Action handling loop let action_tx = self.action_tx.clone(); loop { @@ -141,6 +116,8 @@ impl App { Ok(()) } + /// Handle incoming events and produce actions if necessary. These actions are then sent to the + /// action channel for processing. async fn handle_events(&mut self, tui: &mut Tui) -> Result<()> { let Some(event) = tui.next_event().await else { return Ok(()); @@ -154,7 +131,7 @@ impl App { Event::Key(key) => self.handle_key_event(key)?, _ => {} } - for component in self.components.iter_mut() { + for component in self.components.lock().unwrap().iter_mut() { if let Some(action) = component.handle_events(Some(event.clone()))? { action_tx.send(action)?; } @@ -200,11 +177,14 @@ impl App { Action::Suspend => self.should_suspend = true, Action::Resume => self.should_suspend = false, Action::ClearScreen => tui.terminal.clear()?, - Action::Resize(w, h) => self.handle_resize(tui, w, h)?, - Action::Render => self.render(tui)?, + // This needs to send a particular RenderingMessage to the rendering task + // in order to do: + // >> tui.resize(Rect::new(0, 0, w, h))?; + Action::Resize(w, h) => self.render_tx.send(RenderingTask::Resize(w, h))?, + Action::Render => self.render_tx.send(RenderingTask::Render)?, _ => {} } - for component in self.components.iter_mut() { + for component in self.components.lock().unwrap().iter_mut() { if let Some(action) = component.update(action.clone())? { self.action_tx.send(action)? }; @@ -213,22 +193,53 @@ impl App { Ok(()) } - fn handle_resize(&mut self, tui: &mut Tui, w: u16, h: u16) -> Result<()> { - tui.resize(Rect::new(0, 0, w, h))?; - self.render(tui)?; - Ok(()) - } + async fn render(&mut self, tui: Tui) -> Result<()> { + let components = self.components.clone(); + for component in components.lock().unwrap().iter_mut() { + component.register_action_handler(self.action_tx.clone()); + } + for component in components.lock().unwrap().iter_mut() { + component.register_config_handler(self.config.clone()); + } + for component in components.lock().unwrap().iter_mut() { + component.init(tui.size().unwrap()); + } - fn render(&mut self, tui: &mut Tui) -> Result<()> { - tui.draw(|frame| { - for component in self.components.iter_mut() { - if let Err(err) = component.draw(frame, frame.area()) { - let _ = self - .action_tx - .send(Action::Error(format!("Failed to draw: {:?}", err))); + // Rendering loop + loop { + if let Some(task) = self.render_rx.recv().await { + match task { + RenderingTask::ClearScreen => { + tui.terminal.clear()?; + } + RenderingTask::Render => { + let mut c = components.lock().unwrap(); + tui.terminal.draw(|frame| { + for component in c.iter_mut() { + if let Err(err) = component.draw(frame, frame.area()) { + let _ = self + .action_tx + .send(Action::Error(format!("Failed to draw: {:?}", err))); + } + } + }); + } + RenderingTask::Resize(w, h) => { + tui.resize(Rect::new(0, 0, w, h))?; + let _ = self.action_tx.send(Action::Render); + } + RenderingTask::Quit => { + break Ok(()); + } } } - })?; - Ok(()) + } } } + +enum RenderingTask { + ClearScreen, + Render, + Resize(u16, u16), + Quit, +} diff --git a/src/components.rs b/src/components.rs index 84c12c9..e500658 100644 --- a/src/components.rs +++ b/src/components.rs @@ -15,7 +15,7 @@ pub mod home; /// /// Implementors of this trait can be registered with the main application loop and will be able to /// receive events, update state, and be rendered on the screen. -pub trait Component { +pub trait Component: Send { /// Register an action handler that can send actions for processing if necessary. /// /// # Arguments diff --git a/src/tui.rs b/src/tui.rs index 9796e8d..ca83a36 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -104,6 +104,16 @@ impl Tui { }); } + // TODO: I think we can decouple the event loop from the Tui struct to better + // separate responsibilities and avoid having to share Tui references between + // the event loop and the rendering loop. + // + // Goals: + // - Have a dedicated event loop independent of the Tui struct + // - Events should be directly translated into actions inside the event loop + // and then sent to the action handling loop + // - Have a dedicated rendering loop that can own the Tui struct + // Et voilà async fn event_loop( event_tx: UnboundedSender, cancellation_token: CancellationToken,