table.go

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