1use presentar_core::{
6 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
7 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
8};
9use std::any::Any;
10use std::time::Duration;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14pub enum TcpState {
15 #[default]
16 Established,
17 Listen,
18 TimeWait,
19 CloseWait,
20 SynSent,
21 SynRecv,
22 FinWait1,
23 FinWait2,
24 LastAck,
25 Closing,
26 Closed,
27}
28
29impl TcpState {
30 pub fn short(&self) -> &'static str {
32 match self {
33 Self::Established => "EST",
34 Self::Listen => "LSN",
35 Self::TimeWait => "TW",
36 Self::CloseWait => "CW",
37 Self::SynSent => "SS",
38 Self::SynRecv => "SR",
39 Self::FinWait1 => "FW1",
40 Self::FinWait2 => "FW2",
41 Self::LastAck => "LA",
42 Self::Closing => "CLG",
43 Self::Closed => "CLD",
44 }
45 }
46
47 pub fn color(&self) -> Color {
49 match self {
50 Self::Established => Color::new(0.4, 0.9, 0.4, 1.0), Self::Listen => Color::new(0.4, 0.6, 1.0, 1.0), Self::TimeWait => Color::new(0.6, 0.6, 0.6, 1.0), Self::CloseWait => Color::new(1.0, 0.8, 0.2, 1.0), _ => Color::new(0.7, 0.7, 0.7, 1.0), }
56 }
57}
58
59#[derive(Debug, Clone)]
61pub struct ConnectionEntry {
62 pub protocol: String,
64 pub local_addr: String,
66 pub local_port: u16,
68 pub remote_addr: String,
70 pub remote_port: u16,
72 pub state: TcpState,
74 pub process: Option<String>,
76 pub pid: Option<u32>,
78}
79
80impl ConnectionEntry {
81 #[must_use]
83 pub fn tcp(local_port: u16, remote_addr: impl Into<String>, remote_port: u16) -> Self {
84 Self {
85 protocol: "tcp".to_string(),
86 local_addr: "0.0.0.0".to_string(),
87 local_port,
88 remote_addr: remote_addr.into(),
89 remote_port,
90 state: TcpState::Established,
91 process: None,
92 pid: None,
93 }
94 }
95
96 #[must_use]
98 pub fn listen(port: u16) -> Self {
99 Self {
100 protocol: "tcp".to_string(),
101 local_addr: "0.0.0.0".to_string(),
102 local_port: port,
103 remote_addr: "*".to_string(),
104 remote_port: 0,
105 state: TcpState::Listen,
106 process: None,
107 pid: None,
108 }
109 }
110
111 #[must_use]
113 pub fn with_state(mut self, state: TcpState) -> Self {
114 self.state = state;
115 self
116 }
117
118 #[must_use]
120 pub fn with_process(mut self, name: impl Into<String>, pid: u32) -> Self {
121 self.process = Some(name.into());
122 self.pid = Some(pid);
123 self
124 }
125
126 #[must_use]
128 pub fn with_local_addr(mut self, addr: impl Into<String>) -> Self {
129 self.local_addr = addr.into();
130 self
131 }
132
133 pub fn service_name(&self) -> &str {
135 match self.local_port {
136 22 => "ssh",
137 80 => "http",
138 443 => "https",
139 3306 => "mysql",
140 5432 => "pgsql",
141 6379 => "redis",
142 8080 => "http-alt",
143 27017 => "mongodb",
144 _ => "",
145 }
146 }
147
148 pub fn local_display(&self) -> String {
150 format!(":{}", self.local_port)
151 }
152
153 pub fn remote_display(&self) -> String {
155 if self.remote_addr == "*" || self.remote_addr == "0.0.0.0" {
156 "*".to_string()
157 } else {
158 format!("{}:{}", self.remote_addr, self.remote_port)
159 }
160 }
161}
162
163#[derive(Debug, Clone)]
165pub struct ConnectionsPanel {
166 connections: Vec<ConnectionEntry>,
168 show_listening: bool,
170 show_established: bool,
172 max_connections: usize,
174 show_headers: bool,
176 bounds: Rect,
178}
179
180impl Default for ConnectionsPanel {
181 fn default() -> Self {
182 Self::new()
183 }
184}
185
186impl ConnectionsPanel {
187 #[must_use]
189 pub fn new() -> Self {
190 Self {
191 connections: Vec::new(),
192 show_listening: true,
193 show_established: true,
194 max_connections: 10,
195 show_headers: true,
196 bounds: Rect::default(),
197 }
198 }
199
200 pub fn add_connection(&mut self, connection: ConnectionEntry) {
202 self.connections.push(connection);
203 }
204
205 #[must_use]
207 pub fn with_connections(mut self, connections: Vec<ConnectionEntry>) -> Self {
208 self.connections = connections;
209 self
210 }
211
212 #[must_use]
214 pub fn show_listening(mut self, show: bool) -> Self {
215 self.show_listening = show;
216 self
217 }
218
219 #[must_use]
221 pub fn show_established(mut self, show: bool) -> Self {
222 self.show_established = show;
223 self
224 }
225
226 #[must_use]
228 pub fn max_connections(mut self, max: usize) -> Self {
229 self.max_connections = max;
230 self
231 }
232
233 #[must_use]
235 pub fn show_headers(mut self, show: bool) -> Self {
236 self.show_headers = show;
237 self
238 }
239
240 pub fn established_count(&self) -> usize {
242 self.connections
243 .iter()
244 .filter(|c| c.state == TcpState::Established)
245 .count()
246 }
247
248 pub fn listening_count(&self) -> usize {
250 self.connections
251 .iter()
252 .filter(|c| c.state == TcpState::Listen)
253 .count()
254 }
255
256 fn visible_connections(&self) -> impl Iterator<Item = &ConnectionEntry> {
258 self.connections
259 .iter()
260 .filter(|c| {
261 (self.show_listening && c.state == TcpState::Listen)
262 || (self.show_established && c.state == TcpState::Established)
263 || (c.state != TcpState::Listen && c.state != TcpState::Established)
264 })
265 .take(self.max_connections)
266 }
267
268 fn draw_header(&self, canvas: &mut dyn Canvas, x: f32, y: f32) {
270 let header = "SVC LOCAL → REMOTE ST PROC";
271 canvas.draw_text(
272 header,
273 Point::new(x, y),
274 &TextStyle {
275 color: Color::new(0.6, 0.6, 0.6, 1.0),
276 ..Default::default()
277 },
278 );
279 }
280
281 fn draw_connection(
283 &self,
284 canvas: &mut dyn Canvas,
285 conn: &ConnectionEntry,
286 x: f32,
287 y: f32,
288 width: f32,
289 ) {
290 let svc = {
292 let name = conn.service_name();
293 if name.is_empty() {
294 format!("{:5}", conn.local_port)
295 } else {
296 format!("{name:5}")
297 }
298 };
299 canvas.draw_text(
300 &svc,
301 Point::new(x, y),
302 &TextStyle {
303 color: Color::WHITE,
304 ..Default::default()
305 },
306 );
307
308 canvas.draw_text(
310 &conn.local_display(),
311 Point::new(x + 6.0, y),
312 &TextStyle {
313 color: Color::new(0.6, 0.8, 1.0, 1.0),
314 ..Default::default()
315 },
316 );
317
318 canvas.draw_text(
320 "→",
321 Point::new(x + 12.0, y),
322 &TextStyle {
323 color: Color::new(0.5, 0.5, 0.5, 1.0),
324 ..Default::default()
325 },
326 );
327
328 let remote = {
330 let r = conn.remote_display();
331 if r.len() > 14 {
332 format!("{}...", &r[..11])
333 } else {
334 format!("{r:14}")
335 }
336 };
337 canvas.draw_text(
338 &remote,
339 Point::new(x + 14.0, y),
340 &TextStyle {
341 color: Color::new(0.8, 0.8, 0.8, 1.0),
342 ..Default::default()
343 },
344 );
345
346 canvas.draw_text(
348 conn.state.short(),
349 Point::new(x + 29.0, y),
350 &TextStyle {
351 color: conn.state.color(),
352 ..Default::default()
353 },
354 );
355
356 if let Some(ref proc) = conn.process {
358 let proc_x = x + 33.0;
359 if proc_x < x + width {
360 let max_len = ((width - 33.0) as usize).min(10);
361 let name = if proc.len() > max_len {
362 format!("{}...", &proc[..max_len.saturating_sub(3)])
363 } else {
364 proc.clone()
365 };
366 canvas.draw_text(
367 &name,
368 Point::new(proc_x, y),
369 &TextStyle {
370 color: Color::new(0.6, 0.6, 0.6, 1.0),
371 ..Default::default()
372 },
373 );
374 }
375 }
376 }
377}
378
379impl Brick for ConnectionsPanel {
380 fn brick_name(&self) -> &'static str {
381 "connections_panel"
382 }
383
384 fn assertions(&self) -> &[BrickAssertion] {
385 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(8)];
386 ASSERTIONS
387 }
388
389 fn budget(&self) -> BrickBudget {
390 BrickBudget::uniform(8)
391 }
392
393 fn verify(&self) -> BrickVerification {
394 BrickVerification {
395 passed: vec![BrickAssertion::max_latency_ms(8)],
396 failed: vec![],
397 verification_time: Duration::from_micros(25),
398 }
399 }
400
401 fn to_html(&self) -> String {
402 String::new()
403 }
404
405 fn to_css(&self) -> String {
406 String::new()
407 }
408}
409
410impl Widget for ConnectionsPanel {
411 fn type_id(&self) -> TypeId {
412 TypeId::of::<Self>()
413 }
414
415 fn measure(&self, constraints: Constraints) -> Size {
416 let header_lines = usize::from(self.show_headers);
417 let visible = self.visible_connections().count();
418 let height = ((header_lines + visible) as f32)
419 .max(1.0)
420 .min(constraints.max_height);
421 Size::new(constraints.max_width, height)
422 }
423
424 fn layout(&mut self, bounds: Rect) -> LayoutResult {
425 self.bounds = bounds;
426 LayoutResult {
427 size: Size::new(bounds.width, bounds.height),
428 }
429 }
430
431 fn paint(&self, canvas: &mut dyn Canvas) {
432 if self.bounds.width < 20.0 || self.bounds.height < 1.0 {
433 return;
434 }
435
436 let mut y = self.bounds.y;
437 let x = self.bounds.x;
438
439 if self.show_headers {
441 self.draw_header(canvas, x, y);
442 y += 1.0;
443 }
444
445 for conn in self.visible_connections() {
447 if y >= self.bounds.y + self.bounds.height {
448 break;
449 }
450 self.draw_connection(canvas, conn, x, y, self.bounds.width);
451 y += 1.0;
452 }
453
454 if self.connections.is_empty() {
456 canvas.draw_text(
457 "No connections",
458 Point::new(x, y),
459 &TextStyle {
460 color: Color::new(0.5, 0.5, 0.5, 1.0),
461 ..Default::default()
462 },
463 );
464 }
465 }
466
467 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
468 None
469 }
470
471 fn children(&self) -> &[Box<dyn Widget>] {
472 &[]
473 }
474
475 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
476 &mut []
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483
484 #[test]
485 fn test_tcp_state_short() {
486 assert_eq!(TcpState::Established.short(), "EST");
487 assert_eq!(TcpState::Listen.short(), "LSN");
488 assert_eq!(TcpState::TimeWait.short(), "TW");
489 }
490
491 #[test]
492 fn test_connection_entry_tcp() {
493 let conn = ConnectionEntry::tcp(443, "1.2.3.4", 52341).with_process("nginx", 1234);
494
495 assert_eq!(conn.local_port, 443);
496 assert_eq!(conn.remote_port, 52341);
497 assert_eq!(conn.process, Some("nginx".to_string()));
498 assert_eq!(conn.service_name(), "https");
499 }
500
501 #[test]
502 fn test_connection_entry_listen() {
503 let conn = ConnectionEntry::listen(8080);
504 assert_eq!(conn.state, TcpState::Listen);
505 assert_eq!(conn.remote_display(), "*");
506 assert_eq!(conn.service_name(), "http-alt");
507 }
508
509 #[test]
510 fn test_panel_counts() {
511 let mut panel = ConnectionsPanel::new();
512 panel.add_connection(ConnectionEntry::listen(80));
513 panel.add_connection(ConnectionEntry::listen(443));
514 panel.add_connection(ConnectionEntry::tcp(443, "1.2.3.4", 52341));
515
516 assert_eq!(panel.listening_count(), 2);
517 assert_eq!(panel.established_count(), 1);
518 }
519
520 #[test]
521 fn test_panel_builder() {
522 let panel = ConnectionsPanel::new()
523 .show_listening(false)
524 .show_established(true)
525 .max_connections(5)
526 .show_headers(false);
527
528 assert!(!panel.show_listening);
529 assert!(panel.show_established);
530 assert_eq!(panel.max_connections, 5);
531 assert!(!panel.show_headers);
532 }
533
534 #[test]
535 fn test_tcp_state_all_short() {
536 assert_eq!(TcpState::CloseWait.short(), "CW");
537 assert_eq!(TcpState::SynSent.short(), "SS");
538 assert_eq!(TcpState::SynRecv.short(), "SR");
539 assert_eq!(TcpState::FinWait1.short(), "FW1");
540 assert_eq!(TcpState::FinWait2.short(), "FW2");
541 assert_eq!(TcpState::LastAck.short(), "LA");
542 assert_eq!(TcpState::Closing.short(), "CLG");
543 assert_eq!(TcpState::Closed.short(), "CLD");
544 }
545
546 #[test]
547 fn test_tcp_state_colors() {
548 for state in [
550 TcpState::Established,
551 TcpState::Listen,
552 TcpState::TimeWait,
553 TcpState::CloseWait,
554 TcpState::SynSent,
555 TcpState::SynRecv,
556 TcpState::FinWait1,
557 TcpState::FinWait2,
558 TcpState::LastAck,
559 TcpState::Closing,
560 TcpState::Closed,
561 ] {
562 let color = state.color();
563 assert!(color.r >= 0.0 && color.r <= 1.0);
564 }
565 }
566
567 #[test]
568 fn test_connection_entry_service_names() {
569 assert_eq!(ConnectionEntry::listen(22).service_name(), "ssh");
570 assert_eq!(ConnectionEntry::listen(80).service_name(), "http");
571 assert_eq!(ConnectionEntry::listen(443).service_name(), "https");
572 assert_eq!(ConnectionEntry::listen(3306).service_name(), "mysql");
573 assert_eq!(ConnectionEntry::listen(5432).service_name(), "pgsql");
574 assert_eq!(ConnectionEntry::listen(6379).service_name(), "redis");
575 assert_eq!(ConnectionEntry::listen(27017).service_name(), "mongodb");
576 assert_eq!(ConnectionEntry::listen(9999).service_name(), "");
577 }
578
579 #[test]
580 fn test_connection_entry_with_local_addr() {
581 let conn = ConnectionEntry::listen(80).with_local_addr("127.0.0.1");
582 assert_eq!(conn.local_addr, "127.0.0.1");
583 }
584
585 #[test]
586 fn test_connection_entry_with_state() {
587 let conn = ConnectionEntry::tcp(443, "1.2.3.4", 12345).with_state(TcpState::TimeWait);
588 assert_eq!(conn.state, TcpState::TimeWait);
589 }
590
591 #[test]
592 fn test_connection_entry_local_display() {
593 let conn = ConnectionEntry::listen(8080);
594 assert_eq!(conn.local_display(), ":8080");
595 }
596
597 #[test]
598 fn test_connection_entry_remote_display_zero() {
599 let conn = ConnectionEntry::tcp(80, "0.0.0.0", 0);
600 assert_eq!(conn.remote_display(), "*");
601 }
602
603 #[test]
604 fn test_connection_entry_remote_display_normal() {
605 let conn = ConnectionEntry::tcp(443, "192.168.1.1", 54321);
606 assert_eq!(conn.remote_display(), "192.168.1.1:54321");
607 }
608
609 #[test]
610 fn test_connections_panel_with_connections() {
611 let connections = vec![
612 ConnectionEntry::listen(80),
613 ConnectionEntry::tcp(443, "1.2.3.4", 12345),
614 ];
615 let panel = ConnectionsPanel::new().with_connections(connections);
616 assert_eq!(panel.listening_count() + panel.established_count(), 2);
617 }
618
619 #[test]
620 fn test_connections_panel_brick_traits() {
621 let panel = ConnectionsPanel::new();
622 assert_eq!(panel.brick_name(), "connections_panel");
623 assert!(!panel.assertions().is_empty());
624 assert!(panel.budget().paint_ms > 0);
625 assert!(panel.verify().is_valid());
626 assert!(panel.to_html().is_empty());
627 assert!(panel.to_css().is_empty());
628 }
629
630 #[test]
631 fn test_connections_panel_widget_traits() {
632 let mut panel = ConnectionsPanel::new().with_connections(vec![ConnectionEntry::listen(80)]);
633
634 let size = panel.measure(Constraints {
636 min_width: 0.0,
637 min_height: 0.0,
638 max_width: 80.0,
639 max_height: 20.0,
640 });
641 assert!(size.width > 0.0);
642 assert!(size.height > 0.0);
643
644 let result = panel.layout(Rect::new(0.0, 0.0, 80.0, 10.0));
646 assert_eq!(result.size.width, 80.0);
647
648 assert_eq!(Widget::type_id(&panel), TypeId::of::<ConnectionsPanel>());
650
651 assert!(panel
653 .event(&Event::key_down(presentar_core::Key::Enter))
654 .is_none());
655
656 assert!(panel.children().is_empty());
658 assert!(panel.children_mut().is_empty());
659 }
660
661 #[test]
662 fn test_connections_panel_paint_with_header() {
663 use crate::direct::{CellBuffer, DirectTerminalCanvas};
664
665 let connections = vec![
666 ConnectionEntry::listen(80).with_process("nginx", 1234),
667 ConnectionEntry::tcp(443, "192.168.1.100", 54321).with_process("curl", 5678),
668 ConnectionEntry::tcp(3306, "10.0.0.1", 12345).with_state(TcpState::CloseWait),
669 ];
670
671 let mut panel = ConnectionsPanel::new()
672 .with_connections(connections)
673 .show_headers(true);
674
675 panel.layout(Rect::new(0.0, 0.0, 60.0, 10.0));
676
677 let mut buffer = CellBuffer::new(60, 10);
678 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
679 panel.paint(&mut canvas);
680 }
681
682 #[test]
683 fn test_connections_panel_paint_without_header() {
684 use crate::direct::{CellBuffer, DirectTerminalCanvas};
685
686 let connections = vec![ConnectionEntry::listen(443)];
687
688 let mut panel = ConnectionsPanel::new()
689 .with_connections(connections)
690 .show_headers(false);
691
692 panel.layout(Rect::new(0.0, 0.0, 60.0, 10.0));
693
694 let mut buffer = CellBuffer::new(60, 10);
695 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
696 panel.paint(&mut canvas);
697 }
698
699 #[test]
700 fn test_connections_panel_paint_empty() {
701 use crate::direct::{CellBuffer, DirectTerminalCanvas};
702
703 let mut panel = ConnectionsPanel::new();
704 panel.layout(Rect::new(0.0, 0.0, 60.0, 10.0));
705
706 let mut buffer = CellBuffer::new(60, 10);
707 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
708 panel.paint(&mut canvas);
709 }
710
711 #[test]
712 fn test_connections_panel_paint_small_bounds() {
713 use crate::direct::{CellBuffer, DirectTerminalCanvas};
714
715 let connections = vec![ConnectionEntry::listen(80)];
716 let mut panel = ConnectionsPanel::new().with_connections(connections);
717 panel.layout(Rect::new(0.0, 0.0, 10.0, 0.5)); let mut buffer = CellBuffer::new(10, 1);
720 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
721 panel.paint(&mut canvas); }
723
724 #[test]
725 fn test_connections_panel_long_process_name() {
726 use crate::direct::{CellBuffer, DirectTerminalCanvas};
727
728 let connections = vec![ConnectionEntry::tcp(443, "1.2.3.4", 12345)
729 .with_process("very_long_process_name_here", 1234)];
730
731 let mut panel = ConnectionsPanel::new().with_connections(connections);
732 panel.layout(Rect::new(0.0, 0.0, 60.0, 10.0));
733
734 let mut buffer = CellBuffer::new(60, 10);
735 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
736 panel.paint(&mut canvas);
737 }
738
739 #[test]
740 fn test_connections_panel_long_remote_address() {
741 use crate::direct::{CellBuffer, DirectTerminalCanvas};
742
743 let connections = vec![ConnectionEntry::tcp(
744 443,
745 "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
746 12345,
747 )];
748
749 let mut panel = ConnectionsPanel::new().with_connections(connections);
750 panel.layout(Rect::new(0.0, 0.0, 60.0, 10.0));
751
752 let mut buffer = CellBuffer::new(60, 10);
753 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
754 panel.paint(&mut canvas);
755 }
756
757 #[test]
758 fn test_connections_panel_filter_listening() {
759 let connections = vec![
760 ConnectionEntry::listen(80),
761 ConnectionEntry::tcp(443, "1.2.3.4", 12345),
762 ConnectionEntry::listen(8080),
763 ];
764
765 let panel = ConnectionsPanel::new()
766 .with_connections(connections)
767 .show_listening(false)
768 .show_established(true);
769
770 assert_eq!(panel.visible_connections().count(), 1);
771 }
772
773 #[test]
774 fn test_connections_panel_filter_established() {
775 let connections = vec![
776 ConnectionEntry::listen(80),
777 ConnectionEntry::tcp(443, "1.2.3.4", 12345),
778 ];
779
780 let panel = ConnectionsPanel::new()
781 .with_connections(connections)
782 .show_listening(true)
783 .show_established(false);
784
785 assert_eq!(panel.visible_connections().count(), 1);
786 }
787
788 #[test]
789 fn test_connections_panel_default() {
790 let panel = ConnectionsPanel::default();
791 assert!(panel.show_listening);
792 assert!(panel.show_established);
793 assert!(panel.show_headers);
794 assert_eq!(panel.max_connections, 10);
795 }
796
797 #[test]
798 fn test_tcp_state_default() {
799 let state = TcpState::default();
800 assert_eq!(state, TcpState::Established);
801 }
802
803 #[test]
804 fn test_connections_panel_other_states_visible() {
805 let connections = vec![
806 ConnectionEntry::tcp(443, "1.2.3.4", 12345).with_state(TcpState::TimeWait),
807 ConnectionEntry::tcp(443, "1.2.3.5", 12346).with_state(TcpState::CloseWait),
808 ];
809
810 let panel = ConnectionsPanel::new()
812 .with_connections(connections)
813 .show_listening(false)
814 .show_established(false);
815
816 assert_eq!(panel.visible_connections().count(), 2);
817 }
818
819 #[test]
820 fn test_connections_panel_unknown_port() {
821 use crate::direct::{CellBuffer, DirectTerminalCanvas};
822
823 let connections = vec![ConnectionEntry::listen(12345)];
825
826 let mut panel = ConnectionsPanel::new().with_connections(connections);
827 panel.layout(Rect::new(0.0, 0.0, 60.0, 10.0));
828
829 let mut buffer = CellBuffer::new(60, 10);
830 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
831 panel.paint(&mut canvas);
832 }
833}