feat: Add scrollable packet details viewport and TAB focus switching

This commit is contained in:
Jose Luis Montañes Ojados
2026-01-19 16:43:08 +01:00
parent 0566cb9268
commit c4794ad787

View File

@@ -82,6 +82,10 @@ type Model struct {
flowList list.Model
viewport viewport.Model
// Packet Details View
detailsViewport viewport.Model
focusPacketDetails bool
// File browser for pcap import
fileBrowser FileBrowserModel
loadedPcapPath string
@@ -568,13 +572,30 @@ func (m *Model) updateSubView(msg tea.Msg) (tea.Model, tea.Cmd) {
case SubViewCallDetail:
var cmd tea.Cmd
// We handle scrolling manually via selection now
// Handle Focus Switching
if keyMsg, ok := msg.(tea.KeyMsg); ok {
switch keyMsg.String() {
case "esc", "q":
m.subView = SubViewNone
m.selectedPacketIndex = 0 // Reset selection
m.focusPacketDetails = false
return m, nil
case "tab":
m.focusPacketDetails = !m.focusPacketDetails
return m, nil
}
}
// Route keys based on focus
if m.focusPacketDetails {
// Forward keys to Details Viewport for scrolling
m.detailsViewport, cmd = m.detailsViewport.Update(msg)
return m, cmd
} else {
// Handle Flow Navigation
if keyMsg, ok := msg.(tea.KeyMsg); ok {
switch keyMsg.String() {
case "up", "k":
if m.selectedPacketIndex > 0 {
m.selectedPacketIndex--
@@ -582,6 +603,11 @@ func (m *Model) updateSubView(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.selectedPacketIndex < m.viewport.YOffset {
m.viewport.SetYOffset(m.selectedPacketIndex)
}
// Force update of details content happens in View currently, but for state consistency
// we should probably do it here or let View handle it.
// Since View is pure function of state, it's fine.
// But detailsViewport scroll position should reset if packet changes?
m.detailsViewport.GotoTop()
}
case "down", "j":
flows := m.callFlowStore.GetRecentFlows(20)
@@ -593,16 +619,12 @@ func (m *Model) updateSubView(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.selectedPacketIndex >= m.viewport.YOffset+m.viewport.Height {
m.viewport.SetYOffset(m.selectedPacketIndex - m.viewport.Height + 1)
}
m.detailsViewport.GotoTop()
}
}
}
}
}
// We generally don't need viewport.Update for keys anymore as we handle scroll manually
// But we might need it for resizing or other generic msgs?
// Actually viewport.Update handles scroll keys by default.
// If we return cmd from it, it might conflict.
// Let's NOT call viewport.Update for keys if we consumed them.
return m, cmd
}
@@ -889,25 +911,22 @@ func (m Model) renderCallDetail() string {
}
// --- Left Pane BOTTOM (Selected Packet Details) ---
var leftBot strings.Builder
leftBot.WriteString(m.styles.Title.Render("📦 Packet Details"))
leftBot.WriteString("\n\n")
var detailsContent string
if m.selectedPacketIndex < len(flow.Packets) {
pkt := flow.Packets[m.selectedPacketIndex]
leftBot.WriteString(fmt.Sprintf("Time: %s\n", pkt.Timestamp.Format("15:04:05.000")))
leftBot.WriteString(fmt.Sprintf("IP: %s -> %s\n\n", pkt.SourceIP, pkt.DestIP))
// Render Payload/Raw
// Truncate if too long?
raw := pkt.Raw
// Simple header parsing or just raw dump?
// Raw dump is useful.
leftBot.WriteString(raw)
detailsContent = fmt.Sprintf("Time: %s\nIP: %s -> %s\n\n%s",
pkt.Timestamp.Format("15:04:05.000"),
pkt.SourceIP, pkt.DestIP,
pkt.Raw)
} else {
leftBot.WriteString("No packet selected")
detailsContent = "No packet selected"
}
// Update details viewport content
m.detailsViewport.SetContent(detailsContent)
m.detailsViewport.Width = leftW - 2 // Account for padding/borders roughly
m.detailsViewport.Height = leftBotH
// --- Right Pane (Transaction Flow) ---
var right strings.Builder
// right.WriteString("Transaction Flow:\n\n") // Removed header to save space or added to viewport content
@@ -988,14 +1007,25 @@ func (m Model) renderCallDetail() string {
// Set content to viewport
m.viewport.SetContent(right.String())
// Determine Border Colors based on Focus
detailsBorderColor := lipgloss.Color("#44475A")
flowBorderColor := lipgloss.Color("#44475A")
if m.focusPacketDetails {
detailsBorderColor = lipgloss.Color("#bd93f9") // Active Purple
} else {
flowBorderColor = lipgloss.Color("#bd93f9")
}
// Layout Construction
leftTopStyle := lipgloss.NewStyle().Width(leftW).Height(leftTopH).Padding(1, 2)
leftBotStyle := lipgloss.NewStyle().Width(leftW).Height(leftBotH).Padding(1, 2).Border(lipgloss.NormalBorder(), true, false, false, false).BorderForeground(lipgloss.Color("#44475A")) // Top border for bottom pane
rightStyle := lipgloss.NewStyle().Width(rightW).Padding(0, 1).Border(lipgloss.NormalBorder(), false, false, false, true).BorderForeground(lipgloss.Color("#44475A")) // Left border for right pane
leftBotStyle := lipgloss.NewStyle().Width(leftW).Height(leftBotH).Padding(0, 1).Border(lipgloss.NormalBorder(), true, false, false, false).BorderForeground(detailsBorderColor)
rightStyle := lipgloss.NewStyle().Width(rightW).Padding(0, 1).Border(lipgloss.NormalBorder(), false, false, false, true).BorderForeground(flowBorderColor)
// Render left pane parts
leftTopRendered := leftTopStyle.Render(leftTop.String())
leftBotRendered := leftBotStyle.Render(leftBot.String())
// Use detailsViewport view
leftBotRendered := leftBotStyle.Render(m.detailsViewport.View())
// Combine Left
leftCol := lipgloss.JoinVertical(lipgloss.Left, leftTopRendered, leftBotRendered)