Files
Turbine/pkg/launchpad/logview/viewer.go

172 lines
4.3 KiB
Go

package logview
import (
"fmt"
"strings"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// LogMessage is a message containing a line from a service's log.
type LogMessage struct {
ServiceName string
Line string
Color string
}
type model struct {
services []string
logChan <-chan LogMessage
logs map[string][]string // Key: service name, Value: log lines
allLogs []string
viewports map[string]viewport.Model
activeTab int
ready bool
width int
height int
}
var (
tabStyle = lipgloss.NewStyle().Border(lipgloss.NormalBorder(), false, false, true, false).Padding(0, 1)
activeTabStyle = tabStyle.Copy().Border(lipgloss.ThickBorder(), false, false, true, false).Foreground(lipgloss.Color("62"))
inactiveTabStyle = tabStyle.Copy()
windowStyle = lipgloss.NewStyle().Border(lipgloss.NormalBorder()).Padding(1, 0)
)
func NewModel(logChan <-chan LogMessage, services []string) model {
m := model{
services: append([]string{"All"}, services...),
logChan: logChan,
logs: make(map[string][]string),
viewports: make(map[string]viewport.Model),
activeTab: 0,
}
for _, s := range m.services {
m.logs[s] = []string{}
}
return m
}
func (m model) Init() tea.Cmd {
return m.waitForLogs()
}
// waitForLogs waits for the next log message from the channel.
func (m model) waitForLogs() tea.Cmd {
return func() tea.Msg {
return <-m.logChan
}
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "right", "l":
m.activeTab = (m.activeTab + 1) % len(m.services)
m.updateViewportContent()
case "left", "h":
m.activeTab--
if m.activeTab < 0 {
m.activeTab = len(m.services) - 1
}
m.updateViewportContent()
}
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
headerHeight := lipgloss.Height(m.renderHeader())
vpHeight := m.height - headerHeight - windowStyle.GetVerticalFrameSize()
if !m.ready {
for _, serviceName := range m.services {
m.viewports[serviceName] = viewport.New(m.width-windowStyle.GetHorizontalFrameSize(), vpHeight)
}
m.ready = true
} else {
for _, serviceName := range m.services {
vp := m.viewports[serviceName]
vp.Width = m.width - windowStyle.GetHorizontalFrameSize()
vp.Height = vpHeight
m.viewports[serviceName] = vp
}
}
m.updateViewportContent()
case LogMessage:
logLine := fmt.Sprintf("%s%s%s", msg.Color, msg.Line, "\033[0m")
allLogLine := fmt.Sprintf("%s[%-10s]%s %s", msg.Color, msg.ServiceName, "\033[0m", msg.Line)
m.logs[msg.ServiceName] = append(m.logs[msg.ServiceName], logLine)
m.allLogs = append(m.allLogs, allLogLine)
m.updateViewportContent()
cmds = append(cmds, m.waitForLogs())
}
activeViewport := m.viewports[m.services[m.activeTab]]
activeViewport, cmd = activeViewport.Update(msg)
m.viewports[m.services[m.activeTab]] = activeViewport
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
func (m *model) updateViewportContent() {
if !m.ready {
return
}
activeServiceName := m.services[m.activeTab]
var content string
if activeServiceName == "All" {
content = strings.Join(m.allLogs, "\n")
} else {
content = strings.Join(m.logs[activeServiceName], "\n")
}
vp := m.viewports[activeServiceName]
vp.SetContent(content)
vp.GotoBottom()
m.viewports[activeServiceName] = vp
}
func (m model) View() string {
if !m.ready {
return "Initializing..."
}
header := m.renderHeader()
activeService := m.services[m.activeTab]
viewport := m.viewports[activeService]
return fmt.Sprintf("%s\n%s", header, windowStyle.Render(viewport.View()))
}
func (m model) renderHeader() string {
var tabs []string
for i, service := range m.services {
if i == m.activeTab {
tabs = append(tabs, activeTabStyle.Render(service))
} else {
tabs = append(tabs, inactiveTabStyle.Render(service))
}
}
return lipgloss.JoinHorizontal(lipgloss.Top, tabs...)
}
// Start runs the log viewer program.
func Start(logChan <-chan LogMessage, services []string) error {
m := NewModel(logChan, services)
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
return p.Start()
}