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