use aether_renderer_core::config::PartialConfig;
use aether_renderer_core::{is_stdin_input, RenderConfig, RenderProgress, RenderReport};
use clap::{CommandFactory, Parser};
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(name = "aether-renderer-core", version, about, long_about = None)]
#[command(after_help = "Examples:\n \
aether-renderer-core -i frames/ -o out.webm\n \
aether-renderer-core -i frames.zip -o out.gif --fps 24\n \
aether-renderer-core -i frames/ -o out.mov (ProRes 4444 with alpha, for video editors)\n \
aether-renderer-core -i frames/ -o out.mp4 --background '#1e1e1e'\n \
aether-renderer-core -i frames/ -o out.webm --progress json (one JSON line per update, for other programs)\n \
aether-renderer-capture -i anim.html --stdout | aether-renderer-core -i - -o out.webm --frames 60\n \
aether-renderer-core --config render.toml --fps 60 -o draft.webm")]
struct Args {
#[arg(short, long)]
config: Option<PathBuf>,
#[arg(short, long)]
input: Option<PathBuf>,
#[arg(long, value_name = "N")]
frames: Option<u32>,
#[arg(short, long)]
output: Option<String>,
#[arg(long)]
file_pattern: Option<String>,
#[arg(long)]
fps: Option<u32>,
#[arg(long, value_parser = ["webm", "mp4", "gif", "mov"])]
format: Option<String>,
#[arg(long, value_name = "SECONDS")]
fade_in: Option<f32>,
#[arg(long, value_name = "SECONDS")]
fade_out: Option<f32>,
#[arg(long)]
bitrate: Option<String>,
#[arg(long, value_name = "COLOR")]
background: Option<String>,
#[arg(long)]
crf: Option<u32>,
#[arg(long)]
open: bool,
#[arg(long, num_args = 0..=1, value_name = "N")]
preview: Option<Option<usize>>,
#[arg(short, long)]
verbose: bool,
#[arg(long, value_name = "FORMAT", value_parser = ["json"])]
progress: Option<String>,
#[arg(long)]
verbose_ffmpeg: bool,
}
impl Args {
fn overrides(&self) -> PartialConfig {
PartialConfig {
input: self.input.clone(),
output: self.output.clone(),
fps: self.fps,
format: self.format.clone(),
fade_in: self.fade_in,
fade_out: self.fade_out,
bitrate: self.bitrate.clone(),
crf: self.crf,
open: self.open.then_some(true),
preview: self.preview,
file_pattern: self.file_pattern.clone(),
verbose: self.verbose.then_some(true),
verbose_ffmpeg: self.verbose_ffmpeg.then_some(true),
background: self.background.clone(),
}
}
}
fn main() {
let args = Args::parse();
if args.config.is_none() && args.input.is_none() {
let _ = Args::command().print_help();
eprintln!("\n❌ Nothing to render: pass --input <folder or .zip> or --config <file>.");
std::process::exit(2);
}
let json = args.progress.as_deref() == Some("json");
if let Err(e) = run(&args, json) {
if json {
emit(serde_json::json!({ "event": "error", "message": e }));
}
eprintln!("{e}");
std::process::exit(1);
}
}
fn run(args: &Args, json: bool) -> Result<(), String> {
let file = match &args.config {
Some(path) => {
if args.verbose {
println!("Loading config from {}", path.display());
}
PartialConfig::from_file(&path.to_string_lossy())?
}
None => PartialConfig::default(),
};
let cfg = args.overrides().or(file).into_config()?;
let stdin = is_stdin_input(&cfg.input);
if args.frames.is_some() && !stdin {
return Err("❌ --frames only applies to a stream on stdin (--input -).".into());
}
let frames = args.frames;
let verbose = cfg.verbose;
if !json {
let report = render(cfg, stdin, frames, |_| {})?;
print_report(&report, verbose);
return Ok(());
}
let mut last = None;
let report = render(cfg, stdin, frames, |p| {
if last != Some(p.frame) {
last = Some(p.frame);
let mut line = serde_json::to_value(p).unwrap_or_default();
line["event"] = "progress".into();
emit(line);
}
})?;
emit(serde_json::json!({
"event": "done",
"output": report.output_path,
"frames": report.frames_rendered,
"warnings": report.ffmpeg_warnings,
"notes": report.notes,
}));
Ok(())
}
fn render<F: FnMut(RenderProgress)>(
cfg: RenderConfig,
stdin: bool,
frames: Option<u32>,
on_progress: F,
) -> Result<RenderReport, String> {
if stdin {
aether_renderer_core::render_stream(cfg, std::io::stdin(), frames, on_progress)
} else {
aether_renderer_core::render_with_progress(cfg, on_progress)
}
}
fn emit(value: serde_json::Value) {
use std::io::Write;
let mut out = std::io::stdout().lock();
let _ = writeln!(out, "{value}");
let _ = out.flush();
}
fn print_report(report: &RenderReport, verbose: bool) {
println!(
"✅ {} → {}",
report.notes.as_deref().unwrap_or("Render complete."),
report.output_path.display()
);
if verbose {
println!("{}", report.summary());
}
}