tui_term/
widget.rs

1use ratatui::{
2    buffer::Buffer,
3    layout::Rect,
4    style::{Color, Modifier, Style},
5    widgets::{Block, Clear, Widget},
6};
7
8use crate::state;
9
10/// A trait representing a pseudo-terminal screen.
11///
12/// Implementing this trait allows for backends other than `vt100` to be used
13/// with the `PseudoTerminal` widget.
14pub trait Screen {
15    /// The type of cell this screen contains
16    type C: Cell;
17
18    /// Returns the cell at the given location if it exists.
19    fn cell(&self, row: u16, col: u16) -> Option<&Self::C>;
20    /// Returns whether the terminal should be hidden
21    fn hide_cursor(&self) -> bool;
22    /// Returns cursor position of screen.
23    ///
24    /// The return value is expected to be (row, column)
25    fn cursor_position(&self) -> (u16, u16);
26}
27
28/// A trait for representing a single cell on a screen.
29pub trait Cell {
30    /// Whether the cell has any contents that could be rendered to the screen.
31    fn has_contents(&self) -> bool;
32    /// Apply the contents and styling of this cell to the provided buffer cell.
33    fn apply(&self, cell: &mut ratatui::buffer::Cell);
34}
35
36/// A widget representing a pseudo-terminal screen.
37///
38/// The `PseudoTerminal` widget displays the contents of a pseudo-terminal screen,
39/// which is typically populated with text and control sequences from a terminal emulator.
40/// It provides a visual representation of the terminal output within a TUI application.
41///
42/// The contents of the pseudo-terminal screen are represented by a `vt100::Screen` object.
43/// The `vt100` library provides functionality for parsing and processing terminal control sequences
44/// and handling terminal state, allowing the `PseudoTerminal` widget to accurately render the
45/// terminal output.
46///
47/// # Examples
48///
49/// ```rust
50/// use ratatui::{
51///     style::{Color, Modifier, Style},
52///     widgets::{Block, Borders},
53/// };
54/// use tui_term::widget::PseudoTerminal;
55/// use vt100::Parser;
56///
57/// let mut parser = vt100::Parser::new(24, 80, 0);
58/// let pseudo_term = PseudoTerminal::new(parser.screen())
59///     .block(Block::default().title("Terminal").borders(Borders::ALL))
60///     .style(
61///         Style::default()
62///             .fg(Color::White)
63///             .bg(Color::Black)
64///             .add_modifier(Modifier::BOLD),
65///     );
66/// ```
67#[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    /// Sets the symbol for the cursor.
85    ///
86    /// # Arguments
87    ///
88    /// * `symbol`: The symbol to set as the cursor.
89    ///
90    /// # Example
91    ///
92    /// ```
93    /// use ratatui::style::Style;
94    /// use tui_term::widget::Cursor;
95    ///
96    /// let cursor = Cursor::default().symbol("|");
97    /// ```
98    #[inline]
99    #[must_use]
100    pub fn symbol(mut self, symbol: &str) -> Self {
101        self.symbol = symbol.into();
102        self
103    }
104
105    /// Sets the style for the cursor.
106    ///
107    /// # Arguments
108    ///
109    /// * `style`: The `Style` to set for the cursor.
110    ///
111    /// # Example
112    ///
113    /// ```
114    /// use ratatui::style::Style;
115    /// use tui_term::widget::Cursor;
116    ///
117    /// let cursor = Cursor::default().style(Style::default());
118    /// ```
119    #[inline]
120    #[must_use]
121    pub const fn style(mut self, style: Style) -> Self {
122        self.style = style;
123        self
124    }
125
126    /// Sets the overlay style for the cursor.
127    ///
128    /// The overlay style is used when the cursor overlaps with existing content on the screen.
129    ///
130    /// # Arguments
131    ///
132    /// * `overlay_style`: The `Style` to set as the overlay style for the cursor.
133    ///
134    /// # Example
135    ///
136    /// ```
137    /// use ratatui::style::Style;
138    /// use tui_term::widget::Cursor;
139    ///
140    /// let cursor = Cursor::default().overlay_style(Style::default());
141    /// ```
142    #[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    /// Set the visibility of the cursor (default = shown)
150    #[inline]
151    #[must_use]
152    pub const fn visibility(mut self, show: bool) -> Self {
153        self.show = show;
154        self
155    }
156
157    /// Show the cursor (default)
158    #[inline]
159    pub fn show(&mut self) {
160        self.show = true;
161    }
162
163    /// Hide the cursor
164    #[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(), //"█".
176            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    /// Creates a new instance of `PseudoTerminal`.
184    ///
185    /// # Arguments
186    ///
187    /// * `screen`: The reference to the `Screen`.
188    ///
189    /// # Example
190    ///
191    /// ```
192    /// use tui_term::widget::PseudoTerminal;
193    /// use vt100::Parser;
194    ///
195    /// let mut parser = vt100::Parser::new(24, 80, 0);
196    /// let pseudo_term = PseudoTerminal::new(parser.screen());
197    /// ```
198    #[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    /// Sets the block for the `PseudoTerminal`.
210    ///
211    /// # Arguments
212    ///
213    /// * `block`: The `Block` to set.
214    ///
215    /// # Example
216    ///
217    /// ```
218    /// use ratatui::widgets::Block;
219    /// use tui_term::widget::PseudoTerminal;
220    /// use vt100::Parser;
221    ///
222    /// let mut parser = vt100::Parser::new(24, 80, 0);
223    /// let block = Block::default();
224    /// let pseudo_term = PseudoTerminal::new(parser.screen()).block(block);
225    /// ```
226    #[inline]
227    #[must_use]
228    pub fn block(mut self, block: Block<'a>) -> Self {
229        self.block = Some(block);
230        self
231    }
232
233    /// Sets the cursor configuration for the `PseudoTerminal`.
234    ///
235    /// The `cursor` method allows configuring the appearance of the cursor within the
236    /// `PseudoTerminal` widget.
237    ///
238    /// # Arguments
239    ///
240    /// * `cursor`: The `Cursor` configuration to set.
241    ///
242    /// # Example
243    ///
244    /// ```rust
245    /// use ratatui::style::Style;
246    /// use tui_term::widget::{Cursor, PseudoTerminal};
247    ///
248    /// let mut parser = vt100::Parser::new(24, 80, 0);
249    /// let cursor = Cursor::default().symbol("|").style(Style::default());
250    /// let pseudo_term = PseudoTerminal::new(parser.screen()).cursor(cursor);
251    /// ```
252    #[inline]
253    #[must_use]
254    pub fn cursor(mut self, cursor: Cursor) -> Self {
255        self.cursor = cursor;
256        self
257    }
258
259    /// Sets the style for `PseudoTerminal`.
260    ///
261    /// # Arguments
262    ///
263    /// * `style`: The `Style` to set.
264    ///
265    /// # Example
266    ///
267    /// ```
268    /// use ratatui::style::Style;
269    /// use tui_term::widget::PseudoTerminal;
270    ///
271    /// let mut parser = vt100::Parser::new(24, 80, 0);
272    /// let style = Style::default();
273    /// let pseudo_term = PseudoTerminal::new(parser.screen()).style(style);
274    /// ```
275    #[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        // Make the backend on purpose much smaller
341        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"This line will be displayed in italic. 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"This line will be displayed with an underline. 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"This line will be displayed bold. 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"This line will be displayed inversed. 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"This line will be displayed in italic and underlined. 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}