diff --git a/CHANGELOG.md b/CHANGELOG.md index 60fc7624..fb96061f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 the selected process group ### Changed +- **TUI visual polish**: Activity uses compact TX/RX summaries, a roomier + process ranking, and a separate capture-quality sidebar on wide terminals. + Share bars use theme-derived gradients with textured ANSI fallbacks. + The default keeps TX blue / RX green; other themes retain their own traffic + tokens. Shared section rules and table headings improve hierarchy throughout + the TUI, with responsive navigation and NO_COLOR support. - **Staleness Cue**: idle connection rows now show a stripe at their left edge and a removal countdown in the bandwidth column from halfway through their timeout (previously 75%), both running yellow to red as cleanup diff --git a/USAGE.md b/USAGE.md index a6b9cf12..ac2b8ae1 100644 --- a/USAGE.md +++ b/USAGE.md @@ -866,9 +866,11 @@ The table adapts to the available terminal width, so narrower terminals hide som | **Remote** | Number of unique remote socket endpoints across the retained connections. An endpoint is an IP address plus port, so one host contacted on two ports counts as two remotes. Repeated connections to the same endpoint count once. A `+` suffix means the count exceeded the 256-destination display cap. | | **Top remote peer** | Remote endpoint with the most retained traffic in the selected direction. | -Traffic Pulse shows the current captured rate, but calculates coverage from captured bytes and interface-counter bytes over the same rolling 60-second window. This avoids the large fluctuations caused by comparing independently sampled instantaneous rates. Coverage divides the captured total by the interface total and caps the displayed percentage at 100%, because slightly different window endpoints or counter visibility can otherwise produce small overages. Both raw totals remain visible for diagnosis. When RustNet captures one named interface, it compares directly with that interface. With multi-interface capture, RustNet compares against a host-wide interface aggregate and prefixes the value with `~` because VPN and virtual interface counters can overlap. +The paired RX/TX summaries show the current captured rate, but calculate coverage from captured bytes and interface-counter bytes over the same rolling 60-second window. This avoids the large fluctuations caused by comparing independently sampled instantaneous rates. Coverage divides the captured total by the interface total and caps the displayed percentage at 100%, because slightly different window endpoints or counter visibility can otherwise produce small overages. Wide terminals show the captured totals in the summaries and the interface totals in the coverage sidebar. When RustNet captures one named interface, it compares directly with that interface. With multi-interface capture, RustNet compares against a host-wide interface aggregate and prefixes the value with `~` because VPN and virtual interface counters can overlap. -Press `d` to switch between Egress (TX, blue) and Ingress (RX, green), `s` to cycle the Activity sort metric, and `S` to reverse its order. The detailed interface table lives on the Host tab (press `5`, then `i`). +Wide terminals place capture coverage, process attribution, and interface rates in a sidebar, leaving the process table as the main view. Narrower terminals fold coverage and attribution into the summaries and hide the interface panel. Share bars blend the selected theme's neutral and traffic colours, ending at its TX/RX token rather than white. ANSI themes use shaded block textures in the terminal's own palette. + +Press `d` to switch between Egress (TX, blue) and Ingress (RX, green) in the default theme, `s` to cycle the Activity sort metric, and `S` to reverse its order. Other themes keep their own TX/RX colours. The detailed interface table lives on the Host tab (press `5`, then `i`). For a quick security review, sort Egress by the rolling or retained byte count, look for an unexpected high-volume process, and inspect its top remote peer. Retained traffic keeps a short-lived uploader visible after its socket closes. diff --git a/assets/screenshots/activity-redesign-catppuccin.png b/assets/screenshots/activity-redesign-catppuccin.png new file mode 100644 index 00000000..be65490a Binary files /dev/null and b/assets/screenshots/activity-redesign-catppuccin.png differ diff --git a/assets/screenshots/activity-redesign-compact.png b/assets/screenshots/activity-redesign-compact.png new file mode 100644 index 00000000..7c7d084b Binary files /dev/null and b/assets/screenshots/activity-redesign-compact.png differ diff --git a/assets/screenshots/activity-redesign-tokyo-night.png b/assets/screenshots/activity-redesign-tokyo-night.png new file mode 100644 index 00000000..b470539c Binary files /dev/null and b/assets/screenshots/activity-redesign-tokyo-night.png differ diff --git a/assets/screenshots/activity-redesign.png b/assets/screenshots/activity-redesign.png new file mode 100644 index 00000000..27da20de Binary files /dev/null and b/assets/screenshots/activity-redesign.png differ diff --git a/src/ui/connection_table.rs b/src/ui/connection_table.rs index b0cf604b..259f919f 100644 --- a/src/ui/connection_table.rs +++ b/src/ui/connection_table.rs @@ -391,7 +391,7 @@ pub(in crate::ui) fn build_header<'a>(columns: &[Column], ui_state: &UiState) -> let style = if active { theme::bold_underline_fg(theme::accent()) } else { - theme::fg(theme::heading()) + theme::bold_fg(theme::heading()) }; if col.id == ColumnId::Bandwidth { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 2b890e98..a02cbbfd 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -165,6 +165,14 @@ pub(crate) fn section_header<'a, T: Into>>( let mut line: Line = title.into(); line.spans .insert(0, Span::styled("▎", theme::fg(theme::accent()))); + // A quiet rule links each title to its panel without boxing in the data. + let remaining = usize::from(area.width).saturating_sub(line.width() + 1); + if remaining > 0 { + line.spans.push(Span::styled( + format!(" {}", "─".repeat(remaining)), + theme::fg(theme::faint()), + )); + } f.render_widget( Paragraph::new(line), Rect::new(area.x, area.y, area.width, 1), @@ -177,10 +185,9 @@ pub(crate) fn section_header<'a, T: Into>>( ) } -/// Bold default-foreground title span for [`section_header`]. +/// Theme-aware bold heading span for [`section_header`]. pub(crate) fn section_title(text: impl Into) -> ratatui::text::Span<'static> { - use ratatui::style::{Modifier, Style}; - ratatui::text::Span::styled(text.into(), Style::default().add_modifier(Modifier::BOLD)) + ratatui::text::Span::styled(text.into(), theme::bold_fg(theme::heading())) } /// Muted empty-state text filling a section that has nothing to show yet. @@ -2432,6 +2439,75 @@ mod snapshot_tests { insta::assert_snapshot!(render_activity(&app, ActivityDirection::Ingress)); } + #[test] + fn activity_tab_compact_terminal() { + let app = seeded_activity_app(); + let ui_state = UiState { + selected_tab: 2, + ..Default::default() + }; + let output = render_app(&app, &ui_state, &app.get_connections(), None, 80, 24); + assert!(output.contains("firefox (2001)")); + assert!(output.contains("Coverage")); + assert!(!output.contains("Share (60s)")); + insta::assert_snapshot!(output); + } + + #[test] + fn activity_tab_medium_terminal() { + let app = seeded_activity_app(); + let ui_state = UiState { + selected_tab: 2, + ..Default::default() + }; + let output = render_app(&app, &ui_state, &app.get_connections(), None, 110, 32); + assert!(output.contains("Top remote peer")); + assert!(output.contains("firefox (2001)")); + insta::assert_snapshot!(output); + } + + #[test] + fn activity_tab_empty_terminal() { + let app = test_app(); + let ui_state = UiState { + selected_tab: 2, + ..Default::default() + }; + let output = render_app(&app, &ui_state, &[], None, 80, 24); + assert!(output.contains("n/a")); + assert!(output.contains("Waiting for process traffic")); + insta::assert_snapshot!(output); + } + + #[test] + fn narrow_tabs_keep_all_five_shortcuts_visible_and_clickable() { + for width in [24, 40, 50, 60] { + let mut regions = ClickableRegions::default(); + let ui_state = UiState { + selected_tab: 4, + ..Default::default() + }; + let output = render(width, 2, |f| { + draw_tabs( + f, + &ui_state, + &CaptureCluster::default(), + f.area(), + &mut regions, + ) + }); + assert!( + output.lines().next().unwrap().contains('5'), + "width {width}" + ); + assert!( + (0..width) + .any(|x| matches!(regions.hit_test(x, 0), Some(ClickAction::SwitchTab(4)))), + "width {width}" + ); + } + } + #[test] fn graph_tab_empty_history() { let app = test_app(); diff --git a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__activity_tab_compact_terminal.snap b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__activity_tab_compact_terminal.snap new file mode 100644 index 00000000..80825551 --- /dev/null +++ b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__activity_tab_compact_terminal.snap @@ -0,0 +1,28 @@ +--- +source: src/ui/mod.rs +expression: output +--- + rustnet 1 Overview 2 Details 3 Activity 4 Graph 5 Host ● eth0 +─────────────────────────────────────━━━━━━━━━━───────────────────────────────── +▎ Egress (TX) ───────────────────────── ▎ Ingress (RX) ─────────────────────── +2.88 MB/s now 1.70 MB/s now + +Captured · 60s 5.85 MB Captured · 60s 3.67 MB +Retained 5.85 MB Retained 3.67 MB +Coverage · 60s 73.1% Coverage · 60s 91.8% +Attributed 100.0% Attributed 100.0% + +▎ Processes · Egress (TX) Retained TX ↓ ─────────────────────────────────────── +Process TX now 60s % Retained Conns + +firefox (2001) 2.38 MB/s 81.7% 4.78 MB 1/1 +░░░▒▒▒▓▓▓██▌·· +sshd (1500) 488.28 KB/s 17.7% 1.04 MB 1/1 +██▌··········· +systemd-resolved (8… 15.62 KB/s 0.5% 32.42 KB 1/1 +▏············· +curl (9876) - 0.0% 0 B 1/1 +·············· + + + d tx/rx s sort S order esc back h help q quit diff --git a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__activity_tab_empty_terminal.snap b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__activity_tab_empty_terminal.snap new file mode 100644 index 00000000..e45a37d4 --- /dev/null +++ b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__activity_tab_empty_terminal.snap @@ -0,0 +1,28 @@ +--- +source: src/ui/mod.rs +expression: output +--- + rustnet 1 Overview 2 Details 3 Activity 4 Graph 5 Host ● eth0 +─────────────────────────────────────━━━━━━━━━━───────────────────────────────── +▎ Egress (TX) ───────────────────────── ▎ Ingress (RX) ─────────────────────── +- now - now + +Captured · 60s 0 B Captured · 60s 0 B +Retained 0 B Retained 0 B +Coverage · 60s n/a Coverage · 60s n/a +Attributed 0.0% Attributed 0.0% + +▎ Processes · Egress (TX) Retained TX ↓ ─────────────────────────────────────── +Waiting for process traffic... + + + + + + + + + + + + d tx/rx s sort S order esc back h help q quit diff --git a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__activity_tab_medium_terminal.snap b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__activity_tab_medium_terminal.snap new file mode 100644 index 00000000..fda9847d --- /dev/null +++ b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__activity_tab_medium_terminal.snap @@ -0,0 +1,36 @@ +--- +source: src/ui/mod.rs +expression: output +--- + rustnet 1 Overview 2 Details 3 Activity 4 Graph 5 Host ● eth0 +─────────────────────────────────────━━━━━━━━━━─────────────────────────────────────────────────────────────── +▎ Egress (TX) ──────────────────────────────────────── ▎ Ingress (RX) ────────────────────────────────────── +2.88 MB/s now 1.70 MB/s now + +Captured · 60s 5.85 MB Captured · 60s 3.67 MB +Retained 5.85 MB Retained 3.67 MB +Coverage · 60s 73.1% Coverage · 60s 91.8% +Attributed 100.0% Attributed 100.0% + +▎ Processes · Egress (TX) Retained TX ↓ ───────────────────────────────────────────────────────────────────── +Process Share TX now 60s % Retained Conns Remote Top remote peer + +firefox (2001) ░░░░▒▒▒▓▓▓▓██▏·· 2.38 MB/s 81.7% 4.78 MB 1/1 1 140.82.121.4:443 + +sshd (1500) ██▉············· 488.28 KB/s 17.7% 1.04 MB 1/1 1 10.0.0.5:51022 + +systemd-resolved (8… ▏··············· 15.62 KB/s 0.5% 32.42 KB 1/1 1 1.1.1.1:53 + +curl (9876) ················ - 0.0% 0 B 1/1 1 151.101.1.195:443 + + + + + + + + + + + + d tx/rx s sort S order esc back h help q quit diff --git a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__activity_tab_process_egress.snap b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__activity_tab_process_egress.snap index e82d5878..035c3ee5 100644 --- a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__activity_tab_process_egress.snap +++ b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__activity_tab_process_egress.snap @@ -4,22 +4,27 @@ expression: "render_activity(&app, ActivityDirection::Egress)" --- rustnet 1 Overview 2 Details 3 Activity 4 Graph 5 Host ● eth0 ─────────────────────────────────────━━━━━━━━━━─────────────────────────────────────────────────────────────────────────────────────────────────────── -▎ Traffic Pulse -TX now 2.88 MB/s 60s captured 5.85 MB / eth0 8.00 MB 73.1% observed -TX 60s coverage ███████████████████████████████████████████████████████████████████████████████████████████████··································· -RX now 1.70 MB/s 60s captured 3.67 MB / eth0 4.00 MB 91.8% observed -RX 60s coverage ███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▍·········· -retained TX 5.85 MB RX 3.67 MB -process attribution TX 100.0% unknown 0 B RX 100.0% unknown 0 B -60s coverage = captured ÷ interface | Retained = active + recent closed | Attribution = mapped to PID/name +▎ Egress (TX) ─────────────────────────────────────── ▎ Ingress (RX) ────────────────────────────────────── ▎ Capture coverage · 60s ───────────── +2.88 MB/s now 1.70 MB/s now TX 73.1% + ░░░░░░░▒▒▒▒▒▒▒▓▓▓▓▓▓▓██████▊·········· +Captured · 60s 5.85 MB Captured · 60s 3.67 MB +Retained 5.85 MB Retained 3.67 MB RX 91.8% + ░░░░░░░░░▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓███████▉··· +▎ Processes · Egress (TX) Retained TX ↓ ──────────────────────────────────────────────────────────────────── eth0 TX 8.00 MB +Process Share TX now 60s % Retained Conns Remote Top remote peer eth0 RX 4.00 MB + +firefox (2001) ░░░░▒▒▒▓▓▓██▎·· 2.38 MB/s 81.7% 4.78 MB 1/1 1 140.82.121.4:443 ▎ Process attribution ──────────────── + TX 100.0% mapped +sshd (1500) ██▋············ 488.28 KB/s 17.7% 1.04 MB 1/1 1 10.0.0.5:51022 Unknown 0 B + +systemd-resolved (8… ▏·············· 15.62 KB/s 0.5% 32.42 KB 1/1 1 1.1.1.1:53 RX 100.0% mapped + Unknown 0 B +curl (9876) ··············· - 0.0% 0 B 1/1 1 151.101.1.195:443 + + ▎ Interfaces · TX ──────────────────── + eth0 ░░░▒▒▒▓▓▓██ ↑4.0M ↓2.0M -▎ Top Processes: Egress (TX) Retained TX ↓ -Process Pulse TX now Peak TX 60s % Iface 60s TX 60s Retained Conns Remote Top remote peer -firefox (2001) ███████████▌·· 2.38 MB/s 2.38 MB/s 81.7% 59.8% 4.78 MB 4.78 MB 1/1 1 140.82.121.4:443 -sshd (1500) ██▌··········· 488.28 KB/s 488.28 KB/s 17.7% 13.0% 1.04 MB 1.04 MB 1/1 1 10.0.0.5:51022 -systemd-resolved (820) ▏············· 15.62 KB/s 15.62 KB/s 0.5% 0.4% 32.42 KB 32.42 KB 1/1 1 1.1.1.1:53 -curl (9876) ·············· - - 0.0% 0.0% 0 B 0 B 1/1 1 151.101.1.195:443 @@ -32,11 +37,6 @@ curl (9876) ·············· - - -▎ Egress (TX) Share (60s) ▎ Interface Pulse: TX -firefox (2001) ███████████████████████████████████████████████▍·········· 81.7% eth0 ████████████████████████████████████ ↑4.0M ↓2.0M -sshd (1500) ██████████▎··············································· 17.7% -systemd-resolved… ▍························································· 0.5% -curl (9876) ·························································· 0.0% diff --git a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__activity_tab_process_ingress.snap b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__activity_tab_process_ingress.snap index 42b2b741..3c15fc89 100644 --- a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__activity_tab_process_ingress.snap +++ b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__activity_tab_process_ingress.snap @@ -4,22 +4,27 @@ expression: "render_activity(&app, ActivityDirection::Ingress)" --- rustnet 1 Overview 2 Details 3 Activity 4 Graph 5 Host ● eth0 ─────────────────────────────────────━━━━━━━━━━─────────────────────────────────────────────────────────────────────────────────────────────────────── -▎ Traffic Pulse -TX now 2.88 MB/s 60s captured 5.85 MB / eth0 8.00 MB 73.1% observed -TX 60s coverage ███████████████████████████████████████████████████████████████████████████████████████████████··································· -RX now 1.70 MB/s 60s captured 3.67 MB / eth0 4.00 MB 91.8% observed -RX 60s coverage ███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▍·········· -retained TX 5.85 MB RX 3.67 MB -process attribution TX 100.0% unknown 0 B RX 100.0% unknown 0 B -60s coverage = captured ÷ interface | Retained = active + recent closed | Attribution = mapped to PID/name +▎ Egress (TX) ─────────────────────────────────────── ▎ Ingress (RX) ────────────────────────────────────── ▎ Capture coverage · 60s ───────────── +2.88 MB/s now 1.70 MB/s now TX 73.1% + ░░░░░░░▒▒▒▒▒▒▒▓▓▓▓▓▓▓██████▊·········· +Captured · 60s 5.85 MB Captured · 60s 3.67 MB +Retained 5.85 MB Retained 3.67 MB RX 91.8% + ░░░░░░░░░▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓███████▉··· +▎ Processes · Ingress (RX) Retained RX ↓ ─────────────────────────────────────────────────────────────────── eth0 TX 8.00 MB +Process Share RX now 60s % Retained Conns Remote Top remote peer eth0 RX 4.00 MB + +firefox (2001) ░░░░▒▒▒▓▓▓██▋·· 1.43 MB/s 84.1% 3.09 MB 1/1 1 140.82.121.4:443 ▎ Process attribution ──────────────── + TX 100.0% mapped +sshd (1500) ██▏············ 244.14 KB/s 14.1% 529.30 KB 1/1 1 10.0.0.5:51022 Unknown 0 B + +systemd-resolved (8… ▎·············· 31.25 KB/s 1.8% 65.82 KB 1/1 1 1.1.1.1:53 RX 100.0% mapped + Unknown 0 B +curl (9876) ··············· - 0.0% 1.50 KB 1/1 1 151.101.1.195:443 + + ▎ Interfaces · RX ──────────────────── + eth0 ░░░▒▒▒▓▓▓██ ↑4.0M ↓2.0M -▎ Top Processes: Ingress (RX) Retained RX ↓ -Process Pulse RX now Peak RX 60s % Iface 60s RX 60s Retained Conns Remote Top remote peer -firefox (2001) ███████████▊·· 1.43 MB/s 1.43 MB/s 84.1% 77.2% 3.09 MB 3.09 MB 1/1 1 140.82.121.4:443 -sshd (1500) ██············ 244.14 KB/s 244.14 KB/s 14.1% 12.9% 529.30 KB 529.30 KB 1/1 1 10.0.0.5:51022 -systemd-resolved (820) ▎············· 31.25 KB/s 31.25 KB/s 1.8% 1.6% 65.82 KB 65.82 KB 1/1 1 1.1.1.1:53 -curl (9876) ·············· - - 0.0% 0.0% 1.50 KB 1.50 KB 1/1 1 151.101.1.195:443 @@ -32,11 +37,6 @@ curl (9876) ·············· - - -▎ Ingress (RX) Share (60s) ▎ Interface Pulse: RX -firefox (2001) ████████████████████████████████████████████████▊········· 84.1% eth0 ████████████████████████████████████ ↑4.0M ↓2.0M -sshd (1500) ████████▏················································· 14.1% -systemd-resolved… █························································· 1.8% -curl (9876) ·························································· 0.0% diff --git a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_arp_application_card.snap b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_arp_application_card.snap index 45a3421e..d6a1901e 100644 --- a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_arp_application_card.snap +++ b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_arp_application_card.snap @@ -10,7 +10,7 @@ expression: output -▎ ? → 192.168.1.1:0 [ARP_WHO_HAS 192.168.1.1 (Apple, Inc.)] · click a field to copy +▎ ? → 192.168.1.1:0 [ARP_WHO_HAS 192.168.1.1 (Apple, Inc.)] · click a field to copy ──────────────────────────────────────────────────────── Connection Application: ARP ▐ Protocol ARP Operation Request ▐ Status Active (last seen ago) Sender MAC 68:5e:dd:09:15:5e ▐ @@ -34,7 +34,7 @@ City - ASN - -▎ Traffic Statistics +▎ Traffic Statistics ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ↓ RX - → peak - ↑ TX - → peak - Total 0 B · 0 packets Total 0 B · 0 packets ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ diff --git a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_dns_response_application_card.snap b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_dns_response_application_card.snap index 048b6df1..bb6af0f4 100644 --- a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_dns_response_application_card.snap +++ b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_dns_response_application_card.snap @@ -10,7 +10,7 @@ expression: output -▎ proc → 203.0.113.7:4433 [DNS_RESPONSE] · click a field to copy +▎ proc → 203.0.113.7:4433 [DNS_RESPONSE] · click a field to copy ─────────────────────────────────────────────────────────────────────────── Connection Application: DNS ▐ Protocol UDP DNS Query example.com ▐ Status Active (last seen ago) DNS Type A ▐ @@ -34,7 +34,7 @@ City - ASN - -▎ Traffic Statistics +▎ Traffic Statistics ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ↓ RX - → peak - ↑ TX - → peak - Total 0 B · 0 packets Total 0 B · 0 packets ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ diff --git a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_http_application_card.snap b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_http_application_card.snap index 0b8e05a8..c63b64b3 100644 --- a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_http_application_card.snap +++ b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_http_application_card.snap @@ -10,7 +10,7 @@ expression: output -▎ proc → 203.0.113.7:4433 [ESTABLISHED] · click a field to copy +▎ proc → 203.0.113.7:4433 [ESTABLISHED] · click a field to copy ──────────────────────────────────────────────────────────────────────────── Connection Application: HTTP ▐ Protocol TCP HTTP Version HTTP/1.1 ▐ Status Active (last seen ago) HTTP Method GET ▐ @@ -34,7 +34,7 @@ City - ASN - -▎ Traffic Statistics +▎ Traffic Statistics ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ↓ RX - → peak - ↑ TX - → peak - Total 0 B · 0 packets Total 0 B · 0 packets ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ diff --git a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_https_without_tls_info.snap b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_https_without_tls_info.snap index 28955dfd..6e848dfa 100644 --- a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_https_without_tls_info.snap +++ b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_https_without_tls_info.snap @@ -10,7 +10,7 @@ expression: output -▎ proc → 203.0.113.7:4433 [ESTABLISHED] · click a field to copy +▎ proc → 203.0.113.7:4433 [ESTABLISHED] · click a field to copy ──────────────────────────────────────────────────────────────────────────── Connection Application: HTTPS ▐ Protocol TCP SNI - ▐ Status Active (last seen ago) ALPN - ▐ @@ -34,7 +34,7 @@ City - ASN - -▎ Traffic Statistics +▎ Traffic Statistics ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ↓ RX - → peak - ↑ TX - → peak - Total 0 B · 0 packets Total 0 B · 0 packets ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ diff --git a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_icmp_echo_application_card.snap b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_icmp_echo_application_card.snap index 8803f840..2a4e920f 100644 --- a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_icmp_echo_application_card.snap +++ b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_icmp_echo_application_card.snap @@ -10,7 +10,7 @@ expression: output -▎ ping → 8.8.8.8:0 [ECHO_REQ(4660)] [RTT 8.7ms] · click a field to copy +▎ ping → 8.8.8.8:0 [ECHO_REQ(4660)] [RTT 8.7ms] · click a field to copy ──────────────────────────────────────────────────────────────────── Connection Application: ICMP ▐ Protocol ICMP Message Echo Request ▐ Status Active (last seen ago) Echo ID 4660 ▐ @@ -34,7 +34,7 @@ City - ASN - -▎ Traffic Statistics +▎ Traffic Statistics ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ↓ RX - → peak - ↑ TX - → peak - Total 0 B · 0 packets Total 0 B · 0 packets ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ diff --git a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_icmpv6_ndp_application_card.snap b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_icmpv6_ndp_application_card.snap index a7e066cb..be3b86a4 100644 --- a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_icmpv6_ndp_application_card.snap +++ b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_icmpv6_ndp_application_card.snap @@ -10,7 +10,7 @@ expression: output -▎ ? → [fe80::2]:0 [ICMP_OTHER] · click a field to copy +▎ ? → [fe80::2]:0 [ICMP_OTHER] · click a field to copy ───────────────────────────────────────────────────────────────────────────────────── Connection Application: ICMPv6 ▐ Protocol ICMP Message Neighbor Advertisement ▐ Status Active (last seen ago) Echo ID - ▐ @@ -34,7 +34,7 @@ City - ASN - -▎ Traffic Statistics +▎ Traffic Statistics ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ↓ RX - → peak - ↑ TX - → peak - Total 0 B · 0 packets Total 0 B · 0 packets ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ diff --git a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_igmp_application_card.snap b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_igmp_application_card.snap index 4ae91a9b..8832669d 100644 --- a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_igmp_application_card.snap +++ b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_igmp_application_card.snap @@ -10,7 +10,7 @@ expression: output -▎ ? → 224.0.0.251:0 [REPORT_V2(224.0.0.251)] · click a field to copy +▎ ? → 224.0.0.251:0 [REPORT_V2(224.0.0.251)] · click a field to copy ─────────────────────────────────────────────────────────────────────── Connection Application: IGMP ▐ Protocol IGMP Message Membership Report v2 ▐ Status Active (last seen ago) Group Address 224.0.0.251 ▐ @@ -34,7 +34,7 @@ City - ASN - -▎ Traffic Statistics +▎ Traffic Statistics ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ↓ RX - → peak - ↑ TX - → peak - Total 0 B · 0 packets Total 0 B · 0 packets ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ diff --git a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_ntp_application_card.snap b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_ntp_application_card.snap index c8b1567b..2412ce0d 100644 --- a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_ntp_application_card.snap +++ b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_ntp_application_card.snap @@ -10,7 +10,7 @@ expression: output -▎ proc → 203.0.113.7:4433 [NTP] · click a field to copy +▎ proc → 203.0.113.7:4433 [NTP] · click a field to copy ──────────────────────────────────────────────────────────────────────────────────── Connection Application: NTP ▐ Protocol UDP NTP Version 4 ▐ Status Active (last seen ago) NTP Mode Server ▐ @@ -34,7 +34,7 @@ City - ASN - -▎ Traffic Statistics +▎ Traffic Statistics ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ↓ RX - → peak - ↑ TX - → peak - Total 0 B · 0 packets Total 0 B · 0 packets ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ diff --git a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_ssh_application_card.snap b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_ssh_application_card.snap index 8605fb7c..fe042a16 100644 --- a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_ssh_application_card.snap +++ b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_ssh_application_card.snap @@ -10,7 +10,7 @@ expression: output -▎ proc → 203.0.113.7:4433 [ESTABLISHED] · click a field to copy +▎ proc → 203.0.113.7:4433 [ESTABLISHED] · click a field to copy ──────────────────────────────────────────────────────────────────────────── Connection Application: SSH ▐ Protocol TCP SSH Version SSH-2 ▐ Status Active (last seen ago) Connection State Established ▐ @@ -34,7 +34,7 @@ City - ASN - -▎ Traffic Statistics +▎ Traffic Statistics ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ↓ RX - → peak - ↑ TX - → peak - Total 0 B · 0 packets Total 0 B · 0 packets ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ diff --git a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_tcp_https.snap b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_tcp_https.snap index 4dc42bfb..a945eb02 100644 --- a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_tcp_https.snap +++ b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_tcp_https.snap @@ -10,7 +10,7 @@ expression: output systemd-resolved (82… 1.1.1.1:53 192.168.1.10:53 dns UDP UDP_ACTIVE - - -/- sshd (1500) 10.0.0.5:51022 192.168.1.10:22 ssh TCP ESTABLISHED - ok -/- -▎ firefox → 140.82.121.4:443 [ESTABLISHED] · click a field to copy +▎ firefox → 140.82.121.4:443 [ESTABLISHED] · click a field to copy ───────────────────────────────────────────────────────────────────────── Connection Application ▐ Protocol TCP Detected - ▐ Status Active (last seen ago) ▐ @@ -34,7 +34,7 @@ City - ASN - -▎ Traffic Statistics +▎ Traffic Statistics ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ↓ RX - → peak - ↑ TX - → peak - Total 234.38 KB · 234 packets Total 12.21 KB · 12 packets ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ diff --git a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__graph_tab_empty_history.snap b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__graph_tab_empty_history.snap index 7f53fec7..ed8bffc1 100644 --- a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__graph_tab_empty_history.snap +++ b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__graph_tab_empty_history.snap @@ -4,7 +4,7 @@ expression: output --- rustnet 1 Overview 2 Details 3 Activity 4 Graph 5 Host ● eth0 ──────────────────────────────────────────────────━━━━━━━─────────────────────────────────────────────────────────────────────────────────── -▎ Traffic Over Time (60s) ▎ Connection Lifecycle +▎ Traffic Over Time (60s) ────────────────────────────────────────────────────────────────────── ▎ Connection Lifecycle ─────────────────── Waiting for traffic data... Waiting for connection data... @@ -18,7 +18,7 @@ Waiting for traffic data... -▎ Observed Network Health ▎ TCP Counters ▎ Observed TCP States +▎ Observed Network Health ───────────────────── ▎ TCP Counters ──────────────────────────────── ▎ Observed TCP States ──────────────────── Waiting for health data... Retransmits 0 ESTAB █████████████████████████ 2 Out of Order 0 TIME_WAIT ████████████ 1 Fast Retrans 0 @@ -29,7 +29,7 @@ Waiting for health data... Retransmits 0 -▎ Application Distribution ▎ Top Processes +▎ Application Distribution ────────────────────────────────────────── ▎ Top Processes ───────────────────────────────────────────────────── Other ██████████████████████████████████████████████████████ 100.0% No active processes diff --git a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__graph_tab_small_terminal_drops_bottom_sections.snap b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__graph_tab_small_terminal_drops_bottom_sections.snap index 44b1efc6..afb3c485 100644 --- a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__graph_tab_small_terminal_drops_bottom_sections.snap +++ b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__graph_tab_small_terminal_drops_bottom_sections.snap @@ -1,11 +1,10 @@ --- source: src/ui/mod.rs -assertion_line: 2426 expression: output --- rustnet 1 Overview 2 Details 3 Activity 4 Graph 5 Host ● eth0 ──────────────────────────────────────────────────━━━━━━━─────────────────────── -▎ Traffic Over Time (60s) ▎ Connection Lifecycle +▎ Traffic Over Time (60s) ──────────────────────────── ▎ Connection Lifecycle ─ Waiting for traffic data... Waiting for connection d @@ -16,7 +15,7 @@ Waiting for traffic data... Waiting for connection d -▎ Observed Network Health ▎ TCP Counters ▎ Observed TCP States +▎ Observed Network Health ▎ TCP Counters ─────────── ▎ Observed TCP States ── Waiting for health data... Retransmits 0 ESTAB ███████ 2 Out of Order 0 TIME_WAIT ███ 1 Fast Retrans 0 diff --git a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__host_interface_details.snap b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__host_interface_details.snap index 6c2c7846..241008d0 100644 --- a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__host_interface_details.snap +++ b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__host_interface_details.snap @@ -6,7 +6,7 @@ expression: output ────────────────────────────────────────────────────────────━━━━━━────────────────────────────────────────────────────────────────────────── Host Sockets · Interfaces -▎ Interface Statistics +▎ Interface Statistics ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Interface RX Rate TX Rate RX Packets TX Packets RX Err TX Err RX Drop TX Drop Collisions eth0 512.00 KB/s 128.00 KB/s 1200000 800000 0 0 12 0 0 diff --git a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__host_socket_inventory.snap b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__host_socket_inventory.snap index 191b2deb..88b17067 100644 --- a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__host_socket_inventory.snap +++ b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__host_socket_inventory.snap @@ -6,13 +6,13 @@ expression: output ────────────────────────────────────────────────────────────━━━━━━────────────────────────────────────────────────────────────────────────── Host Sockets · Interfaces -▎ Host Socket States +▎ Host Socket States ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── TCP 2 LISTEN 1 ESTAB 1 OPENING 0 CLOSING 0 TIME_WAIT 0 OTHER 0 UDP BOUND 1 Observed RTT 1 samples average 24ms max 24ms -▎ Listening and Bound Endpoints 2 rows +▎ Listening and Bound Endpoints 2 rows ──────────────────────────────────────────────────────────────────────────────────────────────────── Proto State Local endpoint Peer Service PID Process TCP LISTEN 0.0.0.0:8080 - webcache 4242 web-server UDP BOUND 127.0.0.1:53 - domain 53 resolver diff --git a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__overview_live_connections.snap b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__overview_live_connections.snap index 2ad28a75..d73001f7 100644 --- a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__overview_live_connections.snap +++ b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__overview_live_connections.snap @@ -4,7 +4,7 @@ expression: render_overview(false) --- rustnet 1 Overview 2 Details 3 Activity 4 Graph 5 Host ● eth0 ────────────━━━━━━━━━━────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -▎ Live Connections +▎ Live Connections ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Process Remote Local Service App State RTT Hlth Rx↓/Tx↑ ▌ firefox (2001) 140.82.121.4:443 192.168.1.10:51234 https TCP ESTABLISHED 23ms R3/O1 3.1M/928K diff --git a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__overview_process_aggregate.snap b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__overview_process_aggregate.snap index 3b4deb02..0c8830f3 100644 --- a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__overview_process_aggregate.snap +++ b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__overview_process_aggregate.snap @@ -4,7 +4,7 @@ expression: render_overview(true) --- rustnet 1 Overview 2 Details 3 Activity 4 Graph 5 Host ● eth0 ────────────━━━━━━━━━━────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -▎ Process Aggregate +▎ Process Aggregate ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Process Remote Local Service App State RTT Hlth Rx↓/Tx↑ ▌▸ curl (1) TCP:1 UDP:0 -/- diff --git a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__overview_stale_connection.snap b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__overview_stale_connection.snap index 6c775556..47edd05a 100644 --- a/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__overview_stale_connection.snap +++ b/src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__overview_stale_connection.snap @@ -4,7 +4,7 @@ expression: output --- rustnet 1 Overview 2 Details 3 Activity 4 Graph 5 Host ● eth0 ────────────━━━━━━━━━━────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -▎ Live Connections +▎ Live Connections ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Process Remote Local Service App State RTT Hlth Rx↓/Tx↑ ▌▎firefox (2001) 140.82.121.4:443 192.168.1.10:51234 https TCP·HTTPS ESTABLISHED 23ms R3/O1 2m left diff --git a/src/ui/tabs/activity.rs b/src/ui/tabs/activity.rs index f2d9be36..ef4c885f 100644 --- a/src/ui/tabs/activity.rs +++ b/src/ui/tabs/activity.rs @@ -1,18 +1,18 @@ -//! Activity tab: retained process traffic, glowing traffic-share bars, +//! Activity tab: retained process traffic, directional traffic-share bars, //! and attribution coverage. use anyhow::Result; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use ratatui::{ Frame, - layout::{Constraint, Direction, Layout, Rect}, + layout::{Constraint, Layout, Rect}, style::{Color, Style}, text::{Line, Span}, widgets::{Cell, Paragraph, Row, Table}, }; use crate::app::App; -use crate::network::process_activity::{ProcessActivity, ProcessActivitySnapshot, ProcessIdentity}; +use crate::network::process_activity::{ProcessActivity, ProcessActivitySnapshot}; use crate::ui::{ ActivityDirection, ActivitySort, ClickableRegions, Component, ComponentContext, Effect, HandlerContext, UiState, draw_placeholder, @@ -98,210 +98,213 @@ pub(in crate::ui) fn draw_activity( let snapshot = app.get_process_activity_snapshot(); let basis = interface_basis(app); - let chunks = Layout::default() - .direction(Direction::Vertical) - .spacing(1) - .constraints([ - Constraint::Length(9), - Constraint::Min(8), + // Keep capture quality separate from process ranking on wide terminals; + // narrow terminals fold the essentials into the traffic summaries. + let sidebar = area.width >= 140 && area.height >= 22; + let columns = Layout::horizontal([ + Constraint::Min(0), + Constraint::Length(if sidebar { 38 } else { 0 }), + ]) + .spacing(if sidebar { 3 } else { 0 }) + .split(area); + let main = Layout::vertical([ + Constraint::Length(if sidebar { 6 } else { 7 }), + Constraint::Min(5), + ]) + .spacing(1) + .split(columns[0]); + let summary = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) + .spacing(3) + .split(main[0]); + draw_direction_pulse( + f, + &snapshot, + &basis, + ActivityDirection::Egress, + sidebar, + summary[0], + ); + draw_direction_pulse( + f, + &snapshot, + &basis, + ActivityDirection::Ingress, + sidebar, + summary[1], + ); + draw_process_table(f, &snapshot, &basis, ui_state, main[1]); + if sidebar { + let panels = Layout::vertical([ Constraint::Length(9), + Constraint::Length(7), + Constraint::Min(4), ]) - .split(area); - - draw_traffic_pulse(f, &snapshot, &basis, chunks[0]); - draw_process_table(f, &snapshot, &basis, ui_state, chunks[1]); - - let bottom = Layout::default() - .direction(Direction::Horizontal) - .spacing(2) - .constraints([Constraint::Percentage(58), Constraint::Percentage(42)]) - .split(chunks[2]); - draw_traffic_share(f, &snapshot, ui_state.activity_direction, bottom[0]); - draw_interface_pulse(f, app, ui_state.activity_direction, bottom[1]); - + .spacing(1) + .split(columns[1]); + draw_coverage(f, &snapshot, &basis, panels[0]); + draw_attribution(f, &snapshot, panels[1]); + draw_interface_pulse(f, app, ui_state.activity_direction, panels[2]); + } Ok(()) } -/// The "now / 60s captured" summary line and the coverage bar for one -/// direction. The TX and RX rows differ only in labels, colors, and -/// which counters they read, so both come from here. -fn pulse_lines( +fn metric_line(label: &str, value: String, style: Style, width: u16) -> Line<'static> { + let value_width = Span::raw(&value).width(); + let label_width = usize::from(width).saturating_sub(value_width + 1); + let label = truncate_with_ellipsis(label, label_width); + let gap = usize::from(width).saturating_sub(Span::raw(&label).width() + value_width); + Line::from(vec![ + Span::styled(label, theme::fg(theme::muted())), + Span::raw(" ".repeat(gap)), + Span::styled(value, style), + ]) +} + +fn draw_direction_pulse( + f: &mut Frame, snapshot: &ProcessActivitySnapshot, basis: &InterfaceBasis, direction: ActivityDirection, - bar_width: usize, -) -> [Line<'static>; 2] { - let fraction = coverage_fraction( - snapshot_window_bytes(snapshot, direction), - interface_window_bytes(basis, direction), + sidebar: bool, + area: Rect, +) { + let inner = section_header( + f, + area, + section_title(format!(" {}", direction.display_name_with_rate())), ); - let basis_marker = if basis.exact { "" } else { "~" }; - let label = direction.rate_label(); - - let summary = Line::from(vec![ - Span::styled(format!("{label} now "), theme::fg(theme::muted())), - Span::styled( - format_rate(snapshot_current_bps(snapshot, direction)), - theme::bold_fg(direction_ramp(direction)(1.0)), - ), - Span::styled(" 60s captured ", theme::fg(theme::muted())), - Span::styled( - format_bytes(snapshot_window_bytes(snapshot, direction)), - theme::bold_fg(direction_color(direction)), - ), - Span::styled(" / ", theme::fg(theme::muted())), - Span::styled( - format!( - "{} {}", - basis.label, - format_bytes(interface_window_bytes(basis, direction)) + let color = direction_color(direction); + let captured = snapshot_window_bytes(snapshot, direction); + let retained = direction.pick(snapshot.retained_tx_bytes, snapshot.retained_rx_bytes); + let mut lines = vec![ + Line::from(vec![ + Span::styled( + format_rate(snapshot_current_bps(snapshot, direction)), + theme::bold_fg(color), ), - theme::fg(theme::accent()), + Span::styled(" now", theme::fg(theme::muted())), + ]), + Line::default(), + metric_line( + "Captured · 60s", + format_bytes(captured), + theme::fg(theme::text()), + inner.width, ), - Span::styled( - coverage_label(fraction, basis_marker), - theme::fg(if fraction.is_some_and(|fraction| fraction > 1.05) { - theme::warn() - } else { - theme::muted() - }), + metric_line( + "Retained", + format_bytes(retained), + theme::fg(theme::text()), + inner.width, ), - ]); - - let mut spans = vec![Span::styled( - format!("{label} 60s coverage "), - theme::fg(theme::muted()), - )]; - spans.extend(glow_bar::spans( - fraction.unwrap_or_default(), - bar_width, - direction_ramp(direction), - )); + ]; + if !sidebar { + let coverage = coverage_text( + coverage_fraction(captured, interface_window_bytes(basis, direction)), + basis.exact, + ); + lines.push(metric_line( + "Coverage · 60s", + coverage, + theme::fg(color), + inner.width, + )); + let percentage = + direction.pick(snapshot.tx_attribution_pct(), snapshot.rx_attribution_pct()); + lines.push(metric_line( + "Attributed", + format!("{percentage:.1}%"), + attribution_style(percentage), + inner.width, + )); + } + f.render_widget(Paragraph::new(lines), inner); +} - [summary, Line::from(spans)] +fn coverage_text(fraction: Option, exact: bool) -> String { + fraction.map_or_else( + || "n/a".to_string(), + |f| format!("{}{:.1}%", if exact { "" } else { "~" }, f * 100.0), + ) } -fn draw_traffic_pulse( +fn draw_coverage( f: &mut Frame, snapshot: &ProcessActivitySnapshot, basis: &InterfaceBasis, area: Rect, ) { - let inner = section_header(f, area, section_title(" Traffic Pulse")); - if inner.height == 0 { - return; - } - - let unknown_tx = snapshot - .retained_tx_bytes - .saturating_sub(snapshot.attributed_tx_bytes); - let unknown_rx = snapshot - .retained_rx_bytes - .saturating_sub(snapshot.attributed_rx_bytes); - - let bar_width = inner.width.saturating_sub(20) as usize; - let [tx_line, tx_bar] = pulse_lines(snapshot, basis, ActivityDirection::Egress, bar_width); - let [rx_line, rx_bar] = pulse_lines(snapshot, basis, ActivityDirection::Ingress, bar_width); - - f.render_widget( - Paragraph::new(tx_line), - Rect::new(inner.x, inner.y, inner.width, 1), - ); - - if inner.height > 1 { - f.render_widget( - Paragraph::new(tx_bar), - Rect::new(inner.x, inner.y + 1, inner.width, 1), - ); - } - - if inner.height > 2 { - f.render_widget( - Paragraph::new(rx_line), - Rect::new(inner.x, inner.y + 2, inner.width, 1), - ); - } - - if inner.height > 3 { - f.render_widget( - Paragraph::new(rx_bar), - Rect::new(inner.x, inner.y + 3, inner.width, 1), - ); - } - - if inner.height > 4 { - let retained = Line::from(vec![ - Span::styled("retained TX ", theme::fg(theme::muted())), - Span::styled( - format_bytes(snapshot.retained_tx_bytes), - theme::bold_fg(theme::tx()), - ), - Span::styled(" RX ", theme::fg(theme::muted())), - Span::styled( - format_bytes(snapshot.retained_rx_bytes), - theme::bold_fg(theme::rx()), - ), - ]); - f.render_widget( - Paragraph::new(retained), - Rect::new(inner.x, inner.y + 4, inner.width, 1), - ); - } - - if inner.height > 5 { - let attribution = Line::from(vec![ - Span::styled("process attribution TX ", theme::fg(theme::muted())), - Span::styled( - format!("{:.1}%", snapshot.tx_attribution_pct()), - attribution_style(snapshot.tx_attribution_pct()), - ), - Span::styled( - format!(" unknown {}", format_bytes(unknown_tx)), - unknown_style(unknown_tx), - ), - Span::styled(" RX ", theme::fg(theme::muted())), - Span::styled( - format!("{:.1}%", snapshot.rx_attribution_pct()), - attribution_style(snapshot.rx_attribution_pct()), - ), - Span::styled( - format!(" unknown {}", format_bytes(unknown_rx)), - unknown_style(unknown_rx), - ), - ]); - f.render_widget( - Paragraph::new(attribution), - Rect::new(inner.x, inner.y + 5, inner.width, 1), + let inner = section_header(f, area, section_title(" Capture coverage · 60s")); + let mut lines = Vec::new(); + for direction in [ActivityDirection::Egress, ActivityDirection::Ingress] { + let color = direction_color(direction); + let fraction = coverage_fraction( + snapshot_window_bytes(snapshot, direction), + interface_window_bytes(basis, direction), ); + lines.push(metric_line( + direction.rate_label(), + coverage_text(fraction, basis.exact), + theme::fg(color), + inner.width, + )); + lines.push(Line::from(glow_bar::themed_spans( + fraction.unwrap_or_default(), + usize::from(inner.width), + color, + ))); + lines.push(Line::default()); } + lines.push(metric_line( + &format!("{} TX", basis.label), + format_bytes(basis.tx_window_bytes), + theme::fg(theme::muted()), + inner.width, + )); + lines.push(metric_line( + &format!("{} RX", basis.label), + format_bytes(basis.rx_window_bytes), + theme::fg(theme::muted()), + inner.width, + )); + f.render_widget(Paragraph::new(lines), inner); +} - if inner.height > 6 { - let legend = Line::from(vec![ - Span::styled("60s coverage", theme::fg(theme::accent())), - Span::styled(" = captured ÷ interface | ", theme::fg(theme::muted())), - Span::styled("Retained", theme::fg(theme::accent())), - Span::styled(" = active + recent closed | ", theme::fg(theme::muted())), - Span::styled("Attribution", theme::fg(theme::accent())), - Span::styled(" = mapped to PID/name", theme::fg(theme::muted())), - ]); - f.render_widget( - Paragraph::new(legend), - Rect::new(inner.x, inner.y + 6, inner.width, 1), +fn draw_attribution(f: &mut Frame, snapshot: &ProcessActivitySnapshot, area: Rect) { + let inner = section_header(f, area, section_title(" Process attribution")); + let mut lines = Vec::new(); + for direction in [ActivityDirection::Egress, ActivityDirection::Ingress] { + let percentage = + direction.pick(snapshot.tx_attribution_pct(), snapshot.rx_attribution_pct()); + let unknown = direction.pick( + snapshot + .retained_tx_bytes + .saturating_sub(snapshot.attributed_tx_bytes), + snapshot + .retained_rx_bytes + .saturating_sub(snapshot.attributed_rx_bytes), ); + lines.push(metric_line( + direction.rate_label(), + format!("{percentage:.1}% mapped"), + attribution_style(percentage), + inner.width, + )); + lines.push(metric_line( + "Unknown", + format_bytes(unknown), + unknown_style(unknown), + inner.width, + )); + lines.push(Line::default()); } + f.render_widget(Paragraph::new(lines), inner); } fn coverage_fraction(captured_bytes: u64, interface_bytes: u64) -> Option { (interface_bytes > 0).then(|| (captured_bytes as f64 / interface_bytes as f64).min(1.0)) } -fn coverage_label(fraction: Option, basis_marker: &str) -> String { - fraction.map_or_else( - || " coverage n/a".to_string(), - |fraction| format!(" {basis_marker}{:.1}% observed", fraction * 100.0), - ) -} - fn attribution_style(percentage: f64) -> Style { theme::fg(if percentage >= 90.0 { theme::ok() @@ -318,10 +321,6 @@ fn unknown_style(bytes: u64) -> Style { }) } -fn direction_ramp(direction: ActivityDirection) -> fn(f64) -> Color { - direction.pick(theme::tx_wave, theme::rx_wave) -} - fn direction_color(direction: ActivityDirection) -> Color { direction.pick(theme::tx(), theme::rx()) } @@ -430,7 +429,7 @@ fn draw_process_table( area, Line::from(vec![ section_title(format!( - " Top Processes: {}", + " Processes · {}", traffic_direction.display_name_with_rate() )), Span::styled( @@ -456,12 +455,21 @@ fn draw_process_table( ); let visible = inner .height - .saturating_sub(1) + .saturating_sub(2) + .div_euclid(2) .min(MAX_VISIBLE_PROCESSES as u16) as usize; - let wide = inner.width >= 132; - let medium = inner.width >= 90; - let pulse_width = if wide { 14 } else { 10 }; - let name_width = if wide { 24 } else { 20 }; + let wide = inner.width >= 148; + let medium = inner.width >= 105; + let pulse_width = if wide { + 14 + } else { + usize::from(inner.width).saturating_sub(94).clamp(10, 22) + }; + let name_width = if wide { + 24 + } else { + usize::from(inner.width / 4).clamp(8, 20) + }; let rows: Vec = processes .into_iter() @@ -473,26 +481,43 @@ fn draw_process_table( retained_share(&process, traffic_direction) / 100.0 }; let name = truncate_with_ellipsis(&process.identity.display_name(), name_width); - let name_cell = match process_tint(&process.identity) { - Some(style) => Cell::from(name).style(style), - None => Cell::from(name), - }; + let name_cell = Cell::from(if medium { + vec![Line::from(name)] + } else { + vec![ + Line::from(name), + Line::from(glow_bar::themed_spans( + pulse_fraction, + name_width.min(14), + if process.identity.attributed { + direction_color(traffic_direction) + } else { + theme::warn() + }, + )), + ] + }) + .style(if process.identity.attributed { + theme::bold_fg(theme::text()) + } else { + theme::bold_fg(theme::warn()) + }); let mut cells = vec![name_cell]; if medium { - cells.push(Cell::from(Line::from(glow_bar::spans( + cells.push(Cell::from(Line::from(glow_bar::themed_spans( pulse_fraction, pulse_width, if process.identity.attributed { - direction_ramp(traffic_direction) + direction_color(traffic_direction) } else { - theme::warn_wave + theme::warn() }, )))); } - cells.push(right_cell(format_rate(current_rate( - &process, - traffic_direction, - )))); + cells.push( + right_cell(format_rate(current_rate(&process, traffic_direction))) + .style(theme::bold_fg(direction_color(traffic_direction))), + ); if wide { cells.push(right_cell(format_rate(peak_rate( &process, @@ -550,14 +575,17 @@ fn draw_process_table( } else { theme::fg(theme::warn()) }; - Row::new(cells).style(style) + Row::new(cells) + .style(style) + .height(if medium { 1 } else { 2 }) + .bottom_margin(u16::from(medium)) }) .collect(); let mut headers = vec![Cell::from("Process")]; let mut constraints = vec![Constraint::Length(name_width as u16)]; if medium { - headers.push(Cell::from("Pulse")); + headers.push(Cell::from("Share")); constraints.push(Constraint::Length(pulse_width as u16)); } headers.push(right_cell(format!( @@ -591,11 +619,14 @@ fn draw_process_table( headers.push(right_cell("Remote".to_string())); headers.push(Cell::from("Top remote peer")); constraints.push(Constraint::Length(8)); - constraints.push(Constraint::Min(12)); + constraints.push(Constraint::Min(20)); } - let table = - Table::new(rows, constraints).header(Row::new(headers).style(theme::fg(theme::heading()))); + let table = Table::new(rows, constraints).header( + Row::new(headers) + .style(theme::bold_fg(theme::heading())) + .bottom_margin(1), + ); f.render_widget(table, inner); } @@ -603,80 +634,11 @@ fn right_cell(value: String) -> Cell<'static> { Cell::from(Line::from(value).right_aligned()) } -/// Stable per-process tint for a process name, so the same process keeps -/// the same hue wherever it appears. `None` keeps the caller's own style: -/// the theme withholds a tint on NO_COLOR, non-truecolor terminals, and -/// the vivid preset, and unattributed rows keep their warning color so -/// the "could not be mapped" cue is never painted over. -fn process_tint(identity: &ProcessIdentity) -> Option