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