gclip 0.1.0

A smart clipboard tool that copies all files in a directory (respecting .gitignore) to the clipboard. Ideal for AI prompts.
use arboard::Clipboard;
use clap::Parser;
use ignore::WalkBuilder;
use std::fs;
use std::path::PathBuf;

/// A smart clipboard tool that copies all files in a directory (respecting .gitignore)
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Cli {
    /// The root directory to search for files. Defaults to the current directory.
    #[arg(default_value = ".")]
    path: PathBuf,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. PARSE COMMAND-LINE ARGUMENTS
    let cli = Cli::parse();
    let root_path = cli.path;

    if !root_path.is_dir() {
        eprintln!(
            "Error: Provided path '{}' is not a directory.",
            root_path.display()
        );
        return Err("Invalid path".into());
    }

    println!(
        "Scanning directory: {}",
        root_path.canonicalize()?.display()
    );

    // This will hold the concatenated content of all files.
    let mut full_content = String::new();
    let mut file_count: u32 = 0;

    // 2. WALK THE DIRECTORY, RESPECTING .gitignore
    // The 'ignore' crate is incredibly powerful. It automatically finds
    // and respects .gitignore, .ignore, and global ignore files.
    let walker = WalkBuilder::new(&root_path).build(); // Create the directory walker

    for result in walker {
        match result {
            Ok(entry) => {
                // We only care about files. The walker gives us directories, symlinks, etc. too.
                if let Some(file_type) = entry.file_type() {
                    if file_type.is_file() {
                        let path = entry.path();

                        // 3. READ THE FILE CONTENT
                        match fs::read_to_string(path) {
                            Ok(content) => {
                                // Add a header to separate files in the clipboard
                                full_content.push_str("--- FILENAME: ");
                                full_content.push_str(
                                    &path
                                        .strip_prefix(&root_path)
                                        .unwrap_or(path)
                                        .display()
                                        .to_string(),
                                );
                                full_content.push_str(" ---\n\n");

                                full_content.push_str(&content);
                                full_content.push_str("\n\n");
                                file_count += 1;
                            }
                            Err(_) => {
                                // This might happen if the file is not valid UTF-8 (e.g., an image)
                                // We'll just print a warning and skip it.
                                eprintln!(
                                    "Warning: Could not read non-UTF8 file, skipping: {}",
                                    path.display()
                                );
                            }
                        }
                    }
                }
            }
            Err(err) => eprintln!("ERROR: {}", err),
        }
    }

    if file_count == 0 {
        println!("No files found to copy.");
        return Ok(());
    }

    let total_bytes = full_content.len();

    // 4. COPY TO CLIPBOARD
    // 'arboard' provides a simple, cross-platform API.
    let mut clipboard = Clipboard::new()?;
    clipboard.set_text(full_content)?; // The '?' will propagate any errors

    println!("\n✅ Success!");
    println!(
        "Copied {} files ({} bytes) to the clipboard.",
        file_count, total_bytes
    );

    Ok(())
}