Add result-final event
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled

- Fires like result, but only after the input stream closes
- Use for one-shot per-query actions that would otherwise re-fire on
  every intermediate snapshot during loading

Close #4835
This commit is contained in:
Junegunn Choi
2026-06-14 19:37:22 +09:00
parent f5fbfd848e
commit 5dd698b869
7 changed files with 59 additions and 6 deletions
+11
View File
@@ -1,6 +1,17 @@
CHANGELOG
=========
0.74.0 (WIP)
------------
- Added `result-final` event, a variant of `result` that is not triggered while the input stream is still open (#4835)
- Use it for one-shot, per-query actions that would otherwise re-fire on every intermediate snapshot during loading
```sh
# 'result' fires per intermediate snapshot (header keeps updating during load);
# 'result-final' fires once after the stream closes (footer shows the final count)
(seq 100; sleep 1; seq 100) | fzf --query 1 \
--bind 'result:transform-header(echo result: $FZF_MATCH_COUNT),result-final:transform-footer(echo final: $FZF_MATCH_COUNT)'
```
0.73.1
------
- Bug fixes
+15 -1
View File
@@ -226,7 +226,7 @@ Enable processing of ANSI color codes
Synchronous search for multi-staged filtering. If specified, fzf will launch
the finder only after the input stream is complete and the initial filtering
and the associated actions (bound to any of \fBstart\fR, \fBload\fR,
\fBresult\fR, or \fBfocus\fR) are complete.
\fBresult\fR, \fBresult\-final\fR, or \fBfocus\fR) are complete.
.RS
e.g. \fB# Avoid rendering both fzf instances at the same time
@@ -1855,6 +1855,20 @@ e.g.
# * Note that you can't use 'change' event in this case because the second position may not be available
fzf \-\-sync \-\-bind 'result:transform:[[ \-z {q} ]] && echo "pos(2)"'\fR
.RE
\fIresult\-final\fR
.RS
Same as \fIresult\fR, but suppressed while the input stream is still open. Use
this when you want a one-shot action per query instead of one per intermediate
snapshot during loading.
e.g.
\fB# 'result' fires per intermediate snapshot (header keeps updating during load);
# 'result-final' fires once after the stream closes (footer shows the final count)
(seq 100; sleep 1; seq 100) | fzf \-\-query 1 \\
\-\-bind 'result:transform\-header(echo result: $FZF_MATCH_COUNT),result\-final:transform\-footer(echo final: $FZF_MATCH_COUNT)'\fR
.RE
\fIchange\fR
.RS
Triggered whenever the query string is changed
+2
View File
@@ -1063,6 +1063,8 @@ func parseKeyChords(str string, message string) (map[tui.Event]string, []tui.Eve
add(tui.Focus)
case "result":
add(tui.Result)
case "result-final":
add(tui.ResultFinal)
case "resize":
add(tui.Resize)
case "one":
+16 -3
View File
@@ -1152,7 +1152,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
bgSemaphore: make(chan struct{}, maxBgProcesses),
bgSemaphores: make(map[action]chan struct{}),
keyChan: make(chan tui.Event),
eventChan: make(chan tui.Event, 6), // start | (load + result + zero|one) | (focus) | (resize)
eventChan: make(chan tui.Event, 7), // start | (load + result + result-final + zero|one) | (focus) | (resize)
timerChan: make(chan tui.Event), // unbuffered: every() ticks coalesce when main loop is busy
tui: renderer,
ttyDefault: opts.TtyDefault,
@@ -1345,6 +1345,9 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
}
_, t.hasStartActions = t.keymap[tui.Start.AsEvent()]
_, t.hasResultActions = t.keymap[tui.Result.AsEvent()]
if _, prs := t.keymap[tui.ResultFinal.AsEvent()]; prs {
t.hasResultActions = true
}
_, t.hasFocusActions = t.keymap[tui.Focus.AsEvent()]
_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
@@ -2022,8 +2025,18 @@ func (t *Terminal) UpdateList(result MatchResult) {
}
}
if t.hasResultActions {
t.pendingReqList = true
t.eventChan <- tui.Result.AsEvent()
result := tui.Result.AsEvent()
if _, prs := t.keymap[result]; prs {
t.pendingReqList = true
t.eventChan <- result
}
if !t.reading {
resultFinal := tui.ResultFinal.AsEvent()
if _, prs := t.keymap[resultFinal]; prs {
t.pendingReqList = true
t.eventChan <- resultFinal
}
}
}
updateList := !t.trackBlocked && !t.pendingReqList
updatePrompt := trackWasBlocked && !t.trackBlocked
+3 -2
View File
@@ -164,11 +164,12 @@ func _() {
_ = x[ClickFooter-153]
_ = x[Multi-154]
_ = x[Every-155]
_ = x[ResultFinal-156]
}
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteShiftHomeShiftEndShiftPageUpShiftPageDownF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltDeleteAltHomeAltEndAltPageUpAltPageDownAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltShiftDeleteAltShiftHomeAltShiftEndAltShiftPageUpAltShiftPageDownCtrlUpCtrlDownCtrlLeftCtrlRightCtrlHomeCtrlEndCtrlBackspaceCtrlDeleteCtrlPageUpCtrlPageDownAltCtrlAltCtrlAltUpCtrlAltDownCtrlAltLeftCtrlAltRightCtrlAltHomeCtrlAltEndCtrlAltBackspaceCtrlAltDeleteCtrlAltPageUpCtrlAltPageDownCtrlShiftUpCtrlShiftDownCtrlShiftLeftCtrlShiftRightCtrlShiftHomeCtrlShiftEndCtrlShiftDeleteCtrlShiftPageUpCtrlShiftPageDownCtrlAltShiftUpCtrlAltShiftDownCtrlAltShiftLeftCtrlAltShiftRightCtrlAltShiftHomeCtrlAltShiftEndCtrlAltShiftDeleteCtrlAltShiftPageUpCtrlAltShiftPageDownMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownInvalidFatalBracketedPasteBeginBracketedPasteEndResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeaderClickFooterMultiEvery"
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteShiftHomeShiftEndShiftPageUpShiftPageDownF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltDeleteAltHomeAltEndAltPageUpAltPageDownAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltShiftDeleteAltShiftHomeAltShiftEndAltShiftPageUpAltShiftPageDownCtrlUpCtrlDownCtrlLeftCtrlRightCtrlHomeCtrlEndCtrlBackspaceCtrlDeleteCtrlPageUpCtrlPageDownAltCtrlAltCtrlAltUpCtrlAltDownCtrlAltLeftCtrlAltRightCtrlAltHomeCtrlAltEndCtrlAltBackspaceCtrlAltDeleteCtrlAltPageUpCtrlAltPageDownCtrlShiftUpCtrlShiftDownCtrlShiftLeftCtrlShiftRightCtrlShiftHomeCtrlShiftEndCtrlShiftDeleteCtrlShiftPageUpCtrlShiftPageDownCtrlAltShiftUpCtrlAltShiftDownCtrlAltShiftLeftCtrlAltShiftRightCtrlAltShiftHomeCtrlAltShiftEndCtrlAltShiftDeleteCtrlAltShiftPageUpCtrlAltShiftPageDownMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownInvalidFatalBracketedPasteBeginBracketedPasteEndResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeaderClickFooterMultiEveryResultFinal"
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 157, 173, 182, 191, 199, 208, 214, 220, 228, 230, 234, 238, 243, 247, 250, 256, 263, 272, 281, 291, 302, 311, 319, 330, 343, 345, 347, 349, 351, 353, 355, 357, 359, 361, 364, 367, 370, 382, 387, 394, 401, 409, 418, 425, 431, 440, 451, 461, 473, 485, 498, 512, 524, 535, 549, 565, 571, 579, 587, 596, 604, 611, 624, 634, 644, 656, 659, 666, 675, 686, 697, 709, 720, 730, 746, 759, 772, 787, 798, 811, 824, 838, 851, 863, 878, 893, 910, 924, 940, 956, 973, 989, 1004, 1022, 1040, 1060, 1065, 1076, 1085, 1095, 1105, 1116, 1124, 1134, 1143, 1154, 1169, 1186, 1193, 1198, 1217, 1234, 1240, 1246, 1257, 1262, 1266, 1271, 1274, 1278, 1284, 1288, 1298, 1309, 1320, 1325, 1330}
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 157, 173, 182, 191, 199, 208, 214, 220, 228, 230, 234, 238, 243, 247, 250, 256, 263, 272, 281, 291, 302, 311, 319, 330, 343, 345, 347, 349, 351, 353, 355, 357, 359, 361, 364, 367, 370, 382, 387, 394, 401, 409, 418, 425, 431, 440, 451, 461, 473, 485, 498, 512, 524, 535, 549, 565, 571, 579, 587, 596, 604, 611, 624, 634, 644, 656, 659, 666, 675, 686, 697, 709, 720, 730, 746, 759, 772, 787, 798, 811, 824, 838, 851, 863, 878, 893, 910, 924, 940, 956, 973, 989, 1004, 1022, 1040, 1060, 1065, 1076, 1085, 1095, 1105, 1116, 1124, 1134, 1143, 1154, 1169, 1186, 1193, 1198, 1217, 1234, 1240, 1246, 1257, 1262, 1266, 1271, 1274, 1278, 1284, 1288, 1298, 1309, 1320, 1325, 1330, 1341}
func (i EventType) String() string {
if i < 0 || i >= EventType(len(_EventType_index)-1) {
+1
View File
@@ -234,6 +234,7 @@ const (
ClickFooter
Multi
Every
ResultFinal
)
func (t EventType) AsEvent() Event {
+11
View File
@@ -1405,6 +1405,17 @@ class TestCore < TestInteractive
tmux.until { |lines| assert_includes lines, '> 1' }
end
def test_result_final_event
tmux.send_keys %[(seq 100; sleep 1; seq 100) | #{FZF} \\
--query 1 \\
--bind 'result:transform-header(echo "R=$FZF_MATCH_COUNT")' \\
--bind 'result-final:transform-footer(echo "F=$FZF_MATCH_COUNT")'], :Enter
tmux.until { |lines| assert lines.any_include?('R=20') }
tmux.until { |lines| refute lines.any_include?('F=20') }
tmux.until { |lines| assert lines.any_include?('R=40') }
tmux.until { |lines| assert lines.any_include?('F=40') }
end
def test_every_event
tmux.send_keys %(seq 100 | fzf --bind 'every(0.2):transform-prompt(cat #{tempname})'), :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }