From a5ba4d6c116c0a5627f3113ce4689868b8477173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Luis=20Monta=C3=B1es=20Ojados?= Date: Mon, 19 Jan 2026 23:07:25 +0100 Subject: [PATCH] fix: Implement proper scrolling and sorting for Analysis calls list --- internal/sip/callflow.go | 21 +++++++++++ internal/tui/model.go | 81 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 95 insertions(+), 7 deletions(-) diff --git a/internal/sip/callflow.go b/internal/sip/callflow.go index 15f83fa..38e02ba 100644 --- a/internal/sip/callflow.go +++ b/internal/sip/callflow.go @@ -153,6 +153,27 @@ func (s *CallFlowStore) GetRecentFlows(n int) []*CallFlow { return flows } +// GetSortedFlows returns all call flows sorted by StartTime (oldest first) +func (s *CallFlowStore) GetSortedFlows() []*CallFlow { + flows := s.GetAllFlows() + + // Sort by start time ascending (oldest first), then by CallID for stable order + for i := 0; i < len(flows)-1; i++ { + for j := i + 1; j < len(flows); j++ { + // Compare by StartTime first + if flows[i].StartTime.After(flows[j].StartTime) { + flows[i], flows[j] = flows[j], flows[i] + } else if flows[i].StartTime.Equal(flows[j].StartTime) { + // If same time, sort by CallID for stable order + if flows[i].CallID > flows[j].CallID { + flows[i], flows[j] = flows[j], flows[i] + } + } + } + } + return flows +} + // Count returns the number of call flows func (s *CallFlowStore) Count() int { s.mu.RLock() diff --git a/internal/tui/model.go b/internal/tui/model.go index d2eb0ca..bd43139 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -79,6 +79,7 @@ type Model struct { callFlowStore *sip.CallFlowStore // Call flow analysis + analysisOffset int // For pagination in analysis view selectedFlow int selectedPacketIndex int flowList list.Model @@ -484,11 +485,51 @@ func (m *Model) handleViewKeys(msg tea.KeyMsg) tea.Cmd { case "up", "k": if m.selectedFlow > 0 { m.selectedFlow-- + if m.selectedFlow < m.analysisOffset { + m.analysisOffset = m.selectedFlow + } } case "down", "j": - flows := m.callFlowStore.GetRecentFlows(20) + flows := m.callFlowStore.GetSortedFlows() if m.selectedFlow < len(flows)-1 { m.selectedFlow++ + // Check if we need to scroll down + // Visible height calculation (approximate) + headerHeight := 4 // Title + Call count + padding + footerHeight := 2 // Help text + availableHeight := m.height - headerHeight - footerHeight + if availableHeight < 5 { + availableHeight = 5 + } + + if m.selectedFlow >= m.analysisOffset+availableHeight { + m.analysisOffset++ + } + } + case "pgup": + m.selectedFlow -= 10 + if m.selectedFlow < 0 { + m.selectedFlow = 0 + } + if m.selectedFlow < m.analysisOffset { + m.analysisOffset = m.selectedFlow + } + case "pgdown": + flows := m.callFlowStore.GetSortedFlows() + m.selectedFlow += 10 + if m.selectedFlow >= len(flows) { + m.selectedFlow = len(flows) - 1 + } + + headerHeight := 4 + footerHeight := 2 + availableHeight := m.height - headerHeight - footerHeight + if availableHeight < 5 { + availableHeight = 5 + } + + if m.selectedFlow >= m.analysisOffset+availableHeight { + m.analysisOffset = m.selectedFlow - availableHeight + 1 } case "enter": m.subView = SubViewCallDetail @@ -1209,7 +1250,7 @@ func (m Model) viewCapture() string { func (m Model) viewAnalysis() string { title := m.styles.Title.Render("📊 Analysis") - flows := m.callFlowStore.GetRecentFlows(20) + flows := m.callFlowStore.GetSortedFlows() if len(flows) == 0 { return lipgloss.JoinVertical(lipgloss.Left, title, @@ -1221,8 +1262,9 @@ func (m Model) viewAnalysis() string { } var lines []string - lines = append(lines, fmt.Sprintf("Calls: %d", len(flows))) - lines = append(lines, "") + // Header is handled in return statement now + // lines = append(lines, fmt.Sprintf("Calls: %d", len(flows))) + // lines = append(lines, "") for i, flow := range flows { prefix := " " @@ -1253,10 +1295,35 @@ func (m Model) viewAnalysis() string { lines = append(lines, style.Render(summary)) } - lines = append(lines, "") - lines = append(lines, m.styles.Help.Render("↑/↓ select • Enter details • q quit")) + // Apply Viewport/Scrolling + headerHeight := 4 // Title + Call count + padding + footerHeight := 2 // Help text + availableHeight := m.height - headerHeight - footerHeight + if availableHeight < 5 { + availableHeight = 5 + } - return lipgloss.JoinVertical(lipgloss.Left, title, lipgloss.JoinVertical(lipgloss.Left, lines...)) + // Sanity check offset + if m.analysisOffset > len(lines)-1 { + m.analysisOffset = len(lines) - 1 + } + if m.analysisOffset < 0 { + m.analysisOffset = 0 + } + + end := m.analysisOffset + availableHeight + if end > len(lines) { + end = len(lines) + } + + visibleLines := lines[m.analysisOffset:end] + + return lipgloss.JoinVertical(lipgloss.Left, title, + fmt.Sprintf("Calls: %d (Showing %d-%d)", len(flows), m.analysisOffset+1, end), + "", + lipgloss.JoinVertical(lipgloss.Left, visibleLines...), + "", + m.styles.Help.Render("↑/↓ select • Enter details • q quit")) } func (m Model) viewNetworkMap() string {