package main import ( "context" "log/slog" "os" "path/filepath" "strings" "sync" "time" tea "github.com/charmbracelet/bubbletea/v2" zone "github.com/lrstanley/bubblezone" "github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/pubsub" "github.com/sst/opencode/internal/tui" "github.com/sst/opencode/pkg/client" ) var Version = "dev" func main() { url := os.Getenv("OPENCODE_SERVER") httpClient, err := client.NewClientWithResponses(url) if err != nil { slog.Error("Failed to create client", "error", err) os.Exit(1) } paths, err := httpClient.PostPathGetWithResponse(context.Background()) if err != nil { panic(err) } logfile := filepath.Join(paths.JSON200.Data, "log", "tui.log") if _, err := os.Stat(filepath.Dir(logfile)); os.IsNotExist(err) { err := os.MkdirAll(filepath.Dir(logfile), 0755) if err != nil { slog.Error("Failed to create log directory", "error", err) os.Exit(1) } } file, err := os.Create(logfile) if err != nil { slog.Error("Failed to create log file", "error", err) os.Exit(1) } defer file.Close() logger := slog.New(slog.NewTextHandler(file, &slog.HandlerOptions{Level: slog.LevelDebug})) slog.SetDefault(logger) // Create main context for the application ctx, cancel := context.WithCancel(context.Background()) defer cancel() version := Version if version != "dev" && !strings.HasPrefix(Version, "v") { version = "v" + Version } app_, err := app.New(ctx, version, httpClient) if err != nil { panic(err) } // Set up the TUI zone.NewGlobal() program := tea.NewProgram( tui.NewModel(app_), // tea.WithMouseCellMotion(), tea.WithKeyboardEnhancements(), tea.WithAltScreen(), ) eventClient, err := client.NewClient(url) if err != nil { slog.Error("Failed to create event client", "error", err) os.Exit(1) } evts, err := eventClient.Event(ctx) if err != nil { slog.Error("Failed to subscribe to events", "error", err) os.Exit(1) } go func() { for item := range evts { program.Send(item) } }() // Setup the subscriptions, this will send services events to the TUI ch, cancelSubs := setupSubscriptions(app_, ctx) // Create a context for the TUI message handler tuiCtx, tuiCancel := context.WithCancel(ctx) var tuiWg sync.WaitGroup tuiWg.Add(1) // Set up message handling for the TUI go func() { defer tuiWg.Done() // defer logging.RecoverPanic("TUI-message-handler", func() { // attemptTUIRecovery(program) // }) for { select { case <-tuiCtx.Done(): slog.Info("TUI message handler shutting down") return case msg, ok := <-ch: if !ok { slog.Info("TUI message channel closed") return } program.Send(msg) } } }() // Cleanup function for when the program exits cleanup := func() { // Cancel subscriptions first cancelSubs() // Then cancel TUI message handler tuiCancel() // Wait for TUI message handler to finish tuiWg.Wait() slog.Info("All goroutines cleaned up") } // Run the TUI result, err := program.Run() cleanup() if err != nil { slog.Error("TUI error", "error", err) // return fmt.Errorf("TUI error: %v", err) } slog.Info("TUI exited", "result", result) } func setupSubscriber[T any]( ctx context.Context, wg *sync.WaitGroup, name string, subscriber func(context.Context) <-chan pubsub.Event[T], outputCh chan<- tea.Msg, ) { wg.Add(1) go func() { defer wg.Done() // defer logging.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil) subCh := subscriber(ctx) if subCh == nil { slog.Warn("subscription channel is nil", "name", name) return } for { select { case event, ok := <-subCh: if !ok { slog.Info("subscription channel closed", "name", name) return } var msg tea.Msg = event select { case outputCh <- msg: case <-time.After(2 * time.Second): slog.Warn("message dropped due to slow consumer", "name", name) case <-ctx.Done(): slog.Info("subscription cancelled", "name", name) return } case <-ctx.Done(): slog.Info("subscription cancelled", "name", name) return } } }() } func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg, func()) { ch := make(chan tea.Msg, 100) wg := sync.WaitGroup{} ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context setupSubscriber(ctx, &wg, "status", app.Status.Subscribe, ch) cleanupFunc := func() { slog.Info("Cancelling all subscriptions") cancel() // Signal all goroutines to stop waitCh := make(chan struct{}) go func() { // defer logging.RecoverPanic("subscription-cleanup", nil) wg.Wait() close(waitCh) }() select { case <-waitCh: slog.Info("All subscription goroutines completed successfully") close(ch) // Only close after all writers are confirmed done case <-time.After(5 * time.Second): slog.Warn("Timed out waiting for some subscription goroutines to complete") close(ch) } } return ch, cleanupFunc }