use std::io; use std::sync::mpsc; use std::time::Duration; use crate::block::Block; use crate::input::{self, Key, RawMode}; use crate::terminal; use crate::view::View; /// A Component is a pure state machine combined with a renderer. /// Uses closures (matching Gleam's record-of-functions pattern). pub struct Component { pub state: S, pub render: Box Block>, pub update: Box Update>, } /// The result of processing a key event in a component. pub enum Update { /// Key handled, new state -- keep the event loop running Continue(S), /// Yield a value while continuing (for multi-step flows) Emit(S, R), /// Component is done -- return this final value Finish(R), /// User cancelled (Escape/CtrlC) Cancel, /// Key not handled by this component (for focus management) Ignore, } /// Errors that can occur when running a component. #[derive(Debug)] pub enum RunError { /// User pressed Escape or Ctrl+C Cancelled, /// Error reading from the terminal InputError(io::Error), } impl Component { pub fn new( state: S, render: impl Fn(&S) -> Block + 'static, update: impl Fn(&S, &Key) -> Update + 'static, ) -> Self { Component { state, render: Box::new(render), update: Box::new(update), } } /// Run this component's interactive event loop. /// Installs a SIGWINCH handler so that terminal resizes cause an immediate /// redraw at the new width. pub fn run(self, mode: &RawMode) -> Result { let width = terminal::get_size() .map(|(cols, _)| cols as usize) .unwrap_or(80); terminal::install_resize_handler(); self.run_with_width(mode, width) } /// Run with an explicit rendering width. pub fn run_with_width(self, mode: &RawMode, width: usize) -> Result { let Component { state, render, update, } = self; let initial_block = (render)(&state); let mut view = View::create(&initial_block, width); let result = event_loop(&render, &update, state, &mut view, mode); match &result { Ok(_) => view.finish(), Err(RunError::Cancelled) => view.erase(), Err(RunError::InputError(_)) => view.finish(), } result } /// Run with periodic tick updates (for animated components like spinners). pub fn run_animated( self, mode: &RawMode, tick_ms: u64, on_tick: impl Fn(S) -> S, ) -> Result { let width = terminal::get_size() .map(|(cols, _)| cols as usize) .unwrap_or(80); let Component { state, render, update, } = self; let initial_block = (render)(&state); let mut view = View::create(&initial_block, width); let result = animated_loop(&render, &update, state, &mut view, mode, tick_ms, &on_tick); match &result { Ok(_) => view.finish(), Err(RunError::Cancelled) => view.erase(), Err(RunError::InputError(_)) => view.finish(), } result } /// Run with a message channel. The event loop multiplexes between stdin /// keypresses and messages from background threads. /// /// `on_msg` is called for each received message and produces an `Update`, /// just like the `update` function for keypresses. /// /// Uses a short `read_key_timeout` interval (10 ms) to poll the channel /// between key reads, keeping message latency low without busy-spinning. pub fn run_with_channel( self, mode: &RawMode, rx: mpsc::Receiver, on_msg: impl Fn(&S, Msg) -> Update + 'static, ) -> Result { let width = terminal::get_size() .map(|(cols, _)| cols as usize) .unwrap_or(80); terminal::install_resize_handler(); let Component { state, render, update, } = self; let initial_block = (render)(&state); let mut view = View::create(&initial_block, width); let result = channel_loop(&render, &update, &on_msg, state, &mut view, mode, &rx); match &result { Ok(_) => view.finish(), Err(RunError::Cancelled) => view.erase(), Err(RunError::InputError(_)) => view.finish(), } result } /// Transform the result type of this component. pub fn map(self, f: impl Fn(R) -> R2 + 'static) -> Component where S: 'static, R: 'static, { let update = self.update; Component { state: self.state, render: self.render, update: Box::new(move |state, key| match (update)(state, key) { Update::Continue(s) => Update::Continue(s), Update::Emit(s, r) => Update::Emit(s, f(r)), Update::Finish(r) => Update::Finish(f(r)), Update::Cancel => Update::Cancel, Update::Ignore => Update::Ignore, }), } } /// Set the initial state. pub fn with_state(self, state: S) -> Self { Component { state, ..self } } } fn event_loop( render: &dyn Fn(&S) -> Block, update: &dyn Fn(&S, &Key) -> Update, mut state: S, view: &mut View, mode: &RawMode, ) -> Result { loop { // Check for terminal resize before blocking on the next key if terminal::take_resize() { let new_width = terminal::get_size() .map(|(c, _)| c as usize) .unwrap_or(view.width()); let block = (render)(&state); view.resize(&block, new_width); } let key = input::read_key(mode).map_err(RunError::InputError)?; match (update)(&state, &key) { Update::Continue(new_state) => { state = new_state; let block = (render)(&state); view.update(&block); } Update::Emit(_, result) => return Ok(result), Update::Finish(result) => return Ok(result), Update::Cancel => return Err(RunError::Cancelled), Update::Ignore => {} } } } fn channel_loop( render: &dyn Fn(&S) -> Block, update: &dyn Fn(&S, &Key) -> Update, on_msg: &dyn Fn(&S, Msg) -> Update, mut state: S, view: &mut View, mode: &RawMode, rx: &mpsc::Receiver, ) -> Result { // Poll interval — short enough for responsive messages, not a busy spin. let poll_ms = Duration::from_millis(10); loop { // Handle terminal resize if terminal::take_resize() { let new_width = terminal::get_size() .map(|(c, _)| c as usize) .unwrap_or(view.width()); let block = (render)(&state); view.resize(&block, new_width); } // Drain all pending channel messages loop { match rx.try_recv() { Ok(msg) => match on_msg(&state, msg) { Update::Continue(new_state) => { state = new_state; let block = (render)(&state); view.update(&block); } Update::Emit(_, result) => return Ok(result), Update::Finish(result) => return Ok(result), Update::Cancel => return Err(RunError::Cancelled), Update::Ignore => {} }, Err(mpsc::TryRecvError::Empty) => break, Err(mpsc::TryRecvError::Disconnected) => break, } } // Wait briefly for a keypress match input::read_key_timeout(mode, poll_ms).map_err(RunError::InputError)? { None => {} // timeout — loop again to check channel Some(key) => match (update)(&state, &key) { Update::Continue(new_state) => { state = new_state; let block = (render)(&state); view.update(&block); } Update::Emit(_, result) => return Ok(result), Update::Finish(result) => return Ok(result), Update::Cancel => return Err(RunError::Cancelled), Update::Ignore => {} }, } } } fn animated_loop( render: &dyn Fn(&S) -> Block, update: &dyn Fn(&S, &Key) -> Update, mut state: S, view: &mut View, mode: &RawMode, tick_ms: u64, on_tick: &dyn Fn(S) -> S, ) -> Result { let timeout = Duration::from_millis(tick_ms); loop { // Handle terminal resize if terminal::take_resize() { let new_width = terminal::get_size() .map(|(c, _)| c as usize) .unwrap_or(view.width()); let block = (render)(&state); view.resize(&block, new_width); } match input::read_key_timeout(mode, timeout).map_err(RunError::InputError)? { None => { state = on_tick(state); let block = (render)(&state); view.update(&block); } Some(key) => match (update)(&state, &key) { Update::Continue(new_state) => { state = new_state; let block = (render)(&state); view.update(&block); } Update::Emit(_, result) => return Ok(result), Update::Finish(result) => return Ok(result), Update::Cancel => return Err(RunError::Cancelled), Update::Ignore => {} }, } } }