use crate::checkpoint::timestamp;
use crate::logger::output::Loggable;
use crate::logger::stdout_writer::{stderr_write_line, stdout_write_line};
use crate::logger::{
Colors, ARROW, BOX_BL, BOX_BR, BOX_H, BOX_TL, BOX_TR, BOX_V, CHECK, CROSS, INFO, WARN,
};
use crate::workspace::Workspace;
use std::sync::Arc;
use crate::logger::ansi_stripper::strip_ansi_codes;
use crate::logger::file_writer::append_to_file;
pub struct Logger {
colors: Colors,
log_file: Option<String>,
workspace: Option<Arc<dyn Workspace>>,
workspace_log_path: Option<String>,
}
impl Logger {
#[must_use]
pub const fn new(colors: Colors) -> Self {
Self {
colors,
log_file: None,
workspace: None,
workspace_log_path: None,
}
}
#[must_use]
pub fn with_log_file(self, path: &str) -> Self {
Self {
colors: self.colors,
log_file: Some(path.to_string()),
workspace: self.workspace,
workspace_log_path: self.workspace_log_path,
}
}
#[must_use]
pub fn with_workspace_log(self, workspace: Arc<dyn Workspace>, relative_path: &str) -> Self {
Self {
colors: self.colors,
log_file: self.log_file,
workspace: Some(workspace),
workspace_log_path: Some(relative_path.to_string()),
}
}
fn log_to_file(&self, msg: &str) {
let clean_msg = strip_ansi_codes(msg);
if let (Some(workspace), Some(ref path)) = (&self.workspace, &self.workspace_log_path) {
let path = std::path::Path::new(path);
if let Some(parent) = path.parent() {
let _ = workspace.create_dir_all(parent);
}
let _ = workspace.append_bytes(path, format!("{clean_msg}\n").as_bytes());
return;
}
if let Some(ref path) = self.log_file {
let _ = append_to_file(path, &clean_msg);
}
}
pub fn info(&self, msg: &str) {
let c = &self.colors;
let formatted = format!(
"{}[{}]{} {}{}{} {}",
c.dim(),
timestamp(),
c.reset(),
c.blue(),
INFO,
c.reset(),
msg
);
let _ = stdout_write_line(&formatted);
self.log_to_file(&format!("[{}] [INFO] {}", timestamp(), msg));
}
pub fn success(&self, msg: &str) {
let c = &self.colors;
let formatted = format!(
"{}[{}]{} {}{}{} {}{}{}",
c.dim(),
timestamp(),
c.reset(),
c.green(),
CHECK,
c.reset(),
c.green(),
msg,
c.reset()
);
let _ = stdout_write_line(&formatted);
self.log_to_file(&format!("[{}] [OK] {}", timestamp(), msg));
}
pub fn warn(&self, msg: &str) {
let c = &self.colors;
let formatted = format!(
"{}[{}]{} {}{}{} {}{}{}",
c.dim(),
timestamp(),
c.reset(),
c.yellow(),
WARN,
c.reset(),
c.yellow(),
msg,
c.reset()
);
let _ = stdout_write_line(&formatted);
self.log_to_file(&format!("[{}] [WARN] {}", timestamp(), msg));
}
pub fn error(&self, msg: &str) {
let c = &self.colors;
let formatted = format!(
"{}[{}]{} {}{}{} {}{}{}",
c.dim(),
timestamp(),
c.reset(),
c.red(),
CROSS,
c.reset(),
c.red(),
msg,
c.reset()
);
let _ = stderr_write_line(&formatted);
self.log_to_file(&format!("[{}] [ERROR] {}", timestamp(), msg));
}
pub fn step(&self, msg: &str) {
let c = &self.colors;
let formatted = format!(
"{}[{}]{} {}{}{} {}",
c.dim(),
timestamp(),
c.reset(),
c.magenta(),
ARROW,
c.reset(),
msg
);
let _ = stdout_write_line(&formatted);
self.log_to_file(&format!("[{}] [STEP] {}", timestamp(), msg));
}
pub fn header(&self, title: &str, color_fn: fn(Colors) -> &'static str) {
let c = self.colors;
let color = color_fn(c);
let width = 60;
let title_len = title.chars().count();
let padding = (width - title_len - 2) / 2;
let _ = stdout_write_line("");
let line1 = format!(
"{}{}{}{}{}{}",
color,
c.bold(),
BOX_TL,
BOX_H.to_string().repeat(width),
BOX_TR,
c.reset()
);
let _ = stdout_write_line(&line1);
let line2 = format!(
"{}{}{}{}{}{}{}{}{}{}",
color,
c.bold(),
BOX_V,
" ".repeat(padding),
c.white(),
title,
color,
" ".repeat(width - padding - title_len),
BOX_V,
c.reset()
);
let _ = stdout_write_line(&line2);
let line3 = format!(
"{}{}{}{}{}{}",
color,
c.bold(),
BOX_BL,
BOX_H.to_string().repeat(width),
BOX_BR,
c.reset()
);
let _ = stdout_write_line(&line3);
}
pub fn subheader(&self, title: &str) {
let c = &self.colors;
let _ = stdout_write_line("");
let line1 = format!("{}{}{} {}{}", c.bold(), c.blue(), ARROW, title, c.reset());
let _ = stdout_write_line(&line1);
let line2 = format!("{}{}──{}", c.dim(), "─".repeat(title.len()), c.reset());
let _ = stdout_write_line(&line2);
}
}
impl Default for Logger {
fn default() -> Self {
Self::new(Colors::new())
}
}
impl Loggable for Logger {
fn log(&self, msg: &str) {
self.log_to_file(msg);
}
fn info(&self, msg: &str) {
let c = &self.colors;
let formatted = format!(
"{}[{}]{} {}{}{} {}",
c.dim(),
timestamp(),
c.reset(),
c.blue(),
INFO,
c.reset(),
msg
);
let _ = stdout_write_line(&formatted);
self.log(&format!("[{}] [INFO] {msg}", timestamp()));
}
fn success(&self, msg: &str) {
let c = &self.colors;
let formatted = format!(
"{}[{}]{} {}{}{} {}{}{}",
c.dim(),
timestamp(),
c.reset(),
c.green(),
CHECK,
c.reset(),
c.green(),
msg,
c.reset()
);
let _ = stdout_write_line(&formatted);
self.log(&format!("[{}] [OK] {msg}", timestamp()));
}
fn warn(&self, msg: &str) {
let c = &self.colors;
let formatted = format!(
"{}[{}]{} {}{}{} {}{}{}",
c.dim(),
timestamp(),
c.reset(),
c.yellow(),
WARN,
c.reset(),
c.yellow(),
msg,
c.reset()
);
let _ = stdout_write_line(&formatted);
self.log(&format!("[{}] [WARN] {msg}", timestamp()));
}
fn error(&self, msg: &str) {
let c = &self.colors;
let formatted = format!(
"{}[{}]{} {}{}{} {}{}{}",
c.dim(),
timestamp(),
c.reset(),
c.red(),
CROSS,
c.reset(),
c.red(),
msg,
c.reset()
);
let _ = stderr_write_line(&formatted);
self.log(&format!("[{}] [ERROR] {msg}", timestamp()));
}
fn header(&self, title: &str, color_fn: fn(Colors) -> &'static str) {
let c = self.colors;
let color = color_fn(c);
let width = 60;
let title_len = title.chars().count();
let padding = (width - title_len - 2) / 2;
let _ = stdout_write_line("");
let line1 = format!(
"{}{}{}{}{}{}",
color,
c.bold(),
BOX_TL,
BOX_H.to_string().repeat(width),
BOX_TR,
c.reset()
);
let _ = stdout_write_line(&line1);
let line2 = format!(
"{}{}{}{}{}{}{}{}{}{}",
color,
c.bold(),
BOX_V,
" ".repeat(padding),
c.white(),
title,
color,
" ".repeat(width - padding - title_len),
BOX_V,
c.reset()
);
let _ = stdout_write_line(&line2);
let line3 = format!(
"{}{}{}{}{}{}",
color,
c.bold(),
BOX_BL,
BOX_H.to_string().repeat(width),
BOX_BR,
c.reset()
);
let _ = stdout_write_line(&line3);
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "test-utils")]
mod workspace_tests {
use super::super::*;
use crate::workspace::MemoryWorkspace;
#[test]
fn test_logger_with_workspace_writes_to_file() {
let workspace = Arc::new(MemoryWorkspace::new_test());
let logger = Logger::new(Colors::new())
.with_workspace_log(workspace.clone(), ".agent/logs/test.log");
Loggable::info(&logger, "test message");
let content = workspace.get_file(".agent/logs/test.log").unwrap();
assert!(content.contains("test message"));
assert!(content.contains("[INFO]"));
}
#[test]
fn test_logger_with_workspace_strips_ansi_codes() {
let workspace = Arc::new(MemoryWorkspace::new_test());
let logger = Logger::new(Colors::new())
.with_workspace_log(workspace.clone(), ".agent/logs/test.log");
logger.log("[INFO] \x1b[31mcolored\x1b[0m message");
let content = workspace.get_file(".agent/logs/test.log").unwrap();
assert!(content.contains("colored message"));
assert!(!content.contains("\x1b["));
}
#[test]
fn test_logger_with_workspace_creates_parent_dirs() {
let workspace = Arc::new(MemoryWorkspace::new_test());
let logger = Logger::new(Colors::new())
.with_workspace_log(workspace.clone(), ".agent/logs/nested/deep/test.log");
Loggable::info(&logger, "nested log");
assert!(workspace.exists(std::path::Path::new(".agent/logs/nested/deep")));
let content = workspace
.get_file(".agent/logs/nested/deep/test.log")
.unwrap();
assert!(content.contains("nested log"));
}
}
}