1use 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#[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 #[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 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 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#[derive(Clone, Debug)]
69#[non_exhaustive]
70pub struct StartOptions {
71 pub(crate) viewport: Viewport,
73 pub(crate) colors: TerminalColors,
75 pub(crate) visible: bool,
77 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 #[must_use]
95 pub fn with_viewport(mut self, viewport: Viewport) -> Self {
96 self.viewport = viewport;
97 self
98 }
99
100 #[must_use]
102 pub fn with_colors(mut self, colors: TerminalColors) -> Self {
103 self.colors = colors;
104 self
105 }
106
107 #[must_use]
109 pub fn with_visible(mut self, visible: bool) -> Self {
110 self.visible = visible;
111 self
112 }
113
114 #[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 pub fn viewport(&self) -> Viewport {
123 self.viewport
124 }
125
126 pub fn colors(&self) -> &TerminalColors {
128 &self.colors
129 }
130
131 pub fn visible(&self) -> bool {
133 self.visible
134 }
135
136 pub fn cursor(&self) -> Option<(CursorShape, bool)> {
138 self.cursor
139 }
140}
141
142pub trait FrameSource: 'static {
144 fn start(
146 self: Box<Self>,
147 sink: FrameSink,
148 options: StartOptions,
149 ) -> io::Result<Box<dyn FrameHandle>>;
150}
151
152pub trait FrameHandle: 'static {
154 fn input(&self, input: TerminalInput) -> Result<(), InputRejected>;
156 fn input_backlog(&self) -> usize {
158 0
159 }
160 fn resize(&self, viewport: Viewport);
162 fn set_visible(&self, visible: bool);
164 fn request_frame(&self);
166 fn set_colors(&self, colors: TerminalColors) {
168 let _ = colors;
169 }
170 fn set_cursor(&self, shape: CursorShape, blinking: bool) {
172 let _ = (shape, blinking);
173 }
174 fn copy(&self, selection: CellSelection) -> Option<async_util::Receiver<io::Result<String>>> {
177 let _ = selection;
178 None
179 }
180 fn close(&self);
182}
183
184#[derive(Debug)]
190pub struct FixtureSource {
191 bytes: Vec<u8>,
192 options: EngineOptions,
193 inputs: Arc<Mutex<Vec<TerminalInput>>>,
194}
195
196impl FixtureSource {
197 #[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 #[must_use]
209 pub fn with_options(mut self, options: EngineOptions) -> Self {
210 self.options = options;
211 self
212 }
213
214 #[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}