Files
telephony-inspector/internal/tui/model.go

217 lines
5.0 KiB
Go
Raw Normal View History

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...))
}