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	i.width = width
165	i.height = height
166	i.viewport.SetWidth(i.width - 4)
167	i.viewport.SetHeight(i.height - 4)
168	i.updateContent()
169	return nil
170}
171
172func NewLogsDetails() DetailComponent {
173	return &detailCmp{
174		viewport: viewport.New(),
175	}
176}