use std::fs::OpenOptions;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
const CAP: usize = 1_000_000;
static LOG_PATH: OnceLock<Option<PathBuf>> = OnceLock::new();
pub fn init() {
let resolved = resolve();
if let Some(p) = &resolved {
truncate_if_oversized(p);
}
let _ = LOG_PATH.set(resolved);
}
#[allow(dead_code)]
pub fn path() -> Option<PathBuf> {
LOG_PATH.get().cloned().flatten()
}
pub fn line(area: &str, msg: &str) {
if let Some(Some(p)) = LOG_PATH.get() {
write_line(p, area, msg);
}
}
fn resolve() -> Option<PathBuf> {
let base = match std::env::var_os("XDG_STATE_HOME") {
Some(x) if !x.is_empty() => PathBuf::from(x),
_ => PathBuf::from(std::env::var_os("HOME")?).join(".local").join("state"),
};
Some(base.join("dextui").join("log"))
}
fn format_line(area: &str, msg: &str) -> String {
let ts = chrono::Local::now().format("%H:%M:%S");
format!("{ts} {area:<8} {msg}\n")
}
fn write_line(path: &Path, area: &str, msg: &str) {
if let Some(dir) = path.parent() {
if std::fs::create_dir_all(dir).is_err() {
return;
}
}
let Ok(mut file) = OpenOptions::new().append(true).create(true).open(path) else {
return;
};
let _ = file.write_all(format_line(area, msg).as_bytes());
}
fn truncate_if_oversized(path: &Path) {
let Ok(meta) = std::fs::metadata(path) else {
return;
};
if meta.len() > CAP as u64 {
let _ = OpenOptions::new().write(true).truncate(true).open(path);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_line_is_appended_and_the_directory_is_created() {
let dir = std::env::temp_dir().join(format!("dextui-log-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let file = dir.join("nested").join("log");
write_line(&file, "watch", "registered /x/.dex");
write_line(&file, "dex", "list /x/.dex - 14 tasks 173ms");
let text = std::fs::read_to_string(&file).unwrap();
assert!(text.contains("registered /x/.dex"), "{text}");
assert!(text.contains("14 tasks"), "{text}");
assert_eq!(text.lines().count(), 2, "appended, not overwritten: {text}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn an_unwritable_path_is_silently_ignored() {
write_line(std::path::Path::new("/dev/null/x/log"), "watch", "ignored");
}
#[test]
fn an_oversized_log_is_truncated_at_startup() {
let dir = std::env::temp_dir().join(format!("dextui-log-cap-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("log");
std::fs::write(&file, "x".repeat(CAP + 1)).unwrap();
truncate_if_oversized(&file);
assert_eq!(std::fs::metadata(&file).unwrap().len(), 0);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_log_within_the_cap_survives_startup() {
let dir = std::env::temp_dir().join(format!("dextui-log-keep-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("log");
std::fs::write(&file, "kept").unwrap();
truncate_if_oversized(&file);
assert_eq!(std::fs::read_to_string(&file).unwrap(), "kept");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn the_area_column_is_aligned() {
let a = format_line("watch", "one");
let b = format_line("registry", "two");
let col = |s: &str| s.find("one").or_else(|| s.find("two")).unwrap();
assert_eq!(col(&a), col(&b), "ragged columns:\n{a}\n{b}");
}
}