m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-14 22:33:47 -05:00

Compare commits

...

26 Commits
0.9.1 ... 0.9.3

Author SHA1 Message Date
Junegunn Choi
4a1752d3fc 0.9.3 2015-02-18 13:19:20 +09:00
Junegunn Choi
b9b1eeffce Update Vader tests 2015-02-18 12:12:59 +09:00
Junegunn Choi
5667667d1f Add test case for --sync option 2015-02-18 12:07:54 +09:00
Junegunn Choi
f5b034095a Fix race condition in asynchronous -1 and -0 2015-02-18 00:51:44 +09:00
Junegunn Choi
95e5beb34e Update Homebrew instruction 2015-02-18 00:22:17 +09:00
Junegunn Choi
e808151c28 Make --select-1 and --exit-0 asynchronous 2015-02-18 00:08:17 +09:00
Junegunn Choi
d760b790b3 Fix typo in code 2015-02-17 19:28:10 +09:00
Junegunn Choi
1b5599972a Update installation instruction 2015-02-17 13:15:16 +09:00
Junegunn Choi
6c2ce28d0d Add --sync option 2015-02-13 12:25:19 +09:00
Junegunn Choi
ff09c275d4 Fix bash script when fzf_base contains spaces 2015-02-12 10:14:05 +09:00
Junegunn Choi
93dcd932e8 Merge pull request #123 from junegunn/fix-travis-ci
Fix Travis CI build
2015-01-29 17:44:11 +09:00
Junegunn Choi
e6a0de4094 Fix Travis CI build 2015-01-29 17:41:28 +09:00
Junegunn Choi
9f39671e65 Update README.md
Update outdated --help output
2015-01-28 01:45:34 +09:00
Junegunn Choi
423317b82a Update README.md 2015-01-28 01:18:20 +09:00
Junegunn Choi
47201c2c4d Merge pull request #122 from blueyed/improve-find-cdwidget
Improve `find` command for ALT-C: exclude proc/dev
2015-01-25 11:20:20 +09:00
Daniel Hahler
53d5d9d162 Improve find command for cd widgets: exclude proc/dev etc
When using the widget in "/", it would descend into 'dev/'.
Using '*' for the starting path would do so also with the new '-fstype'
excludes.

`cut -b3-` and `sed 1d` have been added to massage the different format
of the list.

This also uses `-L` with all calls to find, especially for the file
finders.

Ref: https://github.com/junegunn/fzf/pull/122
2015-01-25 03:09:02 +01:00
Junegunn Choi
9cb0cdb4ac 0.9.2 2015-01-24 14:49:21 +09:00
Junegunn Choi
448132c46c Fix error when --query contains wide-length characters 2015-01-24 13:26:33 +09:00
Junegunn Choi
1476fc7f3b Refactor test code 2015-01-24 13:25:11 +09:00
Junegunn Choi
71a7b3a26f Improve rendering performance by caching rune widths
Related: 8bead4a
2015-01-24 12:28:00 +09:00
Junegunn Choi
a47c06cb61 Fix update_assets script 2015-01-23 20:32:56 +09:00
Junegunn Choi
48e16edb47 Redraw and adjust upon terminal resize 2015-01-23 20:30:50 +09:00
Junegunn Choi
c35d98dc42 Nullify --nth option when it's irrelevant 2015-01-23 06:26:00 +09:00
Junegunn Choi
8bead4ae34 Improved handling of tab characters 2015-01-18 16:59:04 +09:00
Junegunn Choi
1b6cb3532d Update src/README.md 2015-01-18 16:34:10 +09:00
Junegunn Choi
0a0955755a Add note on installation 2015-01-18 16:32:37 +09:00
18 changed files with 386 additions and 166 deletions

View File

@@ -1,6 +1,7 @@
language: ruby language: ruby
install: install:
- sudo apt-get update
- sudo apt-get install -y libncurses-dev lib32ncurses5-dev - sudo apt-get install -y libncurses-dev lib32ncurses5-dev
- sudo add-apt-repository -y ppa:pi-rho/dev - sudo add-apt-repository -y ppa:pi-rho/dev
- sudo apt-get update - sudo apt-get update

View File

