table.go

  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}