From c36ddce36fce8e2e11a822366df81c5c15644a14 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 21 Jun 2025 17:21:03 +0900 Subject: [PATCH] Add bg-cancel action to ignore running background transforms Close #4430 Example: # Implement popup that disappears after 1 second # * Use footer as the popup # * Use `bell` to ring the terminal bell # * Use `bg-transform-footer` to clear the footer after 1 second # * Use `bg-cancel` to ignore currently running background transform actions fzf --multi --list-border \ --bind 'enter:execute-silent(echo -n {+} | pbcopy)+bell' \ --bind 'enter:+transform-footer(echo Copied {} to clipboard)' \ --bind 'enter:+bg-cancel+bg-transform-footer(sleep 1)' --- CHANGELOG.md | 15 +++++++- src/actiontype_string.go | 81 ++++++++++++++++++++-------------------- src/options.go | 2 + src/terminal.go | 30 +++++++++++---- test/test_core.rb | 17 +++++++++ 5 files changed, 96 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11a55411..44de2ef2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,8 +33,19 @@ CHANGELOG seq 10000 | fzf --preview "awk '{sum += \$1} END {print sum}' {*f}" ``` - Use this with caution, as it can make fzf sluggish for large lists. -- Added background variants of transform actions with `bg-` prefix that run asynchronously in the background +- Added asynchronous transform actions with `bg-` prefix that run asynchronously in the background, along with `bg-cancel` action to ignore currently running `bg-transform` actions. ```sh + # Implement popup that disappears after 1 second + # * Use footer as the popup + # * Use `bell` to ring the terminal bell + # * Use `bg-transform-footer` to clear the footer after 1 second + # * Use `bg-cancel` to ignore currently running background transform actions + fzf --multi --list-border \ + --bind 'enter:execute-silent(echo -n {+} | pbcopy)+bell' \ + --bind 'enter:+transform-footer(echo Copied {} to clipboard)' \ + --bind 'enter:+bg-cancel+bg-transform-footer(sleep 1)' + + # It's okay for the commands to take a little while because they run in the background GETTER='curl -s http://metaphorpsum.com/sentences/1' fzf --style full --border --preview : \ --bind "focus:bg-transform-header:$GETTER" \ @@ -48,6 +59,8 @@ CHANGELOG --bind "focus:+bg-transform-ghost:$GETTER" \ --bind "focus:+bg-transform-prompt:$GETTER" ``` +- SSH completion enhancements by @akinomyoga +- Bug fixes and improvements 0.62.0 ------ diff --git a/src/actiontype_string.go b/src/actiontype_string.go index 2740399c..e163f746 100644 --- a/src/actiontype_string.go +++ b/src/actiontype_string.go @@ -128,49 +128,50 @@ func _() { _ = x[actBgTransformPrompt-117] _ = x[actBgTransformQuery-118] _ = x[actBgTransformSearch-119] - _ = x[actSearch-120] - _ = x[actPreview-121] - _ = x[actPreviewTop-122] - _ = x[actPreviewBottom-123] - _ = x[actPreviewUp-124] - _ = x[actPreviewDown-125] - _ = x[actPreviewPageUp-126] - _ = x[actPreviewPageDown-127] - _ = x[actPreviewHalfPageUp-128] - _ = x[actPreviewHalfPageDown-129] - _ = x[actPrevHistory-130] - _ = x[actPrevSelected-131] - _ = x[actPrint-132] - _ = x[actPut-133] - _ = x[actNextHistory-134] - _ = x[actNextSelected-135] - _ = x[actExecute-136] - _ = x[actExecuteSilent-137] - _ = x[actExecuteMulti-138] - _ = x[actSigStop-139] - _ = x[actFirst-140] - _ = x[actLast-141] - _ = x[actReload-142] - _ = x[actReloadSync-143] - _ = x[actDisableSearch-144] - _ = x[actEnableSearch-145] - _ = x[actSelect-146] - _ = x[actDeselect-147] - _ = x[actUnbind-148] - _ = x[actRebind-149] - _ = x[actToggleBind-150] - _ = x[actBecome-151] - _ = x[actShowHeader-152] - _ = x[actHideHeader-153] - _ = x[actBell-154] - _ = x[actExclude-155] - _ = x[actExcludeMulti-156] - _ = x[actAsync-157] + _ = x[actBgCancel-120] + _ = x[actSearch-121] + _ = x[actPreview-122] + _ = x[actPreviewTop-123] + _ = x[actPreviewBottom-124] + _ = x[actPreviewUp-125] + _ = x[actPreviewDown-126] + _ = x[actPreviewPageUp-127] + _ = x[actPreviewPageDown-128] + _ = x[actPreviewHalfPageUp-129] + _ = x[actPreviewHalfPageDown-130] + _ = x[actPrevHistory-131] + _ = x[actPrevSelected-132] + _ = x[actPrint-133] + _ = x[actPut-134] + _ = x[actNextHistory-135] + _ = x[actNextSelected-136] + _ = x[actExecute-137] + _ = x[actExecuteSilent-138] + _ = x[actExecuteMulti-139] + _ = x[actSigStop-140] + _ = x[actFirst-141] + _ = x[actLast-142] + _ = x[actReload-143] + _ = x[actReloadSync-144] + _ = x[actDisableSearch-145] + _ = x[actEnableSearch-146] + _ = x[actSelect-147] + _ = x[actDeselect-148] + _ = x[actUnbind-149] + _ = x[actRebind-150] + _ = x[actToggleBind-151] + _ = x[actBecome-152] + _ = x[actShowHeader-153] + _ = x[actHideHeader-154] + _ = x[actBell-155] + _ = x[actExclude-156] + _ = x[actExcludeMulti-157] + _ = x[actAsync-158] } -const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync" +const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync" -var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 249, 269, 283, 298, 313, 333, 353, 372, 390, 404, 416, 432, 448, 469, 491, 506, 520, 534, 547, 564, 572, 585, 601, 613, 621, 635, 649, 660, 671, 689, 706, 713, 732, 744, 758, 767, 782, 794, 807, 818, 829, 841, 855, 876, 891, 904, 922, 938, 953, 967, 979, 991, 1008, 1015, 1020, 1029, 1040, 1051, 1064, 1079, 1090, 1103, 1118, 1125, 1138, 1151, 1168, 1183, 1196, 1210, 1224, 1240, 1260, 1272, 1295, 1312, 1330, 1348, 1371, 1394, 1416, 1437, 1452, 1471, 1495, 1513, 1530, 1548, 1562, 1587, 1606, 1626, 1646, 1671, 1696, 1720, 1743, 1760, 1781, 1807, 1827, 1846, 1866, 1875, 1885, 1898, 1914, 1926, 1940, 1956, 1974, 1994, 2016, 2030, 2045, 2053, 2059, 2073, 2088, 2098, 2114, 2129, 2139, 2147, 2154, 2163, 2176, 2192, 2207, 2216, 2227, 2236, 2245, 2258, 2267, 2280, 2293, 2300, 2310, 2325, 2333} +var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 249, 269, 283, 298, 313, 333, 353, 372, 390, 404, 416, 432, 448, 469, 491, 506, 520, 534, 547, 564, 572, 585, 601, 613, 621, 635, 649, 660, 671, 689, 706, 713, 732, 744, 758, 767, 782, 794, 807, 818, 829, 841, 855, 876, 891, 904, 922, 938, 953, 967, 979, 991, 1008, 1015, 1020, 1029, 1040, 1051, 1064, 1079, 1090, 1103, 1118, 1125, 1138, 1151, 1168, 1183, 1196, 1210, 1224, 1240, 1260, 1272, 1295, 1312, 1330, 1348, 1371, 1394, 1416, 1437, 1452, 1471, 1495, 1513, 1530, 1548, 1562, 1587, 1606, 1626, 1646, 1671, 1696, 1720, 1743, 1760, 1781, 1807, 1827, 1846, 1866, 1877, 1886, 1896, 1909, 1925, 1937, 1951, 1967, 1985, 2005, 2027, 2041, 2056, 2064, 2070, 2084, 2099, 2109, 2125, 2140, 2150, 2158, 2165, 2174, 2187, 2203, 2218, 2227, 2238, 2247, 2256, 2269, 2278, 2291, 2304, 2311, 2321, 2336, 2344} func (i actionType) String() string { if i < 0 || i >= actionType(len(_actionType_index)-1) { diff --git a/src/options.go b/src/options.go index a5abe571..c724f2eb 100644 --- a/src/options.go +++ b/src/options.go @@ -1703,6 +1703,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA appendAction(actExclude) case "exclude-multi": appendAction(actExcludeMulti) + case "bg-cancel": + appendAction(actBgCancel) default: t := isExecuteAction(specLower) if t == actIgnore { diff --git a/src/terminal.go b/src/terminal.go index 1171163c..f4f2bb2c 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -230,6 +230,11 @@ type Status struct { Selected []StatusItem `json:"selected"` } +type versionedCallback struct { + version int64 + callback func() +} + // Terminal represents terminal input/output type Terminal struct { initDelay time.Duration @@ -371,6 +376,7 @@ type Terminal struct { selected map[int32]selectedItem version int64 revision revision + bgVersion int64 reqBox *util.EventBox initialPreviewOpts previewOpts previewOpts previewOpts @@ -388,7 +394,7 @@ type Terminal struct { startChan chan fitpad killChan chan bool serverInputChan chan []*action - callbackChan chan func() + callbackChan chan versionedCallback bgQueue map[action][]func() bgSemaphore chan struct{} bgSemaphores map[action]chan struct{} @@ -601,6 +607,8 @@ const ( actBgTransformQuery actBgTransformSearch + actBgCancel + actSearch actPreview actPreviewTop @@ -1038,7 +1046,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor startChan: make(chan fitpad, 1), killChan: make(chan bool), serverInputChan: make(chan []*action, 100), - callbackChan: make(chan func(), maxBgProcesses), + callbackChan: make(chan versionedCallback, maxBgProcesses), bgQueue: make(map[action][]func()), bgSemaphore: make(chan struct{}, maxBgProcesses), bgSemaphores: make(map[action]chan struct{}), @@ -4360,10 +4368,10 @@ func (t *Terminal) captureLines(template string) string { func (t *Terminal) captureAsync(a action, firstLineOnly bool, callback func(string)) { _, list := t.buildPlusList(a.a, false) command, tempFiles := t.replacePlaceholder(a.a, false, string(t.input), list) + version := t.bgVersion + cmd := t.executor.ExecCommand(command, false) + cmd.Env = t.environ() item := func() { - cmd := t.executor.ExecCommand(command, false) - cmd.Env = t.environ() - out, _ := cmd.StdoutPipe() reader := bufio.NewReader(out) var output string @@ -4379,7 +4387,7 @@ func (t *Terminal) captureAsync(a action, firstLineOnly bool, callback func(stri } removeFiles(tempFiles) - t.callbackChan <- func() { callback(output) } + t.callbackChan <- versionedCallback{version, func() { callback(output) }} } queue, prs := t.bgQueue[a] if !prs { @@ -5318,12 +5326,16 @@ func (t *Terminal) Loop() error { case callback := <-t.callbackChan: event = tui.Invalid.AsEvent() actions = append(actions, &action{t: actAsync}) - callbacks = append(callbacks, callback) + if callback.version == t.bgVersion { + callbacks = append(callbacks, callback.callback) + } DrainCallback: for { select { case callback = <-t.callbackChan: - callbacks = append(callbacks, callback) + if callback.version == t.bgVersion { + callbacks = append(callbacks, callback.callback) + } continue DrainCallback default: break DrainCallback @@ -5696,6 +5708,8 @@ func (t *Terminal) Loop() error { doActions(actions) } }) + case actBgCancel: + t.bgVersion++ case actChangePrompt: t.promptString = a.a t.prompt, t.promptLen = t.parsePrompt(a.a) diff --git a/test/test_core.rb b/test/test_core.rb index f714e1c3..b7eba741 100644 --- a/test/test_core.rb +++ b/test/test_core.rb @@ -1964,4 +1964,21 @@ class TestCore < TestInteractive elapsed = Time.now - time assert elapsed < 2 end + + def test_bg_cancel + tmux.send_keys %(seq 0 1 | #{FZF} --bind 'space:bg-cancel+bg-transform-header(sleep {}; echo [{}])'), :Enter + tmux.until { assert_equal 2, it.match_count } + tmux.send_keys '1' + tmux.until { assert_equal 1, it.match_count } + tmux.send_keys :Space + tmux.send_keys :BSpace + tmux.until { assert_equal 2, it.match_count } + tmux.send_keys :Space + tmux.until { |lines| assert lines.any_include?('[0]') } + sleep 2 + tmux.until do |lines| + assert lines.any_include?('[0]') + refute lines.any_include?('[1]') + end + end end