1use std::io;
4
5use urushi::StyledGrapheme;
6use urushi_terminal::{CommandWriter, Position, TerminalSize};
7
8use super::Rect;
9use super::output::CellWriter;
10use crate::cell::{Buffer, BufferSizeError, CellWriteError};
11
12pub struct Screen<W> {
25 writer: W,
26 committed: Buffer,
27 working: Buffer,
28 needs_clear: bool,
29}
30
31impl<W: CommandWriter> Screen<W> {
32 pub fn new(writer: W, size: TerminalSize) -> io::Result<Self> {
34 let committed = Buffer::new(size).map_err(buffer_size_error)?;
35 let working = Buffer::new(size).map_err(buffer_size_error)?;
36 Ok(Self {
37 writer,
38 committed,
39 working,
40 needs_clear: true,
41 })
42 }
43
44 pub const fn size(&self) -> TerminalSize {
46 self.working.size()
47 }
48
49 pub const fn writer(&self) -> &W {
51 &self.writer
52 }
53
54 pub fn into_inner(self) -> W {
56 self.writer
57 }
58
59 pub fn resize(&mut self, size: TerminalSize) -> io::Result<()> {
61 let working = Buffer::new(size).map_err(buffer_size_error)?;
62 self.committed.resize(size).map_err(buffer_size_error)?;
63 self.working = working;
64 self.needs_clear = true;
65 Ok(())
66 }
67
68 pub fn invalidate(&mut self) {
74 self.needs_clear = true;
75 }
76
77 pub fn modify_surface(
84 &mut self,
85 output: impl FnOnce(&mut W) -> io::Result<()>,
86 ) -> io::Result<()> {
87 self.needs_clear = true;
88 output(&mut self.writer)
89 }
90
91 pub fn draw(&mut self, draw: impl FnOnce(&mut Frame<'_>)) -> io::Result<()> {
97 self.draw_with(draw, |_| Ok(()))
98 }
99
100 pub fn draw_with(
109 &mut self,
110 draw: impl FnOnce(&mut Frame<'_>),
111 present: impl FnOnce(&mut W) -> io::Result<()>,
112 ) -> io::Result<()> {
113 self.working.reset();
114 let mut frame = Frame {
115 buffer: &mut self.working,
116 area: Rect::from_size(self.committed.size()),
117 cursor: None,
118 };
119 draw(&mut frame);
120 let cursor = frame.cursor;
121
122 let blank;
123 let baseline = if self.needs_clear {
124 blank = Buffer::new(self.working.size()).map_err(buffer_size_error)?;
125 &blank
126 } else {
127 &self.committed
128 };
129 let changes = baseline.diff(&self.working).map_err(io::Error::other)?;
130 let result = (|| {
131 if self.needs_clear {
132 self.writer.clear()?;
133 }
134 self.writer.draw(changes)?;
135 self.writer.set_cursor(cursor)?;
136 present(&mut self.writer)?;
137 self.writer.flush()
138 })();
139
140 if let Err(error) = result {
141 self.needs_clear = true;
142 return Err(error);
143 }
144
145 std::mem::swap(&mut self.committed, &mut self.working);
146 self.needs_clear = false;
147 Ok(())
148 }
149}
150
151fn buffer_size_error(error: BufferSizeError) -> io::Error {
152 io::Error::new(io::ErrorKind::InvalidInput, error)
153}
154
155pub struct Frame<'a> {
160 buffer: &'a mut Buffer,
161 area: Rect,
162 cursor: Option<Position>,
163}
164
165impl Frame<'_> {
166 pub const fn area(&self) -> Rect {
168 self.area
169 }
170
171 pub fn put(&mut self, column: usize, row: usize, cell: &StyledGrapheme) {
173 match self.buffer.write(column, row, cell) {
174 Ok(()) | Err(CellWriteError::ZeroWidth) | Err(CellWriteError::OutOfBounds { .. }) => {}
175 }
176 }
177
178 pub fn set_cursor(&mut self, at: Option<Position>) {
180 self.cursor = at;
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use urushi::{Available, Color, TextStyle, View, resolve};
188 use urushi_terminal::{ClearRegion, Command, CursorMove, TerminalOutput, TerminalStyle};
189
190 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
191 enum Failure {
192 Clear,
193 Print,
194 Cursor,
195 Flush,
196 }
197
198 #[derive(Debug, PartialEq, Eq)]
199 enum Recorded {
200 Clear,
201 Move(Position),
202 CursorVisible(bool),
203 Style(TerminalStyle),
204 ResetStyle,
205 Print(String),
206 Flush,
207 }
208
209 #[derive(Default)]
210 struct RecordingCommands {
211 commands: Vec<Recorded>,
212 failure: Option<Failure>,
213 reset_failure_countdown: Option<usize>,
214 }
215
216 impl RecordingCommands {
217 fn fail_once(&mut self, failure: Failure) {
218 self.failure = Some(failure);
219 }
220
221 fn take_failure(&mut self, failure: Failure) -> io::Result<()> {
222 if self.failure == Some(failure) {
223 self.failure = None;
224 Err(io::Error::other("planned terminal failure"))
225 } else {
226 Ok(())
227 }
228 }
229
230 fn fail_reset_after(&mut self, successful_resets: usize) {
231 self.reset_failure_countdown = Some(successful_resets);
232 }
233
234 fn clear_count(&self) -> usize {
235 self.commands
236 .iter()
237 .filter(|command| matches!(command, Recorded::Clear))
238 .count()
239 }
240 }
241
242 impl TerminalOutput for RecordingCommands {
243 fn flush(&mut self) -> io::Result<()> {
244 self.commands.push(Recorded::Flush);
245 self.take_failure(Failure::Flush)
246 }
247 }
248
249 impl CommandWriter for RecordingCommands {
250 fn write_command(&mut self, command: Command<'_>) -> io::Result<()> {
251 match command {
252 Command::MoveCursor(CursorMove::To(position)) => {
253 self.commands.push(Recorded::Move(position));
254 }
255 Command::SetCursorVisible(visible) => {
256 self.commands.push(Recorded::CursorVisible(visible));
257 self.take_failure(Failure::Cursor)?;
258 }
259 Command::SetStyle(style) => self.commands.push(Recorded::Style(style)),
260 Command::ResetStyle => {
261 self.commands.push(Recorded::ResetStyle);
262 if let Some(countdown) = &mut self.reset_failure_countdown {
263 if *countdown == 0 {
264 self.reset_failure_countdown = None;
265 return Err(io::Error::other("planned terminal failure"));
266 }
267 *countdown -= 1;
268 }
269 }
270 Command::Print(text) => {
271 self.commands
272 .push(Recorded::Print(text.as_str().to_owned()));
273 self.take_failure(Failure::Print)?;
274 }
275 Command::Clear(ClearRegion::Screen) => {
276 self.commands.push(Recorded::Clear);
277 self.take_failure(Failure::Clear)?;
278 }
279 _ => panic!("unexpected cell command: {command:?}"),
280 }
281 Ok(())
282 }
283 }
284
285 fn grapheme(symbol: &str) -> StyledGrapheme {
286 resolve(&View::text(symbol, TextStyle::new()), Available::size(2, 1))
287 .expect("test view resolves")
288 .rows()[0][0]
289 .clone()
290 }
291
292 fn draw_pair(
293 screen: &mut Screen<RecordingCommands>,
294 left: &StyledGrapheme,
295 right: &StyledGrapheme,
296 ) -> io::Result<()> {
297 screen.draw(|frame| {
298 frame.put(0, 0, left);
299 frame.put(1, 0, right);
300 })
301 }
302
303 #[test]
304 fn successful_draw_commits_the_incremental_baseline() {
305 let mut screen = Screen::new(RecordingCommands::default(), TerminalSize::new(2, 1))
306 .expect("screen size is valid");
307 let a = grapheme("a");
308 let b = grapheme("b");
309 let c = grapheme("c");
310 draw_pair(&mut screen, &a, &b).expect("initial frame succeeds");
311 let next = screen.writer().commands.len();
312
313 draw_pair(&mut screen, &a, &c).expect("incremental frame succeeds");
314
315 let prints = screen.writer().commands[next..]
316 .iter()
317 .filter_map(|command| match command {
318 Recorded::Print(symbol) => Some(symbol.as_str()),
319 _ => None,
320 })
321 .collect::<Vec<_>>();
322 assert_eq!(prints, ["c"]);
323 assert_eq!(screen.writer().clear_count(), 1);
324 }
325
326 #[test]
327 fn any_partial_output_failure_forces_a_complete_repair() {
328 for failure in [Failure::Print, Failure::Cursor, Failure::Flush] {
329 let mut screen = Screen::new(RecordingCommands::default(), TerminalSize::new(2, 1))
330 .expect("screen size is valid");
331 let a = grapheme("a");
332 let b = grapheme("b");
333 let c = grapheme("c");
334 let d = grapheme("d");
335 draw_pair(&mut screen, &a, &b).expect("baseline frame succeeds");
336
337 screen.writer.fail_once(failure);
338 assert!(draw_pair(&mut screen, &c, &d).is_err());
339 let repair = screen.writer().commands.len();
340 draw_pair(&mut screen, &a, &c).expect("repair frame succeeds");
341
342 let repaired = &screen.writer().commands[repair..];
343 assert!(repaired.starts_with(&[Recorded::ResetStyle, Recorded::Clear]));
344 let prints = repaired
345 .iter()
346 .filter_map(|command| match command {
347 Recorded::Print(symbol) => Some(symbol.as_str()),
348 _ => None,
349 })
350 .collect::<Vec<_>>();
351 assert_eq!(prints, ["a", "c"], "{failure:?}");
352 assert_eq!(screen.writer().clear_count(), 2, "{failure:?}");
353 }
354 }
355
356 #[test]
357 fn extension_failure_prevents_cell_commit_and_forces_complete_repair() {
358 let mut screen = Screen::new(RecordingCommands::default(), TerminalSize::new(2, 1))
359 .expect("screen size is valid");
360 let a = grapheme("a");
361 let b = grapheme("b");
362
363 let error = screen
364 .draw_with(
365 |frame| {
366 frame.put(0, 0, &a);
367 frame.put(1, 0, &b);
368 },
369 |_| Err(io::Error::other("planned extension failure")),
370 )
371 .expect_err("extension failure aborts the transaction");
372 assert_eq!(error.to_string(), "planned extension failure");
373
374 let repair = screen.writer().commands.len();
375 screen
376 .draw(|frame| {
377 frame.put(0, 0, &a);
378 frame.put(1, 0, &b);
379 })
380 .expect("the complete cell frame is retried");
381
382 assert!(
383 screen.writer().commands[repair..]
384 .starts_with(&[Recorded::ResetStyle, Recorded::Clear])
385 );
386 let prints = screen.writer().commands[repair..]
387 .iter()
388 .filter_map(|command| match command {
389 Recorded::Print(symbol) => Some(symbol.as_str()),
390 _ => None,
391 })
392 .collect::<Vec<_>>();
393 assert_eq!(prints, ["a", "b"]);
394 }
395
396 #[test]
397 fn direct_surface_output_forces_the_next_draw_to_reconstruct_cells() {
398 let mut screen = Screen::new(RecordingCommands::default(), TerminalSize::new(2, 1))
399 .expect("screen size is valid");
400 let a = grapheme("a");
401 let b = grapheme("b");
402 draw_pair(&mut screen, &a, &b).expect("baseline frame succeeds");
403
404 screen
405 .modify_surface(|writer| writer.clear())
406 .expect("direct output succeeds");
407 let repair = screen.writer().commands.len();
408 draw_pair(&mut screen, &a, &b).expect("unchanged cells are reconstructed");
409
410 let repaired = &screen.writer().commands[repair..];
411 assert!(repaired.starts_with(&[Recorded::ResetStyle, Recorded::Clear]));
412 let prints = repaired
413 .iter()
414 .filter_map(|command| match command {
415 Recorded::Print(symbol) => Some(symbol.as_str()),
416 _ => None,
417 })
418 .collect::<Vec<_>>();
419 assert_eq!(prints, ["a", "b"]);
420 }
421
422 #[test]
423 fn failed_clear_is_retried_before_cells_are_sent() {
424 let mut writer = RecordingCommands::default();
425 writer.fail_once(Failure::Clear);
426 let mut screen =
427 Screen::new(writer, TerminalSize::new(1, 1)).expect("screen size is valid");
428 let a = grapheme("a");
429
430 assert!(screen.draw(|frame| frame.put(0, 0, &a)).is_err());
431 assert!(
432 !screen
433 .writer()
434 .commands
435 .iter()
436 .any(|command| matches!(command, Recorded::Print(_)))
437 );
438 screen
439 .draw(|frame| frame.put(0, 0, &a))
440 .expect("clear and frame are retried");
441
442 assert_eq!(screen.writer().clear_count(), 2);
443 }
444
445 #[test]
446 fn empty_repair_resets_style_before_clearing() {
447 let mut screen = Screen::new(RecordingCommands::default(), TerminalSize::new(1, 1))
448 .expect("screen size is valid");
449 let styled = resolve(
450 &View::text("x", TextStyle::new().background(Color::BLUE)),
451 Available::size(1, 1),
452 )
453 .expect("test view resolves")
454 .rows()[0][0]
455 .clone();
456 screen.draw(|_| {}).expect("baseline frame succeeds");
457
458 screen.writer.fail_reset_after(1);
459 assert!(screen.draw(|frame| frame.put(0, 0, &styled)).is_err());
460 let repair = screen.writer().commands.len();
461 screen.draw(|_| {}).expect("empty repair frame succeeds");
462
463 assert!(
464 screen.writer().commands[repair..]
465 .starts_with(&[Recorded::ResetStyle, Recorded::Clear])
466 );
467 }
468
469 #[test]
470 fn resize_discards_the_old_diff_baseline() {
471 let mut screen = Screen::new(RecordingCommands::default(), TerminalSize::new(2, 1))
472 .expect("screen size is valid");
473 let a = grapheme("a");
474 screen
475 .draw(|frame| frame.put(0, 0, &a))
476 .expect("initial frame succeeds");
477
478 screen
479 .resize(TerminalSize::new(3, 1))
480 .expect("new size is valid");
481 screen
482 .draw(|frame| frame.put(0, 0, &a))
483 .expect("resized frame succeeds");
484
485 assert_eq!(screen.size(), TerminalSize::new(3, 1));
486 assert_eq!(screen.writer().clear_count(), 2);
487 }
488
489 #[test]
490 fn frame_exposes_area_and_cursor_without_owning_output() {
491 let mut screen = Screen::new(RecordingCommands::default(), TerminalSize::new(2, 1))
492 .expect("screen size is valid");
493 let wide = grapheme("界");
494
495 screen
496 .draw(|frame| {
497 assert_eq!(frame.area(), Rect::from_size(TerminalSize::new(2, 1)));
498 frame.put(1, 0, &wide);
499 frame.set_cursor(Some(Position::new(1, 0)));
500 })
501 .expect("out-of-bounds cells are ignored");
502
503 assert!(
504 screen
505 .writer()
506 .commands
507 .contains(&Recorded::CursorVisible(true))
508 );
509 assert!(
510 screen
511 .writer()
512 .commands
513 .contains(&Recorded::Move(Position::new(1, 0)))
514 );
515 }
516}