""" Modern TUI Interface for StreamLens Focused on Flow Analysis, Packet Decoding, and Statistical Analysis """ import curses from typing import TYPE_CHECKING, List, Optional from enum import Enum from .navigation import NavigationHandler from .modern_views import FlowAnalysisView, PacketDecoderView, StatisticalAnalysisView from ..utils.signal_visualizer import signal_visualizer if TYPE_CHECKING: from ..analysis.core import EthernetAnalyzer class ViewMode(Enum): FLOW_ANALYSIS = "flow" PACKET_DECODER = "decode" STATISTICAL_ANALYSIS = "stats" class ModernTUIInterface: """ Modern StreamLens TUI Interface Three primary views accessed via 1/2/3: - 1: Flow Analysis - Visual flow overview with enhanced protocol detection - 2: Packet Decoder - Deep protocol inspection and field extraction - 3: Statistical Analysis - Timing analysis, outliers, and quality metrics """ def __init__(self, analyzer: 'EthernetAnalyzer'): self.analyzer = analyzer self.navigation = NavigationHandler() # Current view mode self.current_view = ViewMode.FLOW_ANALYSIS # Initialize view controllers self.flow_view = FlowAnalysisView(analyzer) self.decoder_view = PacketDecoderView(analyzer) self.stats_view = StatisticalAnalysisView(analyzer) # Global state self.selected_flow_key = None self.show_help = False def run(self, stdscr): """Main TUI loop for modern interface""" curses.curs_set(0) # Hide cursor stdscr.keypad(True) # Set timeout based on live mode if self.analyzer.is_live: stdscr.timeout(500) # 0.5 second for live updates else: stdscr.timeout(1000) # 1 second for static analysis while True: try: stdscr.clear() # Draw header with view indicators self._draw_header(stdscr) # Draw main content based on current view if self.current_view == ViewMode.FLOW_ANALYSIS: self.flow_view.draw(stdscr, self.selected_flow_key) elif self.current_view == ViewMode.PACKET_DECODER: self.decoder_view.draw(stdscr, self.selected_flow_key) elif self.current_view == ViewMode.STATISTICAL_ANALYSIS: self.stats_view.draw(stdscr, self.selected_flow_key) # Draw status bar self._draw_status_bar(stdscr) # Overlay help if requested if self.show_help: self._draw_help_overlay(stdscr) stdscr.refresh() # Handle input key = stdscr.getch() # Handle timeout (no key pressed) - refresh for live capture if key == -1 and self.analyzer.is_live: continue action = self._handle_input(key) if action == 'quit': if self.analyzer.is_live: self.analyzer.stop_capture = True break except curses.error: # Handle terminal resize or other curses errors pass def _draw_header(self, stdscr): """Draw the header with application title and view tabs""" height, width = stdscr.getmaxyx() # Title title = "StreamLens - Ethernet Traffic Analyzer" stdscr.addstr(0, 2, title, curses.A_BOLD) # Live indicator if self.analyzer.is_live: live_text = "[LIVE]" stdscr.addstr(0, width - len(live_text) - 2, live_text, curses.A_BOLD | curses.A_BLINK) # View tabs tab_line = 1 tab_x = 2 # 1: Flow Analysis if self.current_view == ViewMode.FLOW_ANALYSIS: stdscr.addstr(tab_line, tab_x, "[1: Flow Analysis]", curses.A_REVERSE) else: stdscr.addstr(tab_line, tab_x, " 1: Flow Analysis ", curses.A_DIM) tab_x += 19 # 2: Packet Decoder if self.current_view == ViewMode.PACKET_DECODER: stdscr.addstr(tab_line, tab_x, "[2: Packet Decoder]", curses.A_REVERSE) else: stdscr.addstr(tab_line, tab_x, " 2: Packet Decoder ", curses.A_DIM) tab_x += 20 # 3: Statistical Analysis if self.current_view == ViewMode.STATISTICAL_ANALYSIS: stdscr.addstr(tab_line, tab_x, "[3: Statistical Analysis]", curses.A_REVERSE) else: stdscr.addstr(tab_line, tab_x, " 3: Statistical Analysis ", curses.A_DIM) # Draw separator line stdscr.addstr(2, 0, "─" * width) def _draw_status_bar(self, stdscr): """Draw status bar with context-sensitive help""" height, width = stdscr.getmaxyx() status_y = height - 1 # Base controls status_text = "[1-3]Views [↑↓]Navigate [Enter]Select [H]Help [Q]Quit" # Add view-specific controls if self.current_view == ViewMode.FLOW_ANALYSIS: status_text += " [V]Visualize [D]Decode" elif self.current_view == ViewMode.PACKET_DECODER: status_text += " [E]Export [C]Copy" elif self.current_view == ViewMode.STATISTICAL_ANALYSIS: status_text += " [R]Refresh [O]Outliers" # Add live capture controls if self.analyzer.is_live: status_text += " [P]Pause" stdscr.addstr(status_y, 0, status_text[:width-1], curses.A_REVERSE) def _draw_help_overlay(self, stdscr): """Draw help overlay with comprehensive controls""" height, width = stdscr.getmaxyx() # Calculate overlay size overlay_height = min(20, height - 4) overlay_width = min(80, width - 4) start_y = (height - overlay_height) // 2 start_x = (width - overlay_width) // 2 # Create help window help_lines = [ "StreamLens - Help", "", "VIEWS:", " 1 - Flow Analysis: Visual flow overview and protocol detection", " 2 - Packet Decoder: Deep packet inspection and field extraction", " 3 - Statistical Analysis: Timing analysis and quality metrics", "", "NAVIGATION:", " ↑/↓ - Navigate items", " Enter - Select flow/packet", " Tab - Switch panels (when available)", " PgUp/PgDn - Scroll large lists", "", "ANALYSIS:", " V - Visualize signals (Flow Analysis)", " D - Deep decode selected flow", " E - Export decoded data", " R - Refresh statistics", " O - Show outlier details", "", "GENERAL:", " H - Toggle this help", " Q - Quit application", "", "Press any key to close help..." ] # Draw background for y in range(overlay_height): stdscr.addstr(start_y + y, start_x, " " * overlay_width, curses.A_REVERSE) # Draw help content for i, line in enumerate(help_lines[:overlay_height-1]): if start_y + i < height - 1: display_line = line[:overlay_width-2] attr = curses.A_REVERSE | curses.A_BOLD if i == 0 else curses.A_REVERSE stdscr.addstr(start_y + i, start_x + 1, display_line, attr) def _handle_input(self, key: int) -> str: """Handle keyboard input with view-specific actions""" # Global controls if key == ord('q') or key == ord('Q'): return 'quit' elif key == ord('h') or key == ord('H'): self.show_help = not self.show_help return 'help_toggle' elif self.show_help: # Any key closes help self.show_help = False return 'help_close' # View switching elif key == ord('1'): self.current_view = ViewMode.FLOW_ANALYSIS return 'view_change' elif key == ord('2'): self.current_view = ViewMode.PACKET_DECODER return 'view_change' elif key == ord('3'): self.current_view = ViewMode.STATISTICAL_ANALYSIS return 'view_change' # Delegate to current view elif self.current_view == ViewMode.FLOW_ANALYSIS: return self.flow_view.handle_input(key, self._get_flows_list()) elif self.current_view == ViewMode.PACKET_DECODER: return self.decoder_view.handle_input(key, self._get_flows_list()) elif self.current_view == ViewMode.STATISTICAL_ANALYSIS: return self.stats_view.handle_input(key, self._get_flows_list()) return 'none' def _get_flows_list(self): """Get prioritized list of flows for analysis""" flows_list = list(self.analyzer.flows.values()) # Sort by relevance: enhanced flows first, then by packet count flows_list.sort(key=lambda x: ( x.enhanced_analysis.decoder_type != "Standard", # Enhanced first self.analyzer.statistics_engine.get_max_sigma_deviation(x), # High outliers x.frame_count # Packet count ), reverse=True) return flows_list def get_selected_flow(self): """Get currently selected flow for cross-view communication""" if self.selected_flow_key: return self.analyzer.flows.get(self.selected_flow_key) return None def set_selected_flow(self, flow_key): """Set selected flow for cross-view communication""" self.selected_flow_key = flow_key