diff --git a/CHANGELOG.md b/CHANGELOG.md index 03afc90d..4a8cb3a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,24 @@ CHANGELOG ========= +0.63.0 +------ +- Added background variants of transform actions with `bg-` prefix that run asynchronously in the background + ```sh + GETTER='curl -s http://metaphorpsum.com/sentences/1' + fzf --style full --border --preview : \ + --bind "focus:bg-transform-header:$GETTER" \ + --bind "focus:+bg-transform-footer:$GETTER" \ + --bind "focus:+bg-transform-border-label:$GETTER" \ + --bind "focus:+bg-transform-preview-label:$GETTER" \ + --bind "focus:+bg-transform-input-label:$GETTER" \ + --bind "focus:+bg-transform-list-label:$GETTER" \ + --bind "focus:+bg-transform-header-label:$GETTER" \ + --bind "focus:+bg-transform-footer-label:$GETTER" \ + --bind "focus:+bg-transform-ghost:$GETTER" \ + --bind "focus:+bg-transform-prompt:$GETTER" + ``` + 0.62.0 ------ - Relaxed the `--color` option syntax to allow whitespace-separated entries (in addition to commas), making multi-line definitions easier to write and read diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 834df205..a64b5e0d 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -1805,6 +1805,9 @@ A key or an event can be bound to one or more of the following actions. \fBup\fR \fIctrl\-k ctrl\-p up\fR \fByank\fR \fIctrl\-y\fR +Each \fBtransform*\fR action has a corresponding \fBbg\-transform*\fR +variant that runs the command in the background. + .SS ACTION COMPOSITION Multiple actions can be chained using \fB+\fR separator. @@ -1929,6 +1932,14 @@ e.g. echo "change\-header:Invalid selection"' \fR +.SS TRANSFORM IN THE BACKGROUND + +Transform actions are synchronous, meaning fzf becomes unresponsive while the +command runs. To avoid this, each \fBtransform*\fR action has a corresponding +\fBbg\-transform*\fR variant that runs in the background. Unless you need to +chain multiple transform actions where later ones depend on earlier results, +prefer using the \fBbg\fR variant. + .SS PREVIEW BINDING With \fBpreview(...)\fR action, you can specify multiple different preview diff --git a/src/actiontype_string.go b/src/actiontype_string.go index bd141fde..2740399c 100644 --- a/src/actiontype_string.go +++ b/src/actiontype_string.go @@ -113,48 +113,64 @@ func _() { _ = x[actTransformPrompt-102] _ = x[actTransformQuery-103] _ = x[actTransformSearch-104] - _ = x[actSearch-105] - _ = x[actPreview-106] - _ = x[actPreviewTop-107] - _ = x[actPreviewBottom-108] - _ = x[actPreviewUp-109] - _ = x[actPreviewDown-110] - _ = x[actPreviewPageUp-111] - _ = x[actPreviewPageDown-112] - _ = x[actPreviewHalfPageUp-113] - _ = x[actPreviewHalfPageDown-114] - _ = x[actPrevHistory-115] - _ = x[actPrevSelected-116] - _ = x[actPrint-117] - _ = x[actPut-118] - _ = x[actNextHistory-119] - _ = x[actNextSelected-120] - _ = x[actExecute-121] - _ = x[actExecuteSilent-122] - _ = x[actExecuteMulti-123] - _ = x[actSigStop-124] - _ = x[actFirst-125] - _ = x[actLast-126] - _ = x[actReload-127] - _ = x[actReloadSync-128] - _ = x[actDisableSearch-129] - _ = x[actEnableSearch-130] - _ = x[actSelect-131] - _ = x[actDeselect-132] - _ = x[actUnbind-133] - _ = x[actRebind-134] - _ = x[actToggleBind-135] - _ = x[actBecome-136] - _ = x[actShowHeader-137] - _ = x[actHideHeader-138] - _ = x[actBell-139] - _ = x[actExclude-140] - _ = x[actExcludeMulti-141] + _ = x[actBgTransform-105] + _ = x[actBgTransformBorderLabel-106] + _ = x[actBgTransformGhost-107] + _ = x[actBgTransformHeader-108] + _ = x[actBgTransformFooter-109] + _ = x[actBgTransformHeaderLabel-110] + _ = x[actBgTransformFooterLabel-111] + _ = x[actBgTransformInputLabel-112] + _ = x[actBgTransformListLabel-113] + _ = x[actBgTransformNth-114] + _ = x[actBgTransformPointer-115] + _ = x[actBgTransformPreviewLabel-116] + _ = 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] } -const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMulti" +const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync" -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, 1557, 1567, 1580, 1596, 1608, 1622, 1638, 1656, 1676, 1698, 1712, 1727, 1735, 1741, 1755, 1770, 1780, 1796, 1811, 1821, 1829, 1836, 1845, 1858, 1874, 1889, 1898, 1909, 1918, 1927, 1940, 1949, 1962, 1975, 1982, 1992, 2007} +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} func (i actionType) String() string { if i < 0 || i >= actionType(len(_actionType_index)-1) { diff --git a/src/constants.go b/src/constants.go index ececdb97..6f56fd58 100644 --- a/src/constants.go +++ b/src/constants.go @@ -29,6 +29,10 @@ const ( maxPatternLength = 1000 maxMulti = math.MaxInt32 + // Background processes + maxBgProcesses = 30 + maxBgProcessesPerAction = 3 + // Matcher numPartitionsMultiplier = 8 maxPartitions = 32 diff --git a/src/options.go b/src/options.go index 2fd6f821..a5abe571 100644 --- a/src/options.go +++ b/src/options.go @@ -1435,7 +1435,7 @@ const ( func init() { executeRegexp = regexp.MustCompile( - `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|(?:border|list|preview|input|header|footer)-label|header|footer|search|nth|pointer|ghost)|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search)`) + `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|bg-transform|transform)-(?:query|prompt|(?:border|list|preview|input|header|footer)-label|header|footer|search|nth|pointer|ghost)|bg-transform|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search)`) splitRegexp = regexp.MustCompile("[,:]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") } @@ -1892,6 +1892,36 @@ func isExecuteAction(str string) actionType { return actTransformQuery case "transform-search": return actTransformSearch + case "bg-transform": + return actBgTransform + case "bg-transform-list-label": + return actBgTransformListLabel + case "bg-transform-border-label": + return actBgTransformBorderLabel + case "bg-transform-preview-label": + return actBgTransformPreviewLabel + case "bg-transform-input-label": + return actBgTransformInputLabel + case "bg-transform-header-label": + return actBgTransformHeaderLabel + case "bg-transform-footer-label": + return actBgTransformFooterLabel + case "bg-transform-footer": + return actBgTransformFooter + case "bg-transform-header": + return actBgTransformHeader + case "bg-transform-ghost": + return actBgTransformGhost + case "bg-transform-nth": + return actBgTransformNth + case "bg-transform-pointer": + return actBgTransformPointer + case "bg-transform-prompt": + return actBgTransformPrompt + case "bg-transform-query": + return actBgTransformQuery + case "bg-transform-search": + return actBgTransformSearch case "search": return actSearch } @@ -2857,7 +2887,13 @@ func parseOptions(index *int, opts *Options, allArgs []string) error { return err } if opts.ListBorderShape == tui.BorderLine { - return errors.New("list border cannot be 'line'") + if hasArg { + // '--list-border line' is not allowed + return errors.New("list border cannot be 'line'") + } + // This is when '--style full:line' is previously specified and + // '--list-border' is specified without an argument. + opts.ListBorderShape = tui.BorderRounded } case "--no-list-border": opts.ListBorderShape = tui.BorderNone diff --git a/src/terminal.go b/src/terminal.go index 898d34c7..85a51112 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -388,6 +388,10 @@ type Terminal struct { startChan chan fitpad killChan chan bool serverInputChan chan []*action + callbackChan chan func() + bgQueue map[action][]func() + bgSemaphore chan struct{} + bgSemaphores map[action]chan struct{} keyChan chan tui.Event eventChan chan tui.Event slab *util.Slab @@ -489,6 +493,7 @@ const ( actBackwardDeleteCharEof actBackwardWord actCancel + actChangeBorderLabel actChangeGhost actChangeHeader @@ -505,6 +510,7 @@ const ( actChangePreviewWindow actChangePrompt actChangeQuery + actClearScreen actClearQuery actClearSelection @@ -561,6 +567,7 @@ const ( actHidePreview actTogglePreview actTogglePreviewWrap + actTransform actTransformBorderLabel actTransformGhost @@ -576,6 +583,23 @@ const ( actTransformPrompt actTransformQuery actTransformSearch + + actBgTransform + actBgTransformBorderLabel + actBgTransformGhost + actBgTransformHeader + actBgTransformFooter + actBgTransformHeaderLabel + actBgTransformFooterLabel + actBgTransformInputLabel + actBgTransformListLabel + actBgTransformNth + actBgTransformPointer + actBgTransformPreviewLabel + actBgTransformPrompt + actBgTransformQuery + actBgTransformSearch + actSearch actPreview actPreviewTop @@ -613,6 +637,7 @@ const ( actBell actExclude actExcludeMulti + actAsync ) func (a actionType) Name() string { @@ -623,10 +648,34 @@ func processExecution(action actionType) bool { switch action { case actTransform, actTransformBorderLabel, + actTransformGhost, actTransformHeader, + actTransformFooter, + actTransformHeaderLabel, + actTransformFooterLabel, + actTransformInputLabel, + actTransformListLabel, + actTransformNth, + actTransformPointer, actTransformPreviewLabel, actTransformPrompt, actTransformQuery, + actTransformSearch, + actBgTransform, + actBgTransformBorderLabel, + actBgTransformGhost, + actBgTransformHeader, + actBgTransformFooter, + actBgTransformHeaderLabel, + actBgTransformFooterLabel, + actBgTransformInputLabel, + actBgTransformListLabel, + actBgTransformNth, + actBgTransformPointer, + actBgTransformPreviewLabel, + actBgTransformPrompt, + actBgTransformQuery, + actBgTransformSearch, actPreview, actChangePreview, actRefreshPreview, @@ -773,7 +822,7 @@ func mayTriggerPreview(opts *Options) bool { for _, actions := range opts.Keymap { for _, action := range actions { switch action.t { - case actPreview, actChangePreview, actTransform: + case actPreview, actChangePreview, actTransform, actBgTransform: return true } } @@ -987,6 +1036,10 @@ 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), + bgQueue: make(map[action][]func()), + 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) tui: renderer, @@ -2578,7 +2631,9 @@ func (t *Terminal) printPrompt() { before, after := t.updatePromptOffset() if len(before) == 0 && len(after) == 0 && len(t.ghost) > 0 { - w.CPrint(tui.ColGhost, t.ghost) + maxWidth := util.Max(1, w.Width()-t.promptLen-1) + runes, _ := t.trimRight([]rune(t.ghost), maxWidth) + w.CPrint(tui.ColGhost, string(runes)) return } @@ -4291,6 +4346,75 @@ func (t *Terminal) captureLines(template string) string { return t.executeCommand(template, false, true, true, false, "") } +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) + item := func() { + cmd := t.executor.ExecCommand(command, false) + cmd.Env = t.environ() + + out, _ := cmd.StdoutPipe() + reader := bufio.NewReader(out) + var output string + if err := cmd.Start(); err == nil { + if firstLineOnly { + output, _ = reader.ReadString('\n') + output = strings.TrimRight(output, "\r\n") + } else { + bytes, _ := io.ReadAll(reader) + output = string(bytes) + } + cmd.Wait() + } + removeFiles(tempFiles) + + t.callbackChan <- func() { callback(output) } + } + queue, prs := t.bgQueue[a] + if !prs { + queue = []func(){} + } + queue = append(queue, item) + t.bgQueue[a] = queue +} + +func (t *Terminal) dispatchAsync() { +Loop: + for a, queue := range t.bgQueue { + delete(t.bgQueue, a) + if len(queue) == 0 { + continue + } + + semaphore, prs := t.bgSemaphores[a] + if !prs { + semaphore = make(chan struct{}, maxBgProcessesPerAction) + t.bgSemaphores[a] = semaphore + } + for _, item := range queue { + select { + // Acquire local semaphore + case semaphore <- struct{}{}: + default: + // Failed to acquire local semaphore, putting only the last one back to the queue + t.bgQueue[a] = queue[len(queue)-1:] + continue Loop + } + todo := item + go func() { + // Acquire global semaphore + t.bgSemaphore <- struct{}{} + + todo() + // Release local semaphore + <-semaphore + // Release global semaphore + <-t.bgSemaphore + }() + } + } +} + func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, capture bool, firstLineOnly bool, info string) string { line := "" valid, list := t.buildPlusList(template, forcePlus) @@ -5089,11 +5213,27 @@ func (t *Terminal) Loop() error { barrier <- true needBarrier = false } + + // These variables are defined outside the loop to be accessible from closures + events := []util.EventType{} + changed := false + var newNth *[]Range + req := func(evts ...util.EventType) { + for _, event := range evts { + events = append(events, event) + if event == reqClose || event == reqQuit { + looping = false + } + } + } + + // The main event loop for loopIndex := int64(0); looping; loopIndex++ { var newCommand *commandSpec - var newNth *[]Range var reloadSync bool - changed := false + events = []util.EventType{} + changed = false + newNth = nil beof := false queryChanged := false denylist := []int32{} @@ -5110,6 +5250,7 @@ func (t *Terminal) Loop() error { var event tui.Event actions := []*action{} + callbacks := []func(){} select { case event = <-t.keyChan: needBarrier = true @@ -5141,6 +5282,20 @@ func (t *Terminal) Loop() error { } } } + case callback := <-t.callbackChan: + event = tui.Invalid.AsEvent() + actions = append(actions, &action{t: actAsync}) + callbacks = append(callbacks, callback) + DrainCallback: + for { + select { + case callback = <-t.callbackChan: + callbacks = append(callbacks, callback) + continue DrainCallback + default: + break DrainCallback + } + } } t.mutex.Lock() @@ -5155,15 +5310,6 @@ func (t *Terminal) Loop() error { previousInput := t.input previousCx := t.cx t.lastKey = event.KeyName() - events := []util.EventType{} - req := func(evts ...util.EventType) { - for _, event := range evts { - events = append(events, event) - if event == reqClose || event == reqQuit { - looping = false - } - } - } updatePreviewWindow := func(forcePreview bool) { t.resizeWindows(forcePreview, false) req(reqPrompt, reqList, reqInfo, reqHeader, reqFooter) @@ -5238,9 +5384,29 @@ func (t *Terminal) Loop() error { // actions to allow changing the query even when the input is hidden // e.g. fzf --no-input --bind 'space:show-input+change-query(foo)+hide-input' currentInput := t.input + capture := func(firstLineOnly bool, callback func(string)) { + if a.t >= actBgTransform { + // bg-transform-* + t.captureAsync(*a, firstLineOnly, callback) + } else if a.t >= actTransform { + // transform-* + if firstLineOnly { + callback(t.captureLine(a.a)) + } else { + callback(t.captureLines(a.a)) + } + } else { + // change-* + callback(a.a) + } + } Action: switch a.t { case actIgnore, actStart, actClick: + case actAsync: + for _, callback := range callbacks { + callback() + } case actBecome: valid, list := t.buildPlusList(a.a, false) if valid { @@ -5333,15 +5499,17 @@ func (t *Terminal) Loop() error { t.previewed.version = 0 req(reqPreviewRefresh) } - case actTransformPrompt: - prompt := t.captureLine(a.a) - t.promptString = prompt - t.prompt, t.promptLen = t.parsePrompt(prompt) - req(reqPrompt) - case actTransformQuery: - query := t.captureLine(a.a) - t.input = []rune(query) - t.cx = len(t.input) + case actTransformPrompt, actBgTransformPrompt: + capture(true, func(prompt string) { + t.promptString = prompt + t.prompt, t.promptLen = t.parsePrompt(prompt) + req(reqPrompt) + }) + case actTransformQuery, actBgTransformQuery: + capture(true, func(query string) { + t.input = []rune(query) + t.cx = len(t.input) + }) case actToggleSort: t.sort = !t.sort changed = true @@ -5399,119 +5567,102 @@ func (t *Terminal) Loop() error { } t.multi = multi req(reqList, reqInfo) - case actChangeNth, actTransformNth: - expr := a.a - if a.t == actTransformNth { - expr = t.captureLine(a.a) - } - - // Split nth expression - tokens := strings.Split(expr, "|") - if nth, err := splitNth(tokens[0]); err == nil { - // Changed - newNth = &nth - } else { - // The default - newNth = &t.nth - } - // Cycle - if len(tokens) > 1 { - a.a = strings.Join(append(tokens[1:], tokens[0]), "|") - } - if !compareRanges(t.nthCurrent, *newNth) { - changed = true - t.nthCurrent = *newNth - t.forceRerenderList() - } + case actChangeNth, actTransformNth, actBgTransformNth: + capture(true, func(expr string) { + // Split nth expression + tokens := strings.Split(expr, "|") + if nth, err := splitNth(tokens[0]); err == nil { + // Changed + newNth = &nth + } else { + // The default + newNth = &t.nth + } + // Cycle + if len(tokens) > 1 { + a.a = strings.Join(append(tokens[1:], tokens[0]), "|") + } + if !compareRanges(t.nthCurrent, *newNth) { + changed = true + t.nthCurrent = *newNth + t.forceRerenderList() + } + }) case actChangeQuery: t.input = []rune(a.a) t.cx = len(t.input) - case actChangeHeader, actTransformHeader: - header := a.a - if a.t == actTransformHeader { - header = t.captureLines(a.a) - } - if t.changeHeader(header) { - if t.headerWindow != nil { - // Need to resize header window + case actChangeHeader, actTransformHeader, actBgTransformHeader: + capture(false, func(header string) { + if t.changeHeader(header) { + if t.headerWindow != nil { + // Need to resize header window + req(reqFullRedraw) + } else { + req(reqHeader, reqList, reqPrompt, reqInfo) + } + } else { + req(reqHeader) + } + }) + case actChangeFooter, actTransformFooter, actBgTransformFooter: + capture(false, func(footer string) { + if t.changeFooter(footer) { req(reqFullRedraw) } else { - req(reqHeader, reqList, reqPrompt, reqInfo) + req(reqFooter) } - } else { - req(reqHeader) - } - case actChangeFooter, actTransformFooter: - footer := a.a - if a.t == actTransformFooter { - footer = t.captureLines(a.a) - } - if t.changeFooter(footer) { - req(reqFullRedraw) - } else { - req(reqFooter) - } - case actChangeHeaderLabel, actTransformHeaderLabel: - label := a.a - if a.t == actTransformHeaderLabel { - label = t.captureLine(a.a) - } - t.headerLabelOpts.label = label - t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(label, &tui.ColHeaderLabel, false) - req(reqRedrawHeaderLabel) - case actChangeFooterLabel, actTransformFooterLabel: - label := a.a - if a.t == actTransformFooterLabel { - label = t.captureLine(a.a) - } - t.footerLabelOpts.label = label - t.footerLabel, t.footerLabelLen = t.ansiLabelPrinter(label, &tui.ColFooterLabel, false) - req(reqRedrawFooterLabel) - case actChangeInputLabel, actTransformInputLabel: - label := a.a - if a.t == actTransformInputLabel { - label = t.captureLine(a.a) - } - t.inputLabelOpts.label = label - if t.inputBorder != nil { - t.inputLabel, t.inputLabelLen = t.ansiLabelPrinter(label, &tui.ColInputLabel, false) - req(reqRedrawInputLabel) - } - case actChangeListLabel, actTransformListLabel: - label := a.a - if a.t == actTransformListLabel { - label = t.captureLine(a.a) - } - t.listLabelOpts.label = label - if t.wborder != nil { - t.listLabel, t.listLabelLen = t.ansiLabelPrinter(label, &tui.ColListLabel, false) - req(reqRedrawListLabel) - } - case actChangeBorderLabel, actTransformBorderLabel: - label := a.a - if a.t == actTransformBorderLabel { - label = t.captureLine(a.a) - } - t.borderLabelOpts.label = label - if t.border != nil { - t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false) - req(reqRedrawBorderLabel) - } - case actChangePreviewLabel, actTransformPreviewLabel: - label := a.a - if a.t == actTransformPreviewLabel { - label = t.captureLine(a.a) - } - t.previewLabelOpts.label = label - if t.pborder != nil { - t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false) - req(reqRedrawPreviewLabel) - } - case actTransform: - body := t.captureLines(a.a) - if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil { - return doActions(actions) - } + }) + case actChangeHeaderLabel, actTransformHeaderLabel, actBgTransformHeaderLabel: + capture(true, func(label string) { + t.headerLabelOpts.label = label + t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(label, &tui.ColHeaderLabel, false) + req(reqRedrawHeaderLabel) + }) + case actChangeFooterLabel, actTransformFooterLabel, actBgTransformFooterLabel: + capture(true, func(label string) { + t.footerLabelOpts.label = label + t.footerLabel, t.footerLabelLen = t.ansiLabelPrinter(label, &tui.ColFooterLabel, false) + req(reqRedrawFooterLabel) + }) + case actChangeInputLabel, actTransformInputLabel, actBgTransformInputLabel: + capture(true, func(label string) { + t.inputLabelOpts.label = label + if t.inputBorder != nil { + t.inputLabel, t.inputLabelLen = t.ansiLabelPrinter(label, &tui.ColInputLabel, false) + req(reqRedrawInputLabel) + } + }) + case actChangeListLabel, actTransformListLabel, actBgTransformListLabel: + capture(true, func(label string) { + t.listLabelOpts.label = label + if t.wborder != nil { + t.listLabel, t.listLabelLen = t.ansiLabelPrinter(label, &tui.ColListLabel, false) + req(reqRedrawListLabel) + } + }) + case actChangeBorderLabel, actTransformBorderLabel, actBgTransformBorderLabel: + capture(true, func(label string) { + t.borderLabelOpts.label = label + if t.border != nil { + t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false) + req(reqRedrawBorderLabel) + } + }) + case actChangePreviewLabel, actTransformPreviewLabel, actBgTransformPreviewLabel: + capture(true, func(label string) { + t.previewLabelOpts.label = label + if t.pborder != nil { + t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false) + req(reqRedrawPreviewLabel) + } + }) + case actTransform, actBgTransform: + capture(false, func(body string) { + if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil { + // NOTE: We're not properly passing the return value here + doActions(actions) + } + }) case actChangePrompt: t.promptString = a.a t.prompt, t.promptLen = t.parsePrompt(a.a) @@ -5933,10 +6084,12 @@ func (t *Terminal) Loop() error { override := []rune(a.a) t.inputOverride = &override changed = true - case actTransformSearch: - override := []rune(t.captureLine(a.a)) - t.inputOverride = &override - changed = true + case actTransformSearch, actBgTransformSearch: + capture(true, func(query string) { + override := []rune(query) + t.inputOverride = &override + changed = true + }) case actEnableSearch: t.paused = false changed = true @@ -6276,30 +6429,26 @@ func (t *Terminal) Loop() error { } } } - case actChangeGhost, actTransformGhost: - ghost := a.a - if a.t == actTransformGhost { - ghost = t.captureLine(a.a) - } - t.ghost = ghost - if len(t.input) == 0 { - req(reqPrompt) - } - case actChangePointer, actTransformPointer: - pointer := a.a - if a.t == actTransformPointer { - pointer = t.captureLine(a.a) - } - length := uniseg.StringWidth(pointer) - if length <= 2 { - if length != t.pointerLen { - t.forceRerenderList() + case actChangeGhost, actTransformGhost, actBgTransformGhost: + capture(true, func(ghost string) { + t.ghost = ghost + if len(t.input) == 0 { + req(reqPrompt) } - t.pointer = pointer - t.pointerLen = length - t.pointerEmpty = strings.Repeat(" ", t.pointerLen) - req(reqList) - } + }) + case actChangePointer, actTransformPointer, actBgTransformPointer: + capture(true, func(pointer string) { + length := uniseg.StringWidth(pointer) + if length <= 2 { + if length != t.pointerLen { + t.forceRerenderList() + } + t.pointer = pointer + t.pointerLen = length + t.pointerEmpty = strings.Repeat(" ", t.pointerLen) + req(reqList) + } + }) case actChangePreview: if t.previewOpts.command != a.a { t.previewOpts.command = a.a @@ -6451,6 +6600,10 @@ func (t *Terminal) Loop() error { if reload { reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, command: newCommand, environ: t.environ(), changed: changed, denylist: denylist, revision: t.merger.Revision()} } + + // Dispatch queued background requests + t.dispatchAsync() + t.mutex.Unlock() // Must be unlocked before touching reqBox if reload { diff --git a/test/test_core.rb b/test/test_core.rb index 94ed9251..f714e1c3 100644 --- a/test/test_core.rb +++ b/test/test_core.rb @@ -1939,4 +1939,29 @@ class TestCore < TestInteractive tmux.send_keys %(echo -en "foo\n" | fzf --read0 --no-multi-line), :Enter tmux.until { |lines| assert_includes lines, '> foo␊' } end + + def test_async_transform + time = Time.now + tmux.send_keys %( + seq 100 | #{FZF} --style full --border --preview : \ + --bind 'focus:bg-transform-header(sleep 0.5; echo th.)' \ + --bind 'focus:+bg-transform-footer(sleep 0.5; echo tf.)' \ + --bind 'focus:+bg-transform-border-label(sleep 0.5; echo tbl.)' \ + --bind "focus:+bg-transform-preview-label(sleep 0.5; echo tpl.)" \ + --bind 'focus:+bg-transform-input-label(sleep 0.5; echo til.)' \ + --bind 'focus:+bg-transform-list-label(sleep 0.5; echo tll.)' \ + --bind 'focus:+bg-transform-header-label(sleep 0.5; echo thl.)' \ + --bind 'focus:+bg-transform-footer-label(sleep 0.5; echo tfl.)' \ + --bind 'focus:+bg-transform-prompt(sleep 0.5; echo tp.)' \ + --bind 'focus:+bg-transform-ghost(sleep 0.5; echo tg.)' + ).strip, :Enter + tmux.until do |lines| + assert lines.any_include?('100/100') + %w[th tf tbl tpl til tll thl tfl tp tg].each do + assert lines.any_include?("#{it}.") + end + end + elapsed = Time.now - time + assert elapsed < 2 + end end