Allow Then and BatchUIUpdates to work with an async refresh

Then, and BatchUIUpdates, previously only worked for a SYNC refresh: the
calling goroutine blocked in wg.Wait until every scope had finished, and
only then flushed the batch and ran Then. An ASYNC refresh had no such
join point — it dispatched each scope onto its own worker and returned
right away — so Then was forbidden (it would have run before the scopes
finished) and a batch would never be drained.

Give the async path a join of its own. Both paths now register their
scopes in the WaitGroup, and the finishing work — wg.Wait, the batch
flush, and Then — moves into a closure. A SYNC refresh runs it inline as
before; an ASYNC refresh dispatches it to a worker, so the caller still
returns immediately but the batch and Then run once every scope is done.

Besides lifting the restriction, this makes SYNC and ASYNC differ only
in whether the finishing work blocks the caller, which is what lets a
later commit drop the mode entirely and key the choice off the calling
thread instead.
This commit is contained in:
Stefan Haller 2026-07-08 14:47:06 +02:00
parent f319522d5b
commit bfd3b7b47e

View file

@ -142,10 +142,6 @@ func (self *refreshBounceBatch) close() []func() {
}
func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFromWorker bool) {
if options.Mode == types.ASYNC && options.Then != nil {
panic("RefreshOptions.Then doesn't work with mode ASYNC")
}
t := time.Now()
defer func() {
self.c.Log.Infof("Refresh took %s", time.Since(t))
@ -234,16 +230,18 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr
wg := sync.WaitGroup{}
refresh := func(name string, f func()) {
wg.Add(1)
// if we're in a demo we don't want any async refreshes because
// everything happens fast and it's better to have everything update
// in the one frame
if !self.c.InDemo() && options.Mode == types.ASYNC {
self.onWorker(env.background, func(t gocui.Task) error {
defer wg.Done()
f()
return nil
})
} else {
wg.Add(1)
go utils.Safe(func() {
t := time.Now()
defer wg.Done()
@ -410,30 +408,41 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr
self.refreshStatus(env)
wg.Wait()
waitAndFinalize := func() {
wg.Wait()
if env.batch != nil {
// Apply all the scopes' collected bounces in a single UI-thread task,
// so they land in one frame: gocui drains every queued event before it
// redraws, so one task means one repaint. Bounces enqueued from within
// these (see refreshBounceBatch) run as ordinary follow-ups.
bounces := env.batch.close()
self.onUIThread(env.background, func() error {
for _, bounce := range bounces {
bounce()
}
return nil
})
if env.batch != nil {
// Apply all the scopes' collected bounces in a single UI-thread task,
// so they land in one frame: gocui drains every queued event before it
// redraws, so one task means one repaint. Bounces enqueued from within
// these (see refreshBounceBatch) run as ordinary follow-ups.
bounces := env.batch.close()
self.onUIThread(env.background, func() error {
for _, bounce := range bounces {
bounce()
}
return nil
})
}
if options.Then != nil {
// Queue Then via OnUIThread so it runs *after* the refresh-scope
// functions' model-update bounces (which are already queued by
// now), not synchronously here — at this point the workers have
// returned but their bounces haven't been processed yet, so
// invoking Then synchronously would run it on a model that's
// still pre-refresh.
self.onUIThread(env.background, options.Then)
}
}
if options.Then != nil {
// Queue Then via OnUIThread so it runs *after* the refresh-scope
// functions' model-update bounces (which are already queued by
// now), not synchronously here — at this point the workers have
// returned but their bounces haven't been processed yet, so
// invoking Then synchronously would run it on a model that's
// still pre-refresh.
self.onUIThread(env.background, options.Then)
if options.Mode == types.SYNC {
waitAndFinalize()
} else {
self.onWorker(env.background, func(t gocui.Task) error {
waitAndFinalize()
return nil
})
}
}