Major Features: - Complete modern TUI interface with three focused views - Enhanced multi-column layout: Source | Proto | Destination | Extended | Frame Type | Metrics - Simplified navigation with 1/2/3 hotkeys instead of F1/F2/F3 - Protocol hierarchy: Transport (TCP/UDP) → Extended (CH10/PTP) → Frame Types - Classic TUI preserved with --classic flag Views Implemented: 1. Flow Analysis View: Enhanced multi-column flow overview with protocol detection 2. Packet Decoder View: Three-panel deep inspection (Flows | Frames | Fields) 3. Statistical Analysis View: Four analysis modes with timing and quality metrics Technical Improvements: - Left-aligned text columns with IP:port precision - Transport protocol separation from extended protocols - Frame type identification (CH10-Data, TMATS, PTP Sync) - Cross-view communication with persistent flow selection - Context-sensitive help and status bars - Comprehensive error handling with console fallback
272 lines
9.9 KiB
Python
272 lines
9.9 KiB
Python
"""
|
|
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 |