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}