treer 0.1.3

Another 'tree' command provides better support for recursive searching and highlights match results.
Documentation
use std::{io, path::Path};

use clap::Parser;
use glob::Pattern;

use crate::cmd::traversal::print_tree;

#[derive(Default, Debug)]
pub struct Opts {
    pub show_hidden: bool,
    pub ascii: bool,
    pub exclude_patterns: Vec<Pattern>,
    pub full_path: bool,
    pub dir_only: bool,
    pub last_modify: bool,
    pub level: Option<u32>,
    pub no_indent: bool,
    pub print_size: bool,
    pub print_permissions: bool,
    pub patterns: Vec<Pattern>,
    pub sort_by_time: bool,
}

#[derive(Parser, Debug)]
pub struct Cmd {
    #[arg(default_value = ".", help = "Path to the directory.")]
    pub path: String,

    #[arg(short = 'a', long = "all", help = "All (include hidden) directories.")]
    pub show_hidden: bool,

    #[arg(short = 'A', long = "ascii", help = "Use ascii characters to indent.")]
    pub ascii: bool,

    #[arg(short = 'd', long = "directories", help = "List directories only.")]
    pub dir_only: bool,

    #[arg(short = 'D', long = "date", help = "Print last modification date.")]
    pub last_modify: bool,

    #[arg(short = 'f', long = "full", help = "Print full path prefix")]
    pub full_path: bool,

    #[arg(
        short = 'L',
        long = "level",
        help = "Descend only level directories deep."
    )]
    pub level: Option<u32>,

    #[arg(short = 'i', long = "no-indent", help = "Disable indentation.")]
    pub no_indent: bool,

    #[arg(
        short = 'I',
        long = "exclude",
        help = "Ignore files/folders that match the wild-card pattern. May have multiple -I."
    )]
    pub exclude: Vec<String>,

    #[arg(short = 's', long = "size", help = "Print file size.")]
    pub print_size: bool,

    #[arg(short = 'p', long = "permissions", help = "Print permissions.")]
    pub print_permissions: bool,

    #[arg(
        short = 'P',
        long = "pattern",
        help = "List only directories that match the wild-card pattern. May have multiple -P."
    )]
    pub pattern: Vec<String>,

    #[arg(short = 't', long = "time", help = "Sort by last modification time.")]
    pub sort_by_time: bool,
}

fn parse_glob_pattern(s: &str) -> Result<Pattern, String> {
    Pattern::new(s).map_err(|err| err.to_string())
}

fn cmd_to_opts(cmd: &Cmd) -> Result<Opts, String> {
    let glob_patterns: Vec<Pattern> = cmd
        .pattern
        .iter()
        .map(|p| parse_glob_pattern(p))
        .collect::<Result<Vec<_>, _>>()?;
    let exclude_patterns: Vec<Pattern> = cmd
        .exclude
        .iter()
        .map(|p| parse_glob_pattern(p))
        .collect::<Result<Vec<_>, _>>()?;
    Ok(Opts {
        show_hidden: cmd.show_hidden,
        ascii: cmd.ascii,
        exclude_patterns,
        full_path: cmd.full_path,
        dir_only: cmd.dir_only,
        print_permissions: cmd.print_permissions,
        last_modify: cmd.last_modify,
        level: cmd.level,
        no_indent: cmd.no_indent,
        print_size: cmd.print_size,
        patterns: glob_patterns,
        sort_by_time: cmd.sort_by_time,
    })
}

pub fn run() -> io::Result<()> {
    let cmd = Cmd::parse();
    let opts = cmd_to_opts(&cmd).map_err(|e| {
        io::Error::new(io::ErrorKind::InvalidInput, format!("Invalid pattern: {e}"))
    })?;

    let root_path = Path::new(&cmd.path);
    print_tree(root_path, &opts)
}