1use presentar_core::{
14 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
15 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
16};
17use std::any::Any;
18use std::time::Duration;
19
20fn color_for_cpu_percent(pct: f32) -> Color {
25 if pct > 50.0 {
26 Color {
27 r: 1.0,
28 g: 0.4,
29 b: 0.4,
30 a: 1.0,
31 } } else if pct > 10.0 {
33 Color {
34 r: 1.0,
35 g: 0.8,
36 b: 0.4,
37 a: 1.0,
38 } } else if pct > 1.0 {
40 Color {
41 r: 0.6,
42 g: 0.9,
43 b: 0.6,
44 a: 1.0,
45 } } else {
47 Color {
48 r: 0.5,
49 g: 0.5,
50 b: 0.6,
51 a: 1.0,
52 } }
54}
55
56fn color_for_status(level: HealthLevel) -> Color {
57 match level {
58 HealthLevel::Critical => Color {
59 r: 1.0,
60 g: 0.2,
61 b: 0.2,
62 a: 1.0,
63 },
64 HealthLevel::High => Color {
65 r: 1.0,
66 g: 0.5,
67 b: 0.3,
68 a: 1.0,
69 },
70 HealthLevel::Moderate => Color {
71 r: 1.0,
72 g: 0.8,
73 b: 0.4,
74 a: 1.0,
75 },
76 HealthLevel::Ok => Color {
77 r: 0.5,
78 g: 0.9,
79 b: 0.5,
80 a: 1.0,
81 },
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum HealthLevel {
88 Critical,
89 High,
90 Moderate,
91 Ok,
92}
93
94impl HealthLevel {
95 pub fn as_str(&self) -> &'static str {
96 match self {
97 Self::Critical => "CRITICAL",
98 Self::High => "HIGH",
99 Self::Moderate => "MODERATE",
100 Self::Ok => "OK",
101 }
102 }
103}
104
105#[derive(Debug, Clone)]
111pub struct CpuConsumer {
112 pub pid: u32,
113 pub cpu_percent: f32,
114 pub memory_bytes: u64,
115 pub name: String,
116}
117
118impl CpuConsumer {
119 pub fn new(pid: u32, cpu_percent: f32, memory_bytes: u64, name: impl Into<String>) -> Self {
120 Self {
121 pid,
122 cpu_percent,
123 memory_bytes,
124 name: name.into(),
125 }
126 }
127
128 fn memory_display(&self) -> String {
129 if self.memory_bytes > 1_073_741_824 {
130 format!("{:.1}G", self.memory_bytes as f64 / 1_073_741_824.0)
131 } else if self.memory_bytes > 1_048_576 {
132 format!("{:.0}M", self.memory_bytes as f64 / 1_048_576.0)
133 } else {
134 format!("{:.0}K", self.memory_bytes as f64 / 1024.0)
135 }
136 }
137}
138
139#[derive(Debug, Clone)]
142pub struct TopProcessesTable {
143 processes: Vec<CpuConsumer>,
145 total_cpu: f32,
147 max_display: usize,
149 bounds: Rect,
151}
152
153impl Default for TopProcessesTable {
154 fn default() -> Self {
155 Self::new(vec![], 0.0)
156 }
157}
158
159impl TopProcessesTable {
160 #[must_use]
162 pub fn new(mut processes: Vec<CpuConsumer>, total_cpu: f32) -> Self {
163 processes.sort_by(|a, b| {
165 b.cpu_percent
166 .partial_cmp(&a.cpu_percent)
167 .unwrap_or(std::cmp::Ordering::Equal)
168 });
169 Self {
170 processes,
171 total_cpu,
172 max_display: 10,
173 bounds: Rect::default(),
174 }
175 }
176
177 #[must_use]
179 pub fn with_max_display(mut self, max: usize) -> Self {
180 self.max_display = max;
181 self
182 }
183
184 pub fn set_processes(&mut self, mut processes: Vec<CpuConsumer>, total_cpu: f32) {
186 processes.sort_by(|a, b| {
187 b.cpu_percent
188 .partial_cmp(&a.cpu_percent)
189 .unwrap_or(std::cmp::Ordering::Equal)
190 });
191 self.processes = processes;
192 self.total_cpu = total_cpu;
193 }
194}
195
196impl Brick for TopProcessesTable {
197 fn brick_name(&self) -> &'static str {
198 "top_processes_table"
199 }
200 fn assertions(&self) -> &[BrickAssertion] {
201 static A: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
202 A
203 }
204 fn budget(&self) -> BrickBudget {
205 BrickBudget::uniform(16)
206 }
207 fn verify(&self) -> BrickVerification {
208 BrickVerification {
209 passed: self.assertions().to_vec(),
210 failed: vec![],
211 verification_time: Duration::from_micros(10),
212 }
213 }
214 fn to_html(&self) -> String {
215 String::new()
216 }
217 fn to_css(&self) -> String {
218 String::new()
219 }
220}
221
222impl Widget for TopProcessesTable {
223 fn type_id(&self) -> TypeId {
224 TypeId::of::<Self>()
225 }
226
227 fn measure(&self, constraints: Constraints) -> Size {
228 let height = (self.max_display + 3) as f32; constraints.constrain(Size::new(constraints.max_width, height))
230 }
231
232 fn layout(&mut self, bounds: Rect) -> LayoutResult {
233 self.bounds = bounds;
234 self.max_display = ((bounds.height - 3.0) as usize).max(3);
236 LayoutResult {
237 size: Size::new(bounds.width, bounds.height),
238 }
239 }
240
241 fn paint(&self, canvas: &mut dyn Canvas) {
242 let x = self.bounds.x;
243 let mut y = self.bounds.y;
244 let w = self.bounds.width as usize;
245
246 let header_style = TextStyle {
247 color: Color {
248 r: 0.6,
249 g: 0.8,
250 b: 1.0,
251 a: 1.0,
252 },
253 ..Default::default()
254 };
255 let dim_style = TextStyle {
256 color: Color {
257 r: 0.5,
258 g: 0.5,
259 b: 0.6,
260 a: 1.0,
261 },
262 ..Default::default()
263 };
264
265 let header = format!("TOP CPU CONSUMERS ({:.0}% total)", self.total_cpu);
267 canvas.draw_text(
268 &header[..header.len().min(w)],
269 Point::new(x, y),
270 &header_style,
271 );
272 y += 1.0;
273
274 let col_header = format!("{:>7} {:>6} {:>6} {:<}", "PID", "CPU%", "MEM", "COMMAND");
276 canvas.draw_text(
277 &col_header[..col_header.len().min(w)],
278 Point::new(x, y),
279 &dim_style,
280 );
281 y += 1.0;
282
283 let mut other_cpu = 0.0_f32;
285 let mut other_count = 0_usize;
286
287 for (i, proc) in self.processes.iter().enumerate() {
288 if i < self.max_display && y < self.bounds.y + self.bounds.height - 1.0 {
289 let color = color_for_cpu_percent(proc.cpu_percent);
290 let max_name = w.saturating_sub(22);
291 let name = if proc.name.len() > max_name {
292 format!("{}...", &proc.name[..max_name.saturating_sub(3)])
293 } else {
294 proc.name.clone()
295 };
296
297 let line = format!(
298 "{:>7} {:>5.1}% {:>6} {}",
299 proc.pid,
300 proc.cpu_percent,
301 proc.memory_display(),
302 name
303 );
304 canvas.draw_text(
305 &line[..line.len().min(w)],
306 Point::new(x, y),
307 &TextStyle {
308 color,
309 ..Default::default()
310 },
311 );
312 y += 1.0;
313 } else {
314 other_cpu += proc.cpu_percent;
315 other_count += 1;
316 }
317 }
318
319 if other_count > 0 && y < self.bounds.y + self.bounds.height {
321 let other_line = format!(" [{other_count} other processes totaling {other_cpu:.1}%]");
322 canvas.draw_text(
323 &other_line[..other_line.len().min(w)],
324 Point::new(x, y),
325 &dim_style,
326 );
327 }
328 }
329
330 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
331 None
332 }
333 fn children(&self) -> &[Box<dyn Widget>] {
334 &[]
335 }
336 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
337 &mut []
338 }
339}
340
341#[derive(Debug, Clone)]
347pub struct CoreUtilizationHistogram {
348 core_percentages: Vec<f64>,
350 bounds: Rect,
352}
353
354impl Default for CoreUtilizationHistogram {
355 fn default() -> Self {
356 Self::new(vec![])
357 }
358}
359
360impl CoreUtilizationHistogram {
361 #[must_use]
362 pub fn new(core_percentages: Vec<f64>) -> Self {
363 Self {
364 core_percentages,
365 bounds: Rect::default(),
366 }
367 }
368
369 pub fn set_percentages(&mut self, percentages: Vec<f64>) {
370 self.core_percentages = percentages;
371 }
372
373 fn bucket_counts(&self) -> (usize, usize, usize, usize, usize) {
374 let mut b100 = 0; let mut bhigh = 0; let mut bmed = 0; let mut blow = 0; let mut bidle = 0; for &pct in &self.core_percentages {
381 if pct >= 95.0 {
382 b100 += 1;
383 } else if pct >= 70.0 {
384 bhigh += 1;
385 } else if pct >= 30.0 {
386 bmed += 1;
387 } else if pct >= 1.0 {
388 blow += 1;
389 } else {
390 bidle += 1;
391 }
392 }
393 (b100, bhigh, bmed, blow, bidle)
394 }
395}
396
397impl Brick for CoreUtilizationHistogram {
398 fn brick_name(&self) -> &'static str {
399 "core_utilization_histogram"
400 }
401 fn assertions(&self) -> &[BrickAssertion] {
402 static A: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
403 A
404 }
405 fn budget(&self) -> BrickBudget {
406 BrickBudget::uniform(16)
407 }
408 fn verify(&self) -> BrickVerification {
409 BrickVerification {
410 passed: self.assertions().to_vec(),
411 failed: vec![],
412 verification_time: Duration::from_micros(10),
413 }
414 }
415 fn to_html(&self) -> String {
416 String::new()
417 }
418 fn to_css(&self) -> String {
419 String::new()
420 }
421}
422
423impl Widget for CoreUtilizationHistogram {
424 fn type_id(&self) -> TypeId {
425 TypeId::of::<Self>()
426 }
427
428 fn measure(&self, constraints: Constraints) -> Size {
429 constraints.constrain(Size::new(constraints.max_width, 6.0)) }
431
432 fn layout(&mut self, bounds: Rect) -> LayoutResult {
433 self.bounds = bounds;
434 LayoutResult {
435 size: Size::new(bounds.width, bounds.height),
436 }
437 }
438
439 fn paint(&self, canvas: &mut dyn Canvas) {
440 let x = self.bounds.x;
441 let mut y = self.bounds.y;
442 let w = self.bounds.width as usize;
443
444 let header_style = TextStyle {
445 color: Color {
446 r: 0.6,
447 g: 0.8,
448 b: 1.0,
449 a: 1.0,
450 },
451 ..Default::default()
452 };
453 let dim_style = TextStyle {
454 color: Color {
455 r: 0.5,
456 g: 0.5,
457 b: 0.6,
458 a: 1.0,
459 },
460 ..Default::default()
461 };
462 let bright_style = TextStyle {
463 color: Color {
464 r: 1.0,
465 g: 1.0,
466 b: 1.0,
467 a: 1.0,
468 },
469 ..Default::default()
470 };
471
472 canvas.draw_text("CORE UTILIZATION", Point::new(x, y), &header_style);
473 y += 1.0;
474
475 let (b100, bhigh, bmed, blow, bidle) = self.bucket_counts();
476 let total = self.core_percentages.len();
477 let bar_max = w.saturating_sub(20);
478
479 let draw_bar =
480 |canvas: &mut dyn Canvas, y: f32, label: &str, count: usize, color: Color| {
481 if count == 0 {
482 return;
483 }
484 let bar_w = if total > 0 {
485 (count * bar_max) / total
486 } else {
487 0
488 };
489 let bar: String = "█".repeat(bar_w);
490 let pad: String = "░".repeat(bar_max - bar_w);
491
492 canvas.draw_text(
493 label,
494 Point::new(x, y),
495 &TextStyle {
496 color,
497 ..Default::default()
498 },
499 );
500 canvas.draw_text(
501 &bar,
502 Point::new(x + 10.0, y),
503 &TextStyle {
504 color,
505 ..Default::default()
506 },
507 );
508 canvas.draw_text(&pad, Point::new(x + 10.0 + bar_w as f32, y), &dim_style);
509 canvas.draw_text(
510 &format!(" x{count}"),
511 Point::new(x + 10.0 + bar_max as f32, y),
512 &bright_style,
513 );
514 };
515
516 if b100 > 0 {
517 draw_bar(
518 canvas,
519 y,
520 " 100%",
521 b100,
522 Color {
523 r: 1.0,
524 g: 0.3,
525 b: 0.3,
526 a: 1.0,
527 },
528 );
529 y += 1.0;
530 }
531 if bhigh > 0 {
532 draw_bar(
533 canvas,
534 y,
535 " 70-95%",
536 bhigh,
537 Color {
538 r: 1.0,
539 g: 0.6,
540 b: 0.3,
541 a: 1.0,
542 },
543 );
544 y += 1.0;
545 }
546 if bmed > 0 {
547 draw_bar(
548 canvas,
549 y,
550 " 30-70%",
551 bmed,
552 Color {
553 r: 1.0,
554 g: 1.0,
555 b: 0.4,
556 a: 1.0,
557 },
558 );
559 y += 1.0;
560 }
561 if blow > 0 {
562 draw_bar(
563 canvas,
564 y,
565 " 1-30%",
566 blow,
567 Color {
568 r: 0.5,
569 g: 0.9,
570 b: 0.5,
571 a: 1.0,
572 },
573 );
574 y += 1.0;
575 }
576 if bidle > 0 {
577 draw_bar(
578 canvas,
579 y,
580 " idle",
581 bidle,
582 Color {
583 r: 0.4,
584 g: 0.4,
585 b: 0.5,
586 a: 1.0,
587 },
588 );
589 }
590 }
591
592 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
593 None
594 }
595 fn children(&self) -> &[Box<dyn Widget>] {
596 &[]
597 }
598 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
599 &mut []
600 }
601}
602
603#[derive(Debug, Clone)]
609pub struct TrendSparkline {
610 history: Vec<f64>,
612 title: String,
614 is_percentage: bool,
616 bounds: Rect,
618}
619
620impl Default for TrendSparkline {
621 fn default() -> Self {
622 Self::new("TREND", vec![])
623 }
624}
625
626impl TrendSparkline {
627 #[must_use]
628 pub fn new(title: impl Into<String>, history: Vec<f64>) -> Self {
629 Self {
630 history,
631 title: title.into(),
632 is_percentage: true,
633 bounds: Rect::default(),
634 }
635 }
636
637 #[must_use]
639 pub fn normalized(mut self) -> Self {
640 self.is_percentage = false;
641 self
642 }
643
644 pub fn set_history(&mut self, history: Vec<f64>) {
645 self.history = history;
646 }
647
648 pub fn push(&mut self, value: f64) {
649 self.history.push(value);
650 if self.history.len() > 120 {
651 self.history.remove(0);
652 }
653 }
654
655 fn stats(&self) -> (f64, f64, f64, f64) {
656 if self.history.is_empty() {
657 return (0.0, 0.0, 0.0, 0.0);
658 }
659 let mult = if self.is_percentage { 1.0 } else { 100.0 };
660 let current = self.history.last().copied().unwrap_or(0.0) * mult;
661 let min = self.history.iter().copied().fold(f64::MAX, f64::min) * mult;
662 let max = self.history.iter().copied().fold(f64::MIN, f64::max) * mult;
663 let avg = self.history.iter().sum::<f64>() / self.history.len() as f64 * mult;
664 (current, min, max, avg)
665 }
666}
667
668impl Brick for TrendSparkline {
669 fn brick_name(&self) -> &'static str {
670 "trend_sparkline"
671 }
672 fn assertions(&self) -> &[BrickAssertion] {
673 static A: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
674 A
675 }
676 fn budget(&self) -> BrickBudget {
677 BrickBudget::uniform(16)
678 }
679 fn verify(&self) -> BrickVerification {
680 BrickVerification {
681 passed: self.assertions().to_vec(),
682 failed: vec![],
683 verification_time: Duration::from_micros(10),
684 }
685 }
686 fn to_html(&self) -> String {
687 String::new()
688 }
689 fn to_css(&self) -> String {
690 String::new()
691 }
692}
693
694impl Widget for TrendSparkline {
695 fn type_id(&self) -> TypeId {
696 TypeId::of::<Self>()
697 }
698
699 fn measure(&self, constraints: Constraints) -> Size {
700 constraints.constrain(Size::new(constraints.max_width, 4.0)) }
702
703 fn layout(&mut self, bounds: Rect) -> LayoutResult {
704 self.bounds = bounds;
705 LayoutResult {
706 size: Size::new(bounds.width, bounds.height),
707 }
708 }
709
710 fn paint(&self, canvas: &mut dyn Canvas) {
711 let x = self.bounds.x;
712 let mut y = self.bounds.y;
713 let w = self.bounds.width as usize;
714
715 let header_style = TextStyle {
716 color: Color {
717 r: 0.6,
718 g: 0.8,
719 b: 1.0,
720 a: 1.0,
721 },
722 ..Default::default()
723 };
724 let dim_style = TextStyle {
725 color: Color {
726 r: 0.5,
727 g: 0.5,
728 b: 0.6,
729 a: 1.0,
730 },
731 ..Default::default()
732 };
733 let bright_style = TextStyle {
734 color: Color {
735 r: 1.0,
736 g: 1.0,
737 b: 1.0,
738 a: 1.0,
739 },
740 ..Default::default()
741 };
742
743 canvas.draw_text(&self.title, Point::new(x, y), &header_style);
744 y += 1.0;
745
746 let (current, min, max, avg) = self.stats();
747
748 let trend_color = if current > 80.0 {
750 Color {
751 r: 1.0,
752 g: 0.4,
753 b: 0.4,
754 a: 1.0,
755 }
756 } else if current > 50.0 {
757 Color {
758 r: 1.0,
759 g: 0.8,
760 b: 0.4,
761 a: 1.0,
762 }
763 } else {
764 Color {
765 r: 0.5,
766 g: 0.9,
767 b: 0.5,
768 a: 1.0,
769 }
770 };
771
772 let mult = if self.is_percentage { 1.0 } else { 100.0 };
774 let chars: String = self
775 .history
776 .iter()
777 .rev()
778 .take(w)
779 .rev()
780 .map(|&v| {
781 let pct = (v * mult).clamp(0.0, 100.0);
782 let idx = ((pct / 100.0) * 7.0).round() as usize;
783 ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'][idx.min(7)]
784 })
785 .collect();
786
787 canvas.draw_text(
788 &chars,
789 Point::new(x, y),
790 &TextStyle {
791 color: trend_color,
792 ..Default::default()
793 },
794 );
795 y += 1.0;
796
797 let now_avg = format!("Now: {current:.0}% Avg: {avg:.0}%");
799 canvas.draw_text(
800 &now_avg[..now_avg.len().min(w)],
801 Point::new(x, y),
802 &bright_style,
803 );
804 y += 1.0;
805
806 let min_max = format!("Min: {min:.0}% Max: {max:.0}%");
807 canvas.draw_text(
808 &min_max[..min_max.len().min(w)],
809 Point::new(x, y),
810 &dim_style,
811 );
812 }
813
814 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
815 None
816 }
817 fn children(&self) -> &[Box<dyn Widget>] {
818 &[]
819 }
820 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
821 &mut []
822 }
823}
824
825#[derive(Debug, Clone)]
831pub struct SystemStatus {
832 load_1m: f64,
834 load_5m: f64,
835 load_15m: f64,
836 core_count: usize,
838 thermal: Option<(f64, f64)>,
840 bounds: Rect,
842}
843
844impl Default for SystemStatus {
845 fn default() -> Self {
846 Self::new(0.0, 0.0, 0.0, 1)
847 }
848}
849
850impl SystemStatus {
851 #[must_use]
852 pub fn new(load_1m: f64, load_5m: f64, load_15m: f64, core_count: usize) -> Self {
853 Self {
854 load_1m,
855 load_5m,
856 load_15m,
857 core_count: core_count.max(1),
858 thermal: None,
859 bounds: Rect::default(),
860 }
861 }
862
863 #[must_use]
864 pub fn with_thermal(mut self, avg_temp: f64, max_temp: f64) -> Self {
865 self.thermal = Some((avg_temp, max_temp));
866 self
867 }
868
869 pub fn set_load(&mut self, l1: f64, l5: f64, l15: f64) {
870 self.load_1m = l1;
871 self.load_5m = l5;
872 self.load_15m = l15;
873 }
874
875 pub fn set_thermal(&mut self, avg: f64, max: f64) {
876 self.thermal = Some((avg, max));
877 }
878
879 pub fn load_status(&self) -> HealthLevel {
881 let per_core = self.load_1m / self.core_count as f64;
882 if per_core > 1.5 {
883 HealthLevel::Critical
884 } else if per_core > 1.0 {
885 HealthLevel::High
886 } else if per_core > 0.7 {
887 HealthLevel::Moderate
888 } else {
889 HealthLevel::Ok
890 }
891 }
892
893 pub fn thermal_status(&self) -> Option<HealthLevel> {
895 self.thermal.map(|(_, max)| {
896 if max > 90.0 {
897 HealthLevel::Critical
898 } else if max > 80.0 {
899 HealthLevel::High
900 } else if max > 70.0 {
901 HealthLevel::Moderate
902 } else {
903 HealthLevel::Ok
904 }
905 })
906 }
907}
908
909impl Brick for SystemStatus {
910 fn brick_name(&self) -> &'static str {
911 "system_status"
912 }
913 fn assertions(&self) -> &[BrickAssertion] {
914 static A: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
915 A
916 }
917 fn budget(&self) -> BrickBudget {
918 BrickBudget::uniform(16)
919 }
920 fn verify(&self) -> BrickVerification {
921 BrickVerification {
922 passed: self.assertions().to_vec(),
923 failed: vec![],
924 verification_time: Duration::from_micros(10),
925 }
926 }
927 fn to_html(&self) -> String {
928 String::new()
929 }
930 fn to_css(&self) -> String {
931 String::new()
932 }
933}
934
935impl Widget for SystemStatus {
936 fn type_id(&self) -> TypeId {
937 TypeId::of::<Self>()
938 }
939
940 fn measure(&self, constraints: Constraints) -> Size {
941 let height = if self.thermal.is_some() { 2.0 } else { 1.0 };
942 constraints.constrain(Size::new(constraints.max_width, height))
943 }
944
945 fn layout(&mut self, bounds: Rect) -> LayoutResult {
946 self.bounds = bounds;
947 LayoutResult {
948 size: Size::new(bounds.width, bounds.height),
949 }
950 }
951
952 fn paint(&self, canvas: &mut dyn Canvas) {
953 let x = self.bounds.x;
954 let mut y = self.bounds.y;
955 let w = self.bounds.width as usize;
956
957 let load_stat = self.load_status();
959 let per_core = self.load_1m / self.core_count as f64;
960 let load_line = format!(
961 "LOAD: {:.2} / {:.2} / {:.2} ({:.2}/core) - {}",
962 self.load_1m,
963 self.load_5m,
964 self.load_15m,
965 per_core,
966 load_stat.as_str()
967 );
968 canvas.draw_text(
969 &load_line[..load_line.len().min(w)],
970 Point::new(x, y),
971 &TextStyle {
972 color: color_for_status(load_stat),
973 ..Default::default()
974 },
975 );
976
977 if let Some((avg, max)) = self.thermal {
979 y += 1.0;
980 let therm_stat = self.thermal_status().unwrap_or(HealthLevel::Ok);
981 let therm_line = format!(
982 "THERMAL: {:.0}°C avg, {:.0}°C max - {}",
983 avg,
984 max,
985 therm_stat.as_str()
986 );
987 canvas.draw_text(
988 &therm_line[..therm_line.len().min(w)],
989 Point::new(x, y),
990 &TextStyle {
991 color: color_for_status(therm_stat),
992 ..Default::default()
993 },
994 );
995 }
996 }
997
998 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
999 None
1000 }
1001 fn children(&self) -> &[Box<dyn Widget>] {
1002 &[]
1003 }
1004 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
1005 &mut []
1006 }
1007}
1008
1009#[cfg(test)]
1010mod tests {
1011 use super::*;
1012 use crate::direct::{CellBuffer, DirectTerminalCanvas};
1013
1014 #[test]
1016 fn test_health_level_as_str() {
1017 assert_eq!(HealthLevel::Critical.as_str(), "CRITICAL");
1018 assert_eq!(HealthLevel::High.as_str(), "HIGH");
1019 assert_eq!(HealthLevel::Moderate.as_str(), "MODERATE");
1020 assert_eq!(HealthLevel::Ok.as_str(), "OK");
1021 }
1022
1023 #[test]
1025 fn test_color_for_cpu_percent_high() {
1026 let color = color_for_cpu_percent(75.0);
1027 assert!(color.r > 0.9); }
1029
1030 #[test]
1031 fn test_color_for_cpu_percent_medium() {
1032 let color = color_for_cpu_percent(30.0);
1033 assert!(color.r > 0.9 && color.g > 0.7); }
1035
1036 #[test]
1037 fn test_color_for_cpu_percent_low() {
1038 let color = color_for_cpu_percent(5.0);
1039 assert!(color.g > 0.8); }
1041
1042 #[test]
1043 fn test_color_for_cpu_percent_idle() {
1044 let color = color_for_cpu_percent(0.5);
1045 assert!(color.r < 0.6 && color.g < 0.6); }
1047
1048 #[test]
1049 fn test_color_for_status() {
1050 let critical = color_for_status(HealthLevel::Critical);
1051 assert!(critical.r > 0.9 && critical.g < 0.3);
1052
1053 let high = color_for_status(HealthLevel::High);
1054 assert!(high.r > 0.9);
1055
1056 let moderate = color_for_status(HealthLevel::Moderate);
1057 assert!(moderate.r > 0.9 && moderate.g > 0.7);
1058
1059 let ok = color_for_status(HealthLevel::Ok);
1060 assert!(ok.g > 0.8);
1061 }
1062
1063 #[test]
1065 fn test_cpu_consumer_new() {
1066 let proc = CpuConsumer::new(123, 45.5, 1_000_000_000, "firefox");
1067 assert_eq!(proc.pid, 123);
1068 assert!((proc.cpu_percent - 45.5).abs() < 0.01);
1069 assert_eq!(proc.memory_bytes, 1_000_000_000);
1070 assert_eq!(proc.name, "firefox");
1071 }
1072
1073 #[test]
1074 fn test_cpu_consumer_memory_display_gb() {
1075 let proc = CpuConsumer::new(1, 10.0, 2_000_000_000, "test");
1076 let display = proc.memory_display();
1077 assert!(display.contains('G'));
1078 }
1079
1080 #[test]
1081 fn test_cpu_consumer_memory_display_mb() {
1082 let proc = CpuConsumer::new(1, 10.0, 500_000_000, "test");
1083 let display = proc.memory_display();
1084 assert!(display.contains('M'));
1085 }
1086
1087 #[test]
1088 fn test_cpu_consumer_memory_display_kb() {
1089 let proc = CpuConsumer::new(1, 10.0, 500_000, "test");
1090 let display = proc.memory_display();
1091 assert!(display.contains('K'));
1092 }
1093
1094 #[test]
1096 fn test_top_processes_table() {
1097 let procs = vec![
1098 CpuConsumer::new(123, 45.0, 1_000_000_000, "firefox"),
1099 CpuConsumer::new(456, 23.0, 500_000_000, "chrome"),
1100 ];
1101 let table = TopProcessesTable::new(procs, 68.0);
1102 assert!(table.verify().is_valid());
1103 }
1104
1105 #[test]
1106 fn test_top_processes_table_default() {
1107 let table = TopProcessesTable::default();
1108 assert!(table.processes.is_empty());
1109 }
1110
1111 #[test]
1112 fn test_top_processes_table_with_max_display() {
1113 let table = TopProcessesTable::new(vec![], 0.0).with_max_display(5);
1114 assert_eq!(table.max_display, 5);
1115 }
1116
1117 #[test]
1118 fn test_top_processes_table_set_processes() {
1119 let mut table = TopProcessesTable::new(vec![], 0.0);
1120 table.set_processes(
1121 vec![
1122 CpuConsumer::new(1, 20.0, 100, "a"),
1123 CpuConsumer::new(2, 50.0, 200, "b"),
1124 ],
1125 70.0,
1126 );
1127 assert_eq!(table.processes.len(), 2);
1128 assert_eq!(table.total_cpu, 70.0);
1129 assert_eq!(table.processes[0].pid, 2);
1131 }
1132
1133 #[test]
1134 fn test_top_processes_table_sorts_by_cpu() {
1135 let procs = vec![
1136 CpuConsumer::new(1, 10.0, 100, "low"),
1137 CpuConsumer::new(2, 90.0, 100, "high"),
1138 CpuConsumer::new(3, 50.0, 100, "mid"),
1139 ];
1140 let table = TopProcessesTable::new(procs, 150.0);
1141 assert_eq!(table.processes[0].pid, 2); assert_eq!(table.processes[1].pid, 3); assert_eq!(table.processes[2].pid, 1); }
1145
1146 #[test]
1147 fn test_top_processes_table_brick_name() {
1148 let table = TopProcessesTable::default();
1149 assert_eq!(table.brick_name(), "top_processes_table");
1150 }
1151
1152 #[test]
1153 fn test_top_processes_table_measure() {
1154 let table = TopProcessesTable::default();
1155 let constraints = Constraints::tight(Size::new(80.0, 40.0));
1156 let size = table.measure(constraints);
1157 assert!(size.height > 0.0);
1158 }
1159
1160 #[test]
1161 fn test_top_processes_table_layout() {
1162 let mut table = TopProcessesTable::default();
1163 let bounds = Rect::new(0.0, 0.0, 80.0, 20.0);
1164 let result = table.layout(bounds);
1165 assert_eq!(result.size.width, 80.0);
1166 assert_eq!(result.size.height, 20.0);
1167 }
1168
1169 #[test]
1170 fn test_top_processes_table_paint() {
1171 let mut table =
1172 TopProcessesTable::new(vec![CpuConsumer::new(1, 50.0, 1_000_000_000, "test")], 50.0);
1173 table.bounds = Rect::new(0.0, 0.0, 80.0, 20.0);
1174 let mut buffer = CellBuffer::new(80, 20);
1175 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1176 table.paint(&mut canvas);
1177 }
1178
1179 #[test]
1180 fn test_top_processes_table_children() {
1181 let table = TopProcessesTable::default();
1182 assert!(table.children().is_empty());
1183 }
1184
1185 #[test]
1187 fn test_core_histogram() {
1188 let hist = CoreUtilizationHistogram::new(vec![95.0, 96.0, 50.0, 10.0, 0.5]);
1189 let (b100, _bhigh, bmed, blow, bidle) = hist.bucket_counts();
1190 assert_eq!(b100, 2);
1191 assert_eq!(bmed, 1);
1192 assert_eq!(blow, 1);
1193 assert_eq!(bidle, 1);
1194 }
1195
1196 #[test]
1197 fn test_core_histogram_default() {
1198 let hist = CoreUtilizationHistogram::default();
1199 assert!(hist.core_percentages.is_empty());
1200 }
1201
1202 #[test]
1203 fn test_core_histogram_bucket_counts_empty() {
1204 let hist = CoreUtilizationHistogram::new(vec![]);
1205 let (b100, bhigh, bmed, blow, bidle) = hist.bucket_counts();
1206 assert_eq!(b100 + bhigh + bmed + blow + bidle, 0);
1207 }
1208
1209 #[test]
1210 fn test_core_histogram_all_buckets() {
1211 let hist = CoreUtilizationHistogram::new(vec![
1214 97.0, 80.0, 50.0, 15.0, 0.5, ]);
1220 let (b100, bhigh, bmed, blow, bidle) = hist.bucket_counts();
1221 assert_eq!(b100, 1);
1222 assert_eq!(bhigh, 1);
1223 assert_eq!(bmed, 1);
1224 assert_eq!(blow, 1);
1225 assert_eq!(bidle, 1);
1226 }
1227
1228 #[test]
1229 fn test_core_histogram_brick_name() {
1230 let hist = CoreUtilizationHistogram::default();
1231 assert_eq!(hist.brick_name(), "core_utilization_histogram");
1232 }
1233
1234 #[test]
1235 fn test_core_histogram_measure() {
1236 let hist = CoreUtilizationHistogram::new(vec![50.0; 8]);
1237 let constraints = Constraints::tight(Size::new(40.0, 10.0));
1238 let size = hist.measure(constraints);
1239 assert!(size.width > 0.0);
1240 }
1241
1242 #[test]
1243 fn test_core_histogram_layout() {
1244 let mut hist = CoreUtilizationHistogram::new(vec![50.0; 4]);
1245 let bounds = Rect::new(0.0, 0.0, 40.0, 8.0);
1246 let result = hist.layout(bounds);
1247 assert_eq!(result.size.width, 40.0);
1248 }
1249
1250 #[test]
1251 fn test_core_histogram_paint() {
1252 let mut hist = CoreUtilizationHistogram::new(vec![50.0, 75.0, 25.0, 90.0]);
1253 hist.bounds = Rect::new(0.0, 0.0, 40.0, 8.0);
1254 let mut buffer = CellBuffer::new(40, 8);
1255 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1256 hist.paint(&mut canvas);
1257 }
1258
1259 #[test]
1260 fn test_core_histogram_children() {
1261 let hist = CoreUtilizationHistogram::default();
1262 assert!(hist.children().is_empty());
1263 }
1264
1265 #[test]
1267 fn test_trend_sparkline() {
1268 let mut trend = TrendSparkline::new("CPU", vec![50.0, 60.0, 70.0]);
1269 trend.push(80.0);
1270 let (current, min, max, _avg) = trend.stats();
1271 assert!((current - 80.0).abs() < 0.01);
1272 assert!((min - 50.0).abs() < 0.01);
1273 assert!((max - 80.0).abs() < 0.01);
1274 }
1275
1276 #[test]
1277 fn test_trend_sparkline_default() {
1278 let trend = TrendSparkline::default();
1279 assert!(trend.history.is_empty());
1280 }
1281
1282 #[test]
1283 fn test_trend_sparkline_empty_stats() {
1284 let trend = TrendSparkline::new("Empty", vec![]);
1285 let (current, min, max, avg) = trend.stats();
1286 assert_eq!(current, 0.0);
1287 assert_eq!(min, 0.0);
1288 assert_eq!(max, 0.0);
1289 assert_eq!(avg, 0.0);
1290 }
1291
1292 #[test]
1293 fn test_trend_sparkline_single_value() {
1294 let trend = TrendSparkline::new("Single", vec![42.0]);
1295 let (current, min, max, avg) = trend.stats();
1296 assert_eq!(current, 42.0);
1297 assert_eq!(min, 42.0);
1298 assert_eq!(max, 42.0);
1299 assert_eq!(avg, 42.0);
1300 }
1301
1302 #[test]
1303 fn test_trend_sparkline_push_overflow() {
1304 let mut trend = TrendSparkline::new("Test", vec![1.0; 120]);
1305 for i in 0..10 {
1306 trend.push(i as f64);
1307 }
1308 assert!(trend.history.len() <= 120);
1310 }
1311
1312 #[test]
1313 fn test_trend_sparkline_brick_name() {
1314 let trend = TrendSparkline::default();
1315 assert_eq!(trend.brick_name(), "trend_sparkline");
1316 }
1317
1318 #[test]
1319 fn test_trend_sparkline_measure() {
1320 let trend = TrendSparkline::new("Test", vec![50.0; 10]);
1321 let constraints = Constraints::tight(Size::new(60.0, 5.0));
1322 let size = trend.measure(constraints);
1323 assert!(size.width > 0.0);
1324 }
1325
1326 #[test]
1327 fn test_trend_sparkline_layout() {
1328 let mut trend = TrendSparkline::new("Test", vec![50.0; 10]);
1329 let bounds = Rect::new(0.0, 0.0, 60.0, 5.0);
1330 let result = trend.layout(bounds);
1331 assert_eq!(result.size.width, 60.0);
1332 }
1333
1334 #[test]
1335 fn test_trend_sparkline_paint() {
1336 let mut trend = TrendSparkline::new("CPU", vec![30.0, 50.0, 70.0, 90.0]);
1337 trend.bounds = Rect::new(0.0, 0.0, 60.0, 5.0);
1338 let mut buffer = CellBuffer::new(60, 5);
1339 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1340 trend.paint(&mut canvas);
1341 }
1342
1343 #[test]
1344 fn test_trend_sparkline_children() {
1345 let trend = TrendSparkline::default();
1346 assert!(trend.children().is_empty());
1347 }
1348
1349 #[test]
1351 fn test_system_status() {
1352 let status = SystemStatus::new(4.0, 3.0, 2.0, 8).with_thermal(65.0, 72.0);
1353 assert_eq!(status.load_status(), HealthLevel::Ok); assert_eq!(status.thermal_status(), Some(HealthLevel::Moderate)); }
1356
1357 #[test]
1358 fn test_system_status_default() {
1359 let status = SystemStatus::default();
1360 assert!((status.load_1m - 0.0).abs() < 0.01);
1361 assert_eq!(status.core_count, 1);
1362 }
1363
1364 #[test]
1365 fn test_system_status_load_critical() {
1366 let status = SystemStatus::new(8.0, 6.0, 4.0, 4);
1368 assert_eq!(status.load_status(), HealthLevel::Critical);
1369 }
1370
1371 #[test]
1372 fn test_system_status_load_high() {
1373 let status = SystemStatus::new(5.0, 4.0, 3.0, 4);
1375 assert_eq!(status.load_status(), HealthLevel::High);
1376 }
1377
1378 #[test]
1379 fn test_system_status_load_moderate() {
1380 let status = SystemStatus::new(3.2, 3.0, 2.8, 4);
1382 assert_eq!(status.load_status(), HealthLevel::Moderate);
1383 }
1384
1385 #[test]
1386 fn test_system_status_thermal_critical() {
1387 let status = SystemStatus::new(1.0, 1.0, 1.0, 4).with_thermal(95.0, 100.0);
1388 assert_eq!(status.thermal_status(), Some(HealthLevel::Critical));
1389 }
1390
1391 #[test]
1392 fn test_system_status_thermal_high() {
1393 let status = SystemStatus::new(1.0, 1.0, 1.0, 4).with_thermal(80.0, 85.0);
1394 assert_eq!(status.thermal_status(), Some(HealthLevel::High));
1395 }
1396
1397 #[test]
1398 fn test_system_status_thermal_ok() {
1399 let status = SystemStatus::new(1.0, 1.0, 1.0, 4).with_thermal(50.0, 55.0);
1400 assert_eq!(status.thermal_status(), Some(HealthLevel::Ok));
1401 }
1402
1403 #[test]
1404 fn test_system_status_no_thermal() {
1405 let status = SystemStatus::new(1.0, 1.0, 1.0, 4);
1406 assert_eq!(status.thermal_status(), None);
1407 }
1408
1409 #[test]
1410 fn test_system_status_brick_name() {
1411 let status = SystemStatus::default();
1412 assert_eq!(status.brick_name(), "system_status");
1413 }
1414
1415 #[test]
1416 fn test_system_status_measure() {
1417 let status = SystemStatus::new(1.0, 1.0, 1.0, 4);
1418 let constraints = Constraints::tight(Size::new(80.0, 10.0));
1419 let size = status.measure(constraints);
1420 assert!(size.width > 0.0);
1421 }
1422
1423 #[test]
1424 fn test_system_status_layout() {
1425 let mut status = SystemStatus::new(1.0, 1.0, 1.0, 4);
1426 let bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1427 let result = status.layout(bounds);
1428 assert_eq!(result.size.width, 80.0);
1429 }
1430
1431 #[test]
1432 fn test_system_status_paint() {
1433 let mut status = SystemStatus::new(2.0, 1.5, 1.0, 4).with_thermal(60.0, 65.0);
1434 status.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1435 let mut buffer = CellBuffer::new(80, 10);
1436 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1437 status.paint(&mut canvas);
1438 }
1439
1440 #[test]
1441 fn test_system_status_children() {
1442 let status = SystemStatus::default();
1443 assert!(status.children().is_empty());
1444 }
1445
1446 #[test]
1452 fn test_top_processes_table_type_id() {
1453 let table = TopProcessesTable::default();
1454 let id = Widget::type_id(&table);
1455 assert_eq!(id, TypeId::of::<TopProcessesTable>());
1456 }
1457
1458 #[test]
1459 fn test_top_processes_table_event() {
1460 let mut table = TopProcessesTable::default();
1461 let result = table.event(&Event::FocusIn);
1462 assert!(result.is_none());
1463 }
1464
1465 #[test]
1466 fn test_top_processes_table_children_mut() {
1467 let mut table = TopProcessesTable::default();
1468 assert!(table.children_mut().is_empty());
1469 }
1470
1471 #[test]
1472 fn test_top_processes_table_assertions() {
1473 let table = TopProcessesTable::default();
1474 assert!(!table.assertions().is_empty());
1475 }
1476
1477 #[test]
1478 fn test_top_processes_table_budget() {
1479 let table = TopProcessesTable::default();
1480 let budget = table.budget();
1481 assert!(budget.total_ms > 0);
1482 }
1483
1484 #[test]
1485 fn test_top_processes_table_to_html() {
1486 let table = TopProcessesTable::default();
1487 assert!(table.to_html().is_empty());
1488 }
1489
1490 #[test]
1491 fn test_top_processes_table_to_css() {
1492 let table = TopProcessesTable::default();
1493 assert!(table.to_css().is_empty());
1494 }
1495
1496 #[test]
1497 fn test_top_processes_table_paint_many_processes() {
1498 let procs: Vec<CpuConsumer> = (0..20)
1500 .map(|i| CpuConsumer::new(i, i as f32 * 5.0, 1_000_000 * i as u64, format!("proc{i}")))
1501 .collect();
1502 let mut table = TopProcessesTable::new(procs, 190.0).with_max_display(5);
1503 table.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1504 let mut buffer = CellBuffer::new(80, 10);
1505 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1506 table.paint(&mut canvas);
1507 }
1508
1509 #[test]
1510 fn test_top_processes_table_paint_long_name() {
1511 let proc = CpuConsumer::new(
1512 123,
1513 50.0,
1514 1_000_000_000,
1515 "this_is_a_very_long_process_name_that_should_be_truncated",
1516 );
1517 let mut table = TopProcessesTable::new(vec![proc], 50.0);
1518 table.bounds = Rect::new(0.0, 0.0, 50.0, 10.0);
1519 let mut buffer = CellBuffer::new(50, 10);
1520 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1521 table.paint(&mut canvas);
1522 }
1523
1524 #[test]
1525 fn test_top_processes_table_clone() {
1526 let table = TopProcessesTable::new(vec![CpuConsumer::new(1, 10.0, 100, "test")], 10.0);
1527 let cloned = table;
1528 assert_eq!(cloned.processes.len(), 1);
1529 }
1530
1531 #[test]
1532 fn test_top_processes_table_debug() {
1533 let table = TopProcessesTable::default();
1534 let debug = format!("{:?}", table);
1535 assert!(debug.contains("TopProcessesTable"));
1536 }
1537
1538 #[test]
1540 fn test_core_histogram_type_id() {
1541 let hist = CoreUtilizationHistogram::default();
1542 let id = Widget::type_id(&hist);
1543 assert_eq!(id, TypeId::of::<CoreUtilizationHistogram>());
1544 }
1545
1546 #[test]
1547 fn test_core_histogram_event() {
1548 let mut hist = CoreUtilizationHistogram::default();
1549 let result = hist.event(&Event::FocusIn);
1550 assert!(result.is_none());
1551 }
1552
1553 #[test]
1554 fn test_core_histogram_children_mut() {
1555 let mut hist = CoreUtilizationHistogram::default();
1556 assert!(hist.children_mut().is_empty());
1557 }
1558
1559 #[test]
1560 fn test_core_histogram_set_percentages() {
1561 let mut hist = CoreUtilizationHistogram::default();
1562 hist.set_percentages(vec![25.0, 50.0, 75.0]);
1563 assert_eq!(hist.core_percentages.len(), 3);
1564 }
1565
1566 #[test]
1567 fn test_core_histogram_assertions() {
1568 let hist = CoreUtilizationHistogram::default();
1569 assert!(!hist.assertions().is_empty());
1570 }
1571
1572 #[test]
1573 fn test_core_histogram_budget() {
1574 let hist = CoreUtilizationHistogram::default();
1575 let budget = hist.budget();
1576 assert!(budget.total_ms > 0);
1577 }
1578
1579 #[test]
1580 fn test_core_histogram_to_html() {
1581 let hist = CoreUtilizationHistogram::default();
1582 assert!(hist.to_html().is_empty());
1583 }
1584
1585 #[test]
1586 fn test_core_histogram_to_css() {
1587 let hist = CoreUtilizationHistogram::default();
1588 assert!(hist.to_css().is_empty());
1589 }
1590
1591 #[test]
1592 fn test_core_histogram_clone() {
1593 let hist = CoreUtilizationHistogram::new(vec![50.0, 60.0]);
1594 let cloned = hist;
1595 assert_eq!(cloned.core_percentages.len(), 2);
1596 }
1597
1598 #[test]
1599 fn test_core_histogram_debug() {
1600 let hist = CoreUtilizationHistogram::default();
1601 let debug = format!("{:?}", hist);
1602 assert!(debug.contains("CoreUtilizationHistogram"));
1603 }
1604
1605 #[test]
1606 fn test_core_histogram_paint_all_buckets_populated() {
1607 let mut hist = CoreUtilizationHistogram::new(vec![
1609 99.0, 80.0, 50.0, 15.0, 0.5, ]);
1611 hist.bounds = Rect::new(0.0, 0.0, 60.0, 10.0);
1612 let mut buffer = CellBuffer::new(60, 10);
1613 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1614 hist.paint(&mut canvas);
1615 }
1616
1617 #[test]
1619 fn test_trend_sparkline_type_id() {
1620 let trend = TrendSparkline::default();
1621 let id = Widget::type_id(&trend);
1622 assert_eq!(id, TypeId::of::<TrendSparkline>());
1623 }
1624
1625 #[test]
1626 fn test_trend_sparkline_event() {
1627 let mut trend = TrendSparkline::default();
1628 let result = trend.event(&Event::FocusIn);
1629 assert!(result.is_none());
1630 }
1631
1632 #[test]
1633 fn test_trend_sparkline_children_mut() {
1634 let mut trend = TrendSparkline::default();
1635 assert!(trend.children_mut().is_empty());
1636 }
1637
1638 #[test]
1639 fn test_trend_sparkline_normalized() {
1640 let trend = TrendSparkline::new("Test", vec![0.5]).normalized();
1641 assert!(!trend.is_percentage);
1642 }
1643
1644 #[test]
1645 fn test_trend_sparkline_set_history() {
1646 let mut trend = TrendSparkline::default();
1647 trend.set_history(vec![10.0, 20.0, 30.0]);
1648 assert_eq!(trend.history.len(), 3);
1649 }
1650
1651 #[test]
1652 fn test_trend_sparkline_stats_normalized() {
1653 let trend = TrendSparkline::new("Test", vec![0.5, 0.6, 0.7]).normalized();
1654 let (current, min, max, avg) = trend.stats();
1655 assert!((current - 70.0).abs() < 0.1);
1657 assert!((min - 50.0).abs() < 0.1);
1658 assert!((max - 70.0).abs() < 0.1);
1659 assert!((avg - 60.0).abs() < 0.1);
1660 }
1661
1662 #[test]
1663 fn test_trend_sparkline_assertions() {
1664 let trend = TrendSparkline::default();
1665 assert!(!trend.assertions().is_empty());
1666 }
1667
1668 #[test]
1669 fn test_trend_sparkline_budget() {
1670 let trend = TrendSparkline::default();
1671 let budget = trend.budget();
1672 assert!(budget.total_ms > 0);
1673 }
1674
1675 #[test]
1676 fn test_trend_sparkline_to_html() {
1677 let trend = TrendSparkline::default();
1678 assert!(trend.to_html().is_empty());
1679 }
1680
1681 #[test]
1682 fn test_trend_sparkline_to_css() {
1683 let trend = TrendSparkline::default();
1684 assert!(trend.to_css().is_empty());
1685 }
1686
1687 #[test]
1688 fn test_trend_sparkline_clone() {
1689 let trend = TrendSparkline::new("Test", vec![1.0, 2.0, 3.0]);
1690 let cloned = trend;
1691 assert_eq!(cloned.history.len(), 3);
1692 assert_eq!(cloned.title, "Test");
1693 }
1694
1695 #[test]
1696 fn test_trend_sparkline_debug() {
1697 let trend = TrendSparkline::default();
1698 let debug = format!("{:?}", trend);
1699 assert!(debug.contains("TrendSparkline"));
1700 }
1701
1702 #[test]
1703 fn test_trend_sparkline_paint_high_values() {
1704 let mut trend = TrendSparkline::new("CPU", vec![85.0, 90.0, 95.0]);
1705 trend.bounds = Rect::new(0.0, 0.0, 60.0, 5.0);
1706 let mut buffer = CellBuffer::new(60, 5);
1707 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1708 trend.paint(&mut canvas);
1709 }
1710
1711 #[test]
1712 fn test_trend_sparkline_paint_medium_values() {
1713 let mut trend = TrendSparkline::new("CPU", vec![55.0, 60.0, 65.0]);
1714 trend.bounds = Rect::new(0.0, 0.0, 60.0, 5.0);
1715 let mut buffer = CellBuffer::new(60, 5);
1716 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1717 trend.paint(&mut canvas);
1718 }
1719
1720 #[test]
1721 fn test_trend_sparkline_paint_normalized() {
1722 let mut trend = TrendSparkline::new("Mem", vec![0.3, 0.5, 0.7]).normalized();
1723 trend.bounds = Rect::new(0.0, 0.0, 60.0, 5.0);
1724 let mut buffer = CellBuffer::new(60, 5);
1725 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1726 trend.paint(&mut canvas);
1727 }
1728
1729 #[test]
1731 fn test_system_status_type_id() {
1732 let status = SystemStatus::default();
1733 let id = Widget::type_id(&status);
1734 assert_eq!(id, TypeId::of::<SystemStatus>());
1735 }
1736
1737 #[test]
1738 fn test_system_status_event() {
1739 let mut status = SystemStatus::default();
1740 let result = status.event(&Event::FocusIn);
1741 assert!(result.is_none());
1742 }
1743
1744 #[test]
1745 fn test_system_status_children_mut() {
1746 let mut status = SystemStatus::default();
1747 assert!(status.children_mut().is_empty());
1748 }
1749
1750 #[test]
1751 fn test_system_status_set_load() {
1752 let mut status = SystemStatus::default();
1753 status.set_load(2.0, 1.5, 1.0);
1754 assert!((status.load_1m - 2.0).abs() < 0.01);
1755 assert!((status.load_5m - 1.5).abs() < 0.01);
1756 assert!((status.load_15m - 1.0).abs() < 0.01);
1757 }
1758
1759 #[test]
1760 fn test_system_status_set_thermal() {
1761 let mut status = SystemStatus::default();
1762 status.set_thermal(60.0, 70.0);
1763 assert_eq!(status.thermal, Some((60.0, 70.0)));
1764 }
1765
1766 #[test]
1767 fn test_system_status_assertions() {
1768 let status = SystemStatus::default();
1769 assert!(!status.assertions().is_empty());
1770 }
1771
1772 #[test]
1773 fn test_system_status_budget() {
1774 let status = SystemStatus::default();
1775 let budget = status.budget();
1776 assert!(budget.total_ms > 0);
1777 }
1778
1779 #[test]
1780 fn test_system_status_to_html() {
1781 let status = SystemStatus::default();
1782 assert!(status.to_html().is_empty());
1783 }
1784
1785 #[test]
1786 fn test_system_status_to_css() {
1787 let status = SystemStatus::default();
1788 assert!(status.to_css().is_empty());
1789 }
1790
1791 #[test]
1792 fn test_system_status_clone() {
1793 let status = SystemStatus::new(1.0, 2.0, 3.0, 4).with_thermal(50.0, 60.0);
1794 let cloned = status;
1795 assert!((cloned.load_1m - 1.0).abs() < 0.01);
1796 assert_eq!(cloned.core_count, 4);
1797 assert!(cloned.thermal.is_some());
1798 }
1799
1800 #[test]
1801 fn test_system_status_debug() {
1802 let status = SystemStatus::default();
1803 let debug = format!("{:?}", status);
1804 assert!(debug.contains("SystemStatus"));
1805 }
1806
1807 #[test]
1808 fn test_system_status_paint_no_thermal() {
1809 let mut status = SystemStatus::new(1.0, 1.0, 1.0, 4);
1810 status.bounds = Rect::new(0.0, 0.0, 80.0, 5.0);
1811 let mut buffer = CellBuffer::new(80, 5);
1812 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1813 status.paint(&mut canvas);
1814 }
1815
1816 #[test]
1817 fn test_system_status_measure_with_thermal() {
1818 let status = SystemStatus::new(1.0, 1.0, 1.0, 4).with_thermal(60.0, 70.0);
1819 let constraints = Constraints::loose(Size::new(80.0, 100.0));
1821 let size = status.measure(constraints);
1822 assert_eq!(size.height, 2.0); }
1824
1825 #[test]
1826 fn test_system_status_measure_no_thermal() {
1827 let status = SystemStatus::new(1.0, 1.0, 1.0, 4);
1828 let constraints = Constraints::loose(Size::new(80.0, 100.0));
1830 let size = status.measure(constraints);
1831 assert_eq!(size.height, 1.0); }
1833
1834 #[test]
1836 fn test_cpu_consumer_clone() {
1837 let proc = CpuConsumer::new(123, 50.0, 1_000_000, "test");
1838 let cloned = proc;
1839 assert_eq!(cloned.pid, 123);
1840 }
1841
1842 #[test]
1843 fn test_cpu_consumer_debug() {
1844 let proc = CpuConsumer::new(123, 50.0, 1_000_000, "test");
1845 let debug = format!("{:?}", proc);
1846 assert!(debug.contains("CpuConsumer"));
1847 }
1848
1849 #[test]
1851 fn test_health_level_clone() {
1852 let level = HealthLevel::Critical;
1853 let cloned = level;
1854 assert_eq!(cloned, HealthLevel::Critical);
1855 }
1856
1857 #[test]
1858 fn test_health_level_debug() {
1859 let level = HealthLevel::Ok;
1860 let debug = format!("{:?}", level);
1861 assert!(debug.contains("Ok"));
1862 }
1863
1864 #[test]
1865 fn test_health_level_copy() {
1866 let level = HealthLevel::High;
1867 let copied = level;
1868 assert_eq!(copied, HealthLevel::High);
1869 }
1870}