/// File logger — writes structured log lines alongside the pretty terminal output. use std::fs::{File, OpenOptions}; use std::io::Write; use std::sync::{Mutex, OnceLock}; static LOG_FILE: OnceLock> = OnceLock::new(); static LOG_PATH: OnceLock = OnceLock::new(); pub fn init() { let path = log_path(); // Create parent dir if needed if let Some(parent) = std::path::Path::new(&path).parent() { let _ = std::fs::create_dir_all(parent); } match OpenOptions::new().create(true).append(true).open(&path) { Ok(f) => { LOG_PATH.set(path.clone()).ok(); LOG_FILE.set(Mutex::new(f)).ok(); write_line("INFO", "helios-remote started"); } Err(e) => eprintln!("[logger] Failed to open log file {path}: {e}"), } } fn log_path() -> String { #[cfg(windows)] { let base = std::env::var("LOCALAPPDATA") .unwrap_or_else(|_| "C:\\Temp".to_string()); format!("{base}\\helios-remote\\helios-remote.log") } #[cfg(not(windows))] { "/tmp/helios-remote.log".to_string() } } pub fn get_log_path() -> String { LOG_PATH.get().cloned().unwrap_or_else(log_path) } pub fn write_line(level: &str, msg: &str) { let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S"); let line = format!("{now} [{level:<5}] {msg}\n"); if let Some(mutex) = LOG_FILE.get() { if let Ok(mut f) = mutex.lock() { let _ = f.write_all(line.as_bytes()); } } } /// Read the last `n` lines from the log file. pub fn tail(n: u32) -> String { let path = get_log_path(); let content = std::fs::read_to_string(&path).unwrap_or_default(); let lines: Vec<&str> = content.lines().collect(); let start = lines.len().saturating_sub(n as usize); lines[start..].join("\n") }