From d6584543e993ae74615057cb02c4c831350b75bd Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 26 Jan 2025 15:04:30 +0900 Subject: [PATCH] Make click-header export $FZF_CLICK_HEADER_{NTH,WORD} --- CHANGELOG.md | 12 ++++++ man/man1/fzf.1 | 17 ++++++-- src/actiontype_string.go | 85 ++++++++++++++++++++-------------------- src/options.go | 4 +- src/terminal.go | 78 +++++++++++++++++++++++++++++++++--- src/tokenizer.go | 12 ++++++ 6 files changed, 157 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab795978..8aae63db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,18 @@ CHANGELOG --bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \ --header-lines-border bottom --no-list-border ``` +- `click-header` event will also set `$FZF_CLICK_HEADER_WORD` and `$FZF_CLICK_HEADER_NTH`. You can use it to implement a clickable header that changes the search scope using the new `transform-nth` action. + ```sh + # Click on the header line to limit search scope + ps -ef | fzf --style full --layout reverse --header-lines 1 \ + --header-lines-border bottom --no-list-border \ + --color fg:dim,nth:regular \ + --bind 'click-header:transform-nth( + echo $FZF_CLICK_HEADER_NTH + )+transform-prompt( + echo "$FZF_CLICK_HEADER_WORD> " + )' + ``` - Added `search(...)` and `transform-search(...)` action to trigger an fzf search with an arbitrary query string. This can be used to extend the search syntax of fzf. In the following example, fzf will use the first word of the query to trigger ripgrep search, and use the rest of the query to perform fzf search within the result. ```sh TRANSFORMER=' diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 89883b4b..b5f0ce68 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -1519,11 +1519,21 @@ e.g. \fIclick\-header\fR .RS -Triggered when a mouse click occurs within the header. Sets \fBFZF_CLICK_HEADER_LINE\fR and \fBFZF_CLICK_HEADER_COLUMN\fR environment variables starting from 1. +Triggered when a mouse click occurs within the header. Sets +\fBFZF_CLICK_HEADER_LINE\fR and \fBFZF_CLICK_HEADER_COLUMN\fR environment +variables starting from 1. It optionally sets \fBFZF_CLICK_HEADER_WORD\fR and +\fBFZF_CLICK_HEADER_NTH\fR if clicked on a word. e.g. - \fBprintf "head1\\nhead2" | fzf \-\-header\-lines=2 \-\-bind 'click\-header:transform\-prompt:printf ${FZF_CLICK_HEADER_LINE}x${FZF_CLICK_HEADER_COLUMN}'\fR - + \fB# Click on the header line to limit search scope + ps \-ef | fzf \-\-style full \-\-layout reverse \-\-header\-lines 1 \\ + \-\-header\-lines\-border bottom \-\-no\-list\-border \\ + \-\-color fg:dim,nth:regular \\ + \-\-bind 'click\-header:transform\-nth( + echo $FZF_CLICK_HEADER_NTH + )+transform\-prompt( + echo "$FZF_CLICK_HEADER_WORD> " + )'\fR .RE .SS AVAILABLE ACTIONS: @@ -1637,6 +1647,7 @@ A key or an event can be bound to one or more of the following actions. \fBtransform\-header\-label(...)\fR (transform header label using an external command) \fBtransform\-input\-label(...)\fR (transform input label using an external command) \fBtransform\-list\-label(...)\fR (transform list label using an external command) + \fBtransform\-nth(...)\fR (transform nth using an external command) \fBtransform\-preview\-label(...)\fR (transform preview label using an external command) \fBtransform\-prompt(...)\fR (transform prompt string using an external command) \fBtransform\-query(...)\fR (transform query string using an external command) diff --git a/src/actiontype_string.go b/src/actiontype_string.go index 143e02b6..b37446c5 100644 --- a/src/actiontype_string.go +++ b/src/actiontype_string.go @@ -93,51 +93,52 @@ func _() { _ = x[actTransformInputLabel-82] _ = x[actTransformHeader-83] _ = x[actTransformHeaderLabel-84] - _ = x[actTransformPreviewLabel-85] - _ = x[actTransformPrompt-86] - _ = x[actTransformQuery-87] - _ = x[actTransformSearch-88] - _ = x[actSearch-89] - _ = x[actPreview-90] - _ = x[actChangePreview-91] - _ = x[actChangePreviewWindow-92] - _ = x[actPreviewTop-93] - _ = x[actPreviewBottom-94] - _ = x[actPreviewUp-95] - _ = x[actPreviewDown-96] - _ = x[actPreviewPageUp-97] - _ = x[actPreviewPageDown-98] - _ = x[actPreviewHalfPageUp-99] - _ = x[actPreviewHalfPageDown-100] - _ = x[actPrevHistory-101] - _ = x[actPrevSelected-102] - _ = x[actPrint-103] - _ = x[actPut-104] - _ = x[actNextHistory-105] - _ = x[actNextSelected-106] - _ = x[actExecute-107] - _ = x[actExecuteSilent-108] - _ = x[actExecuteMulti-109] - _ = x[actSigStop-110] - _ = x[actFirst-111] - _ = x[actLast-112] - _ = x[actReload-113] - _ = x[actReloadSync-114] - _ = x[actDisableSearch-115] - _ = x[actEnableSearch-116] - _ = x[actSelect-117] - _ = x[actDeselect-118] - _ = x[actUnbind-119] - _ = x[actRebind-120] - _ = x[actBecome-121] - _ = x[actShowHeader-122] - _ = x[actHideHeader-123] - _ = x[actBell-124] + _ = x[actTransformNth-85] + _ = x[actTransformPreviewLabel-86] + _ = x[actTransformPrompt-87] + _ = x[actTransformQuery-88] + _ = x[actTransformSearch-89] + _ = x[actSearch-90] + _ = x[actPreview-91] + _ = x[actChangePreview-92] + _ = x[actChangePreviewWindow-93] + _ = x[actPreviewTop-94] + _ = x[actPreviewBottom-95] + _ = x[actPreviewUp-96] + _ = x[actPreviewDown-97] + _ = x[actPreviewPageUp-98] + _ = x[actPreviewPageDown-99] + _ = x[actPreviewHalfPageUp-100] + _ = x[actPreviewHalfPageDown-101] + _ = x[actPrevHistory-102] + _ = x[actPrevSelected-103] + _ = x[actPrint-104] + _ = x[actPut-105] + _ = x[actNextHistory-106] + _ = x[actNextSelected-107] + _ = x[actExecute-108] + _ = x[actExecuteSilent-109] + _ = x[actExecuteMulti-110] + _ = x[actSigStop-111] + _ = x[actFirst-112] + _ = x[actLast-113] + _ = x[actReload-114] + _ = x[actReloadSync-115] + _ = x[actDisableSearch-116] + _ = x[actEnableSearch-117] + _ = x[actSelect-118] + _ = x[actDeselect-119] + _ = x[actUnbind-120] + _ = x[actRebind-121] + _ = x[actBecome-122] + _ = x[actShowHeader-123] + _ = x[actHideHeader-124] + _ = x[actBell-125] } -const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeListLabelactChangeInputLabelactChangeHeaderactChangeHeaderLabelactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactChangeNthactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformListLabelactTransformInputLabelactTransformHeaderactTransformHeaderLabelactTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactSearchactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactShowHeaderactHideHeaderactBell" +const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeListLabelactChangeInputLabelactChangeHeaderactChangeHeaderLabelactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactChangeNthactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformListLabelactTransformInputLabelactTransformHeaderactTransformHeaderLabelactTransformNthactTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactSearchactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactShowHeaderactHideHeaderactBell" -var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 245, 264, 279, 299, 313, 334, 349, 363, 375, 389, 402, 419, 427, 440, 456, 468, 476, 490, 504, 515, 526, 544, 561, 568, 587, 599, 613, 622, 637, 649, 662, 673, 684, 696, 710, 731, 746, 759, 777, 793, 808, 825, 832, 837, 846, 857, 868, 881, 896, 907, 920, 935, 942, 955, 968, 985, 1000, 1013, 1027, 1041, 1057, 1077, 1089, 1112, 1133, 1155, 1173, 1196, 1220, 1238, 1255, 1273, 1282, 1292, 1308, 1330, 1343, 1359, 1371, 1385, 1401, 1419, 1439, 1461, 1475, 1490, 1498, 1504, 1518, 1533, 1543, 1559, 1574, 1584, 1592, 1599, 1608, 1621, 1637, 1652, 1661, 1672, 1681, 1690, 1699, 1712, 1725, 1732} +var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 245, 264, 279, 299, 313, 334, 349, 363, 375, 389, 402, 419, 427, 440, 456, 468, 476, 490, 504, 515, 526, 544, 561, 568, 587, 599, 613, 622, 637, 649, 662, 673, 684, 696, 710, 731, 746, 759, 777, 793, 808, 825, 832, 837, 846, 857, 868, 881, 896, 907, 920, 935, 942, 955, 968, 985, 1000, 1013, 1027, 1041, 1057, 1077, 1089, 1112, 1133, 1155, 1173, 1196, 1211, 1235, 1253, 1270, 1288, 1297, 1307, 1323, 1345, 1358, 1374, 1386, 1400, 1416, 1434, 1454, 1476, 1490, 1505, 1513, 1519, 1533, 1548, 1558, 1574, 1589, 1599, 1607, 1614, 1623, 1636, 1652, 1667, 1676, 1687, 1696, 1705, 1714, 1727, 1740, 1747} 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 2b9a5196..5865f0ea 100644 --- a/src/options.go +++ b/src/options.go @@ -1332,7 +1332,7 @@ const ( func init() { executeRegexp = regexp.MustCompile( - `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|(?:border|list|preview|input|header)-label|header|search)|transform|change-(?:preview-window|preview|multi|nth)|(?:re|un)bind|pos|put|print|search)`) + `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|(?:border|list|preview|input|header)-label|header|search|nth)|transform|change-(?:preview-window|preview|multi)|(?:re|un)bind|pos|put|print|search)`) splitRegexp = regexp.MustCompile("[,:]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") } @@ -1740,6 +1740,8 @@ func isExecuteAction(str string) actionType { return actTransformHeaderLabel case "transform-header": return actTransformHeader + case "transform-nth": + return actTransformNth case "transform-prompt": return actTransformPrompt case "transform-query": diff --git a/src/terminal.go b/src/terminal.go index 4d58ca8c..56b70e9b 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -531,6 +531,7 @@ const ( actTransformInputLabel actTransformHeader actTransformHeaderLabel + actTransformNth actTransformPreviewLabel actTransformPrompt actTransformQuery @@ -1065,6 +1066,7 @@ func (t *Terminal) environImpl(forPreview bool) []string { env = append(env, fmt.Sprintf("FZF_POS=%d", util.Min(t.merger.Length(), t.cy+1))) env = append(env, fmt.Sprintf("FZF_CLICK_HEADER_LINE=%d", t.clickHeaderLine)) env = append(env, fmt.Sprintf("FZF_CLICK_HEADER_COLUMN=%d", t.clickHeaderColumn)) + env = t.addClickHeaderWord(env) // Add preview environment variables if preview is enabled pwindowSize := t.pwindowSize() @@ -1393,6 +1395,8 @@ func (t *Terminal) changeHeader(header string) bool { } needFullRedraw := len(t.header0) != len(lines) t.header0 = lines + t.clickHeaderLine = 0 + t.clickHeaderColumn = 0 return needFullRedraw } @@ -4089,6 +4093,64 @@ func (t *Terminal) currentIndex() int32 { return minItem.Index() } +func (t *Terminal) addClickHeaderWord(env []string) []string { + /* + * echo $'HL1\nHL2' | fzf --header-lines 3 --header $'H1\nH2' --header-lines-border --bind 'click-header:preview:env | grep FZF_CLICK' + * + * REVERSE DEFAULT + * H1 1 1 + * H2 2 HL2 2 + * ------- HL1 3 + * HL1 3 ------- + * HL2 4 H1 4 + * 5 H2 5 + */ + lineNum := t.clickHeaderLine - 1 + if lineNum < 0 { + // Never clicked on the header + return env + } + + var line string + if t.layout == layoutReverse { + if lineNum < len(t.header0) { + line = t.header0[lineNum] + } else if lineNum-len(t.header0) < len(t.header) { + line = t.header[lineNum-len(t.header0)] + } + } else { + // NOTE: t.header is padded with empty strings so that its size is equal to t.headerLines + if lineNum < len(t.header) { + line = t.header[len(t.header)-lineNum-1] + } else if lineNum-len(t.header) < len(t.header0) { + line = t.header0[lineNum-len(t.header)] + } + } + if len(line) == 0 { + return env + } + + colNum := t.clickHeaderColumn - 1 + words := Tokenize(line, t.delimiter) + for idx, token := range words { + prefixWidth := int(token.prefixLength) + word := token.text.ToString() + trimmed := strings.TrimSpace(word) + trimWidth, _ := util.RunesWidth([]rune(trimmed), prefixWidth, t.tabstop, math.MaxInt32) + + if colNum >= prefixWidth && colNum < prefixWidth+trimWidth { + env = append(env, fmt.Sprintf("FZF_CLICK_HEADER_WORD=%s", trimmed)) + nth := fmt.Sprintf("FZF_CLICK_HEADER_NTH=%d", idx+1) + if idx == len(words)-1 { + nth += ".." + } + env = append(env, nth) + return env + } + } + return env +} + // Loop is called to start Terminal I/O func (t *Terminal) Loop() error { // prof := profile.Start(profile.ProfilePath("/tmp/")) @@ -4833,11 +4895,14 @@ func (t *Terminal) Loop() error { } t.multi = multi req(reqList, reqInfo) - case actChangeNth: - changed = true + case actChangeNth, actTransformNth: + expr := a.a + if a.t == actTransformNth { + expr = t.captureLine(a.a) + } // Split nth expression - tokens := strings.Split(a.a, "|") + tokens := strings.Split(expr, "|") if nth, err := splitNth(tokens[0]); err == nil { // Changed newNth = &nth @@ -4845,12 +4910,15 @@ func (t *Terminal) Loop() error { // The default newNth = &t.nth } - t.nthCurrent = *newNth // Cycle if len(tokens) > 1 { a.a = strings.Join(append(tokens[1:], tokens[0]), "|") } - t.forceRerenderList() + if !compareRanges(t.nthCurrent, *newNth) { + changed = true + t.nthCurrent = *newNth + t.forceRerenderList() + } case actChangeQuery: t.input = []rune(a.a) t.cx = len(t.input) diff --git a/src/tokenizer.go b/src/tokenizer.go index e5a8e977..f5d1483b 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -22,6 +22,18 @@ func (r Range) IsFull() bool { return r.begin == rangeEllipsis && r.end == rangeEllipsis } +func compareRanges(r1 []Range, r2 []Range) bool { + if len(r1) != len(r2) { + return false + } + for idx := range r1 { + if r1[idx] != r2[idx] { + return false + } + } + return true +} + func RangesToString(ranges []Range) string { strs := []string{} for _, r := range ranges {