use std::env;
use std::io::{self, Write};
use std::process::Command;
use std::str::FromStr;
use std::thread;
use std::time::Duration;
use aisling::{Cell, Color, Frame, Loader, LoaderConfig, LoaderKind, LoaderProgress};
fn main() {
let args = Args::parse();
if args.list {
for kind in LoaderKind::all() {
println!("{}", kind.name());
}
return;
}
let frame_delay = Duration::from_millis((1000 / args.fps.max(1)) as u64);
let total_ticks = (args.seconds.unwrap_or(20) * args.fps.max(1)) as usize;
let progress_cycle = total_ticks.max(1);
let terminal = terminal_size();
let mut stdout = io::stdout();
let _guard = TerminalGuard::enter(&mut stdout);
let mut tick = 0usize;
loop {
write!(stdout, "\x1b[H").unwrap();
if args.all {
render_all(&mut stdout, &args, tick, progress_cycle, terminal).unwrap();
} else {
let progress =
LoaderProgress::from_counts((tick % progress_cycle) as u64, progress_cycle as u64);
let loader = Loader::with_config(args.kind, args.config_for(args.kind));
write!(stdout, "{}", loader.frame(tick, progress).to_ansi_string()).unwrap();
}
stdout.flush().unwrap();
tick += 1;
if args.once || args.seconds.is_some() && tick >= total_ticks {
break;
}
thread::sleep(frame_delay);
}
}
#[derive(Clone, Debug)]
struct Args {
kind: LoaderKind,
all: bool,
list: bool,
label: String,
unit: String,
width: usize,
height: usize,
seconds: Option<u64>,
fps: u64,
once: bool,
no_color: bool,
}
impl Args {
fn parse() -> Self {
let mut args = env::args().skip(1);
let mut parsed = Self {
kind: LoaderKind::Tqdm,
all: false,
list: false,
label: "download".to_string(),
unit: "B".to_string(),
width: 64,
height: 1,
seconds: Some(20),
fps: 10,
once: false,
no_color: false,
};
while let Some(arg) = args.next() {
match arg.as_str() {
"all" => parsed.all = true,
"list" => parsed.list = true,
"-h" | "--help" | "help" => {
usage();
std::process::exit(0);
}
"--label" => {
if let Some(value) = args.next() {
parsed.label = value;
}
}
"--unit" => {
if let Some(value) = args.next() {
parsed.unit = value;
}
}
"--width" => {
parsed.width = args
.next()
.and_then(|value| value.parse().ok())
.unwrap_or(parsed.width)
}
"--height" => {
parsed.height = args
.next()
.and_then(|value| value.parse().ok())
.unwrap_or(parsed.height)
}
"--seconds" => parsed.seconds = args.next().and_then(|value| value.parse().ok()),
"--fps" => {
parsed.fps = args
.next()
.and_then(|value| value.parse().ok())
.unwrap_or(parsed.fps)
}
"--forever" => parsed.seconds = None,
"--once" => parsed.once = true,
"--no-color" => parsed.no_color = true,
value => {
if let Ok(kind) = LoaderKind::from_str(value) {
parsed.kind = kind;
}
}
}
}
parsed.width = parsed.width.max(12);
parsed.height = parsed.height.max(1);
parsed
}
fn config_for(&self, kind: LoaderKind) -> LoaderConfig {
let height = if kind.is_effect_style() {
self.height.max(5)
} else {
self.height
};
let config = LoaderConfig::default()
.with_size(self.width, height)
.with_duration((self.seconds.unwrap_or(20) * self.fps.max(1)) as usize)
.with_label(self.label.clone())
.with_unit(self.unit.clone())
.with_fraction(matches!(
kind,
LoaderKind::Tqdm | LoaderKind::Download | LoaderKind::Upload
));
if self.no_color {
config.without_color()
} else {
config
}
}
}
fn render_all(
stdout: &mut io::Stdout,
args: &Args,
tick: usize,
total_ticks: usize,
terminal: (usize, usize),
) -> io::Result<()> {
let (screen_width, screen_height) = terminal;
let layout = GridLayout::new(
screen_width.max(96),
screen_height.max(44),
LoaderKind::all().len(),
);
let mut screen =
Frame::with_background(layout.width, layout.height, Some(Color::rgb(4, 7, 11)));
put_text(
&mut screen,
1,
0,
"Aisling loader effects",
Color::rgb(255, 178, 76),
true,
);
put_text(
&mut screen,
1,
1,
"all loaders at once | slower grid playback | pass --fps N or --seconds N to tune",
Color::rgb(126, 186, 255),
false,
);
for (index, kind) in LoaderKind::all().iter().enumerate() {
let progress_tick = (tick + index * 5) % total_ticks.max(1);
let progress = LoaderProgress::from_counts(progress_tick as u64, total_ticks.max(1) as u64);
let x = (index % layout.columns) * layout.card_width;
let y = layout.header_height + (index / layout.columns) * layout.card_height;
let card_width = layout.card_width.min(layout.width.saturating_sub(x));
let card_height = layout.card_height.min(layout.height.saturating_sub(y));
let inner_width = card_width.saturating_sub(2).max(1);
let inner_height = card_height.saturating_sub(1).max(1).min(args.height.max(1));
let config = args
.config_for(*kind)
.with_label(kind.name())
.with_size(inner_width, inner_height);
let loader = Loader::with_config(*kind, config);
let frame = loader.frame(tick + index, progress);
draw_card(
&mut screen,
&layout,
x,
y,
card_width,
card_height,
*kind,
&frame,
);
}
write!(stdout, "{}", screen.to_ansi_string())?;
Ok(())
}
#[derive(Clone, Copy, Debug)]
struct GridLayout {
width: usize,
height: usize,
columns: usize,
card_width: usize,
card_height: usize,
header_height: usize,
}
impl GridLayout {
fn new(width: usize, height: usize, count: usize) -> Self {
let header_height = 2;
let usable_height = height.saturating_sub(header_height).max(1);
let max_columns = (width / 12).clamp(1, 8).min(count.max(1));
let mut best = None;
for columns in 1..=max_columns {
let rows = count.div_ceil(columns).max(1);
let card_height = usable_height / rows;
let card_width = width / columns;
let score = if card_height >= 3 {
card_height * 100 + card_width
} else {
columns * 10 + card_height
};
if best.map_or(true, |(_, best_score)| score > best_score) {
best = Some((columns, score));
}
}
let columns = best.map(|(columns, _)| columns).unwrap_or(1);
let rows = count.div_ceil(columns).max(1);
Self {
width,
height,
columns,
card_width: (width / columns).max(1),
card_height: (usable_height / rows).max(1),
header_height,
}
}
}
fn draw_card(
screen: &mut Frame,
layout: &GridLayout,
x: usize,
y: usize,
width: usize,
height: usize,
kind: LoaderKind,
frame: &Frame,
) {
if width == 0 || height == 0 {
return;
}
let pane_bg = Color::rgb(9, 14, 22);
let border = Color::rgb(48, 72, 96);
let title = loader_title_color(kind);
for py in 0..height {
for px in 0..width {
screen.set(
x + px,
y + py,
colored_cell(' ', Color::rgb(40, 48, 56), Some(pane_bg), false, false),
);
}
}
if height >= 3 && width >= 8 {
for px in 0..width {
screen.set(
x + px,
y,
colored_cell('-', border, Some(pane_bg), false, true),
);
}
screen.set(x, y, colored_cell('+', border, Some(pane_bg), false, false));
screen.set(
x + width - 1,
y,
colored_cell('+', border, Some(pane_bg), false, false),
);
put_text(screen, x + 2, y, kind.name(), title, true);
} else {
put_text(screen, x, y, kind.name(), title, true);
}
if height == 1 {
return;
}
let inner_y = y + 1;
let inner_x = if width > 2 { x + 1 } else { x };
let inner_width = width.saturating_sub(2).max(1);
let inner_height = height.saturating_sub(1).max(1);
for fy in 0..frame.height().min(inner_height) {
for fx in 0..frame.width().min(inner_width) {
if let Some(cell) = frame.cell(fx, fy) {
if cell.ch != ' ' || cell.has_style() {
screen.set(inner_x + fx, inner_y + fy, cell.clone());
}
}
}
}
if layout.card_height <= 2 && width > 8 {
let percent_x = x + width.saturating_sub(5);
put_text(
screen,
percent_x,
y,
" ",
Color::rgb(120, 132, 144),
false,
);
}
}
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 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
}
fn loader_title_color(kind: LoaderKind) -> Color {
match kind {
LoaderKind::Burn
| LoaderKind::BouncyBalls
| LoaderKind::Crumble
| LoaderKind::Fireworks
| LoaderKind::Spray
| LoaderKind::Thunderstorm
| LoaderKind::Unstable => Color::rgb(255, 142, 74),
LoaderKind::BinaryPath
| LoaderKind::ColorShift
| LoaderKind::Decrypt
| LoaderKind::ErrorCorrect
| LoaderKind::Matrix
| LoaderKind::RandomSequence
| LoaderKind::SynthGrid
| LoaderKind::Wipe => Color::rgb(80, 255, 170),
LoaderKind::Beams
| LoaderKind::Bubbles
| LoaderKind::LaserEtch
| LoaderKind::Overflow
| LoaderKind::Pour
| LoaderKind::Rain
| LoaderKind::Slide
| LoaderKind::VhsTape
| LoaderKind::Waves => Color::rgb(104, 216, 255),
LoaderKind::Blackhole
| LoaderKind::Expand
| LoaderKind::MiddleOut
| LoaderKind::OrbittingVolley
| LoaderKind::Rings
| LoaderKind::Scattered
| LoaderKind::Slice
| LoaderKind::Smoke
| LoaderKind::Spotlights
| LoaderKind::Swarm
| LoaderKind::Sweep => Color::rgb(178, 154, 255),
LoaderKind::Highlight | LoaderKind::Print => Color::rgb(255, 232, 128),
_ => Color::rgb(226, 236, 244),
}
}
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);
}
}
}
}
}
(132, 44)
}
fn usage() {
println!("Usage:");
println!(" cargo run --example loaders -- [loader|all|list] [options]");
println!();
println!("Examples:");
println!(" cargo run --example loaders -- tqdm --label download");
println!(" cargo run --example loaders -- all --seconds 20");
println!(" cargo run --example loaders -- list");
}
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();
}
}