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}