lla 0.3.7

A lightweight ls replacement
use crate::commands::args::Args;
use crate::config::Config;
use crate::error::Result;
use crate::filter::{
    CaseInsensitiveFilter, CompositeFilter, ExtensionFilter, FileFilter, FilterOperation,
    GlobFilter, PatternFilter, RegexFilter,
};
use crate::formatter::{
    DefaultFormatter, FileFormatter, FuzzyFormatter, GitFormatter, GridFormatter, LongFormatter,
    RecursiveFormatter, SizeMapFormatter, TableFormatter, TimelineFormatter, TreeFormatter,
};
use crate::lister::{BasicLister, FileLister, FuzzyLister, RecursiveLister};
use crate::plugin::PluginManager;
use crate::sorter::{AlphabeticalSorter, DateSorter, FileSorter, SizeSorter, SortOptions};
use lla_plugin_interface::proto::{DecoratedEntry, EntryMetadata};
use rayon::prelude::*;
use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::UNIX_EPOCH;

pub fn list_directory(
    args: &Args,
    plugin_manager: &mut PluginManager,
    config_error: Option<crate::error::LlaError>,
) -> Result<()> {
    if let Some(error) = config_error {
        eprintln!("Warning: {}", error);
    }

    for plugin in &args.enable_plugin {
        if let Err(e) = plugin_manager.enable_plugin(plugin) {
            eprintln!("Failed to enable plugin '{}': {}", plugin, e);
        }
    }
    for plugin in &args.disable_plugin {
        if let Err(e) = plugin_manager.disable_plugin(plugin) {
            eprintln!("Failed to disable plugin '{}': {}", plugin, e);
        }
    }

    let lister = create_lister(args);
    let sorter = create_sorter(args);
    let filter = create_filter(args);
    let formatter = create_formatter(args);
    let format = get_format(args);

    let decorated_files = list_and_decorate_files(args, &lister, &filter, plugin_manager, format)?;

    let decorated_files = if !args.tree_format && !args.recursive_format {
        sort_files(decorated_files, &sorter, args)?
    } else {
        decorated_files
    };

    let formatted_output =
        formatter.format_files(decorated_files.as_slice(), plugin_manager, args.depth)?;
    println!("{}", formatted_output);
    Ok(())
}

pub fn get_format(args: &Args) -> &'static str {
    if args.fuzzy_format {
        "fuzzy"
    } else if args.long_format {
        "long"
    } else if args.tree_format {
        "tree"
    } else if args.table_format {
        "table"
    } else if args.grid_format {
        "grid"
    } else if args.recursive_format {
        "recursive"
    } else {
        "default"
    }
}

pub fn convert_metadata(metadata: &std::fs::Metadata) -> EntryMetadata {
    EntryMetadata {
        size: metadata.len(),
        modified: metadata
            .modified()
            .map(|t| t.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs())
            .unwrap_or(0),
        accessed: metadata
            .accessed()
            .map(|t| t.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs())
            .unwrap_or(0),
        created: metadata
            .created()
            .map(|t| t.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs())
            .unwrap_or(0),
        is_dir: metadata.is_dir(),
        is_file: metadata.is_file(),
        is_symlink: metadata.is_symlink(),
        permissions: metadata.mode(),
        uid: metadata.uid(),
        gid: metadata.gid(),
    }
}

fn calculate_dir_size(path: &std::path::Path) -> std::io::Result<u64> {
    use rayon::prelude::*;

    if !path.is_dir() {
        return Ok(0);
    }

    let entries: Vec<_> = std::fs::read_dir(path)?.collect::<std::io::Result<_>>()?;

    entries
        .par_iter()
        .try_fold(
            || 0u64,
            |acc, entry| {
                let metadata = entry.metadata()?;
                if metadata.is_symlink() {
                    return Ok(acc);
                }

                let path = entry.path();
                let size = if metadata.is_dir() {
                    calculate_dir_size(&path)?
                } else {
                    metadata.len()
                };

                Ok(acc + size)
            },
        )
        .try_reduce(|| 0, |a, b| Ok(a + b))
}

pub fn list_and_decorate_files(
    args: &Args,
    lister: &Arc<dyn FileLister + Send + Sync>,
    filter: &Arc<dyn FileFilter + Send + Sync>,
    plugin_manager: &mut PluginManager,
    format: &str,
) -> Result<Vec<DecoratedEntry>> {
    let mut entries: Vec<DecoratedEntry> = lister
        .list_files(
            &args.directory,
            args.tree_format || args.recursive_format,
            args.depth,
        )?
        .into_par_iter()
        .filter_map(|path| {
            let fs_metadata = path.metadata().ok()?;
            let mut metadata = convert_metadata(&fs_metadata);

            if args.include_dirs && metadata.is_dir {
                if let Ok(dir_size) = calculate_dir_size(&path) {
                    metadata.size = dir_size;
                }
            }

            if !filter
                .filter_files(std::slice::from_ref(&path))
                .map(|v| !v.is_empty())
                .unwrap_or(false)
            {
                return None;
            }

            Some(DecoratedEntry {
                path: path.to_string_lossy().into_owned(),
                metadata: Some(metadata),
                custom_fields: Default::default(),
            })
        })
        .collect();

    for entry in &mut entries {
        plugin_manager.decorate_entry(entry, format);
    }

    Ok(entries)
}

