Skip to main content

presentar_terminal/widgets/
connections_panel.rs

1//! `ConnectionsPanel` widget for TCP/UDP connection monitoring.
2//!
3//! Displays active network connections with state and process mapping.
4
5use 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/// TCP connection state.
13#[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    /// Get short display string.
31    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    /// Get state color.
48    pub fn color(&self) -> Color {
49        match self {
50            Self::Established => Color::new(0.4, 0.9, 0.4, 1.0), // Green
51            Self::Listen => Color::new(0.4, 0.6, 1.0, 1.0),      // Blue
52            Self::TimeWait => Color::new(0.6, 0.6, 0.6, 1.0),    // Gray
53            Self::CloseWait => Color::new(1.0, 0.8, 0.2, 1.0),   // Yellow
54            _ => Color::new(0.7, 0.7, 0.7, 1.0),                 // Default gray
55        }
56    }
57}
58
59/// A network connection entry.
60#[derive(Debug, Clone)]
61pub struct ConnectionEntry {
62    /// Protocol (tcp, udp).
63    pub protocol: String,
64    /// Local address.
65    pub local_addr: String,
66    /// Local port.
67    pub local_port: u16,
68    /// Remote address.
69    pub remote_addr: String,
70    /// Remote port.
71    pub remote_port: u16,
72    /// Connection state.
73    pub state: TcpState,
74    /// Process name (if available).
75    pub process: Option<String>,
76    /// Process ID.
77    pub pid: Option<u32>,
78}
79
80impl ConnectionEntry {
81    /// Create a new TCP connection.
82    #[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    /// Create a listening socket.
97    #[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    /// Set connection state.
112    #[must_use]
113    pub fn with_state(mut self, state: TcpState) -> Self {
114        self.state = state;
115        self
116    }
117
118    /// Set process info.
119    #[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    /// Set local address.
127    #[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    /// Get service name from port.
134    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    /// Format local endpoint.
149    pub fn local_display(&self) -> String {
150        format!(":{}", self.local_port)
151    }
152
153    /// Format remote endpoint.
154    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/// Connections panel displaying network connections.
164#[derive(Debug, Clone)]
165pub struct ConnectionsPanel {
166    /// Connection entries.
167    connections: Vec<ConnectionEntry>,
168    /// Show listening sockets.
169    show_listening: bool,
170    /// Show established connections.
171    show_established: bool,
172    /// Max connections to show.
173    max_connections: usize,
174    /// Show column headers.
175    show_headers: bool,
176    /// Cached bounds.
177    bounds: Rect,
178}
179
180impl Default for ConnectionsPanel {
181    fn default() -> Self {
182        Self::new()
183    }
184}
185
186impl ConnectionsPanel {
187    /// Create a new connections panel.
188    #[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    /// Add a connection.
201    pub fn add_connection(&mut self, connection: ConnectionEntry) {
202        self.connections.push(connection);
203    }
204
205    /// Set all connections.
206    #[must_use]
207    pub fn with_connections(mut self, connections: Vec<ConnectionEntry>) -> Self {
208        self.connections = connections;
209        self
210    }
211
212    /// Toggle listening sockets.
213    #[must_use]
214    pub fn show_listening(mut self, show: bool) -> Self {
215        self.show_listening = show;
216        self
217    }
218
219    /// Toggle established connections.
220    #[must_use]
221    pub fn show_established(mut self, show: bool) -> Self {
222        self.show_established = show;
223        self
224    }
225
226    /// Set max connections.
227    #[must_use]
228    pub fn max_connections(mut self, max: usize) -> Self {
229        self.max_connections = max;
230        self
231    }
232
233    /// Toggle headers.
234    #[must_use]
235    pub fn show_headers(mut self, show: bool) -> Self {
236        self.show_headers = show;
237        self
238    }
239
240    /// Get established count.
241    pub fn established_count(&self) -> usize {
242        self.connections
243            .iter()
244            .filter(|c| c.state == TcpState::Established)
245            .count()
246    }
247
248    /// Get listening count.
249    pub fn listening_count(&self) -> usize {
250        self.connections
251            .iter()
252            .filter(|c| c.state == TcpState::Listen)
253            .count()
254    }
255
256    /// Get visible connections (filtered).
257    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    /// Draw header row.
269    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    /// Draw a connection line.
282    fn draw_connection(
283        &self,
284        canvas: &mut dyn Canvas,
285        conn: &ConnectionEntry,
286        x: f32,
287        y: f32,
288        width: f32,
289    ) {
290        // Service name or port
291        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        // Local port
309        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        // Arrow
319        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        // Remote (truncated)
329        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        // State
347        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        // Process name (if available and fits)
357        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        // Draw header
440        if self.show_headers {
441            self.draw_header(canvas, x, y);
442            y += 1.0;
443        }
444
445        // Draw connections
446        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 no connections, show message
455        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        // Test all states return valid colors
549        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        // Measure
635        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        // Layout
645        let result = panel.layout(Rect::new(0.0, 0.0, 80.0, 10.0));
646        assert_eq!(result.size.width, 80.0);
647
648        // Type ID
649        assert_eq!(Widget::type_id(&panel), TypeId::of::<ConnectionsPanel>());
650
651        // Event
652        assert!(panel
653            .event(&Event::key_down(presentar_core::Key::Enter))
654            .is_none());
655
656        // Children
657        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)); // Too small
718
719        let mut buffer = CellBuffer::new(10, 1);
720        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
721        panel.paint(&mut canvas); // Should early return
722    }
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        // These are neither listening nor established, but should be visible
811        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        // Port with no service name
824        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}