1use std::io::{self, Write};
2
3use crossterm::cursor::{Hide, MoveTo, Show};
4use crossterm::style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor};
5use crossterm::terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen};
6use crossterm::{QueueableCommand as _, queue};
7use image::GenericImageView as _;
8
9use crate::raw::{Raster, RawImage};
10
11pub const MIRROR_RASTER: Raster = match Raster::new(1, 1) {
12 Some(raster) => raster,
13 None => panic!("a pixel of one is not zero"),
14};
15
16const UPPER_HALF_BLOCK: char = '▀';
17const UNLIT: Color = Color::Rgb { r: 0, g: 0, b: 0 };
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct Cells {
21 pub columns: u32,
22 pub rows: u32,
23}
24
25impl Cells {
26 pub fn of(image: &RawImage) -> Self {
27 Self {
28 columns: image.width(),
29 rows: image.height().div_ceil(2),
30 }
31 }
32}
33
34#[derive(Debug)]
35pub struct Mirror<W: Write> {
36 out: W,
37 active: bool,
38}
39
40impl<W: Write> Mirror<W> {
41 pub fn new(out: W) -> Self {
42 Self { out, active: false }
43 }
44
45 pub fn enter(&mut self) -> io::Result<()> {
46 if self.active {
47 return Ok(());
48 }
49
50 self.active = true;
51
52 queue!(self.out, EnterAlternateScreen, Hide, Clear(ClearType::All))?;
53 self.out.flush()
54 }
55
56 pub fn leave(&mut self) -> io::Result<()> {
57 if !self.active {
58 return Ok(());
59 }
60
61 self.active = false;
62
63 queue!(self.out, ResetColor, Show, LeaveAlternateScreen)?;
64 self.out.flush()
65 }
66
67 pub fn draw(&mut self, image: &RawImage) -> io::Result<()> {
68 let cells = Cells::of(image);
69 let (columns, rows) = self::viewport();
70
71 if cells.columns > columns || cells.rows > rows {
72 return self.notice(&format!(
73 "the terminal is {columns}x{rows} cells, but this frame needs {}x{}",
74 cells.columns, cells.rows
75 ));
76 }
77
78 self::paint(&mut self.out, image, cells)?;
79 self.out.flush()
80 }
81
82 pub fn notice(&mut self, text: &str) -> io::Result<()> {
83 queue!(
84 self.out,
85 ResetColor,
86 Clear(ClearType::All),
87 MoveTo(0, 0),
88 Print(text)
89 )?;
90 self.out.flush()
91 }
92}
93
94impl<W: Write> Drop for Mirror<W> {
95 fn drop(&mut self) {
96 let _ = self.leave();
97 }
98}
99
100fn viewport() -> (u32, u32) {
101 match crossterm::terminal::size() {
102 Ok((columns, rows)) => (u32::from(columns), u32::from(rows)),
103 Err(_) => (u32::MAX, u32::MAX),
104 }
105}
106
107fn paint<W: Write>(out: &mut W, image: &RawImage, cells: Cells) -> io::Result<()> {
108 for row in 0..cells.rows {
109 let mut foreground = None;
110 let mut background = None;
111
112 out.queue(MoveTo(0, self::coordinate(row)))?;
113
114 for column in 0..cells.columns {
115 let top = self::sample(image, column, row * 2);
116 let bottom = self::sample(image, column, row * 2 + 1);
117
118 if foreground != Some(top) {
119 out.queue(SetForegroundColor(top))?;
120 foreground = Some(top);
121 }
122
123 if background != Some(bottom) {
124 out.queue(SetBackgroundColor(bottom))?;
125 background = Some(bottom);
126 }
127
128 out.queue(Print(UPPER_HALF_BLOCK))?;
129 }
130
131 queue!(out, ResetColor, Clear(ClearType::UntilNewLine))?;
132 }
133
134 queue!(
135 out,
136 MoveTo(0, self::coordinate(cells.rows)),
137 Clear(ClearType::FromCursorDown)
138 )?;
139
140 Ok(())
141}
142
143fn sample(image: &RawImage, x: u32, y: u32) -> Color {
144 if y >= image.height() {
145 return UNLIT;
146 }
147
148 let [r, g, b, _] = image.buffer().get_pixel(x, y).0;
149
150 Color::Rgb { r, g, b }
151}
152
153fn coordinate(value: u32) -> u16 {
154 u16::try_from(value).unwrap_or(u16::MAX)
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use crate::raw::PixelLayout;
161
162 fn render(image: &RawImage) -> String {
163 let mut out = Vec::new();
164 self::paint(&mut out, image, Cells::of(image)).unwrap();
165
166 String::from_utf8(out).unwrap()
167 }
168
169 #[test]
170 fn a_cell_stacks_two_pixel_rows() {
171 let image = RawImage::new(3, 4, PixelLayout::Gray8, &[0; 12]).unwrap();
172
173 assert_eq!(
174 Cells::of(&image),
175 Cells {
176 columns: 3,
177 rows: 2
178 }
179 );
180 }
181
182 #[test]
183 fn an_odd_pixel_row_still_gets_a_whole_cell() {
184 let image = RawImage::new(72, 31, PixelLayout::Gray8, &[0; 72 * 31]).unwrap();
185
186 assert_eq!(
187 Cells::of(&image),
188 Cells {
189 columns: 72,
190 rows: 16
191 }
192 );
193 }
194
195 #[test]
196 fn the_mirror_raster_spaces_a_front_frame_out_over_the_terminal_grid() {
197 let image = RawImage::new(72, 16, PixelLayout::Rgb888, &[0; 72 * 16 * 3]).unwrap();
198 let rastered = image.with_raster(MIRROR_RASTER).unwrap();
199
200 assert_eq!((rastered.width(), rastered.height()), (143, 31));
201 assert_eq!(
202 Cells::of(&rastered),
203 Cells {
204 columns: 143,
205 rows: 16
206 }
207 );
208 }
209
210 #[test]
211 fn a_column_carries_its_top_pixel_as_the_foreground_and_its_bottom_as_the_background() {
212 let image = RawImage::new(
213 1,
214 2,
215 PixelLayout::Rgb888,
216 &[0x11, 0x22, 0x33, 0x44, 0x55, 0x66],
217 )
218 .unwrap();
219
220 let rendered = render(&image);
221
222 assert!(rendered.contains("\x1b[38;2;17;34;51m"));
223 assert!(rendered.contains("\x1b[48;2;68;85;102m"));
224 assert!(rendered.contains(UPPER_HALF_BLOCK));
225 }
226
227 #[test]
228 fn a_missing_bottom_pixel_reads_as_an_unlit_one() {
229 let image = RawImage::new(1, 1, PixelLayout::Rgb888, &[0xff, 0xff, 0xff]).unwrap();
230
231 let rendered = render(&image);
232
233 assert!(rendered.contains("\x1b[38;2;255;255;255m"));
234 assert!(rendered.contains("\x1b[48;2;0;0;0m"));
235 }
236
237 #[test]
238 fn a_grayscale_pixel_repeats_over_all_three_channels() {
239 let image = RawImage::new(1, 2, PixelLayout::Gray8, &[0x40, 0x80]).unwrap();
240
241 let rendered = render(&image);
242
243 assert!(rendered.contains("\x1b[38;2;64;64;64m"));
244 assert!(rendered.contains("\x1b[48;2;128;128;128m"));
245 }
246
247 #[test]
248 fn a_run_of_equal_pixels_only_sets_its_colours_once() {
249 let image = RawImage::new(4, 2, PixelLayout::Gray8, &[0x10; 8]).unwrap();
250
251 let rendered = render(&image);
252
253 assert_eq!(rendered.matches("\x1b[38;2;16;16;16m").count(), 1);
254 assert_eq!(rendered.matches("\x1b[48;2;16;16;16m").count(), 1);
255 assert_eq!(rendered.matches(UPPER_HALF_BLOCK).count(), 4);
256 }
257
258 #[test]
259 fn every_row_is_placed_at_a_fixed_position_so_the_frame_stays_put() {
260 let image = RawImage::new(1, 6, PixelLayout::Gray8, &[0; 6]).unwrap();
261
262 let rendered = render(&image);
263
264 assert!(rendered.starts_with("\x1b[1;1H"));
265 assert!(rendered.contains("\x1b[2;1H"));
266 assert!(rendered.contains("\x1b[3;1H"));
267 assert!(rendered.ends_with("\x1b[4;1H\x1b[J"));
268 }
269
270 #[test]
271 fn a_frame_which_fits_the_viewport_is_painted() {
272 let image = RawImage::new(4, 4, PixelLayout::Gray8, &[0x10; 16]).unwrap();
273
274 let mut mirror = Mirror::new(Vec::new());
275 mirror.draw(&image).unwrap();
276
277 let rendered = String::from_utf8(std::mem::take(&mut mirror.out)).unwrap();
278
279 assert_eq!(rendered.matches(UPPER_HALF_BLOCK).count(), 8);
280 }
281
282 #[test]
283 fn a_frame_which_outgrows_the_viewport_says_how_much_room_it_needs() {
284 let image = RawImage::new(4096, 2, PixelLayout::Gray8, &[0; 8192]).unwrap();
285
286 let mut mirror = Mirror::new(Vec::new());
287 mirror.draw(&image).unwrap();
288
289 let rendered = String::from_utf8(std::mem::take(&mut mirror.out)).unwrap();
290
291 assert!(!rendered.contains(UPPER_HALF_BLOCK));
292 assert!(rendered.ends_with("but this frame needs 4096x1"));
293 }
294
295 #[test]
296 fn entering_and_leaving_restores_the_terminal_once() {
297 let mut mirror = Mirror::new(Vec::new());
298
299 mirror.enter().unwrap();
300 mirror.enter().unwrap();
301 mirror.leave().unwrap();
302 mirror.leave().unwrap();
303
304 let rendered = String::from_utf8(std::mem::take(&mut mirror.out)).unwrap();
305
306 assert_eq!(rendered.matches("\x1b[?1049h").count(), 1);
307 assert_eq!(rendered.matches("\x1b[?1049l").count(), 1);
308 assert_eq!(rendered.matches("\x1b[?25l").count(), 1);
309 assert_eq!(rendered.matches("\x1b[?25h").count(), 1);
310 }
311
312 #[test]
313 fn a_notice_replaces_whatever_was_on_screen() {
314 let mut mirror = Mirror::new(Vec::new());
315 mirror
316 .notice("the device streamed a deflate frame")
317 .unwrap();
318
319 let rendered = String::from_utf8(std::mem::take(&mut mirror.out)).unwrap();
320
321 assert!(rendered.contains("\x1b[2J"));
322 assert!(rendered.ends_with("the device streamed a deflate frame"));
323 }
324}