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