Files
lazygit/pkg/gocui/block_events_test.go
Stefan HallerandClaude Opus 4.8 6893d9a759 Add gocui primitives to block input during an operation
Long-running operations that lazygit drives itself (rebases, and the
commit surgery built on them) can be corrupted by keys the user presses
while they run: pressing e to start an interactive rebase, then up+d
before it finishes, must act on the resulting todo list, not race the
rebase. WithWaitingStatusSync gets this today only as a side effect of
freezing the UI thread, which the rest of this branch is moving away
from.

Add a nestable counter, BeginBlockingEvents/EndBlockingEvents, that
withholds input at the event-dispatch layer without freezing anything:
while blocked, key events are buffered and replayed in order once the
count returns to zero (so they act on the now-current context), mouse
clicks and hover are dropped (replaying them against a changed layout
would target the wrong thing), and scrolling, resize, focus and all
rendering keep flowing. These are the reusable core; a gui-level helper
that brackets them around a worker operation follows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 12:32:32 +02:00

99 lines
2.9 KiB
Go

package gocui
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestEventWithheldWhileBlocking(t *testing.T) {
scenarios := []struct {
name string
event GocuiEvent
withheld bool
}{
{"key", GocuiEvent{Type: eventKey, Key: NewKeyRune('x')}, true},
{"mouse click", GocuiEvent{Type: eventMouse, Key: NewKeyName(MouseLeft)}, true},
{"mouse scroll", GocuiEvent{Type: eventMouse, Key: NewKeyName(MouseWheelDown)}, false},
{"mouse move", GocuiEvent{Type: eventMouseMove}, true},
{"resize", GocuiEvent{Type: eventResize}, false},
{"focus", GocuiEvent{Type: eventFocus}, false},
{"paste", GocuiEvent{Type: eventPaste}, false},
{"error", GocuiEvent{Type: eventError}, false},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
assert.Equal(t, s.withheld, eventWithheldWhileBlocking(&s.event))
})
}
}
// setupKeyRecorder wires a keybinding on a focused view that records each time
// it fires, and returns the key event that triggers it plus the record slice.
func setupKeyRecorder(t *testing.T, g *Gui) (GocuiEvent, *[]int) {
t.Helper()
_, _ = g.SetView("main", 0, 0, 80, 22, 0)
_, err := g.SetCurrentView("main")
assert.NoError(t, err)
fired := []int{}
callCount := 0
key := NewKeyRune('x')
g.SetKeybinding("main", key, func(*Gui, *View) error {
callCount++
fired = append(fired, callCount)
return nil
})
return GocuiEvent{Type: eventKey, Key: key}, &fired
}
func TestBlockingEvents_KeysBufferedAndReplayed(t *testing.T) {
g := newTestGui(t)
keyEvent, fired := setupKeyRecorder(t, g)
// Not blocking: the key dispatches immediately.
assert.NoError(t, g.handleEvent(&keyEvent))
assert.Len(t, *fired, 1)
// While blocking: the key is buffered, not dispatched.
g.BeginBlockingEvents()
assert.NoError(t, g.handleEvent(&keyEvent))
assert.NoError(t, g.handleEvent(&keyEvent))
assert.Len(t, *fired, 1, "buffered keys must not dispatch while blocking")
// Unblocking replays the buffered keys.
assert.NoError(t, g.EndBlockingEvents())
assert.Len(t, *fired, 3, "both buffered keys should replay on unblock")
assert.Empty(t, g.bufferedKeyEvents)
}
func TestBlockingEvents_NestsWithCounter(t *testing.T) {
g := newTestGui(t)
keyEvent, fired := setupKeyRecorder(t, g)
g.BeginBlockingEvents()
g.BeginBlockingEvents()
assert.NoError(t, g.handleEvent(&keyEvent))
// The inner block ending still leaves us blocked: no replay yet.
assert.NoError(t, g.EndBlockingEvents())
assert.Empty(t, *fired)
// Only the outermost block ending replays.
assert.NoError(t, g.EndBlockingEvents())
assert.Len(t, *fired, 1)
}
func TestBlockingEvents_MouseClicksDroppedNotBuffered(t *testing.T) {
g := newTestGui(t)
g.BeginBlockingEvents()
click := GocuiEvent{Type: eventMouse, Key: NewKeyName(MouseLeft)}
assert.NoError(t, g.handleEvent(&click))
assert.Empty(t, g.bufferedKeyEvents, "mouse clicks must be dropped, not buffered")
assert.NoError(t, g.EndBlockingEvents())
}