details.go

  1package logs
  2
  3import (
  4	"fmt"
  5	"strings"
  6	"time"
  7
  8	"github.com/charmbracelet/bubbles/key"
  9	"github.com/charmbracelet/bubbles/viewport"
 10	tea "github.com/charmbracelet/bubbletea"
 11	"github.com/charmbracelet/lipgloss"
 12	"github.com/opencode-ai/opencode/internal/logging"
 13	"github.com/opencode-ai/opencode/internal/tui/layout"
 14	"github.com/opencode-ai/opencode/internal/tui/styles"
 15	"github.com/opencode-ai/opencode/internal/tui/theme"
 16)
 17
 18type DetailComponent interface {
 19	tea.Model
 20	layout.Sizeable
 21	layout.Bindings
 22}
 23
 24type detailCmp struct {
 25	width, height int
 26	currentLog    logging.LogMessage
 27	viewport      viewport.Model
 28}
 29
 30func (i *detailCmp) Init() tea.Cmd {
 31	messages := logging.List()
 32	if len(messages) == 0 {
 33		return nil
 34	}
 35	i.currentLog = messages[0]
 36	return nil
 37}
 38
 39func (i *detailCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 40	switch msg := msg.(type) {
 41	case selectedLogMsg:
 42		if msg.ID != i.currentLog.ID {
 43			i.currentLog = logging.LogMessage(msg)
 44			i.updateContent()
 45		}
 46	}
 47
 48	return i, nil
 49}
 50
 51func (i *detailCmp) updateContent() {
 52	var content strings.Builder
 53	t := theme.CurrentTheme()
 54
 55	// Format the header with timestamp and level
 56	timeStyle := lipgloss.NewStyle().Foreground(t.TextMuted())
 57	levelStyle := getLevelStyle(i.currentLog.Level)
 58
 59	header := lipgloss.JoinHorizontal(
 60		lipgloss.Center,
 61		timeStyle.Render(i.currentLog.Time.Format(time.RFC3339)),
 62		"  ",
 63		levelStyle.Render(i.currentLog.Level),
 64	)
 65
 66	content.WriteString(lipgloss.NewStyle().Bold(true).Render(header))
 67	content.WriteString("\n\n")
 68
 69	// Message with styling
 70	messageStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text())
 71	content.WriteString(messageStyle.Render("Message:"))
 72	content.WriteString("\n")
 73	content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(i.currentLog.Message))
 74	content.WriteString("\n\n")
 75
 76	// Attributes section
 77	if len(i.currentLog.Attributes) > 0 {
 78		attrHeaderStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text())
 79		content.WriteString(attrHeaderStyle.Render("Attributes:"))
 80		content.WriteString("\n")
 81
 82		// Create a table-like display for attributes
 83		keyStyle := lipgloss.NewStyle().Foreground(t.Primary()).Bold(true)
 84		valueStyle := lipgloss.NewStyle().Foreground(t.Text())
 85
 86		for _, attr := range i.currentLog.Attributes {
 87			attrLine := fmt.Sprintf("%s: %s",
 88				keyStyle.Render(attr.Key),
 89				valueStyle.Render(attr.Value),
 90			)
 91			content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(attrLine))
 92			content.WriteString("\n")
 93		}
 94	}
 95
 96	i.viewport.SetContent(content.String())
 97}
 98
 99func getLevelStyle(level string) lipgloss.Style {
100	style := lipgloss.NewStyle().Bold(true)
101	t := theme.CurrentTheme()
102	
103	switch strings.ToLower(level) {
104	case "info":
105		return style.Foreground(t.Info())
106	case "warn", "warning":
107		return style.Foreground(t.Warning())
108	case "error", "err":
109		return style.Foreground(t.Error())
110	case "debug":
111		return style.Foreground(t.Success())
112	default:
113		return style.Foreground(t.Text())
114	}
115}
116
117func (i *detailCmp) View() string {
118	t := theme.CurrentTheme()
119	return styles.ForceReplaceBackgroundWithLipgloss(i.viewport.View(), t.Background())
120}
121
122func (i *detailCmp) GetSize() (int, int) {
123	return i.width, i.height
124}
125
126func (i *detailCmp) SetSize(width int, height int) tea.Cmd {
127	i.width = width
128	i.height = height
129	i.viewport.Width = i.width
130	i.viewport.Height = i.height
131	i.updateContent()
132	return nil
133}
134
135func (i *detailCmp) BindingKeys() []key.Binding {
136	return layout.KeyMapToSlice(i.viewport.KeyMap)
137}
138
139func NewLogsDetails() DetailComponent {
140	return &detailCmp{
141		viewport: viewport.New(0, 0),
142	}
143}