1use ratatui_core::{
2 buffer::Buffer,
3 layout::Rect,
4 style::{Color, Modifier, Style},
5 widgets::Widget,
6};
7use ratatui_widgets::{block::Block, clear::Clear};
8
9use crate::state;
10
11pub trait Screen {
16 type C: Cell;
18
19 fn cell(&self, row: u16, col: u16) -> Option<&Self::C>;
21 fn hide_cursor(&self) -> bool;
23 fn cursor_position(&self) -> (u16, u16);
27}
28
29pub trait Cell {
31 fn has_contents(&self) -> bool;
33 fn apply(&self, cell: &mut ratatui_core::buffer::Cell);
35}
36
37#[non_exhaustive]
67pub struct PseudoTerminal<'a, S> {
68 screen: &'a S,
69 pub(crate) block: Option<Block<'a>>,
70 style: Option<Style>,
71 pub(crate) cursor: Cursor,
72}
73
74#[non_exhaustive]
75pub struct Cursor {
76 pub(crate) show: bool,
77 pub(crate) symbol: String,
78 pub(crate) style: Style,
79 pub(crate) overlay_style: Style,
80}
81
82impl Cursor {
83 #[inline]
98 #[must_use]
99 pub fn symbol(mut self, symbol: &str) -> Self {
100 self.symbol = symbol.into();
101 self
102 }
103
104 #[inline]
119 #[must_use]
120 pub const fn style(mut self, style: Style) -> Self {
121 self.style = style;
122 self
123 }
124
125 #[inline]
142 #[must_use]
143 pub const fn overlay_style(mut self, overlay_style: Style) -> Self {
144 self.overlay_style = overlay_style;
145 self
146 }
147
148 #[inline]
150 #[must_use]
151 pub const fn visibility(mut self, show: bool) -> Self {
152 self.show = show;
153 self
154 }
155
156 #[inline]
158 pub fn show(&mut self) {
159 self.show = true;
160 }
161
162 #[inline]
164 pub fn hide(&mut self) {
165 self.show = false;
166 }
167}
168
169impl Default for Cursor {
170 #[inline]
171 fn default() -> Self {
172 Self {
173 show: true,
174 symbol: "\u{2588}".into(), style: Style::default().fg(Color::Gray),
176 overlay_style: Style::default().add_modifier(Modifier::REVERSED),
177 }
178 }
179}
180
181impl<'a, S: Screen> PseudoTerminal<'a, S> {
182 #[inline]
198 #[must_use]
199 pub fn new(screen: &'a S) -> Self {
200 PseudoTerminal {
201 screen,
202 block: None,
203 style: None,
204 cursor: Cursor::default(),
205 }
206 }
207
208 #[inline]
226 #[must_use]
227 pub fn block(mut self, block: Block<'a>) -> Self {
228 self.block = Some(block);
229 self
230 }
231
232 #[inline]
252 #[must_use]
253 pub fn cursor(mut self, cursor: Cursor) -> Self {
254 self.cursor = cursor;
255 self
256 }
257
258 #[inline]
275 #[must_use]
276 pub const fn style(mut self, style: Style) -> Self {
277 self.style = Some(style);
278 self
279 }
280
281 #[inline]
282 #[must_use]
283 pub const fn screen(&self) -> &S {
284 self.screen
285 }
286}
287
288impl<S: Screen> Widget for PseudoTerminal<'_, S> {
289 #[inline]
290 fn render(self, area: Rect, buf: &mut Buffer) {
291 Clear.render(area, buf);
292 let area = self.block.as_ref().map_or(area, |b| {
293 let inner_area = b.inner(area);
294 b.clone().render(area, buf);
295 inner_area
296 });
297 state::handle(&self, area, buf);
298 }
299}
300
301#[cfg(all(test, feature = "vt100"))]
302mod tests {
303 use ratatui::Terminal;
304 use ratatui_core::backend::TestBackend;
305 use ratatui_widgets::borders::Borders;
306
307 use super::*;
308
309 fn snapshot_typescript(stream: &[u8]) -> String {
310 let backend = TestBackend::new(80, 24);
311 let mut terminal = Terminal::new(backend).unwrap();
312 let mut parser = vt100::Parser::new(24, 80, 0);
313 parser.process(stream);
314 let pseudo_term = PseudoTerminal::new(parser.screen());
315 terminal
316 .draw(|f| {
317 f.render_widget(pseudo_term, f.area());
318 })
319 .unwrap();
320 format!("{:?}", terminal.backend().buffer())
321 }
322
323 #[test]
324 fn empty_actions() {
325 let backend = TestBackend::new(80, 24);
326 let mut terminal = Terminal::new(backend).unwrap();
327 let mut parser = vt100::Parser::new(24, 80, 0);
328 parser.process(b" ");
329 let pseudo_term = PseudoTerminal::new(parser.screen());
330 terminal
331 .draw(|f| {
332 f.render_widget(pseudo_term, f.area());
333 })
334 .unwrap();
335 let view = format!("{:?}", terminal.backend().buffer());
336 insta::assert_snapshot!(view);
337 }
338 #[test]
339 fn boundary_rows_overshot_no_panic() {
340 let stream = include_bytes!("../test/typescript/simple_ls.typescript");
341 let backend = TestBackend::new(80, 4);
343 let mut terminal = Terminal::new(backend).unwrap();
344 let mut parser = vt100::Parser::new(24, 80, 0);
345 parser.process(stream);
346 let pseudo_term = PseudoTerminal::new(parser.screen());
347 terminal
348 .draw(|f| {
349 f.render_widget(pseudo_term, f.area());
350 })
351 .unwrap();
352 let view = format!("{:?}", terminal.backend().buffer());
353 insta::assert_snapshot!(view);
354 }
355
356 #[test]
357 fn simple_ls() {
358 let stream = include_bytes!("../test/typescript/simple_ls.typescript");
359 let view = snapshot_typescript(stream);
360 insta::assert_snapshot!(view);
361 }
362 #[test]
363 fn simple_cursor_alternate_symbol() {
364 let stream = include_bytes!("../test/typescript/simple_ls.typescript");
365 let backend = TestBackend::new(80, 24);
366 let mut terminal = Terminal::new(backend).unwrap();
367 let mut parser = vt100::Parser::new(24, 80, 0);
368 let cursor = Cursor::default().symbol("|");
369 parser.process(stream);
370 let pseudo_term = PseudoTerminal::new(parser.screen()).cursor(cursor);
371 terminal
372 .draw(|f| {
373 f.render_widget(pseudo_term, f.area());
374 })
375 .unwrap();
376 let view = format!("{:?}", terminal.backend().buffer());
377 insta::assert_snapshot!(view);
378 }
379 #[test]
380 fn simple_cursor_styled() {
381 let stream = include_bytes!("../test/typescript/simple_ls.typescript");
382 let backend = TestBackend::new(80, 24);
383 let mut terminal = Terminal::new(backend).unwrap();
384 let mut parser = vt100::Parser::new(24, 80, 0);
385 let style = Style::default().bg(Color::Cyan).fg(Color::LightRed);
386 let cursor = Cursor::default().symbol("|").style(style);
387 parser.process(stream);
388 let pseudo_term = PseudoTerminal::new(parser.screen()).cursor(cursor);
389 terminal
390 .draw(|f| {
391 f.render_widget(pseudo_term, f.area());
392 })
393 .unwrap();
394 let view = format!("{:?}", terminal.backend().buffer());
395 insta::assert_snapshot!(view);
396 }
397 #[test]
398 fn simple_cursor_hide() {
399 let stream = include_bytes!("../test/typescript/simple_ls.typescript");
400 let backend = TestBackend::new(80, 24);
401 let mut terminal = Terminal::new(backend).unwrap();
402 let mut parser = vt100::Parser::new(24, 80, 0);
403 let cursor = Cursor::default().visibility(false);
404 parser.process(stream);
405 let pseudo_term = PseudoTerminal::new(parser.screen()).cursor(cursor);
406 terminal
407 .draw(|f| {
408 f.render_widget(pseudo_term, f.area());
409 })
410 .unwrap();
411 let view = format!("{:?}", terminal.backend().buffer());
412 insta::assert_snapshot!(view);
413 }
414 #[test]
415 fn simple_cursor_hide_alt() {
416 let stream = include_bytes!("../test/typescript/simple_ls.typescript");
417 let backend = TestBackend::new(80, 24);
418 let mut terminal = Terminal::new(backend).unwrap();
419 let mut parser = vt100::Parser::new(24, 80, 0);
420 let mut cursor = Cursor::default();
421 cursor.hide();
422 parser.process(stream);
423 let pseudo_term = PseudoTerminal::new(parser.screen()).cursor(cursor);
424 terminal
425 .draw(|f| {
426 f.render_widget(pseudo_term, f.area());
427 })
428 .unwrap();
429 let view = format!("{:?}", terminal.backend().buffer());
430 insta::assert_snapshot!(view);
431 }
432 #[test]
433 fn overlapping_cursor() {
434 let stream = include_bytes!("../test/typescript/overlapping_cursor.typescript");
435 let view = snapshot_typescript(stream);
436 insta::assert_snapshot!(view);
437 }
438 #[test]
439 fn overlapping_cursor_alternate_style() {
440 let stream = include_bytes!("../test/typescript/overlapping_cursor.typescript");
441 let backend = TestBackend::new(80, 24);
442 let mut terminal = Terminal::new(backend).unwrap();
443 let mut parser = vt100::Parser::new(24, 80, 0);
444 let style = Style::default().bg(Color::Cyan).fg(Color::LightRed);
445 let cursor = Cursor::default().overlay_style(style);
446 parser.process(stream);
447 let pseudo_term = PseudoTerminal::new(parser.screen()).cursor(cursor);
448 terminal
449 .draw(|f| {
450 f.render_widget(pseudo_term, f.area());
451 })
452 .unwrap();
453 let view = format!("{:?}", terminal.backend().buffer());
454 insta::assert_snapshot!(view);
455 }
456 #[test]
457 fn simple_ls_with_block() {
458 let stream = include_bytes!("../test/typescript/simple_ls.typescript");
459 let backend = TestBackend::new(100, 24);
460 let mut terminal = Terminal::new(backend).unwrap();
461 let mut parser = vt100::Parser::new(24, 80, 0);
462 parser.process(stream);
463 let block = Block::default().borders(Borders::ALL).title("ls");
464 let pseudo_term = PseudoTerminal::new(parser.screen()).block(block);
465 terminal
466 .draw(|f| {
467 f.render_widget(pseudo_term, f.area());
468 })
469 .unwrap();
470 let view = format!("{:?}", terminal.backend().buffer());
471 insta::assert_snapshot!(view);
472 }
473 #[test]
474 fn simple_ls_no_style_from_block() {
475 let stream = include_bytes!("../test/typescript/simple_ls.typescript");
476 let backend = TestBackend::new(100, 24);
477 let mut terminal = Terminal::new(backend).unwrap();
478 let mut parser = vt100::Parser::new(24, 80, 0);
479 parser.process(stream);
480 let block = Block::default()
481 .borders(Borders::ALL)
482 .style(Style::default().add_modifier(Modifier::BOLD))
483 .title("ls");
484 let pseudo_term = PseudoTerminal::new(parser.screen()).block(block);
485 terminal
486 .draw(|f| {
487 f.render_widget(pseudo_term, f.area());
488 })
489 .unwrap();
490 let view = format!("{:?}", terminal.backend().buffer());
491 insta::assert_snapshot!(view);
492 }
493 #[test]
494 fn italic_text() {
495 let stream = b"[3mThis line will be displayed in italic.[0m This should have no style.";
496 let view = snapshot_typescript(stream);
497 insta::assert_snapshot!(view);
498 }
499 #[test]
500 fn underlined_text() {
501 let stream =
502 b"[4mThis line will be displayed with an underline.[0m This should have no style.";
503 let view = snapshot_typescript(stream);
504 insta::assert_snapshot!(view);
505 }
506 #[test]
507 fn bold_text() {
508 let stream = b"[1mThis line will be displayed bold.[0m This should have no style.";
509 let view = snapshot_typescript(stream);
510 insta::assert_snapshot!(view);
511 }
512 #[test]
513 fn inverse_text() {
514 let stream = b"[7mThis line will be displayed inversed.[0m This should have no style.";
515 let view = snapshot_typescript(stream);
516 insta::assert_snapshot!(view);
517 }
518 #[test]
519 fn combined_modifier_text() {
520 let stream =
521 b"[4m[3mThis line will be displayed in italic and underlined.[0m This should have no style.";
522 let view = snapshot_typescript(stream);
523 insta::assert_snapshot!(view);
524 }
525
526 #[test]
527 fn vttest_02_01() {
528 let stream = include_bytes!("../test/typescript/vttest_02_01.typescript");
529 let view = snapshot_typescript(stream);
530 insta::assert_snapshot!(view);
531 }
532 #[test]
533 fn vttest_02_02() {
534 let stream = include_bytes!("../test/typescript/vttest_02_02.typescript");
535 let view = snapshot_typescript(stream);
536 insta::assert_snapshot!(view);
537 }
538 #[test]
539 fn vttest_02_03() {
540 let stream = include_bytes!("../test/typescript/vttest_02_03.typescript");
541 let view = snapshot_typescript(stream);
542 insta::assert_snapshot!(view);
543 }
544 #[test]
545 fn vttest_02_04() {
546 let stream = include_bytes!("../test/typescript/vttest_02_04.typescript");
547 let view = snapshot_typescript(stream);
548 insta::assert_snapshot!(view);
549 }
550 #[test]
551 fn vttest_02_05() {
552 let stream = include_bytes!("../test/typescript/vttest_02_05.typescript");
553 let view = snapshot_typescript(stream);
554 insta::assert_snapshot!(view);
555 }
556 #[test]
557 fn vttest_02_06() {
558 let stream = include_bytes!("../test/typescript/vttest_02_06.typescript");
559 let view = snapshot_typescript(stream);
560 insta::assert_snapshot!(view);
561 }
562 #[test]
563 fn vttest_02_07() {
564 let stream = include_bytes!("../test/typescript/vttest_02_07.typescript");
565 let view = snapshot_typescript(stream);
566 insta::assert_snapshot!(view);
567 }
568 #[test]
569 fn vttest_02_08() {
570 let stream = include_bytes!("../test/typescript/vttest_02_08.typescript");
571 let view = snapshot_typescript(stream);
572 insta::assert_snapshot!(view);
573 }
574 #[test]
575 fn vttest_02_09() {
576 let stream = include_bytes!("../test/typescript/vttest_02_09.typescript");
577 let view = snapshot_typescript(stream);
578 insta::assert_snapshot!(view);
579 }
580 #[test]
581 fn vttest_02_10() {
582 let stream = include_bytes!("../test/typescript/vttest_02_10.typescript");
583 let view = snapshot_typescript(stream);
584 insta::assert_snapshot!(view);
585 }
586 #[test]
587 fn vttest_02_11() {
588 let stream = include_bytes!("../test/typescript/vttest_02_11.typescript");
589 let view = snapshot_typescript(stream);
590 insta::assert_snapshot!(view);
591 }
592 #[test]
593 fn vttest_02_12() {
594 let stream = include_bytes!("../test/typescript/vttest_02_12.typescript");
595 let view = snapshot_typescript(stream);
596 insta::assert_snapshot!(view);
597 }
598 #[test]
599 fn vttest_02_13() {
600 let stream = include_bytes!("../test/typescript/vttest_02_13.typescript");
601 let view = snapshot_typescript(stream);
602 insta::assert_snapshot!(view);
603 }
604 #[test]
605 fn vttest_02_14() {
606 let stream = include_bytes!("../test/typescript/vttest_02_14.typescript");
607 let view = snapshot_typescript(stream);
608 insta::assert_snapshot!(view);
609 }
610 #[test]
611 fn vttest_02_15() {
612 let stream = include_bytes!("../test/typescript/vttest_02_15.typescript");
613 let view = snapshot_typescript(stream);
614 insta::assert_snapshot!(view);
615 }
616}