1use std::env;
2use std::io::{self, Write};
3use std::process::Command;
4use std::thread;
5use std::time::Duration;
6
7use aisling::{Cell, Color, Frame, Loader, LoaderConfig, LoaderKind, LoaderProgress};
8
9const NEW_LOADERS: &[LoaderKind] = &[
10 LoaderKind::Ember,
11 LoaderKind::Cipher,
12 LoaderKind::Glitch,
13 LoaderKind::Vortex,
14 LoaderKind::StormFlicker,
15 LoaderKind::LaserSweep,
16 LoaderKind::Cascade,
17 LoaderKind::GridPulse,
18];
19
20#[derive(Clone, Copy, Debug)]
21struct Args {
22 seconds: u64,
23 fps: u64,
24 width_override: Option<usize>,
25 height_override: Option<usize>,
26}
27
28fn main() {
29 let args = Args::parse();
30 let (terminal_width, terminal_height) = terminal_size();
31 let width = args.width_override.unwrap_or(terminal_width.max(80));
32 let height = args
33 .height_override
34 .unwrap_or(terminal_height.max(24))
35 .max(24);
36
37 let fps = args.fps.max(1);
38 let total_ticks = (args.seconds.max(1) * fps) as usize;
39 let per_loader_ticks = (total_ticks / NEW_LOADERS.len().max(1)).max(1);
40 let mut tick = 0usize;
41
42 let mut stdout = io::stdout();
43 let _guard = TerminalGuard::enter(&mut stdout);
44
45 loop {
46 let local_cycle = tick / per_loader_ticks;
47 let current = local_cycle % NEW_LOADERS.len();
48 let local_tick = tick % per_loader_ticks;
49 let progress =
50 LoaderProgress::from_counts(local_tick as u64, per_loader_ticks.max(1) as u64);
51
52 let kind = NEW_LOADERS[current];
53 let mut screen = Frame::with_background(width, height, Some(Color::rgb(4, 7, 11)));
54
55 draw_header(
56 &mut screen,
57 width,
58 fps,
59 current,
60 kind,
61 args.seconds,
62 local_cycle + 1,
63 );
64 let inner_w = width.saturating_sub(4).max(20);
65 let inner_h = height.saturating_sub(8).max(14);
66
67 let config = LoaderConfig::default()
68 .with_size(inner_w, inner_h)
69 .with_duration(per_loader_ticks.max(1))
70 .with_label(format!(
71 "{} ({}/{})",
72 kind.name(),
73 current + 1,
74 NEW_LOADERS.len()
75 ));
76 let loader = Loader::with_config(kind, config);
77 let frame = loader.frame(local_tick, progress);
78 overlay_frame(&mut screen, 2, 4, &frame);
79
80 write!(stdout, "\x1b[H{}", screen.to_ansi_string()).unwrap();
81 stdout.flush().unwrap();
82
83 tick += 1;
84 if tick >= total_ticks {
85 break;
86 }
87 thread::sleep(Duration::from_millis(1000 / fps));
88 }
89}
90
91fn draw_header(
92 screen: &mut Frame,
93 width: usize,
94 fps: u64,
95 current: usize,
96 kind: LoaderKind,
97 seconds: u64,
98 cycle: usize,
99) {
100 put_text(
101 screen,
102 1,
103 0,
104 "Aisling new loader showcase",
105 Color::rgb(255, 178, 76),
106 true,
107 );
108
109 let status = format!(
110 "pulse/effect variants | {}s | {}fps | variant {}/{} | {}",
111 seconds,
112 fps,
113 current + 1,
114 NEW_LOADERS.len(),
115 kind.name(),
116 );
117 put_text(screen, 1, 1, &status, Color::rgb(126, 186, 255), false);
118
119 let timeline = NEW_LOADERS
120 .iter()
121 .enumerate()
122 .map(|(i, kind)| {
123 if i == current {
124 format!("[{kind}] ")
125 } else {
126 format!(" {kind} ")
127 }
128 })
129 .collect::<String>();
130 let mut timeline = timeline;
131 if timeline.len() > width.saturating_sub(2) {
132 timeline.truncate(width.saturating_sub(2));
133 }
134 put_text(
135 screen,
136 1,
137 2,
138 &format!("cycle #{:02} • {}", cycle, timeline),
139 Color::rgb(130, 146, 160),
140 false,
141 );
142
143 if width > 20 {
144 let key = "Press Ctrl+C to stop, or let it run for the full duration.";
145 let mut help = key.to_string();
146 if help.len() > width.saturating_sub(2) {
147 help.truncate(width.saturating_sub(2));
148 }
149 put_text(
150 screen,
151 1,
152 height_hint(screen),
153 &help,
154 Color::rgb(86, 102, 118),
155 false,
156 );
157 }
158}
159
160fn overlay_frame(screen: &mut Frame, x0: usize, y0: usize, src: &Frame) {
161 for y in 0..src.height() {
162 for x in 0..src.width() {
163 if let Some(cell) = src.cell(x, y) {
164 if cell.ch == ' ' && !cell.has_style() {
165 continue;
166 }
167 if x + x0 < screen.width() && y + y0 < screen.height() {
168 screen.set(x + x0, y + y0, cell.clone());
169 }
170 }
171 }
172 }
173}
174
175fn put_text(screen: &mut Frame, x: usize, y: usize, text: &str, color: Color, bold: bool) {
176 if y >= screen.height() {
177 return;
178 }
179 for (offset, ch) in text.chars().enumerate() {
180 let px = x + offset;
181 if px >= screen.width() {
182 break;
183 }
184 screen.set(px, y, colored_cell(ch, color, None, bold, false));
185 }
186}
187
188fn height_hint(screen: &Frame) -> usize {
189 screen.height().saturating_sub(1).max(3)
190}
191
192fn colored_cell(ch: char, fg: Color, bg: Option<Color>, bold: bool, dim: bool) -> Cell {
193 let mut cell = Cell::new(ch);
194 cell.colors.fg = Some(fg);
195 cell.colors.bg = bg;
196 cell.bold = bold;
197 cell.dim = dim;
198 cell
199}
200
201impl Args {
202 fn parse() -> Self {
203 let mut args = env::args().skip(1);
204 let mut parsed = Self {
205 seconds: 24,
206 fps: 10,
207 width_override: None,
208 height_override: None,
209 };
210
211 while let Some(arg) = args.next() {
212 match arg.as_str() {
213 "--seconds" => {
214 parsed.seconds = args
215 .next()
216 .and_then(|v| v.parse().ok())
217 .unwrap_or(parsed.seconds);
218 }
219 "--fps" => {
220 parsed.fps = args
221 .next()
222 .and_then(|v| v.parse().ok())
223 .unwrap_or(parsed.fps);
224 }
225 "--width" => {
226 parsed.width_override = args.next().and_then(|v| v.parse().ok());
227 }
228 "--height" => {
229 parsed.height_override = args.next().and_then(|v| v.parse().ok());
230 }
231 "--help" | "-h" | "help" => {
232 usage();
233 std::process::exit(0);
234 }
235 _ => {}
236 }
237 }
238
239 parsed
240 }
241}
242
243fn usage() {
244 println!("Usage:");
245 println!(" cargo run --example new_loaders -- [options]");
246 println!("Options:");
247 println!(" --seconds <n> Total seconds to run (default: 24)");
248 println!(" --fps <n> Frame rate (default: 10)");
249 println!(" --width <n> Terminal width to target");
250 println!(" --height <n> Terminal height to target");
251 println!(" --help Show this help text");
252}
253
254fn terminal_size() -> (usize, usize) {
255 let env_size = env::var("COLUMNS")
256 .ok()
257 .and_then(|columns| columns.parse::<usize>().ok())
258 .zip(
259 env::var("LINES")
260 .ok()
261 .and_then(|lines| lines.parse::<usize>().ok()),
262 );
263
264 if let Some((columns, lines)) = env_size {
265 return (columns, lines);
266 }
267
268 let output = Command::new("stty").arg("size").output();
269 if let Ok(output) = output {
270 if output.status.success() {
271 if let Ok(text) = String::from_utf8(output.stdout) {
272 let mut parts = text.split_whitespace();
273 if let (Some(lines), Some(columns)) = (parts.next(), parts.next()) {
274 if let (Ok(lines), Ok(columns)) =
275 (lines.parse::<usize>(), columns.parse::<usize>())
276 {
277 return (columns, lines);
278 }
279 }
280 }
281 }
282 }
283
284 (120, 40)
285}
286
287struct TerminalGuard;
288
289impl TerminalGuard {
290 fn enter(stdout: &mut io::Stdout) -> Self {
291 write!(stdout, "\x1b[?25l\x1b[2J\x1b[H").unwrap();
292 stdout.flush().unwrap();
293 Self
294 }
295}
296
297impl Drop for TerminalGuard {
298 fn drop(&mut self) {
299 let mut stdout = io::stdout();
300 write!(stdout, "\x1b[?25h\n").unwrap();
301 stdout.flush().unwrap();
302 }
303}