caliban_plugins/
overlay.rs1use crate::cli::ListedPlugin;
7
8#[must_use]
11pub fn render_overlay(rows: &[ListedPlugin]) -> Vec<String> {
12 if rows.is_empty() {
13 return vec![
14 "No plugins installed.".to_string(),
15 "Install one with `caliban plugin install <name>@<marketplace>` or".to_string(),
16 "drop a directory under `.caliban/plugins/<name>/`.".to_string(),
17 ];
18 }
19 let mut out = Vec::with_capacity(rows.len() + 1);
20 out.push(format!("{} plugin(s) installed:", rows.len()));
21 for r in rows {
22 let glyph = if r.enabled { '\u{25cf}' } else { '\u{25cb}' };
23 let status = if r.enabled { "" } else { " DISABLED" };
24 let summary = if r.summary.is_empty() {
25 String::new()
26 } else {
27 format!(" {}", r.summary)
28 };
29 out.push(format!(
30 " {glyph} {name} v{version} ({source}){status}{summary}",
31 name = r.name,
32 version = r.version,
33 source = r.source,
34 ));
35 }
36 out
37}
38
39#[cfg(test)]
40mod tests {
41 use super::*;
42
43 #[test]
44 fn renders_empty_overlay() {
45 let lines = render_overlay(&[]);
46 assert!(lines[0].contains("No plugins installed"));
47 }
48
49 #[test]
50 fn renders_plugin_list() {
51 let rows = vec![
52 ListedPlugin {
53 name: "demo".into(),
54 version: "1.0.0".into(),
55 source: "user".into(),
56 enabled: true,
57 summary: "2 skills".into(),
58 },
59 ListedPlugin {
60 name: "off".into(),
61 version: "0.1.0".into(),
62 source: "user".into(),
63 enabled: false,
64 summary: String::new(),
65 },
66 ];
67 let lines = render_overlay(&rows);
68 assert!(lines[0].contains("2 plugin(s) installed"));
69 let demo = lines.iter().find(|l| l.contains("demo")).unwrap();
70 assert!(demo.contains("\u{25cf}"));
71 assert!(demo.contains("v1.0.0"));
72 assert!(demo.contains("2 skills"));
73 let off = lines.iter().find(|l| l.contains("off")).unwrap();
74 assert!(off.contains("\u{25cb}"));
75 assert!(off.contains("DISABLED"));
76 }
77
78 #[test]
79 fn renders_invalid_plugin_with_error() {
80 let rows = vec![ListedPlugin {
81 name: "broken".into(),
82 version: "?".into(),
83 source: "user".into(),
84 enabled: false,
85 summary: "invalid: missing name field".into(),
86 }];
87 let lines = render_overlay(&rows);
88 let row = lines.iter().find(|l| l.contains("broken")).unwrap();
89 assert!(row.contains("invalid"));
90 }
91}