use std::sync::OnceLock;
static COLOR: OnceLock<bool> = OnceLock::new();
static JSON: OnceLock<bool> = OnceLock::new();
pub fn init_format() {
let json = matches!(std::env::var("LOOOP_LOG_FORMAT").as_deref(), Ok("json"));
let _ = JSON.set(json);
unsafe { std::env::set_var("LOOOP_LOG_FORMAT", if json { "json" } else { "human" }) };
}
pub fn is_json() -> bool {
*JSON.get().unwrap_or(&false)
}
pub fn init_color() {
let enabled = if is_json() {
false
} else {
match std::env::var("LOOOP_COLOR") {
Ok(v) if v == "1" => true,
Ok(v) if v == "0" => false,
_ => is_stdout_tty() && std::env::var_os("NO_COLOR").is_none(),
}
};
let _ = COLOR.set(enabled);
unsafe { std::env::set_var("LOOOP_COLOR", if enabled { "1" } else { "0" }) };
}
fn color_on() -> bool {
*COLOR.get().unwrap_or(&false)
}
#[cfg(unix)]
fn is_stdout_tty() -> bool {
unsafe { libc_isatty(1) }
}
#[cfg(not(unix))]
fn is_stdout_tty() -> bool {
false
}
#[cfg(unix)]
unsafe fn libc_isatty(fd: i32) -> bool {
unsafe extern "C" {
fn isatty(fd: i32) -> i32;
}
unsafe { isatty(fd) == 1 }
}
macro_rules! code {
($name:ident, $seq:expr) => {
pub fn $name() -> &'static str {
if color_on() { $seq } else { "" }
}
};
}
code!(rst, "\x1b[0m");
code!(dim, "\x1b[2m");
code!(b, "\x1b[1m");
code!(cyan, "\x1b[36m");
code!(grn, "\x1b[32m");
code!(red, "\x1b[31m");
code!(yel, "\x1b[33m");
#[derive(Clone, Copy)]
pub enum Level {
Info,
Step,
Ok,
Warn,
Error,
}
impl Level {
fn tag(self) -> &'static str {
match self {
Level::Info => "info",
Level::Step => "step",
Level::Ok => "ok",
Level::Warn => "warn",
Level::Error => "error",
}
}
fn color(self) -> &'static str {
match self {
Level::Info => "",
Level::Step => cyan(),
Level::Ok => grn(),
Level::Warn => yel(),
Level::Error => red(),
}
}
fn glyph(self) -> &'static str {
match self {
Level::Info => "·",
Level::Step => "→",
Level::Ok => "✓",
Level::Warn => "⚡",
Level::Error => "✗",
}
}
}
pub fn event(level: Level, event: &str, msg: &str, fields: &[(&str, serde_json::Value)]) {
if is_json() {
let ts = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
println!("{}", json_event_line(&ts, level, event, msg, fields));
return;
}
let glyph = level.glyph();
if matches!(level, Level::Info | Level::Step) {
println!("{}[{}] {} {}{}", dim(), hms(), glyph, msg, rst());
return;
}
let c = level.color();
let bold = if matches!(level, Level::Ok | Level::Error) {
b()
} else {
""
};
let msg_c = if matches!(level, Level::Warn | Level::Error) {
c
} else {
""
};
let msg_rst = if msg_c.is_empty() { "" } else { rst() };
println!(
"{}[{}]{} {}{}{}{} {}{}{}",
dim(),
hms(),
rst(),
bold,
c,
glyph,
rst(),
msg_c,
msg,
msg_rst
);
}
fn json_event_line(
ts: &str,
level: Level,
event: &str,
msg: &str,
fields: &[(&str, serde_json::Value)],
) -> String {
let mut obj = serde_json::Map::new();
obj.insert("ts".into(), serde_json::Value::String(ts.into()));
obj.insert(
"level".into(),
serde_json::Value::String(level.tag().into()),
);
obj.insert("event".into(), serde_json::Value::String(event.into()));
obj.insert("msg".into(), serde_json::Value::String(msg.into()));
for (k, v) in fields {
obj.insert((*k).to_string(), v.clone());
}
serde_json::Value::Object(obj).to_string()
}
pub fn hms() -> String {
chrono::Local::now().format("%H:%M:%S").to_string()
}
pub fn date_fmt(fmt: &str) -> String {
chrono::Local::now().format(fmt).to_string()
}
pub fn content_hash(input: &[u8]) -> String {
const OFFSET: u128 = 0x6c62272e07bb014262b821756295c58d;
const PRIME: u128 = 0x0000000001000000000000000000013b;
let mut h = OFFSET;
for &b in input {
h ^= b as u128;
h = h.wrapping_mul(PRIME);
}
format!("{h:032x}")
}
pub fn on_path(cmd: &str) -> bool {
let Some(path) = std::env::var_os("PATH") else {
return false;
};
std::env::split_paths(&path).any(|dir| {
let p = dir.join(cmd);
p.is_file() && is_executable(&p)
})
}
#[cfg(unix)]
fn is_executable(p: &std::path::Path) -> bool {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(p)
.map(|m| m.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(not(unix))]
fn is_executable(_p: &std::path::Path) -> bool {
true
}
#[cfg(unix)]
pub fn pid_alive(pid: &str) -> bool {
let Ok(pid) = pid.trim().parse::<i32>() else {
return false;
};
if pid <= 0 {
return false;
}
unsafe extern "C" {
fn kill(pid: i32, sig: i32) -> i32;
}
unsafe { kill(pid, 0) == 0 }
}
#[cfg(not(unix))]
pub fn pid_alive(_pid: &str) -> bool {
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn json_event_line_is_valid_and_ordered() {
let line = json_event_line(
"2026-01-02T03:04:05Z",
Level::Ok,
"tick.decided",
"decided in 3s",
&[
("secs", serde_json::json!(3)),
("runner", serde_json::json!("claude")),
],
);
let v: serde_json::Value = serde_json::from_str(&line).unwrap();
assert_eq!(v["ts"], "2026-01-02T03:04:05Z");
assert_eq!(v["level"], "ok");
assert_eq!(v["event"], "tick.decided");
assert_eq!(v["msg"], "decided in 3s");
assert_eq!(v["secs"], 3);
assert_eq!(v["runner"], "claude");
}
#[test]
fn level_tags_are_stable() {
assert_eq!(Level::Info.tag(), "info");
assert_eq!(Level::Step.tag(), "step");
assert_eq!(Level::Ok.tag(), "ok");
assert_eq!(Level::Warn.tag(), "warn");
assert_eq!(Level::Error.tag(), "error");
}
#[test]
fn level_glyphs_map_importance() {
assert_eq!(Level::Info.glyph(), "·");
assert_eq!(Level::Step.glyph(), "→");
assert_eq!(Level::Ok.glyph(), "✓");
assert_eq!(Level::Warn.glyph(), "⚡");
assert_eq!(Level::Error.glyph(), "✗");
}
#[test]
fn content_hash_is_deterministic_and_change_sensitive() {
assert_eq!(content_hash(b"hello world"), content_hash(b"hello world"));
assert_ne!(content_hash(b"hello world"), content_hash(b"hello worle"));
let h = content_hash(b"");
assert_eq!(h.len(), 32);
assert!(h.chars().all(|c| c.is_ascii_hexdigit()));
}
#[cfg(unix)]
#[test]
fn pid_alive_detects_self_and_rejects_garbage() {
assert!(pid_alive(&std::process::id().to_string()));
assert!(!pid_alive("not-a-pid"));
assert!(!pid_alive(""));
assert!(!pid_alive("0"));
assert!(!pid_alive("-1"));
}
}