main.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. package main
  2. import (
  3. "context"
  4. "log/slog"
  5. "os"
  6. "path/filepath"
  7. "sync"
  8. "time"
  9. tea "github.com/charmbracelet/bubbletea"
  10. zone "github.com/lrstanley/bubblezone"
  11. "github.com/sst/opencode/internal/pubsub"
  12. "github.com/sst/opencode/internal/tui"
  13. "github.com/sst/opencode/internal/tui/app"
  14. "github.com/sst/opencode/pkg/client"
  15. )
  16. func main() {
  17. url := "http://localhost:16713"
  18. httpClient, err := client.NewClientWithResponses(url)
  19. if err != nil {
  20. slog.Error("Failed to create client", "error", err)
  21. os.Exit(1)
  22. }
  23. paths, _ := httpClient.PostPathGetWithResponse(context.Background())
  24. logfile := filepath.Join(paths.JSON200.Data, "log", "tui.log")
  25. if _, err := os.Stat(filepath.Dir(logfile)); os.IsNotExist(err) {
  26. err := os.MkdirAll(filepath.Dir(logfile), 0755)
  27. if err != nil {
  28. slog.Error("Failed to create log directory", "error", err)
  29. os.Exit(1)
  30. }
  31. }
  32. file, err := os.Create(logfile)
  33. if err != nil {
  34. slog.Error("Failed to create log file", "error", err)
  35. os.Exit(1)
  36. }
  37. defer file.Close()
  38. logger := slog.New(slog.NewTextHandler(file, &slog.HandlerOptions{Level: slog.LevelDebug}))
  39. slog.SetDefault(logger)
  40. // Create main context for the application
  41. ctx, cancel := context.WithCancel(context.Background())
  42. defer cancel()
  43. app, err := app.New(ctx, httpClient)
  44. if err != nil {
  45. slog.Error("Failed to create app", "error", err)
  46. // return err
  47. }
  48. // Set up the TUI
  49. zone.NewGlobal()
  50. program := tea.NewProgram(
  51. tui.New(app),
  52. tea.WithAltScreen(),
  53. )
  54. eventClient, err := client.NewClient(url)
  55. if err != nil {
  56. slog.Error("Failed to create event client", "error", err)
  57. os.Exit(1)
  58. }
  59. evts, err := eventClient.Event(ctx)
  60. if err != nil {
  61. slog.Error("Failed to subscribe to events", "error", err)
  62. os.Exit(1)
  63. }
  64. go func() {
  65. for item := range evts {
  66. program.Send(item)
  67. }
  68. }()
  69. // Setup the subscriptions, this will send services events to the TUI
  70. ch, cancelSubs := setupSubscriptions(app, ctx)
  71. // Create a context for the TUI message handler
  72. tuiCtx, tuiCancel := context.WithCancel(ctx)
  73. var tuiWg sync.WaitGroup
  74. tuiWg.Add(1)
  75. // Set up message handling for the TUI
  76. go func() {
  77. defer tuiWg.Done()
  78. // defer logging.RecoverPanic("TUI-message-handler", func() {
  79. // attemptTUIRecovery(program)
  80. // })
  81. for {
  82. select {
  83. case <-tuiCtx.Done():
  84. slog.Info("TUI message handler shutting down")
  85. return
  86. case msg, ok := <-ch:
  87. if !ok {
  88. slog.Info("TUI message channel closed")
  89. return
  90. }
  91. program.Send(msg)
  92. }
  93. }
  94. }()
  95. // Cleanup function for when the program exits
  96. cleanup := func() {
  97. // Cancel subscriptions first
  98. cancelSubs()
  99. // Then shutdown the app
  100. app.Shutdown()
  101. // Then cancel TUI message handler
  102. tuiCancel()
  103. // Wait for TUI message handler to finish
  104. tuiWg.Wait()
  105. slog.Info("All goroutines cleaned up")
  106. }
  107. // Run the TUI
  108. result, err := program.Run()
  109. cleanup()
  110. if err != nil {
  111. slog.Error("TUI error", "error", err)
  112. // return fmt.Errorf("TUI error: %v", err)
  113. }
  114. slog.Info("TUI exited", "result", result)
  115. }
  116. func setupSubscriber[T any](
  117. ctx context.Context,
  118. wg *sync.WaitGroup,
  119. name string,
  120. subscriber func(context.Context) <-chan pubsub.Event[T],
  121. outputCh chan<- tea.Msg,
  122. ) {
  123. wg.Add(1)
  124. go func() {
  125. defer wg.Done()
  126. // defer logging.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil)
  127. subCh := subscriber(ctx)
  128. if subCh == nil {
  129. slog.Warn("subscription channel is nil", "name", name)
  130. return
  131. }
  132. for {
  133. select {
  134. case event, ok := <-subCh:
  135. if !ok {
  136. slog.Info("subscription channel closed", "name", name)
  137. return
  138. }
  139. var msg tea.Msg = event
  140. select {
  141. case outputCh <- msg:
  142. case <-time.After(2 * time.Second):
  143. slog.Warn("message dropped due to slow consumer", "name", name)
  144. case <-ctx.Done():
  145. slog.Info("subscription cancelled", "name", name)
  146. return
  147. }
  148. case <-ctx.Done():
  149. slog.Info("subscription cancelled", "name", name)
  150. return
  151. }
  152. }
  153. }()
  154. }
  155. func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg, func()) {
  156. ch := make(chan tea.Msg, 100)
  157. wg := sync.WaitGroup{}
  158. ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context
  159. setupSubscriber(ctx, &wg, "status", app.Status.Subscribe, ch)
  160. cleanupFunc := func() {
  161. slog.Info("Cancelling all subscriptions")
  162. cancel() // Signal all goroutines to stop
  163. waitCh := make(chan struct{})
  164. go func() {
  165. // defer logging.RecoverPanic("subscription-cleanup", nil)
  166. wg.Wait()
  167. close(waitCh)
  168. }()
  169. select {
  170. case <-waitCh:
  171. slog.Info("All subscription goroutines completed successfully")
  172. close(ch) // Only close after all writers are confirmed done
  173. case <-time.After(5 * time.Second):
  174. slog.Warn("Timed out waiting for some subscription goroutines to complete")
  175. close(ch)
  176. }
  177. }
  178. return ch, cleanupFunc
  179. }