rproj 0.15.0

Guided bootstrap-to-game-dev CLI for Roblox: takes a fresh Windows PC to a working Roblox/Luau setup, then scaffolds projects on it
use std::fs;

use anyhow::{Context, Result};
use arboard::Clipboard;
use walkdir::WalkDir;

pub fn run() -> Result<()> {
    let cwd = std::env::current_dir().context("failed to read current directory")?;
    run_in(&cwd)
}

pub(super) fn run_in(cwd: &std::path::Path) -> Result<()> {
    run_with(cwd, copy_to_clipboard)
}

fn run_with(cwd: &std::path::Path, write: impl FnOnce(&str) -> Result<()>) -> Result<()> {
    let src_dir = cwd.join("src");

    if !src_dir.exists() {
        crate::diagnostics::event("copy.skip", "no src folder");
        println!("No 'src' folder found in the current directory.");
        return Ok(());
    }

    let mut output = String::new();
    let mut file_count = 0;

    for entry in WalkDir::new(&src_dir).into_iter().filter_map(|e| e.ok()) {
        if !entry.file_type().is_file() {
            continue;
        }
        let path = entry.path();
        let relative = path.strip_prefix(cwd).unwrap_or(path);
        let relative = relative.to_string_lossy().replace('\\', "/");

        match fs::read_to_string(path) {
            Ok(content) => {
                output.push_str(&format!("// --- {relative} --- \n"));
                output.push_str(&content);
                output.push_str("\n\n");
                file_count += 1;
            }
            Err(err) => {
                crate::diagnostics::event("copy.skip", format!("{relative}: {err}"));
                eprintln!("skipped {relative}: {err}");
            }
        }
    }

    if file_count == 0 {
        crate::diagnostics::event("copy.skip", "no readable source files");
        println!("'src' folder is empty. Nothing to copy.");
        return Ok(());
    }

    println!(
        "Copying {} characters from {file_count} file(s) to clipboard...",
        output.len()
    );
    write(&output)?;
    crate::diagnostics::event(
        "copy.complete",
        format!(
            "{file_count} files; {} bytes; content omitted",
            output.len()
        ),
    );
    println!("All files from 'src' successfully copied to clipboard!");
    Ok(())
}

fn copy_to_clipboard(text: &str) -> Result<()> {
    let mut clipboard = Clipboard::new().context("failed to access the system clipboard")?;
    clipboard
        .set_text(text.to_string())
        .context("failed to write to the system clipboard")?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn selected_directory_controls_copy_without_using_the_clipboard() {
        let temp = tempfile::tempdir().unwrap();
        let a = temp.path().join("A");
        let b = temp.path().join("B");
        for path in [&a, &b] {
            fs::create_dir_all(path.join("src")).unwrap();
        }
        fs::write(a.join("src/main.luau"), "selected project").unwrap();
        fs::write(b.join("src/main.luau"), "other project").unwrap();
        let cwd = std::env::current_dir().unwrap();
        let mut copied = String::new();
        run_with(&a, |text| {
            copied = text.into();
            Ok(())
        })
        .unwrap();
        assert!(copied.contains("// --- src/main.luau ---"));
        assert!(copied.contains("selected project"));
        assert!(!copied.contains("other project"));
        assert_eq!(std::env::current_dir().unwrap(), cwd);
    }
}