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

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
version=0.9.5 version=0.9.6
cd $(dirname $BASH_SOURCE) cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd) fzf_base=$(pwd)

@ -108,6 +108,17 @@ Filter mode. Do not start interactive finder.
.B "--print-query" .B "--print-query"
Print query as the first line Print query as the first line
.TP .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" .B "--sync"
Synchronous search for multi-staged filtering Synchronous search for multi-staged filtering
.RS .RS

@ -61,10 +61,20 @@ const (
PgUp PgUp
PgDn PgDn
F1
F2
F3
F4
AltBS
AltA
AltB AltB
AltF AltC
AltD AltD
AltBS AltE
AltF
AltZ = AltA + 'z' - 'a'
) )
// Pallete // Pallete
@ -324,6 +334,14 @@ func escSequence(sz *int) Event {
return Event{CtrlE, 0, nil} return Event{CtrlE, 0, nil}
case 77: case 77:
return mouseSequence(sz) 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: case 49, 50, 51, 52, 53, 54:
if len(_buf) < 4 { if len(_buf) < 4 {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
@ -369,6 +387,9 @@ func escSequence(sz *int) Event {
} // _buf[2] } // _buf[2]
} // _buf[2] } // _buf[2]
} // _buf[1] } // _buf[1]
if _buf[1] >= 'a' && _buf[1] <= 'z' {
return Event{AltA + int(_buf[1]) - 'a', 0, nil}
}
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }

@ -5,6 +5,9 @@ import (
"os" "os"
"regexp" "regexp"
"strings" "strings"
"unicode/utf8"
"github.com/junegunn/fzf/src/curses"
"github.com/junegunn/go-shellwords" "github.com/junegunn/go-shellwords"
) )
@ -43,6 +46,7 @@ const usage = `usage: fzf [options]
-0, --exit-0 Exit immediately when there's no match -0, --exit-0 Exit immediately when there's no match
-f, --filter=STR Filter mode. Do not start interactive finder. -f, --filter=STR Filter mode. Do not start interactive finder.
--print-query Print query as the first line --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 --sync Synchronous search for multi-staged filtering
(e.g. 'fzf --multi | fzf --sync') (e.g. 'fzf --multi | fzf --sync')
@ -93,6 +97,7 @@ type Options struct {
Select1 bool Select1 bool
Exit0 bool Exit0 bool
Filter *string Filter *string
Expect []int
PrintQuery bool PrintQuery bool
Sync bool Sync bool
Version bool Version bool
@ -119,6 +124,7 @@ func defaultOptions() *Options {
Select1: false, Select1: false,
Exit0: false, Exit0: false,
Filter: nil, Filter: nil,
Expect: []int{},
PrintQuery: false, PrintQuery: false,
Sync: false, Sync: false,
Version: false} Version: false}
@ -191,6 +197,29 @@ func delimiterRegexp(str string) *regexp.Regexp {
return rx 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) { func parseOptions(opts *Options, allArgs []string) {
for i := 0; i < len(allArgs); i++ { for i := 0; i < len(allArgs); i++ {
arg := allArgs[i] arg := allArgs[i]
@ -208,6 +237,8 @@ func parseOptions(opts *Options, allArgs []string) {
case "-f", "--filter": case "-f", "--filter":
filter := nextString(allArgs, &i, "query string required") filter := nextString(allArgs, &i, "query string required")
opts.Filter = &filter opts.Filter = &filter
case "--expect":
opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"))
case "-d", "--delimiter": case "-d", "--delimiter":
opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required")) opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required"))
case "-n", "--nth": case "-n", "--nth":
@ -285,6 +316,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.WithNth = splitNth(value) opts.WithNth = splitNth(value)
} else if match, _ := optString(arg, "-s|--sort="); match { } else if match, _ := optString(arg, "-s|--sort="); match {
opts.Sort = 1 // Don't care opts.Sort = 1 // Don't care
} else if match, value := optString(arg, "--expect="); match {
opts.Expect = parseKeyChords(value)
} else { } else {
errorExit("unknown option: " + arg) errorExit("unknown option: " + arg)
} }

@ -1,6 +1,10 @@
package fzf package fzf
import "testing" import (
"testing"
"github.com/junegunn/fzf/src/curses"
)
func TestDelimiterRegex(t *testing.T) { func TestDelimiterRegex(t *testing.T) {
rx := delimiterRegexp("*") 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 yanked []rune
input []rune input []rune
multi bool multi bool
expect []int
pressed int
printQuery bool printQuery bool
count int count int
progress int progress int
@ -91,6 +93,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
yanked: []rune{}, yanked: []rune{},
input: input, input: input,
multi: opts.Multi, multi: opts.Multi,
expect: opts.Expect,
pressed: 0,
printQuery: opts.PrintQuery, printQuery: opts.PrintQuery,
merger: EmptyMerger, merger: EmptyMerger,
selected: make(map[*string]selectedItem), selected: make(map[*string]selectedItem),
@ -150,6 +154,19 @@ func (t *Terminal) output() {
if t.printQuery { if t.printQuery {
fmt.Println(string(t.input)) 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 { if len(t.selected) == 0 {
cnt := t.merger.Length() cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy { if cnt > 0 && cnt > t.cy {
@ -535,6 +552,13 @@ func (t *Terminal) Loop() {
req(reqInfo) 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 { switch event.Type {
case C.Invalid: case C.Invalid:
t.mutex.Unlock() t.mutex.Unlock()

@ -61,6 +61,10 @@ func DurWithin(
return val return val
} }
func Between(val int, min int, max int) bool {
return val >= min && val <= max
}
// IsTty returns true is stdin is a terminal // IsTty returns true is stdin is a terminal
func IsTty() bool { func IsTty() bool {
return int(C.isatty(C.int(os.Stdin.Fd()))) != 0 return int(C.isatty(C.int(os.Stdin.Fd()))) != 0

@ -425,6 +425,33 @@ class TestGoFZF < TestBase
tmux.send_keys :BTab, :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab, :BTab, :Enter
assert_equal %w[1000 900 800], readonce.split($/) assert_equal %w[1000 900 800], readonce.split($/)
end 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 end
module TestShell module TestShell

Loading…
Cancel
Save