leenfetch_core/modules/linux/info/
cpu.rs1#![allow(clippy::collapsible_if)]
2
3use std::fs;
4use std::path::Path;
5
6pub 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(); 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 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}