xps 13 3980
This commit is contained in:
353
src/ui/dashboard.rs
Normal file
353
src/ui/dashboard.rs
Normal file
@@ -0,0 +1,353 @@
|
||||
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,
|
||||
};
|
||||
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<String>,
|
||||
}
|
||||
|
||||
impl DashboardState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
logs: vec!["FerroTherm 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::Length(3), // Cooling
|
||||
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);
|
||||
}
|
||||
|
||||
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(" FERROTHERM 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 info = Line::from(vec![
|
||||
Span::styled(" Tier: ", Style::default().fg(C_LAVENDER)),
|
||||
Span::styled(&state.fan_tier, Style::default().fg(C_TEAL)),
|
||||
Span::styled(" | RPM: ", Style::default().fg(C_LAVENDER)),
|
||||
Span::styled(format!("{}", state.fan_rpm), Style::default().fg(C_TEXT)),
|
||||
]);
|
||||
f.render_widget(Paragraph::new(info), 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<ListItem> = 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<ListItem> = 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
|
||||
);
|
||||
}
|
||||
2
src/ui/mod.rs
Normal file
2
src/ui/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod theme;
|
||||
pub mod dashboard;
|
||||
14
src/ui/theme.rs
Normal file
14
src/ui/theme.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use ratatui::style::Color;
|
||||
|
||||
pub const C_SURFACE0: Color = Color::Rgb(54, 58, 79);
|
||||
pub const C_OVERLAY0: Color = Color::Rgb(108, 112, 134);
|
||||
pub const C_TEXT: Color = Color::Rgb(205, 214, 244);
|
||||
pub const C_LAVENDER: Color = Color::Rgb(180, 190, 254);
|
||||
pub const C_SAPPHIRE: Color = Color::Rgb(116, 199, 236);
|
||||
pub const C_TEAL: Color = Color::Rgb(148, 226, 213);
|
||||
pub const C_GREEN: Color = Color::Rgb(166, 227, 161);
|
||||
pub const C_YELLOW: Color = Color::Rgb(249, 226, 175);
|
||||
pub const C_PEACH: Color = Color::Rgb(250, 179, 135);
|
||||
pub const C_RED: Color = Color::Rgb(243, 139, 168);
|
||||
pub const C_MAUVE: Color = Color::Rgb(203, 166, 247);
|
||||
pub const C_SUBTEXT: Color = Color::Rgb(166, 173, 200);
|
||||
Reference in New Issue
Block a user