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}