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

586 lines
28 KiB
Markdown

---
phase: 02-interactive-tui
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- Cargo.toml
- tcptop/Cargo.toml
- tcptop/src/lib.rs
- tcptop/src/main.rs
- tcptop/src/tui/mod.rs
- tcptop/src/tui/app.rs
- tcptop/src/tui/draw.rs
- tcptop/src/tui/event.rs
autonomous: true
requirements:
- DISP-01
- DISP-02
- DISP-04
- DISP-05
- DISP-06
- DISP-08
must_haves:
truths:
- "User sees a live-updating table of connections with columns Proto, Local, Remote, PID, Process, State, Rate In, Rate Out, RTT"
- "User sees a 3-4 line summary header with TCP/UDP connection counts and aggregate bandwidth"
- "User sees a bottom status bar showing current sort column and direction"
- "User can quit with 'q' or Ctrl-C and terminal restores cleanly"
- "Connections are color-coded by bandwidth intensity (dim to bright)"
- "Display refreshes at a configurable interval (--interval flag, default 1s)"
artifacts:
- path: "tcptop/src/tui/app.rs"
provides: "App state struct with sort, filter, scroll, input mode"
exports: ["App", "InputMode", "SortColumn", "CliFilters"]
- path: "tcptop/src/tui/draw.rs"
provides: "Rendering functions for header, table, status bar"
contains: "fn draw"
- path: "tcptop/src/tui/event.rs"
provides: "Keyboard event handler"
contains: "fn handle_event"
- path: "tcptop/src/main.rs"
provides: "Terminal init/restore, EventStream in select loop, clap Cli struct"
contains: "ratatui::init"
key_links:
- from: "tcptop/src/main.rs"
to: "tcptop/src/tui/app.rs"
via: "App::new() and app.draw()"
pattern: "app\\.draw"
- from: "tcptop/src/tui/draw.rs"
to: "tcptop/src/output.rs"
via: "format_rate, format_rtt reuse"
pattern: "format_rate|format_rtt"
- from: "tcptop/src/main.rs"
to: "crossterm::event::EventStream"
via: "event_stream.next() in tokio::select!"
pattern: "event_stream\\.next\\(\\)"
---
<objective>
Build the core TUI: ratatui terminal with live connection table, summary header, status bar, bandwidth coloring, and basic quit handling. Replace Phase 1's stdout streaming with a full interactive terminal interface.
Purpose: Transform tcptop from a streaming text dump into a usable `top`-like tool with a real terminal UI.
Output: Working TUI that renders live connection data in a sortable table with header stats and status bar.
</objective>
<execution_context>
@/Users/zrowitsch/local_src/tcptop/.claude/get-shit-done/workflows/execute-plan.md
@/Users/zrowitsch/local_src/tcptop/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/02-interactive-tui/02-CONTEXT.md
@.planning/phases/02-interactive-tui/02-RESEARCH.md
<interfaces>
<!-- Existing types the executor needs -->
From tcptop/src/model.rs:
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Protocol { Tcp, Udp }
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ConnectionKey {
pub protocol: Protocol,
pub local_addr: IpAddr,
pub local_port: u16,
pub remote_addr: IpAddr,
pub remote_port: u16,
}
#[derive(Debug, Clone)]
pub struct ConnectionRecord {
pub key: ConnectionKey,
pub pid: u32,
pub process_name: String,
pub tcp_state: Option<TcpState>,
pub bytes_in: u64,
pub bytes_out: u64,
pub packets_in: u64,
pub packets_out: u64,
pub rate_in: f64,
pub rate_out: f64,
pub prev_bytes_in: u64,
pub prev_bytes_out: u64,
pub rtt_us: Option<u32>,
pub last_seen: Instant,
pub is_partial: bool,
pub is_closed: bool,
}
impl TcpState {
pub fn as_str(&self) -> &'static str;
}
```
From tcptop/src/output.rs:
```rust
pub fn format_bytes(bytes: u64) -> String;
pub fn format_rate(rate: f64) -> String;
pub fn format_rtt(rtt_us: Option<u32>) -> String;
```
From tcptop/src/aggregator.rs:
```rust
pub struct ConnectionTable { ... }
impl ConnectionTable {
pub fn new() -> Self;
pub fn seed(&mut self, records: Vec<ConnectionRecord>);
pub fn update(&mut self, event: CollectorEvent);
pub fn tick(&mut self) -> (Vec<&ConnectionRecord>, Vec<ConnectionRecord>);
pub fn connection_count(&self) -> usize;
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add dependencies and create TUI module with App state</name>
<read_first>
- Cargo.toml (workspace root - current workspace.dependencies)
- tcptop/Cargo.toml (userspace crate deps)
- tcptop/src/lib.rs (module declarations)
- tcptop/src/model.rs (ConnectionKey, ConnectionRecord, Protocol, TcpState types)
</read_first>
<files>Cargo.toml, tcptop/Cargo.toml, tcptop/src/lib.rs, tcptop/src/tui/mod.rs, tcptop/src/tui/app.rs</files>
<action>
1. Add workspace dependencies to `Cargo.toml` under `[workspace.dependencies]`:
```toml
ratatui = { version = "0.30.0", features = ["crossterm"] }
crossterm = { version = "0.29.0", features = ["event-stream"] }
futures = "0.3"
```
2. Add to `tcptop/Cargo.toml` under `[dependencies]`:
```toml
ratatui = { workspace = true }
crossterm = { workspace = true }
futures = { workspace = true }
```
3. Add `pub mod tui;` to `tcptop/src/lib.rs` (after `pub mod output;`).
4. Create `tcptop/src/tui/mod.rs` with:
```rust
pub mod app;
pub mod draw;
pub mod event;
```
5. Create `tcptop/src/tui/app.rs` with the App state struct. This is the core TUI state:
- `InputMode` enum: `Normal`, `Filter` (per D-07 state machine)
- `SortColumn` enum: `Proto`, `LocalAddr`, `RemoteAddr`, `Pid`, `Process`, `State`, `RateIn`, `RateOut`, `Rtt`, `BytesIn`, `BytesOut`, `PacketsIn`, `PacketsOut` (per D-01, D-04)
- `CliFilters` struct: `port: Option<u16>`, `pid: Option<u32>`, `process: Option<String>`, `tcp_only: bool`, `udp_only: bool` (for FILT-01..04, populated by Plan 02)
- `App` struct with fields:
- `sort_column: SortColumn` (default `RateIn`)
- `sort_ascending: bool` (default `false` - highest rate first)
- `filter_text: String` (empty)
- `input_mode: InputMode` (Normal)
- `table_state: ratatui::widgets::TableState` (default)
- `show_extra_columns: bool` (false, per D-04)
- `show_help: bool` (false, per D-08)
- `selected_header_col: usize` (0, for Tab navigation per D-06)
- `new_connections: HashSet<ConnectionKey>` (for D-11 green highlight)
- `closing_connections: HashSet<ConnectionKey>` (for D-11 red highlight)
- `previous_keys: HashSet<ConnectionKey>` (for detecting new connections)
- `cli_filters: CliFilters`
- `App::new(cli_filters: CliFilters) -> Self` constructor
- `App::toggle_sort(&mut self, column: SortColumn)` - if same column, flip ascending; if different column, set column and ascending=false
- `App::sort_records(&self, records: &mut Vec<&ConnectionRecord>)` - sort in place using `sort_column` and `sort_ascending`. Use `partial_cmp` for f64 fields (rate_in, rate_out), `cmp` for others. Match all SortColumn variants including LocalAddr (format as string then cmp), RemoteAddr similarly.
- `App::update_highlights(&mut self, active: &[&ConnectionRecord], closed: &[ConnectionRecord])` - compute new_connections as keys in active but not in previous_keys. Set closing_connections from closed records' keys. Then set previous_keys = current active keys.
- `App::filter_records<'a>(&self, records: &[&'a ConnectionRecord], live_filter: &str) -> Vec<&'a ConnectionRecord>` - apply cli_filters (port, pid, process, tcp_only, udp_only) then live_filter string matching against addr:port and process_name. Case-insensitive contains matching.
</action>
<verify>
<automated>cd /Users/zrowitsch/local_src/tcptop && cargo check 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>
- Cargo.toml contains `ratatui = { version = "0.30.0", features = ["crossterm"] }`
- Cargo.toml contains `crossterm = { version = "0.29.0", features = ["event-stream"] }`
- Cargo.toml contains `futures = "0.3"`
- tcptop/Cargo.toml contains `ratatui = { workspace = true }`
- tcptop/Cargo.toml contains `crossterm = { workspace = true }`
- tcptop/Cargo.toml contains `futures = { workspace = true }`
- tcptop/src/lib.rs contains `pub mod tui;`
- tcptop/src/tui/mod.rs contains `pub mod app;` and `pub mod draw;` and `pub mod event;`
- tcptop/src/tui/app.rs contains `pub enum InputMode` with `Normal` and `Filter` variants
- tcptop/src/tui/app.rs contains `pub enum SortColumn` with `Proto`, `LocalAddr`, `RemoteAddr`, `Pid`, `Process`, `State`, `RateIn`, `RateOut`, `Rtt` variants
- tcptop/src/tui/app.rs contains `pub struct App` with `sort_column`, `filter_text`, `input_mode`, `table_state`, `show_extra_columns`, `show_help`, `new_connections`, `closing_connections` fields
- tcptop/src/tui/app.rs contains `pub struct CliFilters`
- tcptop/src/tui/app.rs contains `pub fn toggle_sort`
- tcptop/src/tui/app.rs contains `pub fn sort_records`
- tcptop/src/tui/app.rs contains `pub fn update_highlights`
- tcptop/src/tui/app.rs contains `pub fn filter_records`
- `cargo check` succeeds (exit code 0)
</acceptance_criteria>
<done>TUI module scaffolded with App state struct, all enums, filter/sort/highlight methods. Compiles clean.</done>
</task>
<task type="auto">
<name>Task 2: Create draw.rs and event.rs for rendering and keyboard handling</name>
<read_first>
- tcptop/src/tui/app.rs (App struct just created)
- tcptop/src/output.rs (format_rate, format_rtt, format_bytes to reuse)
- tcptop/src/model.rs (ConnectionRecord, Protocol, TcpState for display)
- .planning/phases/02-interactive-tui/02-RESEARCH.md (rendering patterns, bandwidth_style, layout composition)
</read_first>
<files>tcptop/src/tui/draw.rs, tcptop/src/tui/event.rs</files>
<action>
1. Create `tcptop/src/tui/draw.rs` with rendering functions:
- `pub fn draw(frame: &mut Frame, app: &mut App, connections: &[&ConnectionRecord])` - main draw function. Uses `Layout::vertical` with 3 chunks:
- `Constraint::Length(4)` for summary header (per D-13)
- `Constraint::Min(5)` for connection table (DISP-01)
- `Constraint::Length(1)` for status bar (per D-14)
Calls `draw_header`, `draw_table`, `draw_status_bar`. If `app.show_help`, calls `draw_help_overlay` on top.
- `fn draw_header(frame: &mut Frame, area: Rect, connections: &[&ConnectionRecord])` - renders a `Paragraph` widget (per D-13). Content:
- Line 1: `"tcptop — {total} connections ({tcp_count} TCP, {udp_count} UDP)"` - count by protocol
- Line 2: `"Bandwidth: In: {total_rate_in} Out: {total_rate_out}"` - sum of all rate_in/rate_out formatted via `crate::output::format_rate()`
- Line 3: horizontal separator using `"─".repeat(area.width)`
Use `Block::default()` with no borders.
- `fn draw_table(frame: &mut Frame, area: Rect, app: &mut App, connections: &[&ConnectionRecord])` - builds a ratatui `Table` widget:
- Header row (per D-01): `["Proto", "Local Addr:Port", "Remote Addr:Port", "PID", "Process", "State", "Rate In", "Rate Out", "RTT"]`. If `app.show_extra_columns` (D-04), append `["Bytes In", "Bytes Out", "Pkts In", "Pkts Out"]`.
- Header style: `Style::default().add_modifier(Modifier::BOLD)`. The column matching `app.sort_column` gets underline modifier and a direction indicator arrow appended: " ↑" if ascending, " ↓" if descending.
- For Tab navigation (D-06): the column at index `app.selected_header_col` gets `bg(Color::DarkGray)` to show selection.
- Data rows: for each ConnectionRecord, create a `Row` with cells:
- Proto: `"TCP"` or `"UDP"` from `record.key.protocol`
- Local: `format!("{}:{}", record.key.local_addr, record.key.local_port)` (per D-02)
- Remote: `format!("{}:{}", record.key.remote_addr, record.key.remote_port)` (per D-02)
- PID: `record.pid.to_string()` (per D-03)
- Process: `record.process_name.clone()` + `"*"` suffix if `record.is_partial` (per D-12). Truncate with ellipsis if needed (D-05, ratatui handles this via Constraint but also add `..` manually if >15 chars for safety).
- State: `record.tcp_state.map(|s| s.as_str()).unwrap_or("UDP")` (per D-07 in Phase 1)
- Rate In: `crate::output::format_rate(record.rate_in)`
- Rate Out: `crate::output::format_rate(record.rate_out)`
- RTT: `crate::output::format_rtt(record.rtt_us)`
- If extra columns: `crate::output::format_bytes(record.bytes_in)`, `format_bytes(record.bytes_out)`, `record.packets_in.to_string()`, `record.packets_out.to_string()`
- Row styling (per D-10 and D-11):
- If key is in `app.new_connections`: `Style::default().bg(Color::DarkGreen)` (per D-11)
- Else if key is in `app.closing_connections`: `Style::default().bg(Color::DarkRed)` (per D-11)
- Else: `bandwidth_style(record.rate_in + record.rate_out)` (per D-10)
- Column widths: `[Length(5), Min(15), Min(15), Length(7), Length(15), Length(12), Length(10), Length(10), Length(8)]`. If extra columns, append `[Length(12), Length(12), Length(8), Length(8)]`.
- Render with `frame.render_stateful_widget(table, area, &mut app.table_state)` for scroll support.
- Row highlight style: `Style::default().add_modifier(Modifier::REVERSED)` for selected row.
- `fn draw_status_bar(frame: &mut Frame, area: Rect, app: &App)` - renders a single-line `Paragraph` (per D-14, D-15):
- Normal mode: `"Sort: {column_name} {arrow} | /:filter ?:help c:columns q:quit"`
- Filter mode: `"Filter: {app.filter_text}_"` (underscore as cursor indicator)
- Style: `bg(Color::DarkGray)`, `fg(Color::White)`
- `fn draw_help_overlay(frame: &mut Frame, area: Rect)` - centered popup (per D-08):
- Calculate centered rect: 60 wide, 20 tall (or 60%/60% of terminal, whichever smaller)
- `Block::bordered()` with title `" Help "`
- Content: list of keybindings:
```
q / Ctrl-C Quit
/ Filter (type to search, Esc to clear)
? Toggle this help
c Toggle extra columns (bytes, packets)
r / R Sort by Rate In / Rate Out
p / n Sort by PID / Process Name
s / t Sort by State / RTT
a / A Sort by Local Addr / Remote Addr
P Sort by Protocol
Tab / Enter Navigate/select column header
j/k or ↑/↓ Scroll rows
* next to process name = pre-existing connection (partial data)
```
- Clear the area behind the popup before rendering.
- `fn bandwidth_style(total_rate: f64) -> Style` (per D-10):
- `< 1024.0` (< 1 KB/s): `fg(Color::DarkGray)` (dim)
- `< 102400.0` (< 100 KB/s): `fg(Color::White)` (normal)
- `< 1048576.0` (< 1 MB/s): `fg(Color::White).add_modifier(Modifier::BOLD)` (bright)
- `>= 1048576.0` (>= 1 MB/s): `fg(Color::Yellow).add_modifier(Modifier::BOLD)` (very bright)
- `fn centered_rect(width: u16, height: u16, area: Rect) -> Rect` - helper to compute centered rectangle for overlay.
2. Create `tcptop/src/tui/event.rs` with keyboard event handling:
- `pub enum Action { Continue, Quit }` - return type from event handler
- `pub fn handle_event(app: &mut App, event: crossterm::event::Event) -> Action` - process keyboard events:
- Filter to `Event::Key(key)` where `key.kind == KeyEventKind::Press` only (avoid double events per Pitfall 3 in research)
- Check for Ctrl-C first (any mode): `key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL)` -> return `Action::Quit`
- Match on `app.input_mode`:
- `InputMode::Normal`:
- `KeyCode::Char('q')` -> `Action::Quit`
- `KeyCode::Char('/')` -> set `app.input_mode = InputMode::Filter`
- `KeyCode::Char('?')` -> toggle `app.show_help`
- `KeyCode::Char('c')` -> toggle `app.show_extra_columns` (per D-04)
- `KeyCode::Char('r')` -> `app.toggle_sort(SortColumn::RateIn)` (per D-06)
- `KeyCode::Char('R')` -> `app.toggle_sort(SortColumn::RateOut)`
- `KeyCode::Char('p')` -> `app.toggle_sort(SortColumn::Pid)`
- `KeyCode::Char('n')` -> `app.toggle_sort(SortColumn::Process)`
- `KeyCode::Char('s')` -> `app.toggle_sort(SortColumn::State)`
- `KeyCode::Char('t')` -> `app.toggle_sort(SortColumn::Rtt)`
- `KeyCode::Char('a')` -> `app.toggle_sort(SortColumn::LocalAddr)`
- `KeyCode::Char('A')` -> `app.toggle_sort(SortColumn::RemoteAddr)`
- `KeyCode::Char('P')` -> `app.toggle_sort(SortColumn::Proto)`
- `KeyCode::Tab` -> advance `app.selected_header_col` (mod number of visible columns)
- `KeyCode::Enter` -> sort by column at `app.selected_header_col` using a mapping array
- `KeyCode::Char('j') | KeyCode::Down` -> `app.table_state.select_next()`
- `KeyCode::Char('k') | KeyCode::Up` -> `app.table_state.select_previous()`
- _ -> do nothing
- `InputMode::Filter` (per D-07):
- `KeyCode::Esc` -> clear `app.filter_text`, set `app.input_mode = InputMode::Normal`, reset table_state selection to 0
- `KeyCode::Enter` -> set `app.input_mode = InputMode::Normal` (keep filter text)
- `KeyCode::Char(c)` -> push `c` to `app.filter_text`
- `KeyCode::Backspace` -> pop from `app.filter_text`
- _ -> do nothing
- Return `Action::Continue` for all non-quit cases.
</action>
<verify>
<automated>cd /Users/zrowitsch/local_src/tcptop && cargo check 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>
- tcptop/src/tui/draw.rs contains `pub fn draw(frame: &mut Frame, app: &mut App, connections: &[&ConnectionRecord])`
- tcptop/src/tui/draw.rs contains `fn draw_header`
- tcptop/src/tui/draw.rs contains `fn draw_table`
- tcptop/src/tui/draw.rs contains `fn draw_status_bar`
- tcptop/src/tui/draw.rs contains `fn draw_help_overlay`
- tcptop/src/tui/draw.rs contains `fn bandwidth_style`
- tcptop/src/tui/draw.rs contains `Layout::vertical` or `Layout::new(Direction::Vertical`
- tcptop/src/tui/draw.rs contains `format_rate` (reuse from output.rs)
- tcptop/src/tui/draw.rs contains `format_rtt` (reuse from output.rs)
- tcptop/src/tui/draw.rs contains `Color::DarkGreen` (D-11 new connection highlight)
- tcptop/src/tui/draw.rs contains `Color::DarkRed` (D-11 closing connection highlight)
- tcptop/src/tui/event.rs contains `pub enum Action` with `Continue` and `Quit`
- tcptop/src/tui/event.rs contains `pub fn handle_event`
- tcptop/src/tui/event.rs contains `KeyEventKind::Press` (filter for press-only events)
- tcptop/src/tui/event.rs contains `KeyModifiers::CONTROL` (Ctrl-C handling)
- tcptop/src/tui/event.rs contains `InputMode::Filter` (filter state machine)
- `cargo check` succeeds (exit code 0)
</acceptance_criteria>
<done>draw.rs renders header (3-4 lines with connection counts and bandwidth per D-13), connection table (9 default columns per D-01, extra 4 via toggle per D-04), status bar (sort/filter info per D-14), and help overlay (per D-08). event.rs handles all keyboard shortcuts including mnemonic sort keys (per D-06), filter-as-you-type (per D-07), quit (per D-08), column toggle, help toggle, and row scrolling. Bandwidth coloring applied per D-10. Connection highlights for new/closing per D-11. Partial marker asterisk per D-12.</done>
</task>
<task type="auto">
<name>Task 3: Wire TUI into main.rs with terminal lifecycle and clap CLI</name>
<read_first>
- tcptop/src/main.rs (current event loop to replace)
- tcptop/src/tui/app.rs (App::new, CliFilters)
- tcptop/src/tui/draw.rs (draw function signature)
- tcptop/src/tui/event.rs (handle_event, Action enum)
- tcptop/src/aggregator.rs (ConnectionTable::tick signature)
- .planning/phases/02-interactive-tui/02-RESEARCH.md (Pattern 2: Async Event Loop, terminal lifecycle)
</read_first>
<files>tcptop/src/main.rs</files>
<action>
1. Add clap `Parser` derive struct at the top of main.rs (or in a separate section):
```rust
use clap::Parser;
#[derive(Parser, Debug)]
#[command(name = "tcptop", about = "Real-time per-connection network monitor")]
struct Cli {
/// Filter by port (matches source or destination)
#[arg(long)]
port: Option<u16>,
/// Filter by process ID
#[arg(long)]
pid: Option<u32>,
/// Filter by process name (substring match)
#[arg(long)]
process: Option<String>,
/// Network interface to monitor
#[arg(long, short = 'i')]
interface: Option<String>,
/// Show only TCP connections
#[arg(long)]
tcp: bool,
/// Show only UDP connections
#[arg(long)]
udp: bool,
/// Refresh interval in seconds (default: 1)
#[arg(long, default_value = "1")]
interval: u64,
}
```
2. Update `main()` to parse CLI args and init terminal:
```rust
#[tokio::main]
async fn main() -> anyhow::Result<()> {
env_logger::init();
tcptop::privilege::check_privileges();
let cli = Cli::parse();
#[cfg(target_os = "linux")]
{
// Initialize terminal BEFORE entering async code
let mut terminal = ratatui::init();
let result = run_linux(&mut terminal, &cli).await;
ratatui::restore();
result?;
}
#[cfg(not(target_os = "linux"))]
{
eprintln!("tcptop: eBPF collector only supported on Linux. macOS backend planned for Phase 4.");
std::process::exit(1);
}
#[allow(unreachable_code)]
Ok(())
}
```
3. Rewrite `run_linux()` to accept terminal and CLI args:
```rust
#[cfg(target_os = "linux")]
async fn run_linux(terminal: &mut ratatui::DefaultTerminal, cli: &Cli) -> anyhow::Result<()> {
// Same collector/table setup as before
let mut collector = LinuxCollector::new()?;
let mut table = ConnectionTable::new();
// Bootstrap
match collector.bootstrap_existing() { /* same as before */ }
// Channel + spawn collector (same as before)
let (tx, mut rx) = mpsc::channel(4096);
let collector_handle = tokio::spawn(async move {
if let Err(e) = collector.start(tx).await {
log::error!("Collector error: {}", e);
}
});
// Create App state with CLI filters
let cli_filters = tcptop::tui::app::CliFilters {
port: cli.port,
pid: cli.pid,
process: cli.process.clone(),
tcp_only: cli.tcp,
udp_only: cli.udp,
};
let mut app = tcptop::tui::app::App::new(cli_filters);
// Use CLI-specified interval (DISP-05)
let mut tick = interval(Duration::from_secs(cli.interval));
// Async keyboard input stream
let mut event_stream = crossterm::event::EventStream::new();
// Signal handlers (same as before)
let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())?;
let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
loop {
tokio::select! {
Some(event) = rx.recv() => {
table.update(event);
}
_ = tick.tick() => {
let (active, closed) = table.tick();
// Update new/closing connection highlights (D-11)
app.update_highlights(&active, &closed);
// Filter (CLI + live filter) then sort
let mut filtered = app.filter_records(&active, &app.filter_text.clone());
app.sort_records(&mut filtered);
// Clamp selection to filtered length (Pitfall 4)
if let Some(sel) = app.table_state.selected() {
if sel >= filtered.len() {
app.table_state.select(Some(filtered.len().saturating_sub(1)));
}
}
// Render
terminal.draw(|frame| {
tcptop::tui::draw::draw(frame, &mut app, &filtered);
})?;
}
Some(Ok(evt)) = event_stream.next() => {
if let tcptop::tui::event::Action::Quit = tcptop::tui::event::handle_event(&mut app, evt) {
break;
}
}
_ = sigint.recv() => break,
_ = sigterm.recv() => break,
}
}
collector_handle.abort();
Ok(())
}
```
4. Add required imports at the top of main.rs:
```rust
use futures::StreamExt; // for event_stream.next()
use clap::Parser;
```
5. Remove the `tcptop::output::print_header()` and `tcptop::output::print_tick()` calls -- they are replaced by TUI rendering.
NOTE: The `--interface` flag is parsed but not yet passed to the collector (FILT-03). The `LinuxCollector::new()` takes no args currently. Plan 02 Task 3 will wire interface filtering. For now the flag is accepted by clap but ignored with a log::warn if provided.
</action>
<verify>
<automated>cd /Users/zrowitsch/local_src/tcptop && cargo check 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>
- tcptop/src/main.rs contains `#[derive(Parser` (clap derive struct)
- tcptop/src/main.rs contains `--port` or `port: Option<u16>` (FILT-01 flag)
- tcptop/src/main.rs contains `--pid` or `pid: Option<u32>` (FILT-02 flag)
- tcptop/src/main.rs contains `--process` or `process: Option<String>` (FILT-02 flag)
- tcptop/src/main.rs contains `--interface` or `interface: Option<String>` (FILT-03 flag)
- tcptop/src/main.rs contains `--tcp` or `tcp: bool` (FILT-04 flag)
- tcptop/src/main.rs contains `--udp` or `udp: bool` (FILT-04 flag)
- tcptop/src/main.rs contains `--interval` or `interval: u64` (DISP-05 flag)
- tcptop/src/main.rs contains `ratatui::init()` (terminal initialization)
- tcptop/src/main.rs contains `ratatui::restore()` (terminal restoration)
- tcptop/src/main.rs contains `EventStream::new()` or `event_stream` (async keyboard input)
- tcptop/src/main.rs contains `event_stream.next()` (keyboard events in select loop)
- tcptop/src/main.rs contains `handle_event` (keyboard event processing)
- tcptop/src/main.rs contains `terminal.draw` (TUI rendering)
- tcptop/src/main.rs does NOT contain `print_header()` (removed)
- tcptop/src/main.rs does NOT contain `print_tick(` (removed, replaced by TUI draw)
- `cargo check` succeeds (exit code 0)
</acceptance_criteria>
<done>main.rs wired to TUI: terminal init/restore around event loop, clap Cli struct with all flags (--port, --pid, --process, --interface, --tcp, --udp, --interval), EventStream in tokio::select! for non-blocking keyboard input, render on tick with filter+sort+draw pipeline. Phase 1 stdout output replaced with ratatui rendering.</done>
</task>
</tasks>
<verification>
- `cargo check` passes with no errors
- All files in tcptop/src/tui/ exist: mod.rs, app.rs, draw.rs, event.rs
- main.rs has clap derive struct with all 7 CLI flags
- main.rs uses ratatui::init() and ratatui::restore() for terminal lifecycle
- main.rs uses EventStream for async keyboard input in tokio::select!
- draw.rs renders 3-region layout (header, table, status bar)
- event.rs handles q, Ctrl-C, /, ?, c, sort keys, j/k/arrows, Tab/Enter
</verification>
<success_criteria>
- Running `cargo check` succeeds
- TUI module exists with App state, draw functions, event handler
- main.rs replaces stdout output with TUI rendering
- All 9 default columns rendered in table (Proto, Local, Remote, PID, Process, State, Rate In, Rate Out, RTT)
- Summary header shows connection counts and aggregate bandwidth
- Status bar shows sort column and direction
- Bandwidth color coding applied (dim to bright per D-10)
- New/closing connection highlights implemented (D-11)
- Quit works via 'q' or Ctrl-C (DISP-08)
- --interval flag configures refresh rate (DISP-05)
</success_criteria>
<output>
After completion, create `.planning/phases/02-interactive-tui/02-01-SUMMARY.md`
</output>