windows-troll 0.1.0

Modular Windows prank library
//! CLI for the [`window_party`](windows_troll::modules::window_party) module.

use std::time::Duration;

use clap::{arg, Arg, ArgMatches, Command};

use windows_troll::modules::window_party::{self, AnimationStyle, PartySettings};

use crate::cli::CliModule;

/// CLI registration for the window-party module.
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})")
    })
}