Skip to main content

busybar_render/
terminal.rs

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) = 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        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, coordinate(row)))?;
113
114        for column in 0..cells.columns {
115            let top = sample(image, column, row * 2);
116            let bottom = 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, 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        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}