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
18pub struct Sparkles<'stream> {
23 rng: SmallRng,
24 stdout: StdoutLock<'stream>,
25}
26
27impl<'stream> Sparkles<'stream> {
28 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 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 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
107pub 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 SPACE
169 } else if self.curr_index % self.terminal_size.0 < self.left_margin_length {
170 self.in_right_side = false;
172 SPACE
173 } else if self.in_right_side {
174 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 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 #[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}