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

[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
 "anstyle",
 "anstyle-parse",
 "anstyle-query",
 "anstyle-wincon",
 "colorchoice",
 "is_terminal_polyfill",
 "utf8parse",
]

[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"

[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
 "utf8parse",
]

[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
 "windows-sys",
]

[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
 "anstyle",
 "once_cell_polyfill",
 "windows-sys",
]

[[package]]
name = "clap"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
dependencies = [
 "clap_builder",
 "clap_derive",
]

[[package]]
name = "clap_builder"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
 "anstream",
 "anstyle",
 "clap_lex",
 "strsim",
]

[[package]]
name = "clap_derive"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
dependencies = [
 "heck",
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"

[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"

[[package]]
name = "file2txt"
version = "0.1.1"
dependencies = [
 "clap",
 "walkdir",
]

[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"

[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"

[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"

[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
 "unicode-ident",
]

[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
 "proc-macro2",
]

[[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 = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"

[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
 "proc-macro2",
 "quote",
 "unicode-ident",
]

[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"

[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"

[[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.1"
edition = "2024"
description = "将目录下所有文本文件内容聚合到一个文件"
license = "MIT"
repository = "https://github.com/klp-xkjt/file2txt"
readme = "readme.md"

[[bin]]
name = "file2txt"
path = "src/main.rs"

[dependencies]
walkdir = "2"
clap = { version = "4.6", features = ["derive"] }

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

[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
 "anstyle",
 "anstyle-parse",
 "anstyle-query",
 "anstyle-wincon",
 "colorchoice",
 "is_terminal_polyfill",
 "utf8parse",
]

[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"

[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
 "utf8parse",
]

[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
 "windows-sys",
]

[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
 "anstyle",
 "once_cell_polyfill",
 "windows-sys",
]

[[package]]
name = "clap"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
dependencies = [
 "clap_builder",
 "clap_derive",
]

[[package]]
name = "clap_builder"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
 "anstream",
 "anstyle",
 "clap_lex",
 "strsim",
]

[[package]]
name = "clap_derive"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
dependencies = [
 "heck",
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"

[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"

[[package]]
name = "file2txt"
version = "0.1.1"
dependencies = [
 "clap",
 "walkdir",
]

[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"

[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"

[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"

[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
 "unicode-ident",
]

[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
 "proc-macro2",
]

[[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 = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"

[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
 "proc-macro2",
 "quote",
 "unicode-ident",
]

[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"

[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"

[[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.1"
edition = "2024"
description = "将目录下所有文本文件内容聚合到一个文件"
license = "MIT"
repository = "https://github.com/klp-xkjt/file2txt"
readme = "readme.md"

[[bin]]
name = "file2txt"
path = "src/main.rs"

[dependencies]
walkdir = "2"
clap = { version = "4.6", features = ["derive"] }

--- .\output.txt ---
--- .\Cargo.toml ---
[package]
name = "file2txt"
version = "0.1.1"
edition = "2024"
description = "将目录下所有文本文件内容聚合到一个文件"
license = "MIT"
repository = "https://github.com/klp-xkjt/file2txt"
readme = "readme.md"

[[bin]]
name = "file2txt"
path = "src/main.rs"

[dependencies]
walkdir = "2"
clap = { version = "4.6", features = ["derive"] }

--- .\src\filter_config.rs ---
// 默认的常用扩展名配置
pub 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 std::io::Write;
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 }
    }
    // 通过文件路径来创建 File
    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
        })
    }
}

// 用 Walkdir 循环递归目录，返回 Result
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?;
        // 判断可以添加进 Vector 的 File
        if filter.should_process(&entry) {
            if let Ok(file) = File::from_path(entry.path()) {
                files.push(file);
            }
        }
    }
    
    Ok(files)
}

// 将 File 相关信息写入输出文件
pub fn write_bundle(files: &[File], output_path: &str) -> Result<(), std::io::Error> {
    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, DEFAULT_EXTENSIONS};
use clap::Parser;

#[derive(Parser)]
struct Cli {
    /// 指定输出文件 默认是 output.txt
    #[arg(short, long, default_value = "output.txt")]
    output: String,

    /// 指定文件最大大小（KB），默认 1024
    #[arg(short, long, default_value = "1024")]
    max_size: u64,

    /// 指定保留哪些后缀名文件（逗号分隔，例如：-e rs,toml,md）
    /// 不指定时使用内置的常用扩展名列表
    #[arg(short, long, value_delimiter = ',')]
    ext: Option<Vec<String>>,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let cli = Cli::parse();
    let extensions = match cli.ext {
        Some(exts) => exts,  // 用户指定了，用用户的
        None => DEFAULT_EXTENSIONS.iter().map(|s| s.to_string()).collect(),  // 用户没指定，用默认的
    };

    let filter = FilterConfig {
        extensions,
        exclude_dirs: vec![".git".to_string(), "target".to_string()],
        max_size: cli.max_size * 1024
    };
    let files = collect_files(&filter)?;
    println!("找到 {} 个文件", files.len());
    
    let output = cli.output;
    write_bundle(&files, &output)?; 
    Ok(())
}



--- .\readme.md ---
# File2TXT

将当前目录下的所有文本文件内容聚合到一个文件，方便喂给 AI 分析或代码审查。

## 安装
```bash
cargo install file2txt
```

## 快速开始
在任意项目目录下运行：
```bash
file2txt
```

## 命令行选项

| 参数 | 说明 | 示例 |
|------|------|------|
| `-o, --output` | 输出文件名（默认：`output.txt`） | `file2txt -o bundle.txt` |
| `-e, --ext` | 只处理指定扩展名（逗号分隔） | `file2txt -e rs,toml,md` |
| `-m, --max-size` | 最大文件大小，单位 KB（默认：1024） | `file2txt -m 500` |

## 使用示例

```bash
# 只收集 Rust 和 Markdown 文件
file2txt -e rs,md

# 限制文件大小，跳过大于 500KB 的
file2txt -m 500

# 自定义输出文件名
file2txt -o analysis.txt

# 组合使用
file2txt -e rs,toml -m 1024 -o code_bundle.txt
```

## 默认行为

- 递归遍历当前目录下的所有文件
- 自动排除 `.git`、`target`、`node_modules` 目录
- 只处理常见文本文件扩展名（除非用 `-e` 指定）
- 跳过大于 1MB 的文件（除非用 `-m` 调整）
- 读取出错的文件自动跳过，不影响继续执行

## 输出格式

```
--- ./src/main.rs ---
[文件内容]

--- ./src/lib.rs ---
[文件内容]
```

--- .\src\filter_config.rs ---
// 默认的常用扩展名配置
pub 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 std::io::Write;
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 }
    }
    // 通过文件路径来创建 File
    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
        })
    }
}

// 用 Walkdir 循环递归目录，返回 Result
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?;
        // 判断可以添加进 Vector 的 File
        if filter.should_process(&entry) {
            if let Ok(file) = File::from_path(entry.path()) {
                files.push(file);
            }
        }
    }
    
    Ok(files)
}

// 将 File 相关信息写入输出文件
pub fn write_bundle(files: &[File], output_path: &str) -> Result<(), std::io::Error> {
    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, DEFAULT_EXTENSIONS};
use clap::Parser;

#[derive(Parser)]
struct Cli {
    /// 指定输出文件 默认是 output.txt
    #[arg(short, long, default_value = "output.txt")]
    output: String,

    /// 指定文件最大大小（KB），默认 1024
    #[arg(short, long, default_value = "1024")]
    max_size: u64,

    /// 指定保留哪些后缀名文件（逗号分隔，例如：-e rs,toml,md）
    /// 不指定时使用内置的常用扩展名列表
    #[arg(short='e', long, value_delimiter = ',')]
    extensions: Option<Vec<String>>,

    /// 指定排除哪些目录（命令运行的同级目录）(逗号分隔，例如 --exclude_dirs .git,node_modules,target)
    /// 不指定时默认排除 .git node_modules target
    #[arg(short='d', long, value_delimiter = ',')]
    exclude_dirs: Option<Vec<String>>
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let cli = Cli::parse();

    let extensions = match cli.extensions {
        Some(exts) => exts,  // 用户指定了，用用户的
        None => DEFAULT_EXTENSIONS.iter().map(|s| s.to_string()).collect(),  // 用户没指定，用默认的
    };
    let exclude_dirs = match cli.exclude_dirs {
        Some(exc) => exc,
        None =>  vec![".git".to_string(), "target".to_string(), "node_modules".to_string()]
    };

    let filter = FilterConfig {
        extensions,
        exclude_dirs,
        max_size: cli.max_size * 1024
    };
    let files = collect_files(&filter)?;
    println!("找到 {} 个文件", files.len());
    
    let output = cli.output;
    write_bundle(&files, &output)?; 
    Ok(())
}



--- .\readme.md ---
# File2TXT

将当前目录下的所有文本文件内容聚合到一个文件，方便喂给 AI 分析或代码审查。

## 安装
```bash
cargo install file2txt
```

## 快速开始
在任意项目目录下运行：
```bash
file2txt
```

## 命令行选项

| 参数 | 说明 | 示例 |
|------|------|------|
| `-o, --output` | 输出文件名（默认：`output.txt`） | `file2txt -o bundle.txt` |
| `-e, --ext` | 只处理指定扩展名（逗号分隔） | `file2txt -e rs,toml,md` |
| `-m, --max-size` | 最大文件大小，单位 KB（默认：1024） | `file2txt -m 500` |

## 使用示例

```bash
# 只收集 Rust 和 Markdown 文件
file2txt -e rs,md

# 限制文件大小，跳过大于 500KB 的
file2txt -m 500

# 自定义输出文件名
file2txt -o analysis.txt

# 组合使用
file2txt -e rs,toml -m 1024 -o code_bundle.txt
```

## 默认行为

- 递归遍历当前目录下的所有文件
- 自动排除 `.git`、`target`、`node_modules` 目录
- 只处理常见文本文件扩展名（除非用 `-e` 指定）
- 跳过大于 1MB 的文件（除非用 `-m` 调整）
- 读取出错的文件自动跳过，不影响继续执行

## 输出格式

```
--- ./src/main.rs ---
[文件内容]

--- ./src/lib.rs ---
[文件内容]
```

--- .\src\filter_config.rs ---
// 默认的常用扩展名配置
pub 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 enum FilterDecision {
    Keep,
    ExcludeDir,
    ExcludeExt,
    ExcludeSize,
    ExcludeNotFile,
}

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 decide(&self, entry: &walkdir::DirEntry) -> FilterDecision {
        let path = entry.path();

        // 判断是否为路径并排除
        if !entry.file_type().is_file() {
            return FilterDecision::ExcludeNotFile;
        }

        // 判断是否为被忽略的目录并排除其中文件
        for exclude in &self.exclude_dirs {
            if path.components().any(|e| &e.as_os_str().to_string_lossy().to_string() == exclude) {
                return FilterDecision::ExcludeDir;
            }
        }
        // 判断文件扩展名
        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 FilterDecision::ExcludeExt;
                }
            } else {
                return FilterDecision::ExcludeExt;
            }
        }
        // 判断文件大小
        if self.max_size > 0 {
            if let Ok(meta) = entry.metadata() {
                if meta.len() > self.max_size {
                    return FilterDecision::ExcludeSize;
                }
            }
        }
        FilterDecision::Keep
    }
}

--- .\src\lib.rs ---
mod filter_config;
pub use filter_config::*;

use std::fs;
use std::io::Write;
use std::error::Error;
use std::path::Path;
use walkdir::WalkDir;

pub struct CollectStats {
    pub all_processed: usize, // 总扫描的数量
    pub included: usize, // 最终包含的文件数量
    pub excluded_by_ext: usize, // 以后缀排除的文件数
    pub excluded_by_dir: usize, // 以目录排除的文件数
    pub excluded_by_size: usize, // 以文件大小排除的文件数
    pub exclude_by_not_file: usize // 排除的二进制文件或其他不是文件的数量
}
impl Default for CollectStats {
    fn default() -> Self {
        Self { all_processed: 0, included: 0, excluded_by_ext: 0, excluded_by_dir: 0, excluded_by_size: 0, exclude_by_not_file: 0}
    }
}

#[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 }
    }
    // 通过文件路径来创建 File
    pub fn from_path(path: &Path) -> Result<Self, Box<dyn 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(Box::new(e))
        };
        let dir = path.parent().unwrap_or_else(|| path).to_string_lossy().to_string();
        Ok(Self {
            name,
            content,
            dir
        })
    }
}

