1use std::collections::VecDeque;
4use std::io::{Read, Write};
5use std::path::Path;
6use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
7use std::thread;
8use std::time::{Duration, Instant};
9
10use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize};
11
12use crate::screen::{Snapshot, Theme};
13use crate::terminal::Terminal;
14
15pub const FRAME_RATE: f64 = 12.0;
18pub const MAX_VIDEO: Duration = Duration::from_secs(300);
21const MAX_BATCH: usize = 32 * 1024;
25const MAX_SEEN: usize = 16 * 1024 * 1024;
27
28#[derive(Clone, Debug, PartialEq, Eq)]
30pub enum Event {
31 Output(String),
33 Input(String),
35 Resize { columns: u16, rows: u16 },
37}
38
39#[derive(Clone, Debug, Default)]
41pub struct Timeline {
42 pub events: Vec<(f64, Event)>,
43 pub frames: Vec<(f64, Snapshot)>,
45 pub keys: Vec<(f64, String)>,
47 pub truncated: bool,
50}
51
52struct State {
53 terminal: Terminal,
54 theme: Theme,
55 hidden: bool,
56 hidden_since: Instant,
57 hidden_total: Duration,
58 start: Instant,
59 utf8: Vec<u8>,
60 seen: VecDeque<String>,
64 seen_bytes: usize,
65 batch: String,
67 overflow: bool,
69 pending: Option<f64>,
70 last_frame: Option<f64>,
72 timeline: Timeline,
73 alive: bool,
74 error: Option<String>,
76}
77
78impl State {
79 fn new(rows: u16, columns: u16, theme: Theme) -> State {
81 let now = Instant::now();
82 State {
83 terminal: Terminal::new(rows, columns),
84 theme,
85 hidden: true,
86 hidden_since: now,
87 hidden_total: Duration::ZERO,
88 start: now,
89 utf8: Vec::new(),
90 seen: VecDeque::new(),
91 seen_bytes: 0,
92 batch: String::new(),
93 overflow: false,
94 pending: None,
95 last_frame: None,
96 timeline: Timeline::default(),
97 alive: true,
98 error: None,
99 }
100 }
101
102 fn resize(&mut self, columns: u16, rows: u16) -> std::io::Result<()> {
105 if !self.hidden {
106 self.flush();
107 }
108 if let Err(error) = self.terminal.set_size(rows, columns) {
109 self.error = Some(error.to_string());
110 return Err(error);
111 }
112 if !self.hidden {
113 let t = self.now();
114 self.timeline
115 .events
116 .push((t, Event::Resize { columns, rows }));
117 self.frame(t);
118 }
119 Ok(())
120 }
121
122 fn now(&self) -> f64 {
123 (self.start.elapsed() - self.hidden_total).as_secs_f64()
124 }
125
126 fn frame(&mut self, t: f64) {
127 self.last_frame = Some(t);
128 if t > MAX_VIDEO.as_secs_f64() {
129 self.timeline.truncated = true;
130 return;
131 }
132 let snapshot = self.terminal.snapshot(&self.theme);
133 if self
134 .timeline
135 .frames
136 .last()
137 .is_some_and(|(_, last)| *last == snapshot)
138 {
139 return;
140 }
141 self.timeline.frames.push((t, snapshot));
142 }
143
144 fn see(&mut self) {
146 let screen = self.terminal.contents();
147 if self.seen.back() == Some(&screen) {
148 return;
149 }
150 self.seen_bytes += screen.len();
151 self.seen.push_back(screen);
152 while self.seen_bytes > MAX_SEEN && self.seen.len() > 1 {
153 let old = self.seen.pop_front().expect("more than one screen");
154 self.seen_bytes -= old.len();
155 }
156 }
157
158 fn output(&mut self, t: f64, bytes: &[u8]) {
162 self.utf8.extend_from_slice(bytes);
163 let text = self.decode();
164 if !self.overflow {
165 self.batch.push_str(&text);
166 if self.batch.len() > MAX_BATCH {
167 self.overflow = true;
168 self.batch = String::new();
169 }
170 }
171 self.pending = Some(t);
172 if self
173 .last_frame
174 .is_none_or(|last| t - last >= 1.0 / FRAME_RATE)
175 {
176 self.flush();
177 }
178 }
179
180 fn flush(&mut self) {
182 let Some(t) = self.pending.take() else {
183 return;
184 };
185 if self.overflow {
186 self.overflow = false;
189 let snapshot = self.terminal.snapshot(&self.theme);
190 let repaint = crate::render::cast::repaint(&snapshot, &self.theme);
191 self.timeline.events.push((t, Event::Output(repaint)));
192 } else if !self.batch.is_empty() {
193 let text = std::mem::take(&mut self.batch);
194 self.timeline.events.push((t, Event::Output(text)));
195 }
196 self.frame(t);
197 }
198
199 fn decode(&mut self) -> String {
201 let valid = match std::str::from_utf8(&self.utf8) {
202 Ok(_) => self.utf8.len(),
203 Err(e) if e.error_len().is_none() => e.valid_up_to(),
205 Err(_) => self.utf8.len(),
206 };
207 let text = String::from_utf8_lossy(&self.utf8[..valid]).into_owned();
208 self.utf8.drain(..valid);
209 text
210 }
211}
212
213pub struct Session {
215 state: Arc<Mutex<State>>,
216 master: Box<dyn MasterPty + Send>,
217 writer: Box<dyn Write + Send>,
218 child: Box<dyn Child + Send + Sync>,
219 reaped: bool,
221}
222
223fn lock(state: &Mutex<State>) -> MutexGuard<'_, State> {
225 state.lock().unwrap_or_else(PoisonError::into_inner)
226}
227
228fn check_size(columns: u16, rows: u16) -> std::io::Result<()> {
229 if crate::tape::size_allowed(columns, rows) {
230 return Ok(());
231 }
232 Err(std::io::Error::new(
233 std::io::ErrorKind::InvalidInput,
234 format!(
235 "a terminal of {columns}x{rows} is outside {}x{} to {}x{}",
236 crate::tape::MIN_COLUMNS,
237 crate::tape::MIN_ROWS,
238 crate::tape::MAX_COLUMNS,
239 crate::tape::MAX_ROWS
240 ),
241 ))
242}
243
244impl Session {
245 pub fn start(
248 command: &[String],
249 workspace: &Path,
250 columns: u16,
251 rows: u16,
252 env: &[(String, String)],
253 theme: Theme,
254 ) -> std::io::Result<Session> {
255 check_size(columns, rows)?;
256 let pty = native_pty_system()
257 .openpty(PtySize {
258 rows,
259 cols: columns,
260 pixel_width: 0,
261 pixel_height: 0,
262 })
263 .map_err(std::io::Error::other)?;
264 let (program, args) = command.split_first().expect("a shell command");
265 let mut command = CommandBuilder::new(program);
266 command.args(args);
267 command.env_clear();
268 for (key, value) in env {
269 command.env(key, value);
270 }
271 command.cwd(workspace);
272 let child = pty
273 .slave
274 .spawn_command(command)
275 .map_err(std::io::Error::other)?;
276 drop(pty.slave);
277 let mut reader = pty
278 .master
279 .try_clone_reader()
280 .map_err(std::io::Error::other)?;
281 let writer = pty.master.take_writer().map_err(std::io::Error::other)?;
282 let state = Arc::new(Mutex::new(State::new(rows, columns, theme)));
283 let shared = Arc::clone(&state);
284 thread::spawn(move || {
285 let mut buffer = [0u8; 65536];
286 loop {
287 let read = match reader.read(&mut buffer) {
288 Ok(0) | Err(_) => break,
289 Ok(read) => read,
290 };
291 let mut state = lock(&shared);
292 if let Err(error) = state.terminal.process(&buffer[..read]) {
293 state.error = Some(error.to_string());
295 break;
296 }
297 state.see();
298 if !state.hidden {
299 let t = state.now();
300 state.output(t, &buffer[..read]);
301 }
302 }
303 lock(&shared).alive = false;
304 });
305 Ok(Session {
306 state,
307 master: pty.master,
308 writer,
309 child,
310 reaped: false,
311 })
312 }
313
314 fn lock(&self) -> MutexGuard<'_, State> {
315 lock(&self.state)
316 }
317
318 pub fn error(&self) -> Option<String> {
320 self.lock().error.clone()
321 }
322
323 pub fn send(&mut self, data: &str, label: Option<String>) -> std::io::Result<()> {
325 {
326 let mut state = self.lock();
327 if !state.hidden {
328 state.flush();
329 let t = state.now();
330 state
331 .timeline
332 .events
333 .push((t, Event::Input(data.to_string())));
334 if let Some(label) = label {
335 state.timeline.keys.push((t, label));
336 }
337 }
338 }
339 self.writer.write_all(data.as_bytes())?;
340 self.writer.flush()
341 }
342
343 pub fn resize(&mut self, columns: u16, rows: u16) -> std::io::Result<()> {
344 check_size(columns, rows)?;
345 self.master
346 .resize(PtySize {
347 rows,
348 cols: columns,
349 pixel_width: 0,
350 pixel_height: 0,
351 })
352 .map_err(std::io::Error::other)?;
353 self.lock().resize(columns, rows)
354 }
355
356 pub fn mark(&self) {
359 let mut state = self.lock();
360 let screen = state.terminal.contents();
361 state.seen_bytes = screen.len();
362 state.seen = VecDeque::from([screen]);
363 }
364
365 pub fn seen(&self, test: impl Fn(&str) -> bool) -> bool {
368 let state = self.lock();
369 test(&state.terminal.contents()) || state.seen.iter().any(|screen| test(screen))
370 }
371
372 pub fn contents(&self) -> String {
374 self.lock().terminal.contents()
375 }
376
377 pub fn alive(&self) -> bool {
378 self.lock().alive
379 }
380
381 pub fn snapshot(&self) -> Snapshot {
382 let state = self.lock();
383 state.terminal.snapshot(&state.theme)
384 }
385
386 pub fn hide(&self) {
387 let mut state = self.lock();
388 if !state.hidden {
389 state.flush();
390 state.hidden = true;
391 state.hidden_since = Instant::now();
392 }
393 }
394
395 pub fn show(&self) {
398 let mut state = self.lock();
399 if state.hidden {
400 let hidden = state.hidden_since.elapsed();
401 state.hidden_total += hidden;
402 state.hidden = false;
403 state.utf8.clear();
404 state.batch.clear();
405 state.overflow = false;
406 let t = state.now();
407 let snapshot = state.terminal.snapshot(&state.theme);
408 let repaint = crate::render::cast::repaint(&snapshot, &state.theme);
409 state.timeline.events.push((t, Event::Output(repaint)));
410 state.frame(t);
411 }
412 }
413
414 fn stop(&mut self) {
416 if !self.reaped {
417 self.reaped = true;
418 let _ = self.child.kill();
419 let _ = self.child.wait();
420 }
421 }
422
423 pub fn finish(mut self) -> Timeline {
425 self.hide();
426 self.stop();
427 std::mem::take(&mut self.lock().timeline)
428 }
429}
430
431impl Drop for Session {
432 fn drop(&mut self) {
434 self.stop();
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441
442 fn row_text(snapshot: &Snapshot, row: usize) -> String {
443 snapshot.rows[row]
444 .iter()
445 .map(|cell| cell.text.as_str())
446 .collect()
447 }
448
449 #[test]
450 fn output_queued_before_a_resize_is_recorded_at_the_old_size() {
451 let mut state = State::new(4, 20, Theme::default());
452 state.hidden = false;
453 let t = state.now();
454 state.frame(t);
455 state.terminal.process(b"hello").unwrap();
457 let t = state.now();
458 state.output(t, b"hello");
459 assert!(state.pending.is_some());
460 state.resize(30, 6).unwrap();
461
462 let events = &state.timeline.events;
463 let output = events
464 .iter()
465 .position(|(_, event)| *event == Event::Output("hello".into()))
466 .expect("the output is recorded");
467 let resize = events
468 .iter()
469 .position(|(_, event)| matches!(event, Event::Resize { .. }))
470 .expect("the resize is recorded");
471 assert!(output < resize, "{events:?}");
472 assert!(events[output].0 <= events[resize].0, "{events:?}");
473
474 let frames = &state.timeline.frames;
476 let (_, before) = &frames[frames.len() - 2];
477 assert_eq!((before.rows.len(), before.rows[0].len()), (4, 20));
478 assert!(row_text(before, 0).starts_with("hello"));
479 let (_, after) = frames.last().unwrap();
480 assert_eq!((after.rows.len(), after.rows[0].len()), (6, 30));
481 }
482}