Skip to main content

katui_present/
cells.rs

1//! The cell presenter: scene cells as SGR sequences, plus the cursor and
2//! clear commands every presenter consumer needs. Every cell
3//! re-establishes its whole style because it cannot know what the cell
4//! before it left behind.
5
6use crate::Sink;
7use std::fmt::Write as _;
8
9use katui_scene::cell::{Attrs, Cell, Color, Position};
10
11/// SGR codes for the attributes, in emission order.
12const ATTR_CODES: &[(Attrs, u8)] = &[
13    (Attrs::BOLD, 1),
14    (Attrs::DIM, 2),
15    (Attrs::ITALIC, 3),
16    (Attrs::UNDERLINE, 4),
17    (Attrs::BLINK, 5),
18    (Attrs::RAPID_BLINK, 6),
19    (Attrs::REVERSE, 7),
20    (Attrs::HIDDEN, 8),
21    (Attrs::CROSSED, 9),
22];
23
24/// The region a clear erases.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ClearKind {
27    /// The whole visible screen.
28    All,
29    /// From the cursor through the end of the screen.
30    AfterCursor,
31    /// From the start of the screen through the cursor.
32    BeforeCursor,
33    /// The cursor's whole line.
34    CurrentLine,
35    /// From the cursor through the end of its line.
36    UntilNewLine,
37}
38
39/// Write `cell` at `at`: cursor addressing, reset, colors, attributes,
40/// symbol.
41pub fn render_cell<W: Sink>(out: &mut W, at: Position, cell: &Cell) -> std::io::Result<()> {
42    let mut text = String::new();
43    push_cursor(&mut text, at);
44    text.push_str("\x1b[0m");
45    write_color(&mut text, 38, 39, cell.fg);
46    write_color(&mut text, 48, 49, cell.bg);
47    for (attr, code) in ATTR_CODES {
48        if cell.attrs.contains(*attr) {
49            let _ = write!(text, "\x1b[{code}m");
50        }
51    }
52    text.push_str(&cell.symbol);
53    out.write_all(text.as_bytes())
54}
55
56/// Address the cursor to `at`, one-based through `u32` so the extremes
57/// cannot wrap.
58pub fn move_cursor<W: Sink>(out: &mut W, at: Position) -> std::io::Result<()> {
59    let mut text = String::new();
60    push_cursor(&mut text, at);
61    out.write_all(text.as_bytes())
62}
63
64pub fn hide_cursor<W: Sink>(out: &mut W) -> std::io::Result<()> {
65    out.write_all(b"\x1b[?25l")
66}
67
68pub fn show_cursor<W: Sink>(out: &mut W) -> std::io::Result<()> {
69    out.write_all(b"\x1b[?25h")
70}
71
72/// Erase `kind`'s region.
73pub fn clear_region<W: Sink>(out: &mut W, kind: ClearKind) -> std::io::Result<()> {
74    let sequence = match kind {
75        ClearKind::All => "\x1b[2J",
76        ClearKind::AfterCursor => "\x1b[0J",
77        ClearKind::BeforeCursor => "\x1b[1J",
78        ClearKind::CurrentLine => "\x1b[2K",
79        ClearKind::UntilNewLine => "\x1b[0K",
80    };
81    out.write_all(sequence.as_bytes())
82}
83
84fn push_cursor(text: &mut String, at: Position) {
85    let _ = write!(
86        text,
87        "\x1b[{};{}H",
88        u32::from(at.y) + 1,
89        u32::from(at.x) + 1
90    );
91}
92
93fn write_color(text: &mut String, set: u8, reset: u8, color: Color) {
94    match color {
95        Color::Reset => {
96            let _ = write!(text, "\x1b[{reset}m");
97        }
98        Color::Indexed(index) => {
99            let _ = write!(text, "\x1b[{set};5;{index}m");
100        }
101        Color::Rgb(r, g, b) => {
102            let _ = write!(text, "\x1b[{set};2;{r};{g};{b}m");
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::{ClearKind, clear_region, hide_cursor, move_cursor, render_cell, show_cursor};
110    use katui_scene::cell::{Attrs, Cell, Color, Position};
111
112    #[test]
113    fn a_styled_cell_writes_golden_bytes() {
114        let cell = Cell {
115            symbol: "A".to_owned(),
116            fg: Color::Rgb(1, 2, 3),
117            bg: Color::Indexed(4),
118            attrs: Attrs::BOLD.union(Attrs::UNDERLINE),
119        };
120        let mut out = Vec::new();
121        render_cell(&mut out, Position { x: 2, y: 1 }, &cell)
122            .expect("writing to a Vec cannot fail");
123        assert_eq!(
124            String::from_utf8_lossy(&out),
125            "\x1b[2;3H\x1b[0m\x1b[38;2;1;2;3m\x1b[48;5;4m\x1b[1m\x1b[4mA"
126        );
127    }
128
129    #[test]
130    fn a_plain_cell_resets_colors() {
131        let mut out = Vec::new();
132        render_cell(&mut out, Position { x: 0, y: 0 }, &Cell::default())
133            .expect("writing to a Vec cannot fail");
134        assert_eq!(
135            String::from_utf8_lossy(&out),
136            "\x1b[1;1H\x1b[0m\x1b[39m\x1b[49m "
137        );
138    }
139
140    #[test]
141    fn cursor_addressing_survives_the_u16_extremes() {
142        let mut out = Vec::new();
143        move_cursor(
144            &mut out,
145            Position {
146                x: u16::MAX,
147                y: u16::MAX,
148            },
149        )
150        .expect("writing to a Vec cannot fail");
151        assert_eq!(String::from_utf8_lossy(&out), "\x1b[65536;65536H");
152    }
153
154    #[test]
155    fn cursor_visibility_and_clears_have_one_sequence_each() {
156        let mut out = Vec::new();
157        hide_cursor(&mut out).expect("writing to a Vec cannot fail");
158        show_cursor(&mut out).expect("writing to a Vec cannot fail");
159        for (kind, sequence) in [
160            (ClearKind::All, "\x1b[2J"),
161            (ClearKind::AfterCursor, "\x1b[0J"),
162            (ClearKind::BeforeCursor, "\x1b[1J"),
163            (ClearKind::CurrentLine, "\x1b[2K"),
164            (ClearKind::UntilNewLine, "\x1b[0K"),
165        ] {
166            clear_region(&mut out, kind).expect("writing to a Vec cannot fail");
167            assert!(String::from_utf8_lossy(&out).ends_with(sequence));
168        }
169        assert_eq!(
170            String::from_utf8_lossy(&out),
171            "\x1b[?25l\x1b[?25h\x1b[2J\x1b[0J\x1b[1J\x1b[2K\x1b[0K"
172        );
173    }
174}