use log::{debug, error, trace, warn};
const TARGET: &str = "codex_codes::stderr";
fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == 0x1b && i + 1 < bytes.len() && bytes[i + 1] == b'[' {
i += 2;
while i < bytes.len() {
let c = bytes[i];
i += 1;
if !(c.is_ascii_digit() || c == b';') {
break;
}
}
} else {
out.push(bytes[i] as char);
i += 1;
}
}
out
}
pub(crate) fn forward_line(raw: &str) {
let line = strip_ansi(raw);
let trimmed = line.trim_end_matches(['\n', '\r']);
if trimmed.is_empty() {
return;
}
if trimmed.contains(" ERROR ") {
error!(target: TARGET, "{}", trimmed);
} else if trimmed.contains(" WARN ") {
warn!(target: TARGET, "{}", trimmed);
} else if trimmed.contains(" DEBUG ") {
debug!(target: TARGET, "{}", trimmed);
} else {
trace!(target: TARGET, "{}", trimmed);
}
}
#[cfg(feature = "async-client")]
pub(crate) fn spawn_async(stderr: tokio::process::ChildStderr) -> tokio::task::JoinHandle<()> {
use tokio::io::{AsyncBufReadExt, BufReader};
tokio::spawn(async move {
let mut reader = BufReader::new(stderr);
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line).await {
Ok(0) => break,
Ok(_) => forward_line(&line),
Err(_) => break,
}
}
})
}
#[cfg(feature = "sync-client")]
pub(crate) fn spawn_sync(stderr: std::process::ChildStderr) -> std::thread::JoinHandle<()> {
use std::io::{BufRead, BufReader};
std::thread::Builder::new()
.name("codex-stderr-drain".to_string())
.spawn(move || {
let mut reader = BufReader::new(stderr);
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line) {
Ok(0) => break,
Ok(_) => forward_line(&line),
Err(_) => break,
}
}
})
.expect("failed to spawn stderr drain thread")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strip_ansi_removes_color_codes() {
let raw = "\x1b[32m INFO\x1b[0m hello \x1b[2mworld\x1b[0m";
assert_eq!(strip_ansi(raw), " INFO hello world");
}
#[test]
fn test_strip_ansi_passthrough() {
assert_eq!(strip_ansi("plain text"), "plain text");
}
#[test]
fn test_strip_ansi_handles_complex_params() {
let raw = "\x1b[1;32;4mbold green underline\x1b[0m";
assert_eq!(strip_ansi(raw), "bold green underline");
}
}