windows-troll 0.1.0

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

use std::time::Duration;

use clap::{arg, Arg, ArgMatches, Command};
use windows::Win32::Foundation::HWND;

use windows_troll::modules::window_wobbler::{self, WobbleDirection, WobbleSettings};

use crate::cli::CliModule;

/// CLI registration for the window-wobbler module.
pub struct WindowWobblerCli;

impl CliModule for WindowWobblerCli {
    fn name(&self) -> &'static str {
        "window-wobbler"
    }

    fn about(&self) -> &'static str {
        "Wobble (shake) windows in place"
    }

    fn command(&self) -> Command {
        Command::new(self.name())
            .about(self.about())
            .subcommand_required(true)
            .arg_required_else_help(true)
            .subcommand(
                Command::new("wobble")
                    .about("Wobble a window by handle")
                    .arg(arg!(<HWND> "Window handle (decimal, as shown by `window-hider list`)").value_parser(clap::value_parser!(isize)))
                    .args(wobble_args()),
            )
            .subcommand(
                Command::new("random")
                    .about("Wobble a random window")
                    .args(wobble_args()),
            )
            .subcommand(
                Command::new("title")
                    .about("Wobble the first window with a matching title")
                    .arg(arg!(<TITLE> "Exact window title"))
                    .args(wobble_args()),
            )
            .subcommand(
                Command::new("foreground")
                    .about("Wobble the currently focused window")
                    .args(wobble_args()),
            )
    }

    fn run(&self, matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
        match matches.subcommand() {
            Some(("wobble", m)) => run_wobble(m),
            Some(("random", m)) => run_random(m),
            Some(("title", m)) => run_title(m),
            Some(("foreground", m)) => run_foreground(m),
            _ => unreachable!("exhausted subcommands"),
        }
    }
}

fn wobble_args() -> Vec<Arg> {
    vec![
        arg!(-d --duration <SECONDS> "How long to wobble, in seconds")
            .default_value("2")
            .value_parser(clap::value_parser!(u64)),
        arg!(--direction <DIRECTION> "Wobble axis: horizontal, vertical, or both")
            .default_value("horizontal")
            .value_parser(parse_direction),
        arg!(--min <PIXELS> "Minimum wobble amplitude, in pixels")
            .default_value("4")
            .value_parser(clap::value_parser!(i32)),
        arg!(--max <PIXELS> "Maximum wobble amplitude, in pixels")
            .default_value("8")
            .value_parser(clap::value_parser!(i32)),
    ]
}

fn parse_direction(s: &str) -> Result<WobbleDirection, String> {
    match s.to_ascii_lowercase().as_str() {
        "horizontal" | "h" => Ok(WobbleDirection::Horizontal),
        "vertical" | "v" => Ok(WobbleDirection::Vertical),
        "both" | "b" | "circular" => Ok(WobbleDirection::Both),
        other => Err(format!(
            "unknown direction '{other}' (expected horizontal, vertical, or both)"
        )),
    }
}

fn build_settings(m: &ArgMatches) -> WobbleSettings {
    WobbleSettings {
        duration: Duration::from_secs(*m.get_one::<u64>("duration").expect("has default")),
        direction: *m.get_one::<WobbleDirection>("direction").expect("has default"),
        min_amplitude: *m.get_one::<i32>("min").expect("has default"),
        max_amplitude: *m.get_one::<i32>("max").expect("has default"),
    }
}

fn run_wobble(m: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
    let raw = *m.get_one::<isize>("HWND").expect("required argument");
    let hwnd = HWND(raw as *mut _);
    if window_wobbler::wobble_window(hwnd, build_settings(m)) {
        println!("Wobbled window handle {raw}");
    } else {
        println!("Failed to wobble window handle {raw}");
    }
    Ok(())
}

fn run_random(m: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
    match window_wobbler::wobble_random_window(build_settings(m)) {
        Some(w) => println!("Wobbled window: \"{}\"", w.title()),
        None => println!("No eligible windows found"),
    }
    Ok(())
}

fn run_title(m: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
    let title = m.get_one::<String>("TITLE").expect("required argument");
    match window_wobbler::wobble_by_title(title, build_settings(m)) {
        Some(w) => println!("Wobbled window: \"{}\"", w.title()),
        None => println!("No window with title \"{}\" found", title),
    }
    Ok(())
}

fn run_foreground(m: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
    if window_wobbler::wobble_foreground_window(build_settings(m)) {
        println!("Wobbled foreground window");
    } else {
        println!("No foreground window");
    }
    Ok(())
}