Make click-header export $FZF_CLICK_HEADER_{NTH,WORD}

This commit is contained in:
Junegunn Choi 2025-01-26 15:04:30 +09:00
parent c13228f346
commit d6584543e9
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
6 changed files with 157 additions and 51 deletions

View File

@ -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='

View File

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

View File

@ -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) {

View File

@ -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":

View File

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

View File

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