leenfetch 1.4.1

Fast, minimal, customizable system info tool in Rust (Neofetch alternative)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
pub mod defaults;
pub mod settings;

use self::{
    defaults::{
        CLASSIC_NEOFETCH_CONFIG, CLEAN_MONO_CONFIG, DEFAULT_CONFIG, HARDWARE_HEAVY_CONFIG,
        MINIMAL_CONFIG, NEOFETCH_CONFIG, SCREENSHOT_CONFIG, SYSTEM_ADMIN_CONFIG, TINY_CONFIG,
    },
    settings::{Config, Flags, LayoutItem},
};
use dirs::config_dir;
use json5;
use once_cell::sync::Lazy;
use std::io::Write;
use std::path::PathBuf;
use std::{
    collections::HashMap,
    fs::{self, File},
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigPreset {
    Default,
    Neofetch,
    ClassicNeofetch,
    Minimal,
    Screenshot,
    HardwareHeavy,
    SystemAdmin,
    CleanMono,
    Tiny,
}

static DEFAULT_CONFIG_CACHE: Lazy<Config> = Lazy::new(|| {
    json5::from_str(DEFAULT_CONFIG)
        .unwrap_or_else(|e| panic!("Built-in default config is invalid JSON: {e}"))
});

static NEOFETCH_CONFIG_CACHE: Lazy<Config> = Lazy::new(|| {
    json5::from_str(NEOFETCH_CONFIG)
        .unwrap_or_else(|e| panic!("Built-in neofetch config is invalid JSON: {e}"))
});

static CLASSIC_NEOFETCH_CONFIG_CACHE: Lazy<Config> = Lazy::new(|| {
    json5::from_str(CLASSIC_NEOFETCH_CONFIG)
        .unwrap_or_else(|e| panic!("Built-in classic neofetch config is invalid JSON: {e}"))
});

static MINIMAL_CONFIG_CACHE: Lazy<Config> = Lazy::new(|| {
    json5::from_str(MINIMAL_CONFIG)
        .unwrap_or_else(|e| panic!("Built-in minimal config is invalid JSON: {e}"))
});

static SCREENSHOT_CONFIG_CACHE: Lazy<Config> = Lazy::new(|| {
    json5::from_str(SCREENSHOT_CONFIG)
        .unwrap_or_else(|e| panic!("Built-in screenshot config is invalid JSON: {e}"))
});

static HARDWARE_HEAVY_CONFIG_CACHE: Lazy<Config> = Lazy::new(|| {
    json5::from_str(HARDWARE_HEAVY_CONFIG)
        .unwrap_or_else(|e| panic!("Built-in hardware-heavy config is invalid JSON: {e}"))
});

static SYSTEM_ADMIN_CONFIG_CACHE: Lazy<Config> = Lazy::new(|| {
    json5::from_str(SYSTEM_ADMIN_CONFIG)
        .unwrap_or_else(|e| panic!("Built-in system-admin config is invalid JSON: {e}"))
});

static CLEAN_MONO_CONFIG_CACHE: Lazy<Config> = Lazy::new(|| {
    json5::from_str(CLEAN_MONO_CONFIG)
        .unwrap_or_else(|e| panic!("Built-in clean mono config is invalid JSON: {e}"))
});

static TINY_CONFIG_CACHE: Lazy<Config> = Lazy::new(|| {
    json5::from_str(TINY_CONFIG)
        .unwrap_or_else(|e| panic!("Built-in tiny config is invalid JSON: {e}"))
});

/// Loads the unified configuration from `config.jsonc`.
fn load_config() -> Result<Config, String> {
    let path = config_file("config.jsonc");
    let data = fs::read_to_string(&path)
        .map_err(|e| format!("Failed to read config.jsonc ({}): {}", path.display(), e))?;
    load_config_from_str(&data)
}

fn load_config_from_str(data: &str) -> Result<Config, String> {
    json5::from_str(data).map_err(|e| format!("Invalid JSONC in config.jsonc: {}", e))
}

pub fn load_preset_config(preset: ConfigPreset) -> Config {
    match preset {
        ConfigPreset::Default => default_config(),
        ConfigPreset::Neofetch => NEOFETCH_CONFIG_CACHE.clone(),
        ConfigPreset::ClassicNeofetch => CLASSIC_NEOFETCH_CONFIG_CACHE.clone(),
        ConfigPreset::Minimal => MINIMAL_CONFIG_CACHE.clone(),
        ConfigPreset::Screenshot => SCREENSHOT_CONFIG_CACHE.clone(),
        ConfigPreset::HardwareHeavy => HARDWARE_HEAVY_CONFIG_CACHE.clone(),
        ConfigPreset::SystemAdmin => SYSTEM_ADMIN_CONFIG_CACHE.clone(),
        ConfigPreset::CleanMono => CLEAN_MONO_CONFIG_CACHE.clone(),
        ConfigPreset::Tiny => TINY_CONFIG_CACHE.clone(),
    }
}

pub fn preset_label(preset: ConfigPreset) -> &'static str {
    match preset {
        ConfigPreset::Default => "leenfetch default",
        ConfigPreset::Neofetch => "neofetch-compatible",
        ConfigPreset::ClassicNeofetch => "classic neofetch",
        ConfigPreset::Minimal => "minimal",
        ConfigPreset::Screenshot => "screenshot",
        ConfigPreset::HardwareHeavy => "hardware-heavy",
        ConfigPreset::SystemAdmin => "system-admin",
        ConfigPreset::CleanMono => "clean mono",
        ConfigPreset::Tiny => "tiny",
    }
}

