Skip to main content

systemprompt_logging/services/cli/
service.rs

1//! The [`CliService`] facade over CLI output.
2//!
3//! Aggregates the display, table, and progress helpers into a single entry
4//! point for command code: levelled messages (which also publish a log event),
5//! structured output (`json`/`yaml`), and spinners and progress bars. Output is
6//! the sanctioned stderr/stdout sink, not `tracing`.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11use std::io::Write;
12use std::time::Duration;
13
14use indicatif::{ProgressBar, ProgressStyle};
15use serde::Serialize;
16
17use super::display;
18use super::output::OutputState;
19use super::theme::{EmphasisType, ItemStatus, MessageLevel, Theme};
20
21#[derive(Copy, Clone, Debug)]
22pub struct CliService;
23
24impl CliService {
25    pub fn success(message: &str) {
26        display::message(MessageLevel::Success, message);
27    }
28
29    pub fn warning(message: &str) {
30        display::message(MessageLevel::Warning, message);
31    }
32
33    pub fn error(message: &str) {
34        display::message(MessageLevel::Error, message);
35    }
36
37    pub fn info(message: &str) {
38        display::message(MessageLevel::Info, message);
39    }
40
41    pub fn debug(message: &str) {
42        let debug_msg = format!("DEBUG: {message}");
43        display::message(MessageLevel::Info, &debug_msg);
44    }
45
46    pub fn verbose(message: &str) {
47        display::message(MessageLevel::Info, message);
48    }
49
50    pub fn fatal(message: &str, exit_code: i32) -> ! {
51        let fatal_msg = format!("FATAL: {message}");
52        display::message(MessageLevel::Error, &fatal_msg);
53        std::process::exit(exit_code);
54    }
55
56    pub fn section(title: &str) {
57        display::section_header(title);
58    }
59
60    pub fn subsection(title: &str) {
61        display::subsection_header(title);
62    }
63
64    pub fn clear_screen() {
65        let mut stderr = std::io::stderr();
66        if let Err(e) = write!(stderr, "\x1B[2J\x1B[1;1H") {
67            tracing::debug!(error = %e, "cli clear_screen write failed");
68        }
69    }
70
71    pub fn output(content: &str) {
72        let mut stdout = std::io::stdout();
73        if let Err(e) = writeln!(stdout, "{content}") {
74            tracing::debug!(error = %e, "cli output write failed");
75        }
76    }
77
78    pub fn json_string<T: Serialize + ?Sized>(value: &T) -> Result<String, serde_json::Error> {
79        serde_json::to_string_pretty(value)
80    }
81
82    pub fn json<T: Serialize>(value: &T) {
83        OutputState::current().mark_structured_emitted();
84        match Self::json_string(value) {
85            Ok(json) => {
86                let mut stdout = std::io::stdout();
87                if let Err(e) = writeln!(stdout, "{json}") {
88                    tracing::debug!(error = %e, "cli json write failed");
89                }
90            },
91            Err(e) => Self::error(&format!("Failed to format log entry: {e}")),
92        }
93    }
94
95    pub fn json_compact<T: Serialize>(value: &T) {
96        OutputState::current().mark_structured_emitted();
97        match serde_json::to_string(value) {
98            Ok(json) => {
99                let mut stdout = std::io::stdout();
100                if let Err(e) = writeln!(stdout, "{json}") {
101                    tracing::debug!(error = %e, "cli json_compact write failed");
102                }
103            },
104            Err(e) => Self::error(&format!("Failed to format log entry: {e}")),
105        }
106    }
107
108    pub fn yaml<T: Serialize>(value: &T) {
109        OutputState::current().mark_structured_emitted();
110        match serde_yaml::to_string(value) {
111            Ok(yaml) => {
112                let mut stdout = std::io::stdout();
113                if let Err(e) = write!(stdout, "{yaml}") {
114                    tracing::debug!(error = %e, "cli yaml write failed");
115                }
116            },
117            Err(e) => Self::error(&format!("Failed to format log entry: {e}")),
118        }
119    }
120
121    pub fn key_value(label: &str, value: &str) {
122        let mut stderr = std::io::stderr();
123        if let Err(e) = writeln!(
124            stderr,
125            "{}: {}",
126            Theme::color(label, EmphasisType::Bold),
127            Theme::color(value, EmphasisType::Highlight)
128        ) {
129            tracing::debug!(error = %e, "cli key_value write failed");
130        }
131    }
132
133    pub fn status_line(label: &str, value: &str, status: ItemStatus) {
134        let mut stderr = std::io::stderr();
135        if let Err(e) = writeln!(
136            stderr,
137            "{} {}: {}",
138            Theme::icon(status),
139            Theme::color(label, EmphasisType::Bold),
140            Theme::color(value, status)
141        ) {
142            tracing::debug!(error = %e, "cli status_line write failed");
143        }
144    }
145
146    pub fn spinner(message: &str) -> ProgressBar {
147        let pb = ProgressBar::new_spinner();
148        pb.set_style(
149            ProgressStyle::default_spinner()
150                .template("{spinner:.cyan} {msg}")
151                .unwrap_or_else(|_| ProgressStyle::default_spinner()),
152        );
153        pb.set_message(message.to_owned());
154        pb.enable_steady_tick(Duration::from_millis(100));
155        pb
156    }
157
158    pub fn progress_bar(total: u64) -> ProgressBar {
159        let pb = ProgressBar::new(total);
160        pb.set_style(
161            ProgressStyle::default_bar()
162                .template(
163                    "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}",
164                )
165                .unwrap_or_else(|_| ProgressStyle::default_bar())
166                .progress_chars("#>-"),
167        );
168        pb
169    }
170
171    pub fn table(headers: &[&str], rows: &[Vec<String>]) {
172        super::table::render_table(headers, rows);
173    }
174}