Skip to main content

saya_cli/commands/
output.rs

1use crate::render::{RenderFormat, TerminalEvent, render_event};
2use saya_types::ConnectionError;
3use std::cell::RefCell;
4
5// A thread-local capture buffer. When set, `emit` appends rendered output here
6// instead of writing to the process stdout/stderr, so tests can assert on the
7// exact bytes a command would have printed without racing the global file
8// descriptors under parallel test runs. Production code never sets it, so the
9// real CLI path is unchanged: `emit` prints exactly as before.
10thread_local! {
11    static CAPTURE: RefCell<Option<(String, String)>> = const { RefCell::new(None) };
12}
13
14pub fn emit(event: TerminalEvent, format: RenderFormat) {
15    let rendered = render_event(&event, format);
16    CAPTURE.with(|cell| match cell.borrow_mut().as_mut() {
17        Some((stdout, stderr)) => {
18            stdout.push_str(&rendered.stdout);
19            stderr.push_str(&rendered.stderr);
20        }
21        None => {
22            print!("{}", rendered.stdout);
23            eprint!("{}", rendered.stderr);
24        }
25    });
26}
27
28/// Begins capturing `emit` output on this thread. Pair with
29/// [`capture_output_take`]. Nested starts are a programming error and panic so a
30/// forgotten `take` cannot silently swallow a later test's output.
31pub fn capture_output_start() {
32    CAPTURE.with(|cell| {
33        let mut slot = cell.borrow_mut();
34        assert!(
35            slot.is_none(),
36            "capture_output_start called without a matching capture_output_take"
37        );
38        *slot = Some((String::new(), String::new()));
39    });
40}
41
42/// Returns and clears the (stdout, stderr) captured since
43/// [`capture_output_start`]; panics if capture was not started.
44pub fn capture_output_take() -> (String, String) {
45    CAPTURE.with(|cell| {
46        cell.borrow_mut()
47            .take()
48            .expect("capture_output_take called without a matching capture_output_start")
49    })
50}
51
52pub fn result(message: String, format: RenderFormat) -> Result<i32, Box<dyn std::error::Error>> {
53    emit(TerminalEvent::Result { message }, format);
54    Ok(0)
55}
56
57pub fn failure(
58    code: i32,
59    error: ConnectionError,
60    format: RenderFormat,
61) -> Result<i32, Box<dyn std::error::Error>> {
62    emit(
63        TerminalEvent::Error {
64            message: error.to_string(),
65        },
66        format,
67    );
68    Ok(code)
69}
70
71pub fn failure_message(
72    code: i32,
73    message: String,
74    format: RenderFormat,
75) -> Result<i32, Box<dyn std::error::Error>> {
76    emit(TerminalEvent::Error { message }, format);
77    Ok(code)
78}