use std::time::Duration;
use clap::{arg, Arg, ArgMatches, Command};
use windows_troll::modules::window_party::{self, AnimationStyle, PartySettings};
use crate::cli::CliModule;
pub struct WindowPartyCli;
impl CliModule for WindowPartyCli {
fn name(&self) -> &'static str {
"window-party"
}
fn about(&self) -> &'static str {
"Scramble your screen into an animated grid party"
}
fn command(&self) -> Command {
Command::new(self.name())
.about(self.about())
.arg_required_else_help(false)
.arg(
arg!(--grid <N> "Tiles per side of the grid (N x N tiles)")
.default_value("20")
.value_parser(clap::value_parser!(i32)),
)
.arg(
arg!(--duration <SECONDS> "How long to run before restoring the screen")
.default_value("120")
.value_parser(clap::value_parser!(u64)),
)
.arg(
arg!(--animation <NAME> "Animation style to start with")
.default_value("illuminati")
.value_parser(parse_animation),
)
.arg(
arg!(--cycle <SECONDS> "Auto-switch animation every N seconds (0 = stay)")
.default_value("0")
.value_parser(clap::value_parser!(u64)),
)
.arg(
arg!(--fps <N> "Target frame rate")
.default_value("60")
.value_parser(clap::value_parser!(u32)),
)
.arg(
arg!(--speed <RATE> "Tile easing speed (0.05 slow .. 1.0 instant)")
.default_value("0.2")
.value_parser(clap::value_parser!(f32)),
)
.arg(
arg!(--seed <SEED> "Seed for deterministic tile placement")
.value_parser(clap::value_parser!(u64)),
)
.arg(
Arg::new("no-interactive")
.long("no-interactive")
.help("Ignore keyboard controls (left/right, space, esc)")
.action(clap::ArgAction::SetTrue),
)
}
fn run(&self, matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let animation_names = AnimationStyle::ALL
.iter()
.map(|s| s.cli_name())
.collect::<Vec<_>>()
.join(", ");
let settings = PartySettings {
grid_size: *matches.get_one::<i32>("grid").expect("has default"),
duration: Duration::from_secs(*matches.get_one::<u64>("duration").expect("has default")),
animation: *matches.get_one::<AnimationStyle>("animation").expect("has default"),
auto_cycle: Duration::from_secs(*matches.get_one::<u64>("cycle").expect("has default")),
fps: *matches.get_one::<u32>("fps").expect("has default"),
cell_speed: *matches.get_one::<f32>("speed").expect("has default"),
seed: matches.get_one::<u64>("seed").copied(),
interactive: !matches.get_flag("no-interactive"),
};
println!(
"Screen party: {}x{} grid, {} animation{}, {}s, {} fps",
settings.grid_size,
settings.grid_size,
settings.animation.name(),
if settings.auto_cycle.is_zero() {
String::new()
} else {
format!(" (cycling every {}s)", settings.auto_cycle.as_secs())
},
settings.duration.as_secs(),
settings.fps,
);
if settings.interactive {
println!("Controls: LEFT/RIGHT switch animation, SPACE pause/resume, ESC stop early");
}
println!("Available animations: {animation_names}");
let report = window_party::run(&settings)?;
println!(
"Party over: {} tiles animated for {}s using {}",
report.cells,
report.duration.as_secs_f64().round(),
report
.styles_used
.iter()
.map(|s| s.name())
.collect::<Vec<_>>()
.join(", "),
);
Ok(())
}
}
fn parse_animation(s: &str) -> Result<AnimationStyle, String> {
AnimationStyle::from_name(s).ok_or_else(|| {
let names = AnimationStyle::ALL
.iter()
.map(|s| s.cli_name())
.collect::<Vec<_>>()
.join(", ");
format!("unknown animation '{s}' (expected one of: {names})")
})
}