logs.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. package page
  2. import (
  3. "github.com/charmbracelet/bubbles/key"
  4. tea "github.com/charmbracelet/bubbletea"
  5. "github.com/charmbracelet/lipgloss"
  6. "github.com/sst/opencode/internal/app"
  7. "github.com/sst/opencode/internal/tui/components/logs"
  8. "github.com/sst/opencode/internal/tui/layout"
  9. "github.com/sst/opencode/internal/tui/styles"
  10. "github.com/sst/opencode/internal/tui/theme"
  11. )
  12. var LogsPage PageID = "logs"
  13. type LogPage interface {
  14. tea.Model
  15. layout.Sizeable
  16. layout.Bindings
  17. }
  18. // Custom keybindings for logs page
  19. type logsKeyMap struct {
  20. Left key.Binding
  21. Right key.Binding
  22. Tab key.Binding
  23. }
  24. var logsKeys = logsKeyMap{
  25. Left: key.NewBinding(
  26. key.WithKeys("left", "h"),
  27. key.WithHelp("←/h", "left pane"),
  28. ),
  29. Right: key.NewBinding(
  30. key.WithKeys("right", "l"),
  31. key.WithHelp("→/l", "right pane"),
  32. ),
  33. Tab: key.NewBinding(
  34. key.WithKeys("tab"),
  35. key.WithHelp("tab", "switch panes"),
  36. ),
  37. }
  38. type logsPage struct {
  39. width, height int
  40. table layout.Container
  41. details layout.Container
  42. activePane int // 0 = table, 1 = details
  43. keyMap logsKeyMap
  44. }
  45. // Message to switch active pane
  46. type switchPaneMsg struct {
  47. pane int // 0 = table, 1 = details
  48. }
  49. func (p *logsPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  50. var cmds []tea.Cmd
  51. switch msg := msg.(type) {
  52. case tea.WindowSizeMsg:
  53. p.width = msg.Width
  54. p.height = msg.Height
  55. return p, p.SetSize(msg.Width, msg.Height)
  56. case switchPaneMsg:
  57. p.activePane = msg.pane
  58. if p.activePane == 0 {
  59. p.table.Focus()
  60. p.details.Blur()
  61. } else {
  62. p.table.Blur()
  63. p.details.Focus()
  64. }
  65. return p, nil
  66. case tea.KeyMsg:
  67. // Handle navigation keys
  68. switch {
  69. case key.Matches(msg, p.keyMap.Left):
  70. return p, func() tea.Msg {
  71. return switchPaneMsg{pane: 0}
  72. }
  73. case key.Matches(msg, p.keyMap.Right):
  74. return p, func() tea.Msg {
  75. return switchPaneMsg{pane: 1}
  76. }
  77. case key.Matches(msg, p.keyMap.Tab):
  78. return p, func() tea.Msg {
  79. return switchPaneMsg{pane: (p.activePane + 1) % 2}
  80. }
  81. }
  82. }
  83. // Update the active pane first to handle keyboard input
  84. if p.activePane == 0 {
  85. table, cmd := p.table.Update(msg)
  86. cmds = append(cmds, cmd)
  87. p.table = table.(layout.Container)
  88. // Update details pane without focus
  89. details, cmd := p.details.Update(msg)
  90. cmds = append(cmds, cmd)
  91. p.details = details.(layout.Container)
  92. } else {
  93. details, cmd := p.details.Update(msg)
  94. cmds = append(cmds, cmd)
  95. p.details = details.(layout.Container)
  96. // Update table pane without focus
  97. table, cmd := p.table.Update(msg)
  98. cmds = append(cmds, cmd)
  99. p.table = table.(layout.Container)
  100. }
  101. return p, tea.Batch(cmds...)
  102. }
  103. func (p *logsPage) View() string {
  104. t := theme.CurrentTheme()
  105. // Add padding to the right of the table view
  106. tableView := lipgloss.NewStyle().PaddingRight(3).Render(p.table.View())
  107. // Add border to the active pane
  108. tableStyle := lipgloss.NewStyle()
  109. detailsStyle := lipgloss.NewStyle()
  110. if p.activePane == 0 {
  111. tableStyle = tableStyle.BorderForeground(t.Primary())
  112. } else {
  113. detailsStyle = detailsStyle.BorderForeground(t.Primary())
  114. }
  115. tableView = tableStyle.Render(tableView)
  116. detailsView := detailsStyle.Render(p.details.View())
  117. return styles.ForceReplaceBackgroundWithLipgloss(
  118. lipgloss.JoinVertical(
  119. lipgloss.Left,
  120. styles.Bold().Render(" esc")+styles.Muted().Render(" to go back")+
  121. " "+styles.Bold().Render(" tab/←→/h/l")+styles.Muted().Render(" to switch panes"),
  122. "",
  123. lipgloss.JoinHorizontal(lipgloss.Top,
  124. tableView,
  125. detailsView,
  126. ),
  127. "",
  128. ),
  129. t.Background(),
  130. )
  131. }
  132. func (p *logsPage) BindingKeys() []key.Binding {
  133. // Add our custom keybindings
  134. bindings := []key.Binding{
  135. p.keyMap.Left,
  136. p.keyMap.Right,
  137. p.keyMap.Tab,
  138. }
  139. // Add the active pane's keybindings
  140. if p.activePane == 0 {
  141. bindings = append(bindings, p.table.BindingKeys()...)
  142. } else {
  143. bindings = append(bindings, p.details.BindingKeys()...)
  144. }
  145. return bindings
  146. }
  147. // GetSize implements LogPage.
  148. func (p *logsPage) GetSize() (int, int) {
  149. return p.width, p.height
  150. }
  151. // SetSize implements LogPage.
  152. func (p *logsPage) SetSize(width int, height int) tea.Cmd {
  153. p.width = width
  154. p.height = height
  155. // Account for padding between panes (3 characters)
  156. const padding = 3
  157. leftPaneWidth := (width - padding) / 2
  158. rightPaneWidth := width - leftPaneWidth - padding
  159. return tea.Batch(
  160. p.table.SetSize(leftPaneWidth, height-3),
  161. p.details.SetSize(rightPaneWidth, height-3),
  162. )
  163. }
  164. func (p *logsPage) Init() tea.Cmd {
  165. // Start with table pane active
  166. p.activePane = 0
  167. p.table.Focus()
  168. p.details.Blur()
  169. // Force an initial selection to update the details pane
  170. var cmds []tea.Cmd
  171. cmds = append(cmds, p.table.Init())
  172. cmds = append(cmds, p.details.Init())
  173. // Send a key down and then key up to select the first row
  174. // This ensures the details pane is populated when returning to the logs page
  175. cmds = append(cmds, func() tea.Msg {
  176. return tea.KeyMsg{Type: tea.KeyDown}
  177. })
  178. cmds = append(cmds, func() tea.Msg {
  179. return tea.KeyMsg{Type: tea.KeyUp}
  180. })
  181. return tea.Batch(cmds...)
  182. }
  183. func NewLogsPage(app *app.App) tea.Model {
  184. // Create containers with borders to visually indicate active pane
  185. tableContainer := layout.NewContainer(logs.NewLogsTable(app), layout.WithBorderHorizontal())
  186. detailsContainer := layout.NewContainer(logs.NewLogsDetails(), layout.WithBorderHorizontal())
  187. return &logsPage{
  188. table: tableContainer,
  189. details: detailsContainer,
  190. activePane: 0, // Start with table pane active
  191. keyMap: logsKeys,
  192. }
  193. }