pub fn preset_content(preset: ConfigPreset) -> &'static str {
    match preset {
        ConfigPreset::Default => DEFAULT_CONFIG,
        ConfigPreset::Neofetch => NEOFETCH_CONFIG,
        ConfigPreset::ClassicNeofetch => CLASSIC_NEOFETCH_CONFIG,
        ConfigPreset::Minimal => MINIMAL_CONFIG,
        ConfigPreset::Screenshot => SCREENSHOT_CONFIG,
        ConfigPreset::HardwareHeavy => HARDWARE_HEAVY_CONFIG,
        ConfigPreset::SystemAdmin => SYSTEM_ADMIN_CONFIG,
        ConfigPreset::CleanMono => CLEAN_MONO_CONFIG,
        ConfigPreset::Tiny => TINY_CONFIG,
    }
}

pub fn config_path() -> PathBuf {
    config_file("config.jsonc")
}

pub fn config_file_path(name: &str) -> PathBuf {
    config_file(name)
}

pub fn config_exists() -> bool {
    config_path().exists()
}

/// Loads configuration from a custom path when provided.
pub fn load_config_at(path: Option<&str>) -> Result<Config, String> {
    match path {
        Some(custom_path) => {
            let data = fs::read_to_string(custom_path)
                .map_err(|err| format!("Failed to read config at {}: {}", custom_path, err))?;
            load_config_from_str(&data)
        }
        None => load_config(),
    }
}

/// Returns the built-in default configuration.
pub fn default_config() -> Config {
    DEFAULT_CONFIG_CACHE.clone()
}

/// Returns the built-in default layout section.
pub fn default_layout() -> Vec<LayoutItem> {
    default_config().layout
}

/// Loads the effective configuration, falling back to built-in defaults on any error.
pub fn load_effective_config_at(path: Option<&str>) -> Config {
    let mut config = load_config_at(path).unwrap_or_else(|_| default_config());
    if config.layout.is_empty() {
        config.layout = default_layout();
    }
    config
}

/// Loads the effective configuration from the default path, falling back to built-in defaults.
pub fn load_effective_config() -> Config {
    load_effective_config_at(None)
}

/// Loads the modules section from `config.jsonc`.
///
/// # Returns
///
/// A `Vec<LayoutItem>` containing the loaded module configuration.
pub fn load_print_layout() -> Vec<LayoutItem> {
    let layout = load_config()
        .unwrap_or_else(|e| {
            eprintln!("leenfetch: config error: {e}; using defaults");
            default_config()
        })
        .layout;
    if layout.is_empty() {
        default_layout()
    } else {
        layout
    }
}

/// Loads the configuration flags from `config.jsonc`.
///
/// # Returns
///
/// A `Flags` struct containing the loaded configuration.
pub fn load_flags() -> Flags {
    load_config()
        .unwrap_or_else(|e| {
            eprintln!("leenfetch: config error: {e}; using defaults");
            default_config()
        })
        .flags
}

/// Generates the default unified configuration file.
///
/// Writes `config.jsonc` with the default contents. Returns a map with the filename
/// and whether the operation succeeded, matching the previous multi-file API.
pub fn generate_config_files() -> HashMap<String, bool> {
    let mut results = HashMap::new();

    let result = write_config_preset(ConfigPreset::Default).is_ok();
    results.insert("config.jsonc".to_string(), result);

    results
}

pub fn write_config_preset(preset: ConfigPreset) -> std::io::Result<()> {
    save_to_config_file("config.jsonc", preset_content(preset))
}

/// Saves the provided content to a configuration file with the specified file name.
///
/// The function ensures that the directory for the file exists, creating it if necessary.
/// It then writes the content to the file, overwriting any existing content.
///
/// # Arguments
///
/// * `file_name` - A string slice that holds the name of the file to be created or overwritten.
/// * `content` - A string slice containing the content to write to the file.
///
/// # Returns
///
/// A `Result` which is:
///
/// * `Ok(())` if the operation is successful.
/// * `Err` if an error occurs during directory creation or file writing.
fn save_to_config_file(file_name: &str, content: &str) -> std::io::Result<()> {
    let path = config_file(file_name);

    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }

    let mut file = File::create(&path)?;
    file.write_all(content.as_bytes())?;

    Ok(())
}

