trueno_gpu/monitor/tui_layout/
widgets.rs1use std::collections::VecDeque;
4
5#[derive(Debug, Clone)]
11pub enum Widget {
12 Gauge(GaugeWidget),
14 Sparkline(SparklineWidget),
16 ProgressBar(ProgressBarWidget),
18 Table(TableWidget),
20 Text(TextWidget),
22}
23
24#[derive(Debug, Clone)]
26pub struct GaugeWidget {
27 pub label: String,
29 pub value_pct: f64,
31 pub warning_threshold: f64,
33 pub critical_threshold: f64,
35 pub max_value: f64,
37 pub suffix: String,
39}
40
41impl GaugeWidget {
42 #[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 #[must_use]
57 pub fn with_value(mut self, value: f64) -> Self {
58 self.value_pct = value;
59 self
60 }
61
62 #[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 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum GaugeColor {
103 Ok,
105 Warning,
107 Critical,
109}
110
111#[derive(Debug, Clone)]
113pub struct SparklineWidget {
114 pub data: VecDeque<f64>,
116 pub label: String,
118 pub baseline: Option<f64>,
120 pub auto_scale: bool,
122}
123
124impl SparklineWidget {
125 #[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 #[must_use]
138 pub fn with_data(mut self, data: VecDeque<f64>) -> Self {
139 self.data = data;
140 self
141 }
142
143 #[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#[derive(Debug, Clone)]
178pub struct ProgressBarWidget {
179 pub label: String,
181 pub progress: f64,
183 pub total_desc: String,
185}
186
187impl ProgressBarWidget {
188 #[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 #[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 #[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 #[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#[derive(Debug, Clone)]
230pub struct TableWidget {
231 pub headers: Vec<String>,
233 pub rows: Vec<Vec<String>>,
235 pub highlight_row: Option<usize>,
237 pub column_widths: Vec<usize>,
239}
240
241impl TableWidget {
242 #[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 pub fn add_row(&mut self, row: Vec<String>) {
255 self.rows.push(row);
256 }
257
258 pub fn highlight(&mut self, row: usize) {
260 self.highlight_row = Some(row);
261 }
262
263 #[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#[derive(Debug, Clone)]
282pub struct TextWidget {
283 pub content: String,
285 pub style: TextStyle,
287}
288
289impl TextWidget {
290 #[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 #[must_use]
301 pub fn with_style(mut self, style: TextStyle) -> Self {
302 self.style = style;
303 self
304 }
305}
306
307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
309pub enum TextStyle {
310 Normal,
312 Bold,
314 Dim,
316 Italic,
318 Header,
320 Error,
322 Warning,
324 Success,
326}