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    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()
111        .try_into()
112        .unwrap_or(u32::MAX)
113}
114
115fn extract_speed(cpuinfo: &str) -> Option<u32> {
116    for line in cpuinfo.lines() {
117        if line.contains("cpu MHz") {
118            if let Some((_, val)) = line.split_once(':') {
119                return val.trim().parse::<f32>().ok().map(|v| v.round() as u32);
120            }
121        }
122    }
123    None
124}
125
126fn extract_temp(hwmon_root: &str) -> Option<f32> {
127    let root = Path::new(hwmon_root);
128    if !root.exists() {
129        return None;
130    }
131
132    for entry in fs::read_dir(root).ok()? {
133        let path = entry.ok()?.path();
134        let name_path = path.join("name");
135
136        let name = fs::read_to_string(name_path).ok()?;
137        if name.contains("coretemp") || name.contains("k10temp") || name.contains("cpu_thermal") {
138            if let Ok(entries) = fs::read_dir(&path) {
139                for entry in entries.flatten() {
140                    let file_name = entry.file_name();
141                    let file_name_str = file_name.to_string_lossy();
142                    if file_name_str.starts_with("temp") && file_name_str.ends_with("_input") {
143                        if let Ok(content) = fs::read_to_string(entry.path()) {
144                            if let Ok(raw) = content.trim().parse::<f32>() {
145                                return Some(raw / 1000.0);
146                            }
147                        }
148                    }
149                }
150            }
151        }
152    }
153    None
154}
155
156fn sanitize_cpu_model(model: &str, show_brand: bool) -> String {
157    let mut s = model.to_string();
158
159    let replacements = [
160        "(TM)",
161        "(tm)",
162        "(R)",
163        "(r)",
164        "CPU",
165        "Processor",
166        "Dual-Core",
167        "Quad-Core",
168        "Six-Core",
169        "Eight-Core",
170        "with Radeon",
171        "FPU",
172        "Technologies, Inc",
173        "Core2",
174        "Chip Revision",
175        "Compute Cores",
176        "Core ",
177    ];
178
179    for pat in replacements.iter() {
180        s = s.replace(pat, "");
181    }
182
183    if !show_brand {
184        let brands = ["AMD ", "Intel ", "Qualcomm ", "Core? Duo ", "Apple "];
185        for brand in brands.iter() {
186            s = s.replacen(brand, "", 1);
187        }
188    }
189
190    s = s
191        .split_whitespace()
192        .filter(|word| {
193            if let Some(stripped) = word.strip_suffix("-Core") {
194                return !stripped.chars().all(|c| c.is_ascii_digit());
195            }
196            true
197        })
198        .collect::<Vec<_>>()
199        .join(" ");
200
201    s.trim().to_string()
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207    use std::env;
208    use std::fs::{self, File};
209    use std::io::Write;
210
211    const MOCK_CPUINFO: &str = r#"
212processor   : 0
213vendor_id   : GenuineIntel
214cpu MHz     : 2200.000
215model name  : Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
216
217processor   : 1
218cpu MHz     : 2200.000
219"#;
220
221    const MOCK_CPUINFO_DECIMAL: &str = r#"
222processor   : 0
223cpu MHz     : 2199.6
224"#;
225
226    #[test]
227    fn test_extract_cpu_model() {
228        let model = extract_cpu_model(MOCK_CPUINFO);
229        assert_eq!(model, "Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz");
230    }
231
232    #[test]
233    fn test_extract_cpu_model_unknown() {
234        let empty_info = "";
235        assert_eq!(extract_cpu_model(empty_info), "Unknown CPU");
236    }
237
238    #[test]
239    fn test_count_cores() {
240        let core_count = count_cores(MOCK_CPUINFO);
241        assert_eq!(core_count, 2);
242    }
243
244    #[test]
245    fn test_extract_speed() {
246        let speed = extract_speed(MOCK_CPUINFO);
247        assert_eq!(speed, Some(2200));
248    }
249
250    #[test]
251    fn test_extract_speed_rounds_up() {
252        let speed = extract_speed(MOCK_CPUINFO_DECIMAL);
253        assert_eq!(speed, Some(2200));
254    }
255
256    #[test]
257    fn test_sanitize_cpu_model_with_brand() {
258        let input = "Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz";
259        let result = sanitize_cpu_model(input, true);
260        assert!(result.contains("Intel"));
261        assert!(result.contains("i7-8550U"));
262        assert!(!result.contains("CPU"));
263    }
264
265    #[test]
266    fn test_sanitize_cpu_model_strips_noise() {
267        let input = "AMD Ryzen(TM) 5 5600X 6-Core Processor";
268        let result = sanitize_cpu_model(input, true);
269        assert!(!result.contains("(TM)"));
270        assert!(!result.contains("6-Core"));
271        assert!(result.contains("Ryzen"));
272    }
273
274    #[test]
275    fn test_sanitize_cpu_model_without_brand() {
276        let input = "Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz";
277        let result = sanitize_cpu_model(input, false);
278        assert!(!result.contains("Intel"));
279        assert!(result.contains("i7-8550U"));
280    }
281
282    #[test]
283    fn test_extract_temp_with_mock_hwmon() {
284        let temp_dir = env::temp_dir().join("test_hwmon");
285        let hwmon_path = temp_dir.join("hwmon0");
286        let name_path = hwmon_path.join("name");
287        let temp_input_path = hwmon_path.join("temp1_input");
288
289        fs::create_dir_all(&hwmon_path).unwrap();
290
291        let mut name_file = File::create(&name_path).unwrap();
292        writeln!(name_file, "coretemp").unwrap();
293
294        let mut temp_file = File::create(&temp_input_path).unwrap();
295        writeln!(temp_file, "47000").unwrap(); // 47.0°C
296
297        let result = extract_temp(temp_dir.to_str().unwrap());
298        assert_eq!(result, Some(47.0));
299
300        fs::remove_dir_all(&temp_dir).unwrap();
301    }
302
303    #[test]
304    fn test_get_cpu_basic() {
305        // This test will only validate that the function returns something,
306        // since it depends on system files.
307        let result = get_cpu(true, true, true, false, false, None);
308        assert!(result.is_some());
309        let output = result.unwrap();
310        assert!(!output.is_empty());
311        println!("CPU Info: {}", output);
312    }
313}