Implement --expect option to support simple key bindings (#163)
This commit is contained in:
parent
9cfecf7f0b
commit
2a167aa030
24
CHANGELOG.md
24
CHANGELOG.md
@ -1,6 +1,30 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.9.6
|
||||
-----
|
||||
|
||||
### New features
|
||||
|
||||
#### Added `--expect` option (#163)
|
||||
|
||||
If you provide a comma-separated list of keys with `--expect` option, fzf will
|
||||
allow you to select the match and complete the finder when any of the keys is
|
||||
pressed. Additionally, fzf will print the name of the key pressed as the first
|
||||
line of the output so that your script can decide what to do next based on the
|
||||
information.
|
||||
|
||||
```sh
|
||||
fzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@
|
||||
```
|
||||
|
||||
The updated vim plugin uses this option to implement
|
||||
[ctrlp](https://github.com/kien/ctrlp.vim)-compatible key bindings.
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Fixed to ignore ANSI escape code `\e[K` (#162)
|
||||
|
||||
0.9.5
|
||||
-----
|
||||
|
||||
|
2
install
2
install
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
version=0.9.5
|
||||
version=0.9.6
|
||||
|
||||
cd $(dirname $BASH_SOURCE)
|
||||
fzf_base=$(pwd)
|
||||
|
@ -108,6 +108,17 @@ Filter mode. Do not start interactive finder.
|
||||
.B "--print-query"
|
||||
Print query as the first line
|
||||
.TP
|
||||
.BI "--expect=" "KEY[,..]"
|
||||
Comma-separated list of keys (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR,
|
||||
or a single character) that can be used to complete fzf in addition to the
|
||||
default enter key. When this option is set, fzf will print the name of the key
|
||||
pressed as the first line of its output (or as the second line if
|
||||
\fB--print-query\fR is also used). The line will be empty if fzf is completed
|
||||
with the default enter key.
|
||||
.RS
|
||||
e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR
|
||||
.RE
|
||||
.TP
|
||||
.B "--sync"
|
||||
Synchronous search for multi-staged filtering
|
||||
.RS
|
||||
|
@ -61,10 +61,20 @@ const (
|
||||
PgUp
|
||||
PgDn
|
||||
|
||||
AltB
|
||||
AltF
|
||||
AltD
|
||||
F1
|
||||
F2
|
||||
F3
|
||||
F4
|
||||
|
||||
AltBS
|
||||
AltA
|
||||
AltB
|
||||
AltC
|
||||
AltD
|
||||
AltE
|
||||
AltF
|
||||
|
||||
AltZ = AltA + 'z' - 'a'
|
||||
)
|
||||
|
||||
// Pallete
|
||||
@ -324,6 +334,14 @@ func escSequence(sz *int) Event {
|
||||
return Event{CtrlE, 0, nil}
|
||||
case 77:
|
||||
return mouseSequence(sz)
|
||||
case 80:
|
||||
return Event{F1, 0, nil}
|
||||
case 81:
|
||||
return Event{F2, 0, nil}
|
||||
case 82:
|
||||
return Event{F3, 0, nil}
|
||||
case 83:
|
||||
return Event{F4, 0, nil}
|
||||
case 49, 50, 51, 52, 53, 54:
|
||||
if len(_buf) < 4 {
|
||||
return Event{Invalid, 0, nil}
|
||||
@ -369,6 +387,9 @@ func escSequence(sz *int) Event {
|
||||
} // _buf[2]
|
||||
} // _buf[2]
|
||||
} // _buf[1]
|
||||
if _buf[1] >= 'a' && _buf[1] <= 'z' {
|
||||
return Event{AltA + int(_buf[1]) - 'a', 0, nil}
|
||||
}
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,9 @@ import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/junegunn/fzf/src/curses"
|
||||
|
||||
"github.com/junegunn/go-shellwords"
|
||||
)
|
||||
@ -43,6 +46,7 @@ const usage = `usage: fzf [options]
|
||||
-0, --exit-0 Exit immediately when there's no match
|
||||
-f, --filter=STR Filter mode. Do not start interactive finder.
|
||||
--print-query Print query as the first line
|
||||
--expect=KEYS Comma-separated list of keys to complete fzf
|
||||
--sync Synchronous search for multi-staged filtering
|
||||
(e.g. 'fzf --multi | fzf --sync')
|
||||
|
||||
@ -93,6 +97,7 @@ type Options struct {
|
||||
Select1 bool
|
||||
Exit0 bool
|
||||
Filter *string
|
||||
Expect []int
|
||||
PrintQuery bool
|
||||
Sync bool
|
||||
Version bool
|
||||
@ -119,6 +124,7 @@ func defaultOptions() *Options {
|
||||
Select1: false,
|
||||
Exit0: false,
|
||||
Filter: nil,
|
||||
Expect: []int{},
|
||||
PrintQuery: false,
|
||||
Sync: false,
|
||||
Version: false}
|
||||
@ -191,6 +197,29 @@ func delimiterRegexp(str string) *regexp.Regexp {
|
||||
return rx
|
||||
}
|
||||
|
||||
func isAlphabet(char uint8) bool {
|
||||
return char >= 'a' && char <= 'z'
|
||||
}
|
||||
|
||||
func parseKeyChords(str string) []int {
|
||||
var chords []int
|
||||
for _, key := range strings.Split(str, ",") {
|
||||
lkey := strings.ToLower(key)
|
||||
if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
|
||||
chords = append(chords, curses.CtrlA+int(lkey[5])-'a')
|
||||
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) {
|
||||
chords = append(chords, curses.AltA+int(lkey[4])-'a')
|
||||
} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '4' {
|
||||
chords = append(chords, curses.F1+int(key[1])-'1')
|
||||
} else if utf8.RuneCountInString(key) == 1 {
|
||||
chords = append(chords, curses.AltZ+int([]rune(key)[0]))
|
||||
} else {
|
||||
errorExit("unsupported key: " + key)
|
||||
}
|
||||
}
|
||||
return chords
|
||||
}
|
||||
|
||||
func parseOptions(opts *Options, allArgs []string) {
|
||||
for i := 0; i < len(allArgs); i++ {
|
||||
arg := allArgs[i]
|
||||
@ -208,6 +237,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
case "-f", "--filter":
|
||||
filter := nextString(allArgs, &i, "query string required")
|
||||
opts.Filter = &filter
|
||||
case "--expect":
|
||||
opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"))
|
||||
case "-d", "--delimiter":
|
||||
opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required"))
|
||||
case "-n", "--nth":
|
||||
@ -285,6 +316,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
opts.WithNth = splitNth(value)
|
||||
} else if match, _ := optString(arg, "-s|--sort="); match {
|
||||
opts.Sort = 1 // Don't care
|
||||
} else if match, value := optString(arg, "--expect="); match {
|
||||
opts.Expect = parseKeyChords(value)
|
||||
} else {
|
||||
errorExit("unknown option: " + arg)
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
package fzf
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/curses"
|
||||
)
|
||||
|
||||
func TestDelimiterRegex(t *testing.T) {
|
||||
rx := delimiterRegexp("*")
|
||||
@ -65,3 +69,22 @@ func TestIrrelevantNth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpectKeys(t *testing.T) {
|
||||
keys := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g")
|
||||
check := func(key int, expected int) {
|
||||
if key != expected {
|
||||
t.Errorf("%d != %d", key, expected)
|
||||
}
|
||||
}
|
||||
check(len(keys), 9)
|
||||
check(keys[0], curses.CtrlZ)
|
||||
check(keys[1], curses.AltZ)
|
||||
check(keys[2], curses.F2)
|
||||
check(keys[3], curses.AltZ+'@')
|
||||
check(keys[4], curses.AltA)
|
||||
check(keys[5], curses.AltZ+'!')
|
||||
check(keys[6], curses.CtrlA+'g'-'a')
|
||||
check(keys[7], curses.AltZ+'J')
|
||||
check(keys[8], curses.AltZ+'g')
|
||||
}
|
||||
|
@ -28,6 +28,8 @@ type Terminal struct {
|
||||
yanked []rune
|
||||
input []rune
|
||||
multi bool
|
||||
expect []int
|
||||
pressed int
|
||||
printQuery bool
|
||||
count int
|
||||
progress int
|
||||
@ -91,6 +93,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
yanked: []rune{},
|
||||
input: input,
|
||||
multi: opts.Multi,
|
||||
expect: opts.Expect,
|
||||
pressed: 0,
|
||||
printQuery: opts.PrintQuery,
|
||||
merger: EmptyMerger,
|
||||
selected: make(map[*string]selectedItem),
|
||||
@ -150,6 +154,19 @@ func (t *Terminal) output() {
|
||||
if t.printQuery {
|
||||
fmt.Println(string(t.input))
|
||||
}
|
||||
if len(t.expect) > 0 {
|
||||
if t.pressed == 0 {
|
||||
fmt.Println()
|
||||
} else if util.Between(t.pressed, C.AltA, C.AltZ) {
|
||||
fmt.Printf("alt-%c\n", t.pressed+'a'-C.AltA)
|
||||
} else if util.Between(t.pressed, C.F1, C.F4) {
|
||||
fmt.Printf("f%c\n", t.pressed+'1'-C.F1)
|
||||
} else if util.Between(t.pressed, C.CtrlA, C.CtrlZ) {
|
||||
fmt.Printf("ctrl-%c\n", t.pressed+'a'-C.CtrlA)
|
||||
} else {
|
||||
fmt.Printf("%c\n", t.pressed-C.AltZ)
|
||||
}
|
||||
}
|
||||
if len(t.selected) == 0 {
|
||||
cnt := t.merger.Length()
|
||||
if cnt > 0 && cnt > t.cy {
|
||||
@ -535,6 +552,13 @@ func (t *Terminal) Loop() {
|
||||
req(reqInfo)
|
||||
}
|
||||
}
|
||||
for _, key := range t.expect {
|
||||
if event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ {
|
||||
t.pressed = key
|
||||
req(reqClose)
|
||||
break
|
||||
}
|
||||
}
|
||||
switch event.Type {
|
||||
case C.Invalid:
|
||||
t.mutex.Unlock()
|
||||
|
@ -61,6 +61,10 @@ func DurWithin(
|
||||
return val
|
||||
}
|
||||
|
||||
func Between(val int, min int, max int) bool {
|
||||
return val >= min && val <= max
|
||||
}
|
||||
|
||||
// IsTty returns true is stdin is a terminal
|
||||
func IsTty() bool {
|
||||
return int(C.isatty(C.int(os.Stdin.Fd()))) != 0
|
||||
|
@ -425,6 +425,33 @@ class TestGoFZF < TestBase
|
||||
tmux.send_keys :BTab, :BTab, :BTab, :Enter
|
||||
assert_equal %w[1000 900 800], readonce.split($/)
|
||||
end
|
||||
|
||||
def test_expect
|
||||
test = lambda do |key, feed, expected = key|
|
||||
tmux.send_keys "seq 1 100 | #{fzf :expect, key}", :Enter
|
||||
tmux.until { |lines| lines[-2].include? '100/100' }
|
||||
tmux.send_keys '55'
|
||||
tmux.send_keys *feed
|
||||
assert_equal [expected, '55'], readonce.split($/)
|
||||
end
|
||||
test.call 'ctrl-t', 'C-T'
|
||||
test.call 'ctrl-t', 'Enter', ''
|
||||
test.call 'alt-c', [:Escape, :c]
|
||||
test.call 'f1', 'f1'
|
||||
test.call 'f2', 'f2'
|
||||
test.call 'f3', 'f3'
|
||||
test.call 'f2,f4', 'f2', 'f2'
|
||||
test.call 'f2,f4', 'f4', 'f4'
|
||||
test.call '@', '@'
|
||||
end
|
||||
|
||||
def test_expect_print_query
|
||||
tmux.send_keys "seq 1 100 | #{fzf '--expect=alt-z', :print_query}", :Enter
|
||||
tmux.until { |lines| lines[-2].include? '100/100' }
|
||||
tmux.send_keys '55'
|
||||
tmux.send_keys :Escape, :z
|
||||
assert_equal ['55', 'alt-z', '55'], readonce.split($/)
|
||||
end
|
||||
end
|
||||
|
||||
module TestShell
|
||||
|
Loading…
x
Reference in New Issue
Block a user