Skip to main content

gpui_cn/terminal/
source.rs

1//! Where a terminal's frames come from.
2//!
3//! A [`FrameSource`] produces [`TerminalSnapshot`]s into a [`FrameSink`] and
4//! accepts input through a [`FrameHandle`]. The engine in this crate is one
5//! source; a fixture that parses canned bytes is another, and a host can
6//! bring its own, for example frames received from a remote server.
7
8use std::io;
9use std::sync::{Arc, Mutex};
10
11use gpui_kit::base::async_util;
12
13use crate::terminal::core::{Core, Effects};
14use crate::terminal::engine::{InputRejected, TerminalSnapshot};
15use crate::terminal::frame::{CellSelection, CursorShape, TerminalColors, Viewport};
16use crate::terminal::input::TerminalInput;
17use crate::terminal::options::EngineOptions;
18
19/// Where the engine publishes snapshots: a latest-wins slot plus a wake.
20///
21/// The UI awaits the receiver, takes the slot and applies it. Several
22/// publishes between two wakes collapse into the latest snapshot.
23#[derive(Clone)]
24pub struct FrameSink {
25    latest: Arc<Mutex<Option<Arc<TerminalSnapshot>>>>,
26    wake: async_util::Sender<()>,
27}
28
29impl std::fmt::Debug for FrameSink {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        f.debug_struct("FrameSink").finish_non_exhaustive()
32    }
33}
34
35impl FrameSink {
36    /// A sink and the receiver the UI awaits.
37    #[must_use]
38    pub fn new() -> (Self, async_util::Receiver<()>) {
39        let (wake, rx) = async_util::unbounded();
40        (
41            Self {
42                latest: Arc::new(Mutex::new(None)),
43                wake,
44            },
45            rx,
46        )
47    }
48
49    /// Publish a snapshot and wake the UI.
50    pub fn publish(&self, snapshot: Arc<TerminalSnapshot>) {
51        *self
52            .latest
53            .lock()
54            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(snapshot);
55        let _ = self.wake.try_send(());
56    }
57
58    /// Take the latest snapshot, if one was published since the last take.
59    pub fn take(&self) -> Option<Arc<TerminalSnapshot>> {
60        self.latest
61            .lock()
62            .unwrap_or_else(std::sync::PoisonError::into_inner)
63            .take()
64    }
65}
66
67/// What a source needs to start.
68#[derive(Clone, Debug)]
69#[non_exhaustive]
70pub struct StartOptions {
71    /// The initial grid.
72    pub(crate) viewport: Viewport,
73    /// The initial colors.
74    pub(crate) colors: TerminalColors,
75    /// Whether the terminal starts visible.
76    pub(crate) visible: bool,
77    /// The default cursor shape and blink, when the host overrides them.
78    pub(crate) cursor: Option<(CursorShape, bool)>,
79}
80
81impl Default for StartOptions {
82    fn default() -> Self {
83        Self {
84            viewport: Viewport::default(),
85            colors: TerminalColors::default(),
86            visible: true,
87            cursor: None,
88        }
89    }
90}
91
92impl StartOptions {
93    /// Set the initial grid.
94    #[must_use]
95    pub fn with_viewport(mut self, viewport: Viewport) -> Self {
96        self.viewport = viewport;
97        self
98    }
99
100    /// Set the initial colors.
101    #[must_use]
102    pub fn with_colors(mut self, colors: TerminalColors) -> Self {
103        self.colors = colors;
104        self
105    }
106
107    /// Set whether the terminal starts visible.
108    #[must_use]
109    pub fn with_visible(mut self, visible: bool) -> Self {
110        self.visible = visible;
111        self
112    }
113
114    /// Set the default cursor shape and blink.
115    #[must_use]
116    pub fn with_cursor(mut self, shape: CursorShape, blinking: bool) -> Self {
117        self.cursor = Some((shape, blinking));
118        self
119    }
120
121    /// The initial grid.
122    pub fn viewport(&self) -> Viewport {
123        self.viewport
124    }
125
126    /// The initial colors.
127    pub fn colors(&self) -> &TerminalColors {
128        &self.colors
129    }
130
131    /// Whether the terminal starts visible.
132    pub fn visible(&self) -> bool {
133        self.visible
134    }
135
136    /// The default cursor shape and blink, when the host overrides them.
137    pub fn cursor(&self) -> Option<(CursorShape, bool)> {
138        self.cursor
139    }
140}
141
142/// A producer of terminal snapshots.
143pub trait FrameSource: 'static {
144    /// Start publishing into `sink` and return the handle for input.
145    fn start(
146        self: Box<Self>,
147        sink: FrameSink,
148        options: StartOptions,
149    ) -> io::Result<Box<dyn FrameHandle>>;
150}
151
152/// The UI's side of a running [`FrameSource`]. Nothing here may block.
153pub trait FrameHandle: 'static {
154    /// Queue input.
155    fn input(&self, input: TerminalInput) -> Result<(), InputRejected>;
156    /// Queued input bytes not yet delivered.
157    fn input_backlog(&self) -> usize {
158        0
159    }
160    /// The grid changed.
161    fn resize(&self, viewport: Viewport);
162    /// The terminal became visible or hidden.
163    fn set_visible(&self, visible: bool);
164    /// The last frame was painted; the source may publish the next one.
165    fn request_frame(&self);
166    /// The default colors changed.
167    fn set_colors(&self, colors: TerminalColors) {
168        let _ = colors;
169    }
170    /// The default cursor changed.
171    fn set_cursor(&self, shape: CursorShape, blinking: bool) {
172        let _ = (shape, blinking);
173    }
174    /// Ask for the selected text, which may reach scrollback the frame does
175    /// not carry. `None` means the caller must read the frame instead.
176    fn copy(&self, selection: CellSelection) -> Option<async_util::Receiver<io::Result<String>>> {
177        let _ = selection;
178        None
179    }
180    /// Stop the source. Idempotent.
181    fn close(&self);
182}
183
184/// A source that parses canned bytes once, with no program behind it.
185///
186/// A resize reflows the parsed screen, as a terminal does. Input is
187/// recorded and can be read back through [`FixtureSource::inputs`], which
188/// makes it the source for UI tests and galleries.
189#[derive(Debug)]
190pub struct FixtureSource {
191    bytes: Vec<u8>,
192    options: EngineOptions,
193    inputs: Arc<Mutex<Vec<TerminalInput>>>,
194}
195
196impl FixtureSource {
197    /// A fixture rendering `bytes` as program output.
198    #[must_use]
199    pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
200        Self {
201            bytes: bytes.into(),
202            options: EngineOptions::default(),
203            inputs: Arc::new(Mutex::new(Vec::new())),
204        }
205    }
206
207    /// Set the engine options the fixture parses with.
208    #[must_use]
209    pub fn with_options(mut self, options: EngineOptions) -> Self {
210        self.options = options;
211        self
212    }
213
214    /// Everything sent to the fixture so far, oldest first.
215    #[must_use]
216    pub fn inputs(&self) -> Arc<Mutex<Vec<TerminalInput>>> {
217        self.inputs.clone()
218    }
219}
220
221struct FixtureHandle {
222    core: Mutex<Core>,
223    bells: u64,
224    inputs: Arc<Mutex<Vec<TerminalInput>>>,
225    sink: FrameSink,
226}
227
228impl FixtureHandle {
229    fn core(&self) -> std::sync::MutexGuard<'_, Core> {
230        self.core
231            .lock()
232            .unwrap_or_else(std::sync::PoisonError::into_inner)
233    }
234
235    fn publish(&self, core: &mut Core) -> io::Result<()> {
236        let frame = core
237            .frame(true)?
238            .ok_or_else(|| io::Error::other("fixture frame missing"))?;
239        let mut snapshot = TerminalSnapshot::for_frame(frame);
240        core.title().clone_into(&mut snapshot.title);
241        snapshot.cwd = core.cwd().cloned();
242        snapshot.bell_count = self.bells;
243        snapshot.at_prompt = core.at_prompt();
244        self.sink.publish(Arc::new(snapshot));
245        Ok(())
246    }
247}
248
249impl FrameSource for FixtureSource {
250    fn start(
251        self: Box<Self>,
252        sink: FrameSink,
253        options: StartOptions,
254    ) -> io::Result<Box<dyn FrameHandle>> {
255        let mut core = Core::new(options.viewport, &self.options, options.colors)?;
256        if let Some((shape, blinking)) = options.cursor {
257            core.set_cursor(shape, blinking)?;
258        }
259        let mut effects = Effects::default();
260        core.write(&self.bytes, &mut effects);
261        let handle = FixtureHandle {
262            core: Mutex::new(core),
263            bells: effects.bells,
264            inputs: self.inputs,
265            sink,
266        };
267        handle.publish(&mut handle.core())?;
268        Ok(Box::new(handle))
269    }
270}
271
272impl FrameHandle for FixtureHandle {
273    fn input(&self, input: TerminalInput) -> Result<(), InputRejected> {
274        self.inputs
275            .lock()
276            .unwrap_or_else(std::sync::PoisonError::into_inner)
277            .push(input);
278        Ok(())
279    }
280
281    fn resize(&self, viewport: Viewport) {
282        let mut core = self.core();
283        let mut effects = Effects::default();
284        if core.resize(viewport, &mut effects).is_ok() {
285            let _ = self.publish(&mut core);
286        }
287    }
288
289    fn set_visible(&self, _visible: bool) {}
290
291    fn request_frame(&self) {}
292
293    fn set_colors(&self, colors: TerminalColors) {
294        let _ = self.core().set_colors(colors);
295    }
296
297    fn set_cursor(&self, shape: CursorShape, blinking: bool) {
298        let _ = self.core().set_cursor(shape, blinking);
299    }
300
301    fn close(&self) {}
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    fn rows(sink: &FrameSink) -> Vec<(String, u64)> {
309        let snapshot = sink.take().expect("a published frame");
310        snapshot
311            .frame
312            .rows
313            .iter()
314            .take(4)
315            .map(|row| (row.text.trim_end().to_owned(), row.revision))
316            .collect()
317    }
318
319    #[test]
320    fn a_fixture_resize_reflows_and_gives_changed_rows_new_revisions() {
321        let (sink, _wake) = FrameSink::new();
322        let handle = Box::new(FixtureSource::new("$ ls\r\naaaa bbbb\r\n$ "))
323            .start(
324                sink.clone(),
325                StartOptions {
326                    viewport: Viewport::new(20, 4, 8, 16).unwrap(),
327                    ..StartOptions::default()
328                },
329            )
330            .unwrap();
331        let before = rows(&sink);
332        handle.resize(Viewport::new(5, 4, 8, 16).unwrap());
333        let after = rows(&sink);
334        let text = |rows: &[(String, u64)]| rows.iter().map(|r| r.0.clone()).collect::<Vec<_>>();
335        assert_eq!(text(&before), vec!["$ ls", "aaaa bbbb", "$", ""]);
336        assert_eq!(text(&after), vec!["$ ls", "aaaa", "bbbb", "$"]);
337        assert!(
338            after[2].1 > before[2].1,
339            "a changed row keeps its cache key: {before:?} -> {after:?}"
340        );
341    }
342}