Files
agent-manager/src/commands/search_cmd.rs
T

121 lines
3.8 KiB
Rust

//! search: keyword search over the catalog.
use super::*;
use crate::output::{boxed_table, print_json};
use serde::Serialize;
#[derive(Serialize)]
struct SearchRow {
name: String,
display_name: String,
category: Option<String>,
installable: bool,
description: Option<String>,
website: Option<String>,
tags: Vec<String>,
/// Personal tags (issue #39).
personal_tags: Vec<String>,
favorite: bool,
}
pub fn run(app: &App, keyword: Option<&str>, category: Option<&str>, tag: Option<&str>) -> Result<i32> {
let keyword = keyword.unwrap_or("");
// An empty keyword (search --tag only) starts from the whole catalog.
let mut hits: Vec<&AgentDef> = if keyword.is_empty() {
app.catalog
.agents()
.iter()
.filter(|a| !a.hidden)
.filter(|a| category.map_or(true, |c| a.category.as_deref() == Some(c)))
.collect()
} else {
app.catalog.search(keyword, category)
};
// Personal tag filter (issue #39): keep agents carrying this tag.
if let Some(t) = tag {
let tagged = crate::commands::annotations_cmd::agents_with_tag(app, t);
if tagged.is_empty() {
app.log.info(&format!(
"no agent is tagged '{t}' — 'am tags' lists every personal tag"
));
return Ok(0);
}
hits.retain(|a| tagged.contains(&a.name));
}
if hits.is_empty() && tag.is_some() {
app.log.info(&format!(
"no agent matches the keyword and the tag '{}'",
tag.unwrap_or("")
));
return Ok(0);
}
if app.json() {
let rows: Vec<SearchRow> = hits
.iter()
.map(|a| {
let personal = crate::commands::annotations_cmd::tags_of(app, &a.name);
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(),
personal_tags: personal,
favorite: crate::commands::annotations_cmd::is_favorite(app, &a.name),
}
})
.collect();
print_json(&rows);
return Ok(0);
}
let mut table_rows = Vec::new();
for a in &hits {
let desc = truncate(a.description.as_deref().unwrap_or(""), 56);
let star = if crate::commands::annotations_cmd::is_favorite(app, &a.name) {
"★ "
} else {
""
};
table_rows.push(vec![
format!("{star}{}", a.title()),
a.category.clone().unwrap_or_else(|| "—".to_string()),
if a.installable { "yes" } else { "no" }.to_string(),
desc,
]);
}
print!(
"{}",
boxed_table(
vec!["NAME", "CATEGORY", "INSTALLABLE", "DESCRIPTION"],
table_rows,
app.theme(),
app.color(),
)
);
if hits.is_empty() && !keyword.is_empty() {
let suggestions = app.catalog.suggest(keyword);
if suggestions.is_empty() {
app.log
.info(&format!("no agent matches '{keyword}' (categories: {})", app.catalog.categories().join(", ")));
} else {
app.log.info(&format!(
"no agent matches '{keyword}' — vouliez-vous dire : {} ?",
suggestions.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
}
}