use std::io::Stdout;
use std::marker::PhantomData;
use std::time::Duration;
use ratatui::backend::{Backend, CrosstermBackend};
use super::Runtime;
use super::builder_configured::ConfiguredRuntimeBuilder;
use super::config::RuntimeConfig;
use crate::app::model::App;
use crate::backend::CaptureBackend;
use crate::error;
pub struct RuntimeBuilder<A: App, B: Backend> {
backend: B,
config: Option<RuntimeConfig>,
_phantom: PhantomData<A>,
}
impl<A: App, B: Backend> RuntimeBuilder<A, B> {
pub(crate) fn new(backend: B) -> Self {
Self {
backend,
config: None,
_phantom: PhantomData,
}
}
pub fn with_args(self, args: A::Args) -> ConfiguredRuntimeBuilder<A, B> {
ConfiguredRuntimeBuilder {
backend: self.backend,
config: self.config,
args,
}
}
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
}
fn config_mut(&mut self) -> &mut RuntimeConfig {
self.config.get_or_insert_with(RuntimeConfig::default)
}
}
impl<A: App, B: Backend> RuntimeBuilder<A, B>
where
A::Args: crate::app::OptionalArgs,
{
pub fn build(self) -> error::Result<Runtime<A, B>> {
use crate::app::model::optional_args::sealed::Sealed;
let args = <A::Args as Sealed>::default_optional_args();
self.with_args(args).build()
}
}
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;
type Args = ();
fn init(_args: ()) -> (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
}
}
struct ArgsApp;
impl App for ArgsApp {
type State = TestState;
type Message = TestMsg;
type Args = TestState;
fn init(args: TestState) -> (Self::State, Command<Self::Message>) {
(args, 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_args() {
let backend = CaptureBackend::new(80, 24);
let state = TestState {
count: 42,
quit: false,
};
let runtime = Runtime::<ArgsApp, _>::builder(backend)
.with_args(state)
.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_args_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::<ArgsApp, _>::builder(backend)
.with_args(state)
.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_args() {
let state = TestState {
count: 99,
quit: false,
};
let runtime = Runtime::<ArgsApp, _>::virtual_builder(80, 24)
.with_args(state)
.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_args_and_config() {
let state = TestState {
count: 10,
quit: false,
};
let mut runtime = Runtime::<ArgsApp, _>::virtual_builder(80, 24)
.with_args(state)
.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;
type Args = ();
fn init(_args: ()) -> (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_args_uses_provided_state() {
struct InitApp;
#[derive(Clone, Default)]
struct InitState {
source: String,
}
#[derive(Clone)]
enum InitMsg {}
impl App for InitApp {
type State = InitState;
type Message = InitMsg;
type Args = String;
fn init(args: String) -> (Self::State, Command<Self::Message>) {
(InitState { source: args }, 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)
.with_args("from args".into())
.build()
.unwrap();
assert_eq!(runtime.state().source, "from args");
}
#[test]
fn test_tick_rate_preserved_across_with_args() {
let custom_tick = Duration::from_millis(123);
let runtime = Runtime::<TestApp, _>::virtual_builder(80, 24)
.tick_rate(custom_tick) .with_args(()) .build()
.unwrap();
assert_eq!(runtime.config().tick_rate, custom_tick);
}
#[test]
fn test_tick_rate_set_after_with_args_is_preserved() {
let custom_tick = Duration::from_millis(456);
let runtime = Runtime::<TestApp, _>::virtual_builder(80, 24)
.with_args(())
.tick_rate(custom_tick) .build()
.unwrap();
assert_eq!(runtime.config().tick_rate, custom_tick);
}
}