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::apps::AppScanner;
use crate::search::SearchEngine;
use crate::types::{EntryType, FileEntry, SearchResult};

use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use walkdir::WalkDir;

pub struct FileIndex {
    entries: Vec<FileEntry>,
    cache_file: PathBuf,
    include_apps: bool,
    search_engine: SearchEngine,
    app_scanner: AppScanner,
}

impl FileIndex {
    pub fn new(include_apps: bool) -> Self {
        let cache_dir = dirs::cache_dir()
            .unwrap_or_else(|| PathBuf::from("."))
            .join("listary");
        
        if !cache_dir.exists() {
            fs::create_dir_all(&cache_dir).ok();
        }
        
        Self {
            entries: Vec::new(),
            cache_file: cache_dir.join("file_index.json"),
            include_apps,
            search_engine: SearchEngine::new(),
            app_scanner: AppScanner::new(),
        }
    }

    pub fn build_index(&mut self, paths: &[PathBuf]) {
        self.entries.clear();
        
        // Index files and directories
        self.scan_files_and_directories(paths);

        // Index applications if requested
        if self.include_apps {
            self.app_scanner.scan_applications(&mut self.entries);
        }

        // Save index to cache file
        self.save_cache();
    }

    pub fn search(&self, query: &str, limit: usize) -> Vec<SearchResult> {
        self.search_engine.search(&self.entries, query, limit)
    }

    pub fn find_single_application_match(&self, query: &str) -> Option<&FileEntry> {
        self.search_engine.find_single_application_match(&self.entries, query)
    }

    pub fn find_exact_application_match(&self, query: &str) -> Option<&FileEntry> {
        self.search_engine.find_exact_application_match(&self.entries, query)
    }

    pub fn launch_application(&self, entry: &FileEntry) -> Result<(), String> {
        self.app_scanner.launch_application(entry)
    }

    pub fn open_entry(&self, entry: &FileEntry) -> Result<(), String> {
        self.app_scanner.open_entry(entry)
    }

    pub fn load_cache(&mut self) -> bool {
        if let Ok(json) = fs::read_to_string(&self.cache_file) {
            if let Ok(entries) = serde_json::from_str::<Vec<FileEntry>>(&json) {
                self.entries = entries;
                return true;
            }
        }
        false
    }

    pub fn entries_count(&self) -> usize {
        self.entries.len()
    }

    fn scan_files_and_directories(&mut self, paths: &[PathBuf]) {
        for base_path in paths {
            if !base_path.exists() {
                continue;
            }

            for entry in WalkDir::new(base_path)
                .follow_links(false)
                .max_depth(10)
                .into_iter()
                .filter_map(|e| e.ok())
            {
                let path = entry.path();
                
                // Skip hidden files and system files
                if self.should_skip_file(path) {
                    continue;
                }

                let metadata = match entry.metadata() {
                    Ok(m) => m,
                    Err(_) => continue,
                };

                let modified = self.get_modified_time(&metadata);
                let name = self.get_file_name(path);

                if !name.is_empty() {
                    self.entries.push(FileEntry {
                        path: path.to_path_buf(),
                        name,
                        size: metadata.len(),
                        modified,
                        entry_type: if metadata.is_dir() { 
                            EntryType::Directory 
                        } else { 
                            EntryType::File 
                        },
                        executable_path: None,
                        description: None,
                    });
                }
            }
        }
    }

    fn should_skip_file(&self, path: &std::path::Path) -> bool {
        path.file_name()
            .and_then(|n| n.to_str())
            .map(|n| n.starts_with('.'))
            .unwrap_or(false)
    }

    fn get_modified_time(&self, metadata: &std::fs::Metadata) -> u64 {
        metadata
            .modified()
            .unwrap_or(SystemTime::UNIX_EPOCH)
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs()
    }

    fn get_file_name(&self, path: &std::path::Path) -> String {
        path.file_name()
            .and_then(|n| n.to_str())
            .unwrap_or("")
            .to_string()
    }

    fn save_cache(&self) {
        if let Ok(json) = serde_json::to_string(&self.entries) {
            fs::write(&self.cache_file, json).ok();
        }
    }
}