121 lines
3.8 KiB
Rust
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
|
|
}
|
|
}
|