a73x

src/output.rs

Ref:   Size: 4.3 KiB   History

//! Where a command's own result ends and a network operation's narration begins.
//!
//! A write command does two separable things: it records an event in the local
//! DAG, and — unless `collab.autoSync` says otherwise — it tries to publish that
//! event to every configured remote. They have different failure modes. The
//! local write either happened or it didn't; the push may fail for reasons that
//! say nothing about whether the event is safely recorded.
//!
//! Those two facts used to arrive as one stream, and worse, split across the
//! wrong pair of streams: the `Auto-syncing with...` banner went to stderr while
//! the fetch and push progress it introduced went to *stdout*, interleaved with
//! the command's own result. `issue open` printed the id you asked for and then
//! four lines of network traffic you did not, and a script capturing stdout got
//! all of it.
//!
//! So `sync` narrates through this module instead of printing directly. Run
//! normally, it prints to stdout as before. Run underneath a write command, the
//! whole narration moves to stderr and every line of it is prefixed, which
//! leaves stdout carrying the command's own result and nothing else, and makes
//! the network operation legible as a separate thing that happened afterwards.
//!
//! Keeping stdout clean is also what stops the `--json` gap (issue a6adfe39,
//! tracked separately) from getting worse: `--json` exists only on read
//! commands today, and read commands never auto-sync, so there is no overlap
//! yet. When a write command does grow `--json`, its stdout is already pure
//! result and the sync status is already separable — by stream, without any
//! caller having to learn a new envelope.

use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};

/// Set while an auto-sync triggered by a write command is running.
///
/// A plain atomic rather than anything scoped because the CLI is a single
/// process running one command to completion, and the alternative — threading
/// a writer through every function in `sync` — buys nothing here.
static IN_AUTO_SYNC: AtomicBool = AtomicBool::new(false);

/// Marks every line auto-sync emits, so a reader can tell at a glance which
/// output came from the command they ran and which came from the network
/// operation it triggered.
pub const AUTO_SYNC_PREFIX: &str = "auto-sync: ";

/// Run `f` with sync's narration redirected to stderr and prefixed.
///
/// Restores the previous channel afterwards, including on the error paths
/// inside `f`, so a failed sync cannot leave the process redirecting.
pub fn during_auto_sync<T>(f: impl FnOnce() -> T) -> T {
    IN_AUTO_SYNC.store(true, Ordering::Relaxed);
    let result = f();
    IN_AUTO_SYNC.store(false, Ordering::Relaxed);
    result
}

pub fn in_auto_sync() -> bool {
    IN_AUTO_SYNC.load(Ordering::Relaxed)
}

/// Emit a line of progress: stdout normally, prefixed stderr under auto-sync.
///
/// Multi-line text is prefixed per line rather than as a block, so no line of
/// an auto-sync ever appears unlabelled. Blank lines stay blank — a prefix on
/// its own reads as a truncated message rather than as the separator it is.
pub fn emit(text: &str) {
    if in_auto_sync() {
        emit_prefixed(&mut std::io::stderr().lock(), text);
    } else {
        let _ = writeln!(std::io::stdout().lock(), "{}", text);
    }
}

/// Emit a warning or error. Always stderr; prefixed under auto-sync so a
/// warning from the push cannot be mistaken for one from the command.
pub fn emit_err(text: &str) {
    let mut err = std::io::stderr().lock();
    if in_auto_sync() {
        emit_prefixed(&mut err, text);
    } else {
        let _ = writeln!(err, "{}", text);
    }
}

fn emit_prefixed(w: &mut impl Write, text: &str) {
    for line in text.split('\n') {
        if line.is_empty() {
            let _ = writeln!(w);
        } else {
            let _ = writeln!(w, "{}{}", AUTO_SYNC_PREFIX, line);
        }
    }
}

/// `println!` for progress that auto-sync must be able to redirect.
#[macro_export]
macro_rules! outln {
    () => { $crate::output::emit("") };
    ($($arg:tt)*) => { $crate::output::emit(&format!($($arg)*)) };
}

/// `eprintln!` for warnings that auto-sync must be able to label.
#[macro_export]
macro_rules! errln {
    () => { $crate::output::emit_err("") };
    ($($arg:tt)*) => { $crate::output::emit_err(&format!($($arg)*)) };
}