use std::collections::VecDeque;
use super::device::DeviceId;
use super::memory::PressureLevel;
#[derive(Debug, Clone)]
pub struct TuiLayout {
pub min_width: u16,
pub min_height: u16,
pub rec_width: u16,
pub rec_height: u16,
pub sections: Vec<Section>,
pub refresh_rate_ms: u64,
pub sparkline_points: usize,
}
impl Default for TuiLayout {
fn default() -> Self {
Self {
min_width: 80,
min_height: 24,
rec_width: 160,
rec_height: 48,
sections: vec![
Section::new("compute", "COMPUTE", 0.25),
Section::new("memory", "MEMORY", 0.20),
Section::new("dataflow", "DATA FLOW", 0.20),
Section::new("kernels", "KERNELS", 0.20),
],
refresh_rate_ms: 100,
sparkline_points: 60,
}
}
}
impl TuiLayout {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_refresh_rate(mut self, ms: u64) -> Self {
self.refresh_rate_ms = ms;
self
}
#[must_use]
pub fn check_size(&self, width: u16, height: u16) -> SizeCheck {
if width >= self.rec_width && height >= self.rec_height {
SizeCheck::Recommended
} else if width >= self.min_width && height >= self.min_height {
SizeCheck::Minimum
} else {
SizeCheck::TooSmall
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SizeCheck {
Recommended,
Minimum,
TooSmall,
}
#[derive(Debug, Clone)]
pub struct Section {
pub id: String,
pub title: String,
pub height_pct: f32,
pub widgets: Vec<Widget>,
pub collapsed: bool,
pub focused: bool,
}
impl Section {
#[must_use]
pub fn new(id: impl Into<String>, title: impl Into<String>, height_pct: f32) -> Self {
Self {
id: id.into(),
title: title.into(),
height_pct,
widgets: Vec::new(),
collapsed: false,
focused: false,
}
}
pub fn add_widget(&mut self, widget: Widget) {
self.widgets.push(widget);
}
pub fn toggle_collapsed(&mut self) {
self.collapsed = !self.collapsed;
}
}
#[derive(Debug, Clone)]
pub enum Widget {
Gauge(GaugeWidget),
Sparkline(SparklineWidget),
ProgressBar(ProgressBarWidget),
Table(TableWidget),
Text(TextWidget),
}
#[derive(Debug, Clone)]
pub struct GaugeWidget {
pub label: String,
pub value_pct: f64,
pub warning_threshold: f64,
pub critical_threshold: f64,
pub max_value: f64,
pub suffix: String,
}
impl GaugeWidget {
#[must_use]
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
value_pct: 0.0,
warning_threshold: 70.0,
critical_threshold: 90.0,
max_value: 100.0,
suffix: "%".to_string(),
}
}
#[must_use]
pub fn with_value(mut self, value: f64) -> Self {
self.value_pct = value;
self
}
#[must_use]
pub fn with_thresholds(mut self, warning: f64, critical: f64) -> Self {
self.warning_threshold = warning;
self.critical_threshold = critical;
self
}
#[must_use]
pub fn color(&self) -> GaugeColor {
if self.value_pct >= self.critical_threshold {
GaugeColor::Critical
} else if self.value_pct >= self.warning_threshold {
GaugeColor::Warning
} else {
GaugeColor::Ok
}
}
#[must_use]
pub fn render_bar(&self, width: usize) -> String {
let ratio = (self.value_pct / self.max_value).min(1.0);
let filled = (ratio * width as f64).round() as usize;
let empty = width.saturating_sub(filled);
format!(
"{}: [{}{}] {:.1}{}",
self.label,
"█".repeat(filled),
"░".repeat(empty),
self.value_pct,
self.suffix
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GaugeColor {
Ok,
Warning,
Critical,
}
#[derive(Debug, Clone)]
pub struct SparklineWidget {
pub data: VecDeque<f64>,
pub label: String,
pub baseline: Option<f64>,
pub auto_scale: bool,
}
impl SparklineWidget {
#[must_use]
pub fn new(label: impl Into<String>) -> Self {
Self {
data: VecDeque::with_capacity(60),
label: label.into(),
baseline: None,
auto_scale: true,
}
}
#[must_use]
pub fn with_data(mut self, data: VecDeque<f64>) -> Self {
self.data = data;
self
}
#[must_use]
pub fn render(&self) -> String {
if self.data.is_empty() {
return String::new();
}
let blocks = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
let (min, max) = if self.auto_scale {
let min = self.data.iter().copied().fold(f64::INFINITY, f64::min);
let max = self.data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
(min, max)
} else {
(0.0, 100.0)
};
let range = (max - min).max(0.001);
self.data
.iter()
.map(|&v| {
let normalized = ((v - min) / range).clamp(0.0, 1.0);
let idx = (normalized * 7.0).round() as usize;
blocks[idx.min(7)]
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct ProgressBarWidget {
pub label: String,
pub progress: f64,
pub total_desc: String,
}
impl ProgressBarWidget {
#[must_use]
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
progress: 0.0,
total_desc: String::new(),
}
}
#[must_use]
pub fn with_progress(mut self, progress: f64) -> Self {
self.progress = progress.clamp(0.0, 1.0);
self
}
#[must_use]
pub fn with_total(mut self, desc: impl Into<String>) -> Self {
self.total_desc = desc.into();
self
}
#[must_use]
pub fn render(&self, width: usize) -> String {
let filled = (self.progress * width as f64).round() as usize;
let empty = width.saturating_sub(filled);
format!(
"{}: [{}{}] {}",
self.label,
"█".repeat(filled),
"░".repeat(empty),
self.total_desc
)
}
}
#[derive(Debug, Clone)]
pub struct TableWidget {
pub headers: Vec<String>,
pub rows: Vec<Vec<String>>,
pub highlight_row: Option<usize>,
pub column_widths: Vec<usize>,
}
impl TableWidget {
#[must_use]
pub fn new(headers: Vec<String>) -> Self {
Self {
headers,
rows: Vec::new(),
highlight_row: None,
column_widths: Vec::new(),
}
}
pub fn add_row(&mut self, row: Vec<String>) {
self.rows.push(row);
}
pub fn highlight(&mut self, row: usize) {
self.highlight_row = Some(row);
}
#[must_use]
pub fn calculate_widths(&self) -> Vec<usize> {
let mut widths: Vec<usize> = self.headers.iter().map(|h| h.len()).collect();
for row in &self.rows {
for (i, cell) in row.iter().enumerate() {
if i < widths.len() {
widths[i] = widths[i].max(cell.len());
}
}
}
widths
}
}
#[derive(Debug, Clone)]
pub struct TextWidget {
pub content: String,
pub style: TextStyle,
}
impl TextWidget {
#[must_use]
pub fn new(content: impl Into<String>) -> Self {
Self {
content: content.into(),
style: TextStyle::Normal,
}
}
#[must_use]
pub fn with_style(mut self, style: TextStyle) -> Self {
self.style = style;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextStyle {
Normal,
Bold,
Dim,
Italic,
Header,
Error,
Warning,
Success,
}
#[derive(Debug, Clone)]
pub struct ColorScheme {
pub ok: RgbColor,
pub warning: RgbColor,
pub critical: RgbColor,
pub neutral: RgbColor,
pub background: RgbColor,
}
impl Default for ColorScheme {
fn default() -> Self {
Self {
ok: RgbColor::new(0x21, 0x91, 0x8c), warning: RgbColor::new(0xfd, 0xe7, 0x25), critical: RgbColor::new(0xf0, 0x3b, 0x20), neutral: RgbColor::new(0x3b, 0x52, 0x8b), background: RgbColor::new(0x44, 0x01, 0x54), }
}
}
#[derive(Debug, Clone, Copy)]
pub struct RgbColor {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl RgbColor {
#[must_use]
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
#[must_use]
pub fn to_ansi_fg(&self) -> String {
format!("\x1b[38;2;{};{};{}m", self.r, self.g, self.b)
}
#[must_use]
pub fn to_ansi_bg(&self) -> String {
format!("\x1b[48;2;{};{};{}m", self.r, self.g, self.b)
}
#[must_use]
pub fn for_pressure_level(level: PressureLevel) -> Self {
match level {
PressureLevel::Ok => Self::new(0x21, 0x91, 0x8c), PressureLevel::Elevated => Self::new(0xfd, 0xe7, 0x25), PressureLevel::Warning => Self::new(0xfd, 0xa6, 0x00), PressureLevel::Critical => Self::new(0xf0, 0x3b, 0x20), }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyAction {
Quit,
Refresh,
ToggleStress,
FocusNext,
NavigateUp,
NavigateDown,
Expand,
Help,
Alerts,
Export,
TogglePause,
}
impl KeyAction {
#[must_use]
pub fn key(&self) -> char {
match self {
Self::Quit => 'q',
Self::Refresh => 'r',
Self::ToggleStress => 's',
Self::FocusNext => '\t',
Self::NavigateUp => '↑',
Self::NavigateDown => '↓',
Self::Expand => '\n',
Self::Help => '?',
Self::Alerts => 'a',
Self::Export => 'e',
Self::TogglePause => 'p',
}
}
#[must_use]
pub fn description(&self) -> &'static str {
match self {
Self::Quit => "Quit",
Self::Refresh => "Refresh",
Self::ToggleStress => "Stress Test",
Self::FocusNext => "Focus",
Self::NavigateUp => "Up",
Self::NavigateDown => "Down",
Self::Expand => "Expand",
Self::Help => "Help",
Self::Alerts => "Alerts",
Self::Export => "Export",
Self::TogglePause => "Pause",
}
}
}
#[derive(Debug, Clone)]
pub struct TuiRenderState {
pub cpu: Option<DeviceRenderState>,
pub gpus: Vec<DeviceRenderState>,
pub memory: MemoryRenderState,
pub data_flow: DataFlowRenderState,
pub kernels: Vec<KernelRenderState>,
pub pressure: PressureLevel,
pub stress_active: bool,
pub paused: bool,
pub focused_section: usize,
pub error: Option<String>,
}
impl Default for TuiRenderState {
fn default() -> Self {
Self {
cpu: None,
gpus: Vec::new(),
memory: MemoryRenderState::default(),
data_flow: DataFlowRenderState::default(),
kernels: Vec::new(),
pressure: PressureLevel::Ok,
stress_active: false,
paused: false,
focused_section: 0,
error: None,
}
}
}
#[derive(Debug, Clone)]
pub struct DeviceRenderState {
pub device_id: DeviceId,
pub name: String,
pub utilization_pct: f64,
pub temperature_c: f64,
pub power_watts: f64,
pub power_limit_watts: f64,
pub clock_mhz: u32,
pub history: VecDeque<f64>,
}
#[derive(Debug, Clone, Default)]
pub struct MemoryRenderState {
pub ram_pct: f64,
pub ram_used_gb: f64,
pub ram_total_gb: f64,
pub swap_pct: f64,
pub swap_used_gb: f64,
pub swap_total_gb: f64,
pub vram: Vec<(DeviceId, f64, f64, f64)>, pub ram_history: VecDeque<f64>,
}
#[derive(Debug, Clone, Default)]
pub struct DataFlowRenderState {
pub pcie_tx_gbps: f64,
pub pcie_rx_gbps: f64,
pub pcie_theoretical_gbps: f64,
pub memory_bus_pct: f64,
pub transfers: Vec<(String, String, f64)>, }
#[derive(Debug, Clone)]
pub struct KernelRenderState {
pub name: String,
pub device_id: DeviceId,
pub progress_pct: f64,
pub grid: String,
pub elapsed_ms: f64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn h042_tui_layout_default() {
let layout = TuiLayout::default();
assert_eq!(layout.min_width, 80);
assert_eq!(layout.min_height, 24);
assert_eq!(layout.refresh_rate_ms, 100);
assert_eq!(layout.sparkline_points, 60);
}
#[test]
fn h042_tui_layout_size_check() {
let layout = TuiLayout::default();
assert_eq!(layout.check_size(160, 48), SizeCheck::Recommended);
assert_eq!(layout.check_size(80, 24), SizeCheck::Minimum);
assert_eq!(layout.check_size(40, 12), SizeCheck::TooSmall);
}
#[test]
fn h043_section_new() {
let section = Section::new("test", "Test Section", 0.25);
assert_eq!(section.id, "test");
assert_eq!(section.title, "Test Section");
assert!((section.height_pct - 0.25).abs() < 0.001);
assert!(!section.collapsed);
assert!(!section.focused);
}
#[test]
fn h043_section_toggle_collapsed() {
let mut section = Section::new("test", "Test", 0.25);
assert!(!section.collapsed);
section.toggle_collapsed();
assert!(section.collapsed);
section.toggle_collapsed();
assert!(!section.collapsed);
}
#[test]
fn h044_gauge_new() {
let gauge = GaugeWidget::new("CPU");
assert_eq!(gauge.label, "CPU");
assert_eq!(gauge.value_pct, 0.0);
}
#[test]
fn h044_gauge_with_value() {
let gauge = GaugeWidget::new("CPU").with_value(75.5);
assert!((gauge.value_pct - 75.5).abs() < 0.01);
}
#[test]
fn h044_gauge_color() {
let ok = GaugeWidget::new("Test").with_value(50.0);
let warn = GaugeWidget::new("Test").with_value(75.0);
let crit = GaugeWidget::new("Test").with_value(95.0);
assert_eq!(ok.color(), GaugeColor::Ok);
assert_eq!(warn.color(), GaugeColor::Warning);
assert_eq!(crit.color(), GaugeColor::Critical);
}
#[test]
fn h044_gauge_render_bar() {
let gauge = GaugeWidget::new("CPU").with_value(50.0);
let bar = gauge.render_bar(20);
assert!(bar.contains("CPU"));
assert!(bar.contains("50.0"));
assert!(bar.contains("█"));
assert!(bar.contains("░"));
}
#[test]
fn h045_sparkline_new() {
let sparkline = SparklineWidget::new("History");
assert_eq!(sparkline.label, "History");
assert!(sparkline.data.is_empty());
}
#[test]
fn h045_sparkline_render_empty() {
let sparkline = SparklineWidget::new("Test");
assert_eq!(sparkline.render(), "");
}
#[test]
fn h045_sparkline_render() {
let mut data = VecDeque::new();
for i in 0..10 {
data.push_back(i as f64 * 10.0);
}
let sparkline = SparklineWidget::new("Test").with_data(data);
let rendered = sparkline.render();
assert_eq!(rendered.chars().count(), 10);
assert!(rendered.starts_with('▁'));
assert!(rendered.ends_with('█'));
}
#[test]
fn h046_progress_bar_new() {
let bar = ProgressBarWidget::new("RAM");
assert_eq!(bar.label, "RAM");
assert_eq!(bar.progress, 0.0);
}
#[test]
fn h046_progress_bar_with_progress() {
let bar = ProgressBarWidget::new("RAM").with_progress(0.75);
assert!((bar.progress - 0.75).abs() < 0.001);
}
#[test]
fn h046_progress_bar_clamp() {
let bar = ProgressBarWidget::new("RAM").with_progress(1.5);
assert_eq!(bar.progress, 1.0);
let bar2 = ProgressBarWidget::new("RAM").with_progress(-0.5);
assert_eq!(bar2.progress, 0.0);
}
#[test]
fn h046_progress_bar_render() {
let bar = ProgressBarWidget::new("RAM")
.with_progress(0.5)
.with_total("32 / 64 GB");
let rendered = bar.render(20);
assert!(rendered.contains("RAM"));
assert!(rendered.contains("32 / 64 GB"));
}
#[test]
fn h047_table_new() {
let table = TableWidget::new(vec!["Name".to_string(), "Value".to_string()]);
assert_eq!(table.headers.len(), 2);
assert!(table.rows.is_empty());
}
#[test]
fn h047_table_add_row() {
let mut table = TableWidget::new(vec!["Name".to_string()]);
table.add_row(vec!["Test".to_string()]);
assert_eq!(table.rows.len(), 1);
}
#[test]
fn h047_table_calculate_widths() {
let mut table = TableWidget::new(vec!["Name".to_string(), "Value".to_string()]);
table.add_row(vec!["Short".to_string(), "LongerValue".to_string()]);
let widths = table.calculate_widths();
assert_eq!(widths[0], 5); assert_eq!(widths[1], 11); }
#[test]
fn h048_color_scheme_default() {
let scheme = ColorScheme::default();
assert!(scheme.ok.r <= 255);
assert!(scheme.warning.r <= 255);
assert!(scheme.critical.r <= 255);
}
#[test]
fn h048_rgb_color_ansi_fg() {
let color = RgbColor::new(255, 128, 64);
let ansi = color.to_ansi_fg();
assert!(ansi.contains("38;2;255;128;64"));
}
#[test]
fn h048_rgb_color_ansi_bg() {
let color = RgbColor::new(255, 128, 64);
let ansi = color.to_ansi_bg();
assert!(ansi.contains("48;2;255;128;64"));
}
#[test]
fn h048_rgb_for_pressure_level() {
let _ = RgbColor::for_pressure_level(PressureLevel::Ok);
let _ = RgbColor::for_pressure_level(PressureLevel::Elevated);
let _ = RgbColor::for_pressure_level(PressureLevel::Warning);
let _ = RgbColor::for_pressure_level(PressureLevel::Critical);
}
#[test]
fn h049_key_action_key() {
assert_eq!(KeyAction::Quit.key(), 'q');
assert_eq!(KeyAction::Refresh.key(), 'r');
assert_eq!(KeyAction::Help.key(), '?');
}
#[test]
fn h049_key_action_description() {
assert_eq!(KeyAction::Quit.description(), "Quit");
assert_eq!(KeyAction::Refresh.description(), "Refresh");
}
#[test]
fn h050_tui_render_state_default() {
let state = TuiRenderState::default();
assert!(state.cpu.is_none());
assert!(state.gpus.is_empty());
assert!(!state.stress_active);
assert!(!state.paused);
assert_eq!(state.focused_section, 0);
}
#[test]
fn h060_tui_layout_with_refresh_rate() {
let layout = TuiLayout::new().with_refresh_rate(50);
assert_eq!(layout.refresh_rate_ms, 50);
}
#[test]
fn h060_section_add_widget() {
let mut section = Section::new("test", "Test", 0.5);
section.add_widget(Widget::Text(TextWidget::new("Hello")));
assert_eq!(section.widgets.len(), 1);
}
#[test]
fn h060_text_widget() {
let text = TextWidget::new("Hello World");
assert_eq!(text.content, "Hello World");
assert_eq!(text.style, TextStyle::Normal);
let styled = TextWidget::new("Error").with_style(TextStyle::Error);
assert_eq!(styled.style, TextStyle::Error);
}
#[test]
fn h060_gauge_with_thresholds() {
let gauge = GaugeWidget::new("Test").with_thresholds(60.0, 80.0);
assert!((gauge.warning_threshold - 60.0).abs() < 0.01);
assert!((gauge.critical_threshold - 80.0).abs() < 0.01);
}
#[test]
fn h060_table_highlight() {
let mut table = TableWidget::new(vec!["Col".to_string()]);
table.add_row(vec!["Row1".to_string()]);
table.add_row(vec!["Row2".to_string()]);
table.highlight(1);
assert_eq!(table.highlight_row, Some(1));
}
#[test]
fn h060_sparkline_no_auto_scale() {
let mut sparkline = SparklineWidget::new("Test");
sparkline.auto_scale = false;
sparkline.data.push_back(25.0);
sparkline.data.push_back(50.0);
sparkline.data.push_back(75.0);
let rendered = sparkline.render();
assert_eq!(rendered.chars().count(), 3);
}
#[test]
fn h060_widget_enum_variants() {
let _gauge = Widget::Gauge(GaugeWidget::new("Test"));
let _sparkline = Widget::Sparkline(SparklineWidget::new("Test"));
let _progress = Widget::ProgressBar(ProgressBarWidget::new("Test"));
let _table = Widget::Table(TableWidget::new(vec![]));
let _text = Widget::Text(TextWidget::new("Test"));
}
#[test]
fn h060_key_action_all_keys() {
let actions = [
KeyAction::Quit,
KeyAction::Refresh,
KeyAction::ToggleStress,
KeyAction::FocusNext,
KeyAction::NavigateUp,
KeyAction::NavigateDown,
KeyAction::Expand,
KeyAction::Help,
KeyAction::Alerts,
KeyAction::Export,
KeyAction::TogglePause,
];
for action in &actions {
let _ = action.key();
let desc = action.description();
assert!(!desc.is_empty());
}
}
#[test]
fn h060_memory_render_state() {
let state = MemoryRenderState::default();
assert_eq!(state.ram_pct, 0.0);
assert!(state.vram.is_empty());
assert!(state.ram_history.is_empty());
}
#[test]
fn h060_data_flow_render_state() {
let state = DataFlowRenderState::default();
assert_eq!(state.pcie_tx_gbps, 0.0);
assert_eq!(state.pcie_rx_gbps, 0.0);
assert!(state.transfers.is_empty());
}
#[test]
fn h060_text_styles() {
let styles = [
TextStyle::Normal,
TextStyle::Bold,
TextStyle::Dim,
TextStyle::Italic,
TextStyle::Header,
TextStyle::Error,
TextStyle::Warning,
TextStyle::Success,
];
for (i, s1) in styles.iter().enumerate() {
for (j, s2) in styles.iter().enumerate() {
if i != j {
assert_ne!(s1, s2);
}
}
}
}
#[test]
fn h060_size_check_equality() {
assert_eq!(SizeCheck::Recommended, SizeCheck::Recommended);
assert_ne!(SizeCheck::Recommended, SizeCheck::Minimum);
assert_ne!(SizeCheck::Minimum, SizeCheck::TooSmall);
}
#[test]
fn h060_gauge_color_equality() {
assert_eq!(GaugeColor::Ok, GaugeColor::Ok);
assert_ne!(GaugeColor::Ok, GaugeColor::Warning);
assert_ne!(GaugeColor::Warning, GaugeColor::Critical);
}
}