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