use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use std::time::SystemTime;
use crate::theme::paths::{resolve_keybinds_config_path, resolve_settings_config_path};
use crate::theme::types::Settings;
use tracing::{debug, warn};
mod normalize;
mod parse_keybinds;
mod parse_settings;
use normalize::normalize;
use parse_keybinds::parse_keybinds;
use parse_settings::parse_settings;
struct SettingsCache {
settings: Settings,
settings_mtime: Option<SystemTime>,
keybinds_mtime: Option<SystemTime>,
settings_size: Option<u64>,
keybinds_size: Option<u64>,
initialized: bool,
}
impl SettingsCache {
fn new() -> Self {
Self {
settings: Settings::default(),
settings_mtime: None,
keybinds_mtime: None,
settings_size: None,
keybinds_size: None,
initialized: false,
}
}
}
static SETTINGS_CACHE: OnceLock<Mutex<SettingsCache>> = OnceLock::new();
#[must_use]
pub fn settings() -> Settings {
let mut cache = SETTINGS_CACHE
.get_or_init(|| Mutex::new(SettingsCache::new()))
.lock()
.expect("Settings cache mutex poisoned");
let mut out = Settings::default();
let settings_path = resolve_settings_config_path().or_else(|| {
env::var("XDG_CONFIG_HOME")
.ok()
.map(PathBuf::from)
.or_else(|| env::var("HOME").ok().map(|h| Path::new(&h).join(".config")))
.map(|base| base.join("pacsea").join("settings.conf"))
});
let settings_metadata = settings_path.as_ref().and_then(|p| fs::metadata(p).ok());
let settings_mtime = settings_metadata.as_ref().and_then(|m| m.modified().ok());
let settings_size = settings_metadata.as_ref().map(std::fs::Metadata::len);
let keybinds_path = resolve_keybinds_config_path();
let keybinds_metadata = keybinds_path.as_ref().and_then(|p| fs::metadata(p).ok());
let keybinds_mtime = keybinds_metadata.as_ref().and_then(|m| m.modified().ok());
let keybinds_size = keybinds_metadata.as_ref().map(std::fs::Metadata::len);
let cache_initialized = cache.initialized;
let mtimes_match = cache_initialized
&& cache.settings_mtime == settings_mtime
&& cache.settings_size == settings_size
&& cache.keybinds_mtime == keybinds_mtime
&& cache.keybinds_size == keybinds_size;
if mtimes_match {
if tracing::enabled!(tracing::Level::TRACE) {
debug!("[Config] Using cached settings (unchanged files)");
}
return cache.settings.clone();
}
if let Some(p) = settings_path.as_ref()
&& let Ok(content) = fs::read_to_string(p)
{
debug!(path = %p.display(), bytes = content.len(), "[Config] Loaded settings.conf");
parse_settings(&content, p, &mut out);
} else if let Some(p) = settings_path.as_ref() {
warn!(
path = %p.display(),
"[Config] settings.conf missing or unreadable, using defaults"
);
}
normalize(&mut out);
if let Some(kp) = keybinds_path.as_ref() {
if let Ok(content) = fs::read_to_string(kp) {
debug!(path = %kp.display(), bytes = content.len(), "[Config] Loaded keybinds.conf");
parse_keybinds(&content, &mut out);
}
} else if let Some(p) = settings_path.as_ref() {
if let Ok(content) = fs::read_to_string(p) {
debug!(
path = %p.display(),
bytes = content.len(),
"[Config] Loaded legacy keybinds from settings.conf"
);
parse_keybinds(&content, &mut out);
}
}
let sum = out
.layout_left_pct
.saturating_add(out.layout_center_pct)
.saturating_add(out.layout_right_pct);
if sum != 100
|| out.layout_left_pct == 0
|| out.layout_center_pct == 0
|| out.layout_right_pct == 0
{
let keymap = out.keymap.clone();
out = Settings::default();
out.keymap = keymap;
debug!(
layout_left = out.layout_left_pct,
layout_center = out.layout_center_pct,
layout_right = out.layout_right_pct,
"[Config] Layout percentages invalid, reset to defaults while preserving keybinds"
);
}
cache.settings_mtime = settings_mtime;
cache.settings_size = settings_size;
cache.keybinds_mtime = keybinds_mtime;
cache.keybinds_size = keybinds_size;
cache.settings = out.clone();
cache.initialized = true;
out
}
#[cfg(test)]
mod tests {
#[test]
fn settings_parse_values_and_keybinds_with_defaults_on_invalid_sum() {
let _guard = crate::theme::test_mutex()
.lock()
.expect("Test mutex poisoned");
let orig_home = std::env::var_os("HOME");
let base = std::env::temp_dir().join(format!(
"pacsea_test_settings_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("System time is before UNIX epoch")
.as_nanos()
));
let cfg = base.join(".config").join("pacsea");
let _ = std::fs::create_dir_all(&cfg);
unsafe { std::env::set_var("HOME", base.display().to_string()) };
let settings_path = cfg.join("settings.conf");
std::fs::write(
&settings_path,
"layout_left_pct=10\nlayout_center_pct=10\nlayout_right_pct=10\nsort_mode=aur_popularity\nclipboard_suffix=OK\nshow_search_history_pane=true\nshow_install_pane=false\nshow_keybinds_footer=true\n",
)
.expect("failed to write test settings file");
let keybinds_path = cfg.join("keybinds.conf");
std::fs::write(&keybinds_path, "keybind_exit = Ctrl+Q\nkeybind_help = F1\n")
.expect("Failed to write test keybinds file");
let s = super::settings();
assert_eq!(
s.layout_left_pct + s.layout_center_pct + s.layout_right_pct,
100
);
assert!(!s.keymap.exit.is_empty());
assert!(!s.keymap.help_overlay.is_empty());
unsafe {
if let Some(v) = orig_home {
std::env::set_var("HOME", v);
} else {
std::env::remove_var("HOME");
}
}
let _ = std::fs::remove_dir_all(&base);
}
}