From 80da0776f85612d527e380226734f9372e7a0719 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 27 Jan 2025 01:46:21 +0900 Subject: [PATCH] Allow actions to multiple keys and events at once Close #4206 --- ADVANCED.md | 10 +++------- man/man1/fzf.1 | 4 +++- src/options.go | 45 ++++++++++++++++++++++++++++----------------- test/test_core.rb | 4 ++-- 4 files changed, 36 insertions(+), 27 deletions(-) diff --git a/ADVANCED.md b/ADVANCED.md index 69c738a9..a045140c 100644 --- a/ADVANCED.md +++ b/ADVANCED.md @@ -515,8 +515,6 @@ remainder of the query is passed to fzf for secondary filtering. ```sh #!/usr/bin/env bash -# Switch between Ripgrep mode and fzf filtering mode (CTRL-T) -RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " INITIAL_QUERY="${*:-}" TRANSFORMER=' words=($FZF_QUERY) @@ -530,15 +528,14 @@ TRANSFORMER=' # restart ripgrep and reload the list elif ! [[ $FZF_QUERY =~ \ $ ]]; then pat=${words[0]} - echo "reload:sleep 0.1; $RG_PREFIX \"$pat\" || true" + echo "reload:sleep 0.1; rg --column --line-number --no-heading --color=always --smart-case \"$pat\" || true" else echo search: fi ' fzf --ansi --disabled --query "$INITIAL_QUERY" \ --with-shell 'bash -c' \ - --bind "start:transform:$TRANSFORMER" \ - --bind "change:transform:$TRANSFORMER" \ + --bind "start,change:transform:$TRANSFORMER" \ --color "hl:-1:underline,hl+:-1:underline:reverse" \ --delimiter : \ --preview 'bat --color=always {1} --highlight-line {2}' \ @@ -575,8 +572,7 @@ pods() { --info=inline --layout=reverse --header-lines=1 \ --prompt "$(kubectl config current-context | sed 's/-context$//')> " \ --header $'╱ Enter (kubectl exec) ╱ CTRL-O (open log in editor) ╱ CTRL-R (reload) ╱\n\n' \ - --bind 'start:reload:$command' \ - --bind 'ctrl-r:reload:$command' \ + --bind 'start,ctrl-r:reload:$command' \ --bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \ --bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash' \ --bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2})' \ diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index b5f0ce68..a36384ae 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -1291,7 +1291,9 @@ more \fBactions\fR. You can use it to customize key bindings or implement dynamic behaviors. \fB\-\-bind\fR takes a comma-separated list of binding expressions. Each binding -expression is \fBKEY:ACTION\fR or \fBEVENT:ACTION\fR. +expression is \fBKEY:ACTION\fR or \fBEVENT:ACTION\fR. You can bind actions to +multiple keys and events by writing comma-separated list of keys and events +before the colon. e.g. \fBKEY1,KEY2,EVENT1,EVENT2:ACTION\fR. e.g. \fBfzf \-\-bind=ctrl\-j:accept,ctrl\-k:kill\-line\fR diff --git a/src/options.go b/src/options.go index 5865f0ea..4d5d0ca2 100644 --- a/src/options.go +++ b/src/options.go @@ -1638,33 +1638,44 @@ func parseKeymap(keymap map[tui.Event][]*action, str string) error { var err error masked := maskActionContents(str) idx := 0 + keys := []string{} for _, pairStr := range strings.Split(masked, ",") { origPairStr := str[idx : idx+len(pairStr)] idx += len(pairStr) + 1 pair := strings.SplitN(pairStr, ":", 2) - if len(pair) < 2 { - return errors.New("bind action not specified: " + origPairStr) + if len(pair[0]) == 0 { + return errors.New("key name required") } - var key tui.Event - if len(pair[0]) == 1 && pair[0][0] == escapedColon { - key = tui.Key(':') - } else if len(pair[0]) == 1 && pair[0][0] == escapedComma { - key = tui.Key(',') - } else if len(pair[0]) == 1 && pair[0][0] == escapedPlus { - key = tui.Key('+') - } else { - keys, err := parseKeyChordsImpl(pair[0], "key name required") + keys = append(keys, pair[0]) + if len(pair) < 2 { + continue + } + for _, keyName := range keys { + var key tui.Event + if len(keyName) == 1 && keyName[0] == escapedColon { + key = tui.Key(':') + } else if len(keyName) == 1 && keyName[0] == escapedComma { + key = tui.Key(',') + } else if len(keyName) == 1 && keyName[0] == escapedPlus { + key = tui.Key('+') + } else { + keys, err := parseKeyChordsImpl(keyName, "key name required") + if err != nil { + return err + } + key = firstKey(keys) + } + putAllowed := key.Type == tui.Rune && unicode.IsGraphic(key.Char) + keymap[key], err = parseActionList(pair[1], origPairStr[len(pair[0])+1:], keymap[key], putAllowed) if err != nil { return err } - key = firstKey(keys) - } - putAllowed := key.Type == tui.Rune && unicode.IsGraphic(key.Char) - keymap[key], err = parseActionList(pair[1], origPairStr[len(pair[0])+1:], keymap[key], putAllowed) - if err != nil { - return err } + keys = keys[:0] + } + if len(keys) > 0 { + return errors.New("bind action not specified: " + strings.Join(keys, ", ")) } return nil } diff --git a/test/test_core.rb b/test/test_core.rb index 0d80644b..75cf3d77 100644 --- a/test/test_core.rb +++ b/test/test_core.rb @@ -418,9 +418,9 @@ class TestCore < TestInteractive end def test_bind - tmux.send_keys "seq 1 1000 | #{fzf('-m --bind=ctrl-j:accept,u:up,T:toggle-up,t:toggle')}", :Enter + tmux.send_keys "seq 1 1000 | #{fzf('-m --bind=ctrl-j:accept,u,U:up,X,Y,Z:toggle-up,t:toggle')}", :Enter tmux.until { |lines| assert_equal ' 1000/1000 (0)', lines[-2] } - tmux.send_keys 'uuu', 'TTT', 'tt', 'uu', 'ttt', 'C-j' + tmux.send_keys 'uUu', 'XYZ', 'tt', 'uu', 'ttt', 'C-j' assert_equal %w[4 5 6 9], fzf_output_lines end