dirwalk 1.1.1

Platform-optimized recursive directory walker with metadata
Documentation
use clap::Parser;
use dirwalk::output::{self, ColorMode, DisplayOptions, Format, GroupBy};
use dirwalk::{Sort, Threads, WalkBuilder};
use std::io::{self, BufWriter, IsTerminal, Write};
use std::process;

#[derive(Parser)]
#[command(
    name = "dirwalk",
    about = "Platform-optimized recursive directory walker"
)]
struct Cli {
    /// Directory to walk
    #[arg(default_value = ".")]
    path: String,

    #[arg(long)]
    max_depth: Option<u32>,

    /// Include hidden files
    #[arg(long)]
    hidden: bool,

    /// Respect .gitignore rules
    #[arg(long)]
    gitignore: bool,

    /// Follow symbolic links
    #[arg(long)]
    follow_links: bool,

    /// Filter by extensions (comma-separated)
    #[arg(long, value_delimiter = ',')]
    extensions: Option<Vec<String>>,

    /// Filter by glob pattern
    #[arg(long)]
    glob: Option<String>,

    /// Minimum file size in bytes
    #[arg(long)]
    min_size: Option<u64>,

    /// Maximum file size in bytes
    #[arg(long)]
    max_size: Option<u64>,

    #[arg(long, value_enum)]
    sort: Option<Sort>,

    /// Sort directories before files
    #[arg(long)]
    dirs_first: bool,

    #[arg(long, value_enum)]
    group_by: Option<GroupBy>,

    /// Print summary statistics
    #[arg(long)]
    stats: bool,

    /// Output format. Defaults to rich long format on TTY, plain when piped.
    #[arg(long, value_enum)]
    format: Option<Format>,

    /// Columnar name-only layout
    #[arg(long, conflicts_with = "format")]
    short: bool,

    /// Color mode
    #[arg(long, value_enum, default_value = "auto")]
    color: ColorMode,

    /// Disable color output (shorthand for --color never)
    #[arg(long, conflicts_with = "color")]
    no_color: bool,

    /// Threads for parallel directory scanning.
    /// Use 0 for all cores, a number like '4', or a fraction like '1/2'.
    #[arg(long, default_value = "0")]
    threads: Threads,
}

fn main() {
    let cli = Cli::parse();

    let mut builder = WalkBuilder::new(&cli.path)
        .hidden(cli.hidden)
        .follow_links(cli.follow_links)
        .gitignore(cli.gitignore)
        .stats(cli.stats)
        .dirs_first(cli.dirs_first)
        .threads(cli.threads);

    if let Some(depth) = cli.max_depth {
        builder = builder.max_depth(depth);
    }
    if let Some(ref exts) = cli.extensions {
        builder = builder.extensions(exts);
    }
    if let Some(ref pattern) = cli.glob {
        builder = match builder.glob(pattern) {
            Ok(b) => b,
            Err(e) => {
                eprintln!("invalid glob pattern: {}", e);
                process::exit(1);
            }
        };
    }
    if let Some(size) = cli.min_size {
        builder = builder.min_size(size);
    }
    if let Some(size) = cli.max_size {
        builder = builder.max_size(size);
    }
    if let Some(sort) = cli.sort {
        builder = builder.sort(sort);
    }

    let result = match builder.build() {
        Ok(r) => r,
        Err(e) => {
            eprintln!("error: {}", e);
            process::exit(1);
        }
    };

    // Resolve display mode.
    let is_tty = io::stdout().is_terminal();
    let color_mode = if cli.no_color {
        ColorMode::Never
    } else {
        cli.color
    };
    let color_enabled = color_mode.resolve(is_tty);

    // Rich output when no explicit --format and either TTY or --short requested.
    let use_rich = cli.format.is_none() && (is_tty || cli.short);
    let display = if use_rich {
        if color_enabled {
            output::color::enable_ansi();
        }
        let terminal_width = terminal_size::terminal_size()
            .map(|(w, _)| w.0)
            .unwrap_or(80);
        let ls_colors = lscolors::LsColors::from_env().unwrap_or_default();
        Some(DisplayOptions {
            short: cli.short,
            classify: true,
            color_enabled,
            terminal_width,
            ls_colors,
        })
    } else {
        None
    };

    let format = cli.format.unwrap_or(Format::Plain);

    let stdout = io::stdout().lock();
    let mut w = BufWriter::new(stdout);

    if let Err(e) = output::write_entries(
        &mut w,
        &result.entries,
        format,
        cli.group_by,
        display.as_ref(),
    ) {
        eprintln!("output error: {}", e);
        process::exit(1);
    }

    if let Some(ref stats) = result.stats
        && let Err(e) = output::write_stats(&mut w, stats)
    {
        eprintln!("output error: {}", e);
        process::exit(1);
    }

    if let Err(e) = w.flush() {
        // Broken pipe (e.g., piped to `head`) is normal — exit silently.
        if e.kind() != std::io::ErrorKind::BrokenPipe {
            eprintln!("output error: {}", e);
            process::exit(1);
        }
    }
}