use crate::types::SearchResponse;
use comfy_table::{modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL, ContentArrangement, Table};
use owo_colors::OwoColorize;
use std::io::IsTerminal;
pub fn render(response: &SearchResponse) {
let use_color = std::io::stdout().is_terminal();
if response.results.is_empty() && response.answers.is_empty() {
if use_color {
eprintln!("{}", "No results found.".yellow());
} else {
eprintln!("No results found.");
}
return;
}
if response.metadata.cached {
let age = response
.metadata
.cache_age_secs
.map(|s| format!(" ({s}s old)"))
.unwrap_or_default();
if use_color {
eprintln!("{}", format!(" cached result{age}").yellow());
} else {
eprintln!(" cached result{age}");
}
}
for answer in &response.answers {
if use_color {
println!("{} {}", "answer".on_green().black().bold(), answer.provider.cyan());
println!(" {}", truncate(&answer.text, 600));
println!();
} else {
println!("[answer via {}]", answer.provider);
println!(" {}", truncate(&answer.text, 600));
println!();
}
}
for warning in &response.metadata.warnings {
if use_color {
eprintln!(" {} {}", "!".yellow().bold(), warning.yellow());
} else {
eprintln!(" ! {warning}");
}
}
if use_color {
eprintln!(
"\n{} {} results for {} [mode: {}]",
"search".bold().cyan(),
response.metadata.result_count.to_string().bold(),
format!("\"{}\"", response.query).white().bold(),
response.mode.green(),
);
eprintln!();
}
for (i, result) in response.results.iter().enumerate() {
let num = format!(" {} ", i + 1);
let title = &result.title;
let url = &result.url;
let snippet = truncate(&result.snippet, 200);
let source = &result.source;
if use_color {
println!("{} {}", num.on_cyan().black().bold(), title.bold(),);
println!(" {} {}", "->".dimmed(), url.blue().underline());
if !snippet.is_empty() {
println!(" {}", snippet.dimmed());
}
let mut meta_parts = vec![format!("via {}", source.cyan())];
if let Some(pub_date) = &result.published {
meta_parts.push(pub_date.dimmed().to_string());
}
println!(" {}", meta_parts.join(" "));
println!();
} else {
println!("[{}] {}", i + 1, title);
println!(" {}", url);
if !snippet.is_empty() {
println!(" {}", snippet);
}
println!(" [{}]", source);
println!();
}
}
if use_color {
eprintln!(
"{}",
format!(
" {} results from {} in {}ms",
response.metadata.result_count,
response
.metadata
.providers_queried
.iter()
.map(|p| p.cyan().to_string())
.collect::<Vec<_>>()
.join(", "),
response.metadata.elapsed_ms,
)
.dimmed()
);
} else {
eprintln!(
" {} results from {} in {}ms",
response.metadata.result_count,
response.metadata.providers_queried.join(", "),
response.metadata.elapsed_ms,
);
}
if !response.metadata.provider_failures.is_empty() {
for f in &response.metadata.provider_failures {
let line = match f.http_status {
Some(s) => format!(
" failed: {} [{} · {}] {}",
f.provider,
f.category.as_str(),
s,
f.reason
),
None => format!(
" failed: {} [{}] {}",
f.provider,
f.category.as_str(),
f.reason
),
};
if use_color {
eprintln!("{}", line.red());
} else {
eprintln!("{line}");
}
}
} else if !response.metadata.providers_failed.is_empty() {
let names = response.metadata.providers_failed.join(", ");
if use_color {
eprintln!(" {} {}", "failed:".red(), names.red());
} else {
eprintln!(" failed: {names}");
}
}
eprintln!();
}
fn truncate(s: &str, max: usize) -> String {
let cleaned: String = s
.chars()
.map(|c| {
if c == '\n' || c == '\r' || c == '\t' {
' '
} else {
c
}
})
.collect::<String>()
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
if cleaned.chars().count() <= max {
cleaned
} else {
let kept: String = cleaned.chars().take(max.saturating_sub(1)).collect();
format!("{kept}…")
}
}
pub fn render_providers(providers: &[(String, bool, Vec<String>)]) {
let use_color = std::io::stdout().is_terminal();
if use_color {
eprintln!("\n{} Provider Status\n", "search".bold().cyan());
}
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.apply_modifier(UTF8_ROUND_CORNERS)
.set_content_arrangement(ContentArrangement::Dynamic);
table.set_header(vec!["Provider", "Status", "Capabilities"]);
for (name, configured, caps) in providers {
let status = if *configured {
if use_color {
"OK".green().to_string()
} else {
"OK".to_string()
}
} else if use_color {
"NOT SET".red().to_string()
} else {
"NOT SET".to_string()
};
let name_display = if use_color {
name.bold().to_string()
} else {
name.clone()
};
table.add_row(vec![name_display, status, caps.join(", ")]);
}
println!("{table}");
let configured_count = providers.iter().filter(|(_, c, _)| *c).count();
if use_color {
eprintln!(
"\n {}/{} providers configured",
configured_count.to_string().bold(),
providers.len()
);
} else {
eprintln!(
"\n {}/{} providers configured",
configured_count,
providers.len()
);
}
eprintln!();
}