systemprompt_cli/shared/command_result/
render.rs1use systemprompt_logging::CliService;
15use systemprompt_models::artifacts::{
16 ChartArtifact, CliArtifact, ListArtifact, PresentationCardArtifact, TableArtifact,
17};
18
19use systemprompt_models::text::truncate_with_ellipsis;
20
21use super::CommandOutput;
22use crate::cli_settings::{CliConfig, OutputFormat};
23
24pub fn render_result(result: &CommandOutput, config: &CliConfig) {
25 match config.output_format() {
26 OutputFormat::Json => CliService::json(result.artifact()),
27 OutputFormat::Yaml => CliService::yaml(result.artifact()),
28 OutputFormat::Table if result.should_skip_render() => {},
29 OutputFormat::Table => render_terminal(result),
30 }
31}
32
33fn render_terminal(result: &CommandOutput) {
34 if let Some(title) = result.title() {
35 CliService::section(title);
36 }
37
38 match result.artifact() {
39 CliArtifact::Text { artifact } => {
40 if result.title().is_none()
41 && let Some(title) = &artifact.title
42 {
43 CliService::section(title);
44 }
45 CliService::output(&artifact.content);
46 },
47 CliArtifact::CopyPasteText { artifact } => {
48 if result.title().is_none()
49 && let Some(title) = &artifact.title
50 {
51 CliService::section(title);
52 }
53 CliService::output(&artifact.content);
54 },
55 CliArtifact::Table { artifact } => render_table(artifact),
56 CliArtifact::List { artifact } => render_list(artifact),
57 CliArtifact::PresentationCard { artifact } => render_card(artifact),
58 CliArtifact::Dashboard { artifact } => {
59 if result.title().is_none() {
60 CliService::section(&artifact.title);
61 }
62 if let Some(description) = &artifact.description {
63 CliService::output(description);
64 }
65 },
66 CliArtifact::Chart { artifact } => render_chart(artifact),
67 CliArtifact::Audio { artifact } => CliService::output(&artifact.src),
68 CliArtifact::Image { artifact } => CliService::output(&artifact.src),
69 CliArtifact::Video { artifact } => CliService::output(&artifact.src),
70 CliArtifact::Message { artifact } => {
71 for line in &artifact.messages {
72 match line.level.as_str() {
73 "success" => CliService::success(&line.text),
74 "warning" => CliService::warning(&line.text),
75 "error" => CliService::error(&line.text),
76 _ => CliService::info(&line.text),
77 }
78 }
79 },
80 }
81}
82
83fn render_table(artifact: &TableArtifact) {
84 let headers: Vec<&str> = artifact
85 .columns
86 .iter()
87 .map(|c| c.label.as_deref().unwrap_or(&c.name))
88 .collect();
89
90 let rows: Vec<Vec<String>> = artifact
91 .items
92 .iter()
93 .map(|item| {
94 artifact
95 .columns
96 .iter()
97 .map(|col| {
98 item.get(&col.name)
99 .map_or_else(String::new, |v| cell_display_within(v, col.width))
100 })
101 .collect()
102 })
103 .collect();
104
105 CliService::table(&headers, &rows);
106}
107
108fn render_list(artifact: &ListArtifact) {
109 for item in &artifact.items {
110 CliService::subsection(&item.title);
111 if !item.summary.is_empty() {
112 CliService::output(&item.summary);
113 }
114 if !item.link.is_empty() {
115 CliService::output(&item.link);
116 }
117 }
118}
119
120fn render_card(artifact: &PresentationCardArtifact) {
121 CliService::section(&artifact.title);
122 if let Some(subtitle) = &artifact.subtitle {
123 CliService::output(subtitle);
124 }
125 for section in &artifact.sections {
126 CliService::subsection(§ion.heading);
127 CliService::output(§ion.content_display());
128 }
129}
130
131fn render_chart(artifact: &ChartArtifact) {
132 let mut headers: Vec<&str> = vec!["label"];
133 for dataset in &artifact.datasets {
134 headers.push(&dataset.label);
135 }
136
137 let rows: Vec<Vec<String>> = artifact
138 .labels
139 .iter()
140 .enumerate()
141 .map(|(row, label)| {
142 let mut cells = vec![label.clone()];
143 for dataset in &artifact.datasets {
144 cells.push(
145 dataset
146 .data
147 .get(row)
148 .map_or_else(String::new, ToString::to_string),
149 );
150 }
151 cells
152 })
153 .collect();
154
155 CliService::table(&headers, &rows);
156}
157
158fn cell_display(value: &serde_json::Value) -> String {
160 match value {
161 serde_json::Value::String(s) => s.clone(),
162 serde_json::Value::Null => String::new(),
163 other => other.to_string(),
164 }
165}
166
167fn cell_display_within(value: &serde_json::Value, width: Option<usize>) -> String {
169 let rendered = cell_display(value);
170 match width {
171 Some(width) => truncate_with_ellipsis(&rendered, width),
172 None => rendered,
173 }
174}