Skip to main content

trueno_gpu/monitor/tui_layout/
widgets.rs

1//! TUI widget types for compute monitoring.
2
3use std::collections::VecDeque;
4
5// ============================================================================
6// Widget Types
7// ============================================================================
8
9/// TUI widget
10#[derive(Debug, Clone)]
11pub enum Widget {
12    /// Progress gauge (0-100%)
13    Gauge(GaugeWidget),
14    /// Sparkline (history graph)
15    Sparkline(SparklineWidget),
16    /// Progress bar with label
17    ProgressBar(ProgressBarWidget),
18    /// Table with rows
19    Table(TableWidget),
20    /// Text label
21    Text(TextWidget),
22}
23
24/// Gauge widget for showing percentages
25#[derive(Debug, Clone)]
26pub struct GaugeWidget {
27    /// Widget label
28    pub label: String,
29    /// Current value (0.0-100.0)
30    pub value_pct: f64,
31    /// Warning threshold
32    pub warning_threshold: f64,
33    /// Critical threshold
34    pub critical_threshold: f64,
35    /// Maximum value (usually 100)
36    pub max_value: f64,
37    /// Suffix text (e.g., "%" or "°C")
38    pub suffix: String,
39}
40
41impl GaugeWidget {
42    /// Create a new gauge
43    #[must_use]
44    pub fn new(label: impl Into<String>) -> Self {
45        Self {
46            label: label.into(),
47            value_pct: 0.0,
48            warning_threshold: 70.0,
49            critical_threshold: 90.0,
50            max_value: 100.0,
51            suffix: "%".to_string(),
52        }
53    }
54
55    /// Set value
56    #[must_use]
57    pub fn with_value(mut self, value: f64) -> Self {
58        self.value_pct = value;
59        self
60    }
61
62    /// Set thresholds
63    #[must_use]
64    pub fn with_thresholds(mut self, warning: f64, critical: f64) -> Self {
65        self.warning_threshold = warning;
66        self.critical_threshold = critical;
67        self
68    }
69
70    /// Get color based on value
71    #[must_use]
72    pub fn color(&self) -> GaugeColor {
73        if self.value_pct >= self.critical_threshold {
74            GaugeColor::Critical
75        } else if self.value_pct >= self.warning_threshold {
76            GaugeColor::Warning
77        } else {
78            GaugeColor::Ok
79        }
80    }
81
82    /// Render as ASCII bar
83    #[must_use]
84    pub fn render_bar(&self, width: usize) -> String {
85        let ratio = (self.value_pct / self.max_value).min(1.0);
86        let filled = (ratio * width as f64).round() as usize;
87        let empty = width.saturating_sub(filled);
88
89        format!(
90            "{}: [{}{}] {:.1}{}",
91            self.label,
92            "\u{2588}".repeat(filled),
93            "\u{2591}".repeat(empty),
94            self.value_pct,
95            self.suffix
96        )
97    }
98}
99
100/// Gauge color
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum GaugeColor {
103    /// Normal (green)
104    Ok,
105    /// Warning (yellow/orange)
106    Warning,
107    /// Critical (red)
108    Critical,
109}
110
111/// Sparkline widget for showing history
112#[derive(Debug, Clone)]
113pub struct SparklineWidget {
114    /// Data points (0.0-1.0 normalized or raw values)
115    pub data: VecDeque<f64>,
116    /// Widget label
117    pub label: String,
118    /// Optional baseline to show
119    pub baseline: Option<f64>,
120    /// Auto-scale to data range
121    pub auto_scale: bool,
122}
123
124impl SparklineWidget {
125    /// Create a new sparkline
126    #[must_use]
127    pub fn new(label: impl Into<String>) -> Self {
128        Self {
129            data: VecDeque::with_capacity(60),
130            label: label.into(),
131            baseline: None,
132            auto_scale: true,
133        }
134    }
135
136    /// Set data
137    #[must_use]
138    pub fn with_data(mut self, data: VecDeque<f64>) -> Self {
139        self.data = data;
140        self
141    }
142
143    /// Render as Unicode sparkline
144    #[must_use]
145    pub fn render(&self) -> String {
146        if self.data.is_empty() {
147            return String::new();
148        }
149
150        let blocks = [
151            '\u{2581}', '\u{2582}', '\u{2583}', '\u{2584}', '\u{2585}', '\u{2586}', '\u{2587}',
152            '\u{2588}',
153        ];
154
155        let (min, max) = if self.auto_scale {
156            let min = self.data.iter().copied().fold(f64::INFINITY, f64::min);
157            let max = self.data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
158            (min, max)
159        } else {
160            (0.0, 100.0)
161        };
162
163        let range = (max - min).max(0.001);
164
165        self.data
166            .iter()
167            .map(|&v| {
168                let normalized = ((v - min) / range).clamp(0.0, 1.0);
169                let idx = (normalized * 7.0).round() as usize;
170                blocks[idx.min(7)]
171            })
172            .collect()
173    }
174}
175
176/// Progress bar widget
177#[derive(Debug, Clone)]
178pub struct ProgressBarWidget {
179    /// Widget label
180    pub label: String,
181    /// Progress (0.0-1.0)
182    pub progress: f64,
183    /// Total description (e.g., "14.0 / 24.0 GB")
184    pub total_desc: String,
185}
186
187impl ProgressBarWidget {
188    /// Create a new progress bar
189    #[must_use]
190    pub fn new(label: impl Into<String>) -> Self {
191        Self {
192            label: label.into(),
193            progress: 0.0,
194            total_desc: String::new(),
195        }
196    }
197
198    /// Set progress
199    #[must_use]
200    pub fn with_progress(mut self, progress: f64) -> Self {
201        self.progress = progress.clamp(0.0, 1.0);
202        self
203    }
204
205    /// Set total description
206    #[must_use]
207    pub fn with_total(mut self, desc: impl Into<String>) -> Self {
208        self.total_desc = desc.into();
209        self
210    }
211
212    /// Render as ASCII bar
213    #[must_use]
214    pub fn render(&self, width: usize) -> String {
215        let filled = (self.progress * width as f64).round() as usize;
216        let empty = width.saturating_sub(filled);
217
218        format!(
219            "{}: [{}{}] {}",
220            self.label,
221            "\u{2588}".repeat(filled),
222            "\u{2591}".repeat(empty),
223            self.total_desc
224        )
225    }
226}
227
228/// Table widget
229#[derive(Debug, Clone)]
230pub struct TableWidget {
231    /// Column headers
232    pub headers: Vec<String>,
233    /// Row data
234    pub rows: Vec<Vec<String>>,
235    /// Currently highlighted row
236    pub highlight_row: Option<usize>,
237    /// Column widths (auto-calculated if empty)
238    pub column_widths: Vec<usize>,
239}
240
241impl TableWidget {
242    /// Create a new table
243    #[must_use]
244    pub fn new(headers: Vec<String>) -> Self {
245        Self {
246            headers,
247            rows: Vec::new(),
248            highlight_row: None,
249            column_widths: Vec::new(),
250        }
251    }
252
253    /// Add a row
254    pub fn add_row(&mut self, row: Vec<String>) {
255        self.rows.push(row);
256    }
257
258    /// Set highlighted row
259    pub fn highlight(&mut self, row: usize) {
260        self.highlight_row = Some(row);
261    }
262
263    /// Calculate column widths
264    #[must_use]
265    pub fn calculate_widths(&self) -> Vec<usize> {
266        let mut widths: Vec<usize> = self.headers.iter().map(|h| h.len()).collect();
267
268        for row in &self.rows {
269            for (i, cell) in row.iter().enumerate() {
270                if i < widths.len() {
271                    widths[i] = widths[i].max(cell.len());
272                }
273            }
274        }
275
276        widths
277    }
278}
279
280/// Text widget
281#[derive(Debug, Clone)]
282pub struct TextWidget {
283    /// Text content
284    pub content: String,
285    /// Text style
286    pub style: TextStyle,
287}
288
289impl TextWidget {
290    /// Create a new text widget
291    #[must_use]
292    pub fn new(content: impl Into<String>) -> Self {
293        Self {
294            content: content.into(),
295            style: TextStyle::Normal,
296        }
297    }
298
299    /// Set style
300    #[must_use]
301    pub fn with_style(mut self, style: TextStyle) -> Self {
302        self.style = style;
303        self
304    }
305}
306
307/// Text style
308#[derive(Debug, Clone, Copy, PartialEq, Eq)]
309pub enum TextStyle {
310    /// Normal text
311    Normal,
312    /// Bold text
313    Bold,
314    /// Dimmed text
315    Dim,
316    /// Italic text
317    Italic,
318    /// Header text
319    Header,
320    /// Error text
321    Error,
322    /// Warning text
323    Warning,
324    /// Success text
325    Success,
326}