use std::env;
use std::io::{self, Write};
use std::process::Command;
use std::thread;
use std::time::Duration;
use aisling::{Cell, Color, Frame, Loader, LoaderConfig, LoaderKind, LoaderProgress};
const NEW_LOADERS: &[LoaderKind] = &[
LoaderKind::Ember,
LoaderKind::Cipher,
LoaderKind::Glitch,
LoaderKind::Vortex,
LoaderKind::StormFlicker,
LoaderKind::LaserSweep,
LoaderKind::Cascade,
LoaderKind::GridPulse,
];
#[derive(Clone, Copy, Debug)]
struct Args {
seconds: u64,
fps: u64,
width_override: Option<usize>,
height_override: Option<usize>,
}
fn main() {
let args = Args::parse();
let (terminal_width, terminal_height) = terminal_size();
let width = args.width_override.unwrap_or(terminal_width.max(80));
let height = args
.height_override
.unwrap_or(terminal_height.max(24))
.max(24);
let fps = args.fps.max(1);
let total_ticks = (args.seconds.max(1) * fps) as usize;
let per_loader_ticks = (total_ticks / NEW_LOADERS.len().max(1)).max(1);
let mut tick = 0usize;
let mut stdout = io::stdout();
let _guard = TerminalGuard::enter(&mut stdout);
loop {
let local_cycle = tick / per_loader_ticks;
let current = local_cycle % NEW_LOADERS.len();
let local_tick = tick % per_loader_ticks;
let progress =
LoaderProgress::from_counts(local_tick as u64, per_loader_ticks.max(1) as u64);
let kind = NEW_LOADERS[current];
let mut screen = Frame::with_background(width, height, Some(Color::rgb(4, 7, 11)));
draw_header(
&mut screen,
width,
fps,
current,
kind,
args.seconds,
local_cycle + 1,
);
let inner_w = width.saturating_sub(4).max(20);
let inner_h = height.saturating_sub(8).max(14);
let config = LoaderConfig::default()
.with_size(inner_w, inner_h)
.with_duration(per_loader_ticks.max(1))
.with_label(format!(
"{} ({}/{})",
kind.name(),
current + 1,
NEW_LOADERS.len()
));
let loader = Loader::with_config(kind, config);
let frame = loader.frame(local_tick, progress);
overlay_frame(&mut screen, 2, 4, &frame);
write!(stdout, "\x1b[H{}", screen.to_ansi_string()).unwrap();
stdout.flush().unwrap();
tick += 1;
if tick >= total_ticks {
break;
}
thread::sleep(Duration::from_millis(1000 / fps));
}
}
fn draw_header(
screen: &mut Frame,
width: usize,
fps: u64,
current: usize,
kind: LoaderKind,
seconds: u64,
cycle: usize,
) {
put_text(
screen,
1,
0,
"Aisling new loader showcase",
Color::rgb(255, 178, 76),
true,
);
let status = format!(
"pulse/effect variants | {}s | {}fps | variant {}/{} | {}",
seconds,
fps,
current + 1,
NEW_LOADERS.len(),
kind.name(),
);
put_text(screen, 1, 1, &status, Color::rgb(126, 186, 255), false);
let timeline = NEW_LOADERS
.iter()
.enumerate()
.map(|(i, kind)| {
if i == current {
format!("[{kind}] ")
} else {
format!(" {kind} ")
}
})
.collect::<String>();
let mut timeline = timeline;
if timeline.len() > width.saturating_sub(2) {
timeline.truncate(width.saturating_sub(2));
}
put_text(
screen,
1,
2,
&format!("cycle #{:02} • {}", cycle, timeline),
Color::rgb(130, 146, 160),
false,
);
if width > 20 {
let key = "Press Ctrl+C to stop, or let it run for the full duration.";
let mut help = key.to_string();
if help.len() > width.saturating_sub(2) {
help.truncate(width.saturating_sub(2));
}
put_text(
screen,
1,
height_hint(screen),
&help,
Color::rgb(86, 102, 118),
false,
);
}
}
fn overlay_frame(screen: &mut Frame, x0: usize, y0: usize, src: &Frame) {
for y in 0..src.height() {
for x in 0..src.width() {
if let Some(cell) = src.cell(x, y) {
if cell.ch == ' ' && !cell.has_style() {
continue;
}
if x + x0 < screen.width() && y + y0 < screen.height() {
screen.set(x + x0, y + y0, cell.clone());
}
}
}
}
}
fn put_text(screen: &mut Frame, x: usize, y: usize, text: &str, color: Color, bold: bool) {
if y >= screen.height() {
return;
}
for (offset, ch) in text.chars().enumerate() {
let px = x + offset;
if px >= screen.width() {
break;
}
screen.set(px, y, colored_cell(ch, color, None, bold, false));
}
}
fn height_hint(screen: &Frame) -> usize {
screen.height().saturating_sub(1).max(3)
}
fn colored_cell(ch: char, fg: Color, bg: Option<Color>, bold: bool, dim: bool) -> Cell {
let mut cell = Cell::new(ch);
cell.colors.fg = Some(fg);
cell.colors.bg = bg;
cell.bold = bold;
cell.dim = dim;
cell
}
impl Args {
fn parse() -> Self {
let mut args = env::args().skip(1);
let mut parsed = Self {
seconds: 24,
fps: 10,
width_override: None,
height_override: None,
};
while let Some(arg) = args.next() {
match arg.as_str() {
"--seconds" => {
parsed.seconds = args
.next()
.and_then(|v| v.parse().ok())
.unwrap_or(parsed.seconds);
}
"--fps" => {
parsed.fps = args
.next()
.and_then(|v| v.parse().ok())
.unwrap_or(parsed.fps);
}
"--width" => {
parsed.width_override = args.next().and_then(|v| v.parse().ok());
}
"--height" => {
parsed.height_override = args.next().and_then(|v| v.parse().ok());
}
"--help" | "-h" | "help" => {
usage();
std::process::exit(0);
}
_ => {}
}
}
parsed
}
}
fn usage() {
println!("Usage:");
println!(" cargo run --example new_loaders -- [options]");
println!("Options:");
println!(" --seconds <n> Total seconds to run (default: 24)");
println!(" --fps <n> Frame rate (default: 10)");
println!(" --width <n> Terminal width to target");
println!(" --height <n> Terminal height to target");
println!(" --help Show this help text");
}
fn terminal_size() -> (usize, usize) {
let env_size = env::var("COLUMNS")
.ok()
.and_then(|columns| columns.parse::<usize>().ok())
.zip(
env::var("LINES")
.ok()
.and_then(|lines| lines.parse::<usize>().ok()),
);
if let Some((columns, lines)) = env_size {
return (columns, lines);
}
let output = Command::new("stty").arg("size").output();
if let Ok(output) = output {
if output.status.success() {
if let Ok(text) = String::from_utf8(output.stdout) {
let mut parts = text.split_whitespace();
if let (Some(lines), Some(columns)) = (parts.next(), parts.next()) {
if let (Ok(lines), Ok(columns)) =
(lines.parse::<usize>(), columns.parse::<usize>())
{
return (columns, lines);
}
}
}
}
}
(120, 40)
}
struct TerminalGuard;
impl TerminalGuard {
fn enter(stdout: &mut io::Stdout) -> Self {
write!(stdout, "\x1b[?25l\x1b[2J\x1b[H").unwrap();
stdout.flush().unwrap();
Self
}
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
let mut stdout = io::stdout();
write!(stdout, "\x1b[?25h\n").unwrap();
stdout.flush().unwrap();
}
}