summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2023-12-25 23:43:46 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2023-12-26 00:14:05 +0900
commit1707b8cdba42492a88b3fc43c15c20d219c5ce2b (patch)
tree268e6950fecc4bec86b38389101e52901134b3a5
parent41d4d70b985f665c8ecc66b83aa10209c8dfbbfd (diff)
Add 'transform' action to conditionally perform a series of actions
'transform' action runs an external command that prints a series of actions to perform. # Disallow selecting an empty line echo -e "1. Hello\n2. Goodbye\n\n3. Exit" | fzf --reverse --header 'Select one' \ --bind 'enter:transform:[[ -n {} ]] && echo accept || echo "change-header:Invalid selection"' # Move cursor past the empty line echo -e "1. Hello\n2. Goodbye\n\n3. Exit" | fzf --reverse --header 'Select one' \ --bind 'enter:transform:[[ -n {} ]] && echo accept || echo "change-header:Invalid selection"' \ --bind 'focus:transform:[[ -n {} ]] && exit; [[ {fzf:action} =~ up$ ]] && echo up || echo down' Close #3368 Close #2980
-rw-r--r--CHANGELOG.md16
-rw-r--r--Makefile5
-rw-r--r--go.mod4
-rw-r--r--go.sum6
-rw-r--r--man/man1/fzf.113
-rw-r--r--src/actiontype_string.go127
-rw-r--r--src/options.go10
-rw-r--r--src/terminal.go62
-rw-r--r--src/terminal_test.go57
-rwxr-xr-xtest/test_go.rb7
10 files changed, 255 insertions, 52 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fec32d97..bb986dee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,22 @@ CHANGELOG
0.45.0
------
+- Added `transform` action to conditionally perform a series of actions
+ ```sh
+ # Disallow selecting an empty line
+ echo -e "1. Hello\n2. Goodbye\n\n3. Exit" |
+ fzf --reverse --header 'Select one' \
+ --bind 'enter:transform:[[ -n {} ]] && echo accept || echo "change-header:Invalid selection"'
+
+ # Move cursor past the empty line
+ echo -e "1. Hello\n2. Goodbye\n\n3. Exit" |
+ fzf --reverse --header 'Select one' \
+ --bind 'enter:transform:[[ -n {} ]] && echo accept || echo "change-header:Invalid selection"' \
+ --bind 'focus:transform:[[ -n {} ]] && exit; [[ {fzf:action} =~ up$ ]] && echo up || echo down'
+ ```
+- Added placeholder expressions
+ - `{fzf:action}` - the name of the last action performed
+ - `{fzf:query}` - synonym for `{q}`
- Added support for negative height
```sh
# Terminal height minus 1, so you can still see the command line
diff --git a/Makefile b/Makefile
index a55c6bdb..32164a6e 100644
--- a/Makefile
+++ b/Makefile
@@ -89,6 +89,9 @@ bench:
install: bin/fzf
+generate:
+ PATH=$(PATH):$(GOPATH)/bin $(GO) generate ./...
+
build:
goreleaser build --rm-dist --snapshot --skip-post-hooks
@@ -181,4 +184,4 @@ update:
$(GO) get -u
$(GO) mod tidy
-.PHONY: all build release test bench install clean docker docker-test update
+.PHONY: all generate build release test bench install clean docker docker-test update
diff --git a/go.mod b/go.mod
index 31f9b0b0..ca3ebb98 100644
--- a/go.mod
+++ b/go.mod
@@ -14,8 +14,10 @@ require (
require (
github.com/gdamore/encoding v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
- golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
+ golang.org/x/mod v0.14.0 // indirect
+ golang.org/x/sync v0.5.0 // indirect
golang.org/x/text v0.5.0 // indirect
+ golang.org/x/tools v0.16.1 // indirect
)
go 1.17
diff --git a/go.sum b/go.sum
index 3270c091..d9bb109f 100644
--- a/go.sum
+++ b/go.sum
@@ -19,6 +19,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
+golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -26,6 +28,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
+golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -46,4 +50,6 @@ golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
+golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index c4fe788c..20e7e17d 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -574,10 +574,6 @@ e.g.
When using a field index expression, leading and trailing whitespace is stripped
from the replacement string. To preserve the whitespace, use the \fBs\fR flag.
-Also, \fB{q}\fR is replaced to the current query string, and \fB{n}\fR is
-replaced to zero-based ordinal index of the line. Use \fB{+n}\fR if you want
-all index numbers when multiple lines are selected.
-
A placeholder expression with \fBf\fR flag is replaced to the path of
a temporary file that holds the evaluated list. This is useful when you
multi-select a large number of items and the length of the evaluated string may
@@ -589,6 +585,15 @@ e.g.
seq 100000 | fzf --multi --bind ctrl-a:select-all \\
--preview "awk '{sum+=\\$1} END {print sum}' {+f}"\fR
+Also,
+
+* \fB{q}\fR (or \fB{fzf:query}\fR) is replaced to the current query string
+.br
+* \fB{n}\fR is replaced to the zero-based ordinal index of the current item.
+ Use \fB{+n}\fR if you want all index numbers when multiple lines are selected.
+.br
+* \fB{fzf:action}\fR is replaced to to the name of the last action performed
+
Note that you can escape a placeholder pattern by prepending a backslash.
Preview window will be updated even when there is no match for the current
diff --git a/src/actiontype_string.go b/src/actiontype_string.go
new file mode 100644
index 00000000..90fa909a
--- /dev/null
+++ b/src/actiontype_string.go
@@ -0,0 +1,127 @@
+// Code generated by "stringer -type=actionType"; DO NOT EDIT.
+
+package fzf
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[actIgnore-0]
+ _ = x[actStart-1]
+ _ = x[actClick-2]
+ _ = x[actInvalid-3]
+ _ = x[actChar-4]
+ _ = x[actMouse-5]
+ _ = x[actBeginningOfLine-6]
+ _ = x[actAbort-7]
+ _ = x[actAccept-8]
+ _ = x[actAcceptNonEmpty-9]
+ _ = x[actAcceptOrPrintQuery-10]
+ _ = x[actBackwardChar-11]
+ _ = x[actBackwardDeleteChar-12]
+ _ = x[actBackwardDeleteCharEof-13]
+ _ = x[actBackwardWord-14]
+ _ = x[actCancel-15]
+ _ = x[actChangeBorderLabel-16]
+ _ = x[actChangeHeader-17]
+ _ = x[actChangePreviewLabel-18]
+ _ = x[actChangePrompt-19]
+ _ = x[actChangeQuery-20]
+ _ = x[actClearScreen-21]
+ _ = x[actClearQuery-22]
+ _ = x[actClearSelection-23]
+ _ = x[actClose-24]
+ _ = x[actDeleteChar-25]
+ _ = x[actDeleteCharEof-26]
+ _ = x[actEndOfLine-27]
+ _ = x[actForwardChar-28]
+ _ = x[actForwardWord-29]
+ _ = x[actKillLine-30]
+ _ = x[actKillWord-31]
+ _ = x[actUnixLineDiscard-32]
+ _ = x[actUnixWordRubout-33]
+ _ = x[actYank-34]
+ _ = x[actBackwardKillWord-35]
+ _ = x[actSelectAll-36]
+ _ = x[actDeselectAll-37]
+ _ = x[actToggle-38]
+ _ = x[actToggleSearch-39]
+ _ = x[actToggleAll-40]
+ _ = x[actToggleDown-41]
+ _ = x[actToggleUp-42]
+ _ = x[actToggleIn-43]
+ _ = x[actToggleOut-44]
+ _ = x[actToggleTrack-45]
+ _ = x[actToggleHeader-46]
+ _ = x[actTrack-47]
+ _ = x[actDown-48]
+ _ = x[actUp-49]
+ _ = x[actPageUp-50]
+ _ = x[actPageDown-51]
+ _ = x[actPosition-52]
+ _ = x[actHalfPageUp-53]
+ _ = x[actHalfPageDown-54]
+ _ = x[actOffsetUp-55]
+ _ = x[actOffsetDown-56]
+ _ = x[actJump-57]
+ _ = x[actJumpAccept-58]
+ _ = x[actPrintQuery-59]
+ _ = x[actRefreshPreview-60]
+ _ = x[actReplaceQuery-61]
+ _ = x[actToggleSort-62]
+ _ = x[actShowPreview-63]
+ _ = x[actHidePreview-64]
+ _ = x[actTogglePreview-65]
+ _ = x[actTogglePreviewWrap-66]
+ _ = x[actTransform-67]
+ _ = x[actTransformBorderLabel-68]
+ _ = x[actTransformHeader-69]
+ _ = x[actTransformPreviewLabel-70]
+ _ = x[actTransformPrompt-71]
+ _ = x[actTransformQuery-72]
+ _ = x[actPreview-73]
+ _ = x[actChangePreview-74]
+ _ = x[actChangePreviewWindow-75]
+ _ = x[actPreviewTop-76]
+ _ = x[actPreviewBottom-77]
+ _ = x[actPreviewUp-78]
+ _ = x[actPreviewDown-79]
+ _ = x[actPreviewPageUp-80]
+ _ = x[actPreviewPageDown-81]
+ _ = x[actPreviewHalfPageUp-82]
+ _ = x[actPreviewHalfPageDown-83]
+ _ = x[actPrevHistory-84]
+ _ = x[actPrevSelected-85]
+ _ = x[actPut-86]
+ _ = x[actNextHistory-87]
+ _ = x[actNextSelected-88]
+ _ = x[actExecute-89]
+ _ = x[actExecuteSilent-90]
+ _ = x[actExecuteMulti-91]
+ _ = x[actSigStop-92]
+ _ = x[actFirst-93]
+ _ = x[actLast-94]
+ _ = x[actReload-95]
+ _ = x[actReloadSync-96]
+ _ = x[actDisableSearch-97]
+ _ = x[actEnableSearch-98]
+ _ = x[actSelect-99]
+ _ = x[actDeselect-100]
+ _ = x[actUnbind-101]
+ _ = x[actRebind-102]
+ _ = x[actBecome-103]
+ _ = x[actResponse-104]
+}
+
+const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleHeaderactTrackactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponse"
+
+var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 263, 278, 292, 306, 319, 336, 344, 357, 373, 385, 399, 413, 424, 435, 453, 470, 477, 496, 508, 522, 531, 546, 558, 571, 582, 593, 605, 619, 634, 642, 649, 654, 663, 674, 685, 698, 713, 724, 737, 744, 757, 770, 787, 802, 815, 829, 843, 859, 879, 891, 914, 932, 956, 974, 991, 1001, 1017, 1039, 1052, 1068, 1080, 1094, 1110, 1128, 1148, 1170, 1184, 1199, 1205, 1219, 1234, 1244, 1260, 1275, 1285, 1293, 1300, 1309, 1322, 1338, 1353, 1362, 1373, 1382, 1391, 1400, 1411}
+
+func (i actionType) String() string {
+ if i < 0 || i >= actionType(len(_actionType_index)-1) {
+ return "actionType(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _actionType_name[_actionType_index[i]:_actionType_index[i+1]]
+}
diff --git a/src/options.go b/src/options.go
index ca8b2476..0822817a 100644
--- a/src/options.go
+++ b/src/options.go
@@ -979,7 +979,7 @@ const (
func init() {
executeRegexp = regexp.MustCompile(
- `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`)
+ `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|transform|change-preview-window|change-preview|(?:re|un)bind|pos|put)`)
splitRegexp = regexp.MustCompile("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
}
@@ -1086,7 +1086,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA
case "backward-delete-char":
appendAction(actBackwardDeleteChar)
case "backward-delete-char/eof":
- appendAction(actBackwardDeleteCharEOF)
+ appendAction(actBackwardDeleteCharEof)
case "backward-word":
appendAction(actBackwardWord)
case "clear-screen":
@@ -1094,7 +1094,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA
case "delete-char":
appendAction(actDeleteChar)
case "delete-char/eof":
- appendAction(actDeleteCharEOF)
+ appendAction(actDeleteCharEof)
case "deselect":
appendAction(actDeselect)
case "end-of-line":
@@ -1213,7 +1213,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actDisableSearch)
case "put":
if putAllowed {
- appendAction(actRune)
+ appendAction(actChar)
} else {
exit("unable to put non-printable character")
}
@@ -1333,6 +1333,8 @@ func isExecuteAction(str string) actionType {
return actExecuteMulti
case "put":
return actPut
+ case "transform":
+ return actTransform
case "transform-border-label":
return actTransformBorderLabel
case "transform-preview-label":
diff --git a/src/terminal.go b/src/terminal.go
index 394be4fc..31339d3e 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -52,11 +52,12 @@ var offsetComponentRegex *regexp.Regexp
var offsetTrimCharsRegex *regexp.Regexp
var activeTempFiles []string
var passThroughRegex *regexp.Regexp
+var actionTypeRegex *regexp.Regexp
const clearCode string = "\x1b[2J"
func init() {
- placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\+?f?nf?})`)
+ placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{fzf:(?:query|action)}|{\+?f?nf?})`)
whiteSuffix = regexp.MustCompile(`\s*$`)
offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
@@ -285,6 +286,7 @@ type Terminal struct {
tui tui.Renderer
executing *util.AtomicBool
termSize tui.TermSize
+ lastAction actionType
}
type selectedItem struct {
@@ -332,12 +334,15 @@ type action struct {
a string
}
+//go:generate stringer -type=actionType
type actionType int
const (
actIgnore actionType = iota
+ actStart
+ actClick
actInvalid
- actRune
+ actChar
actMouse
actBeginningOfLine
actAbort
@@ -346,7 +351,7 @@ const (
actAcceptOrPrintQuery
actBackwardChar
actBackwardDeleteChar
- actBackwardDeleteCharEOF
+ actBackwardDeleteCharEof
actBackwardWord
actCancel
actChangeBorderLabel
@@ -359,7 +364,7 @@ const (
actClearSelection
actClose
actDeleteChar
- actDeleteCharEOF
+ actDeleteCharEof
actEndOfLine
actForwardChar
actForwardWord
@@ -400,6 +405,7 @@ const (
actHidePreview
actTogglePreview
actTogglePreviewWrap
+ actTransform
actTransformBorderLabel
actTransformHeader
actTransformPreviewLabel
@@ -441,13 +447,15 @@ const (
func processExecution(action actionType) bool {
switch action {
- case actTransformBorderLabel,
+ case actTransform,
+ actTransformBorderLabel,
actTransformHeader,
actTransformPreviewLabel,
actTransformPrompt,
actTransformQuery,
actPreview,
actChangePreview,
+ actRefreshPreview,
actExecute,
actExecuteSilent,
actExecuteMulti,
@@ -514,7 +522,7 @@ func defaultKeymap() map[tui.Event][]*action {
add(tui.CtrlG, actAbort)
add(tui.CtrlQ, actAbort)
add(tui.ESC, actAbort)
- add(tui.CtrlD, actDeleteCharEOF)
+ add(tui.CtrlD, actDeleteCharEof)
add(tui.CtrlE, actEndOfLine)
add(tui.CtrlF, actForwardChar)
add(tui.CtrlH, actBackwardDeleteChar)
@@ -556,7 +564,7 @@ func defaultKeymap() map[tui.Event][]*action {
add(tui.SDown, actPreviewDown)
add(tui.Mouse, actMouse)
- add(tui.LeftClick, actIgnore)
+ add(tui.LeftClick, actClick)
add(tui.RightClick, actToggle)
add(tui.SLeftClick, actToggle)
add(tui.SRightClick, actToggle)
@@ -740,7 +748,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
eventChan: make(chan tui.Event, 3), // load / zero|one | GetChar
tui: renderer,
initFunc: func() { renderer.Init() },
- executing: util.NewAtomicBool(false)}
+ executing: util.NewAtomicBool(false),
+ lastAction: actStart}
t.prompt, t.promptLen = t.parsePrompt(opts.Prompt)
t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0)
t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0)
@@ -2344,6 +2353,10 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
return true, match[1:], flags
}
+ if strings.HasPrefix(match, "{fzf:") {
+ return false, match, flags
+ }
+
skipChars := 1
for _, char := range match[1:] {
switch char {
@@ -2408,7 +2421,7 @@ func cleanTemporaryFiles() {
func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) string {
return replacePlaceholder(
- template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list)
+ template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list, t.lastAction)
}
func (t *Terminal) evaluateScrollOffset() int {
@@ -2446,7 +2459,7 @@ func (t *Terminal) evaluateScrollOffset() int {
return util.Max(0, base)
}
-func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
+func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item, lastAction actionType) string {
current := allItems[:1]
selected := allItems[1:]
if current[0] == nil {
@@ -2467,7 +2480,16 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
switch {
case escaped:
return match
- case match == "{q}":
+ case match == "{fzf:action}":
+ name := ""
+ for i, r := range lastAction.String()[3:] {
+ if i > 0 && r >= 'A' && r <= 'Z' {
+ name += "-"
+ }
+ name += string(r)
+ }
+ return strings.ToLower(name)
+ case match == "{q}" || match == "{fzf:query}":
return quoteEntry(query)
case match == "{}":
replace = func(item *Item) string {
@@ -3207,7 +3229,7 @@ func (t *Terminal) Loop() {
}
doAction = func(a *action) bool {
switch a.t {
- case actIgnore:
+ case actIgnore, actStart, actClick:
case actResponse:
t.serverOutputChan <- t.dumpStatus(parseGetParams(a.a))
case actBecome:
@@ -3354,6 +3376,10 @@ func (t *Terminal) Loop() {
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(a.a, &tui.ColPreviewLabel, false)
req(reqRedrawPreviewLabel)
}
+ case actTransform:
+ body := t.executeCommand(a.a, false, true, true, false)
+ actions := parseSingleActionList(strings.Trim(body, "\r\n"), func(message string) {})
+ t.serverInputChan <- actions
case actTransformBorderLabel:
if t.border != nil {
label := t.executeCommand(a.a, false, true, true, true)
@@ -3384,7 +3410,7 @@ func (t *Terminal) Loop() {
req(reqQuit)
case actDeleteChar:
t.delChar()
- case actDeleteCharEOF:
+ case actDeleteCharEof:
if !t.delChar() && t.cx == 0 {
req(reqQuit)
}
@@ -3398,7 +3424,7 @@ func (t *Terminal) Loop() {
t.input = []rune{}
t.cx = 0
}
- case actBackwardDeleteCharEOF:
+ case actBackwardDeleteCharEof:
if len(t.input) == 0 {
req(reqQuit)
} else if t.cx > 0 {
@@ -3617,7 +3643,7 @@ func (t *Terminal) Loop() {
t.yanked = copySlice(t.input[t.cx:])
t.input = t.input[:t.cx]
}
- case actRune:
+ case actChar:
prefix := copySlice(t.input[:t.cx])
t.input = append(append(prefix, event.Char), t.input[t.cx:]...)
t.cx++
@@ -3895,6 +3921,10 @@ func (t *Terminal) Loop() {
}
}
}
+
+ if !processExecution(a.t) {
+ t.lastAction = a.t
+ }
return true
}
@@ -3908,7 +3938,7 @@ func (t *Terminal) Loop() {
actions = t.keymap[event.Comparable()]
}
if len(actions) == 0 && event.Type == tui.Rune {
- doAction(&action{t: actRune})
+ doAction(&action{t: actChar})
} else if !doActions(actions) {
continue
}
diff --git a/src/terminal_test.go b/src/terminal_test.go
index ebb6bda5..271c64a3 100644
--- a/src/terminal_test.go
+++ b/src/terminal_test.go
@@ -52,90 +52,90 @@ func TestReplacePlaceholder(t *testing.T) {
*/
// {}, preserve ansi
- result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1)
+ result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1, actIgnore)
checkFormat("echo {{.O}} foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
// {}, strip ansi
- result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1)
+ result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1, actIgnore)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
// {}, with multiple items
- result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2)
+ result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2, actIgnore)
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
// {..}, strip leading whitespaces, preserve ansi
- result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1)
+ result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1, actIgnore)
checkFormat("echo {{.O}}foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
// {..}, strip leading whitespaces, strip ansi
- result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
+ result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1, actIgnore)
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
// {q}
- result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1)
+ result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1, actIgnore)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}} {{.O}}query{{.O}}")
// {q}, multiple items
- result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2)
+ result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2, actIgnore)
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}")
- result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
+ result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2, actIgnore)
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}}")
- result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1)
+ result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1, actIgnore)
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}bazfoo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
- result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
+ result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2, actIgnore)
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
- result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2)
+ result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2, actIgnore)
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
// forcePlus
- result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2)
+ result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2, actIgnore)
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
// Whitespace preserving flag with "'" delimiter
- result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
+ result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1, actIgnore)
checkFormat("echo {{.O}} foo{{.O}}")
- result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
+ result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1, actIgnore)
checkFormat("echo {{.O}}bar baz{{.O}}")
- result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
+ result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1, actIgnore)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
- result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
+ result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1, actIgnore)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
// Whitespace preserving flag with regex delimiter
regex = regexp.MustCompile(`\w+`)
- result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
+ result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1, actIgnore)
checkFormat("echo {{.O}} {{.O}}")
- result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
+ result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1, actIgnore)
checkFormat("echo {{.O}}{{.I}}{{.O}}")
- result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
+ result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1, actIgnore)
checkFormat("echo {{.O}} {{.O}}")
// No match
- result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
+ result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil}, actIgnore)
check("echo /")
// No match, but with selections
- result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
+ result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1}, actIgnore)
checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}")
// String delimiter
- result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
+ result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1, actIgnore)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.O}}/{{.O}}bar baz{{.O}}")
// Regex delimiter
regex = regexp.MustCompile("[oa]+")
// foo'bar baz
- result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
+ result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1, actIgnore)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}f{{.O}}/{{.O}}r b{{.O}}/{{.O}}{{.I}}bar b{{.O}}")
/*
@@ -198,18 +198,23 @@ func TestReplacePlaceholder(t *testing.T) {
// query flag is not removed after parsing, so it gets doubled
// while the double q is invalid, it is useful here for testing purposes
templateToOutput[`{q}`] = "{{.O}}" + query + "{{.O}}"
+ templateToOutput[`{fzf:query}`] = "{{.O}}" + query + "{{.O}}"
+ templateToOutput[`{fzf:action}`] = "backward-delete-char-eof"
// IV. escaping placeholder
templateToOutput[`\{}`] = `{}`
+ templateToOutput[`\{q}`] = `{q}`
+ templateToOutput[`\{fzf:query}`] = `{fzf:query}`
+ templateToOutput[`\{fzf:action}`] = `{fzf:action}`
templateToOutput[`\{++}`] = `{++}`
templateToOutput[`{++}`] = templateToOutput[`{+}`]
for giveTemplate, wantOutput := range templateToOutput {
- result = replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
+ result = replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3, actBackwardDeleteCharEof)
checkFormat(wantOutput)
}
for giveTemplate, wantOutput := range templateToFile {
- path := replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
+ path := replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3, actIgnore)
data, err := readFile(path)
if err != nil {
@@ -566,7 +571,7 @@ func testCommands(t *testing.T, tests []testCase) {
gotOutput := replacePlaceholder(
test.give.template, stripAnsi, delimiter, printsep, forcePlus,
test.give.query,
- test.give.allItems)
+ test.give.allItems, actIgnore)
switch {
case test.want.output != "":
if gotOutput != test.want.output {
diff --git a/test/test_go.rb b/test/test_go.rb
index 5a9c48b7..771064ee 100755
--- a/test/test_go.rb
+++ b/test/test_go.rb
@@ -2016,6 +2016,13 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal '> RAB', lines[-1] }
end
+ def test_transform
+ tmux.send_keys %{#{FZF} --bind 'focus:transform:echo "change-prompt({fzf:action})"'}, :Enter
+ tmux.until { |lines| assert_equal 'start', lines[-1] }
+ tmux.send_keys :Up
+ tmux.until { |lines| assert_equal 'up', lines[-1] }
+ end
+
def test_clear_selection
tmux.send_keys %(seq 100 | #{FZF} --multi --bind space:clear-selection), :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }