gpui_cn/terminal/stream.rs
1//! A byte source an application drives itself: an ssh session, a
2//! websocket to a remote shell, a pty the application owns, or a recorded
3//! stream.
4//!
5//! [`StreamSource::new`] returns the source and its [`StreamPeer`]. The
6//! source goes to [`Engine`](super::Engine) like any [`ByteSource`]; the
7//! application keeps the peer, writes the program's output to it, reads
8//! the terminal's input and grid sizes from it, and reports the exit.
9//! It works on every target, including those with no local pty.
10//!
11//! ```no_run
12//! use gpui_cn::terminal::{
13//! Engine, EngineOptions, ExitStatus, StreamEvent, StreamSource, TerminalConfig,
14//! TerminalState,
15//! };
16//! use gpui_kit::{AppContext as _, Context, Window};
17//!
18//! fn remote(window: &mut Window, cx: &mut Context<TerminalState>) -> TerminalState {
19//! let (source, peer) = StreamSource::new();
20//! cx.spawn(async move |_, _| {
21//! peer.output(b"connected\r\n$ ");
22//! while let Ok(event) = peer.events().recv().await {
23//! match event {
24//! // Send the keys to the remote program.
25//! StreamEvent::Input(bytes) => peer.output(&bytes),
26//! // Tell the remote side the new size.
27//! StreamEvent::Resize(_) => {}
28//! StreamEvent::Closed => break,
29//! _ => {}
30//! }
31//! }
32//! peer.exit(ExitStatus::new(Some(0), None::<String>));
33//! })
34//! .detach();
35//! let engine = Engine::new(source, EngineOptions::default());
36//! TerminalState::new(engine, TerminalConfig::default(), window, cx)
37//! }
38//! ```
39
40use std::io;
41use std::sync::{Arc, Mutex};
42
43use gpui_kit::base::async_util;
44
45use crate::terminal::engine::{ByteHandle, ByteSink, ByteSource, ExitStatus};
46use crate::terminal::frame::Viewport;
47
48/// What the terminal sends to the application's side of a
49/// [`StreamSource`].
50#[derive(Clone, Debug, PartialEq, Eq)]
51#[non_exhaustive]
52pub enum StreamEvent {
53 /// Bytes for the program: keys, pastes and replies to its queries,
54 /// already encoded for the terminal modes the program set.
55 Input(Vec<u8>),
56 /// The grid changed. The first event is the grid the terminal
57 /// started with.
58 Resize(Viewport),
59 /// The terminal closed. Stop the program.
60 Closed,
61}
62
63/// A [`ByteSource`] fed by its [`StreamPeer`].
64#[derive(Debug)]
65pub struct StreamSource {
66 shared: Arc<Shared>,
67}
68
69/// The application's side of a [`StreamSource`]. Cheap to clone; every
70/// clone feeds the same terminal.
71#[derive(Clone, Debug)]
72pub struct StreamPeer {
73 shared: Arc<Shared>,
74 events: async_util::Receiver<StreamEvent>,
75}
76
77#[derive(Debug)]
78struct Shared {
79 state: Mutex<State>,
80 events: async_util::Sender<StreamEvent>,
81}
82
83/// Output written before the engine started waits here.
84#[derive(Debug, Default)]
85struct State {
86 sink: Option<ByteSink>,
87 pending: Vec<u8>,
88 exit: Option<ExitStatus>,
89}
90
91impl Shared {
92 fn lock(&self) -> std::sync::MutexGuard<'_, State> {
93 self.state
94 .lock()
95 .unwrap_or_else(std::sync::PoisonError::into_inner)
96 }
97}
98
99impl StreamSource {
100 /// A source and the peer that feeds it.
101 #[must_use]
102 pub fn new() -> (Self, StreamPeer) {
103 let (events, receiver) = async_util::unbounded();
104 let shared = Arc::new(Shared {
105 state: Mutex::new(State::default()),
106 events,
107 });
108 (
109 Self {
110 shared: shared.clone(),
111 },
112 StreamPeer {
113 shared,
114 events: receiver,
115 },
116 )
117 }
118}
119
120impl StreamPeer {
121 /// Delivers the program's output to the terminal. Output written
122 /// before the terminal starts is kept until it does.
123 pub fn output(&self, bytes: &[u8]) {
124 let mut state = self.shared.lock();
125 match &state.sink {
126 Some(sink) => {
127 sink.write(bytes);
128 }
129 None => state.pending.extend_from_slice(bytes),
130 }
131 }
132
133 /// Reports that the program ended. The terminal shows the status and
134 /// emits [`TerminalEvent::Exited`](super::TerminalEvent::Exited).
135 pub fn exit(&self, status: ExitStatus) {
136 let mut state = self.shared.lock();
137 state.exit = Some(status);
138 if let Some(sink) = &state.sink {
139 sink.finished();
140 }
141 }
142
143 /// The terminal's input, grid sizes and close, in order. Await
144 /// `recv()` on it, or poll `try_recv()`.
145 pub fn events(&self) -> &async_util::Receiver<StreamEvent> {
146 &self.events
147 }
148}
149
150/// The engine's side of a started [`StreamSource`].
151struct Handle {
152 shared: Arc<Shared>,
153}
154
155impl ByteSource for StreamSource {
156 fn start(
157 self: Box<Self>,
158 sink: ByteSink,
159 viewport: Viewport,
160 ) -> io::Result<Box<dyn ByteHandle>> {
161 {
162 let mut state = self.shared.lock();
163 let pending = std::mem::take(&mut state.pending);
164 if !pending.is_empty() {
165 sink.write(&pending);
166 }
167 if state.exit.is_some() {
168 sink.finished();
169 }
170 state.sink = Some(sink);
171 }
172 let _ = self.shared.events.try_send(StreamEvent::Resize(viewport));
173 Ok(Box::new(Handle {
174 shared: self.shared,
175 }))
176 }
177}
178
179impl ByteHandle for Handle {
180 fn write(&self, bytes: &[u8]) -> io::Result<usize> {
181 let _ = self
182 .shared
183 .events
184 .try_send(StreamEvent::Input(bytes.to_vec()));
185 Ok(bytes.len())
186 }
187
188 fn resize(&self, viewport: Viewport) -> io::Result<()> {
189 let _ = self.shared.events.try_send(StreamEvent::Resize(viewport));
190 Ok(())
191 }
192
193 fn try_wait(&self) -> io::Result<Option<ExitStatus>> {
194 Ok(self.shared.lock().exit.clone())
195 }
196
197 fn close(&self) {
198 let _ = self.shared.events.try_send(StreamEvent::Closed);
199 self.shared.lock().sink = None;
200 }
201}