1use presentar_core::{
7 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
8 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
9};
10use std::any::Any;
11use std::time::Duration;
12
13const SPARK_CHARS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
15
16#[derive(Debug, Clone)]
18pub struct NetworkInterface {
19 pub name: String,
21 pub rx_history: Vec<f64>,
23 pub tx_history: Vec<f64>,
25 pub rx_bps: f64,
27 pub tx_bps: f64,
29 pub rx_total: u64,
31 pub tx_total: u64,
33 pub rx_errors: u64,
35 pub tx_errors: u64,
37 pub rx_dropped: u64,
39 pub tx_dropped: u64,
41 pub errors_per_sec: f64,
43 pub drops_per_sec: f64,
45 pub utilization_percent: Option<f64>,
48}
49
50impl NetworkInterface {
51 #[must_use]
53 pub fn new(name: impl Into<String>) -> Self {
54 Self {
55 name: name.into(),
56 rx_history: Vec::new(),
57 tx_history: Vec::new(),
58 rx_bps: 0.0,
59 tx_bps: 0.0,
60 rx_total: 0,
61 tx_total: 0,
62 rx_errors: 0,
63 tx_errors: 0,
64 rx_dropped: 0,
65 tx_dropped: 0,
66 errors_per_sec: 0.0,
67 drops_per_sec: 0.0,
68 utilization_percent: None,
69 }
70 }
71
72 pub fn set_stats(&mut self, rx_errors: u64, tx_errors: u64, rx_dropped: u64, tx_dropped: u64) {
74 self.rx_errors = rx_errors;
75 self.tx_errors = tx_errors;
76 self.rx_dropped = rx_dropped;
77 self.tx_dropped = tx_dropped;
78 }
79
80 pub fn set_rates(&mut self, errors_per_sec: f64, drops_per_sec: f64) {
82 self.errors_per_sec = errors_per_sec;
83 self.drops_per_sec = drops_per_sec;
84 }
85
86 pub fn set_utilization(&mut self, utilization_percent: Option<f64>) {
88 self.utilization_percent = utilization_percent;
89 }
90
91 #[must_use]
93 pub fn total_errors(&self) -> u64 {
94 self.rx_errors + self.tx_errors
95 }
96
97 #[must_use]
99 pub fn total_dropped(&self) -> u64 {
100 self.rx_dropped + self.tx_dropped
101 }
102
103 pub fn update(&mut self, rx_bps: f64, tx_bps: f64) {
105 self.rx_bps = rx_bps;
106 self.tx_bps = tx_bps;
107 self.rx_history.push(rx_bps);
108 self.tx_history.push(tx_bps);
109 if self.rx_history.len() > 60 {
111 self.rx_history.remove(0);
112 }
113 if self.tx_history.len() > 60 {
114 self.tx_history.remove(0);
115 }
116 }
117
118 pub fn set_totals(&mut self, rx: u64, tx: u64) {
120 self.rx_total = rx;
121 self.tx_total = tx;
122 }
123}
124
125#[derive(Debug, Clone)]
127pub struct NetworkPanel {
128 interfaces: Vec<NetworkInterface>,
130 rx_color: Color,
132 tx_color: Color,
134 spark_width: usize,
136 show_totals: bool,
138 compact: bool,
140 bounds: Rect,
142}
143
144impl Default for NetworkPanel {
145 fn default() -> Self {
146 Self::new()
147 }
148}
149
150impl NetworkPanel {
151 #[must_use]
153 pub fn new() -> Self {
154 Self {
155 interfaces: Vec::new(),
156 rx_color: Color::new(0.3, 0.8, 0.3, 1.0), tx_color: Color::new(0.8, 0.3, 0.3, 1.0), spark_width: 20,
159 show_totals: true,
160 compact: false,
161 bounds: Rect::default(),
162 }
163 }
164
165 pub fn set_interfaces(&mut self, interfaces: Vec<NetworkInterface>) {
167 self.interfaces = interfaces;
168 }
169
170 pub fn add_interface(&mut self, iface: NetworkInterface) {
172 self.interfaces.push(iface);
173 }
174
175 pub fn interface_mut(&mut self, name: &str) -> Option<&mut NetworkInterface> {
177 self.interfaces.iter_mut().find(|i| i.name == name)
178 }
179
180 pub fn clear(&mut self) {
182 self.interfaces.clear();
183 }
184
185 #[must_use]
187 pub fn with_rx_color(mut self, color: Color) -> Self {
188 self.rx_color = color;
189 self
190 }
191
192 #[must_use]
194 pub fn with_tx_color(mut self, color: Color) -> Self {
195 self.tx_color = color;
196 self
197 }
198
199 #[must_use]
201 pub fn with_spark_width(mut self, width: usize) -> Self {
202 self.spark_width = width;
203 self
204 }
205
206 #[must_use]
208 pub fn without_totals(mut self) -> Self {
209 self.show_totals = false;
210 self
211 }
212
213 #[must_use]
215 pub fn compact(mut self) -> Self {
216 self.compact = true;
217 self
218 }
219
220 #[must_use]
222 pub fn len(&self) -> usize {
223 self.interfaces.len()
224 }
225
226 #[must_use]
228 pub fn is_empty(&self) -> bool {
229 self.interfaces.is_empty()
230 }
231
232 fn format_bps(bps: f64) -> String {
234 const KB: f64 = 1024.0;
235 const MB: f64 = KB * 1024.0;
236 const GB: f64 = MB * 1024.0;
237
238 if bps >= GB {
239 format!("{:.1}G/s", bps / GB)
240 } else if bps >= MB {
241 format!("{:.1}M/s", bps / MB)
242 } else if bps >= KB {
243 format!("{:.1}K/s", bps / KB)
244 } else {
245 format!("{bps:.0}B/s")
246 }
247 }
248
249 fn format_bytes(bytes: u64) -> String {
251 const KB: u64 = 1024;
252 const MB: u64 = KB * 1024;
253 const GB: u64 = MB * 1024;
254 const TB: u64 = GB * 1024;
255
256 if bytes >= TB {
257 format!("{:.1}T", bytes as f64 / TB as f64)
258 } else if bytes >= GB {
259 format!("{:.1}G", bytes as f64 / GB as f64)
260 } else if bytes >= MB {
261 format!("{:.1}M", bytes as f64 / MB as f64)
262 } else if bytes >= KB {
263 format!("{:.1}K", bytes as f64 / KB as f64)
264 } else {
265 format!("{bytes}B")
266 }
267 }
268
269 fn render_sparkline(data: &[f64], width: usize) -> String {
271 if data.is_empty() {
272 return " ".repeat(width);
273 }
274
275 let max_val = data
276 .iter()
277 .copied()
278 .fold(f64::NEG_INFINITY, f64::max)
279 .max(1.0);
280 let start = data.len().saturating_sub(width);
281 let visible = &data[start..];
282
283 let mut result = String::with_capacity(width * 3); for &val in visible {
285 let normalized = (val / max_val).clamp(0.0, 1.0);
286 let idx = ((normalized * 7.0).round() as usize).min(7);
287 result.push(SPARK_CHARS[idx]);
288 }
289
290 let char_count = result.chars().count();
292 if char_count < width {
293 let padding = " ".repeat(width - char_count);
294 result.insert_str(0, &padding);
295 }
296
297 result
298 }
299}
300
301impl Brick for NetworkPanel {
302 fn brick_name(&self) -> &'static str {
303 "network_panel"
304 }
305
306 fn assertions(&self) -> &[BrickAssertion] {
307 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(8)];
308 ASSERTIONS
309 }
310
311 fn budget(&self) -> BrickBudget {
312 BrickBudget::uniform(8)
313 }
314
315 fn verify(&self) -> BrickVerification {
316 BrickVerification {
317 passed: vec![BrickAssertion::max_latency_ms(8)],
318 failed: vec![],
319 verification_time: Duration::from_micros(5),
320 }
321 }
322
323 fn to_html(&self) -> String {
324 String::new()
325 }
326
327 fn to_css(&self) -> String {
328 String::new()
329 }
330}
331
332impl Widget for NetworkPanel {
333 fn type_id(&self) -> TypeId {
334 TypeId::of::<Self>()
335 }
336
337 fn measure(&self, constraints: Constraints) -> Size {
338 let rows_per_iface = if self.compact { 1 } else { 2 };
339 let height = (self.interfaces.len() * rows_per_iface + 1) as f32;
340 let min_width = if self.compact { 40.0 } else { 60.0 };
341 let width = constraints.max_width.max(min_width);
342 constraints.constrain(Size::new(width, height.max(2.0)))
343 }
344
345 fn layout(&mut self, bounds: Rect) -> LayoutResult {
346 self.bounds = bounds;
347 LayoutResult {
348 size: Size::new(bounds.width, bounds.height),
349 }
350 }
351
352 #[allow(clippy::too_many_lines)]
353 fn paint(&self, canvas: &mut dyn Canvas) {
354 let width = self.bounds.width as usize;
355 let height = self.bounds.height as usize;
356 if width == 0 || height == 0 {
357 return;
358 }
359
360 let header_style = TextStyle {
362 color: Color::new(0.0, 1.0, 1.0, 1.0),
363 weight: presentar_core::FontWeight::Bold,
364 ..Default::default()
365 };
366 canvas.draw_text(
367 "Network",
368 Point::new(self.bounds.x, self.bounds.y),
369 &header_style,
370 );
371
372 let _rows_per_iface = if self.compact { 1 } else { 2 };
373 let mut y = self.bounds.y + 1.0;
374
375 for iface in &self.interfaces {
376 if y >= self.bounds.y + self.bounds.height {
377 break;
378 }
379
380 if self.compact {
381 let name_w = 8;
384 let spark_w = self.spark_width.min(width.saturating_sub(30) / 2);
385
386 let mut x = self.bounds.x;
387
388 let name = format!("{:name_w$}", iface.name);
390 canvas.draw_text(
391 &name,
392 Point::new(x, y),
393 &TextStyle {
394 color: Color::new(0.8, 0.8, 0.8, 1.0),
395 ..Default::default()
396 },
397 );
398 x += name_w as f32 + 1.0;
399
400 let rx_spark = Self::render_sparkline(&iface.rx_history, spark_w);
402 canvas.draw_text(
403 &rx_spark,
404 Point::new(x, y),
405 &TextStyle {
406 color: self.rx_color,
407 ..Default::default()
408 },
409 );
410 x += spark_w as f32 + 1.0;
411
412 let rx_rate = format!("{:>8}", Self::format_bps(iface.rx_bps));
414 canvas.draw_text(
415 &rx_rate,
416 Point::new(x, y),
417 &TextStyle {
418 color: self.rx_color,
419 ..Default::default()
420 },
421 );
422 x += 9.0;
423
424 canvas.draw_text(
426 "↓",
427 Point::new(x, y),
428 &TextStyle {
429 color: self.rx_color,
430 ..Default::default()
431 },
432 );
433 x += 2.0;
434
435 let tx_spark = Self::render_sparkline(&iface.tx_history, spark_w);
437 canvas.draw_text(
438 &tx_spark,
439 Point::new(x, y),
440 &TextStyle {
441 color: self.tx_color,
442 ..Default::default()
443 },
444 );
445 x += spark_w as f32 + 1.0;
446
447 let tx_rate = format!("{:>8}", Self::format_bps(iface.tx_bps));
449 canvas.draw_text(
450 &tx_rate,
451 Point::new(x, y),
452 &TextStyle {
453 color: self.tx_color,
454 ..Default::default()
455 },
456 );
457 x += 9.0;
458
459 canvas.draw_text(
461 "↑",
462 Point::new(x, y),
463 &TextStyle {
464 color: self.tx_color,
465 ..Default::default()
466 },
467 );
468 x += 2.0;
469
470 if iface.errors_per_sec > 0.0 || iface.drops_per_sec > 0.0 {
473 let (indicator, color) =
475 if iface.errors_per_sec > 10.0 || iface.drops_per_sec > 10.0 {
476 (
477 "●",
478 Color {
479 r: 1.0,
480 g: 0.3,
481 b: 0.3,
482 a: 1.0,
483 },
484 ) } else if iface.errors_per_sec > 1.0 || iface.drops_per_sec > 1.0 {
486 (
487 "◐",
488 Color {
489 r: 1.0,
490 g: 0.8,
491 b: 0.2,
492 a: 1.0,
493 },
494 ) } else {
496 (
497 "○",
498 Color {
499 r: 0.8,
500 g: 0.8,
501 b: 0.3,
502 a: 1.0,
503 },
504 ) };
506
507 let err_drop_text = format!(
509 "{indicator}E:{:.0}/D:{:.0}",
510 iface.errors_per_sec, iface.drops_per_sec
511 );
512 canvas.draw_text(
513 &err_drop_text,
514 Point::new(x, y),
515 &TextStyle {
516 color,
517 ..Default::default()
518 },
519 );
520 x += err_drop_text.len() as f32 + 1.0;
521 }
522
523 if let Some(util_pct) = iface.utilization_percent {
525 let capped = util_pct.min(100.0);
526 let (util_text, util_color) = if util_pct > 80.0 {
527 (
528 format!("●{capped:.0}%"),
529 Color {
530 r: 1.0,
531 g: 0.3,
532 b: 0.3,
533 a: 1.0,
534 }, )
536 } else if util_pct > 50.0 {
537 (
538 format!("◐{util_pct:.0}%"),
539 Color {
540 r: 1.0,
541 g: 0.8,
542 b: 0.2,
543 a: 1.0,
544 }, )
546 } else {
547 (
548 format!("{util_pct:.0}%"),
549 Color {
550 r: 0.5,
551 g: 0.8,
552 b: 0.5,
553 a: 1.0,
554 }, )
556 };
557 canvas.draw_text(
558 &util_text,
559 Point::new(x, y),
560 &TextStyle {
561 color: util_color,
562 ..Default::default()
563 },
564 );
565 }
566 } else {
567 canvas.draw_text(
573 &iface.name,
574 Point::new(self.bounds.x, y),
575 &TextStyle {
576 color: Color::new(0.8, 0.8, 1.0, 1.0),
577 weight: presentar_core::FontWeight::Bold,
578 ..Default::default()
579 },
580 );
581 y += 1.0;
582
583 let spark_w = self.spark_width.min(width.saturating_sub(40) / 2);
584 let mut x = self.bounds.x + 2.0;
585
586 canvas.draw_text(
588 "↓",
589 Point::new(x, y),
590 &TextStyle {
591 color: self.rx_color,
592 ..Default::default()
593 },
594 );
595 x += 2.0;
596
597 let rx_spark = Self::render_sparkline(&iface.rx_history, spark_w);
598 canvas.draw_text(
599 &rx_spark,
600 Point::new(x, y),
601 &TextStyle {
602 color: self.rx_color,
603 ..Default::default()
604 },
605 );
606 x += spark_w as f32 + 1.0;
607
608 let rx_rate = Self::format_bps(iface.rx_bps);
609 canvas.draw_text(
610 &rx_rate,
611 Point::new(x, y),
612 &TextStyle {
613 color: self.rx_color,
614 ..Default::default()
615 },
616 );
617 x += 10.0;
618
619 if self.show_totals {
620 let rx_total = format!("({})", Self::format_bytes(iface.rx_total));
621 canvas.draw_text(
622 &rx_total,
623 Point::new(x, y),
624 &TextStyle {
625 color: Color::new(0.5, 0.5, 0.5, 1.0),
626 ..Default::default()
627 },
628 );
629 x += 10.0;
630 }
631
632 x += 2.0;
633
634 canvas.draw_text(
636 "↑",
637 Point::new(x, y),
638 &TextStyle {
639 color: self.tx_color,
640 ..Default::default()
641 },
642 );
643 x += 2.0;
644
645 let tx_spark = Self::render_sparkline(&iface.tx_history, spark_w);
646 canvas.draw_text(
647 &tx_spark,
648 Point::new(x, y),
649 &TextStyle {
650 color: self.tx_color,
651 ..Default::default()
652 },
653 );
654 x += spark_w as f32 + 1.0;
655
656 let tx_rate = Self::format_bps(iface.tx_bps);
657 canvas.draw_text(
658 &tx_rate,
659 Point::new(x, y),
660 &TextStyle {
661 color: self.tx_color,
662 ..Default::default()
663 },
664 );
665 x += 10.0;
666
667 if self.show_totals {
668 let tx_total = format!("({})", Self::format_bytes(iface.tx_total));
669 canvas.draw_text(
670 &tx_total,
671 Point::new(x, y),
672 &TextStyle {
673 color: Color::new(0.5, 0.5, 0.5, 1.0),
674 ..Default::default()
675 },
676 );
677 }
678 }
679
680 y += 1.0;
681 }
682
683 if self.interfaces.is_empty() && height > 1 {
685 canvas.draw_text(
686 "No interfaces",
687 Point::new(self.bounds.x + 1.0, self.bounds.y + 1.0),
688 &TextStyle {
689 color: Color::new(0.5, 0.5, 0.5, 1.0),
690 ..Default::default()
691 },
692 );
693 }
694 }
695
696 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
697 None
698 }
699
700 fn children(&self) -> &[Box<dyn Widget>] {
701 &[]
702 }
703
704 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
705 &mut []
706 }
707}
708
709#[cfg(test)]
710mod tests {
711 use super::*;
712
713 fn sample_interface() -> NetworkInterface {
714 let mut iface = NetworkInterface::new("eth0");
715 for i in 0..30 {
716 iface.update(i as f64 * 1000.0, i as f64 * 500.0);
717 }
718 iface.set_totals(1024 * 1024 * 1024, 512 * 1024 * 1024);
719 iface
720 }
721
722 #[test]
723 fn test_network_panel_new() {
724 let panel = NetworkPanel::new();
725 assert!(panel.is_empty());
726 }
727
728 #[test]
729 fn test_network_panel_add_interface() {
730 let mut panel = NetworkPanel::new();
731 panel.add_interface(NetworkInterface::new("eth0"));
732 assert_eq!(panel.len(), 1);
733 }
734
735 #[test]
736 fn test_network_panel_set_interfaces() {
737 let mut panel = NetworkPanel::new();
738 panel.set_interfaces(vec![
739 NetworkInterface::new("eth0"),
740 NetworkInterface::new("wlan0"),
741 ]);
742 assert_eq!(panel.len(), 2);
743 }
744
745 #[test]
746 fn test_network_panel_interface_mut() {
747 let mut panel = NetworkPanel::new();
748 panel.add_interface(NetworkInterface::new("eth0"));
749 let iface = panel.interface_mut("eth0").unwrap();
750 iface.update(1000.0, 500.0);
751 assert_eq!(iface.rx_bps, 1000.0);
752 }
753
754 #[test]
755 fn test_network_panel_clear() {
756 let mut panel = NetworkPanel::new();
757 panel.add_interface(NetworkInterface::new("eth0"));
758 panel.clear();
759 assert!(panel.is_empty());
760 }
761
762 #[test]
763 fn test_network_interface_update() {
764 let mut iface = NetworkInterface::new("eth0");
765 iface.update(1000.0, 500.0);
766 assert_eq!(iface.rx_bps, 1000.0);
767 assert_eq!(iface.tx_bps, 500.0);
768 assert_eq!(iface.rx_history.len(), 1);
769 assert_eq!(iface.tx_history.len(), 1);
770 }
771
772 #[test]
773 fn test_network_interface_history_limit() {
774 let mut iface = NetworkInterface::new("eth0");
775 for i in 0..100 {
776 iface.update(i as f64, i as f64);
777 }
778 assert_eq!(iface.rx_history.len(), 60);
779 assert_eq!(iface.tx_history.len(), 60);
780 }
781
782 #[test]
783 fn test_network_panel_with_colors() {
784 let panel = NetworkPanel::new()
785 .with_rx_color(Color::BLUE)
786 .with_tx_color(Color::RED);
787 assert_eq!(panel.rx_color, Color::BLUE);
788 assert_eq!(panel.tx_color, Color::RED);
789 }
790
791 #[test]
792 fn test_network_panel_with_spark_width() {
793 let panel = NetworkPanel::new().with_spark_width(30);
794 assert_eq!(panel.spark_width, 30);
795 }
796
797 #[test]
798 fn test_network_panel_without_totals() {
799 let panel = NetworkPanel::new().without_totals();
800 assert!(!panel.show_totals);
801 }
802
803 #[test]
804 fn test_network_panel_compact() {
805 let panel = NetworkPanel::new().compact();
806 assert!(panel.compact);
807 }
808
809 #[test]
810 fn test_format_bps() {
811 assert_eq!(NetworkPanel::format_bps(500.0), "500B/s");
812 assert_eq!(NetworkPanel::format_bps(1024.0), "1.0K/s");
813 assert_eq!(NetworkPanel::format_bps(1024.0 * 1024.0), "1.0M/s");
814 assert_eq!(NetworkPanel::format_bps(1024.0 * 1024.0 * 1024.0), "1.0G/s");
815 }
816
817 #[test]
818 fn test_format_bytes() {
819 assert_eq!(NetworkPanel::format_bytes(500), "500B");
820 assert_eq!(NetworkPanel::format_bytes(1024), "1.0K");
821 assert_eq!(NetworkPanel::format_bytes(1024 * 1024), "1.0M");
822 assert_eq!(NetworkPanel::format_bytes(1024 * 1024 * 1024), "1.0G");
823 assert_eq!(
824 NetworkPanel::format_bytes(1024u64 * 1024 * 1024 * 1024),
825 "1.0T"
826 );
827 }
828
829 #[test]
830 fn test_render_sparkline() {
831 let data = vec![0.0, 0.5, 1.0];
832 let spark = NetworkPanel::render_sparkline(&data, 5);
833 assert_eq!(spark.chars().count(), 5);
834 }
835
836 #[test]
837 fn test_render_sparkline_empty() {
838 let spark = NetworkPanel::render_sparkline(&[], 5);
839 assert_eq!(spark, " ");
840 }
841
842 #[test]
843 fn test_network_panel_measure() {
844 let mut panel = NetworkPanel::new();
845 panel.add_interface(NetworkInterface::new("eth0"));
846 let size = panel.measure(Constraints::new(0.0, 100.0, 0.0, 50.0));
847 assert!(size.width >= 60.0);
848 assert!(size.height >= 2.0);
849 }
850
851 #[test]
852 fn test_network_panel_layout() {
853 let mut panel = NetworkPanel::new();
854 let result = panel.layout(Rect::new(0.0, 0.0, 80.0, 20.0));
855 assert_eq!(result.size.width, 80.0);
856 }
857
858 #[test]
859 fn test_network_panel_verify() {
860 let panel = NetworkPanel::new();
861 assert!(panel.verify().is_valid());
862 }
863
864 #[test]
865 fn test_network_panel_brick_name() {
866 let panel = NetworkPanel::new();
867 assert_eq!(panel.brick_name(), "network_panel");
868 }
869
870 #[test]
871 fn test_network_panel_default() {
872 let panel = NetworkPanel::default();
873 assert!(panel.is_empty());
874 }
875
876 #[test]
877 fn test_network_panel_children() {
878 let panel = NetworkPanel::new();
879 assert!(panel.children().is_empty());
880 }
881
882 #[test]
883 fn test_network_panel_children_mut() {
884 let mut panel = NetworkPanel::new();
885 assert!(panel.children_mut().is_empty());
886 }
887
888 #[test]
889 fn test_network_panel_type_id() {
890 let panel = NetworkPanel::new();
891 assert_eq!(Widget::type_id(&panel), TypeId::of::<NetworkPanel>());
892 }
893
894 #[test]
895 fn test_network_panel_to_html() {
896 let panel = NetworkPanel::new();
897 assert!(panel.to_html().is_empty());
898 }
899
900 #[test]
901 fn test_network_panel_to_css() {
902 let panel = NetworkPanel::new();
903 assert!(panel.to_css().is_empty());
904 }
905
906 #[test]
907 fn test_network_interface_set_totals() {
908 let mut iface = NetworkInterface::new("eth0");
909 iface.set_totals(1000, 500);
910 assert_eq!(iface.rx_total, 1000);
911 assert_eq!(iface.tx_total, 500);
912 }
913
914 #[test]
915 fn test_network_panel_paint_with_data() {
916 use crate::direct::{CellBuffer, DirectTerminalCanvas};
917
918 let mut panel = NetworkPanel::new();
919 let mut iface = NetworkInterface::new("eth0");
920 for i in 0..30 {
921 iface.update(i as f64 * 1000.0, i as f64 * 500.0);
922 }
923 iface.set_totals(1024 * 1024 * 1024, 512 * 1024 * 1024);
924 panel.add_interface(iface);
925
926 panel.layout(Rect::new(0.0, 0.0, 80.0, 10.0));
927
928 let mut buffer = CellBuffer::new(80, 10);
929 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
930 panel.paint(&mut canvas);
931 }
932
933 #[test]
934 fn test_network_panel_paint_empty() {
935 use crate::direct::{CellBuffer, DirectTerminalCanvas};
936
937 let mut panel = NetworkPanel::new();
938 panel.layout(Rect::new(0.0, 0.0, 60.0, 10.0));
939
940 let mut buffer = CellBuffer::new(60, 10);
941 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
942 panel.paint(&mut canvas);
943 }
944
945 #[test]
946 fn test_network_panel_paint_small_bounds() {
947 use crate::direct::{CellBuffer, DirectTerminalCanvas};
948
949 let mut panel = NetworkPanel::new();
950 panel.add_interface(NetworkInterface::new("eth0"));
951 panel.layout(Rect::new(0.0, 0.0, 5.0, 0.5)); let mut buffer = CellBuffer::new(5, 1);
954 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
955 panel.paint(&mut canvas); }
957
958 #[test]
959 fn test_network_panel_paint_compact() {
960 use crate::direct::{CellBuffer, DirectTerminalCanvas};
961
962 let mut panel = NetworkPanel::new().compact();
963 let mut iface = NetworkInterface::new("wlan0");
964 iface.update(5000.0, 2500.0);
965 panel.add_interface(iface);
966
967 panel.layout(Rect::new(0.0, 0.0, 60.0, 10.0));
968
969 let mut buffer = CellBuffer::new(60, 10);
970 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
971 panel.paint(&mut canvas);
972 }
973
974 #[test]
975 fn test_network_panel_paint_with_totals() {
976 use crate::direct::{CellBuffer, DirectTerminalCanvas};
977
978 let mut panel = NetworkPanel::new(); let mut iface = NetworkInterface::new("eth0");
980 iface.update(1024.0 * 1024.0, 512.0 * 1024.0);
981 iface.set_totals(10 * 1024 * 1024 * 1024, 5 * 1024 * 1024 * 1024);
982 panel.add_interface(iface);
983
984 panel.layout(Rect::new(0.0, 0.0, 80.0, 10.0));
985
986 let mut buffer = CellBuffer::new(80, 10);
987 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
988 panel.paint(&mut canvas);
989 }
990
991 #[test]
992 fn test_network_panel_paint_without_totals() {
993 use crate::direct::{CellBuffer, DirectTerminalCanvas};
994
995 let mut panel = NetworkPanel::new().without_totals();
996 let mut iface = NetworkInterface::new("eth0");
997 iface.update(1024.0, 512.0);
998 panel.add_interface(iface);
999
1000 panel.layout(Rect::new(0.0, 0.0, 60.0, 10.0));
1001
1002 let mut buffer = CellBuffer::new(60, 10);
1003 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1004 panel.paint(&mut canvas);
1005 }
1006
1007 #[test]
1008 fn test_network_panel_multiple_interfaces() {
1009 use crate::direct::{CellBuffer, DirectTerminalCanvas};
1010
1011 let mut panel = NetworkPanel::new();
1012
1013 let mut eth0 = NetworkInterface::new("eth0");
1014 eth0.update(10000.0, 5000.0);
1015 panel.add_interface(eth0);
1016
1017 let mut wlan0 = NetworkInterface::new("wlan0");
1018 wlan0.update(2000.0, 1000.0);
1019 panel.add_interface(wlan0);
1020
1021 let mut lo = NetworkInterface::new("lo");
1022 lo.update(100.0, 100.0);
1023 panel.add_interface(lo);
1024
1025 panel.layout(Rect::new(0.0, 0.0, 80.0, 10.0));
1026
1027 let mut buffer = CellBuffer::new(80, 10);
1028 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1029 panel.paint(&mut canvas);
1030 }
1031
1032 #[test]
1033 fn test_network_panel_event() {
1034 let mut panel = NetworkPanel::new();
1035 let result = panel.event(&Event::KeyDown {
1036 key: presentar_core::Key::Enter,
1037 });
1038 assert!(result.is_none());
1039 }
1040
1041 #[test]
1042 fn test_network_panel_assertions() {
1043 let panel = NetworkPanel::new();
1044 assert!(!panel.assertions().is_empty());
1045 }
1046
1047 #[test]
1048 fn test_network_panel_budget() {
1049 let panel = NetworkPanel::new();
1050 assert!(panel.budget().paint_ms > 0);
1051 }
1052
1053 #[test]
1054 fn test_network_interface_long_name() {
1055 use crate::direct::{CellBuffer, DirectTerminalCanvas};
1056
1057 let mut panel = NetworkPanel::new();
1058 let mut iface = NetworkInterface::new("verylonginterfacename0");
1059 iface.update(1024.0, 512.0);
1060 panel.add_interface(iface);
1061
1062 panel.layout(Rect::new(0.0, 0.0, 80.0, 10.0));
1063
1064 let mut buffer = CellBuffer::new(80, 10);
1065 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1066 panel.paint(&mut canvas);
1067 }
1068
1069 #[test]
1070 fn test_format_bps_edge_cases() {
1071 assert!(NetworkPanel::format_bps(1024.0 * 1024.0 * 1024.0 * 1024.0).contains("G/s"));
1073 assert_eq!(NetworkPanel::format_bps(0.5), "0B/s");
1075 }
1076
1077 #[test]
1078 fn test_render_sparkline_single_value() {
1079 let data = vec![0.5];
1080 let spark = NetworkPanel::render_sparkline(&data, 3);
1081 assert_eq!(spark.chars().count(), 3);
1082 }
1083}