package loader import ( "context" "errors" "fmt" "sync" "galaxy/client/updater" "galaxy/connector" "galaxy/storage" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) const ( loaderLogViewportColumns = 80 loaderLogViewportRows = 12 ) type loader struct { app fyne.App storage storage.Storage connector connector.Connector updater *updater.Manager runner uiRunner debugWindow fyne.Window textGrid *widget.TextGrid btn *widget.Button ctx context.Context resultMu sync.Mutex result error closeMu sync.Mutex closeQuits bool } // loaderLogViewportMinSize derives a stable monospace TextGrid viewport size // from the active Fyne text metrics. func loaderLogViewportMinSize(app fyne.App) fyne.Size { if app == nil || app.Driver() == nil { return fyne.NewSize(0, 0) } cellSize, _ := app.Driver().RenderedTextSize( "M", theme.TextSize(), fyne.TextStyle{Monospace: true}, nil, ) return fyne.NewSize( cellSize.Width*loaderLogViewportColumns, cellSize.Height*loaderLogViewportRows, ) } func NewLoader(s storage.Storage, conn connector.Connector, app fyne.App) (*loader, error) { l := &loader{ app: app, connector: conn, storage: s, updater: updater.NewManager(s, conn), runner: execRunner{}, textGrid: widget.NewTextGrid(), debugWindow: app.NewWindow("Loader"), } l.btn = widget.NewButton("Retry", l.onButtonAction) l.btn.Disable() l.textGrid.Scroll = fyne.ScrollNone l.debugWindow.SetCloseIntercept(l.onWindowClose) logScroll := container.NewScroll(l.textGrid) logScroll.Direction = container.ScrollBoth logScroll.SetMinSize(loaderLogViewportMinSize(app)) actionBar := container.NewCenter(container.NewHBox(l.btn)) content := container.NewBorder(nil, actionBar, nil, nil, logScroll) l.debugWindow.SetContent(content) l.debugWindow.Resize(content.MinSize()) l.debugWindow.SetFixedSize(true) l.debugWindow.CenterOnScreen() return l, nil } func (l *loader) runOnce(ctx context.Context) error { target, err := l.updater.EnsureLaunchTarget() if err != nil { return err } l.logText(fmt.Sprintf("Starting UI client v%s", target.Version)) l.logText(fmt.Sprintf("Executable: %s", target.Path)) exitCode, runErr := l.runner.Run(ctx, target.Path) markErr := l.updater.MarkLaunchResult(target.Version, exitCode, runErr) switch { case runErr != nil: return errors.Join(fmt.Errorf("launch UI client v%s: %w", target.Version, runErr), markErr) case exitCode != 0: return errors.Join(fmt.Errorf("UI client v%s exited with code %d", target.Version, exitCode), markErr) default: return markErr } } // init prepares and launches the standalone UI client, or shows a retry button on failure. func (l *loader) init(ctx context.Context) { l.setCloseQuits(false) fyne.Do(func() { l.textGrid.SetText("") l.btn.Hide() l.btn.Disable() // show debugWindow can be done with future debug mode, e.g. with -debug flag l.debugWindow.Hide() }) err := l.runOnce(ctx) if err == nil || errors.Is(err, context.Canceled) { l.setResult(nil) fyne.Do(func() { l.debugWindow.Hide() l.app.Quit() }) return } l.setCloseQuits(true) l.setResult(err) l.logError(err) fyne.Do(func() { l.btn.SetText("Retry") l.btn.Enable() l.btn.Show() l.debugWindow.Show() }) } func (l *loader) onButtonAction() { if l.ctx == nil { return } go l.init(l.ctx) } func (l *loader) onWindowClose() { if l.getCloseQuits() { l.app.Quit() return } l.debugWindow.Hide() } func (l *loader) logText(v string) { if l.textGrid == nil { return } fyne.Do(func() { l.textGrid.Append(v) }) } func (l *loader) logError(err error) { l.logText(fmt.Sprintf("ERROR: %s", err)) } func (l *loader) setResult(err error) { l.resultMu.Lock() defer l.resultMu.Unlock() l.result = err } func (l *loader) getResult() error { l.resultMu.Lock() defer l.resultMu.Unlock() return l.result } func (l *loader) setCloseQuits(v bool) { l.closeMu.Lock() defer l.closeMu.Unlock() l.closeQuits = v } func (l *loader) getCloseQuits() bool { l.closeMu.Lock() defer l.closeMu.Unlock() return l.closeQuits } // Run starts the loader window, launches the standalone UI process, and returns // the final launch result once the loader application exits. func (l *loader) Run(ctx context.Context) error { l.ctx = ctx go l.init(ctx) go func() { <-ctx.Done() fyne.Do(l.app.Quit) }() l.app.Run() if errors.Is(ctx.Err(), context.Canceled) { return nil } return l.getResult() }