pretty good

This commit is contained in:
2025-07-28 08:14:15 -04:00
parent 36a576dc2c
commit 4dd632012f
21 changed files with 2174 additions and 152 deletions

View File

@@ -8,6 +8,7 @@ from textual.containers import Container, Horizontal, Vertical, ScrollableContai
from textual.widgets import Header, Footer, Static, DataTable, Label
from textual.reactive import reactive
from textual.timer import Timer
from textual.events import MouseDown, MouseMove
from typing import TYPE_CHECKING
from rich.text import Text
from rich.console import Group
@@ -37,6 +38,8 @@ class StreamLensAppV2(App):
"""
CSS_PATH = "styles/streamlens_v2.tcss"
ENABLE_COMMAND_PALETTE = False
AUTO_FOCUS = None
BINDINGS = [
("q", "quit", "Quit"),
@@ -79,40 +82,13 @@ class StreamLensAppV2(App):
yield Header()
with Container(id="main-container"):
# Top metrics bar - compact like TipTop
# Ultra-compact metrics bar
with Horizontal(id="metrics-bar"):
yield MetricCard(
"Flows",
f"{self.total_flows}",
trend="stable",
id="flows-metric"
)
yield MetricCard(
"Packets/s",
f"{self.packets_per_sec:.1f}",
trend="up",
sparkline=True,
id="packets-metric"
)
yield MetricCard(
"Volume/s",
self._format_bytes_per_sec(self.bytes_per_sec),
trend="stable",
sparkline=True,
id="volume-metric"
)
yield MetricCard(
"Enhanced",
f"{self.enhanced_flows}",
color="success",
id="enhanced-metric"
)
yield MetricCard(
"Outliers",
f"{self.outlier_count}",
color="warning" if self.outlier_count > 0 else "normal",
id="outliers-metric"
)
yield MetricCard("Flows", f"{self.total_flows}", id="flows-metric")
yield MetricCard("Pkts/s", f"{self.packets_per_sec:.0f}", id="packets-metric")
yield MetricCard("Vol/s", self._format_bytes_per_sec(self.bytes_per_sec), id="volume-metric")
yield MetricCard("Enhanced", f"{self.enhanced_flows}", color="success", id="enhanced-metric")
yield MetricCard("Outliers", f"{self.outlier_count}", color="warning" if self.outlier_count > 0 else "normal", id="outliers-metric")
# Main content area with horizontal split
with Horizontal(id="content-area"):
@@ -132,7 +108,7 @@ class StreamLensAppV2(App):
yield Footer()
def on_mount(self) -> None:
"""Initialize the application with TipTop-style updates"""
"""Initialize the application with TipTop-style updates"""
self.update_metrics()
# Set up update intervals like TipTop
@@ -141,7 +117,20 @@ class StreamLensAppV2(App):
# Initialize sparkline history
self._initialize_history()
# Set initial focus to the flow table for immediate keyboard navigation
self.call_after_refresh(self._set_initial_focus)
def _set_initial_focus(self):
"""Set initial focus to the flow table after widgets are ready"""
try:
flow_table = self.query_one("#flow-table", EnhancedFlowTable)
data_table = flow_table.query_one("#flows-data-table", DataTable)
data_table.focus()
except Exception:
# If table isn't ready yet, try again after a short delay
self.set_timer(0.1, self._set_initial_focus)
def _initialize_history(self):
"""Initialize metrics history arrays"""
current_time = time.time()
@@ -281,4 +270,12 @@ class StreamLensAppV2(App):
def action_show_details(self) -> None:
"""Show detailed view for selected flow"""
# TODO: Implement detailed flow modal
pass
pass
def on_mouse_down(self, event: MouseDown) -> None:
"""Prevent default mouse down behavior to disable mouse interaction."""
event.prevent_default()
def on_mouse_move(self, event: MouseMove) -> None:
"""Prevent default mouse move behavior to disable mouse interaction."""
event.prevent_default()

View File

@@ -1,88 +1,72 @@
/* StreamLens V2 - TipTop-Inspired Styling */
/* Color Scheme - Dark theme with vibrant accents */
$primary: #0080ff;
$primary-lighten-1: #3399ff;
$primary-lighten-2: #66b3ff;
$primary-lighten-3: #99ccff;
$accent: #00ffcc;
$success: #00ff88;
$warning: #ffcc00;
$error: #ff3366;
$surface: #1a1a1a;
$surface-lighten-1: #262626;
$surface-lighten-2: #333333;
$background: #0d0d0d;
$text: #ffffff;
$text-muted: #999999;
/* StreamLens V2 - Compact Styling */
/* Main Application Layout */
Screen {
background: $background;
background: #0d0d0d;
}
#main-container {
height: 1fr;
background: $background;
background: #0d0d0d;
}
/* Metrics Bar - Horizontal compact display at top */
/* Metrics Bar - Ultra compact display at top */
#metrics-bar {
height: 7;
padding: 1;
background: $surface;
border-bottom: thick $primary;
height: 3;
padding: 0;
background: #1a1a1a;
border-bottom: solid #0080ff;
align: center middle;
}
MetricCard {
width: 1fr;
height: 5;
margin: 0 1;
max-width: 20;
border: tall $primary-lighten-2;
padding: 0 1;
height: 3;
margin: 0;
max-width: 18;
border: none;
padding: 0;
align: center middle;
}
/* Content Area - Three column layout */
/* Content Area - Maximized for grid */
#content-area {
height: 1fr;
padding: 1;
padding: 0;
}
/* Panel Styling */
/* Panel Styling - Minimal borders */
.panel {
border: solid $primary-lighten-3;
padding: 1;
margin: 0 1;
border: solid #99ccff;
padding: 0;
margin: 0;
}
.panel-wide {
border: solid $primary-lighten-3;
padding: 1;
margin: 0 1;
border: solid #99ccff;
padding: 0;
margin: 0;
}
.panel-header {
text-align: center;
text-style: bold;
color: $accent;
color: #00ffcc;
margin-bottom: 1;
}
/* Left Panel - Main Flow Table (expanded) */
/* Left Panel - Main Flow Table (maximized) */
#left-panel {
width: 70%;
background: $surface;
width: 75%;
background: #1a1a1a;
padding: 0;
}
/* Right Panel - Details */
/* Right Panel - Details (compact) */
#right-panel {
width: 30%;
background: $surface;
width: 25%;
background: #1a1a1a;
padding: 0;
}
/* Sparkline Charts */
@@ -95,114 +79,118 @@ SparklineWidget {
/* Enhanced Flow Table */
#flows-data-table {
height: 1fr;
scrollbar-background: $surface-lighten-1;
scrollbar-color: $primary;
scrollbar-background: #262626;
scrollbar-color: #0080ff;
scrollbar-size: 1 1;
}
#flows-data-table > .datatable--header {
background: $surface-lighten-2;
color: $accent;
background: #333333;
color: #00ffcc;
text-style: bold;
}
#flows-data-table > .datatable--cursor {
background: $primary 30%;
color: $text;
background: #0080ff 30%;
color: #ffffff;
}
#flows-data-table > .datatable--hover {
background: $primary 20%;
background: #0080ff 20%;
}
#flows-data-table > .datatable--odd-row {
background: $surface;
background: #1a1a1a;
}
#flows-data-table > .datatable--even-row {
background: $surface-lighten-1;
background: #262626;
}
/* Flow Details Panel */
/* Flow Details Panel - Compact */
FlowDetailsPanel {
padding: 1;
padding: 0;
}
FlowDetailsPanel Panel {
margin-bottom: 1;
margin-bottom: 0;
}
/* Status Colors */
.status-normal {
color: $success;
color: #00ff88;
}
.status-warning {
color: $warning;
color: #ffcc00;
}
.status-error {
color: $error;
color: #ff3366;
}
.status-enhanced {
color: $accent;
color: #00ffcc;
text-style: bold;
}
/* Quality Indicators */
.quality-high {
color: $success;
color: #00ff88;
}
.quality-medium {
color: $warning;
color: #ffcc00;
}
.quality-low {
color: $error;
color: #ff3366;
}
/* Animations and Transitions */
.updating {
background: $primary 10%;
background: #0080ff 10%;
transition: background 200ms;
}
/* Header and Footer */
/* Header and Footer - Ultra compact */
Header {
background: $surface;
color: $text;
border-bottom: solid $primary;
background: #1a1a1a;
color: #ffffff;
border-bottom: solid #0080ff;
height: 1;
padding: 0;
}
Footer {
background: $surface;
color: $text-muted;
border-top: solid $primary;
background: #1a1a1a;
color: #999999;
border-top: solid #0080ff;
height: 1;
padding: 0;
}
/* Scrollbars */
Vertical {
scrollbar-size: 1 1;
scrollbar-background: $surface-lighten-1;
scrollbar-color: $primary;
scrollbar-background: #262626;
scrollbar-color: #0080ff;
}
Horizontal {
scrollbar-size: 1 1;
scrollbar-background: $surface-lighten-1;
scrollbar-color: $primary;
scrollbar-background: #262626;
scrollbar-color: #0080ff;
}
/* Focus States */
DataTable:focus {
border: solid $accent;
border: solid #00ffcc;
}
/* Panel Borders */
Static {
border: round $primary;
border: round #0080ff;
}
/* End of styles */

View File

@@ -69,9 +69,11 @@ class FlowAnalysisWidget(Vertical):
if not self.flow_table:
return
# Preserve cursor position
# Preserve cursor and scroll positions
cursor_row = self.flow_table.cursor_row
cursor_column = self.flow_table.cursor_column
scroll_x = self.flow_table.scroll_x
scroll_y = self.flow_table.scroll_y
selected_row_key = None
if self.flow_table.rows and cursor_row < len(self.flow_table.rows):
selected_row_key = list(self.flow_table.rows.keys())[cursor_row]
@@ -108,6 +110,9 @@ class FlowAnalysisWidget(Vertical):
# If original selection not found, try to maintain row position
new_row = min(cursor_row, self.flow_table.row_count - 1)
self.flow_table.move_cursor(row=new_row, column=cursor_column, animate=False)
# Restore scroll position
self.flow_table.scroll_to(x=scroll_x, y=scroll_y, animate=False)
def _create_flow_row(self, flow_num: int, flow: 'FlowStats') -> List[Text]:
"""Create main flow row with rich text formatting"""

View File

@@ -29,11 +29,15 @@ class EnhancedFlowTable(Vertical):
DEFAULT_CSS = """
EnhancedFlowTable {
height: 1fr;
padding: 0;
margin: 0;
}
EnhancedFlowTable DataTable {
height: 1fr;
scrollbar-gutter: stable;
padding: 0;
margin: 0;
}
"""
@@ -62,17 +66,15 @@ class EnhancedFlowTable(Vertical):
"""Initialize the table"""
table = self.query_one("#flows-data-table", DataTable)
# Add columns with explicit keys to avoid auto-generated keys
table.add_column("#", width=3, key="num")
table.add_column("Source", width=22, key="source")
table.add_column("Proto", width=6, key="proto")
table.add_column("Destination", width=22, key="dest")
table.add_column("Extended", width=10, key="extended")
table.add_column("Frame Type", width=12, key="frame_type")
table.add_column("Rate", width=12, key="rate")
table.add_column("Volume", width=12, key="volume")
table.add_column("Quality", width=12, key="quality")
table.add_column("Status", width=8, key="status")
# Compact columns optimized for data density
table.add_column("#", width=2, key="num")
table.add_column("Source", width=18, key="source")
table.add_column("Proto", width=4, key="proto")
table.add_column("Destination", width=18, key="dest")
table.add_column("Extended", width=8, key="extended")
table.add_column("Frame Type", width=10, key="frame_type")
table.add_column("Pkts", width=6, key="rate")
table.add_column("Size", width=8, key="volume")
self.refresh_data()
@@ -80,9 +82,11 @@ class EnhancedFlowTable(Vertical):
"""Refresh flow table with enhanced visualizations"""
table = self.query_one("#flows-data-table", DataTable)
# Preserve cursor position
# Preserve cursor and scroll positions
cursor_row = table.cursor_row
cursor_column = table.cursor_column
scroll_x = table.scroll_x
scroll_y = table.scroll_y
selected_row_key = None
if table.rows and cursor_row < len(table.rows):
selected_row_key = list(table.rows.keys())[cursor_row]
@@ -148,6 +152,9 @@ class EnhancedFlowTable(Vertical):
# If original selection not found, try to maintain row position
new_row = min(cursor_row, table.row_count - 1)
table.move_cursor(row=new_row, column=cursor_column, animate=False)
# Restore scroll position
table.scroll_to(x=scroll_x, y=scroll_y, animate=False)
def _create_enhanced_row(self, num: int, flow: 'FlowStats', metrics: dict) -> List[Text]:
"""Create enhanced row with inline visualizations"""
@@ -177,10 +184,9 @@ class EnhancedFlowTable(Vertical):
rate_spark = self._create_rate_sparkline(metrics['rate_history'])
rate_text = Text(f"{metrics['rate_history'][-1]:.0f} {rate_spark}")
# Volume with bar chart
volume_bar = self._create_volume_bar(flow.total_bytes)
volume_value = self._format_bytes(flow.total_bytes)
volume_text = Text(f"{volume_value:>6} {volume_bar}")
# Size with actual value
size_value = self._format_bytes(flow.total_bytes)
size_text = Text(f"{size_value:>8}")
# Quality with bar chart and color
quality_bar, quality_color = self._create_quality_bar(flow)
@@ -199,8 +205,7 @@ class EnhancedFlowTable(Vertical):
return [
num_text, source_text, proto_text, dest_text,
extended_text, frame_text, rate_text, volume_text,
quality_text, status_text
extended_text, frame_text, rate_text, size_text
]
def _create_rate_sparkline(self, history: List[float]) -> str:
@@ -308,12 +313,10 @@ class EnhancedFlowTable(Vertical):
Text(""), # Empty source
Text(""), # Empty protocol
Text(""), # Empty destination
Text(f" └─ {extended_proto}", style="dim yellow"),
Text(f" {extended_proto}", style="dim yellow"),
Text(frame_type, style="dim blue"),
Text(f"{count}", style="dim", justify="right"),
Text(f"{percentage:.0f}%", style="dim"),
Text(""), # Empty quality
Text("") # Empty status
Text(f"{self._format_bytes(count * (flow.total_bytes // flow.frame_count) if flow.frame_count > 0 else 0):>8}", style="dim")
]
subrows.append(subrow)

View File

@@ -29,19 +29,23 @@ class MetricCard(Widget):
MetricCard {
width: 1fr;
height: 3;
margin: 0 1;
margin: 0;
padding: 0;
}
MetricCard.success {
border: solid $success;
border: none;
color: #00ff88;
}
MetricCard.warning {
border: solid $warning;
border: none;
color: #ffcc00;
}
MetricCard.error {
border: solid $error;
border: none;
color: #ff3366;
}
"""
@@ -106,18 +110,15 @@ class MetricCard(Widget):
if self.sparkline and self.spark_data:
spark_str = " " + self._create_mini_spark()
# Format content
# Ultra compact - single line format
content = Text()
content.append(f"{self.title}\n", style="dim")
content.append(f"{self.title}: ", style="dim")
content.append(f"{self.value}", style=f"bold {style}")
content.append(trend_icon, style=style)
content.append(spark_str, style="dim cyan")
return Panel(
content,
height=3,
border_style=style if self.color != "normal" else "dim"
)
# Super compact - no panel, just text
return content
def _create_mini_spark(self) -> str:
"""Create mini sparkline for inline display"""