// 用 Walkdir 循环递归目录，返回 Result
pub fn collect_files(filter: &FilterConfig) -> Result<(Vec<File>, CollectStats), Box<dyn Error>> {
    let mut files = Vec::new();
    let mut stats = CollectStats::default();
    
    for entry in WalkDir::new(".") {        
        let entry = entry?;
        stats.all_processed += 1;

        match filter.decide(&entry) {
            FilterDecision::Keep => {
                if let Ok(file) = File::from_path(entry.path()) {
                    files.push(file);
                    stats.included += 1;
                }
            },
            FilterDecision::ExcludeDir => {
                stats.excluded_by_dir += 1;
            },
            FilterDecision::ExcludeExt => {
                stats.excluded_by_ext += 1;
            },
            FilterDecision::ExcludeSize => {
                stats.excluded_by_size += 1;
            },
            FilterDecision::ExcludeNotFile => {
                stats.exclude_by_not_file += 1;
            }
        }
    }
    
    Ok((files, stats))
}

// 将 File 相关信息写入输出文件
pub fn write_bundle(files: &[File], output_path: &str) -> Result<(), Box<dyn Error>> {
    let mut output = 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, DEFAULT_EXTENSIONS};
use clap::Parser;

#[derive(Parser)]
struct Cli {
    /// 指定输出文件 默认是 output.txt
    #[arg(short, long, default_value = "output.txt")]
    output: String,

