1use presentar_core::{
12 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event, Key,
13 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
14};
15use std::any::Any;
16use std::time::Duration;
17
18use super::micro_heat_bar::HeatScheme;
19use super::selection::RowHighlight;
20
21#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
23pub enum CoreStatsSortColumn {
24 #[default]
25 CoreId,
26 Frequency,
27 Temperature,
28 User,
29 System,
30 Total,
31 Idle,
32}
33
34impl CoreStatsSortColumn {
35 #[must_use]
37 pub fn header(self) -> &'static str {
38 match self {
39 Self::CoreId => "CORE",
40 Self::Frequency => "FREQ",
41 Self::Temperature => "TEMP",
42 Self::User => "USR%",
43 Self::System => "SYS%",
44 Self::Total => "TOT%",
45 Self::Idle => "IDL%",
46 }
47 }
48}
49
50#[derive(Debug, Clone)]
52pub struct CoreStatsRow {
53 pub core_id: u32,
55 pub freq_mhz: u32,
57 pub temp_c: Option<f32>,
59 pub user_pct: f32,
61 pub system_pct: f32,
63 pub iowait_pct: f32,
65 pub idle_pct: f32,
67 pub util_history: Vec<f32>,
69 pub ctx_switches: u64,
71 pub interrupts: u64,
73}
74
75impl Default for CoreStatsRow {
76 fn default() -> Self {
77 Self {
78 core_id: 0,
79 freq_mhz: 0,
80 temp_c: None,
81 user_pct: 0.0,
82 system_pct: 0.0,
83 iowait_pct: 0.0,
84 idle_pct: 100.0,
85 util_history: Vec::new(),
86 ctx_switches: 0,
87 interrupts: 0,
88 }
89 }
90}
91
92impl CoreStatsRow {
93 #[must_use]
95 pub fn total_pct(&self) -> f32 {
96 self.user_pct + self.system_pct + self.iowait_pct
97 }
98}
99
100#[derive(Debug, Clone)]
102pub struct CoreStatsDataFrame {
103 rows: Vec<CoreStatsRow>,
105 sort_column: CoreStatsSortColumn,
107 sort_desc: bool,
109 selected_row: Option<usize>,
111 scroll_offset: usize,
113 show_header: bool,
115 show_breakdown_bars: bool,
117 accent_color: Color,
119 user_color: Color,
121 system_color: Color,
123 iowait_color: Color,
125 bounds: Rect,
127}
128
129impl Default for CoreStatsDataFrame {
130 fn default() -> Self {
131 Self::new()
132 }
133}
134
135impl CoreStatsDataFrame {
136 #[must_use]
138 pub fn new() -> Self {
139 Self {
140 rows: Vec::new(),
141 sort_column: CoreStatsSortColumn::CoreId,
142 sort_desc: false,
143 selected_row: None,
144 scroll_offset: 0,
145 show_header: true,
146 show_breakdown_bars: true,
147 accent_color: Color::new(0.4, 0.7, 1.0, 1.0),
148 user_color: Color::new(0.3, 0.7, 0.3, 1.0),
149 system_color: Color::new(0.9, 0.5, 0.2, 1.0),
150 iowait_color: Color::new(0.9, 0.2, 0.2, 1.0),
151 bounds: Rect::default(),
152 }
153 }
154
155 pub fn update_cores(&mut self, cores: Vec<CoreStatsRow>) {
157 self.rows = cores;
158 self.sort_rows();
159 }
160
161 #[must_use]
163 pub fn with_sort(mut self, column: CoreStatsSortColumn, descending: bool) -> Self {
164 self.sort_column = column;
165 self.sort_desc = descending;
166 self
167 }
168
169 #[must_use]
171 pub fn with_accent_color(mut self, color: Color) -> Self {
172 self.accent_color = color;
173 self
174 }
175
176 #[must_use]
178 pub fn with_breakdown_bars(mut self, show: bool) -> Self {
179 self.show_breakdown_bars = show;
180 self
181 }
182
183 pub fn cycle_sort(&mut self) {
185 self.sort_column = match self.sort_column {
186 CoreStatsSortColumn::CoreId => CoreStatsSortColumn::Total,
187 CoreStatsSortColumn::Total => CoreStatsSortColumn::User,
188 CoreStatsSortColumn::User => CoreStatsSortColumn::System,
189 CoreStatsSortColumn::System => CoreStatsSortColumn::Frequency,
190 CoreStatsSortColumn::Frequency => CoreStatsSortColumn::Temperature,
191 CoreStatsSortColumn::Temperature => CoreStatsSortColumn::Idle,
192 CoreStatsSortColumn::Idle => CoreStatsSortColumn::CoreId,
193 };
194 self.sort_rows();
195 }
196
197 pub fn toggle_sort_direction(&mut self) {
199 self.sort_desc = !self.sort_desc;
200 self.sort_rows();
201 }
202
203 fn sort_rows(&mut self) {
204 let desc = self.sort_desc;
205 match self.sort_column {
206 CoreStatsSortColumn::CoreId => {
207 self.rows.sort_by(|a, b| {
208 if desc {
209 b.core_id.cmp(&a.core_id)
210 } else {
211 a.core_id.cmp(&b.core_id)
212 }
213 });
214 }
215 CoreStatsSortColumn::Frequency => {
216 self.rows.sort_by(|a, b| {
217 if desc {
218 b.freq_mhz.cmp(&a.freq_mhz)
219 } else {
220 a.freq_mhz.cmp(&b.freq_mhz)
221 }
222 });
223 }
224 CoreStatsSortColumn::Temperature => {
225 self.rows.sort_by(|a, b| {
226 let a_temp = a.temp_c.unwrap_or(0.0);
227 let b_temp = b.temp_c.unwrap_or(0.0);
228 if desc {
229 b_temp
230 .partial_cmp(&a_temp)
231 .unwrap_or(std::cmp::Ordering::Equal)
232 } else {
233 a_temp
234 .partial_cmp(&b_temp)
235 .unwrap_or(std::cmp::Ordering::Equal)
236 }
237 });
238 }
239 CoreStatsSortColumn::User => {
240 self.rows.sort_by(|a, b| {
241 if desc {
242 b.user_pct
243 .partial_cmp(&a.user_pct)
244 .unwrap_or(std::cmp::Ordering::Equal)
245 } else {
246 a.user_pct
247 .partial_cmp(&b.user_pct)
248 .unwrap_or(std::cmp::Ordering::Equal)
249 }
250 });
251 }
252 CoreStatsSortColumn::System => {
253 self.rows.sort_by(|a, b| {
254 if desc {
255 b.system_pct
256 .partial_cmp(&a.system_pct)
257 .unwrap_or(std::cmp::Ordering::Equal)
258 } else {
259 a.system_pct
260 .partial_cmp(&b.system_pct)
261 .unwrap_or(std::cmp::Ordering::Equal)
262 }
263 });
264 }
265 CoreStatsSortColumn::Total => {
266 self.rows.sort_by(|a, b| {
267 if desc {
268 b.total_pct()
269 .partial_cmp(&a.total_pct())
270 .unwrap_or(std::cmp::Ordering::Equal)
271 } else {
272 a.total_pct()
273 .partial_cmp(&b.total_pct())
274 .unwrap_or(std::cmp::Ordering::Equal)
275 }
276 });
277 }
278 CoreStatsSortColumn::Idle => {
279 self.rows.sort_by(|a, b| {
280 if desc {
281 b.idle_pct
282 .partial_cmp(&a.idle_pct)
283 .unwrap_or(std::cmp::Ordering::Equal)
284 } else {
285 a.idle_pct
286 .partial_cmp(&b.idle_pct)
287 .unwrap_or(std::cmp::Ordering::Equal)
288 }
289 });
290 }
291 }
292 }
293
294 fn visible_rows(&self) -> usize {
295 let header_offset = if self.show_header { 2 } else { 0 };
296 (self.bounds.height as usize).saturating_sub(header_offset)
297 }
298
299 fn render_sparkline(values: &[f32], width: usize) -> String {
300 const BARS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
301
302 if values.is_empty() {
303 return "─".repeat(width);
304 }
305
306 let sample_width = width.min(values.len());
307 let step = values.len().saturating_sub(1) / sample_width.max(1);
308
309 (0..sample_width)
310 .map(|i| {
311 let idx = (i * step.max(1)).min(values.len().saturating_sub(1));
312 let v = values[idx].clamp(0.0, 100.0);
313 let norm = ((v / 100.0) * 7.0).round() as usize;
314 BARS[norm.min(7)]
315 })
316 .collect()
317 }
318
319 fn render_stacked_bar(&self, row: &CoreStatsRow, width: usize) -> Vec<(String, Color)> {
327 let total = 100.0f32;
328 let user_chars = ((row.user_pct / total) * width as f32).round() as usize;
329 let sys_chars = ((row.system_pct / total) * width as f32).round() as usize;
330 let io_chars = ((row.iowait_pct / total) * width as f32).round() as usize;
331 let idle_chars = width.saturating_sub(user_chars + sys_chars + io_chars);
332
333 let scheme = HeatScheme::Thermal;
336
337 let user_block = Self::gradient_block(row.user_pct);
339 let sys_block = Self::gradient_block(row.system_pct);
340 let io_block = Self::gradient_block(row.iowait_pct);
341
342 vec![
343 (
344 user_block.to_string().repeat(user_chars),
345 scheme.color_for_percent(row.user_pct as f64),
346 ),
347 (
348 sys_block.to_string().repeat(sys_chars),
349 scheme.color_for_percent(row.system_pct as f64),
350 ),
351 (
352 io_block.to_string().repeat(io_chars),
353 scheme.color_for_percent(row.iowait_pct as f64),
354 ),
355 ("░".repeat(idle_chars), Color::new(0.2, 0.2, 0.25, 1.0)),
356 ]
357 }
358
359 fn gradient_block(pct: f32) -> char {
361 const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
362 let level = ((pct / 100.0) * 7.0).round() as usize;
363 BLOCKS[level.min(7)]
364 }
365
366 fn format_freq(mhz: u32) -> String {
367 if mhz >= 1000 {
368 format!("{:.1}G", mhz as f32 / 1000.0)
369 } else {
370 format!("{mhz}M")
371 }
372 }
373
374 fn format_temp(temp: Option<f32>) -> String {
375 match temp {
376 Some(t) => format!("{t:.0}°"),
377 None => "─".to_string(),
378 }
379 }
380}
381
382impl Brick for CoreStatsDataFrame {
383 fn brick_name(&self) -> &'static str {
384 "core_stats_dataframe"
385 }
386
387 fn assertions(&self) -> &[BrickAssertion] {
388 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
389 ASSERTIONS
390 }
391
392 fn budget(&self) -> BrickBudget {
393 BrickBudget::uniform(16)
394 }
395
396 fn verify(&self) -> BrickVerification {
397 BrickVerification {
398 passed: self.assertions().to_vec(),
399 failed: vec![],
400 verification_time: Duration::from_micros(10),
401 }
402 }
403
404 fn to_html(&self) -> String {
405 format!(
406 r#"<table class="core-stats-dataframe"><thead><tr>{}</tr></thead></table>"#,
407 ["CORE", "FREQ", "TEMP", "USR%", "SYS%", "TOT%"]
408 .iter()
409 .map(|h| format!("<th>{h}</th>"))
410 .collect::<String>()
411 )
412 }
413
414 fn to_css(&self) -> String {
415 ".core-stats-dataframe { sort: core_id; }".to_string()
416 }
417}
418
419impl Widget for CoreStatsDataFrame {
420 fn type_id(&self) -> TypeId {
421 TypeId::of::<Self>()
422 }
423
424 fn measure(&self, constraints: Constraints) -> Size {
425 let header_height = if self.show_header { 2.0 } else { 0.0 };
426 let row_height = self.rows.len().min(64) as f32;
427 let height = (header_height + row_height).min(constraints.max_height);
428 constraints.constrain(Size::new(constraints.max_width, height))
429 }
430
431 fn layout(&mut self, bounds: Rect) -> LayoutResult {
432 self.bounds = bounds;
433 LayoutResult {
434 size: Size::new(bounds.width, bounds.height),
435 }
436 }
437
438 fn paint(&self, canvas: &mut dyn Canvas) {
439 if self.bounds.width < 50.0 || self.bounds.height < 3.0 {
440 return;
441 }
442
443 let mut y = self.bounds.y;
444 let x_start = self.bounds.x;
445 let width = self.bounds.width as usize;
446
447 let col_core = 5;
449 let col_freq = 6;
450 let col_temp = 5;
451 let col_user = 6;
452 let col_sys = 6;
453 let col_io = 5;
454 let col_idle = 6;
455 let col_bar = 20.min(width.saturating_sub(
456 col_core + col_freq + col_temp + col_user + col_sys + col_io + col_idle + 10,
457 ));
458 let col_sparkline = 12.min(width.saturating_sub(
459 col_core + col_freq + col_temp + col_user + col_sys + col_io + col_idle + col_bar + 12,
460 ));
461
462 let header_style = TextStyle {
463 color: self.accent_color,
464 ..Default::default()
465 };
466
467 let dim_style = TextStyle {
468 color: Color::new(0.5, 0.5, 0.5, 1.0),
469 ..Default::default()
470 };
471
472 if self.show_header {
474 let headers = [
475 (CoreStatsSortColumn::CoreId, col_core, "CORE"),
476 (CoreStatsSortColumn::Frequency, col_freq, "FREQ"),
477 (CoreStatsSortColumn::Temperature, col_temp, "TEMP"),
478 (CoreStatsSortColumn::User, col_user, "USR%"),
479 (CoreStatsSortColumn::System, col_sys, "SYS%"),
480 (CoreStatsSortColumn::Total, col_io, "IO%"),
481 (CoreStatsSortColumn::Idle, col_idle, "IDL%"),
482 ];
483
484 let mut hx = x_start;
485 for (col, w, name) in headers {
486 let is_sorted = col == self.sort_column;
487 let arrow = if is_sorted {
488 if self.sort_desc {
489 "▼"
490 } else {
491 "▲"
492 }
493 } else {
494 ""
495 };
496 let text = format!("{name}{arrow}");
497
498 let style = if is_sorted { &header_style } else { &dim_style };
499 canvas.draw_text(&text, Point::new(hx, y), style);
500 hx += w as f32 + 1.0;
501 }
502
503 if self.show_breakdown_bars && col_bar > 0 {
505 canvas.draw_text("BREAKDOWN", Point::new(hx, y), &dim_style);
506 hx += col_bar as f32 + 1.0;
507 }
508
509 if col_sparkline > 0 {
511 canvas.draw_text("HISTORY", Point::new(hx, y), &dim_style);
512 }
513
514 y += 1.0;
515
516 let sep = "─".repeat(width.min(120));
518 canvas.draw_text(&sep, Point::new(x_start, y), &dim_style);
519 y += 1.0;
520 }
521
522 let visible = self.visible_rows();
524 let end_idx = (self.scroll_offset + visible).min(self.rows.len());
525
526 for (rel_idx, row) in self.rows[self.scroll_offset..end_idx].iter().enumerate() {
527 let abs_idx = self.scroll_offset + rel_idx;
528 let is_selected = self.selected_row == Some(abs_idx);
529
530 let row_bounds = Rect::new(x_start, y, self.bounds.width, 1.0);
533 let row_highlight = RowHighlight::new(row_bounds, is_selected);
534 row_highlight.paint(canvas);
535
536 let text_style = row_highlight.text_style();
538 let text_color = text_style.color;
539
540 let mut x = x_start;
541
542 let core_str = format!("{:>4}", row.core_id);
544 canvas.draw_text(&core_str, Point::new(x, y), &text_style);
545 x += col_core as f32 + 1.0;
546
547 let freq_str = format!("{:>5}", Self::format_freq(row.freq_mhz));
549 canvas.draw_text(&freq_str, Point::new(x, y), &text_style);
550 x += col_freq as f32 + 1.0;
551
552 let temp_str = Self::format_temp(row.temp_c);
554 let temp_color = match row.temp_c {
555 Some(t) if t > 80.0 => Color::new(0.9, 0.2, 0.2, 1.0),
556 Some(t) if t > 60.0 => Color::new(0.9, 0.7, 0.1, 1.0),
557 Some(_) => text_color,
558 None => Color::new(0.5, 0.5, 0.5, 1.0),
559 };
560 canvas.draw_text(
561 &format!("{temp_str:>4}"),
562 Point::new(x, y),
563 &TextStyle {
564 color: temp_color,
565 ..Default::default()
566 },
567 );
568 x += col_temp as f32 + 1.0;
569
570 canvas.draw_text(
572 &format!("{:>5.1}", row.user_pct),
573 Point::new(x, y),
574 &TextStyle {
575 color: self.user_color,
576 ..Default::default()
577 },
578 );
579 x += col_user as f32 + 1.0;
580
581 canvas.draw_text(
583 &format!("{:>5.1}", row.system_pct),
584 Point::new(x, y),
585 &TextStyle {
586 color: self.system_color,
587 ..Default::default()
588 },
589 );
590 x += col_sys as f32 + 1.0;
591
592 canvas.draw_text(
594 &format!("{:>4.1}", row.iowait_pct),
595 Point::new(x, y),
596 &TextStyle {
597 color: self.iowait_color,
598 ..Default::default()
599 },
600 );
601 x += col_io as f32 + 1.0;
602
603 canvas.draw_text(
605 &format!("{:>5.1}", row.idle_pct),
606 Point::new(x, y),
607 &text_style,
608 );
609 x += col_idle as f32 + 1.0;
610
611 if self.show_breakdown_bars && col_bar > 0 {
613 let segments = self.render_stacked_bar(row, col_bar);
614 let mut bx = x;
615 for (chars, color) in segments {
616 canvas.draw_text(
617 &chars,
618 Point::new(bx, y),
619 &TextStyle {
620 color,
621 ..Default::default()
622 },
623 );
624 bx += chars.len() as f32;
625 }
626 x += col_bar as f32 + 1.0;
627 }
628
629 if col_sparkline > 0 {
631 let sparkline = Self::render_sparkline(&row.util_history, col_sparkline);
632 canvas.draw_text(
633 &sparkline,
634 Point::new(x, y),
635 &TextStyle {
636 color: self.accent_color,
637 ..Default::default()
638 },
639 );
640 }
641
642 y += 1.0;
643 }
644 }
645
646 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
647 match event {
648 Event::KeyDown {
649 key: Key::Up | Key::K,
650 ..
651 } => {
652 if let Some(sel) = self.selected_row {
653 if sel > 0 {
654 self.selected_row = Some(sel - 1);
655 }
656 } else if !self.rows.is_empty() {
657 self.selected_row = Some(0);
658 }
659 None
660 }
661 Event::KeyDown {
662 key: Key::Down | Key::J,
663 ..
664 } => {
665 if let Some(sel) = self.selected_row {
666 if sel < self.rows.len().saturating_sub(1) {
667 self.selected_row = Some(sel + 1);
668 }
669 } else if !self.rows.is_empty() {
670 self.selected_row = Some(0);
671 }
672 None
673 }
674 Event::KeyDown { key: Key::S, .. } => {
675 self.cycle_sort();
676 None
677 }
678 Event::KeyDown { key: Key::R, .. } => {
679 self.toggle_sort_direction();
680 None
681 }
682 _ => None,
683 }
684 }
685
686 fn children(&self) -> &[Box<dyn Widget>] {
687 &[]
688 }
689
690 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
691 &mut []
692 }
693}
694
695#[cfg(test)]
696#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
697mod tests {
698 use super::*;
699 use crate::direct::{CellBuffer, DirectTerminalCanvas};
700
701 fn make_test_cores() -> Vec<CoreStatsRow> {
702 vec![
703 CoreStatsRow {
704 core_id: 0,
705 freq_mhz: 2400,
706 temp_c: Some(45.0),
707 user_pct: 10.0,
708 system_pct: 5.0,
709 iowait_pct: 2.0,
710 idle_pct: 83.0,
711 util_history: vec![10.0, 15.0, 12.0],
712 ..Default::default()
713 },
714 CoreStatsRow {
715 core_id: 1,
716 freq_mhz: 3600,
717 temp_c: Some(65.0),
718 user_pct: 50.0,
719 system_pct: 20.0,
720 iowait_pct: 5.0,
721 idle_pct: 25.0,
722 util_history: vec![60.0, 70.0, 75.0],
723 ..Default::default()
724 },
725 CoreStatsRow {
726 core_id: 2,
727 freq_mhz: 1800,
728 temp_c: Some(85.0),
729 user_pct: 30.0,
730 system_pct: 10.0,
731 iowait_pct: 3.0,
732 idle_pct: 57.0,
733 util_history: vec![35.0, 40.0, 43.0],
734 ..Default::default()
735 },
736 ]
737 }
738
739 #[test]
740 fn test_core_stats_creation() {
741 let df = CoreStatsDataFrame::new()
742 .with_sort(CoreStatsSortColumn::Total, true)
743 .with_breakdown_bars(true);
744
745 assert_eq!(df.sort_column, CoreStatsSortColumn::Total);
746 assert!(df.sort_desc);
747 assert!(df.show_breakdown_bars);
748 }
749
750 #[test]
751 fn test_core_stats_default() {
752 let df = CoreStatsDataFrame::default();
753 assert_eq!(df.sort_column, CoreStatsSortColumn::CoreId);
754 assert!(!df.sort_desc);
755 }
756
757 #[test]
758 fn test_core_row_total() {
759 let row = CoreStatsRow {
760 user_pct: 30.0,
761 system_pct: 10.0,
762 iowait_pct: 5.0,
763 idle_pct: 55.0,
764 ..Default::default()
765 };
766
767 assert!((row.total_pct() - 45.0).abs() < 0.001);
768 }
769
770 #[test]
771 fn test_core_row_default() {
772 let row = CoreStatsRow::default();
773 assert_eq!(row.core_id, 0);
774 assert_eq!(row.idle_pct, 100.0);
775 assert_eq!(row.total_pct(), 0.0);
776 }
777
778 #[test]
779 fn test_format_freq() {
780 assert_eq!(CoreStatsDataFrame::format_freq(2400), "2.4G");
781 assert_eq!(CoreStatsDataFrame::format_freq(800), "800M");
782 assert_eq!(CoreStatsDataFrame::format_freq(3600), "3.6G");
783 assert_eq!(CoreStatsDataFrame::format_freq(999), "999M");
784 assert_eq!(CoreStatsDataFrame::format_freq(1000), "1.0G");
785 }
786
787 #[test]
788 fn test_format_temp() {
789 assert_eq!(CoreStatsDataFrame::format_temp(Some(45.0)), "45°");
790 assert_eq!(CoreStatsDataFrame::format_temp(Some(85.5)), "86°");
791 assert_eq!(CoreStatsDataFrame::format_temp(None), "─");
792 }
793
794 #[test]
795 fn test_sort_by_total() {
796 let mut df = CoreStatsDataFrame::new().with_sort(CoreStatsSortColumn::Total, true);
797 df.update_cores(make_test_cores());
798
799 assert_eq!(df.rows[0].core_id, 1); assert_eq!(df.rows[1].core_id, 2); assert_eq!(df.rows[2].core_id, 0); }
804
805 #[test]
806 fn test_sort_by_frequency() {
807 let mut df = CoreStatsDataFrame::new().with_sort(CoreStatsSortColumn::Frequency, true);
808 df.update_cores(make_test_cores());
809
810 assert_eq!(df.rows[0].core_id, 1); assert_eq!(df.rows[1].core_id, 0); assert_eq!(df.rows[2].core_id, 2); }
814
815 #[test]
816 fn test_sort_by_temperature() {
817 let mut df = CoreStatsDataFrame::new().with_sort(CoreStatsSortColumn::Temperature, true);
818 df.update_cores(make_test_cores());
819
820 assert_eq!(df.rows[0].core_id, 2); assert_eq!(df.rows[1].core_id, 1); assert_eq!(df.rows[2].core_id, 0); }
824
825 #[test]
826 fn test_sort_by_user() {
827 let mut df = CoreStatsDataFrame::new().with_sort(CoreStatsSortColumn::User, true);
828 df.update_cores(make_test_cores());
829
830 assert_eq!(df.rows[0].core_id, 1); assert_eq!(df.rows[1].core_id, 2); assert_eq!(df.rows[2].core_id, 0); }
834
835 #[test]
836 fn test_sort_by_system() {
837 let mut df = CoreStatsDataFrame::new().with_sort(CoreStatsSortColumn::System, true);
838 df.update_cores(make_test_cores());
839
840 assert_eq!(df.rows[0].core_id, 1); assert_eq!(df.rows[1].core_id, 2); assert_eq!(df.rows[2].core_id, 0); }
844
845 #[test]
846 fn test_sort_by_idle() {
847 let mut df = CoreStatsDataFrame::new().with_sort(CoreStatsSortColumn::Idle, true);
848 df.update_cores(make_test_cores());
849
850 assert_eq!(df.rows[0].core_id, 0); assert_eq!(df.rows[1].core_id, 2); assert_eq!(df.rows[2].core_id, 1); }
854
855 #[test]
856 fn test_sort_ascending() {
857 let mut df = CoreStatsDataFrame::new().with_sort(CoreStatsSortColumn::Frequency, false);
858 df.update_cores(make_test_cores());
859
860 assert_eq!(df.rows[0].core_id, 2); assert_eq!(df.rows[1].core_id, 0); assert_eq!(df.rows[2].core_id, 1); }
864
865 #[test]
866 fn test_cycle_sort() {
867 let mut df = CoreStatsDataFrame::new();
868 df.update_cores(make_test_cores());
869
870 assert_eq!(df.sort_column, CoreStatsSortColumn::CoreId);
871 df.cycle_sort();
872 assert_eq!(df.sort_column, CoreStatsSortColumn::Total);
873 df.cycle_sort();
874 assert_eq!(df.sort_column, CoreStatsSortColumn::User);
875 df.cycle_sort();
876 assert_eq!(df.sort_column, CoreStatsSortColumn::System);
877 df.cycle_sort();
878 assert_eq!(df.sort_column, CoreStatsSortColumn::Frequency);
879 df.cycle_sort();
880 assert_eq!(df.sort_column, CoreStatsSortColumn::Temperature);
881 df.cycle_sort();
882 assert_eq!(df.sort_column, CoreStatsSortColumn::Idle);
883 df.cycle_sort();
884 assert_eq!(df.sort_column, CoreStatsSortColumn::CoreId);
885 }
886
887 #[test]
888 fn test_toggle_sort_direction() {
889 let mut df = CoreStatsDataFrame::new();
890 assert!(!df.sort_desc);
891 df.toggle_sort_direction();
892 assert!(df.sort_desc);
893 df.toggle_sort_direction();
894 assert!(!df.sort_desc);
895 }
896
897 #[test]
898 fn test_scroll_offset_default() {
899 let df = CoreStatsDataFrame::new();
900 assert_eq!(df.scroll_offset, 0);
901 }
902
903 #[test]
904 fn test_paint() {
905 let mut df = CoreStatsDataFrame::new();
906 df.update_cores(make_test_cores());
907
908 let mut buffer = CellBuffer::new(120, 20);
909 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
910
911 df.layout(Rect::new(0.0, 0.0, 120.0, 20.0));
912 df.paint(&mut canvas);
913
914 let cell = buffer.get(0, 0).unwrap();
916 assert!(!cell.symbol.is_empty());
917 }
918
919 #[test]
920 fn test_paint_small_bounds() {
921 let mut df = CoreStatsDataFrame::new();
922 df.update_cores(make_test_cores());
923
924 let mut buffer = CellBuffer::new(10, 2);
925 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
926
927 df.layout(Rect::new(0.0, 0.0, 10.0, 2.0));
928 df.paint(&mut canvas); }
930
931 #[test]
932 fn test_paint_no_header() {
933 let mut df = CoreStatsDataFrame::new();
934 df.show_header = false;
935 df.update_cores(make_test_cores());
936
937 let mut buffer = CellBuffer::new(120, 20);
938 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
939
940 df.layout(Rect::new(0.0, 0.0, 120.0, 20.0));
941 df.paint(&mut canvas);
942 }
943
944 #[test]
945 fn test_paint_no_breakdown_bars() {
946 let mut df = CoreStatsDataFrame::new().with_breakdown_bars(false);
947 df.update_cores(make_test_cores());
948
949 let mut buffer = CellBuffer::new(120, 20);
950 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
951
952 df.layout(Rect::new(0.0, 0.0, 120.0, 20.0));
953 df.paint(&mut canvas);
954 }
955
956 #[test]
957 fn test_paint_selected_row() {
958 let mut df = CoreStatsDataFrame::new();
959 df.update_cores(make_test_cores());
960 df.selected_row = Some(1);
961
962 let mut buffer = CellBuffer::new(120, 20);
963 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
964
965 df.layout(Rect::new(0.0, 0.0, 120.0, 20.0));
966 df.paint(&mut canvas);
967 }
968
969 #[test]
970 fn test_event_key_down() {
971 let mut df = CoreStatsDataFrame::new();
972 df.update_cores(make_test_cores());
973 df.layout(Rect::new(0.0, 0.0, 100.0, 10.0));
974
975 let event = Event::key_down(Key::Down);
976 let _ = df.event(&event);
977 }
978
979 #[test]
980 fn test_event_key_up() {
981 let mut df = CoreStatsDataFrame::new();
982 df.update_cores(make_test_cores());
983 df.layout(Rect::new(0.0, 0.0, 100.0, 10.0));
984
985 let event = Event::key_down(Key::Up);
986 let _ = df.event(&event);
987 }
988
989 #[test]
990 fn test_event_key_s() {
991 let mut df = CoreStatsDataFrame::new();
992 df.update_cores(make_test_cores());
993
994 let event = Event::key_down(Key::S);
995 let _ = df.event(&event);
996 assert_eq!(df.sort_column, CoreStatsSortColumn::Total);
998 }
999
1000 #[test]
1001 fn test_event_key_r() {
1002 let mut df = CoreStatsDataFrame::new();
1003 df.update_cores(make_test_cores());
1004
1005 let event = Event::key_down(Key::R);
1006 let _ = df.event(&event);
1007 assert!(df.sort_desc);
1009 }
1010
1011 #[test]
1012 fn test_measure() {
1013 let df = CoreStatsDataFrame::new();
1014 let constraints = Constraints {
1015 min_width: 0.0,
1016 max_width: 100.0,
1017 min_height: 0.0,
1018 max_height: 50.0,
1019 };
1020 let size = df.measure(constraints);
1021 assert!(size.width > 0.0);
1022 assert!(size.height > 0.0);
1023 }
1024
1025 #[test]
1026 fn test_brick_name() {
1027 let df = CoreStatsDataFrame::new();
1028 assert_eq!(df.brick_name(), "core_stats_dataframe");
1029 }
1030
1031 #[test]
1032 fn test_brick_verify() {
1033 let mut df = CoreStatsDataFrame::new();
1034 df.update_cores(make_test_cores());
1035 let v = df.verify();
1036 assert!(v.failed.is_empty());
1037 }
1038
1039 #[test]
1040 fn test_brick_html() {
1041 let df = CoreStatsDataFrame::new();
1042 let html = df.to_html();
1043 assert!(html.contains("core-stats-dataframe"));
1044 assert!(html.contains("CORE"));
1045 }
1046
1047 #[test]
1048 fn test_brick_css() {
1049 let df = CoreStatsDataFrame::new();
1050 let css = df.to_css();
1051 assert!(css.contains("core_id"));
1052 }
1053
1054 #[test]
1055 fn test_column_headers() {
1056 assert_eq!(CoreStatsSortColumn::CoreId.header(), "CORE");
1057 assert_eq!(CoreStatsSortColumn::Frequency.header(), "FREQ");
1058 assert_eq!(CoreStatsSortColumn::Temperature.header(), "TEMP");
1059 assert_eq!(CoreStatsSortColumn::User.header(), "USR%");
1060 assert_eq!(CoreStatsSortColumn::System.header(), "SYS%");
1061 assert_eq!(CoreStatsSortColumn::Total.header(), "TOT%");
1062 assert_eq!(CoreStatsSortColumn::Idle.header(), "IDL%");
1063 }
1064
1065 #[test]
1066 fn test_accent_color() {
1067 let color = Color::new(0.8, 0.2, 0.5, 1.0);
1068 let df = CoreStatsDataFrame::new().with_accent_color(color);
1069 assert!((df.accent_color.r - 0.8).abs() < 0.001);
1070 }
1071}