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;
}
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;
}
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), Constraint::Length(3), Constraint::Min(4), Constraint::Length(3), Constraint::Length(3), ]
.as_ref(),
)
.split(frame.area());
let title = Paragraph::new(app.title.as_str()).fg(ratatui::style::Color::Cyan);
frame.render_widget(title, chunks[0]);
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]);
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]);
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]);
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]);
if !app.errors.is_empty() {
let last_error = app.errors.last().unwrap();
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);
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,
}
}