maram 0.2.0

A modern, high-performance alternative to the Unix tree command
Documentation
//! maram - A modern, high-performance alternative to the Unix tree command
//!
//! This crate provides a fast and feature-rich directory tree visualization tool
//! with advanced features like per-nested-path limits, inline file sizes and line counts,
//! filtering, sorting, search, and beautiful size distribution visualizations.
//!
//! # Features
//!
//! - **Fast filesystem traversal**: Custom iterative walker with parallelism
//! - **Rich filtering**: Regex patterns, size ranges, time-based filters
//! - **Beautiful output**: ASCII/Unicode tree with colors and inline details
//! - **Size distribution**: Visual charts showing disk usage by type/extension
//! - **Line counting**: Fast parallel line counting for text files
//! - **Configuration**: Support for .maram.toml config files
//! - **Cross-platform**: Works on Linux, macOS, and Windows

pub mod cli;
pub mod config;
pub mod error;
pub mod filters;
pub mod formatter;
pub mod stats;
pub mod walker;

pub use cli::Args;
pub use config::Config;
pub use error::{Error, Result};
pub use filters::{FilterOptions, SortBy};
pub use formatter::{DistributionFormat, DistributionType, FormatOptions, OutputFormat};
pub use stats::{FileStats, TreeStats};
pub use walker::{TreeEntry, Walker};

use std::io::{self, Write};
use std::path::Path;

/// High-level options for using maram as a library
///
/// This API is independent of the CLI and suitable for embedding.
#[derive(Debug, Clone)]
pub struct MaramOptions {
    /// Output format
    pub output: OutputFormat,
    /// Filtering options
    pub filter: FilterOptions,
    /// Formatting options
    pub format: FormatOptions,
    /// Number of threads (0 = auto)
    pub threads: usize,
    /// Max file size for line counting
    pub max_file_size: u64,
    /// Whether to compute total size summary (Tree output)
    pub total_size: bool,
    /// Whether to compute directory sizes recursively
    pub dir_sizes: bool,
    /// Optional size distribution request
    pub distribution: Option<(DistributionType, usize /* top N */, DistributionFormat)>,
}

impl Default for MaramOptions {
    fn default() -> Self {
        // Auto-detect color like CLI; can be overridden via format.color
        let color = atty::is(atty::Stream::Stdout) && std::env::var("NO_COLOR").is_err();
        Self {
            output: OutputFormat::Tree,
            filter: FilterOptions::default(),
            format: FormatOptions {
                unicode: true,
                color,
                full_path: false,
                show_size: true,
                show_lines: false,
                dir_sizes: false,
            },
            threads: 0,
            max_file_size: 1_073_741_824, // 1GB
            total_size: false,
            dir_sizes: false,
            distribution: None,
        }
    }
}

impl MaramOptions {
    /// Enable line counting up to a maximum file size
    pub fn with_line_counting(mut self, max_size: u64) -> Self {
        self.format.show_lines = true;
        self.max_file_size = max_size;
        self
    }

    /// Enable recursive directory size calculation
    pub fn with_dir_sizes(mut self) -> Self {
        self.dir_sizes = true;
        self.format.dir_sizes = true;
        self
    }

    /// Enable total size summary for tree output
    pub fn with_total_size(mut self) -> Self {
        self.total_size = true;
        self
    }
}

/// Generate formatted output as a String for a given path and options
pub fn generate(path: impl AsRef<Path>, options: &MaramOptions) -> Result<String> {
    let mut buffer: Vec<u8> = Vec::with_capacity(8192);
    generate_to_writer(path, options, &mut buffer)?;
    Ok(String::from_utf8_lossy(&buffer).into_owned())
}

/// Generate formatted output to a writer for a given path and options
pub fn generate_to_writer(
    path: impl AsRef<Path>,
    options: &MaramOptions,
    out: &mut dyn Write,
) -> Result<()> {
    let path = path.as_ref();
    log::debug!("Starting generation at: {:?}", path);

    // Decide if we need buffered tree
    let needs_buffering = matches!(options.output, OutputFormat::Json | OutputFormat::Csv)
        || options.distribution.is_some()
        || options.total_size
        || options.dir_sizes
        || options.filter.sort_by.is_some();

    if !needs_buffering {
        // Streaming path (Tree/Plain)
        let mut stream = walker::StreamWalker::with_writer(
            options.filter.clone(),
            options.output,
            options.format.show_size,
            options.format.show_lines,
            options.format.unicode,
            options.format.color,
            Box::new(WriterAdapter(out)),
        );
        return stream.stream(path);
    }

    // Buffered path for advanced features
    let mut walker = Walker::new(path, options.filter.clone(), options.threads)?;
    walker.set_max_file_size(options.max_file_size);
    if options.format.show_lines {
        walker.enable_line_counting();
    }
    if options.dir_sizes || options.format.dir_sizes {
        walker.enable_dir_sizes();
    }
    let entries = walker.walk()?;

    match options.output {
        OutputFormat::Tree => formatter::write_tree(out, &entries, &options.format)?,
        OutputFormat::Json => formatter::write_json(out, &entries)?,
        OutputFormat::Csv => formatter::write_csv(out, &entries)?,
        OutputFormat::Plain => formatter::write_plain(out, &entries)?,
    }

    if options.total_size && matches!(options.output, OutputFormat::Tree) {
        let stats = TreeStats::from_entries(&entries);
        formatter::write_total_size(out, &stats, &options.format)?;
    }

    if let Some((dist_type, top, dist_fmt)) = &options.distribution {
        formatter::write_distribution(out, &entries, dist_type, *top, dist_fmt, &options.format)?;
    }

    Ok(())
}

