Skip to main content

rich_interact/
push.rs

1//! A backend for push driving: the host owns the clock and the screen.
2//!
3//! [`EventLoop::run`](crate::EventLoop::run) blocks in [`Backend::read`]. A
4//! host that cannot block (a browser tab driving components from wasm,
5//! another program's event loop) feeds events with
6//! [`EventLoop::step`](crate::EventLoop::step) instead, and moves time on
7//! with [`EventLoop::pump`](crate::EventLoop::pump). A [`PushBackend`] is the
8//! backend for that: it never reads, its time is what the host last set
9//! through its [`PushPort`], and what the loop paints collects in the port
10//! for the host to write to its terminal (xterm.js, in the docs
11//! playground).
12//!
13//! ```
14//! use std::time::Duration;
15//! use rich_interact::{Event, EventLoop, Key, LoopOptions, PushBackend, Select};
16//!
17//! let backend = PushBackend::new(24, 4);
18//! let port = backend.port();
19//! let mut event_loop = EventLoop::new(backend, LoopOptions::default());
20//! event_loop.mount(Select::new("Pick", vec!["one".to_string(), "two".to_string()]));
21//! event_loop.pump().unwrap();
22//! let first = port.take_output();
23//! assert!(first.contains("one"));
24//! port.set_elapsed(Duration::from_millis(16));
25//! event_loop.step(Event::Key(Key::char('t'))).unwrap();
26//! assert!(port.take_output().contains('t'));
27//! ```
28
29use std::cell::RefCell;
30use std::io;
31use std::process::Command;
32use std::rc::Rc;
33use std::time::Duration;
34
35use crate::event::Event;
36use crate::session::Backend;
37
38struct State {
39    size: (u16, u16),
40    now: Duration,
41    output: String,
42    copies: Vec<String>,
43}
44
45/// A [`Backend`] a host pushes to: see the [module docs](self).
46///
47/// It paints on the alternate screen unless told otherwise
48/// ([`inline`](Self::inline)), never reads (driving it with
49/// [`EventLoop::run`](crate::EventLoop::run) fails with
50/// [`Unsupported`](io::ErrorKind::Unsupported)), has a clipboard whose
51/// copies the host takes ([`PushPort::take_copies`]), and runs no command:
52/// a hand-off returns at once with no exit code.
53pub struct PushBackend {
54    state: Rc<RefCell<State>>,
55    alternate_screen: bool,
56}
57
58/// The host's side of a [`PushBackend`]: set the time, take the output.
59/// Cheap to clone; every clone is the same port.
60#[derive(Clone)]
61pub struct PushPort {
62    state: Rc<RefCell<State>>,
63}
64
65impl PushBackend {
66    /// A `columns` x `rows` screen, at time zero.
67    pub fn new(columns: u16, rows: u16) -> PushBackend {
68        PushBackend {
69            state: Rc::new(RefCell::new(State {
70                size: (columns, rows),
71                now: Duration::ZERO,
72                output: String::new(),
73                copies: Vec::new(),
74            })),
75            alternate_screen: true,
76        }
77    }
78
79    /// Paint inline, from the row the host's cursor is on, instead of on
80    /// the alternate screen.
81    #[must_use]
82    pub fn inline(mut self) -> PushBackend {
83        self.alternate_screen = false;
84        self
85    }
86
87    /// The port the host keeps once the backend goes into an
88    /// [`EventLoop`](crate::EventLoop).
89    pub fn port(&self) -> PushPort {
90        PushPort {
91            state: Rc::clone(&self.state),
92        }
93    }
94}
95
96impl PushPort {
97    /// The time since the start, as the loop sees it: set it before each
98    /// [`step`](crate::EventLoop::step) and [`pump`](crate::EventLoop::pump).
99    /// Time does not go back: an earlier time than the last is ignored.
100    pub fn set_elapsed(&self, now: Duration) {
101        let mut state = self.state.borrow_mut();
102        state.now = state.now.max(now);
103    }
104
105    /// The time last set.
106    pub fn elapsed(&self) -> Duration {
107        self.state.borrow().now
108    }
109
110    /// Everything painted since the last call: bytes for the terminal.
111    pub fn take_output(&self) -> String {
112        std::mem::take(&mut self.state.borrow_mut().output)
113    }
114
115    /// What components put on the clipboard since the last call, in order.
116    pub fn take_copies(&self) -> Vec<String> {
117        std::mem::take(&mut self.state.borrow_mut().copies)
118    }
119
120    /// Keep the screen's size: the loop learns it from the
121    /// [`Event::Resize`] the host steps with.
122    pub fn set_size(&self, columns: u16, rows: u16) {
123        self.state.borrow_mut().size = (columns, rows);
124    }
125}
126
127impl Backend for PushBackend {
128    fn size(&self) -> (u16, u16) {
129        self.state.borrow().size
130    }
131
132    fn read(&mut self, _timeout: Option<Duration>) -> io::Result<Option<Event>> {
133        Err(io::Error::new(
134            io::ErrorKind::Unsupported,
135            "a push backend is not read: drive the loop with EventLoop::step and pump",
136        ))
137    }
138
139    fn write(&mut self, text: &str) -> io::Result<()> {
140        self.state.borrow_mut().output.push_str(text);
141        Ok(())
142    }
143
144    fn elapsed(&self) -> Duration {
145        self.state.borrow().now
146    }
147
148    fn handoff(&mut self, _command: &mut Command) -> io::Result<Option<i32>> {
149        Ok(None)
150    }
151
152    fn alternate_screen(&self) -> bool {
153        self.alternate_screen
154    }
155
156    fn clipboard(&self) -> Result<(), String> {
157        Ok(())
158    }
159
160    fn copy(&mut self, text: &str) -> io::Result<()> {
161        self.state.borrow_mut().copies.push(text.to_string());
162        Ok(())
163    }
164}