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/kujtimiihoxha/opencode/internal/logging"
13 "github.com/kujtimiihoxha/opencode/internal/tui/layout"
14 "github.com/kujtimiihoxha/opencode/internal/tui/styles"
15)
16
17type DetailComponent interface {
18 tea.Model
19 layout.Sizeable
20 layout.Bindings
21}
22
23type detailCmp struct {
24 width, height int
25 focused bool
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 var (
41 cmd tea.Cmd
42 cmds []tea.Cmd
43 )
44
45 switch msg := msg.(type) {
46 case selectedLogMsg:
47 if msg.ID != i.currentLog.ID {
48 i.currentLog = logging.LogMessage(msg)
49 i.updateContent()
50 }
51 }
52
53 if i.focused {
54 i.viewport, cmd = i.viewport.Update(msg)
55 cmds = append(cmds, cmd)
56 }
57
58 return i, tea.Batch(cmds...)
59}
60
61func (i *detailCmp) updateContent() {
62 var content strings.Builder
63
64 // Format the header with timestamp and level
65 timeStyle := lipgloss.NewStyle().Foreground(styles.SubText0)
66 levelStyle := getLevelStyle(i.currentLog.Level)
67
68 header := lipgloss.JoinHorizontal(
69 lipgloss.Center,
70 timeStyle.Render(i.currentLog.Time.Format(time.RFC3339)),
71 " ",
72 levelStyle.Render(i.currentLog.Level),
73 )
74
75 content.WriteString(lipgloss.NewStyle().Bold(true).Render(header))
76 content.WriteString("\n\n")
77
78 // Message with styling
79 messageStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Text)
80 content.WriteString(messageStyle.Render("Message:"))
81 content.WriteString("\n")
82 content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(i.currentLog.Message))
83 content.WriteString("\n\n")
84
85 // Attributes section
86 if len(i.currentLog.Attributes) > 0 {
87 attrHeaderStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Text)
88 content.WriteString(attrHeaderStyle.Render("Attributes:"))
89 content.WriteString("\n")
90
91 // Create a table-like display for attributes
92 keyStyle := lipgloss.NewStyle().Foreground(styles.Primary).Bold(true)
93 valueStyle := lipgloss.NewStyle().Foreground(styles.Text)
94
95 for _, attr := range i.currentLog.Attributes {
96 attrLine := fmt.Sprintf("%s: %s",
97 keyStyle.Render(attr.Key),
98 valueStyle.Render(attr.Value),
99 )
100 content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(attrLine))
101 content.WriteString("\n")
102 }
103 }
104
105 i.viewport.SetContent(content.String())
106}
107
108func getLevelStyle(level string) lipgloss.Style {
109 style := lipgloss.NewStyle().Bold(true)
110
111 switch strings.ToLower(level) {
112 case "info":
113 return style.Foreground(styles.Blue)
114 case "warn", "warning":
115 return style.Foreground(styles.Warning)
116 case "error", "err":
117 return style.Foreground(styles.Error)
118 case "debug":
119 return style.Foreground(styles.Green)
120 default:
121 return style.Foreground(styles.Text)
122 }
123}
124
125func (i *detailCmp) View() string {
126 return i.viewport.View()
127}
128
129func (i *detailCmp) Blur() tea.Cmd {
130 i.focused = false
131 return nil
132}
133
134func (i *detailCmp) Focus() tea.Cmd {
135 i.focused = true
136 return nil
137}
138
139func (i *detailCmp) IsFocused() bool {
140 return i.focused
141}
142
143func (i *detailCmp) GetSize() (int, int) {
144 return i.width, i.height
145}
146
147func (i *detailCmp) SetSize(width int, height int) {
148 i.width = width
149 i.height = height
150 i.viewport.Width = i.width
151 i.viewport.Height = i.height
152 i.updateContent()
153}
154
155func (i *detailCmp) BindingKeys() []key.Binding {
156 return []key.Binding{
157 i.viewport.KeyMap.PageDown,
158 i.viewport.KeyMap.PageUp,
159 i.viewport.KeyMap.HalfPageDown,
160 i.viewport.KeyMap.HalfPageUp,
161 }
162}
163
164func (i *detailCmp) BorderText() map[layout.BorderPosition]string {
165 return map[layout.BorderPosition]string{
166 layout.TopLeftBorder: "Log Details",
167 }
168}
169
170func NewLogsDetails() DetailComponent {
171 return &detailCmp{
172 viewport: viewport.New(0, 0),
173 }
174}