1package logs
2
3import (
4 "fmt"
5 "strings"
6 "time"
7
8 "github.com/charmbracelet/bubbles/v2/key"
9 "github.com/charmbracelet/bubbles/v2/viewport"
10 tea "github.com/charmbracelet/bubbletea/v2"
11 "github.com/charmbracelet/crush/internal/logging"
12 "github.com/charmbracelet/crush/internal/tui/layout"
13 "github.com/charmbracelet/crush/internal/tui/styles"
14 "github.com/charmbracelet/crush/internal/tui/util"
15 "github.com/charmbracelet/lipgloss/v2"
16)
17
18type DetailComponent interface {
19 util.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 := styles.CurrentTheme()
54
55 if i.currentLog.ID == "" {
56 content.WriteString(t.S().Muted.Render("No log selected"))
57 i.viewport.SetContent(content.String())
58 return
59 }
60
61 // Level badge with background color
62 levelStyle := getLevelStyle(i.currentLog.Level)
63 levelBadge := levelStyle.Padding(0, 1).Render(strings.ToUpper(i.currentLog.Level))
64
65 // Timestamp with relative time
66 timeStr := i.currentLog.Time.Format("2006-01-05 15:04:05 UTC")
67 relativeTime := getRelativeTime(i.currentLog.Time)
68 timeStyle := t.S().Muted
69
70 // Header line
71 header := lipgloss.JoinHorizontal(
72 lipgloss.Left,
73 timeStr,
74 " ",
75 timeStyle.Render(relativeTime),
76 )
77
78 content.WriteString(levelBadge)
79 content.WriteString("\n\n")
80 content.WriteString(header)
81 content.WriteString("\n\n")
82
83 // Message section
84 messageHeaderStyle := t.S().Base.Foreground(t.Blue).Bold(true)
85 content.WriteString(messageHeaderStyle.Render("Message"))
86 content.WriteString("\n")
87 content.WriteString(i.currentLog.Message)
88 content.WriteString("\n\n")
89
90 // Attributes section
91 if len(i.currentLog.Attributes) > 0 {
92 attrHeaderStyle := t.S().Base.Foreground(t.Blue).Bold(true)
93 content.WriteString(attrHeaderStyle.Render("Attributes"))
94 content.WriteString("\n")
95
96 for _, attr := range i.currentLog.Attributes {
97 keyStyle := t.S().Base.Foreground(t.Accent)
98 valueStyle := t.S().Text
99 attrLine := fmt.Sprintf("%s: %s",
100 keyStyle.Render(attr.Key),
101 valueStyle.Render(attr.Value),
102 )
103 content.WriteString(attrLine)
104 content.WriteString("\n")
105 }
106 }
107
108 i.viewport.SetContent(content.String())
109}
110
111func getLevelStyle(level string) lipgloss.Style {
112 t := styles.CurrentTheme()
113 style := t.S().Base.Bold(true)
114
115 switch strings.ToLower(level) {
116 case "info":
117 return style.Foreground(t.White).Background(t.Info)
118 case "warn", "warning":
119 return style.Foreground(t.White).Background(t.Warning)
120 case "error", "err":
121 return style.Foreground(t.White).Background(t.Error)
122 case "debug":
123 return style.Foreground(t.White).Background(t.Success)
124 case "fatal":
125 return style.Foreground(t.White).Background(t.Error)
126 default:
127 return style.Foreground(t.FgBase)
128 }
129}
130
131func getRelativeTime(logTime time.Time) string {
132 now := time.Now()
133 diff := now.Sub(logTime)
134
135 if diff < time.Minute {
136 return fmt.Sprintf("%ds ago", int(diff.Seconds()))
137 } else if diff < time.Hour {
138 return fmt.Sprintf("%dm ago", int(diff.Minutes()))
139 } else if diff < 24*time.Hour {
140 return fmt.Sprintf("%dh ago", int(diff.Hours()))
141 } else if diff < 30*24*time.Hour {
142 return fmt.Sprintf("%dd ago", int(diff.Hours()/24))
143 } else if diff < 365*24*time.Hour {
144 return fmt.Sprintf("%dmo ago", int(diff.Hours()/(24*30)))
145 } else {
146 return fmt.Sprintf("%dy ago", int(diff.Hours()/(24*365)))
147 }
148}
149
150func (i *detailCmp) View() tea.View {
151 t := styles.CurrentTheme()
152 style := t.S().Base.
153 BorderStyle(lipgloss.RoundedBorder()).
154 BorderForeground(t.BorderFocus).
155 Width(i.width - 2). // Adjust width for border
156 Height(i.height - 2). // Adjust height for border
157 Padding(1)
158 return tea.NewView(style.Render(i.viewport.View()))
159}
160
161func (i *detailCmp) GetSize() (int, int) {
162 return i.width, i.height
163}
164
165func (i *detailCmp) SetSize(width int, height int) tea.Cmd {
166 logging.Info("Setting size for detail component", "width", width, "height", height)
167 i.width = width
168 i.height = height
169 i.viewport.SetWidth(i.width - 4)
170 i.viewport.SetHeight(i.height - 4)
171 i.updateContent()
172 return nil
173}
174
175func (i *detailCmp) BindingKeys() []key.Binding {
176 return layout.KeyMapToSlice(i.viewport.KeyMap)
177}
178
179func NewLogsDetails() DetailComponent {
180 return &detailCmp{
181 viewport: viewport.New(),
182 }
183}