refactor(app): internalize dispatch channel
Some checks are pending
ci-build / lint (push) Waiting to run
ci-build / build (push) Blocked by required conditions
ci-build / release (push) Blocked by required conditions
ci-scan / Analyze (go) (push) Waiting to run
ci-scan / Analyze (actions) (push) Waiting to run

This commit is contained in:
Rob Watson 2025-04-29 22:30:34 +02:00
parent caa543703e
commit 2f263b5725
2 changed files with 29 additions and 34 deletions

View File

@ -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 {

View File

@ -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{})
}
},
)