use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, style::{Style, Modifier, Color}, text::{Span, Line}, widgets::{Block, Borders, List, ListItem, Paragraph, Chart, Dataset, Axis, BorderType, GraphType}, symbols::Marker, Frame, prelude::Stylize, }; use crate::mediator::TelemetryState; use crate::ui::theme::*; /// DashboardState maintains UI-specific state that isn't part of the core telemetry, /// such as the accumulated diagnostic logs. pub struct DashboardState { pub logs: Vec, } impl DashboardState { pub fn new() -> Self { Self { logs: vec!["ember-tune Initialized.".to_string()], } } /// Updates the UI state based on new telemetry. pub fn update(&mut self, _state: &TelemetryState) {} } /// Main entry point for drawing the dashboard. pub fn draw_dashboard( f: &mut Frame, area: Rect, state: &TelemetryState, ui_state: &DashboardState, ) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(1), // Header Constraint::Length(3), // Top Block (System Info) Constraint::Min(15), // Middle (Gauges + Blocks + Graphs) Constraint::Length(8), // Bottom (Logs) ]) .split(area); draw_header(f, chunks[0], state); draw_sys_info(f, chunks[1], state); let middle_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Length(32), // Gauges + Small Blocks Constraint::Min(20), // Graphs ]) .split(chunks[2]); let left_side_chunks = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(10), // Gauges Constraint::Min(4), // Cooling (Increased for multiple fans) Constraint::Length(3), // CPU State Constraint::Min(4), // Metadata ]) .split(middle_chunks[0]); draw_gauges(f, left_side_chunks[0], state); draw_cooling(f, left_side_chunks[1], state); draw_cpu_state(f, left_side_chunks[2], state); draw_metadata(f, left_side_chunks[3], state); let right_side_chunks = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Ratio(1, 3), Constraint::Ratio(1, 3), Constraint::Ratio(1, 3), ]) .split(middle_chunks[1]); draw_temp_graph(f, right_side_chunks[0], state); draw_pwr_graph(f, right_side_chunks[1], state); draw_freq_graph(f, right_side_chunks[2], state); draw_logs(f, chunks[3], ui_state); if state.is_emergency { draw_emergency_overlay(f, area, state); } } fn draw_emergency_overlay(f: &mut Frame, area: Rect, state: &TelemetryState) { let block = Block::default() .borders(Borders::ALL) .border_type(BorderType::Double) .border_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)) .bg(Color::Black) .title(" 🚨 EMERGENCY ABORT 🚨 "); let area = centered_rect(60, 20, area); let inner = block.inner(area); f.render_widget(block, area); let reason = state.emergency_reason.as_deref().unwrap_or("Unknown safety trigger"); let text = vec![ Line::from(vec![Span::styled("CRITICAL SAFETY LIMIT TRIGGERED", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD))]), Line::from(""), Line::from(vec![Span::raw("Reason: "), Span::styled(reason, Style::default().fg(Color::Yellow))]), Line::from(""), Line::from("Hardware has been restored to safe defaults."), Line::from("Exiting in 1 second..."), ]; f.render_widget(Paragraph::new(text).alignment(ratatui::layout::Alignment::Center), inner); } fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { let popup_layout = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Percentage((100 - percent_y) / 2), Constraint::Percentage(percent_y), Constraint::Percentage((100 - percent_y) / 2), ]) .split(r); Layout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Percentage((100 - percent_x) / 2), Constraint::Percentage(percent_x), Constraint::Percentage((100 - percent_x) / 2), ]) .split(popup_layout[1])[1] } fn draw_header(f: &mut Frame, area: Rect, state: &TelemetryState) { let uptime_secs = state.tick / 2; let uptime = format!("{:02}:{:02}:{:02}", uptime_secs / 3600, (uptime_secs % 3600) / 60, uptime_secs % 60); let hostname = std::env::var("HOSTNAME").unwrap_or_else(|_| "localhost".into()); let left = Span::styled(format!(" 󰈐 {} ", hostname), Style::default().fg(C_MAUVE).add_modifier(Modifier::BOLD)); let center = Span::styled(" EMBER-TUNE THERMAL BENCH ", Style::default().fg(C_LAVENDER).add_modifier(Modifier::BOLD)); let right = Span::styled(format!(" UPTIME: {} ", uptime), Style::default().fg(C_SUBTEXT)); let total_width = area.width; let left_width = left.width() as u16; let center_width = center.width() as u16; let right_width = right.width() as u16; let space1 = (total_width.saturating_sub(left_width + center_width + right_width)) / 2; let space2 = total_width.saturating_sub(left_width + center_width + right_width + space1); let header_text = Line::from(vec![ left, Span::raw(" ".repeat(space1 as usize)), center, Span::raw(" ".repeat(space2 as usize)), right, ]); f.render_widget(Paragraph::new(header_text).style(Style::default().bg(C_SURFACE0)), area); } fn draw_sys_info(f: &mut Frame, area: Rect, state: &TelemetryState) { let block = Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(C_SURFACE0)) .title(" SYSTEM INFO "); let inner = block.inner(area); f.render_widget(block, area); let info_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Percentage(40), Constraint::Percentage(20), Constraint::Percentage(20), Constraint::Percentage(20), ]) .split(inner); let cpu_info = Line::from(vec![ Span::styled(" CPU: ", Style::default().fg(C_LAVENDER)), Span::styled(&state.cpu_model, Style::default().fg(C_TEXT)), ]); f.render_widget(Paragraph::new(cpu_info), info_chunks[0]); let ram_info = Line::from(vec![ Span::styled(" RAM: ", Style::default().fg(C_LAVENDER)), Span::styled(format!("{} GB", state.total_ram_gb), Style::default().fg(C_TEXT)), ]); f.render_widget(Paragraph::new(ram_info), info_chunks[1]); let gov_info = Line::from(vec![ Span::styled(" GOV: ", Style::default().fg(C_LAVENDER)), Span::styled(&state.governor, Style::default().fg(C_TEAL)), ]); f.render_widget(Paragraph::new(gov_info), info_chunks[2]); let pl_info = Line::from(vec![ Span::styled(" PL1/2: ", Style::default().fg(C_LAVENDER)), Span::styled(format!("{:.1}/{:.1}W", state.pl1_limit, state.pl2_limit), Style::default().fg(C_PEACH)), ]); f.render_widget(Paragraph::new(pl_info), info_chunks[3]); } fn draw_gauges(f: &mut Frame, area: Rect, state: &TelemetryState) { let chunks = Layout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Percentage(50), Constraint::Percentage(50), ]) .split(area); let temp_color = if state.cpu_temp > 90.0 { C_RED } else if state.cpu_temp > 75.0 { C_YELLOW } else { C_TEAL }; draw_vertical_gauge(f, chunks[0], "TEMP", state.cpu_temp, 100.0, temp_color, "°C"); draw_vertical_gauge(f, chunks[1], "PWR", state.power_w, 50.0, C_PEACH, "W"); } fn draw_cooling(f: &mut Frame, area: Rect, state: &TelemetryState) { let block = Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(C_SURFACE0)) .title(" COOLING "); let inner = block.inner(area); f.render_widget(block, area); let mut lines = Vec::new(); // Line 1: Tier lines.push(Line::from(vec![ Span::styled(" Tier: ", Style::default().fg(C_LAVENDER)), Span::styled(&state.fan_tier, Style::default().fg(C_TEAL)), ])); // Line 2+: Fans if state.fans.is_empty() { lines.push(Line::from(vec![ Span::styled(" Fans: ", Style::default().fg(C_LAVENDER)), Span::styled("N/A", Style::default().fg(C_SUBTEXT)), ])); } else if state.fans.len() == 1 { lines.push(Line::from(vec![ Span::styled(" Fan: ", Style::default().fg(C_LAVENDER)), Span::styled(format!("{} RPM", state.fans[0]), Style::default().fg(C_TEXT)), ])); } else { for (i, rpm) in state.fans.iter().enumerate() { lines.push(Line::from(vec![ Span::styled(format!(" Fan {}: ", i + 1), Style::default().fg(C_LAVENDER)), Span::styled(format!("{} RPM", rpm), Style::default().fg(C_TEXT)), ])); } } f.render_widget(Paragraph::new(lines), inner); } fn draw_cpu_state(f: &mut Frame, area: Rect, state: &TelemetryState) { let block = Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(C_SURFACE0)) .title(" CPU STATE "); let inner = block.inner(area); f.render_widget(block, area); let info = Line::from(vec![ Span::styled(" Frequency: ", Style::default().fg(C_LAVENDER)), Span::styled(format!("{:.0} MHz", state.current_freq), Style::default().fg(C_SAPPHIRE)), ]); f.render_widget(Paragraph::new(info), inner); } fn draw_metadata(f: &mut Frame, area: Rect, state: &TelemetryState) { let block = Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(C_SURFACE0)) .title(" HARDWARE INFO "); let inner = block.inner(area); f.render_widget(block, area); if state.metadata.is_empty() { f.render_widget(Paragraph::new(" No extra telemetry data.").style(Style::default().fg(C_OVERLAY0)), inner); return; } let items: Vec = state.metadata.iter().map(|(k, v)| { ListItem::new(Line::from(vec![ Span::styled(format!(" {}: ", k), Style::default().fg(C_LAVENDER)), Span::styled(v, Style::default().fg(C_TEXT)), ])) }).collect(); f.render_widget(List::new(items), inner); } fn draw_vertical_gauge(f: &mut Frame, area: Rect, label: &str, value: f32, max: f32, color: Color, unit: &str) { let block = Block::default() .title(format!(" {} ", label)) .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(C_SURFACE0)); let inner = block.inner(area); f.render_widget(block, area); if inner.height == 0 || inner.width == 0 { return; } let percent = (value / max).clamp(0.0, 1.0); let filled_height = (percent * inner.height as f32).round() as u16; for y in 0..inner.height { let is_filled = y < filled_height; let symbol = if is_filled { "█" } else { "░" }; let style = if is_filled { Style::default().fg(color) } else { Style::default().fg(C_SURFACE0) }; let draw_y = inner.y + inner.height.saturating_sub(1).saturating_sub(y); let row_text = symbol.repeat(inner.width as usize); f.render_widget(Paragraph::new(row_text).style(style), Rect::new(inner.x, draw_y, inner.width, 1)); } let val_str = format!("{:.1}{}", value, unit); let val_len = val_str.len() as u16; let val_x = inner.x + inner.width.saturating_sub(val_len) / 2; let val_y = inner.y + inner.height / 2; f.render_widget( Paragraph::new(val_str).style(Style::default().fg(C_TEXT).bg(color).add_modifier(Modifier::BOLD)), Rect::new(val_x, val_y, inner.width.min(val_len), 1) ); } fn draw_temp_graph(f: &mut Frame, area: Rect, state: &TelemetryState) { let temp_data: Vec<(f64, f64)> = state.history_temp.iter().enumerate() .map(|(i, &v)| (i as f64, v as f64)).collect(); let temp_dataset = Dataset::default() .name("Temp") .marker(Marker::Braille) .graph_type(GraphType::Line) .style(Style::default().fg(C_RED)) .data(&temp_data); let chart = Chart::new(vec![temp_dataset]) .block(Block::default().title(" CPU TEMPERATURE (°C) ").borders(Borders::ALL).border_type(BorderType::Rounded).border_style(Style::default().fg(C_SURFACE0))) .x_axis(Axis::default().bounds([0.0, 120.0]).style(Style::default().fg(C_OVERLAY0))) .y_axis(Axis::default().bounds([30.0, 100.0]).labels(vec![ Line::from("30"), Line::from("65"), Line::from("100"), ]).style(Style::default().fg(C_OVERLAY0))); f.render_widget(chart, area); } fn draw_pwr_graph(f: &mut Frame, area: Rect, state: &TelemetryState) { let pwr_data: Vec<(f64, f64)> = state.history_watts.iter().enumerate() .map(|(i, &v)| (i as f64, v as f64)).collect(); let pwr_dataset = Dataset::default() .name("Power") .marker(Marker::Braille) .graph_type(GraphType::Line) .style(Style::default().fg(C_PEACH)) .data(&pwr_data); let chart = Chart::new(vec![pwr_dataset]) .block(Block::default().title(" PACKAGE POWER (W) ").borders(Borders::ALL).border_type(BorderType::Rounded).border_style(Style::default().fg(C_SURFACE0))) .x_axis(Axis::default().bounds([0.0, 120.0]).style(Style::default().fg(C_OVERLAY0))) .y_axis(Axis::default().bounds([0.0, 50.0]).labels(vec![ Line::from("0"), Line::from("25"), Line::from("50"), ]).style(Style::default().fg(C_OVERLAY0))); f.render_widget(chart, area); } fn draw_freq_graph(f: &mut Frame, area: Rect, state: &TelemetryState) { let freq_data: Vec<(f64, f64)> = state.history_mhz.iter().enumerate() .map(|(i, &v)| (i as f64, v as f64)).collect(); let freq_dataset = Dataset::default() .name("Freq") .marker(Marker::Braille) .graph_type(GraphType::Line) .style(Style::default().fg(C_SAPPHIRE)) .data(&freq_data); let chart = Chart::new(vec![freq_dataset]) .block(Block::default().title(" CPU FREQUENCY (MHz) ").borders(Borders::ALL).border_type(BorderType::Rounded).border_style(Style::default().fg(C_SURFACE0))) .x_axis(Axis::default().bounds([0.0, 120.0]).style(Style::default().fg(C_OVERLAY0))) .y_axis(Axis::default().bounds([0.0, 5000.0]).labels(vec![ Line::from("0"), Line::from("2500"), Line::from("5000"), ]).style(Style::default().fg(C_OVERLAY0))); f.render_widget(chart, area); } fn draw_logs(f: &mut Frame, area: Rect, ui_state: &DashboardState) { let log_items: Vec = ui_state.logs.iter().rev().take(area.height as usize).map(|l| { let style = if l.contains("⚠") || l.contains("failed") || l.contains("Abort") { Style::default().fg(C_RED) } else if l.contains("✓") || l.contains("complete") { Style::default().fg(C_GREEN) } else { Style::default().fg(C_TEAL) }; ListItem::new(format!(" {} ", l)).style(style) }).collect(); f.render_widget( List::new(log_items) .block(Block::default().title(" DIAGNOSTIC LOG ").borders(Borders::ALL).border_type(BorderType::Rounded).border_style(Style::default().fg(C_SURFACE0))), area ); }