Skip to main content

tui_term/
widget.rs

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