use serde_json::{json, Value};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
const MAX_BYTES: u64 = 768 * 1024; const TRIM_KEEP: usize = 2000;
const NOISE_CMDS: [&str; 3] = ["get", "sub", "unsub"];
fn log_path() -> PathBuf {
crate::store::theme_dir().join("hostlog.jsonl")
}
fn now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
fn cmd_of(req: &Value) -> &str {
if let Some(c) = req.get("cmd").and_then(|c| c.as_str()) {
return c;
}
if !req["scheme"].is_null() {
"scheme"
} else if !req["ui"].is_null() {
"ui"
} else {
"?"
}
}
pub fn record(dir: &str, req: &Value, data: &Value) {
let cmd = cmd_of(req);
if cmd == "hostlog"
|| NOISE_CMDS.contains(&cmd)
|| req.get("_nolog").and_then(|b| b.as_bool()).unwrap_or(false)
{
return;
}
let mut summary = data.to_string();
if summary.len() > 400 {
summary.truncate(400);
summary.push('…');
}
let entry = json!({
"t": now_ms(),
"dir": dir,
"cmd": cmd,
"pid": std::process::id(),
"msg": summary,
});
let path = log_path();
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true) .open(&path)
{
let _ = writeln!(f, "{entry}");
}
if std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0) > MAX_BYTES {
trim(&path);
}
}
fn trim(path: &Path) {
let Ok(data) = std::fs::read_to_string(path) else {
return;
};
let lines: Vec<&str> = data.lines().collect();
if lines.len() <= TRIM_KEEP {
return;
}
let tail = lines[lines.len() - TRIM_KEEP..].join("\n");
let tmp = path.with_extension("jsonl.tmp");
if std::fs::write(&tmp, format!("{tail}\n")).is_ok() {
let _ = std::fs::rename(&tmp, path);
}
}
pub fn read_tail(n: usize) -> Vec<Value> {
let Ok(data) = std::fs::read_to_string(log_path()) else {
return Vec::new();
};
let lines: Vec<&str> = data.lines().collect();
let start = lines.len().saturating_sub(n);
lines[start..]
.iter()
.filter_map(|l| serde_json::from_str::<Value>(l).ok())
.collect()
}