use std::cell::RefCell;
use std::collections::VecDeque;
use std::io;
use std::process::Command;
use std::rc::Rc;
use std::time::Duration;
use crate::component::Component;
use crate::event::{Button, Event, Key, Mouse, MouseKind};
use crate::event_loop::{EventLoop, LoopOptions, Outcome};
use crate::session::Backend;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Step {
Event(Event),
Wait(Duration),
}
#[derive(Clone, Debug, Default)]
pub struct Script(pub VecDeque<Step>);
impl Script {
pub fn new() -> Script {
Script::default()
}
pub fn keys(mut self, names: &str) -> Script {
for name in names.split_whitespace() {
let key = Key::parse(name).unwrap_or_else(|| panic!("unknown key {name:?}"));
self.0.push_back(Step::Event(Event::Key(key)));
}
self
}
pub fn text(mut self, text: &str) -> Script {
for c in text.chars() {
self.0.push_back(Step::Event(Event::Key(Key::char(c))));
}
self
}
pub fn event(mut self, event: Event) -> Script {
self.0.push_back(Step::Event(event));
self
}
pub fn wait(mut self, duration: Duration) -> Script {
self.0.push_back(Step::Wait(duration));
self
}
pub fn resize(self, columns: u16, rows: u16) -> Script {
self.event(Event::Resize { columns, rows })
}
pub fn mouse(self, kind: MouseKind, column: u16, row: u16) -> Script {
self.event(Event::Mouse(Mouse::new(kind, column, row)))
}
pub fn click(self, column: u16, row: u16) -> Script {
self.mouse(MouseKind::Down(Button::Left), column, row)
.mouse(MouseKind::Up(Button::Left), column, row)
}
pub fn drag(self, from: (u16, u16), to: (u16, u16)) -> Script {
let mut script = self.mouse(MouseKind::Down(Button::Left), from.0, from.1);
let step: i32 = if to.0 >= from.0 { 1 } else { -1 };
let mut column = i32::from(from.0);
while column != i32::from(to.0) {
column += step;
let row = if column == i32::from(to.0) {
to.1
} else {
from.1
};
script = script.mouse(MouseKind::Drag(Button::Left), column as u16, row);
}
script.mouse(MouseKind::Up(Button::Left), to.0, to.1)
}
pub fn scroll(self, down: bool, column: u16, row: u16) -> Script {
let kind = if down {
MouseKind::ScrollDown
} else {
MouseKind::ScrollUp
};
self.mouse(kind, column, row)
}
}
#[derive(Clone, Debug, Default)]
pub struct Record {
pub writes: Vec<String>,
pub frames: Vec<String>,
pub handoffs: Vec<String>,
pub suspends: usize,
pub copies: Vec<String>,
}
impl Record {
pub fn output(&self) -> String {
self.writes.concat()
}
pub fn last_frame(&self) -> &str {
self.frames.last().map_or("", String::as_str)
}
}
pub struct Headless {
script: VecDeque<Step>,
clock: Duration,
size: (u16, u16),
record: Rc<RefCell<Record>>,
pub handoff_code: Option<i32>,
pub suspendable: bool,
pub clipboard: bool,
}
impl Headless {
pub fn new(script: Script, columns: u16, rows: u16) -> Headless {
Headless {
script: script.0,
clock: Duration::ZERO,
size: (columns, rows),
record: Rc::new(RefCell::new(Record::default())),
handoff_code: Some(0),
suspendable: false,
clipboard: true,
}
}
pub fn record(&self) -> Rc<RefCell<Record>> {
Rc::clone(&self.record)
}
}
impl Backend for Headless {
fn size(&self) -> (u16, u16) {
self.size
}
fn read(&mut self, timeout: Option<Duration>) -> io::Result<Option<Event>> {
loop {
match self.script.front_mut() {
None => {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"the headless script ended before the components finished",
))
}
Some(Step::Wait(left)) => match timeout {
Some(timeout) if timeout <= *left => {
self.clock += timeout;
*left -= timeout;
if left.is_zero() {
self.script.pop_front();
}
return Ok(None);
}
_ => {
self.clock += *left;
self.script.pop_front();
}
},
Some(Step::Event(_)) => {
let Some(Step::Event(event)) = self.script.pop_front() else {
unreachable!()
};
if let Event::Resize { columns, rows } = event {
self.size = (columns, rows);
}
return Ok(Some(event));
}
}
}
}
fn write(&mut self, text: &str) -> io::Result<()> {
self.record.borrow_mut().writes.push(text.to_string());
Ok(())
}
fn elapsed(&self) -> Duration {
self.clock
}
fn handoff(&mut self, command: &mut Command) -> io::Result<Option<i32>> {
let program = command.get_program().to_string_lossy().into_owned();
self.record.borrow_mut().handoffs.push(program);
Ok(self.handoff_code)
}
fn alternate_screen(&self) -> bool {
false
}
fn painted(&mut self, text: &str) {
self.record.borrow_mut().frames.push(text.to_string());
}
fn can_suspend(&self) -> bool {
self.suspendable
}
fn suspend(&mut self) -> io::Result<()> {
self.record.borrow_mut().suspends += 1;
Ok(())
}
fn clipboard(&self) -> Result<(), String> {
if self.clipboard {
Ok(())
} else {
Err("the headless clipboard is off".into())
}
}
fn copy(&mut self, text: &str) -> io::Result<()> {
self.record.borrow_mut().copies.push(text.to_string());
Ok(())
}
}
pub fn run<C: Component>(
component: C,
script: Script,
columns: u16,
rows: u16,
) -> (io::Result<Outcome<C::Output>>, Record) {
let backend = Headless::new(script, columns, rows);
let record = backend.record();
let mut event_loop = EventLoop::new(backend, LoopOptions::default());
let handle = event_loop.mount(component);
let result = event_loop
.run()
.map(|()| handle.take().expect("a finished component has an outcome"));
drop(event_loop);
let record = record.borrow().clone();
(result, record)
}