use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::fmt::writer::MakeWriterExt;
use crate::config::{Config, LoggingConfig};
pub fn init_logging(config: &Config, verbose_override: bool) -> Result<()> {
let effective_config = resolve_logging_config(&config.logging, verbose_override);
let env_filter = create_env_filter(&effective_config.level)?;
match effective_config.file {
Some(ref file_path) => {
init_with_file_logging(&effective_config, file_path, env_filter)?;
}
None => {
init_console_only_logging(&effective_config, env_filter)?;
}
}
tracing::debug!("🔍 Logging initialized successfully");
if verbose_override {
tracing::debug!("Verbose mode enabled via CLI flag");
}
if let Some(file) = &effective_config.file {
tracing::debug!("File logging enabled: {}", file);
}
Ok(())
}
fn resolve_logging_config(config: &LoggingConfig, verbose_override: bool) -> EffectiveLoggingConfig {
let file = config.file.as_ref().filter(|s| !s.trim().is_empty()).cloned();
EffectiveLoggingConfig {
enabled: config.enabled,
level: if verbose_override { "debug".to_string() } else { config.level.clone() },
file,
timestamp: config.timestamp,
verbose_override,
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct EffectiveLoggingConfig {
enabled: bool,
level: String,
file: Option<String>,
timestamp: bool,
verbose_override: bool,
}
fn init_console_only_logging(config: &EffectiveLoggingConfig, env_filter: EnvFilter) -> Result<()> {
if config.timestamp {
tracing_subscriber::fmt()
.with_env_filter(env_filter)
.with_target(false)
.with_thread_ids(false)
.with_file(false)
.with_line_number(false)
.with_level(true)
.with_ansi(true)
.try_init()
.map_err(|e| anyhow::anyhow!("Failed to initialize console logging with timestamps: {}", e))?;
} else {
tracing_subscriber::fmt()
.with_env_filter(env_filter)
.with_target(false)
.with_thread_ids(false)
.with_file(false)
.with_line_number(false)
.with_level(true)
.with_ansi(true)
.without_time()
.try_init()
.map_err(|e| anyhow::anyhow!("Failed to initialize console logging without timestamps: {}", e))?;
}
Ok(())
}
fn init_with_file_logging(config: &EffectiveLoggingConfig, file_path: &str, env_filter: EnvFilter) -> Result<()> {
let expanded_path = shellexpand::full(file_path).context("Failed to expand environment variables in log file path")?;
let log_path = Path::new(expanded_path.as_ref());
if let Some(parent_dir) = log_path.parent() {
fs::create_dir_all(parent_dir).with_context(|| format!("Failed to create log directory: {}", parent_dir.display()))?;
}
let file_appender = tracing_appender::rolling::never(
log_path.parent().unwrap_or_else(|| Path::new(".")),
log_path.file_name().unwrap_or_else(|| std::ffi::OsStr::new("wallflow.log")),
);
if config.timestamp {
tracing_subscriber::fmt()
.with_env_filter(env_filter)
.with_writer(std::io::stderr.and(file_appender))
.with_target(false) .with_thread_ids(false)
.with_file(false)
.with_line_number(false)
.with_level(true)
.with_ansi(true) .try_init()
.map_err(|e| anyhow::anyhow!("Failed to initialize dual logging with timestamps: {}", e))?;
} else {
tracing_subscriber::fmt()
.with_env_filter(env_filter)
.with_writer(std::io::stderr.and(file_appender))
.with_target(false)
.with_thread_ids(false)
.with_file(false)
.with_line_number(false)
.with_level(true)
.with_ansi(true)
.without_time()
.try_init()
.map_err(|e| anyhow::anyhow!("Failed to initialize dual logging without timestamps: {}", e))?;
}
Ok(())
}
fn create_env_filter(level: &str) -> Result<EnvFilter> {
let normalized_level = normalize_log_level(level)?;
let filter_directive = format!("wallflow={}", normalized_level);
EnvFilter::try_new(&filter_directive).with_context(|| format!("Invalid log level configuration: {}", level))
}
fn normalize_log_level(level: &str) -> Result<&str> {
match level.to_lowercase().as_str() {
"trace" => Ok("trace"),
"debug" => Ok("debug"),
"info" => Ok("info"),
"warn" | "warning" => Ok("warn"),
"error" => Ok("error"),
_ => Err(anyhow::anyhow!(
"Invalid log level: '{}'. Valid levels: trace, debug, info, warn, error",
level
)),
}
}
pub fn log_system_info(config: &Config) {
use tracing::debug;
debug!("Configuration loaded:");
debug!(" Local wallpapers: {}", config.paths.local);
debug!(" Downloads: {}", config.paths.downloads);
debug!(" Default source: {}", config.sources.default);
if let Ok(platform_info) = crate::wallpaper::platform_info() {
for line in platform_info.lines() {
if !line.is_empty() {
debug!("Platform: {}", line.trim());
}
}
}
if let Some(file) = &config.logging.file {
debug!("📝 File logging enabled: {}", file);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::LoggingConfig;
#[test]
fn test_normalize_log_level() {
assert_eq!(normalize_log_level("debug").unwrap(), "debug");
assert_eq!(normalize_log_level("DEBUG").unwrap(), "debug");
assert_eq!(normalize_log_level("Info").unwrap(), "info");
assert_eq!(normalize_log_level("warn").unwrap(), "warn");
assert_eq!(normalize_log_level("warning").unwrap(), "warn");
assert_eq!(normalize_log_level("ERROR").unwrap(), "error");
assert!(normalize_log_level("invalid").is_err());
}
#[test]
fn test_resolve_logging_config() {
let config = LoggingConfig {
enabled: true,
level: "info".to_string(),
file: Some("/tmp/test.log".to_string()),
timestamp: true,
};
let effective = resolve_logging_config(&config, false);
assert_eq!(effective.level, "info");
assert!(!effective.verbose_override);
let effective = resolve_logging_config(&config, true);
assert_eq!(effective.level, "debug");
assert!(effective.verbose_override);
}
#[test]
fn test_resolve_logging_config_empty_file_path() {
let config = LoggingConfig {
enabled: true,
level: "info".to_string(),
file: Some("".to_string()),
timestamp: true,
};
let effective = resolve_logging_config(&config, false);
assert!(effective.file.is_none());
let config = LoggingConfig {
enabled: true,
level: "info".to_string(),
file: Some(" ".to_string()),
timestamp: true,
};
let effective = resolve_logging_config(&config, false);
assert!(effective.file.is_none());
let config = LoggingConfig {
enabled: true,
level: "info".to_string(),
file: Some("/tmp/test.log".to_string()),
timestamp: true,
};
let effective = resolve_logging_config(&config, false);
assert_eq!(effective.file, Some("/tmp/test.log".to_string()));
}
}