m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-08 11:23:47 -05:00

Implement asynchronous transform actions (#4419)

Close #4418

Example:

    fzf --bind 'focus:bg-transform-header(sleep 2; date; echo {})'
This commit is contained in:
Junegunn Choi
2025-06-16 00:39:11 +09:00
committed by GitHub
parent 3b68dcdd81
commit 0c00b203e6
7 changed files with 460 additions and 197 deletions

View File

@@ -1,6 +1,24 @@
CHANGELOG 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 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 - Relaxed the `--color` option syntax to allow whitespace-separated entries (in addition to commas), making multi-line definitions easier to write and read

View File

@@ -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 \fBup\fR \fIctrl\-k ctrl\-p up\fR
\fByank\fR \fIctrl\-y\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 .SS ACTION COMPOSITION
Multiple actions can be chained using \fB+\fR separator. Multiple actions can be chained using \fB+\fR separator.
@@ -1929,6 +1932,14 @@ e.g.
echo "change\-header:Invalid selection"' echo "change\-header:Invalid selection"'
\fR \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 .SS PREVIEW BINDING
With \fBpreview(...)\fR action, you can specify multiple different preview With \fBpreview(...)\fR action, you can specify multiple different preview

View File

@@ -113,48 +113,64 @@ func _() {
_ = x[actTransformPrompt-102] _ = x[actTransformPrompt-102]
_ = x[actTransformQuery-103] _ = x[actTransformQuery-103]
_ = x[actTransformSearch-104] _ = x[actTransformSearch-104]
_ = x[actSearch-105] _ = x[actBgTransform-105]
_ = x[actPreview-106] _ = x[actBgTransformBorderLabel-106]
_ = x[actPreviewTop-107] _ = x[actBgTransformGhost-107]
_ = x[actPreviewBottom-108] _ = x[actBgTransformHeader-108]
_ = x[actPreviewUp-109] _ = x[actBgTransformFooter-109]
_ = x[actPreviewDown-110] _ = x[actBgTransformHeaderLabel-110]
_ = x[actPreviewPageUp-111] _ = x[actBgTransformFooterLabel-111]
_ = x[actPreviewPageDown-112] _ = x[actBgTransformInputLabel-112]
_ = x[actPreviewHalfPageUp-113] _ = x[actBgTransformListLabel-113]
_ = x[actPreviewHalfPageDown-114] _ = x[actBgTransformNth-114]
_ = x[actPrevHistory-115] _ = x[actBgTransformPointer-115]
_ = x[actPrevSelected-116] _ = x[actBgTransformPreviewLabel-116]
_ = x[actPrint-117] _ = x[actBgTransformPrompt-117]
_ = x[actPut-118] _ = x[actBgTransformQuery-118]
_ = x[actNextHistory-119] _ = x[actBgTransformSearch-119]
_ = x[actNextSelected-120] _ = x[actSearch-120]
_ = x[actExecute-121] _ = x[actPreview-121]
_ = x[actExecuteSilent-122] _ = x[actPreviewTop-122]
_ = x[actExecuteMulti-123] _ = x[actPreviewBottom-123]
_ = x[actSigStop-124] _ = x[actPreviewUp-124]
_ = x[actFirst-125] _ = x[actPreviewDown-125]
_ = x[actLast-126] _ = x[actPreviewPageUp-126]
_ = x[actReload-127] _ = x[actPreviewPageDown-127]
_ = x[actReloadSync-128] _ = x[actPreviewHalfPageUp-128]
_ = x[actDisableSearch-129] _ = x[actPreviewHalfPageDown-129]
_ = x[actEnableSearch-130] _ = x[actPrevHistory-130]
_ = x[actSelect-131] _ = x[actPrevSelected-131]
_ = x[actDeselect-132] _ = x[actPrint-132]
_ = x[actUnbind-133] _ = x[actPut-133]
_ = x[actRebind-134] _ = x[actNextHistory-134]
_ = x[actToggleBind-135] _ = x[actNextSelected-135]
_ = x[actBecome-136] _ = x[actExecute-136]
_ = x[actShowHeader-137] _ = x[actExecuteSilent-137]
_ = x[actHideHeader-138] _ = x[actExecuteMulti-138]
_ = x[actBell-139] _ = x[actSigStop-139]
_ = x[actExclude-140] _ = x[actFirst-140]
_ = x[actExcludeMulti-141] _ = 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 { func (i actionType) String() string {
if i < 0 || i >= actionType(len(_actionType_index)-1) { if i < 0 || i >= actionType(len(_actionType_index)-1) {

View File

@@ -29,6 +29,10 @@ const (
maxPatternLength = 1000 maxPatternLength = 1000
maxMulti = math.MaxInt32 maxMulti = math.MaxInt32
// Background processes
maxBgProcesses = 30
maxBgProcessesPerAction = 3
// Matcher // Matcher
numPartitionsMultiplier = 8 numPartitionsMultiplier = 8
maxPartitions = 32 maxPartitions = 32

View File

@@ -1435,7 +1435,7 @@ const (
func init() { func init() {
executeRegexp = regexp.MustCompile( 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("[,:]+") splitRegexp = regexp.MustCompile("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
} }
@@ -1892,6 +1892,36 @@ func isExecuteAction(str string) actionType {
return actTransformQuery return actTransformQuery
case "transform-search": case "transform-search":
return actTransformSearch 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": case "search":
return actSearch return actSearch
} }
@@ -2857,7 +2887,13 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
return err return err
} }
if opts.ListBorderShape == tui.BorderLine { 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": case "--no-list-border":
opts.ListBorderShape = tui.BorderNone opts.ListBorderShape = tui.BorderNone

View File

@@ -388,6 +388,10 @@ type Terminal struct {
startChan chan fitpad startChan chan fitpad
killChan chan bool killChan chan bool
serverInputChan chan []*action serverInputChan chan []*action
callbackChan chan func()
bgQueue map[action][]func()
bgSemaphore chan struct{}
bgSemaphores map[action]chan struct{}
keyChan chan tui.Event keyChan chan tui.Event
eventChan chan tui.Event eventChan chan tui.Event
slab *util.Slab slab *util.Slab
@@ -489,6 +493,7 @@ const (
actBackwardDeleteCharEof actBackwardDeleteCharEof
actBackwardWord actBackwardWord
actCancel actCancel
actChangeBorderLabel actChangeBorderLabel
actChangeGhost actChangeGhost
actChangeHeader actChangeHeader
@@ -505,6 +510,7 @@ const (
actChangePreviewWindow actChangePreviewWindow
actChangePrompt actChangePrompt
actChangeQuery actChangeQuery
actClearScreen actClearScreen
actClearQuery actClearQuery
actClearSelection actClearSelection
@@ -561,6 +567,7 @@ const (
actHidePreview actHidePreview
actTogglePreview actTogglePreview
actTogglePreviewWrap actTogglePreviewWrap
actTransform actTransform
actTransformBorderLabel actTransformBorderLabel
actTransformGhost actTransformGhost
@@ -576,6 +583,23 @@ const (
actTransformPrompt actTransformPrompt
actTransformQuery actTransformQuery
actTransformSearch actTransformSearch
actBgTransform
actBgTransformBorderLabel
actBgTransformGhost
actBgTransformHeader
actBgTransformFooter
actBgTransformHeaderLabel
actBgTransformFooterLabel
actBgTransformInputLabel
actBgTransformListLabel
actBgTransformNth
actBgTransformPointer
actBgTransformPreviewLabel
actBgTransformPrompt
actBgTransformQuery
actBgTransformSearch
actSearch actSearch
actPreview actPreview
actPreviewTop actPreviewTop
@@ -613,6 +637,7 @@ const (
actBell actBell
actExclude actExclude
actExcludeMulti actExcludeMulti
actAsync
) )
func (a actionType) Name() string { func (a actionType) Name() string {
@@ -623,10 +648,34 @@ func processExecution(action actionType) bool {
switch action { switch action {
case actTransform, case actTransform,
actTransformBorderLabel, actTransformBorderLabel,
actTransformGhost,
actTransformHeader, actTransformHeader,
actTransformFooter,
actTransformHeaderLabel,
actTransformFooterLabel,
actTransformInputLabel,
actTransformListLabel,
actTransformNth,
actTransformPointer,
actTransformPreviewLabel, actTransformPreviewLabel,
actTransformPrompt, actTransformPrompt,
actTransformQuery, actTransformQuery,
actTransformSearch,
actBgTransform,
actBgTransformBorderLabel,
actBgTransformGhost,
actBgTransformHeader,
actBgTransformFooter,
actBgTransformHeaderLabel,
actBgTransformFooterLabel,
actBgTransformInputLabel,
actBgTransformListLabel,
actBgTransformNth,
actBgTransformPointer,
actBgTransformPreviewLabel,
actBgTransformPrompt,
actBgTransformQuery,
actBgTransformSearch,
actPreview, actPreview,
actChangePreview, actChangePreview,
actRefreshPreview, actRefreshPreview,
@@ -773,7 +822,7 @@ func mayTriggerPreview(opts *Options) bool {
for _, actions := range opts.Keymap { for _, actions := range opts.Keymap {
for _, action := range actions { for _, action := range actions {
switch action.t { switch action.t {
case actPreview, actChangePreview, actTransform: case actPreview, actChangePreview, actTransform, actBgTransform:
return true return true
} }
} }
@@ -987,6 +1036,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
startChan: make(chan fitpad, 1), startChan: make(chan fitpad, 1),
killChan: make(chan bool), killChan: make(chan bool),
serverInputChan: make(chan []*action, 100), 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), keyChan: make(chan tui.Event),
eventChan: make(chan tui.Event, 6), // start | (load + result + zero|one) | (focus) | (resize) eventChan: make(chan tui.Event, 6), // start | (load + result + zero|one) | (focus) | (resize)
tui: renderer, tui: renderer,
@@ -2578,7 +2631,9 @@ func (t *Terminal) printPrompt() {
before, after := t.updatePromptOffset() before, after := t.updatePromptOffset()
if len(before) == 0 && len(after) == 0 && len(t.ghost) > 0 { 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 return
} }
@@ -4291,6 +4346,75 @@ func (t *Terminal) captureLines(template string) string {
return t.executeCommand(template, false, true, true, false, "") 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 { func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, capture bool, firstLineOnly bool, info string) string {
line := "" line := ""
valid, list := t.buildPlusList(template, forcePlus) valid, list := t.buildPlusList(template, forcePlus)
@@ -5089,11 +5213,27 @@ func (t *Terminal) Loop() error {
barrier <- true barrier <- true
needBarrier = false 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++ { for loopIndex := int64(0); looping; loopIndex++ {
var newCommand *commandSpec var newCommand *commandSpec
var newNth *[]Range
var reloadSync bool var reloadSync bool
changed := false events = []util.EventType{}
changed = false
newNth = nil
beof := false beof := false
queryChanged := false queryChanged := false
denylist := []int32{} denylist := []int32{}
@@ -5110,6 +5250,7 @@ func (t *Terminal) Loop() error {
var event tui.Event var event tui.Event
actions := []*action{} actions := []*action{}
callbacks := []func(){}
select { select {
case event = <-t.keyChan: case event = <-t.keyChan:
needBarrier = true 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() t.mutex.Lock()
@@ -5155,15 +5310,6 @@ func (t *Terminal) Loop() error {
previousInput := t.input previousInput := t.input
previousCx := t.cx previousCx := t.cx
t.lastKey = event.KeyName() 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) { updatePreviewWindow := func(forcePreview bool) {
t.resizeWindows(forcePreview, false) t.resizeWindows(forcePreview, false)
req(reqPrompt, reqList, reqInfo, reqHeader, reqFooter) 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 // 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' // e.g. fzf --no-input --bind 'space:show-input+change-query(foo)+hide-input'
currentInput := t.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: Action:
switch a.t { switch a.t {
case actIgnore, actStart, actClick: case actIgnore, actStart, actClick:
case actAsync:
for _, callback := range callbacks {
callback()
}
case actBecome: case actBecome:
valid, list := t.buildPlusList(a.a, false) valid, list := t.buildPlusList(a.a, false)
if valid { if valid {
@@ -5333,15 +5499,17 @@ func (t *Terminal) Loop() error {
t.previewed.version = 0 t.previewed.version = 0
req(reqPreviewRefresh) req(reqPreviewRefresh)
} }
case actTransformPrompt: case actTransformPrompt, actBgTransformPrompt:
prompt := t.captureLine(a.a) capture(true, func(prompt string) {
t.promptString = prompt t.promptString = prompt
t.prompt, t.promptLen = t.parsePrompt(prompt) t.prompt, t.promptLen = t.parsePrompt(prompt)
req(reqPrompt) req(reqPrompt)
case actTransformQuery: })
query := t.captureLine(a.a) case actTransformQuery, actBgTransformQuery:
t.input = []rune(query) capture(true, func(query string) {
t.cx = len(t.input) t.input = []rune(query)
t.cx = len(t.input)
})
case actToggleSort: case actToggleSort:
t.sort = !t.sort t.sort = !t.sort
changed = true changed = true
@@ -5399,119 +5567,102 @@ func (t *Terminal) Loop() error {
} }
t.multi = multi t.multi = multi
req(reqList, reqInfo) req(reqList, reqInfo)
case actChangeNth, actTransformNth: case actChangeNth, actTransformNth, actBgTransformNth:
expr := a.a capture(true, func(expr string) {
if a.t == actTransformNth { // Split nth expression
expr = t.captureLine(a.a) tokens := strings.Split(expr, "|")
} if nth, err := splitNth(tokens[0]); err == nil {
// Changed
// Split nth expression newNth = &nth
tokens := strings.Split(expr, "|") } else {
if nth, err := splitNth(tokens[0]); err == nil { // The default
// Changed newNth = &t.nth
newNth = &nth }
} else { // Cycle
// The default if len(tokens) > 1 {
newNth = &t.nth a.a = strings.Join(append(tokens[1:], tokens[0]), "|")
} }
// Cycle if !compareRanges(t.nthCurrent, *newNth) {
if len(tokens) > 1 { changed = true
a.a = strings.Join(append(tokens[1:], tokens[0]), "|") t.nthCurrent = *newNth
} t.forceRerenderList()
if !compareRanges(t.nthCurrent, *newNth) { }
changed = true })
t.nthCurrent = *newNth
t.forceRerenderList()
}
case actChangeQuery: case actChangeQuery:
t.input = []rune(a.a) t.input = []rune(a.a)
t.cx = len(t.input) t.cx = len(t.input)
case actChangeHeader, actTransformHeader: case actChangeHeader, actTransformHeader, actBgTransformHeader:
header := a.a capture(false, func(header string) {
if a.t == actTransformHeader { if t.changeHeader(header) {
header = t.captureLines(a.a) if t.headerWindow != nil {
} // Need to resize header window
if t.changeHeader(header) { req(reqFullRedraw)
if t.headerWindow != nil { } else {
// Need to resize header window req(reqHeader, reqList, reqPrompt, reqInfo)
}
} else {
req(reqHeader)
}
})
case actChangeFooter, actTransformFooter, actBgTransformFooter:
capture(false, func(footer string) {
if t.changeFooter(footer) {
req(reqFullRedraw) req(reqFullRedraw)
} else { } else {
req(reqHeader, reqList, reqPrompt, reqInfo) req(reqFooter)
} }
} else { })
req(reqHeader) case actChangeHeaderLabel, actTransformHeaderLabel, actBgTransformHeaderLabel:
} capture(true, func(label string) {
case actChangeFooter, actTransformFooter: t.headerLabelOpts.label = label
footer := a.a t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(label, &tui.ColHeaderLabel, false)
if a.t == actTransformFooter { req(reqRedrawHeaderLabel)
footer = t.captureLines(a.a) })
} case actChangeFooterLabel, actTransformFooterLabel, actBgTransformFooterLabel:
if t.changeFooter(footer) { capture(true, func(label string) {
req(reqFullRedraw) t.footerLabelOpts.label = label
} else { t.footerLabel, t.footerLabelLen = t.ansiLabelPrinter(label, &tui.ColFooterLabel, false)
req(reqFooter) req(reqRedrawFooterLabel)
} })
case actChangeHeaderLabel, actTransformHeaderLabel: case actChangeInputLabel, actTransformInputLabel, actBgTransformInputLabel:
label := a.a capture(true, func(label string) {
if a.t == actTransformHeaderLabel { t.inputLabelOpts.label = label
label = t.captureLine(a.a) if t.inputBorder != nil {
} t.inputLabel, t.inputLabelLen = t.ansiLabelPrinter(label, &tui.ColInputLabel, false)
t.headerLabelOpts.label = label req(reqRedrawInputLabel)
t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(label, &tui.ColHeaderLabel, false) }
req(reqRedrawHeaderLabel) })
case actChangeFooterLabel, actTransformFooterLabel: case actChangeListLabel, actTransformListLabel, actBgTransformListLabel:
label := a.a capture(true, func(label string) {
if a.t == actTransformFooterLabel { t.listLabelOpts.label = label
label = t.captureLine(a.a) if t.wborder != nil {
} t.listLabel, t.listLabelLen = t.ansiLabelPrinter(label, &tui.ColListLabel, false)
t.footerLabelOpts.label = label req(reqRedrawListLabel)
t.footerLabel, t.footerLabelLen = t.ansiLabelPrinter(label, &tui.ColFooterLabel, false) }
req(reqRedrawFooterLabel) })
case actChangeInputLabel, actTransformInputLabel: case actChangeBorderLabel, actTransformBorderLabel, actBgTransformBorderLabel:
label := a.a capture(true, func(label string) {
if a.t == actTransformInputLabel { t.borderLabelOpts.label = label
label = t.captureLine(a.a) if t.border != nil {
} t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false)
t.inputLabelOpts.label = label req(reqRedrawBorderLabel)
if t.inputBorder != nil { }
t.inputLabel, t.inputLabelLen = t.ansiLabelPrinter(label, &tui.ColInputLabel, false) })
req(reqRedrawInputLabel) case actChangePreviewLabel, actTransformPreviewLabel, actBgTransformPreviewLabel:
} capture(true, func(label string) {
case actChangeListLabel, actTransformListLabel: t.previewLabelOpts.label = label
label := a.a if t.pborder != nil {
if a.t == actTransformListLabel { t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false)
label = t.captureLine(a.a) req(reqRedrawPreviewLabel)
} }
t.listLabelOpts.label = label })
if t.wborder != nil { case actTransform, actBgTransform:
t.listLabel, t.listLabelLen = t.ansiLabelPrinter(label, &tui.ColListLabel, false) capture(false, func(body string) {
req(reqRedrawListLabel) if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil {
} // NOTE: We're not properly passing the return value here
case actChangeBorderLabel, actTransformBorderLabel: doActions(actions)
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 actChangePrompt: case actChangePrompt:
t.promptString = a.a t.promptString = a.a
t.prompt, t.promptLen = t.parsePrompt(a.a) t.prompt, t.promptLen = t.parsePrompt(a.a)
@@ -5933,10 +6084,12 @@ func (t *Terminal) Loop() error {
override := []rune(a.a) override := []rune(a.a)
t.inputOverride = &override t.inputOverride = &override
changed = true changed = true
case actTransformSearch: case actTransformSearch, actBgTransformSearch:
override := []rune(t.captureLine(a.a)) capture(true, func(query string) {
t.inputOverride = &override override := []rune(query)
changed = true t.inputOverride = &override
changed = true
})
case actEnableSearch: case actEnableSearch:
t.paused = false t.paused = false
changed = true changed = true
@@ -6276,30 +6429,26 @@ func (t *Terminal) Loop() error {
} }
} }
} }
case actChangeGhost, actTransformGhost: case actChangeGhost, actTransformGhost, actBgTransformGhost:
ghost := a.a capture(true, func(ghost string) {
if a.t == actTransformGhost { t.ghost = ghost
ghost = t.captureLine(a.a) if len(t.input) == 0 {
} req(reqPrompt)
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()
} }
t.pointer = pointer })
t.pointerLen = length case actChangePointer, actTransformPointer, actBgTransformPointer:
t.pointerEmpty = strings.Repeat(" ", t.pointerLen) capture(true, func(pointer string) {
req(reqList) 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: case actChangePreview:
if t.previewOpts.command != a.a { if t.previewOpts.command != a.a {
t.previewOpts.command = a.a t.previewOpts.command = a.a
@@ -6451,6 +6600,10 @@ func (t *Terminal) Loop() error {
if reload { if reload {
reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, command: newCommand, environ: t.environ(), changed: changed, denylist: denylist, revision: t.merger.Revision()} 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 t.mutex.Unlock() // Must be unlocked before touching reqBox
if reload { if reload {

View File

@@ -1939,4 +1939,29 @@ class TestCore < TestInteractive
tmux.send_keys %(echo -en "foo\n" | fzf --read0 --no-multi-line), :Enter tmux.send_keys %(echo -en "foo\n" | fzf --read0 --no-multi-line), :Enter
tmux.until { |lines| assert_includes lines, '> foo␊' } tmux.until { |lines| assert_includes lines, '> foo␊' }
end 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 end