summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md18
-rw-r--r--man/man1/fzf.111
-rw-r--r--src/actiontype_string.go94
-rw-r--r--src/constants.go4
-rw-r--r--src/options.go40
-rw-r--r--src/terminal.go465
-rw-r--r--test/test_core.rb25
7 files changed, 460 insertions, 197 deletions
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