diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 3ecb23e5acd68c1666cf9798b17bcc408b9290e1..ace464844b8ba599130b9a92a47f5ccbc11b964d 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -9,9 +9,9 @@ import ( "path/filepath" tea "github.com/charmbracelet/bubbletea/v2" - "github.com/charmbracelet/crush/internal/app" - "github.com/charmbracelet/crush/internal/config" - "github.com/charmbracelet/crush/internal/db" + "github.com/charmbracelet/crush/internal/client" + "github.com/charmbracelet/crush/internal/log" + "github.com/charmbracelet/crush/internal/proto" "github.com/charmbracelet/crush/internal/tui" "github.com/charmbracelet/crush/internal/version" "github.com/charmbracelet/fang" @@ -60,22 +60,30 @@ crush run "Explain the use of context in Go" crush -y `, RunE: func(cmd *cobra.Command, args []string) error { - app, err := setupApp(cmd) + c, err := setupApp(cmd) if err != nil { return err } - defer app.Shutdown() + + m := tui.New(c) + + defer func() { c.DeleteInstance(cmd.Context(), c.ID()) }() // Set up the TUI. program := tea.NewProgram( - tui.New(app), + m, tea.WithAltScreen(), tea.WithContext(cmd.Context()), tea.WithMouseCellMotion(), // Use cell motion instead of all motion to reduce event flooding tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state ) - go app.Subscribe(program) + evc, err := c.SubscribeEvents(cmd.Context()) + if err != nil { + return fmt.Errorf("failed to subscribe to events: %v", err) + } + + go streamEvents(cmd.Context(), evc, program) if _, err := program.Run(); err != nil { slog.Error("TUI run error", "error", err) @@ -96,9 +104,30 @@ func Execute() { } } +func streamEvents(ctx context.Context, evc <-chan any, p *tea.Program) { + defer log.RecoverPanic("app.Subscribe", func() { + slog.Info("TUI subscription panic: attempting graceful shutdown") + p.Quit() + }) + + for { + select { + case <-ctx.Done(): + slog.Debug("TUI message handler shutting down") + return + case ev, ok := <-evc: + if !ok { + slog.Debug("TUI message channel closed") + return + } + p.Send(ev) + } + } +} + // setupApp handles the common setup logic for both interactive and non-interactive modes. // It returns the app instance, config, cleanup function, and any error. -func setupApp(cmd *cobra.Command) (*app.App, error) { +func setupApp(cmd *cobra.Command) (*client.Client, error) { debug, _ := cmd.Flags().GetBool("debug") yolo, _ := cmd.Flags().GetBool("yolo") dataDir, _ := cmd.Flags().GetString("data-dir") @@ -109,33 +138,20 @@ func setupApp(cmd *cobra.Command) (*app.App, error) { return nil, err } - cfg, err := config.Init(cwd, dataDir, debug) - if err != nil { - return nil, err - } - - if cfg.Permissions == nil { - cfg.Permissions = &config.Permissions{} - } - cfg.Permissions.SkipRequests = yolo - - if err := createDotCrushDir(cfg.Options.DataDirectory); err != nil { - return nil, err - } - - // Connect to DB; this will also run migrations. - conn, err := db.Connect(ctx, cfg.Options.DataDirectory) + c, err := client.DefaultClient(cwd) if err != nil { return nil, err } - appInstance, err := app.New(ctx, conn, cfg) - if err != nil { - slog.Error("Failed to create app instance", "error", err) - return nil, err + if _, err := c.CreateInstance(ctx, proto.Instance{ + DataDir: dataDir, + Debug: debug, + YOLO: yolo, + }); err != nil { + return nil, fmt.Errorf("failed to create or connect to instance: %v", err) } - return appInstance, nil + return c, nil } func MaybePrependStdin(prompt string) (string, error) {