From 7941129cc406a07da2b748f41c19b040224585e4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 22 Jul 2025 23:15:10 +0900 Subject: [PATCH] Add 'click-footer' event --- CHANGELOG.md | 21 ++++++++++++++++ man/man1/fzf.1 | 8 +++++++ src/options.go | 2 ++ src/terminal.go | 48 +++++++++++++++++++++++++++++++++++++ src/tui/eventtype_string.go | 7 +++--- src/tui/tui.go | 1 + 6 files changed, 84 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f489606..c0e2a872 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,27 @@ CHANGELOG ========= +0.65.0 +------ +- Added `click-footer` event that is triggered when the footer section is clicked. When the event is triggered, the following environment variables are set: + - `$FZF_CLICK_FOOTER_COLUMN` - clicked column (1-based) + - `$FZF_CLICK_FOOTER_LINE` - clicked line (1-based) + - `$FZF_CLICK_FOOTER_WORD` - the word under the cursor + ```sh + fzf --footer $'[Edit] [View]\n[Copy to clipboard]' \ + --with-shell 'bash -c' \ + --bind 'click-footer:transform: + [[ $FZF_CLICK_FOOTER_WORD =~ Edit ]] && echo "execute:vim \{}" + [[ $FZF_CLICK_FOOTER_WORD =~ View ]] && echo "execute:view \{}" + (( FZF_CLICK_FOOTER_LINE == 2 )) && (( FZF_CLICK_FOOTER_COLUMN < 20 )) && + echo "execute-silent(echo -n \{} | pbcopy)+bell" + ' + ``` +- 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. +- Bug fixes + 0.64.0 ------ - Added `multi` event that is triggered when the multi-selection has changed. diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 5ec02a76..0553fc02 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -1682,6 +1682,14 @@ e.g. )'\fR .RE +\fIclick\-footer\fR +.RS +Triggered when a mouse click occurs within the footer. Sets +\fBFZF_CLICK_FOOTER_LINE\fR and \fBFZF_CLICK_FOOTER_COLUMN\fR environment +variables starting from 1. It optionally sets \fBFZF_CLICK_FOOTER_WORD\fR +if clicked on a word. +.RE + .SS AVAILABLE ACTIONS: A key or an event can be bound to one or more of the following actions. diff --git a/src/options.go b/src/options.go index 6cc13c46..3de1eefa 100644 --- a/src/options.go +++ b/src/options.go @@ -1008,6 +1008,8 @@ func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error add(tui.JumpCancel) case "click-header": add(tui.ClickHeader) + case "click-footer": + add(tui.ClickFooter) case "multi": add(tui.Multi) case "alt-enter", "alt-return": diff --git a/src/terminal.go b/src/terminal.go index 95977614..de134356 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -422,6 +422,8 @@ type Terminal struct { forcePreview bool clickHeaderLine int clickHeaderColumn int + clickFooterLine int + clickFooterColumn int proxyScript string numLinesCache map[int32]numLinesCacheValue } @@ -1259,7 +1261,10 @@ 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 = append(env, fmt.Sprintf("FZF_CLICK_FOOTER_LINE=%d", t.clickFooterLine)) + env = append(env, fmt.Sprintf("FZF_CLICK_FOOTER_COLUMN=%d", t.clickFooterColumn)) env = t.addClickHeaderWord(env) + env = t.addClickFooterWord(env) // Add preview environment variables if preview is enabled pwindowSize := t.pwindowSize() @@ -1634,6 +1639,8 @@ func (t *Terminal) changeFooter(footer string) { lines = strings.Split(strings.TrimSuffix(footer, "\n"), "\n") } t.footer = lines + t.clickFooterLine = 0 + t.clickFooterColumn = 0 } // UpdateHeader updates the header @@ -4803,6 +4810,35 @@ func (t *Terminal) addClickHeaderWord(env []string) []string { return env } +func (t *Terminal) addClickFooterWord(env []string) []string { + clickFooterLine := t.clickFooterLine - 1 + if clickFooterLine < 0 || clickFooterLine >= len(t.footer) { + // Never clicked on the footer + return env + } + + // NOTE: Unlike in click-header, we don't use --delimiter here, since we're + // only interested in the word, not nth. Does this make sense? + words := Tokenize(t.footer[clickFooterLine], Delimiter{}) + colNum := t.clickFooterColumn - 1 + for _, token := range words { + prefixWidth := int(token.prefixLength) + word := token.text.ToString() + trimmed := strings.TrimRightFunc(word, unicode.IsSpace) + trimWidth, _ := util.RunesWidth([]rune(trimmed), prefixWidth, t.tabstop, math.MaxInt32) + + // Find the position of the first non-space character in the word + minPos := strings.IndexFunc(trimmed, func(r rune) bool { + return !unicode.IsSpace(r) + }) + if colNum >= minPos && colNum >= prefixWidth && colNum < prefixWidth+trimWidth { + env = append(env, fmt.Sprintf("FZF_CLICK_FOOTER_WORD=%s", trimmed)) + return env + } + } + return env +} + // Loop is called to start Terminal I/O func (t *Terminal) Loop() error { // prof := profile.Start(profile.ProfilePath("/tmp/")) @@ -6380,6 +6416,18 @@ func (t *Terminal) Loop() error { return doActions(actionsFor(tui.ClickHeader)) } + // Inside the footer window + if clicked && t.footerWindow != nil && t.footerWindow.Enclose(my, mx) { + mx -= t.footerWindow.Left() + t.headerIndent(t.footerBorderShape) + my -= t.footerWindow.Top() + if mx < 0 { + break + } + t.clickFooterLine = my + 1 + t.clickFooterColumn = mx + 1 + return doActions(actionsFor(tui.ClickFooter)) + } + // Ignored if !t.window.Enclose(my, mx) && !barDragging { break diff --git a/src/tui/eventtype_string.go b/src/tui/eventtype_string.go index b4e82f4a..9d4aa77d 100644 --- a/src/tui/eventtype_string.go +++ b/src/tui/eventtype_string.go @@ -110,12 +110,13 @@ func _() { _ = x[Jump-99] _ = x[JumpCancel-100] _ = x[ClickHeader-101] - _ = x[Multi-102] + _ = x[ClickFooter-102] + _ = x[Multi-103] } -const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalBracketedPasteBeginBracketedPasteEndMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeaderMulti" +const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalBracketedPasteBeginBracketedPasteEndMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeaderClickFooterMulti" -var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 466, 483, 488, 499, 508, 518, 528, 539, 547, 557, 566, 577, 592, 609, 615, 621, 632, 637, 641, 646, 649, 653, 659, 663, 673, 684, 689} +var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 466, 483, 488, 499, 508, 518, 528, 539, 547, 557, 566, 577, 592, 609, 615, 621, 632, 637, 641, 646, 649, 653, 659, 663, 673, 684, 695, 700} func (i EventType) String() string { if i < 0 || i >= EventType(len(_EventType_index)-1) { diff --git a/src/tui/tui.go b/src/tui/tui.go index a91e93e2..3f5d4282 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -132,6 +132,7 @@ const ( Jump JumpCancel ClickHeader + ClickFooter Multi )