My personal-knowledge-system, with deeply integrated task tracking and long term goal planning capabilities.

feat/tui: init app #8

closed opened by suri.codes targeting main from tui
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:nxdjlcnomw44wvso6myag2e6/sh.tangled.repo.pull/3mgl5nipbnt22
+196 -3
Diff #0
+192
src/app.rs
··· 1 + use color_eyre::eyre::Result; 2 + use crossterm::event::KeyEvent; 3 + use ratatui::layout::Rect; 4 + use serde::{Deserialize, Serialize}; 5 + use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; 6 + use tracing::{debug, info}; 7 + 8 + use crate::{ 9 + components::Component, 10 + config::Config, 11 + signal::Signal, 12 + tui::{Event, Tui}, 13 + }; 14 + 15 + pub struct App { 16 + config: Config, 17 + tick_rate: f64, 18 + frame_rate: f64, 19 + components: Vec<Box<dyn Component>>, 20 + should_quit: bool, 21 + should_suspend: bool, 22 + #[allow(dead_code)] 23 + region: Region, 24 + last_tick_key_events: Vec<KeyEvent>, 25 + signal_tx: UnboundedSender<Signal>, 26 + signal_rx: UnboundedReceiver<Signal>, 27 + } 28 + 29 + /// The different regions of the application that the user can 30 + /// be interacting with. Think of these kind of like the highest class of 31 + /// components. 32 + #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 33 + pub enum Region { 34 + #[default] 35 + Home, 36 + } 37 + 38 + #[expect(dead_code)] 39 + impl App { 40 + /// Construct a new `App` instance. 41 + pub fn new(tick_rate: f64, frame_rate: f64) -> Self { 42 + let (signal_tx, signal_rx) = mpsc::unbounded_channel(); 43 + 44 + Self { 45 + tick_rate, 46 + frame_rate, 47 + components: vec![], 48 + should_quit: false, 49 + should_suspend: false, 50 + config: Config::new(), 51 + region: Region::default(), 52 + last_tick_key_events: Vec::new(), 53 + signal_tx, 54 + signal_rx, 55 + } 56 + } 57 + 58 + pub async fn run(&mut self) -> Result<()> { 59 + let mut tui = Tui::new()? 60 + .with_tick_rate(self.tick_rate) 61 + .with_frame_rate(self.frame_rate); 62 + tui.enter()?; 63 + 64 + for component in &mut self.components { 65 + component.register_signal_handler(self.signal_tx.clone())?; 66 + } 67 + for component in &mut self.components { 68 + component.register_config_handler(self.config.clone())?; 69 + } 70 + 71 + for component in &mut self.components { 72 + component.init(tui.size()?)?; 73 + } 74 + 75 + let signal_tx = self.signal_tx.clone(); 76 + 77 + loop { 78 + self.handle_events(&mut tui).await?; 79 + 80 + self.handle_signals(&mut tui).await?; 81 + if self.should_suspend { 82 + tui.suspend()?; 83 + 84 + // We are sending resume here because once its done suspending, 85 + // it will continue execution here. 86 + signal_tx.send(Signal::Resume)?; 87 + signal_tx.send(Signal::ClearScreen)?; 88 + tui.enter()?; 89 + } else if self.should_quit { 90 + tui.stop(); 91 + break; 92 + } 93 + } 94 + 95 + tui.exit()?; 96 + 97 + Ok(()) 98 + } 99 + 100 + async fn handle_events(&mut self, tui: &mut Tui) -> Result<()> { 101 + let Some(event) = tui.next_event().await else { 102 + return Ok(()); 103 + }; 104 + 105 + let signal_tx = self.signal_tx.clone(); 106 + 107 + match event { 108 + Event::Quit => signal_tx.send(Signal::Quit)?, 109 + Event::Tick => signal_tx.send(Signal::Tick)?, 110 + Event::Render => signal_tx.send(Signal::Render)?, 111 + Event::Resize(x, y) => signal_tx.send(Signal::Resize(x, y))?, 112 + Event::Key(key) => self.handle_key_event(key)?, 113 + 114 + _ => {} 115 + } 116 + 117 + for component in &mut self.components { 118 + if let Some(signal) = component.handle_events(Some(event.clone()))? { 119 + signal_tx.send(signal)?; 120 + } 121 + } 122 + 123 + Ok(()) 124 + } 125 + 126 + // We are okay with this because we know that this is the function signature, 127 + // we just haven't implemented the keyboard parsing logic just yet, revisit 128 + // this later. 129 + // 130 + // DO NOT LET THIS MERGE INTO MAIN WITH THIS CLIPPY IGNORES 131 + #[allow(clippy::needless_pass_by_ref_mut, clippy::unnecessary_wraps)] 132 + fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> { 133 + let _signal_tx = self.signal_tx.clone(); 134 + 135 + info!("key received: {key:#?}"); 136 + 137 + Ok(()) 138 + } 139 + 140 + async fn handle_signals(&mut self, tui: &mut Tui) -> Result<()> { 141 + while let Some(signal) = self.signal_rx.recv().await { 142 + if signal != Signal::Tick && signal != Signal::Render { 143 + debug!("App: handling signal: {signal:?}"); 144 + } 145 + 146 + match signal { 147 + Signal::Tick => { 148 + self.last_tick_key_events.drain(..); 149 + } 150 + 151 + Signal::Quit => self.should_quit = true, 152 + 153 + Signal::Suspend => self.should_suspend = true, 154 + 155 + Signal::Resume => self.should_suspend = false, 156 + 157 + Signal::ClearScreen => tui.terminal.clear()?, 158 + Signal::Resize(x, y) => self.handle_resize(tui, x, y)?, 159 + Signal::Render => self.render(tui)?, 160 + _ => {} 161 + } 162 + 163 + for component in &mut self.components { 164 + if let Some(signal) = component.update(signal.clone())? { 165 + self.signal_tx.send(signal)?; 166 + } 167 + } 168 + } 169 + Ok(()) 170 + } 171 + 172 + fn handle_resize(&mut self, tui: &mut Tui, w: u16, h: u16) -> Result<()> { 173 + tui.resize(Rect::new(0, 0, w, h))?; 174 + 175 + self.render(tui)?; 176 + Ok(()) 177 + } 178 + 179 + fn render(&mut self, tui: &mut Tui) -> Result<()> { 180 + tui.draw(|frame| { 181 + for component in &mut self.components { 182 + if let Err(err) = component.draw(frame, frame.area()) { 183 + let _ = self 184 + .signal_tx 185 + .send(Signal::Error(format!("Failed to draw: {err:?}"))); 186 + } 187 + } 188 + })?; 189 + 190 + Ok(()) 191 + } 192 + }
+1 -2
src/components/mod.rs
··· 11 11 /// 12 12 /// Implementers of this trait can be registered with the main application loop and will be able to 13 13 /// receive events, update state, and be rendered on the screen. 14 - #[expect(dead_code)] 15 - pub trait Component { 14 + pub trait Component: Send { 16 15 /// Register a signal handler that can send signals for processing if necessary. 17 16 /// 18 17 /// # Arguments
+1 -1
src/config.rs
··· 32 32 33 33 /// Configuration for the App 34 34 #[expect(dead_code)] 35 + #[derive(Debug, Clone)] 35 36 pub struct Config { 36 37 pub app_dirs: AppDirs, // pub data_dir: PathBuf, 37 38 // pub keybindings: KeyBindings, ··· 39 40 // pub styles: Styles, 40 41 } 41 42 42 - #[expect(dead_code)] 43 43 impl Config { 44 44 pub fn new() -> Self { 45 45 todo!()
+1
src/main.rs
··· 2 2 //! My (suri.codes) personal-knowledge-system, with deeply integrated task tracking and long term goal planning capabilities. 3 3 //! 4 4 5 + mod app; 5 6 mod components; 6 7 mod config; 7 8 mod errors;
+1
src/tui.rs
··· 25 25 26 26 /// Events processed by the whole application. 27 27 #[expect(dead_code)] 28 + #[derive(Debug, Clone)] 28 29 pub enum Event { 29 30 /// Application initialized 30 31 Init,

History

1 round 0 comments
sign up or login to add to the discussion
suri.codes submitted #0
1 commit
expand
feat/tui: init app
2/2 success
expand
expand 0 comments
closed without merging