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}