Implement --expect option to support simple key bindings (#163)

pull/170/head
Junegunn Choi 9 years ago
parent 9cfecf7f0b
commit 2a167aa030

@ -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
-----

@ -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
F1
F2
F3
F4
AltBS
AltA
AltB
AltF
AltC
AltD
AltBS
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…
Cancel
Save