1package logs
2
3import (
4 "encoding/json"
5 "slices"
6
7 "github.com/charmbracelet/bubbles/key"
8 "github.com/charmbracelet/bubbles/table"
9 tea "github.com/charmbracelet/bubbletea"
10 "github.com/opencode-ai/opencode/internal/logging"
11 "github.com/opencode-ai/opencode/internal/pubsub"
12 "github.com/opencode-ai/opencode/internal/tui/layout"
13 "github.com/opencode-ai/opencode/internal/tui/styles"
14 "github.com/opencode-ai/opencode/internal/tui/theme"
15 "github.com/opencode-ai/opencode/internal/tui/util"
16)
17
18type TableComponent interface {
19 tea.Model
20 layout.Sizeable
21 layout.Bindings
22}
23
24type tableCmp struct {
25 table table.Model
26}
27
28type selectedLogMsg logging.LogMessage
29
30func (i *tableCmp) Init() tea.Cmd {
31 i.setRows()
32 return nil
33}
34
35func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
36 var cmds []tea.Cmd
37 switch msg.(type) {
38 case pubsub.Event[logging.LogMessage]:
39 i.setRows()
40 return i, nil
41 }
42 prevSelectedRow := i.table.SelectedRow()
43 t, cmd := i.table.Update(msg)
44 cmds = append(cmds, cmd)
45 i.table = t
46 selectedRow := i.table.SelectedRow()
47 if selectedRow != nil {
48 if prevSelectedRow == nil || selectedRow[0] == prevSelectedRow[0] {
49 var log logging.LogMessage
50 for _, row := range logging.List() {
51 if row.ID == selectedRow[0] {
52 log = row
53 break
54 }
55 }
56 if log.ID != "" {
57 cmds = append(cmds, util.CmdHandler(selectedLogMsg(log)))
58 }
59 }
60 }
61 return i, tea.Batch(cmds...)
62}
63
64func (i *tableCmp) View() string {
65 t := theme.CurrentTheme()
66 defaultStyles := table.DefaultStyles()
67 defaultStyles.Selected = defaultStyles.Selected.Foreground(t.Primary())
68 i.table.SetStyles(defaultStyles)
69 return styles.ForceReplaceBackgroundWithLipgloss(i.table.View(), t.Background())
70}
71
72func (i *tableCmp) GetSize() (int, int) {
73 return i.table.Width(), i.table.Height()
74}
75
76func (i *tableCmp) SetSize(width int, height int) tea.Cmd {
77 i.table.SetWidth(width)
78 i.table.SetHeight(height)
79 cloumns := i.table.Columns()
80 for i, col := range cloumns {
81 col.Width = (width / len(cloumns)) - 2
82 cloumns[i] = col
83 }
84 i.table.SetColumns(cloumns)
85 return nil
86}
87
88func (i *tableCmp) BindingKeys() []key.Binding {
89 return layout.KeyMapToSlice(i.table.KeyMap)
90}
91
92func (i *tableCmp) setRows() {
93 rows := []table.Row{}
94
95 logs := logging.List()
96 slices.SortFunc(logs, func(a, b logging.LogMessage) int {
97 if a.Time.Before(b.Time) {
98 return 1
99 }
100 if a.Time.After(b.Time) {
101 return -1
102 }
103 return 0
104 })
105
106 for _, log := range logs {
107 bm, _ := json.Marshal(log.Attributes)
108
109 row := table.Row{
110 log.ID,
111 log.Time.Format("15:04:05"),
112 log.Level,
113 log.Message,
114 string(bm),
115 }
116 rows = append(rows, row)
117 }
118 i.table.SetRows(rows)
119}
120
121func NewLogsTable() TableComponent {
122 columns := []table.Column{
123 {Title: "ID", Width: 4},
124 {Title: "Time", Width: 4},
125 {Title: "Level", Width: 10},
126 {Title: "Message", Width: 10},
127 {Title: "Attributes", Width: 10},
128 }
129
130 tableModel := table.New(
131 table.WithColumns(columns),
132 )
133 tableModel.Focus()
134 return &tableCmp{
135 table: tableModel,
136 }
137}