ratatui_core/terminal.rs
1#![deny(missing_docs)]
2//! Provides the [`Terminal`], [`Frame`], [`CompletedFrame`], and [`Viewport`] types.
3//!
4//! This module contains Ratatui's rendering surface abstraction. [`Terminal`] ties together a
5//! backend, a viewport, and a double-buffered renderer. In a typical application you create a
6//! `Terminal`, render by calling [`Terminal::draw`] or [`Terminal::try_draw`] in a loop, and let
7//! Ratatui diffs successive frames so only changed cells are sent to the backend.
8//!
9//! [`Frame`] is the mutable view used during one render pass. Widgets write into the current
10//! buffer through it, and cursor state for the end of the pass is requested through
11//! [`Frame::set_cursor_position`]. After rendering completes, Ratatui applies the buffer diff,
12//! updates the cursor, swaps buffers, and flushes any buffered backend output.
13//!
14//! This module focuses on rendering contracts. Process-wide terminal setup such as raw mode,
15//! alternate screen handling, and panic restoration lives in the higher-level `ratatui` crate.
16//!
17//! # Example
18//!
19//! ```rust,no_run
20//! # #![allow(unexpected_cfgs)]
21//! # #[cfg(feature = "crossterm")]
22//! # {
23//! use std::io::stdout;
24//!
25//! use ratatui::Terminal;
26//! use ratatui::backend::CrosstermBackend;
27//! use ratatui::widgets::Paragraph;
28//!
29//! let backend = CrosstermBackend::new(stdout());
30//! let mut terminal = Terminal::new(backend)?;
31//! terminal.draw(|frame| {
32//! frame.render_widget(Paragraph::new("Hello world!"), frame.area());
33//! })?;
34//! # }
35//! # Ok::<(), Box<dyn std::error::Error>>(())
36//! ```
37//!
38//! [Crossterm]: https://crates.io/crates/crossterm
39//! [Termion]: https://crates.io/crates/termion
40//! [Termwiz]: https://crates.io/crates/termwiz
41//! [`backend`]: crate::backend
42//! [`Backend`]: crate::backend::Backend
43//! [`Buffer`]: crate::buffer::Buffer
44
45mod backend;
46mod buffers;
47mod cursor;
48mod frame;
49mod init;
50mod inline;
51mod render;
52mod resize;
53mod viewport;
54
55pub use frame::{CompletedFrame, Frame};
56pub use viewport::Viewport;
57
58use crate::backend::Backend;
59use crate::buffer::Buffer;
60use crate::layout::{Position, Rect};
61
62/// An interface to interact and draw [`Frame`]s on the user's terminal.
63///
64/// This is the main entry point for Ratatui's rendering subsystem. It owns the backend-facing
65/// render state: double buffers, viewport bookkeeping, and cursor synchronization for each render
66/// pass.
67///
68/// If you're building a fullscreen application with the `ratatui` crate's default backend
69/// ([Crossterm]), prefer [`ratatui::run`] (or [`ratatui::init`] + [`ratatui::restore`]) over
70/// constructing `Terminal` directly. These helpers enable common terminal modes (raw mode +
71/// alternate screen) and restore them on exit and on panic.
72///
73/// ```rust,no_run
74/// # #![allow(unexpected_cfgs)]
75/// # #[cfg(feature = "crossterm")]
76/// # {
77/// ratatui::run(|terminal| {
78/// let mut should_quit = false;
79/// while !should_quit {
80/// terminal.draw(|frame| {
81/// frame.render_widget("Hello, World!", frame.area());
82/// })?;
83///
84/// // Handle events, update application state, and set `should_quit = true` to exit.
85/// }
86/// Ok(())
87/// })?;
88/// # }
89/// # Ok::<(), Box<dyn std::error::Error>>(())
90/// ```
91///
92/// # Typical Usage
93///
94/// In a typical application, the flow is: set up a terminal, run an event loop, update state, and
95/// draw each frame.
96///
97/// 1. Choose a setup path for a `Terminal`. Most apps call [`ratatui::run`], which passes a
98/// preconfigured `Terminal` into your callback. If you need more control, use [`ratatui::init`]
99/// and [`ratatui::restore`], or construct a `Terminal` manually via [`Terminal::new`]
100/// (fullscreen) or [`Terminal::with_options`] (select a [`Viewport`]).
101/// 2. Enter your application's event loop and call [`Terminal::draw`] (or [`Terminal::try_draw`])
102/// to render the current UI state into a [`Frame`].
103/// 3. Handle input and application state updates between draw calls.
104/// 4. If the terminal is resized, call [`Terminal::draw`] again. Ratatui automatically resizes
105/// fullscreen and inline viewports during `draw`; fixed viewports require an explicit call to
106/// [`Terminal::resize`] if you want the region to change.
107///
108/// The normal mental model is: redraw the whole UI each pass, let Ratatui compute the diff, and
109/// treat `Frame::area` as the source of truth for where this pass can render. Most application
110/// code can stay entirely within that model.
111///
112/// # Rendering Pipeline
113///
114/// A single call to [`Terminal::draw`] (or [`Terminal::try_draw`]) represents one render pass. In
115/// broad strokes, Ratatui:
116///
117/// 1. Checks whether the underlying terminal size changed (see [`Terminal::autoresize`]).
118/// 2. Creates a [`Frame`] backed by the current buffer (see [`Terminal::get_frame`]).
119/// 3. Runs your render callback to populate that buffer.
120/// 4. Diffs the current buffer against the previous buffer and writes the changes (see
121/// [`Terminal::flush`]).
122/// 5. Applies cursor visibility and position requested by the frame (see
123/// [`Frame::set_cursor_position`]).
124/// 6. Swaps the buffers to prepare for the next render pass (see [`Terminal::swap_buffers`]).
125/// 7. Flushes the backend (see [`Backend::flush`]).
126///
127/// Each render pass starts with an empty buffer for the current viewport. Your render callback
128/// should render everything that should be visible in [`Frame::area`], even if it is unchanged
129/// from the previous frame. Ratatui diffs the current and previous buffers and only writes the
130/// changes; anything you don't render is treated as empty and may clear previously drawn content.
131///
132/// If the viewport size changes between render passes (for example via [`Terminal::autoresize`] or
133/// an explicit [`Terminal::resize`]), Ratatui clears the viewport and resets the previous buffer so
134/// the next `draw` is treated as a full redraw.
135///
136/// If [`Terminal::try_draw`] returns an error, the render pass ends early. Depending on where the
137/// failure happened, Ratatui may have already resized internal buffers, written part of the diff,
138/// or left cursor state unapplied. In most applications, treat that error as fatal for the current
139/// terminal session and let higher-level setup code restore terminal state before continuing.
140///
141/// Most applications should use [`Terminal::draw`] / [`Terminal::try_draw`]. Manual rendering is a
142/// separate, lower-level path intended primarily for tests and specialized integrations. In that
143/// mode you build a frame with [`Terminal::get_frame`], apply the current buffer diff with
144/// [`Terminal::flush`], then call [`Terminal::swap_buffers`]. If your backend buffers output, also
145/// call [`Backend::flush`].
146///
147/// [`Terminal::flush`] only knows about Ratatui's two screen buffers. It does not know whether
148/// you have changed terminal modes or switched display surfaces (for example by leaving the
149/// alternate screen). If you call it after such a change, Ratatui may replay a diff computed for
150/// the old surface onto the new one. When you need a complete draw pass that stays synchronized
151/// with cursor updates and backend flushing, prefer [`Terminal::draw`] / [`Terminal::try_draw`].
152///
153/// The same caution applies to direct backend mutation and direct cursor manipulation. If you
154/// write to the backend or move the cursor outside Ratatui's normal render pass, the next draw may
155/// overwrite those changes or may diff against stale assumptions. Use those escape hatches only
156/// when you intentionally manage resynchronization yourself, typically by calling
157/// [`Terminal::clear`] or performing a full render pass afterward.
158///
159/// ```rust,no_run
160/// # mod ratatui {
161/// # pub use ratatui_core::backend;
162/// # pub use ratatui_core::terminal::Terminal;
163/// # }
164/// use ratatui::Terminal;
165/// use ratatui::backend::{Backend, TestBackend};
166///
167/// let backend = TestBackend::new(10, 10);
168/// let mut terminal = Terminal::new(backend)?;
169///
170/// // Manual render pass (roughly what `Terminal::draw` does internally).
171/// {
172/// let mut frame = terminal.get_frame();
173/// frame.render_widget("Hello World!", frame.area());
174/// }
175///
176/// terminal.flush()?;
177/// terminal.swap_buffers();
178/// terminal.backend_mut().flush()?;
179/// # Ok::<(), Box<dyn std::error::Error>>(())
180/// ```
181///
182/// # Viewports
183///
184/// The viewport controls *where* Ratatui draws and therefore what [`Frame::area`] represents.
185/// Most applications use [`Viewport::Fullscreen`], but Ratatui also supports [`Viewport::Inline`]
186/// and [`Viewport::Fixed`].
187///
188/// Choose a viewport based on how the app should fit into the terminal:
189///
190/// - [`Viewport::Fullscreen`]: the standard TUI case where Ratatui owns the whole terminal window.
191/// - [`Viewport::Inline`]: embed the UI into a larger CLI flow with normal terminal output above
192/// it.
193/// - [`Viewport::Fixed`]: render into one region of a larger terminal layout managed elsewhere.
194///
195/// Choose a viewport at initialization time with [`Terminal::with_options`] and
196/// [`TerminalOptions`].
197///
198/// `Frame::area` depends on the active viewport. In fullscreen mode it starts at (0, 0); in fixed
199/// and inline mode it may have a non-zero origin, so prefer using `frame.area()` as your root
200/// layout rectangle. The variant docs on [`Viewport`] describe each mode in more detail, and
201/// inline-specific behavior is covered in the "Inline Viewport" section below.
202///
203/// ```rust,no_run
204/// # #![allow(unexpected_cfgs)]
205/// # #[cfg(feature = "crossterm")]
206/// # {
207/// use ratatui::backend::CrosstermBackend;
208/// use ratatui::layout::{Constraint, Layout, Rect};
209/// use ratatui::{Terminal, TerminalOptions, Viewport};
210///
211/// // Fullscreen (most common):
212/// let fullscreen = Terminal::new(CrosstermBackend::new(std::io::stdout()))?;
213///
214/// // Fixed region (your app manages the coordinates):
215/// let viewport = Viewport::Fixed(Rect::new(0, 0, 30, 10));
216/// let fixed = Terminal::with_options(
217/// CrosstermBackend::new(std::io::stdout()),
218/// TerminalOptions { viewport },
219/// )?;
220///
221/// fixed.draw(|frame| {
222/// // Split the fixed viewport itself instead of assuming the viewport starts at `(0, 0)`.
223/// let [header, body] =
224/// Layout::vertical([Constraint::Length(1), Constraint::Min(0)]).areas(frame.area());
225///
226/// frame.render_widget("Fixed panel header", header);
227/// frame.render_widget("Render the panel body relative to frame.area()", body);
228/// })?;
229/// # }
230/// # Ok::<(), Box<dyn std::error::Error>>(())
231/// ```
232///
233/// Applications should redraw after terminal resizes with [`Terminal::draw`] /
234/// [`Terminal::try_draw`]. Fullscreen and inline viewports resize automatically during those render
235/// passes; fixed viewports do not.
236///
237/// If your event loop receives a resize event, treat that event as a signal to render again rather
238/// than as a complete source of truth for layout. During a render pass, use [`Frame::area`] as the
239/// rectangle that Ratatui has actually prepared for drawing. Ratatui checks the backend's current
240/// size during `draw` / `try_draw` so layout reflects the terminal size that exists at render
241/// time, even if resize events were coalesced, missed, or arrived before your app handled them.
242///
243/// # Inline Viewport
244///
245/// Inline mode is designed for applications that want to embed a UI into a larger CLI flow. In
246/// [`Viewport::Inline`], Ratatui anchors the viewport to the backend cursor row and always starts
247/// drawing at column 0.
248///
249/// To reserve vertical space for the requested height, Ratatui may append lines. When the cursor is
250/// near the bottom edge, terminals scroll; Ratatui accounts for that scrolling by shifting the
251/// computed viewport origin upward so the viewport stays fully visible.
252///
253/// While running in inline mode, [`Terminal::insert_before`] can be used to print output above the
254/// viewport without disturbing the UI's logical position. When Ratatui is built with the
255/// `scrolling-regions` feature, `insert_before` can do this without clearing and redrawing the
256/// viewport.
257///
258/// ```rust,no_run
259/// # #![allow(unexpected_cfgs)]
260/// # #[cfg(feature = "crossterm")]
261/// # {
262/// use ratatui::{TerminalOptions, Viewport};
263///
264/// println!("Some output above the UI");
265///
266/// let options = TerminalOptions {
267/// viewport: Viewport::Inline(10),
268/// };
269/// let mut terminal = ratatui::try_init_with_options(options)?;
270///
271/// terminal.insert_before(1, |buf| {
272/// // Render a single line of output into `buf` before the UI.
273/// // (For example: logs, status updates, or command output.)
274/// })?;
275///
276/// terminal.draw(|frame| {
277/// // Continue rendering the inline UI relative to the inline viewport.
278/// frame.render_widget("inline ui", frame.area());
279/// })?;
280/// # }
281/// # Ok::<(), Box<dyn std::error::Error>>(())
282/// ```
283///
284/// # More Information
285///
286/// - Choosing a viewport: [`Terminal::with_options`], [`TerminalOptions`], and [`Viewport`]
287/// - The rendering pipeline: [`Terminal::draw`] and [`Terminal::try_draw`]
288/// - Resize handling: [`Terminal::autoresize`] and [`Terminal::resize`]
289/// - Cursor behavior: [`Frame::set_cursor_position`], [`Terminal::set_cursor_position`], and
290/// [`Terminal::show_cursor`]
291/// - Manual rendering and testing: [`Terminal::get_frame`], [`Terminal::flush`], and
292/// [`Terminal::swap_buffers`]
293/// - Printing above an inline UI: [`Terminal::insert_before`]
294///
295/// # Initialization
296///
297/// Most interactive TUIs need process-wide terminal setup (for example: raw mode and an alternate
298/// screen) and matching teardown on exit and on panic. In Ratatui, that setup lives in the
299/// `ratatui` crate; `Terminal` itself focuses on rendering and does not implicitly change those
300/// modes.
301///
302/// If you're using the `ratatui` crate with its default backend ([Crossterm]), there are three
303/// common entry points:
304///
305/// - [`ratatui::run`]: recommended for most applications. Provides a [`ratatui::DefaultTerminal`],
306/// runs your closure, and restores terminal state on exit and on panic.
307/// - [`ratatui::init`] + [`ratatui::restore`]: like `run`, but you control the event loop and
308/// decide when to restore.
309/// - [`Terminal::new`] / [`Terminal::with_options`]: manual construction (for example: custom
310/// backends such as [Termion] / [Termwiz], inline UIs, or fixed viewports). You are responsible
311/// for terminal mode setup and teardown.
312///
313/// [`ratatui::run`] was introduced in Ratatui 0.30, so older tutorials may use `init`/`restore` or
314/// manual construction.
315///
316/// Some applications install a custom panic hook to log a crash report, print a friendlier error,
317/// or integrate with error reporting. If you do, install it before calling [`ratatui::init`] /
318/// [`ratatui::run`]. Ratatui wraps the current hook so it can restore terminal state first (for
319/// example: leaving the alternate screen and disabling raw mode) and then delegate to your hook.
320///
321/// Crossterm is cross-platform and is what most Ratatui applications use by default. Ratatui also
322/// supports other backends such as [Termion] and [Termwiz], and third-party backends can integrate
323/// by implementing [`Backend`].
324///
325/// # How it works
326///
327/// `Terminal` ties together a [`Backend`], a [`Viewport`], and a double-buffered diffing renderer.
328/// The high-level flow is described in the "Rendering Pipeline" section above; this section focuses
329/// on how that pipeline is implemented.
330///
331/// `Terminal` is generic over a [`Backend`] implementation and does not depend on a particular
332/// terminal library. It relies on the backend to:
333///
334/// - report the current screen size (used by [`Terminal::autoresize`])
335/// - draw cell updates (used by [`Terminal::flush`])
336/// - clear regions (used by [`Terminal::clear`] and [`Terminal::resize`])
337/// - move and show/hide the cursor (used by [`Terminal::try_draw`])
338/// - optionally append lines (used by inline viewports and by [`Terminal::insert_before`])
339///
340/// ## Buffers and diffing
341///
342/// The `Terminal` maintains two [`Buffer`]s sized to the current viewport. During a render pass,
343/// widgets draw into the "current" buffer via the [`Frame`] passed to your callback. At the end of
344/// the pass, [`Terminal::flush`] diffs the current buffer against the previous buffer and sends
345/// only the changed cells to the backend.
346///
347/// After flushing, [`Terminal::swap_buffers`] flips which buffer is considered "current" and resets
348/// the next buffer. This is why each render pass starts from an empty buffer: your callback is
349/// expected to fully redraw the viewport every time.
350///
351/// The [`CompletedFrame`] returned from [`Terminal::draw`] / [`Terminal::try_draw`] provides a
352/// reference to the buffer that was just rendered, which can be useful for assertions in tests.
353///
354/// ## Viewport state and resizing
355///
356/// The active [`Viewport`] controls how the viewport area is computed:
357///
358/// - Fullscreen: `Frame::area` covers the full backend size.
359/// - Fixed: `Frame::area` is the exact rectangle you provided in terminal coordinates.
360/// - Inline: `Frame::area` is a rectangle anchored to the backend cursor row.
361///
362/// For fullscreen and inline viewports, [`Terminal::autoresize`] checks the backend size during
363/// every render pass and calls [`Terminal::resize`] when it changes. Resizing updates the internal
364/// buffer sizes and clears the affected region; it also resets the previous buffer so the next draw
365/// is treated as a full redraw.
366///
367/// ## Cursor tracking
368///
369/// The cursor position requested by [`Frame::set_cursor_position`] is applied after
370/// [`Terminal::flush`] so the cursor ends up on top of the rendered UI. `Terminal` also tracks a
371/// "last known cursor position" as a best-effort record of where it last wrote, and uses that
372/// information when recomputing inline viewports on resize.
373///
374/// ## Inline-specific behavior
375///
376/// Inline viewports reserve vertical space by calling [`Backend::append_lines`]. If the cursor is
377/// close enough to the bottom edge, terminals scroll as lines are appended. Ratatui accounts for
378/// that scrolling by shifting the computed viewport origin upward so the viewport remains fully
379/// visible. On resize, Ratatui recomputes the inline origin while trying to keep the cursor at the
380/// same relative row inside the viewport.
381///
382/// When Ratatui is built with the `scrolling-regions` feature, [`Terminal::insert_before`] uses
383/// terminal scrolling regions to insert content above an inline viewport without clearing and
384/// redrawing it.
385///
386/// [Crossterm]: https://crates.io/crates/crossterm
387/// [Termion]: https://crates.io/crates/termion
388/// [Termwiz]: https://crates.io/crates/termwiz
389/// [`backend`]: crate::backend
390/// [`Backend`]: crate::backend::Backend
391/// [`Backend::flush`]: crate::backend::Backend::flush
392/// [`Buffer`]: crate::buffer::Buffer
393/// [`ratatui::DefaultTerminal`]: https://docs.rs/ratatui/latest/ratatui/type.DefaultTerminal.html
394/// [`ratatui::init`]: https://docs.rs/ratatui/latest/ratatui/fn.init.html
395/// [`ratatui::restore`]: https://docs.rs/ratatui/latest/ratatui/fn.restore.html
396/// [`ratatui::run`]: https://docs.rs/ratatui/latest/ratatui/fn.run.html
397#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
398pub struct Terminal<B>
399where
400 B: Backend,
401{
402 /// The backend used to write updates to the terminal.
403 ///
404 /// Most application code does not need to interact with the backend directly; see
405 /// [`Terminal::draw`]. Accessing the backend can be useful for backend-specific testing and
406 /// inspection (see [`Terminal::backend`]).
407 backend: B,
408 /// Double-buffered render state for the current viewport.
409 ///
410 /// [`Terminal::flush`] diffs `buffers[current]` against the other buffer to compute the next
411 /// batch of cell updates to send to the backend.
412 buffers: [Buffer; 2],
413 /// Index of the "current" buffer in [`Terminal::buffers`].
414 ///
415 /// This toggles between 0 and 1 and is updated by [`Terminal::swap_buffers`].
416 current: usize,
417 /// Whether Ratatui believes it has hidden the cursor.
418 ///
419 /// This is tracked so [`Drop`] can attempt to restore cursor visibility.
420 hidden_cursor: bool,
421 /// The configured [`Viewport`] mode.
422 ///
423 /// This determines how the initial viewport area is computed during construction, whether
424 /// [`Terminal::autoresize`] runs, how [`Terminal::clear`] behaves, and whether operations like
425 /// [`Terminal::insert_before`] have any effect.
426 viewport: Viewport,
427 /// The current viewport rectangle in terminal coordinates.
428 ///
429 /// This is the area returned by [`Frame::area`] and the size of the internal buffers. It is
430 /// set during construction and updated by [`Terminal::resize`]. In inline mode, calls to
431 /// [`Terminal::insert_before`] can also move the viewport vertically.
432 viewport_area: Rect,
433 /// Last known renderable "screen" area.
434 ///
435 /// For fullscreen and inline viewports this tracks the backend-reported terminal size. For
436 /// fixed viewports, this tracks the user-provided fixed area.
437 ///
438 /// This is used by [`Terminal::autoresize`] to detect size changes and is reported via
439 /// [`CompletedFrame::area`].
440 last_known_area: Rect,
441 /// Last known cursor position in terminal coordinates.
442 ///
443 /// This is updated when:
444 ///
445 /// - [`Terminal::set_cursor_position`] is called directly.
446 /// - [`Frame::set_cursor_position`] is used during [`Terminal::draw`] /
447 /// [`Terminal::try_draw`].
448 /// - [`Terminal::flush`] observes a diff update (used as a proxy for the "last written" cell).
449 ///
450 /// Inline viewports use this during [`Terminal::resize`] to preserve the cursor's relative
451 /// position within the viewport.
452 last_known_cursor_pos: Position,
453 /// Number of frames rendered so far.
454 ///
455 /// This increments after each successful [`Terminal::draw`] / [`Terminal::try_draw`] and wraps
456 /// at `usize::MAX`.
457 frame_count: usize,
458}
459
460/// Options to pass to [`Terminal::with_options`]
461///
462/// Most applications can use [`Terminal::new`]. Use `TerminalOptions` when you need to configure a
463/// non-default [`Viewport`] at initialization time (see [`Terminal`] for an overview).
464#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
465pub struct TerminalOptions {
466 /// Viewport used to draw to the terminal.
467 ///
468 /// See [`Terminal`] for a higher-level overview, and [`Viewport`] for the per-variant
469 /// definition.
470 pub viewport: Viewport,
471}
472
473impl<B> Drop for Terminal<B>
474where
475 B: Backend,
476{
477 fn drop(&mut self) {
478 // Attempt to restore the cursor state
479 if self.hidden_cursor {
480 #[allow(unused_variables)]
481 if let Err(err) = self.show_cursor() {
482 #[cfg(feature = "std")]
483 std::eprintln!("Failed to show the cursor: {err}");
484 }
485 }
486 }
487}