Files
Zachary D. Rowitsch 38e6dcc34a chore: archive v1.0 phase directories to milestones/v1.0-phases/
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 01:33:15 -04:00

546 lines
28 KiB
Markdown

# Phase 2: Interactive TUI - Research
**Researched:** 2026-03-21
**Domain:** Terminal UI with ratatui, async event handling, connection table rendering
**Confidence:** HIGH
## Summary
Phase 2 replaces the Phase 1 streaming stdout output with a full interactive TUI using ratatui 0.30 and crossterm 0.29. The existing codebase provides a clean integration surface: the tokio `select!` event loop in `main.rs` already separates collector events from periodic ticks, and the aggregator's `tick()` method returns exactly the data needed for rendering. The primary work is building the TUI layer (terminal init, keyboard handling, table rendering with sort/filter state) and wiring clap CLI args for filtering flags.
Ratatui 0.30 provides `ratatui::init()` which handles raw mode, alternate screen, and panic hooks in one call. The `Table` widget with `TableState` supports row selection and scrolling natively. Crossterm 0.29's `EventStream` (with the `event-stream` feature) integrates directly into the existing tokio `select!` loop for non-blocking keyboard input. Column sorting, filtering, and color-coding are application-level logic built on top of these primitives.
**Primary recommendation:** Use ratatui 0.30 + crossterm 0.29 with `EventStream` for async keyboard input. Structure the TUI as a single `App` struct holding sort/filter/scroll state, rendered each tick by a `draw()` function that builds the `Table` widget from sorted/filtered `ConnectionRecord` data. Replace `print_tick()` with `terminal.draw(|f| app.draw(f))`.
<user_constraints>
## User Constraints (from CONTEXT.md)
### Locked Decisions
- **D-01:** Default column set (8 columns): Proto, Local Addr:Port, Remote Addr:Port, PID, Process, State, Rate In, Rate Out, RTT
- **D-02:** Addresses displayed as combined `addr:port` in a single column (e.g., `192.168.1.5:443`). IPv6 abbreviated where possible.
- **D-03:** PID and Process Name as separate columns (not combined like `nginx (1234)`)
- **D-04:** Additional columns (Bytes In/Out, Packets In/Out) available via toggle key (`c`), not shown by default
- **D-05:** Narrow terminals (< ~120 cols) handled by truncating addresses and process names with ellipsis. Columns stay, content gets trimmed.
- **D-06:** Two sorting methods: mnemonic single-key shortcuts (`r`/`R`/`p`/`n`/`s`/`t`/`a`/`A`/`P`) and Tab to navigate column headers + Enter to sort. Press same key again to toggle asc/desc.
- **D-07:** `/` opens filter-as-you-type mode. Table filters live as you type. Matches IP, port, or process name. Esc clears the filter. Like vim's `/` search.
- **D-08:** Standard keyboard shortcuts: `q` / Ctrl-C quit, `?` help overlay, arrows / `j`/`k` scroll rows, `/` filter, `c` toggle columns.
- **D-09:** No detail pane. Table is the sole view.
- **D-10:** Bandwidth color coding uses intensity-based single color (dim to bright). Low bandwidth = dim text, high bandwidth = bright/bold.
- **D-11:** New connections get subtle green background highlight for one refresh cycle. Closing connections get red background highlight for one cycle before removal.
- **D-12:** Pre-existing/partial connections indicated with asterisk on process name (e.g., `nginx*`). Help overlay explains the asterisk.
- **D-13:** Header is 3-4 lines: connection counts with TCP/UDP breakdown, aggregate bandwidth in/out, separator line.
- **D-14:** Active sort column and filter shown in a bottom status bar (vim-style), NOT in the header. E.g., `Sort: Rate In down Filter: nginx`
- **D-15:** Header stays pure aggregate stats. Status bar at bottom for UI state.
### Claude's Discretion
- Exact ratatui widget composition and layout structure
- Terminal initialization/restoration details
- Internal event loop architecture (how TUI rendering interleaves with data updates)
- Column width ratios and resize behavior
- Exact intensity gradient thresholds for bandwidth display
- Help overlay content and layout
- How to integrate clap CLI args with the existing main.rs event loop
### Deferred Ideas (OUT OF SCOPE)
- UDP flow idle timeout configurable via CLI flag -- Phase 3 or backlog
- `--batch` or `--once` mode preserving stdout output -- evaluate in Phase 3
- Reverse DNS resolution for remote IPs -- v2 requirement (DISP-V2-01)
- Connection duration/age display -- v2 requirement (DISP-V2-02)
- Freeze/pause display with 'p' key -- v2 requirement (DISP-V2-03)
</user_constraints>
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|------------------|
| DISP-01 | User sees a real-time sortable table with one row per connection showing all stats | Ratatui `Table` + `TableState` with custom sort logic on `ConnectionRecord` fields |
| DISP-02 | User sees a summary header with total connection count and aggregate bandwidth in/out | Ratatui `Paragraph` widget in a `Layout::vertical` chunk above the table |
| DISP-03 | User can sort the table by any column via keyboard | App-level sort state (column enum + direction) applied before rendering; dual input model (mnemonic keys + Tab/Enter) |
| DISP-04 | User sees local address, local port, remote address, remote port per connection | Format as `addr:port` string from `ConnectionKey` fields per D-02 |
| DISP-05 | User can configure refresh interval via CLI flag (default 1 second) | clap derive struct with `--interval` flag, passed to `tokio::time::interval()` |
| DISP-06 | Connections are color-coded by bandwidth usage | Ratatui `Style` with intensity-based color (dim/normal/bold) applied per-row based on rate_in + rate_out |
| DISP-07 | User can search/filter the live view with '/' key | App input mode state machine (Normal / Filter); filter string applied to records before sort+render |
| DISP-08 | User can quit with 'q' or Ctrl-C | Crossterm key event matching in the event handler |
| FILT-01 | User can filter by port via CLI flag (`--port`) | clap `--port` arg parsed and applied as pre-filter on `ConnectionTable::tick()` results |
| FILT-02 | User can filter by process via CLI flag (`--pid` or `--process`) | clap `--pid` and `--process` args |
| FILT-03 | User can select network interface via CLI flag (`--interface`) | clap `--interface` arg; needs to be passed to collector at construction time |
| FILT-04 | User can filter by protocol via CLI flag (`--tcp` / `--udp`) | clap boolean flags |
</phase_requirements>
## Standard Stack
### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| ratatui | 0.30.0 | Terminal UI framework | Community standard Rust TUI. Immediate-mode rendering, `Table` widget with `TableState`, built-in `init()`/`restore()` for terminal lifecycle. |
| crossterm | 0.29.0 | Terminal backend + event input | Default backend for ratatui 0.30. `EventStream` (with `event-stream` feature) provides async `Stream` for tokio integration. |
| clap | 4.6.x | CLI argument parsing | Already in workspace deps with derive feature. Needed for `--port`, `--pid`, `--process`, `--interface`, `--tcp`, `--udp`, `--interval` flags. |
### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| futures | 0.3.x | `StreamExt` trait for `EventStream::next()` | Required to `.next().await` on crossterm's `EventStream` in tokio select. |
### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| crossterm EventStream | crossterm::event::poll() in a blocking loop | Poll requires a dedicated thread or busy-waiting; EventStream integrates cleanly with the existing tokio select! |
| Manual terminal init | ratatui::init() | Manual gives more control but `init()` handles raw mode + alternate screen + panic hook in one call. Use `init()`. |
**Installation (add to workspace Cargo.toml):**
```toml
[workspace.dependencies]
ratatui = { version = "0.30.0", features = ["crossterm"] }
crossterm = { version = "0.29.0", features = ["event-stream"] }
futures = "0.3"
```
**Add to tcptop/Cargo.toml:**
```toml
[dependencies]
ratatui = { workspace = true }
crossterm = { workspace = true }
futures = { workspace = true }
```
## Architecture Patterns
### Recommended Project Structure
```
tcptop/src/
├── main.rs # Entry point, clap parsing, terminal init, event loop
├── lib.rs # Module declarations
├── tui/
│ ├── mod.rs # Re-exports
│ ├── app.rs # App state struct (sort, filter, scroll, input mode)
│ ├── draw.rs # Rendering: header, table, status bar, help overlay
│ └── event.rs # Keyboard event -> Action mapping
├── aggregator.rs # Unchanged from Phase 1
├── model.rs # Unchanged (may add sort helpers)
├── output.rs # Kept for non-TUI use; format helpers reused by tui/draw.rs
├── collector/ # Unchanged from Phase 1
└── ...
```
### Pattern 1: App State Struct
**What:** A single `App` struct owns all TUI state separate from domain data.
**When to use:** Always -- keeps rendering pure and testable.
```rust
pub enum InputMode {
Normal,
Filter,
}
pub enum SortColumn {
Proto, LocalAddr, RemoteAddr, Pid, Process, State, RateIn, RateOut, Rtt,
BytesIn, BytesOut, PacketsIn, PacketsOut,
}
pub struct App {
pub sort_column: SortColumn,
pub sort_ascending: bool,
pub filter_text: String,
pub input_mode: InputMode,
pub table_state: TableState, // ratatui scroll/selection state
pub show_extra_columns: bool, // 'c' toggle per D-04
pub show_help: bool, // '?' overlay per D-08
pub selected_header_col: usize, // Tab navigation per D-06
// Transient display state for D-11 (new/closing highlights)
pub new_connections: HashSet<ConnectionKey>, // green highlight this tick
pub closing_connections: HashSet<ConnectionKey>, // red highlight this tick
}
```
### Pattern 2: Async Event Loop with EventStream
**What:** Integrate crossterm keyboard events into the existing tokio select! loop.
**When to use:** This is the pattern for the main loop.
```rust
use crossterm::event::EventStream;
use futures::StreamExt;
let mut event_stream = EventStream::new();
loop {
tokio::select! {
Some(event) = rx.recv() => {
table.update(event);
}
_ = tick.tick() => {
let (active, closed) = table.tick();
// Update highlight sets for D-11
app.update_highlights(&active, &closed);
// Apply CLI filters (FILT-01..04), then live filter (D-07), then sort (D-06)
let filtered = app.filter_and_sort(&active);
terminal.draw(|f| app.draw(f, &filtered))?;
}
Some(Ok(crossterm_event)) = event_stream.next() => {
if app.handle_event(crossterm_event) == Action::Quit {
break;
}
}
_ = sigint.recv() => break,
_ = sigterm.recv() => break,
}
}
```
### Pattern 3: Layout Composition (3 regions)
**What:** Vertical layout with header, table, and status bar.
**When to use:** The main `draw()` function.
```rust
fn draw(&mut self, frame: &mut Frame, connections: &[&ConnectionRecord]) {
let chunks = Layout::vertical([
Constraint::Length(4), // Summary header (D-13)
Constraint::Min(5), // Connection table (DISP-01)
Constraint::Length(1), // Status bar (D-14)
]).split(frame.area());
self.draw_header(frame, chunks[0], connections);
self.draw_table(frame, chunks[1], connections);
self.draw_status_bar(frame, chunks[2]);
if self.show_help {
self.draw_help_overlay(frame, frame.area()); // Centered popup
}
}
```
### Pattern 4: Filter-as-you-type State Machine
**What:** Input mode switching for the `/` filter.
**When to use:** Keyboard event handler.
```rust
fn handle_event(&mut self, event: CrosstermEvent) -> Action {
if let CrosstermEvent::Key(key) = event {
if key.kind != KeyEventKind::Press { return Action::Continue; }
match self.input_mode {
InputMode::Normal => match key.code {
KeyCode::Char('q') => return Action::Quit,
KeyCode::Char('/') => self.input_mode = InputMode::Filter,
KeyCode::Char('r') => self.toggle_sort(SortColumn::RateIn),
KeyCode::Char('R') => self.toggle_sort(SortColumn::RateOut),
// ... other mnemonic keys per D-06
KeyCode::Tab => self.select_next_header_col(),
KeyCode::Enter => self.sort_by_selected_header(),
KeyCode::Char('j') | KeyCode::Down => self.table_state.select_next(),
KeyCode::Char('k') | KeyCode::Up => self.table_state.select_previous(),
KeyCode::Char('c') => self.show_extra_columns = !self.show_extra_columns,
KeyCode::Char('?') => self.show_help = !self.show_help,
_ => {}
},
InputMode::Filter => match key.code {
KeyCode::Esc => {
self.filter_text.clear();
self.input_mode = InputMode::Normal;
}
KeyCode::Char(c) => self.filter_text.push(c),
KeyCode::Backspace => { self.filter_text.pop(); }
KeyCode::Enter => self.input_mode = InputMode::Normal,
_ => {}
},
}
}
Action::Continue
}
```
### Anti-Patterns to Avoid
- **Blocking event reads in async context:** Never use `crossterm::event::read()` in a tokio task. Use `EventStream` with the `event-stream` feature instead. Blocking reads will freeze the entire async runtime.
- **Rendering on every keypress:** Only render on tick intervals. Keyboard events update state; the next tick renders the new state. This keeps rendering at a consistent cadence and avoids wasted frames.
- **Forgetting terminal restore on all exit paths:** Use `ratatui::init()` which installs a panic hook. But also ensure `ratatui::restore()` is called on normal exit AND on signal handlers. Wrap the main loop in a function and call restore in a defer-like pattern.
- **Processing Key Release/Repeat events:** Crossterm sends Press, Release, and Repeat events. Filter to `KeyEventKind::Press` only, or you get double-handling on some terminals.
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Terminal raw mode / alternate screen | Manual crossterm commands | `ratatui::init()` / `ratatui::restore()` | Handles raw mode, alternate screen, and panic hooks. One call. |
| Async keyboard input | Polling thread or busy loop | crossterm `EventStream` + futures `StreamExt` | Integrates into tokio select! cleanly |
| Table scrolling / selection | Manual offset tracking | `TableState` (ratatui) | Handles scroll position, selection, wrapping |
| Column layout constraints | Manual character counting | `Constraint::Percentage`, `Constraint::Min`, `Constraint::Length` | Ratatui's layout engine handles terminal resize |
| Help overlay popup | Manual screen save/restore | Render a `Clear` + `Block` + `Paragraph` on top | Ratatui's immediate-mode rendering makes overlays trivial |
## Common Pitfalls
### Pitfall 1: Terminal not restored after panic or early exit
**What goes wrong:** If the program panics or exits without restoring the terminal, the user's shell is left in raw mode with the alternate screen active. They see garbled input.
**Why it happens:** Manual terminal setup without panic hook. Signal handlers that exit without restoring.
**How to avoid:** Use `ratatui::init()` which installs a panic hook. Call `ratatui::restore()` before every exit path. In signal handlers, restore before breaking.
**Warning signs:** Testing crashes leave terminal in bad state.
### Pitfall 2: EventStream requires the event-stream feature
**What goes wrong:** `crossterm::event::EventStream` does not exist at compile time.
**Why it happens:** The `event-stream` feature on crossterm is not enabled by default.
**How to avoid:** Add `features = ["event-stream"]` to crossterm dependency.
**Warning signs:** Compilation error about missing `EventStream` type.
### Pitfall 3: Double key events (Press + Release)
**What goes wrong:** Each keypress triggers the handler twice, causing sort toggles to flip back, filter chars to double, etc.
**Why it happens:** Some terminals send both Press and Release events. Crossterm 0.28+ enables `KeyEventKind` reporting.
**How to avoid:** Filter events: `if key.kind != KeyEventKind::Press { return; }`
**Warning signs:** Sort direction flipping back immediately, double characters in filter.
### Pitfall 4: TableState selection out of bounds after filter
**What goes wrong:** Selected row index points past the end of the filtered list, causing panic or rendering glitch.
**Why it happens:** User has row 15 selected, then types a filter that reduces the list to 3 items.
**How to avoid:** After filtering, clamp `table_state.selected()` to `filtered.len().saturating_sub(1)`. Or call `table_state.select(Some(0))` when filter text changes.
**Warning signs:** Panic on draw, or table showing no highlight.
### Pitfall 5: Forgetting to handle Ctrl-C separately from 'q'
**What goes wrong:** Ctrl-C does not quit the app because raw mode intercepts it.
**Why it happens:** In raw mode, Ctrl-C is delivered as `KeyCode::Char('c')` with `KeyModifiers::CONTROL`, not as SIGINT.
**How to avoid:** Match on `KeyCode::Char('c')` when `key.modifiers.contains(KeyModifiers::CONTROL)` in the event handler. The existing SIGINT handler in `tokio::select!` is a backup but the crossterm event arrives first.
**Warning signs:** User presses Ctrl-C and nothing happens.
### Pitfall 6: Rendering performance with many connections
**What goes wrong:** Drawing hundreds of rows causes noticeable lag.
**Why it happens:** Building Row/Cell widgets for connections not visible on screen.
**How to avoid:** Only build Row widgets for visible rows (use `TableState::offset()` + terminal height to calculate visible range). For < 500 connections this is unlikely to matter, but good to keep in mind.
**Warning signs:** Noticeable frame drops with > 200 connections.
### Pitfall 7: ratatui 0.30 breaking change -- HorizontalAlignment
**What goes wrong:** Code using `Alignment::Center` does not compile.
**Why it happens:** Ratatui 0.30 renamed `Alignment` to `HorizontalAlignment`.
**How to avoid:** Use `HorizontalAlignment::Center` or import the renamed type.
**Warning signs:** Compilation error about `Alignment` not found.
## Code Examples
### Terminal Lifecycle (init + restore)
```rust
// Source: https://docs.rs/ratatui/latest/ratatui/fn.init.html
use ratatui::DefaultTerminal;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// init() enables raw mode, alternate screen, installs panic hook
let mut terminal = ratatui::init();
let result = run(&mut terminal).await;
// restore() disables raw mode, leaves alternate screen
ratatui::restore();
result
}
```
### Table Widget with Styled Rows
```rust
// Source: https://ratatui.rs/examples/widgets/table/
use ratatui::widgets::{Row, Table, TableState, Block, Borders};
use ratatui::style::{Style, Color, Modifier};
use ratatui::layout::Constraint;
fn build_table<'a>(records: &'a [&ConnectionRecord], app: &App) -> Table<'a> {
let header = Row::new(vec!["Proto", "Local", "Remote", "PID", "Process", "State", "Rate In", "Rate Out", "RTT"])
.style(Style::default().add_modifier(Modifier::BOLD))
.bottom_margin(1);
let rows: Vec<Row> = records.iter().map(|r| {
let style = bandwidth_style(r.rate_in + r.rate_out);
Row::new(vec![
proto_str(r),
format!("{}:{}", r.key.local_addr, r.key.local_port),
format!("{}:{}", r.key.remote_addr, r.key.remote_port),
r.pid.to_string(),
process_display(r), // adds * for is_partial per D-12
state_str(r),
format_rate(r.rate_in),
format_rate(r.rate_out),
format_rtt(r.rtt_us),
]).style(style)
}).collect();
let widths = [
Constraint::Length(5), // Proto
Constraint::Min(15), // Local
Constraint::Min(15), // Remote
Constraint::Length(7), // PID
Constraint::Length(12), // Process
Constraint::Length(12), // State
Constraint::Length(10), // Rate In
Constraint::Length(10), // Rate Out
Constraint::Length(8), // RTT
];
Table::new(rows, widths)
.header(header)
.block(Block::default().borders(Borders::NONE))
.row_highlight_style(Style::default().add_modifier(Modifier::REVERSED))
}
```
### Bandwidth Intensity Styling (D-10)
```rust
fn bandwidth_style(total_rate: f64) -> Style {
if total_rate < 1024.0 {
// < 1 KB/s: dim
Style::default().fg(Color::DarkGray)
} else if total_rate < 100.0 * 1024.0 {
// 1-100 KB/s: normal
Style::default().fg(Color::White)
} else if total_rate < 1024.0 * 1024.0 {
// 100KB-1MB/s: bright
Style::default().fg(Color::White).add_modifier(Modifier::BOLD)
} else {
// > 1 MB/s: bright + underline for emphasis
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
}
}
```
### Connection Highlight for New/Closing (D-11)
```rust
fn row_style(record: &ConnectionRecord, app: &App) -> Style {
if app.new_connections.contains(&record.key) {
Style::default().bg(Color::DarkGreen)
} else if app.closing_connections.contains(&record.key) {
Style::default().bg(Color::DarkRed)
} else {
bandwidth_style(record.rate_in + record.rate_out)
}
}
```
### Sort Implementation
```rust
fn sort_records(records: &mut Vec<&ConnectionRecord>, column: &SortColumn, ascending: bool) {
records.sort_by(|a, b| {
let ord = match column {
SortColumn::Proto => a.key.protocol.cmp(&b.key.protocol),
SortColumn::RateIn => a.rate_in.partial_cmp(&b.rate_in).unwrap_or(Ordering::Equal),
SortColumn::RateOut => a.rate_out.partial_cmp(&b.rate_out).unwrap_or(Ordering::Equal),
SortColumn::Pid => a.pid.cmp(&b.pid),
SortColumn::Process => a.process_name.cmp(&b.process_name),
SortColumn::Rtt => a.rtt_us.cmp(&b.rtt_us),
// ... other columns
_ => Ordering::Equal,
};
if ascending { ord } else { ord.reverse() }
});
}
```
### Filter Implementation (D-07)
```rust
fn filter_records<'a>(
records: &[&'a ConnectionRecord],
filter: &str,
cli_filters: &CliFilters,
) -> Vec<&'a ConnectionRecord> {
records.iter()
.filter(|r| {
// CLI filters (FILT-01..04)
if let Some(port) = cli_filters.port {
if r.key.local_port != port && r.key.remote_port != port { return false; }
}
if let Some(pid) = cli_filters.pid {
if r.pid != pid { return false; }
}
if let Some(ref proc) = cli_filters.process {
if !r.process_name.contains(proc.as_str()) { return false; }
}
if cli_filters.tcp_only && r.key.protocol != Protocol::Tcp { return false; }
if cli_filters.udp_only && r.key.protocol != Protocol::Udp { return false; }
// Live filter (DISP-07)
if !filter.is_empty() {
let haystack = format!(
"{} {} {} {} {}",
r.key.local_addr, r.key.local_port,
r.key.remote_addr, r.key.remote_port,
r.process_name
);
if !haystack.to_lowercase().contains(&filter.to_lowercase()) { return false; }
}
true
})
.copied()
.collect()
}
```
### Clap Derive Struct
```rust
use clap::Parser;
#[derive(Parser, Debug)]
#[command(name = "tcptop", about = "Real-time per-connection network monitor")]
pub struct Cli {
/// Filter by port (matches source or destination)
#[arg(long)]
pub port: Option<u16>,
/// Filter by process ID
#[arg(long)]
pub pid: Option<u32>,
/// Filter by process name (substring match)
#[arg(long)]
pub process: Option<String>,
/// Network interface to monitor
#[arg(long, short = 'i')]
pub interface: Option<String>,
/// Show only TCP connections
#[arg(long)]
pub tcp: bool,
/// Show only UDP connections
#[arg(long)]
pub udp: bool,
/// Refresh interval in seconds (default: 1)
#[arg(long, default_value = "1")]
pub interval: u64,
}
```
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| `tui-rs` crate | `ratatui` (community fork) | 2023 | tui-rs is deprecated; ratatui is the only maintained option |
| Manual terminal init (enable_raw_mode + EnterAlternateScreen) | `ratatui::init()` | ratatui 0.28+ | One function call handles everything including panic hook |
| `crossterm::event::read()` blocking | `EventStream` async stream | crossterm 0.28+ with event-stream feature | Enables native tokio integration without dedicated thread |
| `Alignment` enum | `HorizontalAlignment` enum | ratatui 0.30.0 | Renamed for clarity; old name no longer compiles |
## Open Questions
1. **Interface filter (FILT-03) implementation depth**
- What we know: `--interface` needs to be passed to the collector, not just the TUI filter layer. The `LinuxCollector::new()` currently takes no arguments.
- What's unclear: Whether eBPF programs can be attached per-interface, or if interface filtering must happen in userspace. For Phase 2, userspace filtering of interface is likely sufficient if records include interface info.
- Recommendation: Add `--interface` to clap args and pass to collector constructor. If eBPF per-interface attachment is complex, filter in userspace as a Phase 2 pragmatic choice and note for future optimization.
2. **New connection detection for D-11 highlight**
- What we know: `ConnectionTable::tick()` returns active and closed lists. Closed connections have `is_closed = true`.
- What's unclear: There's no explicit "new this tick" flag on `ConnectionRecord`.
- Recommendation: Track connection keys seen in previous tick. Diff with current tick to identify new connections. Store previous tick's key set in App state.
## Sources
### Primary (HIGH confidence)
- [Ratatui official docs](https://docs.rs/ratatui/latest/ratatui/) - init/restore, Table, TableState, Layout, widgets
- [Ratatui Table example](https://ratatui.rs/examples/widgets/table/) - Table widget usage pattern with TableState
- [Ratatui v0.30.0 release notes](https://ratatui.rs/highlights/v030/) - Breaking changes (HorizontalAlignment rename), modular workspace
- [Crossterm EventStream docs](https://docs.rs/crossterm/latest/crossterm/event/struct.EventStream.html) - Async event stream for tokio
- [Ratatui async event stream tutorial](https://ratatui.rs/tutorials/counter-async-app/async-event-stream/) - tokio::select! integration pattern
- [Ratatui init() docs](https://docs.rs/ratatui/latest/ratatui/fn.init.html) - Terminal lifecycle management
### Secondary (MEDIUM confidence)
- [Ratatui async-template](https://github.com/ratatui/async-template) - Component-based architecture reference
- [Crossterm event-stream-tokio example](https://github.com/crossterm-rs/crossterm/blob/master/examples/event-stream-tokio.rs) - EventStream usage
### Tertiary (LOW confidence)
- None
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH - ratatui 0.30 and crossterm 0.29 are verified current releases with well-documented APIs
- Architecture: HIGH - async event loop pattern is documented in official ratatui tutorials and matches existing codebase structure
- Pitfalls: HIGH - double key events, terminal restore, and EventStream feature flag are well-documented common issues
**Research date:** 2026-03-21
**Valid until:** 2026-04-21 (ratatui ecosystem is mature and stable)