Group wait state into waitState struct
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled

- All wait state under t.wait.*
- State machine as named methods: blockWait, unblockWait, cancelWait,
  waitFeedback, with the invariant documented above them
This commit is contained in:
Junegunn Choi
2026-07-05 00:58:48 +09:00
parent 4066807727
commit f43dabfbcf
+73 -50
View File
@@ -140,6 +140,13 @@ type quitSignal struct {
err error
}
type waitState struct {
blocked bool
blockedAt time.Time
pending []*action
searching bool // a search is in progress or the input is still loading
}
type previewer struct {
version int64
lines []string
@@ -321,10 +328,7 @@ type Terminal struct {
trackBlocked bool
trackSync bool
trackKeyCache map[int32]bool
waitBlocked bool
waitBlockedAt time.Time
pendingActions []*action
searchInProgress bool
wait waitState
pendingSelections map[string]selectedItem
targetIndex int32
delimiter Delimiter
@@ -1171,8 +1175,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
lastActivity: time.Now(),
// The initial load counts as a search in progress ('start:wait').
// Set before the reader starts so the first final result clears it.
searchInProgress: true,
numLinesCache: make(map[int32]numLinesCacheValue)}
wait: waitState{searching: true},
numLinesCache: make(map[int32]numLinesCacheValue)}
if opts.AcceptNth != nil {
t.acceptNth = opts.AcceptNth(t.delimiter)
}
@@ -1884,19 +1888,19 @@ func (t *Terminal) UpdateProgress(progress float32) {
func (t *Terminal) UpdateList(result MatchResult) {
merger := result.merger
t.mutex.Lock()
waitWasBlocked := t.waitBlocked
waitWasBlocked := t.wait.blocked
wakeUp := false
if result.final() {
t.searchInProgress = false
t.wait.searching = false
// If waiting, unblock so main loop can execute pending actions.
// Note: any final result unblocks the wait, not just the one for the
// search that armed it. Back-to-back searches (--listen, bg-transform
// callbacks, reload+wait) can therefore unblock early and run the
// pending actions on the previous result set. Accepted; a per-search
// generation token through the matcher isn't worth the complexity.
if t.waitBlocked {
if t.wait.blocked {
t.unblockWait()
wakeUp = len(t.pendingActions) > 0
wakeUp = len(t.wait.pending) > 0
}
}
prevIndex := minItem.Index()
@@ -2064,7 +2068,7 @@ func (t *Terminal) UpdateList(result MatchResult) {
}
}
updateList := !t.trackBlocked && !t.pendingReqList
updatePrompt := (trackWasBlocked && !t.trackBlocked) || (waitWasBlocked && !t.waitBlocked)
updatePrompt := (trackWasBlocked && !t.trackBlocked) || (waitWasBlocked && !t.wait.blocked)
t.mutex.Unlock()
// Wake up the main loop to execute pending actions after wait unblocks.
@@ -5825,19 +5829,56 @@ func (t *Terminal) unblockTrack() {
}
}
// Debounce visual feedback so quick searches don't cause flashing
func (t *Terminal) waitFeedback() bool {
return t.waitBlocked && time.Since(t.waitBlockedAt) > progressMinDuration
// The wait state machine. Invariant: arming captures every action after
// 'wait' at any nesting level into wait.pending; while blocked, actions are
// dropped unless they are results of work started before the block
// (bg-transform callbacks, bracketed paste bookkeeping); abort/cancel
// discards everything.
// blockWait blocks action execution and defers the given actions until the
// current search completes (see UpdateList)
func (t *Terminal) blockWait(pending []*action) {
t.wait.blocked = true
t.wait.blockedAt = time.Now()
// Clone so that later joins don't append into the backing array of the
// bound action list
t.wait.pending = slices.Clone(pending)
// Show the waiting feedback only if the search takes long enough,
// so that quick searches don't cause flickering
go func() {
timer := time.NewTimer(progressMinDuration)
<-timer.C
t.mutex.Lock()
blocked := t.wait.blocked
t.mutex.Unlock()
if blocked {
t.reqBox.Set(reqPrompt, nil)
t.reqBox.Set(reqInfo, nil)
}
}()
}
// unblockWait lifts the block, leaving the pending actions to the caller:
// UpdateList keeps them for the main loop to replay, cancelWait discards them
func (t *Terminal) unblockWait() {
t.waitBlocked = false
t.wait.blocked = false
// Restore the cursor unless it's still hidden for another reason
if !t.inputless && !t.trackBlocked {
t.tui.ShowCursor()
}
}
// cancelWait unblocks and discards the pending actions (user abort)
func (t *Terminal) cancelWait() {
t.unblockWait()
t.wait.pending = nil
}
// Debounce visual feedback so quick searches don't cause flashing
func (t *Terminal) waitFeedback() bool {
return t.wait.blocked && time.Since(t.wait.blockedAt) > progressMinDuration
}
func (t *Terminal) addClickHeaderWord(env []string) []string {
/*
* echo $'HL1\nHL2' | fzf --header-lines 3 --header $'H1\nH2' --header-lines-border --bind 'click-header:preview:env | grep FZF_CLICK'
@@ -6684,45 +6725,28 @@ func (t *Terminal) Loop() error {
// Already waiting. Actions parsed from a bg-transform
// result join the current wait; user input can't reset
// the blocked state
if t.waitBlocked {
if t.wait.blocked {
if inBgCallback {
t.pendingActions = append(t.pendingActions, actions[i+1:]...)
t.wait.pending = append(t.wait.pending, actions[i+1:]...)
}
return true
}
// Block if search is in progress or will be triggered
if changed || newCommand != nil || t.searchInProgress || queryBefore != string(t.input) {
t.waitBlocked = true
t.waitBlockedAt = time.Now()
// Clone so that later joins don't append into the
// backing array of the bound action list
t.pendingActions = slices.Clone(actions[i+1:])
// Show the waiting feedback only if the search takes long enough,
// so that quick searches don't cause flickering.
go func() {
timer := time.NewTimer(progressMinDuration)
<-timer.C
t.mutex.Lock()
blocked := t.waitBlocked
t.mutex.Unlock()
if blocked {
t.reqBox.Set(reqPrompt, nil)
t.reqBox.Set(reqInfo, nil)
}
}()
if changed || newCommand != nil || t.wait.searching || queryBefore != string(t.input) {
t.blockWait(actions[i+1:])
return true
}
// No search, wait is a no-op; continue to next action
continue
}
blockedBefore := t.waitBlocked
blockedBefore := t.wait.blocked
if !doAction(action) {
return false
}
// If this action armed the wait through a nested list
// (e.g. via trigger), defer the rest of this list too
if !blockedBefore && t.waitBlocked {
t.pendingActions = append(t.pendingActions, actions[i+1:]...)
if !blockedBefore && t.wait.blocked {
t.wait.pending = append(t.wait.pending, actions[i+1:]...)
return true
}
// A terminal action performed. We should stop processing more.
@@ -6777,10 +6801,9 @@ func (t *Terminal) Loop() error {
passthrough := inBgCallback || a.t == actAsync ||
a.t == actBracketedPasteBegin || a.t == actBracketedPasteEnd
// When wait-blocked, only allow abort/cancel
if t.waitBlocked && !passthrough {
if t.wait.blocked && !passthrough {
if a.t == actAbort || a.t == actCancel {
t.unblockWait()
t.pendingActions = nil
t.cancelWait()
req(reqPrompt, reqInfo)
}
return true
@@ -7673,7 +7696,7 @@ func (t *Terminal) Loop() error {
req(reqPrompt)
case actTrigger:
if _, chords, err := parseKeyChords(a.a, ""); err == nil {
blockedBefore := t.waitBlocked
blockedBefore := t.wait.blocked
for ci, chord := range chords {
if _, prs := triggering[chord]; prs {
// Avoid recursive triggering
@@ -7685,14 +7708,14 @@ func (t *Terminal) Loop() error {
delete(triggering, chord)
}
// If this chord armed the wait, defer the remaining chords
if !blockedBefore && t.waitBlocked {
if !blockedBefore && t.wait.blocked {
for _, rest := range chords[ci+1:] {
if _, prs := triggering[rest]; prs {
// Avoid recursive triggering
continue
}
if acts, prs := t.keymap[rest]; prs {
t.pendingActions = append(t.pendingActions, acts...)
t.wait.pending = append(t.wait.pending, acts...)
}
}
break
@@ -8214,10 +8237,10 @@ func (t *Terminal) Loop() error {
// state first so that a pending 'jump' action isn't cancelled right
// away by the wake-up event below.
jumpingBefore := t.jumping
if len(t.pendingActions) > 0 && !t.waitBlocked {
pendingActions := t.pendingActions
t.pendingActions = nil
if !doActions(pendingActions) {
if len(t.wait.pending) > 0 && !t.wait.blocked {
pending := t.wait.pending
t.wait.pending = nil
if !doActions(pending) {
continue
}
}
@@ -8284,7 +8307,7 @@ func (t *Terminal) Loop() error {
reload := changed || newCommand != nil
if reload {
t.searchInProgress = true
t.wait.searching = true
}
var reloadRequest *searchRequest
if reload {