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
This commit is contained in:
Junegunn Choi
2023-12-25 23:43:46 +09:00
parent 41d4d70b98
commit 1707b8cdba
10 changed files with 255 additions and 52 deletions

View File

@@ -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
}