package tui import ( "fmt" "telephony-inspector/internal/config" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) // View represents the current screen in the TUI type View int const ( ViewDashboard View = iota ViewCapture ViewAnalysis ViewNetworkMap ) // Model holds the application state type Model struct { currentView View width int height int // Network map configuration networkMap *config.NetworkMap // Capture state capturing bool packetCount int lastPackets []string // Last N packet summaries // Style definitions styles Styles } // Styles holds the lipgloss styles for the TUI type Styles struct { Title lipgloss.Style Subtitle lipgloss.Style Active lipgloss.Style Inactive lipgloss.Style Help lipgloss.Style StatusBar lipgloss.Style } func defaultStyles() Styles { return Styles{ Title: lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("#7D56F4")). MarginBottom(1), Subtitle: lipgloss.NewStyle(). Foreground(lipgloss.Color("#AFAFAF")), Active: lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("#04B575")), Inactive: lipgloss.NewStyle(). Foreground(lipgloss.Color("#626262")), Help: lipgloss.NewStyle(). Foreground(lipgloss.Color("#626262")). MarginTop(1), StatusBar: lipgloss.NewStyle(). Background(lipgloss.Color("#7D56F4")). Foreground(lipgloss.Color("#FFFFFF")). Padding(0, 1), } } // NewModel creates a new TUI model with default values func NewModel() Model { return Model{ currentView: ViewDashboard, networkMap: config.NewNetworkMap(), lastPackets: make([]string, 0, 20), styles: defaultStyles(), } } // Init initializes the model func (m Model) Init() tea.Cmd { return nil } // Update handles messages and updates the model func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "q", "ctrl+c": return m, tea.Quit case "1": m.currentView = ViewDashboard case "2": m.currentView = ViewCapture case "3": m.currentView = ViewAnalysis case "4": m.currentView = ViewNetworkMap } case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height } return m, nil } // View renders the TUI func (m Model) View() string { var content string switch m.currentView { case ViewDashboard: content = m.viewDashboard() case ViewCapture: content = m.viewCapture() case ViewAnalysis: content = m.viewAnalysis() case ViewNetworkMap: content = m.viewNetworkMap() } // Navigation bar nav := m.renderNav() // Status bar status := m.styles.StatusBar.Render(" Telephony Inspector v0.1.0 ") return lipgloss.JoinVertical(lipgloss.Left, nav, content, status) } func (m Model) renderNav() string { tabs := []string{"[1] Dashboard", "[2] Capture", "[3] Analysis", "[4] Network Map"} var rendered []string for i, tab := range tabs { if View(i) == m.currentView { rendered = append(rendered, m.styles.Active.Render(tab)) } else { rendered = append(rendered, m.styles.Inactive.Render(tab)) } } return lipgloss.JoinHorizontal(lipgloss.Top, rendered...) + "\n" } func (m Model) viewDashboard() string { title := m.styles.Title.Render("πŸ“ž Dashboard") info := lipgloss.JoinVertical(lipgloss.Left, m.styles.Subtitle.Render("SIP Telephony Inspector"), "", "β€’ Capture SIP traffic via SSH + tcpdump", "β€’ Analyze SIP/SDP packets in real-time", "β€’ Map network topology for better debugging", "", m.styles.Help.Render("Press 1-4 to navigate, q to quit"), ) return lipgloss.JoinVertical(lipgloss.Left, title, info) } func (m Model) viewCapture() string { title := m.styles.Title.Render("πŸ” Capture") status := "Status: " if m.capturing { status += m.styles.Active.Render("● Capturing") } else { status += m.styles.Inactive.Render("β—‹ Stopped") } packetInfo := fmt.Sprintf("Packets captured: %d", m.packetCount) help := m.styles.Help.Render("[c] Connect SSH [s] Start/Stop Capture [q] Quit") return lipgloss.JoinVertical(lipgloss.Left, title, status, packetInfo, "", help) } func (m Model) viewAnalysis() string { title := m.styles.Title.Render("πŸ“Š Analysis") content := lipgloss.JoinVertical(lipgloss.Left, "Call flows will appear here once packets are captured.", "", "Features:", "β€’ Group packets by Call-ID", "β€’ Visualize SIP transaction flow", "β€’ Decode SDP offers/answers", ) return lipgloss.JoinVertical(lipgloss.Left, title, content) } func (m Model) viewNetworkMap() string { title := m.styles.Title.Render("πŸ—ΊοΈ Network Map") if len(m.networkMap.Nodes) == 0 { return lipgloss.JoinVertical(lipgloss.Left, title, "No network nodes configured.", "", m.styles.Help.Render("[a] Add node [l] Load from file"), ) } var nodes []string for _, node := range m.networkMap.Nodes { nodes = append(nodes, fmt.Sprintf("β€’ %s (%s): %s", node.Name, node.Type, node.IP)) } return lipgloss.JoinVertical(lipgloss.Left, title, lipgloss.JoinVertical(lipgloss.Left, nodes...)) }