cpu-temp 0.1.0

An Intel CPU temperature monitoring library for Windows and Linux using MSR access
Documentation
use std::borrow::Cow;

use crossbeam_channel::{Receiver, TryRecvError};
use ratatui::prelude::Stylize;
use ratatui::{
    backend::CrosstermBackend,
    layout::{Constraint, Direction, Layout},
    widgets::{Block, Borders, Cell, Paragraph, Row, Table},
    Frame, Terminal,
};

use crossterm::{
    event::{self, Event, KeyCode, KeyEventKind},
    terminal::{
        disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen,
        LeaveAlternateScreen,
    },
    ExecutableCommand,
};

use crate::cpu::info::CpuInfo;

use crate::cpu::intel::TemperatureData;
use crate::ui::TemperatureMessage::InitError;

pub enum TemperatureMessage {
    InitError(String),
    RuntimeError(anyhow::Error),
    Ok(TemperatureData),
}

impl TemperatureMessage {
    pub fn init_error(&self) -> Option<&str> {
        match self {
            InitError(e) => Some(e.as_str()),
            _ => None,
        }
    }
}

#[derive(Debug)]
pub struct CpuTempApp {
    pub title: String,
    pub cpu_name: String,
    pub data: anyhow::Result<TemperatureData>,
    pub errors: Vec<String>,
    pub running: bool,
}

