···11+use color_eyre::eyre::Result;
22+use crossterm::event::KeyEvent;
33+use ratatui::layout::Rect;
44+use serde::{Deserialize, Serialize};
55+use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
66+use tracing::{debug, info};
77+88+use crate::{
99+ components::Component,
1010+ config::Config,
1111+ signal::Signal,
1212+ tui::{Event, Tui},
1313+};
1414+1515+pub struct App {
1616+ config: Config,
1717+ tick_rate: f64,
1818+ frame_rate: f64,
1919+ components: Vec<Box<dyn Component>>,
2020+ should_quit: bool,
2121+ should_suspend: bool,
2222+ #[allow(dead_code)]
2323+ region: Region,
2424+ last_tick_key_events: Vec<KeyEvent>,
2525+ signal_tx: UnboundedSender<Signal>,
2626+ signal_rx: UnboundedReceiver<Signal>,
2727+}
2828+2929+/// The different regions of the application that the user can
3030+/// be interacting with. Think of these kind of like the highest class of
3131+/// components.
3232+#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3333+pub enum Region {
3434+ #[default]
3535+ Home,
3636+}
3737+3838+#[expect(dead_code)]
3939+impl App {
4040+ /// Construct a new `App` instance.
4141+ pub fn new(tick_rate: f64, frame_rate: f64) -> Self {
4242+ let (signal_tx, signal_rx) = mpsc::unbounded_channel();
4343+4444+ Self {
4545+ tick_rate,
4646+ frame_rate,
4747+ components: vec![],
4848+ should_quit: false,
4949+ should_suspend: false,
5050+ config: Config::new(),
5151+ region: Region::default(),
5252+ last_tick_key_events: Vec::new(),
5353+ signal_tx,
5454+ signal_rx,
5555+ }
5656+ }
5757+5858+ pub async fn run(&mut self) -> Result<()> {
5959+ let mut tui = Tui::new()?
6060+ .with_tick_rate(self.tick_rate)
6161+ .with_frame_rate(self.frame_rate);
6262+ tui.enter()?;
6363+6464+ for component in &mut self.components {
6565+ component.register_signal_handler(self.signal_tx.clone())?;
6666+ }
6767+ for component in &mut self.components {
6868+ component.register_config_handler(self.config.clone())?;
6969+ }
7070+7171+ for component in &mut self.components {
7272+ component.init(tui.size()?)?;
7373+ }
7474+7575+ let signal_tx = self.signal_tx.clone();
7676+7777+ loop {
7878+ self.handle_events(&mut tui).await?;
7979+8080+ self.handle_signals(&mut tui).await?;
8181+ if self.should_suspend {
8282+ tui.suspend()?;
8383+8484+ // We are sending resume here because once its done suspending,
8585+ // it will continue execution here.
8686+ signal_tx.send(Signal::Resume)?;
8787+ signal_tx.send(Signal::ClearScreen)?;
8888+ tui.enter()?;
8989+ } else if self.should_quit {
9090+ tui.stop();
9191+ break;
9292+ }
9393+ }
9494+9595+ tui.exit()?;
9696+9797+ Ok(())
9898+ }
9999+100100+ async fn handle_events(&mut self, tui: &mut Tui) -> Result<()> {
101101+ let Some(event) = tui.next_event().await else {
102102+ return Ok(());
103103+ };
104104+105105+ let signal_tx = self.signal_tx.clone();
106106+107107+ match event {
108108+ Event::Quit => signal_tx.send(Signal::Quit)?,
109109+ Event::Tick => signal_tx.send(Signal::Tick)?,
110110+ Event::Render => signal_tx.send(Signal::Render)?,
111111+ Event::Resize(x, y) => signal_tx.send(Signal::Resize(x, y))?,
112112+ Event::Key(key) => self.handle_key_event(key)?,
113113+114114+ _ => {}
115115+ }
116116+117117+ for component in &mut self.components {
118118+ if let Some(signal) = component.handle_events(Some(event.clone()))? {
119119+ signal_tx.send(signal)?;
120120+ }
121121+ }
122122+123123+ Ok(())
124124+ }
125125+126126+ // We are okay with this because we know that this is the function signature,
127127+ // we just haven't implemented the keyboard parsing logic just yet, revisit
128128+ // this later.
129129+ //
130130+ // DO NOT LET THIS MERGE INTO MAIN WITH THIS CLIPPY IGNORES
131131+ #[allow(clippy::needless_pass_by_ref_mut, clippy::unnecessary_wraps)]
132132+ fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> {
133133+ let _signal_tx = self.signal_tx.clone();
134134+135135+ info!("key received: {key:#?}");
136136+137137+ Ok(())
138138+ }
139139+140140+ async fn handle_signals(&mut self, tui: &mut Tui) -> Result<()> {
141141+ while let Some(signal) = self.signal_rx.recv().await {
142142+ if signal != Signal::Tick && signal != Signal::Render {
143143+ debug!("App: handling signal: {signal:?}");
144144+ }
145145+146146+ match signal {
147147+ Signal::Tick => {
148148+ self.last_tick_key_events.drain(..);
149149+ }
150150+151151+ Signal::Quit => self.should_quit = true,
152152+153153+ Signal::Suspend => self.should_suspend = true,
154154+155155+ Signal::Resume => self.should_suspend = false,
156156+157157+ Signal::ClearScreen => tui.terminal.clear()?,
158158+ Signal::Resize(x, y) => self.handle_resize(tui, x, y)?,
159159+ Signal::Render => self.render(tui)?,
160160+ _ => {}
161161+ }
162162+163163+ for component in &mut self.components {
164164+ if let Some(signal) = component.update(signal.clone())? {
165165+ self.signal_tx.send(signal)?;
166166+ }
167167+ }
168168+ }
169169+ Ok(())
170170+ }
171171+172172+ fn handle_resize(&mut self, tui: &mut Tui, w: u16, h: u16) -> Result<()> {
173173+ tui.resize(Rect::new(0, 0, w, h))?;
174174+175175+ self.render(tui)?;
176176+ Ok(())
177177+ }
178178+179179+ fn render(&mut self, tui: &mut Tui) -> Result<()> {
180180+ tui.draw(|frame| {
181181+ for component in &mut self.components {
182182+ if let Err(err) = component.draw(frame, frame.area()) {
183183+ let _ = self
184184+ .signal_tx
185185+ .send(Signal::Error(format!("Failed to draw: {err:?}")));
186186+ }
187187+ }
188188+ })?;
189189+190190+ Ok(())
191191+ }
192192+}
+1-2
src/components/mod.rs
···1111///
1212/// Implementers of this trait can be registered with the main application loop and will be able to
1313/// receive events, update state, and be rendered on the screen.
1414-#[expect(dead_code)]
1515-pub trait Component {
1414+pub trait Component: Send {
1615 /// Register a signal handler that can send signals for processing if necessary.
1716 ///
1817 /// # Arguments
···22//! My (suri.codes) personal-knowledge-system, with deeply integrated task tracking and long term goal planning capabilities.
33//!
4455+mod app;
56mod components;
67mod config;
78mod errors;
+1
src/tui.rs
···25252626/// Events processed by the whole application.
2727#[expect(dead_code)]
2828+#[derive(Debug, Clone)]
2829pub enum Event {
2930 /// Application initialized
3031 Init,