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 flowList list.Model
viewport viewport.Model viewport viewport.Model
// Packet Details View
detailsViewport viewport.Model
focusPacketDetails bool
// File browser for pcap import // File browser for pcap import
fileBrowser FileBrowserModel fileBrowser FileBrowserModel
loadedPcapPath string loadedPcapPath string
@@ -568,42 +572,60 @@ func (m *Model) updateSubView(msg tea.Msg) (tea.Model, tea.Cmd) {
case SubViewCallDetail: case SubViewCallDetail:
var cmd tea.Cmd var cmd tea.Cmd
// We handle scrolling manually via selection now
// Handle Focus Switching
if keyMsg, ok := msg.(tea.KeyMsg); ok { if keyMsg, ok := msg.(tea.KeyMsg); ok {
switch keyMsg.String() { switch keyMsg.String() {
case "esc", "q": case "esc", "q":
m.subView = SubViewNone m.subView = SubViewNone
m.selectedPacketIndex = 0 // Reset selection m.selectedPacketIndex = 0 // Reset selection
m.focusPacketDetails = false
return m, nil return m, nil
case "up", "k": case "tab":
if m.selectedPacketIndex > 0 { m.focusPacketDetails = !m.focusPacketDetails
m.selectedPacketIndex-- return m, nil
// Sync viewport }
if m.selectedPacketIndex < m.viewport.YOffset { }
m.viewport.SetYOffset(m.selectedPacketIndex)
} // Route keys based on focus
} if m.focusPacketDetails {
case "down", "j": // Forward keys to Details Viewport for scrolling
flows := m.callFlowStore.GetRecentFlows(20) m.detailsViewport, cmd = m.detailsViewport.Update(msg)
if m.selectedFlow < len(flows) { return m, cmd
flow := flows[m.selectedFlow] } else {
if m.selectedPacketIndex < len(flow.Packets)-1 { // Handle Flow Navigation
m.selectedPacketIndex++ if keyMsg, ok := msg.(tea.KeyMsg); ok {
switch keyMsg.String() {
case "up", "k":
if m.selectedPacketIndex > 0 {
m.selectedPacketIndex--
// Sync viewport // Sync viewport
if m.selectedPacketIndex >= m.viewport.YOffset+m.viewport.Height { if m.selectedPacketIndex < m.viewport.YOffset {
m.viewport.SetYOffset(m.selectedPacketIndex - m.viewport.Height + 1) 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)
if m.selectedFlow < len(flows) {
flow := flows[m.selectedFlow]
if m.selectedPacketIndex < len(flow.Packets)-1 {
m.selectedPacketIndex++
// Sync viewport
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 return m, cmd
} }
@@ -889,25 +911,22 @@ func (m Model) renderCallDetail() string {
} }
// --- Left Pane BOTTOM (Selected Packet Details) --- // --- Left Pane BOTTOM (Selected Packet Details) ---
var leftBot strings.Builder var detailsContent string
leftBot.WriteString(m.styles.Title.Render("📦 Packet Details"))
leftBot.WriteString("\n\n")
if m.selectedPacketIndex < len(flow.Packets) { if m.selectedPacketIndex < len(flow.Packets) {
pkt := flow.Packets[m.selectedPacketIndex] pkt := flow.Packets[m.selectedPacketIndex]
leftBot.WriteString(fmt.Sprintf("Time: %s\n", pkt.Timestamp.Format("15:04:05.000"))) detailsContent = fmt.Sprintf("Time: %s\nIP: %s -> %s\n\n%s",
leftBot.WriteString(fmt.Sprintf("IP: %s -> %s\n\n", pkt.SourceIP, pkt.DestIP)) pkt.Timestamp.Format("15:04:05.000"),
pkt.SourceIP, pkt.DestIP,
// Render Payload/Raw pkt.Raw)
// Truncate if too long?
raw := pkt.Raw
// Simple header parsing or just raw dump?
// Raw dump is useful.
leftBot.WriteString(raw)
} else { } 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) --- // --- Right Pane (Transaction Flow) ---
var right strings.Builder var right strings.Builder
// right.WriteString("Transaction Flow:\n\n") // Removed header to save space or added to viewport content // 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 // Set content to viewport
m.viewport.SetContent(right.String()) 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 // Layout Construction
leftTopStyle := lipgloss.NewStyle().Width(leftW).Height(leftTopH).Padding(1, 2) 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 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(lipgloss.Color("#44475A")) // Left border for right pane rightStyle := lipgloss.NewStyle().Width(rightW).Padding(0, 1).Border(lipgloss.NormalBorder(), false, false, false, true).BorderForeground(flowBorderColor)
// Render left pane parts // Render left pane parts
leftTopRendered := leftTopStyle.Render(leftTop.String()) leftTopRendered := leftTopStyle.Render(leftTop.String())
leftBotRendered := leftBotStyle.Render(leftBot.String()) // Use detailsViewport view
leftBotRendered := leftBotStyle.Render(m.detailsViewport.View())
// Combine Left // Combine Left
leftCol := lipgloss.JoinVertical(lipgloss.Left, leftTopRendered, leftBotRendered) leftCol := lipgloss.JoinVertical(lipgloss.Left, leftTopRendered, leftBotRendered)