dirwalk 1.1.1

Platform-optimized recursive directory walker with metadata
Documentation
//! CLI output formatters (plain, long, columns, tree, JSON, JSONL, CSV).
//!
//! Only available with the `cli` feature flag.

pub mod color;
pub mod columns;
pub mod csv;
pub mod format;
pub mod json;
pub mod jsonl;
pub mod long;
pub mod plain;
pub mod tree;

use crate::entry::Entry;
use crate::group;
use crate::walk::Stats;
pub use color::ColorMode;
use lscolors::LsColors;
use serde::Serialize;
use std::borrow::Borrow;
use std::io::{self, Write};

/// Display options for TTY-aware rich output.
pub struct DisplayOptions {
    /// Show columnar name-only layout. When false, long format is used.
    pub short: bool,
    /// Append type indicators (/ for dirs, @ for symlinks).
    pub classify: bool,
    /// Whether color output is enabled (resolved from ColorMode + TTY + NO_COLOR).
    pub color_enabled: bool,
    /// Terminal width in columns (for short/columnar layout).
    pub terminal_width: u16,
    /// LS_COLORS configuration for file type coloring.
    pub ls_colors: LsColors,
}

#[derive(Clone, Copy, clap::ValueEnum)]
pub enum Format {
    Plain,
    Tree,
    Json,
    Jsonl,
    Csv,
}

#[derive(Clone, Copy, clap::ValueEnum)]
pub enum GroupBy {
    Extension,
    Directory,
    Depth,
}

pub fn write_entries(
    w: &mut impl Write,
    entries: &[Entry],
    format: Format,
    group_by: Option<GroupBy>,
    display: Option<&DisplayOptions>,
) -> io::Result<()> {
    if let Some(gb) = group_by {
        write_grouped(w, entries, format, gb, display)
    } else {
        write_flat(w, entries, format, display)
    }
}

pub fn write_stats(w: &mut impl Write, stats: &Stats) -> io::Result<()> {
    writeln!(
        w,
        "\n{} files, {} directories, {} bytes, {:.2?}",
        stats.file_count, stats.dir_count, stats.total_size, stats.duration
    )
}

fn write_flat<E: Borrow<Entry> + Serialize>(
    w: &mut impl Write,
    entries: &[E],
    format: Format,
    display: Option<&DisplayOptions>,
) -> io::Result<()> {
    // Rich TTY output: long or columns when display options are present and format is Plain.
    if let (Format::Plain, Some(opts)) = (format, display) {
        if opts.short {
            return columns::write(w, entries, opts);
        }
        return long::write(w, entries, opts);
    }

    match format {
        Format::Plain => plain::write(w, entries),
        Format::Tree => {
            // tree::to_tree requires &[&Entry] — allocate only for this format.
            let refs: Vec<&Entry> = entries.iter().map(|e| e.borrow()).collect();
            tree::write(w, &refs)
        }
        Format::Json => json::write(w, entries),
        Format::Jsonl => jsonl::write(w, entries),
        Format::Csv => csv::write(w, entries),
    }
}

fn write_grouped(
    w: &mut impl Write,
    entries: &[Entry],
    format: Format,
    group_by: GroupBy,
    display: Option<&DisplayOptions>,
) -> io::Result<()> {
    match group_by {
        GroupBy::Extension => {
            let groups = group::group_by_extension(entries);
            for (ext, group) in &groups {
                writeln!(w, "\n[{}]", ext.unwrap_or("(no extension)"))?;
                write_flat(w, group, format, display)?;
            }
        }
        GroupBy::Directory => {
            let groups = group::group_by_directory(entries);
            for (dir, group) in &groups {
                let label = if dir.is_empty() { "." } else { dir };
                writeln!(w, "\n[{}]", label)?;
                write_flat(w, group, format, display)?;
            }
        }
        GroupBy::Depth => {
            let groups = group::group_by_depth(entries);
            for (depth, group) in &groups {
                writeln!(w, "\n[depth {}]", depth)?;
                write_flat(w, group, format, display)?;
            }
        }
    }
    Ok(())
}