listary 0.0.1

A fast command-line file search utility inspired by Listary, with fuzzy search, application launching, and smart auto-open features
mod apps;
mod display;
mod history;
mod index;
mod search;
mod types;


use clap::{Arg, Command};
use colored::*;
use display::DisplayFormatter;
use history::SearchHistory;
use index::FileIndex;
use rustyline::error::ReadlineError;
use rustyline::{DefaultEditor, Result};
use std::path::PathBuf;
use types::{Config, EntryType, SearchResult};

fn parse_config() -> Config {
    let matches = Command::new("listary")
        .version("0.1.0")
        .author("qiutian00")
        .about("A CLI search utility like Listary - search files and applications")
        .arg(
            Arg::new("query")
                .help("Search query")
                .required(false)
                .index(1),
        )
        .arg(
            Arg::new("paths")
                .short('p')
                .long("paths")
                .help("Paths to search in (comma-separated)")
                .value_name("PATHS")
                .default_value("."),
        )
        .arg(
            Arg::new("limit")
                .short('l')
                .long("limit")
                .help("Maximum number of results")
                .value_name("NUMBER")
                .default_value("50"),
        )
        .arg(
            Arg::new("rebuild")
                .short('r')
                .long("rebuild")
                .help("Rebuild file index")
                .action(clap::ArgAction::SetTrue),
        )
        .arg(
            Arg::new("apps")
                .short('a')
                .long("apps")
                .help("Include installed applications in search")
                .action(clap::ArgAction::SetTrue),
        )
        .arg(
            Arg::new("launch")
                .long("launch")
                .help("Launch the first matching application")
                .value_name("APP_NAME"),
        )
        .arg(
            Arg::new("no-auto-open")
                .long("no-auto-open")
                .help("Disable auto-opening of single application matches")
                .action(clap::ArgAction::SetTrue),
        )
        .get_matches();

    let include_apps = matches.get_flag("apps");
    let rebuild = matches.get_flag("rebuild");
    let auto_open = !matches.get_flag("no-auto-open");
    
    let paths_str = matches.get_one::<String>("paths").unwrap();
    let search_paths: Vec<PathBuf> = paths_str
        .split(',')
        .map(|p| PathBuf::from(p.trim()))
        .collect();

    let limit: usize = matches
        .get_one::<String>("limit")
        .unwrap()
        .parse()
        .unwrap_or(50);

    Config {
        include_apps,
        limit,
        rebuild,
        search_paths,
        auto_open,
    }
}

fn handle_launch_command(index: &FileIndex, app_name: &str) {
    let results = index.search(app_name, 1);
    if let Some(result) = results.first() {
        if matches!(result.entry.entry_type, EntryType::Application) {
            index.launch_application(&result.entry).ok();
        } else {
            println!("{}", "Found item is not an application.".red());
        }
    } else {
        println!("{} '{}'", "No application found with name:".red(), app_name.cyan());
    }
}

fn handle_query_command(index: &FileIndex, formatter: &DisplayFormatter, query: &str, limit: usize) {
    let results = index.search(query, limit);
    formatter.print_results(&results);
}

fn handle_interactive_mode(index: &FileIndex, formatter: &DisplayFormatter, config: &Config) -> Result<()> {
    println!("\n{}", "🔍 Interactive Search Mode".cyan().bold());
    println!("{}", "Type your search query and press Enter. Use ↑↓ arrows for history.".bright_black());
    if config.auto_open && config.include_apps {
        println!("{}", "💡 Single application matches will auto-open automatically.".bright_black());
    }
    
    let mut rl = DefaultEditor::new()?;
    let mut search_history = SearchHistory::new(100);
    
    // Load existing history into rustyline
    for entry in search_history.get_history().iter().rev() {
        rl.add_history_entry(entry).ok();
    }
    
    let mut last_results: Vec<SearchResult> = Vec::new();
    
    loop {
        let readline = rl.readline(&format!("{} ", "search>".green().bold()));
        match readline {
            Ok(line) => {
                let query = line.trim();
                if query.is_empty() {
                    continue;
                }
                
                if query == "q" || query == "quit" {
                    println!("{}", "👋 Goodbye!".cyan());
                    break;
                }
                
                if query == "history" {
                    show_search_history(&search_history);
                    continue;
                }
                
                if query == "clear" {
                    search_history.clear();
                    let _ = rl.clear_history();
                    println!("{}", "✓ Search history cleared.".green());
                    continue;
                }
                
                // Check if input is a number (for opening files/applications)
                if let Ok(num) = query.parse::<usize>() {
                    handle_number_selection(index, &last_results, num);
                    continue;
                }
                
                // Add to history
                rl.add_history_entry(&line).ok();
                search_history.add_query(query);
                
                // Check for auto-open condition (single fuzzy match)
                if config.auto_open && config.include_apps {
                    if let Some(single_match) = index.find_single_application_match(query) {
                        println!("{} Found single application match: {}", 
                            "🎯".cyan(), 
                            single_match.name.yellow()
                        );
                        println!("{} Auto-opening...", "🚀".cyan());
                        
                        if let Err(e) = index.open_entry(single_match) {
                            println!("{}", e.red());
                        }
                        continue;
                    }
                }
                
                // Perform search
                let start = std::time::Instant::now();
                let results = index.search(query, config.limit);
                let duration = start.elapsed();
                
                formatter.print_results(&results);
                last_results = results;
                
                println!("\n{} in {:.2}ms", 
                    "Search completed".bright_black(),
                    duration.as_millis()
                );
                
                // Show interactive help
                formatter.print_interactive_help(last_results.len(), config.auto_open && config.include_apps);
            },
            Err(ReadlineError::Interrupted) => {
                println!("{}", "👋 Goodbye!".cyan());
                break;
            },
            Err(ReadlineError::Eof) => {
                println!("{}", "👋 Goodbye!".cyan());
                break;
            },
            Err(err) => {
                println!("Error: {:?}", err);
                break;
            }
        }
    }
    
    Ok(())
}

