leenfetch_core/modules/linux/info/
cpu.rs

1use std::fs;
2use std::path::Path;
3
4/// Gets the CPU model, number of cores, speed, and temperature.
5///
6/// CPU model is sanitized to remove generic brand prefixes if `cpu_brand` is
7/// false. The number of cores is only included if `show_cores` is true. The
8/// speed is only included if `show_speed` is true. The temperature is only
9/// included if `show_temp` is true.
10///
11/// The speed is formatted as "XMHz" if it is less than 1000, and as "X.YGHz"
12/// if it is greater than or equal to 1000. If `speed_shorthand` is true, the
13/// speed is rounded to the nearest tenth of a GHz before formatting.
14///
15/// The temperature is formatted as "[X.Y]°C" or "[X.Y]°F" depending on
16/// `temp_unit`. If `temp_unit` is not specified, the temperature is in
17/// Celsius.
18pub fn get_cpu(
19    cpu_brand: bool,
20    show_freq: bool,
21    show_cores: bool,
22    show_temp: bool,
23    speed_shorthand: bool,
24    temp_unit: Option<char>,
25) -> Option<String> {
26    let cpuinfo = fs::read_to_string("/proc/cpuinfo").ok()?;
27    let mut cpu_model = extract_cpu_model(&cpuinfo);
28    let cores = if show_cores {
29        Some(count_cores(&cpuinfo))
30    } else {
31        None
32    };
33    let speed = if show_freq {
34        extract_speed(&cpuinfo)
35    } else {
36        None
37    };
38    let temp = if show_temp {
39        extract_temp("/sys/class/hwmon/")
40    } else {
41        None
42    };
43
44    cpu_model = sanitize_cpu_model(&cpu_model, cpu_brand);
45    let mut output = cpu_model;
46
47    if let Some(c) = cores {
48        output = format!("{} ({})", output, c);
49    }
50
51    if let Some(s) = speed {
52        let formatted = if s < 1000 {
53            format!("{}MHz", s)
54        } else {
55            let mut ghz = s as f32 / 1000.0;
56            if speed_shorthand {
57                ghz = (ghz * 10.0).round() / 10.0;
58            }
59            format!("{:.1}GHz", ghz)
60        };
61        output = format!("{} @ {}", output, formatted);
62    }
63
64    if let Some(mut celsius) = temp {
65        if let Some('F') = temp_unit {
66            celsius = celsius * 9.0 / 5.0 + 32.0;
67        }
68        output = format!("{} [{:.1}°{}]", output, celsius, temp_unit.unwrap_or('C'));
69    }
70
71    Some(output)
72}
73
74fn extract_cpu_model(cpuinfo: &str) -> String {
75    for line in cpuinfo.lines() {
76        if line.contains("model name")
77            || line.contains("Model")
78            || line.contains("Hardware")
79            || line.contains("Processor")
80        {
81            if let Some((_, val)) = line.split_once(':') {
82                return val.trim().to_string();
83            }
84        }
85    }
86
87    // Fallback to device tree model
88    if let Ok(model) = fs::read_to_string("/proc/device-tree/model") {
89        let trimmed = model.trim_matches(char::from(0)).trim();
90        if !trimmed.is_empty() {
91            return trimmed.to_string();
92        }
93    }
94
95    // Fallback to SoC machine name
96    if let Ok(machine) = fs::read_to_string("/sys/devices/soc0/machine") {
97        let trimmed = machine.trim();
98        if !trimmed.is_empty() {
99            return trimmed.to_string();
100        }
101    }
102
103    "Unknown CPU".to_string()
104}
105
106fn count_cores(cpuinfo: &str) -> u32 {
107    cpuinfo
108        .lines()
109        .filter(|l| l.starts_with("processor"))
110        .count() as u32
111}
112
113fn extract_speed(cpuinfo: &str) -> Option<u32> {
114    for line in cpuinfo.lines() {
115        if line.contains("cpu MHz") {
116            if let Some((_, val)) = line.split_once(':') {
117                return val.trim().parse::<f32>().ok().map(|v| v.round() as u32);
118            }
119        }
120    }
121    None
122}
123
124fn extract_temp(hwmon_root: &str) -> Option<f32> {
125    let root = Path::new(hwmon_root);
126    if !root.exists() {
127        return None;
128    }
129
130    for entry in fs::read_dir(root).ok()? {
131        let path = entry.ok()?.path();
132        let name_path = path.join("name");
133
134        let name = fs::read_to_string(name_path).ok()?;
135        if name.contains("coretemp") || name.contains("k10temp") || name.contains("cpu_thermal") {
136            if let Ok(entries) = fs::read_dir(&path) {
137                for entry in entries.flatten() {
138                    let file_name = entry.file_name();
139                    let file_name_str = file_name.to_string_lossy();
140                    if file_name_str.starts_with("temp") && file_name_str.ends_with("_input") {
141                        if let Ok(content) = fs::read_to_string(entry.path()) {
142                            if let Ok(raw) = content.trim().parse::<f32>() {
143                                return Some(raw / 1000.0);
144                            }
145                        }
146                    }
147                }
148            }
149        }
150    }
151    None
152}
153
154fn sanitize_cpu_model(model: &str, show_brand: bool) -> String {
155    let mut s = model.to_string();
156
157    let replacements = [
158        "(TM)",
159        "(tm)",
160        "(R)",
161        "(r)",
162        "CPU",
163        "Processor",
164        "Dual-Core",
165        "Quad-Core",
166        "Six-Core",
167        "Eight-Core",
168        "with Radeon",
169        "FPU",
170        "Technologies, Inc",
171        "Core2",
172        "Chip Revision",
173        "Compute Cores",
174        "Core ",
175    ];
176
177    for pat in replacements.iter() {
178        s = s.replace(pat, "");
179    }
180
181    if !show_brand {
182        let brands = ["AMD ", "Intel ", "Qualcomm ", "Core? Duo ", "Apple "];
183        for brand in brands.iter() {
184            s = s.replacen(brand, "", 1);
185        }
186    }
187
188    s = s
189        .split_whitespace()
190        .filter(|word| {
191            if let Some(stripped) = word.strip_suffix("-Core") {
192                return !stripped.chars().all(|c| c.is_ascii_digit());
193            }
194            true
195        })
196        .collect::<Vec<_>>()
197        .join(" ");
198
199    s.trim().to_string()
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use std::env;
206    use std::fs::{self, File};
207    use std::io::Write;
208
209    const MOCK_CPUINFO: &str = r#"
210processor   : 0
211vendor_id   : GenuineIntel
212cpu MHz     : 2200.000
213model name  : Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
214
215processor   : 1
216cpu MHz     : 2200.000
217"#;
218
219    const MOCK_CPUINFO_DECIMAL: &str = r#"
220processor   : 0
221cpu MHz     : 2199.6
222"#;
223
224    #[test]
225    fn test_extract_cpu_model() {
226        let model = extract_cpu_model(MOCK_CPUINFO);
227        assert_eq!(model, "Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz");
228    }
229
230    #[test]
231    fn test_extract_cpu_model_unknown() {
232        let empty_info = "";
233        assert_eq!(extract_cpu_model(empty_info), "Unknown CPU");
234    }
235
236    #[test]
237    fn test_count_cores() {
238        let core_count = count_cores(MOCK_CPUINFO);
239        assert_eq!(core_count, 2);
240    }
241
242    #[test]
243    fn test_extract_speed() {
244        let speed = extract_speed(MOCK_CPUINFO);
245        assert_eq!(speed, Some(2200));
246    }
247
248    #[test]
249    fn test_extract_speed_rounds_up() {
250        let speed = extract_speed(MOCK_CPUINFO_DECIMAL);
251        assert_eq!(speed, Some(2200));
252    }
253
254    #[test]
255    fn test_sanitize_cpu_model_with_brand() {
256        let input = "Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz";
257        let result = sanitize_cpu_model(input, true);
258        assert!(result.contains("Intel"));
259        assert!(result.contains("i7-8550U"));
260        assert!(!result.contains("CPU"));
261    }
262
263    #[test]
264    fn test_sanitize_cpu_model_strips_noise() {
265        let input = "AMD Ryzen(TM) 5 5600X 6-Core Processor";
266        let result = sanitize_cpu_model(input, true);
267        assert!(!result.contains("(TM)"));
268        assert!(!result.contains("6-Core"));
269        assert!(result.contains("Ryzen"));
270    }
271
272    #[test]
273    fn test_sanitize_cpu_model_without_brand() {
274        let input = "Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz";
275        let result = sanitize_cpu_model(input, false);
276        assert!(!result.contains("Intel"));
277        assert!(result.contains("i7-8550U"));
278    }
279
280    #[test]
281    fn test_extract_temp_with_mock_hwmon() {
282        let temp_dir = env::temp_dir().join("test_hwmon");
283        let hwmon_path = temp_dir.join("hwmon0");
284        let name_path = hwmon_path.join("name");
285        let temp_input_path = hwmon_path.join("temp1_input");
286
287        fs::create_dir_all(&hwmon_path).unwrap();
288
289        let mut name_file = File::create(&name_path).unwrap();
290        writeln!(name_file, "coretemp").unwrap();
291
292        let mut temp_file = File::create(&temp_input_path).unwrap();
293        writeln!(temp_file, "47000").unwrap(); // 47.0°C
294
295        let result = extract_temp(temp_dir.to_str().unwrap());
296        assert_eq!(result, Some(47.0));
297
298        fs::remove_dir_all(&temp_dir).unwrap();
299    }
300
301    #[test]
302    fn test_get_cpu_basic() {
303        // This test will only validate that the function returns something,
304        // since it depends on system files.
305        let result = get_cpu(true, true, true, false, false, None);
306        assert!(result.is_some());
307        let output = result.unwrap();
308        assert!(!output.is_empty());
309        println!("CPU Info: {}", output);
310    }
311}