vyctor 0.1.0

A fast CLI tool for semantic file search using vector embeddings
Documentation
//! Implementation of the `vyctor lookup` command

use crate::cli::ensure_daemon_running;
use crate::config::{find_vyctor_root, load_config};
use crate::search::SearchEngine;
use anyhow::Result;
use colored::Colorize;

/// Run the lookup command
pub async fn run(
    query: &str,
    folder: Option<&str>,
    count: usize,
    full: bool,
    verbose: bool,
) -> Result<()> {
    let root = find_vyctor_root()?;
    let config = load_config()?;

    // Auto-start daemon if configured (ensures index stays fresh after this search)
    ensure_daemon_running(&root, &config)?;

    let engine = SearchEngine::new(&root, &config, verbose)?;

    println!("{} Searching for: {}", "".cyan(), query.italic());

    if let Some(folder) = folder {
        println!("  In folder: {}", folder.cyan());
    }

    // Show reranker info if enabled (only in verbose mode)
    if verbose {
        if let Some(model) = engine.reranker_model() {
            println!("  Reranker: {}", model.cyan());
        }
    }

    println!();

    let (results, timing) = engine.search(query, count, folder).await?;

    if results.is_empty() {
        println!("{} No results found.", "!".yellow());
        return Ok(());
    }

    // Build output string - detailed timing only in verbose mode
    if verbose {
        let timing_str = if let Some(rerank_time) = timing.rerank_time {
            format!(
                "embed: {}, search: {}, rerank: {}",
                format!("{:?}", timing.embed_time).dimmed(),
                format!("{:?}", timing.search_time).dimmed(),
                format!("{:?}", rerank_time).dimmed()
            )
        } else {
            format!(
                "embed: {}, search: {}",
                format!("{:?}", timing.embed_time).dimmed(),
                format!("{:?}", timing.search_time).dimmed()
            )
        };

        println!(
            "Found {} result{} in {} ({}):",
            results.len().to_string().green(),
            if results.len() == 1 { "" } else { "s" },
            format!("{:?}", timing.total_time).dimmed(),
            timing_str
        );
    } else {
        println!(
            "Found {} result{} in {}:",
            results.len().to_string().green(),
            if results.len() == 1 { "" } else { "s" },
            format!("{:?}", timing.total_time).dimmed(),
        );
    }
    println!();

    for (i, result) in results.iter().enumerate() {
        let score_pct = (result.score * 100.0) as i32;
        let score_str = format!("{}%", score_pct);

        // Color the score based on relevance
        let colored_score = if score_pct >= 80 {
            score_str.green()
        } else if score_pct >= 60 {
            score_str.yellow()
        } else {
            score_str.red()
        };

        println!(
            "{} {} [{}]",
            format!("{}.", i + 1).bold(),
            result.file_path.cyan(),
            colored_score
        );
        println!("   Lines {}-{}", result.start_line, result.end_line);

        if full {
            // Show full chunk content
            println!();
            for line in result.chunk_content.lines() {
                println!("   {}", line.dimmed());
            }
        } else {
            // Show preview (first 3 lines)
            let preview_lines: Vec<&str> = result.chunk_content.lines().take(3).collect();
            println!();
            for line in preview_lines {
                let truncated = if line.len() > 80 {
                    format!("{}...", &line[..77])
                } else {
                    line.to_string()
                };
                println!("   {}", truncated.dimmed());
            }
            let total_lines = result.chunk_content.lines().count();
            if total_lines > 3 {
                println!(
                    "   {} more lines...",
                    format!("(+{})", total_lines - 3).dimmed()
                );
            }
        }

        println!();
    }

    Ok(())
}