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