Compare commits

..

81 Commits

Author SHA1 Message Date
Junegunn Choi
52771a6226 0.9.13 2015-06-03 02:09:07 +09:00
Junegunn Choi
b00bcf506e Fix #248 - Premature termination of Reader on long input 2015-06-03 01:48:02 +09:00
Junegunn Choi
fdbfe36c0b Color customization (#245) 2015-06-03 01:46:03 +09:00
Junegunn Choi
446e822723 Update CHANGELOG 2015-05-22 02:37:38 +09:00
Junegunn Choi
b68e59a24b Fix ANSI offset calculation 2015-05-22 02:20:10 +09:00
Junegunn Choi
4e0e492427 Minor refactoring 2015-05-22 00:02:14 +09:00
Junegunn Choi
8f99f8fcc6 More test cases for --bind 2015-05-21 21:06:52 +09:00
Junegunn Choi
3cdf71801e Update --help 2015-05-21 01:51:24 +09:00
Junegunn Choi
801cf9ac62 Add unbound "toggle" action for customization 2015-05-21 01:37:16 +09:00
Junegunn Choi
34946b72a5 0.9.12 2015-05-21 00:44:49 +09:00
Junegunn Choi
1592bedbe8 Custom key binding support (#238) 2015-05-21 00:32:03 +09:00
Junegunn Choi
15099eb13b Remove duplicate processing of command-line options 2015-05-20 20:42:45 +09:00
Junegunn Choi
c511b45ff6 Minor tweak in test case
It may take long for find command to spot the temporary file created on
the home directory
2015-05-20 19:47:48 +09:00
Junegunn Choi
40761b11b1 [bash] Ignore asterisk (modified) in history 2015-05-20 19:45:05 +09:00
Junegunn Choi
cca543d0cd [zsh-completion] Fix #236 - zle redisplay 2015-05-20 16:18:30 +09:00
Junegunn Choi
34e5e2dd82 [vim] Use close+bufhidden=wipe instead of bd 2015-05-14 13:29:50 +09:00
Junegunn Choi
2b7c3df66b [neovim] Check tabpagenr() as well 2015-05-14 02:19:40 +09:00
Junegunn Choi
f766531e74 [neovim] Make sure that fzf buffer is closed (#225)
- bd! leaves the window open when there's no other listed buffer
- redraw! seems to help avoid Neovim issues.
2015-05-14 02:14:21 +09:00
Junegunn Choi
7f59b42b05 [vim] Escape % # \ 2015-05-13 23:20:10 +09:00
Junegunn Choi
f41de932d6 [vim] Refocus MacVim window 2015-05-13 23:14:03 +09:00
Junegunn Choi
b4a05ff27e [bash] CTRL-R to use history-expand-line
Close #146
2015-05-13 19:13:27 +09:00
Junegunn Choi
3b91467941 Suppress error message when loading completion.{zsh,bash}
Temporary workaround for https://github.com/Homebrew/homebrew/issues/39669
2015-05-12 22:54:48 +09:00
Junegunn Choi
26d2af5ee8 [zsh-completion] Respect backslash-escaped spaces (#230) 2015-05-12 01:40:44 +09:00
Junegunn Choi
2b61dc6557 [zsh-completion] Do not overwrite $fzf_default_completion 2015-05-11 22:53:35 +09:00
Junegunn Choi
0b770cd48a [zsh-completion] Remember what ^I was originally bound to (#230) 2015-05-11 21:49:40 +09:00
Junegunn Choi
c14aa99ef6 [zsh/bash-completion] Avoid caret expansion
Close #233

setopt extendedglob on zsh caused caret in grep pattern to be expanded.
Problem identified and patch submitted by @lazywei.
2015-05-11 16:59:44 +09:00
Junegunn Choi
8f371ee81c [zsh-completion] fzf-zsh-completion -> fzf-completion 2015-05-11 13:11:42 +09:00
Junegunn Choi
3b63b39810 [zsh-completion] Allow empty prefix & trigger sequence (#232) 2015-05-11 13:06:02 +09:00
Tiziano Santoro
0cd238700c [zsh-completion] Add comment clarifying trigger expansion. (#230) 2015-05-11 10:18:28 +09:00
Tiziano Santoro
14fbe06d9e [zsh-completion] Allow specifying empty completion trigger. (#230) 2015-05-11 10:18:16 +09:00
Junegunn Choi
64949bf467 [bash-completion] Allow specifying empty completion trigger (#230) 2015-05-11 10:17:33 +09:00
Junegunn Choi
732f133940 [test] Make sure to kill background process 2015-05-10 11:24:54 +09:00
Junegunn Choi
5dc4df9570 Fix test cases 2015-05-10 05:01:52 +09:00
Junegunn Choi
7dde8dbbd9 Merge pull request #231 from robinro/head-argument-typo
[zsh] `head -n1` instead of `head -1`
2015-05-10 04:50:28 +09:00
Robin Roth
01405ad92e fix typo in argument of head
at least my version of head wants -n1 to only display the first line
2015-05-09 21:11:01 +02:00
Junegunn Choi
683abb86ef Dump screen content on test failure 2015-05-10 03:25:14 +09:00
Junegunn Choi
207aa07891 [zsh-completion] Temporarily set nonomatch (#230)
No error on ~INVALID_USERNAME**<TAB>
2015-05-10 02:54:22 +09:00
Junegunn Choi
26a141c6a6 [zsh-completion] Fix ~USERNAME** (#230) 2015-05-10 02:37:17 +09:00
Junegunn Choi
dc64568c83 [zsh-completion] Completion for unknown commands 2015-05-09 21:04:59 +09:00
Junegunn Choi
f4a595eedd Fix Travis CI build 2015-05-09 20:42:13 +09:00
Junegunn Choi
5a17a6323a [zsh-completion] "\find" to bypass alias 2015-05-09 20:36:25 +09:00
Junegunn Choi
2b8e445321 Fuzzy completion for zsh (#227) 2015-05-09 20:18:38 +09:00
Junegunn Choi
315499b1d4 Merge pull request #229 from sullyj3/master
fix typo in README.md
2015-05-09 00:36:52 +09:00
James Sully
65a2bdb01d fix typo in README.md 2015-05-09 01:26:34 +10:00
Junegunn Choi
ed8202efc6 [bash-completion] Ignore 0.0.0.0
Close #228
2015-05-08 18:16:55 +09:00
Junegunn Choi
0937bd6c16 [vim] Improve binary detection
/cc @alerque

- Ask for user confirmation before running `install --bin`
- Removed `s:fzf_rb` since `install --bin` will create a wrapper
  executable that just runs Ruby version on the platforms where prebuilt
  binaries are not available.
2015-05-03 00:58:45 +09:00
Junegunn Choi
3d26b5336c [vim] Fix #220 - Prevent error after update 2015-04-28 23:49:52 +09:00
Junegunn Choi
c8f208b96b Merge pull request #171 from oschrenk/vi-insert-mode-key-bindings-fish
Support for vi insert mode in upcoming fish 2.2.0
2015-04-26 02:17:46 +09:00
Oliver Schrenk
2e339e49b8 Support for vi insert mode in upcoming fish 2.2.0 2015-04-25 19:12:11 +02:00
Junegunn Choi
5d9107fd15 Print info after prompt on redraw
This fixes the issue where "inline-info" is not immediately rendered
when the terminal is resized.
2015-04-25 23:20:40 +09:00
Junegunn Choi
4b7c571575 Fix race condition in test case 2015-04-25 10:56:08 +09:00
Junegunn Choi
5502b68a1d Test refactoring 2015-04-25 10:40:58 +09:00
Junegunn Choi
5794fd42df Fix test code 2015-04-25 01:09:25 +09:00
Junegunn Choi
9c6e46ab15 [fzf-tmux] Fix #215 - Prepend env to avoid error on fish 2015-04-24 12:54:57 +09:00
Junegunn Choi
09d0ac0347 [vim] Update default launcher for GVim (#212)
Code submitted by @lydell
2015-04-24 12:45:39 +09:00
Junegunn Choi
22ae7adac8 Update completion for fzf itself 2015-04-23 22:43:48 +09:00
Junegunn Choi
36924d0b1c [zsh] Do not change LBUFFER on empty selection (CTRL-R) 2015-04-23 22:39:07 +09:00
Junegunn Choi
6ed9de9051 [zsh] Temporarily unset no_bang_hist for CTRL-R
Close #214
2015-04-23 22:31:23 +09:00
Junegunn Choi
857619995e [vim] Ignore E325 (#213) 2015-04-23 19:29:59 +09:00
Junegunn Choi
9310ae28ab [vim] Redraw screen after running fzf on tmux pane (#213) 2015-04-23 19:29:01 +09:00
Junegunn Choi
27e26bd1ea [vim] Add g:Fzf_launcher for funcrefs (#212) 2015-04-23 12:51:08 +09:00
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
25 changed files with 1255 additions and 548 deletions

View File

@@ -1,6 +1,45 @@
CHANGELOG CHANGELOG
========= =========
0.9.13
------
### New features
- Color customization with the extended `--color` option
### Bug fixes
- Fixed premature termination of Reader in the presence of a long line which
is longer than 64KB
0.9.12
------
### New features
- Added `--bind` option for custom key bindings
### Bug fixes
- Fixed to update "inline-info" immediately after terminal resize
- Fixed ANSI code offset calculation
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

@@ -8,7 +8,7 @@ fzf is a general-purpose command-line fuzzy finder.
Pros Pros
---- ----
- No dependency - No dependencies
- Blazingly fast - Blazingly fast
- e.g. `locate / | fzf` - e.g. `locate / | fzf`
- Flexible layout - Flexible layout
@@ -27,7 +27,7 @@ fzf project consists of the followings:
- `fzf-tmux` script for launching fzf in a tmux pane - `fzf-tmux` script for launching fzf in a tmux pane
- Shell extensions - Shell extensions
- Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish) - Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish)
- Fuzzy auto-completion (bash only) - Fuzzy auto-completion (bash, zsh)
- Vim/Neovim plugin - Vim/Neovim plugin
You can [download fzf executable][bin] alone, but it's recommended that you You can [download fzf executable][bin] alone, but it's recommended that you
@@ -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
----------------- -----------------
@@ -188,8 +173,8 @@ cat /usr/share/dict/words | fzf-tmux -l 20% --multi --reverse
It will still work even when you're not on tmux, silently ignoring `-[udlr]` It will still work even when you're not on tmux, silently ignoring `-[udlr]`
options, so you can invariably use `fzf-tmux` in your scripts. options, so you can invariably use `fzf-tmux` in your scripts.
Fuzzy completion for bash Fuzzy completion for bash and zsh
------------------------- ---------------------------------
#### Files and directories #### Files and directories
@@ -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])`
@@ -309,7 +294,8 @@ of the selected items.
| `dir` | string | Working directory | | `dir` | string | Working directory |
| `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) | | `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) |
| `window` (*Neovim only*) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) | | `window` (*Neovim only*) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) |
| `launcher` | string | External terminal emulator to start fzf with (Only used in GVim) | | `launcher` | string | External terminal emulator to start fzf with (GVim only) |
| `launcher` | funcref | Function for generating `launcher` string (GVim only) |
_However on Neovim `fzf#run` is asynchronous and does not return values so you _However on Neovim `fzf#run` is asynchronous and does not return values so you
should use `sink` or `sink*` to process the output from fzf._ should use `sink` or `sink*` to process the output from fzf._
@@ -372,10 +358,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
---- ----
@@ -388,9 +370,9 @@ If you have any rendering issues, check the followings:
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 +403,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 +411,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
@@ -109,19 +107,28 @@ fail() {
fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf" fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf"
[ -x "$fzf" ] || fail "fzf executable not found" [ -x "$fzf" ] || fail "fzf executable not found"
envs="" envs="env "
[ -n "$FZF_DEFAULT_OPTS" ] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" [ -n "$FZF_DEFAULT_OPTS" ] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
[ -n "$FZF_DEFAULT_COMMAND" ] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" [ -n "$FZF_DEFAULT_COMMAND" ] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
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

3
fzf
View File

@@ -206,9 +206,10 @@ class FZF
@expect = true @expect = true
when /^--expect=(.*)$/ when /^--expect=(.*)$/
@expect = true @expect = true
when '--toggle-sort', '--tiebreak', '--color' when '--toggle-sort', '--tiebreak', '--color', '--bind'
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', /^--bind=(.*)$/,
/^--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.13
cd $(dirname $BASH_SOURCE) cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd) fzf_base=$(pwd)
@@ -176,8 +176,8 @@ 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/shell/completion.${shell}\"" fzf_completion="[[ \$- =~ i ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null"
if [ $shell != bash -o $auto_completion -ne 0 ]; then if [ $auto_completion -ne 0 ]; then
fzf_completion="# $fzf_completion" fzf_completion="# $fzf_completion"
fi fi

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 "June 2015" "fzf 0.9.13" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -91,21 +91,38 @@ Enable processing of ANSI color codes
.B "--no-mouse" .B "--no-mouse"
Disable mouse Disable mouse
.TP .TP
.B "--color=COL" .BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]"
Color scheme: [dark|light|16|bw] Color configuration. The name of the base color scheme is followed by custom
.br color mappings. Ansi color code of -1 denotes terminal default
(default: dark on 256-color terminal, otherwise 16) foreground/background color.
.br
.R "" .RS
.br e.g. \fBfzf --color=bg+:24\fR
.BR dark " Color scheme for dark 256-color terminal" \fBfzf --color=light,fg:232,bg:255,bg+:116,info:27\fR
.br .RE
.BR light " Color scheme for light 256-color terminal"
.br .RS
.BR 16 " Color scheme for 16-color terminal" .B BASE SCHEME:
.br (default: dark on 256-color terminal, otherwise 16)
.BR bw " No colors"
.br \fBdark \fRColor scheme for dark 256-color terminal
\fBlight \fRColor scheme for light 256-color terminal
\fB16 \fRColor scheme for 16-color terminal
\fBbw \fRNo colors
.B COLOR:
\fBfg \fRText
\fBbg \fRBackground
\fBhl \fRHighlighted substrings
\fBfg+ \fRText (current line)
\fBbg+ \fRBackground (current line)
\fBhl+ \fRHighlighted substrings (current line)
\fBinfo \fRInfo
\fBprompt \fRPrompt
\fBpointer \fRPointer to the current line
\fBmarker \fRMulti-select marker
\fBspinner \fRStreaming input indicator
.RE
.TP .TP
.B "--black" .B "--black"
Use black background Use black background
@@ -116,8 +133,56 @@ 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: '> ')
.TP
.BI "--toggle-sort=" "KEY"
Key to toggle sort (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR,
or any single character)
.TP
.BI "--bind=" "KEYBINDS"
Comma-separated list of custom key bindings. Each key binding expression
follows the following format: \fBKEY:ACTION\fR
.RS
e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
.RE
.RS
.B KEY:
\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR, or any single character
.RE
.RS
.B ACTION:
abort
accept
backward-char
backward-delete-char
backward-kill-word
backward-word
beginning-of-line
clear-screen
delete-char
down
end-of-line
forward-char
forward-word
kill-line (not bound)
kill-word
page-down
page-up
toggle (not bound)
toggle-down
toggle-sort (not bound; equivalent to \fB--toggle-sort\fR)
toggle-up
unix-line-discard
unix-word-rubout
up
yank
.RE
.SS Scripting .SS Scripting
.TP .TP
.BI "-q, --query=" "STR" .BI "-q, --query=" "STR"
@@ -147,10 +212,6 @@ with the default enter key.
e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR
.RE .RE
.TP .TP
.BI "--toggle-sort=" "KEY"
Key to toggle sort (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR,
or any single character)
.TP
.B "--sync" .B "--sync"
Synchronous search for multi-staged filtering. If specified, fzf will launch Synchronous search for multi-staged filtering. If specified, fzf will launch
ncurses finder only after the input stream is complete. ncurses finder only after the input stream is complete.

View File

@@ -22,11 +22,9 @@
" WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. " WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
let s:default_height = '40%' let s:default_height = '40%'
let s:launcher = 'xterm -e bash -ic %s'
let s:fzf_go = expand('<sfile>:h:h').'/bin/fzf' let s:fzf_go = expand('<sfile>:h:h').'/bin/fzf'
let s:install = expand('<sfile>:h:h').'/install' let s:install = expand('<sfile>:h:h').'/install'
let s:installed = 0 let s:installed = 0
let s:fzf_rb = expand('<sfile>:h:h').'/fzf'
let s:fzf_tmux = expand('<sfile>:h:h').'/bin/fzf-tmux' let s:fzf_tmux = expand('<sfile>:h:h').'/bin/fzf-tmux'
let s:cpo_save = &cpo let s:cpo_save = &cpo
@@ -36,7 +34,12 @@ 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) elseif executable('fzf')
let s:exec = 'fzf'
elseif !s:installed && executable(s:install) &&
\ input('fzf executable not found. Download binary? (y/n) ') =~? '^y'
redraw
echo
echohl WarningMsg echohl WarningMsg
echo 'Downloading fzf binary. Please wait ...' echo 'Downloading fzf binary. Please wait ...'
echohl None echohl None
@@ -44,24 +47,11 @@ function! s:fzf_exec()
call system(s:install.' --bin') call system(s:install.' --bin')
return s:fzf_exec() return s:fzf_exec()
else else
let path = split(system('which fzf 2> /dev/null'), '\n') redraw
if !v:shell_error && !empty(path)
let s:exec = path[0]
elseif executable(s:fzf_rb)
let s:exec = s:fzf_rb
else
call system('type fzf')
if v:shell_error
throw 'fzf executable not found' throw 'fzf executable not found'
else
let s:exec = 'fzf'
endif
endif endif
endif endif
return s:exec return s:exec
else
return s:exec
endif
endfunction endfunction
function! s:tmux_enabled() function! s:tmux_enabled()
@@ -86,7 +76,7 @@ function! s:shellesc(arg)
endfunction endfunction
function! s:escape(path) function! s:escape(path)
return substitute(a:path, ' ', '\\ ', 'g') return escape(a:path, ' %#\')
endfunction endfunction
" Upgrade legacy options " Upgrade legacy options
@@ -105,6 +95,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 +142,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, ...)
@@ -196,12 +192,25 @@ function! s:popd(dict)
endif endif
endfunction endfunction
function! s:xterm_launcher()
let fmt = 'xterm -T "[fzf]" -bg "\%s" -fg "\%s" -geometry %dx%d+%d+%d -e bash -ic %%s'
if has('gui_macvim')
let fmt .= '; osascript -e "tell application \"MacVim\" to activate"'
endif
return printf(fmt,
\ synIDattr(hlID("Normal"), "bg"), synIDattr(hlID("Normal"), "fg"),
\ &columns, &lines/2, getwinposx(), getwinposy())
endfunction
unlet! s:launcher
let s:launcher = function('s:xterm_launcher')
function! s:execute(dict, command, temps) function! s:execute(dict, command, temps)
call s:pushd(a:dict) call s:pushd(a:dict)
silent! !clear 2> /dev/null silent! !clear 2> /dev/null
if has('gui_running') if has('gui_running')
let launcher = get(a:dict, 'launcher', get(g:, 'fzf_launcher', s:launcher)) let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher)))
let command = printf(launcher, "'".substitute(a:command, "'", "'\"'\"'", 'g')."'") let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher
let command = printf(fmt, "'".substitute(a:command, "'", "'\"'\"'", 'g')."'")
else else
let command = a:command let command = a:command
endif endif
@@ -227,6 +236,7 @@ function! s:execute_tmux(dict, command, temps)
endif endif
call system(command) call system(command)
redraw!
return s:callback(a:dict, a:temps) return s:callback(a:dict, a:temps)
endfunction endfunction
@@ -262,7 +272,7 @@ function! s:split(dict)
tabnew tabnew
endif endif
finally finally
setlocal winfixwidth winfixheight setlocal winfixwidth winfixheight buftype=nofile bufhidden=wipe nobuflisted
endtry endtry
endfunction endfunction
@@ -273,12 +283,21 @@ function! s:execute_term(dict, command, temps)
let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps } let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps }
function! fzf.on_exit(id, code) function! fzf.on_exit(id, code)
let tab = tabpagenr() let tab = tabpagenr()
execute 'bd!' self.buf if bufnr('') == self.buf
" We use close instead of bd! since Vim does not close the split when
" there's no other listed buffer
close
" FIXME This should be unnecessary due to `bufhidden=wipe` but in some
" cases Neovim fails to clean up the buffer and `bufexists('[FZF]')
" returns 1 even when it cannot be seen anywhere else. e.g. `FZF!`
silent! execute 'bd!' self.buf
endif
if s:ptab == tab if s:ptab == tab
wincmd p wincmd p
endif endif
call s:pushd(self.dict) call s:pushd(self.dict)
try try
redraw!
call s:callback(self.dict, self.temps) call s:callback(self.dict, self.temps)
finally finally
call s:popd(self.dict) call s:popd(self.dict)
@@ -292,6 +311,7 @@ function! s:execute_term(dict, command, temps)
endfunction endfunction
function! s:callback(dict, temps) function! s:callback(dict, temps)
try
if !filereadable(a:temps.result) if !filereadable(a:temps.result)
let lines = [] let lines = []
else else
@@ -315,6 +335,11 @@ function! s:callback(dict, temps)
endfor endfor
return lines return lines
catch
if stridx(v:exception, ':E325:') < 0
echoerr v:exception
endif
endtry
endfunction endfunction
let s:default_action = { let s:default_action = {

View File

@@ -29,13 +29,14 @@ _fzf_opts_completion() {
+s --no-sort +s --no-sort
--tac --tac
--tiebreak --tiebreak
--bind
-m --multi -m --multi
--no-mouse --no-mouse
+c --no-color --color
+2 --no-256
--black --black
--reverse --reverse
--no-hscroll --no-hscroll
--inline-info
--prompt --prompt
-q --query -q --query
-1 --select-1 -1 --select-1
@@ -51,6 +52,10 @@ _fzf_opts_completion() {
COMPREPLY=( $(compgen -W "length begin end index" -- ${cur}) ) COMPREPLY=( $(compgen -W "length begin end index" -- ${cur}) )
return 0 return 0
;; ;;
--color)
COMPREPLY=( $(compgen -W "dark light 16 bw" -- ${cur}) )
return 0
;;
esac esac
if [[ ${cur} =~ ^-|\+ ]]; then if [[ ${cur} =~ ^-|\+ ]]; then
@@ -83,7 +88,7 @@ _fzf_path_completion() {
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g') cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g')
COMPREPLY=() COMPREPLY=()
trigger=${FZF_COMPLETION_TRIGGER:-**} trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
if [[ ${cur} == *"$trigger" ]]; then if [[ ${cur} == *"$trigger" ]]; then
base=${cur:0:${#cur}-${#trigger}} base=${cur:0:${#cur}-${#trigger}}
@@ -124,7 +129,7 @@ _fzf_list_completion() {
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
read -r src read -r src
cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g') cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g')
trigger=${FZF_COMPLETION_TRIGGER:-**} trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
if [[ ${cur} == *"$trigger" ]]; then if [[ ${cur} == *"$trigger" ]]; then
cur=${cur:0:${#cur}-${#trigger}} cur=${cur:0:${#cur}-${#trigger}}
@@ -179,13 +184,13 @@ _fzf_kill_completion() {
_fzf_telnet_completion() { _fzf_telnet_completion() {
_fzf_list_completion '+m' "$@" << "EOF" _fzf_list_completion '+m' "$@" << "EOF"
\grep -v '^\s*\(#\|$\)' /etc/hosts | awk '{if (length($2) > 0) {print $2}}' | sort -u \grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | awk '{if (length($2) > 0) {print $2}}' | sort -u
EOF EOF
} }
_fzf_ssh_completion() { _fzf_ssh_completion() {
_fzf_list_completion '+m' "$@" << "EOF" _fzf_list_completion '+m' "$@" << "EOF"
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i ^host | \grep -v '*') <(\grep -v '^\s*\(#\|$\)' /etc/hosts) | awk '{print $2}' | sort -u cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u
EOF EOF
} }
@@ -218,11 +223,11 @@ a_cmds="
x_cmds="kill ssh telnet unset unalias export" x_cmds="kill ssh telnet unset unalias export"
# Preserve existing completion # Preserve existing completion
if [ "$_fzf_completion_loaded" != '0.8.6-1' ]; then if [ "$_fzf_completion_loaded" != '0.9.12' ]; then
# Really wish I could use associative array but OSX comes with bash 3.2 :( # Really wish I could use associative array but OSX comes with bash 3.2 :(
eval $(complete | \grep '\-F' | \grep -v _fzf_ | eval $(complete | \grep '\-F' | \grep -v _fzf_ |
\grep -E " ($(echo $d_cmds $f_cmds $a_cmds $x_cmds | sed 's/ /|/g' | sed 's/+/\\+/g'))$" | _fzf_orig_completion_filter) \grep -E " ($(echo $d_cmds $f_cmds $a_cmds $x_cmds | sed 's/ /|/g' | sed 's/+/\\+/g'))$" | _fzf_orig_completion_filter)
export _fzf_completion_loaded=0.8.6-1 export _fzf_completion_loaded=0.9.12
fi fi
if type _completion_loader > /dev/null 2>&1; then if type _completion_loader > /dev/null 2>&1; then

158
shell/completion.zsh Normal file
View File

@@ -0,0 +1,158 @@
#!/bin/zsh
# ____ ____
# / __/___ / __/
# / /_/_ / / /_
# / __/ / /_/ __/
# /_/ /___/_/-completion.zsh
#
# - $FZF_TMUX (default: 1)
# - $FZF_TMUX_HEIGHT (default: '40%')
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
_fzf_path_completion() {
local base lbuf find_opts fzf_opts suffix tail fzf dir leftover matches nnm
base=${(Q)1}
lbuf=$2
find_opts=$3
fzf_opts=$4
suffix=$5
tail=$6
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
if ! setopt | grep nonomatch > /dev/null; then
nnm=1
setopt nonomatch
fi
dir="$base"
while [ 1 ]; do
if [ -z "$dir" -o -d ${~dir} ]; then
leftover=${base/#"$dir"}
leftover=${leftover/#\/}
[ "$dir" = './' ] && dir=''
dir=${~dir}
matches=$(\find -L $dir* ${=find_opts} 2> /dev/null | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$leftover" | while read item; do
printf "%q$suffix " "$item"
done)
matches=${matches% }
if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches$tail"
fi
zle redisplay
break
fi
dir=$(dirname "$dir")
dir=${dir%/}/
done
[ -n "$nnm" ] && unsetopt nonomatch
}
_fzf_all_completion() {
_fzf_path_completion "$1" "$2" \
"-name .git -prune -o -name .svn -prune -o -type d -print -o -type f -print -o -type l -print" \
"-m" "" " "
}
_fzf_dir_completion() {
_fzf_path_completion "$1" "$2" \
"-name .git -prune -o -name .svn -prune -o -type d -print" \
"" "/" ""
}
_fzf_list_completion() {
local prefix lbuf fzf_opts src fzf matches
prefix=$1
lbuf=$2
fzf_opts=$3
read -r src
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
matches=$(eval "$src" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$prefix")
if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches "
fi
zle redisplay
}
_fzf_telnet_completion() {
_fzf_list_completion "$1" "$2" '+m' << "EOF"
\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | awk '{if (length($2) > 0) {print $2}}' | sort -u
EOF
}
_fzf_ssh_completion() {
_fzf_list_completion "$1" "$2" '+m' << "EOF"
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u
EOF
}
_fzf_env_var_completion() {
_fzf_list_completion "$1" "$2" '+m' << "EOF"
declare -xp | sed 's/=.*//' | sed 's/.* //'
EOF
}
_fzf_alias_completion() {
_fzf_list_completion "$1" "$2" '+m' << "EOF"
alias | sed 's/=.*//'
EOF
}
fzf-completion() {
local tokens cmd prefix trigger tail fzf matches lbuf d_cmds
# http://zsh.sourceforge.net/FAQ/zshfaq03.html
# http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags
tokens=(${(z)LBUFFER})
if [ ${#tokens} -lt 1 ]; then
eval "zle ${fzf_default_completion:-expand-or-complete}"
return
fi
cmd=${tokens[1]}
# Explicitly allow for empty trigger.
trigger=${FZF_COMPLETION_TRIGGER-'**'}
[ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("")
tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))}
# Kill completion (do not require trigger sequence)
if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
matches=$(ps -ef | sed 1d | ${=fzf} ${=FZF_COMPLETION_OPTS} -m | awk '{print $2}' | tr '\n' ' ')
if [ -n "$matches" ]; then
LBUFFER="$LBUFFER$matches"
fi
zle redisplay
# Trigger sequence given
elif [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
d_cmds=(cd pushd rmdir)
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
[ -z "${tokens[-1]}" ] && lbuf=$LBUFFER || lbuf=${LBUFFER:0:-${#tokens[-1]}}
if [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
_fzf_dir_completion "$prefix" $lbuf
elif [ $cmd = telnet ]; then
_fzf_telnet_completion "$prefix" $lbuf
elif [ $cmd = ssh ]; then
_fzf_ssh_completion "$prefix" $lbuf
elif [ $cmd = unset -o $cmd = export ]; then
_fzf_env_var_completion "$prefix" $lbuf
elif [ $cmd = unalias ]; then
_fzf_alias_completion "$prefix" $lbuf
else
_fzf_all_completion "$prefix" $lbuf
fi
# Fall back to default completion
else
eval "zle ${fzf_default_completion:-expand-or-complete}"
fi
}
[ -z "$fzf_default_completion" ] &&
fzf_default_completion=$(bindkey '^I' | grep -v undefined-key | awk '{print $2}')
zle -N fzf-completion
bindkey '^I' fzf-completion

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,21 @@ __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__() {
local line
line=$(
HISTTIMEFORMAT= history |
$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r |
\grep '^ *[0-9]') && sed 's/ *\([0-9]*\)\** .*/!\1/' <<< "$line"
} }
__use_tmux=0 __use_tmux=0
@@ -35,38 +47,40 @@ __use_tmux=0
if [ -z "$(set -o | \grep '^vi.*on')" ]; then if [ -z "$(set -o | \grep '^vi.*on')" ]; then
# Required to refresh the prompt after fzf # Required to refresh the prompt after fzf
bind '"\er": redraw-current-line' bind '"\er": redraw-current-line'
bind '"\e^": history-expand-line'
# 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\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'
bind '"\C-x^": history-expand-line'
# 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\C-x^\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 rm -f $TMPDIR/fzf.result
end 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
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,29 +32,36 @@ 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
set -q FZF_TMUX; or set FZF_TMUX 1
if [ $FZF_TMUX -eq 1 ]
if set -q FZF_TMUX_HEIGHT if set -q FZF_TMUX_HEIGHT
set height $FZF_TMUX_HEIGHT echo "fzf-tmux -d$FZF_TMUX_HEIGHT"
else else
set height 40% echo "fzf-tmux -d40%"
end end
if echo $height | \grep -q -E '%$'
echo "-p "(echo $height | sed 's/%$//')
else else
echo "-l $height" echo "fzf"
end end
set -e height
end end
bind \ct '__fzf_ctrl_t' bind \ct '__fzf_ctrl_t'
bind \cr '__fzf_ctrl_r' bind \cr '__fzf_ctrl_r'
bind \ec '__fzf_alt_c' bind \ec '__fzf_alt_c'
if bind -M insert > /dev/null 2>&1
bind -M insert \ct '__fzf_ctrl_t'
bind -M insert \cr '__fzf_ctrl_r'
bind -M insert \ec '__fzf_alt_c'
end
end end

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() {
local height
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)" LBUFFER="${LBUFFER}$(__fsel)"
zle redisplay 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
@@ -44,11 +35,18 @@ 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 restore_no_bang_hist
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 -n1 | awk '{print $1}' | sed 's/[^0-9]//g')
if [ -n "$num" ]; then
LBUFFER=!$num LBUFFER=!$num
if setopt | grep nobanghist > /dev/null; then
restore_no_bang_hist=1
unsetopt no_bang_hist
fi
zle expand-history zle expand-history
[ -n "$restore_no_bang_hist" ] && setopt no_bang_hist
fi
fi fi
zle redisplay zle redisplay
} }

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

@@ -50,7 +50,7 @@ func extractColor(str *string) (*string, []ansiOffset) {
if !newState.equals(state) { if !newState.equals(state) {
if state != nil { if state != nil {
// Update last offset // Update last offset
(&offsets[len(offsets)-1]).offset[1] = int32(output.Len()) (&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes()))
} }
if newState.colored() { if newState.colored() {

View File

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

View File

@@ -47,10 +47,9 @@ Matcher -> EvtSearchFin -> Terminal (update list)
*/ */
// Run starts fzf // Run starts fzf
func Run(options *Options) { func Run(opts *Options) {
initProcs() initProcs()
opts := ParseOptions()
sort := opts.Sort > 0 sort := opts.Sort > 0
rankTiebreak = opts.Tiebreak rankTiebreak = opts.Tiebreak

View File

@@ -61,6 +61,16 @@ const (
PgUp PgUp
PgDn PgDn
Up
Down
Left
Right
Home
End
SLeft
SRight
F1 F1
F2 F2
F3 F3
@@ -96,15 +106,18 @@ const (
) )
type ColorTheme struct { type ColorTheme struct {
darkBg C.short UseDefault bool
prompt C.short Fg int16
match C.short Bg int16
current C.short DarkBg int16
currentMatch C.short Prompt int16
spinner C.short Match int16
info C.short Current int16
cursor C.short CurrentMatch int16
selected C.short Spinner int16
Info int16
Cursor int16
Selected int16
} }
type Event struct { type Event struct {
@@ -132,7 +145,10 @@ var (
Default16 *ColorTheme Default16 *ColorTheme
Dark256 *ColorTheme Dark256 *ColorTheme
Light256 *ColorTheme Light256 *ColorTheme
DarkBG C.short FG int
CurrentFG int
BG int
DarkBG int
) )
func init() { func init() {
@@ -140,35 +156,44 @@ func init() {
_clickY = []int{} _clickY = []int{}
_colorMap = make(map[int]int) _colorMap = make(map[int]int)
Default16 = &ColorTheme{ Default16 = &ColorTheme{
darkBg: C.COLOR_BLACK, UseDefault: true,
prompt: C.COLOR_BLUE, Fg: 15,
match: C.COLOR_GREEN, Bg: 0,
current: C.COLOR_YELLOW, DarkBg: C.COLOR_BLACK,
currentMatch: C.COLOR_GREEN, Prompt: C.COLOR_BLUE,
spinner: C.COLOR_GREEN, Match: C.COLOR_GREEN,
info: C.COLOR_WHITE, Current: C.COLOR_YELLOW,
cursor: C.COLOR_RED, CurrentMatch: C.COLOR_GREEN,
selected: C.COLOR_MAGENTA} Spinner: C.COLOR_GREEN,
Info: C.COLOR_WHITE,
Cursor: C.COLOR_RED,
Selected: C.COLOR_MAGENTA}
Dark256 = &ColorTheme{ Dark256 = &ColorTheme{
darkBg: 236, UseDefault: true,
prompt: 110, Fg: 15,
match: 108, Bg: 0,
current: 254, DarkBg: 236,
currentMatch: 151, Prompt: 110,
spinner: 148, Match: 108,
info: 144, Current: 254,
cursor: 161, CurrentMatch: 151,
selected: 168} Spinner: 148,
Info: 144,
Cursor: 161,
Selected: 168}
Light256 = &ColorTheme{ Light256 = &ColorTheme{
darkBg: 251, UseDefault: true,
prompt: 25, Fg: 15,
match: 66, Bg: 0,
current: 237, DarkBg: 251,
currentMatch: 23, Prompt: 25,
spinner: 65, Match: 66,
info: 101, Current: 237,
cursor: 161, CurrentMatch: 23,
selected: 168} Spinner: 65,
Info: 101,
Cursor: 161,
Selected: 168}
} }
func attrColored(pair int, bold bool) C.int { func attrColored(pair int, bold bool) C.int {
@@ -258,23 +283,35 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
} }
func initPairs(theme *ColorTheme, black bool) { func initPairs(theme *ColorTheme, black bool) {
var bg C.short fg := C.short(theme.Fg)
bg := C.short(theme.Bg)
if black { if black {
bg = C.COLOR_BLACK bg = C.COLOR_BLACK
} else { } else if theme.UseDefault {
C.use_default_colors() fg = -1
bg = -1 bg = -1
C.use_default_colors()
}
if theme.UseDefault {
FG = -1
BG = -1
} else {
FG = int(fg)
BG = int(bg)
C.assume_default_colors(C.int(theme.Fg), C.int(bg))
} }
DarkBG = theme.darkBg CurrentFG = int(theme.Current)
C.init_pair(ColPrompt, theme.prompt, bg) DarkBG = int(theme.DarkBg)
C.init_pair(ColMatch, theme.match, bg) darkBG := C.short(DarkBG)
C.init_pair(ColCurrent, theme.current, DarkBG) C.init_pair(ColPrompt, C.short(theme.Prompt), bg)
C.init_pair(ColCurrentMatch, theme.currentMatch, DarkBG) C.init_pair(ColMatch, C.short(theme.Match), bg)
C.init_pair(ColSpinner, theme.spinner, bg) C.init_pair(ColCurrent, C.short(theme.Current), darkBG)
C.init_pair(ColInfo, theme.info, bg) C.init_pair(ColCurrentMatch, C.short(theme.CurrentMatch), darkBG)
C.init_pair(ColCursor, theme.cursor, DarkBG) C.init_pair(ColSpinner, C.short(theme.Spinner), bg)
C.init_pair(ColSelected, theme.selected, DarkBG) C.init_pair(ColInfo, C.short(theme.Info), bg)
C.init_pair(ColCursor, C.short(theme.Cursor), darkBG)
C.init_pair(ColSelected, C.short(theme.Selected), darkBG)
} }
func Close() { func Close() {
@@ -356,19 +393,19 @@ func escSequence(sz *int) Event {
*sz = 3 *sz = 3
switch _buf[2] { switch _buf[2] {
case 68: case 68:
return Event{CtrlB, 0, nil} return Event{Left, 0, nil}
case 67: case 67:
return Event{CtrlF, 0, nil} return Event{Right, 0, nil}
case 66: case 66:
return Event{CtrlJ, 0, nil} return Event{Down, 0, nil}
case 65: case 65:
return Event{CtrlK, 0, nil} return Event{Up, 0, nil}
case 90: case 90:
return Event{BTab, 0, nil} return Event{BTab, 0, nil}
case 72: case 72:
return Event{CtrlA, 0, nil} return Event{Home, 0, nil}
case 70: case 70:
return Event{CtrlE, 0, nil} return Event{End, 0, nil}
case 77: case 77:
return mouseSequence(sz) return mouseSequence(sz)
case 80: case 80:
@@ -390,7 +427,7 @@ func escSequence(sz *int) Event {
case 51: case 51:
return Event{Del, 0, nil} return Event{Del, 0, nil}
case 52: case 52:
return Event{CtrlE, 0, nil} return Event{End, 0, nil}
case 53: case 53:
return Event{PgUp, 0, nil} return Event{PgUp, 0, nil}
case 54: case 54:
@@ -398,7 +435,7 @@ func escSequence(sz *int) Event {
case 49: case 49:
switch _buf[3] { switch _buf[3] {
case 126: case 126:
return Event{CtrlA, 0, nil} return Event{Home, 0, nil}
case 59: case 59:
if len(_buf) != 6 { if len(_buf) != 6 {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
@@ -408,16 +445,16 @@ func escSequence(sz *int) Event {
case 50: case 50:
switch _buf[5] { switch _buf[5] {
case 68: case 68:
return Event{CtrlA, 0, nil} return Event{Home, 0, nil}
case 67: case 67:
return Event{CtrlE, 0, nil} return Event{End, 0, nil}
} }
case 53: case 53:
switch _buf[5] { switch _buf[5] {
case 68: case 68:
return Event{AltB, 0, nil} return Event{SLeft, 0, nil}
case 67: case 67:
return Event{AltF, 0, nil} return Event{SRight, 0, nil}
} }
} // _buf[4] } // _buf[4]
} // _buf[3] } // _buf[3]

View File

@@ -143,13 +143,25 @@ func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset
offset: Offset{int32(start), int32(idx)}, color: color, bold: bold}) offset: Offset{int32(start), int32(idx)}, color: color, bold: bold})
} else { } else {
ansi := item.colors[curr-1] ansi := item.colors[curr-1]
fg := ansi.color.fg
if fg == -1 {
if current {
fg = curses.CurrentFG
} else {
fg = curses.FG
}
}
bg := ansi.color.bg bg := ansi.color.bg
if current && bg == -1 { if bg == -1 {
bg = int(curses.DarkBG) if current {
bg = curses.DarkBG
} else {
bg = curses.BG
}
} }
offsets = append(offsets, colorOffset{ offsets = append(offsets, colorOffset{
offset: Offset{int32(start), int32(idx)}, offset: Offset{int32(start), int32(idx)},
color: curses.PairFor(ansi.color.fg, bg), color: curses.PairFor(fg, bg),
bold: ansi.color.bold || bold}) bold: ansi.color.bold || bold})
} }
} }

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"regexp" "regexp"
"strconv"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
@@ -35,12 +36,14 @@ const usage = `usage: fzf [options]
-m, --multi Enable multi-select with tab/shift-tab -m, --multi Enable multi-select with tab/shift-tab
--ansi Enable processing of ANSI color codes --ansi Enable processing of ANSI color codes
--no-mouse Disable mouse --no-mouse Disable mouse
--color=COL Color scheme; [dark|light|16|bw] --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
(default: dark on 256-color terminal, otherwise 16)
--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: '> ')
--toggle-sort=KEY Key to toggle sort
--bind=KEYBINDS Custom key bindings. Refer to the man page.
Scripting Scripting
-q, --query=STR Start the finder with the given query -q, --query=STR Start the finder with the given query
@@ -49,7 +52,6 @@ const usage = `usage: fzf [options]
-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 --expect=KEYS Comma-separated list of keys to complete fzf
--toggle-sort=KEY Key to toggle sort
--sync Synchronous search for multi-staged filtering --sync Synchronous search for multi-staged filtering
Environment variables Environment variables
@@ -105,26 +107,28 @@ 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
Exit0 bool Exit0 bool
Filter *string Filter *string
ToggleSort int ToggleSort bool
Expect []int Expect []int
Keymap map[int]actionType
PrintQuery bool PrintQuery bool
Sync bool Sync bool
Version bool Version bool
} }
func defaultOptions() *Options { func defaultTheme() *curses.ColorTheme {
var defaultTheme *curses.ColorTheme
if strings.Contains(os.Getenv("TERM"), "256") { if strings.Contains(os.Getenv("TERM"), "256") {
defaultTheme = curses.Dark256 return curses.Dark256
} else {
defaultTheme = curses.Default16
} }
return curses.Default16
}
func defaultOptions() *Options {
return &Options{ return &Options{
Mode: ModeFuzzy, Mode: ModeFuzzy,
Case: CaseSmart, Case: CaseSmart,
@@ -137,17 +141,19 @@ func defaultOptions() *Options {
Multi: false, Multi: false,
Ansi: false, Ansi: false,
Mouse: true, Mouse: true,
Theme: defaultTheme, Theme: defaultTheme(),
Black: false, Black: false,
Reverse: false, Reverse: false,
Hscroll: true, Hscroll: true,
InlineInfo: false,
Prompt: "> ", Prompt: "> ",
Query: "", Query: "",
Select1: false, Select1: false,
Exit0: false, Exit0: false,
Filter: nil, Filter: nil,
ToggleSort: 0, ToggleSort: false,
Expect: []int{}, Expect: []int{},
Keymap: defaultKeymap(),
PrintQuery: false, PrintQuery: false,
Sync: false, Sync: false,
Version: false} Version: false}
@@ -181,6 +187,14 @@ func nextString(args []string, i *int, message string) string {
return args[*i] return args[*i]
} }
func optionalNextString(args []string, i *int) string {
if len(args) > *i+1 {
*i++
return args[*i]
}
return ""
}
func optionalNumeric(args []string, i *int) int { func optionalNumeric(args []string, i *int) int {
if len(args) > *i+1 { if len(args) > *i+1 {
if strings.IndexAny(args[*i+1], "0123456789") == 0 { if strings.IndexAny(args[*i+1], "0123456789") == 0 {
@@ -271,28 +285,155 @@ func parseTiebreak(str string) tiebreak {
return byLength return byLength
} }
func parseTheme(str string) *curses.ColorTheme { func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme {
switch strings.ToLower(str) { dupe := *theme
case "dark": return &dupe
return curses.Dark256
case "light":
return curses.Light256
case "16":
return curses.Default16
case "bw", "no":
return nil
default:
errorExit("invalid color scheme: " + str)
}
return nil
} }
func checkToggleSort(str string) int { func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme {
theme := dupeTheme(defaultTheme)
for _, str := range strings.Split(strings.ToLower(str), ",") {
switch str {
case "dark":
theme = dupeTheme(curses.Dark256)
case "light":
theme = dupeTheme(curses.Light256)
case "16":
theme = dupeTheme(curses.Default16)
case "bw", "no":
theme = nil
default:
fail := func() {
errorExit("invalid color specification: " + str)
}
// Color is disabled
if theme == nil {
errorExit("colors disabled; cannot customize colors")
}
pair := strings.Split(str, ":")
if len(pair) != 2 {
fail()
}
ansi32, err := strconv.Atoi(pair[1])
if err != nil || ansi32 < -1 || ansi32 > 255 {
fail()
}
ansi := int16(ansi32)
switch pair[0] {
case "fg":
theme.Fg = ansi
theme.UseDefault = theme.UseDefault && ansi < 0
case "bg":
theme.Bg = ansi
theme.UseDefault = theme.UseDefault && ansi < 0
case "fg+":
theme.Current = ansi
case "bg+":
theme.DarkBg = ansi
case "hl":
theme.Match = ansi
case "hl+":
theme.CurrentMatch = ansi
case "prompt":
theme.Prompt = ansi
case "spinner":
theme.Spinner = ansi
case "info":
theme.Info = ansi
case "pointer":
theme.Cursor = ansi
case "marker":
theme.Selected = ansi
default:
fail()
}
}
}
return theme
}
func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[int]actionType, bool) {
for _, pairStr := range strings.Split(str, ",") {
fail := func() {
errorExit("invalid key binding: " + pairStr)
}
pair := strings.Split(pairStr, ":")
if len(pair) != 2 {
fail()
}
keys := parseKeyChords(pair[0], "key name required")
if len(keys) != 1 {
fail()
}
key := keys[0]
act := strings.ToLower(pair[1])
switch act {
case "beginning-of-line":
keymap[key] = actBeginningOfLine
case "abort":
keymap[key] = actAbort
case "accept":
keymap[key] = actAccept
case "backward-char":
keymap[key] = actBackwardChar
case "backward-delete-char":
keymap[key] = actBackwardDeleteChar
case "backward-word":
keymap[key] = actBackwardWord
case "clear-screen":
keymap[key] = actClearScreen
case "delete-char":
keymap[key] = actDeleteChar
case "end-of-line":
keymap[key] = actEndOfLine
case "forward-char":
keymap[key] = actForwardChar
case "forward-word":
keymap[key] = actForwardWord
case "kill-line":
keymap[key] = actKillLine
case "kill-word":
keymap[key] = actKillWord
case "unix-line-discard", "line-discard":
keymap[key] = actUnixLineDiscard
case "unix-word-rubout", "word-rubout":
keymap[key] = actUnixWordRubout
case "yank":
keymap[key] = actYank
case "backward-kill-word":
keymap[key] = actBackwardKillWord
case "toggle-down":
keymap[key] = actToggleDown
case "toggle-up":
keymap[key] = actToggleUp
case "toggle":
keymap[key] = actToggle
case "down":
keymap[key] = actDown
case "up":
keymap[key] = actUp
case "page-up":
keymap[key] = actPageUp
case "page-down":
keymap[key] = actPageDown
case "toggle-sort":
keymap[key] = actToggleSort
toggleSort = true
default:
errorExit("unknown action: " + act)
}
}
return keymap, toggleSort
}
func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType {
keys := parseKeyChords(str, "key name required") keys := parseKeyChords(str, "key name required")
if len(keys) != 1 { if len(keys) != 1 {
errorExit("multiple keys specified") errorExit("multiple keys specified")
} }
return keys[0] keymap[keys[0]] = actToggleSort
return keymap
} }
func parseOptions(opts *Options, allArgs []string) { func parseOptions(opts *Options, allArgs []string) {
@@ -316,10 +457,18 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required")
case "--tiebreak": case "--tiebreak":
opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
case "--bind":
opts.Keymap, opts.ToggleSort = parseKeymap(opts.Keymap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required"))
case "--color": case "--color":
opts.Theme = parseTheme(nextString(allArgs, &i, "color scheme name required")) spec := optionalNextString(allArgs, &i)
if len(spec) == 0 {
opts.Theme = defaultTheme()
} else {
opts.Theme = parseTheme(opts.Theme, spec)
}
case "--toggle-sort": case "--toggle-sort":
opts.ToggleSort = checkToggleSort(nextString(allArgs, &i, "key name required")) opts.Keymap = checkToggleSort(opts.Keymap, nextString(allArgs, &i, "key name required"))
opts.ToggleSort = true
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":
@@ -364,6 +513,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":
@@ -402,13 +555,16 @@ func parseOptions(opts *Options, allArgs []string) {
} 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, "--toggle-sort="); match { } else if match, value := optString(arg, "--toggle-sort="); match {
opts.ToggleSort = checkToggleSort(value) opts.Keymap = checkToggleSort(opts.Keymap, value)
opts.ToggleSort = true
} else if match, value := optString(arg, "--expect="); match { } else if match, value := optString(arg, "--expect="); match {
opts.Expect = parseKeyChords(value, "key names required") opts.Expect = parseKeyChords(value, "key names required")
} else if match, value := optString(arg, "--tiebreak="); match { } else if match, value := optString(arg, "--tiebreak="); match {
opts.Tiebreak = parseTiebreak(value) opts.Tiebreak = parseTiebreak(value)
} else if match, value := optString(arg, "--color="); match { } else if match, value := optString(arg, "--color="); match {
opts.Theme = parseTheme(value) opts.Theme = parseTheme(opts.Theme, value)
} else if match, value := optString(arg, "--bind="); match {
opts.Keymap, opts.ToggleSort = parseKeymap(opts.Keymap, opts.ToggleSort, value)
} else { } else {
errorExit("unknown option: " + arg) errorExit("unknown option: " + arg)
} }

View File

@@ -129,3 +129,75 @@ func TestParseKeysWithComma(t *testing.T) {
check(len(keys), 1) check(len(keys), 1)
check(keys[0], curses.AltZ+',') check(keys[0], curses.AltZ+',')
} }
func TestBind(t *testing.T) {
check := func(action actionType, expected actionType) {
if action != expected {
t.Errorf("%d != %d", action, expected)
}
}
keymap := defaultKeymap()
check(actBeginningOfLine, keymap[curses.CtrlA])
keymap, toggleSort :=
parseKeymap(keymap, false,
"ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down")
if !toggleSort {
t.Errorf("toggleSort not set")
}
check(actKillLine, keymap[curses.CtrlA])
check(actToggleSort, keymap[curses.CtrlB])
check(actPageUp, keymap[curses.AltZ+'c'])
check(actPageDown, keymap[curses.AltZ])
keymap, toggleSort = parseKeymap(keymap, false, "f1:abort")
if toggleSort {
t.Errorf("toggleSort set")
}
check(actAbort, keymap[curses.F1])
}
func TestColorSpec(t *testing.T) {
theme := curses.Dark256
dark := parseTheme(theme, "dark")
if *dark != *theme {
t.Errorf("colors should be equivalent")
}
if dark == theme {
t.Errorf("point should not be equivalent")
}
light := parseTheme(theme, "dark,light")
if *light == *theme {
t.Errorf("should not be equivalent")
}
if *light != *curses.Light256 {
t.Errorf("colors should be equivalent")
}
if light == theme {
t.Errorf("point should not be equivalent")
}
customized := parseTheme(theme, "fg:231,bg:232")
if customized.Fg != 231 || customized.Bg != 232 {
t.Errorf("color not customized")
}
if *curses.Dark256 == *customized {
t.Errorf("colors should not be equivalent")
}
customized.Fg = curses.Dark256.Fg
customized.Bg = curses.Dark256.Bg
if *curses.Dark256 == *customized {
t.Errorf("colors should now be equivalent")
}
customized = parseTheme(theme, "fg:231,dark,bg:232")
if customized.Fg != curses.Dark256.Fg || customized.Bg == curses.Dark256.Bg {
t.Errorf("color not customized")
}
if customized.UseDefault {
t.Errorf("not using default colors")
}
if !curses.Dark256.UseDefault {
t.Errorf("using default colors")
}
}

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"
) )
@@ -31,6 +30,7 @@ type term struct {
typ termType typ termType
inv bool inv bool
text []rune text []rune
caseSensitive bool
origText []rune origText []rune
} }
@@ -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
@@ -166,6 +163,7 @@ func parseTerms(mode Mode, str string) []term {
typ: typ, typ: typ,
inv: inv, inv: inv,
text: []rune(text), text: []rune(text),
caseSensitive: caseSensitive,
origText: origText}) origText: origText})
} }
} }
@@ -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

@@ -30,9 +30,29 @@ func (r *Reader) ReadSource() {
} }
func (r *Reader) feed(src io.Reader) { func (r *Reader) feed(src io.Reader) {
if scanner := bufio.NewScanner(src); scanner != nil { reader := bufio.NewReader(src)
for scanner.Scan() { eof := false
r.pusher(scanner.Text()) Loop:
for !eof {
buf := []byte{}
iter := 0 // TODO: max size?
for {
// "ReadLine either returns a non-nil line or it returns an error, never both"
line, isPrefix, err := reader.ReadLine()
eof = err == io.EOF
if eof {
break
} else if err != nil {
break Loop
}
iter++
buf = append(buf, line...)
if !isPrefix {
break
}
}
if iter > 0 {
r.pusher(string(buf))
r.eventBox.Set(EvtReadNew, nil) r.eventBox.Set(EvtReadNew, nil)
} }
} }

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
@@ -30,8 +31,9 @@ type Terminal struct {
input []rune input []rune
multi bool multi bool
sort bool sort bool
toggleSort int toggleSort bool
expect []int expect []int
keymap map[int]actionType
pressed int pressed int
printQuery bool printQuery bool
count int count int
@@ -79,10 +81,93 @@ const (
reqQuit reqQuit
) )
type actionType int
const (
actIgnore actionType = iota
actInvalid
actRune
actMouse
actBeginningOfLine
actAbort
actAccept
actBackwardChar
actBackwardDeleteChar
actBackwardWord
actClearScreen
actDeleteChar
actEndOfLine
actForwardChar
actForwardWord
actKillLine
actKillWord
actUnixLineDiscard
actUnixWordRubout
actYank
actBackwardKillWord
actToggle
actToggleDown
actToggleUp
actDown
actUp
actPageUp
actPageDown
actToggleSort
)
func defaultKeymap() map[int]actionType {
keymap := make(map[int]actionType)
keymap[C.Invalid] = actInvalid
keymap[C.CtrlA] = actBeginningOfLine
keymap[C.CtrlB] = actBackwardChar
keymap[C.CtrlC] = actAbort
keymap[C.CtrlG] = actAbort
keymap[C.CtrlQ] = actAbort
keymap[C.ESC] = actAbort
keymap[C.CtrlD] = actDeleteChar
keymap[C.CtrlE] = actEndOfLine
keymap[C.CtrlF] = actForwardChar
keymap[C.CtrlH] = actBackwardDeleteChar
keymap[C.Tab] = actToggleDown
keymap[C.BTab] = actToggleUp
keymap[C.CtrlJ] = actDown
keymap[C.CtrlK] = actUp
keymap[C.CtrlL] = actClearScreen
keymap[C.CtrlM] = actAccept
keymap[C.CtrlN] = actDown
keymap[C.CtrlP] = actUp
keymap[C.CtrlU] = actUnixLineDiscard
keymap[C.CtrlW] = actUnixWordRubout
keymap[C.CtrlY] = actYank
keymap[C.AltB] = actBackwardWord
keymap[C.SLeft] = actBackwardWord
keymap[C.AltF] = actForwardWord
keymap[C.SRight] = actForwardWord
keymap[C.AltD] = actKillWord
keymap[C.AltBS] = actBackwardKillWord
keymap[C.Up] = actUp
keymap[C.Down] = actDown
keymap[C.Left] = actBackwardChar
keymap[C.Right] = actForwardChar
keymap[C.Home] = actBeginningOfLine
keymap[C.End] = actEndOfLine
keymap[C.Del] = actDeleteChar // FIXME Del vs. CTRL-D
keymap[C.PgUp] = actPageUp
keymap[C.PgDn] = actPageDown
keymap[C.Rune] = actRune
keymap[C.Mouse] = actMouse
return keymap
}
// NewTerminal returns new Terminal object // NewTerminal returns new Terminal object
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,
@@ -95,6 +180,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
sort: opts.Sort > 0, sort: opts.Sort > 0,
toggleSort: opts.ToggleSort, toggleSort: opts.ToggleSort,
expect: opts.Expect, expect: opts.Expect,
keymap: opts.Keymap,
pressed: 0, pressed: 0,
printQuery: opts.PrintQuery, printQuery: opts.PrintQuery,
merger: EmptyMerger, merger: EmptyMerger,
@@ -229,16 +315,25 @@ func (t *Terminal) printPrompt() {
} }
func (t *Terminal) printInfo() { func (t *Terminal) printInfo() {
if t.inlineInfo {
t.move(0, len(t.prompt)+displayWidth(t.input)+1, true)
if t.reading {
C.CPrint(C.ColSpinner, true, " < ")
} else {
C.CPrint(C.ColPrompt, true, " < ")
}
} else {
t.move(1, 0, true) t.move(1, 0, true)
if t.reading { if t.reading {
duration := int64(spinnerDuration) duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
C.CPrint(C.ColSpinner, true, _spinner[idx]) 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 {
if t.sort { if t.sort {
output += "/S" output += "/S"
} else { } else {
@@ -257,10 +352,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)
} }
@@ -419,8 +520,8 @@ func processTabs(runes []rune, prefixWidth int) (string, int) {
func (t *Terminal) printAll() { func (t *Terminal) printAll() {
t.printList() t.printList()
t.printInfo()
t.printPrompt() t.printPrompt()
t.printInfo()
} }
func (t *Terminal) refresh() { func (t *Terminal) refresh() {
@@ -515,6 +616,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:
@@ -581,110 +685,127 @@ func (t *Terminal) Loop() {
break break
} }
} }
if t.toggleSort > 0 {
if keyMatch(t.toggleSort, event) { action := t.keymap[event.Type]
if event.Type == C.Rune {
code := int(event.Char) + int(C.AltZ)
if act, prs := t.keymap[code]; prs {
action = act
}
}
switch action {
case actInvalid:
t.mutex.Unlock()
continue
case actToggleSort:
t.sort = !t.sort t.sort = !t.sort
t.eventBox.Set(EvtSearchNew, t.sort) t.eventBox.Set(EvtSearchNew, t.sort)
t.mutex.Unlock() t.mutex.Unlock()
continue continue
} case actBeginningOfLine:
}
switch event.Type {
case C.Invalid:
t.mutex.Unlock()
continue
case C.CtrlA:
t.cx = 0 t.cx = 0
case C.CtrlB: case actBackwardChar:
if t.cx > 0 { if t.cx > 0 {
t.cx-- t.cx--
} }
case C.CtrlC, C.CtrlG, C.CtrlQ, C.ESC: case actAbort:
req(reqQuit) req(reqQuit)
case C.CtrlD: case actDeleteChar:
if !t.delChar() && t.cx == 0 { if !t.delChar() && t.cx == 0 {
req(reqQuit) req(reqQuit)
} }
case C.CtrlE: case actEndOfLine:
t.cx = len(t.input) t.cx = len(t.input)
case C.CtrlF: case actForwardChar:
if t.cx < len(t.input) { if t.cx < len(t.input) {
t.cx++ t.cx++
} }
case C.CtrlH: case actBackwardDeleteChar:
if t.cx > 0 { if t.cx > 0 {
t.input = append(t.input[:t.cx-1], t.input[t.cx:]...) t.input = append(t.input[:t.cx-1], t.input[t.cx:]...)
t.cx-- t.cx--
} }
case C.Tab: case actToggle:
if t.multi && t.merger.Length() > 0 {
toggle()
req(reqList)
}
case actToggleDown:
if t.multi && t.merger.Length() > 0 { if t.multi && t.merger.Length() > 0 {
toggle() toggle()
t.vmove(-1) t.vmove(-1)
req(reqList) req(reqList)
} }
case C.BTab: case actToggleUp:
if t.multi && t.merger.Length() > 0 { if t.multi && t.merger.Length() > 0 {
toggle() toggle()
t.vmove(1) t.vmove(1)
req(reqList) req(reqList)
} }
case C.CtrlJ, C.CtrlN: case actDown:
t.vmove(-1) t.vmove(-1)
req(reqList) req(reqList)
case C.CtrlK, C.CtrlP: case actUp:
t.vmove(1) t.vmove(1)
req(reqList) req(reqList)
case C.CtrlM: case actAccept:
req(reqClose) req(reqClose)
case C.CtrlL: case actClearScreen:
req(reqRedraw) req(reqRedraw)
case C.CtrlU: case actUnixLineDiscard:
if t.cx > 0 { if t.cx > 0 {
t.yanked = copySlice(t.input[:t.cx]) t.yanked = copySlice(t.input[:t.cx])
t.input = t.input[t.cx:] t.input = t.input[t.cx:]
t.cx = 0 t.cx = 0
} }
case C.CtrlW: case actUnixWordRubout:
if t.cx > 0 { if t.cx > 0 {
t.rubout("\\s\\S") t.rubout("\\s\\S")
} }
case C.AltBS: case actBackwardKillWord:
if t.cx > 0 { if t.cx > 0 {
t.rubout("[^[:alnum:]][[:alnum:]]") t.rubout("[^[:alnum:]][[:alnum:]]")
} }
case C.CtrlY: case actYank:
suffix := copySlice(t.input[t.cx:]) suffix := copySlice(t.input[t.cx:])
t.input = append(append(t.input[:t.cx], t.yanked...), suffix...) t.input = append(append(t.input[:t.cx], t.yanked...), suffix...)
t.cx += len(t.yanked) t.cx += len(t.yanked)
case C.Del: case actPageUp:
t.delChar() t.vmove(t.maxItems() - 1)
case C.PgUp:
t.vmove(maxItems() - 1)
req(reqList) req(reqList)
case C.PgDn: case actPageDown:
t.vmove(-(maxItems() - 1)) t.vmove(-(t.maxItems() - 1))
req(reqList) req(reqList)
case C.AltB: case actBackwardWord:
t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1 t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1
case C.AltF: case actForwardWord:
t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1 t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
case C.AltD: case actKillWord:
ncx := t.cx + ncx := t.cx +
findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1 findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
if ncx > t.cx { if ncx > t.cx {
t.yanked = copySlice(t.input[t.cx:ncx]) t.yanked = copySlice(t.input[t.cx:ncx])
t.input = append(t.input[:t.cx], t.input[ncx:]...) t.input = append(t.input[:t.cx], t.input[ncx:]...)
} }
case C.Rune: case actKillLine:
if t.cx < len(t.input) {
t.yanked = copySlice(t.input[t.cx:])
t.input = t.input[:t.cx]
}
case actRune:
prefix := copySlice(t.input[:t.cx]) prefix := copySlice(t.input[:t.cx])
t.input = append(append(prefix, event.Char), t.input[t.cx:]...) t.input = append(append(prefix, event.Char), t.input[t.cx:]...)
t.cx++ t.cx++
case C.Mouse: case actMouse:
me := event.MouseEvent me := event.MouseEvent
mx, my := util.Constrain(me.X-len(t.prompt), 0, len(t.input)), me.Y mx, my := util.Constrain(me.X-len(t.prompt), 0, len(t.input)), me.Y
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 +817,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 +826,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 +849,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 +882,10 @@ func (t *Terminal) vset(o int) bool {
return t.cy == o return t.cy == o
} }
func maxItems() int { func (t *Terminal) maxItems() int {
if t.inlineInfo {
return C.MaxY() - 1
} else {
return C.MaxY() - 2 return C.MaxY() - 2
}
} }

View File

@@ -4,6 +4,8 @@
require 'minitest/autorun' require 'minitest/autorun'
require 'fileutils' require 'fileutils'
DEFAULT_TIMEOUT = 20
base = File.expand_path('../../', __FILE__) base = File.expand_path('../../', __FILE__)
Dir.chdir base Dir.chdir base
FZF = "#{base}/bin/fzf" FZF = "#{base}/bin/fzf"
@@ -22,26 +24,13 @@ class NilClass
end end
end end
module Temp def wait
def readonce since = Time.now
name = self.class::TEMPNAME while Time.now - since < DEFAULT_TIMEOUT
waited = 0 return if yield
while waited < 5 sleep 0.05
begin
system 'sync'
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
throw 'timeout'
end end
class Shell class Shell
@@ -59,8 +48,6 @@ class Shell
end end
class Tmux class Tmux
include Temp
TEMPNAME = '/tmp/fzf-test.txt' TEMPNAME = '/tmp/fzf-test.txt'
attr_reader :win attr_reader :win
@@ -90,8 +77,10 @@ class Tmux
end end
def close def close
wait do
send_keys 'C-c', 'C-u', 'exit', :Enter send_keys 'C-c', 'C-u', 'exit', :Enter
wait { closed? } closed?
end
end end
def kill def kill
@@ -111,68 +100,54 @@ class Tmux
go("send-keys -t #{target} #{args}") go("send-keys -t #{target} #{args}")
end end
def capture opts = {} def capture pane = 0
timeout, pane = defaults(opts).values_at(:timeout, :pane) File.unlink TEMPNAME while File.exists? TEMPNAME
waited = 0 wait do
loop do go("capture-pane -t #{win}.#{pane} \\; save-buffer #{TEMPNAME} 2> /dev/null")
go("capture-pane -t #{win}.#{pane} \\; save-buffer #{TEMPNAME}") $?.exitstatus == 0
break if $?.exitstatus == 0
if waited > timeout
raise "Window not found"
end end
waited += 0.1 File.read(TEMPNAME).split($/)[0, @lines].reverse.drop_while(&:empty?).reverse
sleep 0.1
end
readonce.split($/)[0, @lines].reverse.drop_while(&:empty?).reverse
end end
def until opts = {} def until pane = 0
lines = nil lines = nil
wait(opts) do begin
yield lines = capture(opts) wait do
lines = capture(pane)
class << lines
def item_count
self[-2] ? self[-2].strip.split('/').last.to_i : 0
end
end
yield lines
end
rescue Exception
puts $!.backtrace
puts '>' * 80
puts lines
puts '<' * 80
raise
end end
lines lines
end end
def prepare def prepare
self.send_keys 'echo hello', :Enter tries = 0
self.until { |lines| lines[-1].start_with?('hello') } begin
self.send_keys 'clear', :Enter self.send_keys 'C-u', 'hello', 'Right'
self.until { |lines| lines.empty? } self.until { |lines| lines[-1].end_with?('hello') }
rescue Exception
(tries += 1) < 5 ? retry : raise
end
self.send_keys 'C-u'
end end
private private
def defaults opts
{ timeout: 10, pane: 0 }.merge(opts)
end
def wait opts = {}
timeout, pane = defaults(opts).values_at(:timeout, :pane)
waited = 0
until yield
if waited > timeout
hl = '=' * 10
puts hl
capture(opts).each_with_index do |line, idx|
puts [idx.to_s.rjust(2), line].join(': ')
end
puts hl
raise "timeout"
end
waited += 0.1
sleep 0.1
end
end
def go *args def go *args
%x[tmux #{args.join ' '}].split($/) %x[tmux #{args.join ' '}].split($/)
end end
end end
class TestBase < Minitest::Test class TestBase < Minitest::Test
include Temp
FIN = 'FIN'
TEMPNAME = '/tmp/output' TEMPNAME = '/tmp/output'
attr_reader :tmux attr_reader :tmux
@@ -180,10 +155,18 @@ class TestBase < Minitest::Test
def setup def setup
ENV.delete 'FZF_DEFAULT_OPTS' ENV.delete 'FZF_DEFAULT_OPTS'
ENV.delete 'FZF_DEFAULT_COMMAND' ENV.delete 'FZF_DEFAULT_COMMAND'
File.unlink TEMPNAME while File.exists?(TEMPNAME)
end
def readonce
wait { File.exists?(TEMPNAME) }
File.read(TEMPNAME)
ensure
File.unlink TEMPNAME while File.exists?(TEMPNAME)
end end
def fzf(*opts) def fzf(*opts)
fzf!(*opts) + " > #{TEMPNAME} && echo #{FIN}" fzf!(*opts) + " > #{TEMPNAME}.tmp; mv #{TEMPNAME}.tmp #{TEMPNAME}"
end end
def fzf!(*opts) def fzf!(*opts)
@@ -214,8 +197,7 @@ class TestGoFZF < TestBase
def test_vanilla def test_vanilla
tmux.send_keys "seq 1 100000 | #{fzf}", :Enter tmux.send_keys "seq 1 100000 | #{fzf}", :Enter
tmux.until(timeout: 20) { |lines| tmux.until { |lines| lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ }
lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ }
lines = tmux.capture lines = tmux.capture
assert_equal ' 2', lines[-4] assert_equal ' 2', lines[-4]
assert_equal '> 1', lines[-3] assert_equal '> 1', lines[-3]
@@ -320,7 +302,6 @@ class TestGoFZF < TestBase
: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?(FIN) }
assert_equal %w[3 2 5 6 8 7], readonce.split($/) assert_equal %w[3 2 5 6 8 7], readonce.split($/)
tmux.close tmux.close
end end
@@ -341,13 +322,11 @@ class TestGoFZF < TestBase
# 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?(FIN) }
assert_equal [' 1st 2nd 3rd/', ' first second third/'], readonce.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?(FIN) }
assert_equal [' 1st 2nd 3rd/'], readonce.split($/) assert_equal [' 1st 2nd 3rd/'], readonce.split($/)
end end
end end
@@ -360,20 +339,17 @@ class TestGoFZF < TestBase
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?(FIN) }
assert_equal '100', readonce.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'}", :Enter tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 5555, :'1'}", :Enter
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal ['5555', '55'], readonce.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'}", :Enter tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 555555, :'0'}", :Enter
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal ['555555'], readonce.split($/) assert_equal ['555555'], readonce.split($/)
end end
@@ -382,16 +358,14 @@ class TestGoFZF < TestBase
tmux.send_keys "seq 1 100 | #{fzf :print_query, :multi, :q, 5, *opt}", :Enter tmux.send_keys "seq 1 100 | #{fzf :print_query, :multi, :q, 5, *opt}", :Enter
tmux.until { |lines| lines.last =~ /^> 5/ } tmux.until { |lines| lines.last =~ /^> 5/ }
tmux.send_keys :BTab, :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab, :BTab, :Enter
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal ['5', '5', '15', '25'], readonce.split($/) assert_equal ['5', '5', '15', '25'], readonce.split($/)
end end
end end
def test_query_unicode def test_query_unicode
tmux.send_keys "(echo abc; echo 가나다) | #{fzf :query, '가다'}", :Enter tmux.send_keys "(echo abc; echo 가나다) | #{fzf :query, '가다'}", :Enter
tmux.until { |lines| lines.last.start_with? '>' } tmux.until { |lines| lines[-2].include? '1/2' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal ['가나다'], readonce.split($/) assert_equal ['가나다'], readonce.split($/)
end end
@@ -425,6 +399,7 @@ class TestGoFZF < TestBase
tmux.send_keys "seq 1 1000 | #{fzf :tac, :no_sort, :multi}", :Enter tmux.send_keys "seq 1 1000 | #{fzf :tac, :no_sort, :multi}", :Enter
tmux.until { |lines| lines[-2].include? '1000/1000' } tmux.until { |lines| lines[-2].include? '1000/1000' }
tmux.send_keys '00' tmux.send_keys '00'
tmux.until { |lines| lines[-2].include? '10/1000' }
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
@@ -434,6 +409,7 @@ class TestGoFZF < TestBase
tmux.send_keys "seq 1 100 | #{fzf :expect, key}", :Enter tmux.send_keys "seq 1 100 | #{fzf :expect, key}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' } tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys '55' tmux.send_keys '55'
tmux.until { |lines| lines[-2].include? '1/100' }
tmux.send_keys *feed tmux.send_keys *feed
assert_equal [expected, '55'], readonce.split($/) assert_equal [expected, '55'], readonce.split($/)
end end
@@ -452,6 +428,7 @@ class TestGoFZF < TestBase
tmux.send_keys "seq 1 100 | #{fzf '--expect=alt-z', :print_query}", :Enter tmux.send_keys "seq 1 100 | #{fzf '--expect=alt-z', :print_query}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' } tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys '55' tmux.send_keys '55'
tmux.until { |lines| lines[-2].include? '1/100' }
tmux.send_keys :Escape, :z tmux.send_keys :Escape, :z
assert_equal ['55', 'alt-z', '55'], readonce.split($/) assert_equal ['55', 'alt-z', '55'], readonce.split($/)
end end
@@ -462,7 +439,8 @@ class TestGoFZF < TestBase
end end
def test_toggle_sort def test_toggle_sort
tmux.send_keys "seq 1 111 | #{fzf '-m +s --tac --toggle-sort=ctrl-r -q11'}", :Enter ['--toggle-sort=ctrl-r', '--bind=ctrl-r:toggle-sort'].each do |opt|
tmux.send_keys "seq 1 111 | #{fzf "-m +s --tac #{opt} -q11"}", :Enter
tmux.until { |lines| lines[-3].include? '> 111' } tmux.until { |lines| lines[-3].include? '> 111' }
tmux.send_keys :Tab tmux.send_keys :Tab
tmux.until { |lines| lines[-2].include? '4/111 (1)' } tmux.until { |lines| lines[-2].include? '4/111 (1)' }
@@ -473,6 +451,7 @@ class TestGoFZF < TestBase
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal ['111', '11'], readonce.split($/) assert_equal ['111', '11'], readonce.split($/)
end end
end
def test_unicode_case def test_unicode_case
tempname = TEMPNAME + Time.now.to_f.to_s tempname = TEMPNAME + Time.now.to_f.to_s
@@ -525,16 +504,42 @@ 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
def test_bind
tmux.send_keys "seq 1 1000 | #{
fzf '-m --bind=ctrl-j:accept,u:up,T:toggle-up,t:toggle'}", :Enter
tmux.until { |lines| lines[-2].end_with? '/1000' }
tmux.send_keys 'uuu', 'TTT', 'tt', 'uu', 'ttt', 'C-j'
assert_equal %w[4 5 6 9], readonce.split($/)
end
def test_long_line
tempname = TEMPNAME + Time.now.to_f.to_s
data = '.' * 256 * 1024
File.open(tempname, 'w') do |f|
f << data
end
assert_equal data, `cat #{tempname} | #{FZF} -f .`.chomp
ensure
File.unlink tempname
end
private private
def writelines path, lines, timeout = 10 def writelines path, lines
File.open(path, 'w') do |f| File.unlink path while File.exists? path
f << lines.join($/) File.open(path, 'w') { |f| f << lines.join($/) }
f.sync
end
since = Time.now
while `cat #{path}`.split($/).length != lines.length && (Time.now - since) < 10
sleep 0.1
end
end end
end end
@@ -550,32 +555,31 @@ module TestShell
def test_ctrl_t def test_ctrl_t
tmux.prepare tmux.prepare
tmux.send_keys 'C-t', pane: 0 tmux.send_keys 'C-t', pane: 0
lines = tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' } lines = tmux.until(1) { |lines| lines.item_count > 1 }
expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ')
tmux.send_keys :BTab, :BTab, :Enter, pane: 1 tmux.send_keys :BTab, :BTab, :Enter, pane: 1
tmux.until(pane: 0) { |lines| lines[-1].include? expected } tmux.until(0) { |lines| lines[-1].include? expected }
tmux.send_keys 'C-c' tmux.send_keys 'C-c'
# FZF_TMUX=0 # FZF_TMUX=0
new_shell new_shell
tmux.send_keys 'C-t', pane: 0 tmux.send_keys 'C-t', pane: 0
lines = tmux.until(pane: 0) { |lines| lines[-1].start_with? '>' } lines = tmux.until(0) { |lines| lines.item_count > 1 }
expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ')
tmux.send_keys :BTab, :BTab, :Enter, pane: 0 tmux.send_keys :BTab, :BTab, :Enter, pane: 0
tmux.until(pane: 0) { |lines| lines[-1].include? expected } tmux.until(0) { |lines| lines[-1].include? expected }
tmux.send_keys 'C-c', 'C-d' tmux.send_keys 'C-c', 'C-d'
end end
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(1) { |lines| lines.item_count > 0 }
expected = lines[-3][2..-1] expected = lines[-3][2..-1]
p expected tmux.send_keys :Enter, pane: 1
tmux.send_keys :Enter
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| lines[-1].end_with?(expected) }
end end
def test_ctrl_r def test_ctrl_r
@@ -585,51 +589,80 @@ 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(1) { |lines| lines.item_count > 0 }
tmux.send_keys '3d' tmux.send_keys '3d', pane: 1
tmux.until { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort tmux.until(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' }
end end
end end
class TestBash < TestBase module CompletionTest
include TestShell
def new_shell
tmux.send_keys "FZF_TMUX=0 #{Shell.bash}", :Enter
tmux.prepare
end
def setup
super
@tmux = Tmux.new :bash
end
def test_file_completion def test_file_completion
tmux.send_keys 'mkdir -p /tmp/fzf-test; touch /tmp/fzf-test/{1..100}', :Enter FileUtils.mkdir_p '/tmp/fzf-test'
FileUtils.mkdir_p '/tmp/fzf test'
(1..100).each { |i| FileUtils.touch "/tmp/fzf-test/#{i}" }
['no~such~user', '/tmp/fzf test/foobar', '~/.fzf-home'].each do |f|
FileUtils.touch File.expand_path(f)
end
tmux.prepare tmux.prepare
tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0 tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0
tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' } tmux.until(1) { |lines| lines.item_count > 0 }
tmux.send_keys :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab, :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
lines[-1].include?('/tmp/fzf-test/10') && lines[-1].include?('/tmp/fzf-test/10') &&
lines[-1].include?('/tmp/fzf-test/100') lines[-1].include?('/tmp/fzf-test/100')
end end
# ~USERNAME**<TAB>
tmux.send_keys 'C-u'
tmux.send_keys "cat ~#{ENV['USER']}**", :Tab, pane: 0
tmux.until(1) { |lines| lines.item_count > 0 }
tmux.send_keys '.fzf-home'
tmux.until(1) { |lines| lines[-3].end_with? '.fzf-home' }
tmux.send_keys :Enter
tmux.until do |lines|
tmux.send_keys 'C-L'
lines[-1].end_with?('.fzf-home')
end
# ~INVALID_USERNAME**<TAB>
tmux.send_keys 'C-u'
tmux.send_keys "cat ~such**", :Tab, pane: 0
tmux.until(1) { |lines| lines[-3].end_with? 'no~such~user' }
tmux.send_keys :Enter
tmux.until do |lines|
tmux.send_keys 'C-L'
lines[-1].end_with?('no~such~user')
end
# /tmp/fzf\ test**<TAB>
tmux.send_keys 'C-u'
tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab, pane: 0
tmux.until(1) { |lines| lines.item_count > 0 }
tmux.send_keys :Enter
tmux.until do |lines|
tmux.send_keys 'C-L'
lines[-1].end_with?('/tmp/fzf\ test/foobar')
end
ensure
['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f|
FileUtils.rm_rf File.expand_path(f)
end
end end
def test_dir_completion def test_dir_completion
tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}; touch /tmp/fzf-test/d55/xxx', :Enter tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}; touch /tmp/fzf-test/d55/xxx', :Enter
tmux.prepare tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test/**', :Tab, pane: 0 tmux.send_keys 'cd /tmp/fzf-test/**', :Tab, pane: 0
tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' } tmux.until(1) { |lines| lines.item_count > 0 }
tmux.send_keys :BTab, :BTab # BTab does not work here tmux.send_keys :BTab, :BTab # BTab does not work here
tmux.send_keys 55 tmux.send_keys 55
tmux.until(pane: 1) { |lines| lines[-2].start_with? ' 1/' } tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
@@ -638,9 +671,11 @@ class TestBash < TestBase
tmux.send_keys :xx tmux.send_keys :xx
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' } tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' }
# Should not match regular files # Should not match regular files (bash-only)
if self.class == TestBash
tmux.send_keys :Tab tmux.send_keys :Tab
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' } tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' }
end
# Fail back to plusdirs # Fail back to plusdirs
tmux.send_keys :BSpace, :BSpace, :BSpace tmux.send_keys :BSpace, :BSpace, :BSpace
@@ -655,19 +690,37 @@ class TestBash < TestBase
pid = lines[-1].split.last pid = lines[-1].split.last
tmux.prepare tmux.prepare
tmux.send_keys 'kill ', :Tab, pane: 0 tmux.send_keys 'kill ', :Tab, pane: 0
tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' } tmux.until(1) { |lines| lines.item_count > 0 }
tmux.send_keys 'sleep12345' tmux.send_keys 'sleep12345'
tmux.until(pane: 1) { |lines| lines[-3].include? 'sleep 12345' } tmux.until(1) { |lines| lines[-3].include? 'sleep 12345' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
lines[-1] == "kill #{pid}" lines[-1] == "kill #{pid}"
end end
ensure
Process.kill 'KILL', pid.to_i rescue nil if pid
end
end
class TestBash < TestBase
include TestShell
include CompletionTest
def new_shell
tmux.send_keys "FZF_TMUX=0 #{Shell.bash}", :Enter
tmux.prepare
end
def setup
super
@tmux = Tmux.new :bash
end end
end end
class TestZsh < TestBase class TestZsh < TestBase
include TestShell include TestShell
include CompletionTest
def new_shell def new_shell
tmux.send_keys "FZF_TMUX=0 #{Shell.zsh}", :Enter tmux.send_keys "FZF_TMUX=0 #{Shell.zsh}", :Enter