use std::io::Stdout;
use std::time::Duration;
use ratatui::backend::{Backend, CrosstermBackend};
use super::Runtime;
use super::config::RuntimeConfig;
use crate::app::command::Command;
use crate::app::model::App;
use crate::backend::CaptureBackend;
use crate::error;
pub struct RuntimeBuilder<A: App, B: Backend> {
backend: B,
state: Option<(A::State, Command<A::Message>)>,
config: Option<RuntimeConfig>,
}
impl<A: App, B: Backend> RuntimeBuilder<A, B> {
pub(crate) fn new(backend: B) -> Self {
Self {
backend,
state: None,
config: None,
}
}
pub fn state(mut self, state: A::State, init_cmd: Command<A::Message>) -> Self {
self.state = Some((state, init_cmd));
self
}
pub fn config(mut self, config: RuntimeConfig) -> Self {
self.config = Some(config);
self
}
pub fn tick_rate(mut self, rate: Duration) -> Self {
self.config_mut().tick_rate = rate;
self
}
pub fn frame_rate(mut self, rate: Duration) -> Self {
self.config_mut().frame_rate = rate;
self
}
pub fn max_messages(mut self, max: usize) -> Self {
self.config_mut().max_messages_per_tick = max;
self
}
pub fn channel_capacity(mut self, capacity: usize) -> Self {
self.config_mut().message_channel_capacity = capacity;
self
}
pub fn build(self) -> error::Result<Runtime<A, B>> {
let config = self.config.unwrap_or_default();
let (state, init_cmd) = self.state.unwrap_or_else(A::init);
Runtime::with_backend_state_and_config(self.backend, state, init_cmd, config)
}
fn config_mut(&mut self) -> &mut RuntimeConfig {
self.config.get_or_insert_with(RuntimeConfig::default)
}
}
impl<A: App, B: Backend> Runtime<A, B> {
pub fn builder(backend: B) -> RuntimeBuilder<A, B> {
RuntimeBuilder::new(backend)
}
}
impl<A: App> Runtime<A, CrosstermBackend<Stdout>> {
pub fn terminal_builder() -> error::Result<RuntimeBuilder<A, CrosstermBackend<Stdout>>> {
let config = RuntimeConfig::default();
let backend = Self::setup_terminal(&config)?;
Ok(RuntimeBuilder::new(backend))
}
}
impl<A: App> Runtime<A, CaptureBackend> {
pub fn virtual_builder(width: u16, height: u16) -> RuntimeBuilder<A, CaptureBackend> {
let backend = CaptureBackend::new(width, height);
RuntimeBuilder::new(backend)
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::app::command::Command;
use crate::app::model::App;
use ratatui::widgets::Paragraph;
struct TestApp;
#[derive(Clone, Default)]
struct TestState {
count: i32,
quit: bool,
}
#[derive(Clone, Debug)]
enum TestMsg {
Increment,
Quit,
}
impl App for TestApp {
type State = TestState;
type Message = TestMsg;
fn init() -> (Self::State, Command<Self::Message>) {
(TestState::default(), Command::none())
}
fn update(state: &mut Self::State, msg: Self::Message) -> Command<Self::Message> {
match msg {
TestMsg::Increment => state.count += 1,
TestMsg::Quit => state.quit = true,
}
Command::none()
}
fn view(state: &Self::State, frame: &mut ratatui::Frame) {
let text = format!("Count: {}", state.count);
frame.render_widget(Paragraph::new(text), frame.area());
}
fn should_quit(state: &Self::State) -> bool {
state.quit
}
}
#[test]
fn test_builder_with_capture_backend() {
let backend = CaptureBackend::new(80, 24);
let runtime = Runtime::<TestApp, _>::builder(backend).build().unwrap();
assert_eq!(runtime.state().count, 0);
}
#[test]
fn test_builder_with_state() {
let backend = CaptureBackend::new(80, 24);
let state = TestState {
count: 42,
quit: false,
};
let runtime = Runtime::<TestApp, _>::builder(backend)
.state(state, Command::none())
.build()
.unwrap();
assert_eq!(runtime.state().count, 42);
}
#[test]
fn test_builder_with_config() {
let backend = CaptureBackend::new(80, 24);
let config = RuntimeConfig::new()
.tick_rate(Duration::from_millis(100))
.max_messages(50);
let runtime = Runtime::<TestApp, _>::builder(backend)
.config(config)
.build()
.unwrap();
assert_eq!(runtime.state().count, 0);
}
#[test]
fn test_builder_with_state_and_config() {
let backend = CaptureBackend::new(80, 24);
let state = TestState {
count: 7,
quit: false,
};
let config = RuntimeConfig::new().tick_rate(Duration::from_millis(200));
let runtime = Runtime::<TestApp, _>::builder(backend)
.state(state, Command::none())
.config(config)
.build()
.unwrap();
assert_eq!(runtime.state().count, 7);
}
#[test]
fn test_virtual_builder_default() {
let runtime = Runtime::<TestApp, _>::virtual_builder(80, 24)
.build()
.unwrap();
assert_eq!(runtime.state().count, 0);
}
#[test]
fn test_virtual_builder_with_state() {
let state = TestState {
count: 99,
quit: false,
};
let runtime = Runtime::<TestApp, _>::virtual_builder(80, 24)
.state(state, Command::none())
.build()
.unwrap();
assert_eq!(runtime.state().count, 99);
}
#[test]
fn test_virtual_builder_with_tick_rate() {
let mut runtime = Runtime::<TestApp, _>::virtual_builder(80, 24)
.tick_rate(Duration::from_millis(200))
.build()
.unwrap();
runtime.dispatch(TestMsg::Increment);
assert_eq!(runtime.state().count, 1);
}
#[test]
fn test_virtual_builder_with_frame_rate() {
let mut runtime = Runtime::<TestApp, _>::virtual_builder(80, 24)
.frame_rate(Duration::from_millis(32))
.build()
.unwrap();
runtime.tick().unwrap();
assert!(runtime.contains_text("Count: 0"));
}
#[test]
fn test_virtual_builder_with_max_messages() {
let runtime = Runtime::<TestApp, _>::virtual_builder(80, 24)
.max_messages(50)
.build()
.unwrap();
assert_eq!(runtime.state().count, 0);
}
#[test]
fn test_virtual_builder_with_channel_capacity() {
let runtime = Runtime::<TestApp, _>::virtual_builder(80, 24)
.channel_capacity(512)
.build()
.unwrap();
assert_eq!(runtime.state().count, 0);
}
#[test]
fn test_virtual_builder_chained_config() {
let mut runtime = Runtime::<TestApp, _>::virtual_builder(80, 24)
.tick_rate(Duration::from_millis(100))
.frame_rate(Duration::from_millis(32))
.max_messages(50)
.channel_capacity(512)
.build()
.unwrap();
runtime.dispatch(TestMsg::Increment);
runtime.dispatch(TestMsg::Increment);
runtime.tick().unwrap();
assert_eq!(runtime.state().count, 2);
assert!(runtime.contains_text("Count: 2"));
}
#[test]
fn test_virtual_builder_state_and_config() {
let state = TestState {
count: 10,
quit: false,
};
let mut runtime = Runtime::<TestApp, _>::virtual_builder(80, 24)
.state(state, Command::none())
.tick_rate(Duration::from_millis(100))
.build()
.unwrap();
assert_eq!(runtime.state().count, 10);
runtime.dispatch(TestMsg::Increment);
assert_eq!(runtime.state().count, 11);
}
#[test]
fn test_virtual_builder_config_overrides_individual_settings() {
let config = RuntimeConfig::new().tick_rate(Duration::from_millis(200));
let runtime = Runtime::<TestApp, _>::virtual_builder(80, 24)
.tick_rate(Duration::from_millis(50)) .config(config)
.build()
.unwrap();
assert_eq!(runtime.state().count, 0);
}
#[test]
fn test_virtual_builder_individual_settings_override_config() {
let config = RuntimeConfig::new().tick_rate(Duration::from_millis(200));
let runtime = Runtime::<TestApp, _>::virtual_builder(80, 24)
.config(config)
.tick_rate(Duration::from_millis(50)) .build()
.unwrap();
assert_eq!(runtime.state().count, 0);
}
#[test]
fn test_built_runtime_dispatch_and_render() {
let mut runtime = Runtime::<TestApp, _>::virtual_builder(40, 10)
.build()
.unwrap();
runtime.dispatch(TestMsg::Increment);
runtime.dispatch(TestMsg::Increment);
runtime.render().unwrap();
assert!(runtime.contains_text("Count: 2"));
}
#[test]
fn test_built_runtime_quit() {
let mut runtime = Runtime::<TestApp, _>::virtual_builder(80, 24)
.build()
.unwrap();
assert!(!runtime.should_quit());
runtime.dispatch(TestMsg::Quit);
runtime.tick().unwrap();
assert!(runtime.should_quit());
}
#[test]
fn test_built_runtime_send_and_tick() {
use crate::input::Event;
struct EventApp;
#[derive(Clone, Default)]
struct EventState {
events: u32,
}
#[derive(Clone)]
enum EventMsg {
KeyPressed,
}
impl App for EventApp {
type State = EventState;
type Message = EventMsg;
fn init() -> (Self::State, Command<Self::Message>) {
(EventState::default(), Command::none())
}
fn update(state: &mut Self::State, msg: Self::Message) -> Command<Self::Message> {
match msg {
EventMsg::KeyPressed => state.events += 1,
}
Command::none()
}
fn view(state: &Self::State, frame: &mut ratatui::Frame) {
let text = format!("Events: {}", state.events);
frame.render_widget(Paragraph::new(text), frame.area());
}
fn handle_event(event: &crate::input::Event) -> Option<Self::Message> {
if event.as_key().is_some() {
Some(EventMsg::KeyPressed)
} else {
None
}
}
}
let mut runtime = Runtime::<EventApp, _>::virtual_builder(80, 24)
.build()
.unwrap();
runtime.send(Event::char('a'));
runtime.send(Event::char('b'));
runtime.tick().unwrap();
assert_eq!(runtime.state().events, 2);
}
#[test]
fn test_built_runtime_with_init_state_skips_app_init() {
struct InitApp;
#[derive(Clone, Default)]
struct InitState {
source: String,
}
#[derive(Clone)]
enum InitMsg {}
impl App for InitApp {
type State = InitState;
type Message = InitMsg;
fn init() -> (Self::State, Command<Self::Message>) {
(
InitState {
source: "App::init".into(),
},
Command::none(),
)
}
fn update(_state: &mut Self::State, _msg: Self::Message) -> Command<Self::Message> {
Command::none()
}
fn view(_state: &Self::State, _frame: &mut ratatui::Frame) {}
}
let runtime = Runtime::<InitApp, _>::virtual_builder(80, 24)
.build()
.unwrap();
assert_eq!(runtime.state().source, "App::init");
let state = InitState {
source: "external".into(),
};
let runtime = Runtime::<InitApp, _>::virtual_builder(80, 24)
.state(state, Command::none())
.build()
.unwrap();
assert_eq!(runtime.state().source, "external");
}
}