use std::fs::{self, File, OpenOptions};
use std::path::PathBuf;
use anyhow::{Context, Result};
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
pub struct TuiLogGuard {
#[cfg(unix)]
saved_stderr_fd: Option<libc::c_int>,
_file: File,
#[allow(dead_code)]
log_path: PathBuf,
}
impl TuiLogGuard {
#[allow(dead_code)]
#[must_use]
pub fn log_path(&self) -> &std::path::Path {
&self.log_path
}
}
#[cfg(unix)]
impl Drop for TuiLogGuard {
fn drop(&mut self) {
if let Some(saved) = self.saved_stderr_fd.take() {
unsafe {
let _ = libc::dup2(saved, libc::STDERR_FILENO);
let _ = libc::close(saved);
}
}
}
}
#[cfg(not(unix))]
impl Drop for TuiLogGuard {
fn drop(&mut self) {}
}
pub fn init() -> Result<TuiLogGuard> {
let log_dir = log_directory().context("could not resolve TUI log directory")?;
fs::create_dir_all(&log_dir)
.with_context(|| format!("failed to create {}", log_dir.display()))?;
let date = chrono::Local::now().format("%Y-%m-%d");
let log_path = log_dir.join(format!("tui-{date}.log"));
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
.with_context(|| format!("failed to open {}", log_path.display()))?;
let subscriber_file = file
.try_clone()
.context("failed to clone log file handle for subscriber")?;
let env_filter = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new("info"))
.unwrap_or_else(|_| EnvFilter::new("info"));
let subscriber = tracing_subscriber::registry().with(env_filter).with(
fmt::layer()
.with_writer(move || {
subscriber_file
.try_clone()
.expect("clone log file handle for tracing writer")
})
.with_ansi(false)
.with_target(true)
.with_thread_ids(false),
);
let _ = tracing::subscriber::set_global_default(subscriber);
#[cfg(unix)]
let saved_stderr_fd = redirect_stderr_to(&file).ok();
Ok(TuiLogGuard {
#[cfg(unix)]
saved_stderr_fd,
_file: file,
log_path,
})
}
fn log_directory() -> Option<PathBuf> {
if let Some(home) = std::env::var_os("HOME").map(PathBuf::from)
&& !home.as_os_str().is_empty()
{
return Some(home.join(".deepseek").join("logs"));
}
if let Some(userprofile) = std::env::var_os("USERPROFILE").map(PathBuf::from)
&& !userprofile.as_os_str().is_empty()
{
return Some(userprofile.join(".deepseek").join("logs"));
}
dirs::home_dir().map(|h| h.join(".deepseek").join("logs"))
}
#[cfg(unix)]
fn redirect_stderr_to(file: &File) -> Result<libc::c_int> {
use std::os::fd::AsRawFd;
let target = file.as_raw_fd();
unsafe {
let saved = libc::dup(libc::STDERR_FILENO);
if saved < 0 {
return Err(
anyhow::Error::from(std::io::Error::last_os_error()).context("dup(STDERR_FILENO)")
);
}
if libc::dup2(target, libc::STDERR_FILENO) < 0 {
let err = std::io::Error::last_os_error();
let _ = libc::close(saved);
return Err(anyhow::Error::from(err).context("dup2(log_file, STDERR_FILENO)"));
}
Ok(saved)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn log_directory_prefers_home() {
let _lock = crate::test_support::lock_test_env();
let tmp = tempfile::TempDir::new().unwrap();
let prev_home = std::env::var_os("HOME");
let prev_userprofile = std::env::var_os("USERPROFILE");
unsafe {
std::env::set_var("HOME", tmp.path());
std::env::set_var("USERPROFILE", "");
}
let resolved = log_directory().expect("log_directory should resolve");
assert_eq!(resolved, tmp.path().join(".deepseek").join("logs"));
unsafe {
match prev_home {
Some(v) => std::env::set_var("HOME", v),
None => std::env::remove_var("HOME"),
}
match prev_userprofile {
Some(v) => std::env::set_var("USERPROFILE", v),
None => std::env::remove_var("USERPROFILE"),
}
}
}
}