    /// 指定文件最大大小（KB），默认 1024
    #[arg(short, long, default_value = "1024")]
    max_size: u64,

    /// 指定保留哪些后缀名文件（逗号分隔，例如：-e rs,toml,md）
    /// 不指定时使用内置的常用扩展名列表
    #[arg(short='e', long, value_delimiter = ',')]
    extensions: Option<Vec<String>>,

    /// 指定排除哪些目录（命令运行的同级目录）(逗号分隔，例如 --exclude_dirs .git,node_modules,target)
    /// 不指定时默认排除 .git node_modules target
    #[arg(short='d', long, value_delimiter = ',')]
    exclude_dirs: Option<Vec<String>>
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let cli = Cli::parse();

    let extensions = match cli.extensions {
        Some(exts) => exts,  // 用户指定了，用用户的
        None => DEFAULT_EXTENSIONS.iter().map(|s| s.to_string()).collect(),  // 用户没指定，用默认的
    };
    let exclude_dirs = match cli.exclude_dirs {
        Some(exc) => exc,
        None =>  vec![".git".to_string(), "target".to_string(), "node_modules".to_string()]
    };

    let filter = FilterConfig {
        extensions,
        exclude_dirs,
        max_size: cli.max_size * 1024
    };
    let (files, stats) = collect_files(&filter)?;
    println!("📊 统计信息:");
    println!("   扫描总数: {}", stats.all_processed);
    println!("   包含文件: {}", stats.included);
    println!("   排除总数: {}", stats.all_processed - stats.included);
    println!("   ├─ 目录排除: {}", stats.excluded_by_dir);
    println!("   ├─ 扩展名排除: {}", stats.excluded_by_ext);
    println!("   ├─ 大小排除 (>{}KB): {}", cli.max_size, stats.excluded_by_size);
    println!("   └─ 二进制或非文件: {}",stats.exclude_by_not_file);
    
    let output = cli.output;
    write_bundle(&files, &output)?; 
    Ok(())
}

