Skip to main content

krait/output/
human.rs

1use std::fmt::Write;
2
3use crate::protocol::Response;
4
5/// Format response as human-readable, aligned output for terminal use.
6#[must_use]
7pub fn format(response: &Response) -> String {
8    if let Some(error) = &response.error {
9        let mut out = format!("Error: {}\nCode:  {}", error.message, error.code);
10        if let Some(advice) = &error.advice {
11            let _ = write!(out, "\n\nAdvice: {advice}");
12        }
13        return out;
14    }
15
16    let Some(data) = &response.data else {
17        return String::new();
18    };
19
20    // Status response
21    if let Some(daemon) = data.get("daemon") {
22        let pid = daemon
23            .get("pid")
24            .and_then(serde_json::Value::as_u64)
25            .unwrap_or(0);
26        let uptime = daemon
27            .get("uptime_secs")
28            .and_then(serde_json::Value::as_u64)
29            .unwrap_or(0);
30        let mut out = format!(
31            "Daemon Status\n  PID:    {pid}\n  Uptime: {}",
32            format_duration_human(uptime)
33        );
34
35        if let Some(lsp) = data.get("lsp") {
36            if !lsp.is_null() {
37                let lang = lsp.get("language").and_then(|v| v.as_str()).unwrap_or("?");
38                let status = lsp.get("status").and_then(|v| v.as_str()).unwrap_or("?");
39                let server = lsp.get("server").and_then(|v| v.as_str()).unwrap_or("?");
40                let _ = write!(out, "\n\nLSP Server\n  Language: {lang}\n  Status:   {status}\n  Server:   {server}");
41            }
42        }
43
44        if let Some(project) = data.get("project") {
45            if let Some(root) = project.get("root").and_then(|v| v.as_str()) {
46                let _ = write!(out, "\n\nProject\n  Root:      {root}");
47            }
48            if let Some(langs) = project.get("languages").and_then(|v| v.as_array()) {
49                let names: Vec<&str> = langs.iter().filter_map(|v| v.as_str()).collect();
50                if !names.is_empty() {
51                    let _ = write!(out, "\n  Languages: {}", names.join(", "));
52                }
53            }
54        }
55
56        return out;
57    }
58
59    // Generic: pretty-printed JSON
60    serde_json::to_string_pretty(data).unwrap_or_default()
61}
62
63fn format_duration_human(secs: u64) -> String {
64    if secs < 60 {
65        format!("{secs} seconds")
66    } else if secs < 3600 {
67        let m = secs / 60;
68        if m == 1 {
69            "1 minute".into()
70        } else {
71            format!("{m} minutes")
72        }
73    } else {
74        let h = secs / 3600;
75        let m = (secs % 3600) / 60;
76        let h_str = if h == 1 {
77            "1 hour".into()
78        } else {
79            format!("{h} hours")
80        };
81        if m == 0 {
82            h_str
83        } else {
84            format!("{h_str} {m} minutes")
85        }
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use serde_json::json;
92
93    use super::*;
94
95    #[test]
96    fn human_status_output() {
97        let resp = Response::ok(json!({"daemon": {"pid": 12345, "uptime_secs": 300}}));
98        let out = format(&resp);
99        assert!(out.contains("Daemon Status"));
100        assert!(out.contains("PID:    12345"));
101        assert!(out.contains("Uptime: 5 minutes"));
102    }
103
104    #[test]
105    fn human_error_with_advice() {
106        let resp = Response::err_with_advice("lsp_not_found", "LSP not detected", "Install it");
107        let out = format(&resp);
108        assert!(out.contains("Error: LSP not detected"));
109        assert!(out.contains("Advice: Install it"));
110    }
111}