Enhance click-header event

* Expose the name of the mouse action as $FZF_KEY
* Trigger click-header on mouse up
* Enhanced clickable header for `kill` completion
This commit is contained in:
Junegunn Choi 2025-01-27 01:10:08 +09:00
parent 2c15cd7923
commit e91f10ab16
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
8 changed files with 151 additions and 54 deletions

View File

@ -529,7 +529,10 @@ TRANSFORMER='
# Otherwise, if the query does not end with a space, # Otherwise, if the query does not end with a space,
# restart ripgrep and reload the list # restart ripgrep and reload the list
elif ! [[ $FZF_QUERY =~ \ $ ]]; then elif ! [[ $FZF_QUERY =~ \ $ ]]; then
echo "reload:sleep 0.1; $RG_PREFIX \"${words[0]}\" || true" pat=${words[0]}
echo "reload:sleep 0.1; $RG_PREFIX \"$pat\" || true"
else
echo search:
fi fi
' '
fzf --ansi --disabled --query "$INITIAL_QUERY" \ fzf --ansi --disabled --query "$INITIAL_QUERY" \

View File

@ -26,6 +26,7 @@ CHANGELOG
echo "$FZF_CLICK_HEADER_WORD> " echo "$FZF_CLICK_HEADER_WORD> "
)' )'
``` ```
- `kill` completion for bash and zsh were updated to use this feature
- 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. - 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 ```sh
TRANSFORMER=' TRANSFORMER='
@ -40,6 +41,8 @@ CHANGELOG
# restart ripgrep and reload the list # restart ripgrep and reload the list
elif ! [[ $FZF_QUERY =~ \ $ ]]; then elif ! [[ $FZF_QUERY =~ \ $ ]]; then
echo "reload:rg --column --color=always --smart-case \"${words[0]}\"" echo "reload:rg --column --color=always --smart-case \"${words[0]}\""
else
echo search:
fi fi
' '
fzf --ansi --disabled \ fzf --ansi --disabled \

View File

@ -409,8 +409,32 @@ _fzf_complete_kill() {
} }
_fzf_proc_completion() { _fzf_proc_completion() {
local transformer
transformer='
if [[ $FZF_KEY =~ ctrl|alt|shift ]] && [[ -n $FZF_NTH ]]; then
nths=( $(tr , " " <<< "$FZF_NTH") )
new_nths=()
found=0
for nth in ${nths[@]}; do
if [[ $nth = $FZF_CLICK_HEADER_NTH ]]; then
found=1
else
new_nths+=($nth)
fi
done
[[ $found = 0 ]] && new_nths+=($FZF_CLICK_HEADER_NTH)
new_nths=$(echo ${new_nths[@]} | tr " " ,)
echo "change-nth($new_nths)+change-prompt($new_nths> )"
else
if [[ $FZF_NTH = $FZF_CLICK_HEADER_NTH ]]; then
echo "change-nth()+change-prompt(> )"
else
echo "change-nth($FZF_CLICK_HEADER_NTH)+change-prompt($FZF_CLICK_HEADER_WORD> )"
fi
fi
'
_fzf_complete -m --header-lines=1 --no-preview --wrap --color fg:dim,nth:regular \ _fzf_complete -m --header-lines=1 --no-preview --wrap --color fg:dim,nth:regular \
--bind 'click-header:transform:echo "change-nth($FZF_CLICK_HEADER_NTH)+change-prompt($FZF_CLICK_HEADER_WORD> )"' -- "$@" < <( --bind "click-header:transform:$transformer" -- "$@" < <(
command ps -eo user,pid,ppid,start,time,command 2> /dev/null || command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
command ps --everyone --full --windows # For cygwin command ps --everyone --full --windows # For cygwin

View File

@ -290,8 +290,32 @@ _fzf_complete_unalias() {
} }
_fzf_complete_kill() { _fzf_complete_kill() {
local transformer
transformer='
if [[ $FZF_KEY =~ ctrl|alt|shift ]] && [[ -n $FZF_NTH ]]; then
nths=( $(tr , " " <<< "$FZF_NTH") )
new_nths=()
found=0
for nth in ${nths[@]}; do
if [[ $nth = $FZF_CLICK_HEADER_NTH ]]; then
found=1
else
new_nths+=($nth)
fi
done
[[ $found = 0 ]] && new_nths+=($FZF_CLICK_HEADER_NTH)
new_nths=$(echo ${new_nths[@]} | tr " " ,)
echo "change-nth($new_nths)+change-prompt($new_nths> )"
else
if [[ $FZF_NTH = $FZF_CLICK_HEADER_NTH ]]; then
echo "change-nth()+change-prompt(> )"
else
echo "change-nth($FZF_CLICK_HEADER_NTH)+change-prompt($FZF_CLICK_HEADER_WORD> )"
fi
fi
'
_fzf_complete -m --header-lines=1 --no-preview --wrap --color fg:dim,nth:regular \ _fzf_complete -m --header-lines=1 --no-preview --wrap --color fg:dim,nth:regular \
--bind 'click-header:transform:echo "change-nth($FZF_CLICK_HEADER_NTH)+change-prompt($FZF_CLICK_HEADER_WORD> )"' -- "$@" < <( --bind "click-header:transform:$transformer" -- "$@" < <(
command ps -eo user,pid,ppid,start,time,command 2> /dev/null || command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
command ps --everyone --full --windows # For cygwin command ps --everyone --full --windows # For cygwin

View File

@ -4620,6 +4620,7 @@ func (t *Terminal) Loop() error {
pbarDragging := false pbarDragging := false
pborderDragging := -1 pborderDragging := -1
wasDown := false wasDown := false
pmx, pmy := -1, -1
needBarrier := true needBarrier := true
// If an action is bound to 'start', we're going to process it before reading // If an action is bound to 'start', we're going to process it before reading
@ -5422,25 +5423,30 @@ func (t *Terminal) Loop() error {
case actMouse: case actMouse:
me := event.MouseEvent me := event.MouseEvent
mx, my := me.X, me.Y mx, my := me.X, me.Y
clicked := !wasDown && me.Down click := !wasDown && me.Down
clicked := wasDown && !me.Down && (mx == pmx && my == pmy)
wasDown = me.Down wasDown = me.Down
if click {
pmx, pmy = mx, my
}
if !me.Down { if !me.Down {
barDragging = false barDragging = false
pbarDragging = false pbarDragging = false
pborderDragging = -1 pborderDragging = -1
previewDraggingPos = -1 previewDraggingPos = -1
pmx, pmy = -1, -1
} }
// Scrolling // Scrolling
if me.S != 0 { if me.S != 0 {
if t.window.Enclose(my, mx) && t.merger.Length() > 0 { if t.window.Enclose(my, mx) && t.merger.Length() > 0 {
evt := tui.ScrollUp evt := tui.ScrollUp
if me.Mod { if me.Mod() {
evt = tui.SScrollUp evt = tui.SScrollUp
} }
if me.S < 0 { if me.S < 0 {
evt = tui.ScrollDown evt = tui.ScrollDown
if me.Mod { if me.Mod() {
evt = tui.SScrollDown evt = tui.SScrollDown
} }
} }
@ -5456,7 +5462,7 @@ func (t *Terminal) Loop() error {
} }
// Preview dragging // Preview dragging
if me.Down && (previewDraggingPos >= 0 || clicked && t.hasPreviewWindow() && t.pwindow.Enclose(my, mx)) { if me.Down && (previewDraggingPos >= 0 || click && t.hasPreviewWindow() && t.pwindow.Enclose(my, mx)) {
if previewDraggingPos > 0 { if previewDraggingPos > 0 {
scrollPreviewBy(previewDraggingPos - my) scrollPreviewBy(previewDraggingPos - my)
} }
@ -5466,7 +5472,7 @@ func (t *Terminal) Loop() error {
// Preview scrollbar dragging // Preview scrollbar dragging
headerLines := t.activePreviewOpts.headerLines headerLines := t.activePreviewOpts.headerLines
pbarDragging = me.Down && (pbarDragging || clicked && t.hasPreviewWindow() && my >= t.pwindow.Top()+headerLines && my < t.pwindow.Top()+t.pwindow.Height() && mx == t.pwindow.Left()+t.pwindow.Width()) pbarDragging = me.Down && (pbarDragging || click && t.hasPreviewWindow() && my >= t.pwindow.Top()+headerLines && my < t.pwindow.Top()+t.pwindow.Height() && mx == t.pwindow.Left()+t.pwindow.Width())
if pbarDragging { if pbarDragging {
effectiveHeight := t.pwindow.Height() - headerLines effectiveHeight := t.pwindow.Height() - headerLines
numLines := len(t.previewer.lines) - headerLines numLines := len(t.previewer.lines) - headerLines
@ -5483,7 +5489,7 @@ func (t *Terminal) Loop() error {
} }
// Preview border dragging (resizing) // Preview border dragging (resizing)
if pborderDragging < 0 && clicked && t.hasPreviewWindow() { if pborderDragging < 0 && click && t.hasPreviewWindow() {
switch t.activePreviewOpts.position { switch t.activePreviewOpts.position {
case posUp: case posUp:
if t.pborder.Enclose(my, mx) && my == t.pborder.Top()+t.pborder.Height()-1 { if t.pborder.Enclose(my, mx) && my == t.pborder.Top()+t.pborder.Height()-1 {
@ -5564,9 +5570,7 @@ func (t *Terminal) Loop() error {
} }
// Inside the header window // Inside the header window
// TODO: Should we trigger this on mouse up instead? if clicked && t.headerVisible && t.headerWindow != nil && t.headerWindow.Enclose(my, mx) {
// Should we still trigger it when the position has changed from the down event?
if t.headerVisible && t.headerWindow != nil && t.headerWindow.Enclose(my, mx) {
mx -= t.headerWindow.Left() + t.headerIndent(t.headerBorderShape) mx -= t.headerWindow.Left() + t.headerIndent(t.headerBorderShape)
my -= t.headerWindow.Top() my -= t.headerWindow.Top()
if mx < 0 { if mx < 0 {
@ -5580,7 +5584,7 @@ func (t *Terminal) Loop() error {
return doActions(actionsFor(tui.ClickHeader)) return doActions(actionsFor(tui.ClickHeader))
} }
if t.headerVisible && t.headerLinesWindow != nil && t.headerLinesWindow.Enclose(my, mx) { if clicked && t.headerVisible && t.headerLinesWindow != nil && t.headerLinesWindow.Enclose(my, mx) {
mx -= t.headerLinesWindow.Left() + t.headerIndent(t.headerLinesShape) mx -= t.headerLinesWindow.Left() + t.headerIndent(t.headerLinesShape)
my -= t.headerLinesWindow.Top() my -= t.headerLinesWindow.Top()
if mx < 0 { if mx < 0 {
@ -5616,7 +5620,7 @@ func (t *Terminal) Loop() error {
} }
// Scrollbar dragging // Scrollbar dragging
barDragging = me.Down && (barDragging || clicked && my >= min && mx == t.window.Width()-1) barDragging = me.Down && (barDragging || click && my >= min && mx == t.window.Width()-1)
if barDragging { if barDragging {
barLength, barStart := t.getScrollbar() barLength, barStart := t.getScrollbar()
if barLength > 0 { if barLength > 0 {
@ -5649,7 +5653,6 @@ func (t *Terminal) Loop() error {
return doActions(actionsFor(tui.DoubleClick)) return doActions(actionsFor(tui.DoubleClick))
} }
} }
break
} }
if me.Down { if me.Down {
@ -5661,40 +5664,40 @@ func (t *Terminal) Loop() error {
t.vset(cy) t.vset(cy)
req(reqList) req(reqList)
evt := tui.RightClick evt := tui.RightClick
if me.Mod { if me.Mod() {
evt = tui.SRightClick evt = tui.SRightClick
} }
if me.Left { if me.Left {
evt = tui.LeftClick evt = tui.LeftClick
if me.Mod { if me.Mod() {
evt = tui.SLeftClick evt = tui.SLeftClick
} }
} }
return doActions(actionsFor(evt)) return doActions(actionsFor(evt))
} else if t.headerVisible && t.headerWindow == nil { }
// Header }
// TODO: Should we trigger this on mouse up instead? if clicked && t.headerVisible && t.headerWindow == nil {
numLines := t.visibleHeaderLinesInList() // Header
lineOffset := 0 numLines := t.visibleHeaderLinesInList()
if t.inputWindow == nil && !t.headerFirst { lineOffset := 0
// offset for info line if t.inputWindow == nil && !t.headerFirst {
if t.noSeparatorLine() { // offset for info line
lineOffset = 1 if t.noSeparatorLine() {
} else { lineOffset = 1
lineOffset = 2 } else {
} lineOffset = 2
} }
my -= lineOffset }
mx -= t.pointerLen + t.markerLen my -= lineOffset
if my >= 0 && my < numLines && mx >= 0 { mx -= t.pointerLen + t.markerLen
if t.layout == layoutReverse { if my >= 0 && my < numLines && mx >= 0 {
t.clickHeaderLine = my + 1 if t.layout == layoutReverse {
} else { t.clickHeaderLine = my + 1
t.clickHeaderLine = numLines - my } else {
} t.clickHeaderLine = numLines - my
t.clickHeaderColumn = mx + 1
return doActions(actionsFor(tui.ClickHeader))
} }
t.clickHeaderColumn = mx + 1
return doActions(actionsFor(tui.ClickHeader))
} }
} }
case actReload, actReloadSync: case actReload, actReloadSync:

View File

@ -626,15 +626,13 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
// middle := t & 0b1 // middle := t & 0b1
left := t&0b11 == 0 left := t&0b11 == 0
ctrl := t&0b10000 > 0
// shift := t & 0b100 alt := t&0b01000 > 0
// ctrl := t & 0b1000 shift := t&0b00100 > 0
mod := t&0b1100 > 0 drag := t&0b100000 > 0 // 32
drag := t&0b100000 > 0
if scroll != 0 { if scroll != 0 {
return Event{Mouse, 0, &MouseEvent{y, x, scroll, false, false, false, mod}} return Event{Mouse, 0, &MouseEvent{y, x, scroll, false, false, false, ctrl, alt, shift}}
} }
double := false double := false
@ -658,7 +656,7 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
} }
} }
} }
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}} return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, ctrl, alt, shift}}
} }
func (r *LightRenderer) smcup() { func (r *LightRenderer) smcup() {

View File

@ -266,7 +266,11 @@ func (r *FullscreenRenderer) GetChar() Event {
// so mouse click is three consecutive events, but the first and last are indistinguishable from movement events (with released buttons) // so mouse click is three consecutive events, but the first and last are indistinguishable from movement events (with released buttons)
// dragging has same structure, it only repeats the middle (main) event appropriately // dragging has same structure, it only repeats the middle (main) event appropriately
x, y := ev.Position() x, y := ev.Position()
mod := ev.Modifiers() != 0
mod := ev.Modifiers()
ctrl := (mod & tcell.ModCtrl) > 0
alt := (mod & tcell.ModAlt) > 0
shift := (mod & tcell.ModShift) > 0
// since we dont have mouse down events (unlike LightRenderer), we need to track state in prevButton // since we dont have mouse down events (unlike LightRenderer), we need to track state in prevButton
prevButton, button := _prevMouseButton, ev.Buttons() prevButton, button := _prevMouseButton, ev.Buttons()
@ -275,9 +279,9 @@ func (r *FullscreenRenderer) GetChar() Event {
switch { switch {
case button&tcell.WheelDown != 0: case button&tcell.WheelDown != 0:
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}} return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, ctrl, alt, shift}}
case button&tcell.WheelUp != 0: case button&tcell.WheelUp != 0:
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}} return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, ctrl, alt, shift}}
case button&tcell.Button1 != 0: case button&tcell.Button1 != 0:
double := false double := false
if !drag { if !drag {
@ -300,9 +304,9 @@ func (r *FullscreenRenderer) GetChar() Event {
} }
} }
// fire single or double click event // fire single or double click event
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, mod}} return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, ctrl, alt, shift}}
case button&tcell.Button2 != 0: case button&tcell.Button2 != 0:
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, mod}} return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, ctrl, alt, shift}}
default: default:
// double and single taps on Windows don't quite work due to // double and single taps on Windows don't quite work due to
// the console acting on the events and not allowing us // the console acting on the events and not allowing us
@ -311,7 +315,11 @@ func (r *FullscreenRenderer) GetChar() Event {
down := left || button&tcell.Button3 != 0 down := left || button&tcell.Button3 != 0
double := false double := false
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}} // No need to report mouse movement events when no button is pressed
if drag {
return Event{Invalid, 0, nil}
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, ctrl, alt, shift}}
} }
// process keyboard: // process keyboard:

View File

@ -150,6 +150,10 @@ func (e Event) Comparable() Event {
} }
func (e Event) KeyName() string { func (e Event) KeyName() string {
if me := e.MouseEvent; me != nil {
return me.Name()
}
if e.Type >= Invalid { if e.Type >= Invalid {
return "" return ""
} }
@ -367,7 +371,37 @@ type MouseEvent struct {
Left bool Left bool
Down bool Down bool
Double bool Double bool
Mod bool Ctrl bool
Alt bool
Shift bool
}
func (e MouseEvent) Mod() bool {
return e.Ctrl || e.Alt || e.Shift
}
func (e MouseEvent) Name() string {
name := ""
if e.Down {
return name
}
if e.Ctrl {
name += "ctrl-"
}
if e.Alt {
name += "alt-"
}
if e.Shift {
name += "shift-"
}
if e.Double {
name += "double-"
}
if !e.Left {
name += "right-"
}
return name + "click"
} }
type BorderShape int type BorderShape int