vibe-action 0.0.1

Command router — execute shell commands and LLM prompts via simple YAML actions.
//! CLI output helpers.
//! Macros for colored terminal output. In debug mode, uses tracing.

/// Strip ANSI color codes from a string.
pub fn strip_ansi(s: &str) -> String {
    let re = regex::Regex::new(r"\x1b\[[0-9;]*m").unwrap();
    re.replace_all(s, "").to_string()
}

/// Format a message for display.
pub fn format_msg(s: &str) -> String {
    // Strip tag_ or tag- prefix if present.
    let s = s
        .strip_prefix("tag_")
        .or_else(|| s.strip_prefix("tag-"))
        .unwrap_or(s);
    // Strip trailing dot only if it's a single dot (not part of "..." or "..").
    let s = if s.ends_with('.') && !s.ends_with("..") {
        s.strip_suffix('.').unwrap_or(s)
    } else {
        s
    };
    // Lowercase the first character.
    let mut chars = s.chars();
    match chars.next() {
        None => String::new(),
        Some(c) => c.to_lowercase().to_string() + chars.as_str(),
    }
}

/// Print a blank line (CLI only, skipped in debug mode).
#[macro_export]
macro_rules! print_newline {
    () => {{
        if !$crate::configs::app::AppConfig::is_debug() {
            println!();
        }
    }};
}

/// Print an error message. CLI: red. Debug: tracing::error.
#[macro_export]
macro_rules! print_error {
    ($($arg:tt)*) => {{
        if $crate::configs::app::AppConfig::is_debug() {
            let msg = format!($($arg)*);
            tracing::error!("{}", $crate::utils::macros::strip_ansi(&msg));
        } else {
            let msg = format!($($arg)*);
            let formatted = $crate::utils::macros::format_msg(&msg);
            println!("{}", format!("\x1b[1m\x1b[91merror\x1b[0m: {}", formatted));
        }
    }};
}

/// Print an info message. CLI: blue. Debug: tracing::info.
#[macro_export]
macro_rules! print_info {
    ($($arg:tt)*) => {{
        if $crate::configs::app::AppConfig::is_debug() {
            let msg = format!($($arg)*);
            tracing::info!("{}", $crate::utils::macros::strip_ansi(&msg));
        } else {
            let msg = format!($($arg)*);
            let formatted = $crate::utils::macros::format_msg(&msg);
            println!("{}", format!("\x1b[1m\x1b[94minfo\x1b[0m: {}", formatted));
        }
    }};
}

/// Print a warning message. CLI: yellow. Debug: tracing::warn.
#[macro_export]
macro_rules! print_warning {
    ($($arg:tt)*) => {{
        if $crate::configs::app::AppConfig::is_debug() {
            let msg = format!($($arg)*);
            tracing::warn!("{}", $crate::utils::macros::strip_ansi(&msg));
        } else {
            let msg = format!($($arg)*);
            let formatted = $crate::utils::macros::format_msg(&msg);
            println!("{}", format!("\x1b[1m\x1b[93mwarning\x1b[0m: {}", formatted));
        }
    }};
}

/// Print a success message. CLI: green. Debug: tracing::info.
#[macro_export]
macro_rules! print_success {
    ($($arg:tt)*) => {{
        if $crate::configs::app::AppConfig::is_debug() {
            let msg = format!($($arg)*);
            tracing::info!("{}", $crate::utils::macros::strip_ansi(&msg));
        } else {
            let msg = format!($($arg)*);
            let formatted = $crate::utils::macros::format_msg(&msg);
            println!("{}", format!("\x1b[1m\x1b[32msuccess\x1b[0m: {}", formatted));
        }
    }};
}

/// Print a state message. CLI: cyan. Debug: tracing::debug.
#[macro_export]
macro_rules! print_state {
    ($($arg:tt)*) => {{
        if $crate::configs::app::AppConfig::is_debug() {
            let msg = format!($($arg)*);
            tracing::debug!("{}", $crate::utils::macros::strip_ansi(&msg));
        } else {
            let msg = format!($($arg)*);
            let formatted = $crate::utils::macros::format_msg(&msg);
            println!("{}", format!("\x1b[1m\x1b[36mstate\x1b[0m: {}", formatted));
        }
    }};
}

/// Print a progress message (CLI only, skipped in debug mode).
#[macro_export]
macro_rules! print_progress {
    ($($arg:tt)*) => {{
        if !$crate::configs::app::AppConfig::is_debug() {
            let msg = format!($($arg)*);
            let formatted = $crate::utils::macros::format_msg(&msg);
            print!("\r\x1b[1m\x1b[36mprogress\x1b[0m: {}\x1b[K", formatted);
            std::io::Write::flush(&mut std::io::stdout()).unwrap();
        }
    }};
}

/// Print an error message and exit with code 1.
#[macro_export]
macro_rules! exit_error {
    ($($arg:tt)*) => {{
        if $crate::configs::app::AppConfig::is_debug() {
            let msg = format!($($arg)*);
            tracing::error!("{}", $crate::utils::macros::strip_ansi(&msg));
        } else {
            let msg = format!($($arg)*);
            let formatted = $crate::utils::macros::format_msg(&msg);
            println!("{}", format!("\x1b[1m\x1b[91merror\x1b[0m: {}", formatted));
        }
        std::process::exit(1);
    }};
}

/// Print a success message with automatic line wrapping and framed borders.
#[macro_export]
macro_rules! print_rich_block {
    ($($arg:tt)*) => {{
        let msg = format!($($arg)*);
        let max_width = 120usize;
        let wrapped: Vec<String> = msg
            .lines()
            .flat_map(|line| {
                let chars: Vec<char> = line.chars().collect();
                if chars.len() <= max_width {
                    vec![line.to_string()]
                } else {
                    let mut result = Vec::new();
                    let mut start = 0;
                    while start < chars.len() {
                        let mut end = (start + max_width).min(chars.len());

                        if end < chars.len() && !chars[end].is_whitespace() && !chars[end - 1].is_whitespace() {
                            let mut space_idx = end;
                            while space_idx > start && !chars[space_idx].is_whitespace() {
                                space_idx -= 1;
                            }
                            if space_idx > start {
                                end = space_idx;
                            }
                        }
                        let sub_str: String = chars[start..end].iter().collect();
                        if start == 0 || !sub_str.trim().is_empty() || sub_str.len() == max_width {
                            result.push(sub_str.to_string());
                        }
                        start = end;
                        if start < chars.len() && chars[start].is_whitespace() {
                            start += 1;
                        }
                    }
                    result
                }
            })
            .collect();

        let width = wrapped.iter()
            .map(|l| l.chars().count())
            .max()
            .unwrap_or(0)
            .max(13);

        let top = format!("── success ──{}", "".repeat(width.saturating_sub(13)));
        let bottom = "".repeat(width);

        println!("\x1b[1m\x1b[32m{}\x1b[0m", top);
        for line in &wrapped {
            println!("\x1b[37m{}\x1b[0m", line);
        }
        println!("\x1b[1m\x1b[32m{}\x1b[0m", bottom);
    }};
}