1use std::{borrow::Borrow, io};
3
4use tui::{backend::Backend, buffer::Buffer, layout::Rect, prelude::Position};
5
6pub 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 if self.hidden_cursor {
35 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 pub fn pre_render(&mut self) -> io::Result<Rect>
101 where
102 B::Error: Send + Sync + 'static,
103 {
104 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 }
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}