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(cursor::Show)?;
104 self.device.queue(terminal::LeaveAlternateScreen)?;
105 } else {
106 if let Some(last_position) = self.current.get_last_position() {
107 self.move_cursor_to(pos!(0, last_position.y()))?;
108 }
109 self.device.queue(cursor::Show)?;
110 }
111
112 self.device.flush()?;
113 self.device.disable_raw_mode()?;
114
115 println!();
116 Ok(())
117 }
118
119 /// Update the interface's text at the specified position. Changes are staged until applied.
120 ///
121 /// # Examples
122 /// ```
123 /// # use tty_interface::{Error, test::VirtualDevice};
124 /// # let mut device = VirtualDevice::new();
125 /// use tty_interface::{Interface, Position, pos};
126 ///
127 /// let mut interface = Interface::new_alternate(&mut device)?;
128 /// interface.set(pos!(1, 1), "Hello, world!");
129 /// # Ok::<(), Error>(())
130 /// ```
131 pub fn set(&mut self, position: Position, text: &str) {
132 self.stage_text(position, text, None)
133 }
134
135 /// Update the interface's text at the specified position. Changes are staged until applied.
136 ///
137 /// # Examples
138 /// ```
139 /// # use tty_interface::{Error, test::VirtualDevice};
140 /// # let mut device = VirtualDevice::new();
141 /// use tty_interface::{Interface, Style, Position, pos};
142 ///
143 /// let mut interface = Interface::new_alternate(&mut device)?;
144 /// interface.set_styled(pos!(1, 1), "Hello, world!", Style::new().set_bold(true));
145 /// # Ok::<(), Error>(())
146 /// ```
147 pub fn set_styled(&mut self, position: Position, text: &str, style: Style) {
148 self.stage_text(position, text, Some(style))
149 }
150
151 /// Clear all text on the specified line. Changes are staged until applied.
152 ///
153 /// # Examples
154 /// ```
155 /// # use tty_interface::{Error, test::VirtualDevice};
156 /// # let mut device = VirtualDevice::new();
157 /// use tty_interface::{Interface, Style, Position, pos};
158 ///
159 /// let mut interface = Interface::new_alternate(&mut device)?;
160 ///
161 /// // Write "Hello," and "world!" on two different lines
162 /// interface.set(pos!(0, 0), "Hello,");
163 /// interface.set(pos!(0, 1), "world!");
164 /// interface.apply()?;
165 ///
166 /// // Clear the second line, "world!"
167 /// interface.clear_line(1);
168 /// interface.apply()?;
169 /// # Ok::<(), Error>(())
170 /// ```
171 pub fn clear_line(&mut self, line: u16) {
172 let alternate = self.alternate.get_or_insert_with(|| self.current.clone());
173 alternate.clear_line(line);
174 }
175
176 /// Clear the remainder of the line from the specified position. Changes are staged until
177 /// applied.
178 ///
179 /// # Examples
180 /// ```
181 /// # use tty_interface::{Error, test::VirtualDevice};
182 /// # let mut device = VirtualDevice::new();
183 /// use tty_interface::{Interface, Style, Position, pos};
184 ///
185 /// let mut interface = Interface::new_alternate(&mut device)?;
186 ///
187 /// // Write "Hello, world!" to the first line
188 /// interface.set(pos!(0, 0), "Hello, world!");
189 /// interface.apply()?;
190 ///
191 /// // Clear everything after "Hello"
192 /// interface.clear_rest_of_line(pos!(5, 0));
193 /// interface.apply()?;
194 /// # Ok::<(), Error>(())
195 /// ```
196 pub fn clear_rest_of_line(&mut self, from: Position) {
197 let alternate = self.alternate.get_or_insert_with(|| self.current.clone());
198 alternate.clear_rest_of_line(from);
199 }
200
201 /// Clear the remainder of the interface from the specified position. Changes are staged until
202 /// applied.
203 ///
204 /// # Examples
205 /// ```
206 /// # use tty_interface::{Error, test::VirtualDevice};
207 /// # let mut device = VirtualDevice::new();
208 /// use tty_interface::{Interface, Style, Position, pos};
209 ///
210 /// let mut interface = Interface::new_alternate(&mut device)?;
211 ///
212 /// // Write two lines of content
213 /// interface.set(pos!(0, 0), "Hello, world!");
214 /// interface.set(pos!(0, 1), "Another line");
215 /// interface.apply()?;
216 ///
217 /// // Clear everything after "Hello", including the second line
218 /// interface.clear_rest_of_interface(pos!(5, 0));
219 /// interface.apply()?;
220 /// # Ok::<(), Error>(())
221 /// ```
222 pub fn clear_rest_of_interface(&mut self, from: Position) {
223 let alternate = self.alternate.get_or_insert_with(|| self.current.clone());
224 alternate.clear_rest_of_interface(from);
225 }
226
227 /// Update the interface's cursor to the specified position, or hide it if unspecified.
228 ///
229 /// # Examples
230 /// ```
231 /// # use tty_interface::{Error, test::VirtualDevice};
232 /// # let mut device = VirtualDevice::new();
233 /// use tty_interface::{Interface, Position, pos};
234 ///
235 /// let mut interface = Interface::new_alternate(&mut device)?;
236 /// interface.set_cursor(Some(pos!(1, 2)));
237 /// # Ok::<(), Error>(())
238 /// ```
239 pub fn set_cursor(&mut self, position: Option<Position>) {
240 self.alternate.get_or_insert_with(|| self.current.clone());
241 self.staged_cursor = position;
242 }
243
244 /// Stages the specified text and optional style at a position in the terminal.
245 fn stage_text(&mut self, position: Position, text: &str, style: Option<Style>) {
246 let alternate = self.alternate.get_or_insert_with(|| self.current.clone());
247
248 let mut line = position.y();
249 let mut column = position.x();
250
251 for grapheme in text.graphemes(true) {
252 if column > self.size.x() {
253 column = 0;
254 line += 1;
255 }
256
257 let cell_position = pos!(column, line);
258 match style {
259 Some(style) => alternate.set_styled_text(cell_position, grapheme, style),
260 None => alternate.set_text(cell_position, grapheme),
261 }
262
263 column += 1;
264 }
265 }
266
267 /// Applies staged changes to the terminal.
268 ///
269 /// # Examples
270 /// ```
271 /// # use tty_interface::{Error, test::VirtualDevice};
272 /// # let mut device = VirtualDevice::new();
273 /// use tty_interface::{Interface, Position, pos};
274 ///
275 /// let mut interface = Interface::new_alternate(&mut device)?;
276 /// interface.set(pos!(1, 1), "Hello, world!");
277 /// interface.apply()?;
278 /// # Ok::<(), Error>(())
279 /// ```
280 pub fn apply(&mut self) -> Result<()> {
281 if self.alternate.is_none() {
282 return Ok(());
283 }
284
285 let mut alternate = self.alternate.take().unwrap();
286 swap(&mut self.current, &mut alternate);
287
288 let dirty_cells: Vec<(Position, Option<Cell>)> = self.current.dirty_iter().collect();
289
290 self.device.queue(cursor::Hide)?;
291
292 for (position, cell) in dirty_cells {
293 if self.cursor != position {
294 self.move_cursor_to(position)?;
295 }
296
297 match cell {
298 Some(cell) => {
299 let mut content_style = ContentStyle::default();
300 if let Some(style) = cell.style() {
301 content_style = get_content_style(*style);
302 }
303
304 let styled_content = StyledContent::new(content_style, cell.grapheme());
305 let print_styled_content = style::PrintStyledContent(styled_content);
306 self.device.queue(print_styled_content)?;
307 }
308 None => {
309 let clear_content = style::Print(' ');
310 self.device.queue(clear_content)?;
311 }
312 }
313
314 self.cursor = self.cursor.translate(1, 0);
315 }
316
317 if let Some(position) = self.staged_cursor {
318 self.move_cursor_to(position)?;
319 self.device.queue(cursor::Show)?;
320 }
321
322 self.device.flush()?;
323
324 self.current.clear_dirty();
325
326 Ok(())
327 }
328
329 /// Move the cursor to the specified position and update it in state.
330 fn move_cursor_to(&mut self, position: Position) -> Result<()> {
331 if self.relative {
332 let diff_x = position.x() as i32 - self.cursor.x() as i32;
333 let diff_y = position.y() as i32 - self.cursor.y() as i32;
334
335 if diff_x > 0 {
336 self.device.queue(cursor::MoveRight(diff_x as u16))?;
337 } else if diff_x < 0 {
338 self.device
339 .queue(cursor::MoveLeft(diff_x.unsigned_abs() as u16))?;
340 }
341
342 if diff_y > 0 {
343 self.device
344 .queue(style::Print("\n".repeat(diff_y as usize)))?;
345 } else if diff_y < 0 {
346 self.device
347 .queue(cursor::MoveUp(diff_y.unsigned_abs() as u16))?;
348 }
349 } else {
350 let move_cursor = cursor::MoveTo(position.x(), position.y());
351 self.device.queue(move_cursor)?;
352 }
353
354 self.cursor = position;
355
356 Ok(())
357 }
358}
359
360/// Converts a style from its internal representation to crossterm's.
361fn get_content_style(style: Style) -> ContentStyle {
362 let mut content_style = ContentStyle::default();
363
364 if let Some(color) = style.foreground() {
365 content_style.foreground_color = Some(get_crossterm_color(color));
366 }
367
368 if let Some(color) = style.background() {
369 content_style.background_color = Some(get_crossterm_color(color));
370 }
371
372 if style.is_bold() {
373 content_style.attributes.set(Attribute::Bold);
374 }
375
376 if style.is_italic() {
377 content_style.attributes.set(Attribute::Italic);
378 }
379
380 if style.is_underlined() {
381 content_style.attributes.set(Attribute::Underlined);
382 }
383
384 content_style
385}
386
387fn get_crossterm_color(color: Color) -> crossterm::style::Color {
388 match color {
389 Color::Black => style::Color::Black,
390 Color::DarkGrey => style::Color::DarkGrey,
391 Color::Red => style::Color::Red,
392 Color::DarkRed => style::Color::DarkRed,
393 Color::Green => style::Color::Green,
394 Color::DarkGreen => style::Color::DarkGreen,
395 Color::Yellow => style::Color::Yellow,
396 Color::DarkYellow => style::Color::DarkYellow,
397 Color::Blue => style::Color::Blue,
398 Color::DarkBlue => style::Color::DarkBlue,
399 Color::Magenta => style::Color::Magenta,
400 Color::DarkMagenta => style::Color::DarkMagenta,
401 Color::Cyan => style::Color::Cyan,
402 Color::DarkCyan => style::Color::DarkCyan,
403 Color::White => style::Color::White,
404 Color::Grey => style::Color::Grey,
405 Color::Reset => style::Color::Reset,
406 }
407}