tty_interface/interface.rs
1use std::mem::swap;
2
3use crossterm::{
4 QueueableCommand, cursor,
5 style::{self, Attribute, ContentStyle, StyledContent},
6 terminal,
7};
8use unicode_segmentation::UnicodeSegmentation;
9
10use crate::{Cell, Color, Device, Position, Result, State, Style, Vector, pos};
11
12/// A TTY-based user-interface providing optimized update rendering.
13pub struct Interface<'a> {
14 device: &'a mut dyn Device,
15 size: Vector,
16 current: State,
17 alternate: Option<State>,
18 staged_cursor: Option<Position>,
19 cursor: Position,
20 relative: bool,
21}
22
23impl Interface<'_> {
24 /// Create a new interface for the specified device on the alternate screen.
25 ///
26 /// # Examples
27 /// ```
28 /// # use tty_interface::{Error, test::VirtualDevice};
29 /// # let mut device = VirtualDevice::new();
30 /// use tty_interface::Interface;
31 ///
32 /// let interface = Interface::new_alternate(&mut device)?;
33 /// # Ok::<(), Error>(())
34 /// ```
35 pub fn new_alternate<'a>(device: &'a mut dyn Device) -> Result<Interface<'a>> {
36 let size = device.get_terminal_size()?;
37
38 let mut interface = Interface {
39 device,
40 size,
41 current: State::new(),
42 alternate: None,
43 staged_cursor: None,
44 cursor: pos!(0, 0),
45 relative: false,
46 };
47
48 let device = &mut interface.device;
49 device.enable_raw_mode()?;
50 device.queue(terminal::EnterAlternateScreen)?;
51 device.queue(terminal::Clear(terminal::ClearType::All))?;
52 device.queue(cursor::Hide)?;
53 device.queue(cursor::MoveTo(0, 0))?;
54 device.flush()?;
55
56 Ok(interface)
57 }
58
59 /// Create a new interface for the specified device which renders relatively in the buffer.
60 ///
61 /// # Examples
62 /// ```
63 /// # use tty_interface::{Error, test::VirtualDevice};
64 /// # let mut device = VirtualDevice::new();
65 /// use tty_interface::Interface;
66 ///
67 /// let interface = Interface::new_relative(&mut device)?;
68 /// # Ok::<(), Error>(())
69 /// ```
70 pub fn new_relative<'a>(device: &'a mut dyn Device) -> Result<Interface<'a>> {
71 let size = device.get_terminal_size()?;
72
73 let mut interface = Interface {
74 device,
75 size,
76 current: State::new(),
77 alternate: None,
78 staged_cursor: None,
79 cursor: pos!(0, 0),
80 relative: true,
81 };
82
83 let device = &mut interface.device;
84 device.enable_raw_mode()?;
85
86 Ok(interface)
87 }
88
89 /// When finished using this interface, uninitialize its terminal configuration.
90 ///
91 /// # Examples
92 /// ```
93 /// # use tty_interface::{Error, test::VirtualDevice};
94 /// # let mut device = VirtualDevice::new();
95 /// use tty_interface::Interface;
96 ///
97 /// let interface = Interface::new_alternate(&mut device)?;
98 /// interface.exit()?;
99 /// # Ok::<(), Error>(())
100 /// ```
101 pub fn exit(mut self) -> Result<()> {
102 if !self.relative {
103 self.device.queue(terminal::LeaveAlternateScreen)?;
104 self.device.flush()?;
105 } else if let Some(last_position) = self.current.get_last_position() {
106 self.move_cursor_to(pos!(0, last_position.y()))?;
107 }
108
109 self.device.disable_raw_mode()?;
110
111 println!();
112 Ok(())
113 }
114
115 /// Update the interface's text at the specified position. Changes are staged until applied.
116 ///
117 /// # Examples
118 /// ```
119 /// # use tty_interface::{Error, test::VirtualDevice};
120 /// # let mut device = VirtualDevice::new();
121 /// use tty_interface::{Interface, Position, pos};
122 ///
123 /// let mut interface = Interface::new_alternate(&mut device)?;
124 /// interface.set(pos!(1, 1), "Hello, world!");
125 /// # Ok::<(), Error>(())
126 /// ```
127 pub fn set(&mut self, position: Position, text: &str) {
128 self.stage_text(position, text, None)
129 }
130
131 /// Update the interface's text at the specified position. Changes are staged until applied.
132 ///
133 /// # Examples
134 /// ```
135 /// # use tty_interface::{Error, test::VirtualDevice};
136 /// # let mut device = VirtualDevice::new();
137 /// use tty_interface::{Interface, Style, Position, pos};
138 ///
139 /// let mut interface = Interface::new_alternate(&mut device)?;
140 /// interface.set_styled(pos!(1, 1), "Hello, world!", Style::new().set_bold(true));
141 /// # Ok::<(), Error>(())
142 /// ```
143 pub fn set_styled(&mut self, position: Position, text: &str, style: Style) {
144 self.stage_text(position, text, Some(style))
145 }
146
147 /// Clear all text on the specified line. Changes are staged until applied.
148 ///
149 /// # Examples
150 /// ```
151 /// # use tty_interface::{Error, test::VirtualDevice};
152 /// # let mut device = VirtualDevice::new();
153 /// use tty_interface::{Interface, Style, Position, pos};
154 ///
155 /// let mut interface = Interface::new_alternate(&mut device)?;
156 ///
157 /// // Write "Hello," and "world!" on two different lines
158 /// interface.set(pos!(0, 0), "Hello,");
159 /// interface.set(pos!(0, 1), "world!");
160 /// interface.apply()?;
161 ///
162 /// // Clear the second line, "world!"
163 /// interface.clear_line(1);
164 /// interface.apply()?;
165 /// # Ok::<(), Error>(())
166 /// ```
167 pub fn clear_line(&mut self, line: u16) {
168 let alternate = self.alternate.get_or_insert_with(|| self.current.clone());
169 alternate.clear_line(line);
170 }
171
172 /// Clear the remainder of the line from the specified position. Changes are staged until
173 /// applied.
174 ///
175 /// # Examples
176 /// ```
177 /// # use tty_interface::{Error, test::VirtualDevice};
178 /// # let mut device = VirtualDevice::new();
179 /// use tty_interface::{Interface, Style, Position, pos};
180 ///
181 /// let mut interface = Interface::new_alternate(&mut device)?;
182 ///
183 /// // Write "Hello, world!" to the first line
184 /// interface.set(pos!(0, 0), "Hello, world!");
185 /// interface.apply()?;
186 ///
187 /// // Clear everything after "Hello"
188 /// interface.clear_rest_of_line(pos!(5, 0));
189 /// interface.apply()?;
190 /// # Ok::<(), Error>(())
191 /// ```
192 pub fn clear_rest_of_line(&mut self, from: Position) {
193 let alternate = self.alternate.get_or_insert_with(|| self.current.clone());
194 alternate.clear_rest_of_line(from);
195 }
196
197 /// Clear the remainder of the interface from the specified position. Changes are staged until
198 /// applied.
199 ///
200 /// # Examples
201 /// ```
202 /// # use tty_interface::{Error, test::VirtualDevice};
203 /// # let mut device = VirtualDevice::new();
204 /// use tty_interface::{Interface, Style, Position, pos};
205 ///
206 /// let mut interface = Interface::new_alternate(&mut device)?;
207 ///
208 /// // Write two lines of content
209 /// interface.set(pos!(0, 0), "Hello, world!");
210 /// interface.set(pos!(0, 1), "Another line");
211 /// interface.apply()?;
212 ///
213 /// // Clear everything after "Hello", including the second line
214 /// interface.clear_rest_of_interface(pos!(5, 0));
215 /// interface.apply()?;
216 /// # Ok::<(), Error>(())
217 /// ```
218 pub fn clear_rest_of_interface(&mut self, from: Position) {
219 let alternate = self.alternate.get_or_insert_with(|| self.current.clone());
220 alternate.clear_rest_of_interface(from);
221 }
222
223 /// Update the interface's cursor to the specified position, or hide it if unspecified.
224 ///
225 /// # Examples
226 /// ```
227 /// # use tty_interface::{Error, test::VirtualDevice};
228 /// # let mut device = VirtualDevice::new();
229 /// use tty_interface::{Interface, Position, pos};
230 ///
231 /// let mut interface = Interface::new_alternate(&mut device)?;
232 /// interface.set_cursor(Some(pos!(1, 2)));
233 /// # Ok::<(), Error>(())
234 /// ```
235 pub fn set_cursor(&mut self, position: Option<Position>) {
236 self.alternate.get_or_insert_with(|| self.current.clone());
237 self.staged_cursor = position;
238 }
239
240 /// Stages the specified text and optional style at a position in the terminal.
241 fn stage_text(&mut self, position: Position, text: &str, style: Option<Style>) {
242 let alternate = self.alternate.get_or_insert_with(|| self.current.clone());
243
244 let mut line = position.y();
245 let mut column = position.x();
246
247 for grapheme in text.graphemes(true) {
248 if column > self.size.x() {
249 column = 0;
250 line += 1;
251 }
252
253 let cell_position = pos!(column, line);
254 match style {
255 Some(style) => alternate.set_styled_text(cell_position, grapheme, style),
256 None => alternate.set_text(cell_position, grapheme),
257 }
258
259 column += 1;
260 }
261 }
262
263 /// Applies staged changes to the terminal.
264 ///
265 /// # Examples
266 /// ```
267 /// # use tty_interface::{Error, test::VirtualDevice};
268 /// # let mut device = VirtualDevice::new();
269 /// use tty_interface::{Interface, Position, pos};
270 ///
271 /// let mut interface = Interface::new_alternate(&mut device)?;
272 /// interface.set(pos!(1, 1), "Hello, world!");
273 /// interface.apply()?;
274 /// # Ok::<(), Error>(())
275 /// ```
276 pub fn apply(&mut self) -> Result<()> {
277 if self.alternate.is_none() {
278 return Ok(());
279 }
280
281 let mut alternate = self.alternate.take().unwrap();
282 swap(&mut self.current, &mut alternate);
283
284 let dirty_cells: Vec<(Position, Option<Cell>)> = self.current.dirty_iter().collect();
285
286 self.device.queue(cursor::Hide)?;
287
288 for (position, cell) in dirty_cells {
289 if self.cursor != position {
290 self.move_cursor_to(position)?;
291 }
292
293 match cell {
294 Some(cell) => {
295 let mut content_style = ContentStyle::default();
296 if let Some(style) = cell.style() {
297 content_style = get_content_style(*style);
298 }
299
300 let styled_content = StyledContent::new(content_style, cell.grapheme());
301 let print_styled_content = style::PrintStyledContent(styled_content);
302 self.device.queue(print_styled_content)?;
303 }
304 None => {
305 let clear_content = style::Print(' ');
306 self.device.queue(clear_content)?;
307 }
308 }
309
310 self.cursor = self.cursor.translate(1, 0);
311 }
312
313 if let Some(position) = self.staged_cursor {
314 self.move_cursor_to(position)?;
315 self.device.queue(cursor::Show)?;
316 }
317
318 self.device.flush()?;
319
320 self.current.clear_dirty();
321
322 Ok(())
323 }
324
325 /// Move the cursor to the specified position and update it in state.
326 fn move_cursor_to(&mut self, position: Position) -> Result<()> {
327 if self.relative {
328 let diff_x = position.x() as i32 - self.cursor.x() as i32;
329 let diff_y = position.y() as i32 - self.cursor.y() as i32;
330
331 if diff_x > 0 {
332 self.device.queue(cursor::MoveRight(diff_x as u16))?;
333 } else if diff_x < 0 {
334 self.device
335 .queue(cursor::MoveLeft(diff_x.unsigned_abs() as u16))?;
336 }
337
338 if diff_y > 0 {
339 self.device
340 .queue(style::Print("\n".repeat(diff_y as usize)))?;
341 } else if diff_y < 0 {
342 self.device
343 .queue(cursor::MoveUp(diff_y.unsigned_abs() as u16))?;
344 }
345 } else {
346 let move_cursor = cursor::MoveTo(position.x(), position.y());
347 self.device.queue(move_cursor)?;
348 }
349
350 self.cursor = position;
351
352 Ok(())
353 }
354}
355
356/// Converts a style from its internal representation to crossterm's.
357fn get_content_style(style: Style) -> ContentStyle {
358 let mut content_style = ContentStyle::default();
359
360 if let Some(color) = style.foreground() {
361 content_style.foreground_color = Some(get_crossterm_color(color));
362 }
363
364 if let Some(color) = style.background() {
365 content_style.background_color = Some(get_crossterm_color(color));
366 }
367
368 if style.is_bold() {
369 content_style.attributes.set(Attribute::Bold);
370 }
371
372 if style.is_italic() {
373 content_style.attributes.set(Attribute::Italic);
374 }
375
376 if style.is_underlined() {
377 content_style.attributes.set(Attribute::Underlined);
378 }
379
380 content_style
381}
382
383fn get_crossterm_color(color: Color) -> crossterm::style::Color {
384 match color {
385 Color::Black => style::Color::Black,
386 Color::DarkGrey => style::Color::DarkGrey,
387 Color::Red => style::Color::Red,
388 Color::DarkRed => style::Color::DarkRed,
389 Color::Green => style::Color::Green,
390 Color::DarkGreen => style::Color::DarkGreen,
391 Color::Yellow => style::Color::Yellow,
392 Color::DarkYellow => style::Color::DarkYellow,
393 Color::Blue => style::Color::Blue,
394 Color::DarkBlue => style::Color::DarkBlue,
395 Color::Magenta => style::Color::Magenta,
396 Color::DarkMagenta => style::Color::DarkMagenta,
397 Color::Cyan => style::Color::Cyan,
398 Color::DarkCyan => style::Color::DarkCyan,
399 Color::White => style::Color::White,
400 Color::Grey => style::Color::Grey,
401 Color::Reset => style::Color::Reset,
402 }
403}