/// Collect the structured entries without rendering
pub fn collect(path: impl AsRef<Path>, options: &MaramOptions) -> Result<Vec<TreeEntry>> {
    let path = path.as_ref();
    let mut walker = Walker::new(path, options.filter.clone(), options.threads)?;
    walker.set_max_file_size(options.max_file_size);
    if options.format.show_lines {
        walker.enable_line_counting();
    }
    if options.dir_sizes || options.format.dir_sizes {
        walker.enable_dir_sizes();
    }
    walker.walk()
}

/// Render previously collected entries as a String using the given options
pub fn render(entries: &[TreeEntry], options: &MaramOptions) -> Result<String> {
    let mut buffer: Vec<u8> = Vec::with_capacity(8192);
    render_to_writer(entries, options, &mut buffer)?;
    Ok(String::from_utf8_lossy(&buffer).into_owned())
}

/// Render previously collected entries to a writer using the given options
pub fn render_to_writer(
    entries: &[TreeEntry],
    options: &MaramOptions,
    out: &mut dyn Write,
) -> Result<()> {
    match options.output {
        OutputFormat::Tree => formatter::write_tree(out, entries, &options.format)?,
        OutputFormat::Json => formatter::write_json(out, entries)?,
        OutputFormat::Csv => formatter::write_csv(out, entries)?,
        OutputFormat::Plain => formatter::write_plain(out, entries)?,
    }
    if options.total_size && matches!(options.output, OutputFormat::Tree) {
        let stats = TreeStats::from_entries(entries);
        formatter::write_total_size(out, &stats, &options.format)?;
    }
    if let Some((dist_type, top, dist_fmt)) = &options.distribution {
        formatter::write_distribution(out, entries, dist_type, *top, dist_fmt, &options.format)?;
    }
    Ok(())
}

/// Adapter to treat a &mut dyn Write as a boxed writer
struct WriterAdapter<'a>(&'a mut dyn Write);
impl<'a> Write for WriterAdapter<'a> {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        self.0.write(buf)
    }
    fn flush(&mut self) -> io::Result<()> {
        self.0.flush()
    }
}

/// Main entry point for the maram tree visualization
///
/// # Arguments
///
/// * `path` - The root path to start traversing from
/// * `args` - Command line arguments parsed by clap
/// * `config` - Configuration loaded from .maram.toml (if exists)
///
/// # Returns
///
/// Returns Ok(()) on success, or an Error if something goes wrong
///
/// # Example
///
/// ```no_run
/// use maram::{run_tree, Args, Config};
/// use std::path::Path;
///
/// let args = Args::default();
/// let config = Config::default();
/// run_tree(Path::new("."), &args, &config).unwrap();
/// ```
/// CLI bridge: run the tree and print to stdout
pub fn run_tree(path: &Path, args: &Args, config: &Config) -> Result<()> {
    let output = run_tree_output(path, args, config)?;
    // Print exactly the produced output
    print!("{}", output);
    Ok(())
}

/// Produce CLI output as a String (no stdout side-effects)
pub fn run_tree_output(path: &Path, args: &Args, config: &Config) -> Result<String> {
    let filter_opts = FilterOptions::from_args_and_config(args, config)?;
    let format_opts = FormatOptions::from_args_and_config(args, config);

    let needs_buffering = matches!(args.output, OutputFormat::Json | OutputFormat::Csv)
        || args.dist.is_some()
        || args.total_size
        || args.dir_sizes
        || filter_opts.sort_by.is_some();

    let mut buffer: Vec<u8> = Vec::with_capacity(8192);
    if !needs_buffering {
        let unicode = args.unicode || config.display.unicode;
        let show_size = args.show_size || config.display.show_size;
        let show_lines = args.show_lines || config.display.show_lines;
        let mut stream_walker = walker::StreamWalker::with_writer(
            filter_opts,
            args.output,
            show_size,
            show_lines,
            unicode,
            format_opts.color,
            Box::new(WriterAdapter(&mut buffer)),
        );
        stream_walker.stream(path)?;
    } else {
        let mut walker = Walker::new(path, filter_opts, args.threads)?;
        walker.set_max_file_size(args.max_file_size);
        if args.show_lines {
            walker.enable_line_counting();
        }
        if args.dir_sizes {
            walker.enable_dir_sizes();
        }
        let entries = walker.walk()?;
        match args.output {
            OutputFormat::Tree => formatter::write_tree(&mut buffer, &entries, &format_opts)?,
            OutputFormat::Json => formatter::write_json(&mut buffer, &entries)?,
            OutputFormat::Csv => formatter::write_csv(&mut buffer, &entries)?,
            OutputFormat::Plain => formatter::write_plain(&mut buffer, &entries)?,
        }
        if args.total_size && matches!(args.output, OutputFormat::Tree) {
            let stats = TreeStats::from_entries(&entries);
            formatter::write_total_size(&mut buffer, &stats, &format_opts)?;
        }
        if let Some(dist_type) = &args.dist {
            formatter::write_distribution(
                &mut buffer,
                &entries,
                dist_type,
                args.top,
                &args.format,
                &format_opts,
            )?;
        }
    }
    Ok(String::from_utf8_lossy(&buffer).into_owned())
}