From 4efcc344c35e8bb7e6ba7bb23e5885051420b361 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 23 Jul 2025 19:39:01 +0900 Subject: [PATCH] Add 'trigger(KEY_OR_EVENT[,...])' action --- CHANGELOG.md | 16 ++++++ man/man1/fzf.1 | 1 + src/actiontype_string.go | 113 ++++++++++++++++++++------------------- src/options.go | 50 ++++++++++------- src/options_test.go | 20 +++---- src/terminal.go | 23 ++++++-- test/test_core.rb | 10 ++++ 7 files changed, 145 insertions(+), 88 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0e2a872..1a362f7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,22 @@ CHANGELOG echo "execute-silent(echo -n \{} | pbcopy)+bell" ' ``` +- Added `trigger(...)` action that triggers events bound to another key or event. + ```sh + # You can click on each key name to trigger the actions bound to that key + fzf --footer 'Ctrl-E: Edit / Ctrl-V: View / Ctrl-Y: Copy to clipboard' \ + --with-shell 'bash -c' \ + --bind 'ctrl-e:execute:vim {}' \ + --bind 'ctrl-v:execute:view {}' \ + --bind 'ctrl-y:execute-silent(echo -n {} | pbcopy)+bell' \ + --bind 'click-footer:transform: + [[ $FZF_CLICK_FOOTER_WORD =~ Ctrl ]] && echo "trigger(${FZF_CLICK_FOOTER_WORD%:})" + ' + ``` + - You can specify a series of keys and events + ```sh + fzf --bind 'a:up,b:trigger(a,a,a)' + ``` - Added support for `{*n}` and `{*nf}` placeholder. - `{*n}` evaluates to the zero-based ordinal index of all matched items. - `{*nf}` evaluates to the temporary file containing that. diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 0553fc02..56aba4ae 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -1817,6 +1817,7 @@ A key or an event can be bound to one or more of the following actions. \fBtransform\-prompt(...)\fR (transform prompt string using an external command) \fBtransform\-query(...)\fR (transform query string using an external command) \fBtransform\-search(...)\fR (trigger fzf search with the output of an external command) + \fBtrigger(...)\fR (trigger actions bound to a comma-separated list of keys and events) \fBunbind(...)\fR (unbind bindings) \fBunix\-line\-discard\fR \fIctrl\-u\fR \fBunix\-word\-rubout\fR \fIctrl\-w\fR diff --git a/src/actiontype_string.go b/src/actiontype_string.go index e163f746..27d76b32 100644 --- a/src/actiontype_string.go +++ b/src/actiontype_string.go @@ -113,65 +113,66 @@ func _() { _ = x[actTransformPrompt-102] _ = x[actTransformQuery-103] _ = x[actTransformSearch-104] - _ = 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[actBgCancel-120] - _ = x[actSearch-121] - _ = x[actPreview-122] - _ = x[actPreviewTop-123] - _ = x[actPreviewBottom-124] - _ = x[actPreviewUp-125] - _ = x[actPreviewDown-126] - _ = x[actPreviewPageUp-127] - _ = x[actPreviewPageDown-128] - _ = x[actPreviewHalfPageUp-129] - _ = x[actPreviewHalfPageDown-130] - _ = x[actPrevHistory-131] - _ = x[actPrevSelected-132] - _ = x[actPrint-133] - _ = x[actPut-134] - _ = x[actNextHistory-135] - _ = x[actNextSelected-136] - _ = x[actExecute-137] - _ = x[actExecuteSilent-138] - _ = x[actExecuteMulti-139] - _ = x[actSigStop-140] - _ = x[actFirst-141] - _ = x[actLast-142] - _ = x[actReload-143] - _ = x[actReloadSync-144] - _ = x[actDisableSearch-145] - _ = x[actEnableSearch-146] - _ = x[actSelect-147] - _ = x[actDeselect-148] - _ = x[actUnbind-149] - _ = x[actRebind-150] - _ = x[actToggleBind-151] - _ = x[actBecome-152] - _ = x[actShowHeader-153] - _ = x[actHideHeader-154] - _ = x[actBell-155] - _ = x[actExclude-156] - _ = x[actExcludeMulti-157] - _ = x[actAsync-158] + _ = x[actTrigger-105] + _ = x[actBgTransform-106] + _ = x[actBgTransformBorderLabel-107] + _ = x[actBgTransformGhost-108] + _ = x[actBgTransformHeader-109] + _ = x[actBgTransformFooter-110] + _ = x[actBgTransformHeaderLabel-111] + _ = x[actBgTransformFooterLabel-112] + _ = x[actBgTransformInputLabel-113] + _ = x[actBgTransformListLabel-114] + _ = x[actBgTransformNth-115] + _ = x[actBgTransformPointer-116] + _ = x[actBgTransformPreviewLabel-117] + _ = x[actBgTransformPrompt-118] + _ = x[actBgTransformQuery-119] + _ = x[actBgTransformSearch-120] + _ = x[actBgCancel-121] + _ = x[actSearch-122] + _ = x[actPreview-123] + _ = x[actPreviewTop-124] + _ = x[actPreviewBottom-125] + _ = x[actPreviewUp-126] + _ = x[actPreviewDown-127] + _ = x[actPreviewPageUp-128] + _ = x[actPreviewPageDown-129] + _ = x[actPreviewHalfPageUp-130] + _ = x[actPreviewHalfPageDown-131] + _ = x[actPrevHistory-132] + _ = x[actPrevSelected-133] + _ = x[actPrint-134] + _ = x[actPut-135] + _ = x[actNextHistory-136] + _ = x[actNextSelected-137] + _ = x[actExecute-138] + _ = x[actExecuteSilent-139] + _ = x[actExecuteMulti-140] + _ = x[actSigStop-141] + _ = x[actFirst-142] + _ = x[actLast-143] + _ = x[actReload-144] + _ = x[actReloadSync-145] + _ = x[actDisableSearch-146] + _ = x[actEnableSearch-147] + _ = x[actSelect-148] + _ = x[actDeselect-149] + _ = x[actUnbind-150] + _ = x[actRebind-151] + _ = x[actToggleBind-152] + _ = x[actBecome-153] + _ = x[actShowHeader-154] + _ = x[actHideHeader-155] + _ = x[actBell-156] + _ = x[actExclude-157] + _ = x[actExcludeMulti-158] + _ = x[actAsync-159] } -const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync" +const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync" -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, 1877, 1886, 1896, 1909, 1925, 1937, 1951, 1967, 1985, 2005, 2027, 2041, 2056, 2064, 2070, 2084, 2099, 2109, 2125, 2140, 2150, 2158, 2165, 2174, 2187, 2203, 2218, 2227, 2238, 2247, 2256, 2269, 2278, 2291, 2304, 2311, 2321, 2336, 2344} +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, 1558, 1572, 1597, 1616, 1636, 1656, 1681, 1706, 1730, 1753, 1770, 1791, 1817, 1837, 1856, 1876, 1887, 1896, 1906, 1919, 1935, 1947, 1961, 1977, 1995, 2015, 2037, 2051, 2066, 2074, 2080, 2094, 2109, 2119, 2135, 2150, 2160, 2168, 2175, 2184, 2197, 2213, 2228, 2237, 2248, 2257, 2266, 2279, 2288, 2301, 2314, 2321, 2331, 2346, 2354} func (i actionType) String() string { if i < 0 || i >= actionType(len(_actionType_index)-1) { diff --git a/src/options.go b/src/options.go index 3de1eefa..e15d43d6 100644 --- a/src/options.go +++ b/src/options.go @@ -932,15 +932,12 @@ func parseBorder(str string, optional bool) (tui.BorderShape, error) { return tui.BorderNone, errors.New("invalid border style (expected: rounded|sharp|bold|block|thinblock|double|horizontal|vertical|top|bottom|left|right|none)") } -func parseKeyChords(str string, message string) (map[tui.Event]string, error) { - return parseKeyChordsImpl(str, message) -} - -func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error) { +func parseKeyChords(str string, message string) (map[tui.Event]string, []tui.Event, error) { if len(str) == 0 { - return nil, errors.New(message) + return nil, nil, errors.New(message) } + list := []tui.Event{} str = regexp.MustCompile("(?i)(alt-),").ReplaceAllString(str, "$1"+string([]rune{escapedComma})) tokens := strings.Split(str, ",") if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Contains(str, ",,,") { @@ -956,6 +953,7 @@ func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error lkey := strings.ToLower(key) add := func(e tui.EventType) { chords[e.AsEvent()] = key + list = append(list, e.AsEvent()) } switch lkey { case "up": @@ -969,7 +967,9 @@ func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error case "enter", "return": add(tui.Enter) case "space": - chords[tui.Key(' ')] = key + evt := tui.Key(' ') + chords[evt] = key + list = append(list, evt) case "backspace", "bspace", "bs": add(tui.Backspace) case "ctrl-space": @@ -1013,9 +1013,13 @@ func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error case "multi": add(tui.Multi) case "alt-enter", "alt-return": - chords[tui.CtrlAltKey('m')] = key + evt := tui.CtrlAltKey('m') + chords[evt] = key + list = append(list, evt) case "alt-space": - chords[tui.AltKey(' ')] = key + evt := tui.AltKey(' ') + chords[evt] = key + list = append(list, evt) case "alt-bs", "alt-bspace", "alt-backspace": add(tui.AltBackspace) case "alt-up": @@ -1093,7 +1097,9 @@ func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error default: runes := []rune(key) if len(key) == 10 && strings.HasPrefix(lkey, "ctrl-alt-") && isAlphabet(lkey[9]) { - chords[tui.CtrlAltKey(rune(key[9]))] = key + evt := tui.CtrlAltKey(rune(key[9])) + chords[evt] = key + list = append(list, evt) } else if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { add(tui.EventType(tui.CtrlA.Int() + int(lkey[5]) - 'a')) } else if len(runes) == 5 && strings.HasPrefix(lkey, "alt-") { @@ -1106,17 +1112,21 @@ func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error case escapedPlus: r = '+' } - chords[tui.AltKey(r)] = key + evt := tui.AltKey(r) + chords[evt] = key + list = append(list, evt) } else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' { add(tui.EventType(tui.F1.Int() + int(key[1]) - '1')) } else if len(runes) == 1 { - chords[tui.Key(runes[0])] = key + evt := tui.Key(runes[0]) + chords[evt] = key + list = append(list, evt) } else { - return nil, errors.New("unsupported key: " + key) + return nil, list, errors.New("unsupported key: " + key) } } } - return chords, nil + return chords, list, nil } func parseScheme(str string) (string, []criterion, error) { @@ -1439,7 +1449,7 @@ const ( func init() { executeRegexp = regexp.MustCompile( - `(?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)`) + `(?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|trigger)`) splitRegexp = regexp.MustCompile("[,:]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") } @@ -1736,7 +1746,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA } switch t { case actUnbind, actRebind, actToggleBind: - if _, err := parseKeyChordsImpl(actionArg, spec[0:offset]+" target required"); err != nil { + if _, _, err := parseKeyChords(actionArg, spec[0:offset]+" target required"); err != nil { return nil, err } case actChangePreviewWindow: @@ -1781,7 +1791,7 @@ func parseKeymap(keymap map[tui.Event][]*action, str string) error { } else if len(keyName) == 1 && keyName[0] == escapedPlus { key = tui.Key('+') } else { - keys, err := parseKeyChordsImpl(keyName, "key name required") + keys, _, err := parseKeyChords(keyName, "key name required") if err != nil { return err } @@ -1928,6 +1938,8 @@ func isExecuteAction(str string) actionType { return actBgTransformQuery case "bg-transform-search": return actBgTransformSearch + case "trigger": + return actTrigger case "search": return actSearch } @@ -1935,7 +1947,7 @@ func isExecuteAction(str string) actionType { } func parseToggleSort(keymap map[tui.Event][]*action, str string) error { - keys, err := parseKeyChords(str, "key name required") + keys, _, err := parseKeyChords(str, "key name required") if err != nil { return err } @@ -2474,7 +2486,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error { if err != nil { return err } - chords, err := parseKeyChords(str, "key names required") + chords, _, err := parseKeyChords(str, "key names required") if err != nil { return err } diff --git a/src/options_test.go b/src/options_test.go index 2105322e..f35c7ee3 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -142,7 +142,7 @@ func TestIrrelevantNth(t *testing.T) { } func TestParseKeys(t *testing.T) { - pairs, _ := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "") + pairs, _, _ := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "") checkEvent := func(e tui.Event, s string) { if pairs[e] != s { t.Errorf("%s != %s", pairs[e], s) @@ -168,7 +168,7 @@ func TestParseKeys(t *testing.T) { checkEvent(tui.AltKey(' '), "alt-SPACE") // Synonyms - pairs, _ = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "") + pairs, _, _ = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "") if len(pairs) != 9 { t.Error(9) } @@ -182,7 +182,7 @@ func TestParseKeys(t *testing.T) { check(tui.Left, "left") check(tui.Right, "right") - pairs, _ = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "") + pairs, _, _ = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "") if len(pairs) != 11 { t.Error(11) } @@ -211,40 +211,40 @@ func TestParseKeysWithComma(t *testing.T) { } } - pairs, _ := parseKeyChords(",", "") + pairs, _, _ := parseKeyChords(",", "") checkN(len(pairs), 1) check(pairs, tui.Key(','), ",") - pairs, _ = parseKeyChords(",,a,b", "") + pairs, _, _ = parseKeyChords(",,a,b", "") checkN(len(pairs), 3) check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('b'), "b") check(pairs, tui.Key(','), ",") - pairs, _ = parseKeyChords("a,b,,", "") + pairs, _, _ = parseKeyChords("a,b,,", "") checkN(len(pairs), 3) check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('b'), "b") check(pairs, tui.Key(','), ",") - pairs, _ = parseKeyChords("a,,,b", "") + pairs, _, _ = parseKeyChords("a,,,b", "") checkN(len(pairs), 3) check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('b'), "b") check(pairs, tui.Key(','), ",") - pairs, _ = parseKeyChords("a,,,b,c", "") + pairs, _, _ = parseKeyChords("a,,,b,c", "") checkN(len(pairs), 4) check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('b'), "b") check(pairs, tui.Key('c'), "c") check(pairs, tui.Key(','), ",") - pairs, _ = parseKeyChords(",,,", "") + pairs, _, _ = parseKeyChords(",,,", "") checkN(len(pairs), 1) check(pairs, tui.Key(','), ",") - pairs, _ = parseKeyChords(",ALT-,,", "") + pairs, _, _ = parseKeyChords(",ALT-,,", "") checkN(len(pairs), 1) check(pairs, tui.AltKey(','), "ALT-,") } diff --git a/src/terminal.go b/src/terminal.go index 37c947be..c0e79066 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -604,6 +604,8 @@ const ( actTransformQuery actTransformSearch + actTrigger + actBgTransform actBgTransformBorderLabel actBgTransformGhost @@ -5443,6 +5445,7 @@ func (t *Terminal) Loop() error { return nil } } + triggering := map[tui.Event]struct{}{} previousInput := t.input previousCx := t.cx previousVersion := t.version @@ -6234,6 +6237,20 @@ func (t *Terminal) Loop() error { case actDisableSearch: t.paused = true req(reqPrompt) + case actTrigger: + if _, chords, err := parseKeyChords(a.a, ""); err == nil { + for _, chord := range chords { + if _, prs := triggering[chord]; prs { + // Avoid recursive triggering + continue + } + if acts, prs := t.keymap[chord]; prs { + triggering[chord] = struct{}{} + doActions(acts) + delete(triggering, chord) + } + } + } case actSigStop: p, err := os.FindProcess(os.Getpid()) if err == nil { @@ -6555,13 +6572,13 @@ func (t *Terminal) Loop() error { t.reading = true } case actUnbind: - if keys, err := parseKeyChords(a.a, "PANIC"); err == nil { + if keys, _, err := parseKeyChords(a.a, "PANIC"); err == nil { for key := range keys { delete(t.keymap, key) } } case actRebind: - if keys, err := parseKeyChords(a.a, "PANIC"); err == nil { + if keys, _, err := parseKeyChords(a.a, "PANIC"); err == nil { for key := range keys { if originalAction, found := t.keymapOrg[key]; found { t.keymap[key] = originalAction @@ -6569,7 +6586,7 @@ func (t *Terminal) Loop() error { } } case actToggleBind: - if keys, err := parseKeyChords(a.a, "PANIC"); err == nil { + if keys, _, err := parseKeyChords(a.a, "PANIC"); err == nil { for key := range keys { if _, bound := t.keymap[key]; bound { delete(t.keymap, key) diff --git a/test/test_core.rb b/test/test_core.rb index 4d21c19c..17578baf 100644 --- a/test/test_core.rb +++ b/test/test_core.rb @@ -2035,4 +2035,14 @@ class TestCore < TestInteractive assert_equal 19, it.select_count end end + + def test_trigger + tmux.send_keys %(seq 100 | #{FZF} --bind 'a:up+trigger(a),b:trigger(a,a,b,a)'), :Enter + tmux.until { assert_equal 100, it.match_count } + tmux.until { |lines| assert_includes lines, '> 1' } + tmux.send_keys :a + tmux.until { |lines| assert_includes lines, '> 3' } + tmux.send_keys :b + tmux.until { |lines| assert_includes lines, '> 9' } + end end