tty_interface/interface.rs
1use std::mem::swap;
2
3use crossterm::{
4 cursor,
5 style::{self, Attribute, ContentStyle, StyledContent},
6 terminal, QueueableCommand,
7};
8use unicode_segmentation::UnicodeSegmentation;
9
10use crate::{pos, Cell, Color, Device, Position, Result, State, Style, Vector};
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 {
106 if let Some(last_position) = self.current.get_last_position() {
107 self.move_cursor_to(pos!(0, last_position.y()))?;
108 }
109 }
110
111 self.device.disable_raw_mode()?;
112
113 println!();
114 Ok(())
115 }
116
117 /// Update the interface's text at the specified position. Changes are staged until applied.
118 ///
119 /// # Examples
120 /// ```
121 /// # use tty_interface::{Error, test::VirtualDevice};
122 /// # let mut device = VirtualDevice::new();
123 /// use tty_interface::{Interface, Position, pos};
124 ///
125 /// let mut interface = Interface::new_alternate(&mut device)?;
126 /// interface.set(pos!(1, 1), "Hello, world!");
127 /// # Ok::<(), Error>(())
128 /// ```
129 pub fn set(&mut self, position: Position, text: &str) {
130 self.stage_text(position, text, None)
131 }
132
133 /// Update the interface's text at the specified position. Changes are staged until applied.
134 ///
135 /// # Examples
136 /// ```
137 /// # use tty_interface::{Error, test::VirtualDevice};
138 /// # let mut device = VirtualDevice::new();
139 /// use tty_interface::{Interface, Style, Position, pos};
140 ///
141 /// let mut interface = Interface::new_alternate(&mut device)?;
142 /// interface.set_styled(pos!(1, 1), "Hello, world!", Style::new().set_bold(true));
143 /// # Ok::<(), Error>(())
144 /// ```
145 pub fn set_styled(&mut self, position: Position, text: &str, style: Style) {
146 self.stage_text(position, text, Some(style))
147 }
148
149 /// Clear all text on the specified line. Changes are staged until applied.
150 ///
151 /// # Examples
152 /// ```
153 /// # use tty_interface::{Error, test::VirtualDevice};
154 /// # let mut device = VirtualDevice::new();
155 /// use tty_interface::{Interface, Style, Position, pos};
156 ///
157 /// let mut interface = Interface::new_alternate(&mut device)?;
158 ///
159 /// // Write "Hello," and "world!" on two different lines
160 /// interface.set(pos!(0, 0), "Hello,");
161 /// interface.set(pos!(0, 1), "world!");
162 /// interface.apply()?;
163 ///
164 /// // Clear the second line, "world!"
165 /// interface.clear_line(1);
166 /// interface.apply()?;
167 /// # Ok::<(), Error>(())
168 /// ```
169 pub fn clear_line(&mut self, line: u16) {
170 let alternate = self.alternate.get_or_insert_with(|| self.current.clone());
171 alternate.clear_line(line);
172 }
173
174 /// Clear the remainder of the line from the specified position. Changes are staged until
175 /// applied.
176 ///
177 /// # Examples
178 /// ```
179 /// # use tty_interface::{Error, test::VirtualDevice};
180 /// # let mut device = VirtualDevice::new();
181 /// use tty_interface::{Interface, Style, Position, pos};
182 ///
183 /// let mut interface = Interface::new_alternate(&mut device)?;
184 ///
185 /// // Write "Hello, world!" to the first line
186 /// interface.set(pos!(0, 0), "Hello, world!");
187 /// interface.apply()?;
188 ///
189 /// // Clear everything after "Hello"
190 /// interface.clear_rest_of_line(pos!(5, 0));
191 /// interface.apply()?;
192 /// # Ok::<(), Error>(())
193 /// ```
194 pub fn clear_rest_of_line(&mut self, from: Position) {
195 let alternate = self.alternate.get_or_insert_with(|| self.current.clone());
196 alternate.clear_rest_of_line(from);
197 }
198
199 /// Clear the remainder of the interface from the specified position. Changes are staged until
200 /// applied.
201 ///
202 /// # Examples
203 /// ```
204 /// # use tty_interface::{Error, test::VirtualDevice};
205 /// # let mut device = VirtualDevice::new();
206 /// use tty_interface::{Interface, Style, Position, pos};
207 ///
208 /// let mut interface = Interface::new_alternate(&mut device)?;
209 ///
210 /// // Write two lines of content
211 /// interface.set(pos!(0, 0), "Hello, world!");
212 /// interface.set(pos!(0, 1), "Another line");
213 /// interface.apply()?;
214 ///
215 /// // Clear everything after "Hello", including the second line
216 /// interface.clear_rest_of_interface(pos!(5, 0));
217 /// interface.apply()?;
218 /// # Ok::<(), Error>(())
219 /// ```
220 pub fn clear_rest_of_interface(&mut self, from: Position) {
221 let alternate = self.alternate.get_or_insert_with(|| self.current.clone());
222 alternate.clear_rest_of_interface(from);
223 }
224
225 /// Update the interface's cursor to the specified position, or hide it if unspecified.
226 ///
227 /// # Examples
228 /// ```
229 /// # use tty_interface::{Error, test::VirtualDevice};
230 /// # let mut device = VirtualDevice::new();
231 /// use tty_interface::{Interface, Position, pos};
232 ///
233 /// let mut interface = Interface::new_alternate(&mut device)?;
234 /// interface.set_cursor(Some(pos!(1, 2)));
235 /// # Ok::<(), Error>(())
236 /// ```
237 pub fn set_cursor(&mut self, position: Option<Position>) {
238 self.alternate.get_or_insert_with(|| self.current.clone());
239 self.staged_cursor = position;
240 }
241
242 /// Stages the specified text and optional style at a position in the terminal.
243 fn stage_text(&mut self, position: Position, text: &str, style: Option<Style>) {
244 let alternate = self.alternate.get_or_insert_with(|| self.current.clone());
245
246 let mut line = position.y().into();
247 let mut column = position.x().into();
248
249 for grapheme in text.graphemes(true) {
250 if column > self.size.x().into() {
251 column = 0;
252 line += 1;
253 }
254
255 let cell_position = pos!(column, line);
256 match style {
257 Some(style) => alternate.set_styled_text(cell_position, grapheme, style),
258 None => alternate.set_text(cell_position, grapheme),
259 }
260
261 column += 1;
262 }
263 }
264
265 /// Applies staged changes to the terminal.
266 ///
267 /// # Examples
268 /// ```
269 /// # use tty_interface::{Error, test::VirtualDevice};
270 /// # let mut device = VirtualDevice::new();
271 /// use tty_interface::{Interface, Position, pos};
272 ///
273 /// let mut interface = Interface::new_alternate(&mut device)?;
274 /// interface.set(pos!(1, 1), "Hello, world!");
275 /// interface.apply()?;
276 /// # Ok::<(), Error>(())
277 /// ```
278 pub fn apply(&mut self) -> Result<()> {
279 if self.alternate.is_none() {
280 return Ok(());
281 }
282
283 let mut alternate = self.alternate.take().unwrap();
284 swap(&mut self.current, &mut alternate);
285
286 let dirty_cells: Vec<(Position, Option<Cell>)> = self.current.dirty_iter().collect();
287
288 self.device.queue(cursor::Hide)?;
289
290 for (position, cell) in dirty_cells {
291 if self.cursor != position {
292 self.move_cursor_to(position)?;
293 }
294
295 match cell {
296 Some(cell) => {
297 let mut content_style = ContentStyle::default();
298 if let Some(style) = cell.style() {
299 content_style = get_content_style(*style);
300 }
301
302 let styled_content = StyledContent::new(content_style, cell.grapheme());
303 let print_styled_content = style::PrintStyledContent(styled_content);
304 self.device.queue(print_styled_content)?;
305 }
306 None => {
307 let clear_content = style::Print(' ');
308 self.device.queue(clear_content)?;
309 }
310 }
311
312 self.cursor = self.cursor.translate(1, 0);
313 }
314
315 if let Some(position) = self.staged_cursor {
316 self.move_cursor_to(position)?;
317 self.device.queue(cursor::Show)?;
318 }
319
320 self.device.flush()?;
321
322 self.current.clear_dirty();
323
324 Ok(())
325 }
326
327 /// Move the cursor to the specified position and update it in state.
328 fn move_cursor_to(&mut self, position: Position) -> Result<()> {
329 if self.relative {
330 let diff_x = position.x() as i32 - self.cursor.x() as i32;
331 let diff_y = position.y() as i32 - self.cursor.y() as i32;
332
333 if diff_x > 0 {
334 self.device.queue(cursor::MoveRight(diff_x as u16))?;
335 } else if diff_x < 0 {
336 self.device.queue(cursor::MoveLeft(diff_x.abs() as u16))?;
337 }
338
339 if diff_y > 0 {
340 self.device
341 .queue(style::Print("\n".repeat(diff_y as usize)))?;
342 } else if diff_y < 0 {
343 self.device.queue(cursor::MoveUp(diff_y.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}