Skip to main content

gpui_cn/terminal/
engine.rs

1//! The owner thread: one thread per terminal that owns the Ghostty state,
2//! parses output, encodes input and publishes frames.
3//!
4//! The thread sleeps on its mailbox. It wakes for output from the byte
5//! source, for input, resizes and visibility changes, and for its timers
6//! (scrollback compression, the render hold watchdog). A frame is built
7//! only when the UI has granted a credit, so a terminal that is not painted
8//! stops producing frames while it keeps parsing.
9//!
10//! Derived from Herdr (Apache-2.0).
11
12use std::collections::VecDeque;
13use std::io;
14use std::path::PathBuf;
15use std::sync::atomic::{AtomicBool, Ordering};
16use std::sync::{Arc, Condvar, Mutex};
17use std::time::{Duration, Instant};
18
19use gpui_kit::base::async_util;
20
21use crate::terminal::core::{Core, Effects};
22use crate::terminal::frame::{CellSelection, CursorShape, TerminalColors, TerminalFrame, Viewport};
23use crate::terminal::input::TerminalInput;
24use crate::terminal::options::EngineOptions;
25use crate::terminal::source::{FrameHandle, FrameSink, FrameSource, StartOptions};
26
27const INPUT_BYTES: usize = 1 << 20;
28const INPUT_COMMANDS: usize = 256;
29const WRITE_CHUNK: usize = 8192;
30/// The most input written in one turn, so output keeps flowing during a
31/// large paste to a program that reads as fast as it comes.
32const WRITE_BURST: usize = 64 * 1024;
33/// While a write waits for room, how often the engine tries again even
34/// without [`ByteSink::writable`], for a source that never calls it.
35const WRITE_RETRY: Duration = Duration::from_millis(50);
36/// The output a source may queue ahead of the parser before
37/// [`ByteSink::wait_for_room`] holds it back.
38const READ_QUEUE_BYTES: usize = 1 << 20;
39const COMPRESSION_IDLE: Duration = Duration::from_millis(250);
40const COMPRESSION_STEP: Duration = Duration::from_millis(16);
41/// How long after output the engine asks its source for the foreground
42/// process. Output is what a new program or a return to the prompt
43/// produces, so the check needs no timer while the terminal is quiet.
44const FOREGROUND_CHECK: Duration = Duration::from_millis(50);
45
46/// How a program ended.
47#[derive(Clone, Debug, PartialEq, Eq)]
48#[non_exhaustive]
49pub struct ExitStatus {
50    pub(crate) code: Option<i32>,
51    pub(crate) signal: Option<String>,
52    pub(crate) runtime: Duration,
53    pub(crate) abnormal: bool,
54}
55
56impl ExitStatus {
57    /// An exit with `code`, or by `signal` when a signal ended the program.
58    /// The engine fills in the run time.
59    pub fn new(code: Option<i32>, signal: Option<impl Into<String>>) -> Self {
60        Self {
61            code,
62            signal: signal.map(Into::into),
63            runtime: Duration::ZERO,
64            abnormal: false,
65        }
66    }
67
68    /// The exit code, `None` when a signal ended the program.
69    pub fn code(&self) -> Option<i32> {
70        self.code
71    }
72
73    /// The signal name, when a signal ended the program.
74    pub fn signal(&self) -> Option<&str> {
75        self.signal.as_deref()
76    }
77
78    /// How long the program ran.
79    pub fn runtime(&self) -> Duration {
80        self.runtime
81    }
82
83    /// Whether the run was shorter than the abnormal-exit threshold.
84    pub fn is_abnormal(&self) -> bool {
85        self.abnormal
86    }
87}
88
89/// The process in the foreground of a terminal: the one that reads its
90/// keys, such as the shell at its prompt or the editor it started.
91///
92/// A local pty reads it from the process table: the terminal's foreground
93/// process group (`tcgetpgrp`), then the name, arguments and working
94/// directory of that group's leader, through `proc_pidinfo` and `sysctl`
95/// on macOS and `/proc` on Linux. Another [`ByteSource`] reports its own,
96/// or none.
97#[derive(Clone, Debug, PartialEq, Eq)]
98#[non_exhaustive]
99pub struct ForegroundProcess {
100    pub(crate) pid: u32,
101    pub(crate) name: String,
102    pub(crate) argv: Vec<String>,
103    pub(crate) cwd: Option<PathBuf>,
104    pub(crate) shell: bool,
105}
106
107impl ForegroundProcess {
108    /// Process `pid`, named `name`, such as `vim`.
109    pub fn new(pid: u32, name: impl Into<String>) -> Self {
110        Self {
111            pid,
112            name: name.into(),
113            argv: Vec::new(),
114            cwd: None,
115            shell: false,
116        }
117    }
118
119    /// Set the arguments, the program's own name first.
120    #[must_use]
121    pub fn with_argv(mut self, argv: impl IntoIterator<Item = impl Into<String>>) -> Self {
122        self.argv = argv.into_iter().map(Into::into).collect();
123        self
124    }
125
126    /// Set the working directory.
127    #[must_use]
128    pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
129        self.cwd = Some(cwd.into());
130        self
131    }
132
133    /// Set whether it is the program the terminal started, such as the
134    /// shell at its prompt.
135    #[must_use]
136    pub fn with_shell(mut self, shell: bool) -> Self {
137        self.shell = shell;
138        self
139    }
140
141    /// The process id.
142    pub fn pid(&self) -> u32 {
143        self.pid
144    }
145
146    /// The executable's name, such as `vim`.
147    pub fn name(&self) -> &str {
148        &self.name
149    }
150
151    /// The arguments, the program's own name first, when they could be
152    /// read.
153    pub fn argv(&self) -> &[String] {
154        &self.argv
155    }
156
157    /// The working directory, when it could be read.
158    pub fn cwd(&self) -> Option<&std::path::Path> {
159        self.cwd.as_deref()
160    }
161
162    /// Whether it is the program the terminal started, such as the shell at
163    /// its prompt, rather than a program that program runs. An application
164    /// asks before it closes a terminal where this is false.
165    pub fn is_shell(&self) -> bool {
166        self.shell
167    }
168}
169
170/// The state of a terminal's program.
171#[derive(Clone, Debug, Default, PartialEq, Eq)]
172#[non_exhaustive]
173pub enum TerminalStatus {
174    /// The program has not started yet.
175    #[default]
176    Starting,
177    /// The program is running.
178    Live,
179    /// The program ended.
180    Exited(ExitStatus),
181    /// The terminal could not start or the engine failed.
182    Failed(String),
183}
184
185/// The latest published terminal state.
186#[derive(Clone, Debug, Default, PartialEq, Eq)]
187#[non_exhaustive]
188pub struct TerminalSnapshot {
189    /// The frame.
190    pub frame: Arc<TerminalFrame>,
191    /// The program's state.
192    pub status: TerminalStatus,
193    /// The title the program set.
194    pub title: String,
195    /// The working directory the program reported.
196    pub cwd: Option<PathBuf>,
197    /// Bells received so far.
198    pub bell_count: u64,
199    /// Increments per clipboard write.
200    pub clipboard_sequence: u64,
201    /// The last clipboard write.
202    pub clipboard: Option<Arc<str>>,
203    /// Increments per desktop notification.
204    pub notification_sequence: u64,
205    /// The last desktop notification as title and body.
206    pub notification: Option<(Arc<str>, Arc<str>)>,
207    /// Whether the cursor was at a shell prompt when the frame was built.
208    pub at_prompt: bool,
209    /// The process in the foreground, when the source reports one.
210    pub foreground: Option<ForegroundProcess>,
211}
212
213impl TerminalSnapshot {
214    /// A snapshot holding only a frame, for fixtures and tests.
215    #[must_use]
216    pub fn for_frame(frame: Arc<TerminalFrame>) -> Self {
217        Self {
218            frame,
219            status: TerminalStatus::Live,
220            ..Self::default()
221        }
222    }
223
224    /// Set the state of the program.
225    #[must_use]
226    pub fn with_status(mut self, status: TerminalStatus) -> Self {
227        self.status = status;
228        self
229    }
230
231    /// Set the title the program set.
232    #[must_use]
233    pub fn with_title(mut self, title: impl Into<String>) -> Self {
234        self.title = title.into();
235        self
236    }
237
238    /// Set the working directory the program reported.
239    #[must_use]
240    pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
241        self.cwd = Some(cwd.into());
242        self
243    }
244
245    /// Set the number of bells received so far. A change rings the bell.
246    #[must_use]
247    pub fn with_bell_count(mut self, count: u64) -> Self {
248        self.bell_count = count;
249        self
250    }
251
252    /// Set the last clipboard write and its sequence number. A new sequence
253    /// number writes `text` to the clipboard.
254    #[must_use]
255    pub fn with_clipboard(mut self, sequence: u64, text: impl Into<Arc<str>>) -> Self {
256        self.clipboard_sequence = sequence;
257        self.clipboard = Some(text.into());
258        self
259    }
260
261    /// Set the last desktop notification and its sequence number.
262    #[must_use]
263    pub fn with_notification(
264        mut self,
265        sequence: u64,
266        title: impl Into<Arc<str>>,
267        body: impl Into<Arc<str>>,
268    ) -> Self {
269        self.notification_sequence = sequence;
270        self.notification = Some((title.into(), body.into()));
271        self
272    }
273
274    /// Set whether the cursor was at a shell prompt.
275    #[must_use]
276    pub fn with_at_prompt(mut self, at_prompt: bool) -> Self {
277        self.at_prompt = at_prompt;
278        self
279    }
280
281    /// Set the process in the foreground.
282    #[must_use]
283    pub fn with_foreground(mut self, foreground: ForegroundProcess) -> Self {
284        self.foreground = Some(foreground);
285        self
286    }
287}
288
289/// A source of terminal output that also accepts the program's input.
290///
291/// The engine calls [`ByteSource::start`] on its owner thread and pumps
292/// everything the source writes to the [`ByteSink`] through the terminal.
293pub trait ByteSource: Send + 'static {
294    /// Start producing output into `sink` at `viewport` and return the
295    /// handle for input.
296    fn start(
297        self: Box<Self>,
298        sink: ByteSink,
299        viewport: Viewport,
300    ) -> io::Result<Box<dyn ByteHandle>>;
301}
302
303/// The engine's side of a byte source.
304pub trait ByteHandle: Send {
305    /// Write input for the program without blocking: as much of `bytes` as
306    /// it takes now, and return how much that was. When that is less than
307    /// all of it, the source calls [`ByteSink::writable`] once more fits,
308    /// and the engine writes the rest then, ahead of newer input.
309    fn write(&self, bytes: &[u8]) -> io::Result<usize>;
310    /// The grid changed.
311    fn resize(&self, viewport: Viewport) -> io::Result<()>;
312    /// Whether the program ended, and how.
313    fn try_wait(&self) -> io::Result<Option<ExitStatus>>;
314    /// Stop the program and release the source.
315    fn close(&self);
316    /// The process in the foreground, if the source knows it. The engine
317    /// asks on its own thread, at most every 50 ms and only after output,
318    /// so it may read the process table but must not block.
319    fn foreground(&self) -> Option<ForegroundProcess> {
320        None
321    }
322}
323
324/// Where a [`ByteSource`] writes its output.
325#[derive(Clone, Debug)]
326pub struct ByteSink {
327    shared: Arc<Shared>,
328}
329
330impl ByteSink {
331    /// Deliver output. Returns false once the terminal is closing. It
332    /// never blocks; a source that reads on its own thread calls
333    /// [`wait_for_room`](Self::wait_for_room) before each read so the
334    /// output waiting for the parser stays bounded.
335    pub fn write(&self, bytes: &[u8]) -> bool {
336        let mut mail = self.shared.lock();
337        if self.shared.closing.load(Ordering::Acquire) {
338            return false;
339        }
340        mail.reads.push_back(bytes.to_vec());
341        mail.read_bytes += bytes.len();
342        mail.generation = mail.generation.wrapping_add(1);
343        self.shared.wake.notify_one();
344        true
345    }
346
347    /// Blocks while a megabyte of output waits for the parser, which then
348    /// holds the program back through the pty instead of filling memory.
349    /// Returns false once the terminal is closing. The engine never waits
350    /// on the source, so this always ends.
351    pub fn wait_for_room(&self) -> bool {
352        let mut mail = self.shared.lock();
353        while mail.read_bytes >= READ_QUEUE_BYTES && !self.shared.closing.load(Ordering::Acquire) {
354            mail = self
355                .shared
356                .room
357                .wait(mail)
358                .unwrap_or_else(std::sync::PoisonError::into_inner);
359        }
360        !self.shared.closing.load(Ordering::Acquire)
361    }
362
363    /// The program can take input again after a short
364    /// [`ByteHandle::write`]; wakes the engine to write the rest.
365    pub fn writable(&self) {
366        let mut mail = self.shared.lock();
367        mail.writable = true;
368        self.shared.bump(&mut mail);
369    }
370
371    /// The source reached end of file.
372    pub fn finished(&self) {
373        let mut mail = self.shared.lock();
374        mail.reader_exited = true;
375        mail.generation = mail.generation.wrapping_add(1);
376        self.shared.wake.notify_one();
377    }
378}
379
380/// Why an input was refused.
381#[derive(Clone, Copy, Debug, PartialEq, Eq)]
382pub enum InputRejected {
383    /// The bounded input queue is full; try again after a frame.
384    QueueFull,
385    /// The terminal is closed.
386    Closed,
387}
388
389impl std::fmt::Display for InputRejected {
390    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391        match self {
392            Self::QueueFull => f.write_str("terminal input queue is full"),
393            Self::Closed => f.write_str("terminal is closed"),
394        }
395    }
396}
397
398impl std::error::Error for InputRejected {}
399
400#[derive(Default)]
401struct Mailbox {
402    commands: VecDeque<TerminalInput>,
403    bytes: usize,
404    reads: VecDeque<Vec<u8>>,
405    /// The bytes in `reads`.
406    read_bytes: usize,
407    /// The source said it can take input again.
408    writable: bool,
409    viewport: Option<Viewport>,
410    visible: bool,
411    credit: bool,
412    /// The UI painted since the owner last looked, which counts as use.
413    painted: bool,
414    reader_exited: bool,
415    reveal: bool,
416    interrupt: Option<TerminalInput>,
417    copy: Option<(CellSelection, async_util::Sender<io::Result<String>>)>,
418    cursor: Option<(CursorShape, bool)>,
419    colors: Option<TerminalColors>,
420    generation: u64,
421}
422
423#[derive(Debug)]
424struct Shared {
425    mailbox: Mutex<Mailbox>,
426    wake: Condvar,
427    /// Signals a source in [`ByteSink::wait_for_room`].
428    room: Condvar,
429    closing: AtomicBool,
430}
431
432impl std::fmt::Debug for Mailbox {
433    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
434        f.debug_struct("Mailbox")
435            .field("commands", &self.commands.len())
436            .field("reads", &self.reads.len())
437            .finish_non_exhaustive()
438    }
439}
440
441impl Shared {
442    fn lock(&self) -> std::sync::MutexGuard<'_, Mailbox> {
443        self.mailbox
444            .lock()
445            .unwrap_or_else(std::sync::PoisonError::into_inner)
446    }
447
448    fn bump(&self, mail: &mut Mailbox) {
449        mail.generation = mail.generation.wrapping_add(1);
450        self.wake.notify_one();
451    }
452
453    /// Marks the terminal closing and frees a source waiting for room.
454    fn set_closing(&self) {
455        self.closing.store(true, Ordering::Release);
456        self.room.notify_all();
457    }
458}
459
460/// The UI's handle to a running engine.
461///
462/// Every method is cheap and never blocks on the owner thread.
463#[derive(Clone, Debug)]
464pub(crate) struct EngineHandle {
465    shared: Arc<Shared>,
466}
467
468impl EngineHandle {
469    /// Queue input. Ctrl+C jumps the queue and discards pending input.
470    pub fn input(&self, input: TerminalInput) -> Result<(), InputRejected> {
471        let mut mail = self.shared.lock();
472        if self.shared.closing.load(Ordering::Acquire) {
473            return Err(InputRejected::Closed);
474        }
475        if input.is_interrupt() {
476            let discarded: usize = mail.commands.iter().map(TerminalInput::budget).sum();
477            mail.commands.clear();
478            mail.bytes = mail.bytes.saturating_sub(discarded);
479            mail.interrupt = Some(input);
480            self.shared.bump(&mut mail);
481            return Ok(());
482        }
483        let budget = input.budget();
484        if mail.commands.len() >= INPUT_COMMANDS || budget > INPUT_BYTES.saturating_sub(mail.bytes)
485        {
486            return Err(InputRejected::QueueFull);
487        }
488        mail.bytes += budget;
489        mail.commands.push_back(input);
490        self.shared.bump(&mut mail);
491        Ok(())
492    }
493
494    /// Queued input bytes not yet written to the program.
495    #[must_use]
496    pub fn input_backlog(&self) -> usize {
497        self.shared.lock().bytes
498    }
499
500    /// Change the grid.
501    pub fn resize(&self, viewport: Viewport) {
502        let mut mail = self.shared.lock();
503        mail.viewport = Some(viewport);
504        self.shared.bump(&mut mail);
505    }
506
507    /// Tell the engine whether the terminal is painted. A hidden terminal
508    /// keeps parsing but builds no frames; revealing it builds one at once.
509    pub fn set_visible(&self, visible: bool) {
510        let mut mail = self.shared.lock();
511        if mail.visible != visible {
512            mail.visible = visible;
513            mail.credit = visible;
514            mail.reveal = visible;
515            self.shared.bump(&mut mail);
516        }
517    }
518
519    /// Grant one frame of credit. The UI calls this after it painted the
520    /// last frame.
521    pub fn request_frame(&self) {
522        let mut mail = self.shared.lock();
523        if !mail.credit || !mail.painted {
524            mail.credit = true;
525            mail.painted = true;
526            self.shared.bump(&mut mail);
527        }
528    }
529
530    /// Set the default cursor shape and blink.
531    pub fn set_cursor(&self, shape: CursorShape, blinking: bool) {
532        let mut mail = self.shared.lock();
533        mail.cursor = Some((shape, blinking));
534        self.shared.bump(&mut mail);
535    }
536
537    /// Change the default colors. The next frame carries them.
538    pub fn set_colors(&self, colors: TerminalColors) {
539        let mut mail = self.shared.lock();
540        mail.colors = Some(colors);
541        self.shared.bump(&mut mail);
542    }
543
544    /// Ask the owner thread for the selected text. The answer arrives on the
545    /// returned receiver.
546    pub fn copy(&self, selection: CellSelection) -> async_util::Receiver<io::Result<String>> {
547        let (tx, rx) = async_util::unbounded();
548        let mut mail = self.shared.lock();
549        mail.copy = Some((selection, tx));
550        self.shared.bump(&mut mail);
551        rx
552    }
553
554    /// Stop the program and the owner thread. Idempotent.
555    pub fn close(&self) {
556        let mut mail = self.shared.lock();
557        self.shared.set_closing();
558        self.shared.bump(&mut mail);
559    }
560}
561
562/// Everything needed to start an engine.
563#[derive(Debug)]
564pub(crate) struct EngineConfig {
565    /// Engine limits.
566    pub options: EngineOptions,
567    /// The initial grid.
568    pub viewport: Viewport,
569    /// The initial colors.
570    pub colors: TerminalColors,
571    /// Whether the terminal starts visible.
572    pub visible: bool,
573    /// The default cursor.
574    pub cursor: Option<(CursorShape, bool)>,
575    /// Bytes written to the program right after it starts.
576    pub input: Vec<u8>,
577    /// A run shorter than this is an abnormal exit.
578    pub abnormal_exit_runtime: Duration,
579}
580
581impl Default for EngineConfig {
582    fn default() -> Self {
583        Self {
584            options: EngineOptions::default(),
585            viewport: Viewport::default(),
586            colors: TerminalColors::default(),
587            visible: true,
588            cursor: None,
589            input: Vec::new(),
590            abnormal_exit_runtime: Duration::from_millis(250),
591        }
592    }
593}
594
595/// Start an owner thread for `source`, publishing to `sink`.
596///
597/// The first snapshot is published as soon as the source started.
598pub(crate) fn spawn(
599    source: Box<dyn ByteSource>,
600    config: EngineConfig,
601    sink: FrameSink,
602) -> io::Result<EngineHandle> {
603    let shared = Arc::new(Shared {
604        // The first frame is published as the source starts, so there is no
605        // credit until the UI paints it, the same as for every later frame.
606        mailbox: Mutex::new(Mailbox {
607            visible: config.visible,
608            credit: false,
609            ..Mailbox::default()
610        }),
611        wake: Condvar::new(),
612        room: Condvar::new(),
613        closing: AtomicBool::new(false),
614    });
615    let handle = EngineHandle {
616        shared: shared.clone(),
617    };
618    let byte_sink = ByteSink {
619        shared: shared.clone(),
620    };
621    std::thread::Builder::new()
622        .name("gpui-cn-terminal".to_owned())
623        .spawn(move || {
624            let mut owner = match Owner::start(source, byte_sink, config, shared.clone(), &sink) {
625                Ok(owner) => owner,
626                Err(error) => {
627                    shared.set_closing();
628                    sink.publish(Arc::new(TerminalSnapshot {
629                        status: TerminalStatus::Failed(error.to_string()),
630                        ..TerminalSnapshot::default()
631                    }));
632                    return;
633                }
634            };
635            owner.run();
636        })?;
637    Ok(handle)
638}
639
640/// A [`FrameSource`] that runs a [`ByteSource`] through the engine.
641#[derive(Debug)]
642pub struct Engine<S> {
643    source: S,
644    options: EngineOptions,
645    abnormal_exit_runtime: Duration,
646    input: Vec<u8>,
647}
648
649impl<S: ByteSource> Engine<S> {
650    /// An engine for `source` with `options`.
651    #[must_use]
652    pub fn new(source: S, options: EngineOptions) -> Self {
653        Self {
654            source,
655            options,
656            abnormal_exit_runtime: Duration::from_millis(250),
657            input: Vec::new(),
658        }
659    }
660
661    /// Set the run time under which an exit is reported as abnormal.
662    #[must_use]
663    pub fn with_abnormal_exit_runtime(mut self, runtime: Duration) -> Self {
664        self.abnormal_exit_runtime = runtime;
665        self
666    }
667
668    /// Set bytes written to the program right after it starts.
669    #[must_use]
670    pub fn with_input(mut self, input: impl Into<Vec<u8>>) -> Self {
671        self.input = input.into();
672        self
673    }
674}
675
676impl<S: ByteSource> FrameSource for Engine<S> {
677    fn start(
678        self: Box<Self>,
679        sink: FrameSink,
680        options: StartOptions,
681    ) -> io::Result<Box<dyn FrameHandle>> {
682        let handle = spawn(
683            Box::new(self.source),
684            EngineConfig {
685                options: self.options,
686                viewport: options.viewport,
687                colors: options.colors,
688                visible: options.visible,
689                cursor: options.cursor,
690                input: self.input,
691                abnormal_exit_runtime: self.abnormal_exit_runtime,
692            },
693            sink,
694        )?;
695        Ok(Box::new(handle))
696    }
697}
698
699impl FrameHandle for EngineHandle {
700    fn input(&self, input: TerminalInput) -> Result<(), InputRejected> {
701        Self::input(self, input)
702    }
703
704    fn input_backlog(&self) -> usize {
705        Self::input_backlog(self)
706    }
707
708    fn resize(&self, viewport: Viewport) {
709        Self::resize(self, viewport);
710    }
711
712    fn set_visible(&self, visible: bool) {
713        Self::set_visible(self, visible);
714    }
715
716    fn request_frame(&self) {
717        Self::request_frame(self);
718    }
719
720    fn set_colors(&self, colors: TerminalColors) {
721        Self::set_colors(self, colors);
722    }
723
724    fn set_cursor(&self, shape: CursorShape, blinking: bool) {
725        Self::set_cursor(self, shape, blinking);
726    }
727
728    fn copy(&self, selection: CellSelection) -> Option<async_util::Receiver<io::Result<String>>> {
729        Some(Self::copy(self, selection))
730    }
731
732    fn close(&self) {
733        Self::close(self);
734    }
735}
736
737struct PendingWrite {
738    bytes: Vec<u8>,
739    offset: usize,
740    budget: usize,
741    paste: bool,
742}
743
744/// The owner's core: live, or parked as a snapshot in the park store.
745struct CoreSlot {
746    live: Option<Core>,
747    parked: Option<crate::terminal::core::Parked>,
748}
749
750impl std::ops::Deref for CoreSlot {
751    type Target = Core;
752
753    fn deref(&self) -> &Core {
754        self.live
755            .as_ref()
756            .expect("the core is restored before it is used")
757    }
758}
759
760impl std::ops::DerefMut for CoreSlot {
761    fn deref_mut(&mut self) -> &mut Core {
762        self.live
763            .as_mut()
764            .expect("the core is restored before it is used")
765    }
766}
767
768/// What the owner needs to park and restore its core.
769struct Parking {
770    options: EngineOptions,
771    key: u64,
772    /// The last output, input, resize, paint or other request.
773    last_use: Instant,
774}
775
776struct Owner {
777    shared: Arc<Shared>,
778    core: CoreSlot,
779    parking: Parking,
780    handle: Box<dyn ByteHandle>,
781    sink: FrameSink,
782    snapshot: TerminalSnapshot,
783    /// Bytes that go ahead of input: replies to the program's queries and
784    /// interrupts.
785    urgent: Vec<u8>,
786    pending: Option<PendingWrite>,
787    /// The source took less than it was given and has not said it has
788    /// room again.
789    write_blocked: bool,
790    exit: Option<ExitStatus>,
791    started: Instant,
792    abnormal_exit_runtime: Duration,
793    compression_deadline: Option<Instant>,
794    /// When to ask the source for the foreground process again.
795    foreground_due: Option<Instant>,
796    dirty: bool,
797    force: bool,
798    effects: Effects,
799}
800
801impl Owner {
802    fn start(
803        source: Box<dyn ByteSource>,
804        byte_sink: ByteSink,
805        config: EngineConfig,
806        shared: Arc<Shared>,
807        sink: &FrameSink,
808    ) -> io::Result<Self> {
809        let mut core = Core::new(config.viewport, &config.options, config.colors)?;
810        if let Some((shape, blinking)) = config.cursor {
811            core.set_cursor(shape, blinking)?;
812        }
813        let frame = core
814            .frame(true)?
815            .ok_or_else(|| io::Error::other("initial terminal frame missing"))?;
816        let handle = source.start(byte_sink, config.viewport)?;
817        let snapshot = TerminalSnapshot {
818            frame,
819            status: TerminalStatus::Live,
820            ..TerminalSnapshot::default()
821        };
822        sink.publish(Arc::new(snapshot.clone()));
823        Ok(Self {
824            shared,
825            core: CoreSlot {
826                live: Some(core),
827                parked: None,
828            },
829            parking: Parking {
830                options: config.options.clone(),
831                key: crate::terminal::park::next_key(),
832                last_use: Instant::now(),
833            },
834            handle,
835            sink: sink.clone(),
836            snapshot,
837            urgent: config.input.clone(),
838            pending: None,
839            write_blocked: false,
840            exit: None,
841            started: Instant::now(),
842            abnormal_exit_runtime: config.abnormal_exit_runtime,
843            compression_deadline: None,
844            foreground_due: Some(Instant::now() + FOREGROUND_CHECK),
845            dirty: true,
846            force: false,
847            effects: Effects::default(),
848        })
849    }
850
851    fn run(&mut self) {
852        while !self.shared.closing.load(Ordering::Acquire) {
853            if let Err(error) = self.turn() {
854                self.snapshot.status = TerminalStatus::Failed(error.to_string());
855                self.publish();
856                break;
857            }
858        }
859        self.cleanup();
860    }
861
862    fn turn(&mut self) -> io::Result<()> {
863        let (
864            reads,
865            viewport,
866            command,
867            visible,
868            credit,
869            eof,
870            reveal,
871            interrupt,
872            copy,
873            cursor,
874            colors,
875            painted,
876            generation,
877        ) = {
878            let mut mail = self.shared.lock();
879            let interrupt = mail.interrupt.take();
880            let command = if interrupt.is_none() && self.pending.is_none() {
881                mail.commands.pop_front()
882            } else {
883                None
884            };
885            mail.read_bytes = 0;
886            self.shared.room.notify_all();
887            if std::mem::take(&mut mail.writable) {
888                self.write_blocked = false;
889            }
890            (
891                std::mem::take(&mut mail.reads),
892                mail.viewport.take(),
893                command,
894                mail.visible,
895                mail.credit,
896                mail.reader_exited,
897                std::mem::take(&mut mail.reveal),
898                interrupt,
899                mail.copy.take(),
900                mail.cursor.take(),
901                mail.colors.take(),
902                std::mem::take(&mut mail.painted),
903                mail.generation,
904            )
905        };
906        let had_work = !reads.is_empty()
907            || viewport.is_some()
908            || command.is_some()
909            || reveal
910            || interrupt.is_some()
911            || copy.is_some()
912            || cursor.is_some()
913            || colors.is_some();
914        {
915            // Anything that needs the terminal restores a parked one first;
916            // with nothing to do, a parked terminal only waits.
917            if had_work || painted || (eof && self.exit.is_none()) {
918                self.parking.last_use = Instant::now();
919                self.unpark()?;
920            } else if self.core.live.is_none() {
921                self.wait(generation, None);
922                return Ok(());
923            }
924        }
925        if reveal {
926            self.dirty = true;
927            self.force = true;
928        }
929        if let Some((shape, blinking)) = cursor {
930            self.core.set_cursor(shape, blinking)?;
931            self.dirty = true;
932        }
933        if let Some(colors) = colors {
934            self.core.set_colors(colors)?;
935            self.dirty = true;
936            self.force = true;
937        }
938        if !reads.is_empty() {
939            for chunk in reads {
940                self.core.write(&chunk, &mut self.effects);
941            }
942            self.compression_deadline = Some(Instant::now() + COMPRESSION_IDLE);
943            self.foreground_due
944                .get_or_insert_with(|| Instant::now() + FOREGROUND_CHECK);
945            self.dirty = true;
946            self.apply_effects()?;
947        }
948        if let Some(interrupt) = interrupt {
949            let partial_paste = self
950                .pending
951                .as_ref()
952                .is_some_and(|pending| pending.paste && pending.offset > 0);
953            if let Some(pending) = self.pending.take() {
954                self.release(pending.budget);
955            }
956            let mut bytes = if partial_paste {
957                b"\x1b[201~".to_vec()
958            } else {
959                Vec::new()
960            };
961            bytes.extend(self.core.input(interrupt)?);
962            self.urgent.extend(bytes);
963            self.dirty = true;
964        }
965        if let Some(viewport) = viewport {
966            self.core.resize(viewport, &mut self.effects)?;
967            self.handle.resize(viewport)?;
968            self.apply_effects()?;
969            self.dirty = true;
970            self.force = true;
971        }
972        if let Some(input) = command {
973            let budget = input.budget();
974            if self.exit.is_some() && !matches!(input, TerminalInput::Scroll(_)) {
975                self.release(budget);
976            } else {
977                let paste = matches!(input, TerminalInput::Paste(_));
978                match self.core.input(input) {
979                    Ok(bytes) if !bytes.is_empty() => {
980                        self.pending = Some(PendingWrite {
981                            bytes,
982                            offset: 0,
983                            budget,
984                            paste,
985                        });
986                    }
987                    Ok(_) => self.release(budget),
988                    Err(error) => {
989                        self.release(budget);
990                        return Err(error);
991                    }
992                }
993                self.dirty = true;
994            }
995        }
996        if let Some((selection, reply)) = copy {
997            let _ = reply.try_send(self.core.copy(selection));
998        }
999        self.flush()?;
1000        if eof
1001            && self.exit.is_none()
1002            && let Some(mut status) = self.handle.try_wait()?
1003        {
1004            status.runtime = self.started.elapsed();
1005            status.abnormal = status.runtime <= self.abnormal_exit_runtime;
1006            self.exit = Some(status.clone());
1007            self.snapshot.status = TerminalStatus::Exited(status);
1008            self.dirty = true;
1009            self.force = true;
1010        }
1011        if self.foreground_due.is_some_and(|due| Instant::now() >= due) {
1012            self.foreground_due = None;
1013            let foreground = if self.exit.is_some() {
1014                None
1015            } else {
1016                self.handle.foreground()
1017            };
1018            if foreground != self.snapshot.foreground {
1019                self.snapshot.foreground = foreground;
1020                // Published with the frame already shown, so a hidden or
1021                // unpainted terminal reports it too.
1022                self.publish();
1023            }
1024        }
1025        let exited = self.exit.is_some();
1026        let held = self.core.held();
1027        if self.dirty && ((visible && credit && !held) || (exited && self.force)) {
1028            if let Some(frame) = self.core.frame(self.force)? {
1029                self.snapshot.frame = frame;
1030                self.snapshot.at_prompt = self.core.at_prompt();
1031                self.shared.lock().credit = false;
1032                self.publish();
1033            }
1034            self.dirty = false;
1035            self.force = false;
1036        }
1037        if self
1038            .compression_deadline
1039            .is_some_and(|deadline| Instant::now() >= deadline)
1040        {
1041            let complete = self.core.compress();
1042            self.compression_deadline = (!complete).then(|| Instant::now() + COMPRESSION_STEP);
1043        }
1044        let unsent = self.pending.is_some() || !self.urgent.is_empty();
1045        if !had_work && !(unsent && !self.write_blocked) {
1046            let delay = if unsent {
1047                Some(WRITE_RETRY)
1048            } else if self.dirty && visible && credit {
1049                self.core
1050                    .hold_remaining()
1051                    .or(Some(Duration::from_millis(1)))
1052            } else if let Some(deadline) = self.compression_deadline {
1053                Some(deadline.saturating_duration_since(Instant::now()))
1054            } else if eof && self.exit.is_none() {
1055                Some(Duration::from_millis(20))
1056            } else {
1057                None
1058            };
1059            let delay = match self.foreground_due {
1060                Some(due) => {
1061                    let until = due.saturating_duration_since(Instant::now());
1062                    Some(delay.map_or(until, |delay| delay.min(until)))
1063                }
1064                None => delay,
1065            };
1066            let delay = match self.park_after() {
1067                Some(_) if self.park_if_idle() => None,
1068                Some(after) => Some(delay.map_or(after, |delay| delay.min(after))),
1069                None => delay,
1070            };
1071            self.wait(generation, delay);
1072        }
1073        Ok(())
1074    }
1075
1076    /// Sleeps until the mailbox changes, or `delay` passes.
1077    fn wait(&self, generation: u64, delay: Option<Duration>) {
1078        let mail = self.shared.lock();
1079        if mail.generation == generation && !self.shared.closing.load(Ordering::Acquire) {
1080            match delay {
1081                None => drop(
1082                    self.shared
1083                        .wake
1084                        .wait(mail)
1085                        .unwrap_or_else(std::sync::PoisonError::into_inner),
1086                ),
1087                Some(delay) => drop(
1088                    self.shared
1089                        .wake
1090                        .wait_timeout(mail, delay.max(Duration::from_millis(1)))
1091                        .unwrap_or_else(std::sync::PoisonError::into_inner),
1092                ),
1093            }
1094        }
1095    }
1096
1097    /// How long until a live terminal may park, when parking is on and
1098    /// nothing keeps it awake.
1099    fn park_after(&mut self) -> Option<Duration> {
1100        let park = &self.parking.options.park;
1101        if !park.enabled
1102            || self.core.live.is_none()
1103            || self.pending.is_some()
1104            || self.exit.is_some()
1105            || self.core.held()
1106        {
1107            return None;
1108        }
1109        Some(park.idle.saturating_sub(self.parking.last_use.elapsed()))
1110    }
1111
1112    /// Parks the terminal when it has been idle long enough. Returns
1113    /// whether it parked.
1114    fn park_if_idle(&mut self) -> bool {
1115        if self.park_after() != Some(Duration::ZERO) {
1116            return false;
1117        }
1118        let store = self.parking.options.park.store.clone();
1119        let parked = self
1120            .core
1121            .park()
1122            .and_then(|(bytes, parked)| store.save(self.parking.key, &bytes).map(|()| parked));
1123        match parked {
1124            Ok(parked) => {
1125                self.core.live = None;
1126                self.core.parked = Some(parked);
1127                self.compression_deadline = None;
1128                true
1129            }
1130            // Try again after another idle period rather than at once.
1131            Err(_) => {
1132                self.parking.last_use = Instant::now();
1133                false
1134            }
1135        }
1136    }
1137
1138    /// Restores a parked terminal from its store.
1139    fn unpark(&mut self) -> io::Result<()> {
1140        let Some(parked) = self.core.parked.take() else {
1141            return Ok(());
1142        };
1143        let bytes = self.parking.options.park.store.take(self.parking.key)?;
1144        self.core.live = Some(Core::restore(&bytes, parked, &self.parking.options)?);
1145        Ok(())
1146    }
1147
1148    fn release(&self, budget: usize) {
1149        let mut mail = self.shared.lock();
1150        mail.bytes = mail.bytes.saturating_sub(budget);
1151    }
1152
1153    /// Writes what the program takes now: the urgent bytes, then up to
1154    /// [`WRITE_BURST`] of the pending input, resuming where the last short
1155    /// write stopped.
1156    fn flush(&mut self) -> io::Result<()> {
1157        if self.write_blocked {
1158            return Ok(());
1159        }
1160        if !self.urgent.is_empty() {
1161            let written = self.handle.write(&self.urgent)?;
1162            self.urgent.drain(..written.min(self.urgent.len()));
1163            if !self.urgent.is_empty() {
1164                self.write_blocked = true;
1165                return Ok(());
1166            }
1167        }
1168        let mut burst = WRITE_BURST;
1169        while burst > 0 {
1170            let Some(pending) = &mut self.pending else {
1171                break;
1172            };
1173            let end = (pending.offset + WRITE_CHUNK.min(burst)).min(pending.bytes.len());
1174            let offered = end - pending.offset;
1175            let written = self
1176                .handle
1177                .write(&pending.bytes[pending.offset..end])?
1178                .min(offered);
1179            pending.offset += written;
1180            burst = burst.saturating_sub(written);
1181            if pending.offset >= pending.bytes.len() {
1182                let budget = pending.budget;
1183                self.pending = None;
1184                self.release(budget);
1185                break;
1186            }
1187            if written < offered {
1188                self.write_blocked = true;
1189                break;
1190            }
1191        }
1192        Ok(())
1193    }
1194
1195    fn apply_effects(&mut self) -> io::Result<()> {
1196        let effects = std::mem::take(&mut self.effects);
1197        self.urgent.extend_from_slice(&effects.replies);
1198        let mut changed = false;
1199        if effects.title_changed {
1200            self.snapshot.title = self.core.title().to_owned();
1201            changed = true;
1202        }
1203        if effects.cwd_changed {
1204            self.snapshot.cwd = self.core.cwd().cloned();
1205            changed = true;
1206        }
1207        if effects.bells > 0 {
1208            self.snapshot.bell_count += effects.bells;
1209            changed = true;
1210        }
1211        if let Some(text) = effects.clipboard.last() {
1212            self.snapshot.clipboard_sequence += effects.clipboard.len() as u64;
1213            self.snapshot.clipboard = Some(Arc::from(text.as_str()));
1214            changed = true;
1215        }
1216        if let Some((title, body)) = effects.notifications.last() {
1217            self.snapshot.notification_sequence += effects.notifications.len() as u64;
1218            self.snapshot.notification =
1219                Some((Arc::from(title.as_str()), Arc::from(body.as_str())));
1220            changed = true;
1221        }
1222        if changed {
1223            self.dirty = true;
1224            self.force = true;
1225        }
1226        Ok(())
1227    }
1228
1229    fn publish(&self) {
1230        self.sink.publish(Arc::new(self.snapshot.clone()));
1231    }
1232
1233    fn cleanup(&mut self) {
1234        self.shared.set_closing();
1235        {
1236            let mut mail = self.shared.lock();
1237            mail.commands.clear();
1238            mail.bytes = 0;
1239            mail.reads.clear();
1240            mail.read_bytes = 0;
1241        }
1242        self.pending = None;
1243        if self.core.parked.take().is_some() {
1244            self.parking.options.park.store.remove(self.parking.key);
1245        }
1246        self.handle.close();
1247        if self.exit.is_none() {
1248            let deadline = Instant::now() + Duration::from_secs(2);
1249            while Instant::now() < deadline {
1250                if let Ok(Some(mut status)) = self.handle.try_wait() {
1251                    status.runtime = self.started.elapsed();
1252                    status.abnormal = status.runtime <= self.abnormal_exit_runtime;
1253                    self.snapshot.status = TerminalStatus::Exited(status.clone());
1254                    self.exit = Some(status);
1255                    break;
1256                }
1257                std::thread::sleep(Duration::from_millis(20));
1258            }
1259        }
1260        self.publish();
1261    }
1262}