Skip to main content

caliban_plugins/
overlay.rs

1//! `/plugins` slash-overlay text renderer.
2//!
3//! Full interactive UI lands with ADR 0040 (slash-command registry).
4//! v1 returns a flat list of lines that the TUI overlay renders verbatim.
5
6use crate::cli::ListedPlugin;
7
8/// Render the `/plugins` overlay body. Returns one display line per
9/// installed plugin plus a header.
10#[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}