windows-troll 0.1.0

Modular Windows prank library
//! Window party module.
//!
//! Ported from the `screen-messy-fun` project: captures the screen into a grid
//! of tiles, then animates the captured tiles around in a configurable pattern,
//! painting them directly onto the desktop. The original layout is restored
//! when the party ends.

mod animations;
mod capture;
mod grid;

use std::thread;
use std::time::{Duration, Instant};

use rand::rngs::StdRng;
use rand::{rng, RngExt, SeedableRng};
use windows::Win32::UI::Input::KeyboardAndMouse::{
    GetAsyncKeyState, VK_ESCAPE, VK_LEFT, VK_RIGHT, VK_SPACE,
};
use windows::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_CXSCREEN, SM_CYSCREEN};

pub use animations::AnimationStyle;
use animations::AnimationController;
use capture::ScreenCapture;
use grid::GridCell;

/// Configuration for a screen party.
///
/// Tune these to get anything from a subtle background shimmy to a full-screen
/// meltdown.
#[derive(Debug, Clone, PartialEq)]
pub struct PartySettings {
    /// Number of tiles per side of the grid (e.g. 20 => 400 tiles).
    pub grid_size: i32,
    /// How long the party runs before the screen is restored.
    pub duration: Duration,
    /// Which animation style to start with.
    pub animation: AnimationStyle,
    /// How often to auto-switch to the next animation (`Duration::ZERO` = stay
    /// on the selected style).
    pub auto_cycle: Duration,
    /// Target frame rate (how fast tiles ease toward their targets).
    pub fps: u32,
    /// Base easing speed per frame for tiles, roughly `0.05..1.0`.
    pub cell_speed: f32,
    /// Optional seed for deterministic tile placement and movement.
    pub seed: Option<u64>,
    /// Whether the running party reacts to keyboard input
    /// (`←`/`→` switch styles, `Space` pause, `Esc` end early).
    pub interactive: bool,
}

impl Default for PartySettings {
    fn default() -> Self {
        Self {
            grid_size: 20,
            duration: Duration::from_secs(120),
            animation: AnimationStyle::Illuminati,
            auto_cycle: Duration::ZERO,
            fps: 60,
            cell_speed: 0.2,
            seed: None,
            interactive: true,
        }
    }
}

/// Summary of a finished party.
#[derive(Debug, Clone, PartialEq)]
pub struct PartyReport {
    /// Number of tiles that were captured and animated.
    pub cells: usize,
    /// The styles that were actually run (in order, de-duplicated).
    pub styles_used: Vec<AnimationStyle>,
    /// How long the party actually lasted.
    pub duration: Duration,
}

/// Runs a screen party according to `settings` and restores the screen when it
/// ends.
pub fn run(settings: &PartySettings) -> Result<PartyReport, String> {
    let screen_width = unsafe { GetSystemMetrics(SM_CXSCREEN) };
    let screen_height = unsafe { GetSystemMetrics(SM_CYSCREEN) };

    let grid_size = settings.grid_size.clamp(2, 64);
    let cell_width = screen_width / grid_size;
    let cell_height = screen_height / grid_size;
    if cell_width <= 0 || cell_height <= 0 {
        return Err(format!(
            "grid size {grid_size} too large for a {screen_width}x{screen_height} screen"
        ));
    }

    let mut rng: StdRng = match settings.seed {
        Some(seed) => StdRng::seed_from_u64(seed),
        None => StdRng::from_rng(&mut rng()),
    };

    let mut cells = Vec::with_capacity((grid_size * grid_size) as usize);
    for y in 0..grid_size {
        for x in 0..grid_size {
            let start_x = x * cell_width;
            let start_y = y * cell_height;
            let capture = ScreenCapture::new(start_x, start_y, cell_width, cell_height)?;
            let speed = settings.cell_speed.max(0.0) * rng.random_range(0.75..1.25);
            cells.push(GridCell::new(capture, start_x, start_y, speed));
        }
    }

    let mut controller = AnimationController::new(screen_width, screen_height, grid_size);
    controller.current_style = settings.animation;

    let frame_delay = Duration::from_secs_f64(1.0 / settings.fps.max(1) as f64);
    let started = Instant::now();
    let mut styles_used = vec![settings.animation];
    let mut last_cycle = Instant::now();
    let mut paused = false;
    let mut last_key = Instant::now();
    const KEY_DEBOUNCE: Duration = Duration::from_millis(200);

    loop {
        let elapsed = started.elapsed();
        if elapsed >= settings.duration {
            break;
        }

        if settings.interactive {
            let key_debounce = last_key.elapsed() > KEY_DEBOUNCE;
            unsafe {
                if GetAsyncKeyState(VK_ESCAPE.0 as i32) as u16 & 0x8000 != 0 {
                    break;
                }
                if key_debounce && GetAsyncKeyState(VK_SPACE.0 as i32) as u16 & 0x8000 != 0 {
                    paused = !paused;
                    last_key = Instant::now();
                    println!("{}", if paused { "Paused" } else { "Resumed" });
                }
                if key_debounce && GetAsyncKeyState(VK_RIGHT.0 as i32) as u16 & 0x8000 != 0 {
                    let style = controller.next_style();
                    last_key = Instant::now();
                    push_unique(&mut styles_used, style);
                    println!("Switched to animation: {}", style.name());
                }
                if key_debounce && GetAsyncKeyState(VK_LEFT.0 as i32) as u16 & 0x8000 != 0 {
                    let style = controller.prev_style();
                    last_key = Instant::now();
                    push_unique(&mut styles_used, style);
                    println!("Switched to animation: {}", style.name());
                }
            }
        }

        if !settings.auto_cycle.is_zero() && last_cycle.elapsed() >= settings.auto_cycle {
            let style = controller.next_style();
            push_unique(&mut styles_used, style);
            last_cycle = Instant::now();
            println!("Auto-switched to animation: {}", style.name());
        }

        if !paused {
            controller.apply_animation(&mut cells);
            for cell in &mut cells {
                cell.update();
            }
            controller.frame_count += 1;
        }

        thread::sleep(frame_delay);
    }

    for cell in &cells {
        if let Err(e) = cell.reset() {
            eprintln!("Failed to restore screen: {e}");
        }
    }

    Ok(PartyReport {
        cells: cells.len(),
        styles_used,
        duration: started.elapsed(),
    })
}

fn push_unique(styles: &mut Vec<AnimationStyle>, style: AnimationStyle) {
    if !styles.contains(&style) {
        styles.push(style);
    }
}