phrona-cli 0.1.1

Command-line interface to the phrona library: search, suggest, extract, ground, probe
use phrona::{Category, EngineReport, ResultItem, SearchResponse};

pub fn result_line(kind: &str, position: usize, title: &str, url: &str, meta: &str) {
    println!(
        "{position:>3}. [{kind:6}] {title}\n     {url}{}",
        if meta.is_empty() {
            String::new()
        } else {
            format!("\n     {meta}")
        }
    );
}

pub fn print_response(r: &SearchResponse) {
    println!(
        "query: {}\ncategory: {} | page: {} | results: {} | elapsed: {} ms",
        r.query,
        r.category.as_str(),
        r.page,
        r.total,
        r.elapsed_ms
    );
    if let Some(answer) = r.answer.as_deref().filter(|a| !a.trim().is_empty()) {
        println!("\nanswer: {answer}");
    }
    if !r.suggestions.is_empty() {
        println!("\nsuggestions: {}", r.suggestions.join(" | "));
    }
    for (i, item) in r.results.iter().enumerate() {
        print_item(i + 1, item);
    }
    if !r.engines.is_empty() {
        println!("\nengines:");
        for e in &r.engines {
            println!(
                "  {:16} {:6} {:>4} results{}",
                e.name,
                e.status,
                e.results,
                e.error
                    .as_deref()
                    .map(|err| format!("  ({err})"))
                    .unwrap_or_default()
            );
        }
    }
}

pub fn print_item(position: usize, item: &ResultItem) {
    match item {
        ResultItem::Web(w) => result_line("web", position, &w.title, &w.url, &w.description),
        ResultItem::Image(i) => result_line(
            "image",
            position,
            &i.title,
            &i.url,
            &format!("{} ({}x{})", i.image_url, i.width, i.height),
        ),
        ResultItem::News(n) => result_line(
            "news",
            position,
            &n.title,
            &n.url,
            &format!(
                "{}{}",
                n.source,
                n.published
                    .as_deref()
                    .map(|p| format!(" - {p}"))
                    .unwrap_or_default()
            ),
        ),
        ResultItem::Video(v) => result_line(
            "video",
            position,
            &v.title,
            &v.url,
            &format!(
                "{} | {} views | {}{}",
                v.uploader,
                v.views,
                v.duration,
                v.published
                    .as_deref()
                    .map(|p| format!(" | {p}"))
                    .unwrap_or_default()
            ),
        ),
        ResultItem::Book(b) => result_line(
            "book",
            position,
            &b.title,
            &b.url,
            &format!("{} | {}{}", b.author, b.publisher, b.info),
        ),
    }
}

pub fn print_engines_table(category: Category) {
    let names: Vec<String> = phrona::available_engines(category)
        .iter()
        .map(|e| e.name.clone())
        .collect();
    println!(
        "{} ({}): {}",
        category.as_str(),
        names.len(),
        names.join(", ")
    );
}

pub fn print_test_report(reports: Vec<(Category, SearchResponse)>) {
    let mut matrix: Vec<EngineReport> = Vec::new();
    let mut printed = std::collections::BTreeSet::new();
    for (cat, resp) in &reports {
        println!(
            "category: {:8} total: {:>3} elapsed: {:>4} ms  answer: {}",
            cat.as_str(),
            resp.total,
            resp.elapsed_ms,
            resp.answer.as_deref().is_some_and(|a| !a.is_empty())
        );
        for e in &resp.engines {
            matrix.push(e.clone());
        }
    }
    println!("\navailability matrix:");
    for e in &matrix {
        if printed.insert(e.name.clone()) {
            println!(
                "  {:16} {:6}",
                e.name,
                if e.status == "ok" {
                    "OK".to_string()
                } else {
                    e.status.clone()
                }
            );
        }
    }
}

pub fn print_grounded(query: &str, resp: &SearchResponse, max_results: usize) {
    let answer = resp
        .answer
        .clone()
        .unwrap_or_else(|| format!("Found {} sources for \"{query}\".", resp.total));
    println!("query: {query}\nanswer: {answer}");
    let mut shown = 0;
    for (i, item) in resp.results.iter().enumerate() {
        if shown >= max_results {
            break;
        }
        let (title, url, content) = match item {
            ResultItem::Web(w) => (&w.title, &w.url, &w.description.as_str()),
            ResultItem::News(n) => (&n.title, &n.url, &n.description.as_str()),
            ResultItem::Video(v) => (&v.title, &v.url, &v.description.as_str()),
            ResultItem::Image(im) => (&im.title, &im.url, &im.source.as_str()),
            ResultItem::Book(b) => (&b.title, &b.url, &b.info.as_str()),
        };
        if content.trim().is_empty() {
            continue;
        }
        println!("\n{}. {title}\n   {url}\n   {content}", i + 1);
        shown += 1;
    }
}