git-workon 0.1.1

A git plugin for managing worktrees
//! Structured CLI output with color support.
//!
//! Provides semantic output functions that handle stream selection (stdout vs stderr)
//! and conditional color formatting automatically.
//!
//! ## Output Categories
//!
//! **stderr** (status/diagnostic — never interferes with piping):
//! - [`warn`] — yellow "Warning:" prefix, for non-fatal issues
//! - [`success`] — green text, for completed actions
//! - [`info`] — bold text, for section headers
//! - [`detail`] — dim text, for secondary information
//! - [`notice`] — yellow text, for dry-run/cancelled/skipped status
//! - [`status`] — plain text, for neutral status messages
//!
//! **stdout** (primary data — pipeable to fzf, grep, etc.):
//! - Use `println!()` directly for primary output
//! - Use [`style`] helpers to build inline-colored strings for stdout
//!
//! ## Color Detection
//!
//! Color is enabled per-stream when the stream is a terminal and `NO_COLOR` env var
//! is not set. See <https://no-color.org/>.

use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::OnceLock;
use std::time::Duration;

use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
use owo_colors::OwoColorize;

static JSON_MODE: AtomicBool = AtomicBool::new(false);
static NO_COLOR: AtomicBool = AtomicBool::new(false);

/// Enable or disable JSON mode. When enabled, all stderr output is suppressed.
pub fn set_json_mode(enabled: bool) {
    JSON_MODE.store(enabled, Ordering::Relaxed);
}

pub fn is_json_mode() -> bool {
    JSON_MODE.load(Ordering::Relaxed)
}

/// Disable color output, overriding terminal detection.
pub fn set_no_color(enabled: bool) {
    NO_COLOR.store(enabled, Ordering::Relaxed);
}

/// Checks if we should use color (terminal + NO_COLOR not set)
fn use_color() -> bool {
    static C: OnceLock<bool> = OnceLock::new();
    *C.get_or_init(|| {
        !NO_COLOR.load(Ordering::Relaxed)
            && std::env::var_os("NO_COLOR").is_none()
            && supports_color::on(supports_color::Stream::Stdout).is_some()
    })
}

/// Print a warning to stderr. Formats as "Warning: {msg}".
pub fn warn(msg: &str) {
    if is_json_mode() {
        return;
    }
    if use_color() {
        eprintln!("{} {}", "Warning:".yellow(), msg);
    } else {
        eprintln!("Warning: {}", msg);
    }
}

/// Print a success message to stderr in green.
pub fn success(msg: &str) {
    if is_json_mode() {
        return;
    }
    if use_color() {
        eprintln!("{}", msg.green());
    } else {
        eprintln!("{}", msg);
    }
}

/// Print a header/info line to stderr in bold.
pub fn info(msg: &str) {
    if is_json_mode() {
        return;
    }
    if use_color() {
        eprintln!("{}", msg.bold());
    } else {
        eprintln!("{}", msg);
    }
}

/// Print a detail line to stderr in dim.
pub fn detail(msg: &str) {
    if is_json_mode() {
        return;
    }
    if use_color() {
        eprintln!("{}", msg.dimmed());
    } else {
        eprintln!("{}", msg);
    }
}

/// Print a notice to stderr in yellow (dry run, cancelled, skipped headers).
pub fn notice(msg: &str) {
    if is_json_mode() {
        return;
    }
    if use_color() {
        eprintln!("{}", msg.yellow());
    } else {
        eprintln!("{}", msg);
    }
}

/// Print a plain status message to stderr.
pub fn status(msg: &str) {
    if is_json_mode() {
        return;
    }
    eprintln!("{}", msg);
}

/// Print a passing check item: `  ✓ label` (green checkmark, dim label).
pub fn check_pass(label: &str) {
    if is_json_mode() {
        return;
    }
    if use_color() {
        eprintln!("  {} {}", "".green(), label.dimmed());
    } else {
        eprintln!("{}", label);
    }
}

/// Print a failing check item: `  ✗ label — detail` (red bold cross).
pub fn check_fail(label: &str, detail: &str) {
    if is_json_mode() {
        return;
    }
    if use_color() {
        eprintln!("  {} {}{}", "".red().bold(), label, detail);
    } else {
        eprintln!("{}{}", label, detail);
    }
}

/// Print a warning check item: `  ⚠ label — detail` (yellow warning sign).
pub fn check_warn(label: &str, detail: &str) {
    if is_json_mode() {
        return;
    }
    if use_color() {
        eprintln!("  {} {}{}", "".yellow(), label, detail);
    } else {
        eprintln!("{}{}", label, detail);
    }
}

/// Create a consistently-styled spinner progress bar.
///
/// The spinner is automatically hidden in JSON mode (draw target set to hidden).
/// Callers should call `pb.set_message(...)` and then `pb.finish_and_clear()` when done.
pub fn create_spinner() -> ProgressBar {
    let pb = ProgressBar::new_spinner();
    if is_json_mode() {
        pb.set_draw_target(ProgressDrawTarget::hidden());
    } else {
        pb.set_style(
            ProgressStyle::with_template("  {spinner:.green} {msg}")
                .unwrap()
                .tick_strings(&["", "", "", "", "", "", "", "", "", "", ""]),
        );
        pb.enable_steady_tick(Duration::from_millis(80));
    }
    pb
}

/// Style module for inline string formatting (checks stdout color support).
pub mod style {
    use super::*;

    pub fn bold(s: &str) -> String {
        if use_color() {
            s.bold().to_string()
        } else {
            s.to_string()
        }
    }

    pub fn yellow(s: &str) -> String {
        if use_color() {
            s.yellow().to_string()
        } else {
            s.to_string()
        }
    }

    pub fn green(s: &str) -> String {
        if use_color() {
            s.green().to_string()
        } else {
            s.to_string()
        }
    }

    pub fn red(s: &str) -> String {
        if use_color() {
            s.red().to_string()
        } else {
            s.to_string()
        }
    }

    pub fn green_bold(s: &str) -> String {
        if use_color() {
            s.green().bold().to_string()
        } else {
            s.to_string()
        }
    }

    pub fn red_bold(s: &str) -> String {
        if use_color() {
            s.red().bold().to_string()
        } else {
            s.to_string()
        }
    }

    pub fn dim(s: &str) -> String {
        if use_color() {
            s.dimmed().to_string()
        } else {
            s.to_string()
        }
    }
}