--- .\Cargo.lock ---
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4

[[package]]
name = "file2txt"
version = "0.1.0"
dependencies = [
 "walkdir",
]

[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
 "winapi-util",
]

[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
 "same-file",
 "winapi-util",
]

[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
 "windows-sys",
]

[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"

[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
 "windows-link",
]


--- .\Cargo.toml ---
[package]
name = "file2txt"
version = "0.1.0"
edition = "2024"

[dependencies]
walkdir = "2"

--- .\src\filter_config.rs ---
const DEFAULT_EXTENSIONS: &[&str] = &[
    "rs", "toml", "md", "txt", "json", "yaml", "yml",
    "js", "ts", "jsx", "tsx", "html", "css", "scss", "less",
    "xml", "svg", "graphql", "gql",
    "sh", "bash", "zsh", "fish",
    "py", "pyc",
    "go", "mod", "sum",
    "c", "cpp", "h", "hpp", "cc", "cxx",
    "java", "kt", "kts", "scala", "clj", "cljs",
    "rb", "erb", "rake",
    "php", "phtml",
    "swift", "m", "mm",
    "rs", "rlib",  // Rust
    "cs", "fs", "fsx", // C#, F#
    "vue", "svelte",
    "ini", "cfg", "conf", "config",
    "log", "gitignore", "gitattributes", "dockerignore",
    "env", "example", "lock", "license", "readme", "authors",
    "sql", "psql",
    "tex", "latex",
    "org", "wiki",
    "adoc", "asciidoc"
];

pub struct FilterConfig {
    pub extensions: Vec<String>,
    pub exclude_dirs: Vec<String>,
    pub max_size: u64
}
impl FilterConfig {
    pub fn default() -> Self {
        Self {
            extensions: DEFAULT_EXTENSIONS.iter().map(|s| s.to_string()).collect(),
            exclude_dirs: vec![".git".to_string(), "target".to_string(), "node_modules".to_string()],
            max_size: 1024 * 1024, // 1MB
        }
    }
    pub fn should_process(&self, entry: &walkdir::DirEntry) -> bool {
        let path = entry.path();

        if !entry.file_type().is_file() {
            return false;
        }

        for exclude in &self.exclude_dirs {
            if path.components().any(|e| &e.as_os_str().to_string_lossy().to_string() == exclude) {
                return false;
            }
        }

        if !self.extensions.is_empty() {
            if let Some(ext) = path.extension().and_then(|x| x.to_str()) {
                if !self.extensions.contains(&ext.to_string()) {
                    return false;
                }
            } else {
                return false;
            }
        }

        if self.max_size > 0 {
            if let Ok(meta) = entry.metadata() {
                if meta.len() > self.max_size {
                    return false;
                }
            }
        }
        true
    }
}

--- .\src\lib.rs ---
mod filter_config;
pub use filter_config::*;
use std::fs;
use walkdir::WalkDir;

#[derive(Debug)]
pub struct File {
    pub name: String,
    pub content: String,
    pub dir: String
}
impl File {
    pub fn new(name: String, content: String, dir: String) -> Self {
        Self { name, content, dir }
    }
    pub fn from_path(path: &std::path::Path) -> Result<Self, std::io::Error> {
        let name = path.to_string_lossy().to_string();
        // let content = fs::read_to_string(path)?;
        let content = match fs::read_to_string(path) {
            Ok(c) => c,
            Err(e) => return Err(e)
        };
        let dir = path.parent().unwrap_or_else(|| path).to_string_lossy().to_string();
        Ok(Self {
            name,
            content,
            dir
        })
    }
}

pub fn collect_files(filter: &FilterConfig) -> Result<Vec<File>, std::io::Error> {
    let mut files = Vec::new();
    
    for entry in WalkDir::new(".") {
        let entry = entry?;
        if filter.should_process(&entry) {
            if let Ok(file) = File::from_path(entry.path()) {
                files.push(file);
            }
        }
    }
    
    Ok(files)
}

pub fn write_bundle(files: &[File], output_path: &str) -> Result<(), std::io::Error> {
    use std::io::Write;
    let mut output = std::fs::File::create(output_path)?;
    
    for file in files {
        writeln!(output, "--- {} ---", file.name)?;
        writeln!(output, "{}", file.content)?;
        writeln!(output)?;
    }
    
    Ok(())
}

--- .\src\main.rs ---
use file2txt::{FilterConfig, collect_files, write_bundle};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let filter = FilterConfig::default();
    
    let files = collect_files(&filter)?;
    println!("找到 {} 个文件", files.len());
    
    write_bundle(&files, "output.txt")?; 
    
    Ok(())
}

