Files
zeekayandHanzo Dev d9d3fe1566 Go 1.27.1, and the code it can say more simply
Modules declare go 1.27.1 and the build images follow. go fix applied the
modernizers, and the ptr helper went once new(expr) could say it; omitzero
is not applied, since it changes what the JSON says.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 19:34:14 -07:00

280 lines
6.2 KiB
Go

package views
import (
"fmt"
"strings"
"time"
"github.com/charmbracelet/lipgloss"
"github.com/insights/insights/livestream/tui/sse"
)
const maxEvents = 200
type PropColumn struct {
Name string
Width int
}
func (c PropColumn) String() string {
name := c.Name
if strings.Contains(name, ":") || strings.Contains(name, ",") {
name = `"` + name + `"`
}
return fmt.Sprintf("%s:%d", name, c.Width)
}
var defaultPropertyColumns = []PropColumn{
{Name: "$current_url", Width: 40},
}
type EventsView struct {
events []sse.EventMsg
cursor int
offset int
width int
height int
paused bool
selected int
propertyColumns []PropColumn
}
func NewEventsView() *EventsView {
return &EventsView{
events: make([]sse.EventMsg, 0, maxEvents),
cursor: 0,
offset: 0,
selected: -1,
propertyColumns: defaultPropertyColumns,
}
}
func (v *EventsView) SetPropertyColumns(cols []PropColumn) {
if len(cols) == 0 {
v.propertyColumns = defaultPropertyColumns
return
}
v.propertyColumns = cols
}
// ColumnsExceedWidth returns true if the given columns would make the table
// wider than the current terminal width.
func (v *EventsView) ColumnsExceedWidth(cols []PropColumn) bool {
if v.width == 0 {
return false
}
return columnsWidth(cols) > v.width
}
// Fixed columns: prefix(1) + event(30) + gap(2) + distinct(40) + gap(2) + time(8) + gap(2) = 85
const fixedColumnsWidth = 85
func columnsWidth(cols []PropColumn) int {
w := fixedColumnsWidth
for _, c := range cols {
w += 2 + c.Width
}
return w
}
func (v *EventsView) PropertyColumns() []PropColumn {
return v.propertyColumns
}
func (v *EventsView) PropertyColumnsString() string {
parts := make([]string, len(v.propertyColumns))
for i, c := range v.propertyColumns {
parts[i] = c.String()
}
return strings.Join(parts, ",")
}
func (v *EventsView) AddEvent(e sse.EventMsg) {
if v.paused {
return
}
v.events = append(v.events, e)
if len(v.events) > maxEvents {
// In-place compaction: copy last maxEvents to the front, zero old slots
// so the GC can collect the Properties maps, then reset length.
n := copy(v.events, v.events[len(v.events)-maxEvents:])
for i := n; i < len(v.events); i++ {
v.events[i] = sse.EventMsg{}
}
v.events = v.events[:n]
}
// Auto-scroll to bottom if user hasn't scrolled up
if v.cursor == len(v.events)-2 || v.cursor == 0 {
v.cursor = len(v.events) - 1
}
}
func (v *EventsView) Clear() {
v.events = v.events[:0]
v.cursor = 0
v.offset = 0
v.selected = -1
}
func (v *EventsView) SetSize(w, h int) {
v.width = w
v.height = h
}
func (v *EventsView) SetPaused(p bool) {
v.paused = p
}
func (v *EventsView) MoveUp() {
if v.cursor > 0 {
v.cursor--
}
}
func (v *EventsView) MoveDown() {
if v.cursor < len(v.events)-1 {
v.cursor++
}
}
func (v *EventsView) Select() int {
if v.cursor >= 0 && v.cursor < len(v.events) {
v.selected = v.cursor
return v.selected
}
return -1
}
func (v *EventsView) SelectedEvent() *sse.EventMsg {
if v.selected >= 0 && v.selected < len(v.events) {
return &v.events[v.selected]
}
return nil
}
func (v *EventsView) Deselect() {
v.selected = -1
}
func (v *EventsView) EventCount() int {
return len(v.events)
}
func relativeTime(t time.Time) string {
d := time.Since(t)
switch {
case d < time.Second:
return "now"
case d < time.Minute:
return fmt.Sprintf("%ds ago", int(d.Seconds()))
case d < time.Hour:
return fmt.Sprintf("%dm ago", int(d.Minutes()))
default:
return fmt.Sprintf("%dh ago", int(d.Hours()))
}
}
func prettyPropHeader(prop string) string {
name := strings.TrimPrefix(prop, "$")
name = strings.ReplaceAll(name, "_", " ")
if len(name) > 0 {
name = strings.ToUpper(name[:1]) + name[1:]
}
return name
}
func (v *EventsView) View() string {
if v.width == 0 || v.height == 0 {
return ""
}
headerStyle := lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.AdaptiveColor{Light: "#FFFFFF", Dark: "#FFFFFF"}).
Background(lipgloss.Color("#333333"))
cursorStyle := lipgloss.NewStyle().
Background(lipgloss.AdaptiveColor{Light: "#DDE4FF", Dark: "#2A2F4A"}).
Foreground(lipgloss.AdaptiveColor{Light: "#1D1F27", Dark: "#EDEDEC"})
mutedStyle := lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#888888", Dark: "#666666"})
colEvent := 30
colDistinct := 40
colTime := 8
// Build header
header := fmt.Sprintf(" %-*s %-*s", colEvent, "Event", colDistinct, "Distinct ID")
for _, col := range v.propertyColumns {
header += fmt.Sprintf(" %-*s", col.Width, prettyPropHeader(col.Name))
}
header += fmt.Sprintf(" %-*s", colTime, "Time")
header = headerStyle.Width(v.width).Render(header)
visibleRows := max(v.height-2, 1)
// Adjust offset so cursor is always visible
if v.cursor < v.offset {
v.offset = v.cursor
}
if v.cursor >= v.offset+visibleRows {
v.offset = v.cursor - visibleRows + 1
}
var rows []string
for i := v.offset; i < len(v.events) && i < v.offset+visibleRows; i++ {
e := v.events[i]
eventName := truncate(e.Event, colEvent)
distinctID := truncate(e.DistinctID, colDistinct)
timeStr := relativeTime(e.ReceivedAt)
prefix := " "
if i == v.cursor {
prefix = "▸"
}
row := fmt.Sprintf("%s%-*s %-*s", prefix, colEvent, eventName, colDistinct, distinctID)
for _, col := range v.propertyColumns {
val := ""
if props := e.Properties; props != nil {
if v, ok := props[col.Name]; ok && v != nil {
val = fmt.Sprint(v)
}
}
row += fmt.Sprintf(" %-*s", col.Width, truncate(val, col.Width))
}
row += fmt.Sprintf(" %-*s", colTime, timeStr)
if i == v.cursor {
row = cursorStyle.Width(v.width).Render(row)
} else {
row = lipgloss.NewStyle().Width(v.width).Render(row)
}
rows = append(rows, row)
}
// Fill remaining rows
for i := len(rows); i < visibleRows; i++ {
rows = append(rows, strings.Repeat(" ", v.width))
}
if len(v.events) == 0 {
empty := mutedStyle.Render(" Waiting for events...")
rows[0] = lipgloss.NewStyle().Width(v.width).Render(empty)
}
return header + "\n" + strings.Join(rows, "\n")
}
func truncate(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
if maxLen <= 3 {
return s[:maxLen]
}
return s[:maxLen-3] + "..."
}