feat: Add scrollable packet details viewport and TAB focus switching
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user