Skip to main content

ratatui_core/terminal/
init.rs

1use crate::backend::Backend;
2use crate::buffer::Buffer;
3use crate::layout::Position;
4use crate::terminal::inline::compute_inline_size;
5use crate::terminal::{Terminal, TerminalOptions, Viewport};
6
7impl<B: Backend> Terminal<B> {
8    /// Creates a new [`Terminal`] with the given [`Backend`] with a full screen viewport.
9    ///
10    /// This is a convenience for [`Terminal::with_options`] with [`Viewport::Fullscreen`].
11    /// Ratatui initializes two empty buffers sized to the backend's current screen area and treats
12    /// future backend size changes as redraw-triggering resizes during render passes.
13    ///
14    /// After creating a terminal, call [`Terminal::draw`] (or [`Terminal::try_draw`]) in a loop to
15    /// render your UI.
16    ///
17    /// Note that unlike [`ratatui::init`], this does not install a panic hook, so it is
18    /// recommended to do that manually when using this function, otherwise any panic messages will
19    /// be printed to the alternate screen and the terminal may be left in an unusable state.
20    ///
21    /// See [how to set up panic hooks](https://ratatui.rs/recipes/apps/panic-hooks/) and
22    /// [`better-panic` example](https://ratatui.rs/recipes/apps/better-panic/) for more
23    /// information.
24    ///
25    /// # Example
26    ///
27    /// ```rust,no_run
28    /// # #![allow(unexpected_cfgs)]
29    /// # #[cfg(feature = "crossterm")]
30    /// # {
31    /// use std::io::stdout;
32    ///
33    /// use ratatui::Terminal;
34    /// use ratatui::backend::CrosstermBackend;
35    ///
36    /// let backend = CrosstermBackend::new(stdout());
37    /// let _terminal = Terminal::new(backend)?;
38    ///
39    /// // Optionally set up a panic hook to restore the terminal on panic.
40    /// let old_hook = std::panic::take_hook();
41    /// std::panic::set_hook(Box::new(move |info| {
42    ///     ratatui::restore();
43    ///     old_hook(info);
44    /// }));
45    /// # }
46    /// # #[cfg(not(feature = "crossterm"))]
47    /// # {
48    /// # use ratatui_core::{backend::TestBackend, terminal::Terminal};
49    /// # let backend = TestBackend::new(10, 10);
50    /// # let _terminal = Terminal::new(backend)?;
51    /// # }
52    /// # Ok::<(), Box<dyn std::error::Error>>(())
53    /// ```
54    ///
55    /// [`ratatui::init`]: https://docs.rs/ratatui/latest/ratatui/fn.init.html
56    pub fn new(backend: B) -> Result<Self, B::Error> {
57        Self::with_options(
58            backend,
59            TerminalOptions {
60                viewport: Viewport::Fullscreen,
61            },
62        )
63    }
64
65    /// Creates a new [`Terminal`] with the given [`Backend`] and [`TerminalOptions`].
66    ///
67    /// The viewport determines what area is exposed to widgets via [`Frame::area`] and how Ratatui
68    /// keeps its internal buffers synchronized with the backend. See [`Viewport`] for an overview
69    /// of the available modes.
70    ///
71    /// For viewport behavior after initialization, see [`Terminal::resize`] and
72    /// [`Terminal::autoresize`].
73    ///
74    /// [`Frame::area`]: crate::terminal::Frame::area
75    /// [`Terminal::autoresize`]: crate::terminal::Terminal::autoresize
76    /// [`Terminal::resize`]: crate::terminal::Terminal::resize
77    ///
78    /// After creating a terminal, call [`Terminal::draw`] (or [`Terminal::try_draw`]) in a loop to
79    /// render your UI.
80    ///
81    /// Resize behavior depends on the selected viewport:
82    ///
83    /// - [`Viewport::Fullscreen`] and [`Viewport::Inline`] are automatically resized during
84    ///   [`Terminal::draw`] / [`Terminal::try_draw`] (via [`Terminal::autoresize`]).
85    /// - [`Viewport::Fixed`] is not automatically resized; call [`Terminal::resize`] if the region
86    ///   should change.
87    ///
88    /// # Example
89    ///
90    /// ```rust,no_run
91    /// # #![allow(unexpected_cfgs)]
92    /// # #[cfg(feature = "crossterm")]
93    /// # {
94    /// use std::io::stdout;
95    ///
96    /// use ratatui::backend::CrosstermBackend;
97    /// use ratatui::layout::Rect;
98    /// use ratatui::{Terminal, TerminalOptions, Viewport};
99    ///
100    /// let backend = CrosstermBackend::new(stdout());
101    /// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
102    /// let _terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
103    /// # }
104    /// # #[cfg(not(feature = "crossterm"))]
105    /// # {
106    /// # use ratatui_core::{
107    /// #     backend::TestBackend,
108    /// #     layout::Rect,
109    /// #     terminal::{Terminal, TerminalOptions, Viewport},
110    /// # };
111    /// # let backend = TestBackend::new(10, 10);
112    /// # let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
113    /// # let _terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
114    /// # }
115    /// # Ok::<(), Box<dyn std::error::Error>>(())
116    /// ```
117    ///
118    /// When the viewport is [`Viewport::Inline`], Ratatui anchors the viewport to the current
119    /// cursor row at initialization time (always starting at column 0). Ratatui may append lines
120    /// and thereby scroll the terminal to make enough room for the requested height so the
121    /// viewport stays fully visible.
122    pub fn with_options(mut backend: B, options: TerminalOptions) -> Result<Self, B::Error> {
123        let area = match options.viewport {
124            Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?.into(),
125            Viewport::Fixed(area) => area,
126        };
127        let (viewport_area, cursor_pos) = match options.viewport {
128            Viewport::Fullscreen => (area, Position::ORIGIN),
129            Viewport::Inline(height) => {
130                compute_inline_size(&mut backend, height, area.as_size(), 0)?
131            }
132            Viewport::Fixed(area) => (area, area.as_position()),
133        };
134        Ok(Self {
135            backend,
136            buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
137            current: 0,
138            hidden_cursor: false,
139            viewport: options.viewport,
140            viewport_area,
141            last_known_area: area,
142            last_known_cursor_pos: cursor_pos,
143            frame_count: 0,
144        })
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use crate::backend::{Backend, TestBackend};
151    use crate::layout::{Position, Rect};
152    use crate::terminal::{Terminal, TerminalOptions, Viewport};
153
154    #[test]
155    fn new_fullscreen_initializes_state() {
156        let backend = TestBackend::new(10, 5);
157        let terminal = Terminal::new(backend).unwrap();
158
159        assert_eq!(terminal.viewport, Viewport::Fullscreen);
160        assert_eq!(terminal.viewport_area, Rect::new(0, 0, 10, 5));
161        assert_eq!(terminal.last_known_area, Rect::new(0, 0, 10, 5));
162        assert_eq!(terminal.last_known_cursor_pos, Position::ORIGIN);
163        assert_eq!(terminal.current, 0);
164        assert!(!terminal.hidden_cursor);
165        assert_eq!(terminal.frame_count, 0);
166        assert_eq!(terminal.buffers[0].area, terminal.viewport_area);
167        assert_eq!(terminal.buffers[1].area, terminal.viewport_area);
168    }
169
170    #[test]
171    fn with_options_fixed_uses_fixed_area() {
172        let backend = TestBackend::new(10, 10);
173        let viewport = Viewport::Fixed(Rect::new(2, 3, 5, 4));
174        let terminal = Terminal::with_options(
175            backend,
176            TerminalOptions {
177                viewport: viewport.clone(),
178            },
179        )
180        .unwrap();
181
182        assert_eq!(terminal.viewport, viewport);
183        assert_eq!(terminal.viewport_area, Rect::new(2, 3, 5, 4));
184        assert_eq!(terminal.last_known_area, Rect::new(2, 3, 5, 4));
185        assert_eq!(terminal.last_known_cursor_pos, Position { x: 2, y: 3 });
186        assert_eq!(terminal.buffers[0].area, terminal.viewport_area);
187        assert_eq!(terminal.buffers[1].area, terminal.viewport_area);
188    }
189
190    #[test]
191    fn with_options_inline_anchors_to_cursor_when_space_available() {
192        let mut backend = TestBackend::new(10, 10);
193        backend
194            .set_cursor_position(Position { x: 0, y: 3 })
195            .unwrap();
196
197        let terminal = Terminal::with_options(
198            backend,
199            TerminalOptions {
200                viewport: Viewport::Inline(4),
201            },
202        )
203        .unwrap();
204
205        assert_eq!(terminal.viewport_area, Rect::new(0, 3, 10, 4));
206        assert_eq!(terminal.last_known_cursor_pos, Position { x: 0, y: 3 });
207    }
208
209    #[test]
210    fn with_options_inline_shifts_up_when_near_bottom() {
211        let mut backend = TestBackend::new(10, 10);
212        backend
213            .set_cursor_position(Position { x: 0, y: 8 })
214            .unwrap();
215
216        let terminal = Terminal::with_options(
217            backend,
218            TerminalOptions {
219                viewport: Viewport::Inline(4),
220            },
221        )
222        .unwrap();
223
224        assert_eq!(terminal.viewport_area, Rect::new(0, 6, 10, 4));
225        assert_eq!(terminal.last_known_cursor_pos, Position { x: 0, y: 8 });
226    }
227
228    #[test]
229    fn with_options_inline_clamps_height_to_terminal() {
230        let mut backend = TestBackend::new(10, 3);
231        backend
232            .set_cursor_position(Position { x: 0, y: 0 })
233            .unwrap();
234
235        let terminal = Terminal::with_options(
236            backend,
237            TerminalOptions {
238                viewport: Viewport::Inline(10),
239            },
240        )
241        .unwrap();
242
243        assert_eq!(terminal.viewport_area, Rect::new(0, 0, 10, 3));
244    }
245}