package client import ( "context" "fmt" "git.netflux.io/rob/octoplex/internal/domain" "git.netflux.io/rob/octoplex/internal/event" pb "git.netflux.io/rob/octoplex/internal/generated/grpc" "git.netflux.io/rob/octoplex/internal/protocol" "git.netflux.io/rob/octoplex/internal/terminal" "github.com/sagikazarmark/slog-shim" "golang.org/x/sync/errgroup" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) // App is the client application. type App struct { bus *event.Bus clipboardAvailable bool buildInfo domain.BuildInfo screen *terminal.Screen logger *slog.Logger } // NewApp creates a new App instance. // // TODO: params func NewApp( clipboardAvailable bool, buildInfo domain.BuildInfo, screen *terminal.Screen, logger *slog.Logger, ) *App { return &App{ bus: event.NewBus(logger), clipboardAvailable: clipboardAvailable, buildInfo: buildInfo, screen: screen, logger: logger, } } // Run starts the application, and blocks until it is closed. func (a *App) Run(ctx context.Context) error { g, ctx := errgroup.WithContext(ctx) conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return fmt.Errorf("connect to gRPC server: %w", err) } apiClient := pb.NewInternalAPIClient(conn) stream, err := apiClient.Communicate(ctx) if err != nil { return fmt.Errorf("create gRPC stream: %w", err) } g.Go(func() error { for { envelope, recErr := stream.Recv() if recErr != nil { return fmt.Errorf("receive envelope: %w", recErr) } evt := envelope.GetEvent() if evt == nil { a.logger.Error("Received envelope without event") continue } a.logger.Debug("Received event", "type", fmt.Sprintf("%T", evt)) a.bus.Send(protocol.EventFromProto(evt)) } }) ui, err := terminal.NewUI(ctx, terminal.Params{ EventBus: a.bus, Dispatcher: func(cmd event.Command) { a.logger.Info("Command dispatched", "cmd", cmd.Name()) if sendErr := stream.Send(&pb.Envelope{Payload: &pb.Envelope_Command{Command: protocol.CommandToProto(cmd)}}); sendErr != nil { a.logger.Error("Error sending command to gRPC API", "err", sendErr) } }, ClipboardAvailable: a.clipboardAvailable, BuildInfo: a.buildInfo, Screen: a.screen, Logger: a.logger.With("component", "ui"), }) if err != nil { return fmt.Errorf("start terminal user interface: %w", err) } defer ui.Close() g.Go(func() error { return ui.Run(ctx) }) if err := g.Wait(); err == terminal.ErrUserClosed { return nil } else { return fmt.Errorf("errgroup.Wait: %w", err) } }