172 lines
4.3 KiB
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()
|
|
}
|