narju/io/headless.rs
1//! Scripted I/O for tests and embeddings.
2//!
3//! `HeadlessIo` replays a fixed list of input lines and records
4//! everything printed. It is the standard way to drive a session
5//! programmatically: an entire interactive transcript — including a
6//! full Purple boot, whose REPL loop is itself a `read` loop in
7//! Pink — can run under it with no terminal at all.
8
9use std::cell::RefCell;
10use std::rc::Rc;
11
12use super::NarjuIo;
13use crate::error::NarjuError;
14
15/// A [`NarjuIo`] that reads from a pre-set script and writes to a
16/// buffer.
17///
18/// `read_line` pops the next scripted line, then reports EOF forever;
19/// `print` appends to [`output`](Self::output) (and, if constructed via
20/// [`new_shared`](Self::new_shared), to a shared sink readable after
21/// the box has been consumed).
22pub struct HeadlessIo {
23 /// Scripted input lines, replayed in order.
24 input: Vec<String>,
25 /// Index of the next unread line of `input`.
26 cursor: usize,
27 /// Everything printed so far, one entry per `print` call.
28 pub output: Vec<String>,
29 /// Optional shared handle onto the printed output; see
30 /// [`new_shared`](Self::new_shared).
31 sink: Option<Rc<RefCell<Vec<String>>>>,
32}
33
34impl HeadlessIo {
35 /// Construct with the given input script and an empty output
36 /// buffer.
37 pub fn new(input: Vec<String>) -> Self {
38 Self {
39 input,
40 cursor: 0,
41 output: Vec::new(),
42 sink: None,
43 }
44 }
45
46 /// Construct with no input at all: `read_line` reports EOF
47 /// immediately. Sufficient for code that never calls `read`.
48 pub fn empty() -> Self {
49 Self::new(vec![])
50 }
51
52 /// Like `new`, but also returns a shared handle to the print output, so
53 /// callers can read what a session printed after the `Box<dyn NarjuIo>`
54 /// has been consumed by `TopLevel`/`EvalCtx`.
55 pub fn new_shared(input: Vec<String>) -> (Self, Rc<RefCell<Vec<String>>>) {
56 let sink = Rc::new(RefCell::new(Vec::new()));
57 let mut io = Self::new(input);
58 io.sink = Some(Rc::clone(&sink));
59 (io, sink)
60 }
61
62 /// Take everything printed so far, leaving the buffer empty.
63 pub fn drain_output(&mut self) -> Vec<String> {
64 std::mem::take(&mut self.output)
65 }
66}
67
68impl NarjuIo for HeadlessIo {
69 fn read_line(&mut self, _prompt: &str) -> Result<Option<String>, NarjuError> {
70 if self.cursor >= self.input.len() {
71 Ok(None) // EOF
72 } else {
73 let line = self.input[self.cursor].clone();
74 self.cursor += 1;
75 Ok(Some(line))
76 }
77 }
78
79 fn print(&mut self, s: &str) -> Result<(), NarjuError> {
80 self.output.push(s.to_string());
81 if let Some(sink) = &self.sink {
82 sink.borrow_mut().push(s.to_string());
83 }
84 Ok(())
85 }
86}