Skip to main content

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