Compare commits

..

20 Commits

Author SHA1 Message Date
Junegunn Choi
305ec3b3ce [fish] Remove buffering delay by not using subroutines
Close #169
2015-04-22 14:33:03 +09:00
Junegunn Choi
f4fe93338b Update README 2015-04-22 02:09:16 +09:00
Junegunn Choi
3b84c80d56 Update README 2015-04-22 02:07:27 +09:00
Junegunn Choi
5e120e7ab5 Update man page 2015-04-22 01:44:56 +09:00
Junegunn Choi
a4cf5510e3 0.9.11 2015-04-22 01:42:38 +09:00
Junegunn Choi
edb5ab5622 Update test cases for #203 2015-04-22 00:57:25 +09:00
Junegunn Choi
06b4f75680 Fix broken FZF_TMUX switch and update test cases (#203) 2015-04-22 00:55:39 +09:00
Junegunn Choi
318edc8c35 Apply fzf-tmux to key bindings (#203)
Note that CTRL-T on bash is still using the old trick of send-keys.
2015-04-22 00:32:18 +09:00
Junegunn Choi
651a8f8cc2 Add --inline-info option
Close #202
2015-04-21 23:50:53 +09:00
Junegunn Choi
9f64a00549 Fix double-click result when scroll offset is positive 2015-04-21 23:23:39 +09:00
Junegunn Choi
a88bf87e2a Update test case 2015-04-21 22:36:40 +09:00
Junegunn Choi
e82eb27787 Smart-case for each term in extended-search mode
Close #208
2015-04-21 22:18:05 +09:00
Junegunn Choi
3f0e6a5806 Fix #209 - Invalid mutation of input on case conversion 2015-04-21 22:10:14 +09:00
Junegunn Choi
917b1759b0 [fzf-tmux/vim] Fixes for fish (#204) 2015-04-20 22:42:12 +09:00
Junegunn Choi
16ca9c688b Revert "[fzf-tmux] Fix #204 - Escape command substitution"
This reverts commit 7b6a27cb5e.
2015-04-20 16:23:15 +09:00
Junegunn Choi
7b6a27cb5e [fzf-tmux] Fix #204 - Escape command substitution 2015-04-20 15:22:59 +09:00
Junegunn Choi
869a234938 [fzf-tmux] Use bash instead of sh (#204)
The default shell can be a non-standard shell (e.g. fish)
2015-04-20 14:58:27 +09:00
Junegunn Choi
537d07c1e5 [vim] Use "system" fzf when available
1. Go binary: ../bin/fzf
2. System fzf: $(which fzf)
3. Download fzf from GitHub or create wrapper script to Ruby version (../fzf)
   when the binary for the platform is not available
4. If install script is not found or for some reason failed, try to use Ruby
   version in its expected location (../fzf)
5. If fzf is found to be a shell function, use it (type fzf)
2015-04-19 17:13:07 +09:00
Junegunn Choi
d091a2c4bb [fzf-tmux] Minor adjustment 2015-04-18 16:27:40 +09:00
Junegunn Choi
d2f95d69fb [fzf-tmux] Fix #200 - Double-quote handling
Related #199
2015-04-18 16:24:57 +09:00
17 changed files with 224 additions and 226 deletions

View File

@@ -1,6 +1,21 @@
CHANGELOG CHANGELOG
========= =========
0.9.11
------
### New features
- Added `--inline-info` option for saving screen estate (#202)
- Useful inside Neovim
- e.g. `let $FZF_DEFAULT_OPTS = $FZF_DEFAULT_OPTS.' --inline-info'`
### Bug fixes
- Invalid mutation of input on case conversion (#209)
- Smart-case for each term in extended-search mode (#208)
- Fixed double-click result when scroll offset is positive
0.9.10 0.9.10
------ ------

View File

@@ -45,17 +45,6 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install ~/.fzf/install
``` ```
#### Using curl
In case you don't have git installed:
```sh
mkdir -p ~/.fzf
curl -L https://github.com/junegunn/fzf/archive/master.tar.gz |
tar xz --strip-components 1 -C ~/.fzf
~/.fzf/install
```
#### Using Homebrew #### Using Homebrew
On OS X, you can use [Homebrew](http://brew.sh/) to install fzf. On OS X, you can use [Homebrew](http://brew.sh/) to install fzf.
@@ -157,18 +146,14 @@ fish.
- Press `CTRL-R` again to toggle sort - Press `CTRL-R` again to toggle sort
- `ALT-C` - cd into the selected directory - `ALT-C` - cd into the selected directory
If you're on a tmux session, `CTRL-T` will launch fzf in a new split-window. You If you're on a tmux session, fzf will start in a split pane. You may disable
may disable this tmux integration by setting `FZF_TMUX` to 0, or change the this tmux integration by setting `FZF_TMUX` to 0, or change the height of the
height of the window with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`). pane with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`).
If you use vi mode on bash, you need to add `set -o vi` *before* `source If you use vi mode on bash, you need to add `set -o vi` *before* `source
~/.fzf.bash` in your .bashrc, so that it correctly sets up key bindings for vi ~/.fzf.bash` in your .bashrc, so that it correctly sets up key bindings for vi
mode. mode.
If you want to customize the key bindings, consider editing the
installer-generated source code: `~/.fzf.bash`, `~/.fzf.zsh`, and
`~/.config/fish/functions/fzf_key_bindings.fish`.
`fzf-tmux` script `fzf-tmux` script
----------------- -----------------
@@ -286,10 +271,10 @@ Similarly to [ctrlp.vim](https://github.com/kien/ctrlp.vim), use enter key,
in new tabs, in horizontal splits, or in vertical splits respectively. in new tabs, in horizontal splits, or in vertical splits respectively.
Note that the environment variables `FZF_DEFAULT_COMMAND` and Note that the environment variables `FZF_DEFAULT_COMMAND` and
`FZF_DEFAULT_OPTS` also apply here. Refer to [the wiki page][vim-examples] for `FZF_DEFAULT_OPTS` also apply here. Refer to [the wiki page][fzf-config] for
customization. customization.
[vim-examples]: https://github.com/junegunn/fzf/wiki/Examples-(vim) [fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-FZF-command-(vim)
#### `fzf#run([options])` #### `fzf#run([options])`
@@ -372,10 +357,6 @@ nnoremap <silent> <Leader><Enter> :call fzf#run({
More examples can be found on [the wiki More examples can be found on [the wiki
page](https://github.com/junegunn/fzf/wiki/Examples-(vim)). page](https://github.com/junegunn/fzf/wiki/Examples-(vim)).
#### Articles
- [fzf+vim+tmux](http://junegunn.kr/2014/04/fzf+vim+tmux)
Tips Tips
---- ----
@@ -384,13 +365,13 @@ Tips
If you have any rendering issues, check the followings: If you have any rendering issues, check the followings:
1. Make sure `$TERM` is correctly set. fzf will use 256-color only if it 1. Make sure `$TERM` is correctly set. fzf will use 256-color only if it
contains `256` (e.g. `xterm-256color`) contains `256` (e.g. `xterm-256color`)
2. If you're on screen or tmux, `$TERM` should be either `screen` or 2. If you're on screen or tmux, `$TERM` should be either `screen` or
`screen-256color` `screen-256color`
3. Some terminal emulators (e.g. mintty) have problem displaying default 3. Some terminal emulators (e.g. mintty) have problem displaying default
background color and make some text unable to read. In that case, try `--black` background color and make some text unable to read. In that case, try
option. And if it solves your problem, I recommend including it in `--black` option. And if it solves your problem, I recommend including it
`FZF_DEFAULT_OPTS` for further convenience. in `FZF_DEFAULT_OPTS` for further convenience.
4. If you still have problem, try `--no-256` option or even `--no-color`. 4. If you still have problem, try `--no-256` option or even `--no-color`.
#### Respecting `.gitignore`, `.hgignore`, and `svn:ignore` #### Respecting `.gitignore`, `.hgignore`, and `svn:ignore`
@@ -421,41 +402,6 @@ export FZF_DEFAULT_COMMAND='
find * -name ".*" -prune -o -type f -print -o -type l -print) 2> /dev/null' find * -name ".*" -prune -o -type f -print -o -type l -print) 2> /dev/null'
``` ```
#### Using fzf with tmux panes
The supplied [fzf-tmux](bin/fzf-tmux) script should suffice in most of the
cases, but if you want to be able to update command line like the default
`CTRL-T` key binding, you'll have to use `send-keys` command of tmux. The
following example will show you how it can be done.
```sh
# This is a helper function that splits the current pane to start the given
# command ($1) and sends its output back to the original pane with any number of
# optional keys (shift; $*).
fzf_tmux_helper() {
[ -n "$TMUX_PANE" ] || return
local cmd=$1
shift
tmux split-window -p 40 \
"bash -c \"\$(tmux send-keys -t $TMUX_PANE \"\$(source ~/.fzf.bash; $cmd)\" $*)\""
}
# This is the function we are going to run in the split pane.
# - "find" to list the directories
# - "sed" will escape spaces in the paths.
# - "paste" will join the selected paths into a single line
fzf_tmux_dir() {
fzf_tmux_helper \
'find * -path "*/\.*" -prune -o -type d -print 2> /dev/null |
fzf --multi |
sed "s/ /\\\\ /g" |
paste -sd" " -' Space
}
# Bind CTRL-X-CTRL-D to fzf_tmux_dir
bind '"\C-x\C-d": "$(fzf_tmux_dir)\e\C-e"'
```
#### Fish shell #### Fish shell
It's [a known bug of fish](https://github.com/fish-shell/fish-shell/issues/1362) It's [a known bug of fish](https://github.com/fish-shell/fish-shell/issues/1362)
@@ -464,19 +410,7 @@ simple `vim (fzf)` won't work as expected. The workaround is to store the result
of fzf to a temporary file. of fzf to a temporary file.
```sh ```sh
function vimf fzf > $TMPDIR/fzf.result; and vim (cat $TMPDIR/fzf.result)
if fzf > $TMPDIR/fzf.result
vim (cat $TMPDIR/fzf.result)
end
end
function fe
set tmp $TMPDIR/fzf.result
fzf --query="$argv[1]" --select-1 --exit-0 > $tmp
if [ (cat $tmp | wc -l) -gt 0 ]
vim (cat $tmp)
end
end
``` ```
#### Handling UTF-8 NFD paths on OSX #### Handling UTF-8 NFD paths on OSX

View File

@@ -89,16 +89,14 @@ fi
set -e set -e
# Build arguments to fzf
[ ${#args[@]} -gt 0 ] && fzf_args=$(printf '\\"%s\\" ' "${args[@]}"; echo '')
# Clean up named pipes on exit # Clean up named pipes on exit
id=$RANDOM id=$RANDOM
argsf=/tmp/fzf-args-$id
fifo1=/tmp/fzf-fifo1-$id fifo1=/tmp/fzf-fifo1-$id
fifo2=/tmp/fzf-fifo2-$id fifo2=/tmp/fzf-fifo2-$id
fifo3=/tmp/fzf-fifo3-$id fifo3=/tmp/fzf-fifo3-$id
cleanup() { cleanup() {
rm -f $fifo1 $fifo2 $fifo3 rm -f $argsf $fifo1 $fifo2 $fifo3
} }
trap cleanup EXIT SIGINT SIGTERM trap cleanup EXIT SIGINT SIGTERM
@@ -115,13 +113,22 @@ envs=""
mkfifo $fifo2 mkfifo $fifo2
mkfifo $fifo3 mkfifo $fifo3
# Build arguments to fzf
opts=""
for arg in "${args[@]}"; do
opts="$opts \"${arg//\"/\\\"}\""
done
if [ -n "$term" -o -t 0 ]; then if [ -n "$term" -o -t 0 ]; then
cat <<< "$fzf $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf
tmux set-window-option -q synchronize-panes off \;\ tmux set-window-option -q synchronize-panes off \;\
split-window $opt "cd $(printf %q "$PWD");$envs"' sh -c "'$fzf' '"$fzf_args"' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap
else else
mkfifo $fifo1 mkfifo $fifo1
cat <<< "$fzf $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf
tmux set-window-option -q synchronize-panes off \;\ tmux set-window-option -q synchronize-panes off \;\
split-window $opt "$envs"' sh -c "'$fzf' '"$fzf_args"' < '$fifo1' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap split-window $opt "$envs bash $argsf" $swap
cat <&0 > $fifo1 & cat <&0 > $fifo1 &
fi fi
cat $fifo2 cat $fifo2

1
fzf
View File

@@ -209,6 +209,7 @@ class FZF
when '--toggle-sort', '--tiebreak', '--color' when '--toggle-sort', '--tiebreak', '--color'
argv.shift argv.shift
when '--tac', '--no-tac', '--sync', '--no-sync', '--hscroll', '--no-hscroll', when '--tac', '--no-tac', '--sync', '--no-sync', '--hscroll', '--no-hscroll',
'--inline-info', '--no-inline-info',
/^--color=(.*)$/, /^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/ /^--color=(.*)$/, /^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/
# XXX # XXX
else else

View File

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

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "April 2015" "fzf 0.9.10" "fzf - a command-line fuzzy finder" .TH fzf 1 "April 2015" "fzf 0.9.11" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -116,6 +116,9 @@ Reverse orientation
.B "--no-hscroll" .B "--no-hscroll"
Disable horizontal scroll Disable horizontal scroll
.TP .TP
.B "--inline-info"
Display finder info inline with the query
.TP
.BI "--prompt=" "STR" .BI "--prompt=" "STR"
Input prompt (default: '> ') Input prompt (default: '> ')
.SS Scripting .SS Scripting

View File

@@ -36,17 +36,17 @@ function! s:fzf_exec()
if !exists('s:exec') if !exists('s:exec')
if executable(s:fzf_go) if executable(s:fzf_go)
let s:exec = s:fzf_go let s:exec = s:fzf_go
elseif !s:installed && executable(s:install)
echohl WarningMsg
echo 'Downloading fzf binary. Please wait ...'
echohl None
let s:installed = 1
call system(s:install.' --bin')
return s:fzf_exec()
else else
let path = split(system('which fzf 2> /dev/null'), '\n') let path = split(system('which fzf 2> /dev/null'), '\n')
if !v:shell_error && !empty(path) if !v:shell_error && !empty(path)
let s:exec = path[0] let s:exec = path[0]
elseif !s:installed && executable(s:install)
echohl WarningMsg
echo 'Downloading fzf binary. Please wait ...'
echohl None
let s:installed = 1
call system(s:install.' --bin')
return s:fzf_exec()
elseif executable(s:fzf_rb) elseif executable(s:fzf_rb)
let s:exec = s:fzf_rb let s:exec = s:fzf_rb
else else
@@ -105,6 +105,9 @@ function! s:upgrade(dict)
endfunction endfunction
function! fzf#run(...) abort function! fzf#run(...) abort
try
let oshell = &shell
set shell=sh
if has('nvim') && bufexists('[FZF]') if has('nvim') && bufexists('[FZF]')
echohl WarningMsg echohl WarningMsg
echomsg 'FZF is already running!' echomsg 'FZF is already running!'
@@ -149,6 +152,9 @@ function! fzf#run(...) abort
finally finally
call s:popd(dict) call s:popd(dict)
endtry endtry
finally
let &shell = oshell
endtry
endfunction endfunction
function! s:present(dict, ...) function! s:present(dict, ...)

View File

@@ -1,6 +1,6 @@
# Key bindings # Key bindings
# ------------ # ------------
__fsel() { __fzf_select__() {
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -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 \
@@ -12,7 +12,11 @@ __fsel() {
if [[ $- =~ i ]]; then if [[ $- =~ i ]]; then
__fsel_tmux() { __fzfcmd() {
[ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
}
__fzf_select_tmux__() {
local height local height
height=${FZF_TMUX_HEIGHT:-40%} height=${FZF_TMUX_HEIGHT:-40%}
if [[ $height =~ %$ ]]; then if [[ $height =~ %$ ]]; then
@@ -20,13 +24,17 @@ __fsel_tmux() {
else else
height="-l $height" height="-l $height"
fi fi
tmux split-window $height "cd $(printf %q "$PWD");bash -c 'source ~/.fzf.bash; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'" tmux split-window $height "cd $(printf %q "$PWD");bash -c 'source ~/.fzf.bash; tmux send-keys -t $TMUX_PANE \"\$(__fzf_select__)\"'"
} }
__fcd() { __fzf_cd__() {
local dir local dir
dir=$(command find -L ${1:-.} \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ 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" -o -type d -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) +m) && printf 'cd %q' "$dir"
}
__fzf_history__() {
HISTTIMEFORMAT= history | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r | sed "s/ *[0-9]* *//"
} }
__use_tmux=0 __use_tmux=0
@@ -38,16 +46,16 @@ if [ -z "$(set -o | \grep '^vi.*on')" ]; then
# CTRL-T - Paste the selected file path into the command line # CTRL-T - Paste the selected file path into the command line
if [ $__use_tmux -eq 1 ]; then if [ $__use_tmux -eq 1 ]; then
bind '"\C-t": " \C-u \C-a\C-k$(__fsel_tmux)\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"' bind '"\C-t": " \C-u \C-a\C-k$(__fzf_select_tmux__)\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"'
else else
bind '"\C-t": " \C-u \C-a\C-k$(__fsel)\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"' bind '"\C-t": " \C-u \C-a\C-k$(__fzf_select__)\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"'
fi fi
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r | sed \"s/ *[0-9]* *//\")\e\C-e\er"' bind '"\C-r": " \C-e\C-u$(__fzf_history__)\e\C-e\er"'
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
bind '"\ec": " \C-e\C-u$(__fcd)\e\C-e\er\C-m"' bind '"\ec": " \C-e\C-u$(__fzf_cd__)\e\C-e\er\C-m"'
else else
bind '"\C-x\C-e": shell-expand-line' bind '"\C-x\C-e": shell-expand-line'
bind '"\C-x\C-r": redraw-current-line' bind '"\C-x\C-r": redraw-current-line'
@@ -55,18 +63,18 @@ else
# CTRL-T - Paste the selected file path into the command line # CTRL-T - Paste the selected file path into the command line
# - FIXME: Selected items are attached to the end regardless of cursor position # - FIXME: Selected items are attached to the end regardless of cursor position
if [ $__use_tmux -eq 1 ]; then if [ $__use_tmux -eq 1 ]; then
bind '"\C-t": "\e$a \eddi$(__fsel_tmux)\C-x\C-e\e0P$xa"' bind '"\C-t": "\e$a \eddi$(__fzf_select_tmux__)\C-x\C-e\e0P$xa"'
else else
bind '"\C-t": "\e$a \eddi$(__fsel)\C-x\C-e\e0Px$a \C-x\C-r\exa "' bind '"\C-t": "\e$a \eddi$(__fzf_select__)\C-x\C-e\e0Px$a \C-x\C-r\exa "'
fi fi
bind -m vi-command '"\C-t": "i\C-t"' bind -m vi-command '"\C-t": "i\C-t"'
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": "\eddi$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r | sed \"s/ *[0-9]* *//\")\C-x\C-e\e$a\C-x\C-r"' bind '"\C-r": "\eddi$(__fzf_history__)\C-x\C-e\e$a\C-x\C-r"'
bind -m vi-command '"\C-r": "i\C-r"' bind -m vi-command '"\C-r": "i\C-r"'
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
bind '"\ec": "\eddi$(__fcd)\C-x\C-e\C-x\C-r\C-m"' bind '"\ec": "\eddi$(__fzf_cd__)\C-x\C-e\C-x\C-r\C-m"'
bind -m vi-command '"\ec": "i\ec"' bind -m vi-command '"\ec": "i\ec"'
fi fi

View File

@@ -7,18 +7,6 @@ function fzf_key_bindings
set -g TMPDIR /tmp set -g TMPDIR /tmp
end end
function __fzf_list
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3-
end
function __fzf_list_dir
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) \
-prune -o -type d -print 2> /dev/null | sed 1d | cut -b3-
end
function __fzf_escape function __fzf_escape
while read item while read item
echo -n (echo -n "$item" | sed -E 's/([ "$~'\''([{<>})])/\\\\\\1/g')' ' echo -n (echo -n "$item" | sed -E 's/([ "$~'\''([{<>})])/\\\\\\1/g')' '
@@ -26,25 +14,17 @@ function fzf_key_bindings
end end
function __fzf_ctrl_t function __fzf_ctrl_t
if [ -n "$TMUX_PANE" -a "$FZF_TMUX" != "0" ] command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
# FIXME need to handle directory with double-quotes -o -type f -print \
tmux split-window (__fzf_tmux_height) "cd \"$PWD\";fish -c 'fzf_key_bindings; __fzf_ctrl_t_tmux \\$TMUX_PANE'" -o -type d -print \
else -o -type l -print 2> /dev/null | sed 1d | cut -b3- | eval (__fzfcmd) -m > $TMPDIR/fzf.result
__fzf_list | fzf -m > $TMPDIR/fzf.result and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape)
and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) commandline -f repaint
commandline -f repaint
rm -f $TMPDIR/fzf.result
end
end
function __fzf_ctrl_t_tmux
__fzf_list | fzf -m > $TMPDIR/fzf.result
and tmux send-keys -t $argv[1] (cat $TMPDIR/fzf.result | __fzf_escape)
rm -f $TMPDIR/fzf.result rm -f $TMPDIR/fzf.result
end end
function __fzf_ctrl_r function __fzf_ctrl_r
history | fzf +s +m --tiebreak=index --toggle-sort=ctrl-r > $TMPDIR/fzf.result history | eval (__fzfcmd) +s +m --tiebreak=index --toggle-sort=ctrl-r > $TMPDIR/fzf.result
and commandline (cat $TMPDIR/fzf.result) and commandline (cat $TMPDIR/fzf.result)
commandline -f repaint commandline -f repaint
rm -f $TMPDIR/fzf.result rm -f $TMPDIR/fzf.result
@@ -52,25 +32,26 @@ function fzf_key_bindings
function __fzf_alt_c function __fzf_alt_c
# Fish hangs if the command before pipe redirects (2> /dev/null) # Fish hangs if the command before pipe redirects (2> /dev/null)
__fzf_list_dir | fzf +m > $TMPDIR/fzf.result command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) \
-prune -o -type d -print 2> /dev/null | sed 1d | cut -b3- | eval (__fzfcmd) +m > $TMPDIR/fzf.result
[ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ] [ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ]
and cd (cat $TMPDIR/fzf.result) and cd (cat $TMPDIR/fzf.result)
commandline -f repaint commandline -f repaint
rm -f $TMPDIR/fzf.result rm -f $TMPDIR/fzf.result
end end
function __fzf_tmux_height function __fzfcmd
if set -q FZF_TMUX_HEIGHT set -q FZF_TMUX; or set FZF_TMUX 1
set height $FZF_TMUX_HEIGHT
if [ $FZF_TMUX -eq 1 ]
if set -q FZF_TMUX_HEIGHT
echo "fzf-tmux -d$FZF_TMUX_HEIGHT"
else
echo "fzf-tmux -d40%"
end
else else
set height 40% echo "fzf"
end end
if echo $height | \grep -q -E '%$'
echo "-p "(echo $height | sed 's/%$//')
else
echo "-l $height"
end
set -e height
end end
bind \ct '__fzf_ctrl_t' bind \ct '__fzf_ctrl_t'

View File

@@ -5,38 +5,29 @@ __fsel() {
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -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 | sed 1d | cut -b3- | fzf -m | while read item; do -o -type l -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) -m | while read item; do
printf '%q ' "$item" printf '%q ' "$item"
done done
echo echo
} }
__fzfcmd() {
[ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
}
if [[ $- =~ i ]]; then if [[ $- =~ i ]]; then
if [ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ]; then fzf-file-widget() {
fzf-file-widget() { LBUFFER="${LBUFFER}$(__fsel)"
local height zle redisplay
height=${FZF_TMUX_HEIGHT:-40%} }
if [[ $height =~ %$ ]]; then
height="-p ${height%\%}"
else
height="-l $height"
fi
tmux split-window $height "cd $(printf %q "$PWD");zsh -c 'source ~/.fzf.zsh; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'"
}
else
fzf-file-widget() {
LBUFFER="${LBUFFER}$(__fsel)"
zle redisplay
}
fi
zle -N fzf-file-widget zle -N fzf-file-widget
bindkey '^T' fzf-file-widget 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 "${$(command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ cd "${$(command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3- | fzf +m):-.}" -o -type d -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) +m):-.}"
zle reset-prompt zle reset-prompt
} }
zle -N fzf-cd-widget zle -N fzf-cd-widget
@@ -45,7 +36,7 @@ bindkey '\ec' fzf-cd-widget
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() { fzf-history-widget() {
local selected local selected
if selected=$(fc -l 1 | fzf +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER"); then if selected=$(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER"); then
num=$(echo "$selected" | head -1 | awk '{print $1}' | sed 's/[^0-9]//g') num=$(echo "$selected" | head -1 | awk '{print $1}' | sed 's/[^0-9]//g')
LBUFFER=!$num LBUFFER=!$num
zle expand-history zle expand-history

View File

@@ -42,10 +42,8 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
// compiler as of now does not inline non-leaf functions.) // compiler as of now does not inline non-leaf functions.)
if char >= 'A' && char <= 'Z' { if char >= 'A' && char <= 'Z' {
char += 32 char += 32
(*runes)[index] = char
} else if char > unicode.MaxASCII { } else if char > unicode.MaxASCII {
char = unicode.To(unicode.LowerCase, char) char = unicode.To(unicode.LowerCase, char)
(*runes)[index] = char
} }
} }
if char == pattern[pidx] { if char == pattern[pidx] {
@@ -63,6 +61,13 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
pidx-- pidx--
for index := eidx - 1; index >= sidx; index-- { for index := eidx - 1; index >= sidx; index-- {
char := (*runes)[index] char := (*runes)[index]
if !caseSensitive {
if char >= 'A' && char <= 'Z' {
char += 32
} else if char > unicode.MaxASCII {
char = unicode.To(unicode.LowerCase, char)
}
}
if char == pattern[pidx] { if char == pattern[pidx] {
if pidx--; pidx < 0 { if pidx--; pidx < 0 {
sidx = index sidx = index

View File

@@ -8,7 +8,7 @@ import (
const ( const (
// Current version // Current version
Version = "0.9.10" Version = "0.9.11"
// Core // Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond coordinatorDelayMax time.Duration = 100 * time.Millisecond

View File

@@ -40,6 +40,7 @@ const usage = `usage: fzf [options]
--black Use black background --black Use black background
--reverse Reverse orientation --reverse Reverse orientation
--no-hscroll Disable horizontal scroll --no-hscroll Disable horizontal scroll
--inline-info Display finder info inline with the query
--prompt=STR Input prompt (default: '> ') --prompt=STR Input prompt (default: '> ')
Scripting Scripting
@@ -105,6 +106,7 @@ type Options struct {
Black bool Black bool
Reverse bool Reverse bool
Hscroll bool Hscroll bool
InlineInfo bool
Prompt string Prompt string
Query string Query string
Select1 bool Select1 bool
@@ -141,6 +143,7 @@ func defaultOptions() *Options {
Black: false, Black: false,
Reverse: false, Reverse: false,
Hscroll: true, Hscroll: true,
InlineInfo: false,
Prompt: "> ", Prompt: "> ",
Query: "", Query: "",
Select1: false, Select1: false,
@@ -364,6 +367,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Hscroll = true opts.Hscroll = true
case "--no-hscroll": case "--no-hscroll":
opts.Hscroll = false opts.Hscroll = false
case "--inline-info":
opts.InlineInfo = true
case "--no-inline-info":
opts.InlineInfo = false
case "-1", "--select-1": case "-1", "--select-1":
opts.Select1 = true opts.Select1 = true
case "+1", "--no-select-1": case "+1", "--no-select-1":

View File

@@ -4,7 +4,6 @@ import (
"regexp" "regexp"
"sort" "sort"
"strings" "strings"
"unicode"
"github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/algo"
) )
@@ -28,10 +27,11 @@ const (
) )
type term struct { type term struct {
typ termType typ termType
inv bool inv bool
text []rune text []rune
origText []rune caseSensitive bool
origText []rune
} }
// Pattern represents search pattern // Pattern represents search pattern
@@ -88,36 +88,27 @@ func BuildPattern(mode Mode, caseMode Case,
caseSensitive, hasInvTerm := true, false caseSensitive, hasInvTerm := true, false
terms := []term{} terms := []term{}
switch caseMode {
case CaseSmart:
hasUppercase := false
for _, r := range runes {
if unicode.IsUpper(r) {
hasUppercase = true
break
}
}
if !hasUppercase {
runes, caseSensitive = []rune(strings.ToLower(asString)), false
}
case CaseIgnore:
runes, caseSensitive = []rune(strings.ToLower(asString)), false
}
switch mode { switch mode {
case ModeExtended, ModeExtendedExact: case ModeExtended, ModeExtendedExact:
terms = parseTerms(mode, string(runes)) terms = parseTerms(mode, caseMode, asString)
for _, term := range terms { for _, term := range terms {
if term.inv { if term.inv {
hasInvTerm = true hasInvTerm = true
} }
} }
default:
lowerString := strings.ToLower(asString)
caseSensitive = caseMode == CaseRespect ||
caseMode == CaseSmart && lowerString != asString
if !caseSensitive {
asString = lowerString
}
} }
ptr := &Pattern{ ptr := &Pattern{
mode: mode, mode: mode,
caseSensitive: caseSensitive, caseSensitive: caseSensitive,
text: runes, text: []rune(asString),
terms: terms, terms: terms,
hasInvTerm: hasInvTerm, hasInvTerm: hasInvTerm,
nth: nth, nth: nth,
@@ -133,11 +124,17 @@ func BuildPattern(mode Mode, caseMode Case,
return ptr return ptr
} }
func parseTerms(mode Mode, str string) []term { func parseTerms(mode Mode, caseMode Case, str string) []term {
tokens := _splitRegex.Split(str, -1) tokens := _splitRegex.Split(str, -1)
terms := []term{} terms := []term{}
for _, token := range tokens { for _, token := range tokens {
typ, inv, text := termFuzzy, false, token typ, inv, text := termFuzzy, false, token
lowerText := strings.ToLower(text)
caseSensitive := caseMode == CaseRespect ||
caseMode == CaseSmart && text != lowerText
if !caseSensitive {
text = lowerText
}
origText := []rune(text) origText := []rune(text)
if mode == ModeExtendedExact { if mode == ModeExtendedExact {
typ = termExact typ = termExact
@@ -163,10 +160,11 @@ func parseTerms(mode Mode, str string) []term {
if len(text) > 0 { if len(text) > 0 {
terms = append(terms, term{ terms = append(terms, term{
typ: typ, typ: typ,
inv: inv, inv: inv,
text: []rune(text), text: []rune(text),
origText: origText}) caseSensitive: caseSensitive,
origText: origText})
} }
} }
return terms return terms
@@ -280,7 +278,7 @@ func dupItem(item *Item, offsets []Offset) *Item {
func (p *Pattern) fuzzyMatch(item *Item) (int, int) { func (p *Pattern) fuzzyMatch(item *Item) (int, int) {
input := p.prepareInput(item) input := p.prepareInput(item)
return p.iter(algo.FuzzyMatch, input, p.text) return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.text)
} }
func (p *Pattern) extendedMatch(item *Item) []Offset { func (p *Pattern) extendedMatch(item *Item) []Offset {
@@ -288,7 +286,7 @@ func (p *Pattern) extendedMatch(item *Item) []Offset {
offsets := []Offset{} offsets := []Offset{}
for _, term := range p.terms { for _, term := range p.terms {
pfun := p.procFun[term.typ] pfun := p.procFun[term.typ]
if sidx, eidx := p.iter(pfun, input, term.text); sidx >= 0 { if sidx, eidx := p.iter(pfun, input, term.caseSensitive, term.text); sidx >= 0 {
if term.inv { if term.inv {
break break
} }
@@ -319,10 +317,10 @@ func (p *Pattern) prepareInput(item *Item) *[]Token {
} }
func (p *Pattern) iter(pfun func(bool, *[]rune, []rune) (int, int), func (p *Pattern) iter(pfun func(bool, *[]rune, []rune) (int, int),
tokens *[]Token, pattern []rune) (int, int) { tokens *[]Token, caseSensitive bool, pattern []rune) (int, int) {
for _, part := range *tokens { for _, part := range *tokens {
prefixLength := part.prefixLength prefixLength := part.prefixLength
if sidx, eidx := pfun(p.caseSensitive, part.text, pattern); sidx >= 0 { if sidx, eidx := pfun(caseSensitive, part.text, pattern); sidx >= 0 {
return sidx + prefixLength, eidx + prefixLength return sidx + prefixLength, eidx + prefixLength
} }
} }

View File

@@ -7,7 +7,7 @@ import (
) )
func TestParseTermsExtended(t *testing.T) { func TestParseTermsExtended(t *testing.T) {
terms := parseTerms(ModeExtended, terms := parseTerms(ModeExtended, CaseSmart,
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$")
if len(terms) != 8 || if len(terms) != 8 ||
terms[0].typ != termFuzzy || terms[0].inv || terms[0].typ != termFuzzy || terms[0].inv ||
@@ -31,7 +31,7 @@ func TestParseTermsExtended(t *testing.T) {
} }
func TestParseTermsExtendedExact(t *testing.T) { func TestParseTermsExtendedExact(t *testing.T) {
terms := parseTerms(ModeExtendedExact, terms := parseTerms(ModeExtendedExact, CaseSmart,
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$")
if len(terms) != 8 || if len(terms) != 8 ||
terms[0].typ != termExact || terms[0].inv || len(terms[0].text) != 3 || terms[0].typ != termExact || terms[0].inv || len(terms[0].text) != 3 ||
@@ -47,7 +47,7 @@ func TestParseTermsExtendedExact(t *testing.T) {
} }
func TestParseTermsEmpty(t *testing.T) { func TestParseTermsEmpty(t *testing.T) {
terms := parseTerms(ModeExtended, "' $ ^ !' !^ !$") terms := parseTerms(ModeExtended, CaseSmart, "' $ ^ !' !^ !$")
if len(terms) != 0 { if len(terms) != 0 {
t.Errorf("%s", terms) t.Errorf("%s", terms)
} }

View File

@@ -20,6 +20,7 @@ import (
// Terminal represents terminal input/output // Terminal represents terminal input/output
type Terminal struct { type Terminal struct {
inlineInfo bool
prompt string prompt string
reverse bool reverse bool
hscroll bool hscroll bool
@@ -83,6 +84,7 @@ const (
func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
input := []rune(opts.Query) input := []rune(opts.Query)
return &Terminal{ return &Terminal{
inlineInfo: opts.InlineInfo,
prompt: opts.Prompt, prompt: opts.Prompt,
reverse: opts.Reverse, reverse: opts.Reverse,
hscroll: opts.Hscroll, hscroll: opts.Hscroll,
@@ -229,14 +231,23 @@ func (t *Terminal) printPrompt() {
} }
func (t *Terminal) printInfo() { func (t *Terminal) printInfo() {
t.move(1, 0, true) if t.inlineInfo {
if t.reading { t.move(0, len(t.prompt)+displayWidth(t.input)+1, true)
duration := int64(spinnerDuration) if t.reading {
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration C.CPrint(C.ColSpinner, true, " < ")
C.CPrint(C.ColSpinner, true, _spinner[idx]) } else {
C.CPrint(C.ColPrompt, true, " < ")
}
} else {
t.move(1, 0, true)
if t.reading {
duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
C.CPrint(C.ColSpinner, true, _spinner[idx])
}
t.move(1, 2, false)
} }
t.move(1, 2, false)
output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count) output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count)
if t.toggleSort > 0 { if t.toggleSort > 0 {
if t.sort { if t.sort {
@@ -257,10 +268,16 @@ func (t *Terminal) printInfo() {
func (t *Terminal) printList() { func (t *Terminal) printList() {
t.constrain() t.constrain()
maxy := maxItems() maxy := t.maxItems()
count := t.merger.Length() - t.offset count := t.merger.Length() - t.offset
for i := 0; i < maxy; i++ { for i := 0; i < maxy; i++ {
t.move(i+2, 0, true) var line int
if t.inlineInfo {
line = i + 1
} else {
line = i + 2
}
t.move(line, 0, true)
if i < count { if i < count {
t.printItem(t.merger.Get(i+t.offset), i == t.cy-t.offset) t.printItem(t.merger.Get(i+t.offset), i == t.cy-t.offset)
} }
@@ -515,6 +532,9 @@ func (t *Terminal) Loop() {
switch req { switch req {
case reqPrompt: case reqPrompt:
t.printPrompt() t.printPrompt()
if t.inlineInfo {
t.printInfo()
}
case reqInfo: case reqInfo:
t.printInfo() t.printInfo()
case reqList: case reqList:
@@ -659,10 +679,10 @@ func (t *Terminal) Loop() {
case C.Del: case C.Del:
t.delChar() t.delChar()
case C.PgUp: case C.PgUp:
t.vmove(maxItems() - 1) t.vmove(t.maxItems() - 1)
req(reqList) req(reqList)
case C.PgDn: case C.PgDn:
t.vmove(-(maxItems() - 1)) t.vmove(-(t.maxItems() - 1))
req(reqList) req(reqList)
case C.AltB: case C.AltB:
t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1 t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1
@@ -685,6 +705,10 @@ func (t *Terminal) Loop() {
if !t.reverse { if !t.reverse {
my = C.MaxY() - my - 1 my = C.MaxY() - my - 1
} }
min := 2
if t.inlineInfo {
min = 1
}
if me.S != 0 { if me.S != 0 {
// Scroll // Scroll
if t.merger.Length() > 0 { if t.merger.Length() > 0 {
@@ -696,8 +720,8 @@ func (t *Terminal) Loop() {
} }
} else if me.Double { } else if me.Double {
// Double-click // Double-click
if my >= 2 { if my >= min {
if t.vset(my-2) && t.cy < t.merger.Length() { if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
req(reqClose) req(reqClose)
} }
} }
@@ -705,9 +729,9 @@ func (t *Terminal) Loop() {
if my == 0 && mx >= 0 { if my == 0 && mx >= 0 {
// Prompt // Prompt
t.cx = mx t.cx = mx
} else if my >= 2 { } else if my >= min {
// List // List
if t.vset(t.offset+my-2) && t.multi && me.Mod { if t.vset(t.offset+my-min) && t.multi && me.Mod {
toggle() toggle()
} }
req(reqList) req(reqList)
@@ -728,7 +752,7 @@ func (t *Terminal) Loop() {
func (t *Terminal) constrain() { func (t *Terminal) constrain() {
count := t.merger.Length() count := t.merger.Length()
height := C.MaxY() - 2 height := t.maxItems()
diffpos := t.cy - t.offset diffpos := t.cy - t.offset
t.cy = util.Constrain(t.cy, 0, count-1) t.cy = util.Constrain(t.cy, 0, count-1)
@@ -761,6 +785,10 @@ func (t *Terminal) vset(o int) bool {
return t.cy == o return t.cy == o
} }
func maxItems() int { func (t *Terminal) maxItems() int {
return C.MaxY() - 2 if t.inlineInfo {
return C.MaxY() - 1
} else {
return C.MaxY() - 2
}
} }

View File

@@ -525,6 +525,20 @@ class TestGoFZF < TestBase
File.unlink tempname File.unlink tempname
end end
def test_invalid_cache
tmux.send_keys "(echo d; echo D; echo x) | #{fzf '-q d'}", :Enter
tmux.until { |lines| lines[-2].include? '2/3' }
tmux.send_keys :BSpace
tmux.until { |lines| lines[-2].include? '3/3' }
tmux.send_keys :D
tmux.until { |lines| lines[-2].include? '1/3' }
tmux.send_keys :Enter
end
def test_smart_case_for_each_term
assert_equal 1, `echo Foo bar | #{FZF} -x -f "foo Fbar" | wc -l`.to_i
end
private private
def writelines path, lines, timeout = 10 def writelines path, lines, timeout = 10
File.open(path, 'w') do |f| File.open(path, 'w') do |f|
@@ -568,11 +582,11 @@ module TestShell
def test_alt_c def test_alt_c
tmux.prepare tmux.prepare
tmux.send_keys :Escape, :c tmux.send_keys :Escape, :c, pane: 0
lines = tmux.until { |lines| lines[-1].start_with? '>' } lines = tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' }
expected = lines[-3][2..-1] expected = lines[-3][2..-1]
p expected p expected
tmux.send_keys :Enter tmux.send_keys :Enter, pane: 1
tmux.prepare tmux.prepare
tmux.send_keys :pwd, :Enter tmux.send_keys :pwd, :Enter
tmux.until { |lines| p lines; lines[-1].end_with?(expected) } tmux.until { |lines| p lines; lines[-1].end_with?(expected) }
@@ -585,11 +599,11 @@ module TestShell
tmux.send_keys 'echo 3d', :Enter; tmux.prepare tmux.send_keys 'echo 3d', :Enter; tmux.prepare
tmux.send_keys 'echo 3rd', :Enter; tmux.prepare tmux.send_keys 'echo 3rd', :Enter; tmux.prepare
tmux.send_keys 'echo 4th', :Enter; tmux.prepare tmux.send_keys 'echo 4th', :Enter; tmux.prepare
tmux.send_keys 'C-r' tmux.send_keys 'C-r', pane: 0
tmux.until { |lines| lines[-1].start_with? '>' } tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' }
tmux.send_keys '3d' tmux.send_keys '3d', pane: 1
tmux.until { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort tmux.until(pane: 1) { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort
tmux.send_keys :Enter tmux.send_keys :Enter, pane: 1
tmux.until { |lines| lines[-1] == 'echo 3rd' } tmux.until { |lines| lines[-1] == 'echo 3rd' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == '3rd' } tmux.until { |lines| lines[-1] == '3rd' }