Skip to main content

neofetch/
temperature.rs

1//! Temperature sensor information collector
2//!
3//! Collects temperature information from various system sensors.
4
5use crate::error::{NeofetchError, Result};
6use std::fmt::Display;
7
8/// Temperature sensor information
9#[derive(Debug, Clone)]
10pub struct TempSensor {
11    /// Sensor label/name
12    pub label: String,
13    /// Temperature in Celsius
14    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/// Get temperature sensors on Linux
24#[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    // Also try hwmon sensors
53    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/// Read hwmon temperature sensors (Linux)
67#[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            // Try to find temp*_input files
83            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                        // Try to get label
96                        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/// Get temperature sensors on Android
118#[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; // Minimum valid temperature (Celsius)
130    const MAX_TEMP: f32 = 120.0; // Maximum valid temperature (Celsius)
131
132    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            // Read temperature
150            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            // Skip invalid temperatures (including -273°C which indicates sensor not active)
159            if temp_celsius < MIN_TEMP || temp_celsius > MAX_TEMP {
160                continue;
161            }
162
163            // Read sensor type/label
164            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            // Classify sensor by type and collect temperatures
170            let label_lower = label.to_lowercase();
171
172            // CPU sensors: cpu, core, tsens (thermal sensor), cluster, silver, gold, prime
173            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            // GPU sensors: gpu, gpuss (GPU subsystem), kgsl (Kernel Graphics Support Layer)
184            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            // Battery sensors: battery, batt, charger
191            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            // Note: Sensors like xoagg_therm (XO aggregate), sdr0_pa (SDR power amplifier),
198            // modem, wifi, camera, etc. are not included in the main categories
199        }
200    }
201
202    let mut sensors = Vec::new();
203
204    // Calculate and add CPU average (ordered first)
205    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    // Calculate and add GPU average (ordered second)
214    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    // Calculate and add Battery average (ordered third)
223    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/// Get temperature sensors on macOS
241#[cfg(target_os = "macos")]
242pub async fn get_temperature_sensors() -> Result<Vec<TempSensor>> {
243    use crate::utils::execute_command_optional;
244
245    // Try using powermetrics (requires sudo, may not work)
246    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    // Try using osx-cpu-temp if available
272    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/// Get temperature sensors on Windows
289#[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; // Minimum valid temperature (Celsius)
305    const MAX_TEMP: f32 = 120.0; // Maximum valid temperature (Celsius)
306
307    // Try WMI thermal zone query (limited support on Windows)
308    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        // WMI returns temperature in tenths of Kelvin
318        let temp_kelvin = zone.current_temperature as f32 / 10.0;
319        let temp_celsius = temp_kelvin - 273.15;
320
321        // Skip invalid temperatures
322        if !(MIN_TEMP..=MAX_TEMP).contains(&temp_celsius) {
323            continue;
324        }
325
326        // Classify sensor by instance name
327        let name_lower = zone.instance_name.to_lowercase();
328
329        // CPU sensors: cpu, processor, core, package
330        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        // GPU sensors: gpu, graphics, video, display adapter
338        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        // Battery sensors: battery, batt, acpi
346        else if name_lower.contains("battery") || name_lower.contains("batt") {
347            battery_temps.push(temp_celsius);
348        }
349        // If no specific category, try to infer from thermal zone naming
350        else if name_lower.contains("tz") || name_lower.contains("thermal") {
351            // Generic thermal zones - classify as CPU by default
352            cpu_temps.push(temp_celsius);
353        }
354    }
355
356    let mut sensors = Vec::new();
357
358    // Calculate and add CPU average (ordered first)
359    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    // Calculate and add GPU average (ordered second)
368    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    // Calculate and add Battery average (ordered third)
377    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/// Get temperature sensors (unsupported platforms)
395#[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
405/// Get CPU temperature (first available sensor)
406pub async fn get_cpu_temperature() -> Result<f32> {
407    let sensors = get_temperature_sensors().await?;
408
409    // Try to find CPU-related sensor
410    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    // Return first sensor if no CPU sensor found
421    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        // May or may not have sensors depending on platform and permissions
435        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        // May or may not be available
442        assert!(result.is_ok() || result.is_err());
443    }
444}