Skip to main content

leenfetch_core/modules/linux/info/
cpu.rs

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