ratatui_core/terminal/buffers.rs
1use crate::backend::{Backend, ClearType};
2use crate::buffer::{Buffer, Cell};
3use crate::layout::{Position, Rect};
4use crate::terminal::{Frame, Terminal, Viewport};
5
6impl<B: Backend> Terminal<B> {
7 /// Returns a [`Frame`] for manual rendering.
8 ///
9 /// Most applications should render via [`Terminal::draw`] / [`Terminal::try_draw`]. This is an
10 /// escape hatch that exposes the frame construction step used by [`Terminal::try_draw`] so
11 /// tests and advanced callers can render without running the full draw pipeline.
12 ///
13 /// This is primarily useful for tests, backend adapters, and specialized integrations that
14 /// intentionally manage presentation themselves.
15 ///
16 /// Unlike `draw` / `try_draw`, this does not call [`Terminal::autoresize`], does not write
17 /// updates to the backend, and does not apply any cursor changes. After rendering, you
18 /// typically call [`Terminal::flush`], [`Terminal::swap_buffers`], and [`Backend::flush`].
19 ///
20 /// For the full render-pass behavior that also handles resizing, cursor updates, buffer
21 /// swapping, and backend flushing, see [`Terminal::draw`] and [`Terminal::try_draw`].
22 ///
23 /// The returned `Frame` mutably borrows the current buffer, so it must be dropped before you
24 /// can call methods like [`Terminal::flush`]. The example below uses a scope to make that
25 /// explicit.
26 ///
27 /// # Example
28 ///
29 /// ```rust,no_run
30 /// # mod ratatui {
31 /// # pub use ratatui_core::backend;
32 /// # pub use ratatui_core::terminal::Terminal;
33 /// # }
34 /// use ratatui::Terminal;
35 /// use ratatui::backend::{Backend, TestBackend};
36 ///
37 /// let backend = TestBackend::new(30, 5);
38 /// let mut terminal = Terminal::new(backend)?;
39 /// {
40 /// let mut frame = terminal.get_frame();
41 /// frame.render_widget("Hello", frame.area());
42 /// }
43 /// // When not using `draw`, present the buffer manually:
44 /// terminal.flush()?;
45 /// terminal.swap_buffers();
46 /// terminal.backend_mut().flush()?;
47 /// # Ok::<(), Box<dyn std::error::Error>>(())
48 /// ```
49 ///
50 /// [`Backend::flush`]: crate::backend::Backend::flush
51 pub const fn get_frame(&mut self) -> Frame<'_> {
52 let count = self.frame_count;
53 Frame {
54 cursor_position: None,
55 viewport_area: self.viewport_area,
56 buffer: self.current_buffer_mut(),
57 count,
58 }
59 }
60
61 /// Gets the current buffer as a mutable reference.
62 ///
63 /// This is the buffer that the next [`Frame`] will render into (see [`Terminal::get_frame`]).
64 /// This is a low-level escape hatch; normal applications should render inside
65 /// [`Terminal::draw`] and access the buffer through widgets, or through [`Frame::buffer_mut`]
66 /// when they intentionally need direct cell access during a render pass.
67 ///
68 /// Mutating this buffer does not update the backend immediately. The changes become visible
69 /// only after a later [`Terminal::flush`] or full draw pass applies the diff. Because this
70 /// bypasses the usual render callback structure, it is mainly useful for tests and specialized
71 /// integrations that intentionally manage presentation themselves.
72 pub const fn current_buffer_mut(&mut self) -> &mut Buffer {
73 &mut self.buffers[self.current]
74 }
75
76 /// Applies the current buffer diff to the backend's active display surface.
77 ///
78 /// This compares the current buffer with the previous buffer and passes only the changed cells
79 /// to [`Backend::draw`]. It is one of the building blocks used by [`Terminal::draw`] /
80 /// [`Terminal::try_draw`].
81 ///
82 /// This method does not swap buffers, does not update cursor visibility or position, and does
83 /// not call [`Backend::flush`]. See [`Terminal::swap_buffers`] and [`Backend::flush`].
84 ///
85 /// `Terminal::flush` only reasons about Ratatui's internal buffers. It does not know whether
86 /// the backend's display surface changed since the last render pass. For example, if you leave
87 /// the alternate screen and then call `Terminal::flush`, Ratatui may replay a diff that was
88 /// computed for the alternate screen onto the main screen. In normal applications, prefer
89 /// [`Terminal::draw`] / [`Terminal::try_draw`] unless you are intentionally managing the whole
90 /// render pipeline yourself.
91 ///
92 /// Implementation note: when there are updates, Ratatui records the position of the last
93 /// updated cell as the "last known cursor position". Inline viewports use this to preserve the
94 /// cursor's relative position within the viewport across resizes.
95 ///
96 /// [`Backend::flush`]: crate::backend::Backend::flush
97 pub fn flush(&mut self) -> Result<(), B::Error> {
98 let previous_buffer = &self.buffers[1 - self.current];
99 let current_buffer = &self.buffers[self.current];
100 let mut last_pos = None;
101
102 let updates = previous_buffer
103 .diff_iter(current_buffer)
104 .inspect(|(col, row, _)| {
105 last_pos = Some(Position { x: *col, y: *row });
106 });
107 self.backend.draw(updates)?;
108
109 if let Some(pos) = last_pos {
110 self.last_known_cursor_pos = pos;
111 }
112
113 Ok(())
114 }
115
116 /// Clears the inactive buffer and swaps it with the current buffer.
117 ///
118 /// This is part of the standard rendering flow (see [`Terminal::try_draw`]). If you render
119 /// manually using [`Terminal::get_frame`] and [`Terminal::flush`], call this immediately
120 /// afterward so the next flush can compute diffs against the correct "previous" buffer.
121 pub fn swap_buffers(&mut self) {
122 self.buffers[1 - self.current].reset();
123 self.current = 1 - self.current;
124 }
125
126 /// Clear the terminal and force a full redraw on the next draw call.
127 ///
128 /// What gets cleared depends on the active [`Viewport`]:
129 ///
130 /// - [`Viewport::Fullscreen`]: clears the entire terminal.
131 /// - [`Viewport::Fixed`]: clears only the viewport region.
132 /// - [`Viewport::Inline`]: clears after the viewport's origin, leaving any content above the
133 /// viewport untouched.
134 ///
135 /// Current behavior: for [`Viewport::Inline`], clearing runs from the viewport origin through
136 /// the end of the visible display area, not just the viewport's rectangle. This is an
137 /// implementation detail rather than a contract; do not rely on it.
138 ///
139 /// This preserves the backend's current cursor position.
140 ///
141 /// This also resets the "previous" buffer so the next [`Terminal::flush`] redraws the full
142 /// viewport.
143 ///
144 /// [`Terminal::resize`]: crate::terminal::Terminal::resize
145 ///
146 /// Implementation note: this uses [`ClearType::AfterCursor`] starting at the viewport origin.
147 pub fn clear(&mut self) -> Result<(), B::Error> {
148 let original_cursor = self.backend.get_cursor_position()?;
149 self.clear_viewport()?;
150 self.backend.set_cursor_position(original_cursor)?;
151 Ok(())
152 }
153
154 /// Clears according to the current viewport and resets the back buffer.
155 ///
156 /// Unlike [`Terminal::clear`], this does not snapshot and restore the backend cursor
157 /// position. Callers that need to preserve the cursor should do so outside this helper.
158 pub(super) fn clear_viewport(&mut self) -> Result<(), B::Error> {
159 match self.viewport {
160 Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
161 Viewport::Inline(_) => {
162 self.backend
163 .set_cursor_position(self.viewport_area.as_position())?;
164 self.backend.clear_region(ClearType::AfterCursor)?;
165 }
166 Viewport::Fixed(_) => {
167 let area = self.viewport_area;
168 self.clear_fixed_viewport(area)?;
169 }
170 }
171 // Reset the back buffer to make sure the next update will redraw everything.
172 self.buffers[1 - self.current].reset();
173 Ok(())
174 }
175
176 /// Clears a fixed viewport using terminal clear commands when possible.
177 ///
178 /// Terminal clear commands can be faster than per-cell updates.
179 fn clear_fixed_viewport(&mut self, area: Rect) -> Result<(), B::Error> {
180 if area.is_empty() {
181 return Ok(());
182 }
183 let size = self.backend.size()?;
184 let is_full_width = area.x == 0 && area.width == size.width;
185 let ends_at_bottom = area.bottom() == size.height;
186 if is_full_width && ends_at_bottom {
187 self.backend.set_cursor_position(area.as_position())?;
188 self.backend.clear_region(ClearType::AfterCursor)?;
189 } else if is_full_width {
190 self.clear_full_width_rows(area)?;
191 } else {
192 self.clear_region_cells(area)?;
193 }
194 Ok(())
195 }
196
197 /// Clears full-width rows using line clear commands.
198 ///
199 /// This avoids per-cell writes when the viewport spans the full width.
200 fn clear_full_width_rows(&mut self, area: Rect) -> Result<(), B::Error> {
201 for y in area.top()..area.bottom() {
202 self.backend.set_cursor_position(Position { x: 0, y })?;
203 self.backend.clear_region(ClearType::CurrentLine)?;
204 }
205 Ok(())
206 }
207
208 /// Clears a non-full-width region by writing empty cells directly.
209 ///
210 /// This is used when line-based clears would affect cells outside the viewport.
211 fn clear_region_cells(&mut self, area: Rect) -> Result<(), B::Error> {
212 let clear_cell = Cell::default();
213 let updates = area.positions().map(|pos| (pos.x, pos.y, &clear_cell));
214 self.backend.draw(updates)?;
215 Ok(())
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use crate::backend::{Backend, TestBackend};
222 use crate::buffer::{Buffer, Cell};
223 use crate::layout::{Position, Rect};
224 use crate::terminal::{Terminal, TerminalOptions, Viewport};
225
226 #[test]
227 fn get_frame_uses_current_viewport_and_frame_count() {
228 let backend = TestBackend::new(5, 3);
229 let mut terminal = Terminal::new(backend).unwrap();
230
231 let frame = terminal.get_frame();
232 assert_eq!(frame.count, 0);
233 assert_eq!(frame.area().width, 5);
234 assert_eq!(frame.area().height, 3);
235 assert_eq!(frame.buffer.area, frame.area());
236 }
237
238 #[test]
239 fn flush_writes_updates_and_tracks_last_updated_cell() {
240 let backend = TestBackend::new(3, 2);
241 let mut terminal = Terminal::new(backend).unwrap();
242
243 {
244 let frame = terminal.get_frame();
245 frame.buffer[(1, 0)].set_symbol("x");
246 }
247
248 terminal.flush().unwrap();
249 terminal.backend().assert_buffer_lines([" x ", " "]);
250 assert_eq!(terminal.last_known_cursor_pos, Position { x: 1, y: 0 });
251 }
252
253 #[test]
254 fn flush_with_no_updates_does_not_change_last_known_cursor_pos() {
255 let backend = TestBackend::new(3, 2);
256 let mut terminal = Terminal::new(backend).unwrap();
257 terminal.set_cursor_position((2, 1)).unwrap();
258
259 terminal.flush().unwrap();
260
261 assert_eq!(terminal.last_known_cursor_pos, Position { x: 2, y: 1 });
262 }
263
264 #[test]
265 fn swap_buffers_resets_new_current_buffer() {
266 let backend = TestBackend::new(3, 2);
267 let mut terminal = Terminal::new(backend).unwrap();
268
269 terminal.buffers[1][(0, 0)].set_symbol("x");
270 terminal.swap_buffers();
271
272 assert_eq!(terminal.current, 1);
273 assert_eq!(
274 terminal.buffers[terminal.current],
275 Buffer::empty(terminal.viewport_area)
276 );
277 }
278
279 #[test]
280 fn clear_fullscreen_clears_backend_and_resets_back_buffer() {
281 let backend = TestBackend::new(3, 2);
282 let mut terminal = Terminal::new(backend).unwrap();
283
284 {
285 let frame = terminal.get_frame();
286 frame.buffer[(0, 0)] = Cell::new("x");
287 }
288 terminal.flush().unwrap();
289 terminal.backend().assert_buffer_lines(["x ", " "]);
290
291 terminal.buffers[1][(2, 1)] = Cell::new("y");
292 terminal.clear().unwrap();
293
294 terminal.backend().assert_buffer_lines([" ", " "]);
295 assert_eq!(
296 terminal.buffers[1 - terminal.current],
297 Buffer::empty(terminal.viewport_area)
298 );
299 }
300
301 #[test]
302 fn clear_inline_clears_after_viewport_origin_and_resets_back_buffer() {
303 // Inline clear is implemented as:
304 // 1) move the backend cursor to the viewport origin
305 // 2) call ClearType::AfterCursor once
306 let mut backend = TestBackend::with_lines([
307 "before 1 ",
308 "before 2 ",
309 "viewport 1",
310 "viewport 2",
311 "after 1 ",
312 "after 2 ",
313 ]);
314 backend
315 .set_cursor_position(Position { x: 2, y: 2 })
316 .unwrap();
317 let options = TerminalOptions {
318 viewport: Viewport::Inline(2),
319 };
320 let mut terminal = Terminal::with_options(backend, options).unwrap();
321 terminal
322 .backend_mut()
323 .set_cursor_position(Position { x: 2, y: 2 })
324 .unwrap();
325
326 terminal.buffers[1][(2, 2)] = Cell::new("x");
327 terminal.clear().unwrap();
328
329 // Inline viewport is anchored to the cursor row (y = 2) with height 2. Clear runs from
330 // the viewport origin through the end of the display, including the rows after it.
331 terminal.backend().assert_buffer_lines([
332 "before 1 ",
333 "before 2 ",
334 " ",
335 " ",
336 " ",
337 " ",
338 ]);
339 assert_eq!(
340 terminal.buffers[1 - terminal.current],
341 Buffer::empty(terminal.viewport_area)
342 );
343 assert_eq!(
344 terminal.backend().cursor_position(),
345 Position { x: 2, y: 2 }
346 );
347 }
348
349 #[test]
350 fn clear_fixed_clears_viewport_rows_and_resets_back_buffer() {
351 // For full-width fixed viewports that reach the terminal bottom, clear uses
352 // ClearType::AfterCursor starting at the viewport origin.
353 let mut backend = TestBackend::with_lines(["before 1 ", "viewport 1", "viewport 2"]);
354 backend.set_cursor_position((2, 0)).unwrap();
355 let options = TerminalOptions {
356 viewport: Viewport::Fixed(Rect::new(0, 1, 10, 2)),
357 };
358 let mut terminal = Terminal::with_options(backend, options).unwrap();
359
360 terminal.clear().unwrap();
361
362 terminal
363 .backend()
364 .assert_buffer_lines(["before 1 ", " ", " "]);
365 assert_eq!(
366 terminal.buffers[1 - terminal.current],
367 Buffer::empty(terminal.viewport_area)
368 );
369 assert_eq!(
370 terminal.backend().cursor_position(),
371 Position { x: 2, y: 0 }
372 );
373 }
374
375 #[test]
376 fn clear_fixed_full_width_not_at_bottom() {
377 let mut backend =
378 TestBackend::with_lines(["before 1 ", "viewport 1", "viewport 2", "after 1 "]);
379 backend.set_cursor_position((1, 0)).unwrap();
380 let options = TerminalOptions {
381 viewport: Viewport::Fixed(Rect::new(0, 1, 10, 2)),
382 };
383 let mut terminal = Terminal::with_options(backend, options).unwrap();
384
385 terminal.clear().unwrap();
386
387 terminal.backend().assert_buffer_lines([
388 "before 1 ",
389 " ",
390 " ",
391 "after 1 ",
392 ]);
393 assert_eq!(
394 terminal.backend().cursor_position(),
395 Position { x: 1, y: 0 }
396 );
397 }
398
399 #[test]
400 fn clear_fixed_respects_non_full_width_viewport() {
401 let mut backend =
402 TestBackend::with_lines(["before 1 ", "viewport 1", "viewport 2", "after 1 "]);
403 backend.set_cursor_position((3, 0)).unwrap();
404 let options = TerminalOptions {
405 viewport: Viewport::Fixed(Rect::new(1, 1, 3, 2)),
406 };
407 let mut terminal = Terminal::with_options(backend, options).unwrap();
408
409 terminal.clear().unwrap();
410
411 terminal.backend().assert_buffer_lines([
412 "before 1 ",
413 "v port 1",
414 "v port 2",
415 "after 1 ",
416 ]);
417 assert_eq!(
418 terminal.backend().cursor_position(),
419 Position { x: 3, y: 0 }
420 );
421 }
422
423 #[test]
424 fn clear_viewport_inline_leaves_cursor_at_viewport_origin() {
425 let mut backend = TestBackend::with_lines([
426 "before 1 ",
427 "before 2 ",
428 "viewport 1",
429 "viewport 2",
430 "after 1 ",
431 "after 2 ",
432 ]);
433 backend
434 .set_cursor_position(Position { x: 2, y: 2 })
435 .unwrap();
436 let options = TerminalOptions {
437 viewport: Viewport::Inline(2),
438 };
439 let mut terminal = Terminal::with_options(backend, options).unwrap();
440
441 terminal.clear_viewport().unwrap();
442
443 assert_eq!(
444 terminal.backend().cursor_position(),
445 terminal.viewport_area.as_position()
446 );
447 }
448}