camshooter 0.1.1

Select a webcam, preview the live stream, and grab PNG snapshots with a keypress.
//! camshooter — pick a webcam, preview the live stream, snap PNGs with a keypress.

mod capture;
mod config;
mod snapshot;
mod ui;

use std::path::{Path, PathBuf};

use clap::Parser;

use crate::snapshot::SnapshotSettings;
use crate::ui::CamshooterApp;

/// Pick a webcam, preview it live, and save PNG snapshots.
///
/// In the window: 1–9 (or click) selects a camera; Space/S takes a snapshot;
/// Esc returns to the picker; Q quits.
#[derive(Parser)]
#[command(name = "camshooter", version, about)]
struct Cli {
    /// Directory to write snapshots into (created if missing). Overrides the
    /// `output_dir` from config; defaults to the current directory. Files are named
    /// `{prefix}{ddMMyyHHmmss}.png`.
    #[arg(short, long, value_name = "DIR")]
    output: Option<PathBuf>,
}

fn main() -> eframe::Result {
    let cli = Cli::parse();

    let config = match config::load() {
        Ok(c) => c,
        Err(msg) => {
            eprintln!("camshooter: {msg}");
            std::process::exit(2);
        }
    };

    // Output directory precedence: -o flag > config.output_dir > current directory.
    let out_dir = cli
        .output
        .or_else(|| config.output_dir_path())
        .unwrap_or_else(|| PathBuf::from("."));

    if let Err(msg) = validate_output_dir(&out_dir) {
        eprintln!("camshooter: {msg}");
        std::process::exit(2);
    }

    let settings = SnapshotSettings {
        dir: out_dir,
        prefix: config.prefix,
        on_collision: config.on_collision,
    };

    let native_options = eframe::NativeOptions {
        viewport: eframe::egui::ViewportBuilder::default()
            .with_title("camshooter")
            .with_inner_size([960.0, 720.0]),
        ..Default::default()
    };

    eframe::run_native(
        "camshooter",
        native_options,
        Box::new(move |cc| Ok(Box::new(CamshooterApp::new(cc, settings)))),
    )
}

/// Validate the snapshot output directory at startup so the user gets a clear, early
/// error instead of a surprise when they press the snapshot key. Creates the directory
/// (and parents) if it doesn't exist, which also surfaces permission problems now.
fn validate_output_dir(dir: &Path) -> Result<(), String> {
    if dir.exists() {
        if !dir.is_dir() {
            return Err(format!("output path {} is not a directory", dir.display()));
        }
        // Probe writability now so a read-only dir fails at startup, not on first snapshot.
        let probe = dir.join(".camshooter-write-probe");
        std::fs::File::create(&probe)
            .and_then(|_| std::fs::remove_file(&probe))
            .map_err(|e| format!("output directory {} is not writable: {e}", dir.display()))?;
    } else {
        std::fs::create_dir_all(dir)
            .map_err(|e| format!("cannot create output directory {}: {e}", dir.display()))?;
    }
    Ok(())
}