@@ -1,8 +1,9 @@
1package logs
1package logs
4− "encoding/json"
4+ "fmt"
7 "github.com/charmbracelet/bubbles/v2/key"
8 "github.com/charmbracelet/bubbles/v2/key"
8 "github.com/charmbracelet/bubbles/v2/table"
9 "github.com/charmbracelet/bubbles/v2/table"
@@ -12,6 +13,7 @@ import (
12 "github.com/charmbracelet/crush/internal/tui/layout"
13 "github.com/charmbracelet/crush/internal/tui/layout"
13 "github.com/charmbracelet/crush/internal/tui/styles"
14 "github.com/charmbracelet/crush/internal/tui/styles"
14 "github.com/charmbracelet/crush/internal/tui/util"
15 "github.com/charmbracelet/crush/internal/tui/util"
16+ "github.com/charmbracelet/lipgloss/v2"
17type TableComponent interface {
19type TableComponent interface {
@@ -22,48 +24,80 @@ type TableComponent interface {
23type tableCmp struct {
25type tableCmp struct {
24 table table.Model
26 table table.Model
27+ logs []logging.LogMessage
27type selectedLogMsg logging.LogMessage
30type selectedLogMsg logging.LogMessage
29func (i *tableCmp) Init() tea.Cmd {
32func (i *tableCmp) Init() tea.Cmd {
33+ i.logs = logging.List()
30 i.setRows()
34 i.setRows()
31 return nil
35 return nil
34func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
38func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
35 var cmds []tea.Cmd
39 var cmds []tea.Cmd
36− switch msg.(type) {
40+ switch msg := msg.(type) {
37 case pubsub.Event[logging.LogMessage]:
41 case pubsub.Event[logging.LogMessage]:
38− i.setRows()
42+ return i, func() tea.Msg {
39− return i, nil
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
50+ i.logs = append(i.logs, msg.Payload)
54+ logToRow(msg.Payload),
60+ return selectedLogMsg(msg.Payload)
41− prevSelectedRow := i.table.SelectedRow()
42 t, cmd := i.table.Update(msg)
63 t, cmd := i.table.Update(msg)
43 cmds = append(cmds, cmd)
64 cmds = append(cmds, cmd)
44 i.table = t
65 i.table = t
45− selectedRow := i.table.SelectedRow()
66+
46− if selectedRow != nil {
67+ cmds = append(cmds, func() tea.Msg {
47− if prevSelectedRow == nil || selectedRow[0] == prevSelectedRow[0] {
48− var log logging.LogMessage
49− for _, row := range logging.List() {
68+ for _, log := range logging.List() {
50− if row.ID == selectedRow[0] {
69+ if log.ID == i.table.SelectedRow()[1] {
51− log = row
70+ // If the selected row matches the log ID, return the selected log message
56− cmds = append(cmds, util.CmdHandler(selectedLogMsg(log)))
71+ return selectedLogMsg(log)
60 return i, tea.Batch(cmds...)
76 return i, tea.Batch(cmds...)
63func (i *tableCmp) View() tea.View {
79func (i *tableCmp) View() tea.View {
64 t := styles.CurrentTheme()
80 t := styles.CurrentTheme()
65 defaultStyles := table.DefaultStyles()
81 defaultStyles := table.DefaultStyles()
84+ defaultStyles.Header = defaultStyles.Header.
85+ Foreground(t.Primary).
87+ BorderStyle(lipgloss.NormalBorder()).
89+ BorderForeground(t.Border)
91+ // Selected row styling
66− defaultStyles.Selected = defaultStyles.Selected.Foreground(t.Primary)
92+ defaultStyles.Selected = defaultStyles.Selected.
93+ Foreground(t.FgSelected).
94+ Background(t.Primary).
98+ defaultStyles.Cell = defaultStyles.Cell.
67 i.table.SetStyles(defaultStyles)
101 i.table.SetStyles(defaultStyles)
68 return tea.NewView(i.table.View())
102 return tea.NewView(i.table.View())
@@ -75,12 +109,30 @@ func (i *tableCmp) GetSize() (int, int) {
75func (i *tableCmp) SetSize(width int, height int) tea.Cmd {
109func (i *tableCmp) SetSize(width int, height int) tea.Cmd {
76 i.table.SetWidth(width)
110 i.table.SetWidth(width)
77 i.table.SetHeight(height)
111 i.table.SetHeight(height)
78− cloumns := i.table.Columns()
112+
79− for i, col := range cloumns {
113+ columnWidth := (width - 10) / 4
80− col.Width = (width / len(cloumns)) - 2
114+ i.table.SetColumns([]table.Column{
81− cloumns[i] = col
115+ {
83− i.table.SetColumns(cloumns)
131+ {
132+ Title: "Attributes",
84 return nil
136 return nil
@@ -91,39 +143,54 @@ func (i *tableCmp) BindingKeys() []key.Binding {
91func (i *tableCmp) setRows() {
143func (i *tableCmp) setRows() {
92 rows := []table.Row{}
144 rows := []table.Row{}
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 {
96 if a.Time.Before(b.Time) {
147 if a.Time.Before(b.Time) {
97− return 1
148+ return -1
99 if a.Time.After(b.Time) {
150 if a.Time.After(b.Time) {
100− return -1
151+ return 1
105− for _, log := range logs {
156+ for _, log := range i.logs {
106− bm, _ := json.Marshal(log.Attributes)
157+ rows = append(rows, logToRow(log))
159+ i.table.SetRows(rows)
108− row := table.Row{
162+func logToRow(log logging.LogMessage) table.Row {
109− log.ID,
163+ // Format attributes as JSON string
110− log.Time.Format("15:04:05"),
164+ var attrStr string
111− log.Level,
165+ if len(log.Attributes) > 0 {
112− log.Message,
166+ var parts []string
113− string(bm),
167+ for _, attr := range log.Attributes {
168+ parts = append(parts, fmt.Sprintf(`{"Key":"%s","Value":"%s"}`, attr.Key, attr.Value))
115− rows = append(rows, row)
170+ attrStr = "[" + strings.Join(parts, ",") + "]"
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
179+ strings.ToUpper(log.Level),
117− i.table.SetRows(rows)
120func NewLogsTable() TableComponent {
187func NewLogsTable() TableComponent {
121 columns := []table.Column{
188 columns := []table.Column{
122− {Title: "ID", Width: 4},
189+ {Title: "Level"},
123− {Title: "Time", Width: 4},
191+ {Title: "Time"},
124− {Title: "Level", Width: 10},
125− {Title: "Message", Width: 10},
192+ {Title: "Message"},
126− {Title: "Attributes", Width: 10},
193+ {Title: "Attributes"},
129 tableModel := table.New(
196 tableModel := table.New(