leenfetch_core/modules/linux/info/
cpu.rs1use std::fs;
2use std::path::Path;
3
4pub 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(); 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 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}