cargo_rail/output.rs
1//! Centralized output control for CLI commands.
2//!
3//! Provides consistent, ergonomic output handling with quiet mode support.
4//!
5//! # Categories
6//!
7//! **Critical** (always shown):
8//! - [`error!`] - Error messages: `error: something went wrong`
9//! - [`warn!`] - Warnings: `warning: deprecated option`
10//! - [`help!`] - Help hints: `help: try --force`
11//!
12//! **Informational** (suppressed with `--quiet`):
13//! - [`status!`] - Progress messages (no prefix)
14//! - [`note!`] - Notes: `note: config found at /path`
15
16use std::sync::atomic::{AtomicBool, Ordering};
17
18// Global State
19
20static QUIET: AtomicBool = AtomicBool::new(false);
21static JSON_MODE: AtomicBool = AtomicBool::new(false);
22
23/// Stable schema version for machine-readable command output envelopes.
24pub const MACHINE_OUTPUT_SCHEMA_VERSION: u32 = 1;
25
26/// Initialize output settings. Call once at startup.
27#[doc(hidden)]
28pub fn init(quiet: bool) {
29 QUIET.store(quiet, Ordering::Relaxed);
30}
31
32/// Check if quiet mode is enabled.
33pub fn is_quiet() -> bool {
34 QUIET.load(Ordering::Relaxed)
35}
36
37/// Check if JSON mode is enabled.
38pub fn is_json_mode() -> bool {
39 JSON_MODE.load(Ordering::Relaxed)
40}
41
42/// Enable JSON mode (automatically enables quiet mode).
43#[doc(hidden)]
44pub fn set_json_mode(json: bool) {
45 JSON_MODE.store(json, Ordering::Relaxed);
46 if json {
47 QUIET.store(true, Ordering::Relaxed);
48 }
49}
50
51/// Build a stable machine-readable JSON envelope.
52///
53/// The returned object always contains:
54/// - `schema_version`
55/// - `command`
56/// - `mode`
57/// - `result`
58/// - `exit_code`
59///
60/// If `payload` is an object, its keys are merged into the top-level envelope
61/// without overriding existing standard keys. Non-object payloads are stored in
62/// `payload`.
63pub fn machine_json_envelope(
64 command: &str,
65 mode: &str,
66 result: &str,
67 exit_code: i32,
68 payload: serde_json::Value,
69) -> serde_json::Value {
70 let mut out = serde_json::Map::new();
71 out.insert(
72 "schema_version".to_string(),
73 serde_json::Value::Number(serde_json::Number::from(MACHINE_OUTPUT_SCHEMA_VERSION)),
74 );
75 out.insert("command".to_string(), serde_json::Value::String(command.to_string()));
76 out.insert("mode".to_string(), serde_json::Value::String(mode.to_string()));
77 out.insert("result".to_string(), serde_json::Value::String(result.to_string()));
78 out.insert(
79 "exit_code".to_string(),
80 serde_json::Value::Number(serde_json::Number::from(exit_code)),
81 );
82
83 match payload {
84 serde_json::Value::Object(map) => {
85 for (key, value) in map {
86 if !out.contains_key(&key) {
87 out.insert(key, value);
88 }
89 }
90 }
91 other => {
92 out.insert("payload".to_string(), other);
93 }
94 }
95
96 serde_json::Value::Object(out)
97}
98
99// Critical Output (always shown)
100
101/// Print an error message to stderr.
102///
103/// Always shown, even in quiet mode. Adds `error: ` prefix.
104///
105/// ```no_run
106/// # fn main() {
107/// cargo_rail::error!("failed to read file");
108/// // Output: error: failed to read file
109/// # }
110/// ```
111#[macro_export]
112macro_rules! error {
113 ($($arg:tt)*) => {
114 eprintln!("error: {}", format_args!($($arg)*))
115 };
116}
117
118/// Print a warning message to stderr.
119///
120/// Always shown, even in quiet mode. Adds `warning: ` prefix.
121///
122/// ```no_run
123/// # fn main() {
124/// cargo_rail::warn!("deprecated option will be removed in v2.0");
125/// // Output: warning: deprecated option will be removed in v2.0
126/// # }
127/// ```
128#[macro_export]
129macro_rules! warn {
130 ($($arg:tt)*) => {
131 eprintln!("warning: {}", format_args!($($arg)*))
132 };
133}
134
135/// Print a help hint to stderr.
136///
137/// Always shown, even in quiet mode. Adds `help: ` prefix.
138/// Typically used after an error to suggest a fix.
139///
140/// ```no_run
141/// # fn main() {
142/// cargo_rail::error!("missing required argument");
143/// cargo_rail::help!("run with --help for usage");
144/// // Output:
145/// // error: missing required argument
146/// // help: run with --help for usage
147/// # }
148/// ```
149#[macro_export]
150macro_rules! help {
151 ($($arg:tt)*) => {
152 eprintln!("help: {}", format_args!($($arg)*))
153 };
154}
155
156/// Print a status/progress message to stderr.
157///
158/// Suppressed in quiet mode. No prefix added.
159/// Use for transient progress info like "analyzing...", "writing files...".
160///
161/// ```no_run
162/// # fn main() {
163/// # let crates = vec![1, 2, 3];
164/// cargo_rail::status!("analyzing {} crates...", crates.len());
165/// // Output: analyzing 3 crates...
166/// # }
167/// ```
168#[macro_export]
169macro_rules! status {
170 ($($arg:tt)*) => {
171 if !$crate::output::is_quiet() {
172 eprintln!($($arg)*)
173 }
174 };
175}
176
177/// Print a note to stderr.
178///
179/// Suppressed in quiet mode. Adds `note: ` prefix.
180/// Use for non-critical informational messages.
181///
182/// ```no_run
183/// # fn main() {
184/// # let path = std::path::Path::new("/project/.config/rail.toml");
185/// cargo_rail::note!("existing config found at {}", path.display());
186/// // Output: note: existing config found at /project/.config/rail.toml
187/// # }
188/// ```
189#[macro_export]
190macro_rules! note {
191 ($($arg:tt)*) => {
192 if !$crate::output::is_quiet() {
193 eprintln!("note: {}", format_args!($($arg)*))
194 }
195 };
196}
197
198/// Alias for [`status!`]. Use whichever reads better in context.
199#[macro_export]
200macro_rules! progress {
201 ($($arg:tt)*) => {
202 $crate::status!($($arg)*)
203 };
204}