1use crate::error::{NeofetchError, Result};
6use std::fmt::Display;
7
8#[derive(Debug, Clone)]
10pub struct TempSensor {
11 pub label: String,
13 pub temperature_celsius: f32,
15}
16
17impl Display for TempSensor {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 write!(f, "{}: {:.1}°C", self.label, self.temperature_celsius)
20 }
21}
22
23#[cfg(target_os = "linux")]
25pub async fn get_temperature_sensors() -> Result<Vec<TempSensor>> {
26 use crate::platform::linux::{
27 get_thermal_zones, read_thermal_zone_temp, read_thermal_zone_type,
28 };
29
30 let zones = get_thermal_zones()?;
31 let mut sensors = Vec::new();
32
33 for zone_path in zones {
34 if let Ok(temp) = read_thermal_zone_temp(&zone_path).await {
35 let label = read_thermal_zone_type(&zone_path)
36 .await
37 .unwrap_or_else(|_| {
38 zone_path
39 .split('/')
40 .next_back()
41 .unwrap_or("unknown")
42 .to_string()
43 });
44
45 sensors.push(TempSensor {
46 label,
47 temperature_celsius: temp,
48 });
49 }
50 }
51
52 if let Ok(hwmon_sensors) = read_hwmon_sensors().await {
54 sensors.extend(hwmon_sensors);
55 }
56
57 if sensors.is_empty() {
58 return Err(NeofetchError::data_unavailable(
59 "No temperature sensors found",
60 ));
61 }
62
63 Ok(sensors)
64}
65
66#[cfg(target_os = "linux")]
68async fn read_hwmon_sensors() -> Result<Vec<TempSensor>> {
69 use crate::utils::read_file_to_string;
70
71 let hwmon_path = std::path::Path::new("/sys/class/hwmon");
72 if !hwmon_path.exists() {
73 return Ok(Vec::new());
74 }
75
76 let mut sensors = Vec::new();
77
78 if let Ok(entries) = std::fs::read_dir(hwmon_path) {
79 for entry in entries.flatten() {
80 let hwmon_dir = entry.path();
81
82 if let Ok(temp_entries) = std::fs::read_dir(&hwmon_dir) {
84 for temp_entry in temp_entries.flatten() {
85 let filename = temp_entry.file_name();
86 let filename_str = filename.to_string_lossy();
87
88 if filename_str.starts_with("temp")
89 && filename_str.ends_with("_input")
90 && let Ok(temp_str) = read_file_to_string(temp_entry.path()).await
91 && let Ok(temp_millidegrees) = temp_str.trim().parse::<i32>()
92 {
93 let temp_celsius = temp_millidegrees as f32 / 1000.0;
94
95 let label_file = filename_str.replace("_input", "_label");
97 let label_path = hwmon_dir.join(&label_file);
98 let label = if let Ok(label_str) = read_file_to_string(&label_path).await {
99 label_str.trim().to_string()
100 } else {
101 filename_str.trim_end_matches("_input").to_string()
102 };
103
104 sensors.push(TempSensor {
105 label,
106 temperature_celsius: temp_celsius,
107 });
108 }
109 }
110 }
111 }
112 }
113
114 Ok(sensors)
115}
116
117#[cfg(target_os = "android")]
119pub async fn get_temperature_sensors() -> Result<Vec<TempSensor>> {
120 use crate::utils::read_file_to_string;
121
122 let thermal_path = std::path::Path::new("/sys/class/thermal");
123 if !thermal_path.exists() {
124 return Err(NeofetchError::data_unavailable(
125 "Thermal sensors not available",
126 ));
127 }
128
129 const MIN_TEMP: f32 = 0.0; const MAX_TEMP: f32 = 120.0; let mut cpu_temps = Vec::new();
133 let mut gpu_temps = Vec::new();
134 let mut battery_temps = Vec::new();
135
136 if let Ok(entries) = std::fs::read_dir(thermal_path) {
137 for entry in entries.flatten() {
138 let zone_path = entry.path();
139 let zone_name = entry.file_name();
140 let zone_name_str = zone_name.to_string_lossy();
141
142 if !zone_name_str.starts_with("thermal_zone") {
143 continue;
144 }
145
146 let temp_file = zone_path.join("temp");
147 let type_file = zone_path.join("type");
148
149 let temp_celsius = match read_file_to_string(&temp_file).await {
151 Ok(temp_str) => match temp_str.trim().parse::<i32>() {
152 Ok(temp_millidegrees) => temp_millidegrees as f32 / 1000.0,
153 Err(_) => continue,
154 },
155 Err(_) => continue,
156 };
157
158 if temp_celsius < MIN_TEMP || temp_celsius > MAX_TEMP {
160 continue;
161 }
162
163 let label = match read_file_to_string(&type_file).await {
165 Ok(type_str) => type_str.trim().to_string(),
166 Err(_) => continue,
167 };
168
169 let label_lower = label.to_lowercase();
171
172 if label_lower.contains("cpu")
174 || label_lower.contains("core")
175 || label_lower.contains("tsens")
176 || label_lower.contains("cluster")
177 || label_lower.contains("silver")
178 || label_lower.contains("gold")
179 || label_lower.contains("prime")
180 {
181 cpu_temps.push(temp_celsius);
182 }
183 else if label_lower.contains("gpu")
185 || label_lower.contains("gpuss")
186 || label_lower.contains("kgsl")
187 {
188 gpu_temps.push(temp_celsius);
189 }
190 else if label_lower.contains("battery")
192 || label_lower.contains("batt")
193 || label_lower.contains("charger")
194 {
195 battery_temps.push(temp_celsius);
196 }
197 }
200 }
201
202 let mut sensors = Vec::new();
203
204 if !cpu_temps.is_empty() {
206 let avg_temp = cpu_temps.iter().sum::<f32>() / cpu_temps.len() as f32;
207 sensors.push(TempSensor {
208 label: format!("CPU (avg of {} sensors)", cpu_temps.len()),
209 temperature_celsius: avg_temp,
210 });
211 }
212
213 if !gpu_temps.is_empty() {
215 let avg_temp = gpu_temps.iter().sum::<f32>() / gpu_temps.len() as f32;
216 sensors.push(TempSensor {
217 label: format!("GPU (avg of {} sensors)", gpu_temps.len()),
218 temperature_celsius: avg_temp,
219 });
220 }
221
222 if !battery_temps.is_empty() {
224 let avg_temp = battery_temps.iter().sum::<f32>() / battery_temps.len() as f32;
225 sensors.push(TempSensor {
226 label: format!("Battery (avg of {} sensors)", battery_temps.len()),
227 temperature_celsius: avg_temp,
228 });
229 }
230
231 if sensors.is_empty() {
232 return Err(NeofetchError::data_unavailable(
233 "No valid temperature sensors found",
234 ));
235 }
236
237 Ok(sensors)
238}
239
240#[cfg(target_os = "macos")]
242pub async fn get_temperature_sensors() -> Result<Vec<TempSensor>> {
243 use crate::utils::execute_command_optional;
244
245 if let Some(output) =
247 execute_command_optional("powermetrics", &["--samplers", "smc", "-i1", "-n1"]).await
248 {
249 let mut sensors = Vec::new();
250
251 for line in output.lines() {
252 if line.contains("CPU die temperature") {
253 if let Some(temp_str) = line.split(':').nth(1) {
254 if let Some(temp_val) = temp_str.trim().split_whitespace().next() {
255 if let Ok(temp) = temp_val.parse::<f32>() {
256 sensors.push(TempSensor {
257 label: "CPU".to_string(),
258 temperature_celsius: temp,
259 });
260 }
261 }
262 }
263 }
264 }
265
266 if !sensors.is_empty() {
267 return Ok(sensors);
268 }
269 }
270
271 if let Some(output) = execute_command_optional("osx-cpu-temp", &[] as &[&str]).await {
273 if let Some(temp_str) = output.split('°').next() {
274 if let Ok(temp) = temp_str.trim().parse::<f32>() {
275 return Ok(vec![TempSensor {
276 label: "CPU".to_string(),
277 temperature_celsius: temp,
278 }]);
279 }
280 }
281 }
282
283 Err(NeofetchError::data_unavailable(
284 "Temperature sensors not available (try installing osx-cpu-temp)",
285 ))
286}
287
288#[cfg(windows)]
290pub async fn get_temperature_sensors() -> Result<Vec<TempSensor>> {
291 use serde::Deserialize;
292
293 use crate::platform::wmi_query_with_ns;
294
295 #[derive(Deserialize, Debug)]
296 #[serde(rename = "MSAcpi_ThermalZoneTemperature")]
297 struct ThermalZoneTemperature {
298 #[serde(rename = "CurrentTemperature")]
299 current_temperature: u32,
300 #[serde(rename = "InstanceName")]
301 instance_name: String,
302 }
303
304 const MIN_TEMP: f32 = 0.0; const MAX_TEMP: f32 = 120.0; let results: Vec<ThermalZoneTemperature> = wmi_query_with_ns("root\\wmi")
309 .await
310 .map_err(|e| NeofetchError::wmi_error(format!("WMI query failed: {}", e)))?;
311
312 let mut cpu_temps = Vec::new();
313 let mut gpu_temps = Vec::new();
314 let mut battery_temps = Vec::new();
315
316 for zone in results {
317 let temp_kelvin = zone.current_temperature as f32 / 10.0;
319 let temp_celsius = temp_kelvin - 273.15;
320
321 if !(MIN_TEMP..=MAX_TEMP).contains(&temp_celsius) {
323 continue;
324 }
325
326 let name_lower = zone.instance_name.to_lowercase();
328
329 if name_lower.contains("cpu")
331 || name_lower.contains("processor")
332 || name_lower.contains("core")
333 || name_lower.contains("package")
334 {
335 cpu_temps.push(temp_celsius);
336 }
337 else if name_lower.contains("gpu")
339 || name_lower.contains("graphics")
340 || name_lower.contains("video")
341 || name_lower.contains("display")
342 {
343 gpu_temps.push(temp_celsius);
344 }
345 else if name_lower.contains("battery") || name_lower.contains("batt") {
347 battery_temps.push(temp_celsius);
348 }
349 else if name_lower.contains("tz") || name_lower.contains("thermal") {
351 cpu_temps.push(temp_celsius);
353 }
354 }
355
356 let mut sensors = Vec::new();
357
358 if !cpu_temps.is_empty() {
360 let avg_temp = cpu_temps.iter().sum::<f32>() / cpu_temps.len() as f32;
361 sensors.push(TempSensor {
362 label: format!("CPU (avg of {} sensors)", cpu_temps.len()),
363 temperature_celsius: avg_temp,
364 });
365 }
366
367 if !gpu_temps.is_empty() {
369 let avg_temp = gpu_temps.iter().sum::<f32>() / gpu_temps.len() as f32;
370 sensors.push(TempSensor {
371 label: format!("GPU (avg of {} sensors)", gpu_temps.len()),
372 temperature_celsius: avg_temp,
373 });
374 }
375
376 if !battery_temps.is_empty() {
378 let avg_temp = battery_temps.iter().sum::<f32>() / battery_temps.len() as f32;
379 sensors.push(TempSensor {
380 label: format!("Battery (avg of {} sensors)", battery_temps.len()),
381 temperature_celsius: avg_temp,
382 });
383 }
384
385 if sensors.is_empty() {
386 return Err(NeofetchError::data_unavailable(
387 "No valid temperature sensors found via WMI",
388 ));
389 }
390
391 Ok(sensors)
392}
393
394#[cfg(not(any(
396 target_os = "linux",
397 target_os = "android",
398 target_os = "macos",
399 windows
400)))]
401pub async fn get_temperature_sensors() -> Result<Vec<TempSensor>> {
402 Err(NeofetchError::UnsupportedPlatform)
403}
404
405pub async fn get_cpu_temperature() -> Result<f32> {
407 let sensors = get_temperature_sensors().await?;
408
409 for sensor in &sensors {
411 let label_lower = sensor.label.to_lowercase();
412 if label_lower.contains("cpu")
413 || label_lower.contains("core")
414 || label_lower.contains("package")
415 {
416 return Ok(sensor.temperature_celsius);
417 }
418 }
419
420 sensors
422 .first()
423 .map(|s| s.temperature_celsius)
424 .ok_or_else(|| NeofetchError::data_unavailable("No temperature sensors available"))
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430
431 #[tokio::test]
432 async fn test_get_temperature_sensors() {
433 let result = get_temperature_sensors().await;
434 assert!(result.is_ok() || result.is_err());
436 }
437
438 #[tokio::test]
439 async fn test_get_cpu_temperature() {
440 let result = get_cpu_temperature().await;
441 assert!(result.is_ok() || result.is_err());
443 }
444}