Skip to main content

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}