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::mark_structured_emitted;
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<T: Serialize>(value: &T) {
79        mark_structured_emitted();
80        match serde_json::to_string_pretty(value) {
81            Ok(json) => {
82                let mut stdout = std::io::stdout();
83                if let Err(e) = writeln!(stdout, "{json}") {
84                    tracing::debug!(error = %e, "cli json write failed");
85                }
86            },
87            Err(e) => Self::error(&format!("Failed to format log entry: {e}")),
88        }
89    }
90
91    pub fn json_compact<T: Serialize>(value: &T) {
92        mark_structured_emitted();
93        match serde_json::to_string(value) {
94            Ok(json) => {
95                let mut stdout = std::io::stdout();
96                if let Err(e) = writeln!(stdout, "{json}") {
97                    tracing::debug!(error = %e, "cli json_compact write failed");
98                }
99            },
100            Err(e) => Self::error(&format!("Failed to format log entry: {e}")),
101        }
102    }
103
104    pub fn yaml<T: Serialize>(value: &T) {
105        mark_structured_emitted();
106        match serde_yaml::to_string(value) {
107            Ok(yaml) => {
108                let mut stdout = std::io::stdout();
109                if let Err(e) = write!(stdout, "{yaml}") {
110                    tracing::debug!(error = %e, "cli yaml write failed");
111                }
112            },
113            Err(e) => Self::error(&format!("Failed to format log entry: {e}")),
114        }
115    }
116
117    pub fn key_value(label: &str, value: &str) {
118        let mut stderr = std::io::stderr();
119        if let Err(e) = writeln!(
120            stderr,
121            "{}: {}",
122            Theme::color(label, EmphasisType::Bold),
123            Theme::color(value, EmphasisType::Highlight)
124        ) {
125            tracing::debug!(error = %e, "cli key_value write failed");
126        }
127    }
128
129    pub fn status_line(label: &str, value: &str, status: ItemStatus) {
130        let mut stderr = std::io::stderr();
131        if let Err(e) = writeln!(
132            stderr,
133            "{} {}: {}",
134            Theme::icon(status),
135            Theme::color(label, EmphasisType::Bold),
136            Theme::color(value, status)
137        ) {
138            tracing::debug!(error = %e, "cli status_line write failed");
139        }
140    }
141
142    pub fn spinner(message: &str) -> ProgressBar {
143        let pb = ProgressBar::new_spinner();
144        pb.set_style(
145            ProgressStyle::default_spinner()
146                .template("{spinner:.cyan} {msg}")
147                .unwrap_or_else(|_| ProgressStyle::default_spinner()),
148        );
149        pb.set_message(message.to_owned());
150        pb.enable_steady_tick(Duration::from_millis(100));
151        pb
152    }
153
154    pub fn progress_bar(total: u64) -> ProgressBar {
155        let pb = ProgressBar::new(total);
156        pb.set_style(
157            ProgressStyle::default_bar()
158                .template(
159                    "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}",
160                )
161                .unwrap_or_else(|_| ProgressStyle::default_bar())
162                .progress_chars("#>-"),
163        );
164        pb
165    }
166
167    pub fn table(headers: &[&str], rows: &[Vec<String>]) {
168        super::table::render_table(headers, rows);
169    }
170}