diff --git a/internal/terminal/app.go b/internal/terminal/app.go index 72919fc3..d4d0690b 100644 --- a/internal/terminal/app.go +++ b/internal/terminal/app.go @@ -480,10 +480,8 @@ func (app *App) loop(ctx context.Context) { stopTimer(messageWarmTimer) defer messageWarmTimer.Stop() - dirty := true + dirty := false for { - dirty = app.drawDirtyFrame(ctx, dirty) - shouldQuit, nextDirty := app.runLoopStep(ctx, workTicker, frameTicker, extensionTimer, messageWarmTimer, dirty) if shouldQuit { return @@ -493,16 +491,6 @@ func (app *App) loop(ctx context.Context) { } } -func (app *App) drawDirtyFrame(ctx context.Context, dirty bool) bool { - if dirty && !app.throttleDraws() { - app.draw(ctx) - - return false - } - - return dirty -} - func (app *App) runLoopStep( ctx context.Context, workTicker *time.Ticker, @@ -530,10 +518,7 @@ func (app *App) runLoopStep( return false, true case <-app.frameTick(frameTicker, dirty): - if dirty { - app.emitExtensionRuntimeEventOrMessage(ctx, extensionEventTick, map[string]any{}) - } - + app.emitExtensionRuntimeEventOrMessage(ctx, extensionEventTick, map[string]any{}) app.draw(ctx) return false, false @@ -649,7 +634,7 @@ func (app *App) workTick(ticker *time.Ticker) <-chan time.Time { } func (app *App) frameTick(ticker *time.Ticker, dirty bool) <-chan time.Time { - if app.throttleDraws() || dirty { + if dirty { return ticker.C } @@ -726,10 +711,6 @@ func stopTimer(timer *time.Timer) { } } -func (app *App) throttleDraws() bool { - return app.busy() -} - func (app *App) busy() bool { return app.working || app.authWorking || app.compacting } diff --git a/internal/terminal/render_internal_test.go b/internal/terminal/render_internal_test.go index 6d6efce0..0d57605f 100644 --- a/internal/terminal/render_internal_test.go +++ b/internal/terminal/render_internal_test.go @@ -866,30 +866,62 @@ func runLoopStepWithContextAndDormantTimers( ) } -func TestDrawDirtyFrame(t *testing.T) { +func TestFrameTickOnlyRunsForDirtyState(t *testing.T) { t.Parallel() - testCases := []struct { - name string - dirty bool - wantDraw bool - }{ - {name: "clean frame skips draw", dirty: false, wantDraw: false}, - {name: "dirty frame draws", dirty: true, wantDraw: true}, - } + ticker := time.NewTicker(time.Hour) + t.Cleanup(ticker.Stop) - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() + app := newScrollableRenderTestApp(t) + app.working = true - app := newScrollableRenderTestApp(t) - screen, ok := app.screen.(*clipboardScreen) - require.True(t, ok) + assert.Nil(t, app.frameTick(ticker, false), "busy but unchanged UI must not redraw") + assert.Equal(t, ticker.C, app.frameTick(ticker, true), "dirty UI must wait for the frame tick") +} - assert.False(t, app.drawDirtyFrame(context.Background(), testCase.dirty)) - assert.Equal(t, testCase.wantDraw, len(screen.content) > 0) - }) - } +func TestRunLoopStepDrawsDirtyFrameAndEmitsTick(t *testing.T) { + t.Parallel() + + app := newExtensionRuntimeTestApp(t, ` +librecode.on("tick", function() + librecode.buf.set_text("tick_events", "tick") +end) +`) + screen := newClipboardScreen() + screen.SetSize(40, 8) + app.screen = screen + app.renderer = tui.NewRenderer(screen) + app.frame = nil + + workTicker := time.NewTicker(time.Hour) + frameTicker := time.NewTicker(time.Millisecond) + extensionTimer := time.NewTimer(time.Hour) + messageWarmTimer := time.NewTimer(time.Hour) + + t.Cleanup(func() { + workTicker.Stop() + frameTicker.Stop() + stopTimer(extensionTimer) + stopTimer(messageWarmTimer) + }) + + ctx, cancel := context.WithTimeout(t.Context(), time.Second) + defer cancel() + + shouldQuit, dirty := app.runLoopStep( + ctx, + workTicker, + frameTicker, + extensionTimer, + messageWarmTimer, + true, + ) + + assert.False(t, shouldQuit) + assert.False(t, dirty) + require.NotNil(t, app.frame, "dirty frame tick must draw") + require.Contains(t, app.extensionUI.Buffers, "tick_events", "dirty frame tick must emit tick") + assert.Equal(t, "tick", app.extensionUI.Buffers["tick_events"].Text) } func TestDrawLatestResize(t *testing.T) {