systemprompt_cli/shared/command_result/
mod.rs1mod output_types;
12mod render;
13
14pub use output_types::{KeyValueItem, KeyValueOutput, SuccessOutput, TableOutput, TextOutput};
15pub use render::render_result;
16pub use systemprompt_models::artifacts::ChartType;
17
18use serde::Serialize;
19use serde_json::Value as JsonValue;
20use systemprompt_models::artifacts::{
21 CardSection, ChartArtifact, CliArtifact, Column, ColumnType, CopyPasteTextArtifact,
22 DashboardArtifact, ListArtifact, ListItem, PresentationCardArtifact, TableArtifact,
23 TextArtifact,
24};
25
26#[derive(Debug, Clone)]
33pub struct CommandOutput {
34 artifact: CliArtifact,
35 title: Option<String>,
36 skip_render: bool,
37}
38
39impl CommandOutput {
40 #[must_use]
41 pub const fn new(artifact: CliArtifact) -> Self {
42 Self {
43 artifact,
44 title: None,
45 skip_render: false,
46 }
47 }
48
49 #[must_use]
50 pub const fn artifact(&self) -> &CliArtifact {
51 &self.artifact
52 }
53
54 #[must_use]
55 pub fn into_artifact(self) -> CliArtifact {
56 self.artifact
57 }
58
59 #[must_use]
60 pub const fn should_skip_render(&self) -> bool {
61 self.skip_render
62 }
63
64 #[must_use]
65 pub const fn with_skip_render(mut self) -> Self {
66 self.skip_render = true;
67 self
68 }
69
70 #[must_use]
71 pub fn with_title(mut self, title: impl Into<String>) -> Self {
72 self.title = Some(title.into());
73 self
74 }
75
76 #[must_use]
77 pub fn title(&self) -> Option<&str> {
78 self.title.as_deref()
79 }
80
81 #[must_use]
82 pub fn text(content: impl Into<String>) -> Self {
83 Self::new(CliArtifact::text(TextArtifact::new(content)))
84 }
85
86 #[must_use]
87 pub fn text_titled(title: impl Into<String>, content: impl Into<String>) -> Self {
88 let title = title.into();
89 Self::new(CliArtifact::text(
90 TextArtifact::new(content).with_title(title.clone()),
91 ))
92 .with_title(title)
93 }
94
95 #[must_use]
96 pub fn copy_paste(content: impl Into<String>) -> Self {
97 Self::new(CliArtifact::copy_paste_text(CopyPasteTextArtifact::new(
98 content,
99 )))
100 }
101
102 #[must_use]
103 pub fn copy_paste_titled(title: impl Into<String>, content: impl Into<String>) -> Self {
104 let title = title.into();
105 Self::new(CliArtifact::copy_paste_text(
106 CopyPasteTextArtifact::new(content).with_title(title.clone()),
107 ))
108 .with_title(title)
109 }
110
111 #[must_use]
112 pub fn table(columns: Vec<impl Into<String>>, rows: Vec<JsonValue>) -> Self {
113 let cols: Vec<Column> = columns
114 .into_iter()
115 .map(|c| Column::new(c, ColumnType::String))
116 .collect();
117 Self::new(CliArtifact::table(TableArtifact::new(cols).with_rows(rows)))
118 }
119
120 #[must_use]
123 pub fn table_of<T: Serialize>(columns: Vec<impl Into<String>>, items: &[T]) -> Self {
124 let rows: Vec<JsonValue> = items
125 .iter()
126 .map(|item| serde_json::to_value(item).unwrap_or(JsonValue::Null))
127 .collect();
128 Self::table(columns, rows)
129 }
130
131 #[must_use]
132 pub const fn table_artifact(artifact: TableArtifact) -> Self {
133 Self::new(CliArtifact::table(artifact))
134 }
135
136 #[must_use]
137 pub fn list(items: Vec<ListItem>) -> Self {
138 Self::new(CliArtifact::list(ListArtifact::new().with_items(items)))
139 }
140
141 #[must_use]
142 pub const fn card(card: PresentationCardArtifact) -> Self {
143 Self::new(CliArtifact::presentation_card(card))
144 }
145
146 #[must_use]
150 pub fn card_value(title: impl Into<String>, value: &impl Serialize) -> Self {
151 let sections = sections_from_value(&serde_json::to_value(value).unwrap_or(JsonValue::Null));
152 Self::card(PresentationCardArtifact::new(title).with_sections(sections))
153 }
154
155 #[must_use]
156 pub const fn chart(chart: ChartArtifact) -> Self {
157 Self::new(CliArtifact::chart(chart))
158 }
159
160 #[must_use]
161 pub const fn dashboard(dashboard: DashboardArtifact) -> Self {
162 Self::new(CliArtifact::dashboard(dashboard))
163 }
164}
165
166impl From<CliArtifact> for CommandOutput {
167 fn from(artifact: CliArtifact) -> Self {
168 Self::new(artifact)
169 }
170}
171
172fn sections_from_value(value: &JsonValue) -> Vec<CardSection> {
176 match value {
177 JsonValue::Object(map) => map
178 .iter()
179 .map(|(key, val)| CardSection::new(key.clone(), value_to_display(val)))
180 .collect(),
181 JsonValue::Null => Vec::new(),
182 other => vec![CardSection::new("Value", value_to_display(other))],
183 }
184}
185
186fn value_to_display(value: &JsonValue) -> String {
187 match value {
188 JsonValue::String(s) => s.clone(),
189 JsonValue::Null => String::new(),
190 JsonValue::Bool(_) | JsonValue::Number(_) => value.to_string(),
191 JsonValue::Array(_) | JsonValue::Object(_) => {
192 serde_json::to_string(value).unwrap_or_else(|_| value.to_string())
193 },
194 }
195}