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}