tui_react/
terminal.rs

1//! Derived from TUI-rs, license: MIT, Copyright (c) 2016 Florian Dehau
2use std::{borrow::Borrow, io};
3
4use tui::{backend::Backend, buffer::Buffer, layout::Rect, prelude::Position};
5
6/// A component meant to be rendered by `Terminal::render(...)`.
7/// All other components don't have to implement this trait, and instead
8/// provide a render method by convention, tuned towards their needs using whichever
9/// generic types or lifetimes they need.
10pub trait ToplevelComponent {
11    type Props;
12
13    fn render(&mut self, props: impl Borrow<Self::Props>, area: Rect, buf: &mut Buffer);
14}
15
16#[derive(Debug)]
17pub struct Terminal<B>
18where
19    B: Backend,
20{
21    pub backend: B,
22    buffers: [Buffer; 2],
23    current: usize,
24    hidden_cursor: bool,
25    known_size: Rect,
26}
27
28impl<B> Drop for Terminal<B>
29where
30    B: Backend,
31{
32    fn drop(&mut self) {
33        // Attempt to restore the cursor state
34        if self.hidden_cursor {
35            // We can't use the where clause in drop, so we need to work around it
36            // For backends that don't implement Into<io::Error>, this won't compile
37            // but that's acceptable since most real backends do implement this
38            let _ = self.backend.show_cursor();
39        }
40    }
41}
42
43impl<B> Terminal<B>
44where
45    B: Backend,
46{
47    pub fn new(backend: B) -> io::Result<Terminal<B>>
48    where
49        B::Error: Send + Sync + 'static,
50    {
51        let size = backend.size().map_err(io::Error::other)?.into();
52        Ok(Terminal {
53            backend,
54            buffers: [Buffer::empty(size), Buffer::empty(size)],
55            current: 0,
56            hidden_cursor: false,
57            known_size: size,
58        })
59    }
60
61    pub fn current_buffer_mut(&mut self) -> &mut Buffer {
62        &mut self.buffers[self.current]
63    }
64
65    pub fn reconcile_and_flush(&mut self) -> io::Result<()>
66    where
67        B::Error: Send + Sync + 'static,
68    {
69        let previous_buffer = &self.buffers[1 - self.current];
70        let current_buffer = &self.buffers[self.current];
71        let updates = previous_buffer.diff(current_buffer);
72        self.backend
73            .draw(updates.into_iter())
74            .map_err(io::Error::other)
75    }
76
77    pub fn resize(&mut self, area: Rect) -> io::Result<()>
78    where
79        B::Error: Send + Sync + 'static,
80    {
81        self.buffers[self.current].resize(area);
82        self.buffers[1 - self.current].reset();
83        self.buffers[1 - self.current].resize(area);
84        self.known_size = area;
85        self.backend.clear().map_err(io::Error::other)
86    }
87
88    pub fn autoresize(&mut self) -> io::Result<()>
89    where
90        B::Error: Send + Sync + 'static,
91    {
92        let size = self.size()?;
93        if self.known_size != size {
94            self.resize(size)?;
95        }
96        Ok(())
97    }
98
99    /// Get ready for rendering and return the maximum display size as `Rect`
100    pub fn pre_render(&mut self) -> io::Result<Rect>
101    where
102        B::Error: Send + Sync + 'static,
103    {
104        // Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
105        // and the terminal (if growing), which may OOB.
106        self.autoresize()?;
107        Ok(self.known_size)
108    }
109
110    pub fn post_render(&mut self) -> io::Result<()>
111    where
112        B::Error: Send + Sync + 'static,
113    {
114        self.reconcile_and_flush()?;
115
116        self.buffers[1 - self.current].reset();
117        self.current = 1 - self.current;
118
119        self.backend.flush().map_err(io::Error::other)?;
120        Ok(())
121    }
122
123    pub fn render<C>(&mut self, component: &mut C, props: impl Borrow<C::Props>) -> io::Result<()>
124    where
125        C: ToplevelComponent,
126        B::Error: Send + Sync + 'static,
127    {
128        self.pre_render()?;
129        component.render(props, self.known_size, self.current_buffer_mut());
130        self.post_render()
131    }
132
133    pub fn hide_cursor(&mut self) -> io::Result<()>
134    where
135        B::Error: Send + Sync + 'static,
136    {
137        self.backend.hide_cursor().map_err(io::Error::other)?;
138        self.hidden_cursor = true;
139        Ok(())
140    }
141    pub fn show_cursor(&mut self) -> io::Result<()>
142    where
143        B::Error: Send + Sync + 'static,
144    {
145        self.backend.show_cursor().map_err(io::Error::other)?;
146        self.hidden_cursor = false;
147        Ok(())
148    }
149    pub fn get_cursor(&mut self) -> io::Result<(u16, u16)>
150    where
151        B::Error: Send + Sync + 'static,
152    {
153        let pos = self
154            .backend
155            .get_cursor_position()
156            .map_err(io::Error::other)?;
157        Ok((pos.x, pos.y))
158    }
159    pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()>
160    where
161        B::Error: Send + Sync + 'static,
162    {
163        self.backend
164            .set_cursor_position(Position { x, y })
165            .map_err(io::Error::other)
166    }
167    pub fn clear(&mut self) -> io::Result<()>
168    where
169        B::Error: Send + Sync + 'static,
170    {
171        self.backend.clear().map_err(io::Error::other)
172    }
173    pub fn size(&self) -> io::Result<Rect>
174    where
175        B::Error: Send + Sync + 'static,
176    {
177        self.backend
178            .size()
179            .map_err(io::Error::other)
180            .map(|size| size.into())
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use tui::backend::TestBackend;
188
189    #[expect(dead_code)]
190    #[derive(Default, Clone)]
191    struct ComplexProps {
192        x: usize,
193        y: String,
194    }
195
196    #[derive(Default)]
197    struct StatefulComponent {
198        x: usize,
199    }
200
201    #[derive(Default)]
202    struct StatelessComponent;
203
204    impl ToplevelComponent for StatefulComponent {
205        type Props = usize;
206
207        fn render(&mut self, props: impl Borrow<Self::Props>, _area: Rect, _buf: &mut Buffer) {
208            self.x += *props.borrow();
209        }
210    }
211
212    impl ToplevelComponent for StatelessComponent {
213        type Props = ComplexProps;
214        fn render(&mut self, _props: impl Borrow<Self::Props>, _area: Rect, _buf: &mut Buffer) {
215            // does not matter - we want to see it compiles essentially
216        }
217    }
218
219    #[test]
220    fn it_does_render_with_simple_and_complex_props() {
221        let mut term = Terminal::new(TestBackend::new(20, 20)).unwrap();
222        let mut c = StatefulComponent::default();
223
224        term.render(&mut c, 3usize).ok();
225        assert_eq!(c.x, 3);
226
227        let mut c = StatelessComponent;
228        term.render(&mut c, ComplexProps::default()).ok();
229    }
230}