✨ Better log viewer in launchpad
This commit is contained in:
171
pkg/launchpad/logview/viewer.go
Normal file
171
pkg/launchpad/logview/viewer.go
Normal file
@@ -0,0 +1,171 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user