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 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 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 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(); 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 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}