@@ -11,6 +11,20 @@ the likes.
Installation Installation
------------ ------------
fzf project consists of the followings:
- `fzf` executable
- Shell extensions
- Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish)
- Fuzzy auto-completion (bash)
You can [download fzf executable][bin] alone, but it's recommended that you
install the extra stuff using the attached install script.
[bin]: https://github.com/junegunn/fzf-bin/releases
### Using git (recommended)
Clone this repository and run Clone this repository and run
[install](https://github.com/junegunn/fzf/blob/master/install) script. [install](https://github.com/junegunn/fzf/blob/master/install) script.
@@ -19,6 +33,8 @@ git clone https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install ~/.fzf/install
``` ```
### Using curl
In case you don't have git installed: In case you don't have git installed:
```sh ```sh
@@ -28,15 +44,16 @@ curl -L https://github.com/junegunn/fzf/archive/master.tar.gz |
~/.fzf/install ~/.fzf/install
``` ```
The script will setup: ### Using Homebrew
- `fzf` function (bash, zsh, fish) On OS X, you can use [Homebrew](http://brew.sh/) to install fzf.
- Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish)
- Fuzzy auto-completion (bash)
If you don't use any of the aforementioned shells, you have to manually place ```sh
fzf executable in a directory included in `$PATH`. Key bindings and brew install fzf
auto-completion will not be available in that case.
# Install shell extensions - this should be done whenever fzf is updated
/usr/local/Cellar/fzf/$(fzf --version)/install
```
### Install as Vim plugin ### Install as Vim plugin
@@ -46,8 +63,7 @@ Once you have cloned the repository, add the following line to your .vimrc.
set rtp+=~/.fzf set rtp+=~/.fzf
``` ```
Or you may use [vim-plug](https://github.com/junegunn/vim-plug) to manage fzf Or you can have [vim-plug](https://github.com/junegunn/vim-plug) manage fzf:
inside Vim:
```vim ```vim
Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': 'yes \| ./install' } Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': 'yes \| ./install' }
@@ -71,7 +87,7 @@ usage: fzf [options]
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
Search result Search result
-s, --sort=MAX Maximum number of matched items to sort (default: 1000) -s, --sort Sort the result
+s, --no-sort Do not sort the result. Keep the sequence unchanged. +s, --no-sort Do not sort the result. Keep the sequence unchanged.
Interface Interface
@@ -89,10 +105,12 @@ 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
--sync Synchronous search for multi-staged filtering
(e.g. 'fzf --multi | fzf --sync')
Environment variables Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty FZF_DEFAULT_COMMAND Default command to use when input is tty
FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m --sort 10000") FZF_DEFAULT_OPTS Defaults options. (e.g. '-x -m')
``` ```
fzf will launch curses-based finder, read the list from STDIN, and write the fzf will launch curses-based finder, read the list from STDIN, and write the
@@ -542,4 +560,3 @@ Author
------ ------
Junegunn Choi Junegunn Choi

29
install
View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
version=0.9.1 version=0.9.3
cd $(dirname $BASH_SOURCE) cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd) fzf_base=$(pwd)
@@ -159,7 +159,7 @@ for shell in bash zsh; do
echo -n "Generate ~/.fzf.$shell ... " echo -n "Generate ~/.fzf.$shell ... "
src=~/.fzf.${shell} src=~/.fzf.${shell}
fzf_completion="[[ \$- =~ i ]] && source $fzf_base/fzf-completion.${shell}" fzf_completion="[[ \$- =~ i ]] && source \"$fzf_base/fzf-completion.${shell}\""
if [ $auto_completion -ne 0 ]; then if [ $auto_completion -ne 0 ]; then
fzf_completion="# $fzf_completion" fzf_completion="# $fzf_completion"
fi fi
@@ -202,10 +202,10 @@ EOF
# Key bindings # Key bindings
# ------------ # ------------
__fsel() { __fsel() {
command find * -path '*/\.*' -prune \ command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | fzf -m | while read item; do -o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf -m | while read item; do
printf '%q ' "$item" printf '%q ' "$item"
done done
echo echo
@@ -226,7 +226,8 @@ __fsel_tmux() {
__fcd() { __fcd() {
local dir local dir
dir=$(command find -L ${1:-*} -path '*/\.*' -prune -o -type d -print 2> /dev/null | fzf +m) && printf 'cd %q' "$dir" dir=$(command find -L ${1:-.} \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3- | fzf +m) && printf 'cd %q' "$dir"
} }
__use_tmux=0 __use_tmux=0
@@ -274,17 +275,16 @@ unset __use_tmux
fi fi
EOFZF EOFZF
else else # zsh
cat >> $src << "EOFZF" cat >> $src << "EOFZF"
# Key bindings # Key bindings
# ------------ # ------------
# CTRL-T - Paste the selected file path(s) into the command line # CTRL-T - Paste the selected file path(s) into the command line
__fsel() { __fsel() {
set -o nonomatch command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
command find * -path '*/\.*' -prune \
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | fzf -m | while read item; do -o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf -m | while read item; do
printf '%q ' "$item" printf '%q ' "$item"
done done
echo echo
@@ -314,8 +314,8 @@ bindkey '^T' fzf-file-widget
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
fzf-cd-widget() { fzf-cd-widget() {
cd "${$(set -o nonomatch; command find -L * -path '*/\.*' -prune \ cd "${$(command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type d -print 2> /dev/null | fzf):-.}" -o -type d -print 2> /dev/null | sed 1d | cut -b3- | fzf +m):-.}"
zle reset-prompt zle reset-prompt
} }
zle -N fzf-cd-widget zle -N fzf-cd-widget
@@ -369,14 +369,15 @@ function fzf_key_bindings
end end
function __fzf_list function __fzf_list
command find * -path '*/\.*' -prune \ command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null -o -type l -print 2> /dev/null | sed 1d | cut -b3-
end end
function __fzf_list_dir function __fzf_list_dir
command find -L * -path '*/\.*' -prune -o -type d -print 2> /dev/null command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) \
-prune -o -type d -print 2> /dev/null | sed 1d | cut -b3-
end end
function __fzf_escape function __fzf_escape

View File

@@ -19,6 +19,9 @@ git pull
./install ./install
``` ```
Otherwise, follow [the instruction][install] as before. You can also install
fzf using Homebrew if you prefer that way.
Motivations Motivations
----------- -----------
@@ -110,6 +113,7 @@ License
[MIT](LICENSE) [MIT](LICENSE)
[install]: https://github.com/junegunn/fzf#installation
[go]: https://golang.org/ [go]: https://golang.org/
[gil]: http://en.wikipedia.org/wiki/Global_Interpreter_Lock [gil]: http://en.wikipedia.org/wiki/Global_Interpreter_Lock
[ncurses]: https://www.gnu.org/software/ncurses/ [ncurses]: https://www.gnu.org/software/ncurses/

View File

@@ -5,7 +5,7 @@ import (
) )
// Current version // Current version
const Version = "0.9.1" const Version = "0.9.3"
// fzf events // fzf events
const ( const (

View File

@@ -95,51 +95,31 @@ func Run(options *Options) {
} }
matcher := NewMatcher(patternBuilder, opts.Sort > 0, eventBox) matcher := NewMatcher(patternBuilder, opts.Sort > 0, eventBox)
// Defered-interactive / Non-interactive // Filtering mode
// --select-1 | --exit-0 | --filter if opts.Filter != nil {
if filtering := opts.Filter != nil; filtering || opts.Select1 || opts.Exit0 { pattern := patternBuilder([]rune(*opts.Filter))
limit := 0
var patternString string
if filtering {
patternString = *opts.Filter
} else {
if opts.Select1 || opts.Exit0 {
limit = 1
}
patternString = opts.Query
}
pattern := patternBuilder([]rune(patternString))
looping := true
eventBox.Unwatch(EvtReadNew) eventBox.Unwatch(EvtReadNew)
for looping { eventBox.WaitFor(EvtReadFin)
eventBox.Wait(func(events *util.Events) {
for evt := range *events {
switch evt {
case EvtReadFin:
looping = false
return
}
}
})
}
snapshot, _ := chunkList.Snapshot() snapshot, _ := chunkList.Snapshot()
merger, cancelled := matcher.scan(MatchRequest{ merger, _ := matcher.scan(MatchRequest{
chunks: snapshot, chunks: snapshot,
pattern: pattern}, limit) pattern: pattern})
if !cancelled && (filtering || if opts.PrintQuery {
opts.Exit0 && merger.Length() == 0 || fmt.Println(*opts.Filter)
opts.Select1 && merger.Length() == 1) {
if opts.PrintQuery {
fmt.Println(patternString)
}
for i := 0; i < merger.Length(); i++ {
fmt.Println(merger.Get(i).AsString())
}
os.Exit(0)
} }
for i := 0; i < merger.Length(); i++ {
fmt.Println(merger.Get(i).AsString())
}
os.Exit(0)
}
// Synchronous search
if opts.Sync {
eventBox.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin)
} }
// Go interactive // Go interactive
@@ -147,7 +127,11 @@ func Run(options *Options) {
// Terminal I/O // Terminal I/O
terminal := NewTerminal(opts, eventBox) terminal := NewTerminal(opts, eventBox)
deferred := opts.Select1 || opts.Exit0
go terminal.Loop() go terminal.Loop()
if !deferred {
terminal.startChan <- true
}
// Event coordination // Event coordination
reading := true reading := true
@@ -165,11 +149,11 @@ func Run(options *Options) {
reading = reading && evt == EvtReadNew reading = reading && evt == EvtReadNew
snapshot, count := chunkList.Snapshot() snapshot, count := chunkList.Snapshot()
terminal.UpdateCount(count, !reading) terminal.UpdateCount(count, !reading)
matcher.Reset(snapshot, terminal.Input(), false) matcher.Reset(snapshot, terminal.Input(), false, !reading)
case EvtSearchNew: case EvtSearchNew:
snapshot, _ := chunkList.Snapshot() snapshot, _ := chunkList.Snapshot()
matcher.Reset(snapshot, terminal.Input(), true) matcher.Reset(snapshot, terminal.Input(), true, !reading)
delay = false delay = false
case EvtSearchProgress: case EvtSearchProgress:
@@ -181,6 +165,25 @@ func Run(options *Options) {
case EvtSearchFin: case EvtSearchFin:
switch val := value.(type) { switch val := value.(type) {
case *Merger: case *Merger:
if deferred {
count := val.Length()
if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 {
deferred = false
terminal.startChan <- true
} else if val.final {
if opts.Exit0 && count == 0 || opts.Select1 && count == 1 {
if opts.PrintQuery {
fmt.Println(opts.Query)
}
for i := 0; i < count; i++ {
fmt.Println(val.Get(i).AsString())
}
os.Exit(0)
}
deferred = false
terminal.startChan <- true
}
}
terminal.UpdateList(val) terminal.UpdateList(val)
} }
} }

View File

@@ -421,6 +421,10 @@ func Clear() {
C.clear() C.clear()
} }
func Endwin() {
C.endwin()
}
func Refresh() { func Refresh() {
C.refresh() C.refresh()
} }

View File

@@ -14,6 +14,7 @@ import (
type MatchRequest struct { type MatchRequest struct {
chunks []*Chunk chunks []*Chunk
pattern *Pattern pattern *Pattern
final bool
} }
// Matcher is responsible for performing search // Matcher is responsible for performing search
@@ -86,11 +87,12 @@ func (m *Matcher) Loop() {
} }
if !foundCache { if !foundCache {
merger, cancelled = m.scan(request, 0) merger, cancelled = m.scan(request)
} }
if !cancelled { if !cancelled {
m.mergerCache[patternString] = merger m.mergerCache[patternString] = merger
merger.final = request.final
m.eventBox.Set(EvtSearchFin, merger) m.eventBox.Set(EvtSearchFin, merger)
} }
} }
@@ -121,7 +123,7 @@ type partialResult struct {
matches []*Item matches []*Item
} }
func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
startedAt := time.Now() startedAt := time.Now()
numChunks := len(request.chunks) numChunks := len(request.chunks)
@@ -175,15 +177,11 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) {
count++ count++
matchCount += matchesInChunk matchCount += matchesInChunk
if limit > 0 && matchCount > limit {
return nil, wait() // For --select-1 and --exit-0
}
if count == numChunks { if count == numChunks {
break break
} }
if !empty && m.reqBox.Peak(reqReset) { if !empty && m.reqBox.Peek(reqReset) {
return nil, wait() return nil, wait()
} }
@@ -201,7 +199,7 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) {
} }
// Reset is called to interrupt/signal the ongoing search // Reset is called to interrupt/signal the ongoing search
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool) { func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool) {
pattern := m.patternBuilder(patternRunes) pattern := m.patternBuilder(patternRunes)
var event util.EventType var event util.EventType
@@ -210,5 +208,5 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool) {
} else { } else {
event = reqRetry event = reqRetry
} }
m.reqBox.Set(event, MatchRequest{chunks, pattern}) m.reqBox.Set(event, MatchRequest{chunks, pattern, final})
} }

View File

@@ -12,6 +12,7 @@ type Merger struct {
merged []*Item merged []*Item
cursors []int cursors []int
sorted bool sorted bool
final bool
count int count int
} }
@@ -22,6 +23,7 @@ func NewMerger(lists [][]*Item, sorted bool) *Merger {
merged: []*Item{}, merged: []*Item{},
cursors: make([]int, len(lists)), cursors: make([]int, len(lists)),
sorted: sorted, sorted: sorted,
final: false,
count: 0} count: 0}
for _, list := range mg.lists { for _, list := range mg.lists {

View File

@@ -41,10 +41,12 @@ 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
--sync Synchronous search for multi-staged filtering
(e.g. 'fzf --multi | fzf --sync')
Environment variables Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty FZF_DEFAULT_COMMAND Default command to use when input is tty
FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m") FZF_DEFAULT_OPTS Defaults options. (e.g. '-x -m')
` `
@@ -88,6 +90,7 @@ type Options struct {
Exit0 bool Exit0 bool
Filter *string Filter *string
PrintQuery bool PrintQuery bool
Sync bool
Version bool Version bool
} }
@@ -111,6 +114,7 @@ func defaultOptions() *Options {
Exit0: false, Exit0: false,
Filter: nil, Filter: nil,
PrintQuery: false, PrintQuery: false,
Sync: false,
Version: false} Version: false}
} }
@@ -244,6 +248,12 @@ func parseOptions(opts *Options, allArgs []string) {
opts.PrintQuery = false opts.PrintQuery = false
case "--prompt": case "--prompt":
opts.Prompt = nextString(allArgs, &i, "prompt string required") opts.Prompt = nextString(allArgs, &i, "prompt string required")
case "--sync":
opts.Sync = true
case "--no-sync":
opts.Sync = false
case "--async":
opts.Sync = false
case "--version": case "--version":
opts.Version = true opts.Version = true
default: default:
@@ -266,6 +276,17 @@ func parseOptions(opts *Options, allArgs []string) {
} }
} }
} }
// If we're not using extended search mode, --nth option becomes irrelevant
// if it contains the whole range
if opts.Mode == ModeFuzzy || len(opts.Nth) == 1 {
for _, r := range opts.Nth {
if r.begin == rangeEllipsis && r.end == rangeEllipsis {
opts.Nth = make([]Range, 0)
return
}
}
}
} }
// ParseOptions parses command-line options // ParseOptions parses command-line options

View File

@@ -21,17 +21,47 @@ func TestSplitNth(t *testing.T) {
} }
} }
{ {
ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2") ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1")
if len(ranges) != 8 || if len(ranges) != 10 ||
ranges[0].begin != rangeEllipsis || ranges[0].end != 3 || ranges[0].begin != rangeEllipsis || ranges[0].end != 3 ||
ranges[1].begin != 1 || ranges[1].end != rangeEllipsis || ranges[1].begin != rangeEllipsis || ranges[1].end != rangeEllipsis ||
ranges[2].begin != 2 || ranges[2].end != 3 || ranges[2].begin != 2 || ranges[2].end != 3 ||
ranges[3].begin != 4 || ranges[3].end != -1 || ranges[3].begin != 4 || ranges[3].end != rangeEllipsis ||
ranges[4].begin != -3 || ranges[4].end != -2 || ranges[4].begin != -3 || ranges[4].end != -2 ||
ranges[5].begin != rangeEllipsis || ranges[5].end != rangeEllipsis || ranges[5].begin != rangeEllipsis || ranges[5].end != rangeEllipsis ||
ranges[6].begin != 2 || ranges[6].end != 2 || ranges[6].begin != 2 || ranges[6].end != 2 ||
ranges[7].begin != -2 || ranges[7].end != -2 { ranges[7].begin != -2 || ranges[7].end != -2 ||
ranges[8].begin != 2 || ranges[8].end != -2 ||
ranges[9].begin != rangeEllipsis || ranges[9].end != rangeEllipsis {
t.Errorf("%s", ranges) t.Errorf("%s", ranges)
} }
} }
} }
func TestIrrelevantNth(t *testing.T) {
{
opts := defaultOptions()
words := []string{"--nth", "..", "-x"}
parseOptions(opts, words)
if len(opts.Nth) != 0 {
t.Errorf("nth should be empty: %s", opts.Nth)
}
}
for _, words := range [][]string{[]string{"--nth", "..,3"}, []string{"--nth", "3,1.."}, []string{"--nth", "..-1,1"}} {
{
opts := defaultOptions()
parseOptions(opts, words)
if len(opts.Nth) != 0 {
t.Errorf("nth should be empty: %s", opts.Nth)
}
}
{
opts := defaultOptions()
words = append(words, "-x")
parseOptions(opts, words)
if len(opts.Nth) != 2 {
t.Errorf("nth should not be empty: %s", opts.Nth)
}
}
}
}

View File

@@ -14,7 +14,7 @@ func TestReadFromCommand(t *testing.T) {
eventBox: eb} eventBox: eb}
// Check EventBox // Check EventBox
if eb.Peak(EvtReadNew) { if eb.Peek(EvtReadNew) {
t.Error("EvtReadNew should not be set yet") t.Error("EvtReadNew should not be set yet")
} }
@@ -25,7 +25,7 @@ func TestReadFromCommand(t *testing.T) {
} }
// Check EventBox again // Check EventBox again
if !eb.Peak(EvtReadNew) { if !eb.Peek(EvtReadNew) {
t.Error("EvtReadNew should be set yet") t.Error("EvtReadNew should be set yet")
} }
@@ -38,7 +38,7 @@ func TestReadFromCommand(t *testing.T) {
}) })
// EventBox is cleared // EventBox is cleared
if eb.Peak(EvtReadNew) { if eb.Peek(EvtReadNew) {
t.Error("EvtReadNew should not be set yet") t.Error("EvtReadNew should not be set yet")
} }
@@ -50,7 +50,7 @@ func TestReadFromCommand(t *testing.T) {
} }
// Check EventBox again // Check EventBox again
if eb.Peak(EvtReadNew) { if eb.Peek(EvtReadNew) {
t.Error("Command failed. EvtReadNew should be set") t.Error("Command failed. EvtReadNew should be set")
} }
} }

View File

@@ -1,11 +1,15 @@
package fzf package fzf
import ( import (
"bytes"
"fmt" "fmt"
"os" "os"
"os/signal"
"regexp" "regexp"
"sort" "sort"
"strings"
"sync" "sync"
"syscall"
"time" "time"
C "github.com/junegunn/fzf/src/curses" C "github.com/junegunn/fzf/src/curses"
@@ -36,6 +40,7 @@ type Terminal struct {
mutex sync.Mutex mutex sync.Mutex
initFunc func() initFunc func()
suppress bool suppress bool
startChan chan bool
} }
type selectedItem struct { type selectedItem struct {
@@ -58,6 +63,7 @@ func (a ByTimeOrder) Less(i, j int) bool {
} }
var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`} var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
var _runeWidths = make(map[rune]int)
const ( const (
reqPrompt util.EventType = iota reqPrompt util.EventType = iota
@@ -81,7 +87,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
prompt: opts.Prompt, prompt: opts.Prompt,
tac: opts.Sort == 0, tac: opts.Sort == 0,
reverse: opts.Reverse, reverse: opts.Reverse,
cx: displayWidth(input), cx: len(input),
cy: 0, cy: 0,
offset: 0, offset: 0,
yanked: []rune{}, yanked: []rune{},
@@ -94,6 +100,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
eventBox: eventBox, eventBox: eventBox,
mutex: sync.Mutex{}, mutex: sync.Mutex{},
suppress: true, suppress: true,
startChan: make(chan bool, 1),
initFunc: func() { initFunc: func() {
C.Init(opts.Color, opts.Color256, opts.Black, opts.Mouse) C.Init(opts.Color, opts.Color256, opts.Black, opts.Mouse)
}} }}
@@ -169,10 +176,22 @@ func (t *Terminal) output() {
} }
} }
func runeWidth(r rune, prefixWidth int) int {
if r == '\t' {
return 8 - prefixWidth%8
} else if w, found := _runeWidths[r]; found {
return w
} else {
w := runewidth.RuneWidth(r)
_runeWidths[r] = w
return w
}
}
func displayWidth(runes []rune) int { func displayWidth(runes []rune) int {
l := 0 l := 0
for _, r := range runes { for _, r := range runes {
l += runewidth.RuneWidth(r) l += runeWidth(r, l)
} }
return l return l
} }
@@ -254,16 +273,27 @@ func (t *Terminal) printItem(item *Item, current bool) {
} }
func trimRight(runes []rune, width int) ([]rune, int) { func trimRight(runes []rune, width int) ([]rune, int) {
currentWidth := displayWidth(runes) // We start from the beginning to handle tab characters
trimmed := 0 l := 0
for idx, r := range runes {
for currentWidth > width && len(runes) > 0 { l += runeWidth(r, l)
sz := len(runes) if idx > 0 && l > width {
currentWidth -= runewidth.RuneWidth(runes[sz-1]) return runes[:idx], len(runes) - idx
runes = runes[:sz-1] }
trimmed++
} }
return runes, trimmed return runes, 0
}
func displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int {
l := 0
for _, r := range runes {
l += runeWidth(r, l+prefixWidth)
if l > limit {
// Early exit
return l
}
}
return l
} }
func trimLeft(runes []rune, width int) ([]rune, int32) { func trimLeft(runes []rune, width int) ([]rune, int32) {
@@ -271,9 +301,9 @@ func trimLeft(runes []rune, width int) ([]rune, int32) {
var trimmed int32 var trimmed int32
for currentWidth > width && len(runes) > 0 { for currentWidth > width && len(runes) > 0 {
currentWidth -= runewidth.RuneWidth(runes[0])
runes = runes[1:] runes = runes[1:]
trimmed++ trimmed++
currentWidth = displayWidthWithLimit(runes, 2, width)
} }
return runes, trimmed return runes, trimmed
} }
@@ -323,18 +353,41 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) {
sort.Sort(ByOrder(offsets)) sort.Sort(ByOrder(offsets))
var index int32 var index int32
var substr string
var prefixWidth int
for _, offset := range offsets { for _, offset := range offsets {
b := util.Max32(index, offset[0]) b := util.Max32(index, offset[0])
e := util.Max32(index, offset[1]) e := util.Max32(index, offset[1])
C.CPrint(col1, bold, string(text[index:b]))
C.CPrint(col2, bold, string(text[b:e])) substr, prefixWidth = processTabs(text[index:b], prefixWidth)
C.CPrint(col1, bold, substr)
substr, prefixWidth = processTabs(text[b:e], prefixWidth)
C.CPrint(col2, bold, substr)
index = e index = e
} }
if index < int32(len(text)) { if index < int32(len(text)) {
C.CPrint(col1, bold, string(text[index:])) substr, _ = processTabs(text[index:], prefixWidth)
C.CPrint(col1, bold, substr)
} }
} }
func processTabs(runes []rune, prefixWidth int) (string, int) {
var strbuf bytes.Buffer
l := prefixWidth
for _, r := range runes {
w := runeWidth(r, l)
l += w
if r == '\t' {
strbuf.WriteString(strings.Repeat(" ", w))
} else {
strbuf.WriteRune(r)
}
}
return strbuf.String(), l
}
func (t *Terminal) printAll() { func (t *Terminal) printAll() {
t.printList() t.printList()
t.printInfo() t.printInfo()
@@ -395,6 +448,7 @@ func (t *Terminal) rubout(pattern string) {
// Loop is called to start Terminal I/O // Loop is called to start Terminal I/O
func (t *Terminal) Loop() { func (t *Terminal) Loop() {
<-t.startChan
{ // Late initialization { // Late initialization
t.mutex.Lock() t.mutex.Lock()
t.initFunc() t.initFunc()
@@ -408,6 +462,15 @@ func (t *Terminal) Loop() {
<-timer.C <-timer.C
t.reqBox.Set(reqRefresh, nil) t.reqBox.Set(reqRefresh, nil)
}() }()
resizeChan := make(chan os.Signal, 1)
signal.Notify(resizeChan, syscall.SIGWINCH)
go func() {
for {
<-resizeChan
t.reqBox.Set(reqRedraw, nil)
}
}()
} }
go func() { go func() {
@@ -427,6 +490,8 @@ func (t *Terminal) Loop() {
t.suppress = false t.suppress = false
case reqRedraw: case reqRedraw:
C.Clear() C.Clear()
C.Endwin()
C.Refresh()
t.printAll() t.printAll()
case reqClose: case reqClose:
C.Close() C.Close()

View File

@@ -28,22 +28,32 @@ type Token struct {
prefixLength int prefixLength int
} }
func newRange(begin int, end int) Range {
if begin == 1 {
begin = rangeEllipsis
}
if end == -1 {
end = rangeEllipsis
}
return Range{begin, end}
}
// ParseRange parses nth-expression and returns the corresponding Range object // ParseRange parses nth-expression and returns the corresponding Range object
func ParseRange(str *string) (Range, bool) { func ParseRange(str *string) (Range, bool) {
if (*str) == ".." { if (*str) == ".." {
return Range{rangeEllipsis, rangeEllipsis}, true return newRange(rangeEllipsis, rangeEllipsis), true
} else if strings.HasPrefix(*str, "..") { } else if strings.HasPrefix(*str, "..") {
end, err := strconv.Atoi((*str)[2:]) end, err := strconv.Atoi((*str)[2:])
if err != nil || end == 0 { if err != nil || end == 0 {
return Range{}, false return Range{}, false
} }
return Range{rangeEllipsis, end}, true return newRange(rangeEllipsis, end), true
} else if strings.HasSuffix(*str, "..") { } else if strings.HasSuffix(*str, "..") {
begin, err := strconv.Atoi((*str)[:len(*str)-2]) begin, err := strconv.Atoi((*str)[:len(*str)-2])
if err != nil || begin == 0 { if err != nil || begin == 0 {
return Range{}, false return Range{}, false
} }
return Range{begin, rangeEllipsis}, true return newRange(begin, rangeEllipsis), true
} else if strings.Contains(*str, "..") { } else if strings.Contains(*str, "..") {
ns := strings.Split(*str, "..") ns := strings.Split(*str, "..")
if len(ns) != 2 { if len(ns) != 2 {
@@ -51,17 +61,17 @@ func ParseRange(str *string) (Range, bool) {
} }
begin, err1 := strconv.Atoi(ns[0]) begin, err1 := strconv.Atoi(ns[0])
end, err2 := strconv.Atoi(ns[1]) end, err2 := strconv.Atoi(ns[1])
if err1 != nil || err2 != nil { if err1 != nil || err2 != nil || begin == 0 || end == 0 {
return Range{}, false return Range{}, false
} }
return Range{begin, end}, true return newRange(begin, end), true
} }
n, err := strconv.Atoi(*str) n, err := strconv.Atoi(*str)
if err != nil || n == 0 { if err != nil || n == 0 {
return Range{}, false return Range{}, false
} }
return Range{n, n}, true return newRange(n, n), true
} }
func withPrefixLengths(tokens []string, begin int) []Token { func withPrefixLengths(tokens []string, begin int) []Token {

View File

@@ -5,6 +5,7 @@ require 'rest_client'
if ARGV.length < 3 if ARGV.length < 3
puts "usage: #$0 <token> <version> <files...>" puts "usage: #$0 <token> <version> <files...>"
exit 1
end end
token, version, *files = ARGV token, version, *files = ARGV

View File

@@ -53,8 +53,8 @@ func (events *Events) Clear() {
} }
} }
// Peak peaks at the event box if the given event is set // Peek peeks at the event box if the given event is set
func (b *EventBox) Peak(event EventType) bool { func (b *EventBox) Peek(event EventType) bool {
b.cond.L.Lock() b.cond.L.Lock()
defer b.cond.L.Unlock() defer b.cond.L.Unlock()
_, ok := b.events[event] _, ok := b.events[event]
@@ -78,3 +78,18 @@ func (b *EventBox) Unwatch(events ...EventType) {
b.ignore[event] = true b.ignore[event] = true
} }
} }
func (b *EventBox) WaitFor(event EventType) {
looping := true
for looping {
b.Wait(func(events *Events) {
for evt := range *events {
switch evt {
case event:
looping = false
return
}
}
})
}
}

View File

@@ -7,16 +7,16 @@ Execute (fzf#run with dir option):
AssertEqual ['fzf.vader'], result AssertEqual ['fzf.vader'], result
let result = sort(fzf#run({ 'options': '--filter e', 'dir': g:dir })) let result = sort(fzf#run({ 'options': '--filter e', 'dir': g:dir }))
AssertEqual ['fzf.vader', 'test_fzf.rb'], result AssertEqual ['fzf.vader', 'test_go.rb', 'test_ruby.rb'], result
Execute (fzf#run with Funcref command): Execute (fzf#run with Funcref command):
let g:ret = [] let g:ret = []
function! g:proc(e) function! g:FzfTest(e)
call add(g:ret, a:e) call add(g:ret, a:e)
endfunction endfunction
let result = sort(fzf#run({ 'sink': function('g:proc'), 'options': '--filter e', 'dir': g:dir })) let result = sort(fzf#run({ 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir }))
AssertEqual ['fzf.vader', 'test_fzf.rb'], result AssertEqual ['fzf.vader', 'test_go.rb', 'test_ruby.rb'], result
AssertEqual ['fzf.vader', 'test_fzf.rb'], sort(g:ret) AssertEqual ['fzf.vader', 'test_go.rb', 'test_ruby.rb'], sort(g:ret)
Execute (fzf#run with string source): Execute (fzf#run with string source):
let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' })) let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' }))

View File

@@ -9,13 +9,36 @@ class NilClass
end end
end end
module Temp
def readonce
name = self.class::TEMPNAME
waited = 0
while waited < 5
begin
data = File.read(name)
return data unless data.empty?
rescue
sleep 0.1
waited += 0.1
end
end
raise "failed to read tempfile"
ensure
while File.exists? name
File.unlink name rescue nil
end
end
end
class Tmux class Tmux
include Temp
TEMPNAME = '/tmp/fzf-test.txt' TEMPNAME = '/tmp/fzf-test.txt'
attr_reader :win attr_reader :win
def initialize shell = 'bash' def initialize shell = 'bash'
@win = go("new-window -d -P -F '#I' 'PS1= bash --rcfile ~/.fzf.#{shell}'").first @win = go("new-window -d -P -F '#I' 'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.#{shell}'").first
@lines = `tput lines`.chomp.to_i @lines = `tput lines`.chomp.to_i
end end
@@ -40,7 +63,7 @@ class Tmux
def capture def capture
go("capture-pane -t #{win} \\; save-buffer #{TEMPNAME}") go("capture-pane -t #{win} \\; save-buffer #{TEMPNAME}")
raise "Window not found" if $?.exitstatus != 0 raise "Window not found" if $?.exitstatus != 0
File.read(TEMPNAME).split($/)[0, @lines].reverse.drop_while(&:empty?).reverse readonce.split($/)[0, @lines].reverse.drop_while(&:empty?).reverse
end end
def until timeout = 1 def until timeout = 1
@@ -71,44 +94,44 @@ private
end end
class TestGoFZF < MiniTest::Unit::TestCase class TestGoFZF < MiniTest::Unit::TestCase
include Temp
FIN = 'FIN'
TEMPNAME = '/tmp/output'
attr_reader :tmux attr_reader :tmux
def tempname
'/tmp/output'
end
def rmtemp
while File.exists? tempname
File.unlink tempname rescue nil
end
end
def readtemp
waited = 0
while waited < 5
begin
return File.read(tempname)
rescue
sleep 0.1
waited += 0.1
end
end
raise "failed to read tempfile"
end
def setup def setup
ENV.delete 'FZF_DEFAULT_OPTS' ENV.delete 'FZF_DEFAULT_OPTS'
ENV.delete 'FZF_DEFAULT_COMMAND' ENV.delete 'FZF_DEFAULT_COMMAND'
@tmux = Tmux.new @tmux = Tmux.new
rmtemp
end end
def teardown def teardown
@tmux.kill @tmux.kill
end end
def fzf(*opts)
fzf!(*opts) + " > #{TEMPNAME} && echo #{FIN}"
end
def fzf!(*opts)
opts = opts.map { |o|
case o
when Symbol
o = o.to_s
o.length > 1 ? "--#{o.gsub('_', '-')}" : "-#{o}"
when String, Numeric
o.to_s
else
nil
end
}.compact
"fzf #{opts.join ' '}"
end
def test_vanilla def test_vanilla
tmux.send_keys "seq 1 100000 | fzf > #{tempname}", :Enter tmux.send_keys "seq 1 100000 | #{fzf}", :Enter
tmux.until(10) { |lines| lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ } tmux.until(10) { |lines| lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ }
lines = tmux.capture lines = tmux.capture
assert_equal ' 2', lines[-4] assert_equal ' 2', lines[-4]
@@ -127,16 +150,16 @@ class TestGoFZF < MiniTest::Unit::TestCase
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.close tmux.close
assert_equal '1391', readtemp.chomp assert_equal '1391', readonce.chomp
end end
def test_fzf_default_command def test_fzf_default_command
tmux.send_keys "FZF_DEFAULT_COMMAND='echo hello' fzf > #{tempname}", :Enter tmux.send_keys "FZF_DEFAULT_COMMAND='echo hello' #{fzf}", :Enter
tmux.until { |lines| lines.last =~ /^>/ } tmux.until { |lines| lines.last =~ /^>/ }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.close tmux.close
assert_equal 'hello', readtemp.chomp assert_equal 'hello', readonce.chomp
end end
def test_key_bindings def test_key_bindings
@@ -206,7 +229,7 @@ class TestGoFZF < MiniTest::Unit::TestCase
end end
def test_multi_order def test_multi_order
tmux.send_keys "seq 1 10 | fzf --multi > #{tempname} && echo -n done", :Enter tmux.send_keys "seq 1 10 | #{fzf :multi}", :Enter
tmux.until { |lines| lines.last =~ /^>/ } tmux.until { |lines| lines.last =~ /^>/ }
tmux.send_keys :Tab, :Up, :Up, :Tab, :Tab, :Tab, # 3, 2 tmux.send_keys :Tab, :Up, :Up, :Tab, :Tab, :Tab, # 3, 2
@@ -214,18 +237,16 @@ class TestGoFZF < MiniTest::Unit::TestCase
:PgUp, 'C-J', :Down, :Tab, :Tab # 8, 7 :PgUp, 'C-J', :Down, :Tab, :Tab # 8, 7
tmux.until { |lines| lines[-2].include? '(6)' } tmux.until { |lines| lines[-2].include? '(6)' }
tmux.send_keys "C-M" tmux.send_keys "C-M"
tmux.until { |lines| lines[-1].include?('done') } tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal %w[3 2 5 6 8 7], readtemp.split($/) assert_equal %w[3 2 5 6 8 7], readonce.split($/)
tmux.close tmux.close
end end
def test_with_nth def test_with_nth
[true, false].each do |multi| [true, false].each do |multi|
rmtemp
tmux.send_keys "(echo ' 1st 2nd 3rd/'; tmux.send_keys "(echo ' 1st 2nd 3rd/';
echo ' first second third/') | echo ' first second third/') |
fzf #{"--multi" if multi} -x --nth 2 --with-nth 2,-1,1 > #{tempname} && echo -n done", #{fzf multi && :multi, :x, :nth, 2, :with_nth, '2,-1,1'}",
:Enter :Enter
tmux.until { |lines| lines[-2].include?('2/2') } tmux.until { |lines| lines[-2].include?('2/2') }
@@ -237,42 +258,69 @@ class TestGoFZF < MiniTest::Unit::TestCase
# However, the output must not be transformed # However, the output must not be transformed
if multi if multi
tmux.send_keys :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab, :Enter
tmux.until { |lines| lines[-1].include?('done') } tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal [' 1st 2nd 3rd/', ' first second third/'], readtemp.split($/) assert_equal [' 1st 2nd 3rd/', ' first second third/'], readonce.split($/)
else else
tmux.send_keys '^', '3' tmux.send_keys '^', '3'
tmux.until { |lines| lines[-2].include?('1/2') } tmux.until { |lines| lines[-2].include?('1/2') }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include?('done') } tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal [' 1st 2nd 3rd/'], readtemp.split($/) assert_equal [' 1st 2nd 3rd/'], readonce.split($/)
end end
end end
end end
def test_scroll def test_scroll
[true, false].each do |rev| [true, false].each do |rev|
rmtemp tmux.send_keys "seq 1 100 | #{fzf rev && :reverse}", :Enter
tmux.until { |lines| lines.include? ' 100/100' }
tmux.send_keys "seq 1 100 | fzf #{'--reverse' if rev} > #{tempname} && echo -n done", :Enter
tmux.until { |lines| rev ? lines.first == '>' : lines.last == '>' }
tmux.send_keys *110.times.map { rev ? :Down : :Up } tmux.send_keys *110.times.map { rev ? :Down : :Up }
tmux.until { |lines| lines.include? '> 100' } tmux.until { |lines| lines.include? '> 100' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include?('done') } tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal '100', readtemp.chomp assert_equal '100', readonce.chomp
end end
end end
def test_select_1 def test_select_1
tmux.send_keys "seq 1 100 | fzf --with-nth ..,.. --print-query -q 5555 -1 > #{tempname} && echo -n done", :Enter tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 5555, :'1'}", :Enter
tmux.until { |lines| lines[-1].include?('done') } tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal ['5555', '55'], readtemp.split($/) assert_equal ['5555', '55'], readonce.split($/)
end end
def test_exit_0 def test_exit_0
tmux.send_keys "seq 1 100 | fzf --with-nth ..,.. --print-query -q 555555 -0 > #{tempname} && echo -n done", :Enter tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 555555, :'0'}", :Enter
tmux.until { |lines| lines[-1].include?('done') } tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal ['555555'], readtemp.split($/) assert_equal ['555555'], readonce.split($/)
end
def test_select_1_exit_0_fail
[:'0', :'1', [:'1', :'0']].each do |opt|
tmux.send_keys "seq 1 100 | #{fzf :print_query, :multi, :q, 5, *opt}", :Enter
tmux.until { |lines| lines.last =~ /^> 5/ }
tmux.send_keys :BTab, :BTab, :BTab, :Enter
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal ['5', '5', '15', '25'], readonce.split($/)
end
end
def test_query_unicode
tmux.send_keys "(echo abc; echo 가나다) | #{fzf :query, '가다'}", :Enter
tmux.until { |lines| lines.last.start_with? '>' }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal ['가나다'], readonce.split($/)
end
def test_sync
tmux.send_keys "seq 1 100 | #{fzf! :multi} | awk '{print \\$1 \\$1}' | #{fzf :sync}", :Enter
tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys 9
tmux.until { |lines| lines[-2] == ' 19/100' }
tmux.send_keys :BTab, :BTab, :BTab, :Enter
tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys 'C-K', :Enter
assert_equal ['1919'], readonce.split($/)
end end
end end