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 let filled = "█".repeat(filled_blocks);
41 let empty = "░".repeat(empty_blocks);
42 format!("[{}{}]", filled, empty)
43}
44
45/// Generates a vector of 2 strings, each containing a row of 8 blocks
46/// colored with different ANSI foreground colors. The first string has
47/// normal colors, the second has bold colors.
48///
49/// The input string `color_blocks` should contain 8 identical block characters
50/// (e.g. █, ░, ▓, ▒, etc.). The output strings will have these blocks
51/// colored with different ANSI colors.
52pub fn get_terminal_color(color_blocks: &str) -> String {
53 let color_codes: [u8; 8] = [30, 31, 32, 33, 34, 35, 36, 37]; // ANSI foreground colors
54
55 let mut normal = Vec::with_capacity(8);
56 // let mut bold = Vec::with_capacity(8);
57
58 for &code in &color_codes {
59 normal.push(format!("\x1b[{}m{}\x1b[0m", code, color_blocks)); // normal
60 // bold.push(format!("\x1b[1;{}m{}\x1b[0m", code, color_blocks)); // bold
61 }
62
63 // vec![normal.join(""), bold.join("")]
64 normal.join("")
65}
66
67// ---------------------------------
68// ASCII ART Functions
69// ---------------------------------
70
71/// Reads a file at the given custom path and returns its content as a string.
72/// If there is an error while reading the file, an empty string is returned.
73///
74/// # Arguments
75///
76/// * `custom_path`: The path to the custom ASCII art file.
77///
78/// # Returns
79///
80/// A string containing the content of the file, or an empty string if there was an error.
81pub fn get_custom_ascii(custom_path: &str) -> String {
82 if let Ok(content) = fs::read_to_string(Path::new(custom_path)) {
83 return content;
84 }
85
86 "".to_string()
87}
88
89/// Given a distro name, returns a string of its corresponding ASCII art.
90/// If the distro isn't found, an empty string is returned.
91/// If the distro is "off", an empty string is returned.
92pub fn get_ascii_and_colors(ascii_distro: &str) -> String {
93 if ascii_distro == "off" {
94 return "".to_string();
95 }
96
97 let ascii_art = get_builtin_ascii_art(ascii_distro);
98
99 ascii_art.to_string()
100}
101
102// ---------------------------------
103// Color Functions
104// ---------------------------------
105
106/// Replaces placeholders in a string with ANSI escape codes to colorize
107/// the output.
108///
109/// Placeholders are in the form of `${{key}}`, where `key` is the key in
110/// the provided `colors` HashMap. The value associated with the `key` is
111/// the ANSI escape code for the color.
112pub fn colorize_text(input: String, colors: &HashMap<&str, &str>) -> String {
113 let mut result = String::new();
114
115 for line in input.lines() {
116 let mut colored = line.to_owned();
117 for (key, code) in colors {
118 let placeholder = format!("${{{}}}", key);
119 colored = colored.replace(&placeholder, code);
120 }
121 result.push_str(&colored);
122 result.push('\n');
123 }
124
125 result
126}
127
128/// Creates a `HashMap` of ANSI color codes from the given entries.
129///
130/// Each entry is a tuple containing a key and a corresponding ANSI
131/// color code. The function populates the `HashMap` with these entries
132/// and also adds bold variants for each color code that starts with
133/// `\x1b[0;`. The bold variant key is prefixed with "bold." (e.g.,
134/// `bold.c1` for `c1`).
135///
136/// Additionally, a "reset" key is included in the map, which maps to
137/// the ANSI reset code `\x1b[0m`.
138///
139/// # Arguments
140///
141/// * `entries` - A slice of tuples where each tuple contains a string
142/// key and an ANSI color code.
143///
144/// # Returns
145///
146/// * A `HashMap` with the original entries, their bold variants, and a
147/// reset entry.
148pub fn color_palette(
149 entries: &[(&'static str, &'static str)],
150) -> HashMap<&'static str, &'static str> {
151 let mut map = HashMap::new();
152 for (k, v) in entries {
153 map.insert(*k, *v);
154
155 // Add bold variant: bold.c1 → \x1b[1;31m if c1 is \x1b[0;31m
156 if let Some(code) = v.strip_prefix("\x1b[0;") {
157 let bold_code = format!("\x1b[1;{}", code);
158 let bold_key = format!("bold.{}", k);
159 map.insert(
160 Box::leak(bold_key.into_boxed_str()),
161 Box::leak(bold_code.into_boxed_str()),
162 );
163 }
164 }
165
166 map.insert("reset", "\x1b[0m");
167 map
168}
169
170/// Given a slice of color indices or a distro name, generates a HashMap
171/// of `cX` keys (where `X` is the index, starting from 1) mapped to the
172/// corresponding ANSI foreground color codes.
173///
174/// The color order is determined by the input slice. The first color is
175/// assigned to `c1`, the second to `c2`, and so on. If the input slice is
176/// shorter than 16 elements, the remaining colors are filled from the
177/// default color palette in the order they appear.
178///
179/// If the input slice is empty, the function returns an empty HashMap.
180///
181/// The HashMap also includes bold variants for each color, which can be
182/// accessed using the `bold.*` keys. For example, `bold.c1` would be the
183/// bold variant of `c1`.
184///
185/// The `reset` key is also included, which resets the text to the default
186/// color.
187pub fn get_colors_in_order(color_order: &[u8]) -> HashMap<&'static str, &'static str> {
188 // Start with c0 = bold black
189 let mut entries: Vec<(&'static str, &'static str)> = vec![("c0", "\x1b[1;30m")];
190
191 let mut used = vec![false; 16]; // support 0–15
192
193 // Fill c1 to cX using given color_order
194 for (i, &idx) in color_order.iter().enumerate() {
195 if idx < 16 {
196 let key: &'static str = Box::leak(format!("c{}", i + 1).into_boxed_str());
197 entries.push((key, DEFAULT_ANSI_ALL_COLORS[idx as usize]));
198 used[idx as usize] = true;
199 }
200 }
201
202 // Fill remaining cX from unused colors
203 let mut next_index = color_order.len() + 1;
204 for (i, &color) in DEFAULT_ANSI_ALL_COLORS.iter().enumerate() {
205 if !used[i] {
206 let key: &'static str = Box::leak(format!("c{}", next_index).into_boxed_str());
207 entries.push((key, color));
208 next_index += 1;
209 }
210 }
211
212 // Generate HashMap with bold.* variants and reset
213 let mut map = color_palette(&entries);
214 map.insert("reset", "\x1b[0m");
215 map
216}
217
218/// Given a string of comma-separated color indices or a distro name, returns a
219/// HashMap of color codes c0 to cX as found in the given distro's color
220/// definition. The color codes are from the ANSI color palette.
221pub fn get_custom_colors_order(colors_str_order: &str) -> HashMap<&'static str, &'static str> {
222 let custom_color_str_list: Vec<&str> = colors_str_order.split(',').map(str::trim).collect();
223
224 // Try to parse all color indices
225 let all_parsed: Option<Vec<u8>> = custom_color_str_list
226 .iter()
227 .map(|s| s.parse::<u8>().ok())
228 .collect();
229
230 let color_list: Vec<u8> = if let Some(list) = all_parsed {
231 list
232 } else {
233 // Fallback: interpret the string as a distro name
234 get_builtin_distro_colors(colors_str_order).to_vec()
235 };
236
237 get_colors_in_order(&color_list)
238}
239
240/// Given a distro name, returns a HashMap of color codes c0 to cX as found
241/// in the given distro's color definition. The color codes are from the
242/// ANSI color palette. If the distro isn't found, an empty HashMap is returned.
243pub fn get_distro_colors(distro: &str) -> HashMap<&'static str, &'static str> {
244 let dist_color = get_builtin_distro_colors(distro);
245
246 get_colors_in_order(dist_color)
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn bar_respects_percentage_bounds() {
255 assert_eq!(get_bar(0), "[░░░░░░░░░░░░░░]");
256 assert_eq!(get_bar(100), "[██████████████]");
257 assert!(get_bar(50).contains('█'));
258 }
259
260 #[test]
261 fn terminal_color_emits_expected_blocks() {
262 let visual = get_terminal_color("■");
263 assert_eq!(visual.matches('■').count(), 8);
264 assert!(visual.contains("\x1b[31m"), "missing ANSI escape: {visual}");
265 }
266
267 #[test]
268 fn color_palette_includes_bold_and_reset() {
269 let map = color_palette(&[("c1", "\x1b[0;31m")]);
270 assert_eq!(map.get("c1"), Some(&"\x1b[0;31m"));
271 let bold_key = map
272 .keys()
273 .find(|k| k.starts_with("bold.c1"))
274 .expect("bold variant missing");
275 assert!(map.get(bold_key).unwrap().starts_with("\x1b[1;"));
276 assert_eq!(map.get("reset"), Some(&"\x1b[0m"));
277 }
278
279 #[test]
280 fn colors_in_order_fills_defaults() {
281 let map = get_colors_in_order(&[1, 2, 3]);
282 assert_eq!(map.get("c1"), Some(&DEFAULT_ANSI_ALL_COLORS[1]));
283 assert_eq!(map.get("c2"), Some(&DEFAULT_ANSI_ALL_COLORS[2]));
284 assert_eq!(map.get("c3"), Some(&DEFAULT_ANSI_ALL_COLORS[3]));
285 assert!(map.contains_key("c4"));
286 assert_eq!(map.get("reset"), Some(&"\x1b[0m"));
287 }
288}