Skip to main content

palladium_cli/
output.rs

1use serde_json::Value;
2
3/// Format a list of actor infos as a human-readable table.
4pub fn format_actor_table(actors: &[Value]) -> String {
5    if actors.is_empty() {
6        return "No actors found.\n".to_string();
7    }
8    let mut out = format!(
9        "{:<40} {:<10} {:>5}  {:<14} {:>10} {:>12}\n",
10        "PATH", "STATE", "CORE", "MAILBOX", "MESSAGES", "COMPUTE"
11    );
12    out.push_str(&"-".repeat(97));
13    out.push('\n');
14    for a in actors {
15        let path = a["path"].as_str().unwrap_or("-");
16        let state = a["state"].as_str().unwrap_or("-");
17        let core_id = a["core_id"].as_u64().unwrap_or(0);
18        let depth = a["mailbox_depth"].as_u64().unwrap_or(0);
19        let cap = a["mailbox_capacity"].as_u64().unwrap_or(0);
20        let msgs = a["message_count"].as_u64().unwrap_or(0);
21        let compute = a["total_compute_time_ns"].as_u64().unwrap_or(0);
22        let mailbox = format!("{depth}/{cap}");
23        out.push_str(&format!(
24            "{:<40} {:<10} {:>5}  {:<14} {:>10} {:>12}\n",
25            path,
26            state,
27            core_id,
28            mailbox,
29            msgs,
30            format_nanos(compute)
31        ));
32    }
33    out
34}
35
36/// Format a single actor's info as a key-value block.
37pub fn format_actor_info(info: &Value) -> String {
38    let mut out = String::new();
39    out.push_str(&format!(
40        "Path:       {}\n",
41        info["path"].as_str().unwrap_or("-")
42    ));
43    out.push_str(&format!(
44        "State:      {}\n",
45        info["state"].as_str().unwrap_or("-")
46    ));
47    out.push_str(&format!(
48        "Core:       {}\n",
49        info["core_id"].as_u64().unwrap_or(0)
50    ));
51    out.push_str(&format!(
52        "Mailbox:    {}/{}\n",
53        info["mailbox_depth"].as_u64().unwrap_or(0),
54        info["mailbox_capacity"].as_u64().unwrap_or(0),
55    ));
56    out.push_str(&format!(
57        "Messages:   {}\n",
58        info["message_count"].as_u64().unwrap_or(0)
59    ));
60    out.push_str(&format!(
61        "Restarts:   {}\n",
62        info["restart_count"].as_u64().unwrap_or(0)
63    ));
64    out.push_str(&format!(
65        "Compute:    {}\n",
66        format_nanos(info["total_compute_time_ns"].as_u64().unwrap_or(0))
67    ));
68    out
69}
70
71fn format_nanos(ns: u64) -> String {
72    if ns < 1000 {
73        format!("{ns}ns")
74    } else if ns < 1_000_000 {
75        format!("{:.2}μs", ns as f64 / 1000.0)
76    } else if ns < 1_000_000_000 {
77        format!("{:.2}ms", ns as f64 / 1_000_000.0)
78    } else {
79        format!("{:.2}s", ns as f64 / 1_000_000_000.0)
80    }
81}
82
83/// Format mailbox depth/capacity for `pd msg inspect`.
84pub fn format_mailbox_info(info: &Value) -> String {
85    let path = info["path"].as_str().unwrap_or("-");
86    let depth = info["mailbox_depth"].as_u64().unwrap_or(0);
87    let cap = info["mailbox_capacity"].as_u64().unwrap_or(0);
88    format!("{path}: mailbox {depth}/{cap}\n")
89}
90
91/// Format engine status as a key-value block.
92pub fn format_status(result: &Value) -> String {
93    let mut out = String::new();
94    out.push_str("Status:   running\n");
95    let uptime_secs = result["uptime_secs"].as_u64().unwrap_or(0);
96    out.push_str(&format!("Uptime:   {}\n", format_uptime(uptime_secs)));
97    out.push_str(&format!(
98        "Cores:    {}\n",
99        result["num_cores"].as_u64().unwrap_or(0)
100    ));
101    out.push_str(&format!(
102        "Actors:   {}/{} running\n",
103        result["running_actors"].as_u64().unwrap_or(0),
104        result["total_actors"].as_u64().unwrap_or(0),
105    ));
106    out
107}
108
109fn format_uptime(total_secs: u64) -> String {
110    let days = total_secs / 86_400;
111    let hours = (total_secs % 86_400) / 3_600;
112    let minutes = (total_secs % 3_600) / 60;
113    let seconds = total_secs % 60;
114    format!("{days}d {hours:02}h {minutes:02}m {seconds:02}s")
115}
116
117/// Format cluster status as a key-value block.
118pub fn format_cluster_status(result: &Value) -> String {
119    let mut out = String::new();
120    out.push_str("Cluster: running\n");
121    if let Some(local) = result["local_engine"].as_str() {
122        out.push_str(&format!("Local:   {local}\n"));
123    }
124    out.push_str(&format!(
125        "Members: {}\n",
126        result["member_count"].as_u64().unwrap_or(0)
127    ));
128    out.push_str(&format!(
129        "Alive:   {}\n",
130        result["alive"].as_u64().unwrap_or(0)
131    ));
132    out.push_str(&format!(
133        "Suspect: {}\n",
134        result["suspect"].as_u64().unwrap_or(0)
135    ));
136    out.push_str(&format!(
137        "Dead:    {}\n",
138        result["dead"].as_u64().unwrap_or(0)
139    ));
140    out.push_str(&format!(
141        "Left:    {}\n",
142        result["left"].as_u64().unwrap_or(0)
143    ));
144    out
145}
146
147/// Format cluster members as a human-readable table.
148pub fn format_cluster_members(members: &[Value]) -> String {
149    if members.is_empty() {
150        return "No cluster members found.\n".to_string();
151    }
152    let mut out = format!(
153        "{:<22} {:<21} {:<8} {:>5} {:>10} {:>8}\n",
154        "ENGINE", "ADDR", "STATE", "GEN", "LAST_MS", "VER"
155    );
156    out.push_str(&"-".repeat(80));
157    out.push('\n');
158    for m in members {
159        let engine = m["engine_id"].as_str().unwrap_or("-");
160        let addr = m["addr"].as_str().unwrap_or("-");
161        let state = m["state"].as_str().unwrap_or("-");
162        let gen = m["generation"].as_u64().unwrap_or(0);
163        let last = m["last_heartbeat_ms"].as_u64().unwrap_or(0);
164        let ver = m["version"].as_u64().unwrap_or(0);
165        out.push_str(&format!(
166            "{:<22} {:<21} {:<8} {:>5} {:>10} {:>8}\n",
167            engine, addr, state, gen, last, ver
168        ));
169    }
170    out
171}