Files
StreamLens/analyzer/tui/modern_interface.py

272 lines
9.9 KiB
Python
Raw Normal View History

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