details.go

  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}