diff --git a/internal/tui/model.go b/internal/tui/model.go index b6f86ff..d49e20d 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -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,42 +572,60 @@ 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 "up", "k": - if m.selectedPacketIndex > 0 { - m.selectedPacketIndex-- - // Sync viewport - if m.selectedPacketIndex < m.viewport.YOffset { - m.viewport.SetYOffset(m.selectedPacketIndex) - } - } - 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++ + 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-- // Sync viewport - if m.selectedPacketIndex >= m.viewport.YOffset+m.viewport.Height { - m.viewport.SetYOffset(m.selectedPacketIndex - m.viewport.Height + 1) + 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) + 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 } @@ -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)