fn show_search_history(history: &SearchHistory) {
    let entries = history.get_history();
    if entries.is_empty() {
        println!("{}", "📜 No search history available.".yellow());
    } else {
        println!("\n{}", "📜 Search History:".cyan().bold());
        println!("{}", "".repeat(50).bright_black());
        for (i, entry) in entries.iter().enumerate() {
            println!("  {}. {}", (i + 1).to_string().bright_blue(), entry.cyan());
        }
        println!("{}", "".repeat(50).bright_black());
    }
}

fn handle_number_selection(index: &FileIndex, last_results: &[SearchResult], num: usize) {
    if num > 0 && num <= last_results.len() {
        let result = &last_results[num - 1];
        let entry = &result.entry;
        
        println!("{} {}...", "🚀 Opening".cyan(), entry.name.yellow());
        
        if let Err(e) = index.open_entry(entry) {
            println!("{}", e.red());
        }
    } else {
        if last_results.is_empty() {
            println!("{}", "⚠️  No search results available. Please search first.".yellow());
        } else {
            println!("{} Please enter a number between 1 and {}.", 
                "⚠️  Invalid selection.".yellow(), 
                last_results.len().to_string().cyan()
            );
        }
    }
}

fn main() {
    let config = parse_config();
    let mut index = FileIndex::new(config.include_apps);
    let formatter = DisplayFormatter::new();
    
    // Build or load index
    if config.rebuild || !index.load_cache() {
        println!("{}", "Building file index...".yellow());
        index.build_index(&config.search_paths);
        println!("{} indexed {} files", "".green(), index.entries_count().to_string().cyan());
    } else {
        println!("{} loaded {} files from cache", "".green(), index.entries_count().to_string().cyan());
    }

    // Parse command line arguments again to check for specific commands
    let matches = Command::new("listary")
        .version("0.1.0")
        .author("qiutian00")
        .about("A CLI search utility like Listary - search files and applications")
        .arg(Arg::new("query").help("Search query").required(false).index(1))
        .arg(Arg::new("paths").short('p').long("paths").help("Paths to search in (comma-separated)").value_name("PATHS").default_value("."))
        .arg(Arg::new("limit").short('l').long("limit").help("Maximum number of results").value_name("NUMBER").default_value("50"))
        .arg(Arg::new("rebuild").short('r').long("rebuild").help("Rebuild file index").action(clap::ArgAction::SetTrue))
        .arg(Arg::new("apps").short('a').long("apps").help("Include installed applications in search").action(clap::ArgAction::SetTrue))
        .arg(Arg::new("launch").long("launch").help("Launch the first matching application").value_name("APP_NAME"))
        .get_matches();

    // Handle launch command
    if let Some(app_name) = matches.get_one::<String>("launch") {
        handle_launch_command(&index, app_name);
        return;
    }

    // Handle direct query
    if let Some(query) = matches.get_one::<String>("query") {
        handle_query_command(&index, &formatter, query, config.limit);
        return;
    }

    // Interactive mode
    if let Err(e) = handle_interactive_mode(&index, &formatter, &config) {
        eprintln!("Error in interactive mode: {}", e);
        std::process::exit(1);
    }
}