diff --git a/CHANGELOG.md b/CHANGELOG.md index cb004226..ef2c957b 100644 --- a/CHANGELOG.md +++ b/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 ----- diff --git a/install b/install index 8f9d355c..eb85a8bc 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash -version=0.9.5 +version=0.9.6 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index bfb358f4..c6bf054e 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 diff --git a/src/curses/curses.go b/src/curses/curses.go index dfd7cf51..d6aafd71 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -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} } diff --git a/src/options.go b/src/options.go index 73c9e188..89b1c368 100644 --- a/src/options.go +++ b/src/options.go @@ -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) } diff --git a/src/options_test.go b/src/options_test.go index 782ad791..b20cd6a3 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -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') +} diff --git a/src/terminal.go b/src/terminal.go index 5570f8d1..2d191a90 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -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() diff --git a/src/util/util.go b/src/util/util.go index 1f53cc76..2d680b1a 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -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 diff --git a/test/test_go.rb b/test/test_go.rb index 6f67da74..adfc0d01 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -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