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

28 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
02-interactive-tui 01 execute 1
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
true
DISP-01
DISP-02
DISP-04
DISP-05
DISP-06
DISP-08
truths artifacts key_links
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)
path provides exports
tcptop/src/tui/app.rs App state struct with sort, filter, scroll, input mode
App
InputMode
SortColumn
CliFilters
path provides contains
tcptop/src/tui/draw.rs Rendering functions for header, table, status bar fn draw
path provides contains
tcptop/src/tui/event.rs Keyboard event handler fn handle_event
path provides contains
tcptop/src/main.rs Terminal init/restore, EventStream in select loop, clap Cli struct ratatui::init
from to via pattern
tcptop/src/main.rs tcptop/src/tui/app.rs App::new() and app.draw() app.draw
from to via pattern
tcptop/src/tui/draw.rs tcptop/src/output.rs format_rate, format_rtt reuse format_rate|format_rtt
from to via pattern
tcptop/src/main.rs crossterm::event::EventStream event_stream.next() in tokio::select! event_stream.next()
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.

<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>

@.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

From tcptop/src/model.rs:

#[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:

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:

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;
}
Task 1: Add dependencies and create TUI module with App state - 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) Cargo.toml, tcptop/Cargo.toml, tcptop/src/lib.rs, tcptop/src/tui/mod.rs, tcptop/src/tui/app.rs 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" ```
  1. Add to tcptop/Cargo.toml under [dependencies]:

    ratatui = { workspace = true }
    crossterm = { workspace = true }
    futures = { workspace = true }
    
  2. Add pub mod tui; to tcptop/src/lib.rs (after pub mod output;).

  3. Create tcptop/src/tui/mod.rs with:

    pub mod app;
    pub mod draw;
    pub mod event;
    
  4. 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. cd /Users/zrowitsch/local_src/tcptop && cargo check 2>&1 | tail -5 <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> TUI module scaffolded with App state struct, all enums, filter/sort/highlight methods. Compiles clean.
Task 2: Create draw.rs and event.rs for rendering and keyboard handling - 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) tcptop/src/tui/draw.rs, tcptop/src/tui/event.rs 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.

  1. 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. cd /Users/zrowitsch/local_src/tcptop && cargo check 2>&1 | tail -5 <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> 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.

Task 3: Wire TUI into main.rs with terminal lifecycle and clap CLI - 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) tcptop/src/main.rs 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,

   /// 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(())
}
  1. Rewrite run_linux() to accept terminal and CLI args:

    #[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(())
    }
    
  2. Add required imports at the top of main.rs:

    use futures::StreamExt;       // for event_stream.next()
    use clap::Parser;
    
  3. 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. cd /Users/zrowitsch/local_src/tcptop && cargo check 2>&1 | tail -5 <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> 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.

- `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

<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>
After completion, create `.planning/phases/02-interactive-tui/02-01-SUMMARY.md`