1pub mod defaults;
2pub mod settings;
3
4use self::{
5 defaults::{
6 CLASSIC_NEOFETCH_CONFIG, CLEAN_MONO_CONFIG, DEFAULT_CONFIG, HARDWARE_HEAVY_CONFIG,
7 MINIMAL_CONFIG, NEOFETCH_CONFIG, SCREENSHOT_CONFIG, SYSTEM_ADMIN_CONFIG, TINY_CONFIG,
8 },
9 settings::{Config, Flags, LayoutItem},
10};
11use dirs::config_dir;
12use json5;
13use once_cell::sync::Lazy;
14use std::io::Write;
15use std::path::PathBuf;
16use std::{
17 collections::HashMap,
18 fs::{self, File},
19};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ConfigPreset {
23 Default,
24 Neofetch,
25 ClassicNeofetch,
26 Minimal,
27 Screenshot,
28 HardwareHeavy,
29 SystemAdmin,
30 CleanMono,
31 Tiny,
32}
33
34static DEFAULT_CONFIG_CACHE: Lazy<Config> = Lazy::new(|| {
35 json5::from_str(DEFAULT_CONFIG)
36 .unwrap_or_else(|e| panic!("Built-in default config is invalid JSON: {e}"))
37});
38
39static NEOFETCH_CONFIG_CACHE: Lazy<Config> = Lazy::new(|| {
40 json5::from_str(NEOFETCH_CONFIG)
41 .unwrap_or_else(|e| panic!("Built-in neofetch config is invalid JSON: {e}"))
42});
43
44static CLASSIC_NEOFETCH_CONFIG_CACHE: Lazy<Config> = Lazy::new(|| {
45 json5::from_str(CLASSIC_NEOFETCH_CONFIG)
46 .unwrap_or_else(|e| panic!("Built-in classic neofetch config is invalid JSON: {e}"))
47});
48
49static MINIMAL_CONFIG_CACHE: Lazy<Config> = Lazy::new(|| {
50 json5::from_str(MINIMAL_CONFIG)
51 .unwrap_or_else(|e| panic!("Built-in minimal config is invalid JSON: {e}"))
52});
53
54static SCREENSHOT_CONFIG_CACHE: Lazy<Config> = Lazy::new(|| {
55 json5::from_str(SCREENSHOT_CONFIG)
56 .unwrap_or_else(|e| panic!("Built-in screenshot config is invalid JSON: {e}"))
57});
58
59static HARDWARE_HEAVY_CONFIG_CACHE: Lazy<Config> = Lazy::new(|| {
60 json5::from_str(HARDWARE_HEAVY_CONFIG)
61 .unwrap_or_else(|e| panic!("Built-in hardware-heavy config is invalid JSON: {e}"))
62});
63
64static SYSTEM_ADMIN_CONFIG_CACHE: Lazy<Config> = Lazy::new(|| {
65 json5::from_str(SYSTEM_ADMIN_CONFIG)
66 .unwrap_or_else(|e| panic!("Built-in system-admin config is invalid JSON: {e}"))
67});
68
69static CLEAN_MONO_CONFIG_CACHE: Lazy<Config> = Lazy::new(|| {
70 json5::from_str(CLEAN_MONO_CONFIG)
71 .unwrap_or_else(|e| panic!("Built-in clean mono config is invalid JSON: {e}"))
72});
73
74static TINY_CONFIG_CACHE: Lazy<Config> = Lazy::new(|| {
75 json5::from_str(TINY_CONFIG)
76 .unwrap_or_else(|e| panic!("Built-in tiny config is invalid JSON: {e}"))
77});
78
79fn load_config() -> Result<Config, String> {
81 let path = config_file("config.jsonc");
82 let data = fs::read_to_string(&path)
83 .map_err(|e| format!("Failed to read config.jsonc ({}): {}", path.display(), e))?;
84 load_config_from_str(&data)
85}
86
87fn load_config_from_str(data: &str) -> Result<Config, String> {
88 json5::from_str(data).map_err(|e| format!("Invalid JSONC in config.jsonc: {}", e))
89}
90
91pub fn load_preset_config(preset: ConfigPreset) -> Config {
92 match preset {
93 ConfigPreset::Default => default_config(),
94 ConfigPreset::Neofetch => NEOFETCH_CONFIG_CACHE.clone(),
95 ConfigPreset::ClassicNeofetch => CLASSIC_NEOFETCH_CONFIG_CACHE.clone(),
96 ConfigPreset::Minimal => MINIMAL_CONFIG_CACHE.clone(),
97 ConfigPreset::Screenshot => SCREENSHOT_CONFIG_CACHE.clone(),
98 ConfigPreset::HardwareHeavy => HARDWARE_HEAVY_CONFIG_CACHE.clone(),
99 ConfigPreset::SystemAdmin => SYSTEM_ADMIN_CONFIG_CACHE.clone(),
100 ConfigPreset::CleanMono => CLEAN_MONO_CONFIG_CACHE.clone(),
101 ConfigPreset::Tiny => TINY_CONFIG_CACHE.clone(),
102 }
103}
104
105pub fn preset_label(preset: ConfigPreset) -> &'static str {
106 match preset {
107 ConfigPreset::Default => "leenfetch default",
108 ConfigPreset::Neofetch => "neofetch-compatible",
109 ConfigPreset::ClassicNeofetch => "classic neofetch",
110 ConfigPreset::Minimal => "minimal",
111 ConfigPreset::Screenshot => "screenshot",
112 ConfigPreset::HardwareHeavy => "hardware-heavy",
113 ConfigPreset::SystemAdmin => "system-admin",
114 ConfigPreset::CleanMono => "clean mono",
115 ConfigPreset::Tiny => "tiny",
116 }
117}
118
119pub fn preset_content(preset: ConfigPreset) -> &'static str {
120 match preset {
121 ConfigPreset::Default => DEFAULT_CONFIG,
122 ConfigPreset::Neofetch => NEOFETCH_CONFIG,
123 ConfigPreset::ClassicNeofetch => CLASSIC_NEOFETCH_CONFIG,
124 ConfigPreset::Minimal => MINIMAL_CONFIG,
125 ConfigPreset::Screenshot => SCREENSHOT_CONFIG,
126 ConfigPreset::HardwareHeavy => HARDWARE_HEAVY_CONFIG,
127 ConfigPreset::SystemAdmin => SYSTEM_ADMIN_CONFIG,
128 ConfigPreset::CleanMono => CLEAN_MONO_CONFIG,
129 ConfigPreset::Tiny => TINY_CONFIG,
130 }
131}
132
133pub fn config_path() -> PathBuf {
134 config_file("config.jsonc")
135}
136
137pub fn config_file_path(name: &str) -> PathBuf {
138 config_file(name)
139}
140
141pub fn config_exists() -> bool {
142 config_path().exists()
143}
144
145pub fn load_config_at(path: Option<&str>) -> Result<Config, String> {
147 match path {
148 Some(custom_path) => {
149 let data = fs::read_to_string(custom_path)
150 .map_err(|err| format!("Failed to read config at {}: {}", custom_path, err))?;
151 load_config_from_str(&data)
152 }
153 None => load_config(),
154 }
155}
156
157pub fn default_config() -> Config {
159 DEFAULT_CONFIG_CACHE.clone()
160}
161
162pub fn default_layout() -> Vec<LayoutItem> {
164 default_config().layout
165}
166
167pub fn load_effective_config_at(path: Option<&str>) -> Config {
169 let mut config = load_config_at(path).unwrap_or_else(|_| default_config());
170 if config.layout.is_empty() {
171 config.layout = default_layout();
172 }
173 config
174}
175
176pub fn load_effective_config() -> Config {
178 load_effective_config_at(None)
179}
180
181pub fn load_print_layout() -> Vec<LayoutItem> {
187 let layout = load_config()
188 .unwrap_or_else(|e| {
189 eprintln!("leenfetch: config error: {e}; using defaults");
190 default_config()
191 })
192 .layout;
193 if layout.is_empty() {
194 default_layout()
195 } else {
196 layout
197 }
198}
199
200pub fn load_flags() -> Flags {
206 load_config()
207 .unwrap_or_else(|e| {
208 eprintln!("leenfetch: config error: {e}; using defaults");
209 default_config()
210 })
211 .flags
212}
213
214pub fn generate_config_files() -> HashMap<String, bool> {
219 let mut results = HashMap::new();
220
221 let result = write_config_preset(ConfigPreset::Default).is_ok();
222 results.insert("config.jsonc".to_string(), result);
223
224 results
225}
226
227pub fn write_config_preset(preset: ConfigPreset) -> std::io::Result<()> {
228 save_to_config_file("config.jsonc", preset_content(preset))
229}
230
231fn save_to_config_file(file_name: &str, content: &str) -> std::io::Result<()> {
248 let path = config_file(file_name);
249
250 if let Some(parent) = path.parent() {
251 fs::create_dir_all(parent)?;
252 }
253
254 let mut file = File::create(&path)?;
255 file.write_all(content.as_bytes())?;
256
257 Ok(())
258}
259
260pub fn delete_config_files() -> HashMap<String, bool> {
266 let mut results = HashMap::new();
267
268 let file = "config.jsonc";
269 let result = delete_config_file(file).is_ok();
270 results.insert(file.to_string(), result);
271
272 results
273}
274
275fn delete_config_file(file_name: &str) -> std::io::Result<()> {
280 let path = config_file(file_name);
281
282 if path.exists() {
283 std::fs::remove_file(path)?;
284 }
285
286 Ok(())
287}
288
289fn config_file(name: &str) -> PathBuf {
296 config_dir()
297 .unwrap_or_else(|| PathBuf::from("."))
298 .join("leenfetch")
299 .join(name)
300}
301
302pub fn ensure_config_files_exist() -> HashMap<String, bool> {
310 let mut results = HashMap::new();
311
312 let filename = "config.jsonc";
313 let created = ensure_config_file_exists(filename, DEFAULT_CONFIG).unwrap_or(false);
314 results.insert(filename.to_string(), created);
315
316 results
317}
318
319fn ensure_config_file_exists(file_name: &str, default_content: &str) -> std::io::Result<bool> {
329 let path = config_file(file_name);
330
331 if path.exists() {
332 return Ok(false); }
334
335 save_to_config_file(file_name, default_content)?;
336 Ok(true) }
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342 use crate::test_utils::EnvLock;
343 use std::fs;
344 use std::time::{SystemTime, UNIX_EPOCH};
345
346 #[test]
347 fn load_effective_config_falls_back_to_defaults_for_invalid_custom_file() {
348 let unique = SystemTime::now()
349 .duration_since(UNIX_EPOCH)
350 .unwrap()
351 .as_nanos();
352 let path = std::env::temp_dir().join(format!("leenfetch_invalid_config_{unique}.jsonc"));
353 fs::write(&path, "{ invalid jsonc").unwrap();
354
355 let config = load_effective_config_at(path.to_str());
356 assert!(!config.layout.is_empty());
357 assert_eq!(
358 config.flags.ascii_distro,
359 default_config().flags.ascii_distro
360 );
361
362 fs::remove_file(path).unwrap();
363 }
364
365 #[test]
366 fn preset_configs_parse_successfully() {
367 for preset in [
368 ConfigPreset::Default,
369 ConfigPreset::Neofetch,
370 ConfigPreset::ClassicNeofetch,
371 ConfigPreset::Minimal,
372 ConfigPreset::Screenshot,
373 ConfigPreset::HardwareHeavy,
374 ConfigPreset::SystemAdmin,
375 ConfigPreset::CleanMono,
376 ConfigPreset::Tiny,
377 ] {
378 let config = load_preset_config(preset);
379 assert!(
380 !config.layout.is_empty(),
381 "{} had an empty layout",
382 preset_label(preset)
383 );
384 }
385 }
386
387 #[test]
388 fn write_config_preset_overwrites_existing_file() {
389 let unique = SystemTime::now()
390 .duration_since(UNIX_EPOCH)
391 .unwrap()
392 .as_nanos();
393 let temp_dir = std::env::temp_dir().join(format!("leenfetch_config_write_{unique}"));
394 fs::create_dir_all(&temp_dir).unwrap();
395
396 let env = EnvLock::acquire(&["XDG_CONFIG_HOME"]);
397 env.set_var("XDG_CONFIG_HOME", temp_dir.to_str().unwrap());
398
399 write_config_preset(ConfigPreset::Tiny).unwrap();
400
401 let written = fs::read_to_string(config_path()).unwrap();
402 assert!(written.contains("\"ascii_distro\": \"off\""));
403 assert!(written.contains("\"modules\""));
404
405 fs::remove_dir_all(temp_dir).unwrap();
406 }
407}