use anyhow::Context;
use colored::Colorize;
use env_logger;
use log::{debug, warn};
use std::fs;
use std::io::Write;
use std::sync::mpsc::Sender;
use crate::PKG_NAME;
use crate::config::RuntimeConfig;
use crate::results::{Output, OutputMode};
use crate::utils::typecheck::is_stderr_tty;
pub enum OutputSink {
Collect,
StreamOnly(Box<dyn Fn(String, Output) + Send + Sync>),
Channel(Sender<(String, Output)>),
}
pub fn build_logger(dev_mode: bool) {
if dev_mode {
env_logger::Builder::from_default_env()
.filter_level(log::LevelFilter::Debug)
.init();
debug!("Debug mode enabled");
} else {
env_logger::Builder::from_default_env()
.filter_level(log::LevelFilter::Info)
.format(|buf, record| {
let level_str = record.level().to_string();
let level_display = match record.level() {
log::Level::Error => level_str.red().to_string(),
log::Level::Warn => level_str.yellow().to_string(),
_ => level_str,
};
writeln!(
buf,
"{}{} {} {}{} {}",
"[".bright_green().bold(),
PKG_NAME.bright_green().bold(),
level_display,
record.target().white(),
"]".bright_green().bold(),
record.args()
)
})
.init();
}
}
pub fn xdg_config_path() -> Option<std::path::PathBuf> {
use std::path::PathBuf;
#[cfg(unix)]
{
let config_dir = std::env::var("XDG_CONFIG_HOME")
.ok()
.map(PathBuf::from)
.or_else(|| {
std::env::var("HOME")
.ok()
.map(|h| PathBuf::from(h).join(".config"))
});
config_dir.map(|d| d.join(PKG_NAME).join(format!("{PKG_NAME}.toml")))
}
#[cfg(windows)]
{
std::env::var("APPDATA").ok().map(|a| {
PathBuf::from(a)
.join(PKG_NAME)
.join(format!("{}.toml", PKG_NAME))
})
}
}
#[must_use]
pub fn load_config() -> RuntimeConfig {
let overlay = xdg_config_path();
match RuntimeConfig::load_config_with_overlay(crate::DEFAULT_CONFIG_TOML, overlay.as_deref()) {
Ok(config) => {
if let Some(p) = overlay.as_ref().filter(|p| p.exists()) {
debug!("Merged config from {}", p.display());
}
config
}
Err(_) => RuntimeConfig::default(),
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct CliRuntimeFlags {
pub progress: bool,
pub dev: bool,
pub full: bool,
pub redact: bool,
pub no_media: bool,
}
pub fn apply_cli_to_config(config: &mut RuntimeConfig, cli: CliRuntimeFlags) -> anyhow::Result<()> {
config.flags.show_progress = match (cli.progress, cli.dev, is_stderr_tty()) {
(true, true, _) => {
debug!("--progress/-p flag was detected but will be disabled (dev mode)");
false
}
(true, false, false) => {
warn!("--progress/-p flag was detected but will be disabled (not a TTY)");
false
}
(true, false, true) => true,
(false, _, _) => false,
};
if cli.full {
config.output_mode = OutputMode::Full;
}
config.flags.redact_paths = cli.redact;
config.flags.skip_media_metadata = cli.no_media;
config
.validate_external()
.map_err(|e| anyhow::anyhow!("Invalid config: {e}"))
}
pub fn resolve_output_paths(
input: Vec<String>,
output: Option<String>,
) -> anyhow::Result<(Vec<String>, Option<String>)> {
if input.is_empty() {
return Err(anyhow::anyhow!("At least one input file is required"));
}
let output = match output {
Some(out) => {
fs::create_dir_all(&out)?;
let canonical = std::path::Path::new(&out)
.canonicalize()
.with_context(|| "Failed to resolve output directory")?;
Some(canonical.to_string_lossy().into_owned())
}
None => None,
};
Ok((input, output))
}