use crate::backend::HeadlessBackend;
use crate::buffer::Buffer;
use crate::component::Component;
use crate::event::{Event, Key, KeyEvent, Modifiers};
use crate::runtime::Runtime;
use crate::Result;
pub struct Pilot {
runtime: Runtime<HeadlessBackend>,
}
impl Pilot {
pub fn new(root: impl Component + 'static, width: u16, height: u16) -> Self {
let backend = HeadlessBackend::new(width, height);
let mut runtime = Runtime::new(root, backend)
.expect("headless runtime creation")
.tick_rate(std::time::Duration::from_millis(10)); runtime.initial_layout_and_paint().expect("initial layout/paint");
runtime.process_timer_requests();
Self { runtime }
}
pub fn press(&mut self, key: Key) -> Result<()> {
self.send_event(Event::Key(KeyEvent {
key,
modifiers: Modifiers::default(),
}))
}
pub fn press_with_modifiers(&mut self, key: Key, modifiers: Modifiers) -> Result<()> {
self.send_event(Event::Key(KeyEvent { key, modifiers }))
}
pub fn send_event(&mut self, event: Event) -> Result<()> {
self.runtime.backend.push_event(event);
self.runtime.step()
}
pub fn send_events(&mut self, events: impl IntoIterator<Item = Event>) -> Result<()> {
for event in events {
self.send_event(event)?;
}
Ok(())
}
pub fn run_until_quit(&mut self, max_steps: usize) -> Result<bool> {
for _ in 0..max_steps {
if self.runtime.quit {
return Ok(true);
}
self.runtime.step()?;
}
Ok(self.runtime.quit)
}
pub fn run_until(
&mut self,
max_steps: usize,
mut condition: impl FnMut(&Buffer) -> bool,
) -> Result<bool> {
for _ in 0..max_steps {
if condition(&self.runtime.front) {
return Ok(true);
}
self.runtime.step()?;
}
Ok(condition(&self.runtime.front))
}
pub fn frame(&self) -> &Buffer {
&self.runtime.front
}
pub fn has_quit(&self) -> bool {
self.runtime.quit
}
pub fn focus_first(&mut self) {
self.runtime.focus_next(false);
let _ = self.runtime.paint_frame();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::EventPhase;
use crate::render::RenderCx;
use crate::style::Style;
use crate::widgets::Label;
struct TestApp {
label_text: String,
}
impl TestApp {
fn new(text: &str) -> Self {
Self {
label_text: text.to_string(),
}
}
}
impl Component for TestApp {
fn render(&self, cx: &mut RenderCx) {
cx.line(&self.label_text);
}
fn event(&mut self, event: &Event, cx: &mut crate::component::EventCx) {
if cx.phase() != EventPhase::Target {
return;
}
if event.is_key(Key::Char('q')) {
cx.quit();
}
}
}
#[test]
fn test_pilot_basic_render() {
let pilot = Pilot::new(TestApp::new("hello"), 10, 3);
assert_eq!(&pilot.frame().cells[0].symbol, "h");
}
#[test]
fn test_pilot_press_key() {
let mut pilot = Pilot::new(TestApp::new("test"), 10, 3);
assert!(!pilot.has_quit());
pilot.press(Key::Char('q')).unwrap();
assert!(pilot.has_quit());
}
#[test]
fn test_pilot_label_renders() {
let pilot = Pilot::new(Label::new("Hello World").style(Style::default()), 20, 3);
assert!(pilot.frame().cells.iter().any(|c| c.symbol == "H"));
}
#[test]
fn test_pilot_run_until_quit() {
let mut pilot = Pilot::new(TestApp::new("x"), 10, 3);
pilot.press(Key::Char('q')).unwrap();
let quit = pilot.run_until_quit(10).unwrap();
assert!(quit);
}
#[test]
fn test_pilot_run_until() {
let mut pilot = Pilot::new(TestApp::new("data"), 10, 3);
let found = pilot
.run_until(10, |buf| buf.cells.iter().any(|c| c.symbol == "d"))
.unwrap();
assert!(found);
}
#[test]
fn test_pilot_input_form() {
use crate::widgets::Checkbox;
let mut pilot = Pilot::new(Checkbox::new("Option"), 20, 3);
let has_check = pilot.frame().cells.iter().any(|c| c.symbol == " ");
assert!(has_check, "should show unchecked marker");
pilot.press(Key::Char(' ')).unwrap();
let has_check = pilot.frame().cells.iter().any(|c| c.symbol == "✓");
assert!(has_check, "should show checked marker after toggle");
}
}