@@ -1,8 +1,9 @@
11package logs
22
33import (
4− "encoding/json"
4+ "fmt"
55 "slices"
6+ "strings"
67
78 "github.com/charmbracelet/bubbles/v2/key"
89 "github.com/charmbracelet/bubbles/v2/table"
@@ -12,6 +13,7 @@ import (
1213 "github.com/charmbracelet/crush/internal/tui/layout"
1314 "github.com/charmbracelet/crush/internal/tui/styles"
1415 "github.com/charmbracelet/crush/internal/tui/util"
16+ "github.com/charmbracelet/lipgloss/v2"
1517)
1618
1719type TableComponent interface {
@@ -22,48 +24,80 @@ type TableComponent interface {
2224
2325type tableCmp struct {
2426 table table.Model
27+ logs []logging.LogMessage
2528}
2629
2730type selectedLogMsg logging.LogMessage
2831
2932func (i *tableCmp) Init() tea.Cmd {
33+ i.logs = logging.List()
3034 i.setRows()
3135 return nil
3236}
3337
3438func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
3539 var cmds []tea.Cmd
36− switch msg.(type) {
40+ switch msg := msg.(type) {
3741 case pubsub.Event[logging.LogMessage]:
38− i.setRows()
39− return i, nil
42+ return i, func() tea.Msg {
43+ if msg.Type == pubsub.CreatedEvent {
44+ rows := i.table.Rows()
45+ for _, row := range rows {
46+ if row[1] == msg.Payload.ID {
47+ return nil // If the log already exists, do not add it again
48+ }
49+ }
50+ i.logs = append(i.logs, msg.Payload)
51+ i.table.SetRows(
52+ append(
53+ []table.Row{
54+ logToRow(msg.Payload),
55+ },
56+ i.table.Rows()...,
57+ ),
58+ )
59+ }
60+ return selectedLogMsg(msg.Payload)
61+ }
4062 }
41− prevSelectedRow := i.table.SelectedRow()
4263 t, cmd := i.table.Update(msg)
4364 cmds = append(cmds, cmd)
4465 i.table = t
45− selectedRow := i.table.SelectedRow()
46− if selectedRow != nil {
47− if prevSelectedRow == nil || selectedRow[0] == prevSelectedRow[0] {
48− var log logging.LogMessage
49− for _, row := range logging.List() {
50− if row.ID == selectedRow[0] {
51− log = row
52− break
53− }
54− }
55− if log.ID != "" {
56− cmds = append(cmds, util.CmdHandler(selectedLogMsg(log)))
66+
67+ cmds = append(cmds, func() tea.Msg {
68+ for _, log := range logging.List() {
69+ if log.ID == i.table.SelectedRow()[1] {
70+ // If the selected row matches the log ID, return the selected log message
71+ return selectedLogMsg(log)
5772 }
5873 }
59− }
74+ return nil
75+ })
6076 return i, tea.Batch(cmds...)
6177}
6278
6379func (i *tableCmp) View() tea.View {
6480 t := styles.CurrentTheme()
6581 defaultStyles := table.DefaultStyles()
66− defaultStyles.Selected = defaultStyles.Selected.Foreground(t.Primary)
82+
83+ // Header styling
84+ defaultStyles.Header = defaultStyles.Header.
85+ Foreground(t.Primary).
86+ Bold(true).
87+ BorderStyle(lipgloss.NormalBorder()).
88+ BorderBottom(true).
89+ BorderForeground(t.Border)
90+
91+ // Selected row styling
92+ defaultStyles.Selected = defaultStyles.Selected.
93+ Foreground(t.FgSelected).
94+ Background(t.Primary).
95+ Bold(false)
96+
97+ // Cell styling
98+ defaultStyles.Cell = defaultStyles.Cell.
99+ Foreground(t.FgBase)
100+
67101 i.table.SetStyles(defaultStyles)
68102 return tea.NewView(i.table.View())
69103}
@@ -75,12 +109,30 @@ func (i *tableCmp) GetSize() (int, int) {
75109func (i *tableCmp) SetSize(width int, height int) tea.Cmd {
76110 i.table.SetWidth(width)
77111 i.table.SetHeight(height)
78− cloumns := i.table.Columns()
79− for i, col := range cloumns {
80− col.Width = (width / len(cloumns)) - 2
81− cloumns[i] = col
82− }
83− i.table.SetColumns(cloumns)
112+
113+ columnWidth := (width - 10) / 4
114+ i.table.SetColumns([]table.Column{
115+ {
116+ Title: "Level",
117+ Width: 10,
118+ },
119+ {
120+ Title: "ID",
121+ Width: columnWidth,
122+ },
123+ {
124+ Title: "Time",
125+ Width: columnWidth,
126+ },
127+ {
128+ Title: "Message",
129+ Width: columnWidth,
130+ },
131+ {
132+ Title: "Attributes",
133+ Width: columnWidth,
134+ },
135+ })
84136 return nil
85137}
86138
@@ -91,39 +143,54 @@ func (i *tableCmp) BindingKeys() []key.Binding {
91143func (i *tableCmp) setRows() {
92144 rows := []table.Row{}
93145
94− logs := logging.List()
95− slices.SortFunc(logs, func(a, b logging.LogMessage) int {
146+ slices.SortFunc(i.logs, func(a, b logging.LogMessage) int {
96147 if a.Time.Before(b.Time) {
97− return 1
148+ return -1
98149 }
99150 if a.Time.After(b.Time) {
100− return -1
151+ return 1
101152 }
102153 return 0
103154 })
104155
105− for _, log := range logs {
106− bm, _ := json.Marshal(log.Attributes)
156+ for _, log := range i.logs {
157+ rows = append(rows, logToRow(log))
158+ }
159+ i.table.SetRows(rows)
160+}
107161
108− row := table.Row{
109− log.ID,
110− log.Time.Format("15:04:05"),
111− log.Level,
112− log.Message,
113− string(bm),
162+func logToRow(log logging.LogMessage) table.Row {
163+ // Format attributes as JSON string
164+ var attrStr string
165+ if len(log.Attributes) > 0 {
166+ var parts []string
167+ for _, attr := range log.Attributes {
168+ parts = append(parts, fmt.Sprintf(`{"Key":"%s","Value":"%s"}`, attr.Key, attr.Value))
114169 }
115− rows = append(rows, row)
170+ attrStr = "[" + strings.Join(parts, ",") + "]"
171+ }
172+
173+ // Format time with relative time
174+ timeStr := log.Time.Format("2006-01-05 15:04:05 UTC")
175+ relativeTime := getRelativeTime(log.Time)
176+ fullTimeStr := timeStr + " " + relativeTime
177+
178+ return table.Row{
179+ strings.ToUpper(log.Level),
180+ log.ID,
181+ fullTimeStr,
182+ log.Message,
183+ attrStr,
116184 }
117− i.table.SetRows(rows)
118185}
119186
120187func NewLogsTable() TableComponent {
121188 columns := []table.Column{
122− {Title: "ID", Width: 4},
123− {Title: "Time", Width: 4},
124− {Title: "Level", Width: 10},
125− {Title: "Message", Width: 10},
126− {Title: "Attributes", Width: 10},
189+ {Title: "Level"},
190+ {Title: "ID"},
191+ {Title: "Time"},
192+ {Title: "Message"},
193+ {Title: "Attributes"},
127194 }
128195
129196 tableModel := table.New(