Skip to main content

leenfetch_core/modules/
utils.rs

1use std::{collections::HashMap, fs, path::Path};
2
3use super::{ascii::get_builtin_ascii_art, colors::get_builtin_distro_colors};
4
5pub const DEFAULT_ANSI_ALL_COLORS: [&str; 16] = [
6    "\x1b[1;30m", // Black
7    "\x1b[1;31m", // Red
8    "\x1b[1;32m", // Green
9    "\x1b[1;33m", // Yellow
10    "\x1b[1;34m", // Blue
11    "\x1b[1;35m", // Magenta
12    "\x1b[1;36m", // Cyan
13    "\x1b[1;37m", // White
14    "\x1b[1;90m", // Bright Black
15    "\x1b[1;91m", // Bright Red
16    "\x1b[1;92m", // Bright Green
17    "\x1b[1;93m", // Bright Yellow
18    "\x1b[1;94m", // Bright Blue
19    "\x1b[1;95m", // Bright Magenta
20    "\x1b[1;96m", // Bright Cyan
21    "\x1b[1;97m", // Bright White
22];
23
24/// Generates a visual bar representation of a percentage using Unicode blocks.
25///
26/// # Arguments
27///
28/// * `percent` - An unsigned 8-bit integer representing the percentage (0-100) to be visualized.
29///
30/// # Returns
31///
32/// * A `String` containing a visual bar representation, with filled blocks (`█`) indicating the
33///   percentage, and empty blocks (`░`) for the remainder. The total length of the bar is 14
34///   characters, enclosed in square brackets.
35pub fn get_bar(percent: u8) -> String {
36    let total_blocks = 14;
37    let filled_blocks = (percent as usize * total_blocks) / 100;
38    let empty_blocks = total_blocks - filled_blocks;
39
40    // Pre-computed block strings for common values (avoid allocations)
41    const BLOCKS: &[&str] = &[
42        "",
43        "█",
44        "██",
45        "███",
46        "████",
47        "█████",
48        "██████",
49        "███████",
50        "████████",
51        "█████████",
52        "██████████",
53        "███████████",
54        "████████████",
55        "█████████████",
56        "██████████████",
57    ];
58    const EMPTY_BLOCKS: &[&str] = &[
59        "",
60        "░",
61        "░░",
62        "░░░",
63        "░░░░",
64        "░░░░░",
65        "░░░░░░",
66        "░░░░░░░",
67        "░░░░░░░░",
68        "░░░░░░░░░",
69        "░░░░░░░░░░",
70        "░░░░░░░░░░░",
71        "░░░░░░░░░░░░",
72        "░░░░░░░░░░░░░",
73        "░░░░░░░░░░░░░░",
74    ];
75
76    let filled = BLOCKS[filled_blocks.min(14)];
77    let empty = EMPTY_BLOCKS[empty_blocks.min(14)];
78
79    format!("[{}{}]", filled, empty)
80}
81
82/// Generates a vector of 2 strings, each containing a row of 8 blocks
83/// colored with different ANSI foreground colors. The first string has
84/// normal colors, the second has bold colors.
85///
86/// The input string `color_blocks` should contain 8 identical block characters
87/// (e.g. █, ░, ▓, ▒, etc.). The output strings will have these blocks
88/// colored with different ANSI colors.
89pub fn get_terminal_color(color_blocks: &str) -> String {
90    let color_codes: [u8; 8] = [30, 31, 32, 33, 34, 35, 36, 37]; // ANSI foreground colors
91
92    let mut normal = Vec::with_capacity(8);
93    // let mut bold = Vec::with_capacity(8);
94
95    for &code in &color_codes {
96        normal.push(format!("\x1b[{}m{}\x1b[0m", code, color_blocks)); // normal
97                                                                       // bold.push(format!("\x1b[1;{}m{}\x1b[0m", code, color_blocks)); // bold
98    }
99
100    // vec![normal.join(""), bold.join("")]
101    normal.join("")
102}
103
104// ---------------------------------
105//        ASCII ART Functions
106// ---------------------------------
107
108/// Reads a file at the given custom path and returns its content as a string.
109/// If there is an error while reading the file, an empty string is returned.
110///
111/// # Arguments
112///
113/// * `custom_path`: The path to the custom ASCII art file.
114///
115/// # Returns
116///
117/// A string containing the content of the file, or an empty string if there was an error.
118pub fn get_custom_ascii(custom_path: &str) -> String {
119    if let Ok(content) = fs::read_to_string(Path::new(custom_path)) {
120        return content;
121    }
122
123    "".to_string()
124}
125
126/// Given a distro name, returns a string of its corresponding ASCII art.
127/// If the distro isn't found, an empty string is returned.
128/// If the distro is "off", an empty string is returned.
129pub fn get_ascii_and_colors(ascii_distro: &str) -> String {
130    if ascii_distro == "off" {
131        return "".to_string();
132    }
133
134    let ascii_art = resolve_ascii_art(ascii_distro);
135
136    ascii_art.to_string()
137}
138
139/// Resolves ASCII art for a given distro name.
140/// Tries direct match first, then falls back to ID_LIKE if available.
141fn resolve_ascii_art(distro: &str) -> &'static str {
142    // 1. Try direct distro name match
143    if let Some(art) = get_builtin_ascii_art(distro) {
144        return art;
145    }
146
147    #[cfg(target_os = "linux")]
148    // 2. No match — try ID_LIKE parent distro
149    if let Some(parent) = crate::modules::linux::system::distro::get_id_like() {
150        if let Some(art) = get_builtin_ascii_art(&parent) {
151            return art;
152        }
153    }
154
155    // 3. Nothing found — return fallback DEFAULT
156    DEFAULT_ASCII
157}
158
159const DEFAULT_ASCII: &str = r#"${c2}        #####
160${c2}       #######
161${c2}       ##${c1}O${c2}#${c1}O${c2}##
162${c2}       #${c3}#####${c2}#
163${c2}     ##${c1}##${c3}###${c1}##${c2}##
164${c2}    #${c1}##########${c2}##
165${c2}   #${c1}############${c2}##
166${c2}   #${c1}############${c2}###
167${c3}  ##${c2}#${c1}###########${c2}##${c3}#
168${c3}######${c2}#${c1}#######${c2}#${c3}######
169${c3}#######${c2}#${c1}#####${c2}#${c3}#######
170${c3}  #####${c2}#######${c3}#####"#;
171
172// ---------------------------------
173//        Color Functions
174// ---------------------------------
175
176/// Replaces placeholders in a string with ANSI escape codes to colorize
177/// the output.
178///
179/// Placeholders are in the form of `${{key}}`, where `key` is the key in
180/// the provided `colors` HashMap. The value associated with the `key` is
181/// the ANSI escape code for the color.
182pub fn colorize_text(input: String, colors: &HashMap<&str, &str>) -> String {
183    let mut result = String::new();
184
185    for line in input.lines() {
186        let mut colored = line.to_owned();
187        for (key, code) in colors {
188            let placeholder = format!("${{{}}}", key);
189            colored = colored.replace(&placeholder, code);
190        }
191        result.push_str(&colored);
192        result.push('\n');
193    }
194
195    result
196}
197
198/// Creates a `HashMap` of ANSI color codes from the given entries.
199///
200/// Each entry is a tuple containing a key and a corresponding ANSI
201/// color code. The function populates the `HashMap` with these entries
202/// and also adds bold variants for each color code that starts with
203/// `\x1b[0;`. The bold variant key is prefixed with "bold." (e.g.,
204/// `bold.c1` for `c1`).
205///
206/// Additionally, a "reset" key is included in the map, which maps to
207/// the ANSI reset code `\x1b[0m`.
208///
209/// # Arguments
210///
211/// * `entries` - A slice of tuples where each tuple contains a string
212///   key and an ANSI color code.
213///
214/// # Returns
215///
216/// * A `HashMap` with the original entries, their bold variants, and a
217///   reset entry.
218pub fn color_palette(
219    entries: &[(&'static str, &'static str)],
220) -> HashMap<&'static str, &'static str> {
221    let mut map = HashMap::new();
222    for (k, v) in entries {
223        map.insert(*k, *v);
224
225        // Add bold variant: bold.c1 → \x1b[1;31m if c1 is \x1b[0;31m
226        if let Some(code) = v.strip_prefix("\x1b[0;") {
227            let bold_code = format!("\x1b[1;{}", code);
228            let bold_key = format!("bold.{}", k);
229            map.insert(
230                Box::leak(bold_key.into_boxed_str()),
231                Box::leak(bold_code.into_boxed_str()),
232            );
233        }
234    }
235
236    map.insert("reset", "\x1b[0m");
237    map
238}
239
240/// Given a slice of color indices or a distro name, generates a HashMap
241/// of `cX` keys (where `X` is the index, starting from 1) mapped to the
242/// corresponding ANSI foreground color codes.
243///
244/// The color order is determined by the input slice. The first color is
245/// assigned to `c1`, the second to `c2`, and so on. If the input slice is
246/// shorter than 16 elements, the remaining colors are filled from the
247/// default color palette in the order they appear.
248///
249/// If the input slice is empty, the function returns an empty HashMap.
250///
251/// The HashMap also includes bold variants for each color, which can be
252/// accessed using the `bold.*` keys. For example, `bold.c1` would be the
253/// bold variant of `c1`.
254///
255/// The `reset` key is also included, which resets the text to the default
256/// color.
257pub fn get_colors_in_order(color_order: &[u8]) -> HashMap<&'static str, &'static str> {
258    // Start with c0 = bold black
259    let mut entries: Vec<(&'static str, &'static str)> = vec![("c0", "\x1b[1;30m")];
260
261    let mut used = vec![false; 16]; // support 0–15
262
263    // Fill c1 to cX using given color_order
264    for (i, &idx) in color_order.iter().enumerate() {
265        if idx < 16 {
266            let key: &'static str = Box::leak(format!("c{}", i + 1).into_boxed_str());
267            entries.push((key, DEFAULT_ANSI_ALL_COLORS[idx as usize]));
268            used[idx as usize] = true;
269        }
270    }
271
272    // Fill remaining cX from unused colors
273    let mut next_index = color_order.len() + 1;
274    for (i, &color) in DEFAULT_ANSI_ALL_COLORS.iter().enumerate() {
275        if !used[i] {
276            let key: &'static str = Box::leak(format!("c{}", next_index).into_boxed_str());
277            entries.push((key, color));
278            next_index += 1;
279        }
280    }
281
282    // Generate HashMap with bold.* variants and reset
283    let mut map = color_palette(&entries);
284    map.insert("reset", "\x1b[0m");
285    map
286}
287
288/// Given a string of comma-separated color indices or a distro name, returns a
289/// HashMap of color codes c0 to cX as found in the given distro's color
290/// definition. The color codes are from the ANSI color palette.
291pub fn get_custom_colors_order(colors_str_order: &str) -> HashMap<&'static str, &'static str> {
292    let custom_color_str_list: Vec<&str> = colors_str_order.split(',').map(str::trim).collect();
293
294    // Try to parse all color indices
295    let all_parsed: Option<Vec<u8>> = custom_color_str_list
296        .iter()
297        .map(|s| s.parse::<u8>().ok())
298        .collect();
299
300    let color_list: Vec<u8> = if let Some(list) = all_parsed {
301        list
302    } else {
303        // Fallback: interpret the string as a distro name
304        get_builtin_distro_colors(colors_str_order).to_vec()
305    };
306
307    get_colors_in_order(&color_list)
308}
309
310/// Given a distro name, returns a HashMap of color codes c0 to cX as found
311/// in the given distro's color definition. The color codes are from the
312/// ANSI color palette. If the distro isn't found, an empty HashMap is returned.
313pub fn get_distro_colors(distro: &str) -> HashMap<&'static str, &'static str> {
314    let dist_color = get_builtin_distro_colors(distro);
315
316    get_colors_in_order(dist_color)
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    #[test]
324    fn bar_respects_percentage_bounds() {
325        assert_eq!(get_bar(0), "[░░░░░░░░░░░░░░]");
326        assert_eq!(get_bar(100), "[██████████████]");
327        assert!(get_bar(50).contains('█'));
328    }
329
330    #[test]
331    fn terminal_color_emits_expected_blocks() {
332        let visual = get_terminal_color("■");
333        assert_eq!(visual.matches('■').count(), 8);
334        assert!(visual.contains("\x1b[31m"), "missing ANSI escape: {visual}");
335    }
336
337    #[test]
338    fn color_palette_includes_bold_and_reset() {
339        let map = color_palette(&[("c1", "\x1b[0;31m")]);
340        assert_eq!(map.get("c1"), Some(&"\x1b[0;31m"));
341        let bold_key = map
342            .keys()
343            .find(|k| k.starts_with("bold.c1"))
344            .expect("bold variant missing");
345        assert!(map.get(bold_key).unwrap().starts_with("\x1b[1;"));
346        assert_eq!(map.get("reset"), Some(&"\x1b[0m"));
347    }
348
349    #[test]
350    fn colors_in_order_fills_defaults() {
351        let map = get_colors_in_order(&[1, 2, 3]);
352        assert_eq!(map.get("c1"), Some(&DEFAULT_ANSI_ALL_COLORS[1]));
353        assert_eq!(map.get("c2"), Some(&DEFAULT_ANSI_ALL_COLORS[2]));
354        assert_eq!(map.get("c3"), Some(&DEFAULT_ANSI_ALL_COLORS[3]));
355        assert!(map.contains_key("c4"));
356        assert_eq!(map.get("reset"), Some(&"\x1b[0m"));
357    }
358}