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()
89 .or_else(|| {
90 crate::theme::paths::config_dir_override().map(|root| root.join("settings.conf"))
91 })
92 .or_else(|| {
93 env::var("XDG_CONFIG_HOME")
94 .ok()
95 .map(PathBuf::from)
96 .or_else(|| env::var("HOME").ok().map(|h| Path::new(&h).join(".config")))
97 .map(|base| base.join("pacsea").join("settings.conf"))
98 });
99
100 let settings_metadata = settings_path.as_ref().and_then(|p| fs::metadata(p).ok());
101 let settings_mtime = settings_metadata.as_ref().and_then(|m| m.modified().ok());
102 let settings_size = settings_metadata.as_ref().map(std::fs::Metadata::len);
103
104 let keybinds_path = resolve_keybinds_config_path();
105 let keybinds_metadata = keybinds_path.as_ref().and_then(|p| fs::metadata(p).ok());
106 let keybinds_mtime = keybinds_metadata.as_ref().and_then(|m| m.modified().ok());
107 let keybinds_size = keybinds_metadata.as_ref().map(std::fs::Metadata::len);
108
109 let cache_initialized = cache.initialized;
110 let mtimes_match = cache_initialized
111 && cache.settings_mtime == settings_mtime
112 && cache.settings_size == settings_size
113 && cache.keybinds_mtime == keybinds_mtime
114 && cache.keybinds_size == keybinds_size;
115 if mtimes_match {
116 if tracing::enabled!(tracing::Level::TRACE) {
117 debug!("[Config] Using cached settings (unchanged files)");
118 }
119 return cache.settings.clone();
120 }
121
122 if let Some(p) = settings_path.as_ref()
123 && let Ok(content) = fs::read_to_string(p)
124 {
125 debug!(path = %p.display(), bytes = content.len(), "[Config] Loaded settings.conf");
126 parse_settings(&content, p, &mut out);
127 } else if let Some(p) = settings_path.as_ref() {
128 warn!(
129 path = %p.display(),
130 "[Config] settings.conf missing or unreadable, using defaults"
131 );
132 }
133
134 normalize(&mut out);
136
137 if let Some(kp) = keybinds_path.as_ref() {
139 if let Ok(content) = fs::read_to_string(kp) {
140 debug!(path = %kp.display(), bytes = content.len(), "[Config] Loaded keybinds.conf");
141 parse_keybinds(&content, &mut out);
142 }
144 } else if let Some(p) = settings_path.as_ref() {
145 if let Ok(content) = fs::read_to_string(p) {
147 debug!(
148 path = %p.display(),
149 bytes = content.len(),
150 "[Config] Loaded legacy keybinds from settings.conf"
151 );
152 parse_keybinds(&content, &mut out);
153 }
154 }
155
156 let sum = out
158 .layout_left_pct
159 .saturating_add(out.layout_center_pct)
160 .saturating_add(out.layout_right_pct);
161 if sum != 100
162 || out.layout_left_pct == 0
163 || out.layout_center_pct == 0
164 || out.layout_right_pct == 0
165 {
166 let keymap = out.keymap.clone();
168 out = Settings::default();
169 out.keymap = keymap;
170 debug!(
171 layout_left = out.layout_left_pct,
172 layout_center = out.layout_center_pct,
173 layout_right = out.layout_right_pct,
174 "[Config] Layout percentages invalid, reset to defaults while preserving keybinds"
175 );
176 }
177 cache.settings_mtime = settings_mtime;
178 cache.settings_size = settings_size;
179 cache.keybinds_mtime = keybinds_mtime;
180 cache.keybinds_size = keybinds_size;
181 cache.settings = out.clone();
182 cache.initialized = true;
183 out
184}
185
186#[cfg(test)]
187mod tests {
188 #[test]
189 fn settings_parse_values_and_keybinds_with_defaults_on_invalid_sum() {
200 let _guard = crate::theme::test_mutex()
201 .lock()
202 .expect("Test mutex poisoned");
203 let orig_home = std::env::var_os("HOME");
204 let base = std::env::temp_dir().join(format!(
205 "pacsea_test_settings_{}_{}",
206 std::process::id(),
207 std::time::SystemTime::now()
208 .duration_since(std::time::UNIX_EPOCH)
209 .expect("System time is before UNIX epoch")
210 .as_nanos()
211 ));
212 let cfg = base.join(".config").join("pacsea");
213 let _ = std::fs::create_dir_all(&cfg);
214 unsafe { std::env::set_var("HOME", base.display().to_string()) };
215
216 let settings_path = cfg.join("settings.conf");
218 std::fs::write(
219 &settings_path,
220 "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",
221 )
222 .expect("failed to write test settings file");
223 let keybinds_path = cfg.join("keybinds.conf");
225 std::fs::write(&keybinds_path, "keybind_exit = Ctrl+Q\nkeybind_help = F1\n")
226 .expect("Failed to write test keybinds file");
227
228 let s = super::settings();
229 assert_eq!(
231 s.layout_left_pct + s.layout_center_pct + s.layout_right_pct,
232 100
233 );
234 assert!(!s.keymap.exit.is_empty());
236 assert!(!s.keymap.help_overlay.is_empty());
237
238 unsafe {
239 if let Some(v) = orig_home {
240 std::env::set_var("HOME", v);
241 } else {
242 std::env::remove_var("HOME");
243 }
244 }
245 let _ = std::fs::remove_dir_all(&base);
246 }
247}