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}