use super::runtime::RuntimeHandle;
use super::scope::Scope;
use crate::element::Element;
use crate::input::poll_key;
use crate::renderer::Blaeck;
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use std::io::{self, Write};
use std::time::Duration;
#[derive(Clone)]
pub struct ReactiveAppConfig {
pub poll_interval: Duration,
pub exit_on_ctrl_c: bool,
}
impl Default for ReactiveAppConfig {
fn default() -> Self {
Self {
poll_interval: Duration::from_millis(50),
exit_on_ctrl_c: true,
}
}
}
pub struct ReactiveAppResult {
pub exit_reason: ReactiveExitReason,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReactiveExitReason {
UserExit,
Completed,
}
pub struct ReactiveApp<W: Write> {
runtime: RuntimeHandle,
blaeck: Blaeck<W>,
config: ReactiveAppConfig,
should_exit: bool,
exit_reason: ReactiveExitReason,
}
impl ReactiveApp<io::Stdout> {
pub fn run<F>(component: F) -> io::Result<ReactiveAppResult>
where
F: Fn(Scope) -> Element,
{
Self::run_with_config(component, ReactiveAppConfig::default())
}
pub fn run_with_config<F>(
component: F,
config: ReactiveAppConfig,
) -> io::Result<ReactiveAppResult>
where
F: Fn(Scope) -> Element,
{
let app = Self::new(config)?;
app.run_component(component)
}
pub fn new(config: ReactiveAppConfig) -> io::Result<Self> {
Self::with_writer(io::stdout(), config)
}
}
impl<W: Write> ReactiveApp<W> {
pub fn with_writer(writer: W, config: ReactiveAppConfig) -> io::Result<Self> {
let runtime = RuntimeHandle::new();
let blaeck = Blaeck::new(writer)?;
Ok(Self {
runtime,
blaeck,
config,
should_exit: false,
exit_reason: ReactiveExitReason::Completed,
})
}
pub fn exit(&mut self) {
self.should_exit = true;
self.exit_reason = ReactiveExitReason::UserExit;
}
pub fn runtime(&self) -> &RuntimeHandle {
&self.runtime
}
pub fn blaeck(&self) -> &Blaeck<W> {
&self.blaeck
}
pub fn blaeck_mut(&mut self) -> &mut Blaeck<W> {
&mut self.blaeck
}
fn run_component<F>(mut self, component: F) -> io::Result<ReactiveAppResult>
where
F: Fn(Scope) -> Element,
{
let root_id = self.runtime.create_instance();
enable_raw_mode()?;
let scope = Scope::new(self.runtime.clone(), root_id);
self.runtime.set_current_instance(Some(root_id));
self.runtime.reset_hook_cursor(root_id);
let element = component(scope);
self.runtime.set_current_instance(None);
self.blaeck.render(element)?;
self.runtime.clear_dirty();
while !self.should_exit {
if let Some(key) = poll_key(self.config.poll_interval)? {
if self.config.exit_on_ctrl_c && key.is_ctrl_c() {
self.should_exit = true;
self.exit_reason = ReactiveExitReason::UserExit;
break;
}
self.runtime.dispatch_input(&key);
}
if self.runtime.needs_render() {
let scope = Scope::new(self.runtime.clone(), root_id);
self.runtime.set_current_instance(Some(root_id));
self.runtime.reset_hook_cursor(root_id);
let element = component(scope);
self.runtime.set_current_instance(None);
self.blaeck.render(element)?;
self.runtime.clear_dirty();
}
}
disable_raw_mode()?;
self.blaeck.unmount()?;
Ok(ReactiveAppResult {
exit_reason: self.exit_reason,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_default() {
let config = ReactiveAppConfig::default();
assert_eq!(config.poll_interval, Duration::from_millis(50));
assert!(config.exit_on_ctrl_c);
}
#[test]
fn test_config_custom() {
let config = ReactiveAppConfig {
poll_interval: Duration::from_millis(100),
exit_on_ctrl_c: false,
};
assert_eq!(config.poll_interval, Duration::from_millis(100));
assert!(!config.exit_on_ctrl_c);
}
#[test]
fn test_exit_reason_eq() {
assert_eq!(ReactiveExitReason::UserExit, ReactiveExitReason::UserExit);
assert_ne!(ReactiveExitReason::UserExit, ReactiveExitReason::Completed);
}
#[test]
fn test_exit_reason_clone() {
let reason = ReactiveExitReason::Completed;
let cloned = reason.clone();
assert_eq!(reason, cloned);
}
#[test]
fn test_with_writer() {
let buf = Vec::new();
let config = ReactiveAppConfig::default();
let app = ReactiveApp::with_writer(buf, config);
assert!(app.is_ok());
}
#[test]
fn test_runtime_access() {
let buf = Vec::new();
let config = ReactiveAppConfig::default();
let app = ReactiveApp::with_writer(buf, config).unwrap();
let _rt = app.runtime();
}
#[test]
fn test_blaeck_access() {
let buf = Vec::new();
let config = ReactiveAppConfig::default();
let app = ReactiveApp::with_writer(buf, config).unwrap();
let _width = app.blaeck().width();
}
#[test]
fn test_config_clone() {
let config = ReactiveAppConfig::default();
let cloned = config.clone();
assert_eq!(config.poll_interval, cloned.poll_interval);
assert_eq!(config.exit_on_ctrl_c, cloned.exit_on_ctrl_c);
}
#[test]
fn test_two_runtimes_coexist() {
let buf1 = Vec::new();
let buf2 = Vec::new();
let config = ReactiveAppConfig::default();
let app1 = ReactiveApp::with_writer(buf1, config.clone()).unwrap();
let app2 = ReactiveApp::with_writer(buf2, config).unwrap();
let rt1 = app1.runtime();
let rt2 = app2.runtime();
let signal1 = rt1.create_signal(100i32);
let signal2 = rt2.create_signal(200i32);
assert_eq!(rt1.get_signal::<i32>(signal1), 100);
assert_eq!(rt2.get_signal::<i32>(signal2), 200);
rt1.set_signal(signal1, 999);
assert_eq!(rt1.get_signal::<i32>(signal1), 999);
assert_eq!(rt2.get_signal::<i32>(signal2), 200); }
}