Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/screenshots/activity-redesign-compact.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/screenshots/activity-redesign.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/ui/connection_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
82 changes: 79 additions & 3 deletions src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,14 @@ pub(crate) fn section_header<'a, T: Into<Line<'a>>>(
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),
Expand All @@ -177,10 +185,9 @@ pub(crate) fn section_header<'a, T: Into<Line<'a>>>(
)
}

/// Bold default-foreground title span for [`section_header`].
/// Theme-aware bold heading span for [`section_header`].
pub(crate) fn section_title(text: impl Into<String>) -> 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.
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Loading