agcodex_tui/
custom_terminal.rs

1// This is derived from `ratatui::Terminal`, which is licensed under the following terms:
2//
3// The MIT License (MIT)
4// Copyright (c) 2016-2022 Florian Dehau
5// Copyright (c) 2023-2025 The Ratatui Developers
6//
7// Permission is hereby granted, free of charge, to any person obtaining a copy
8// of this software and associated documentation files (the "Software"), to deal
9// in the Software without restriction, including without limitation the rights
10// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11// copies of the Software, and to permit persons to whom the Software is
12// furnished to do so, subject to the following conditions:
13//
14// The above copyright notice and this permission notice shall be included in all
15// copies or substantial portions of the Software.
16//
17// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23// SOFTWARE.
24use std::io;
25
26use ratatui::backend::Backend;
27use ratatui::backend::ClearType;
28use ratatui::buffer::Buffer;
29use ratatui::layout::Position;
30use ratatui::layout::Rect;
31use ratatui::layout::Size;
32use ratatui::widgets::StatefulWidget;
33use ratatui::widgets::StatefulWidgetRef;
34use ratatui::widgets::Widget;
35use ratatui::widgets::WidgetRef;
36
37#[derive(Debug, Hash)]
38pub struct Frame<'a> {
39    /// Where should the cursor be after drawing this frame?
40    ///
41    /// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x,
42    /// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`.
43    pub(crate) cursor_position: Option<Position>,
44
45    /// The area of the viewport
46    pub(crate) viewport_area: Rect,
47
48    /// The buffer that is used to draw the current frame
49    pub(crate) buffer: &'a mut Buffer,
50
51    /// The frame count indicating the sequence number of this frame.
52    pub(crate) count: usize,
53}
54
55#[allow(dead_code)]
56impl Frame<'_> {
57    /// The area of the current frame
58    ///
59    /// This is guaranteed not to change during rendering, so may be called multiple times.
60    ///
61    /// If your app listens for a resize event from the backend, it should ignore the values from
62    /// the event for any calculations that are used to render the current frame and use this value
63    /// instead as this is the area of the buffer that is used to render the current frame.
64    pub const fn area(&self) -> Rect {
65        self.viewport_area
66    }
67
68    /// Render a [`Widget`] to the current buffer using [`Widget::render`].
69    ///
70    /// Usually the area argument is the size of the current frame or a sub-area of the current
71    /// frame (which can be obtained using [`Layout`] to split the total area).
72    ///
73    /// # Example
74    ///
75    /// ```rust
76    /// # use ratatui::{backend::TestBackend, Terminal};
77    /// # let backend = TestBackend::new(5, 5);
78    /// # let mut terminal = Terminal::new(backend).unwrap();
79    /// # let mut frame = terminal.get_frame();
80    /// use ratatui::{layout::Rect, widgets::Block};
81    ///
82    /// let block = Block::new();
83    /// let area = Rect::new(0, 0, 5, 5);
84    /// frame.render_widget(block, area);
85    /// ```
86    ///
87    /// [`Layout`]: crate::layout::Layout
88    pub fn render_widget<W: Widget>(&mut self, widget: W, area: Rect) {
89        widget.render(area, self.buffer);
90    }
91
92    /// Render a [`WidgetRef`] to the current buffer using [`WidgetRef::render_ref`].
93    ///
94    /// Usually the area argument is the size of the current frame or a sub-area of the current
95    /// frame (which can be obtained using [`Layout`] to split the total area).
96    ///
97    /// # Example
98    ///
99    /// ```rust
100    /// # #[cfg(feature = "unstable-widget-ref")] {
101    /// # use ratatui::{backend::TestBackend, Terminal};
102    /// # let backend = TestBackend::new(5, 5);
103    /// # let mut terminal = Terminal::new(backend).unwrap();
104    /// # let mut frame = terminal.get_frame();
105    /// use ratatui::{layout::Rect, widgets::Block};
106    ///
107    /// let block = Block::new();
108    /// let area = Rect::new(0, 0, 5, 5);
109    /// frame.render_widget_ref(block, area);
110    /// # }
111    /// ```
112    #[allow(clippy::needless_pass_by_value)]
113    pub fn render_widget_ref<W: WidgetRef>(&mut self, widget: W, area: Rect) {
114        widget.render_ref(area, self.buffer);
115    }
116
117    /// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
118    ///
119    /// Usually the area argument is the size of the current frame or a sub-area of the current
120    /// frame (which can be obtained using [`Layout`] to split the total area).
121    ///
122    /// The last argument should be an instance of the [`StatefulWidget::State`] associated to the
123    /// given [`StatefulWidget`].
124    ///
125    /// # Example
126    ///
127    /// ```rust
128    /// # use ratatui::{backend::TestBackend, Terminal};
129    /// # let backend = TestBackend::new(5, 5);
130    /// # let mut terminal = Terminal::new(backend).unwrap();
131    /// # let mut frame = terminal.get_frame();
132    /// use ratatui::{
133    ///     layout::Rect,
134    ///     widgets::{List, ListItem, ListState},
135    /// };
136    ///
137    /// let mut state = ListState::default().with_selected(Some(1));
138    /// let list = List::new(vec![ListItem::new("Item 1"), ListItem::new("Item 2")]);
139    /// let area = Rect::new(0, 0, 5, 5);
140    /// frame.render_stateful_widget(list, area, &mut state);
141    /// ```
142    ///
143    /// [`Layout`]: crate::layout::Layout
144    pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
145    where
146        W: StatefulWidget,
147    {
148        widget.render(area, self.buffer, state);
149    }
150
151    /// Render a [`StatefulWidgetRef`] to the current buffer using
152    /// [`StatefulWidgetRef::render_ref`].
153    ///
154    /// Usually the area argument is the size of the current frame or a sub-area of the current
155    /// frame (which can be obtained using [`Layout`] to split the total area).
156    ///
157    /// The last argument should be an instance of the [`StatefulWidgetRef::State`] associated to
158    /// the given [`StatefulWidgetRef`].
159    ///
160    /// # Example
161    ///
162    /// ```rust
163    /// # #[cfg(feature = "unstable-widget-ref")] {
164    /// # use ratatui::{backend::TestBackend, Terminal};
165    /// # let backend = TestBackend::new(5, 5);
166    /// # let mut terminal = Terminal::new(backend).unwrap();
167    /// # let mut frame = terminal.get_frame();
168    /// use ratatui::{
169    ///     layout::Rect,
170    ///     widgets::{List, ListItem, ListState},
171    /// };
172    ///
173    /// let mut state = ListState::default().with_selected(Some(1));
174    /// let list = List::new(vec![ListItem::new("Item 1"), ListItem::new("Item 2")]);
175    /// let area = Rect::new(0, 0, 5, 5);
176    /// frame.render_stateful_widget_ref(list, area, &mut state);
177    /// # }
178    /// ```
179    #[allow(clippy::needless_pass_by_value)]
180    pub fn render_stateful_widget_ref<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
181    where
182        W: StatefulWidgetRef,
183    {
184        widget.render_ref(area, self.buffer, state);
185    }
186
187    /// After drawing this frame, make the cursor visible and put it at the specified (x, y)
188    /// coordinates. If this method is not called, the cursor will be hidden.
189    ///
190    /// Note that this will interfere with calls to [`Terminal::hide_cursor`],
191    /// [`Terminal::show_cursor`], and [`Terminal::set_cursor_position`]. Pick one of the APIs and
192    /// stick with it.
193    ///
194    /// [`Terminal::hide_cursor`]: crate::Terminal::hide_cursor
195    /// [`Terminal::show_cursor`]: crate::Terminal::show_cursor
196    /// [`Terminal::set_cursor_position`]: crate::Terminal::set_cursor_position
197    pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) {
198        self.cursor_position = Some(position.into());
199    }
200
201    /// Gets the buffer that this `Frame` draws into as a mutable reference.
202    pub const fn buffer_mut(&mut self) -> &mut Buffer {
203        self.buffer
204    }
205
206    /// Returns the current frame count.
207    ///
208    /// This method provides access to the frame count, which is a sequence number indicating
209    /// how many frames have been rendered up to (but not including) this one. It can be used
210    /// for purposes such as animation, performance tracking, or debugging.
211    ///
212    /// Each time a frame has been rendered, this count is incremented,
213    /// providing a consistent way to reference the order and number of frames processed by the
214    /// terminal. When count reaches its maximum value (`usize::MAX`), it wraps around to zero.
215    ///
216    /// This count is particularly useful when dealing with dynamic content or animations where the
217    /// state of the display changes over time. By tracking the frame count, developers can
218    /// synchronize updates or changes to the content with the rendering process.
219    ///
220    /// # Examples
221    ///
222    /// ```rust
223    /// # use ratatui::{backend::TestBackend, Terminal};
224    /// # let backend = TestBackend::new(5, 5);
225    /// # let mut terminal = Terminal::new(backend).unwrap();
226    /// # let mut frame = terminal.get_frame();
227    /// let current_count = frame.count();
228    /// println!("Current frame count: {}", current_count);
229    /// ```
230    pub const fn count(&self) -> usize {
231        self.count
232    }
233}
234
235#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
236pub struct Terminal<B>
237where
238    B: Backend,
239{
240    /// The backend used to interface with the terminal
241    backend: B,
242    /// Holds the results of the current and previous draw calls. The two are compared at the end
243    /// of each draw pass to output the necessary updates to the terminal
244    buffers: [Buffer; 2],
245    /// Index of the current buffer in the previous array
246    current: usize,
247    /// Whether the cursor is currently hidden
248    hidden_cursor: bool,
249    /// Area of the viewport
250    pub viewport_area: Rect,
251    /// Last known size of the terminal. Used to detect if the internal buffers have to be resized.
252    pub last_known_screen_size: Size,
253    /// Last known position of the cursor. Used to find the new area when the viewport is inlined
254    /// and the terminal resized.
255    pub last_known_cursor_pos: Position,
256    /// Number of frames rendered up until current time.
257    frame_count: usize,
258}
259
260impl<B> Drop for Terminal<B>
261where
262    B: Backend,
263{
264    #[allow(clippy::print_stderr)]
265    fn drop(&mut self) {
266        // Attempt to restore the cursor state
267        if self.hidden_cursor
268            && let Err(err) = self.show_cursor()
269        {
270            eprintln!("Failed to show the cursor: {err}");
271        }
272    }
273}
274
275impl<B> Terminal<B>
276where
277    B: Backend,
278{
279    /// Creates a new [`Terminal`] with the given [`Backend`] and [`TerminalOptions`].
280    ///
281    /// # Example
282    ///
283    /// ```rust
284    /// use std::io::stdout;
285    ///
286    /// use ratatui::{backend::CrosstermBackend, layout::Rect, Terminal, TerminalOptions, Viewport};
287    ///
288    /// let backend = CrosstermBackend::new(stdout());
289    /// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
290    /// let terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
291    /// # std::io::Result::Ok(())
292    /// ```
293    pub fn with_options(mut backend: B) -> io::Result<Self> {
294        let screen_size = backend.size()?;
295        let cursor_pos = backend.get_cursor_position()?;
296        Ok(Self {
297            backend,
298            buffers: [
299                Buffer::empty(Rect::new(0, 0, 0, 0)),
300                Buffer::empty(Rect::new(0, 0, 0, 0)),
301            ],
302            current: 0,
303            hidden_cursor: false,
304            viewport_area: Rect::new(0, cursor_pos.y, 0, 0),
305            last_known_screen_size: screen_size,
306            last_known_cursor_pos: cursor_pos,
307            frame_count: 0,
308        })
309    }
310
311    /// Get a Frame object which provides a consistent view into the terminal state for rendering.
312    pub const fn get_frame(&mut self) -> Frame<'_> {
313        let count = self.frame_count;
314        Frame {
315            cursor_position: None,
316            viewport_area: self.viewport_area,
317            buffer: self.current_buffer_mut(),
318            count,
319        }
320    }
321
322    /// Gets the current buffer as a mutable reference.
323    pub const fn current_buffer_mut(&mut self) -> &mut Buffer {
324        &mut self.buffers[self.current]
325    }
326
327    /// Gets the backend
328    pub const fn backend(&self) -> &B {
329        &self.backend
330    }
331
332    /// Gets the backend as a mutable reference
333    pub const fn backend_mut(&mut self) -> &mut B {
334        &mut self.backend
335    }
336
337    /// Obtains a difference between the previous and the current buffer and passes it to the
338    /// current backend for drawing.
339    pub fn flush(&mut self) -> io::Result<()> {
340        let previous_buffer = &self.buffers[1 - self.current];
341        let current_buffer = &self.buffers[self.current];
342        let updates = previous_buffer.diff(current_buffer);
343        if let Some((col, row, _)) = updates.last() {
344            self.last_known_cursor_pos = Position { x: *col, y: *row };
345        }
346        self.backend.draw(updates.into_iter())
347    }
348
349    /// Updates the Terminal so that internal buffers match the requested area.
350    ///
351    /// Requested area will be saved to remain consistent when rendering. This leads to a full clear
352    /// of the screen.
353    pub const fn resize(&mut self, screen_size: Size) -> io::Result<()> {
354        self.last_known_screen_size = screen_size;
355        Ok(())
356    }
357
358    /// Sets the viewport area.
359    pub fn set_viewport_area(&mut self, area: Rect) {
360        self.buffers[self.current].resize(area);
361        self.buffers[1 - self.current].resize(area);
362        self.viewport_area = area;
363    }
364
365    /// Queries the backend for size and resizes if it doesn't match the previous size.
366    pub fn autoresize(&mut self) -> io::Result<()> {
367        let screen_size = self.size()?;
368        if screen_size != self.last_known_screen_size {
369            self.resize(screen_size)?;
370        }
371        Ok(())
372    }
373
374    /// Draws a single frame to the terminal.
375    ///
376    /// Returns a [`CompletedFrame`] if successful, otherwise a [`std::io::Error`].
377    ///
378    /// If the render callback passed to this method can fail, use [`try_draw`] instead.
379    ///
380    /// Applications should call `draw` or [`try_draw`] in a loop to continuously render the
381    /// terminal. These methods are the main entry points for drawing to the terminal.
382    ///
383    /// [`try_draw`]: Terminal::try_draw
384    ///
385    /// This method will:
386    ///
387    /// - autoresize the terminal if necessary
388    /// - call the render callback, passing it a [`Frame`] reference to render to
389    /// - flush the current internal state by copying the current buffer to the backend
390    /// - move the cursor to the last known position if it was set during the rendering closure
391    ///
392    /// The render callback should fully render the entire frame when called, including areas that
393    /// are unchanged from the previous frame. This is because each frame is compared to the
394    /// previous frame to determine what has changed, and only the changes are written to the
395    /// terminal. If the render callback does not fully render the frame, the terminal will not be
396    /// in a consistent state.
397    ///
398    /// # Examples
399    ///
400    /// ```
401    /// # let backend = ratatui::backend::TestBackend::new(10, 10);
402    /// # let mut terminal = ratatui::Terminal::new(backend)?;
403    /// use ratatui::{layout::Position, widgets::Paragraph};
404    ///
405    /// // with a closure
406    /// terminal.draw(|frame| {
407    ///     let area = frame.area();
408    ///     frame.render_widget(Paragraph::new("Hello World!"), area);
409    ///     frame.set_cursor_position(Position { x: 0, y: 0 });
410    /// })?;
411    ///
412    /// // or with a function
413    /// terminal.draw(render)?;
414    ///
415    /// fn render(frame: &mut ratatui::Frame) {
416    ///     frame.render_widget(Paragraph::new("Hello World!"), frame.area());
417    /// }
418    /// # std::io::Result::Ok(())
419    /// ```
420    pub fn draw<F>(&mut self, render_callback: F) -> io::Result<()>
421    where
422        F: FnOnce(&mut Frame),
423    {
424        self.try_draw(|frame| {
425            render_callback(frame);
426            io::Result::Ok(())
427        })
428    }
429
430    /// Tries to draw a single frame to the terminal.
431    ///
432    /// Returns [`Result::Ok`] containing a [`CompletedFrame`] if successful, otherwise
433    /// [`Result::Err`] containing the [`std::io::Error`] that caused the failure.
434    ///
435    /// This is the equivalent of [`Terminal::draw`] but the render callback is a function or
436    /// closure that returns a `Result` instead of nothing.
437    ///
438    /// Applications should call `try_draw` or [`draw`] in a loop to continuously render the
439    /// terminal. These methods are the main entry points for drawing to the terminal.
440    ///
441    /// [`draw`]: Terminal::draw
442    ///
443    /// This method will:
444    ///
445    /// - autoresize the terminal if necessary
446    /// - call the render callback, passing it a [`Frame`] reference to render to
447    /// - flush the current internal state by copying the current buffer to the backend
448    /// - move the cursor to the last known position if it was set during the rendering closure
449    /// - return a [`CompletedFrame`] with the current buffer and the area of the terminal
450    ///
451    /// The render callback passed to `try_draw` can return any [`Result`] with an error type that
452    /// can be converted into an [`std::io::Error`] using the [`Into`] trait. This makes it possible
453    /// to use the `?` operator to propagate errors that occur during rendering. If the render
454    /// callback returns an error, the error will be returned from `try_draw` as an
455    /// [`std::io::Error`] and the terminal will not be updated.
456    ///
457    /// The [`CompletedFrame`] returned by this method can be useful for debugging or testing
458    /// purposes, but it is often not used in regular applicationss.
459    ///
460    /// The render callback should fully render the entire frame when called, including areas that
461    /// are unchanged from the previous frame. This is because each frame is compared to the
462    /// previous frame to determine what has changed, and only the changes are written to the
463    /// terminal. If the render function does not fully render the frame, the terminal will not be
464    /// in a consistent state.
465    ///
466    /// # Examples
467    ///
468    /// ```should_panic
469    /// # use ratatui::layout::Position;;
470    /// # let backend = ratatui::backend::TestBackend::new(10, 10);
471    /// # let mut terminal = ratatui::Terminal::new(backend)?;
472    /// use std::io;
473    ///
474    /// use ratatui::widgets::Paragraph;
475    ///
476    /// // with a closure
477    /// terminal.try_draw(|frame| {
478    ///     let value: u8 = "not a number".parse().map_err(io::Error::other)?;
479    ///     let area = frame.area();
480    ///     frame.render_widget(Paragraph::new("Hello World!"), area);
481    ///     frame.set_cursor_position(Position { x: 0, y: 0 });
482    ///     io::Result::Ok(())
483    /// })?;
484    ///
485    /// // or with a function
486    /// terminal.try_draw(render)?;
487    ///
488    /// fn render(frame: &mut ratatui::Frame) -> io::Result<()> {
489    ///     let value: u8 = "not a number".parse().map_err(io::Error::other)?;
490    ///     frame.render_widget(Paragraph::new("Hello World!"), frame.area());
491    ///     Ok(())
492    /// }
493    /// # io::Result::Ok(())
494    /// ```
495    pub fn try_draw<F, E>(&mut self, render_callback: F) -> io::Result<()>
496    where
497        F: FnOnce(&mut Frame) -> Result<(), E>,
498        E: Into<io::Error>,
499    {
500        // Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
501        // and the terminal (if growing), which may OOB.
502        self.autoresize()?;
503
504        let mut frame = self.get_frame();
505
506        render_callback(&mut frame).map_err(Into::into)?;
507
508        // We can't change the cursor position right away because we have to flush the frame to
509        // stdout first. But we also can't keep the frame around, since it holds a &mut to
510        // Buffer. Thus, we're taking the important data out of the Frame and dropping it.
511        let cursor_position = frame.cursor_position;
512
513        // Draw to stdout
514        self.flush()?;
515
516        match cursor_position {
517            None => self.hide_cursor()?,
518            Some(position) => {
519                self.show_cursor()?;
520                self.set_cursor_position(position)?;
521            }
522        }
523
524        self.swap_buffers();
525
526        // Flush
527        self.backend.flush()?;
528
529        // increment frame count before returning from draw
530        self.frame_count = self.frame_count.wrapping_add(1);
531
532        Ok(())
533    }
534
535    /// Hides the cursor.
536    pub fn hide_cursor(&mut self) -> io::Result<()> {
537        self.backend.hide_cursor()?;
538        self.hidden_cursor = true;
539        Ok(())
540    }
541
542    /// Shows the cursor.
543    pub fn show_cursor(&mut self) -> io::Result<()> {
544        self.backend.show_cursor()?;
545        self.hidden_cursor = false;
546        Ok(())
547    }
548
549    /// Gets the current cursor position.
550    ///
551    /// This is the position of the cursor after the last draw call.
552    #[allow(dead_code)]
553    pub fn get_cursor_position(&mut self) -> io::Result<Position> {
554        self.backend.get_cursor_position()
555    }
556
557    /// Sets the cursor position.
558    pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
559        let position = position.into();
560        self.backend.set_cursor_position(position)?;
561        self.last_known_cursor_pos = position;
562        Ok(())
563    }
564
565    /// Clear the terminal and force a full redraw on the next draw call.
566    pub fn clear(&mut self) -> io::Result<()> {
567        if self.viewport_area.is_empty() {
568            return Ok(());
569        }
570        self.backend
571            .set_cursor_position(self.viewport_area.as_position())?;
572        self.backend.clear_region(ClearType::AfterCursor)?;
573        // Reset the back buffer to make sure the next update will redraw everything.
574        self.buffers[1 - self.current].reset();
575        Ok(())
576    }
577
578    /// Clears the inactive buffer and swaps it with the current buffer
579    pub fn swap_buffers(&mut self) {
580        self.buffers[1 - self.current].reset();
581        self.current = 1 - self.current;
582    }
583
584    /// Queries the real size of the backend.
585    pub fn size(&self) -> io::Result<Size> {
586        self.backend.size()
587    }
588}