1use std::{
2 io::{self, Write, BufWriter},
3 time::{Duration, Instant},
4 sync::atomic::Ordering,
5};
6
7use crate::theme::{Theme, ColorMode};
8use crate::simulation::Rng;
9use crate::input::check_input;
10use crate::terminal::{get_size, EXIT_REQUESTED};
11
12#[cfg(unix)]
13use crate::terminal::RESIZE_REQUESTED;
14
15pub fn precompile_color_codes(theme: &Theme) -> [Vec<u8>; 4] {
16 [
17 format!("\x1b[{}m", theme.colors[0]).into_bytes(),
18 format!("\x1b[{}m", theme.colors[1]).into_bytes(),
19 format!("\x1b[{}m", theme.colors[2]).into_bytes(),
20 format!("\x1b[{}m", theme.colors[3]).into_bytes(),
21 ]
22}
23
24pub const fn precompile_chars() -> [[u8; 4]; 10] {
25 let mut result = [[0u8; 4]; 10];
26 let mut i = 0;
27 while i < 10 {
28 let ch = Theme::CHARS[i] as u32;
29 assert!(ch <= 127, "CHARS must contain only ASCII characters");
30 result[i][0] = ch as u8;
31 i += 1;
32 }
33 result
34}
35
36#[inline(always)]
37pub fn push_truecolor(buf: &mut Vec<u8>, r: u8, g: u8, b: u8) {
38 #[inline(always)]
39 fn push_u8(buf: &mut Vec<u8>, mut n: u8) {
40 if n >= 100 {
41 buf.push(b'0' + n / 100);
42 n %= 100;
43 buf.push(b'0' + n / 10);
44 buf.push(b'0' + n % 10);
45 } else if n >= 10 {
46 buf.push(b'0' + n / 10);
47 buf.push(b'0' + n % 10);
48 } else {
49 buf.push(b'0' + n);
50 }
51 }
52 buf.extend_from_slice(b"\x1b[38;2;");
53 push_u8(buf, r);
54 buf.push(b';');
55 push_u8(buf, g);
56 buf.push(b';');
57 push_u8(buf, b);
58 buf.push(b'm');
59}
60
61pub fn run_main_loop(
62 theme: &Theme,
63 color_mode: ColorMode,
64 fps: u32,
65 use_color: bool,
66) -> io::Result<()> {
67 use crate::simulation::simulate_step;
68 use crate::theme::hue_to_color_bytes;
69
70 let stdout = io::stdout();
71 let mut stdout = BufWriter::with_capacity(128 * 1024, stdout);
72
73 stdout.write_all(b"\x1b[?1049h\x1b[?25l\x1b[2J\x1b[H")?;
74 stdout.flush()?;
75
76 let mut current_size = get_size();
77 let (mut w, mut h) = current_size;
78 let mut size = w * h;
79 let mut buf = vec![0u8; size + w + 1];
80
81 let render_interval = Duration::from_secs_f64(1.0 / fps as f64);
82 let physics_step = 1.0 / fps as f64;
83
84 let mut screen = Vec::with_capacity((w + 1) * h * 20);
85 let mut rng = Rng::new();
86 let mut last_instant = Instant::now();
87 let mut accumulator = 0.0f64;
88
89 let color_codes = precompile_color_codes(theme);
90 let char_bytes = precompile_chars();
91 let mut rainbow_offset: f32 = 0.0;
92
93 loop {
94 let frame_start = Instant::now();
95
96 #[cfg(unix)]
97 if RESIZE_REQUESTED.load(Ordering::Relaxed) {
98 RESIZE_REQUESTED.store(false, Ordering::Relaxed);
99 let new_size = get_size();
100 if new_size != current_size {
101 current_size = new_size;
102 w = current_size.0;
103 h = current_size.1;
104 size = w * h;
105 buf = vec![0u8; size + w + 1];
106 screen = Vec::with_capacity((w + 1) * h * 20);
107 stdout.write_all(b"\x1b[2J")?;
108 stdout.flush()?;
109 }
110 }
111 #[cfg(windows)]
112 {
113 let new_size = get_size();
114 if new_size != current_size {
115 current_size = new_size;
116 w = current_size.0;
117 h = current_size.1;
118 size = w * h;
119 buf = vec![0u8; size + w + 1];
120 screen = Vec::with_capacity((w + 1) * h * 20);
121 stdout.write_all(b"\x1b[2J")?;
122 stdout.flush()?;
123 }
124 }
125
126 let now = Instant::now();
127 let dt = now.duration_since(last_instant);
128 last_instant = now;
129 accumulator += dt.as_secs_f64();
130
131 if accumulator > 0.25 {
132 accumulator = 0.25;
133 }
134
135 let mut steps = 0;
136 while accumulator >= physics_step && steps < 5 {
137 simulate_step(&mut buf, w, h, &mut rng);
138 accumulator -= physics_step;
139 steps += 1;
140 }
141 if color_mode == ColorMode::Rainbow {
142 rainbow_offset = (rainbow_offset + 0.5).rem_euclid(360.0);
143 }
144 screen.clear();
145 screen.extend_from_slice(b"\x1b[1;1H");
146
147 let mut last_color_idx: Option<usize> = None;
148 let mut last_rgb: Option<(u8, u8, u8)> = None;
149
150 for y in 0..h {
151 let row_start = y * w;
152 for i in row_start..row_start + w {
153 let heat = buf[i] as usize;
154
155 if use_color {
156 match color_mode {
157 ColorMode::Theme => {
158 let color_idx = match heat {
159 0..=4 => 0,
160 5..=9 => 1,
161 10..=15 => 2,
162 _ => 3,
163 };
164 if last_color_idx != Some(color_idx) {
165 screen.extend_from_slice(&color_codes[color_idx]);
166 last_color_idx = Some(color_idx);
167 last_rgb = None;
168 }
169 }
170 ColorMode::Rainbow => {
171 if heat > 0 {
172 let x = i % w;
173 let hue = (rainbow_offset + x as f32 * 360.0 / w as f32)
174 .rem_euclid(360.0);
175 let [r, g, b] = hue_to_color_bytes(hue, heat);
176 if last_rgb != Some((r, g, b)) {
177 push_truecolor(&mut screen, r, g, b);
178 last_rgb = Some((r, g, b));
179 last_color_idx = None;
180 }
181 } else {
182 last_rgb = None;
183 }
184 }
185 }
186 }
187
188 let ch_idx = heat.min(9);
189 screen.push(char_bytes[ch_idx][0]);
190 }
191
192 if use_color {
193 screen.extend_from_slice(b"\x1b[0m");
194 last_color_idx = None;
195 last_rgb = None;
196 }
197
198 screen.extend_from_slice(b"\x1b[0K");
199 if y < h - 1 {
200 screen.extend_from_slice(b"\r\n");
201 }
202 }
203
204 stdout.write_all(&screen)?;
205 stdout.flush()?;
206
207 if check_input() || EXIT_REQUESTED.load(Ordering::Relaxed) {
208 break;
209 }
210
211 let frame_elapsed = frame_start.elapsed();
212 if frame_elapsed < render_interval {
213 std::thread::sleep(render_interval - frame_elapsed);
214 }
215 }
216
217 Ok(())
218}