Skip to main content

altui_core/
terminal.rs

1use crate::{backend::Backend, buffer::Buffer, layout::Rect, widgets::Widget};
2use std::io;
3
4#[derive(Debug, Clone, PartialEq)]
5/// UNSTABLE
6enum ResizeBehavior {
7    Fixed,
8    Auto,
9}
10
11#[derive(Debug, Clone, PartialEq)]
12/// UNSTABLE
13pub struct Viewport {
14    area: Rect,
15    resize_behavior: ResizeBehavior,
16}
17
18impl Viewport {
19    /// UNSTABLE
20    pub fn fixed(area: Rect) -> Viewport {
21        Viewport {
22            area,
23            resize_behavior: ResizeBehavior::Fixed,
24        }
25    }
26}
27
28#[derive(Debug, Clone, PartialEq)]
29/// Options to pass to [`Terminal::with_options`]
30pub struct TerminalOptions {
31    /// Viewport used to draw to the terminal
32    pub viewport: Viewport,
33}
34
35/// Interface to the terminal backed by Termion
36#[derive(Debug)]
37pub struct Terminal<B>
38where
39    B: Backend,
40{
41    backend: B,
42    /// Holds the results of the current and previous draw calls. The two are compared at the end
43    /// of each draw pass to output the necessary updates to the terminal
44    buffers: [Buffer; 2],
45    /// Index of the current buffer in the previous array
46    current: usize,
47    /// Whether the cursor is currently hidden
48    hidden_cursor: bool,
49    /// Viewport
50    viewport: Viewport,
51}
52
53/// Represents a consistent terminal interface for rendering.
54pub struct Frame<'a, B: 'a>
55where
56    B: Backend,
57{
58    terminal: &'a mut Terminal<B>,
59
60    /// Where should the cursor be after drawing this frame?
61    ///
62    /// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x,
63    /// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`.
64    cursor_position: Option<(u16, u16)>,
65}
66
67impl<'a, B> Frame<'a, B>
68where
69    B: Backend,
70{
71    /// Terminal size, guaranteed not to change when rendering.
72    pub fn size(&self) -> Rect {
73        self.terminal.viewport.area
74    }
75
76    /// Render a [`Widget`] to the current buffer using [`Widget::render`].
77    ///
78    /// # Examples
79    ///
80    /// ```rust
81    /// # use altui_core::Terminal;
82    /// # use altui_core::backend::TestBackend;
83    /// # use altui_core::layout::Rect;
84    /// # use altui_core::widgets::Block;
85    /// # let backend = TestBackend::new(5, 5);
86    /// # let mut terminal = Terminal::new(backend).unwrap();
87    /// let mut block = Block::default();
88    /// let area = Rect::new(0, 0, 5, 5);
89    /// let mut frame = terminal.get_frame();
90    /// frame.render_widget(&mut block, area);
91    /// ```
92    pub fn render_widget<W>(&mut self, widget: &mut W, area: Rect)
93    where
94        W: Widget,
95    {
96        widget.render(area, self.terminal.current_buffer_mut());
97    }
98
99    pub fn current_buffer_mut(&mut self) -> &mut Buffer {
100        self.terminal.current_buffer_mut()
101    }
102
103    /// After drawing this frame, make the cursor visible and put it at the specified (x, y)
104    /// coordinates. If this method is not called, the cursor will be hidden.
105    ///
106    /// Note that this will interfere with calls to `Terminal::hide_cursor()`,
107    /// `Terminal::show_cursor()`, and `Terminal::set_cursor()`. Pick one of the APIs and stick
108    /// with it.
109    pub fn set_cursor(&mut self, x: u16, y: u16) {
110        self.cursor_position = Some((x, y));
111    }
112}
113
114/// CompletedFrame represents the state of the terminal after all changes performed in the last
115/// [`Terminal::draw`] call have been applied. Therefore, it is only valid until the next call to
116/// [`Terminal::draw`].
117pub struct CompletedFrame<'a> {
118    pub buffer: &'a Buffer,
119    pub area: Rect,
120}
121
122impl<B> Drop for Terminal<B>
123where
124    B: Backend,
125{
126    fn drop(&mut self) {
127        // Attempt to restore the cursor state
128        if self.hidden_cursor {
129            if let Err(err) = self.show_cursor() {
130                eprintln!("Failed to show the cursor: {}", err);
131            }
132        }
133    }
134}
135
136impl<B> Terminal<B>
137where
138    B: Backend,
139{
140    /// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and
141    /// default colors for the foreground and the background
142    pub fn new(backend: B) -> io::Result<Terminal<B>> {
143        let size = backend.size()?;
144        Terminal::with_options(
145            backend,
146            TerminalOptions {
147                viewport: Viewport {
148                    area: size,
149                    resize_behavior: ResizeBehavior::Auto,
150                },
151            },
152        )
153    }
154
155    /// UNSTABLE
156    pub fn with_options(backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
157        Ok(Terminal {
158            backend,
159            buffers: [
160                Buffer::empty(options.viewport.area),
161                Buffer::empty(options.viewport.area),
162            ],
163            current: 0,
164            hidden_cursor: false,
165            viewport: options.viewport,
166        })
167    }
168
169    /// Get a Frame object which provides a consistent view into the terminal state for rendering.
170    pub fn get_frame(&'_ mut self) -> Frame<'_, B> {
171        Frame {
172            terminal: self,
173            cursor_position: None,
174        }
175    }
176
177    pub fn current_buffer_mut(&mut self) -> &mut Buffer {
178        &mut self.buffers[self.current]
179    }
180
181    pub fn backend(&self) -> &B {
182        &self.backend
183    }
184
185    pub fn backend_mut(&mut self) -> &mut B {
186        &mut self.backend
187    }
188
189    /// Obtains a difference between the previous and the current buffer and passes it to the
190    /// current backend for drawing.
191    pub fn flush(&mut self) -> io::Result<()> {
192        let previous_buffer = &self.buffers[1 - self.current];
193        let current_buffer = &self.buffers[self.current];
194        let updates = previous_buffer.diff(current_buffer);
195        self.backend.draw(updates.into_iter())
196    }
197
198    /// Updates the Terminal so that internal buffers match the requested size. Requested size will
199    /// be saved so the size can remain consistent when rendering.
200    /// This leads to a full clear of the screen.
201    pub fn resize(&mut self, area: Rect) -> io::Result<()> {
202        self.buffers[self.current].resize(area);
203        self.buffers[1 - self.current].resize(area);
204        self.viewport.area = area;
205        self.clear()
206    }
207
208    /// Queries the backend for size and resizes if it doesn't match the previous size.
209    pub fn autoresize(&mut self) -> io::Result<()> {
210        if self.viewport.resize_behavior == ResizeBehavior::Auto {
211            let size = self.size()?;
212            if size != self.viewport.area {
213                self.resize(size)?;
214            }
215        };
216        Ok(())
217    }
218
219    /// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
220    /// and prepares for the next draw call.
221    pub fn draw<F>(&'_ mut self, f: F) -> io::Result<CompletedFrame<'_>>
222    where
223        F: FnOnce(&mut Frame<B>),
224    {
225        // Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
226        // and the terminal (if growing), which may OOB.
227        self.autoresize()?;
228
229        let mut frame = self.get_frame();
230        f(&mut frame);
231        // We can't change the cursor position right away because we have to flush the frame to
232        // stdout first. But we also can't keep the frame around, since it holds a &mut to
233        // Terminal. Thus, we're taking the important data out of the Frame and dropping it.
234        let cursor_position = frame.cursor_position;
235
236        // Draw to stdout
237        self.flush()?;
238
239        match cursor_position {
240            None => self.hide_cursor()?,
241            Some((x, y)) => {
242                self.show_cursor()?;
243                self.set_cursor(x, y)?;
244            }
245        }
246
247        // Swap buffers
248        self.buffers[1 - self.current].reset();
249        self.current = 1 - self.current;
250
251        // Flush
252        self.backend.flush()?;
253        Ok(CompletedFrame {
254            buffer: &self.buffers[1 - self.current],
255            area: self.viewport.area,
256        })
257    }
258
259    pub fn hide_cursor(&mut self) -> io::Result<()> {
260        self.backend.hide_cursor()?;
261        self.hidden_cursor = true;
262        Ok(())
263    }
264
265    pub fn show_cursor(&mut self) -> io::Result<()> {
266        self.backend.show_cursor()?;
267        self.hidden_cursor = false;
268        Ok(())
269    }
270
271    pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
272        self.backend.get_cursor()
273    }
274
275    pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
276        self.backend.set_cursor(x, y)
277    }
278
279    /// Clear the terminal and force a full redraw on the next draw call.
280    pub fn clear(&mut self) -> io::Result<()> {
281        self.backend.clear()?;
282        // Reset the back buffer to make sure the next update will redraw everything.
283        self.buffers[1 - self.current].reset();
284        Ok(())
285    }
286
287    /// Queries the real size of the backend.
288    pub fn size(&self) -> io::Result<Rect> {
289        self.backend.size()
290    }
291}