1use crate::Sink;
7use std::fmt::Write as _;
8
9use katui_scene::cell::{Attrs, Cell, Color, Position};
10
11const 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ClearKind {
27 All,
29 AfterCursor,
31 BeforeCursor,
33 CurrentLine,
35 UntilNewLine,
37}
38
39pub 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
56pub 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
72pub 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}