table.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. package logs
  2. import (
  3. "slices"
  4. "github.com/charmbracelet/bubbles/key"
  5. "github.com/charmbracelet/bubbles/table"
  6. tea "github.com/charmbracelet/bubbletea"
  7. "github.com/opencode-ai/opencode/internal/logging"
  8. "github.com/opencode-ai/opencode/internal/pubsub"
  9. "github.com/opencode-ai/opencode/internal/tui/layout"
  10. // "github.com/opencode-ai/opencode/internal/tui/styles"
  11. "github.com/opencode-ai/opencode/internal/tui/theme"
  12. "github.com/opencode-ai/opencode/internal/tui/util"
  13. )
  14. type TableComponent interface {
  15. tea.Model
  16. layout.Sizeable
  17. layout.Bindings
  18. }
  19. type tableCmp struct {
  20. table table.Model
  21. focused bool
  22. }
  23. type selectedLogMsg logging.LogMessage
  24. func (i *tableCmp) Init() tea.Cmd {
  25. i.setRows()
  26. return nil
  27. }
  28. func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  29. var cmds []tea.Cmd
  30. switch msg.(type) {
  31. case pubsub.Event[logging.LogMessage]:
  32. i.setRows()
  33. return i, nil
  34. }
  35. // Only process keyboard input when focused
  36. if _, ok := msg.(tea.KeyMsg); ok && !i.focused {
  37. return i, nil
  38. }
  39. t, cmd := i.table.Update(msg)
  40. cmds = append(cmds, cmd)
  41. i.table = t
  42. selectedRow := i.table.SelectedRow()
  43. if selectedRow != nil {
  44. // Always send the selected log message when a row is selected
  45. // This fixes the issue where navigation doesn't update the detail pane
  46. // when returning to the logs page
  47. var log logging.LogMessage
  48. for _, row := range logging.List() {
  49. if row.ID == selectedRow[0] {
  50. log = row
  51. break
  52. }
  53. }
  54. if log.ID != "" {
  55. cmds = append(cmds, util.CmdHandler(selectedLogMsg(log)))
  56. }
  57. }
  58. return i, tea.Batch(cmds...)
  59. }
  60. func (i *tableCmp) View() string {
  61. t := theme.CurrentTheme()
  62. defaultStyles := table.DefaultStyles()
  63. defaultStyles.Selected = defaultStyles.Selected.Foreground(t.Primary())
  64. i.table.SetStyles(defaultStyles)
  65. return i.table.View()
  66. }
  67. func (i *tableCmp) GetSize() (int, int) {
  68. return i.table.Width(), i.table.Height()
  69. }
  70. func (i *tableCmp) SetSize(width int, height int) tea.Cmd {
  71. i.table.SetWidth(width)
  72. i.table.SetHeight(height)
  73. columns := i.table.Columns()
  74. // Calculate widths for visible columns
  75. timeWidth := 8 // Fixed width for Time column
  76. levelWidth := 7 // Fixed width for Level column
  77. // Message column gets the remaining space
  78. messageWidth := width - timeWidth - levelWidth - 5 // 5 for padding and borders
  79. // Set column widths
  80. columns[0].Width = 0 // ID column (hidden)
  81. columns[1].Width = timeWidth
  82. columns[2].Width = levelWidth
  83. columns[3].Width = messageWidth
  84. i.table.SetColumns(columns)
  85. return nil
  86. }
  87. func (i *tableCmp) BindingKeys() []key.Binding {
  88. return layout.KeyMapToSlice(i.table.KeyMap)
  89. }
  90. func (i *tableCmp) setRows() {
  91. rows := []table.Row{}
  92. logs := logging.List()
  93. slices.SortFunc(logs, func(a, b logging.LogMessage) int {
  94. if a.Time.Before(b.Time) {
  95. return 1
  96. }
  97. if a.Time.After(b.Time) {
  98. return -1
  99. }
  100. return 0
  101. })
  102. for _, log := range logs {
  103. // Include ID as hidden first column for selection
  104. row := table.Row{
  105. log.ID,
  106. log.Time.Format("15:04:05"),
  107. log.Level,
  108. log.Message,
  109. }
  110. rows = append(rows, row)
  111. }
  112. i.table.SetRows(rows)
  113. }
  114. func NewLogsTable() TableComponent {
  115. columns := []table.Column{
  116. {Title: "ID", Width: 0}, // ID column with zero width
  117. {Title: "Time", Width: 8},
  118. {Title: "Level", Width: 7},
  119. {Title: "Message", Width: 30},
  120. }
  121. tableModel := table.New(
  122. table.WithColumns(columns),
  123. )
  124. tableModel.Focus()
  125. return &tableCmp{
  126. table: tableModel,
  127. }
  128. }
  129. // Focus implements the focusable interface
  130. func (i *tableCmp) Focus() {
  131. i.focused = true
  132. i.table.Focus()
  133. }
  134. // Blur implements the blurable interface
  135. func (i *tableCmp) Blur() {
  136. i.focused = false
  137. // Table doesn't have a Blur method, but we can implement it here
  138. // to satisfy the interface
  139. }