1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
use crate::os_glue::Glue;
use crate::{Features, Key, TermOut};
use stakker::{fwd, timer_max, Fwd, MaxTimerKey, Share, CX};
use std::error::Error;
use std::mem;
use std::panic::PanicInfo;
use std::sync::Arc;
use std::time::Duration;
/// Actor that manages the connection to the terminal
pub struct Terminal {
resize: Fwd<Option<Share<TermOut>>>,
input: Fwd<Key>,
termout: Share<TermOut>,
glue: Glue,
disable_output: bool,
paused: bool,
inbuf: Vec<u8>,
check_enable: bool,
force_timer: MaxTimerKey,
check_timer: MaxTimerKey,
cleanup: Vec<u8>,
panic_hook: Arc<Box<dyn Fn(&PanicInfo<'_>) + 'static + Sync + Send>>,
}
impl Terminal {
/// Set up the terminal. Sends a message back to `resize`
/// immediately, which provides a reference to the shared
/// [`TermOut`] which is used to buffer and flush terminal output
/// data.
///
/// Whenever the window size changes, a new `resize` message is
/// sent. When the terminal output is paused, `None` is sent to
/// `resize` to let the app know that there is no output available
/// right now.
///
/// Input keys received are sent to `input` once decoded.
///
/// In case of an error that can't be handled, cleans up the
/// terminal state and terminates the actor with
/// `ActorDied::Failed`. The actor that created the terminal can
/// catch that and do whatever cleanup is necessary before
/// aborting the process.
///
/// # Panic handling
///
/// When Rust panics, the terminal must be restored to its normal
/// state otherwise things would be left in a bad state for the
/// user (in cooked mode with no echo, requiring the user to
/// blindly type `reset` on the command-line). So this code saves
/// a copy of the current panic handler (using
/// `std::panic::take_hook`), and then installs its own handler
/// that does terminal cleanup before calling on to the saved
/// panic handler. This mean that if any custom panic handler is
/// needed by the application, then it must be set up before the
/// call to [`Terminal::init`].
///
/// [`TermOut`]: struct.TermOut.html
pub fn init(cx: CX![], resize: Fwd<Option<Share<TermOut>>>, input: Fwd<Key>) -> Option<Self> {
// TODO: Query TERM/terminfo/environment for features to put in Features
let features = Features { colour_256: false };
let term = cx.this().clone();
let glue = match Glue::new(cx, term) {
Ok(v) => v,
Err(e) => {
cx.fail(e);
return None;
}
};
let termout = Share::new(cx, TermOut::new(features));
let mut this = Self {
resize,
input,
termout,
glue,
disable_output: false,
paused: false,
inbuf: Vec::new(),
check_enable: false,
force_timer: MaxTimerKey::default(),
check_timer: MaxTimerKey::default(),
cleanup: b"\x1Bc".to_vec(),
panic_hook: Arc::new(std::panic::take_hook()),
};
this.handle_resize(cx);
this.update_panic_hook();
Some(this)
}
/// Enable or disable generation of the [`Key::Check`] keypress,
/// which occurs in a gap in typing, 300ms after the last key
/// pressed. This may be used to do validation if that's too
/// expensive to do on every keypress.
///
/// [`Key::Check`]: enum.Key.html#variant.Check
pub fn check(&mut self, _cx: CX![], enable: bool) {
self.check_enable = enable;
}
/// Ring the bell (i.e. beep) immediately. Doesn't wait for the
/// buffered terminal data to be flushed. Will output even when
/// paused.
pub fn bell(&mut self, cx: CX![]) {
if !self.disable_output {
if let Err(e) = self.glue.write(&b"\x07"[..]) {
self.disable_output = true;
self.failure(cx, e);
}
}
}
/// Pause terminal input and output handling. Sends the cleanup
/// sequence to the terminal, and switches to cooked mode. Sends
/// a `resize` message with `None` to tell the app that output is
/// disabled.
///
/// This call should be used before forking off a process which
/// might prompt the user and receive user input, otherwise this
/// process would compete with the sub-process for user input.
/// Resume after the subprocess has finished with the `resume`
/// call.
pub fn pause(&mut self, cx: CX![]) {
if !self.paused {
fwd!([self.resize], None);
self.glue.input(false);
self.termout.rw(cx).discard();
self.termout.rw(cx).bytes(&self.cleanup[..]);
self.termout.rw(cx).flush();
self.flush(cx);
self.paused = true;
self.update_panic_hook();
}
}
/// Resume terminal output and input handling. Switches to raw
/// mode and sends a resize message to trigger a full redraw.
pub fn resume(&mut self, cx: CX![]) {
if self.paused {
self.paused = false;
self.glue.input(true);
self.termout.rw(cx).discard();
self.handle_resize(cx);
self.update_panic_hook();
}
}
// Handle an unrecoverable failure. Try to clean up before
// terminating the actor.
fn failure(&mut self, cx: CX![], e: impl Error + 'static) {
self.pause(cx);
cx.fail(e);
}
/// Flush to the terminal all the data that's ready for sending
/// from the TermOut buffer. Use [`TermOut::flush`] first to mark
/// the point up to which data should be flushed.
///
/// [`TermOut::flush`]: struct.TermOut.html#method.flush
pub fn flush(&mut self, cx: CX![]) {
if self.termout.rw(cx).new_cleanup.is_some() {
// Don't replace unless we're sure there's a new value
if let Some(cleanup) = mem::replace(&mut self.termout.rw(cx).new_cleanup, None) {
self.cleanup = cleanup;
self.update_panic_hook();
}
}
if !self.disable_output {
if self.paused {
// Just drop the output whilst paused. We'll trigger
// a full refresh on resuming
self.termout.rw(cx).drain_flush();
} else {
let ob = self.termout.rw(cx);
let result = self.glue.write(ob.data_to_flush());
ob.drain_flush();
if let Err(e) = result {
self.disable_output = true;
self.failure(cx, e);
}
}
}
}
/// Handle a resize event from the TTY. Gets new size, and
/// notifies upstream.
pub(crate) fn handle_resize(&mut self, cx: CX![]) {
match self.glue.get_size() {
Ok((sy, sx)) => {
self.termout.rw(cx).set_size(sy, sx);
fwd!([self.resize], Some(self.termout.clone()));
}
Err(e) => self.failure(cx, e),
}
}
/// Handle an I/O error on the TTY input
pub(crate) fn handle_error_in(&mut self, cx: CX![], err: std::io::Error) {
self.failure(cx, err);
}
/// Handle new bytes from the TTY input
pub(crate) fn handle_data_in(&mut self, cx: CX![]) {
self.glue.read_data(&mut self.inbuf);
self.do_data_in(cx, false);
}
fn do_data_in(&mut self, cx: CX![], force: bool) {
let mut pos = 0;
let len = self.inbuf.len();
if len != 0 {
if !force {
// Note that this is too fast to catch M-Esc passed
// through screen, as that seems to apply a 300ms
// pause between the two Esc chars. For everything
// else including real terminals it should be okay.
timer_max!(
&mut self.force_timer,
cx.now() + Duration::from_millis(100),
[cx],
do_data_in(true)
);
}
while pos < len {
match Key::decode(&self.inbuf[pos..len], force) {
None => break,
Some((count, key)) => {
pos += count;
fwd!([self.input], key);
if self.check_enable {
let check_expiry = cx.now() + Duration::from_millis(300);
timer_max!(&mut self.check_timer, check_expiry, [cx], check_key());
}
}
}
}
}
self.inbuf.drain(..pos);
}
fn check_key(&mut self, _cx: CX![]) {
if self.check_enable {
fwd!([self.input], Key::Check);
}
}
// Install a panic hook that (if necessary) outputs the current
// cleanup string, restores cooked mode and then does the default
// panic action (e.g. dump out backtrace). This should be called
// every time we switch to/from raw mode, and every time the
// cleanup string is changed.
fn update_panic_hook(&mut self) {
// Discard old hook
let _ = std::panic::take_hook();
let defhook = self.panic_hook.clone();
if self.paused {
std::panic::set_hook(Box::new(move |info| defhook(info)));
} else {
let cleanup_fn = self.glue.cleanup_fn();
let cleanup = self.cleanup.clone();
std::panic::set_hook(Box::new(move |info| {
cleanup_fn(&cleanup[..]);
defhook(info);
}));
}
}
}
impl Drop for Terminal {
fn drop(&mut self) {
// Drop panic hook and clean up terminal
let _ = std::panic::take_hook();
if !self.paused {
self.glue.cleanup_fn()(&self.cleanup[..]);
}
}
}