Version: 1.4.0 Last Updated: 2025-11-27 Status: Phase 6 COMPLETE (Sprints 6.1-6.8), Full TUI Implementation with Interactive Widgets
- Overview
- Architecture Diagram
- Core Components
- Event Flow
- Performance Characteristics
- State Management
- Terminal Lifecycle
- Testing Strategy
- Future Enhancements
- References
The ProRT-IP TUI (Terminal User Interface) is designed to provide real-time visualization of network scanning operations with the following objectives:
- Real-time Updates: Display scan progress, discovered hosts, and open ports as they're found
- High Performance: Handle 10,000+ events/second without UI lag or dropped events
- Responsive: Maintain 60 FPS rendering for smooth user experience
- Separation of Concerns: TUI is consumer-only, scanner has no TUI dependencies
- Graceful Degradation: Clean terminal restoration on all exit paths (normal, Ctrl+C, panic)
- ratatui 0.29+: Modern TUI framework with immediate mode rendering
- crossterm: Cross-platform terminal manipulation
- tokio: Async runtime for event loop coordination
- parking_lot: High-performance RwLock for shared state
- prtip-core: EventBus integration for scan events
- Target FPS: 60 (16.67ms frame budget)
- Event Throughput: 10,000+ events/second
- Event Aggregation: 16ms batching interval (60 FPS)
- Max Buffer Size: 1,000 events before dropping
- Test Coverage: 228 tests (190 unit, 38 integration) [Sprint 6.7-6.8]
┌─────────────────────────────────────────────────────────────────────┐
│ ProRT-IP Scanner │
│ (prtip-core, no TUI deps) │
└────────────────┬────────────────────────────────────────────────────┘
│
│ publishes
▼
┌─────────────────────────────────────────────────────────────────────┐
│ EventBus │
│ (mpsc::unbounded_channel, broadcast) │
└────────────────┬────────────────────────────────────────────────────┘
│
│ subscribe
▼
┌─────────────────────────────────────────────────────────────────────┐
│ TUI Event Loop │
│ (tokio::select! pattern) │
│ │
│ ┌───────────────┐ ┌────────────────┐ ┌─────────────────┐ │
│ │ Keyboard │ │ EventBus RX │ │ 60 FPS Timer │ │
│ │ (crossterm) │ │ (scan events) │ │ (tick_interval)│ │
│ └───────┬───────┘ └────────┬───────┘ └────────┬────────┘ │
│ │ │ │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────────┐ ┌─────────────────┐ │
│ │ Key Handler │ │ Event Aggregator │ │ Flush & Render │ │
│ │ (quit, nav) │ │ (rate limiting) │ │ (update state) │ │
│ └──────┬───────┘ └──────┬───────────┘ └──────────┬──────┘ │
│ │ │ │ │
│ └──────────────────┴─────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ State Update Logic │ │
│ │ (scan_state, ui_state) │ │
│ └───────────┬─────────────┘ │
└───────────────────────────┼─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Rendering Pipeline │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Layout │──▶│ Widgets │──▶│ ratatui │ │
│ │ (chunks) │ │ (components) │ │ (diffing) │ │
│ └─────────────┘ └──────────────┘ └──────┬───────┘ │
│ │ │
└──────────────────────────────────────────────┼──────────────────────┘
│
▼
┌─────────────────┐
│ Terminal │
│ (crossterm) │
└─────────────────┘
Data Flow Legend:
════════════════
│ Event flow (one-way)
▼ Processing direction
┌┐ Component boundary
- Consumer-Only TUI: Scanner publishes to EventBus, TUI subscribes (one-way flow)
- Immediate Mode Rendering: Full UI redrawn every frame, ratatui diffs and updates terminal
- Event Aggregation: Batch high-frequency events (PortFound, HostDiscovered) to prevent overload
- Shared State:
Arc<RwLock<ScanState>>for thread-safe scanner ↔ TUI communication - Graceful Cleanup: ratatui 0.29+ automatic panic hook ensures terminal restoration
Purpose: Main TUI application lifecycle manager
Responsibilities:
- Terminal initialization (raw mode, alternate screen)
- EventBus subscription
- Event loop coordination (keyboard, EventBus, timer)
- Terminal restoration on exit
Key Methods:
pub fn new(event_bus: Arc<EventBus>) -> Self
pub async fn run(&mut self) -> Result<()>
pub fn should_quit(&self) -> bool
pub fn scan_state(&self) -> Arc<RwLock<ScanState>>Event Loop Pattern:
loop {
// Render UI (60 FPS)
terminal.draw(|frame| ui::render(frame, &scan_state, &ui_state))?;
// Process events (tokio::select!)
let control = process_events(...).await;
if matches!(control, LoopControl::Quit) {
break;
}
}Purpose: Shared state between scanner and TUI
Type: Arc<RwLock<ScanState>> (thread-safe, shared ownership)
Fields:
pub struct ScanState {
pub stage: ScanStage, // Current scan phase
pub progress_percentage: f32, // 0.0 - 100.0
pub completed: u64, // Ports scanned
pub total: u64, // Total ports
pub open_ports: usize, // Open ports found
pub closed_ports: usize, // Closed ports
pub filtered_ports: usize, // Filtered ports
pub detected_services: usize, // Services detected
pub errors: usize, // Error count
pub discovered_hosts: Vec<IpAddr>, // Live hosts
pub warnings: Vec<String>, // Warnings
}Access Pattern:
// Read (many readers, non-blocking)
let state = scan_state.read();
let open_ports = state.open_ports;
// Write (exclusive, blocks readers)
let mut state = scan_state.write();
state.open_ports += 10;Purpose: Local TUI-only state (ephemeral, not shared)
Type: UIState (single-threaded, no locking needed)
Fields:
pub struct UIState {
pub selected_pane: SelectedPane, // Main | Help
pub cursor_position: usize, // Cursor position
pub scroll_offset: usize, // Scroll offset
pub input_buffer: String, // Text input
pub show_help: bool, // Help visibility
pub fps: f32, // Debug FPS counter
pub aggregator_dropped_events: usize, // Dropped event count
}Navigation Methods:
pub fn next_pane(&mut self) // Tab key
pub fn prev_pane(&mut self) // Shift+Tab
pub fn toggle_help(&mut self) // F1/? keyPurpose: Rate limiting for high-frequency events (10K+/sec)
Strategy:
- Aggregate: Count PortFound, HostDiscovered, ServiceDetected (don't buffer individual events)
- Buffer: Store lifecycle events (ScanStarted, ScanCompleted, errors, warnings)
- Flush: Process batches every 16ms (60 FPS) to prevent UI overload
Constants:
const MAX_BUFFER_SIZE: usize = 1000; // Drop events if exceeded
const MIN_EVENT_INTERVAL: Duration = 16ms; // 60 FPS flush rateEvent Statistics:
pub struct EventStats {
pub ports_found: usize, // Aggregated count
pub hosts_discovered: usize, // Aggregated count
pub services_detected: usize, // Aggregated count
pub discovered_ips: HashMap<IpAddr, usize>, // Deduplication
pub total_events: usize, // Total processed
pub dropped_events: usize, // Rate limit drops
}Methods:
pub fn add_event(&mut self, event: ScanEvent) -> bool
pub fn should_flush(&self) -> bool
pub fn flush(&mut self) -> (Vec<ScanEvent>, EventStats)
pub fn stats(&self) -> &EventStatsPerformance:
- Throughput: 10,000+ events/second
- Latency: 16ms max (60 FPS)
- Memory: ~1,000 events × event size (lifecycle only, aggregated events don't buffer)
- Overhead: ~100 bytes per event (estimate)
Purpose: Coordinate keyboard, EventBus, and timer events
Pattern: tokio::select! for concurrent event handling
pub async fn process_events(
event_bus: Arc<EventBus>,
scan_state: Arc<RwLock<ScanState>>,
ui_state: &mut UIState,
event_rx: &mut mpsc::UnboundedReceiver<ScanEvent>,
crossterm_rx: &mut EventStream,
aggregator: &mut EventAggregator,
) -> LoopControlEvent Handling:
tokio::select! {
// Keyboard events (Ctrl+C, quit, navigation)
Some(Ok(crossterm_event)) = crossterm_rx.next() => {
if matches!(crossterm_event, Event::Key(key) if key.code == KeyCode::Char('q')) {
return LoopControl::Quit;
}
// ... other key handlers
}
// EventBus events (add to aggregator, don't process immediately)
Some(scan_event) = event_rx.recv() => {
aggregator.add_event(scan_event);
}
// 60 FPS timer (flush aggregator, update state)
_ = tick_interval.tick() => {
if aggregator.should_flush() {
let (events, stats) = aggregator.flush();
// Process buffered lifecycle events
for event in events {
handle_scan_event(event, Arc::clone(&scan_state));
}
// Apply aggregated statistics
let mut state = scan_state.write();
state.open_ports += stats.ports_found;
state.detected_services += stats.services_detected;
// ... deduplication for discovered_hosts
ui_state.aggregator_dropped_events = stats.dropped_events;
}
}
}Purpose: Define TUI layout structure
Layout Structure:
┌─────────────────────────────────────────┐
│ Header (scan info) │ 10% height
├─────────────────────────────────────────┤
│ │
│ Main Area (results) │ 80% height
│ │
├─────────────────────────────────────────┤
│ Footer (help text, FPS, stats) │ 10% height
└─────────────────────────────────────────┘
Key Functions:
pub fn create_layout(area: Rect) -> Rc<[Rect]>
pub fn render_header(scan_state: &ScanState) -> Paragraph
pub fn render_main_area(scan_state: &ScanState) -> Paragraph
pub fn render_footer(ui_state: &UIState) -> Paragraph
pub fn render_help_screen() -> ParagraphPurpose: Immediate mode rendering (60 FPS)
Rendering Pipeline:
pub fn render(frame: &mut Frame, scan_state: &ScanState, ui_state: &UIState) {
// 1. Create layout chunks
let chunks = layout::create_layout(frame.area());
// 2. Render header
frame.render_widget(layout::render_header(scan_state), chunks[0]);
// 3. Render main area
frame.render_widget(layout::render_main_area(scan_state), chunks[1]);
// 4. Render footer
frame.render_widget(layout::render_footer(ui_state), chunks[2]);
// 5. Render help screen (overlay) if visible
if ui_state.show_help {
frame.render_widget(layout::render_help_screen(), frame.area());
}
}Performance Budget:
- Frame time: 16.67ms (60 FPS)
- Rendering: <5ms (ratatui diffing)
- State access: <1ms (read lock)
- Event processing: <10ms (aggregated)
- Margin: ~1ms for system overhead
ProRT-IP TUI includes 11 production-ready widgets as of Phase 6 completion:
- Phase 6.1 Widgets (4): StatusBar, MainWidget, LogWidget, HelpWidget
- Phase 6.2 Dashboard Widgets (3): PortTableWidget, ServiceTableWidget, MetricsDashboardWidget
- Phase 6.7-6.8 Interactive Widgets (4): FileBrowserWidget, PortSelectionWidget, TemplateSelectionWidget, ShortcutsWidget
Purpose: Common interface for TUI components
Trait Definition:
pub trait Component {
/// Render the component to a frame
fn render(&mut self, frame: &mut Frame, area: Rect);
/// Handle keyboard input
fn handle_key(&mut self, key: KeyEvent) -> anyhow::Result<()>;
/// Update component state
fn update(&mut self) -> anyhow::Result<()>;
}Purpose: Header widget displaying scan metadata and overall progress
Location: src/widgets/status_bar.rs
Features:
- Scan stage indicator (Initializing, Scanning, Complete, Error)
- Target information (IP/CIDR range)
- Scan type display (SYN, Connect, UDP, etc.)
- Overall progress percentage
- Color-coded status: Green (active), Yellow (warning), Red (error)
Layout:
┌─────────────────────────────────────────────────────────────┐
│ ProRT-IP Scanner | Target: 192.168.1.0/24 | Type: SYN | 45% │
└─────────────────────────────────────────────────────────────┘
Rendering:
- Fixed height: 3 lines (10% of terminal)
- Horizontal layout with styled text spans
- Immediate mode: Full redraw every frame
Tests: Unit tests for status formatting, color selection
Purpose: Central content area displaying scan results summary
Location: src/widgets/main.rs
Features:
- Live host count (discovered IPs)
- Port statistics (open/closed/filtered counts)
- Service detection summary
- Error/warning counters
- Scrollable content area
Layout:
┌─────────────────────────────────────────────────────────────┐
│ Discovered Hosts: 24 │
│ │
│ Open Ports: 156 │
│ Closed Ports: 12,844 │
│ Filtered Ports: 0 │
│ │
│ Services Detected: 89 │
│ Errors: 0 │
└─────────────────────────────────────────────────────────────┘
Rendering:
- Variable height: 80% of terminal (main area)
- Paragraph widget with line-wrapped text
- Responsive layout (adjusts to terminal width)
Tests: Integration tests for state synchronization
Purpose: Real-time event log with scrolling and filtering
Location: src/widgets/log.rs
Features:
- Circular buffer (1,000 most recent events)
- Timestamped log entries
- Event type filtering (Info, Warning, Error)
- Auto-scroll toggle (follow mode)
- Keyboard navigation (scroll, search)
Layout:
┌─────────────────────────────────────────────────────────────┐
│ [12:34:56] INFO Port 80 open on 192.168.1.1 │
│ [12:34:57] INFO Service detected: nginx 1.18.0 │
│ [12:34:58] WARN Slow response from 192.168.1.10 │
│ [12:34:59] INFO Port 443 open on 192.168.1.1 │
│ ... │
│ [Auto-scroll: ON] | Filters: ALL | Lines: 156/1000 │
└─────────────────────────────────────────────────────────────┘
Keyboard Shortcuts:
↑/↓orj/k: Scroll up/downPage Up/Down: Scroll by pageHome/End: Jump to start/enda: Toggle auto-scroll
Rendering:
- List widget with stateful scrolling
- Color-coded entries: Info=White, Warn=Yellow, Error=Red
- Performance: <5ms for 1,000 entries
Tests: Unit tests for circular buffer, filtering logic
Purpose: Overlay widget showing keyboard shortcuts and commands
Location: src/widgets/help.rs
Features:
- Comprehensive keybinding reference
- Grouped by category (Navigation, Filtering, Views)
- Centered popup overlay
- Semi-transparent background (Clear widget)
Layout:
┌─────────────────────────────────────────────────────────────┐
│ ┌───────────────────┐ │
│ │ Keyboard Shortcuts │ │
│ ├───────────────────┤ │
│ │ q, Ctrl+C - Quit │ │
│ │ ?, F1 - Help │ │
│ │ Tab - Switch │ │
│ │ ↑/↓ - Scroll │ │
│ │ ... │ │
│ │ Press ? to close │ │
│ └───────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Rendering:
- Block widget with border and title
- Centered with Popup constraint (50% width × 60% height)
- Rendered on top of main content (overlay)
Tests: Integration tests for toggle visibility
Purpose: Real-time port discovery visualization with sortable table
Location: src/widgets/port_table.rs (~700 lines, 14 tests)
Features:
- Data Source: 1,000-entry circular buffer (PortDiscovery events from EventBus)
- Columns (6): Timestamp, IP Address, Port, State, Protocol, Scan Type
- Multi-Column Sorting: All 6 columns × ascending/descending (12 sort modes)
- Triple Filtering:
- State filter: All, Open, Closed, Filtered
- Protocol filter: All, TCP, UDP
- Search: Fuzzy match on IP address or port number
- Color Coding: Open=Green, Closed=Red, Filtered=Yellow
- Auto-Scroll: Follow mode for live discoveries
Data Structure:
pub struct PortDiscovery {
pub timestamp: DateTime<Utc>,
pub ip: IpAddr,
pub port: u16,
pub state: PortState, // Open, Closed, Filtered
pub protocol: Protocol, // TCP, UDP
pub scan_type: ScanType, // SYN, Connect, UDP, etc.
}Keyboard Shortcuts:
t: Sort by timestampi: Sort by IP addressp: Sort by port numbers: Sort by stater: Sort by protocolc: Sort by scan typea: Toggle auto-scrollf: Cycle state filterd: Cycle protocol filter/: Search mode (IP or port)↑/↓: Navigate rowsPage Up/Down: Scroll by page
Rendering Performance:
- Frame time: <5ms for 1,000 entries
- Table widget with stateful selection
- Row highlighting for selected item
- Header with sort direction indicators (▲/▼)
Layout:
┌─────────────────────────────────────────────────────────────┐
│ Timestamp │ IP Address │ Port │ State │ Proto │ Type │
├──────────────┼───────────────┼──────┼───────┼───────┼──────┤
│ 12:34:56.123 │ 192.168.1.1 │ 80 │ Open │ TCP │ SYN │
│ 12:34:56.234 │ 192.168.1.1 │ 443 │ Open │ TCP │ SYN │
│ 12:34:56.345 │ 192.168.1.2 │ 22 │ Filt │ TCP │ SYN │
│ 12:34:56.456 │ 192.168.1.3 │ 53 │ Open │ UDP │ UDP │
│ ... │
│ [Sort: Port ▲] | [Filter: Open+TCP] | 156/1000 | Auto: ON │
└─────────────────────────────────────────────────────────────┘
Integration:
- Subscribes to EventBus for PortDiscovery events
- Updates ringbuffer in real-time (no blocking)
- Renders in Dashboard Tab 1 (Port Table view)
Tests (14):
- Sorting: 6 columns × 2 directions = 12 tests
- Filtering: State filter, protocol filter (2 tests)
- Unit tests only (integration via App event loop)
Purpose: Real-time service detection visualization with confidence scoring
Location: src/widgets/service_table.rs (~700 lines, 21 tests)
Features:
- Data Source: 500-entry circular buffer (ServiceDetection events from EventBus)
- Columns (6): Timestamp, IP Address, Port, Service Name, Version, Confidence
- Confidence-Based Color Coding:
- High (≥90%): Green
- Medium (50-89%): Yellow
- Low (<50%): Red
- Multi-Level Filtering: All, Low (≥50%), Medium (≥75%), High (≥90%)
- Sorting: All 6 columns with ascending/descending
- Search: Fuzzy match on service name, IP, or port
- Auto-Scroll: Follow mode for live detections
Data Structure:
pub struct ServiceDetection {
pub timestamp: DateTime<Utc>,
pub ip: IpAddr,
pub port: u16,
pub service_name: String, // "nginx", "ssh", "mysql"
pub service_version: String, // "1.18.0", "OpenSSH_8.2p1"
pub confidence: u8, // 0-100 (detection confidence %)
}Keyboard Shortcuts:
1: Sort by timestamp2: Sort by IP address3: Sort by port4: Sort by service name5: Sort by service version6: Sort by confidencec: Cycle confidence filter (All → Low → Medium → High)a: Toggle auto-scroll/: Search mode (service, IP, or port)↑/↓: Navigate rowsPage Up/Down: Scroll by page
Rendering Performance:
- Frame time: <5ms for 500 entries
- Table widget with confidence color styling
- Row-level color application based on confidence
- Header with sort indicators and active filter
Layout:
┌─────────────────────────────────────────────────────────────┐
│ Time │ IP Address │ Port │ Service │ Version │ Conf │
├──────────┼───────────────┼──────┼─────────┼──────────┼──────┤
│ 12:34:56 │ 192.168.1.1 │ 80 │ nginx │ 1.18.0 │ 95% │ (Green)
│ 12:34:57 │ 192.168.1.1 │ 443 │ nginx │ 1.18.0 │ 95% │ (Green)
│ 12:34:58 │ 192.168.1.2 │ 22 │ ssh │ OpenSSH…│ 72% │ (Yellow)
│ 12:34:59 │ 192.168.1.3 │ 3306 │ mysql │ 5.7.31 │ 42% │ (Red)
│ ... │
│ [Sort: Conf ▼] | [Filter: High ≥90%] | 89/500 | Auto: ON │
└─────────────────────────────────────────────────────────────┘
Integration:
- Subscribes to EventBus for ServiceDetection events
- Confidence calculation from service detection engine
- Renders in Dashboard Tab 2 (Service Table view)
Tests (21):
- Sorting: 6 columns × 2 directions = 12 tests
- Filtering: 4 confidence levels = 4 tests
- Color coding: 3 confidence ranges = 3 tests
- Search: 2 tests (service name, port)
Purpose: Real-time performance metrics and scan statistics
Location: src/widgets/metrics_dashboard.rs (~740 lines, 24 tests)
Features:
- 3-Column Layout: Progress | Throughput | Statistics
- Progress Column:
- Scan percentage (0-100%)
- Completed/total ports
- ETA calculation (based on 5-second rolling average)
- Scan stage indicator
- Throughput Column:
- Current ports/second (instantaneous)
- Average ports/second (5-second window)
- Peak ports/second (session max)
- Current packets/second
- Average packets/second
- Statistics Column:
- Open ports count
- Detected services count
- Error count
- Scan duration (HH:MM:SS format)
- Status indicator (Active, Paused, Error)
Data Source: Reads from Arc<RwLock<ScanState>> shared state
Throughput Calculation:
// 5-second rolling average
pub struct ThroughputSample {
pub timestamp: Instant,
pub ports_per_second: f64,
pub packets_per_second: f64,
}
// Keep last 5 samples (1-second intervals)
// Average = sum(samples) / sample_countHuman-Readable Formatting:
- Durations: "1h 12m 45s", "23m 15s", "45s"
- Numbers: "12,345" (comma separators)
- Throughput: "1.23K pps", "456.7 pps", "12.3M pps"
Color Coding:
- Status: Green (Active), Yellow (Paused), Red (Error)
- ETA: White (normal), Yellow (>1h remaining), Red (stalled)
- Throughput: Green (≥target), Yellow (50-99%), Red (<50%)
Keyboard Shortcuts:
- No interactive controls (read-only metrics display)
- Auto-updates every frame (60 FPS)
Rendering Performance:
- Frame time: <5ms (3× under 16.67ms budget)
- 3 Block widgets with bordered panels
- Paragraph widgets for metric text
- Gauge widget for progress percentage
Layout:
┌──────────────────┬──────────────────┬──────────────────┐
│ PROGRESS │ THROUGHPUT │ STATISTICS │
├──────────────────┼──────────────────┼──────────────────┤
│ Scan: 45% │ Current: 1.2K/s │ Open Ports: 156 │
│ ████████░░░░░░░░ │ Average: 1.1K/s │ Services: 89 │
│ Completed: 2,925 │ Peak: 2.3K/s │ Errors: 0 │
│ Total: 6,500 │ │ Duration: 2m 15s │
│ ETA: 5m 12s │ Packets: 4.5K/s │ Status: Active │
│ Stage: Scanning │ Avg Pkt: 4.2K/s │ │
└──────────────────┴──────────────────┴──────────────────┘
Integration:
- Reads shared
ScanStateevery frame - Calculates derived metrics (ETA, averages, throughput)
- Renders in Dashboard Tab 3 (Metrics view)
Tests (24):
- Throughput calculations: 5 tests
- ETA calculations: 5 tests
- Human-readable formatting: 8 tests (durations, numbers, throughput)
- Color selection: 3 tests (status, ETA, throughput)
- Edge cases: 3 tests (zero values, overflow, no data)
Architecture: 3-tab dashboard with keyboard navigation
Tab Switching:
pub enum DashboardTab {
PortTable, // Tab 1: Real-time port discoveries
ServiceTable, // Tab 2: Service detection results
Metrics, // Tab 3: Performance metrics
}
impl UIState {
pub fn switch_tab(&mut self) {
self.active_tab = match self.active_tab {
DashboardTab::PortTable => DashboardTab::ServiceTable,
DashboardTab::ServiceTable => DashboardTab::Metrics,
DashboardTab::Metrics => DashboardTab::PortTable, // Cycle
};
}
}Keyboard Shortcuts:
Tab: Switch to next dashboard (Port → Service → Metrics → Port)Shift+Tab: Switch to previous dashboard (reverse direction)
Rendering:
- Tabs widget at top (shows all 3 tab names, highlights active)
- Conditional rendering: Only active tab widget is rendered
- Each widget receives full dashboard area (80% of terminal)
Visual Tab Bar:
┌─────────────────────────────────────────────────────────────┐
│ [Port Table] | Service Table | Metrics │
├─────────────────────────────────────────────────────────────┤
│ [Active Dashboard Widget Content] │
│ ... │
└─────────────────────────────────────────────────────────────┘
Event Routing:
- Active tab receives keyboard events (sorting, filtering, navigation)
- Inactive tabs do not process events (performance optimization)
Integration Tests (3):
- Tab switching sequence (Port → Service → Metrics → Port)
- Event routing to active tab only
- State persistence across tab switches
Purpose: Interactive file browser for target list import/export
Location: src/widgets/file_browser.rs
Features:
- Directory navigation with keyboard controls
- File selection with visual feedback
- File type filtering (.txt, .csv for target lists)
- Support for large directories (10K+ files)
- Breadcrumb path display
Keyboard Shortcuts:
↑/↓orj/k: Navigate filesEnter: Select file/enter directoryBackspace: Go up one directoryEsc: Cancel file browser
Purpose: Interactive port range selection with templates
Location: src/widgets/port_selection.rs
Features:
- Visual port range selector (1-65535)
- Pre-defined port templates (top 100, top 1000, common services)
- Custom range input (e.g., "80,443,8080-8090")
- Port validation and error handling
- Real-time selection count display
Keyboard Shortcuts:
t: Toggle template selectionc: Custom range input modea: Select all portsr: Reset selection
Purpose: Interactive keyboard shortcuts help overlay
Location: src/widgets/shortcuts.rs
Features:
- Categorized shortcuts (Navigation, Selection, Actions, Views)
- Context-aware help (shows relevant shortcuts for active widget)
- Searchable shortcuts list
- Color-coded key bindings
- Scrollable multi-page layout
Keyboard Shortcuts:
?orF1: Toggle shortcuts panel/: Search shortcutsEsc: Close shortcuts panel
Purpose: Interactive scan template selector
Location: src/widgets/template_selection.rs
Features:
- 10+ built-in scan templates (Quick, Stealth, Aggressive, etc.)
- Custom template creation and saving
- Template preview with parameter details
- Case-insensitive filtering
- Template categories (Speed, Stealth, Comprehensive)
Keyboard Shortcuts:
↑/↓: Navigate templatesEnter: Select templaten: Create new template/: Filter templates
Future Components (Phase 7+):
- ChartWidget: Sparkline throughput graph (real-time performance visualization)
- ConfigWidget: Interactive scan parameter editor (pause, adjust timing, change targets)
- ExportWidget: Live results export to JSON/XML/CSV during scan
Sprint 6.6 Enhancement: Added lifecycle event publishing (ScanStarted, StageChanged, ScanCompleted) to enable live TUI updates during scan execution.
Scanner Thread EventBus TUI Thread
────────────── ──────── ──────────
scan_initialization()
│
│ publishes ScanStarted (NEW in 6.6)
├──────────────────────▶ broadcast ─────────────▶ event_rx.recv()
│ │
│ ▼
│ aggregator.add_event()
│ │ (lifecycle event buffered)
│ ▼
port_scan()
│
│ publishes PortFound
├──────────────────────▶ broadcast ─────────────▶ event_rx.recv()
│ │
│ ▼
│ aggregator.add_event()
│ │
│ │ (aggregates, no processing)
│ ▼
│ (buffered)
│
│ publishes HostDiscovered
├──────────────────────▶ broadcast ─────────────▶ event_rx.recv()
│ │
│ ▼
│ aggregator.add_event()
│ │
│ ▼
│ (buffered)
│
stage_transition()
│
│ publishes StageChanged (NEW in 6.6)
├──────────────────────▶ broadcast ─────────────▶ event_rx.recv()
│ │
│ ▼
│ aggregator.add_event()
│ │ (lifecycle event buffered)
│ ▼
[16ms passes]
│ tick_interval.tick()
│ │
│ ▼
│ aggregator.should_flush()
│ │ (true)
│ ▼
│ flush() → (events, stats)
│ │
│ ▼
│ update scan_state
│ │
│ ▼
│ terminal.draw(render)
│
scan_completion()
│
│ publishes ScanCompleted (NEW in 6.6)
├──────────────────────▶ broadcast ─────────────▶ event_rx.recv()
│ │
│ ▼
│ aggregator.add_event()
│ │ (lifecycle event buffered)
│ ▼
New Lifecycle Events (Sprint 6.6):
- ScanStarted: Published at scan initialization, includes target info and scan type
- StageChanged: Published when scanner transitions between phases (Discovery → Enumeration → Deep Inspection)
- ScanCompleted: Published when scan finishes, includes final statistics
Terminal crossterm TUI Event Loop State
──────── ───────── ────────────── ─────
User presses 'q'
│
├──────────▶ EventStream.next()
│ │
│ ├──────────────▶ process_events()
│ │ │
│ │ │ matches KeyCode::Char('q')
│ │ ▼
│ │ return LoopControl::Quit
│ │ │
│ │ ▼
│ │ App::run() breaks loop
│ │ │
│ │ ▼
│ │ ratatui::restore()
│ │ │
│ │ ▼
│ │ Terminal restored
│
Scenario: Scanner finds 1,000 open ports in 10ms
Time Event Aggregator State TUI State
──── ───── ──────────────── ─────────
0ms PortFound #1 stats.ports_found = 1 (no update)
1ms PortFound #2 stats.ports_found = 2 (no update)
2ms PortFound #3 stats.ports_found = 3 (no update)
... ... ... ...
10ms PortFound #1000 stats.ports_found = 1000 (no update)
16ms (timer tick) flush() scan_state.open_ports += 1000
stats.ports_found = 0 (reset) terminal.draw(render)
UI displays: "Open Ports: 1000" (single update, no lag)
Without Aggregation: 1,000 state updates, 1,000 renders, UI freezes
With Aggregation: 1 batch update, 1 render, smooth 60 FPS
| Metric | Target | Achieved | Notes |
|---|---|---|---|
| FPS | 60 | 60 | 16.67ms frame budget |
| Event Rate | 10,000/sec | 10,000+ | Event aggregation |
| Latency | <100ms | 16ms | Max aggregation delay |
| Memory | <10MB | ~5MB | Event buffer + state |
| CPU | <10% | ~5% | Rendering overhead |
Test: 10,000 PortFound events in 1 second
Without Aggregation:
- Events: 10,000
- State Updates: 10,000
- Renders: 10,000 (impossible at 60 FPS)
- Result: UI freezes, dropped frames
With Aggregation (16ms interval):
- Events: 10,000
- Batches: 62 (1000ms / 16ms)
- State Updates: 62
- Renders: 60 (capped at 60 FPS)
- Result: Smooth UI, no dropped frames
Sprint 6.6 Enhancement: Memory-mapped I/O (mmap) for scan results reduces RAM footprint by 77-86% during internet-scale scans.
Component Size Notes
───────── ──── ─────
ScanState ~1 KB Arc<RwLock<T>>
UIState ~100 bytes Stack-allocated
EventAggregator ~100 KB 1,000 × ~100 bytes/event
Event Buffer ~100 KB MAX_BUFFER_SIZE
Terminal Buffer ~10 KB ratatui screen buffer
MmapResultWriter Variable Disk-backed (mmap mode only)
Total: ~211 KB + mmap (negligible overhead)
Memory-Mapped I/O (Sprint 6.6):
ProRT-IP supports two result storage modes:
-
Memory Mode (default for small scans):
- Results stored in
Vec<ScanResult> - Fast random access (O(1))
- Limited by available RAM (~10M results max)
- Results stored in
-
Mmap Mode (automatic for large scans):
- Results streamed to memory-mapped file
- Auto-grows in 1MB increments
- Zero-copy reads via
MmapResultReader - 77-86% RAM reduction (100K results: 35MB → 5MB)
- Transparent to TUI (same API)
Auto-Switching Threshold:
- Default: >10,000 expected results → mmap mode
- Configurable via
--mmap-threshold N
Implementation:
- MmapResultWriter (124 lines): bincode serialization, auto-growth
- MmapResultReader (219 lines): zero-copy iteration, index offsets
- ResultWriter enum: Memory vs Mmap mode abstraction
Component % CPU Notes
───────── ───── ─────
Event Processing ~2% Aggregation logic
State Updates ~1% RwLock write overhead
Rendering (ratatui) ~3% Diffing + terminal I/O
Keyboard Handling <1% Rare events
System Overhead ~1% tokio runtime
Total: ~8% CPU (on modern CPU at 10,000 events/sec)
Challenge: Scanner (background thread) needs to update state while TUI (main thread) reads it
Solution: Arc<RwLock<ScanState>>
// Scanner thread (writer)
let state = scan_state.write(); // Exclusive lock (blocks readers)
state.open_ports += 1;
drop(state); // Release lock ASAP
// TUI thread (reader)
let state = scan_state.read(); // Shared lock (many readers)
let open_ports = state.open_ports;
drop(state); // Release lock ASAPBest Practices:
- Hold locks briefly: Read/write data, then drop lock immediately
- Avoid nested locks: Prevents deadlocks
- Batch updates: Write multiple fields in single lock acquisition
- Read consistency: Take read lock once per frame, copy data to local vars
Problem: High-frequency writes can block readers (UI stutters)
Solution 1: Event aggregation (batch writes every 16ms)
// Before: 1,000 writes/second (1ms each = UI blocked)
for port in ports {
let mut state = scan_state.write(); // LOCK
state.open_ports += 1; // WRITE
} // UNLOCK
// After: 60 writes/second (16ms batches = smooth UI)
let (events, stats) = aggregator.flush();
let mut state = scan_state.write(); // LOCK ONCE
state.open_ports += stats.ports_found; // BATCH WRITE
drop(state); // UNLOCKSolution 2: parking_lot::RwLock (faster than std::sync::RwLock)
- Fast path: Lock-free reads when no writers
- Writer priority: Prevents writer starvation
- Benchmarks: 2-3× faster than std::sync::RwLock
// 1. Initialize terminal (ratatui 0.29+ handles panic hook automatically)
let mut terminal = ratatui::init();
// What this does internally:
// - crossterm::terminal::enable_raw_mode()
// - crossterm::execute!(stdout, EnterAlternateScreen)
// - Set panic hook for cleanup// 2. Restore terminal on normal exit
ratatui::restore();
// What this does internally:
// - crossterm::execute!(stdout, LeaveAlternateScreen)
// - crossterm::terminal::disable_raw_mode()// ratatui 0.29+ automatically handles panic restoration
// No manual cleanup needed!
// Before (manual):
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
ratatui::restore();
original_hook(panic_info);
}));
// After (automatic):
// ratatui::init() handles this for you// Keyboard event loop detects Ctrl+C
tokio::select! {
Some(Ok(Event::Key(key))) = crossterm_rx.next() => {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
return LoopControl::Quit; // Graceful exit
}
// ...
}
}
}
}
// Main loop breaks, terminal restored in App::run() cleanupLocation: src/events/aggregator.rs
Coverage:
- Event aggregation logic (count PortFound, HostDiscovered)
- Buffer limit enforcement (drop events when full)
- Flush behavior (reset statistics after flush)
- Deduplication (unique IPs in discovered_ips)
Example:
#[test]
fn test_aggregator_buffer_limit() {
let mut agg = EventAggregator::new();
// Fill buffer to MAX_BUFFER_SIZE
for i in 0..MAX_BUFFER_SIZE {
assert!(agg.add_event(ScanEvent::ProgressUpdate { ... }));
}
// Next event should be dropped
assert!(!agg.add_event(ScanEvent::ProgressUpdate { ... }));
assert_eq!(agg.stats().dropped_events, 1);
}Location: tests/integration_test.rs
Coverage:
- App creation and lifecycle
- ScanState initialization and shared state
- UIState pane navigation, help toggle, cursor movement
- EventAggregator timing (16ms flush interval)
- EventBus subscription (async test)
- Multiple apps sharing state
Example:
#[test]
fn test_scan_state_shared() {
let state1 = ScanState::shared();
let state2 = Arc::clone(&state1);
// Modify via state1
{ let mut s = state1.write(); s.open_ports = 10; }
// Read via state2 (sees changes)
{ let s = state2.read(); assert_eq!(s.open_ports, 10); }
}Location: Inline in source code
Coverage:
- App::new() example
- Crate-level example (lib.rs)
- Component trait (ignored until implementation)
Example:
/// # Examples
///
/// ```rust,no_run
/// use prtip_tui::App;
/// use prtip_core::event_bus::EventBus;
/// use std::sync::Arc;
///
/// #[tokio::main]
/// async fn main() -> anyhow::Result<()> {
/// let event_bus = Arc::new(EventBus::new(1000));
/// let mut app = App::new(event_bus);
/// app.run().await?;
/// Ok(())
/// }
/// ```
Phase 6 Complete (Sprints 6.1-6.8):
Test Type Count Status Coverage
───────── ───── ────── ────────
Unit Tests 190 ✓ Pass Aggregator, All Widgets (137 widget tests)
Integration 38 ✓ Pass App, State, Events, Tab switching, Interactive widgets
Doctests 2 ✓ Pass Public API examples
1 Ignored Future Component trait
Total 231 228 Pass Comprehensive (3 ignored)
Widget Test Breakdown (137 unit tests):
- PortTableWidget: 14 tests (sorting, filtering)
- ServiceTableWidget: 21 tests (sorting, filtering, color coding)
- MetricsDashboardWidget: 24 tests (calculations, formatting, edge cases)
- FileBrowserWidget: 22 tests (navigation, selection, filtering)
- PortSelectionWidget: 18 tests (range validation, templates)
- ShortcutsWidget: 16 tests (categorization, search, display)
- TemplateSelectionWidget: 22 tests (template loading, filtering, creation)
Goal: Implement production-ready TUI components
Components:
- MainWidget: Sortable port table with service info
- StatusBar: Real-time progress bar with ETA
- LogWidget: Scrollable event log with filtering
- ChartWidget: Throughput graph (sparkline)
Implementation Plan:
- Use
tui-textareafor scrollable content - Implement
Componenttrait for all widgets - Add keyboard navigation (arrow keys, Page Up/Down)
- Support terminal resize events
Features:
- Interactive Mode: Pause/resume scans, adjust parameters
- Export: Save results to JSON/XML during scan
- Themes: Customizable color schemes
- Plugins: Lua scripts for custom widgets
Optimizations:
- Zero-copy Rendering: Avoid cloning state in render loop
- Incremental Rendering: Only redraw changed areas
- GPU Acceleration: kitty/wezterm graphics protocol
- Compression: Compress event buffer for low-memory systems
- ratatui: https://ratatui.rs/
- crossterm: https://docs.rs/crossterm/
- tokio::select!: https://docs.rs/tokio/latest/tokio/macro.select.html
- parking_lot: https://docs.rs/parking_lot/
- 00-ARCHITECTURE.md: Overall system design
- 01-ROADMAP.md: Phase 6 TUI development plan
- 10-PROJECT-STATUS.md: Current sprint status
crates/prtip-tui/
├── src/
│ ├── lib.rs # Public API exports
│ ├── app.rs # App lifecycle
│ ├── state/
│ │ ├── mod.rs # State re-exports
│ │ ├── scan_state.rs # Shared scanner state
│ │ └── ui_state.rs # Local TUI state
│ ├── events/
│ │ ├── mod.rs # Event re-exports
│ │ ├── aggregator.rs # Event rate limiting
│ │ ├── loop.rs # Event loop coordination
│ │ └── handlers.rs # Keyboard handlers
│ ├── ui/
│ │ ├── mod.rs # UI re-exports
│ │ ├── renderer.rs # 60 FPS rendering
│ │ ├── layout.rs # Layout functions
│ │ └── theme.rs # Color schemes
│ └── widgets/
│ ├── mod.rs # Widget re-exports
│ ├── component.rs # Component trait
│ ├── main.rs # Main area widget (Phase 6.2)
│ ├── status.rs # Status bar (Phase 6.2)
│ └── help.rs # Help screen (Phase 6.2)
├── tests/
│ └── integration_test.rs # Integration tests
├── Cargo.toml # Dependencies
└── README.md # Crate overview
- Immediate Mode Rendering: Full UI redrawn every frame, framework diffs and updates
- Event Aggregation: Batching high-frequency events to prevent UI overload
- 60 FPS: 60 frames per second (16.67ms per frame)
- RwLock: Read-write lock (many readers, one writer)
- Arc: Atomic reference counting (shared ownership across threads)
- EventBus: Publish-subscribe event system
- ratatui: Rust TUI framework (fork of tui-rs)
- crossterm: Cross-platform terminal manipulation
- tokio::select!: Concurrent event handling macro
End of TUI Architecture Documentation
For questions or feedback, see ProRT-IP repository issues.