pacsea/theme/settings/
mod.rs1use std::env;
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::sync::{Mutex, OnceLock};
5use std::time::SystemTime;
6
7use crate::theme::paths::{resolve_keybinds_config_path, resolve_settings_config_path};
8use crate::theme::types::Settings;
9use tracing::{debug, warn};
10
11mod normalize;
13mod parse_keybinds;
15mod parse_settings;
17
18use normalize::normalize;
19use parse_keybinds::parse_keybinds;
20use parse_settings::parse_settings;
21
22struct SettingsCache {
30 settings: Settings,
32 settings_mtime: Option<SystemTime>,
34 keybinds_mtime: Option<SystemTime>,
36 settings_size: Option<u64>,
38 keybinds_size: Option<u64>,
40 initialized: bool,
42}
43
44impl SettingsCache {
45 fn new() -> Self {
53 Self {
54 settings: Settings::default(),
55 settings_mtime: None,
56 keybinds_mtime: None,
57 settings_size: None,
58 keybinds_size: None,
59 initialized: false,
60 }
61 }
62}
63
64static SETTINGS_CACHE: OnceLock<Mutex<SettingsCache>> = OnceLock::new();
68
69#[must_use]
80pub fn settings() -> Settings {
81 let mut cache = SETTINGS_CACHE
82 .get_or_init(|| Mutex::new(SettingsCache::new()))
83 .lock()
84 .expect("Settings cache mutex poisoned");
85
86 let mut out = Settings::default();
87 let settings_path = resolve_settings_config_path().or_else(|| {
89 env::var("XDG_CONFIG_HOME")
90 .ok()
91 .map(PathBuf::from)
92 .or_else(|| env::var("HOME").ok().map(|h| Path::new(&h).join(".config")))
93 .map(|base| base.join("pacsea").join("settings.conf"))
94 });
95
96 let settings_metadata = settings_path.as_ref().and_then(|p| fs::metadata(p).ok());
97 let settings_mtime = settings_metadata.as_ref().and_then(|m| m.modified().ok());
98 let settings_size = settings_metadata.as_ref().map(std::fs::Metadata::len);
99
100 let keybinds_path = resolve_keybinds_config_path();
101 let keybinds_metadata = keybinds_path.as_ref().and_then(|p| fs::metadata(p).ok());
102 let keybinds_mtime = keybinds_metadata.as_ref().and_then(|m| m.modified().ok());
103 let keybinds_size = keybinds_metadata.as_ref().map(std::fs::Metadata::len);
104
105 let cache_initialized = cache.initialized;
106 let mtimes_match = cache_initialized
107 && cache.settings_mtime == settings_mtime
108 && cache.settings_size == settings_size
109 && cache.keybinds_mtime == keybinds_mtime
110 && cache.keybinds_size == keybinds_size;
111 if mtimes_match {
112 if tracing::enabled!(tracing::Level::TRACE) {
113 debug!("[Config] Using cached settings (unchanged files)");
114 }
115 return cache.settings.clone();
116 }
117
118 if let Some(p) = settings_path.as_ref()
119 && let Ok(content) = fs::read_to_string(p)
120 {
121 debug!(path = %p.display(), bytes = content.len(), "[Config] Loaded settings.conf");
122 parse_settings(&content, p, &mut out);
123 } else if let Some(p) = settings_path.as_ref() {
124 warn!(
125 path = %p.display(),
126 "[Config] settings.conf missing or unreadable, using defaults"
127 );
128 }
129
130 normalize(&mut out);
132
133 if let Some(kp) = keybinds_path.as_ref() {
135 if let Ok(content) = fs::read_to_string(kp) {
136 debug!(path = %kp.display(), bytes = content.len(), "[Config] Loaded keybinds.conf");
137 parse_keybinds(&content, &mut out);
138 }
140 } else if let Some(p) = settings_path.as_ref() {
141 if let Ok(content) = fs::read_to_string(p) {
143 debug!(
144 path = %p.display(),
145 bytes = content.len(),
146 "[Config] Loaded legacy keybinds from settings.conf"
147 );
148 parse_keybinds(&content, &mut out);
149 }
150 }
151
152 let sum = out
154 .layout_left_pct
155 .saturating_add(out.layout_center_pct)
156 .saturating_add(out.layout_right_pct);
157 if sum != 100
158 || out.layout_left_pct == 0
159 || out.layout_center_pct == 0
160 || out.layout_right_pct == 0
161 {
162 let keymap = out.keymap.clone();
164 out = Settings::default();
165 out.keymap = keymap;
166 debug!(
167 layout_left = out.layout_left_pct,
168 layout_center = out.layout_center_pct,
169 layout_right = out.layout_right_pct,
170 "[Config] Layout percentages invalid, reset to defaults while preserving keybinds"
171 );
172 }
173 cache.settings_mtime = settings_mtime;
174 cache.settings_size = settings_size;
175 cache.keybinds_mtime = keybinds_mtime;
176 cache.keybinds_size = keybinds_size;
177 cache.settings = out.clone();
178 cache.initialized = true;
179 out
180}
181
182#[cfg(test)]
183mod tests {
184 #[test]
185 fn settings_parse_values_and_keybinds_with_defaults_on_invalid_sum() {
196 let _guard = crate::theme::test_mutex()
197 .lock()
198 .expect("Test mutex poisoned");
199 let orig_home = std::env::var_os("HOME");
200 let base = std::env::temp_dir().join(format!(
201 "pacsea_test_settings_{}_{}",
202 std::process::id(),
203 std::time::SystemTime::now()
204 .duration_since(std::time::UNIX_EPOCH)
205 .expect("System time is before UNIX epoch")
206 .as_nanos()
207 ));
208 let cfg = base.join(".config").join("pacsea");
209 let _ = std::fs::create_dir_all(&cfg);
210 unsafe { std::env::set_var("HOME", base.display().to_string()) };
211
212 let settings_path = cfg.join("settings.conf");
214 std::fs::write(
215 &settings_path,
216 "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",
217 )
218 .expect("failed to write test settings file");
219 let keybinds_path = cfg.join("keybinds.conf");
221 std::fs::write(&keybinds_path, "keybind_exit = Ctrl+Q\nkeybind_help = F1\n")
222 .expect("Failed to write test keybinds file");
223
224 let s = super::settings();
225 assert_eq!(
227 s.layout_left_pct + s.layout_center_pct + s.layout_right_pct,
228 100
229 );
230 assert!(!s.keymap.exit.is_empty());
232 assert!(!s.keymap.help_overlay.is_empty());
233
234 unsafe {
235 if let Some(v) = orig_home {
236 std::env::set_var("HOME", v);
237 } else {
238 std::env::remove_var("HOME");
239 }
240 }
241 let _ = std::fs::remove_dir_all(&base);
242 }
243}