prox 0.1.1

Rusty development process manager like foreman, but better!
Documentation
use chrono::Local;
use owo_colors::{AnsiColors, OwoColorize};
use std::{env, fmt::Display, sync::OnceLock};

static LOG_CONFIG: OnceLock<LogConfig> = OnceLock::new();

/// Levels for log filtering
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Level {
    /// Show logs at the Debug level and higher
    Debug,
    /// Show logs at the Info level and higher
    Info,
    /// Show logs at the Warn level and higher
    Warn,
    /// Show logs only at the Error level
    Error,
}

impl Level {
    /// Get the string representation of the log level
    pub fn as_str(&self) -> &'static str {
        match self {
            Level::Debug => "DEBUG",
            Level::Info => "INFO",
            Level::Warn => "WARN",
            Level::Error => "ERROR",
        }
    }

    /// Create a log level from a string (case insensitive)
    pub fn from_str(s: &str) -> Option<Self> {
        match s.to_uppercase().as_str() {
            "DEBUG" => Some(Level::Debug),
            "INFO" => Some(Level::Info),
            "WARN" | "WARNING" => Some(Level::Warn),
            "ERROR" | "ERR" => Some(Level::Error),
            _ => None,
        }
    }

    /// Get the log level from the RUST_LOG environment variable, defaulting to Info
    pub fn from_env() -> Self {
        env::var("RUST_LOG")
            .ok()
            .and_then(|s| Self::from_str(&s))
            .unwrap_or(Level::Info)
    }
}

impl Display for Level {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

pub(crate) const DEFAULT_PROX_PREFIX: &str = "prox";

/// Configuration for logging
#[derive(Debug, Clone)]
pub struct LogConfig {
    /// Prefix to use for prox internal logs
    pub prox_prefix: String,
    /// Minimum width of the process prefix in log output
    pub prefix_width: usize,
    /// Whether to show timestamps in prox logs (not process logs)
    pub timestamp: bool,
    /// Minimum log level to show
    pub level: Level,
}

impl Default for LogConfig {
    fn default() -> Self {
        Self {
            prox_prefix: DEFAULT_PROX_PREFIX.to_string(),
            prefix_width: 8,
            timestamp: false,
            level: Level::from_env(),
        }
    }
}

/// Initialize logging with the given configuration
pub fn init_logging(config: LogConfig) {
    LOG_CONFIG.set(config).ok();
}

fn should_log(level: Level) -> bool {
    let config = LOG_CONFIG.get().cloned().unwrap_or_default();
    level >= config.level
}

/// Logging function that gets called by the logging macros and prints the log message
pub fn log_prox_internal(level: Level, message: &str) {
    if !should_log(level) {
        return;
    }

    let config = LOG_CONFIG.get().cloned().unwrap_or_default();

    let color = match level {
        Level::Debug => AnsiColors::BrightBlack,
        Level::Info => AnsiColors::Green,
        Level::Warn => AnsiColors::Yellow,
        Level::Error => AnsiColors::Red,
    };

    let prefix = format_prefix(&config.prox_prefix, config.prefix_width, None);

    let level_str = format!("[{level}]").color(color).to_string();

    if config.timestamp {
        let timestamp = Local::now().format("%H:%M:%S%.3f");
        println!("{prefix} {level_str} [{timestamp}] {message}");
    } else {
        println!("{prefix} {level_str} {message}");
    }
}

// Direct functions for process logs (no macros needed for these)
pub(crate) fn log_proc(proc_name: &str, message: &str, color: Option<AnsiColors>) {
    let config = LOG_CONFIG.get().cloned().unwrap_or_default();
    let prefix = format_prefix(proc_name, config.prefix_width, color);
    println!("{prefix} {message}");
}

pub(crate) fn log_proc_error(proc_name: &str, message: &str, color: Option<AnsiColors>) {
    let config = LOG_CONFIG.get().cloned().unwrap_or_default();
    let prefix = format_prefix(proc_name, config.prefix_width, color);
    eprintln!("{prefix} ERROR: {message}");
}

fn format_prefix(name: &str, width: usize, color: Option<AnsiColors>) -> String {
    let formatted = format!("[{name:^0$}]", width);
    match color {
        Some(color) => formatted.color(color).to_string(),
        None => formatted,
    }
}

/// Log to the prox logger at debug level
#[macro_export]
macro_rules! debug {
    ($($arg:tt)*) => {
        $crate::logging::log_prox_internal($crate::logging::Level::Debug, &format!($($arg)*))
    };
}

/// Log to the prox logger at info level
#[macro_export]
macro_rules! info {
    ($($arg:tt)*) => {
        $crate::logging::log_prox_internal($crate::logging::Level::Info, &format!($($arg)*))
    };
}

/// Log to the prox logger at warn level
#[macro_export]
macro_rules! warn {
    ($($arg:tt)*) => {
        $crate::logging::log_prox_internal($crate::logging::Level::Warn, &format!($($arg)*))
    };
}

/// Log to the prox logger at error level
#[macro_export]
macro_rules! error {
    ($($arg:tt)*) => {
        $crate::logging::log_prox_internal($crate::logging::Level::Error, &format!($($arg)*))
    };
}