listary 0.0.1

A fast command-line file search utility inspired by Listary, with fuzzy search, application launching, and smart auto-open features
use crate::types::{EntryType, SearchResult};
use colored::*;
use std::collections::HashSet;

pub struct DisplayFormatter;

impl DisplayFormatter {
    pub fn new() -> Self {
        Self
    }

    pub fn print_results(&self, results: &[SearchResult]) {
        if results.is_empty() {
            println!("{}", "No results found.".red());
            return;
        }

        println!("\n{} {}:", "Found".green().bold(), results.len().to_string().cyan().bold());
        println!("{}", "".repeat(80).bright_black());

        for (i, result) in results.iter().enumerate() {
            let entry = &result.entry;
            let highlighted_name = self.highlight_matches(&entry.name, &result.matched_indices);
            
            let file_type = self.get_file_type_icon(&entry.entry_type, &entry.path);
            let size_str = self.format_entry_size(&entry.entry_type, entry.size);

            println!(
                "{:2}. {} {} {} {}",
                (i + 1).to_string().bright_blue(),
                file_type,
                highlighted_name,
                size_str,
                entry.path.parent()
                    .and_then(|p| p.to_str())
                    .unwrap_or("")
                    .bright_black()
            );
        }
    }

    pub fn print_interactive_help(&self, results_count: usize, auto_open_enabled: bool) {
        if results_count > 0 {
            println!("\n{}", "".repeat(80).bright_black());
            println!("{}", "💡 Actions:".yellow().bold());
            println!("  {} Type a number (1-{}) to open the file/application", "".cyan(), results_count);
            println!("  {} Type a new search query to search again", "".cyan());
            if auto_open_enabled {
                println!("  {} Single app matches will auto-open", "".cyan());
            }
            println!("  {} Use ↑↓ arrow keys to browse search history", "".cyan());
            println!("  {} Type 'history' to view search history", "".cyan());
            println!("  {} Type 'clear' to clear search history", "".cyan());
            println!("  {} Type 'q' or 'quit' to exit", "".cyan());
            println!("  {} Press Ctrl+C to exit", "".cyan());
        } else {
            let auto_hint = if auto_open_enabled { ", single app matches auto-open" } else { "" };
            println!("{}", format!("Type a new search query{}, use ↑↓ for history, or 'q' to quit.", auto_hint).bright_black());
        }
    }

    fn highlight_matches(&self, text: &str, indices: &[usize]) -> String {
        let mut result = String::new();
        let chars: Vec<char> = text.chars().collect();
        let index_set: HashSet<usize> = indices.iter().copied().collect();

        for (i, &ch) in chars.iter().enumerate() {
            if index_set.contains(&i) {
                result.push_str(&ch.to_string().yellow().bold().to_string());
            } else {
                result.push(ch);
            }
        }
        result
    }

    fn get_file_type_icon(&self, entry_type: &EntryType, path: &std::path::PathBuf) -> String {
        match entry_type {
            EntryType::Directory => "📁".to_string(),
            EntryType::Application => "🚀".to_string(),
            EntryType::File => {
                match path.extension().and_then(|s| s.to_str()) {
                    Some("txt") | Some("md") | Some("rst") => "📄",
                    Some("rs") | Some("py") | Some("js") | Some("ts") | Some("go") | Some("java") => "💻",
                    Some("png") | Some("jpg") | Some("jpeg") | Some("gif") | Some("svg") => "🖼️",
                    Some("mp3") | Some("wav") | Some("flac") | Some("ogg") => "🎵",
                    Some("mp4") | Some("avi") | Some("mkv") | Some("mov") => "🎬",
                    Some("zip") | Some("rar") | Some("7z") | Some("tar") | Some("gz") => "📦",
                    Some("pdf") => "📕",
                    Some("exe") | Some("msi") => "⚙️",
                    _ => "📄",
                }.to_string()
            }
        }
    }

    fn format_entry_size(&self, entry_type: &EntryType, size: u64) -> String {
        match entry_type {
            EntryType::Directory => "<DIR>".bright_black().to_string(),
            EntryType::Application => "<APP>".bright_green().to_string(),
            EntryType::File => self.format_file_size(size).bright_black().to_string(),
        }
    }

    fn format_file_size(&self, size: u64) -> String {
        const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
        let mut size = size as f64;
        let mut unit_index = 0;

        while size >= 1024.0 && unit_index < UNITS.len() - 1 {
            size /= 1024.0;
            unit_index += 1;
        }

        if unit_index == 0 {
            format!("{} {}", size as u64, UNITS[unit_index])
        } else {
            format!("{:.1} {}", size, UNITS[unit_index])
        }
    }
}