pub fn sort_files(
    files: Vec<DecoratedEntry>,
    sorter: &Arc<dyn FileSorter + Send + Sync>,
    args: &Args,
) -> Result<Vec<DecoratedEntry>> {
    let mut entries_with_paths: Vec<(PathBuf, &DecoratedEntry)> = files
        .iter()
        .map(|entry| (PathBuf::from(&entry.path), entry))
        .collect();

    let options = SortOptions {
        reverse: args.sort_reverse,
        dirs_first: args.sort_dirs_first,
        case_sensitive: args.sort_case_sensitive,
        natural: args.sort_natural,
    };

    sorter.sort_files_with_metadata(&mut entries_with_paths, options)?;

    let sorted_files = entries_with_paths
        .into_iter()
        .map(|(_, entry)| entry)
        .cloned()
        .collect();

    Ok(sorted_files)
}

pub fn create_lister(args: &Args) -> Arc<dyn FileLister + Send + Sync> {
    if args.fuzzy_format {
        let config = Config::load(&Config::get_config_path()).unwrap_or_default();
        Arc::new(FuzzyLister::new(config))
    } else if args.tree_format || args.recursive_format {
        let config = Config::load(&Config::get_config_path()).unwrap_or_default();
        Arc::new(RecursiveLister::new(config))
    } else {
        Arc::new(BasicLister)
    }
}

pub fn create_sorter(args: &Args) -> Arc<dyn FileSorter + Send + Sync> {
    let sorter: Arc<dyn FileSorter + Send + Sync> = match args.sort_by.as_str() {
        "name" => Arc::new(AlphabeticalSorter),
        "size" => Arc::new(SizeSorter),
        "date" => Arc::new(DateSorter),
        _ => Arc::new(AlphabeticalSorter),
    };

    sorter
}

pub fn create_filter(args: &Args) -> Arc<dyn FileFilter + Send + Sync> {
    match &args.filter {
        Some(filter_str) => {
            if filter_str.contains(" AND ") {
                let mut composite = CompositeFilter::new(FilterOperation::And);
                for part in filter_str.split(" AND ") {
                    composite.add_filter(create_base_filter(part.trim(), !args.case_sensitive));
                }
                Arc::new(composite)
            } else if filter_str.contains(" OR ") {
                let mut composite = CompositeFilter::new(FilterOperation::Or);
                for part in filter_str.split(" OR ") {
                    composite.add_filter(create_base_filter(part.trim(), !args.case_sensitive));
                }
                Arc::new(composite)
            } else if filter_str.starts_with("NOT ") {
                let mut composite = CompositeFilter::new(FilterOperation::Not);
                composite.add_filter(create_base_filter(&filter_str[4..], !args.case_sensitive));
                Arc::new(composite)
            } else if filter_str.starts_with("XOR ") {
                let mut composite = CompositeFilter::new(FilterOperation::Xor);
                composite.add_filter(create_base_filter(&filter_str[4..], !args.case_sensitive));
                Arc::new(composite)
            } else {
                Arc::from(create_base_filter(filter_str, !args.case_sensitive))
            }
        }
        None => Arc::new(PatternFilter::new("".to_string())),
    }
}

fn create_base_filter(pattern: &str, case_insensitive: bool) -> Box<dyn FileFilter + Send + Sync> {
    let base_filter: Box<dyn FileFilter + Send + Sync> = if pattern.starts_with("regex:") {
        Box::new(RegexFilter::new(pattern[6..].to_string()))
    } else if pattern.starts_with("glob:") {
        Box::new(GlobFilter::new(pattern[5..].to_string()))
    } else if pattern.starts_with('.') {
        Box::new(ExtensionFilter::new(pattern[1..].to_string()))
    } else {
        Box::new(PatternFilter::new(pattern.to_string()))
    };

    if case_insensitive {
        Box::new(CaseInsensitiveFilter::new(base_filter))
    } else {
        base_filter
    }
}

pub fn create_formatter(args: &Args) -> Box<dyn FileFormatter> {
    if args.fuzzy_format {
        Box::new(FuzzyFormatter::new(args.show_icons))
    } else if args.long_format {
        Box::new(LongFormatter::new(args.show_icons))
    } else if args.tree_format {
        Box::new(TreeFormatter::new(args.show_icons))
    } else if args.table_format {
        Box::new(TableFormatter::new(args.show_icons))
    } else if args.grid_format {
        Box::new(GridFormatter::new(args.show_icons))
    } else if args.sizemap_format {
        Box::new(SizeMapFormatter::new(args.show_icons))
    } else if args.timeline_format {
        Box::new(TimelineFormatter::new(args.show_icons))
    } else if args.git_format {
        Box::new(GitFormatter::new(args.show_icons))
    } else if args.recursive_format {
        Box::new(RecursiveFormatter::new(args.show_icons))
    } else {
        Box::new(DefaultFormatter::new(args.show_icons))
    }
}