From 2f263b572589fb0bae41845f15a6a9668dcce354 Mon Sep 17 00:00:00 2001 From: Rob Watson Date: Tue, 29 Apr 2025 22:30:34 +0200 Subject: [PATCH] refactor(app): internalize dispatch channel --- internal/app/app.go | 22 ++++++++++--------- internal/terminal/terminal.go | 41 +++++++++++++++-------------------- 2 files changed, 29 insertions(+), 34 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index 4916d8b..deabec9 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -1,6 +1,7 @@ package app import ( + "cmp" "context" "errors" "fmt" @@ -23,6 +24,7 @@ type App struct { cfg config.Config configService *config.Service eventBus *event.Bus + dispatchC chan event.Command dockerClient container.DockerClient screen *terminal.Screen // Screen may be nil. clipboardAvailable bool @@ -35,6 +37,7 @@ type App struct { type Params struct { ConfigService *config.Service DockerClient container.DockerClient + ChanSize int Screen *terminal.Screen // Screen may be nil. ClipboardAvailable bool ConfigFilePath string @@ -42,12 +45,16 @@ type Params struct { Logger *slog.Logger } +// defaultChanSize is the default size of the dispatch channel. +const defaultChanSize = 64 + // New creates a new application instance. func New(params Params) *App { return &App{ cfg: params.ConfigService.Current(), configService: params.ConfigService, eventBus: event.NewBus(params.Logger.With("component", "event_bus")), + dispatchC: make(chan event.Command, cmp.Or(params.ChanSize, defaultChanSize)), dockerClient: params.DockerClient, screen: params.Screen, clipboardAvailable: params.ClipboardAvailable, @@ -70,6 +77,7 @@ func (a *App) Run(ctx context.Context) error { ui, err := terminal.StartUI(ctx, terminal.StartParams{ EventBus: a.eventBus, + Dispatcher: func(cmd event.Command) { a.dispatchC <- cmd }, Screen: a.screen, ClipboardAvailable: a.clipboardAvailable, ConfigFilePath: a.configFilePath, @@ -107,7 +115,7 @@ func (a *App) Run(ctx context.Context) error { a.eventBus.Send(event.FatalErrorOccurredEvent{Message: msg}) emptyUI() - <-ui.C() + <-a.dispatchC return err } defer containerClient.Close() @@ -139,7 +147,7 @@ func (a *App) Run(ctx context.Context) error { err = fmt.Errorf("create mediaserver: %w", err) a.eventBus.Send(event.FatalErrorOccurredEvent{Message: err.Error()}) emptyUI() - <-ui.C() + <-a.dispatchC return err } defer srv.Close() @@ -159,7 +167,7 @@ func (a *App) Run(ctx context.Context) error { if ok, startupErr := doStartupCheck(ctx, containerClient, a.eventBus); startupErr != nil { startupErr = fmt.Errorf("startup check: %w", startupErr) a.eventBus.Send(event.FatalErrorOccurredEvent{Message: startupErr.Error()}) - <-ui.C() + <-a.dispatchC return startupErr } else if ok { startMediaServerC <- struct{}{} @@ -175,13 +183,7 @@ func (a *App) Run(ctx context.Context) error { a.eventBus.Send(event.MediaServerStartedEvent{RTMPURL: srv.RTMPURL(), RTMPSURL: srv.RTMPSURL()}) case <-a.configService.C(): // No-op, config updates are handled synchronously for now. - case cmd, ok := <-ui.C(): - if !ok { - // TODO: keep UI open until all containers have closed - a.logger.Info("UI closed") - return nil - } - + case cmd := <-a.dispatchC: if ok, err := a.handleCommand(ctx, cmd, state, repl, containerClient, startMediaServerC); err != nil { return fmt.Errorf("handle command: %w", err) } else if !ok { diff --git a/internal/terminal/terminal.go b/internal/terminal/terminal.go index 743b5dd..5bbb530 100644 --- a/internal/terminal/terminal.go +++ b/internal/terminal/terminal.go @@ -42,7 +42,7 @@ const ( // UI is responsible for managing the terminal user interface. type UI struct { eventBus *event.Bus - commandC chan event.Command + dispatch func(event.Command) clipboardAvailable bool configFilePath string rtmpURL, rtmpsURL string @@ -96,7 +96,7 @@ type ScreenCapture struct { // interface. type StartParams struct { EventBus *event.Bus - ChanSize int + Dispatcher func(event.Command) Logger *slog.Logger ClipboardAvailable bool ConfigFilePath string @@ -104,13 +104,8 @@ type StartParams struct { Screen *Screen // Screen may be nil. } -const defaultChanSize = 64 - // StartUI starts the terminal user interface. func StartUI(ctx context.Context, params StartParams) (*UI, error) { - chanSize := cmp.Or(params.ChanSize, defaultChanSize) - commandCh := make(chan event.Command, chanSize) - app := tview.NewApplication() var screen tcell.Screen @@ -213,8 +208,8 @@ func StartUI(ctx context.Context, params StartParams) (*UI, error) { app.EnableMouse(false) ui := &UI{ - commandC: commandCh, eventBus: params.EventBus, + dispatch: params.Dispatcher, clipboardAvailable: params.ClipboardAvailable, configFilePath: params.ConfigFilePath, buildInfo: params.BuildInfo, @@ -271,13 +266,11 @@ func (ui *UI) renderAboutView() { ui.aboutView.AddItem(tview.NewTextView().SetDynamicColors(true).SetText("[grey]?[-] About"), 1, 0, false) } -// C returns a channel that receives commands from the user interface. -func (ui *UI) C() <-chan event.Command { - return ui.commandC -} - func (ui *UI) run(ctx context.Context) { - defer close(ui.commandC) + defer func() { + // Ensure the application is stopped when the UI is closed. + ui.dispatch(event.CommandQuit{}) + }() eventC := ui.eventBus.Register() @@ -425,9 +418,9 @@ func (ui *UI) handleOtherInstanceDetected(event.OtherInstanceDetectedEvent) { false, func(buttonIndex int, _ string) { if buttonIndex == 0 { - ui.commandC <- event.CommandCloseOtherInstance{} + ui.dispatch(event.CommandCloseOtherInstance{}) } else { - ui.commandC <- event.CommandQuit{} + ui.dispatch(event.CommandQuit{}) } }, ) @@ -457,7 +450,7 @@ func (ui *UI) handleFatalErrorOccurred(evt event.FatalErrorOccurredEvent) { []string{"Quit"}, false, func(int, string) { - ui.commandC <- event.CommandQuit{} + ui.dispatch(event.CommandQuit{}) }, ) } @@ -702,7 +695,7 @@ func (ui *UI) handleMediaServerClosed(exitReason string) { SetBackgroundColor(tcell.ColorBlack). SetTextColor(tcell.ColorWhite). SetDoneFunc(func(int, string) { - ui.commandC <- event.CommandQuit{} + ui.dispatch(event.CommandQuit{}) }) modal.SetBorderStyle(tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite)) @@ -873,10 +866,10 @@ func (ui *UI) addDestination() { AddInputField(inputLabelName, "My stream", inputLen, nil, nil). AddInputField(inputLabelURL, "rtmp://", inputLen, nil, nil). AddButton("Add", func() { - ui.commandC <- event.CommandAddDestination{ + ui.dispatch(event.CommandAddDestination{ DestinationName: form.GetFormItemByLabel(inputLabelName).(*tview.InputField).GetText(), URL: form.GetFormItemByLabel(inputLabelURL).(*tview.InputField).GetText(), - } + }) }). AddButton("Cancel", func() { ui.closeAddDestinationForm() @@ -931,7 +924,7 @@ func (ui *UI) removeDestination() { false, func(buttonIndex int, _ string) { if buttonIndex == 0 { - ui.commandC <- event.CommandRemoveDestination{URL: url} + ui.dispatch(event.CommandRemoveDestination{URL: url}) } }, ) @@ -1007,12 +1000,12 @@ func (ui *UI) toggleDestination() { switch ss { case startStateNotStarted: ui.urlsToStartState[url] = startStateStarting - ui.commandC <- event.CommandStartDestination{URL: url} + ui.dispatch(event.CommandStartDestination{URL: url}) case startStateStarting: // do nothing return case startStateStarted: - ui.commandC <- event.CommandStopDestination{URL: url} + ui.dispatch(event.CommandStopDestination{URL: url}) } } @@ -1065,7 +1058,7 @@ func (ui *UI) confirmQuit() { false, func(buttonIndex int, _ string) { if buttonIndex == 0 { - ui.commandC <- event.CommandQuit{} + ui.dispatch(event.CommandQuit{}) } }, )