diff --git a/cmd/client/main.go b/cmd/client/main.go index d3e08c5..87276e0 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "errors" "fmt" "log/slog" "os" @@ -90,7 +89,7 @@ func run() error { } }) - ui, err := terminal.StartUI(ctx, terminal.StartParams{ + ui, err := terminal.NewUI(ctx, terminal.Params{ EventBus: bus, Dispatcher: func(cmd event.Command) { logger.Info("Command dispatched", "cmd", cmd.Name()) @@ -112,18 +111,11 @@ func run() error { } defer ui.Close() - errUIClosed := errors.New("UI closed") - g.Go(func() error { - ui.Wait() - logger.Info("UI closed!") - return errUIClosed - }) + g.Go(func() error { return ui.Run(ctx) }) - if err := g.Wait(); err == errUIClosed { - logger.Info("UI closed, exiting") + if err := g.Wait(); err == terminal.ErrUserClosed { return nil } else { - logger.Error("UI closed with error", "err", err) return fmt.Errorf("errgroup.Wait: %w", err) } } diff --git a/internal/terminal/terminal.go b/internal/terminal/terminal.go index dc0a289..78f9866 100644 --- a/internal/terminal/terminal.go +++ b/internal/terminal/terminal.go @@ -3,6 +3,7 @@ package terminal import ( "cmp" "context" + "errors" "fmt" "log/slog" "maps" @@ -46,7 +47,7 @@ type UI struct { clipboardAvailable bool rtmpURL, rtmpsURL string buildInfo domain.BuildInfo - doneC chan struct{} + appExitC chan error logger *slog.Logger // tview state @@ -92,9 +93,9 @@ type ScreenCapture struct { Width, Height int } -// StartParams contains the parameters for starting a new terminal user +// Params contains the parameters for starting a new terminal user // interface. -type StartParams struct { +type Params struct { EventBus *event.Bus Dispatcher func(event.Command) Logger *slog.Logger @@ -103,8 +104,9 @@ type StartParams struct { Screen *Screen // Screen may be nil. } -// StartUI starts the terminal user interface. -func StartUI(ctx context.Context, params StartParams) (*UI, error) { +// NewUI creates the user interface. Call [Run] on the *UI instance to block +// until it is completed. +func NewUI(ctx context.Context, params Params) (*UI, error) { app := tview.NewApplication() var screen tcell.Screen @@ -210,7 +212,7 @@ func StartUI(ctx context.Context, params StartParams) (*UI, error) { eventBus: params.EventBus, dispatch: params.Dispatcher, clipboardAvailable: params.ClipboardAvailable, - doneC: make(chan struct{}, 1), + appExitC: make(chan error, 1), buildInfo: params.BuildInfo, logger: params.Logger, app: app, @@ -236,8 +238,6 @@ func StartUI(ctx context.Context, params StartParams) (*UI, error) { app.SetInputCapture(ui.inputCaptureHandler) app.SetAfterDrawFunc(ui.afterDrawHandler) - go ui.run(ctx) - return ui, nil } @@ -264,23 +264,28 @@ func (ui *UI) renderAboutView() { ui.aboutView.AddItem(tview.NewTextView().SetDynamicColors(true).SetText("[grey]?[-] About"), 1, 0, false) } -func (ui *UI) run(ctx context.Context) { +var ErrUserClosed = errors.New("user closed UI") + +// Run runs the user interface. It always returns a non-nil error, which will +// be [ErrUserClosed] if the user voluntarily closed the UI. +func (ui *UI) Run(ctx context.Context) error { eventC := ui.eventBus.Register() defer ui.eventBus.Deregister(eventC) go func() { - defer close(ui.doneC) - - if err := ui.app.Run(); err != nil { + err := ui.app.Run() + if err != nil { ui.logger.Error("Error in UI run loop, exiting", "err", err) } + ui.appExitC <- err }() for { select { case evt, ok := <-eventC: if !ok { - return + // should never happen + return errors.New("event channel closed") } ui.app.QueueUpdateDraw(func() { switch evt := evt.(type) { @@ -307,12 +312,11 @@ func (ui *UI) run(ctx context.Context) { default: ui.logger.Warn("unhandled event", "event", evt) } - }) case <-ctx.Done(): - return - case <-ui.doneC: - return + return ctx.Err() + case err := <-ui.appExitC: + return cmp.Or(err, ErrUserClosed) } } } @@ -819,7 +823,7 @@ func (ui *UI) Close() { // Wait waits for the terminal user interface to finish. func (ui *UI) Wait() { - <-ui.doneC + <-ui.appExitC } func (ui *UI) addDestination() {