impl Default for CpuTempApp {
    fn default() -> Self {
        let cpu_name = CpuInfo::get_core_cpu_mapping()
            .map(|m| {
                m.iter()
                    .filter_map(|c| c.first().copied())
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default();

        Self {
            title: "CPU Temperature Monitor".to_string(),
            cpu_name: format!("{} ({} cores)", "Unknown CPU", cpu_name.len()),
            data: Err(anyhow::anyhow!("Waiting")),
            errors: Vec::new(),
            running: true,
        }
    }
}

impl CpuTempApp {
    pub fn run_ui(&mut self) -> anyhow::Result<()> {
        enable_raw_mode()?;
        std::io::stderr().execute(EnterAlternateScreen)?;
        std::io::stderr().execute(Clear(ClearType::All))?;
        let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;

        let result = run_app(&mut terminal, self);

        disable_raw_mode()?;
        std::io::stderr().execute(LeaveAlternateScreen)?;

        result
    }

    pub fn run_with_data_receiver(
        &mut self,
        rx: Receiver<TemperatureMessage>,
    ) -> anyhow::Result<()> {
        enable_raw_mode()?;
        std::io::stderr().execute(EnterAlternateScreen)?;
        std::io::stderr().execute(Clear(ClearType::All))?;
        let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;

        let result = run_app_with_receiver(&mut terminal, self, rx);

        disable_raw_mode()?;
        std::io::stderr().execute(LeaveAlternateScreen)?;

        result
    }
}

fn run_app<B: std::io::Write>(
    terminal: &mut Terminal<CrosstermBackend<B>>,
    app: &mut CpuTempApp,
) -> anyhow::Result<()> {
    loop {
        terminal.draw(|frame| ui(frame, app))?;

        if !app.running {
            break;
        }

        // Check for exit key (Ctrl+C or Escape)
        if event::poll(std::time::Duration::from_millis(100))? {
            if let Event::Key(key) = event::read()? {
                if key.kind == KeyEventKind::Press {
                    match key.code {
                        KeyCode::Char('c')
                            if key
                                .modifiers
                                .contains(crossterm::event::KeyModifiers::CONTROL) =>
                        {
                            app.running = false;
                            break;
                        }
                        KeyCode::Esc => {
                            app.running = false;
                            break;
                        }
                        _ => {}
                    }
                }
            }
        }
    }

    Ok(())
}

fn run_app_with_receiver<B: std::io::Write>(
    terminal: &mut Terminal<CrosstermBackend<B>>,
    app: &mut CpuTempApp,
    rx: Receiver<TemperatureMessage>,
) -> anyhow::Result<()> {
    let mut init_error = false;
    loop {
        // 尝试从通道接收温度数据
        if !init_error {
            match rx.try_recv() {
                Ok(msg) => {
                    app.data = match msg {
                        InitError(e) => {
                            init_error = true;
                            Err(anyhow::anyhow!(e))
                        }
                        TemperatureMessage::RuntimeError(e) => Err(e),
                        TemperatureMessage::Ok(d) => Ok(d),
                    };
                }
                Err(TryRecvError::Empty) => {
                    std::thread::yield_now();
                    continue;
                }
                Err(TryRecvError::Disconnected) => {}
            };
        }

        terminal.draw(|frame| ui(frame, app))?;

        if !app.running {
            break;
        }

        // Check for exit key (Ctrl+C or Escape)
        if event::poll(std::time::Duration::from_millis(100))? {
            if let Event::Key(key) = event::read()? {
                if key.kind == KeyEventKind::Press {
                    match key.code {
                        KeyCode::Char('c')
                            if key
                                .modifiers
                                .contains(crossterm::event::KeyModifiers::CONTROL) =>
                        {
                            app.running = false;
                            break;
                        }
                        KeyCode::Esc => {
                            app.running = false;
                            break;
                        }
                        _ => {}
                    }
                }
            }
        }
    }

    Ok(())
}

fn ui(frame: &mut Frame<'_>, app: &mut CpuTempApp) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .margin(1)
        .constraints(
            [
                Constraint::Length(1), // Title
                Constraint::Length(3), // CPU Info
                Constraint::Min(4),    // Core temperatures table
                Constraint::Length(3), // Package temperature
                Constraint::Length(3), // Status bar
            ]
            .as_ref(),
        )
        .split(frame.area());

    // Title
    let title = Paragraph::new(app.title.as_str()).fg(ratatui::style::Color::Cyan);
    frame.render_widget(title, chunks[0]);

    // CPU Info
    let cpu_info = Paragraph::new(app.cpu_name.as_str())
        .block(Block::default().borders(Borders::ALL).title(" CPU Info "));
    frame.render_widget(cpu_info, chunks[1]);

    // Core temperatures table
    let header = Row::new(vec![
        Cell::from("Core"),
        Cell::from("Temperature (°C)"),
        Cell::from("TjMax (°C)"),
    ])
    .bold()
    .bg(ratatui::style::Color::DarkGray);

    let core_rows: Vec<Row> = if let Ok(data) = &app.data {
        data.core_temps
            .iter()
            .map(|core| {
                Row::new(vec![
                    Cell::from(format!("{}", core.physical_id + 1)),
                    Cell::from(format!("{:.1}", core.core_temp.temperature))
                        .fg(temp_color(core.core_temp.temperature, data.tj_max)),
                    Cell::from(format!("{:.1}", core.core_temp.tj_max)),
                ])
            })
            .collect()
    } else {
        vec![Row::new(vec![
            Cell::from("N/A"),
            Cell::from("N/A"),
            Cell::from("N/A"),
        ])]
    };

    let core_table = Table::new(
        core_rows,
        [
            Constraint::Length(6),
            Constraint::Length(18),
            Constraint::Length(10),
            Constraint::Length(12),
        ],
    )
    .header(header)
    .block(
        Block::default()
            .borders(Borders::ALL)
            .title(" Core Temperatures "),
    );

    frame.render_widget(core_table, chunks[2]);

    // Package temperature
    let package_temp_text = match &app.data {
        Ok(data) => match &data.package_temp {
            Ok(pkg) => format!(
                "Package: {:.1}°C | TjMax: {:.1}°C",
                pkg.temperature, pkg.tj_max
            ),
            Err(e) => format!("Package: {}", e),
        },
        Err(_) => "Package: N/A".to_string(),
    };

    let package_temp = Paragraph::new(package_temp_text.as_str())
        .block(
            Block::default()
                .borders(Borders::ALL)
                .title(" Package Temperature "),
        )
        .fg(ratatui::style::Color::Yellow);
    frame.render_widget(package_temp, chunks[3]);

    // Status bar - 始终渲染在最底层
    let status_text: Cow<'static, str> = if !app.running {
        Cow::Borrowed("Exiting...")
    } else if let Err(e) = &app.data {
        Cow::Owned(e.to_string()) // 只有在出错时才进行堆分配
    } else {
        Cow::Borrowed("Press Ctrl+C or Esc to exit")
    };

    let status = Paragraph::new(status_text)
        .block(Block::default().borders(Borders::ALL).title(" Status "))
        .fg(ratatui::style::Color::Green);
    frame.render_widget(status, chunks[4]);

    // Errors display - 在status bar上覆盖显示错误信息(不遮挡整个状态栏)
    if !app.errors.is_empty() {
        let last_error = app.errors.last().unwrap();
        // 只截取前50个字符显示,避免过长
        let error_display = if last_error.len() > 50 {
            &last_error[..50]
        } else {
            last_error.as_str()
        };
        let error_msg = Paragraph::new(error_display)
            .block(Block::default().borders(Borders::NONE).title(" Error "))
            .fg(ratatui::style::Color::Red);

        // 在status bar内部左侧显示错误
        frame.render_widget(error_msg, chunks[4]);
    }
}

fn temp_color(temp: f32, tj_max: f32) -> ratatui::style::Color {
    let ratio = if tj_max > 0.0 { temp / tj_max } else { 0.5 };

    match ratio {
        r if r < 0.6 => ratatui::style::Color::Green,
        r if r < 0.75 => ratatui::style::Color::Yellow,
        r if r < 0.9 => ratatui::style::Color::Rgb(255, 165, 0),
        _ => ratatui::style::Color::Red,
    }
}