//! search: keyword search over the catalog. use super::*; use crate::output::{print_json, Table}; use serde::Serialize; #[derive(Serialize)] struct SearchRow { name: String, display_name: String, category: Option, installable: bool, description: Option, website: Option, tags: Vec, } pub fn run(app: &App, keyword: &str, category: Option<&str>) -> Result { let hits = app.catalog.search(keyword, category); if app.json() { let rows: Vec = hits .iter() .map(|a| SearchRow { name: a.name.clone(), display_name: a.title().to_string(), category: a.category.clone(), installable: a.installable, description: a.description.clone(), website: a.website.clone(), tags: a.tags.clone(), }) .collect(); print_json(&rows); return Ok(0); } let mut table = Table::new(vec!["NAME", "CATEGORY", "INSTALLABLE", "DESCRIPTION"]); for a in hits { let desc = truncate(a.description.as_deref().unwrap_or(""), 56); table.row(vec![ a.title().to_string(), a.category.clone().unwrap_or_else(|| "—".to_string()), if a.installable { "yes" } else { "no" }.to_string(), desc, ]); } print!("{}", table.render()); if app.catalog.search(keyword, category).is_empty() { app.log .info(&format!("no agent matches '{keyword}' (categories: {})", app.catalog.categories().join(", "))); } Ok(0) } pub fn truncate(s: &str, max: usize) -> String { if s.chars().count() <= max { s.to_string() } else { let mut cut: String = s.chars().take(max - 1).collect(); cut.push('…'); cut } }