217 lines
5.0 KiB
Go
217 lines
5.0 KiB
Go
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...))
|
|
}
|