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,
# restart ripgrep and reload the list
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
'
fzf --ansi --disabled --query "$INITIAL_QUERY" \

View File

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

View File

@ -409,8 +409,32 @@ _fzf_complete_kill() {
}
_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 \
--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,time,args 2> /dev/null || # For BusyBox
command ps --everyone --full --windows # For cygwin

View File

@ -290,8 +290,32 @@ _fzf_complete_unalias() {
}
_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 \
--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,time,args 2> /dev/null || # For BusyBox
command ps --everyone --full --windows # For cygwin

View File

@ -4620,6 +4620,7 @@ func (t *Terminal) Loop() error {
pbarDragging := false
pborderDragging := -1
wasDown := false
pmx, pmy := -1, -1
needBarrier := true
// 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:
me := event.MouseEvent
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
if click {
pmx, pmy = mx, my
}
if !me.Down {
barDragging = false
pbarDragging = false
pborderDragging = -1
previewDraggingPos = -1
pmx, pmy = -1, -1
}
// Scrolling
if me.S != 0 {
if t.window.Enclose(my, mx) && t.merger.Length() > 0 {
evt := tui.ScrollUp
if me.Mod {
if me.Mod() {
evt = tui.SScrollUp
}
if me.S < 0 {
evt = tui.ScrollDown
if me.Mod {
if me.Mod() {
evt = tui.SScrollDown
}
}
@ -5456,7 +5462,7 @@ func (t *Terminal) Loop() error {
}
// 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 {
scrollPreviewBy(previewDraggingPos - my)
}
@ -5466,7 +5472,7 @@ func (t *Terminal) Loop() error {
// Preview scrollbar dragging
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 {
effectiveHeight := t.pwindow.Height() - headerLines
numLines := len(t.previewer.lines) - headerLines
@ -5483,7 +5489,7 @@ func (t *Terminal) Loop() error {
}
// Preview border dragging (resizing)
if pborderDragging < 0 && clicked && t.hasPreviewWindow() {
if pborderDragging < 0 && click && t.hasPreviewWindow() {
switch t.activePreviewOpts.position {
case posUp:
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
// TODO: Should we trigger this on mouse up instead?
// 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) {
if clicked && t.headerVisible && t.headerWindow != nil && t.headerWindow.Enclose(my, mx) {
mx -= t.headerWindow.Left() + t.headerIndent(t.headerBorderShape)
my -= t.headerWindow.Top()
if mx < 0 {
@ -5580,7 +5584,7 @@ func (t *Terminal) Loop() error {
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)
my -= t.headerLinesWindow.Top()
if mx < 0 {
@ -5616,7 +5620,7 @@ func (t *Terminal) Loop() error {
}
// 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 {
barLength, barStart := t.getScrollbar()
if barLength > 0 {
@ -5649,7 +5653,6 @@ func (t *Terminal) Loop() error {
return doActions(actionsFor(tui.DoubleClick))
}
}
break
}
if me.Down {
@ -5661,40 +5664,40 @@ func (t *Terminal) Loop() error {
t.vset(cy)
req(reqList)
evt := tui.RightClick
if me.Mod {
if me.Mod() {
evt = tui.SRightClick
}
if me.Left {
evt = tui.LeftClick
if me.Mod {
if me.Mod() {
evt = tui.SLeftClick
}
}
return doActions(actionsFor(evt))
} else if t.headerVisible && t.headerWindow == nil {
// Header
// TODO: Should we trigger this on mouse up instead?
numLines := t.visibleHeaderLinesInList()
lineOffset := 0
if t.inputWindow == nil && !t.headerFirst {
// offset for info line
if t.noSeparatorLine() {
lineOffset = 1
} else {
lineOffset = 2
}
}
}
if clicked && t.headerVisible && t.headerWindow == nil {
// Header
numLines := t.visibleHeaderLinesInList()
lineOffset := 0
if t.inputWindow == nil && !t.headerFirst {
// offset for info line
if t.noSeparatorLine() {
lineOffset = 1
} else {
lineOffset = 2
}
my -= lineOffset
mx -= t.pointerLen + t.markerLen
if my >= 0 && my < numLines && mx >= 0 {
if t.layout == layoutReverse {
t.clickHeaderLine = my + 1
} else {
t.clickHeaderLine = numLines - my
}
t.clickHeaderColumn = mx + 1
return doActions(actionsFor(tui.ClickHeader))
}
my -= lineOffset
mx -= t.pointerLen + t.markerLen
if my >= 0 && my < numLines && mx >= 0 {
if t.layout == layoutReverse {
t.clickHeaderLine = my + 1
} else {
t.clickHeaderLine = numLines - my
}
t.clickHeaderColumn = mx + 1
return doActions(actionsFor(tui.ClickHeader))
}
}
case actReload, actReloadSync:

View File

@ -626,15 +626,13 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
// middle := t & 0b1
left := t&0b11 == 0
// shift := t & 0b100
// ctrl := t & 0b1000
mod := t&0b1100 > 0
drag := t&0b100000 > 0
ctrl := t&0b10000 > 0
alt := t&0b01000 > 0
shift := t&0b00100 > 0
drag := t&0b100000 > 0 // 32
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
@ -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() {

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)
// dragging has same structure, it only repeats the middle (main) event appropriately
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
prevButton, button := _prevMouseButton, ev.Buttons()
@ -275,9 +279,9 @@ func (r *FullscreenRenderer) GetChar() Event {
switch {
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:
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:
double := false
if !drag {
@ -300,9 +304,9 @@ func (r *FullscreenRenderer) GetChar() 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:
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:
// double and single taps on Windows don't quite work due to
// 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
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:

View File

@ -150,6 +150,10 @@ func (e Event) Comparable() Event {
}
func (e Event) KeyName() string {
if me := e.MouseEvent; me != nil {
return me.Name()
}
if e.Type >= Invalid {
return ""
}
@ -367,7 +371,37 @@ type MouseEvent struct {
Left bool
Down 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