Skip to main content

new_loaders/
new_loaders.rs

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}