prettier_print/
sparkles.rs

1use crate::game_of_life::{Board, Cell};
2use crate::prettier_printer::{PrettierPrinter, Seed};
3use crossterm::cursor;
4use crossterm::cursor::{MoveTo, MoveToNextLine};
5use crossterm::event::poll;
6use crossterm::style::{Color, Colors, Print, SetBackgroundColor, SetColors};
7use crossterm::terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType};
8use crossterm::{queue, terminal};
9use rand::rngs::SmallRng;
10use rand::SeedableRng;
11use std::fmt::Debug;
12use std::io::{StdoutLock, Write};
13use std::iter::once;
14use std::str::Chars;
15use std::thread::sleep;
16use std::time::Duration;
17
18/// Prints the debug string, and runs game of life on top of the printed string. The output covers
19/// the full terminal screen.
20///
21/// The frame rate is very slow on Windows and I don't know why.
22pub struct Sparkles<'stream> {
23    rng: SmallRng,
24    stdout: StdoutLock<'stream>,
25}
26
27impl<'stream> Sparkles<'stream> {
28    /// Initializes with random seed.
29    pub fn new(stdout: StdoutLock<'stream>) -> Self {
30        Self {
31            rng: SmallRng::from_entropy(),
32            stdout,
33        }
34    }
35
36    pub fn new_with_seed(seed: Seed, stdout: StdoutLock<'stream>) -> Self {
37        Self {
38            rng: SmallRng::from_seed(seed),
39            stdout,
40        }
41    }
42
43    /// Runs the output screen. Press any key to stop.
44    pub fn run<T>(&mut self, what: &T) -> std::io::Result<()>
45    where
46        T: Debug,
47    {
48        enable_raw_mode().unwrap();
49        queue!(
50            self.stdout,
51            Clear(ClearType::All),
52            MoveTo(0, 0),
53            SetColors(Colors::new(Color::Reset, Color::Reset)),
54            cursor::Hide,
55        )?;
56
57        let terminal_size = terminal::size().unwrap();
58
59        let debug_str = format!("{:#?}", what);
60
61        let mut board = Board::new(PrettierPrinter::gen_seed(&mut self.rng), terminal_size);
62        while !poll(Duration::from_secs(0))? {
63            queue!(self.stdout, MoveTo(0, 0))?;
64
65            let mut debug_str = CenteredDebugString::new(
66                &debug_str,
67                (terminal_size.0 as usize, terminal_size.1 as usize),
68            );
69
70            for (i, cell) in board.cell_array().iter().enumerate() {
71                let color = match cell {
72                    Cell::Dead => Color::Reset,
73                    Cell::Live => Color::White,
74                };
75                queue!(
76                    self.stdout,
77                    SetBackgroundColor(color),
78                    Print(debug_str.next().unwrap())
79                )?;
80
81                // Line break
82                if i % terminal_size.0 as usize == terminal_size.0 as usize - 1 {
83                    queue!(
84                        self.stdout,
85                        SetBackgroundColor(Color::Reset),
86                        MoveToNextLine(1),
87                    )?;
88                }
89                self.stdout.flush()?;
90            }
91
92            board.tick();
93
94            sleep(Duration::from_millis(50));
95        }
96
97        disable_raw_mode().unwrap();
98        queue!(
99            self.stdout,
100            SetColors(Colors::new(Color::Reset, Color::Reset)),
101            cursor::Show,
102        )?;
103        self.stdout.flush()
104    }
105}
106
107/// Turns the debug string into a grid of chars.  
108pub struct CenteredDebugString<'chars> {
109    char_iter: Chars<'chars>,
110    top_margin_length: usize,
111    left_margin_length: usize,
112    terminal_size: (usize, usize),
113    curr_index: usize,
114    in_right_side: bool,
115}
116
117impl<'chars> CenteredDebugString<'chars> {
118    pub fn new(s: &'chars str, terminal_size: (usize, usize)) -> Self {
119        Self {
120            char_iter: s.chars(),
121            top_margin_length: CenteredDebugString::margin_length(
122                terminal_size.1,
123                s.chars().filter(|&c| c == '\n').count() + 1,
124            ),
125            left_margin_length: CenteredDebugString::margin_length(
126                terminal_size.0,
127                CenteredDebugString::longest_line(s),
128            ),
129            curr_index: 0,
130            terminal_size,
131            in_right_side: false,
132        }
133    }
134
135    fn longest_line(s: &str) -> usize {
136        let mut max = 0_usize;
137        let mut curr_line_length = 0_usize;
138        for c in s.chars().chain(once('\n')) {
139            if c == '\n' {
140                if curr_line_length > max {
141                    max = curr_line_length;
142                }
143                curr_line_length = 0;
144            } else {
145                curr_line_length += 1;
146            }
147        }
148        max
149    }
150
151    fn margin_length(max_length: usize, content_length: usize) -> usize {
152        (max_length.saturating_sub(content_length)) / 2
153    }
154
155    pub fn len(&self) -> usize {
156        self.terminal_size.0 * self.terminal_size.1
157    }
158}
159
160impl Iterator for CenteredDebugString<'_> {
161    type Item = char;
162
163    fn next(&mut self) -> Option<Self::Item> {
164        const SPACE: char = ' ';
165
166        let result = if self.curr_index / self.terminal_size.0 < self.top_margin_length {
167            // Top margin
168            SPACE
169        } else if self.curr_index % self.terminal_size.0 < self.left_margin_length {
170            // Left margin
171            self.in_right_side = false;
172            SPACE
173        } else if self.in_right_side {
174            // Right spacing
175            SPACE
176        } else if let Some(c) = self.char_iter.next() {
177            if c == '\n' {
178                self.in_right_side = true;
179                SPACE
180            } else {
181                c
182            }
183        } else {
184            // Bottom spacing
185            SPACE
186        };
187        self.curr_index += 1;
188        Some(result)
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use rstest::rstest;
196    use std::collections::HashMap;
197    use std::io::stdout;
198
199    // #[test]
200    #[allow(dead_code)]
201    fn run_sparkles() {
202        #[derive(Debug)]
203        struct Type {
204            a: String,
205            b: Vec<i32>,
206            c: HashMap<&'static str, &'static str>,
207        }
208
209        let input = Type {
210            a: "a".to_string(),
211            b: vec![0, 1],
212            c: {
213                let mut map = HashMap::new();
214                map.insert("So", "pretty");
215                map
216            },
217        };
218
219        let stdout = stdout();
220        Sparkles::new(stdout.lock()).run(&input).unwrap();
221    }
222
223    #[rstest]
224    #[case("", (0, 0), &[])]
225    #[case("a", (0, 0), &[])]
226    #[case("a", (1, 1), &['a'])]
227    #[case("a", (2, 3), &[' ', ' ', 'a', ' ', ' ', ' '])]
228    #[case("a", (3, 2), &[' ', 'a', ' ', ' ', ' ', ' '])]
229    #[case("a", (3, 3), &[' ', ' ', ' ', ' ', 'a', ' ', ' ', ' ', ' '])]
230    #[case("a\nb", (4, 3), &[' ', 'a', ' ', ' ', ' ', 'b', ' ', ' ', ' ', ' ', ' ', ' '])]
231    fn debug_string_grid(
232        #[case] s: &str,
233        #[case] terminal_size: (usize, usize),
234        #[case] expected: &[char],
235    ) {
236        let mut debug_string_grid = CenteredDebugString::new(s, terminal_size);
237        let result: Vec<char> = (0..debug_string_grid.len())
238            .map(|_| debug_string_grid.next().unwrap())
239            .collect();
240        assert_eq!(result, expected);
241    }
242
243    #[test]
244    fn longest_line() {
245        assert_eq!(CenteredDebugString::longest_line(""), 0);
246        assert_eq!(CenteredDebugString::longest_line("\n"), 0);
247        assert_eq!(CenteredDebugString::longest_line("1\n"), 1);
248        assert_eq!(CenteredDebugString::longest_line("\n1"), 1);
249    }
250}