/// Deletes the generated configuration file.
///
/// This function is used in the `--clean-config` flag, and it removes the default config file.
/// It returns a HashMap where the key is the config file name and the value indicates
/// whether the file was deleted.
pub fn delete_config_files() -> HashMap<String, bool> {
    let mut results = HashMap::new();

    let file = "config.jsonc";
    let result = delete_config_file(file).is_ok();
    results.insert(file.to_string(), result);

    results
}

/// Deletes the given configuration file, returning an error if the operation fails.
///
/// This function is used by `delete_config_files` to remove the generated configuration.
/// It does not report an error if the file does not exist.
fn delete_config_file(file_name: &str) -> std::io::Result<()> {
    let path = config_file(file_name);

    if path.exists() {
        std::fs::remove_file(path)?;
    }

    Ok(())
}

/// Returns a `PathBuf` for the configuration file with the given `name`.
///
/// If `XDG_CONFIG_HOME` is set, the function will return a path in that directory.
/// If `XDG_CONFIG_HOME` is not set, the function will return a path in the current directory.
///
/// The returned path will have the "leenfetch" directory as its parent, and the given `name` as its file name.
fn config_file(name: &str) -> PathBuf {
    config_dir()
        .unwrap_or_else(|| PathBuf::from("."))
        .join("leenfetch")
        .join(name)
}

/// Ensures that the configuration file exists.
///
/// This function creates the `leenfetch` directory and the config file if they do not exist.
/// It will not overwrite an existing config file.
///
/// Returns a `HashMap` where the key is the config file name and the value indicates
/// whether the file was created.
pub fn ensure_config_files_exist() -> HashMap<String, bool> {
    let mut results = HashMap::new();

    let filename = "config.jsonc";
    let created = ensure_config_file_exists(filename, DEFAULT_CONFIG).unwrap_or(false);
    results.insert(filename.to_string(), created);

    results
}

/// Ensures that the configuration file with the given `file_name` exists.
///
/// If the file does not exist, the function will create it with the given `default_content`.
/// If the file already exists, the function will return `false` without modifying it.
///
/// # Returns
///
/// A `Result` which is `Ok(true)` if the file was created, or `Ok(false)` if the file already existed.
/// If an error occurs while attempting to create the file, the function will return `Err`.
fn ensure_config_file_exists(file_name: &str, default_content: &str) -> std::io::Result<bool> {
    let path = config_file(file_name);

    if path.exists() {
        return Ok(false); // Already exists
    }

    save_to_config_file(file_name, default_content)?;
    Ok(true) // Created
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_utils::EnvLock;
    use std::fs;
    use std::time::{SystemTime, UNIX_EPOCH};

    #[test]
    fn load_effective_config_falls_back_to_defaults_for_invalid_custom_file() {
        let unique = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let path = std::env::temp_dir().join(format!("leenfetch_invalid_config_{unique}.jsonc"));
        fs::write(&path, "{ invalid jsonc").unwrap();

        let config = load_effective_config_at(path.to_str());
        assert!(!config.layout.is_empty());
        assert_eq!(
            config.flags.ascii_distro,
            default_config().flags.ascii_distro
        );

        fs::remove_file(path).unwrap();
    }

    #[test]
    fn preset_configs_parse_successfully() {
        for preset in [
            ConfigPreset::Default,
            ConfigPreset::Neofetch,
            ConfigPreset::ClassicNeofetch,
            ConfigPreset::Minimal,
            ConfigPreset::Screenshot,
            ConfigPreset::HardwareHeavy,
            ConfigPreset::SystemAdmin,
            ConfigPreset::CleanMono,
            ConfigPreset::Tiny,
        ] {
            let config = load_preset_config(preset);
            assert!(
                !config.layout.is_empty(),
                "{} had an empty layout",
                preset_label(preset)
            );
        }
    }

    #[test]
    fn write_config_preset_overwrites_existing_file() {
        let unique = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let temp_dir = std::env::temp_dir().join(format!("leenfetch_config_write_{unique}"));
        fs::create_dir_all(&temp_dir).unwrap();

        let env = EnvLock::acquire(&["XDG_CONFIG_HOME"]);
        env.set_var("XDG_CONFIG_HOME", temp_dir.to_str().unwrap());

        write_config_preset(ConfigPreset::Tiny).unwrap();

        let written = fs::read_to_string(config_path()).unwrap();
        assert!(written.contains("\"ascii_distro\": \"off\""));
        assert!(written.contains("\"modules\""));

        fs::remove_dir_all(temp_dir).unwrap();
    }
}