diff --git a/.gitignore b/.gitignore
index 1627430..0915467 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+bin
+src/fzf/fzf_*
pkg
Gemfile.lock
.DS_Store
diff --git a/.travis.yml b/.travis.yml
index 7633c4f..cbd5c11 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,10 +1,24 @@
language: ruby
-sudo: false
rvm:
- - "1.8.7"
- - "1.9.3"
- - "2.0.0"
- - "2.1.1"
+- 2.2.0
-install: gem install curses minitest
+install:
+- sudo apt-get update
+- sudo apt-get install -y libncurses-dev lib32ncurses5-dev
+- sudo add-apt-repository -y ppa:pi-rho/dev
+- sudo apt-add-repository -y ppa:fish-shell/release-2
+- sudo apt-get update
+- sudo apt-get install -y tmux=1.9a-1~ppa1~p
+- sudo apt-get install -y zsh fish
+script: |
+ export GOPATH=~/go
+ export FZF_BASE=$GOPATH/src/github.com/junegunn/fzf
+
+ mkdir -p $GOPATH/src/github.com/junegunn
+ ln -s $(pwd) $FZF_BASE
+
+ cd $FZF_BASE/src && make test fzf/fzf-linux_amd64 install &&
+ cd $FZF_BASE/bin && ln -sf fzf-linux_amd64 fzf-$(./fzf --version)-linux_amd64 &&
+ cd $FZF_BASE && yes | ./install && rm -f fzf &&
+ tmux new "ruby test/test_go.rb > out && touch ok" && cat out && [ -e ok ]
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..f10cbca
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,302 @@
+CHANGELOG
+=========
+
+0.10.6
+------
+
+- Replaced `--header-file` with `--header` option
+- `--header` and `--header-lines` can be used together
+- Changed exit status
+ - 0: Okay
+ - 1: No match
+ - 2: Error/Interrupted
+
+0.10.5
+------
+
+- `'`-prefix to unquote the term in `--extended-exact` mode
+- Backward scan when `--tiebreak=end` is set
+
+0.10.4
+------
+
+- Fixed to remove ANSI code from output when `--with-nth` is set
+
+0.10.3
+------
+
+- Fixed slow performance of `--with-nth` when used with `--delimiter`
+ - Regular expression engine of Golang as of now is very slow, so the fixed
+ version will treat the given delimiter pattern as a plain string instead
+ of a regular expression unless it contains special characters and is
+ a valid regular expression.
+ - Simpler regular expression for delimiter for better performance
+
+0.10.2
+------
+
+### Fixes and improvements
+
+- Improvement in perceived response time of queries
+ - Eager, efficient rune array conversion
+- Graceful exit when failed to initialize ncurses (invalid $TERM)
+- Improved ranking algorithm when `--nth` option is set
+- Changed the default command not to fail when there are files whose names
+ start with dash
+
+0.10.1
+------
+
+### New features
+
+- Added `--margin` option
+- Added options for sticky header
+ - `--header-file`
+ - `--header-lines`
+- Added `cancel` action which clears the input or closes the finder when the
+ input is already empty
+ - e.g. `export FZF_DEFAULT_OPTS="--bind esc:cancel"`
+- Added `delete-char/eof` action to differentiate `CTRL-D` and `DEL`
+
+### Minor improvements/fixes
+
+- Fixed to allow binding colon and comma keys
+- Fixed ANSI processor to handle color regions spanning multiple lines
+
+0.10.0
+------
+
+### New features
+
+- More actions for `--bind`
+ - `select-all`
+ - `deselect-all`
+ - `toggle-all`
+ - `ignore`
+- `execute(...)` action for running arbitrary command without leaving fzf
+ - `fzf --bind "ctrl-m:execute(less {})"`
+ - `fzf --bind "ctrl-t:execute(tmux new-window -d 'vim {}')"`
+ - If the command contains parentheses, use any of the follows alternative
+ notations to avoid parse errors
+ - `execute[...]`
+ - `execute~...~`
+ - `execute!...!`
+ - `execute@...@`
+ - `execute#...#`
+ - `execute$...$`
+ - `execute%...%`
+ - `execute^...^`
+ - `execute&...&`
+ - `execute*...*`
+ - `execute;...;`
+ - `execute/.../`
+ - `execute|...|`
+ - `execute:...`
+ - This is the special form that frees you from parse errors as it
+ does not expect the closing character
+ - The catch is that it should be the last one in the
+ comma-separated list
+- Added support for optional search history
+ - `--history HISTORY_FILE`
+ - When used, `CTRL-N` and `CTRL-P` are automatically remapped to
+ `next-history` and `previous-history`
+ - `--history-size MAX_ENTRIES` (default: 1000)
+- Cyclic scrolling can be enabled with `--cycle`
+- Fixed the bug where the spinner was not spinning on idle input stream
+ - e.g. `sleep 100 | fzf`
+
+### Minor improvements/fixes
+
+- Added synonyms for key names that can be specified for `--bind`,
+ `--toggle-sort`, and `--expect`
+- Fixed the color of multi-select marker on the current line
+- Fixed to allow `^pattern$` in extended-search mode
+
+
+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
+------
+
+### Improvements
+
+- Performance optimization
+- Less aggressive memoization to limit memory usage
+
+### New features
+
+- Added color scheme for light background: `--color=light`
+
+0.9.9
+-----
+
+### New features
+
+- Added `--tiebreak` option (#191)
+- Added `--no-hscroll` option (#193)
+- Visual indication of `--toggle-sort` (#194)
+
+0.9.8
+-----
+
+### Bug fixes
+
+- Fixed Unicode case handling (#186)
+- Fixed to terminate on RuneError (#185)
+
+0.9.7
+-----
+
+### New features
+
+- Added `--toggle-sort` option (#173)
+ - `--toggle-sort=ctrl-r` is applied to `CTRL-R` shell extension
+
+### Bug fixes
+
+- Fixed to print empty line if `--expect` is set and fzf is completed by
+ `--select-1` or `--exit-0` (#172)
+- Fixed to allow comma character as an argument to `--expect` option
+
+0.9.6
+-----
+
+### New features
+
+#### Added `--expect` option (#163)
+
+If you provide a comma-separated list of keys with `--expect` option, fzf will
+allow you to select the match and complete the finder when any of the keys is
+pressed. Additionally, fzf will print the name of the key pressed as the first
+line of the output so that your script can decide what to do next based on the
+information.
+
+```sh
+fzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@
+```
+
+The updated vim plugin uses this option to implement
+[ctrlp](https://github.com/kien/ctrlp.vim)-compatible key bindings.
+
+### Bug fixes
+
+- Fixed to ignore ANSI escape code `\e[K` (#162)
+
+0.9.5
+-----
+
+### New features
+
+#### Added `--ansi` option (#150)
+
+If you give `--ansi` option to fzf, fzf will interpret ANSI color codes from
+the input, display the item with the ANSI colors (true colors are not
+supported), and strips the codes from the output. This option is off by
+default as it entails some overhead.
+
+### Improvements
+
+#### Reduced initial memory footprint (#151)
+
+By removing unnecessary copy of pointers, fzf will use significantly smaller
+amount of memory when it's started. The difference is hugely noticeable when
+the input is extremely large. (e.g. `locate / | fzf`)
+
+### Bug fixes
+
+- Fixed panic on `--no-sort --filter ''` (#149)
+
+0.9.4
+-----
+
+### New features
+
+#### Added `--tac` option to reverse the order of the input.
+
+One might argue that this option is unnecessary since we can already put `tac`
+or `tail -r` in the command pipeline to achieve the same result. However, the
+advantage of `--tac` is that it does not block until the input is complete.
+
+### *Backward incompatible changes*
+
+#### Changed behavior on `--no-sort`
+
+`--no-sort` option will no longer reverse the display order within finder. You
+may want to use the new `--tac` option with `--no-sort`.
+
+```
+history | fzf +s --tac
+```
+
+### Improvements
+
+#### `--filter` will not block when sort is disabled
+
+When fzf works in filtering mode (`--filter`) and sort is disabled
+(`--no-sort`), there's no need to block until input is complete. The new
+version of fzf will print the matches on-the-fly when the following condition
+is met:
+
+ --filter TERM --no-sort [--no-tac --no-sync]
+
+or simply:
+
+ -f TERM +s
+
+This change removes unnecessary delay in the use cases like the following:
+
+ fzf -f xxx +s | head -5
+
+However, in this case, fzf processes the lines sequentially, so it cannot
+utilize multiple cores, and fzf will run slightly slower than the previous
+mode of execution where filtering is done in parallel after the entire input
+is loaded. If the user is concerned about this performance problem, one can
+add `--sync` option to re-enable buffering.
+
+0.9.3
+-----
+
+### New features
+- Added `--sync` option for multi-staged filtering
+
+### Improvements
+- `--select-1` and `--exit-0` will start finder immediately when the condition
+ cannot be met
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..fe4c31a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Junegunn Choi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+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
+THE SOFTWARE.
diff --git a/README.md b/README.md
index 0b52864..956dd1e 100644
--- a/README.md
+++ b/README.md
@@ -1,49 +1,62 @@
-fzf - Fuzzy finder for your shell
-=================================
+ [![travis-ci](https://travis-ci.org/junegunn/fzf.svg?branch=master)](https://travis-ci.org/junegunn/fzf)
+===
-fzf is a general-purpose fuzzy finder for your shell.
+fzf is a general-purpose command-line fuzzy finder.
![](https://raw.github.com/junegunn/i/master/fzf.gif)
-It was heavily inspired by [ctrlp.vim](https://github.com/kien/ctrlp.vim) and
-the likes.
+Pros
+----
-Requirements
-------------
-
-fzf requires Ruby (>= 1.8.5).
+- No dependencies
+- Blazingly fast
+ - e.g. `locate / | fzf`
+- Flexible layout
+ - Runs in fullscreen or in horizontal/vertical split using tmux
+- The most comprehensive feature set
+ - Try `fzf --help` and be surprised
+- Batteries included
+ - Vim/Neovim plugin, key bindings and fuzzy auto-completion
Installation
------------
+fzf project consists of the followings:
+
+- `fzf` executable
+- `fzf-tmux` script for launching fzf in a tmux pane
+- Shell extensions
+ - Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish)
+ - Fuzzy auto-completion (bash, zsh)
+- Vim/Neovim plugin
+
+You can [download fzf executable][bin] alone, but it's recommended that you
+install the extra stuff using the attached install script.
+
+[bin]: https://github.com/junegunn/fzf-bin/releases
+
+#### Using git (recommended)
+
Clone this repository and run
[install](https://github.com/junegunn/fzf/blob/master/install) script.
```sh
-git clone https://github.com/junegunn/fzf.git ~/.fzf
+git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install
```
-In case you don't have git installed:
+#### Using Homebrew
+
+On OS X, you can use [Homebrew](http://brew.sh/) to install fzf.
```sh
-mkdir -p ~/.fzf
-curl -L https://github.com/junegunn/fzf/archive/master.tar.gz |
- tar xz --strip-components 1 -C ~/.fzf
-~/.fzf/install
+brew reinstall --HEAD fzf
+
+# Install shell extensions
+/usr/local/Cellar/fzf/HEAD/install
```
-The script will setup:
-
-- `fzf` function (bash, zsh, fish)
-- Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish)
-- Fuzzy auto-completion (bash)
-
-If you don't use any of the aforementioned shells, you have to manually place
-fzf executable in a directory included in `$PATH`. Key bindings and
-auto-completion will not be available in that case.
-
-### Install as Vim plugin
+#### Install as Vim plugin
Once you have cloned the repository, add the following line to your .vimrc.
@@ -51,55 +64,26 @@ Once you have cloned the repository, add the following line to your .vimrc.
set rtp+=~/.fzf
```
-Or you may use [vim-plug](https://github.com/junegunn/vim-plug) to manage fzf
-inside Vim:
+Or you can have [vim-plug](https://github.com/junegunn/vim-plug) manage fzf
+(recommended):
```vim
Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': 'yes \| ./install' }
```
+#### Upgrading fzf
+
+fzf is being actively developed and you might want to upgrade it once in a
+while. Please follow the instruction below depending on the installation
+method.
+
+- git: `cd ~/.fzf && git pull && ./install`
+- brew: `brew reinstall --HEAD fzf`
+- vim-plug: `:PlugUpdate fzf`
+
Usage
-----
-```
-usage: fzf [options]
-
- Search
- -x, --extended Extended-search mode
- -e, --extended-exact Extended-search mode (exact match)
- -i Case-insensitive match (default: smart-case match)
- +i Case-sensitive match
- -n, --nth=N[,..] Comma-separated list of field index expressions
- for limiting search scope. Each can be a non-zero
- integer or a range expression ([BEGIN]..[END])
- --with-nth=N[,..] Transform the item using index expressions for search
- -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
-
- Search result
- -s, --sort=MAX Maximum number of matched items to sort (default: 1000)
- +s, --no-sort Do not sort the result. Keep the sequence unchanged.
-
- Interface
- -m, --multi Enable multi-select with tab/shift-tab
- --no-mouse Disable mouse
- +c, --no-color Disable colors
- +2, --no-256 Disable 256-color
- --black Use black background
- --reverse Reverse orientation
- --prompt=STR Input prompt (default: '> ')
-
- Scripting
- -q, --query=STR Start the finder with the given query
- -1, --select-1 Automatically select the only match
- -0, --exit-0 Exit immediately when there's no match
- -f, --filter=STR Filter mode. Do not start interactive finder.
- --print-query Print query as the first line
-
- Environment variables
- FZF_DEFAULT_COMMAND Default command to use when input is tty
- FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m --sort 10000")
-```
-
fzf will launch curses-based finder, read the list from STDIN, and write the
selected item to STDOUT.
@@ -115,34 +99,16 @@ files excluding hidden ones. (You can override the default command with
vim $(fzf)
```
-If you want to preserve the exact sequence of the input, provide `--no-sort` (or
-`+s`) option.
+#### Using the finder
-```sh
-history | fzf +s
-```
+- `CTRL-J` / `CTRL-K` (or `CTRL-N` / `CTRL-P)` to move cursor up and down
+- `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit
+- On multi-select mode (`-m`), `TAB` and `Shift-TAB` to mark multiple items
+- Emacs style key bindings
+- Mouse: scroll, click, double-click; shift-click and shift-scroll on
+ multi-select mode
-### Keys
-
-Use CTRL-J and CTRL-K (or CTRL-N and CTRL-P) to change the selection, press
-enter key to select the item. CTRL-C, CTRL-G, or ESC will terminate the finder.
-
-The following readline key bindings should also work as expected.
-
-- CTRL-A / CTRL-E
-- CTRL-B / CTRL-F
-- CTRL-H / CTRL-D
-- CTRL-W / CTRL-U / CTRL-Y
-- ALT-B / ALT-F
-
-If you enable multi-select mode with `-m` option, you can select multiple items
-with TAB or Shift-TAB key.
-
-You can also use mouse. Double-click on an item to select it or shift-click (or
-ctrl-click) to select multiple items. Use mouse wheel to move the cursor up and
-down.
-
-### Extended-search mode
+#### Extended-search mode
With `-x` or `--extended` option, fzf will start in "extended-search mode".
@@ -158,43 +124,23 @@ such as: `^music .mp3$ sbtrkt !rmx`
| `'wild` | Items that include `wild` | exact-match (quoted) |
| `!'fire` | Items that do not include `fire` | inverse-exact-match |
-If you don't need fuzzy matching and do not wish to "quote" every word, start
-fzf with `-e` or `--extended-exact` option.
+If you don't prefer fuzzy matching and do not wish to "quote" every word,
+start fzf with `-e` or `--extended-exact` option. Note that in
+`--extended-exact` mode, `'`-prefix "unquotes" the term.
-Useful examples
----------------
+#### Environment variables
-```sh
-# fe [FUZZY PATTERN] - Open the selected file with the default editor
-# - Bypass fuzzy finder if there's only one match (--select-1)
-# - Exit if there's no match (--exit-0)
-fe() {
- local file
- file=$(fzf --query="$1" --select-1 --exit-0)
- [ -n "$file" ] && ${EDITOR:-vim} "$file"
-}
+- `FZF_DEFAULT_COMMAND`
+ - Default command to use when input is tty
+- `FZF_DEFAULT_OPTS`
+ - Default options. e.g. `export FZF_DEFAULT_OPTS="--extended --cycle"`
-# fd - cd to selected directory
-fd() {
- local dir
- dir=$(find ${1:-*} -path '*/\.*' -prune \
- -o -type d -print 2> /dev/null | fzf +m) &&
- cd "$dir"
-}
+Examples
+--------
-# fh - repeat history
-fh() {
- eval $(([ -n "$ZSH_NAME" ] && fc -l 1 || history) | fzf +s | sed 's/ *[0-9]* *//')
-}
-
-# fkill - kill process
-fkill() {
- ps -ef | sed 1d | fzf -m | awk '{print $2}' | xargs kill -${1:-9}
-}
-```
-
-For more examples, see [the wiki
-page](https://github.com/junegunn/fzf/wiki/examples).
+Many useful examples can be found on [the wiki
+page](https://github.com/junegunn/fzf/wiki/examples). Feel free to add your
+own as well.
Key bindings for command line
-----------------------------
@@ -202,29 +148,42 @@ Key bindings for command line
The install script will setup the following key bindings for bash, zsh, and
fish.
-- `CTRL-T` - Paste the selected file path(s) into the command line
-- `CTRL-R` - Paste the selected command from history into the command line
+- `CTRL-T` - Paste the selected files and directories onto the command line
+ - Set `FZF_CTRL_T_COMMAND` to override the default command
+- `CTRL-R` - Paste the selected command from history onto the command line
+ - Sort is disabled by default to respect chronological ordering
+ - Press `CTRL-R` again to toggle sort
- `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
-may disable this tmux integration by setting `FZF_TMUX` to 0, or change the
-height of the window with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`).
+If you're on a tmux session, fzf will start in a split pane. You may disable
+this tmux integration by setting `FZF_TMUX` to 0, or change the height of the
+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
~/.fzf.bash` in your .bashrc, so that it correctly sets up key bindings for vi
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
+-----------------
-Auto-completion
----------------
+[fzf-tmux](bin/fzf-tmux) is a bash script that opens fzf in a tmux pane.
-Disclaimer: *Auto-completion feature is currently experimental, it can change
-over time*
+```sh
+# usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
+# (-[udlr]: up/down/left/right)
-### bash
+# select git branches in horizontal split below (15 lines)
+git branch | fzf-tmux -d 15
+
+# select multiple words in vertical split on the left (20% of screen width)
+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]`
+options, so you can invariably use `fzf-tmux` in your scripts.
+
+Fuzzy completion for bash and zsh
+---------------------------------
#### Files and directories
@@ -293,18 +252,15 @@ export FZF_COMPLETION_TRIGGER='~~'
export FZF_COMPLETION_OPTS='+c -x'
```
-### zsh
-
-TODO :smiley:
-
-(Pull requests are appreciated.)
-
Usage as Vim plugin
-------------------
+This repository only enables basic integration with Vim. If you're looking for
+more, check out [fzf.vim](https://github.com/junegunn/fzf.vim) project.
+
(Note: To use fzf in GVim, an external terminal emulator is required.)
-### `:FZF[!]`
+#### `:FZF[!]`
If you have set up fzf for Vim, `:FZF` command will be added.
@@ -317,142 +273,61 @@ If you have set up fzf for Vim, `:FZF` command will be added.
" With options
:FZF --no-sort -m /tmp
+
+" Bang version starts in fullscreen instead of using tmux pane or Neovim split
+:FZF!
```
-Note that the environment variables `FZF_DEFAULT_COMMAND` and `FZF_DEFAULT_OPTS`
-also apply here.
+Similarly to [ctrlp.vim](https://github.com/kien/ctrlp.vim), use enter key,
+`CTRL-T`, `CTRL-X` or `CTRL-V` to open selected files in the current window,
+in new tabs, in horizontal splits, or in vertical splits respectively.
-If you're on a tmux session, `:FZF` will launch fzf in a new split-window whose
-height can be adjusted with `g:fzf_tmux_height` (default: '40%'). However, the
-bang version (`:FZF!`) will always start in fullscreen.
+Note that the environment variables `FZF_DEFAULT_COMMAND` and
+`FZF_DEFAULT_OPTS` also apply here. Refer to [the wiki page][fzf-config] for
+customization.
-In GVim, you need an external terminal emulator to start fzf with. `xterm`
-command is used by default, but you can customize it with `g:fzf_launcher`.
+[fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-FZF-command-(vim)
-```vim
-" This is the default. %s is replaced with fzf command
-let g:fzf_launcher = 'xterm -e bash -ic %s'
+#### `fzf#run([options])`
-" Use urxvt instead
-let g:fzf_launcher = 'urxvt -geometry 120x30 -e sh -c %s'
-```
+For more advanced uses, you can use `fzf#run()` function with the following
+options.
-If you're running MacVim on OSX, I recommend you to use iTerm2 as the launcher.
-Refer to the [this wiki
-page](https://github.com/junegunn/fzf/wiki/fzf-with-MacVim-and-iTerm2) to see
-how to set up.
+| Option name | Type | Description |
+| -------------------------- | ------------- | ---------------------------------------------------------------- |
+| `source` | string | External command to generate input to fzf (e.g. `find .`) |
+| `source` | list | Vim list as input to fzf |
+| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
+| `sink` | funcref | Reference to function to process each selected item |
+| `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once |
+| `options` | string | Options to fzf |
+| `dir` | string | Working directory |
+| `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`) |
+| `launcher` | string | External terminal emulator to start fzf with (GVim only) |
+| `launcher` | funcref | Function for generating `launcher` string (GVim only) |
-### `fzf#run([options])`
-
-For more advanced uses, you can call `fzf#run()` function which returns the list
-of the selected items.
-
-`fzf#run()` may take an options-dictionary:
-
-| Option name | Type | Description |
-| --------------- | ------------- | ------------------------------------------------------------------ |
-| `source` | string | External command to generate input to fzf (e.g. `find .`) |
-| `source` | list | Vim list as input to fzf |
-| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
-| `sink` | funcref | Reference to function to process each selected item |
-| `options` | string | Options to fzf |
-| `dir` | string | Working directory |
-| `tmux_width` | number/string | Use tmux vertical split with the given height (e.g. `20`, `50%`) |
-| `tmux_height` | number/string | Use tmux horizontal split with the given height (e.g. `20`, `50%`) |
-| `launcher` | string | External terminal emulator to start fzf with (Only used in GVim) |
-
-#### Examples
-
-If `sink` option is not given, `fzf#run` will simply return the list.
-
-```vim
-let items = fzf#run({ 'options': '-m +c', 'dir': '~', 'source': 'ls' })
-```
-
-But if `sink` is given as a string, the command will be executed for each
-selected item.
-
-```vim
-" Each selected item will be opened in a new tab
-let items = fzf#run({ 'sink': 'tabe', 'options': '-m +c', 'dir': '~', 'source': 'ls' })
-```
-
-We can also use a Vim list as the source as follows:
-
-```vim
-" Choose a color scheme with fzf
-nnoremap C :call fzf#run({
-\ 'source':
-\ map(split(globpath(&rtp, "colors/*.vim"), "\n"),
-\ "substitute(fnamemodify(v:val, ':t'), '\\..\\{-}$', '', '')"),
-\ 'sink': 'colo',
-\ 'options': '+m',
-\ 'tmux_width': 20,
-\ 'launcher': 'xterm -geometry 20x30 -e bash -ic %s'
-\ })
-```
-
-`sink` option can be a function reference. The following example creates a
-handy mapping that selects an open buffer.
-
-```vim
-" List of buffers
-function! BufList()
- redir => ls
- silent ls
- redir END
- return split(ls, '\n')
-endfunction
-
-function! BufOpen(e)
- execute 'buffer '. matchstr(a:e, '^[ 0-9]*')
-endfunction
-
-nnoremap :call fzf#run({
-\ 'source': reverse(BufList()),
-\ 'sink': function('BufOpen'),
-\ 'options': '+m',
-\ 'tmux_height': '40%'
-\ })
-```
-
-### Articles
-
-- [fzf+vim+tmux](http://junegunn.kr/2014/04/fzf+vim+tmux)
+Examples can be found on [the wiki
+page](https://github.com/junegunn/fzf/wiki/Examples-(vim)).
Tips
----
-### Rendering issues
+#### Rendering issues
If you have any rendering issues, check the followings:
1. Make sure `$TERM` is correctly set. fzf will use 256-color only if it
- contains `256` (e.g. `xterm-256color`)
+ contains `256` (e.g. `xterm-256color`)
2. If you're on screen or tmux, `$TERM` should be either `screen` or
- `screen-256color`
+ `screen-256color`
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`
- option. And if it solves your problem, I recommend including it in
- `FZF_DEFAULT_OPTS` for further convenience.
+ background color and make some text unable to read. In that case, try
+ `--black` option. And if it solves your problem, I recommend including it
+ in `FZF_DEFAULT_OPTS` for further convenience.
4. If you still have problem, try `--no-256` option or even `--no-color`.
-5. Ruby 1.9 or above is required for correctly displaying unicode characters.
-### Ranking algorithm
-
-fzf sorts the result first by the length of the matched substring, then by the
-length of the whole string. However it only does so when the number of matches
-is less than the limit which is by default 1000, in order to avoid the cost of
-sorting a large list and limit the response time of the query.
-
-This limit can be adjusted with `-s` option, or with the environment variable
-`FZF_DEFAULT_OPTS`.
-
-```sh
-export FZF_DEFAULT_OPTS="--sort 20000"
-```
-
-### Respecting `.gitignore`, `.hgignore`, and `svn:ignore`
+#### Respecting `.gitignore`, `.hgignore`, and `svn:ignore`
[ag](https://github.com/ggreer/the_silver_searcher) or
[pt](https://github.com/monochromegane/the_platinum_searcher) will do the
@@ -467,62 +342,23 @@ export FZF_DEFAULT_COMMAND='ag -l -g ""'
# Now fzf (w/o pipe) will use ag instead of find
fzf
+
+# To apply the command to CTRL-T as well
+export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
```
-### `git ls-tree` for fast traversal
+#### `git ls-tree` for fast traversal
If you're running fzf in a large git repository, `git ls-tree` can boost up the
speed of the traversal.
```sh
-# Copy the original fzf function to __fzf
-declare -f __fzf > /dev/null ||
- eval "$(echo "__fzf() {"; declare -f fzf | \grep -v '^{' | tail -n +2)"
-
-# Use git ls-tree when possible
-fzf() {
- if [ -n "$(git rev-parse HEAD 2> /dev/null)" ]; then
- FZF_DEFAULT_COMMAND="git ls-tree -r --name-only HEAD" __fzf "$@"
- else
- __fzf "$@"
- fi
-}
+export FZF_DEFAULT_COMMAND='
+ (git ls-tree -r --name-only HEAD ||
+ find * -name ".*" -prune -o -type f -print -o -type l -print) 2> /dev/null'
```
-### Using fzf with tmux splits
-
-It isn't too hard to write your own fzf-tmux combo like the default
-CTRL-T key binding. (Or is it?)
-
-```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)
that it doesn't allow reading from STDIN in command substitution, which means
@@ -530,42 +366,15 @@ simple `vim (fzf)` won't work as expected. The workaround is to store the result
of fzf to a temporary file.
```sh
-function vimf
- 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
-```
-
-### Windows
-
-fzf works on [Cygwin](http://www.cygwin.com/) and
-[MSYS2](http://sourceforge.net/projects/msys2/). You may need to use `--black`
-option on MSYS2 to avoid rendering issues.
-
-### Handling UTF-8 NFD paths on OSX
-
-Use iconv to convert NFD paths to NFC:
-
-```sh
-find . | iconv -f utf-8-mac -t utf8//ignore | fzf
+fzf > $TMPDIR/fzf.result; and vim (cat $TMPDIR/fzf.result)
```
License
-------
-MIT
+[MIT](LICENSE)
Author
------
Junegunn Choi
-
diff --git a/Rakefile b/Rakefile
deleted file mode 100644
index 933a039..0000000
--- a/Rakefile
+++ /dev/null
@@ -1,9 +0,0 @@
-require "bundler/gem_tasks"
-require 'rake/testtask'
-
-Rake::TestTask.new(:test) do |test|
- test.pattern = 'test/**/test_*.rb'
- test.verbose = true
-end
-
-task :default => :test
diff --git a/bin/fzf-tmux b/bin/fzf-tmux
new file mode 100755
index 0000000..239c878
--- /dev/null
+++ b/bin/fzf-tmux
@@ -0,0 +1,138 @@
+#!/usr/bin/env bash
+# fzf-tmux: starts fzf in a tmux pane
+# usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
+
+args=()
+opt=""
+skip=""
+swap=""
+close=""
+term=""
+while [ $# -gt 0 ]; do
+ arg="$1"
+ case "$arg" in
+ -)
+ term=1
+ ;;
+ -w*|-h*|-d*|-u*|-r*|-l*)
+ if [ -n "$skip" ]; then
+ args+=("$1")
+ shift
+ continue
+ fi
+ if [[ "$arg" =~ ^.[lrw] ]]; then
+ opt="-h"
+ if [[ "$arg" =~ ^.l ]]; then
+ opt="$opt -d"
+ swap="; swap-pane -D ; select-pane -L"
+ close="; tmux swap-pane -D"
+ fi
+ else
+ opt=""
+ if [[ "$arg" =~ ^.u ]]; then
+ opt="$opt -d"
+ swap="; swap-pane -D ; select-pane -U"
+ close="; tmux swap-pane -D"
+ fi
+ fi
+ if [ ${#arg} -gt 2 ]; then
+ size="${arg:2}"
+ else
+ shift
+ if [[ "$1" =~ ^[0-9]+%?$ ]]; then
+ size="$1"
+ else
+ [ -n "$1" -a "$1" != "--" ] && args+=("$1")
+ shift
+ continue
+ fi
+ fi
+
+ if [[ "$size" =~ %$ ]]; then
+ size=${size:0:((${#size}-1))}
+ if [ -n "$swap" ]; then
+ opt="$opt -p $(( 100 - size ))"
+ else
+ opt="$opt -p $size"
+ fi
+ else
+ if [ -n "$swap" ]; then
+ if [[ "$arg" =~ ^.l ]]; then
+ [ -n "$COLUMNS" ] && max=$COLUMNS || max=$(tput cols)
+ else
+ [ -n "$LINES" ] && max=$LINES || max=$(tput lines)
+ fi
+ size=$(( max - size ))
+ [ $size -lt 0 ] && size=0
+ opt="$opt -l $size"
+ else
+ opt="$opt -l $size"
+ fi
+ fi
+ ;;
+ --)
+ # "--" can be used to separate fzf-tmux options from fzf options to
+ # avoid conflicts
+ skip=1
+ ;;
+ *)
+ args+=("$1")
+ ;;
+ esac
+ shift
+done
+
+if [ -z "$TMUX_PANE" ] || tmux list-panes -F '#F' | grep -q Z; then
+ fzf "${args[@]}"
+ exit $?
+fi
+
+set -e
+
+# Clean up named pipes on exit
+id=$RANDOM
+argsf=/tmp/fzf-args-$id
+fifo1=/tmp/fzf-fifo1-$id
+fifo2=/tmp/fzf-fifo2-$id
+fifo3=/tmp/fzf-fifo3-$id
+cleanup() {
+ rm -f $argsf $fifo1 $fifo2 $fifo3
+}
+trap cleanup EXIT SIGINT SIGTERM
+
+fail() {
+ >&2 echo "$1"
+ exit 2
+}
+fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf"
+[ -x "$fzf" ] || fail "fzf executable not found"
+
+envs="env TERM=$TERM "
+[ -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")"
+
+mkfifo $fifo2
+mkfifo $fifo3
+
+# Build arguments to fzf
+opts=""
+for arg in "${args[@]}"; do
+ arg="${arg//\"/\\\"}"
+ arg="${arg//\`/\\\`}"
+ opts="$opts \"$arg\""
+done
+
+if [ -n "$term" -o -t 0 ]; then
+ cat <<< "$fzf $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf
+ tmux set-window-option -q synchronize-panes off \;\
+ split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap
+else
+ mkfifo $fifo1
+ cat <<< "$fzf $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf
+ tmux set-window-option -q synchronize-panes off \;\
+ split-window $opt "$envs bash $argsf" $swap
+ cat <&0 > $fifo1 &
+fi
+cat $fifo2
+exit "$(cat $fifo3)"
+
diff --git a/ext/mkrf_conf.rb b/ext/mkrf_conf.rb
deleted file mode 100644
index 8e7bf46..0000000
--- a/ext/mkrf_conf.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-require 'rubygems/dependency_installer'
-
-if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.1.0')
- Gem::DependencyInstaller.new.install 'curses', '~> 1.0'
-end
-
-File.open(File.expand_path('../Rakefile', __FILE__), 'w') do |f|
- f.puts 'task :default'
-end
diff --git a/fzf b/fzf
index 3b049b9..17f86b5 100755
--- a/fzf
+++ b/fzf
@@ -8,6 +8,8 @@
# /_/ /___/_/ Fuzzy finder for your shell
#
# Version: 0.8.9 (Dec 24, 2014)
+# Deprecation alert:
+# This script is no longer maintained. Use the new Go version.
#
# Author: Junegunn Choi
# URL: https://github.com/junegunn/fzf
@@ -124,6 +126,7 @@ class FZF
@reverse = false
@prompt = '> '
@shr_mtx = Mutex.new
+ @expect = false
@print_query = false
argv =
@@ -198,6 +201,17 @@ class FZF
when '--no-print-query' then @print_query = false
when '-e', '--extended-exact' then @extended = :exact
when '+e', '--no-extended-exact' then @extended = nil
+ when '--expect'
+ argv.shift
+ @expect = true
+ when /^--expect=(.*)$/
+ @expect = true
+ when '--toggle-sort', '--tiebreak', '--color', '--bind', '--history', '--history-size'
+ argv.shift
+ when '--tac', '--no-tac', '--sync', '--no-sync', '--hscroll', '--no-hscroll',
+ '--inline-info', '--no-inline-info', '--read0', '--cycle', /^--bind=(.*)$/,
+ /^--color=(.*)$/, /^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/, /^--history(-max)?=(.*)$/
+ # XXX
else
usage 1, "illegal option: #{o}"
end
@@ -272,10 +286,12 @@ class FZF
if loaded
if @select1 && len == 1
puts @query if @print_query
+ puts if @expect
burp(empty ? matches.first : matches.first.first)
exit 0
elsif @exit0 && len == 0
puts @query if @print_query
+ puts if @expect
exit 0
end
end
@@ -619,7 +635,8 @@ class FZF
def start_reader
stream =
if @source.tty?
- if default_command = ENV['FZF_DEFAULT_COMMAND']
+ default_command = ENV['FZF_DEFAULT_COMMAND']
+ if default_command && !default_command.empty?
IO.popen(default_command)
elsif !`which find`.empty?
IO.popen("find * -path '*/\\.*' -prune -o -type f -print -o -type l -print 2> /dev/null")
@@ -1148,6 +1165,7 @@ class FZF
C.close_screen
q, selects = geta(:@query, :@selects)
@stdout.puts q if @print_query
+ @stdout.puts if @expect
if got
if selects.empty?
burp got
diff --git a/fzf-completion.zsh b/fzf-completion.zsh
deleted file mode 100644
index 5e2db28..0000000
--- a/fzf-completion.zsh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/zsh
-# ____ ____
-# / __/___ / __/
-# / /_/_ / / /_
-# / __/ / /_/ __/
-# /_/ /___/_/-completion.zsh
-#
-
-# TODO
diff --git a/fzf.gemspec b/fzf.gemspec
deleted file mode 100644
index 0a6c74d..0000000
--- a/fzf.gemspec
+++ /dev/null
@@ -1,17 +0,0 @@
-# coding: utf-8
-Gem::Specification.new do |spec|
- spec.name = 'fzf'
- spec.version = '0.8.4'
- spec.authors = ['Junegunn Choi']
- spec.email = ['junegunn.c@gmail.com']
- spec.description = %q{Fuzzy finder for your shell}
- spec.summary = %q{Fuzzy finder for your shell}
- spec.homepage = 'https://github.com/junegunn/fzf'
- spec.license = 'MIT'
-
- spec.bindir = '.'
- spec.files = %w[fzf.gemspec]
- spec.executables = 'fzf'
-
- spec.extensions += ['ext/mkrf_conf.rb']
-end
diff --git a/install b/install
index d0a016d..9203c98 100755
--- a/install
+++ b/install
@@ -1,62 +1,178 @@
#!/usr/bin/env bash
-cd `dirname $BASH_SOURCE`
-fzf_base=`pwd`
+[[ "$@" =~ --pre ]] && version=0.10.5 pre=1 ||
+ version=0.10.5 pre=0
-# ruby executable
-echo -n "Checking Ruby executable ... "
-ruby=`which ruby`
-if [ $? -ne 0 ]; then
- echo "ruby executable not found!"
- exit 1
-fi
+cd $(dirname $BASH_SOURCE)
+fzf_base=$(pwd)
-# System ruby is preferred
-system_ruby=/usr/bin/ruby
-if [ -x $system_ruby -a $system_ruby != "$ruby" ]; then
- $system_ruby --disable-gems -rcurses -e0 2> /dev/null
- [ $? -eq 0 ] && ruby=$system_ruby
-fi
+# If stdin is a tty, we are "interactive".
+[ -t 0 ] && interactive=yes
-echo "OK ($ruby)"
+ask() {
+ # non-interactive shell: wait for a linefeed
+ # interactive shell: continue after a single keypress
+ [ -n "$interactive" ] && read_n='-n 1' || read_n=
-# Curses-support
-echo -n "Checking Curses support ... "
-"$ruby" -rcurses -e0 2> /dev/null
-if [ $? -eq 0 ]; then
- echo "OK"
-else
- echo "Not found"
- echo "Installing 'curses' gem ... "
- if (( EUID )); then
- /usr/bin/env gem install curses --user-install
- else
- /usr/bin/env gem install curses
- fi
+ read -p "$1 ([y]/n) " $read_n -r
+ echo
+ [[ ! $REPLY =~ ^[Nn]$ ]]
+}
+
+check_binary() {
+ echo -n " - Checking fzf executable ... "
+ local output
+ output=$("$fzf_base"/bin/fzf --version 2>&1)
if [ $? -ne 0 ]; then
- echo
- echo "Failed to install 'curses' gem."
- if [[ $(uname -r) =~ 'ARCH' ]]; then
- echo "Make sure that base-devel package group is installed."
+ echo "Error: $output"
+ binary_error="Invalid binary"
+ elif [ "$version" != "$output" ]; then
+ echo "$output != $version"
+ binary_error="Invalid version"
+ else
+ echo "$output"
+ binary_error=""
+ return 0
+ fi
+ rm -f "$fzf_base"/bin/fzf
+ return 1
+}
+
+symlink() {
+ echo " - Creating symlink: bin/$1 -> bin/fzf"
+ (cd "$fzf_base"/bin &&
+ rm -f fzf &&
+ ln -sf $1 fzf)
+ if [ $? -ne 0 ]; then
+ binary_error="Failed to create symlink"
+ return 1
+ fi
+}
+
+download() {
+ echo "Downloading bin/fzf ..."
+ if [ $pre = 0 ]; then
+ if [ -x "$fzf_base"/bin/fzf ]; then
+ echo " - Already exists"
+ check_binary && return
+ elif [ -x "$fzf_base"/bin/$1 ]; then
+ symlink $1 && check_binary && return
fi
+ fi
+ mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin
+ if [ $? -ne 0 ]; then
+ binary_error="Failed to create bin directory"
+ return
+ fi
+
+ local url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz
+ if which curl > /dev/null; then
+ curl -fL $url | tar -xz
+ elif which wget > /dev/null; then
+ wget -O - $url | tar -xz
+ else
+ binary_error="curl or wget not found"
+ return
+ fi
+
+ if [ ! -f $1 ]; then
+ binary_error="Failed to download ${1}"
+ return
+ fi
+
+ chmod +x $1 && symlink $1 || return 1
+ if [[ $1 =~ linux_amd64$ ]]; then
+ check_binary || download $1-static
+ else
+ check_binary
+ fi
+}
+
+# Try to download binary executable
+archi=$(uname -sm)
+binary_available=1
+binary_error=""
+case "$archi" in
+ Darwin\ x86_64) download fzf-$version-darwin_amd64 ;;
+ Darwin\ i*86) download fzf-$version-darwin_386 ;;
+ Linux\ x86_64) download fzf-$version-linux_amd64 ;;
+ Linux\ i*86) download fzf-$version-linux_386 ;;
+ *) binary_available=0 binary_error=1 ;;
+esac
+
+cd "$fzf_base"
+if [ -n "$binary_error" ]; then
+ if [ $binary_available -eq 0 ]; then
+ echo "No prebuilt binary for $archi ... "
+ else
+ echo " - $binary_error !!!"
exit 1
fi
-fi
+ echo "Installing legacy Ruby version ..."
-# Ruby version
-echo -n "Checking Ruby version ... "
-"$ruby" -e 'exit RUBY_VERSION >= "1.9"'
-if [ $? -eq 0 ]; then
- echo ">= 1.9"
- "$ruby" --disable-gems -rcurses -e0 2> /dev/null
+ # ruby executable
+ echo -n "Checking Ruby executable ... "
+ ruby=`which ruby`
+ if [ $? -ne 0 ]; then
+ echo "ruby executable not found !!!"
+ exit 1
+ fi
+
+ # System ruby is preferred
+ system_ruby=/usr/bin/ruby
+ if [ -x $system_ruby -a $system_ruby != "$ruby" ]; then
+ $system_ruby --disable-gems -rcurses -e0 2> /dev/null
+ [ $? -eq 0 ] && ruby=$system_ruby
+ fi
+
+ echo "OK ($ruby)"
+
+ # Curses-support
+ echo -n "Checking Curses support ... "
+ "$ruby" -rcurses -e0 2> /dev/null
if [ $? -eq 0 ]; then
- fzf_cmd="$ruby --disable-gems $fzf_base/fzf"
+ echo "OK"
else
+ echo "Not found"
+ echo "Installing 'curses' gem ... "
+ if (( EUID )); then
+ /usr/bin/env gem install curses --user-install
+ else
+ /usr/bin/env gem install curses
+ fi
+ if [ $? -ne 0 ]; then
+ echo
+ echo "Failed to install 'curses' gem."
+ if [[ $(uname -r) =~ 'ARCH' ]]; then
+ echo "Make sure that base-devel package group is installed."
+ fi
+ exit 1
+ fi
+ fi
+
+ # Ruby version
+ echo -n "Checking Ruby version ... "
+ "$ruby" -e 'exit RUBY_VERSION >= "1.9"'
+ if [ $? -eq 0 ]; then
+ echo ">= 1.9"
+ "$ruby" --disable-gems -rcurses -e0 2> /dev/null
+ if [ $? -eq 0 ]; then
+ fzf_cmd="$ruby --disable-gems $fzf_base/fzf"
+ else
+ fzf_cmd="$ruby $fzf_base/fzf"
+ fi
+ else
+ echo "< 1.9"
fzf_cmd="$ruby $fzf_base/fzf"
fi
-else
- echo "< 1.9"
- fzf_cmd="$ruby $fzf_base/fzf"
+
+ # Create fzf script
+ echo -n "Creating wrapper script for fzf ... "
+ rm -f "$fzf_base"/bin/fzf
+ echo "#!/bin/sh" > "$fzf_base"/bin/fzf
+ echo "$fzf_cmd \"\$@\"" >> "$fzf_base"/bin/fzf
+ chmod +x "$fzf_base"/bin/fzf
+ echo "OK"
fi
if [[ $1 == "-y" ]] ; then
@@ -65,15 +181,11 @@ if [[ $1 == "-y" ]] ; then
key_bindings=0
else
# Auto-completion
- read -p "Do you want to add auto-completion support? ([y]/n) " -n 1 -r
- echo
- [[ ! $REPLY =~ ^[Nn]$ ]]
+ ask "Do you want to add auto-completion support?"
auto_completion=$?
# Key-bindings
- read -p "Do you want to add key bindings? ([y]/n) " -n 1 -r
- echo
- [[ ! $REPLY =~ ^[Nn]$ ]]
+ ask "Do you want to add key bindings?"
key_bindings=$?
fi
@@ -82,268 +194,62 @@ for shell in bash zsh; do
echo -n "Generate ~/.fzf.$shell ... "
src=~/.fzf.${shell}
- fzf_completion="[[ \$- =~ i ]] && source $fzf_base/fzf-completion.${shell}"
+ fzf_completion="[[ \$- =~ i ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null"
if [ $auto_completion -ne 0 ]; then
fzf_completion="# $fzf_completion"
fi
+ fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\""
+ if [ $key_bindings -ne 0 ]; then
+ fzf_key_bindings="# $fzf_key_bindings"
+ fi
+
cat > $src << EOF
-# Setup fzf function
-# ------------------
-unalias fzf 2> /dev/null
-fzf() {
- $fzf_cmd "\$@"
-}
-export -f fzf > /dev/null
+# Setup fzf
+# ---------
+if [[ ! "\$PATH" =~ "$fzf_base/bin" ]]; then
+ export PATH="\$PATH:$fzf_base/bin"
+fi
+
+# Man path
+# --------
+if [[ ! "\$MANPATH" =~ "$fzf_base/man" && -d "$fzf_base/man" ]]; then
+ export MANPATH="\$MANPATH:$fzf_base/man"
+fi
# Auto-completion
# ---------------
$fzf_completion
+# Key bindings
+# ------------
+$fzf_key_bindings
+
EOF
-
- if [ $key_bindings -eq 0 ]; then
- if [ $shell = bash ]; then
- cat >> $src << "EOFZF"
-# Key bindings
-# ------------
-__fsel() {
- command find * -path '*/\.*' -prune \
- -o -type f -print \
- -o -type d -print \
- -o -type l -print 2> /dev/null | fzf -m | while read item; do
- printf '%q ' "$item"
- done
- echo
-}
-
-if [[ $- =~ i ]]; then
-
-__fsel_tmux() {
- local height
- height=${FZF_TMUX_HEIGHT:-40%}
- if [[ $height =~ %$ ]]; then
- height="-p ${height%\%}"
- else
- height="-l $height"
- fi
- tmux split-window $height "bash -c 'source ~/.fzf.bash; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'"
-}
-
-__fcd() {
- local dir
- dir=$(command find -L ${1:-*} -path '*/\.*' -prune -o -type d -print 2> /dev/null | fzf +m) && printf 'cd %q' "$dir"
-}
-
-__use_tmux=0
-[ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ] && __use_tmux=1
-
-if [ -z "$(set -o | \grep '^vi.*on')" ]; then
- # Required to refresh the prompt after fzf
- bind '"\er": redraw-current-line'
-
- # CTRL-T - Paste the selected file path into the command line
- 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"'
- 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"'
- fi
-
- # CTRL-R - Paste the selected command from history into the command line
- bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s +m -n2..,.. | sed \"s/ *[0-9]* *//\")\e\C-e\er"'
-
- # ALT-C - cd into the selected directory
- bind '"\ec": " \C-e\C-u$(__fcd)\e\C-e\er\C-m"'
-else
- bind '"\C-x\C-e": shell-expand-line'
- bind '"\C-x\C-r": redraw-current-line'
-
- # CTRL-T - Paste the selected file path into the command line
- # - FIXME: Selected items are attached to the end regardless of cursor position
- if [ $__use_tmux -eq 1 ]; then
- bind '"\C-t": "\e$a \eddi$(__fsel_tmux)\C-x\C-e\e0P$xa"'
- else
- bind '"\C-t": "\e$a \eddi$(__fsel)\C-x\C-e\e0Px$a \C-x\C-r\exa "'
- fi
- bind -m vi-command '"\C-t": "i\C-t"'
-
- # CTRL-R - Paste the selected command from history into the command line
- bind '"\C-r": "\eddi$(HISTTIMEFORMAT= history | fzf +s +m -n2..,.. | sed \"s/ *[0-9]* *//\")\C-x\C-e\e$a\C-x\C-r"'
- bind -m vi-command '"\C-r": "i\C-r"'
-
- # ALT-C - cd into the selected directory
- bind '"\ec": "\eddi$(__fcd)\C-x\C-e\C-x\C-r\C-m"'
- bind -m vi-command '"\ec": "i\ec"'
-fi
-
-unset __use_tmux
-
-fi
-EOFZF
- else
- cat >> $src << "EOFZF"
-# Key bindings
-# ------------
-# CTRL-T - Paste the selected file path(s) into the command line
-__fsel() {
- set -o nonomatch
- command find * -path '*/\.*' -prune \
- -o -type f -print \
- -o -type d -print \
- -o -type l -print 2> /dev/null | fzf -m | while read item; do
- printf '%q ' "$item"
- done
- echo
-}
-
-if [[ $- =~ i ]]; then
-
-if [ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ]; then
- 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 "zsh -c 'source ~/.fzf.zsh; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'"
- }
-else
- fzf-file-widget() {
- LBUFFER="${LBUFFER}$(__fsel)"
- zle redisplay
- }
-fi
-zle -N fzf-file-widget
-bindkey '^T' fzf-file-widget
-
-# ALT-C - cd into the selected directory
-fzf-cd-widget() {
- cd "${$(set -o nonomatch; command find -L * -path '*/\.*' -prune \
- -o -type d -print 2> /dev/null | fzf):-.}"
- zle reset-prompt
-}
-zle -N fzf-cd-widget
-bindkey '\ec' fzf-cd-widget
-
-# CTRL-R - Paste the selected command from history into the command line
-fzf-history-widget() {
- LBUFFER=$(fc -l 1 | fzf +s +m -n2..,.. | sed "s/ *[0-9*]* *//")
- zle redisplay
-}
-zle -N fzf-history-widget
-bindkey '^R' fzf-history-widget
-
-fi
-EOFZF
- fi
- fi
-
echo "OK"
done
# fish
has_fish=0
-if [ -n "$(which fish)" ]; then
+if [ -n "$(which fish 2> /dev/null)" ]; then
has_fish=1
- echo -n "Generate ~/.config/fish/functions/fzf.fish ... "
+ echo -n "Update fish_user_paths ... "
+ fish << EOF
+ echo \$fish_user_paths | grep $fzf_base/bin > /dev/null
+ or set --universal fish_user_paths \$fish_user_paths $fzf_base/bin
+EOF
+ [ $? -eq 0 ] && echo "OK" || echo "Failed"
+
mkdir -p ~/.config/fish/functions
- cat > ~/.config/fish/functions/fzf.fish << EOFZF
-function fzf
- $fzf_cmd \$argv
-end
-EOFZF
- echo "OK"
+ if [ -e ~/.config/fish/functions/fzf.fish ]; then
+ echo -n "Remove unnecessary ~/.config/fish/functions/fzf.fish ... "
+ rm -f ~/.config/fish/functions/fzf.fish && echo "OK" || echo "Failed"
+ fi
if [ $key_bindings -eq 0 ]; then
- echo -n "Generate ~/.config/fish/functions/fzf_key_bindings.fish ... "
- cat > ~/.config/fish/functions/fzf_key_bindings.fish << "EOFZF"
-function fzf_key_bindings
- # Due to a bug of fish, we cannot use command substitution,
- # so we use temporary file instead
- if [ -z "$TMPDIR" ]
- set -g TMPDIR /tmp
- end
-
- function __fzf_list
- command find * -path '*/\.*' -prune \
- -o -type f -print \
- -o -type d -print \
- -o -type l -print 2> /dev/null
- end
-
- function __fzf_list_dir
- command find -L * -path '*/\.*' -prune -o -type d -print 2> /dev/null
- end
-
- function __fzf_escape
- while read item
- echo -n (echo -n "$item" | sed -E 's/([ "$~'\''([{<>})])/\\\\\\1/g')' '
- end
- end
-
- function __fzf_ctrl_t
- if [ -n "$TMUX_PANE" -a "$FZF_TMUX" != "0" ]
- tmux split-window (__fzf_tmux_height) "fish -c 'fzf_key_bindings; __fzf_ctrl_t_tmux \\$TMUX_PANE'"
- else
- __fzf_list | fzf -m > $TMPDIR/fzf.result
- and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape)
- commandline -f repaint
- rm -f $TMPDIR/fzf.result
- end
- end
-
- function __fzf_ctrl_t_tmux
- __fzf_list | fzf -m > $TMPDIR/fzf.result
- and tmux send-keys -t $argv[1] (cat $TMPDIR/fzf.result | __fzf_escape)
- rm -f $TMPDIR/fzf.result
- end
-
- function __fzf_reverse
- if which tac > /dev/null
- tac $argv
- else
- tail -r $argv
- end
- end
-
- function __fzf_ctrl_r
- history | __fzf_reverse | fzf +s +m > $TMPDIR/fzf.result
- and commandline (cat $TMPDIR/fzf.result)
- commandline -f repaint
- rm -f $TMPDIR/fzf.result
- end
-
- function __fzf_alt_c
- # Fish hangs if the command before pipe redirects (2> /dev/null)
- __fzf_list_dir | fzf +m > $TMPDIR/fzf.result
- [ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ]
- and cd (cat $TMPDIR/fzf.result)
- commandline -f repaint
- rm -f $TMPDIR/fzf.result
- end
-
- function __fzf_tmux_height
- if set -q FZF_TMUX_HEIGHT
- set height $FZF_TMUX_HEIGHT
- else
- set height 40%
- end
- if echo $height | \grep -q -E '%$'
- echo "-p "(echo $height | sed 's/%$//')
- else
- echo "-l $height"
- end
- set -e height
- end
-
- bind \ct '__fzf_ctrl_t'
- bind \cr '__fzf_ctrl_r'
- bind \ec '__fzf_alt_c'
-end
-EOFZF
- echo "OK"
+ echo -n "Symlink ~/.config/fish/functions/fzf_key_bindings.fish ... "
+ ln -sf $fzf_base/shell/key-bindings.fish \
+ ~/.config/fish/functions/fzf_key_bindings.fish && echo "OK" || echo "Failed"
fi
fi
@@ -359,6 +265,7 @@ append_line() {
if [ -n "$line" ]; then
echo " - Already exists: line #$line"
else
+ echo >> "$2"
echo "$1" >> "$2"
echo " + Added"
fi
@@ -373,12 +280,6 @@ done
if [ $key_bindings -eq 0 -a $has_fish -eq 1 ]; then
bind_file=~/.config/fish/functions/fish_user_key_bindings.fish
append_line "fzf_key_bindings" "$bind_file"
-
- echo ' * Due to a known bug of fish, you may have issues running fzf on fish.'
- echo ' * If that happens, try the following:'
- echo ' - Remove ~/.config/fish/functions/fzf.fish'
- echo ' - Place fzf executable in a directory included in $PATH'
- echo
fi
cat << EOF
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
new file mode 100644
index 0000000..4147371
--- /dev/null
+++ b/man/man1/fzf.1
@@ -0,0 +1,413 @@
+.ig
+The MIT License (MIT)
+
+Copyright (c) 2015 Junegunn Choi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+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
+THE SOFTWARE.
+..
+.TH fzf 1 "Sep 2015" "fzf 0.10.5" "fzf - a command-line fuzzy finder"
+
+.SH NAME
+fzf - a command-line fuzzy finder
+
+.SH SYNOPSIS
+fzf [options]
+
+.SH DESCRIPTION
+fzf is a general-purpose command-line fuzzy finder.
+
+.SH OPTIONS
+.SS Search mode
+.TP
+.B "-x, --extended"
+Extended-search mode
+.TP
+.B "-e, --extended-exact"
+Extended-search mode (exact match)
+.TP
+.B "-i"
+Case-insensitive match (default: smart-case match)
+.TP
+.B "+i"
+Case-sensitive match
+.TP
+.BI "-n, --nth=" "N[,..]"
+Comma-separated list of field index expressions for limiting search scope.
+See \fBFIELD INDEX EXPRESSION\fR for details.
+.TP
+.BI "--with-nth=" "N[,..]"
+Transform each item using index expressions within finder
+.TP
+.BI "-d, --delimiter=" "STR"
+Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style)
+.SS Search result
+.TP
+.B "+s, --no-sort"
+Do not sort the result
+.TP
+.B "--tac"
+Reverse the order of the input
+.RS
+e.g. \fBhistory | fzf --tac --no-sort\fR
+.RE
+.TP
+.BI "--tiebreak=" "CRI"
+Sort criterion to use when the scores are tied
+.br
+.R ""
+.br
+.BR length " Prefers item with shorter length"
+.br
+.BR begin " Prefers item with matched substring closer to the beginning"
+.br
+.BR end " Prefers item with matched substring closer to the end"
+.br
+.BR index " Prefers item that appeared earlier in the input stream"
+.br
+.SS Interface
+.TP
+.B "-m, --multi"
+Enable multi-select with tab/shift-tab
+.TP
+.B "--ansi"
+Enable processing of ANSI color codes
+.TP
+.B "--no-mouse"
+Disable mouse
+.TP
+.BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]"
+Color configuration. The name of the base color scheme is followed by custom
+color mappings. Ansi color code of -1 denotes terminal default
+foreground/background color.
+
+.RS
+e.g. \fBfzf --color=bg+:24\fR
+ \fBfzf --color=light,fg:232,bg:255,bg+:116,info:27\fR
+.RE
+
+.RS
+.B BASE SCHEME:
+ (default: dark on 256-color terminal, otherwise 16)
+
+ \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
+ \fBheader \fRHeader
+.RE
+.TP
+.B "--black"
+Use black background
+.TP
+.B "--reverse"
+Reverse orientation
+.TP
+.BI "--margin=" MARGIN
+Comma-separated expression for margins around the finder.
+.br
+.R ""
+.br
+.RS
+.BR TRBL " Same margin for top, right, bottom, and left"
+.br
+.BR TB,RL " Vertical, horizontal margin"
+.br
+.BR T,RL,B " Top, horizontal, bottom margin"
+.br
+.BR T,R,B,L " Top, right, bottom, left margin"
+.br
+.R ""
+.br
+Each part can be given in absolute number or in percentage relative to the
+terminal size with \fB%\fR suffix.
+.br
+.R ""
+.br
+e.g. \fBfzf --margin 10%\fR
+ \fBfzf --margin 1,5%\fR
+.RE
+.TP
+.B "--cycle"
+Enable cyclic scroll
+.TP
+.B "--no-hscroll"
+Disable horizontal scroll
+.TP
+.B "--inline-info"
+Display finder info inline with the query
+.TP
+.BI "--prompt=" "STR"
+Input prompt (default: '> ')
+.TP
+.BI "--toggle-sort=" "KEY"
+Key to toggle sort. For the list of the allowed key names, see \fB--bind\fR.
+.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 AVAILABLE KEYS:
+ \fIctrl-[a-z]\fR
+ \fIalt-[a-z]\fR
+ \fIf[1-4]\fR
+ \fIenter\fR (\fIreturn\fR)
+ \fIspace\fR
+ \fIbspace\fR (\fIbs\fR)
+ \fIalt-bspace\fR (\fIalt-bs\fR)
+ \fItab\fR
+ \fIbtab\fR (\fIshift-tab\fR)
+ \fIesc\fR
+ \fIdel\fR
+ \fIup\fR
+ \fIdown\fR
+ \fIleft\fR
+ \fIright\fR
+ \fIhome\fR
+ \fIend\fR
+ \fIpgup\fR (\fIpage-up\fR)
+ \fIpgdn\fR (\fIpage-down\fR)
+ \fIshift-left\fR
+ \fIshift-right\fR
+ or any single character
+.RE
+
+.RS
+ \fBACTION: DEFAULT BINDINGS:
+ \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
+ \fBaccept\fR \fIctrl-m (enter)\fR
+ \fBbackward-char\fR \fIctrl-b left\fR
+ \fBbackward-delete-char\fR \fIctrl-h bspace\fR
+ \fBbackward-kill-word\fR \fIalt-bs\fR
+ \fBbackward-word\fR \fIalt-b shift-left\fR
+ \fBbeginning-of-line\fR \fIctrl-a home\fR
+ \fBcancel\fR
+ \fBclear-screen\fR \fIctrl-l\fR
+ \fBdelete-char\fR \fIdel\fR
+ \fBdelete-char/eof\fR \fIctrl-d\fR
+ \fBdeselect-all\fR
+ \fBdown\fR \fIctrl-j ctrl-n down\fR
+ \fBend-of-line\fR \fIctrl-e end\fR
+ \fBexecute(...)\fR (see below for the details)
+ \fBforward-char\fR \fIctrl-f right\fR
+ \fBforward-word\fR \fIalt-f shift-right\fR
+ \fBignore\fR
+ \fBkill-line\fR
+ \fBkill-word\fR \fIalt-d\fR
+ \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
+ \fBpage-down\fR \fIpgdn\fR
+ \fBpage-up\fR \fIpgup\fR
+ \fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
+ \fBselect-all\fR
+ \fBtoggle\fR
+ \fBtoggle-all\fR
+ \fBtoggle-down\fR \fIctrl-i (tab)\fR
+ \fBtoggle-sort\fR (equivalent to \fB--toggle-sort\fR)
+ \fBtoggle-up\fR \fIbtab (shift-tab)\fR
+ \fBunix-line-discard\fR \fIctrl-u\fR
+ \fBunix-word-rubout\fR \fIctrl-w\fR
+ \fBup\fR \fIctrl-k ctrl-p up\fR
+ \fByank\fR \fIctrl-y\fR
+.RE
+
+.RS
+With \fBexecute(...)\fR action, you can execute arbitrary commands without
+leaving fzf. For example, you can turn fzf into a simple file browser by
+binding \fBenter\fR key to \fBless\fR command like follows.
+
+.RS
+\fBfzf --bind "enter:execute(less {})"\fR
+.RE
+
+\fB{}\fR is the placeholder for the double-quoted string of the current line.
+If the command contains parentheses, you can use any of the following
+alternative notations to avoid parse errors.
+
+ \fBexecute[...]\fR
+ \fBexecute~...~\fR
+ \fBexecute!...!\fR
+ \fBexecute@...@\fR
+ \fBexecute#...#\fR
+ \fBexecute$...$\fR
+ \fBexecute%...%\fR
+ \fBexecute^...^\fR
+ \fBexecute&...&\fR
+ \fBexecute*...*\fR
+ \fBexecute;...;\fR
+ \fBexecute/.../\fR
+ \fBexecute|...|\fR
+ \fBexecute:...\fR
+.RS
+This is the special form that frees you from parse errors as it does not expect
+the closing character. The catch is that it should be the last one in the
+comma-separated list.
+.RE
+.RE
+.TP
+.BI "--history=" "HISTORY_FILE"
+Load search history from the specified file and update the file on completion.
+When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically remapped to
+\fBnext-history\fR and \fBprevious-history\fR.
+.TP
+.BI "--history-size=" "N"
+Maximum number of entries in the history file (default: 1000). The file is
+automatically truncated when the number of the lines exceeds the value.
+.TP
+.BI "--header=" "STR"
+The given string will be printed as the sticky header. The lines are displayed
+in the given order from top to bottom regardless of \fB--reverse\fR option, and
+are not affected by \fB--with-nth\fR. ANSI color codes are processed even when
+\fB--ansi\fR is not set.
+.TP
+.BI "--header-lines=" "N"
+The first N lines of the input are treated as the sticky header. When
+\fB--with-nth\fR is set, the lines are transformed just like the other
+lines that follow.
+.SS Scripting
+.TP
+.BI "-q, --query=" "STR"
+Start the finder with the given query
+.TP
+.B "-1, --select-1"
+Automatically select the only match
+.TP
+.B "-0, --exit-0"
+Exit immediately when there's no match
+.TP
+.BI "-f, --filter=" "STR"
+Filter mode. Do not start interactive finder. When used with \fB--no-sort\fR,
+fzf becomes a fuzzy-version of grep.
+.TP
+.B "--print-query"
+Print query as the first line
+.TP
+.BI "--expect=" "KEY[,..]"
+Comma-separated list of keys that can be used to complete fzf in addition to
+the default enter key. When this option is set, fzf will print the name of the
+key pressed as the first line of its output (or as the second line if
+\fB--print-query\fR is also used). The line will be empty if fzf is completed
+with the default enter key.
+.RS
+e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR
+.RE
+.TP
+.B "--sync"
+Synchronous search for multi-staged filtering. If specified, fzf will launch
+ncurses finder only after the input stream is complete.
+.RS
+e.g. \fBfzf --multi | fzf --sync\fR
+.RE
+
+.SH ENVIRONMENT
+.TP
+.B FZF_DEFAULT_COMMAND
+Default command to use when input is tty
+.TP
+.B FZF_DEFAULT_OPTS
+Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR
+
+.SH EXIT STATUS
+.BR 0 " Normal exit"
+.br
+.BR 1 " Interrupted with \fBCTRL-C\fR or \fBESC\fR"
+
+.SH FIELD INDEX EXPRESSION
+
+A field index expression can be a non-zero integer or a range expression
+([BEGIN]..[END]). \fB--nth\fR and \fB--with-nth\fR take a comma-separated list
+of field index expressions.
+
+.SS Examples
+.BR 1 " The 1st field"
+.br
+.BR 2 " The 2nd field"
+.br
+.BR -1 " The last field"
+.br
+.BR -2 " The 2nd to last field"
+.br
+.BR 3..5 " From the 3rd field to the 5th field"
+.br
+.BR 2.. " From the 2nd field to the last field"
+.br
+.BR ..-3 " From the 1st field to the 3rd to the last field"
+.br
+.BR .. " All the fields"
+.br
+
+.SH EXTENDED SEARCH MODE
+
+With \fB-x\fR or \fB--extended\fR option, fzf will start in "extended-search
+mode". In this mode, you can specify multiple patterns delimited by spaces,
+such as: \fB'wild ^music .mp3$ sbtrkt !rmx\fR
+
+.SS Exact-match (quoted)
+A term that is prefixed by a single-quote character (\fB'\fR) is interpreted as
+an "exact-match" (or "non-fuzzy") term. fzf will search for the exact
+occurrences of the string.
+
+.SS Anchored-match
+A term can be prefixed by \fB^\fR, or suffixed by \fB$\fR to become an
+anchored-match term. Then fzf will search for the items that start with or end
+with the given string. An anchored-match term is also an exact-match term.
+
+.SS Negation
+If a term is prefixed by \fB!\fR, fzf will exclude the items that satisfy the
+term from the result.
+
+.SS Extended-exact mode
+If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with
+\fB'\fR) every word, start fzf with \fB-e\fR or \fB--extended-exact\fR option
+(instead of \fB-x\fR or \fB--extended\fR). Note that in \fB--extended-exact\fR
+mode, \fB'\fR-prefix "unquotes" the term.
+
+.SH AUTHOR
+Junegunn Choi (\fIjunegunn.c@gmail.com\fR)
+
+.SH SEE ALSO
+.B Project homepage:
+.RS
+.I https://github.com/junegunn/fzf
+.RE
+.br
+.R ""
+.br
+.B Extra Vim plugin:
+.RS
+.I https://github.com/junegunn/fzf.vim
+.RE
+
+.SH LICENSE
+MIT
diff --git a/plugin/fzf.vim b/plugin/fzf.vim
index db3c649..d9ced8e 100644
--- a/plugin/fzf.vim
+++ b/plugin/fzf.vim
@@ -1,4 +1,4 @@
-" Copyright (c) 2014 Junegunn Choi
+" Copyright (c) 2015 Junegunn Choi
"
" MIT License
"
@@ -21,30 +21,41 @@
" OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
" WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-let s:min_tmux_width = 10
-let s:min_tmux_height = 3
-let s:default_tmux_height = '40%'
-let s:launcher = 'xterm -e bash -ic %s'
-let s:fzf_rb = expand(':h:h').'/fzf'
+let s:default_height = '40%'
+let s:fzf_go = expand(':h:h').'/bin/fzf'
+let s:install = expand(':h:h').'/install'
+let s:installed = 0
+let s:fzf_tmux = expand(':h:h').'/bin/fzf-tmux'
let s:cpo_save = &cpo
set cpo&vim
function! s:fzf_exec()
if !exists('s:exec')
- call system('type fzf')
- if v:shell_error
- let s:exec = executable(s:fzf_rb) ? s:fzf_rb : ''
- else
+ if executable(s:fzf_go)
+ let s:exec = s:fzf_go
+ 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
+ echo 'Downloading fzf binary. Please wait ...'
+ echohl None
+ let s:installed = 1
+ call system(s:install.' --bin')
+ return s:fzf_exec()
+ else
+ redraw
+ throw 'fzf executable not found'
endif
- return s:fzf_exec()
- elseif empty(s:exec)
- unlet s:exec
- throw 'fzf executable not found'
- else
- return s:exec
endif
+ return s:exec
+endfunction
+
+function! s:tmux_not_zoomed()
+ return system('tmux list-panes -F "#F"') !~# 'Z'
endfunction
function! s:tmux_enabled()
@@ -53,15 +64,15 @@ function! s:tmux_enabled()
endif
if exists('s:tmux')
- return s:tmux
+ return s:tmux && s:tmux_not_zoomed()
endif
let s:tmux = 0
- if exists('$TMUX')
+ if exists('$TMUX') && executable(s:fzf_tmux)
let output = system('tmux -V')
let s:tmux = !v:shell_error && output >= 'tmux 1.7'
endif
- return s:tmux
+ return s:tmux && s:tmux_not_zoomed()
endfunction
function! s:shellesc(arg)
@@ -69,11 +80,35 @@ function! s:shellesc(arg)
endfunction
function! s:escape(path)
- return substitute(a:path, ' ', '\\ ', 'g')
+ return escape(a:path, ' %#''"\')
+endfunction
+
+" Upgrade legacy options
+function! s:upgrade(dict)
+ let copy = copy(a:dict)
+ if has_key(copy, 'tmux')
+ let copy.down = remove(copy, 'tmux')
+ endif
+ if has_key(copy, 'tmux_height')
+ let copy.down = remove(copy, 'tmux_height')
+ endif
+ if has_key(copy, 'tmux_width')
+ let copy.right = remove(copy, 'tmux_width')
+ endif
+ return copy
endfunction
function! fzf#run(...) abort
- let dict = exists('a:1') ? a:1 : {}
+try
+ let oshell = &shell
+ set shell=sh
+ if has('nvim') && bufexists('term://*:FZF')
+ echohl WarningMsg
+ echomsg 'FZF is already running!'
+ echohl None
+ return []
+ endif
+ let dict = exists('a:1') ? s:upgrade(a:1) : {}
let temps = { 'result': tempname() }
let optstr = get(dict, 'options', '')
try
@@ -97,26 +132,68 @@ function! fzf#run(...) abort
else
let prefix = ''
endif
- let command = prefix.fzf_exec.' '.optstr.' > '.temps.result
+ let tmux = !has('nvim') && s:tmux_enabled() && s:splittable(dict)
+ let command = prefix.(tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
- if s:tmux_enabled() && s:tmux_splittable(dict)
- return s:execute_tmux(dict, command, temps)
- else
- return s:execute(dict, command, temps)
- endif
+ try
+ if tmux
+ return s:execute_tmux(dict, command, temps)
+ elseif has('nvim')
+ return s:execute_term(dict, command, temps)
+ else
+ return s:execute(dict, command, temps)
+ endif
+ finally
+ call s:popd(dict)
+ endtry
+finally
+ let &shell = oshell
+endtry
endfunction
-function! s:tmux_splittable(dict)
- return
- \ min([&columns, get(a:dict, 'tmux_width', 0)]) >= s:min_tmux_width ||
- \ min([&lines, get(a:dict, 'tmux_height', get(a:dict, 'tmux', 0))]) >= s:min_tmux_height
+function! s:present(dict, ...)
+ for key in a:000
+ if !empty(get(a:dict, key, ''))
+ return 1
+ endif
+ endfor
+ return 0
+endfunction
+
+function! s:fzf_tmux(dict)
+ let size = ''
+ for o in ['up', 'down', 'left', 'right']
+ if s:present(a:dict, o)
+ let spec = a:dict[o]
+ if (o == 'up' || o == 'down') && spec[0] == '~'
+ let size = '-'.o[0].s:calc_size(&lines, spec[1:], a:dict)
+ else
+ " Legacy boolean option
+ let size = '-'.o[0].(spec == 1 ? '' : spec)
+ endif
+ break
+ endif
+ endfor
+ return printf('LINES=%d COLUMNS=%d %s %s %s --',
+ \ &lines, &columns, s:fzf_tmux, size, (has_key(a:dict, 'source') ? '' : '-'))
+endfunction
+
+function! s:splittable(dict)
+ return s:present(a:dict, 'up', 'down', 'left', 'right')
endfunction
function! s:pushd(dict)
- if !empty(get(a:dict, 'dir', ''))
- let a:dict.prev_dir = getcwd()
+ if s:present(a:dict, 'dir')
+ let cwd = getcwd()
+ if get(a:dict, 'prev_dir', '') ==# cwd
+ return 1
+ endif
+ let a:dict.prev_dir = cwd
execute 'chdir '.s:escape(a:dict.dir)
+ let a:dict.dir = getcwd()
+ return 1
endif
+ return 0
endfunction
function! s:popd(dict)
@@ -125,124 +202,218 @@ function! s:popd(dict)
endif
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)
call s:pushd(a:dict)
- silent !clear
+ silent! !clear 2> /dev/null
+ let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#')
if has('gui_running')
- let launcher = get(a:dict, 'launcher', get(g:, 'fzf_launcher', s:launcher))
- let command = printf(launcher, "'".substitute(a:command, "'", "'\"'\"'", 'g')."'")
+ let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher)))
+ let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher
+ let command = printf(fmt, "'".substitute(escaped, "'", "'\"'\"'", 'g')."'")
else
- let command = a:command
+ let command = escaped
endif
execute 'silent !'.command
redraw!
if v:shell_error
- " Do not print error message on exit status 1
- if v:shell_error > 1
+ " Do not print error message on exit status 1 (no match) or 130 (interrupt)
+ if v:shell_error == 2
echohl ErrorMsg
echo 'Error running ' . command
+ echohl None
endif
return []
else
- return s:callback(a:dict, a:temps, 0)
- endif
-endfunction
-
-function! s:env_var(name)
- if exists('$'.a:name)
- return a:name . "='". substitute(expand('$'.a:name), "'", "'\\\\''", 'g') . "' "
- else
- return ''
+ return s:callback(a:dict, a:temps)
endif
endfunction
function! s:execute_tmux(dict, command, temps)
- let command = s:env_var('FZF_DEFAULT_OPTS').s:env_var('FZF_DEFAULT_COMMAND').a:command
- if !empty(get(a:dict, 'dir', ''))
+ let command = a:command
+ if s:pushd(a:dict)
+ " -c '#{pane_current_path}' is only available on tmux 1.9 or above
let command = 'cd '.s:escape(a:dict.dir).' && '.command
endif
- let splitopt = '-v'
- if has_key(a:dict, 'tmux_width')
- let splitopt = '-h'
- let size = a:dict.tmux_width
- else
- let size = get(a:dict, 'tmux_height', get(a:dict, 'tmux'))
- endif
-
- if type(size) == 1 && size =~ '%$'
- let sizeopt = '-p '.size[0:-2]
- else
- let sizeopt = '-l '.size
- endif
-
- let s:pane = substitute(
- \ system(
- \ printf(
- \ 'tmux split-window %s %s -P -F "#{pane_id}" %s',
- \ splitopt, sizeopt, s:shellesc(command))), '\n', '', 'g')
- let s:dict = a:dict
- let s:temps = a:temps
-
- augroup fzf_tmux
- autocmd!
- autocmd VimResized * nested call s:tmux_check()
- augroup END
+ call system(command)
+ redraw!
+ return s:callback(a:dict, a:temps)
endfunction
-function! s:tmux_check()
- let panes = split(system('tmux list-panes -a -F "#{pane_id}"'), '\n')
-
- if index(panes, s:pane) < 0
- augroup fzf_tmux
- autocmd!
- augroup END
-
- call s:callback(s:dict, s:temps, 1)
- redraw
+function! s:calc_size(max, val, dict)
+ if a:val =~ '%$'
+ let size = a:max * str2nr(a:val[:-2]) / 100
+ else
+ let size = min([a:max, str2nr(a:val)])
endif
+
+ let srcsz = -1
+ if type(get(a:dict, 'source', 0)) == type([])
+ let srcsz = len(a:dict.source)
+ endif
+
+ let opts = get(a:dict, 'options', '').$FZF_DEFAULT_OPTS
+ let margin = stridx(opts, '--inline-info') > stridx(opts, '--no-inline-info') ? 1 : 2
+ return srcsz >= 0 ? min([srcsz + margin, size]) : size
endfunction
-function! s:callback(dict, temps, cd)
+function! s:getpos()
+ return {'tab': tabpagenr(), 'win': winnr(), 'cnt': winnr('$')}
+endfunction
+
+function! s:split(dict)
+ let directions = {
+ \ 'up': ['topleft', 'resize', &lines],
+ \ 'down': ['botright', 'resize', &lines],
+ \ 'left': ['vertical topleft', 'vertical resize', &columns],
+ \ 'right': ['vertical botright', 'vertical resize', &columns] }
+ let s:ppos = s:getpos()
+ try
+ for [dir, triple] in items(directions)
+ let val = get(a:dict, dir, '')
+ if !empty(val)
+ let [cmd, resz, max] = triple
+ if (dir == 'up' || dir == 'down') && val[0] == '~'
+ let sz = s:calc_size(max, val[1:], a:dict)
+ else
+ let sz = s:calc_size(max, val, {})
+ endif
+ execute cmd sz.'new'
+ execute resz sz
+ return
+ endif
+ endfor
+ if s:present(a:dict, 'window')
+ execute a:dict.window
+ else
+ tabnew
+ endif
+ finally
+ setlocal winfixwidth winfixheight buftype=nofile bufhidden=wipe nobuflisted
+ endtry
+endfunction
+
+function! s:execute_term(dict, command, temps)
+ call s:split(a:dict)
+ call s:pushd(a:dict)
+
+ let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps, 'name': 'FZF' }
+ function! fzf.on_exit(id, code)
+ let pos = s:getpos()
+ let inplace = pos == s:ppos " {'window': 'enew'}
+ if !inplace
+ if bufnr('') == self.buf
+ " We use close instead of bd! since Vim does not close the split when
+ " there's no other listed buffer (nvim +'set nobuflisted')
+ close
+ endif
+ if pos.tab == s:ppos.tab
+ wincmd p
+ endif
+ endif
+ call s:pushd(self.dict)
+ try
+ redraw!
+ call s:callback(self.dict, self.temps)
+
+ if inplace && bufnr('') == self.buf
+ execute "normal! \"
+ " No other listed buffer
+ if bufnr('') == self.buf
+ bd!
+ endif
+ endif
+ finally
+ call s:popd(self.dict)
+ endtry
+ endfunction
+
+ call termopen(a:command, fzf)
+ startinsert
+ return []
+endfunction
+
+function! s:callback(dict, temps)
+try
if !filereadable(a:temps.result)
let lines = []
else
- if a:cd | call s:pushd(a:dict) | endif
-
let lines = readfile(a:temps.result)
if has_key(a:dict, 'sink')
for line in lines
if type(a:dict.sink) == 2
call a:dict.sink(line)
else
- execute a:dict.sink.' '.s:escape(line)
+ execute a:dict.sink s:escape(line)
endif
endfor
endif
+ if has_key(a:dict, 'sink*')
+ call a:dict['sink*'](lines)
+ endif
endif
for tf in values(a:temps)
silent! call delete(tf)
endfor
- call s:popd(a:dict)
-
return lines
+catch
+ if stridx(v:exception, ':E325:') < 0
+ echoerr v:exception
+ endif
+endtry
+endfunction
+
+let s:default_action = {
+ \ 'ctrl-m': 'e',
+ \ 'ctrl-t': 'tab split',
+ \ 'ctrl-x': 'split',
+ \ 'ctrl-v': 'vsplit' }
+
+function! s:cmd_callback(lines) abort
+ if empty(a:lines)
+ return
+ endif
+ let key = remove(a:lines, 0)
+ let cmd = get(s:action, key, 'e')
+ try
+ let autochdir = &autochdir
+ set noautochdir
+ for item in a:lines
+ execute cmd s:escape(item)
+ endfor
+ finally
+ let &autochdir = autochdir
+ endtry
endfunction
function! s:cmd(bang, ...) abort
- let args = copy(a:000)
+ let s:action = get(g:, 'fzf_action', s:default_action)
+ let args = extend(['--expect='.join(keys(s:action), ',')], a:000)
let opts = {}
if len(args) > 0 && isdirectory(expand(args[-1]))
- let opts.dir = remove(args, -1)
+ let opts.dir = substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g')
endif
if !a:bang
- let opts.tmux = get(g:, 'fzf_tmux_height', s:default_tmux_height)
+ let opts.down = get(g:, 'fzf_height', get(g:, 'fzf_tmux_height', s:default_height))
endif
- call fzf#run(extend({ 'sink': 'e', 'options': join(args) }, opts))
+ call fzf#run(extend({'options': join(args), 'sink*': function('cmd_callback')}, opts))
endfunction
-command! -nargs=* -complete=dir -bang FZF call s:cmd('' == '!', )
+command! -nargs=* -complete=dir -bang FZF call s:cmd(0, )
let &cpo = s:cpo_save
unlet s:cpo_save
diff --git a/fzf-completion.bash b/shell/completion.bash
similarity index 71%
rename from fzf-completion.bash
rename to shell/completion.bash
index 61797b1..d7fdf26 100644
--- a/fzf-completion.bash
+++ b/shell/completion.bash
@@ -5,6 +5,8 @@
# / __/ / /_/ __/
# /_/ /___/_/-completion.bash
#
+# - $FZF_TMUX (default: 1)
+# - $FZF_TMUX_HEIGHT (default: '40%')
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
@@ -24,23 +26,44 @@ _fzf_opts_completion() {
-i +i
-n --nth
-d --delimiter
- -s --sort +s
+ +s --no-sort
+ --tac
+ --tiebreak
+ --bind
-m --multi
--no-mouse
- +c --no-color
- +2 --no-256
+ --color
--black
--reverse
+ --no-hscroll
+ --inline-info
--prompt
-q --query
-1 --select-1
-0 --exit-0
-f --filter
- --print-query"
+ --print-query
+ --expect
+ --toggle-sort
+ --sync
+ --cycle
+ --history
+ --history-size
+ --header
+ --header-lines
+ --margin"
case "${prev}" in
- --sort|-s)
- COMPREPLY=( $(compgen -W "$(seq 2000 1000 10000)" -- ${cur}) )
+ --tiebreak)
+ COMPREPLY=( $(compgen -W "length begin end index" -- ${cur}) )
+ return 0
+ ;;
+ --color)
+ COMPREPLY=( $(compgen -W "dark light 16 bw" -- ${cur}) )
+ return 0
+ ;;
+ --history)
+ COMPREPLY=()
return 0
;;
esac
@@ -54,9 +77,10 @@ _fzf_opts_completion() {
}
_fzf_handle_dynamic_completion() {
- local cmd orig ret
+ local cmd orig ret orig_cmd
cmd="$1"
shift
+ orig_cmd="$1"
orig=$(eval "echo \$_fzf_orig_completion_$cmd")
if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then
@@ -64,17 +88,18 @@ _fzf_handle_dynamic_completion() {
elif [ -n "$_fzf_completion_loader" ]; then
_completion_loader "$@"
ret=$?
- eval $(complete | \grep "\-F.* $cmd$" | _fzf_orig_completion_filter)
+ eval $(complete | \grep "\-F.* $orig_cmd$" | _fzf_orig_completion_filter)
source $BASH_SOURCE
return $ret
fi
}
_fzf_path_completion() {
- local cur base dir leftover matches trigger cmd
+ local cur base dir leftover matches trigger cmd 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')
COMPREPLY=()
- trigger=${FZF_COMPLETION_TRIGGER:-**}
+ trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}"
if [[ ${cur} == *"$trigger" ]]; then
base=${cur:0:${#cur}-${#trigger}}
@@ -87,8 +112,8 @@ _fzf_path_completion() {
leftover=${leftover/#\/}
[ "$dir" = './' ] && dir=''
tput sc
- matches=$(find -L "$dir"* $1 2> /dev/null | fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read item; do
- printf '%q ' "$item"
+ matches=$(find -L "$dir"* $1 2> /dev/null | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read item; do
+ printf "%q$3 " "$item"
done)
matches=${matches% }
if [ -n "$matches" ]; then
@@ -103,6 +128,7 @@ _fzf_path_completion() {
[[ "$dir" =~ /$ ]] || dir="$dir"/
done
else
+ shift
shift
shift
_fzf_handle_dynamic_completion "$cmd" "$@"
@@ -110,16 +136,17 @@ _fzf_path_completion() {
}
_fzf_list_completion() {
- local cur selected trigger cmd src
+ local cur selected trigger cmd src fzf
+ [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
read -r src
cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g')
- trigger=${FZF_COMPLETION_TRIGGER:-**}
+ trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}"
if [[ ${cur} == *"$trigger" ]]; then
cur=${cur:0:${#cur}-${#trigger}}
tput sc
- selected=$(eval "$src | fzf $FZF_COMPLETION_OPTS $1 -q '$cur'" | tr '\n' ' ')
+ selected=$(eval "$src | $fzf $FZF_COMPLETION_OPTS $1 -q '$cur'" | tr '\n' ' ')
selected=${selected% }
tput rc
@@ -136,27 +163,28 @@ _fzf_list_completion() {
_fzf_all_completion() {
_fzf_path_completion \
"-name .git -prune -o -name .svn -prune -o -type d -print -o -type f -print -o -type l -print" \
- "-m" "$@"
+ "-m" "" "$@"
}
_fzf_file_completion() {
_fzf_path_completion \
"-name .git -prune -o -name .svn -prune -o -type f -print -o -type l -print" \
- "-m" "$@"
+ "-m" "" "$@"
}
_fzf_dir_completion() {
_fzf_path_completion \
"-name .git -prune -o -name .svn -prune -o -type d -print" \
- "" "$@"
+ "" "/" "$@"
}
_fzf_kill_completion() {
[ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1
- local selected
+ local selected fzf
+ [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
tput sc
- selected=$(ps -ef | sed 1d | fzf -m $FZF_COMPLETION_OPTS | awk '{print $2}' | tr '\n' ' ')
+ selected=$(ps -ef | sed 1d | $fzf -m $FZF_COMPLETION_OPTS | awk '{print $2}' | tr '\n' ' ')
tput rc
if [ -n "$selected" ]; then
@@ -167,13 +195,13 @@ _fzf_kill_completion() {
_fzf_telnet_completion() {
_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
}
_fzf_ssh_completion() {
_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
}
@@ -190,7 +218,7 @@ EOF
}
# fzf options
-complete -F _fzf_opts_completion fzf
+complete -o default -F _fzf_opts_completion fzf
d_cmds="cd pushd rmdir"
f_cmds="
@@ -206,11 +234,11 @@ a_cmds="
x_cmds="kill ssh telnet unset unalias export"
# 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 :(
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)
- export _fzf_completion_loaded=0.8.6-1
+ export _fzf_completion_loaded=0.9.12
fi
if type _completion_loader > /dev/null 2>&1; then
@@ -219,7 +247,7 @@ fi
# Directory
for cmd in $d_cmds; do
- complete -F _fzf_dir_completion -o default -o bashdefault $cmd
+ complete -F _fzf_dir_completion -o nospace -o plusdirs $cmd
done
# File
diff --git a/shell/completion.zsh b/shell/completion.zsh
new file mode 100644
index 0000000..f45c538
--- /dev/null
+++ b/shell/completion.zsh
@@ -0,0 +1,163 @@
+#!/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 sws
+ if setopt | grep shwordsplit > /dev/null; then
+ sws=1
+ unsetopt shwordsplit
+ fi
+
+ # 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
+ [ -n "$sws" ] && setopt shwordsplit
+}
+
+[ -z "$fzf_default_completion" ] &&
+ fzf_default_completion=$(bindkey '^I' | grep -v undefined-key | awk '{print $2}')
+
+zle -N fzf-completion
+bindkey '^I' fzf-completion
+
diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash
new file mode 100644
index 0000000..0bfd50d
--- /dev/null
+++ b/shell/key-bindings.bash
@@ -0,0 +1,96 @@
+# Key bindings
+# ------------
+__fzf_select__() {
+ local cmd="${FZF_CTRL_T_COMMAND:-"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-"}"
+ eval "$cmd" | fzf -m | while read item; do
+ printf '%q ' "$item"
+ done
+ echo
+}
+
+if [[ $- =~ i ]]; then
+
+__fzfcmd() {
+ [ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
+}
+
+__fzf_select_tmux__() {
+ 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"); FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") bash -c 'source ~/.fzf.bash; tmux send-keys -t $TMUX_PANE \"\$(__fzf_select__)\"'"
+}
+
+__fzf_cd__() {
+ local dir
+ dir=$(command \find -L ${1:-.} \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
+ -o -type d -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) +m) && printf 'cd %q' "$dir"
+}
+
+__fzf_history__() (
+ local line
+ shopt -u nocaseglob nocasematch
+ line=$(
+ HISTTIMEFORMAT= history |
+ $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r |
+ \grep '^ *[0-9]') &&
+ if [[ $- =~ H ]]; then
+ sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line"
+ else
+ sed 's/^ *\([0-9]*\)\** *//' <<< "$line"
+ fi
+)
+
+__use_tmux=0
+[ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ] && __use_tmux=1
+
+if [ -z "$(set -o | \grep '^vi.*on')" ]; then
+ # Required to refresh the prompt after fzf
+ bind '"\er": redraw-current-line'
+ bind '"\e^": history-expand-line'
+
+ # CTRL-T - Paste the selected file path into the command line
+ if [ $__use_tmux -eq 1 ]; then
+ 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
+ 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
+
+ # CTRL-R - Paste the selected command from history into the command line
+ bind '"\C-r": " \C-e\C-u$(__fzf_history__)\e\C-e\e^\er"'
+
+ # ALT-C - cd into the selected directory
+ bind '"\ec": " \C-e\C-u$(__fzf_cd__)\e\C-e\er\C-m"'
+else
+ bind '"\C-x\C-e": shell-expand-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
+ # - FIXME: Selected items are attached to the end regardless of cursor position
+ if [ $__use_tmux -eq 1 ]; then
+ bind '"\C-t": "\e$a \eddi$(__fzf_select_tmux__)\C-x\C-e\e0P$xa"'
+ else
+ bind '"\C-t": "\e$a \eddi$(__fzf_select__)\C-x\C-e\e0Px$a \C-x\C-r\exa "'
+ fi
+ bind -m vi-command '"\C-t": "i\C-t"'
+
+ # CTRL-R - Paste the selected command from history into the command line
+ 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"'
+
+ # ALT-C - cd into the selected directory
+ bind '"\ec": "\eddi$(__fzf_cd__)\C-x\C-e\C-x\C-r\C-m"'
+ bind -m vi-command '"\ec": "i\ec"'
+fi
+
+unset __use_tmux
+
+fi
diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish
new file mode 100644
index 0000000..a08df3d
--- /dev/null
+++ b/shell/key-bindings.fish
@@ -0,0 +1,69 @@
+# Key bindings
+# ------------
+function fzf_key_bindings
+ # Due to a bug of fish, we cannot use command substitution,
+ # so we use temporary file instead
+ if [ -z "$TMPDIR" ]
+ set -g TMPDIR /tmp
+ end
+
+ function __fzf_escape
+ while read item
+ echo -n (echo -n "$item" | sed -E 's/([ "$~'\''([{<>})])/\\\\\\1/g')' '
+ end
+ end
+
+ function __fzf_ctrl_t
+ set -q FZF_CTRL_T_COMMAND; or set -l FZF_CTRL_T_COMMAND "
+ 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-"
+ eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m > $TMPDIR/fzf.result"
+ and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape)
+ commandline -f repaint
+ rm -f $TMPDIR/fzf.result
+ end
+
+ function __fzf_ctrl_r
+ history | eval (__fzfcmd) +s +m --tiebreak=index --toggle-sort=ctrl-r > $TMPDIR/fzf.result
+ and commandline (cat $TMPDIR/fzf.result)
+ commandline -f repaint
+ rm -f $TMPDIR/fzf.result
+ end
+
+ function __fzf_alt_c
+ # Fish hangs if the command before pipe redirects (2> /dev/null)
+ command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) \
+ -prune -o -type d -print 2> /dev/null | sed 1d | cut -b3- | eval (__fzfcmd) +m > $TMPDIR/fzf.result
+ [ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ]
+ and cd (cat $TMPDIR/fzf.result)
+ commandline -f repaint
+ rm -f $TMPDIR/fzf.result
+ end
+
+ function __fzfcmd
+ set -q FZF_TMUX; or set FZF_TMUX 1
+
+ if [ $FZF_TMUX -eq 1 ]
+ if set -q FZF_TMUX_HEIGHT
+ echo "fzf-tmux -d$FZF_TMUX_HEIGHT"
+ else
+ echo "fzf-tmux -d40%"
+ end
+ else
+ echo "fzf"
+ end
+ end
+
+ bind \ct '__fzf_ctrl_t'
+ bind \cr '__fzf_ctrl_r'
+ 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
+
diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh
new file mode 100644
index 0000000..735aa50
--- /dev/null
+++ b/shell/key-bindings.zsh
@@ -0,0 +1,52 @@
+# Key bindings
+# ------------
+if [[ $- =~ i ]]; then
+
+# CTRL-T - Paste the selected file path(s) into the command line
+__fsel() {
+ local cmd="${FZF_CTRL_T_COMMAND:-"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-"}"
+ eval "$cmd" | $(__fzfcmd) -m | while read item; do
+ printf '%q ' "$item"
+ done
+ echo
+}
+
+__fzfcmd() {
+ [ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
+}
+
+fzf-file-widget() {
+ LBUFFER="${LBUFFER}$(__fsel)"
+ zle redisplay
+}
+zle -N fzf-file-widget
+bindkey '^T' fzf-file-widget
+
+# ALT-C - cd into the selected directory
+fzf-cd-widget() {
+ cd "${$(command \find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
+ -o -type d -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) +m):-.}"
+ zle reset-prompt
+}
+zle -N fzf-cd-widget
+bindkey '\ec' fzf-cd-widget
+
+# CTRL-R - Paste the selected command from history into the command line
+fzf-history-widget() {
+ local selected restore_no_bang_hist
+ if selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER") ); then
+ num=$selected[1]
+ if [ -n "$num" ]; then
+ zle vi-fetch-history -n $num
+ fi
+ fi
+ zle redisplay
+}
+zle -N fzf-history-widget
+bindkey '^R' fzf-history-widget
+
+fi
+
diff --git a/src/Dockerfile.arch b/src/Dockerfile.arch
new file mode 100644
index 0000000..d014651
--- /dev/null
+++ b/src/Dockerfile.arch
@@ -0,0 +1,28 @@
+FROM base/archlinux:2014.07.03
+MAINTAINER Junegunn Choi
+
+# apt-get
+RUN pacman-key --populate archlinux && pacman-key --refresh-keys
+RUN pacman-db-upgrade && pacman -Syu --noconfirm base-devel git
+
+# Install Go 1.4
+RUN cd / && curl \
+ https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
+ tar -xz && mv go go1.4
+
+ENV GOPATH /go
+ENV GOROOT /go1.4
+ENV PATH /go1.4/bin:$PATH
+
+# For i386 build
+RUN echo '[multilib]' >> /etc/pacman.conf && \
+ echo 'Include = /etc/pacman.d/mirrorlist' >> /etc/pacman.conf && \
+ pacman-db-upgrade && yes | pacman -Sy gcc-multilib lib32-ncurses && \
+ cd $GOROOT/src && GOARCH=386 ./make.bash
+
+# Volume
+VOLUME /go
+
+# Default CMD
+CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash
+
diff --git a/src/Dockerfile.centos b/src/Dockerfile.centos
new file mode 100644
index 0000000..c03f43a
--- /dev/null
+++ b/src/Dockerfile.centos
@@ -0,0 +1,21 @@
+FROM centos:centos7
+MAINTAINER Junegunn Choi
+
+# yum
+RUN yum install -y git gcc make tar ncurses-devel
+
+# Install Go 1.4
+RUN cd / && curl \
+ https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
+ tar -xz && mv go go1.4
+
+ENV GOPATH /go
+ENV GOROOT /go1.4
+ENV PATH /go1.4/bin:$PATH
+
+# Volume
+VOLUME /go
+
+# Default CMD
+CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash
+
diff --git a/src/Dockerfile.ubuntu b/src/Dockerfile.ubuntu
new file mode 100644
index 0000000..d9de120
--- /dev/null
+++ b/src/Dockerfile.ubuntu
@@ -0,0 +1,26 @@
+FROM ubuntu:14.04
+MAINTAINER Junegunn Choi
+
+# apt-get
+RUN apt-get update && apt-get -y upgrade && \
+ apt-get install -y --force-yes git curl build-essential libncurses-dev libgpm-dev
+
+# Install Go 1.4
+RUN cd / && curl \
+ https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
+ tar -xz && mv go go1.4
+
+ENV GOPATH /go
+ENV GOROOT /go1.4
+ENV PATH /go1.4/bin:$PATH
+
+# For i386 build
+RUN apt-get install -y lib32ncurses5-dev && \
+ cd $GOROOT/src && GOARCH=386 ./make.bash
+
+# Volume
+VOLUME /go
+
+# Default CMD
+CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash
+
diff --git a/src/LICENSE b/src/LICENSE
new file mode 100644
index 0000000..fe4c31a
--- /dev/null
+++ b/src/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Junegunn Choi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+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
+THE SOFTWARE.
diff --git a/src/Makefile b/src/Makefile
new file mode 100644
index 0000000..3304f57
--- /dev/null
+++ b/src/Makefile
@@ -0,0 +1,97 @@
+ifndef GOPATH
+$(error GOPATH is undefined)
+endif
+
+UNAME_S := $(shell uname -s)
+ifeq ($(UNAME_S),Darwin)
+ GOOS := darwin
+ LDFLAGS :=
+ifdef STATIC
+$(error Static linking not possible on OS X)
+endif
+else ifeq ($(UNAME_S),Linux)
+ GOOS := linux
+ifdef STATIC
+ SUFFIX := -static
+ LDFLAGS := --ldflags '-extldflags "-static -ltinfo -lgpm"'
+else
+ SUFFIX :=
+ LDFLAGS :=
+endif
+endif
+
+ifneq ($(shell uname -m),x86_64)
+$(error "Build on $(UNAME_M) is not supported, yet.")
+endif
+
+SOURCES := $(wildcard *.go */*.go)
+BINDIR := ../bin
+
+BINARY32 := fzf-$(GOOS)_386$(SUFFIX)
+BINARY64 := fzf-$(GOOS)_amd64$(SUFFIX)
+VERSION = $(shell fzf/$(BINARY64) --version)
+RELEASE32 = fzf-$(VERSION)-$(GOOS)_386$(SUFFIX)
+RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64$(SUFFIX)
+
+all: release
+
+release: build
+ -cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32)
+ cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \
+ rm -f $(RELEASE32) $(RELEASE64)
+
+ifndef STATIC
+build: test fzf/$(BINARY32) fzf/$(BINARY64)
+else
+build: test fzf/$(BINARY64)
+endif
+
+test:
+ go get
+ go test -v ./...
+
+install: $(BINDIR)/fzf
+
+uninstall:
+ rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY64)
+
+clean:
+ cd fzf && rm -f fzf-*
+
+fzf/$(BINARY32): $(SOURCES)
+ cd fzf && GOARCH=386 CGO_ENABLED=1 go build -o $(BINARY32)
+
+fzf/$(BINARY64): $(SOURCES)
+ cd fzf && go build $(LDFLAGS) -o $(BINARY64)
+
+$(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR)
+ cp -f fzf/$(BINARY64) $(BINDIR)
+ cd $(BINDIR) && ln -sf $(BINARY64) fzf
+
+$(BINDIR):
+ mkdir -p $@
+
+docker-arch:
+ docker build -t junegunn/arch-sandbox - < Dockerfile.arch
+
+docker-ubuntu:
+ docker build -t junegunn/ubuntu-sandbox - < Dockerfile.ubuntu
+
+arch: docker-arch
+ docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \
+ sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash'
+
+ubuntu: docker-ubuntu
+ docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \
+ sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash'
+
+linux: docker-arch
+ docker run -i -t -v $(GOPATH):/go junegunn/arch-sandbox \
+ /bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make'
+
+linux-static: docker-ubuntu
+ docker run -i -t -v $(GOPATH):/go junegunn/ubuntu-sandbox \
+ /bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make STATIC=1'
+
+.PHONY: all build release test install uninstall clean docker \
+ linux linux-static arch ubuntu docker-arch docker-ubuntu
diff --git a/src/README.md b/src/README.md
new file mode 100644
index 0000000..18ccec3
--- /dev/null
+++ b/src/README.md
@@ -0,0 +1,121 @@
+fzf in Go
+=========
+
+
+
+This directory contains the source code for the new fzf implementation in
+[Go][go].
+
+Upgrade from Ruby version
+-------------------------
+
+The install script has been updated to download the right binary for your
+system. If you already have installed fzf, simply git-pull the repository and
+rerun the install script.
+
+```sh
+cd ~/.fzf
+git pull
+./install
+```
+
+Otherwise, follow [the instruction][install] as before. You can also install
+fzf using Homebrew if you prefer that way.
+
+Motivations
+-----------
+
+### No Ruby dependency
+
+There have always been complaints about fzf being a Ruby script. To make
+matters worse, Ruby 2.1 removed ncurses binding from its standard libary.
+Because of the change, users running Ruby 2.1 or above are forced to build C
+extensions of curses gem to meet the requirement of fzf. The new Go version
+will be distributed as an executable binary so it will be much more accessible
+and should be easier to setup.
+
+### Performance
+
+Many people have been surprised to see how fast fzf is even when it was
+written in Ruby. It stays quite responsive even for 100k+ lines, which is
+well above the size of the usual input.
+
+The new Go version, of course, is significantly faster than that. It has all
+the performance optimization techniques used in Ruby implementation and more.
+It also doesn't suffer from [GIL][gil], so the search performance scales
+proportional to the number of CPU cores. On my MacBook Pro (Mid 2012), the new
+version was shown to be an order of magnitude faster on certain cases. It also
+starts much faster though the difference may not be noticeable.
+
+Differences with Ruby version
+-----------------------------
+
+The Go version is designed to be perfectly compatible with the previous Ruby
+version. The only behavioral difference is that the new version ignores the
+numeric argument to `--sort=N` option and always sorts the result regardless
+of the number of matches. The value was introduced to limit the response time
+of the query, but the Go version is blazingly fast (almost instant response
+even for 1M+ items) so I decided that it's no longer required.
+
+System requirements
+-------------------
+
+Currently, prebuilt binaries are provided only for OS X and Linux. The install
+script will fall back to the legacy Ruby version on the other systems, but if
+you have Go 1.4 installed, you can try building it yourself.
+
+However, as pointed out in [golang.org/doc/install][req], the Go version may
+not run on CentOS/RHEL 5.x, and if that's the case, the install script will
+choose the Ruby version instead.
+
+The Go version depends on [ncurses][ncurses] and some Unix system calls, so it
+shouldn't run natively on Windows at the moment. But it won't be impossible to
+support Windows by falling back to a cross-platform alternative such as
+[termbox][termbox] only on Windows. If you're interested in making fzf work on
+Windows, please let me know.
+
+Build
+-----
+
+```sh
+# Build fzf executables and tarballs
+make
+
+# Install the executable to ../bin directory
+make install
+
+# Build executables and tarballs for Linux using Docker
+make linux
+```
+
+Contribution
+------------
+
+For the time being, I will not add or accept any new features until we can be
+sure that the implementation is stable and we have a sufficient number of test
+cases. However, fixes for obvious bugs and new test cases are welcome.
+
+I also care much about the performance of the implementation, so please make
+sure that your change does not result in performance regression. And please be
+noted that we don't have a quantitative measure of the performance yet.
+
+Third-party libraries used
+--------------------------
+
+- [ncurses][ncurses]
+- [mattn/go-runewidth](https://github.com/mattn/go-runewidth)
+ - Licensed under [MIT](http://mattn.mit-license.org/2013)
+- [mattn/go-shellwords](https://github.com/mattn/go-shellwords)
+ - Licensed under [MIT](http://mattn.mit-license.org/2014)
+
+License
+-------
+
+[MIT](LICENSE)
+
+[install]: https://github.com/junegunn/fzf#installation
+[go]: https://golang.org/
+[gil]: http://en.wikipedia.org/wiki/Global_Interpreter_Lock
+[ncurses]: https://www.gnu.org/software/ncurses/
+[req]: http://golang.org/doc/install
+[termbox]: https://github.com/nsf/termbox-go
diff --git a/src/algo/algo.go b/src/algo/algo.go
new file mode 100644
index 0000000..ac7bd8b
--- /dev/null
+++ b/src/algo/algo.go
@@ -0,0 +1,199 @@
+package algo
+
+import (
+ "strings"
+ "unicode"
+
+ "github.com/junegunn/fzf/src/util"
+)
+
+/*
+ * String matching algorithms here do not use strings.ToLower to avoid
+ * performance penalty. And they assume pattern runes are given in lowercase
+ * letters when caseSensitive is false.
+ *
+ * In short: They try to do as little work as possible.
+ */
+
+func runeAt(runes []rune, index int, max int, forward bool) rune {
+ if forward {
+ return runes[index]
+ }
+ return runes[max-index-1]
+}
+
+// FuzzyMatch performs fuzzy-match
+func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) {
+ if len(pattern) == 0 {
+ return 0, 0
+ }
+
+ // 0. (FIXME) How to find the shortest match?
+ // a_____b__c__abc
+ // ^^^^^^^^^^ ^^^
+ // 1. forward scan (abc)
+ // *-----*-----*>
+ // a_____b___abc__
+ // 2. reverse scan (cba)
+ // a_____b___abc__
+ // <***
+ pidx := 0
+ sidx := -1
+ eidx := -1
+
+ lenRunes := len(runes)
+ lenPattern := len(pattern)
+
+ for index := range runes {
+ char := runeAt(runes, index, lenRunes, forward)
+ // This is considerably faster than blindly applying strings.ToLower to the
+ // whole string
+ if !caseSensitive {
+ // Partially inlining `unicode.ToLower`. Ugly, but makes a noticeable
+ // difference in CPU cost. (Measured on Go 1.4.1. Also note that the Go
+ // compiler as of now does not inline non-leaf functions.)
+ if char >= 'A' && char <= 'Z' {
+ char += 32
+ } else if char > unicode.MaxASCII {
+ char = unicode.To(unicode.LowerCase, char)
+ }
+ }
+ pchar := runeAt(pattern, pidx, lenPattern, forward)
+ if char == pchar {
+ if sidx < 0 {
+ sidx = index
+ }
+ if pidx++; pidx == lenPattern {
+ eidx = index + 1
+ break
+ }
+ }
+ }
+
+ if sidx >= 0 && eidx >= 0 {
+ pidx--
+ for index := eidx - 1; index >= sidx; index-- {
+ char := runeAt(runes, index, lenRunes, forward)
+ if !caseSensitive {
+ if char >= 'A' && char <= 'Z' {
+ char += 32
+ } else if char > unicode.MaxASCII {
+ char = unicode.To(unicode.LowerCase, char)
+ }
+ }
+
+ pchar := runeAt(pattern, pidx, lenPattern, forward)
+ if char == pchar {
+ if pidx--; pidx < 0 {
+ sidx = index
+ break
+ }
+ }
+ }
+ if forward {
+ return sidx, eidx
+ }
+ return lenRunes - eidx, lenRunes - sidx
+ }
+ return -1, -1
+}
+
+// ExactMatchNaive is a basic string searching algorithm that handles case
+// sensitivity. Although naive, it still performs better than the combination
+// of strings.ToLower + strings.Index for typical fzf use cases where input
+// strings and patterns are not very long.
+//
+// We might try to implement better algorithms in the future:
+// http://en.wikipedia.org/wiki/String_searching_algorithm
+func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) {
+ if len(pattern) == 0 {
+ return 0, 0
+ }
+
+ lenRunes := len(runes)
+ lenPattern := len(pattern)
+
+ if lenRunes < lenPattern {
+ return -1, -1
+ }
+
+ pidx := 0
+ for index := 0; index < lenRunes; index++ {
+ char := runeAt(runes, index, lenRunes, forward)
+ if !caseSensitive {
+ if char >= 'A' && char <= 'Z' {
+ char += 32
+ } else if char > unicode.MaxASCII {
+ char = unicode.To(unicode.LowerCase, char)
+ }
+ }
+ pchar := runeAt(pattern, pidx, lenPattern, forward)
+ if pchar == char {
+ pidx++
+ if pidx == lenPattern {
+ if forward {
+ return index - lenPattern + 1, index + 1
+ }
+ return lenRunes - (index + 1), lenRunes - (index - lenPattern + 1)
+ }
+ } else {
+ index -= pidx
+ pidx = 0
+ }
+ }
+ return -1, -1
+}
+
+// PrefixMatch performs prefix-match
+func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) {
+ if len(runes) < len(pattern) {
+ return -1, -1
+ }
+
+ for index, r := range pattern {
+ char := runes[index]
+ if !caseSensitive {
+ char = unicode.ToLower(char)
+ }
+ if char != r {
+ return -1, -1
+ }
+ }
+ return 0, len(pattern)
+}
+
+// SuffixMatch performs suffix-match
+func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune) (int, int) {
+ runes := util.TrimRight(input)
+ trimmedLen := len(runes)
+ diff := trimmedLen - len(pattern)
+ if diff < 0 {
+ return -1, -1
+ }
+
+ for index, r := range pattern {
+ char := runes[index+diff]
+ if !caseSensitive {
+ char = unicode.ToLower(char)
+ }
+ if char != r {
+ return -1, -1
+ }
+ }
+ return trimmedLen - len(pattern), trimmedLen
+}
+
+// EqualMatch performs equal-match
+func EqualMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) {
+ if len(runes) != len(pattern) {
+ return -1, -1
+ }
+ runesStr := string(runes)
+ if !caseSensitive {
+ runesStr = strings.ToLower(runesStr)
+ }
+ if runesStr == string(pattern) {
+ return 0, len(pattern)
+ }
+ return -1, -1
+}
diff --git a/src/algo/algo_test.go b/src/algo/algo_test.go
new file mode 100644
index 0000000..95a020b
--- /dev/null
+++ b/src/algo/algo_test.go
@@ -0,0 +1,69 @@
+package algo
+
+import (
+ "strings"
+ "testing"
+)
+
+func assertMatch(t *testing.T, fun func(bool, bool, []rune, []rune) (int, int), caseSensitive bool, forward bool, input string, pattern string, sidx int, eidx int) {
+ if !caseSensitive {
+ pattern = strings.ToLower(pattern)
+ }
+ s, e := fun(caseSensitive, forward, []rune(input), []rune(pattern))
+ if s != sidx {
+ t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", s, sidx, input, pattern)
+ }
+ if e != eidx {
+ t.Errorf("Invalid end index: %d (expected: %d, %s / %s)", e, eidx, input, pattern)
+ }
+}
+
+func TestFuzzyMatch(t *testing.T) {
+ assertMatch(t, FuzzyMatch, false, true, "fooBarbaz", "oBZ", 2, 9)
+ assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBZ", -1, -1)
+ assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBz", 2, 9)
+ assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "fooBarbazz", -1, -1)
+}
+
+func TestFuzzyMatchBackward(t *testing.T) {
+ assertMatch(t, FuzzyMatch, false, true, "foobar fb", "fb", 0, 4)
+ assertMatch(t, FuzzyMatch, false, false, "foobar fb", "fb", 7, 9)
+}
+
+func TestExactMatchNaive(t *testing.T) {
+ for _, dir := range []bool{true, false} {
+ assertMatch(t, ExactMatchNaive, false, dir, "fooBarbaz", "oBA", 2, 5)
+ assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "oBA", -1, -1)
+ assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "fooBarbazz", -1, -1)
+ }
+}
+
+func TestExactMatchNaiveBackward(t *testing.T) {
+ assertMatch(t, FuzzyMatch, false, true, "foobar foob", "oo", 1, 3)
+ assertMatch(t, FuzzyMatch, false, false, "foobar foob", "oo", 8, 10)
+}
+
+func TestPrefixMatch(t *testing.T) {
+ for _, dir := range []bool{true, false} {
+ assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3)
+ assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1)
+ assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "baz", -1, -1)
+ }
+}
+
+func TestSuffixMatch(t *testing.T) {
+ for _, dir := range []bool{true, false} {
+ assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "Foo", -1, -1)
+ assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "baz", 6, 9)
+ assertMatch(t, SuffixMatch, true, dir, "fooBarbaz", "Baz", -1, -1)
+ }
+}
+
+func TestEmptyPattern(t *testing.T) {
+ for _, dir := range []bool{true, false} {
+ assertMatch(t, FuzzyMatch, true, dir, "foobar", "", 0, 0)
+ assertMatch(t, ExactMatchNaive, true, dir, "foobar", "", 0, 0)
+ assertMatch(t, PrefixMatch, true, dir, "foobar", "", 0, 0)
+ assertMatch(t, SuffixMatch, true, dir, "foobar", "", 6, 6)
+ }
+}
diff --git a/src/ansi.go b/src/ansi.go
new file mode 100644
index 0000000..876229f
--- /dev/null
+++ b/src/ansi.go
@@ -0,0 +1,149 @@
+package fzf
+
+import (
+ "bytes"
+ "regexp"
+ "strconv"
+ "strings"
+ "unicode/utf8"
+)
+
+type ansiOffset struct {
+ offset [2]int32
+ color ansiState
+}
+
+type ansiState struct {
+ fg int
+ bg int
+ bold bool
+}
+
+func (s *ansiState) colored() bool {
+ return s.fg != -1 || s.bg != -1 || s.bold
+}
+
+func (s *ansiState) equals(t *ansiState) bool {
+ if t == nil {
+ return !s.colored()
+ }
+ return s.fg == t.fg && s.bg == t.bg && s.bold == t.bold
+}
+
+var ansiRegex *regexp.Regexp
+
+func init() {
+ ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]")
+}
+
+func extractColor(str string, state *ansiState) (string, []ansiOffset, *ansiState) {
+ var offsets []ansiOffset
+ var output bytes.Buffer
+
+ if state != nil {
+ offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state})
+ }
+
+ idx := 0
+ for _, offset := range ansiRegex.FindAllStringIndex(str, -1) {
+ output.WriteString(str[idx:offset[0]])
+ newState := interpretCode(str[offset[0]:offset[1]], state)
+
+ if !newState.equals(state) {
+ if state != nil {
+ // Update last offset
+ (&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes()))
+ }
+
+ if newState.colored() {
+ // Append new offset
+ state = newState
+ newLen := int32(utf8.RuneCount(output.Bytes()))
+ offsets = append(offsets, ansiOffset{[2]int32{newLen, newLen}, *state})
+ } else {
+ // Discard state
+ state = nil
+ }
+ }
+
+ idx = offset[1]
+ }
+
+ rest := str[idx:]
+ if len(rest) > 0 {
+ output.WriteString(rest)
+ if state != nil {
+ // Update last offset
+ (&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes()))
+ }
+ }
+ return output.String(), offsets, state
+}
+
+func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
+ // State
+ var state *ansiState
+ if prevState == nil {
+ state = &ansiState{-1, -1, false}
+ } else {
+ state = &ansiState{prevState.fg, prevState.bg, prevState.bold}
+ }
+ if ansiCode[len(ansiCode)-1] == 'K' {
+ return state
+ }
+
+ ptr := &state.fg
+ state256 := 0
+
+ init := func() {
+ state.fg = -1
+ state.bg = -1
+ state.bold = false
+ state256 = 0
+ }
+
+ ansiCode = ansiCode[2 : len(ansiCode)-1]
+ if len(ansiCode) == 0 {
+ init()
+ }
+ for _, code := range strings.Split(ansiCode, ";") {
+ if num, err := strconv.Atoi(code); err == nil {
+ switch state256 {
+ case 0:
+ switch num {
+ case 38:
+ ptr = &state.fg
+ state256++
+ case 48:
+ ptr = &state.bg
+ state256++
+ case 39:
+ state.fg = -1
+ case 49:
+ state.bg = -1
+ case 1:
+ state.bold = true
+ case 0:
+ init()
+ default:
+ if num >= 30 && num <= 37 {
+ state.fg = num - 30
+ } else if num >= 40 && num <= 47 {
+ state.bg = num - 40
+ }
+ }
+ case 1:
+ switch num {
+ case 5:
+ state256++
+ default:
+ state256 = 0
+ }
+ case 2:
+ *ptr = num
+ state256 = 0
+ }
+ }
+ }
+ return state
+}
diff --git a/src/ansi_test.go b/src/ansi_test.go
new file mode 100644
index 0000000..e278fe9
--- /dev/null
+++ b/src/ansi_test.go
@@ -0,0 +1,152 @@
+package fzf
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestExtractColor(t *testing.T) {
+ assert := func(offset ansiOffset, b int32, e int32, fg int, bg int, bold bool) {
+ if offset.offset[0] != b || offset.offset[1] != e ||
+ offset.color.fg != fg || offset.color.bg != bg || offset.color.bold != bold {
+ t.Error(offset, b, e, fg, bg, bold)
+ }
+ }
+
+ src := "hello world"
+ var state *ansiState
+ clean := "\x1b[0m"
+ check := func(assertion func(ansiOffsets []ansiOffset, state *ansiState)) {
+ output, ansiOffsets, newState := extractColor(src, state)
+ state = newState
+ if output != "hello world" {
+ t.Errorf("Invalid output: {}", output)
+ }
+ fmt.Println(src, ansiOffsets, clean)
+ assertion(ansiOffsets, state)
+ }
+
+ check(func(offsets []ansiOffset, state *ansiState) {
+ if len(offsets) > 0 {
+ t.Fail()
+ }
+ })
+
+ state = nil
+ src = "\x1b[0mhello world"
+ check(func(offsets []ansiOffset, state *ansiState) {
+ if len(offsets) > 0 {
+ t.Fail()
+ }
+ })
+
+ state = nil
+ src = "\x1b[1mhello world"
+ check(func(offsets []ansiOffset, state *ansiState) {
+ if len(offsets) != 1 {
+ t.Fail()
+ }
+ assert(offsets[0], 0, 11, -1, -1, true)
+ })
+
+ state = nil
+ src = "\x1b[1mhello \x1b[mworld"
+ check(func(offsets []ansiOffset, state *ansiState) {
+ if len(offsets) != 1 {
+ t.Fail()
+ }
+ assert(offsets[0], 0, 6, -1, -1, true)
+ })
+
+ state = nil
+ src = "\x1b[1mhello \x1b[Kworld"
+ check(func(offsets []ansiOffset, state *ansiState) {
+ if len(offsets) != 1 {
+ t.Fail()
+ }
+ assert(offsets[0], 0, 11, -1, -1, true)
+ })
+
+ state = nil
+ src = "hello \x1b[34;45;1mworld"
+ check(func(offsets []ansiOffset, state *ansiState) {
+ if len(offsets) != 1 {
+ t.Fail()
+ }
+ assert(offsets[0], 6, 11, 4, 5, true)
+ })
+
+ state = nil
+ src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld"
+ check(func(offsets []ansiOffset, state *ansiState) {
+ if len(offsets) != 1 {
+ t.Fail()
+ }
+ assert(offsets[0], 6, 11, 4, 5, true)
+ })
+
+ state = nil
+ src = "hello \x1b[34;45;1mwor\x1b[0mld"
+ check(func(offsets []ansiOffset, state *ansiState) {
+ if len(offsets) != 1 {
+ t.Fail()
+ }
+ assert(offsets[0], 6, 9, 4, 5, true)
+ })
+
+ state = nil
+ src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md"
+ check(func(offsets []ansiOffset, state *ansiState) {
+ if len(offsets) != 3 {
+ t.Fail()
+ }
+ assert(offsets[0], 6, 8, 4, 233, true)
+ assert(offsets[1], 8, 9, 161, 233, true)
+ assert(offsets[2], 10, 11, 161, -1, false)
+ })
+
+ // {38,48};5;{38,48}
+ state = nil
+ src = "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md"
+ check(func(offsets []ansiOffset, state *ansiState) {
+ if len(offsets) != 2 {
+ t.Fail()
+ }
+ assert(offsets[0], 6, 9, 38, 48, true)
+ assert(offsets[1], 9, 10, 48, 38, true)
+ })
+
+ src = "hello \x1b[32;1mworld"
+ check(func(offsets []ansiOffset, state *ansiState) {
+ if len(offsets) != 1 {
+ t.Fail()
+ }
+ if state.fg != 2 || state.bg != -1 || !state.bold {
+ t.Fail()
+ }
+ assert(offsets[0], 6, 11, 2, -1, true)
+ })
+
+ src = "hello world"
+ check(func(offsets []ansiOffset, state *ansiState) {
+ if len(offsets) != 1 {
+ t.Fail()
+ }
+ if state.fg != 2 || state.bg != -1 || !state.bold {
+ t.Fail()
+ }
+ assert(offsets[0], 0, 11, 2, -1, true)
+ })
+
+ src = "hello \x1b[0;38;5;200;48;5;100mworld"
+ check(func(offsets []ansiOffset, state *ansiState) {
+ if len(offsets) != 2 {
+ t.Fail()
+ }
+ if state.fg != 200 || state.bg != 100 || state.bold {
+ t.Fail()
+ }
+ assert(offsets[0], 0, 6, 2, -1, true)
+ assert(offsets[1], 6, 11, 200, 100, false)
+ })
+}
diff --git a/src/cache.go b/src/cache.go
new file mode 100644
index 0000000..d2ec00b
--- /dev/null
+++ b/src/cache.go
@@ -0,0 +1,53 @@
+package fzf
+
+import "sync"
+
+// queryCache associates strings to lists of items
+type queryCache map[string][]*Item
+
+// ChunkCache associates Chunk and query string to lists of items
+type ChunkCache struct {
+ mutex sync.Mutex
+ cache map[*Chunk]*queryCache
+}
+
+// NewChunkCache returns a new ChunkCache
+func NewChunkCache() ChunkCache {
+ return ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)}
+}
+
+// Add adds the list to the cache
+func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) {
+ if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax {
+ return
+ }
+
+ cc.mutex.Lock()
+ defer cc.mutex.Unlock()
+
+ qc, ok := cc.cache[chunk]
+ if !ok {
+ cc.cache[chunk] = &queryCache{}
+ qc = cc.cache[chunk]
+ }
+ (*qc)[key] = list
+}
+
+// Find is called to lookup ChunkCache
+func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Item, bool) {
+ if len(key) == 0 || !chunk.IsFull() {
+ return nil, false
+ }
+
+ cc.mutex.Lock()
+ defer cc.mutex.Unlock()
+
+ qc, ok := cc.cache[chunk]
+ if ok {
+ list, ok := (*qc)[key]
+ if ok {
+ return list, true
+ }
+ }
+ return nil, false
+}
diff --git a/src/cache_test.go b/src/cache_test.go
new file mode 100644
index 0000000..05f904c
--- /dev/null
+++ b/src/cache_test.go
@@ -0,0 +1,40 @@
+package fzf
+
+import "testing"
+
+func TestChunkCache(t *testing.T) {
+ cache := NewChunkCache()
+ chunk2 := make(Chunk, chunkSize)
+ chunk1p := &Chunk{}
+ chunk2p := &chunk2
+ items1 := []*Item{&Item{}}
+ items2 := []*Item{&Item{}, &Item{}}
+ cache.Add(chunk1p, "foo", items1)
+ cache.Add(chunk2p, "foo", items1)
+ cache.Add(chunk2p, "bar", items2)
+
+ { // chunk1 is not full
+ cached, found := cache.Find(chunk1p, "foo")
+ if found {
+ t.Error("Cached disabled for non-empty chunks", found, cached)
+ }
+ }
+ {
+ cached, found := cache.Find(chunk2p, "foo")
+ if !found || len(cached) != 1 {
+ t.Error("Expected 1 item cached", found, cached)
+ }
+ }
+ {
+ cached, found := cache.Find(chunk2p, "bar")
+ if !found || len(cached) != 2 {
+ t.Error("Expected 2 items cached", found, cached)
+ }
+ }
+ {
+ cached, found := cache.Find(chunk1p, "foobar")
+ if found {
+ t.Error("Expected 0 item cached", found, cached)
+ }
+ }
+}
diff --git a/src/chunklist.go b/src/chunklist.go
new file mode 100644
index 0000000..a953fae
--- /dev/null
+++ b/src/chunklist.go
@@ -0,0 +1,93 @@
+package fzf
+
+import "sync"
+
+// Chunk is a list of Item pointers whose size has the upper limit of chunkSize
+type Chunk []*Item // >>> []Item
+
+// ItemBuilder is a closure type that builds Item object from a pointer to a
+// string and an integer
+type ItemBuilder func([]byte, int) *Item
+
+// ChunkList is a list of Chunks
+type ChunkList struct {
+ chunks []*Chunk
+ count int
+ mutex sync.Mutex
+ trans ItemBuilder
+}
+
+// NewChunkList returns a new ChunkList
+func NewChunkList(trans ItemBuilder) *ChunkList {
+ return &ChunkList{
+ chunks: []*Chunk{},
+ count: 0,
+ mutex: sync.Mutex{},
+ trans: trans}
+}
+
+func (c *Chunk) push(trans ItemBuilder, data []byte, index int) bool {
+ item := trans(data, index)
+ if item != nil {
+ *c = append(*c, item)
+ return true
+ }
+ return false
+}
+
+// IsFull returns true if the Chunk is full
+func (c *Chunk) IsFull() bool {
+ return len(*c) == chunkSize
+}
+
+func (cl *ChunkList) lastChunk() *Chunk {
+ return cl.chunks[len(cl.chunks)-1]
+}
+
+// CountItems returns the total number of Items
+func CountItems(cs []*Chunk) int {
+ if len(cs) == 0 {
+ return 0
+ }
+ return chunkSize*(len(cs)-1) + len(*(cs[len(cs)-1]))
+}
+
+// Push adds the item to the list
+func (cl *ChunkList) Push(data []byte) bool {
+ cl.mutex.Lock()
+ defer cl.mutex.Unlock()
+
+ if len(cl.chunks) == 0 || cl.lastChunk().IsFull() {
+ newChunk := Chunk(make([]*Item, 0, chunkSize))
+ cl.chunks = append(cl.chunks, &newChunk)
+ }
+
+ if cl.lastChunk().push(cl.trans, data, cl.count) {
+ cl.count++
+ return true
+ }
+ return false
+}
+
+// Snapshot returns immutable snapshot of the ChunkList
+func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
+ cl.mutex.Lock()
+ defer cl.mutex.Unlock()
+
+ ret := make([]*Chunk, len(cl.chunks))
+ copy(ret, cl.chunks)
+
+ // Duplicate the last chunk
+ if cnt := len(ret); cnt > 0 {
+ ret[cnt-1] = ret[cnt-1].dupe()
+ }
+ return ret, cl.count
+}
+
+func (c *Chunk) dupe() *Chunk {
+ newChunk := make(Chunk, len(*c))
+ for idx, ptr := range *c {
+ newChunk[idx] = ptr
+ }
+ return &newChunk
+}
diff --git a/src/chunklist_test.go b/src/chunklist_test.go
new file mode 100644
index 0000000..26795ef
--- /dev/null
+++ b/src/chunklist_test.go
@@ -0,0 +1,74 @@
+package fzf
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestChunkList(t *testing.T) {
+ cl := NewChunkList(func(s []byte, i int) *Item {
+ return &Item{text: []rune(string(s)), rank: Rank{0, 0, uint32(i * 2)}}
+ })
+
+ // Snapshot
+ snapshot, count := cl.Snapshot()
+ if len(snapshot) > 0 || count > 0 {
+ t.Error("Snapshot should be empty now")
+ }
+
+ // Add some data
+ cl.Push([]byte("hello"))
+ cl.Push([]byte("world"))
+
+ // Previously created snapshot should remain the same
+ if len(snapshot) > 0 {
+ t.Error("Snapshot should not have changed")
+ }
+
+ // But the new snapshot should contain the added items
+ snapshot, count = cl.Snapshot()
+ if len(snapshot) != 1 && count != 2 {
+ t.Error("Snapshot should not be empty now")
+ }
+
+ // Check the content of the ChunkList
+ chunk1 := snapshot[0]
+ if len(*chunk1) != 2 {
+ t.Error("Snapshot should contain only two items")
+ }
+ if string((*chunk1)[0].text) != "hello" || (*chunk1)[0].rank.index != 0 ||
+ string((*chunk1)[1].text) != "world" || (*chunk1)[1].rank.index != 2 {
+ t.Error("Invalid data")
+ }
+ if chunk1.IsFull() {
+ t.Error("Chunk should not have been marked full yet")
+ }
+
+ // Add more data
+ for i := 0; i < chunkSize*2; i++ {
+ cl.Push([]byte(fmt.Sprintf("item %d", i)))
+ }
+
+ // Previous snapshot should remain the same
+ if len(snapshot) != 1 {
+ t.Error("Snapshot should stay the same")
+ }
+
+ // New snapshot
+ snapshot, count = cl.Snapshot()
+ if len(snapshot) != 3 || !snapshot[0].IsFull() ||
+ !snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 {
+ t.Error("Expected two full chunks and one more chunk")
+ }
+ if len(*snapshot[2]) != 2 {
+ t.Error("Unexpected number of items")
+ }
+
+ cl.Push([]byte("hello"))
+ cl.Push([]byte("world"))
+
+ lastChunkCount := len(*snapshot[len(snapshot)-1])
+ if lastChunkCount != 2 {
+ t.Error("Unexpected number of items:", lastChunkCount)
+ }
+}
diff --git a/src/constants.go b/src/constants.go
new file mode 100644
index 0000000..1cc4f81
--- /dev/null
+++ b/src/constants.go
@@ -0,0 +1,56 @@
+package fzf
+
+import (
+ "time"
+
+ "github.com/junegunn/fzf/src/util"
+)
+
+const (
+ // Current version
+ version = "0.10.5"
+
+ // Core
+ coordinatorDelayMax time.Duration = 100 * time.Millisecond
+ coordinatorDelayStep time.Duration = 10 * time.Millisecond
+
+ // Reader
+ defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//`
+
+ // Terminal
+ initialDelay = 100 * time.Millisecond
+ spinnerDuration = 200 * time.Millisecond
+
+ // Matcher
+ progressMinDuration = 200 * time.Millisecond
+
+ // Capacity of each chunk
+ chunkSize int = 100
+
+ // Do not cache results of low selectivity queries
+ queryCacheMax int = chunkSize / 5
+
+ // Not to cache mergers with large lists
+ mergerCacheMax int = 100000
+
+ // History
+ defaultHistoryMax int = 1000
+)
+
+// fzf events
+const (
+ EvtReadNew util.EventType = iota
+ EvtReadFin
+ EvtSearchNew
+ EvtSearchProgress
+ EvtSearchFin
+ EvtHeader
+ EvtClose
+)
+
+const (
+ exitOk = 0
+ exitNoMatch = 1
+ exitError = 2
+ exitInterrupt = 130
+)
diff --git a/src/core.go b/src/core.go
new file mode 100644
index 0000000..35d7ced
--- /dev/null
+++ b/src/core.go
@@ -0,0 +1,283 @@
+/*
+Package fzf implements fzf, a command-line fuzzy finder.
+
+The MIT License (MIT)
+
+Copyright (c) 2015 Junegunn Choi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+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
+THE SOFTWARE.
+*/
+package fzf
+
+import (
+ "fmt"
+ "os"
+ "runtime"
+ "time"
+
+ "github.com/junegunn/fzf/src/util"
+)
+
+func initProcs() {
+ runtime.GOMAXPROCS(runtime.NumCPU())
+}
+
+/*
+Reader -> EvtReadFin
+Reader -> EvtReadNew -> Matcher (restart)
+Terminal -> EvtSearchNew:bool -> Matcher (restart)
+Matcher -> EvtSearchProgress -> Terminal (update info)
+Matcher -> EvtSearchFin -> Terminal (update list)
+Matcher -> EvtHeader -> Terminal (update header)
+*/
+
+// Run starts fzf
+func Run(opts *Options) {
+ initProcs()
+
+ sort := opts.Sort > 0
+ rankTiebreak = opts.Tiebreak
+
+ if opts.Version {
+ fmt.Println(version)
+ os.Exit(exitOk)
+ }
+
+ // Event channel
+ eventBox := util.NewEventBox()
+
+ // ANSI code processor
+ ansiProcessor := func(data []byte) ([]rune, []ansiOffset) {
+ return util.BytesToRunes(data), nil
+ }
+ ansiProcessorRunes := func(data []rune) ([]rune, []ansiOffset) {
+ return data, nil
+ }
+ if opts.Ansi {
+ if opts.Theme != nil {
+ var state *ansiState
+ ansiProcessor = func(data []byte) ([]rune, []ansiOffset) {
+ trimmed, offsets, newState := extractColor(string(data), state)
+ state = newState
+ return []rune(trimmed), offsets
+ }
+ } else {
+ // When color is disabled but ansi option is given,
+ // we simply strip out ANSI codes from the input
+ ansiProcessor = func(data []byte) ([]rune, []ansiOffset) {
+ trimmed, _, _ := extractColor(string(data), nil)
+ return []rune(trimmed), nil
+ }
+ }
+ ansiProcessorRunes = func(data []rune) ([]rune, []ansiOffset) {
+ return ansiProcessor([]byte(string(data)))
+ }
+ }
+
+ // Chunk list
+ var chunkList *ChunkList
+ header := make([]string, 0, opts.HeaderLines)
+ if len(opts.WithNth) == 0 {
+ chunkList = NewChunkList(func(data []byte, index int) *Item {
+ if len(header) < opts.HeaderLines {
+ header = append(header, string(data))
+ eventBox.Set(EvtHeader, header)
+ return nil
+ }
+ runes, colors := ansiProcessor(data)
+ return &Item{
+ text: runes,
+ index: uint32(index),
+ colors: colors,
+ rank: Rank{0, 0, uint32(index)}}
+ })
+ } else {
+ chunkList = NewChunkList(func(data []byte, index int) *Item {
+ runes := util.BytesToRunes(data)
+ tokens := Tokenize(runes, opts.Delimiter)
+ trans := Transform(tokens, opts.WithNth)
+ if len(header) < opts.HeaderLines {
+ header = append(header, string(joinTokens(trans)))
+ eventBox.Set(EvtHeader, header)
+ return nil
+ }
+ item := Item{
+ text: joinTokens(trans),
+ origText: &runes,
+ index: uint32(index),
+ colors: nil,
+ rank: Rank{0, 0, uint32(index)}}
+
+ trimmed, colors := ansiProcessorRunes(item.text)
+ item.text = trimmed
+ item.colors = colors
+ return &item
+ })
+ }
+
+ // Reader
+ streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
+ if !streamingFilter {
+ reader := Reader{func(data []byte) bool {
+ return chunkList.Push(data)
+ }, eventBox, opts.ReadZero}
+ go reader.ReadSource()
+ }
+
+ // Matcher
+ patternBuilder := func(runes []rune) *Pattern {
+ return BuildPattern(
+ opts.Mode, opts.Case, opts.Tiebreak != byEnd,
+ opts.Nth, opts.Delimiter, runes)
+ }
+ matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)
+
+ // Filtering mode
+ if opts.Filter != nil {
+ if opts.PrintQuery {
+ fmt.Println(*opts.Filter)
+ }
+
+ pattern := patternBuilder([]rune(*opts.Filter))
+
+ found := false
+ if streamingFilter {
+ reader := Reader{
+ func(runes []byte) bool {
+ item := chunkList.trans(runes, 0)
+ if item != nil && pattern.MatchItem(item) {
+ fmt.Println(string(item.text))
+ found = true
+ }
+ return false
+ }, eventBox, opts.ReadZero}
+ reader.ReadSource()
+ } else {
+ eventBox.Unwatch(EvtReadNew)
+ eventBox.WaitFor(EvtReadFin)
+
+ snapshot, _ := chunkList.Snapshot()
+ merger, _ := matcher.scan(MatchRequest{
+ chunks: snapshot,
+ pattern: pattern})
+ for i := 0; i < merger.Length(); i++ {
+ fmt.Println(merger.Get(i).AsString(opts.Ansi))
+ found = true
+ }
+ }
+ if found {
+ os.Exit(exitOk)
+ }
+ os.Exit(exitNoMatch)
+ }
+
+ // Synchronous search
+ if opts.Sync {
+ eventBox.Unwatch(EvtReadNew)
+ eventBox.WaitFor(EvtReadFin)
+ }
+
+ // Go interactive
+ go matcher.Loop()
+
+ // Terminal I/O
+ terminal := NewTerminal(opts, eventBox)
+ deferred := opts.Select1 || opts.Exit0
+ go terminal.Loop()
+ if !deferred {
+ terminal.startChan <- true
+ }
+
+ // Event coordination
+ reading := true
+ ticks := 0
+ eventBox.Watch(EvtReadNew)
+ for {
+ delay := true
+ ticks++
+ eventBox.Wait(func(events *util.Events) {
+ defer events.Clear()
+ for evt, value := range *events {
+ switch evt {
+
+ case EvtReadNew, EvtReadFin:
+ reading = reading && evt == EvtReadNew
+ snapshot, count := chunkList.Snapshot()
+ terminal.UpdateCount(count, !reading)
+ matcher.Reset(snapshot, terminal.Input(), false, !reading, sort)
+
+ case EvtSearchNew:
+ switch val := value.(type) {
+ case bool:
+ sort = val
+ }
+ snapshot, _ := chunkList.Snapshot()
+ matcher.Reset(snapshot, terminal.Input(), true, !reading, sort)
+ delay = false
+
+ case EvtSearchProgress:
+ switch val := value.(type) {
+ case float32:
+ terminal.UpdateProgress(val)
+ }
+
+ case EvtHeader:
+ terminal.UpdateHeader(value.([]string))
+
+ case EvtSearchFin:
+ switch val := value.(type) {
+ case *Merger:
+ if deferred {
+ count := val.Length()
+ if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 {
+ deferred = false
+ terminal.startChan <- true
+ } else if val.final {
+ if opts.Exit0 && count == 0 || opts.Select1 && count == 1 {
+ if opts.PrintQuery {
+ fmt.Println(opts.Query)
+ }
+ if len(opts.Expect) > 0 {
+ fmt.Println()
+ }
+ for i := 0; i < count; i++ {
+ fmt.Println(val.Get(i).AsString(opts.Ansi))
+ }
+ if count > 0 {
+ os.Exit(exitOk)
+ }
+ os.Exit(exitNoMatch)
+ }
+ deferred = false
+ terminal.startChan <- true
+ }
+ }
+ terminal.UpdateList(val)
+ }
+ }
+ }
+ })
+ if delay && reading {
+ dur := util.DurWithin(
+ time.Duration(ticks)*coordinatorDelayStep,
+ 0, coordinatorDelayMax)
+ time.Sleep(dur)
+ }
+ }
+}
diff --git a/src/curses/curses.go b/src/curses/curses.go
new file mode 100644
index 0000000..59cea3b
--- /dev/null
+++ b/src/curses/curses.go
@@ -0,0 +1,556 @@
+package curses
+
+/*
+#include
+#include
+#cgo LDFLAGS: -lncurses
+*/
+import "C"
+
+import (
+ "fmt"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+ "unicode/utf8"
+)
+
+// Types of user action
+const (
+ Rune = iota
+
+ CtrlA
+ CtrlB
+ CtrlC
+ CtrlD
+ CtrlE
+ CtrlF
+ CtrlG
+ CtrlH
+ Tab
+ CtrlJ
+ CtrlK
+ CtrlL
+ CtrlM
+ CtrlN
+ CtrlO
+ CtrlP
+ CtrlQ
+ CtrlR
+ CtrlS
+ CtrlT
+ CtrlU
+ CtrlV
+ CtrlW
+ CtrlX
+ CtrlY
+ CtrlZ
+ ESC
+
+ Invalid
+ Mouse
+
+ BTab
+ BSpace
+
+ Del
+ PgUp
+ PgDn
+
+ Up
+ Down
+ Left
+ Right
+ Home
+ End
+
+ SLeft
+ SRight
+
+ F1
+ F2
+ F3
+ F4
+
+ AltBS
+ AltA
+ AltB
+ AltC
+ AltD
+ AltE
+ AltF
+
+ AltZ = AltA + 'z' - 'a'
+)
+
+// Pallete
+const (
+ ColNormal = iota
+ ColPrompt
+ ColMatch
+ ColCurrent
+ ColCurrentMatch
+ ColSpinner
+ ColInfo
+ ColCursor
+ ColSelected
+ ColHeader
+ ColUser
+)
+
+const (
+ doubleClickDuration = 500 * time.Millisecond
+)
+
+type ColorTheme struct {
+ UseDefault bool
+ Fg int16
+ Bg int16
+ DarkBg int16
+ Prompt int16
+ Match int16
+ Current int16
+ CurrentMatch int16
+ Spinner int16
+ Info int16
+ Cursor int16
+ Selected int16
+ Header int16
+}
+
+type Event struct {
+ Type int
+ Char rune
+ MouseEvent *MouseEvent
+}
+
+type MouseEvent struct {
+ Y int
+ X int
+ S int
+ Down bool
+ Double bool
+ Mod bool
+}
+
+var (
+ _buf []byte
+ _in *os.File
+ _color func(int, bool) C.int
+ _colorMap map[int]int
+ _prevDownTime time.Time
+ _clickY []int
+ _screen *C.SCREEN
+ Default16 *ColorTheme
+ Dark256 *ColorTheme
+ Light256 *ColorTheme
+ FG int
+ CurrentFG int
+ BG int
+ DarkBG int
+)
+
+func init() {
+ _prevDownTime = time.Unix(0, 0)
+ _clickY = []int{}
+ _colorMap = make(map[int]int)
+ Default16 = &ColorTheme{
+ UseDefault: true,
+ Fg: 15,
+ Bg: 0,
+ DarkBg: C.COLOR_BLACK,
+ Prompt: C.COLOR_BLUE,
+ Match: C.COLOR_GREEN,
+ Current: C.COLOR_YELLOW,
+ CurrentMatch: C.COLOR_GREEN,
+ Spinner: C.COLOR_GREEN,
+ Info: C.COLOR_WHITE,
+ Cursor: C.COLOR_RED,
+ Selected: C.COLOR_MAGENTA,
+ Header: C.COLOR_CYAN}
+ Dark256 = &ColorTheme{
+ UseDefault: true,
+ Fg: 15,
+ Bg: 0,
+ DarkBg: 236,
+ Prompt: 110,
+ Match: 108,
+ Current: 254,
+ CurrentMatch: 151,
+ Spinner: 148,
+ Info: 144,
+ Cursor: 161,
+ Selected: 168,
+ Header: 109}
+ Light256 = &ColorTheme{
+ UseDefault: true,
+ Fg: 15,
+ Bg: 0,
+ DarkBg: 251,
+ Prompt: 25,
+ Match: 66,
+ Current: 237,
+ CurrentMatch: 23,
+ Spinner: 65,
+ Info: 101,
+ Cursor: 161,
+ Selected: 168,
+ Header: 31}
+}
+
+func attrColored(pair int, bold bool) C.int {
+ var attr C.int
+ if pair > ColNormal {
+ attr = C.COLOR_PAIR(C.int(pair))
+ }
+ if bold {
+ attr = attr | C.A_BOLD
+ }
+ return attr
+}
+
+func attrMono(pair int, bold bool) C.int {
+ var attr C.int
+ switch pair {
+ case ColCurrent:
+ if bold {
+ attr = C.A_REVERSE
+ }
+ case ColMatch:
+ attr = C.A_UNDERLINE
+ case ColCurrentMatch:
+ attr = C.A_UNDERLINE | C.A_REVERSE
+ }
+ if bold {
+ attr = attr | C.A_BOLD
+ }
+ return attr
+}
+
+func MaxX() int {
+ return int(C.COLS)
+}
+
+func MaxY() int {
+ return int(C.LINES)
+}
+
+func getch(nonblock bool) int {
+ b := make([]byte, 1)
+ syscall.SetNonblock(int(_in.Fd()), nonblock)
+ _, err := _in.Read(b)
+ if err != nil {
+ return -1
+ }
+ return int(b[0])
+}
+
+func Init(theme *ColorTheme, black bool, mouse bool) {
+ {
+ in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0)
+ if err != nil {
+ panic("Failed to open /dev/tty")
+ }
+ _in = in
+ // Break STDIN
+ // syscall.Dup2(int(in.Fd()), int(os.Stdin.Fd()))
+ }
+
+ C.setlocale(C.LC_ALL, C.CString(""))
+ _screen = C.newterm(nil, C.stderr, C.stdin)
+ if _screen == nil {
+ fmt.Println("Invalid $TERM: " + os.Getenv("TERM"))
+ os.Exit(2)
+ }
+ C.set_term(_screen)
+ if mouse {
+ C.mousemask(C.ALL_MOUSE_EVENTS, nil)
+ }
+ C.noecho()
+ C.raw() // stty dsusp undef
+
+ intChan := make(chan os.Signal, 1)
+ signal.Notify(intChan, os.Interrupt, os.Kill)
+ go func() {
+ <-intChan
+ Close()
+ os.Exit(2)
+ }()
+
+ if theme != nil {
+ C.start_color()
+ initPairs(theme, black)
+ _color = attrColored
+ } else {
+ _color = attrMono
+ }
+}
+
+func initPairs(theme *ColorTheme, black bool) {
+ fg := C.short(theme.Fg)
+ bg := C.short(theme.Bg)
+ if black {
+ bg = C.COLOR_BLACK
+ } else if theme.UseDefault {
+ fg = -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))
+ }
+
+ CurrentFG = int(theme.Current)
+ DarkBG = int(theme.DarkBg)
+ darkBG := C.short(DarkBG)
+ C.init_pair(ColPrompt, C.short(theme.Prompt), bg)
+ C.init_pair(ColMatch, C.short(theme.Match), bg)
+ C.init_pair(ColCurrent, C.short(theme.Current), darkBG)
+ C.init_pair(ColCurrentMatch, C.short(theme.CurrentMatch), darkBG)
+ C.init_pair(ColSpinner, C.short(theme.Spinner), bg)
+ 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)
+ C.init_pair(ColHeader, C.short(theme.Header), bg)
+}
+
+func Close() {
+ C.endwin()
+ C.delscreen(_screen)
+}
+
+func GetBytes() []byte {
+ c := getch(false)
+ _buf = append(_buf, byte(c))
+
+ for {
+ c = getch(true)
+ if c == -1 {
+ break
+ }
+ _buf = append(_buf, byte(c))
+ }
+
+ return _buf
+}
+
+// 27 (91 79) 77 type x y
+func mouseSequence(sz *int) Event {
+ if len(_buf) < 6 {
+ return Event{Invalid, 0, nil}
+ }
+ *sz = 6
+ switch _buf[3] {
+ case 32, 36, 40, 48, // mouse-down / shift / cmd / ctrl
+ 35, 39, 43, 51: // mouse-up / shift / cmd / ctrl
+ mod := _buf[3] >= 36
+ down := _buf[3]%2 == 0
+ x := int(_buf[4] - 33)
+ y := int(_buf[5] - 33)
+ double := false
+ if down {
+ now := time.Now()
+ if now.Sub(_prevDownTime) < doubleClickDuration {
+ _clickY = append(_clickY, y)
+ } else {
+ _clickY = []int{y}
+ }
+ _prevDownTime = now
+ } else {
+ if len(_clickY) > 1 && _clickY[0] == _clickY[1] &&
+ time.Now().Sub(_prevDownTime) < doubleClickDuration {
+ double = true
+ }
+ }
+ return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}}
+ case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl
+ 97, 101, 105, 113: // scroll-down / shift / cmd / ctrl
+ mod := _buf[3] >= 100
+ s := 1 - int(_buf[3]%2)*2
+ return Event{Mouse, 0, &MouseEvent{0, 0, s, false, false, mod}}
+ }
+ return Event{Invalid, 0, nil}
+}
+
+func escSequence(sz *int) Event {
+ if len(_buf) < 2 {
+ return Event{ESC, 0, nil}
+ }
+ *sz = 2
+ switch _buf[1] {
+ case 98:
+ return Event{AltB, 0, nil}
+ case 100:
+ return Event{AltD, 0, nil}
+ case 102:
+ return Event{AltF, 0, nil}
+ case 127:
+ return Event{AltBS, 0, nil}
+ case 91, 79:
+ if len(_buf) < 3 {
+ return Event{Invalid, 0, nil}
+ }
+ *sz = 3
+ switch _buf[2] {
+ case 68:
+ return Event{Left, 0, nil}
+ case 67:
+ return Event{Right, 0, nil}
+ case 66:
+ return Event{Down, 0, nil}
+ case 65:
+ return Event{Up, 0, nil}
+ case 90:
+ return Event{BTab, 0, nil}
+ case 72:
+ return Event{Home, 0, nil}
+ case 70:
+ return Event{End, 0, nil}
+ case 77:
+ return mouseSequence(sz)
+ case 80:
+ return Event{F1, 0, nil}
+ case 81:
+ return Event{F2, 0, nil}
+ case 82:
+ return Event{F3, 0, nil}
+ case 83:
+ return Event{F4, 0, nil}
+ case 49, 50, 51, 52, 53, 54:
+ if len(_buf) < 4 {
+ return Event{Invalid, 0, nil}
+ }
+ *sz = 4
+ switch _buf[2] {
+ case 50:
+ return Event{Invalid, 0, nil} // INS
+ case 51:
+ return Event{Del, 0, nil}
+ case 52:
+ return Event{End, 0, nil}
+ case 53:
+ return Event{PgUp, 0, nil}
+ case 54:
+ return Event{PgDn, 0, nil}
+ case 49:
+ switch _buf[3] {
+ case 126:
+ return Event{Home, 0, nil}
+ case 59:
+ if len(_buf) != 6 {
+ return Event{Invalid, 0, nil}
+ }
+ *sz = 6
+ switch _buf[4] {
+ case 50:
+ switch _buf[5] {
+ case 68:
+ return Event{Home, 0, nil}
+ case 67:
+ return Event{End, 0, nil}
+ }
+ case 53:
+ switch _buf[5] {
+ case 68:
+ return Event{SLeft, 0, nil}
+ case 67:
+ return Event{SRight, 0, nil}
+ }
+ } // _buf[4]
+ } // _buf[3]
+ } // _buf[2]
+ } // _buf[2]
+ } // _buf[1]
+ if _buf[1] >= 'a' && _buf[1] <= 'z' {
+ return Event{AltA + int(_buf[1]) - 'a', 0, nil}
+ }
+ return Event{Invalid, 0, nil}
+}
+
+func GetChar() Event {
+ if len(_buf) == 0 {
+ _buf = GetBytes()
+ }
+ if len(_buf) == 0 {
+ panic("Empty _buffer")
+ }
+
+ sz := 1
+ defer func() {
+ _buf = _buf[sz:]
+ }()
+
+ switch _buf[0] {
+ case CtrlC:
+ return Event{CtrlC, 0, nil}
+ case CtrlG:
+ return Event{CtrlG, 0, nil}
+ case CtrlQ:
+ return Event{CtrlQ, 0, nil}
+ case 127:
+ return Event{BSpace, 0, nil}
+ case ESC:
+ return escSequence(&sz)
+ }
+
+ // CTRL-A ~ CTRL-Z
+ if _buf[0] <= CtrlZ {
+ return Event{int(_buf[0]), 0, nil}
+ }
+ r, rsz := utf8.DecodeRune(_buf)
+ if r == utf8.RuneError {
+ return Event{ESC, 0, nil}
+ }
+ sz = rsz
+ return Event{Rune, r, nil}
+}
+
+func Move(y int, x int) {
+ C.move(C.int(y), C.int(x))
+}
+
+func MoveAndClear(y int, x int) {
+ Move(y, x)
+ C.clrtoeol()
+}
+
+func Print(text string) {
+ C.addstr(C.CString(text))
+}
+
+func CPrint(pair int, bold bool, text string) {
+ attr := _color(pair, bold)
+ C.attron(attr)
+ Print(text)
+ C.attroff(attr)
+}
+
+func Clear() {
+ C.clear()
+}
+
+func Endwin() {
+ C.endwin()
+}
+
+func Refresh() {
+ C.refresh()
+}
+
+func PairFor(fg int, bg int) int {
+ key := (fg << 8) + bg
+ if found, prs := _colorMap[key]; prs {
+ return found
+ }
+
+ id := len(_colorMap) + ColUser
+ C.init_pair(C.short(id), C.short(fg), C.short(bg))
+ _colorMap[key] = id
+ return id
+}
diff --git a/src/curses/curses_test.go b/src/curses/curses_test.go
new file mode 100644
index 0000000..db75c40
--- /dev/null
+++ b/src/curses/curses_test.go
@@ -0,0 +1,14 @@
+package curses
+
+import (
+ "testing"
+)
+
+func TestPairFor(t *testing.T) {
+ if PairFor(30, 50) != PairFor(30, 50) {
+ t.Fail()
+ }
+ if PairFor(-1, 10) != PairFor(-1, 10) {
+ t.Fail()
+ }
+}
diff --git a/src/fzf/main.go b/src/fzf/main.go
new file mode 100644
index 0000000..29d4767
--- /dev/null
+++ b/src/fzf/main.go
@@ -0,0 +1,7 @@
+package main
+
+import "github.com/junegunn/fzf/src"
+
+func main() {
+ fzf.Run(fzf.ParseOptions())
+}
diff --git a/src/history.go b/src/history.go
new file mode 100644
index 0000000..4aa87fc
--- /dev/null
+++ b/src/history.go
@@ -0,0 +1,96 @@
+package fzf
+
+import (
+ "errors"
+ "io/ioutil"
+ "os"
+ "strings"
+)
+
+// History struct represents input history
+type History struct {
+ path string
+ lines []string
+ modified map[int]string
+ maxSize int
+ cursor int
+}
+
+// NewHistory returns the pointer to a new History struct
+func NewHistory(path string, maxSize int) (*History, error) {
+ fmtError := func(e error) error {
+ if os.IsPermission(e) {
+ return errors.New("permission denied: " + path)
+ }
+ return errors.New("invalid history file: " + e.Error())
+ }
+
+ // Read history file
+ data, err := ioutil.ReadFile(path)
+ if err != nil {
+ // If it doesn't exist, check if we can create a file with the name
+ if os.IsNotExist(err) {
+ data = []byte{}
+ if err := ioutil.WriteFile(path, data, 0600); err != nil {
+ return nil, fmtError(err)
+ }
+ } else {
+ return nil, fmtError(err)
+ }
+ }
+ // Split lines and limit the maximum number of lines
+ lines := strings.Split(strings.Trim(string(data), "\n"), "\n")
+ if len(lines[len(lines)-1]) > 0 {
+ lines = append(lines, "")
+ }
+ return &History{
+ path: path,
+ maxSize: maxSize,
+ lines: lines,
+ modified: make(map[int]string),
+ cursor: len(lines) - 1}, nil
+}
+
+func (h *History) append(line string) error {
+ // We don't append empty lines
+ if len(line) == 0 {
+ return nil
+ }
+
+ lines := append(h.lines[:len(h.lines)-1], line)
+ if len(lines) > h.maxSize {
+ lines = lines[len(lines)-h.maxSize : len(lines)]
+ }
+ h.lines = append(lines, "")
+ return ioutil.WriteFile(h.path, []byte(strings.Join(h.lines, "\n")), 0600)
+}
+
+func (h *History) override(str string) {
+ // You can update the history but they're not written to the file
+ if h.cursor == len(h.lines)-1 {
+ h.lines[h.cursor] = str
+ } else if h.cursor < len(h.lines)-1 {
+ h.modified[h.cursor] = str
+ }
+}
+
+func (h *History) current() string {
+ if str, prs := h.modified[h.cursor]; prs {
+ return str
+ }
+ return h.lines[h.cursor]
+}
+
+func (h *History) previous() string {
+ if h.cursor > 0 {
+ h.cursor--
+ }
+ return h.current()
+}
+
+func (h *History) next() string {
+ if h.cursor < len(h.lines)-1 {
+ h.cursor++
+ }
+ return h.current()
+}
diff --git a/src/history_test.go b/src/history_test.go
new file mode 100644
index 0000000..fa6f106
--- /dev/null
+++ b/src/history_test.go
@@ -0,0 +1,59 @@
+package fzf
+
+import (
+ "os/user"
+ "testing"
+)
+
+func TestHistory(t *testing.T) {
+ maxHistory := 50
+
+ // Invalid arguments
+ user, _ := user.Current()
+ paths := []string{"/etc", "/proc"}
+ if user.Name != "root" {
+ paths = append(paths, "/etc/sudoers")
+ }
+ for _, path := range paths {
+ if _, e := NewHistory(path, maxHistory); e == nil {
+ t.Error("Error expected for: " + path)
+ }
+ }
+ { // Append lines
+ h, _ := NewHistory("/tmp/fzf-history", maxHistory)
+ for i := 0; i < maxHistory+10; i++ {
+ h.append("foobar")
+ }
+ }
+ { // Read lines
+ h, _ := NewHistory("/tmp/fzf-history", maxHistory)
+ if len(h.lines) != maxHistory+1 {
+ t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
+ }
+ for i := 0; i < maxHistory; i++ {
+ if h.lines[i] != "foobar" {
+ t.Error("Expected: foobar, actual: " + h.lines[i])
+ }
+ }
+ }
+ { // Append lines
+ h, _ := NewHistory("/tmp/fzf-history", maxHistory)
+ h.append("barfoo")
+ h.append("")
+ h.append("foobarbaz")
+ }
+ { // Read lines again
+ h, _ := NewHistory("/tmp/fzf-history", maxHistory)
+ if len(h.lines) != maxHistory+1 {
+ t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
+ }
+ compare := func(idx int, exp string) {
+ if h.lines[idx] != exp {
+ t.Errorf("Expected: %s, actual: %s\n", exp, h.lines[idx])
+ }
+ }
+ compare(maxHistory-3, "foobar")
+ compare(maxHistory-2, "barfoo")
+ compare(maxHistory-1, "foobarbaz")
+ }
+}
diff --git a/src/item.go b/src/item.go
new file mode 100644
index 0000000..12ca3df
--- /dev/null
+++ b/src/item.go
@@ -0,0 +1,267 @@
+package fzf
+
+import (
+ "math"
+
+ "github.com/junegunn/fzf/src/curses"
+)
+
+// Offset holds two 32-bit integers denoting the offsets of a matched substring
+type Offset [2]int32
+
+type colorOffset struct {
+ offset [2]int32
+ color int
+ bold bool
+}
+
+// Item represents each input line
+type Item struct {
+ text []rune
+ origText *[]rune
+ transformed []Token
+ index uint32
+ offsets []Offset
+ colors []ansiOffset
+ rank Rank
+}
+
+// Rank is used to sort the search result
+type Rank struct {
+ matchlen uint16
+ tiebreak uint16
+ index uint32
+}
+
+// Tiebreak criterion to use. Never changes once fzf is started.
+var rankTiebreak tiebreak
+
+// Rank calculates rank of the Item
+func (item *Item) Rank(cache bool) Rank {
+ if cache && (item.rank.matchlen > 0 || item.rank.tiebreak > 0) {
+ return item.rank
+ }
+ matchlen := 0
+ prevEnd := 0
+ minBegin := math.MaxUint16
+ for _, offset := range item.offsets {
+ begin := int(offset[0])
+ end := int(offset[1])
+ if prevEnd > begin {
+ begin = prevEnd
+ }
+ if end > prevEnd {
+ prevEnd = end
+ }
+ if end > begin {
+ if begin < minBegin {
+ minBegin = begin
+ }
+ matchlen += end - begin
+ }
+ }
+ var tiebreak uint16
+ switch rankTiebreak {
+ case byLength:
+ // It is guaranteed that .transformed in not null in normal execution
+ if item.transformed != nil {
+ lenSum := 0
+ for _, token := range item.transformed {
+ lenSum += len(token.text)
+ }
+ tiebreak = uint16(lenSum)
+ } else {
+ tiebreak = uint16(len(item.text))
+ }
+ case byBegin:
+ // We can't just look at item.offsets[0][0] because it can be an inverse term
+ tiebreak = uint16(minBegin)
+ case byEnd:
+ if prevEnd > 0 {
+ tiebreak = uint16(1 + len(item.text) - prevEnd)
+ } else {
+ // Empty offsets due to inverse terms.
+ tiebreak = 1
+ }
+ case byIndex:
+ tiebreak = 1
+ }
+ rank := Rank{uint16(matchlen), tiebreak, item.index}
+ if cache {
+ item.rank = rank
+ }
+ return rank
+}
+
+// AsString returns the original string
+func (item *Item) AsString(stripAnsi bool) string {
+ return *item.StringPtr(stripAnsi)
+}
+
+// StringPtr returns the pointer to the original string
+func (item *Item) StringPtr(stripAnsi bool) *string {
+ if item.origText != nil {
+ if stripAnsi {
+ trimmed, _, _ := extractColor(string(*item.origText), nil)
+ return &trimmed
+ }
+ orig := string(*item.origText)
+ return &orig
+ }
+ str := string(item.text)
+ return &str
+}
+
+func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset {
+ if len(item.colors) == 0 {
+ var offsets []colorOffset
+ for _, off := range item.offsets {
+ offsets = append(offsets, colorOffset{offset: off, color: color, bold: bold})
+ }
+ return offsets
+ }
+
+ // Find max column
+ var maxCol int32
+ for _, off := range item.offsets {
+ if off[1] > maxCol {
+ maxCol = off[1]
+ }
+ }
+ for _, ansi := range item.colors {
+ if ansi.offset[1] > maxCol {
+ maxCol = ansi.offset[1]
+ }
+ }
+ cols := make([]int, maxCol)
+
+ for colorIndex, ansi := range item.colors {
+ for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
+ cols[i] = colorIndex + 1 // XXX
+ }
+ }
+
+ for _, off := range item.offsets {
+ for i := off[0]; i < off[1]; i++ {
+ cols[i] = -1
+ }
+ }
+
+ // sort.Sort(ByOrder(offsets))
+
+ // Merge offsets
+ // ------------ ---- -- ----
+ // ++++++++ ++++++++++
+ // --++++++++-- --++++++++++---
+ curr := 0
+ start := 0
+ var offsets []colorOffset
+ add := func(idx int) {
+ if curr != 0 && idx > start {
+ if curr == -1 {
+ offsets = append(offsets, colorOffset{
+ offset: Offset{int32(start), int32(idx)}, color: color, bold: bold})
+ } else {
+ 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
+ if bg == -1 {
+ if current {
+ bg = curses.DarkBG
+ } else {
+ bg = curses.BG
+ }
+ }
+ offsets = append(offsets, colorOffset{
+ offset: Offset{int32(start), int32(idx)},
+ color: curses.PairFor(fg, bg),
+ bold: ansi.color.bold || bold})
+ }
+ }
+ }
+ for idx, col := range cols {
+ if col != curr {
+ add(idx)
+ start = idx
+ curr = col
+ }
+ }
+ add(int(maxCol))
+ return offsets
+}
+
+// ByOrder is for sorting substring offsets
+type ByOrder []Offset
+
+func (a ByOrder) Len() int {
+ return len(a)
+}
+
+func (a ByOrder) Swap(i, j int) {
+ a[i], a[j] = a[j], a[i]
+}
+
+func (a ByOrder) Less(i, j int) bool {
+ ioff := a[i]
+ joff := a[j]
+ return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1])
+}
+
+// ByRelevance is for sorting Items
+type ByRelevance []*Item
+
+func (a ByRelevance) Len() int {
+ return len(a)
+}
+
+func (a ByRelevance) Swap(i, j int) {
+ a[i], a[j] = a[j], a[i]
+}
+
+func (a ByRelevance) Less(i, j int) bool {
+ irank := a[i].Rank(true)
+ jrank := a[j].Rank(true)
+
+ return compareRanks(irank, jrank, false)
+}
+
+// ByRelevanceTac is for sorting Items
+type ByRelevanceTac []*Item
+
+func (a ByRelevanceTac) Len() int {
+ return len(a)
+}
+
+func (a ByRelevanceTac) Swap(i, j int) {
+ a[i], a[j] = a[j], a[i]
+}
+
+func (a ByRelevanceTac) Less(i, j int) bool {
+ irank := a[i].Rank(true)
+ jrank := a[j].Rank(true)
+
+ return compareRanks(irank, jrank, true)
+}
+
+func compareRanks(irank Rank, jrank Rank, tac bool) bool {
+ if irank.matchlen < jrank.matchlen {
+ return true
+ } else if irank.matchlen > jrank.matchlen {
+ return false
+ }
+
+ if irank.tiebreak < jrank.tiebreak {
+ return true
+ } else if irank.tiebreak > jrank.tiebreak {
+ return false
+ }
+
+ return (irank.index <= jrank.index) != tac
+}
diff --git a/src/item_test.go b/src/item_test.go
new file mode 100644
index 0000000..5b9232a
--- /dev/null
+++ b/src/item_test.go
@@ -0,0 +1,104 @@
+package fzf
+
+import (
+ "sort"
+ "testing"
+
+ "github.com/junegunn/fzf/src/curses"
+)
+
+func TestOffsetSort(t *testing.T) {
+ offsets := []Offset{
+ Offset{3, 5}, Offset{2, 7},
+ Offset{1, 3}, Offset{2, 9}}
+ sort.Sort(ByOrder(offsets))
+
+ if offsets[0][0] != 1 || offsets[0][1] != 3 ||
+ offsets[1][0] != 2 || offsets[1][1] != 7 ||
+ offsets[2][0] != 2 || offsets[2][1] != 9 ||
+ offsets[3][0] != 3 || offsets[3][1] != 5 {
+ t.Error("Invalid order:", offsets)
+ }
+}
+
+func TestRankComparison(t *testing.T) {
+ if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}, false) ||
+ !compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}, false) ||
+ !compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}, false) ||
+ !compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}, false) {
+ t.Error("Invalid order")
+ }
+
+ if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}, true) ||
+ !compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}, false) ||
+ !compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}, true) ||
+ !compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}, false) {
+ t.Error("Invalid order (tac)")
+ }
+}
+
+// Match length, string length, index
+func TestItemRank(t *testing.T) {
+ strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
+ item1 := Item{text: strs[0], index: 1, offsets: []Offset{}}
+ rank1 := item1.Rank(true)
+ if rank1.matchlen != 0 || rank1.tiebreak != 3 || rank1.index != 1 {
+ t.Error(item1.Rank(true))
+ }
+ // Only differ in index
+ item2 := Item{text: strs[0], index: 0, offsets: []Offset{}}
+
+ items := []*Item{&item1, &item2}
+ sort.Sort(ByRelevance(items))
+ if items[0] != &item2 || items[1] != &item1 {
+ t.Error(items)
+ }
+
+ items = []*Item{&item2, &item1, &item1, &item2}
+ sort.Sort(ByRelevance(items))
+ if items[0] != &item2 || items[1] != &item2 ||
+ items[2] != &item1 || items[3] != &item1 {
+ t.Error(items)
+ }
+
+ // Sort by relevance
+ item3 := Item{text: strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}}
+ item4 := Item{text: strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}}
+ item5 := Item{text: strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}}
+ item6 := Item{text: strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}}
+ items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6}
+ sort.Sort(ByRelevance(items))
+ if items[0] != &item2 || items[1] != &item1 ||
+ items[2] != &item6 || items[3] != &item4 ||
+ items[4] != &item5 || items[5] != &item3 {
+ t.Error(items)
+ }
+}
+
+func TestColorOffset(t *testing.T) {
+ // ------------ 20 ---- -- ----
+ // ++++++++ ++++++++++
+ // --++++++++-- --++++++++++---
+ item := Item{
+ offsets: []Offset{Offset{5, 15}, Offset{25, 35}},
+ colors: []ansiOffset{
+ ansiOffset{[2]int32{0, 20}, ansiState{1, 5, false}},
+ ansiOffset{[2]int32{22, 27}, ansiState{2, 6, true}},
+ ansiOffset{[2]int32{30, 32}, ansiState{3, 7, false}},
+ ansiOffset{[2]int32{33, 40}, ansiState{4, 8, true}}}}
+ // [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}]
+
+ offsets := item.colorOffsets(99, false, true)
+ assert := func(idx int, b int32, e int32, c int, bold bool) {
+ o := offsets[idx]
+ if o.offset[0] != b || o.offset[1] != e || o.color != c || o.bold != bold {
+ t.Error(o)
+ }
+ }
+ assert(0, 0, 5, curses.ColUser, false)
+ assert(1, 5, 15, 99, false)
+ assert(2, 15, 20, curses.ColUser, false)
+ assert(3, 22, 25, curses.ColUser+1, true)
+ assert(4, 25, 35, 99, false)
+ assert(5, 35, 40, curses.ColUser+2, true)
+}
diff --git a/src/matcher.go b/src/matcher.go
new file mode 100644
index 0000000..3ea2fbe
--- /dev/null
+++ b/src/matcher.go
@@ -0,0 +1,221 @@
+package fzf
+
+import (
+ "fmt"
+ "runtime"
+ "sort"
+ "sync"
+ "time"
+
+ "github.com/junegunn/fzf/src/util"
+)
+
+// MatchRequest represents a search request
+type MatchRequest struct {
+ chunks []*Chunk
+ pattern *Pattern
+ final bool
+ sort bool
+}
+
+// Matcher is responsible for performing search
+type Matcher struct {
+ patternBuilder func([]rune) *Pattern
+ sort bool
+ tac bool
+ eventBox *util.EventBox
+ reqBox *util.EventBox
+ partitions int
+ mergerCache map[string]*Merger
+}
+
+const (
+ reqRetry util.EventType = iota
+ reqReset
+)
+
+// NewMatcher returns a new Matcher
+func NewMatcher(patternBuilder func([]rune) *Pattern,
+ sort bool, tac bool, eventBox *util.EventBox) *Matcher {
+ return &Matcher{
+ patternBuilder: patternBuilder,
+ sort: sort,
+ tac: tac,
+ eventBox: eventBox,
+ reqBox: util.NewEventBox(),
+ partitions: runtime.NumCPU(),
+ mergerCache: make(map[string]*Merger)}
+}
+
+// Loop puts Matcher in action
+func (m *Matcher) Loop() {
+ prevCount := 0
+
+ for {
+ var request MatchRequest
+
+ m.reqBox.Wait(func(events *util.Events) {
+ for _, val := range *events {
+ switch val := val.(type) {
+ case MatchRequest:
+ request = val
+ default:
+ panic(fmt.Sprintf("Unexpected type: %T", val))
+ }
+ }
+ events.Clear()
+ })
+
+ if request.sort != m.sort {
+ m.sort = request.sort
+ m.mergerCache = make(map[string]*Merger)
+ clearChunkCache()
+ }
+
+ // Restart search
+ patternString := request.pattern.AsString()
+ var merger *Merger
+ cancelled := false
+ count := CountItems(request.chunks)
+
+ foundCache := false
+ if count == prevCount {
+ // Look up mergerCache
+ if cached, found := m.mergerCache[patternString]; found {
+ foundCache = true
+ merger = cached
+ }
+ } else {
+ // Invalidate mergerCache
+ prevCount = count
+ m.mergerCache = make(map[string]*Merger)
+ }
+
+ if !foundCache {
+ merger, cancelled = m.scan(request)
+ }
+
+ if !cancelled {
+ if merger.cacheable() {
+ m.mergerCache[patternString] = merger
+ }
+ merger.final = request.final
+ m.eventBox.Set(EvtSearchFin, merger)
+ }
+ }
+}
+
+func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk {
+ perSlice := len(chunks) / m.partitions
+
+ // No need to parallelize
+ if perSlice == 0 {
+ return [][]*Chunk{chunks}
+ }
+
+ slices := make([][]*Chunk, m.partitions)
+ for i := 0; i < m.partitions; i++ {
+ start := i * perSlice
+ end := start + perSlice
+ if i == m.partitions-1 {
+ end = len(chunks)
+ }
+ slices[i] = chunks[start:end]
+ }
+ return slices
+}
+
+type partialResult struct {
+ index int
+ matches []*Item
+}
+
+func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
+ startedAt := time.Now()
+
+ numChunks := len(request.chunks)
+ if numChunks == 0 {
+ return EmptyMerger, false
+ }
+ pattern := request.pattern
+ if pattern.IsEmpty() {
+ return PassMerger(&request.chunks, m.tac), false
+ }
+
+ cancelled := util.NewAtomicBool(false)
+
+ slices := m.sliceChunks(request.chunks)
+ numSlices := len(slices)
+ resultChan := make(chan partialResult, numSlices)
+ countChan := make(chan int, numChunks)
+ waitGroup := sync.WaitGroup{}
+
+ for idx, chunks := range slices {
+ waitGroup.Add(1)
+ go func(idx int, chunks []*Chunk) {
+ defer func() { waitGroup.Done() }()
+ sliceMatches := []*Item{}
+ for _, chunk := range chunks {
+ matches := request.pattern.Match(chunk)
+ sliceMatches = append(sliceMatches, matches...)
+ if cancelled.Get() {
+ return
+ }
+ countChan <- len(matches)
+ }
+ if m.sort {
+ if m.tac {
+ sort.Sort(ByRelevanceTac(sliceMatches))
+ } else {
+ sort.Sort(ByRelevance(sliceMatches))
+ }
+ }
+ resultChan <- partialResult{idx, sliceMatches}
+ }(idx, chunks)
+ }
+
+ wait := func() bool {
+ cancelled.Set(true)
+ waitGroup.Wait()
+ return true
+ }
+
+ count := 0
+ matchCount := 0
+ for matchesInChunk := range countChan {
+ count++
+ matchCount += matchesInChunk
+
+ if count == numChunks {
+ break
+ }
+
+ if m.reqBox.Peek(reqReset) {
+ return nil, wait()
+ }
+
+ if time.Now().Sub(startedAt) > progressMinDuration {
+ m.eventBox.Set(EvtSearchProgress, float32(count)/float32(numChunks))
+ }
+ }
+
+ partialResults := make([][]*Item, numSlices)
+ for range slices {
+ partialResult := <-resultChan
+ partialResults[partialResult.index] = partialResult.matches
+ }
+ return NewMerger(partialResults, m.sort, m.tac), false
+}
+
+// Reset is called to interrupt/signal the ongoing search
+func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool) {
+ pattern := m.patternBuilder(patternRunes)
+
+ var event util.EventType
+ if cancel {
+ event = reqReset
+ } else {
+ event = reqRetry
+ }
+ m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort})
+}
diff --git a/src/merger.go b/src/merger.go
new file mode 100644
index 0000000..cce8a94
--- /dev/null
+++ b/src/merger.go
@@ -0,0 +1,118 @@
+package fzf
+
+import "fmt"
+
+// Merger with no data
+var EmptyMerger = NewMerger([][]*Item{}, false, false)
+
+// Merger holds a set of locally sorted lists of items and provides the view of
+// a single, globally-sorted list
+type Merger struct {
+ lists [][]*Item
+ merged []*Item
+ chunks *[]*Chunk
+ cursors []int
+ sorted bool
+ tac bool
+ final bool
+ count int
+}
+
+// PassMerger returns a new Merger that simply returns the items in the
+// original order
+func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
+ mg := Merger{
+ chunks: chunks,
+ tac: tac,
+ count: 0}
+
+ for _, chunk := range *mg.chunks {
+ mg.count += len(*chunk)
+ }
+ return &mg
+}
+
+// NewMerger returns a new Merger
+func NewMerger(lists [][]*Item, sorted bool, tac bool) *Merger {
+ mg := Merger{
+ lists: lists,
+ merged: []*Item{},
+ chunks: nil,
+ cursors: make([]int, len(lists)),
+ sorted: sorted,
+ tac: tac,
+ final: false,
+ count: 0}
+
+ for _, list := range mg.lists {
+ mg.count += len(list)
+ }
+ return &mg
+}
+
+// Length returns the number of items
+func (mg *Merger) Length() int {
+ return mg.count
+}
+
+// Get returns the pointer to the Item object indexed by the given integer
+func (mg *Merger) Get(idx int) *Item {
+ if mg.chunks != nil {
+ if mg.tac {
+ idx = mg.count - idx - 1
+ }
+ chunk := (*mg.chunks)[idx/chunkSize]
+ return (*chunk)[idx%chunkSize]
+ }
+
+ if mg.sorted {
+ return mg.mergedGet(idx)
+ }
+
+ if mg.tac {
+ idx = mg.count - idx - 1
+ }
+ for _, list := range mg.lists {
+ numItems := len(list)
+ if idx < numItems {
+ return list[idx]
+ }
+ idx -= numItems
+ }
+ panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count))
+}
+
+func (mg *Merger) cacheable() bool {
+ return mg.count < mergerCacheMax
+}
+
+func (mg *Merger) mergedGet(idx int) *Item {
+ for i := len(mg.merged); i <= idx; i++ {
+ minRank := Rank{0, 0, 0}
+ minIdx := -1
+ for listIdx, list := range mg.lists {
+ cursor := mg.cursors[listIdx]
+ if cursor < 0 || cursor == len(list) {
+ mg.cursors[listIdx] = -1
+ continue
+ }
+ if cursor >= 0 {
+ rank := list[cursor].Rank(false)
+ if minIdx < 0 || compareRanks(rank, minRank, mg.tac) {
+ minRank = rank
+ minIdx = listIdx
+ }
+ }
+ mg.cursors[listIdx] = cursor
+ }
+
+ if minIdx >= 0 {
+ chosen := mg.lists[minIdx]
+ mg.merged = append(mg.merged, chosen[mg.cursors[minIdx]])
+ mg.cursors[minIdx]++
+ } else {
+ panic(fmt.Sprintf("Index out of bounds (sorted, %d/%d)", i, mg.count))
+ }
+ }
+ return mg.merged[idx]
+}
diff --git a/src/merger_test.go b/src/merger_test.go
new file mode 100644
index 0000000..b7a2993
--- /dev/null
+++ b/src/merger_test.go
@@ -0,0 +1,93 @@
+package fzf
+
+import (
+ "fmt"
+ "math/rand"
+ "sort"
+ "testing"
+)
+
+func assert(t *testing.T, cond bool, msg ...string) {
+ if !cond {
+ t.Error(msg)
+ }
+}
+
+func randItem() *Item {
+ str := fmt.Sprintf("%d", rand.Uint32())
+ offsets := make([]Offset, rand.Int()%3)
+ for idx := range offsets {
+ sidx := int32(rand.Uint32() % 20)
+ eidx := sidx + int32(rand.Uint32()%20)
+ offsets[idx] = Offset{sidx, eidx}
+ }
+ return &Item{
+ text: []rune(str),
+ index: rand.Uint32(),
+ offsets: offsets}
+}
+
+func TestEmptyMerger(t *testing.T) {
+ assert(t, EmptyMerger.Length() == 0, "Not empty")
+ assert(t, EmptyMerger.count == 0, "Invalid count")
+ assert(t, len(EmptyMerger.lists) == 0, "Invalid lists")
+ assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list")
+}
+
+func buildLists(partiallySorted bool) ([][]*Item, []*Item) {
+ numLists := 4
+ lists := make([][]*Item, numLists)
+ cnt := 0
+ for i := 0; i < numLists; i++ {
+ numItems := rand.Int() % 20
+ cnt += numItems
+ lists[i] = make([]*Item, numItems)
+ for j := 0; j < numItems; j++ {
+ item := randItem()
+ lists[i][j] = item
+ }
+ if partiallySorted {
+ sort.Sort(ByRelevance(lists[i]))
+ }
+ }
+ items := []*Item{}
+ for _, list := range lists {
+ items = append(items, list...)
+ }
+ return lists, items
+}
+
+func TestMergerUnsorted(t *testing.T) {
+ lists, items := buildLists(false)
+ cnt := len(items)
+
+ // Not sorted: same order
+ mg := NewMerger(lists, false, false)
+ assert(t, cnt == mg.Length(), "Invalid Length")
+ for i := 0; i < cnt; i++ {
+ assert(t, items[i] == mg.Get(i), "Invalid Get")
+ }
+}
+
+func TestMergerSorted(t *testing.T) {
+ lists, items := buildLists(true)
+ cnt := len(items)
+
+ // Sorted sorted order
+ mg := NewMerger(lists, true, false)
+ assert(t, cnt == mg.Length(), "Invalid Length")
+ sort.Sort(ByRelevance(items))
+ for i := 0; i < cnt; i++ {
+ if items[i] != mg.Get(i) {
+ t.Error("Not sorted", items[i], mg.Get(i))
+ }
+ }
+
+ // Inverse order
+ mg2 := NewMerger(lists, true, false)
+ for i := cnt - 1; i >= 0; i-- {
+ if items[i] != mg2.Get(i) {
+ t.Error("Not sorted", items[i], mg2.Get(i))
+ }
+ }
+}
diff --git a/src/options.go b/src/options.go
new file mode 100644
index 0000000..9d8aaa1
--- /dev/null
+++ b/src/options.go
@@ -0,0 +1,892 @@
+package fzf
+
+import (
+ "os"
+ "regexp"
+ "strconv"
+ "strings"
+ "unicode/utf8"
+
+ "github.com/junegunn/fzf/src/curses"
+
+ "github.com/junegunn/go-shellwords"
+)
+
+const usage = `usage: fzf [options]
+
+ Search
+ -x, --extended Extended-search mode
+ -e, --extended-exact Extended-search mode (exact match)
+ -i Case-insensitive match (default: smart-case match)
+ +i Case-sensitive match
+ -n, --nth=N[,..] Comma-separated list of field index expressions
+ for limiting search scope. Each can be a non-zero
+ integer or a range expression ([BEGIN]..[END])
+ --with-nth=N[,..] Transform item using index expressions within finder
+ -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
+ +s, --no-sort Do not sort the result
+ --tac Reverse the order of the input
+ --tiebreak=CRITERION Sort criterion when the scores are tied;
+ [length|begin|end|index] (default: length)
+
+ Interface
+ -m, --multi Enable multi-select with tab/shift-tab
+ --ansi Enable processing of ANSI color codes
+ --no-mouse Disable mouse
+ --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
+ --black Use black background
+ --reverse Reverse orientation
+ --margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
+ --cycle Enable cyclic scroll
+ --no-hscroll Disable horizontal scroll
+ --inline-info Display finder info inline with the query
+ --prompt=STR Input prompt (default: '> ')
+ --bind=KEYBINDS Custom key bindings. Refer to the man page.
+ --history=FILE History file
+ --history-size=N Maximum number of history entries (default: 1000)
+ --header=STR String to print as header
+ --header-lines=N The first N lines of the input are treated as header
+
+ Scripting
+ -q, --query=STR Start the finder with the given query
+ -1, --select-1 Automatically select the only match
+ -0, --exit-0 Exit immediately when there's no match
+ -f, --filter=STR Filter mode. Do not start interactive finder.
+ --print-query Print query as the first line
+ --expect=KEYS Comma-separated list of keys to complete fzf
+ --sync Synchronous search for multi-staged filtering
+
+ Environment variables
+ FZF_DEFAULT_COMMAND Default command to use when input is tty
+ FZF_DEFAULT_OPTS Defaults options. (e.g. '-x -m')
+
+`
+
+// Mode denotes the current search mode
+type Mode int
+
+// Search modes
+const (
+ ModeFuzzy Mode = iota
+ ModeExtended
+ ModeExtendedExact
+)
+
+// Case denotes case-sensitivity of search
+type Case int
+
+// Case-sensitivities
+const (
+ CaseSmart Case = iota
+ CaseIgnore
+ CaseRespect
+)
+
+// Sort criteria
+type tiebreak int
+
+const (
+ byLength tiebreak = iota
+ byBegin
+ byEnd
+ byIndex
+)
+
+func defaultMargin() [4]string {
+ return [4]string{"0", "0", "0", "0"}
+}
+
+// Options stores the values of command-line options
+type Options struct {
+ Mode Mode
+ Case Case
+ Nth []Range
+ WithNth []Range
+ Delimiter Delimiter
+ Sort int
+ Tac bool
+ Tiebreak tiebreak
+ Multi bool
+ Ansi bool
+ Mouse bool
+ Theme *curses.ColorTheme
+ Black bool
+ Reverse bool
+ Cycle bool
+ Hscroll bool
+ InlineInfo bool
+ Prompt string
+ Query string
+ Select1 bool
+ Exit0 bool
+ Filter *string
+ ToggleSort bool
+ Expect map[int]string
+ Keymap map[int]actionType
+ Execmap map[int]string
+ PrintQuery bool
+ ReadZero bool
+ Sync bool
+ History *History
+ Header []string
+ HeaderLines int
+ Margin [4]string
+ Version bool
+}
+
+func defaultTheme() *curses.ColorTheme {
+ if strings.Contains(os.Getenv("TERM"), "256") {
+ return curses.Dark256
+ }
+ return curses.Default16
+}
+
+func defaultOptions() *Options {
+ return &Options{
+ Mode: ModeFuzzy,
+ Case: CaseSmart,
+ Nth: make([]Range, 0),
+ WithNth: make([]Range, 0),
+ Delimiter: Delimiter{},
+ Sort: 1000,
+ Tac: false,
+ Tiebreak: byLength,
+ Multi: false,
+ Ansi: false,
+ Mouse: true,
+ Theme: defaultTheme(),
+ Black: false,
+ Reverse: false,
+ Cycle: false,
+ Hscroll: true,
+ InlineInfo: false,
+ Prompt: "> ",
+ Query: "",
+ Select1: false,
+ Exit0: false,
+ Filter: nil,
+ ToggleSort: false,
+ Expect: make(map[int]string),
+ Keymap: defaultKeymap(),
+ Execmap: make(map[int]string),
+ PrintQuery: false,
+ ReadZero: false,
+ Sync: false,
+ History: nil,
+ Header: make([]string, 0),
+ HeaderLines: 0,
+ Margin: defaultMargin(),
+ Version: false}
+}
+
+func help(code int) {
+ os.Stderr.WriteString(usage)
+ os.Exit(code)
+}
+
+func errorExit(msg string) {
+ os.Stderr.WriteString(msg + "\n")
+ os.Exit(exitError)
+}
+
+func optString(arg string, prefixes ...string) (bool, string) {
+ for _, prefix := range prefixes {
+ if strings.HasPrefix(arg, prefix) {
+ return true, arg[len(prefix):]
+ }
+ }
+ return false, ""
+}
+
+func nextString(args []string, i *int, message string) string {
+ if len(args) > *i+1 {
+ *i++
+ } else {
+ errorExit(message)
+ }
+ return args[*i]
+}
+
+func optionalNextString(args []string, i *int) string {
+ if len(args) > *i+1 {
+ *i++
+ return args[*i]
+ }
+ return ""
+}
+
+func atoi(str string) int {
+ num, err := strconv.Atoi(str)
+ if err != nil {
+ errorExit("not a valid integer: " + str)
+ }
+ return num
+}
+
+func atof(str string) float64 {
+ num, err := strconv.ParseFloat(str, 64)
+ if err != nil {
+ errorExit("not a valid number: " + str)
+ }
+ return num
+}
+
+func nextInt(args []string, i *int, message string) int {
+ if len(args) > *i+1 {
+ *i++
+ } else {
+ errorExit(message)
+ }
+ return atoi(args[*i])
+}
+
+func optionalNumeric(args []string, i *int) int {
+ if len(args) > *i+1 {
+ if strings.IndexAny(args[*i+1], "0123456789") == 0 {
+ *i++
+ }
+ }
+ return 1 // Don't care
+}
+
+func splitNth(str string) []Range {
+ if match, _ := regexp.MatchString("^[0-9,-.]+$", str); !match {
+ errorExit("invalid format: " + str)
+ }
+
+ tokens := strings.Split(str, ",")
+ ranges := make([]Range, len(tokens))
+ for idx, s := range tokens {
+ r, ok := ParseRange(&s)
+ if !ok {
+ errorExit("invalid format: " + str)
+ }
+ ranges[idx] = r
+ }
+ return ranges
+}
+
+func delimiterRegexp(str string) Delimiter {
+ // Special handling of \t
+ str = strings.Replace(str, "\\t", "\t", -1)
+
+ // 1. Pattern does not contain any special character
+ if regexp.QuoteMeta(str) == str {
+ return Delimiter{str: &str}
+ }
+
+ rx, e := regexp.Compile(str)
+ // 2. Pattern is not a valid regular expression
+ if e != nil {
+ return Delimiter{str: &str}
+ }
+
+ // 3. Pattern as regular expression. Slow.
+ return Delimiter{regex: rx}
+}
+
+func isAlphabet(char uint8) bool {
+ return char >= 'a' && char <= 'z'
+}
+
+func parseKeyChords(str string, message string) map[int]string {
+ if len(str) == 0 {
+ errorExit(message)
+ }
+
+ tokens := strings.Split(str, ",")
+ if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Index(str, ",,,") >= 0 {
+ tokens = append(tokens, ",")
+ }
+
+ chords := make(map[int]string)
+ for _, key := range tokens {
+ if len(key) == 0 {
+ continue // ignore
+ }
+ lkey := strings.ToLower(key)
+ chord := 0
+ switch lkey {
+ case "up":
+ chord = curses.Up
+ case "down":
+ chord = curses.Down
+ case "left":
+ chord = curses.Left
+ case "right":
+ chord = curses.Right
+ case "enter", "return":
+ chord = curses.CtrlM
+ case "space":
+ chord = curses.AltZ + int(' ')
+ case "bspace", "bs":
+ chord = curses.BSpace
+ case "alt-bs", "alt-bspace":
+ chord = curses.AltBS
+ case "tab":
+ chord = curses.Tab
+ case "btab", "shift-tab":
+ chord = curses.BTab
+ case "esc":
+ chord = curses.ESC
+ case "del":
+ chord = curses.Del
+ case "home":
+ chord = curses.Home
+ case "end":
+ chord = curses.End
+ case "pgup", "page-up":
+ chord = curses.PgUp
+ case "pgdn", "page-down":
+ chord = curses.PgDn
+ case "shift-left":
+ chord = curses.SLeft
+ case "shift-right":
+ chord = curses.SRight
+ default:
+ if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
+ chord = curses.CtrlA + int(lkey[5]) - 'a'
+ } else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) {
+ chord = curses.AltA + int(lkey[4]) - 'a'
+ } else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '4' {
+ chord = curses.F1 + int(key[1]) - '1'
+ } else if utf8.RuneCountInString(key) == 1 {
+ chord = curses.AltZ + int([]rune(key)[0])
+ } else {
+ errorExit("unsupported key: " + key)
+ }
+ }
+ if chord > 0 {
+ chords[chord] = key
+ }
+ }
+ return chords
+}
+
+func parseTiebreak(str string) tiebreak {
+ switch strings.ToLower(str) {
+ case "length":
+ return byLength
+ case "index":
+ return byIndex
+ case "begin":
+ return byBegin
+ case "end":
+ return byEnd
+ default:
+ errorExit("invalid sort criterion: " + str)
+ }
+ return byLength
+}
+
+func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme {
+ dupe := *theme
+ return &dupe
+}
+
+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
+ case "header":
+ theme.Header = ansi
+ default:
+ fail()
+ }
+ }
+ }
+ return theme
+}
+
+var executeRegexp *regexp.Regexp
+
+func firstKey(keymap map[int]string) int {
+ for k := range keymap {
+ return k
+ }
+ return 0
+}
+
+const (
+ escapedColon = 0
+ escapedComma = 1
+)
+
+func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort bool, str string) (map[int]actionType, map[int]string, bool) {
+ if executeRegexp == nil {
+ // Backreferences are not supported.
+ // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
+ executeRegexp = regexp.MustCompile(
+ "(?s):execute:.*|:execute(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)")
+ }
+ masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
+ return ":execute(" + strings.Repeat(" ", len(src)-10) + ")"
+ })
+ masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1)
+ masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1)
+
+ idx := 0
+ for _, pairStr := range strings.Split(masked, ",") {
+ origPairStr := str[idx : idx+len(pairStr)]
+ idx += len(pairStr) + 1
+
+ pair := strings.SplitN(pairStr, ":", 2)
+ if len(pair) < 2 {
+ errorExit("bind action not specified: " + origPairStr)
+ }
+ var key int
+ if len(pair[0]) == 1 && pair[0][0] == escapedColon {
+ key = ':' + curses.AltZ
+ } else if len(pair[0]) == 1 && pair[0][0] == escapedComma {
+ key = ',' + curses.AltZ
+ } else {
+ keys := parseKeyChords(pair[0], "key name required")
+ key = firstKey(keys)
+ }
+
+ act := origPairStr[len(pair[0])+1 : len(origPairStr)]
+ actLower := strings.ToLower(act)
+ switch actLower {
+ case "ignore":
+ keymap[key] = actIgnore
+ 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 "delete-char/eof":
+ keymap[key] = actDeleteCharEOF
+ case "end-of-line":
+ keymap[key] = actEndOfLine
+ case "cancel":
+ keymap[key] = actCancel
+ 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-all":
+ keymap[key] = actToggleAll
+ case "select-all":
+ keymap[key] = actSelectAll
+ case "deselect-all":
+ keymap[key] = actDeselectAll
+ 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 "previous-history":
+ keymap[key] = actPreviousHistory
+ case "next-history":
+ keymap[key] = actNextHistory
+ case "toggle-sort":
+ keymap[key] = actToggleSort
+ toggleSort = true
+ default:
+ if isExecuteAction(actLower) {
+ keymap[key] = actExecute
+ if act[7] == ':' {
+ execmap[key] = act[8:]
+ } else {
+ execmap[key] = act[8 : len(act)-1]
+ }
+ } else {
+ errorExit("unknown action: " + act)
+ }
+ }
+ }
+ return keymap, execmap, toggleSort
+}
+
+func isExecuteAction(str string) bool {
+ if !strings.HasPrefix(str, "execute") || len(str) < 9 {
+ return false
+ }
+ b := str[7]
+ e := str[len(str)-1]
+ if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' ||
+ b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") {
+ return true
+ }
+ return false
+}
+
+func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType {
+ keys := parseKeyChords(str, "key name required")
+ if len(keys) != 1 {
+ errorExit("multiple keys specified")
+ }
+ keymap[firstKey(keys)] = actToggleSort
+ return keymap
+}
+
+func strLines(str string) []string {
+ return strings.Split(strings.TrimSuffix(str, "\n"), "\n")
+}
+
+func parseMargin(margin string) [4]string {
+ margins := strings.Split(margin, ",")
+ checked := func(str string) string {
+ if strings.HasSuffix(str, "%") {
+ val := atof(str[:len(str)-1])
+ if val < 0 {
+ errorExit("margin must be non-negative")
+ }
+ if val > 100 {
+ errorExit("margin too large")
+ }
+ } else {
+ val := atoi(str)
+ if val < 0 {
+ errorExit("margin must be non-negative")
+ }
+ }
+ return str
+ }
+ switch len(margins) {
+ case 1:
+ m := checked(margins[0])
+ return [4]string{m, m, m, m}
+ case 2:
+ tb := checked(margins[0])
+ rl := checked(margins[1])
+ return [4]string{tb, rl, tb, rl}
+ case 3:
+ t := checked(margins[0])
+ rl := checked(margins[1])
+ b := checked(margins[2])
+ return [4]string{t, rl, b, rl}
+ case 4:
+ return [4]string{
+ checked(margins[0]), checked(margins[1]),
+ checked(margins[2]), checked(margins[3])}
+ default:
+ errorExit("invalid margin: " + margin)
+ }
+ return defaultMargin()
+}
+
+func parseOptions(opts *Options, allArgs []string) {
+ keymap := make(map[int]actionType)
+ var historyMax int
+ if opts.History == nil {
+ historyMax = defaultHistoryMax
+ } else {
+ historyMax = opts.History.maxSize
+ }
+ setHistory := func(path string) {
+ h, e := NewHistory(path, historyMax)
+ if e != nil {
+ errorExit(e.Error())
+ }
+ opts.History = h
+ }
+ setHistoryMax := func(max int) {
+ historyMax = max
+ if historyMax < 1 {
+ errorExit("history max must be a positive integer")
+ }
+ if opts.History != nil {
+ opts.History.maxSize = historyMax
+ }
+ }
+ for i := 0; i < len(allArgs); i++ {
+ arg := allArgs[i]
+ switch arg {
+ case "-h", "--help":
+ help(exitOk)
+ case "-x", "--extended":
+ opts.Mode = ModeExtended
+ case "-e", "--extended-exact":
+ opts.Mode = ModeExtendedExact
+ case "+x", "--no-extended", "+e", "--no-extended-exact":
+ opts.Mode = ModeFuzzy
+ case "-q", "--query":
+ opts.Query = nextString(allArgs, &i, "query string required")
+ case "-f", "--filter":
+ filter := nextString(allArgs, &i, "query string required")
+ opts.Filter = &filter
+ case "--expect":
+ opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required")
+ case "--tiebreak":
+ opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
+ case "--bind":
+ keymap, opts.Execmap, opts.ToggleSort =
+ parseKeymap(keymap, opts.Execmap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required"))
+ case "--color":
+ spec := optionalNextString(allArgs, &i)
+ if len(spec) == 0 {
+ opts.Theme = defaultTheme()
+ } else {
+ opts.Theme = parseTheme(opts.Theme, spec)
+ }
+ case "--toggle-sort":
+ keymap = checkToggleSort(keymap, nextString(allArgs, &i, "key name required"))
+ opts.ToggleSort = true
+ case "-d", "--delimiter":
+ opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required"))
+ case "-n", "--nth":
+ opts.Nth = splitNth(nextString(allArgs, &i, "nth expression required"))
+ case "--with-nth":
+ opts.WithNth = splitNth(nextString(allArgs, &i, "nth expression required"))
+ case "-s", "--sort":
+ opts.Sort = optionalNumeric(allArgs, &i)
+ case "+s", "--no-sort":
+ opts.Sort = 0
+ case "--tac":
+ opts.Tac = true
+ case "--no-tac":
+ opts.Tac = false
+ case "-i":
+ opts.Case = CaseIgnore
+ case "+i":
+ opts.Case = CaseRespect
+ case "-m", "--multi":
+ opts.Multi = true
+ case "+m", "--no-multi":
+ opts.Multi = false
+ case "--ansi":
+ opts.Ansi = true
+ case "--no-ansi":
+ opts.Ansi = false
+ case "--no-mouse":
+ opts.Mouse = false
+ case "+c", "--no-color":
+ opts.Theme = nil
+ case "+2", "--no-256":
+ opts.Theme = curses.Default16
+ case "--black":
+ opts.Black = true
+ case "--no-black":
+ opts.Black = false
+ case "--reverse":
+ opts.Reverse = true
+ case "--no-reverse":
+ opts.Reverse = false
+ case "--cycle":
+ opts.Cycle = true
+ case "--no-cycle":
+ opts.Cycle = false
+ case "--hscroll":
+ opts.Hscroll = true
+ case "--no-hscroll":
+ opts.Hscroll = false
+ case "--inline-info":
+ opts.InlineInfo = true
+ case "--no-inline-info":
+ opts.InlineInfo = false
+ case "-1", "--select-1":
+ opts.Select1 = true
+ case "+1", "--no-select-1":
+ opts.Select1 = false
+ case "-0", "--exit-0":
+ opts.Exit0 = true
+ case "+0", "--no-exit-0":
+ opts.Exit0 = false
+ case "--read0":
+ opts.ReadZero = true
+ case "--no-read0":
+ opts.ReadZero = false
+ case "--print-query":
+ opts.PrintQuery = true
+ case "--no-print-query":
+ opts.PrintQuery = false
+ case "--prompt":
+ opts.Prompt = nextString(allArgs, &i, "prompt string required")
+ case "--sync":
+ opts.Sync = true
+ case "--no-sync":
+ opts.Sync = false
+ case "--async":
+ opts.Sync = false
+ case "--no-history":
+ opts.History = nil
+ case "--history":
+ setHistory(nextString(allArgs, &i, "history file path required"))
+ case "--history-size":
+ setHistoryMax(nextInt(allArgs, &i, "history max size required"))
+ case "--no-header":
+ opts.Header = []string{}
+ case "--no-header-lines":
+ opts.HeaderLines = 0
+ case "--header":
+ opts.Header = strLines(nextString(allArgs, &i, "header string required"))
+ case "--header-lines":
+ opts.HeaderLines = atoi(
+ nextString(allArgs, &i, "number of header lines required"))
+ case "--no-margin":
+ opts.Margin = defaultMargin()
+ case "--margin":
+ opts.Margin = parseMargin(
+ nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
+ case "--version":
+ opts.Version = true
+ default:
+ if match, value := optString(arg, "-q", "--query="); match {
+ opts.Query = value
+ } else if match, value := optString(arg, "-f", "--filter="); match {
+ opts.Filter = &value
+ } else if match, value := optString(arg, "-d", "--delimiter="); match {
+ opts.Delimiter = delimiterRegexp(value)
+ } else if match, value := optString(arg, "--prompt="); match {
+ opts.Prompt = value
+ } else if match, value := optString(arg, "-n", "--nth="); match {
+ opts.Nth = splitNth(value)
+ } else if match, value := optString(arg, "--with-nth="); match {
+ opts.WithNth = splitNth(value)
+ } else if match, _ := optString(arg, "-s", "--sort="); match {
+ opts.Sort = 1 // Don't care
+ } else if match, value := optString(arg, "--toggle-sort="); match {
+ keymap = checkToggleSort(keymap, value)
+ opts.ToggleSort = true
+ } else if match, value := optString(arg, "--expect="); match {
+ opts.Expect = parseKeyChords(value, "key names required")
+ } else if match, value := optString(arg, "--tiebreak="); match {
+ opts.Tiebreak = parseTiebreak(value)
+ } else if match, value := optString(arg, "--color="); match {
+ opts.Theme = parseTheme(opts.Theme, value)
+ } else if match, value := optString(arg, "--bind="); match {
+ keymap, opts.Execmap, opts.ToggleSort =
+ parseKeymap(keymap, opts.Execmap, opts.ToggleSort, value)
+ } else if match, value := optString(arg, "--history="); match {
+ setHistory(value)
+ } else if match, value := optString(arg, "--history-size="); match {
+ setHistoryMax(atoi(value))
+ } else if match, value := optString(arg, "--header="); match {
+ opts.Header = strLines(value)
+ } else if match, value := optString(arg, "--header-lines="); match {
+ opts.HeaderLines = atoi(value)
+ } else if match, value := optString(arg, "--margin="); match {
+ opts.Margin = parseMargin(value)
+ } else {
+ errorExit("unknown option: " + arg)
+ }
+ }
+ }
+
+ if opts.HeaderLines < 0 {
+ errorExit("header lines must be a non-negative integer")
+ }
+
+ // Change default actions for CTRL-N / CTRL-P when --history is used
+ if opts.History != nil {
+ if _, prs := keymap[curses.CtrlP]; !prs {
+ keymap[curses.CtrlP] = actPreviousHistory
+ }
+ if _, prs := keymap[curses.CtrlN]; !prs {
+ keymap[curses.CtrlN] = actNextHistory
+ }
+ }
+
+ // Override default key bindings
+ for key, act := range keymap {
+ opts.Keymap[key] = act
+ }
+
+ // If we're not using extended search mode, --nth option becomes irrelevant
+ // if it contains the whole range
+ if opts.Mode == ModeFuzzy || len(opts.Nth) == 1 {
+ for _, r := range opts.Nth {
+ if r.begin == rangeEllipsis && r.end == rangeEllipsis {
+ opts.Nth = make([]Range, 0)
+ return
+ }
+ }
+ }
+}
+
+// ParseOptions parses command-line options
+func ParseOptions() *Options {
+ opts := defaultOptions()
+
+ // Options from Env var
+ words, _ := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS"))
+ parseOptions(opts, words)
+
+ // Options from command-line arguments
+ parseOptions(opts, os.Args[1:])
+ return opts
+}
diff --git a/src/options_test.go b/src/options_test.go
new file mode 100644
index 0000000..1f96f78
--- /dev/null
+++ b/src/options_test.go
@@ -0,0 +1,318 @@
+package fzf
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/junegunn/fzf/src/curses"
+)
+
+func TestDelimiterRegex(t *testing.T) {
+ // Valid regex
+ delim := delimiterRegexp(".")
+ if delim.regex == nil || delim.str != nil {
+ t.Error(delim)
+ }
+ // Broken regex -> string
+ delim = delimiterRegexp("[0-9")
+ if delim.regex != nil || *delim.str != "[0-9" {
+ t.Error(delim)
+ }
+ // Valid regex
+ delim = delimiterRegexp("[0-9]")
+ if delim.regex.String() != "[0-9]" || delim.str != nil {
+ t.Error(delim)
+ }
+ // Tab character
+ delim = delimiterRegexp("\t")
+ if delim.regex != nil || *delim.str != "\t" {
+ t.Error(delim)
+ }
+ // Tab expression
+ delim = delimiterRegexp("\\t")
+ if delim.regex != nil || *delim.str != "\t" {
+ t.Error(delim)
+ }
+ // Tabs -> regex
+ delim = delimiterRegexp("\t+")
+ if delim.regex == nil || delim.str != nil {
+ t.Error(delim)
+ }
+}
+
+func TestDelimiterRegexString(t *testing.T) {
+ delim := delimiterRegexp("*")
+ tokens := Tokenize([]rune("-*--*---**---"), delim)
+ if delim.regex != nil ||
+ string(tokens[0].text) != "-*" ||
+ string(tokens[1].text) != "--*" ||
+ string(tokens[2].text) != "---*" ||
+ string(tokens[3].text) != "*" ||
+ string(tokens[4].text) != "---" {
+ t.Errorf("%s %s %d", delim, tokens, len(tokens))
+ }
+}
+
+func TestDelimiterRegexRegex(t *testing.T) {
+ delim := delimiterRegexp("--\\*")
+ tokens := Tokenize([]rune("-*--*---**---"), delim)
+ if delim.str != nil ||
+ string(tokens[0].text) != "-*--*" ||
+ string(tokens[1].text) != "---*" ||
+ string(tokens[2].text) != "*---" {
+ t.Errorf("%s %d", tokens, len(tokens))
+ }
+}
+
+func TestSplitNth(t *testing.T) {
+ {
+ ranges := splitNth("..")
+ if len(ranges) != 1 ||
+ ranges[0].begin != rangeEllipsis ||
+ ranges[0].end != rangeEllipsis {
+ t.Errorf("%s", ranges)
+ }
+ }
+ {
+ ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1")
+ if len(ranges) != 10 ||
+ ranges[0].begin != rangeEllipsis || ranges[0].end != 3 ||
+ ranges[1].begin != rangeEllipsis || ranges[1].end != rangeEllipsis ||
+ ranges[2].begin != 2 || ranges[2].end != 3 ||
+ ranges[3].begin != 4 || ranges[3].end != rangeEllipsis ||
+ ranges[4].begin != -3 || ranges[4].end != -2 ||
+ ranges[5].begin != rangeEllipsis || ranges[5].end != rangeEllipsis ||
+ ranges[6].begin != 2 || ranges[6].end != 2 ||
+ ranges[7].begin != -2 || ranges[7].end != -2 ||
+ ranges[8].begin != 2 || ranges[8].end != -2 ||
+ ranges[9].begin != rangeEllipsis || ranges[9].end != rangeEllipsis {
+ t.Errorf("%s", ranges)
+ }
+ }
+}
+
+func TestIrrelevantNth(t *testing.T) {
+ {
+ opts := defaultOptions()
+ words := []string{"--nth", "..", "-x"}
+ parseOptions(opts, words)
+ if len(opts.Nth) != 0 {
+ t.Errorf("nth should be empty: %s", opts.Nth)
+ }
+ }
+ for _, words := range [][]string{[]string{"--nth", "..,3"}, []string{"--nth", "3,1.."}, []string{"--nth", "..-1,1"}} {
+ {
+ opts := defaultOptions()
+ parseOptions(opts, words)
+ if len(opts.Nth) != 0 {
+ t.Errorf("nth should be empty: %s", opts.Nth)
+ }
+ }
+ {
+ opts := defaultOptions()
+ words = append(words, "-x")
+ parseOptions(opts, words)
+ if len(opts.Nth) != 2 {
+ t.Errorf("nth should not be empty: %s", opts.Nth)
+ }
+ }
+ }
+}
+
+func TestParseKeys(t *testing.T) {
+ pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g", "")
+ check := func(i int, s string) {
+ if pairs[i] != s {
+ t.Errorf("%s != %s", pairs[i], s)
+ }
+ }
+ if len(pairs) != 9 {
+ t.Error(9)
+ }
+ check(curses.CtrlZ, "ctrl-z")
+ check(curses.AltZ, "alt-z")
+ check(curses.F2, "f2")
+ check(curses.AltZ+'@', "@")
+ check(curses.AltA, "Alt-a")
+ check(curses.AltZ+'!', "!")
+ check(curses.CtrlA+'g'-'a', "ctrl-G")
+ check(curses.AltZ+'J', "J")
+ check(curses.AltZ+'g', "g")
+
+ // Synonyms
+ pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
+ if len(pairs) != 9 {
+ t.Error(9)
+ }
+ check(curses.CtrlM, "Return")
+ check(curses.AltZ+' ', "space")
+ check(curses.Tab, "tab")
+ check(curses.BTab, "btab")
+ check(curses.ESC, "esc")
+ check(curses.Up, "up")
+ check(curses.Down, "down")
+ check(curses.Left, "left")
+ check(curses.Right, "right")
+
+ pairs = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
+ if len(pairs) != 11 {
+ t.Error(11)
+ }
+ check(curses.Tab, "Ctrl-I")
+ check(curses.PgUp, "page-up")
+ check(curses.PgDn, "Page-Down")
+ check(curses.Home, "Home")
+ check(curses.End, "End")
+ check(curses.AltBS, "Alt-BSpace")
+ check(curses.SLeft, "shift-left")
+ check(curses.SRight, "shift-right")
+ check(curses.BTab, "shift-tab")
+ check(curses.CtrlM, "Enter")
+ check(curses.BSpace, "bspace")
+}
+
+func TestParseKeysWithComma(t *testing.T) {
+ checkN := func(a int, b int) {
+ if a != b {
+ t.Errorf("%d != %d", a, b)
+ }
+ }
+ check := func(pairs map[int]string, i int, s string) {
+ if pairs[i] != s {
+ t.Errorf("%s != %s", pairs[i], s)
+ }
+ }
+
+ pairs := parseKeyChords(",", "")
+ checkN(len(pairs), 1)
+ check(pairs, curses.AltZ+',', ",")
+
+ pairs = parseKeyChords(",,a,b", "")
+ checkN(len(pairs), 3)
+ check(pairs, curses.AltZ+'a', "a")
+ check(pairs, curses.AltZ+'b', "b")
+ check(pairs, curses.AltZ+',', ",")
+
+ pairs = parseKeyChords("a,b,,", "")
+ checkN(len(pairs), 3)
+ check(pairs, curses.AltZ+'a', "a")
+ check(pairs, curses.AltZ+'b', "b")
+ check(pairs, curses.AltZ+',', ",")
+
+ pairs = parseKeyChords("a,,,b", "")
+ checkN(len(pairs), 3)
+ check(pairs, curses.AltZ+'a', "a")
+ check(pairs, curses.AltZ+'b', "b")
+ check(pairs, curses.AltZ+',', ",")
+
+ pairs = parseKeyChords("a,,,b,c", "")
+ checkN(len(pairs), 4)
+ check(pairs, curses.AltZ+'a', "a")
+ check(pairs, curses.AltZ+'b', "b")
+ check(pairs, curses.AltZ+'c', "c")
+ check(pairs, curses.AltZ+',', ",")
+
+ pairs = parseKeyChords(",,,", "")
+ checkN(len(pairs), 1)
+ check(pairs, curses.AltZ+',', ",")
+}
+
+func TestBind(t *testing.T) {
+ check := func(action actionType, expected actionType) {
+ if action != expected {
+ t.Errorf("%d != %d", action, expected)
+ }
+ }
+ checkString := func(action string, expected string) {
+ if action != expected {
+ t.Errorf("%d != %d", action, expected)
+ }
+ }
+ keymap := defaultKeymap()
+ execmap := make(map[int]string)
+ check(actBeginningOfLine, keymap[curses.CtrlA])
+ keymap, execmap, toggleSort :=
+ parseKeymap(keymap, execmap, false,
+ "ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+
+ "f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
+ "alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+
+ ",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)")
+ if !toggleSort {
+ t.Errorf("toggleSort not set")
+ }
+ check(actKillLine, keymap[curses.CtrlA])
+ check(actToggleSort, keymap[curses.CtrlB])
+ check(actPageUp, keymap[curses.AltZ+'c'])
+ check(actAbort, keymap[curses.AltZ+','])
+ check(actAccept, keymap[curses.AltZ+':'])
+ check(actPageDown, keymap[curses.AltZ])
+ check(actExecute, keymap[curses.F1])
+ check(actExecute, keymap[curses.F2])
+ check(actExecute, keymap[curses.F3])
+ check(actExecute, keymap[curses.F4])
+ checkString("ls {}", execmap[curses.F1])
+ checkString("echo {}, {}, {}", execmap[curses.F2])
+ checkString("echo '({})'", execmap[curses.F3])
+ checkString("less {}", execmap[curses.F4])
+ checkString("echo (,),[,],/,:,;,%,{}", execmap[curses.AltA])
+ checkString("echo (,),[,],/,:,@,%,{}", execmap[curses.AltB])
+ checkString("\nfoobar,Y:execute(baz)", execmap[curses.AltZ+'X'])
+
+ for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
+ keymap, execmap, toggleSort =
+ parseKeymap(keymap, execmap, false, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
+ checkString("foobar", execmap[curses.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])])
+ }
+
+ keymap, execmap, toggleSort = parseKeymap(keymap, execmap, 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")
+ }
+}
diff --git a/src/pattern.go b/src/pattern.go
new file mode 100644
index 0000000..5466b86
--- /dev/null
+++ b/src/pattern.go
@@ -0,0 +1,338 @@
+package fzf
+
+import (
+ "regexp"
+ "sort"
+ "strings"
+
+ "github.com/junegunn/fzf/src/algo"
+)
+
+// fuzzy
+// 'exact
+// ^exact-prefix
+// exact-suffix$
+// !not-fuzzy
+// !'not-exact
+// !^not-exact-prefix
+// !not-exact-suffix$
+
+type termType int
+
+const (
+ termFuzzy termType = iota
+ termExact
+ termPrefix
+ termSuffix
+ termEqual
+)
+
+type term struct {
+ typ termType
+ inv bool
+ text []rune
+ caseSensitive bool
+ origText []rune
+}
+
+// Pattern represents search pattern
+type Pattern struct {
+ mode Mode
+ caseSensitive bool
+ forward bool
+ text []rune
+ terms []term
+ hasInvTerm bool
+ delimiter Delimiter
+ nth []Range
+ procFun map[termType]func(bool, bool, []rune, []rune) (int, int)
+}
+
+var (
+ _patternCache map[string]*Pattern
+ _splitRegex *regexp.Regexp
+ _cache ChunkCache
+)
+
+func init() {
+ _splitRegex = regexp.MustCompile("\\s+")
+ clearPatternCache()
+ clearChunkCache()
+}
+
+func clearPatternCache() {
+ // We can uniquely identify the pattern for a given string since
+ // mode and caseMode do not change while the program is running
+ _patternCache = make(map[string]*Pattern)
+}
+
+func clearChunkCache() {
+ _cache = NewChunkCache()
+}
+
+// BuildPattern builds Pattern object from the given arguments
+func BuildPattern(mode Mode, caseMode Case, forward bool,
+ nth []Range, delimiter Delimiter, runes []rune) *Pattern {
+
+ var asString string
+ switch mode {
+ case ModeExtended, ModeExtendedExact:
+ asString = strings.Trim(string(runes), " ")
+ default:
+ asString = string(runes)
+ }
+
+ cached, found := _patternCache[asString]
+ if found {
+ return cached
+ }
+
+ caseSensitive, hasInvTerm := true, false
+ terms := []term{}
+
+ switch mode {
+ case ModeExtended, ModeExtendedExact:
+ terms = parseTerms(mode, caseMode, asString)
+ for _, term := range terms {
+ if term.inv {
+ hasInvTerm = true
+ }
+ }
+ default:
+ lowerString := strings.ToLower(asString)
+ caseSensitive = caseMode == CaseRespect ||
+ caseMode == CaseSmart && lowerString != asString
+ if !caseSensitive {
+ asString = lowerString
+ }
+ }
+
+ ptr := &Pattern{
+ mode: mode,
+ caseSensitive: caseSensitive,
+ forward: forward,
+ text: []rune(asString),
+ terms: terms,
+ hasInvTerm: hasInvTerm,
+ nth: nth,
+ delimiter: delimiter,
+ procFun: make(map[termType]func(bool, bool, []rune, []rune) (int, int))}
+
+ ptr.procFun[termFuzzy] = algo.FuzzyMatch
+ ptr.procFun[termEqual] = algo.EqualMatch
+ ptr.procFun[termExact] = algo.ExactMatchNaive
+ ptr.procFun[termPrefix] = algo.PrefixMatch
+ ptr.procFun[termSuffix] = algo.SuffixMatch
+
+ _patternCache[asString] = ptr
+ return ptr
+}
+
+func parseTerms(mode Mode, caseMode Case, str string) []term {
+ tokens := _splitRegex.Split(str, -1)
+ terms := []term{}
+ for _, token := range tokens {
+ typ, inv, text := termFuzzy, false, token
+ lowerText := strings.ToLower(text)
+ caseSensitive := caseMode == CaseRespect ||
+ caseMode == CaseSmart && text != lowerText
+ if !caseSensitive {
+ text = lowerText
+ }
+ origText := []rune(text)
+ if mode == ModeExtendedExact {
+ typ = termExact
+ }
+
+ if strings.HasPrefix(text, "!") {
+ inv = true
+ text = text[1:]
+ }
+
+ if strings.HasPrefix(text, "'") {
+ if mode == ModeExtended {
+ typ = termExact
+ text = text[1:]
+ } else if mode == ModeExtendedExact {
+ typ = termFuzzy
+ text = text[1:]
+ }
+ } else if strings.HasPrefix(text, "^") {
+ if strings.HasSuffix(text, "$") {
+ typ = termEqual
+ text = text[1 : len(text)-1]
+ } else {
+ typ = termPrefix
+ text = text[1:]
+ }
+ } else if strings.HasSuffix(text, "$") {
+ typ = termSuffix
+ text = text[:len(text)-1]
+ }
+
+ if len(text) > 0 {
+ terms = append(terms, term{
+ typ: typ,
+ inv: inv,
+ text: []rune(text),
+ caseSensitive: caseSensitive,
+ origText: origText})
+ }
+ }
+ return terms
+}
+
+// IsEmpty returns true if the pattern is effectively empty
+func (p *Pattern) IsEmpty() bool {
+ if p.mode == ModeFuzzy {
+ return len(p.text) == 0
+ }
+ return len(p.terms) == 0
+}
+
+// AsString returns the search query in string type
+func (p *Pattern) AsString() string {
+ return string(p.text)
+}
+
+// CacheKey is used to build string to be used as the key of result cache
+func (p *Pattern) CacheKey() string {
+ if p.mode == ModeFuzzy {
+ return p.AsString()
+ }
+ cacheableTerms := []string{}
+ for _, term := range p.terms {
+ if term.inv {
+ continue
+ }
+ cacheableTerms = append(cacheableTerms, string(term.origText))
+ }
+ return strings.Join(cacheableTerms, " ")
+}
+
+// Match returns the list of matches Items in the given Chunk
+func (p *Pattern) Match(chunk *Chunk) []*Item {
+ space := chunk
+
+ // ChunkCache: Exact match
+ cacheKey := p.CacheKey()
+ if !p.hasInvTerm { // Because we're excluding Inv-term from cache key
+ if cached, found := _cache.Find(chunk, cacheKey); found {
+ return cached
+ }
+ }
+
+ // ChunkCache: Prefix/suffix match
+Loop:
+ for idx := 1; idx < len(cacheKey); idx++ {
+ // [---------| ] | [ |---------]
+ // [--------| ] | [ |--------]
+ // [-------| ] | [ |-------]
+ prefix := cacheKey[:len(cacheKey)-idx]
+ suffix := cacheKey[idx:]
+ for _, substr := range [2]*string{&prefix, &suffix} {
+ if cached, found := _cache.Find(chunk, *substr); found {
+ cachedChunk := Chunk(cached)
+ space = &cachedChunk
+ break Loop
+ }
+ }
+ }
+
+ matches := p.matchChunk(space)
+
+ if !p.hasInvTerm {
+ _cache.Add(chunk, cacheKey, matches)
+ }
+ return matches
+}
+
+func (p *Pattern) matchChunk(chunk *Chunk) []*Item {
+ matches := []*Item{}
+ if p.mode == ModeFuzzy {
+ for _, item := range *chunk {
+ if sidx, eidx := p.fuzzyMatch(item); sidx >= 0 {
+ matches = append(matches,
+ dupItem(item, []Offset{Offset{int32(sidx), int32(eidx)}}))
+ }
+ }
+ } else {
+ for _, item := range *chunk {
+ if offsets := p.extendedMatch(item); len(offsets) == len(p.terms) {
+ matches = append(matches, dupItem(item, offsets))
+ }
+ }
+ }
+ return matches
+}
+
+// MatchItem returns true if the Item is a match
+func (p *Pattern) MatchItem(item *Item) bool {
+ if p.mode == ModeFuzzy {
+ sidx, _ := p.fuzzyMatch(item)
+ return sidx >= 0
+ }
+ offsets := p.extendedMatch(item)
+ return len(offsets) == len(p.terms)
+}
+
+func dupItem(item *Item, offsets []Offset) *Item {
+ sort.Sort(ByOrder(offsets))
+ return &Item{
+ text: item.text,
+ origText: item.origText,
+ transformed: item.transformed,
+ index: item.index,
+ offsets: offsets,
+ colors: item.colors,
+ rank: Rank{0, 0, item.index}}
+}
+
+func (p *Pattern) fuzzyMatch(item *Item) (int, int) {
+ input := p.prepareInput(item)
+ return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text)
+}
+
+func (p *Pattern) extendedMatch(item *Item) []Offset {
+ input := p.prepareInput(item)
+ offsets := []Offset{}
+ for _, term := range p.terms {
+ pfun := p.procFun[term.typ]
+ if sidx, eidx := p.iter(pfun, input, term.caseSensitive, p.forward, term.text); sidx >= 0 {
+ if term.inv {
+ break
+ }
+ offsets = append(offsets, Offset{int32(sidx), int32(eidx)})
+ } else if term.inv {
+ offsets = append(offsets, Offset{0, 0})
+ }
+ }
+ return offsets
+}
+
+func (p *Pattern) prepareInput(item *Item) []Token {
+ if item.transformed != nil {
+ return item.transformed
+ }
+
+ var ret []Token
+ if len(p.nth) > 0 {
+ tokens := Tokenize(item.text, p.delimiter)
+ ret = Transform(tokens, p.nth)
+ } else {
+ ret = []Token{Token{text: item.text, prefixLength: 0}}
+ }
+ item.transformed = ret
+ return ret
+}
+
+func (p *Pattern) iter(pfun func(bool, bool, []rune, []rune) (int, int),
+ tokens []Token, caseSensitive bool, forward bool, pattern []rune) (int, int) {
+ for _, part := range tokens {
+ prefixLength := part.prefixLength
+ if sidx, eidx := pfun(caseSensitive, forward, part.text, pattern); sidx >= 0 {
+ return sidx + prefixLength, eidx + prefixLength
+ }
+ }
+ return -1, -1
+}
diff --git a/src/pattern_test.go b/src/pattern_test.go
new file mode 100644
index 0000000..d508612
--- /dev/null
+++ b/src/pattern_test.go
@@ -0,0 +1,132 @@
+package fzf
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/junegunn/fzf/src/algo"
+)
+
+func TestParseTermsExtended(t *testing.T) {
+ terms := parseTerms(ModeExtended, CaseSmart,
+ "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ ^iii$")
+ if len(terms) != 9 ||
+ terms[0].typ != termFuzzy || terms[0].inv ||
+ terms[1].typ != termExact || terms[1].inv ||
+ terms[2].typ != termPrefix || terms[2].inv ||
+ terms[3].typ != termSuffix || terms[3].inv ||
+ terms[4].typ != termFuzzy || !terms[4].inv ||
+ terms[5].typ != termExact || !terms[5].inv ||
+ terms[6].typ != termPrefix || !terms[6].inv ||
+ terms[7].typ != termSuffix || !terms[7].inv ||
+ terms[8].typ != termEqual || terms[8].inv {
+ t.Errorf("%s", terms)
+ }
+ for idx, term := range terms {
+ if len(term.text) != 3 {
+ t.Errorf("%s", term)
+ }
+ if idx > 0 && len(term.origText) != 4+idx/5 {
+ t.Errorf("%s", term)
+ }
+ }
+}
+
+func TestParseTermsExtendedExact(t *testing.T) {
+ terms := parseTerms(ModeExtendedExact, CaseSmart,
+ "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$")
+ if len(terms) != 8 ||
+ terms[0].typ != termExact || terms[0].inv || len(terms[0].text) != 3 ||
+ terms[1].typ != termFuzzy || terms[1].inv || len(terms[1].text) != 3 ||
+ terms[2].typ != termPrefix || terms[2].inv || len(terms[2].text) != 3 ||
+ terms[3].typ != termSuffix || terms[3].inv || len(terms[3].text) != 3 ||
+ terms[4].typ != termExact || !terms[4].inv || len(terms[4].text) != 3 ||
+ terms[5].typ != termFuzzy || !terms[5].inv || len(terms[5].text) != 3 ||
+ terms[6].typ != termPrefix || !terms[6].inv || len(terms[6].text) != 3 ||
+ terms[7].typ != termSuffix || !terms[7].inv || len(terms[7].text) != 3 {
+ t.Errorf("%s", terms)
+ }
+}
+
+func TestParseTermsEmpty(t *testing.T) {
+ terms := parseTerms(ModeExtended, CaseSmart, "' $ ^ !' !^ !$")
+ if len(terms) != 0 {
+ t.Errorf("%s", terms)
+ }
+}
+
+func TestExact(t *testing.T) {
+ defer clearPatternCache()
+ clearPatternCache()
+ pattern := BuildPattern(ModeExtended, CaseSmart, true,
+ []Range{}, Delimiter{}, []rune("'abc"))
+ sidx, eidx := algo.ExactMatchNaive(
+ pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), pattern.terms[0].text)
+ if sidx != 7 || eidx != 10 {
+ t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx)
+ }
+}
+
+func TestEqual(t *testing.T) {
+ defer clearPatternCache()
+ clearPatternCache()
+ pattern := BuildPattern(ModeExtended, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$"))
+
+ match := func(str string, sidxExpected int, eidxExpected int) {
+ sidx, eidx := algo.EqualMatch(
+ pattern.caseSensitive, pattern.forward, []rune(str), pattern.terms[0].text)
+ if sidx != sidxExpected || eidx != eidxExpected {
+ t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx)
+ }
+ }
+ match("ABC", -1, -1)
+ match("AbC", 0, 3)
+}
+
+func TestCaseSensitivity(t *testing.T) {
+ defer clearPatternCache()
+ clearPatternCache()
+ pat1 := BuildPattern(ModeFuzzy, CaseSmart, true, []Range{}, Delimiter{}, []rune("abc"))
+ clearPatternCache()
+ pat2 := BuildPattern(ModeFuzzy, CaseSmart, true, []Range{}, Delimiter{}, []rune("Abc"))
+ clearPatternCache()
+ pat3 := BuildPattern(ModeFuzzy, CaseIgnore, true, []Range{}, Delimiter{}, []rune("abc"))
+ clearPatternCache()
+ pat4 := BuildPattern(ModeFuzzy, CaseIgnore, true, []Range{}, Delimiter{}, []rune("Abc"))
+ clearPatternCache()
+ pat5 := BuildPattern(ModeFuzzy, CaseRespect, true, []Range{}, Delimiter{}, []rune("abc"))
+ clearPatternCache()
+ pat6 := BuildPattern(ModeFuzzy, CaseRespect, true, []Range{}, Delimiter{}, []rune("Abc"))
+
+ if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
+ string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
+ string(pat3.text) != "abc" || pat3.caseSensitive != false ||
+ string(pat4.text) != "abc" || pat4.caseSensitive != false ||
+ string(pat5.text) != "abc" || pat5.caseSensitive != true ||
+ string(pat6.text) != "Abc" || pat6.caseSensitive != true {
+ t.Error("Invalid case conversion")
+ }
+}
+
+func TestOrigTextAndTransformed(t *testing.T) {
+ pattern := BuildPattern(ModeExtended, CaseSmart, true, []Range{}, Delimiter{}, []rune("jg"))
+ tokens := Tokenize([]rune("junegunn"), Delimiter{})
+ trans := Transform(tokens, []Range{Range{1, 1}})
+
+ origRunes := []rune("junegunn.choi")
+ for _, mode := range []Mode{ModeFuzzy, ModeExtended} {
+ chunk := Chunk{
+ &Item{
+ text: []rune("junegunn"),
+ origText: &origRunes,
+ transformed: trans},
+ }
+ pattern.mode = mode
+ matches := pattern.matchChunk(&chunk)
+ if string(matches[0].text) != "junegunn" || string(*matches[0].origText) != "junegunn.choi" ||
+ matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 ||
+ !reflect.DeepEqual(matches[0].transformed, trans) {
+ t.Error("Invalid match result", matches)
+ }
+ }
+}
diff --git a/src/reader.go b/src/reader.go
new file mode 100644
index 0000000..3e2cf0a
--- /dev/null
+++ b/src/reader.go
@@ -0,0 +1,73 @@
+package fzf
+
+import (
+ "bufio"
+ "io"
+ "os"
+ "os/exec"
+
+ "github.com/junegunn/fzf/src/util"
+)
+
+// Reader reads from command or standard input
+type Reader struct {
+ pusher func([]byte) bool
+ eventBox *util.EventBox
+ delimNil bool
+}
+
+// ReadSource reads data from the default command or from standard input
+func (r *Reader) ReadSource() {
+ if util.IsTty() {
+ cmd := os.Getenv("FZF_DEFAULT_COMMAND")
+ if len(cmd) == 0 {
+ cmd = defaultCommand
+ }
+ r.readFromCommand(cmd)
+ } else {
+ r.readFromStdin()
+ }
+ r.eventBox.Set(EvtReadFin, nil)
+}
+
+func (r *Reader) feed(src io.Reader) {
+ delim := byte('\n')
+ if r.delimNil {
+ delim = '\000'
+ }
+ reader := bufio.NewReader(src)
+ for {
+ // ReadBytes returns err != nil if and only if the returned data does not
+ // end in delim.
+ bytea, err := reader.ReadBytes(delim)
+ if len(bytea) > 0 {
+ if err == nil {
+ bytea = bytea[:len(bytea)-1]
+ }
+ if r.pusher(bytea) {
+ r.eventBox.Set(EvtReadNew, nil)
+ }
+ }
+ if err != nil {
+ break
+ }
+ }
+}
+
+func (r *Reader) readFromStdin() {
+ r.feed(os.Stdin)
+}
+
+func (r *Reader) readFromCommand(cmd string) {
+ listCommand := exec.Command("sh", "-c", cmd)
+ out, err := listCommand.StdoutPipe()
+ if err != nil {
+ return
+ }
+ err = listCommand.Start()
+ if err != nil {
+ return
+ }
+ defer listCommand.Wait()
+ r.feed(out)
+}
diff --git a/src/reader_test.go b/src/reader_test.go
new file mode 100644
index 0000000..d5c218c
--- /dev/null
+++ b/src/reader_test.go
@@ -0,0 +1,56 @@
+package fzf
+
+import (
+ "testing"
+
+ "github.com/junegunn/fzf/src/util"
+)
+
+func TestReadFromCommand(t *testing.T) {
+ strs := []string{}
+ eb := util.NewEventBox()
+ reader := Reader{
+ pusher: func(s []byte) bool { strs = append(strs, string(s)); return true },
+ eventBox: eb}
+
+ // Check EventBox
+ if eb.Peek(EvtReadNew) {
+ t.Error("EvtReadNew should not be set yet")
+ }
+
+ // Normal command
+ reader.readFromCommand(`echo abc && echo def`)
+ if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" {
+ t.Errorf("%s", strs)
+ }
+
+ // Check EventBox again
+ if !eb.Peek(EvtReadNew) {
+ t.Error("EvtReadNew should be set yet")
+ }
+
+ // Wait should return immediately
+ eb.Wait(func(events *util.Events) {
+ if _, found := (*events)[EvtReadNew]; !found {
+ t.Errorf("%s", events)
+ }
+ events.Clear()
+ })
+
+ // EventBox is cleared
+ if eb.Peek(EvtReadNew) {
+ t.Error("EvtReadNew should not be set yet")
+ }
+
+ // Failing command
+ reader.readFromCommand(`no-such-command`)
+ strs = []string{}
+ if len(strs) > 0 {
+ t.Errorf("%s", strs)
+ }
+
+ // Check EventBox again
+ if eb.Peek(EvtReadNew) {
+ t.Error("Command failed. EvtReadNew should be set")
+ }
+}
diff --git a/src/terminal.go b/src/terminal.go
new file mode 100644
index 0000000..9a92ba4
--- /dev/null
+++ b/src/terminal.go
@@ -0,0 +1,1119 @@
+package fzf
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "os/exec"
+ "os/signal"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "syscall"
+ "time"
+
+ C "github.com/junegunn/fzf/src/curses"
+ "github.com/junegunn/fzf/src/util"
+
+ "github.com/junegunn/go-runewidth"
+)
+
+// Terminal represents terminal input/output
+type Terminal struct {
+ inlineInfo bool
+ prompt string
+ reverse bool
+ hscroll bool
+ cx int
+ cy int
+ offset int
+ yanked []rune
+ input []rune
+ multi bool
+ sort bool
+ toggleSort bool
+ expect map[int]string
+ keymap map[int]actionType
+ execmap map[int]string
+ pressed string
+ printQuery bool
+ history *History
+ cycle bool
+ header []string
+ header0 []string
+ ansi bool
+ margin [4]string
+ marginInt [4]int
+ count int
+ progress int
+ reading bool
+ merger *Merger
+ selected map[uint32]selectedItem
+ reqBox *util.EventBox
+ eventBox *util.EventBox
+ mutex sync.Mutex
+ initFunc func()
+ suppress bool
+ startChan chan bool
+}
+
+type selectedItem struct {
+ at time.Time
+ text *string
+}
+
+type byTimeOrder []selectedItem
+
+func (a byTimeOrder) Len() int {
+ return len(a)
+}
+
+func (a byTimeOrder) Swap(i, j int) {
+ a[i], a[j] = a[j], a[i]
+}
+
+func (a byTimeOrder) Less(i, j int) bool {
+ return a[i].at.Before(a[j].at)
+}
+
+var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
+var _runeWidths = make(map[rune]int)
+
+const (
+ reqPrompt util.EventType = iota
+ reqInfo
+ reqHeader
+ reqList
+ reqRefresh
+ reqRedraw
+ reqClose
+ reqQuit
+)
+
+type actionType int
+
+const (
+ actIgnore actionType = iota
+ actInvalid
+ actRune
+ actMouse
+ actBeginningOfLine
+ actAbort
+ actAccept
+ actBackwardChar
+ actBackwardDeleteChar
+ actBackwardWord
+ actCancel
+ actClearScreen
+ actDeleteChar
+ actDeleteCharEOF
+ actEndOfLine
+ actForwardChar
+ actForwardWord
+ actKillLine
+ actKillWord
+ actUnixLineDiscard
+ actUnixWordRubout
+ actYank
+ actBackwardKillWord
+ actSelectAll
+ actDeselectAll
+ actToggle
+ actToggleAll
+ actToggleDown
+ actToggleUp
+ actDown
+ actUp
+ actPageUp
+ actPageDown
+ actToggleSort
+ actPreviousHistory
+ actNextHistory
+ actExecute
+)
+
+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] = actDeleteCharEOF
+ keymap[C.CtrlE] = actEndOfLine
+ keymap[C.CtrlF] = actForwardChar
+ keymap[C.CtrlH] = actBackwardDeleteChar
+ keymap[C.BSpace] = 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
+ keymap[C.PgUp] = actPageUp
+ keymap[C.PgDn] = actPageDown
+
+ keymap[C.Rune] = actRune
+ keymap[C.Mouse] = actMouse
+ return keymap
+}
+
+// NewTerminal returns new Terminal object
+func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
+ input := []rune(opts.Query)
+ var header []string
+ if opts.Reverse {
+ header = opts.Header
+ } else {
+ header = reverseStringArray(opts.Header)
+ }
+ return &Terminal{
+ inlineInfo: opts.InlineInfo,
+ prompt: opts.Prompt,
+ reverse: opts.Reverse,
+ hscroll: opts.Hscroll,
+ cx: len(input),
+ cy: 0,
+ offset: 0,
+ yanked: []rune{},
+ input: input,
+ multi: opts.Multi,
+ sort: opts.Sort > 0,
+ toggleSort: opts.ToggleSort,
+ expect: opts.Expect,
+ keymap: opts.Keymap,
+ execmap: opts.Execmap,
+ pressed: "",
+ printQuery: opts.PrintQuery,
+ history: opts.History,
+ margin: opts.Margin,
+ marginInt: [4]int{0, 0, 0, 0},
+ cycle: opts.Cycle,
+ header: header,
+ header0: header,
+ ansi: opts.Ansi,
+ reading: true,
+ merger: EmptyMerger,
+ selected: make(map[uint32]selectedItem),
+ reqBox: util.NewEventBox(),
+ eventBox: eventBox,
+ mutex: sync.Mutex{},
+ suppress: true,
+ startChan: make(chan bool, 1),
+ initFunc: func() {
+ C.Init(opts.Theme, opts.Black, opts.Mouse)
+ }}
+}
+
+// Input returns current query string
+func (t *Terminal) Input() []rune {
+ t.mutex.Lock()
+ defer t.mutex.Unlock()
+ return copySlice(t.input)
+}
+
+// UpdateCount updates the count information
+func (t *Terminal) UpdateCount(cnt int, final bool) {
+ t.mutex.Lock()
+ t.count = cnt
+ t.reading = !final
+ t.mutex.Unlock()
+ t.reqBox.Set(reqInfo, nil)
+ if final {
+ t.reqBox.Set(reqRefresh, nil)
+ }
+}
+
+func reverseStringArray(input []string) []string {
+ size := len(input)
+ reversed := make([]string, size)
+ for idx, str := range input {
+ reversed[size-idx-1] = str
+ }
+ return reversed
+}
+
+// UpdateHeader updates the header
+func (t *Terminal) UpdateHeader(header []string) {
+ t.mutex.Lock()
+ t.header = append(append([]string{}, t.header0...), header...)
+ t.mutex.Unlock()
+ t.reqBox.Set(reqHeader, nil)
+}
+
+// UpdateProgress updates the search progress
+func (t *Terminal) UpdateProgress(progress float32) {
+ t.mutex.Lock()
+ newProgress := int(progress * 100)
+ changed := t.progress != newProgress
+ t.progress = newProgress
+ t.mutex.Unlock()
+
+ if changed {
+ t.reqBox.Set(reqInfo, nil)
+ }
+}
+
+// UpdateList updates Merger to display the list
+func (t *Terminal) UpdateList(merger *Merger) {
+ t.mutex.Lock()
+ t.progress = 100
+ t.merger = merger
+ t.mutex.Unlock()
+ t.reqBox.Set(reqInfo, nil)
+ t.reqBox.Set(reqList, nil)
+}
+
+func (t *Terminal) output() bool {
+ if t.printQuery {
+ fmt.Println(string(t.input))
+ }
+ if len(t.expect) > 0 {
+ fmt.Println(t.pressed)
+ }
+ found := len(t.selected) > 0
+ if !found {
+ cnt := t.merger.Length()
+ if cnt > 0 && cnt > t.cy {
+ fmt.Println(t.merger.Get(t.cy).AsString(t.ansi))
+ found = true
+ }
+ } else {
+ sels := make([]selectedItem, 0, len(t.selected))
+ for _, sel := range t.selected {
+ sels = append(sels, sel)
+ }
+ sort.Sort(byTimeOrder(sels))
+ for _, sel := range sels {
+ fmt.Println(*sel.text)
+ }
+ }
+ return found
+}
+
+func runeWidth(r rune, prefixWidth int) int {
+ if r == '\t' {
+ return 8 - prefixWidth%8
+ } else if w, found := _runeWidths[r]; found {
+ return w
+ } else {
+ w := runewidth.RuneWidth(r)
+ _runeWidths[r] = w
+ return w
+ }
+}
+
+func displayWidth(runes []rune) int {
+ l := 0
+ for _, r := range runes {
+ l += runeWidth(r, l)
+ }
+ return l
+}
+
+const minWidth = 16
+const minHeight = 4
+
+func (t *Terminal) calculateMargins() {
+ screenWidth := C.MaxX()
+ screenHeight := C.MaxY()
+ for idx, str := range t.margin {
+ if str == "0" {
+ t.marginInt[idx] = 0
+ } else if strings.HasSuffix(str, "%") {
+ num, _ := strconv.ParseFloat(str[:len(str)-1], 64)
+ var val float64
+ if idx%2 == 0 {
+ val = float64(screenHeight)
+ } else {
+ val = float64(screenWidth)
+ }
+ t.marginInt[idx] = int(val * num * 0.01)
+ } else {
+ num, _ := strconv.Atoi(str)
+ t.marginInt[idx] = num
+ }
+ }
+ adjust := func(idx1 int, idx2 int, max int, min int) {
+ if max >= min {
+ margin := t.marginInt[idx1] + t.marginInt[idx2]
+ if max-margin < min {
+ desired := max - min
+ t.marginInt[idx1] = desired * t.marginInt[idx1] / margin
+ t.marginInt[idx2] = desired * t.marginInt[idx2] / margin
+ }
+ }
+ }
+ adjust(1, 3, screenWidth, minWidth)
+ adjust(0, 2, screenHeight, minHeight)
+}
+
+func (t *Terminal) move(y int, x int, clear bool) {
+ x += t.marginInt[3]
+ maxy := C.MaxY()
+ if !t.reverse {
+ y = maxy - y - 1 - t.marginInt[2]
+ } else {
+ y += t.marginInt[0]
+ }
+
+ if clear {
+ C.MoveAndClear(y, x)
+ } else {
+ C.Move(y, x)
+ }
+}
+
+func (t *Terminal) placeCursor() {
+ t.move(0, len(t.prompt)+displayWidth(t.input[:t.cx]), false)
+}
+
+func (t *Terminal) printPrompt() {
+ t.move(0, 0, true)
+ C.CPrint(C.ColPrompt, true, t.prompt)
+ C.CPrint(C.ColNormal, true, string(t.input))
+}
+
+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)
+ if t.reading {
+ duration := int64(spinnerDuration)
+ idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
+ C.CPrint(C.ColSpinner, true, _spinner[idx])
+ }
+ t.move(1, 2, false)
+ }
+
+ output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count)
+ if t.toggleSort {
+ if t.sort {
+ output += "/S"
+ } else {
+ output += " "
+ }
+ }
+ if t.multi && len(t.selected) > 0 {
+ output += fmt.Sprintf(" (%d)", len(t.selected))
+ }
+ if t.progress > 0 && t.progress < 100 {
+ output += fmt.Sprintf(" (%d%%)", t.progress)
+ }
+ C.CPrint(C.ColInfo, false, output)
+}
+
+func (t *Terminal) maxHeight() int {
+ return C.MaxY() - t.marginInt[0] - t.marginInt[2]
+}
+
+func (t *Terminal) printHeader() {
+ if len(t.header) == 0 {
+ return
+ }
+ max := t.maxHeight()
+ var state *ansiState
+ for idx, lineStr := range t.header {
+ line := idx + 2
+ if t.inlineInfo {
+ line--
+ }
+ if line >= max {
+ continue
+ }
+ trimmed, colors, newState := extractColor(lineStr, state)
+ state = newState
+ item := &Item{
+ text: []rune(trimmed),
+ index: 0,
+ colors: colors,
+ rank: Rank{0, 0, 0}}
+
+ t.move(line, 2, true)
+ t.printHighlighted(item, false, C.ColHeader, 0, false)
+ }
+}
+
+func (t *Terminal) printList() {
+ t.constrain()
+
+ maxy := t.maxItems()
+ count := t.merger.Length() - t.offset
+ for i := 0; i < maxy; i++ {
+ line := i + 2 + len(t.header)
+ if t.inlineInfo {
+ line--
+ }
+ t.move(line, 0, true)
+ if i < count {
+ t.printItem(t.merger.Get(i+t.offset), i == t.cy-t.offset)
+ }
+ }
+}
+
+func (t *Terminal) printItem(item *Item, current bool) {
+ _, selected := t.selected[item.index]
+ if current {
+ C.CPrint(C.ColCursor, true, ">")
+ if selected {
+ C.CPrint(C.ColSelected, true, ">")
+ } else {
+ C.CPrint(C.ColCurrent, true, " ")
+ }
+ t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch, true)
+ } else {
+ C.CPrint(C.ColCursor, true, " ")
+ if selected {
+ C.CPrint(C.ColSelected, true, ">")
+ } else {
+ C.Print(" ")
+ }
+ t.printHighlighted(item, false, 0, C.ColMatch, false)
+ }
+}
+
+func trimRight(runes []rune, width int) ([]rune, int) {
+ // We start from the beginning to handle tab characters
+ l := 0
+ for idx, r := range runes {
+ l += runeWidth(r, l)
+ if idx > 0 && l > width {
+ return runes[:idx], len(runes) - idx
+ }
+ }
+ return runes, 0
+}
+
+func displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int {
+ l := 0
+ for _, r := range runes {
+ l += runeWidth(r, l+prefixWidth)
+ if l > limit {
+ // Early exit
+ return l
+ }
+ }
+ return l
+}
+
+func trimLeft(runes []rune, width int) ([]rune, int32) {
+ currentWidth := displayWidth(runes)
+ var trimmed int32
+
+ for currentWidth > width && len(runes) > 0 {
+ runes = runes[1:]
+ trimmed++
+ currentWidth = displayWidthWithLimit(runes, 2, width)
+ }
+ return runes, trimmed
+}
+
+func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, current bool) {
+ var maxe int32
+ for _, offset := range item.offsets {
+ if offset[1] > maxe {
+ maxe = offset[1]
+ }
+ }
+
+ // Overflow
+ text := make([]rune, len(item.text))
+ copy(text, item.text)
+ offsets := item.colorOffsets(col2, bold, current)
+ maxWidth := C.MaxX() - 3 - t.marginInt[1] - t.marginInt[3]
+ fullWidth := displayWidth(text)
+ if fullWidth > maxWidth {
+ if t.hscroll {
+ // Stri..
+ matchEndWidth := displayWidth(text[:maxe])
+ if matchEndWidth <= maxWidth-2 {
+ text, _ = trimRight(text, maxWidth-2)
+ text = append(text, []rune("..")...)
+ } else {
+ // Stri..
+ if matchEndWidth < fullWidth-2 {
+ text = append(text[:maxe], []rune("..")...)
+ }
+ // ..ri..
+ var diff int32
+ text, diff = trimLeft(text, maxWidth-2)
+
+ // Transform offsets
+ for idx, offset := range offsets {
+ b, e := offset.offset[0], offset.offset[1]
+ b += 2 - diff
+ e += 2 - diff
+ b = util.Max32(b, 2)
+ offsets[idx].offset[0] = b
+ offsets[idx].offset[1] = util.Max32(b, e)
+ }
+ text = append([]rune(".."), text...)
+ }
+ } else {
+ text, _ = trimRight(text, maxWidth-2)
+ text = append(text, []rune("..")...)
+
+ for idx, offset := range offsets {
+ offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-2))
+ offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
+ }
+ }
+ }
+
+ var index int32
+ var substr string
+ var prefixWidth int
+ maxOffset := int32(len(text))
+ for _, offset := range offsets {
+ b := util.Constrain32(offset.offset[0], index, maxOffset)
+ e := util.Constrain32(offset.offset[1], index, maxOffset)
+
+ substr, prefixWidth = processTabs(text[index:b], prefixWidth)
+ C.CPrint(col1, bold, substr)
+
+ if b < e {
+ substr, prefixWidth = processTabs(text[b:e], prefixWidth)
+ C.CPrint(offset.color, offset.bold, substr)
+ }
+
+ index = e
+ if index >= maxOffset {
+ break
+ }
+ }
+ if index < maxOffset {
+ substr, _ = processTabs(text[index:], prefixWidth)
+ C.CPrint(col1, bold, substr)
+ }
+}
+
+func processTabs(runes []rune, prefixWidth int) (string, int) {
+ var strbuf bytes.Buffer
+ l := prefixWidth
+ for _, r := range runes {
+ w := runeWidth(r, l)
+ l += w
+ if r == '\t' {
+ strbuf.WriteString(strings.Repeat(" ", w))
+ } else {
+ strbuf.WriteRune(r)
+ }
+ }
+ return strbuf.String(), l
+}
+
+func (t *Terminal) printAll() {
+ t.calculateMargins()
+ t.printList()
+ t.printPrompt()
+ t.printInfo()
+ t.printHeader()
+}
+
+func (t *Terminal) refresh() {
+ if !t.suppress {
+ C.Refresh()
+ }
+}
+
+func (t *Terminal) delChar() bool {
+ if len(t.input) > 0 && t.cx < len(t.input) {
+ t.input = append(t.input[:t.cx], t.input[t.cx+1:]...)
+ return true
+ }
+ return false
+}
+
+func findLastMatch(pattern string, str string) int {
+ rx, err := regexp.Compile(pattern)
+ if err != nil {
+ return -1
+ }
+ locs := rx.FindAllStringIndex(str, -1)
+ if locs == nil {
+ return -1
+ }
+ return locs[len(locs)-1][0]
+}
+
+func findFirstMatch(pattern string, str string) int {
+ rx, err := regexp.Compile(pattern)
+ if err != nil {
+ return -1
+ }
+ loc := rx.FindStringIndex(str)
+ if loc == nil {
+ return -1
+ }
+ return loc[0]
+}
+
+func copySlice(slice []rune) []rune {
+ ret := make([]rune, len(slice))
+ copy(ret, slice)
+ return ret
+}
+
+func (t *Terminal) rubout(pattern string) {
+ pcx := t.cx
+ after := t.input[t.cx:]
+ t.cx = findLastMatch(pattern, string(t.input[:t.cx])) + 1
+ t.yanked = copySlice(t.input[t.cx:pcx])
+ t.input = append(t.input[:t.cx], after...)
+}
+
+func keyMatch(key int, event C.Event) bool {
+ return event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ
+}
+
+func executeCommand(template string, current string) {
+ command := strings.Replace(template, "{}", fmt.Sprintf("%q", current), -1)
+ cmd := exec.Command("sh", "-c", command)
+ cmd.Stdin = os.Stdin
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ C.Endwin()
+ cmd.Run()
+ C.Refresh()
+}
+
+// Loop is called to start Terminal I/O
+func (t *Terminal) Loop() {
+ <-t.startChan
+ { // Late initialization
+ t.mutex.Lock()
+ t.initFunc()
+ t.calculateMargins()
+ t.printPrompt()
+ t.placeCursor()
+ C.Refresh()
+ t.printInfo()
+ t.printHeader()
+ t.mutex.Unlock()
+ go func() {
+ timer := time.NewTimer(initialDelay)
+ <-timer.C
+ t.reqBox.Set(reqRefresh, nil)
+ }()
+
+ resizeChan := make(chan os.Signal, 1)
+ signal.Notify(resizeChan, syscall.SIGWINCH)
+ go func() {
+ for {
+ <-resizeChan
+ t.reqBox.Set(reqRedraw, nil)
+ }
+ }()
+
+ // Keep the spinner spinning
+ go func() {
+ for {
+ t.mutex.Lock()
+ reading := t.reading
+ t.mutex.Unlock()
+ if !reading {
+ break
+ }
+ time.Sleep(spinnerDuration)
+ t.reqBox.Set(reqInfo, nil)
+ }
+ }()
+ }
+
+ exit := func(code int) {
+ if code <= exitNoMatch && t.history != nil {
+ t.history.append(string(t.input))
+ }
+ os.Exit(code)
+ }
+
+ go func() {
+ for {
+ t.reqBox.Wait(func(events *util.Events) {
+ defer events.Clear()
+ t.mutex.Lock()
+ for req := range *events {
+ switch req {
+ case reqPrompt:
+ t.printPrompt()
+ if t.inlineInfo {
+ t.printInfo()
+ }
+ case reqInfo:
+ t.printInfo()
+ case reqList:
+ t.printList()
+ case reqHeader:
+ t.printHeader()
+ case reqRefresh:
+ t.suppress = false
+ case reqRedraw:
+ C.Clear()
+ C.Endwin()
+ C.Refresh()
+ t.printAll()
+ case reqClose:
+ C.Close()
+ if t.output() {
+ exit(exitOk)
+ }
+ exit(exitNoMatch)
+ case reqQuit:
+ C.Close()
+ exit(exitInterrupt)
+ }
+ }
+ t.placeCursor()
+ t.mutex.Unlock()
+ })
+ t.refresh()
+ }
+ }()
+
+ looping := true
+ for looping {
+ event := C.GetChar()
+
+ t.mutex.Lock()
+ previousInput := t.input
+ events := []util.EventType{reqPrompt}
+ req := func(evts ...util.EventType) {
+ for _, event := range evts {
+ events = append(events, event)
+ if event == reqClose || event == reqQuit {
+ looping = false
+ }
+ }
+ }
+ selectItem := func(item *Item) bool {
+ if _, found := t.selected[item.index]; !found {
+ t.selected[item.index] = selectedItem{time.Now(), item.StringPtr(t.ansi)}
+ return true
+ }
+ return false
+ }
+ toggleY := func(y int) {
+ item := t.merger.Get(y)
+ if !selectItem(item) {
+ delete(t.selected, item.index)
+ }
+ }
+ toggle := func() {
+ if t.cy < t.merger.Length() {
+ toggleY(t.cy)
+ req(reqInfo)
+ }
+ }
+ for key, ret := range t.expect {
+ if keyMatch(key, event) {
+ t.pressed = ret
+ req(reqClose)
+ break
+ }
+ }
+
+ action := t.keymap[event.Type]
+ mapkey := event.Type
+ if event.Type == C.Rune {
+ mapkey = int(event.Char) + int(C.AltZ)
+ if act, prs := t.keymap[mapkey]; prs {
+ action = act
+ }
+ }
+ switch action {
+ case actIgnore:
+ case actExecute:
+ if t.cy >= 0 && t.cy < t.merger.Length() {
+ item := t.merger.Get(t.cy)
+ executeCommand(t.execmap[mapkey], item.AsString(t.ansi))
+ }
+ case actInvalid:
+ t.mutex.Unlock()
+ continue
+ case actToggleSort:
+ t.sort = !t.sort
+ t.eventBox.Set(EvtSearchNew, t.sort)
+ t.mutex.Unlock()
+ continue
+ case actBeginningOfLine:
+ t.cx = 0
+ case actBackwardChar:
+ if t.cx > 0 {
+ t.cx--
+ }
+ case actAbort:
+ req(reqQuit)
+ case actDeleteChar:
+ t.delChar()
+ case actDeleteCharEOF:
+ if !t.delChar() && t.cx == 0 {
+ req(reqQuit)
+ }
+ case actEndOfLine:
+ t.cx = len(t.input)
+ case actCancel:
+ if len(t.input) == 0 {
+ req(reqQuit)
+ } else {
+ t.yanked = t.input
+ t.input = []rune{}
+ t.cx = 0
+ }
+ case actForwardChar:
+ if t.cx < len(t.input) {
+ t.cx++
+ }
+ case actBackwardDeleteChar:
+ if t.cx > 0 {
+ t.input = append(t.input[:t.cx-1], t.input[t.cx:]...)
+ t.cx--
+ }
+ case actSelectAll:
+ if t.multi {
+ for i := 0; i < t.merger.Length(); i++ {
+ item := t.merger.Get(i)
+ selectItem(item)
+ }
+ req(reqList, reqInfo)
+ }
+ case actDeselectAll:
+ if t.multi {
+ for i := 0; i < t.merger.Length(); i++ {
+ item := t.merger.Get(i)
+ delete(t.selected, item.index)
+ }
+ req(reqList, reqInfo)
+ }
+ case actToggle:
+ if t.multi && t.merger.Length() > 0 {
+ toggle()
+ req(reqList)
+ }
+ case actToggleAll:
+ if t.multi {
+ for i := 0; i < t.merger.Length(); i++ {
+ toggleY(i)
+ }
+ req(reqList, reqInfo)
+ }
+ case actToggleDown:
+ if t.multi && t.merger.Length() > 0 {
+ toggle()
+ t.vmove(-1)
+ req(reqList)
+ }
+ case actToggleUp:
+ if t.multi && t.merger.Length() > 0 {
+ toggle()
+ t.vmove(1)
+ req(reqList)
+ }
+ case actDown:
+ t.vmove(-1)
+ req(reqList)
+ case actUp:
+ t.vmove(1)
+ req(reqList)
+ case actAccept:
+ req(reqClose)
+ case actClearScreen:
+ req(reqRedraw)
+ case actUnixLineDiscard:
+ if t.cx > 0 {
+ t.yanked = copySlice(t.input[:t.cx])
+ t.input = t.input[t.cx:]
+ t.cx = 0
+ }
+ case actUnixWordRubout:
+ if t.cx > 0 {
+ t.rubout("\\s\\S")
+ }
+ case actBackwardKillWord:
+ if t.cx > 0 {
+ t.rubout("[^[:alnum:]][[:alnum:]]")
+ }
+ case actYank:
+ suffix := copySlice(t.input[t.cx:])
+ t.input = append(append(t.input[:t.cx], t.yanked...), suffix...)
+ t.cx += len(t.yanked)
+ case actPageUp:
+ t.vmove(t.maxItems() - 1)
+ req(reqList)
+ case actPageDown:
+ t.vmove(-(t.maxItems() - 1))
+ req(reqList)
+ case actBackwardWord:
+ t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1
+ case actForwardWord:
+ t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
+ case actKillWord:
+ ncx := t.cx +
+ findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
+ if ncx > t.cx {
+ t.yanked = copySlice(t.input[t.cx:ncx])
+ t.input = append(t.input[:t.cx], t.input[ncx:]...)
+ }
+ 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])
+ t.input = append(append(prefix, event.Char), t.input[t.cx:]...)
+ t.cx++
+ case actPreviousHistory:
+ if t.history != nil {
+ t.history.override(string(t.input))
+ t.input = []rune(t.history.previous())
+ t.cx = len(t.input)
+ }
+ case actNextHistory:
+ if t.history != nil {
+ t.history.override(string(t.input))
+ t.input = []rune(t.history.next())
+ t.cx = len(t.input)
+ }
+ case actMouse:
+ me := event.MouseEvent
+ mx, my := me.X, me.Y
+ if me.S != 0 {
+ // Scroll
+ if t.merger.Length() > 0 {
+ if t.multi && me.Mod {
+ toggle()
+ }
+ t.vmove(me.S)
+ req(reqList)
+ }
+ } else if mx >= t.marginInt[3] && mx < C.MaxX()-t.marginInt[1] &&
+ my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] {
+ mx -= t.marginInt[3]
+ my -= t.marginInt[0]
+ mx = util.Constrain(mx-len(t.prompt), 0, len(t.input))
+ if !t.reverse {
+ my = t.maxHeight() - my - 1
+ }
+ min := 2 + len(t.header)
+ if t.inlineInfo {
+ min--
+ }
+ if me.Double {
+ // Double-click
+ if my >= min {
+ if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
+ req(reqClose)
+ }
+ }
+ } else if me.Down {
+ if my == 0 && mx >= 0 {
+ // Prompt
+ t.cx = mx
+ } else if my >= min {
+ // List
+ if t.vset(t.offset+my-min) && t.multi && me.Mod {
+ toggle()
+ }
+ req(reqList)
+ }
+ }
+ }
+ }
+ changed := string(previousInput) != string(t.input)
+ t.mutex.Unlock() // Must be unlocked before touching reqBox
+
+ if changed {
+ t.eventBox.Set(EvtSearchNew, t.sort)
+ }
+ for _, event := range events {
+ t.reqBox.Set(event, nil)
+ }
+ }
+}
+
+func (t *Terminal) constrain() {
+ count := t.merger.Length()
+ height := t.maxItems()
+ diffpos := t.cy - t.offset
+
+ t.cy = util.Constrain(t.cy, 0, count-1)
+
+ if t.cy > t.offset+(height-1) {
+ // Ceil
+ t.offset = t.cy - (height - 1)
+ } else if t.offset > t.cy {
+ // Floor
+ t.offset = t.cy
+ }
+
+ // Adjustment
+ if count-t.offset < height {
+ t.offset = util.Max(0, count-height)
+ t.cy = util.Constrain(t.offset+diffpos, 0, count-1)
+ }
+ t.offset = util.Max(0, t.offset)
+}
+
+func (t *Terminal) vmove(o int) {
+ if t.reverse {
+ o *= -1
+ }
+ dest := t.cy + o
+ if t.cycle {
+ max := t.merger.Length() - 1
+ if dest > max {
+ if t.cy == max {
+ dest = 0
+ }
+ } else if dest < 0 {
+ if t.cy == 0 {
+ dest = max
+ }
+ }
+ }
+ t.vset(dest)
+}
+
+func (t *Terminal) vset(o int) bool {
+ t.cy = util.Constrain(o, 0, t.merger.Length()-1)
+ return t.cy == o
+}
+
+func (t *Terminal) maxItems() int {
+ max := t.maxHeight() - 2 - len(t.header)
+ if t.inlineInfo {
+ max++
+ }
+ return util.Max(max, 0)
+}
diff --git a/src/tokenizer.go b/src/tokenizer.go
new file mode 100644
index 0000000..a8d0400
--- /dev/null
+++ b/src/tokenizer.go
@@ -0,0 +1,239 @@
+package fzf
+
+import (
+ "regexp"
+ "strconv"
+ "strings"
+
+ "github.com/junegunn/fzf/src/util"
+)
+
+const rangeEllipsis = 0
+
+// Range represents nth-expression
+type Range struct {
+ begin int
+ end int
+}
+
+// Token contains the tokenized part of the strings and its prefix length
+type Token struct {
+ text []rune
+ prefixLength int
+}
+
+// Delimiter for tokenizing the input
+type Delimiter struct {
+ regex *regexp.Regexp
+ str *string
+}
+
+func newRange(begin int, end int) Range {
+ if begin == 1 {
+ begin = rangeEllipsis
+ }
+ if end == -1 {
+ end = rangeEllipsis
+ }
+ return Range{begin, end}
+}
+
+// ParseRange parses nth-expression and returns the corresponding Range object
+func ParseRange(str *string) (Range, bool) {
+ if (*str) == ".." {
+ return newRange(rangeEllipsis, rangeEllipsis), true
+ } else if strings.HasPrefix(*str, "..") {
+ end, err := strconv.Atoi((*str)[2:])
+ if err != nil || end == 0 {
+ return Range{}, false
+ }
+ return newRange(rangeEllipsis, end), true
+ } else if strings.HasSuffix(*str, "..") {
+ begin, err := strconv.Atoi((*str)[:len(*str)-2])
+ if err != nil || begin == 0 {
+ return Range{}, false
+ }
+ return newRange(begin, rangeEllipsis), true
+ } else if strings.Contains(*str, "..") {
+ ns := strings.Split(*str, "..")
+ if len(ns) != 2 {
+ return Range{}, false
+ }
+ begin, err1 := strconv.Atoi(ns[0])
+ end, err2 := strconv.Atoi(ns[1])
+ if err1 != nil || err2 != nil || begin == 0 || end == 0 {
+ return Range{}, false
+ }
+ return newRange(begin, end), true
+ }
+
+ n, err := strconv.Atoi(*str)
+ if err != nil || n == 0 {
+ return Range{}, false
+ }
+ return newRange(n, n), true
+}
+
+func withPrefixLengths(tokens [][]rune, begin int) []Token {
+ ret := make([]Token, len(tokens))
+
+ prefixLength := begin
+ for idx, token := range tokens {
+ // Need to define a new local variable instead of the reused token to take
+ // the pointer to it
+ ret[idx] = Token{text: token, prefixLength: prefixLength}
+ prefixLength += len(token)
+ }
+ return ret
+}
+
+const (
+ awkNil = iota
+ awkBlack
+ awkWhite
+)
+
+func awkTokenizer(input []rune) ([][]rune, int) {
+ // 9, 32
+ ret := [][]rune{}
+ str := []rune{}
+ prefixLength := 0
+ state := awkNil
+ for _, r := range input {
+ white := r == 9 || r == 32
+ switch state {
+ case awkNil:
+ if white {
+ prefixLength++
+ } else {
+ state = awkBlack
+ str = append(str, r)
+ }
+ case awkBlack:
+ str = append(str, r)
+ if white {
+ state = awkWhite
+ }
+ case awkWhite:
+ if white {
+ str = append(str, r)
+ } else {
+ ret = append(ret, str)
+ state = awkBlack
+ str = []rune{r}
+ }
+ }
+ }
+ if len(str) > 0 {
+ ret = append(ret, str)
+ }
+ return ret, prefixLength
+}
+
+// Tokenize tokenizes the given string with the delimiter
+func Tokenize(runes []rune, delimiter Delimiter) []Token {
+ if delimiter.str == nil && delimiter.regex == nil {
+ // AWK-style (\S+\s*)
+ tokens, prefixLength := awkTokenizer(runes)
+ return withPrefixLengths(tokens, prefixLength)
+ }
+
+ var tokens []string
+ if delimiter.str != nil {
+ tokens = strings.Split(string(runes), *delimiter.str)
+ for i := 0; i < len(tokens)-1; i++ {
+ tokens[i] = tokens[i] + *delimiter.str
+ }
+ } else if delimiter.regex != nil {
+ str := string(runes)
+ for len(str) > 0 {
+ loc := delimiter.regex.FindStringIndex(str)
+ if loc == nil {
+ loc = []int{0, len(str)}
+ }
+ last := util.Max(loc[1], 1)
+ tokens = append(tokens, str[:last])
+ str = str[last:]
+ }
+ }
+ asRunes := make([][]rune, len(tokens))
+ for i, token := range tokens {
+ asRunes[i] = []rune(token)
+ }
+ return withPrefixLengths(asRunes, 0)
+}
+
+func joinTokens(tokens []Token) []rune {
+ ret := []rune{}
+ for _, token := range tokens {
+ ret = append(ret, token.text...)
+ }
+ return ret
+}
+
+func joinTokensAsRunes(tokens []Token) []rune {
+ ret := []rune{}
+ for _, token := range tokens {
+ ret = append(ret, token.text...)
+ }
+ return ret
+}
+
+// Transform is used to transform the input when --with-nth option is given
+func Transform(tokens []Token, withNth []Range) []Token {
+ transTokens := make([]Token, len(withNth))
+ numTokens := len(tokens)
+ for idx, r := range withNth {
+ part := []rune{}
+ minIdx := 0
+ if r.begin == r.end {
+ idx := r.begin
+ if idx == rangeEllipsis {
+ part = append(part, joinTokensAsRunes(tokens)...)
+ } else {
+ if idx < 0 {
+ idx += numTokens + 1
+ }
+ if idx >= 1 && idx <= numTokens {
+ minIdx = idx - 1
+ part = append(part, tokens[idx-1].text...)
+ }
+ }
+ } else {
+ var begin, end int
+ if r.begin == rangeEllipsis { // ..N
+ begin, end = 1, r.end
+ if end < 0 {
+ end += numTokens + 1
+ }
+ } else if r.end == rangeEllipsis { // N..
+ begin, end = r.begin, numTokens
+ if begin < 0 {
+ begin += numTokens + 1
+ }
+ } else {
+ begin, end = r.begin, r.end
+ if begin < 0 {
+ begin += numTokens + 1
+ }
+ if end < 0 {
+ end += numTokens + 1
+ }
+ }
+ minIdx = util.Max(0, begin-1)
+ for idx := begin; idx <= end; idx++ {
+ if idx >= 1 && idx <= numTokens {
+ part = append(part, tokens[idx-1].text...)
+ }
+ }
+ }
+ var prefixLength int
+ if minIdx < numTokens {
+ prefixLength = tokens[minIdx].prefixLength
+ } else {
+ prefixLength = 0
+ }
+ transTokens[idx] = Token{part, prefixLength}
+ }
+ return transTokens
+}
diff --git a/src/tokenizer_test.go b/src/tokenizer_test.go
new file mode 100644
index 0000000..0f95aa1
--- /dev/null
+++ b/src/tokenizer_test.go
@@ -0,0 +1,110 @@
+package fzf
+
+import "testing"
+
+func TestParseRange(t *testing.T) {
+ {
+ i := ".."
+ r, _ := ParseRange(&i)
+ if r.begin != rangeEllipsis || r.end != rangeEllipsis {
+ t.Errorf("%s", r)
+ }
+ }
+ {
+ i := "3.."
+ r, _ := ParseRange(&i)
+ if r.begin != 3 || r.end != rangeEllipsis {
+ t.Errorf("%s", r)
+ }
+ }
+ {
+ i := "3..5"
+ r, _ := ParseRange(&i)
+ if r.begin != 3 || r.end != 5 {
+ t.Errorf("%s", r)
+ }
+ }
+ {
+ i := "-3..-5"
+ r, _ := ParseRange(&i)
+ if r.begin != -3 || r.end != -5 {
+ t.Errorf("%s", r)
+ }
+ }
+ {
+ i := "3"
+ r, _ := ParseRange(&i)
+ if r.begin != 3 || r.end != 3 {
+ t.Errorf("%s", r)
+ }
+ }
+}
+
+func TestTokenize(t *testing.T) {
+ // AWK-style
+ input := " abc: def: ghi "
+ tokens := Tokenize([]rune(input), Delimiter{})
+ if string(tokens[0].text) != "abc: " || tokens[0].prefixLength != 2 {
+ t.Errorf("%s", tokens)
+ }
+
+ // With delimiter
+ tokens = Tokenize([]rune(input), delimiterRegexp(":"))
+ if string(tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 {
+ t.Errorf("%s", tokens)
+ }
+
+ // With delimiter regex
+ tokens = Tokenize([]rune(input), delimiterRegexp("\\s+"))
+ if string(tokens[0].text) != " " || tokens[0].prefixLength != 0 ||
+ string(tokens[1].text) != "abc: " || tokens[1].prefixLength != 2 ||
+ string(tokens[2].text) != "def: " || tokens[2].prefixLength != 8 ||
+ string(tokens[3].text) != "ghi " || tokens[3].prefixLength != 14 {
+ t.Errorf("%s", tokens)
+ }
+}
+
+func TestTransform(t *testing.T) {
+ input := " abc: def: ghi: jkl"
+ {
+ tokens := Tokenize([]rune(input), Delimiter{})
+ {
+ ranges := splitNth("1,2,3")
+ tx := Transform(tokens, ranges)
+ if string(joinTokens(tx)) != "abc: def: ghi: " {
+ t.Errorf("%s", tx)
+ }
+ }
+ {
+ ranges := splitNth("1..2,3,2..,1")
+ tx := Transform(tokens, ranges)
+ if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
+ len(tx) != 4 ||
+ string(tx[0].text) != "abc: def: " || tx[0].prefixLength != 2 ||
+ string(tx[1].text) != "ghi: " || tx[1].prefixLength != 14 ||
+ string(tx[2].text) != "def: ghi: jkl" || tx[2].prefixLength != 8 ||
+ string(tx[3].text) != "abc: " || tx[3].prefixLength != 2 {
+ t.Errorf("%s", tx)
+ }
+ }
+ }
+ {
+ tokens := Tokenize([]rune(input), delimiterRegexp(":"))
+ {
+ ranges := splitNth("1..2,3,2..,1")
+ tx := Transform(tokens, ranges)
+ if string(joinTokens(tx)) != " abc: def: ghi: def: ghi: jkl abc:" ||
+ len(tx) != 4 ||
+ string(tx[0].text) != " abc: def:" || tx[0].prefixLength != 0 ||
+ string(tx[1].text) != " ghi:" || tx[1].prefixLength != 12 ||
+ string(tx[2].text) != " def: ghi: jkl" || tx[2].prefixLength != 6 ||
+ string(tx[3].text) != " abc:" || tx[3].prefixLength != 0 {
+ t.Errorf("%s", tx)
+ }
+ }
+ }
+}
+
+func TestTransformIndexOutOfBounds(t *testing.T) {
+ Transform([]Token{}, splitNth("1"))
+}
diff --git a/src/update_assets.rb b/src/update_assets.rb
new file mode 100755
index 0000000..c849f9e
--- /dev/null
+++ b/src/update_assets.rb
@@ -0,0 +1,42 @@
+#!/usr/bin/env ruby
+
+# http://www.rubydoc.info/github/rest-client/rest-client/RestClient
+require 'rest_client'
+
+if ARGV.length < 3
+ puts "usage: #$0 "
+ exit 1
+end
+
+token, version, *files = ARGV
+base = "https://api.github.com/repos/junegunn/fzf-bin/releases"
+
+# List releases
+rels = JSON.parse(RestClient.get(base, :authorization => "token #{token}"))
+rel = rels.find { |r| r['tag_name'] == version }
+unless rel
+ puts "#{version} not found"
+ exit 1
+end
+
+# List assets
+assets = Hash[rel['assets'].map { |a| a.values_at *%w[name id] }]
+
+files.select { |f| File.exists? f }.each do |file|
+ name = File.basename file
+
+ if asset_id = assets[name]
+ puts "#{name} found. Deleting asset id #{asset_id}."
+ RestClient.delete "#{base}/assets/#{asset_id}",
+ :authorization => "token #{token}"
+ else
+ puts "#{name} not found"
+ end
+
+ puts "Uploading #{name}"
+ RestClient.post(
+ "#{base.sub 'api', 'uploads'}/#{rel['id']}/assets?name=#{name}",
+ File.read(file),
+ :authorization => "token #{token}",
+ :content_type => "application/octet-stream")
+end
diff --git a/src/util/atomicbool.go b/src/util/atomicbool.go
new file mode 100644
index 0000000..9e1bdc8
--- /dev/null
+++ b/src/util/atomicbool.go
@@ -0,0 +1,32 @@
+package util
+
+import "sync"
+
+// AtomicBool is a boxed-class that provides synchronized access to the
+// underlying boolean value
+type AtomicBool struct {
+ mutex sync.Mutex
+ state bool
+}
+
+// NewAtomicBool returns a new AtomicBool
+func NewAtomicBool(initialState bool) *AtomicBool {
+ return &AtomicBool{
+ mutex: sync.Mutex{},
+ state: initialState}
+}
+
+// Get returns the current boolean value synchronously
+func (a *AtomicBool) Get() bool {
+ a.mutex.Lock()
+ defer a.mutex.Unlock()
+ return a.state
+}
+
+// Set updates the boolean value synchronously
+func (a *AtomicBool) Set(newState bool) bool {
+ a.mutex.Lock()
+ defer a.mutex.Unlock()
+ a.state = newState
+ return a.state
+}
diff --git a/src/util/atomicbool_test.go b/src/util/atomicbool_test.go
new file mode 100644
index 0000000..1feff79
--- /dev/null
+++ b/src/util/atomicbool_test.go
@@ -0,0 +1,17 @@
+package util
+
+import "testing"
+
+func TestAtomicBool(t *testing.T) {
+ if !NewAtomicBool(true).Get() || NewAtomicBool(false).Get() {
+ t.Error("Invalid initial value")
+ }
+
+ ab := NewAtomicBool(true)
+ if ab.Set(false) {
+ t.Error("Invalid return value")
+ }
+ if ab.Get() {
+ t.Error("Invalid state")
+ }
+}
diff --git a/src/util/eventbox.go b/src/util/eventbox.go
new file mode 100644
index 0000000..ccdbb99
--- /dev/null
+++ b/src/util/eventbox.go
@@ -0,0 +1,96 @@
+package util
+
+import "sync"
+
+// EventType is the type for fzf events
+type EventType int
+
+// Events is a type that associates EventType to any data
+type Events map[EventType]interface{}
+
+// EventBox is used for coordinating events
+type EventBox struct {
+ events Events
+ cond *sync.Cond
+ ignore map[EventType]bool
+}
+
+// NewEventBox returns a new EventBox
+func NewEventBox() *EventBox {
+ return &EventBox{
+ events: make(Events),
+ cond: sync.NewCond(&sync.Mutex{}),
+ ignore: make(map[EventType]bool)}
+}
+
+// Wait blocks the goroutine until signaled
+func (b *EventBox) Wait(callback func(*Events)) {
+ b.cond.L.Lock()
+ defer b.cond.L.Unlock()
+
+ if len(b.events) == 0 {
+ b.cond.Wait()
+ }
+
+ callback(&b.events)
+}
+
+// Set turns on the event type on the box
+func (b *EventBox) Set(event EventType, value interface{}) {
+ b.cond.L.Lock()
+ defer b.cond.L.Unlock()
+ b.events[event] = value
+ if _, found := b.ignore[event]; !found {
+ b.cond.Broadcast()
+ }
+}
+
+// Clear clears the events
+// Unsynchronized; should be called within Wait routine
+func (events *Events) Clear() {
+ for event := range *events {
+ delete(*events, event)
+ }
+}
+
+// Peek peeks at the event box if the given event is set
+func (b *EventBox) Peek(event EventType) bool {
+ b.cond.L.Lock()
+ defer b.cond.L.Unlock()
+ _, ok := b.events[event]
+ return ok
+}
+
+// Watch deletes the events from the ignore list
+func (b *EventBox) Watch(events ...EventType) {
+ b.cond.L.Lock()
+ defer b.cond.L.Unlock()
+ for _, event := range events {
+ delete(b.ignore, event)
+ }
+}
+
+// Unwatch adds the events to the ignore list
+func (b *EventBox) Unwatch(events ...EventType) {
+ b.cond.L.Lock()
+ defer b.cond.L.Unlock()
+ for _, event := range events {
+ b.ignore[event] = true
+ }
+}
+
+// WaitFor blocks the execution until the event is received
+func (b *EventBox) WaitFor(event EventType) {
+ looping := true
+ for looping {
+ b.Wait(func(events *Events) {
+ for evt := range *events {
+ switch evt {
+ case event:
+ looping = false
+ return
+ }
+ }
+ })
+ }
+}
diff --git a/src/util/eventbox_test.go b/src/util/eventbox_test.go
new file mode 100644
index 0000000..5a9dc30
--- /dev/null
+++ b/src/util/eventbox_test.go
@@ -0,0 +1,61 @@
+package util
+
+import "testing"
+
+// fzf events
+const (
+ EvtReadNew EventType = iota
+ EvtReadFin
+ EvtSearchNew
+ EvtSearchProgress
+ EvtSearchFin
+ EvtClose
+)
+
+func TestEventBox(t *testing.T) {
+ eb := NewEventBox()
+
+ // Wait should return immediately
+ ch := make(chan bool)
+
+ go func() {
+ eb.Set(EvtReadNew, 10)
+ ch <- true
+ <-ch
+ eb.Set(EvtSearchNew, 10)
+ eb.Set(EvtSearchNew, 15)
+ eb.Set(EvtSearchNew, 20)
+ eb.Set(EvtSearchProgress, 30)
+ ch <- true
+ <-ch
+ eb.Set(EvtSearchFin, 40)
+ ch <- true
+ <-ch
+ }()
+
+ count := 0
+ sum := 0
+ looping := true
+ for looping {
+ <-ch
+ eb.Wait(func(events *Events) {
+ for _, value := range *events {
+ switch val := value.(type) {
+ case int:
+ sum += val
+ looping = sum < 100
+ }
+ }
+ events.Clear()
+ })
+ ch <- true
+ count++
+ }
+
+ if count != 3 {
+ t.Error("Invalid number of events", count)
+ }
+ if sum != 100 {
+ t.Error("Invalid sum", sum)
+ }
+}
diff --git a/src/util/util.go b/src/util/util.go
new file mode 100644
index 0000000..aa5f227
--- /dev/null
+++ b/src/util/util.go
@@ -0,0 +1,102 @@
+package util
+
+// #include
+import "C"
+
+import (
+ "os"
+ "time"
+ "unicode/utf8"
+)
+
+// Max returns the largest integer
+func Max(first int, items ...int) int {
+ max := first
+ for _, item := range items {
+ if item > max {
+ max = item
+ }
+ }
+ return max
+}
+
+// Min32 returns the smallest 32-bit integer
+func Min32(first int32, second int32) int32 {
+ if first <= second {
+ return first
+ }
+ return second
+}
+
+// Max32 returns the largest 32-bit integer
+func Max32(first int32, second int32) int32 {
+ if first > second {
+ return first
+ }
+ return second
+}
+
+// Constrain32 limits the given 32-bit integer with the upper and lower bounds
+func Constrain32(val int32, min int32, max int32) int32 {
+ if val < min {
+ return min
+ }
+ if val > max {
+ return max
+ }
+ return val
+}
+
+// Constrain limits the given integer with the upper and lower bounds
+func Constrain(val int, min int, max int) int {
+ if val < min {
+ return min
+ }
+ if val > max {
+ return max
+ }
+ return val
+}
+
+// DurWithin limits the given time.Duration with the upper and lower bounds
+func DurWithin(
+ val time.Duration, min time.Duration, max time.Duration) time.Duration {
+ if val < min {
+ return min
+ }
+ if val > max {
+ return max
+ }
+ return val
+}
+
+// IsTty returns true is stdin is a terminal
+func IsTty() bool {
+ return int(C.isatty(C.int(os.Stdin.Fd()))) != 0
+}
+
+func TrimRight(runes []rune) []rune {
+ var i int
+ for i = len(runes) - 1; i >= 0; i-- {
+ char := runes[i]
+ if char != ' ' && char != '\t' {
+ break
+ }
+ }
+ return runes[0 : i+1]
+}
+
+func BytesToRunes(bytea []byte) []rune {
+ runes := make([]rune, 0, len(bytea))
+ for i := 0; i < len(bytea); {
+ if bytea[i] < utf8.RuneSelf {
+ runes = append(runes, rune(bytea[i]))
+ i++
+ } else {
+ r, sz := utf8.DecodeRune(bytea[i:])
+ i += sz
+ runes = append(runes, r)
+ }
+ }
+ return runes
+}
diff --git a/src/util/util_test.go b/src/util/util_test.go
new file mode 100644
index 0000000..06cfd4f
--- /dev/null
+++ b/src/util/util_test.go
@@ -0,0 +1,22 @@
+package util
+
+import "testing"
+
+func TestMax(t *testing.T) {
+ if Max(-2, 5, 1, 4, 3) != 5 {
+ t.Error("Invalid result")
+ }
+}
+
+func TestContrain(t *testing.T) {
+ if Constrain(-3, -1, 3) != -1 {
+ t.Error("Expected", -1)
+ }
+ if Constrain(2, -1, 3) != 2 {
+ t.Error("Expected", 2)
+ }
+
+ if Constrain(5, -1, 3) != 3 {
+ t.Error("Expected", 3)
+ }
+}
diff --git a/test/fzf.vader b/test/fzf.vader
index 5d40142..c27fddc 100644
--- a/test/fzf.vader
+++ b/test/fzf.vader
@@ -3,20 +3,23 @@ Execute (Setup):
Log 'Test directory: ' . g:dir
Execute (fzf#run with dir option):
+ let cwd = getcwd()
let result = fzf#run({ 'options': '--filter=vdr', 'dir': g:dir })
AssertEqual ['fzf.vader'], result
+ AssertEqual getcwd(), cwd
let result = sort(fzf#run({ 'options': '--filter e', 'dir': g:dir }))
- AssertEqual ['fzf.vader', 'test_fzf.rb'], result
+ AssertEqual ['fzf.vader', 'test_go.rb'], result
+ AssertEqual getcwd(), cwd
Execute (fzf#run with Funcref command):
let g:ret = []
- function! g:proc(e)
+ function! g:FzfTest(e)
call add(g:ret, a:e)
endfunction
- let result = sort(fzf#run({ 'sink': function('g:proc'), 'options': '--filter e', 'dir': g:dir }))
- AssertEqual ['fzf.vader', 'test_fzf.rb'], result
- AssertEqual ['fzf.vader', 'test_fzf.rb'], sort(g:ret)
+ let result = sort(fzf#run({ 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir }))
+ AssertEqual ['fzf.vader', 'test_go.rb'], result
+ AssertEqual ['fzf.vader', 'test_go.rb'], sort(g:ret)
Execute (fzf#run with string source):
let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' }))
diff --git a/test/test_fzf.rb b/test/test_fzf.rb
deleted file mode 100644
index e11f2c5..0000000
--- a/test/test_fzf.rb
+++ /dev/null
@@ -1,850 +0,0 @@
-#!/usr/bin/env ruby
-# encoding: utf-8
-
-require 'rubygems'
-require 'curses'
-require 'timeout'
-require 'stringio'
-require 'minitest/autorun'
-require 'tempfile'
-$LOAD_PATH.unshift File.expand_path('../..', __FILE__)
-ENV['FZF_EXECUTABLE'] = '0'
-load 'fzf'
-
-class MockTTY
- def initialize
- @buffer = ''
- @mutex = Mutex.new
- @condv = ConditionVariable.new
- end
-
- def read_nonblock sz
- @mutex.synchronize do
- take sz
- end
- end
-
- def take sz
- if @buffer.length >= sz
- ret = @buffer[0, sz]
- @buffer = @buffer[sz..-1]
- ret
- end
- end
-
- def getc
- sleep 0.1
- while true
- @mutex.synchronize do
- if char = take(1)
- return char
- else
- @condv.wait(@mutex)
- end
- end
- end
- end
-
- def << str
- @mutex.synchronize do
- @buffer << str
- @condv.broadcast
- end
- self
- end
-end
-
-class TestFZF < MiniTest::Unit::TestCase
- def setup
- ENV.delete 'FZF_DEFAULT_SORT'
- ENV.delete 'FZF_DEFAULT_OPTS'
- ENV.delete 'FZF_DEFAULT_COMMAND'
- end
-
- def test_default_options
- fzf = FZF.new []
- assert_equal 1000, fzf.sort
- assert_equal false, fzf.multi
- assert_equal true, fzf.color
- assert_equal nil, fzf.rxflag
- assert_equal true, fzf.mouse
- assert_equal nil, fzf.nth
- assert_equal nil, fzf.with_nth
- assert_equal true, fzf.color
- assert_equal false, fzf.black
- assert_equal true, fzf.ansi256
- assert_equal '', fzf.query
- assert_equal false, fzf.select1
- assert_equal false, fzf.exit0
- assert_equal nil, fzf.filter
- assert_equal nil, fzf.extended
- assert_equal false, fzf.reverse
- assert_equal '> ', fzf.prompt
- assert_equal false, fzf.print_query
- end
-
- def test_environment_variables
- # Deprecated
- ENV['FZF_DEFAULT_SORT'] = '20000'
- fzf = FZF.new []
- assert_equal 20000, fzf.sort
- assert_equal nil, fzf.nth
-
- ENV['FZF_DEFAULT_OPTS'] =
- '-x -m -s 10000 -q " hello world " +c +2 --select-1 -0 ' <<
- '--no-mouse -f "goodbye world" --black --with-nth=3,-3..,2 --nth=3,-1,2 --reverse --print-query'
- fzf = FZF.new []
- assert_equal 10000, fzf.sort
- assert_equal ' hello world ',
- fzf.query
- assert_equal 'goodbye world',
- fzf.filter
- assert_equal :fuzzy, fzf.extended
- assert_equal true, fzf.multi
- assert_equal false, fzf.color
- assert_equal false, fzf.ansi256
- assert_equal true, fzf.black
- assert_equal false, fzf.mouse
- assert_equal true, fzf.select1
- assert_equal true, fzf.exit0
- assert_equal true, fzf.reverse
- assert_equal true, fzf.print_query
- assert_equal [2..2, -1..-1, 1..1], fzf.nth
- assert_equal [2..2, -3..-1, 1..1], fzf.with_nth
- end
-
- def test_option_parser
- # Long opts
- fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query hello --select-1
- --exit-0 --filter=howdy --extended-exact
- --no-mouse --no-256 --nth=1 --with-nth=.. --reverse --prompt (hi)
- --print-query]
- assert_equal 2000, fzf.sort
- assert_equal true, fzf.multi
- assert_equal false, fzf.color
- assert_equal false, fzf.ansi256
- assert_equal false, fzf.black
- assert_equal false, fzf.mouse
- assert_equal 0, fzf.rxflag
- assert_equal 'hello', fzf.query
- assert_equal true, fzf.select1
- assert_equal true, fzf.exit0
- assert_equal 'howdy', fzf.filter
- assert_equal :exact, fzf.extended
- assert_equal [0..0], fzf.nth
- assert_equal nil, fzf.with_nth
- assert_equal true, fzf.reverse
- assert_equal '(hi)', fzf.prompt
- assert_equal true, fzf.print_query
-
- # Long opts (left-to-right)
- fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query=hello
- --filter a --filter b --no-256 --black --nth -1 --nth -2
- --select-1 --exit-0 --no-select-1 --no-exit-0
- --no-sort -i --color --no-multi --256
- --reverse --no-reverse --prompt (hi) --prompt=(HI)
- --print-query --no-print-query]
- assert_equal nil, fzf.sort
- assert_equal false, fzf.multi
- assert_equal true, fzf.color
- assert_equal true, fzf.ansi256
- assert_equal true, fzf.black
- assert_equal true, fzf.mouse
- assert_equal 1, fzf.rxflag
- assert_equal 'b', fzf.filter
- assert_equal 'hello', fzf.query
- assert_equal false, fzf.select1
- assert_equal false, fzf.exit0
- assert_equal nil, fzf.extended
- assert_equal [-2..-2], fzf.nth
- assert_equal false, fzf.reverse
- assert_equal '(HI)', fzf.prompt
- assert_equal false, fzf.print_query
-
- # Short opts
- fzf = FZF.new %w[-s2000 +c -m +i -qhello -x -fhowdy +2 -n3 -1 -0]
- assert_equal 2000, fzf.sort
- assert_equal true, fzf.multi
- assert_equal false, fzf.color
- assert_equal false, fzf.ansi256
- assert_equal 0, fzf.rxflag
- assert_equal 'hello', fzf.query
- assert_equal 'howdy', fzf.filter
- assert_equal :fuzzy, fzf.extended
- assert_equal [2..2], fzf.nth
- assert_equal true, fzf.select1
- assert_equal true, fzf.exit0
-
- # Left-to-right
- fzf = FZF.new %w[-s 2000 +c -m +i -qhello -x -fgoodbye +2 -n3 -n4,5
- -s 3000 -c +m -i -q world +x -fworld -2 --black --no-black
- -1 -0 +1 +0
- ]
- assert_equal 3000, fzf.sort
- assert_equal false, fzf.multi
- assert_equal true, fzf.color
- assert_equal true, fzf.ansi256
- assert_equal false, fzf.black
- assert_equal 1, fzf.rxflag
- assert_equal 'world', fzf.query
- assert_equal false, fzf.select1
- assert_equal false, fzf.exit0
- assert_equal 'world', fzf.filter
- assert_equal nil, fzf.extended
- assert_equal [3..3, 4..4], fzf.nth
- rescue SystemExit => e
- assert false, "Exited"
- end
-
- def test_invalid_option
- [
- %w[--unknown],
- %w[yo dawg],
- %w[--nth=0],
- %w[-n 0],
- %w[-n 1..2..3],
- %w[-n 1....],
- %w[-n ....3],
- %w[-n 1....3],
- %w[-n 1..0],
- %w[--nth ..0],
- ].each do |argv|
- assert_raises(SystemExit) do
- fzf = FZF.new argv
- end
- end
- end
-
- def test_width
- fzf = FZF.new []
- assert_equal 5, fzf.width('abcde')
- assert_equal 4, fzf.width('한글')
- assert_equal 5, fzf.width('한글.')
- end if RUBY_VERSION >= '1.9'
-
- def test_trim
- fzf = FZF.new []
- assert_equal ['사.', 6], fzf.trim('가나다라마바사.', 4, true)
- assert_equal ['바사.', 5], fzf.trim('가나다라마바사.', 5, true)
- assert_equal ['바사.', 5], fzf.trim('가나다라마바사.', 6, true)
- assert_equal ['마바사.', 4], fzf.trim('가나다라마바사.', 7, true)
- assert_equal ['가나', 6], fzf.trim('가나다라마바사.', 4, false)
- assert_equal ['가나', 6], fzf.trim('가나다라마바사.', 5, false)
- assert_equal ['가나a', 6], fzf.trim('가나ab라마바사.', 5, false)
- assert_equal ['가나ab', 5], fzf.trim('가나ab라마바사.', 6, false)
- assert_equal ['가나ab', 5], fzf.trim('가나ab라마바사.', 7, false)
- end if RUBY_VERSION >= '1.9'
-
- def test_format
- fzf = FZF.new []
- assert_equal [['01234..', false]], fzf.format('0123456789', 7, [])
- assert_equal [['012', false], ['34', true], ['..', false]],
- fzf.format('0123456789', 7, [[3, 5]])
- assert_equal [['..56', false], ['789', true]],
- fzf.format('0123456789', 7, [[7, 10]])
- assert_equal [['..56', false], ['78', true], ['9', false]],
- fzf.format('0123456789', 7, [[7, 9]])
-
- (3..5).each do |i|
- assert_equal [['..', false], ['567', true], ['89', false]],
- fzf.format('0123456789', 7, [[i, 8]])
- end
-
- assert_equal [['..', false], ['345', true], ['..', false]],
- fzf.format('0123456789', 7, [[3, 6]])
- assert_equal [['012', false], ['34', true], ['..', false]],
- fzf.format('0123456789', 7, [[3, 5]])
-
- # Multi-region
- assert_equal [["0", true], ["1", false], ["2", true], ["34..", false]],
- fzf.format('0123456789', 7, [[0, 1], [2, 3]])
-
- assert_equal [["..", false], ["5", true], ["6", false], ["78", true], ["9", false]],
- fzf.format('0123456789', 7, [[3, 6], [7, 9]])
-
- assert_equal [["..", false], ["3", true], ["4", false], ["5", true], ["..", false]],
- fzf.format('0123456789', 7, [[3, 4], [5, 6]])
-
- # Multi-region Overlap
- assert_equal [["..", false], ["345", true], ["..", false]],
- fzf.format('0123456789', 7, [[4, 5], [3, 6]])
- end
-
- def test_fuzzy_matcher
- matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE
- list = %w[
- juice
- juiceful
- juiceless
- juicily
- juiciness
- juicy]
- assert matcher.caches.empty?
- assert_equal(
- [["juice", [[0, 1]]],
- ["juiceful", [[0, 1]]],
- ["juiceless", [[0, 1]]],
- ["juicily", [[0, 1]]],
- ["juiciness", [[0, 1]]],
- ["juicy", [[0, 1]]]], matcher.match(list, 'j', '', '').sort)
- assert !matcher.caches.empty?
- assert_equal [list.object_id], matcher.caches.keys
- assert_equal 1, matcher.caches[list.object_id].length
- assert_equal 6, matcher.caches[list.object_id]['j'].length
-
- assert_equal(
- [["juicily", [[0, 5]]],
- ["juiciness", [[0, 5]]]], matcher.match(list, 'jii', '', '').sort)
-
- assert_equal(
- [["juicily", [[2, 5]]],
- ["juiciness", [[2, 5]]]], matcher.match(list, 'ii', '', '').sort)
-
- assert_equal 3, matcher.caches[list.object_id].length
- assert_equal 2, matcher.caches[list.object_id]['ii'].length
-
- # TODO : partial_cache
- end
-
- def test_fuzzy_matcher_rxflag
- assert_equal nil, FZF::FuzzyMatcher.new(nil).rxflag
- assert_equal 0, FZF::FuzzyMatcher.new(0).rxflag
- assert_equal 1, FZF::FuzzyMatcher.new(1).rxflag
-
- assert_equal 1, FZF::FuzzyMatcher.new(nil).rxflag_for('abc')
- assert_equal 0, FZF::FuzzyMatcher.new(nil).rxflag_for('Abc')
- assert_equal 0, FZF::FuzzyMatcher.new(0).rxflag_for('abc')
- assert_equal 0, FZF::FuzzyMatcher.new(0).rxflag_for('Abc')
- assert_equal 1, FZF::FuzzyMatcher.new(1).rxflag_for('abc')
- assert_equal 1, FZF::FuzzyMatcher.new(1).rxflag_for('Abc')
- end
-
- def test_fuzzy_matcher_case_sensitive
- # Smart-case match (Uppercase found)
- assert_equal [['Fruit', [[0, 5]]]],
- FZF::FuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], 'Fruit', '', '').sort
-
- # Smart-case match (Uppercase not-found)
- assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]],
- FZF::FuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], 'fruit', '', '').sort
-
- # Case-sensitive match (-i)
- assert_equal [['Fruit', [[0, 5]]]],
- FZF::FuzzyMatcher.new(0).match(%w[Fruit Grapefruit], 'Fruit', '', '').sort
-
- # Case-insensitive match (+i)
- assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]],
- FZF::FuzzyMatcher.new(Regexp::IGNORECASE).
- match(%w[Fruit Grapefruit], 'Fruit', '', '').sort
- end
-
- def test_extended_fuzzy_matcher_case_sensitive
- %w['Fruit Fruit$].each do |q|
- # Smart-case match (Uppercase found)
- assert_equal [['Fruit', [[0, 5]]]],
- FZF::ExtendedFuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], q, '', '').sort
-
- # Smart-case match (Uppercase not-found)
- assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]],
- FZF::ExtendedFuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], q.downcase, '', '').sort
-
- # Case-sensitive match (-i)
- assert_equal [['Fruit', [[0, 5]]]],
- FZF::ExtendedFuzzyMatcher.new(0).match(%w[Fruit Grapefruit], q, '', '').sort
-
- # Case-insensitive match (+i)
- assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]],
- FZF::ExtendedFuzzyMatcher.new(Regexp::IGNORECASE).
- match(%w[Fruit Grapefruit], q, '', '').sort
- end
- end
-
- def test_extended_fuzzy_matcher
- matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE
- list = %w[
- juice
- juiceful
- juiceless
- juicily
- juiciness
- juicy
- _juice]
- match = proc { |q, prefix|
- matcher.match(list, q, prefix, '').sort.map { |p| [p.first, p.last.sort] }
- }
-
- assert matcher.caches.empty?
- 3.times do
- ['y j', 'j y'].each do |pat|
- (0..pat.length - 1).each do |prefix_length|
- prefix = pat[0, prefix_length]
- assert_equal(
- [["juicily", [[0, 1], [6, 7]]],
- ["juicy", [[0, 1], [4, 5]]]],
- match.call(pat, prefix))
- end
- end
-
- # $
- assert_equal [["juiceful", [[7, 8]]]], match.call('l$', '')
- assert_equal [["juiceful", [[7, 8]]],
- ["juiceless", [[5, 6]]],
- ["juicily", [[5, 6]]]], match.call('l', '')
-
- # ^
- assert_equal list.length, match.call('j', '').length
- assert_equal list.length - 1, match.call('^j', '').length
-
- # ^ + $
- assert_equal 0, match.call('^juici$', '').length
- assert_equal 1, match.call('^juice$', '').length
- assert_equal 0, match.call('^.*$', '').length
-
- # !
- assert_equal 0, match.call('!j', '').length
-
- # ! + ^
- assert_equal [["_juice", []]], match.call('!^j', '')
-
- # ! + $
- assert_equal list.length - 1, match.call('!l$', '').length
-
- # ! + f
- assert_equal [["juicy", [[4, 5]]]], match.call('y !l', '')
-
- # '
- assert_equal %w[juiceful juiceless juicily],
- match.call('il', '').map { |e| e.first }
- assert_equal %w[juicily],
- match.call("'il", '').map { |e| e.first }
- assert_equal (list - %w[juicily]).sort,
- match.call("!'il", '').map { |e| e.first }.sort
- end
- assert !matcher.caches.empty?
- end
-
- def test_xfuzzy_matcher_prefix_cache
- matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE
- list = %w[
- a.java
- b.java
- java.jive
- c.java$
- d.java
- ]
- 2.times do
- assert_equal 5, matcher.match(list, 'java', 'java', '').length
- assert_equal 3, matcher.match(list, 'java$', 'java$', '').length
- assert_equal 1, matcher.match(list, 'java$$', 'java$$', '').length
-
- assert_equal 0, matcher.match(list, '!java', '!java', '').length
- assert_equal 4, matcher.match(list, '!^jav', '!^jav', '').length
- assert_equal 4, matcher.match(list, '!^java', '!^java', '').length
- assert_equal 2, matcher.match(list, '!^java !b !c', '!^java', '').length
- end
- end
-
- def test_sort_by_rank
- matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE
- xmatcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE
- list = %w[
- 0____1
- 0_____1
- 01
- ____0_1
- 01_
- _01_
- 0______1
- ___01___
- ]
- assert_equal(
- [["01", [[0, 2]]],
- ["01_", [[0, 2]]],
- ["_01_", [[1, 3]]],
- ["___01___", [[3, 5]]],
- ["____0_1", [[4, 7]]],
- ["0____1", [[0, 6]]],
- ["0_____1", [[0, 7]]],
- ["0______1", [[0, 8]]]],
- FZF.sort(matcher.match(list, '01', '', '')))
-
- assert_equal(
- [["01", [[0, 1], [1, 2]]],
- ["01_", [[0, 1], [1, 2]]],
- ["_01_", [[1, 2], [2, 3]]],
- ["0____1", [[0, 1], [5, 6]]],
- ["0_____1", [[0, 1], [6, 7]]],
- ["____0_1", [[4, 5], [6, 7]]],
- ["0______1", [[0, 1], [7, 8]]],
- ["___01___", [[3, 4], [4, 5]]]],
- FZF.sort(xmatcher.match(list, '0 1', '', '')))
-
- assert_equal(
- [["_01_", [[1, 3], [0, 4]], [4, 4, "_01_"]],
- ["___01___", [[3, 5], [0, 2]], [4, 8, "___01___"]],
- ["____0_1", [[4, 7], [0, 2]], [5, 7, "____0_1"]],
- ["0____1", [[0, 6], [1, 3]], [6, 6, "0____1"]],
- ["0_____1", [[0, 7], [1, 3]], [7, 7, "0_____1"]],
- ["0______1", [[0, 8], [1, 3]], [8, 8, "0______1"]]],
- FZF.sort(xmatcher.match(list, '01 __', '', '')).map { |tuple|
- tuple << FZF.rank(tuple)
- }
- )
- end
-
- def test_extended_exact_mode
- exact = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :exact
- fuzzy = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy
- list = %w[
- extended-exact-mode-not-fuzzy
- extended'-fuzzy-mode
- ]
- assert_equal 2, fuzzy.match(list, 'extended', '', '').length
- assert_equal 2, fuzzy.match(list, 'mode extended', '', '').length
- assert_equal 2, fuzzy.match(list, 'xtndd', '', '').length
- assert_equal 2, fuzzy.match(list, "'-fuzzy", '', '').length
-
- assert_equal 2, exact.match(list, 'extended', '', '').length
- assert_equal 2, exact.match(list, 'mode extended', '', '').length
- assert_equal 0, exact.match(list, 'xtndd', '', '').length
- assert_equal 1, exact.match(list, "'-fuzzy", '', '').length
- assert_equal 2, exact.match(list, "-fuzzy", '', '').length
- end
-
- # ^$ -> matches empty item
- def test_format_empty_item
- fzf = FZF.new []
- item = ['', [[0, 0]]]
- line, offsets = item
- tokens = fzf.format line, 80, offsets
- assert_equal [], tokens
- end
-
- def test_mouse_event
- interval = FZF::MouseEvent::DOUBLE_CLICK_INTERVAL
- me = FZF::MouseEvent.new nil
- me.v = 10
- assert_equal false, me.double?(10)
- assert_equal false, me.double?(20)
- me.v = 20
- assert_equal false, me.double?(10)
- assert_equal false, me.double?(20)
- me.v = 20
- assert_equal false, me.double?(10)
- assert_equal true, me.double?(20)
- sleep interval
- assert_equal false, me.double?(20)
- end
-
- def test_nth_match
- list = [
- ' first second third',
- 'fourth fifth sixth',
- ]
-
- matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE
- assert_equal list, matcher.match(list, 'f', '', '').map(&:first)
- assert_equal [
- [list[0], [[2, 5]]],
- [list[1], [[9, 17]]]], matcher.match(list, 'is', '', '')
-
- matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..1]
- assert_equal [[list[1], [[8, 9]]]], matcher.match(list, 'f', '', '')
- assert_equal [[list[0], [[8, 9]]]], matcher.match(list, 's', '', '')
-
- matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2..2]
- assert_equal [[list[0], [[19, 20]]]], matcher.match(list, 'r', '', '')
-
- # Comma-separated
- matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2..2, 0..0]
- assert_equal [[list[0], [[19, 20]]], [list[1], [[3, 4]]]], matcher.match(list, 'r', '', '')
-
- # Ordered
- matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [0..0, 2..2]
- assert_equal [[list[0], [[3, 4]]], [list[1], [[3, 4]]]], matcher.match(list, 'r', '', '')
-
- regex = FZF.build_delim_regex "\t"
- matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [0..0], regex
- assert_equal [[list[0], [[3, 10]]]], matcher.match(list, 're', '', '')
-
- matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..1], regex
- assert_equal [], matcher.match(list, 'r', '', '')
- assert_equal [[list[1], [[9, 17]]]], matcher.match(list, 'is', '', '')
-
- # Negative indexing
- matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [-1..-1], regex
- assert_equal [[list[0], [[3, 6]]]], matcher.match(list, 'rt', '', '')
- assert_equal [[list[0], [[2, 5]]], [list[1], [[9, 17]]]], matcher.match(list, 'is', '', '')
-
- # Regex delimiter
- regex = FZF.build_delim_regex "[ \t]+"
- matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [0..0], regex
- assert_equal [list[1]], matcher.match(list, 'f', '', '').map(&:first)
-
- matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..1], regex
- assert_equal [[list[0], [[1, 2]]], [list[1], [[8, 9]]]], matcher.match(list, 'f', '', '')
- end
-
- def test_nth_match_range
- list = [
- ' first second third',
- 'fourth fifth sixth',
- ]
-
- matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..2]
- assert_equal [[list[0], [[8, 20]]]], matcher.match(list, 'sr', '', '')
- assert_equal [], matcher.match(list, 'fo', '', '')
-
- matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..-1, 0..0]
- assert_equal [[list[0], [[8, 20]]]], matcher.match(list, 'sr', '', '')
- assert_equal [[list[1], [[0, 2]]]], matcher.match(list, 'fo', '', '')
-
- matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy, [0..0, 1..2]
- assert_equal [], matcher.match(list, '^t', '', '')
-
- matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy, [0..1, 2..2]
- assert_equal [[list[0], [[16, 17]]]], matcher.match(list, '^t', '', '')
-
- matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy, [1..-1]
- assert_equal [[list[0], [[8, 9]]]], matcher.match(list, '^s', '', '')
- end
-
- def stream_for str, delay = 0
- StringIO.new(str).tap do |sio|
- sio.instance_eval do
- alias org_gets gets
-
- def gets
- org_gets.tap { |e| sleep(@delay) unless e.nil? }
- end
-
- def reopen _
- end
- end
- sio.instance_variable_set :@delay, delay
- end
- end
-
- def assert_fzf_output opts, given, expected
- stream = stream_for given
- output = stream_for ''
-
- def sorted_lines line
- line.split($/).sort
- end
-
- begin
- tty = MockTTY.new
- $stdout = output
- fzf = FZF.new(opts, stream)
- fzf.instance_variable_set :@tty, tty
- thr = block_given? && Thread.new { yield tty }
- fzf.start
- thr && thr.join
- rescue SystemExit => e
- assert_equal 0, e.status
- assert_equal sorted_lines(expected), sorted_lines(output.string)
- ensure
- $stdout = STDOUT
- end
- end
-
- def test_filter
- {
- %w[--filter=ol] => 'World',
- %w[--filter=ol --print-query] => "ol\nWorld",
- }.each do |opts, expected|
- assert_fzf_output opts, "Hello\nWorld", expected
- end
- end
-
- def test_select_1
- {
- %w[--query=ol --select-1] => 'World',
- %w[--query=ol --select-1 --print-query] => "ol\nWorld",
- }.each do |opts, expected|
- assert_fzf_output opts, "Hello\nWorld", expected
- end
- end
-
- def test_select_1_without_query
- assert_fzf_output %w[--select-1], 'Hello World', 'Hello World'
- end
-
- def test_select_1_ambiguity
- begin
- Timeout::timeout(0.5) do
- assert_fzf_output %w[--query=o --select-1], "hello\nworld", "should not match"
- end
- rescue Timeout::Error
- Curses.close_screen
- end
- end
-
- def test_exit_0
- {
- %w[--query=zz --exit-0] => '',
- %w[--query=zz --exit-0 --print-query] => 'zz',
- }.each do |opts, expected|
- assert_fzf_output opts, "Hello\nWorld", expected
- end
- end
-
- def test_exit_0_without_query
- assert_fzf_output %w[--exit-0], '', ''
- end
-
- def test_with_nth
- source = "hello world\nbatman"
- assert_fzf_output %w[-0 -1 --with-nth=2,1 -x -q ^worl],
- source, 'hello world'
- assert_fzf_output %w[-0 -1 --with-nth=2,1 -x -q llo$],
- source, 'hello world'
- assert_fzf_output %w[-0 -1 --with-nth=.. -x -q llo$],
- source, ''
- assert_fzf_output %w[-0 -1 --with-nth=2,2,2,..,1 -x -q worlworlworlhellworlhell],
- source, 'hello world'
- assert_fzf_output %w[-0 -1 --with-nth=1,1,-1,1 -x -q batbatbatbat],
- source, 'batman'
- end
-
- def test_with_nth_transform
- fzf = FZF.new %w[--with-nth 2..,1]
- assert_equal 'my world hello', fzf.transform('hello my world')
- assert_equal 'my world hello', fzf.transform('hello my world')
- assert_equal 'my world hello', fzf.transform('hello my world ')
-
- fzf = FZF.new %w[--with-nth 2,-1,2]
- assert_equal 'my world my', fzf.transform('hello my world')
- assert_equal 'world world world', fzf.transform('hello world')
- assert_equal 'world world world', fzf.transform('hello world ')
- end
-
- def test_ranking_overlap_match_regions
- list = [
- '1 3 4 2',
- '1 2 3 4'
- ]
- assert_equal [
- ['1 2 3 4', [[0, 13], [16, 22]]],
- ['1 3 4 2', [[0, 24], [12, 17]]],
- ], FZF.sort(FZF::ExtendedFuzzyMatcher.new(nil).match(list, '12 34', '', ''))
- end
-
- def test_constrain
- fzf = FZF.new []
-
- # [#**** ]
- assert_equal [false, 0, 0], fzf.constrain(0, 0, 5, 100)
-
- # *****[**#** ... ] => [**#******* ... ]
- assert_equal [true, 0, 2], fzf.constrain(5, 7, 10, 100)
-
- # [**********]**#** => ***[*********#]**
- assert_equal [true, 3, 12], fzf.constrain(0, 12, 15, 10)
-
- # *****[**#** ] => ***[**#****]
- assert_equal [true, 3, 5], fzf.constrain(5, 7, 10, 7)
-
- # *****[**#** ] => ****[**#***]
- assert_equal [true, 4, 6], fzf.constrain(5, 7, 10, 6)
-
- # ***** [#] => ****[#]
- assert_equal [true, 4, 4], fzf.constrain(10, 10, 5, 1)
-
- # [ ] #**** => [#]****
- assert_equal [true, 0, 0], fzf.constrain(-5, 0, 5, 1)
-
- # [ ] **#** => **[#]**
- assert_equal [true, 2, 2], fzf.constrain(-5, 2, 5, 1)
-
- # [***** #] => [****# ]
- assert_equal [true, 0, 4], fzf.constrain(0, 7, 5, 10)
-
- # **[***** #] => [******# ]
- assert_equal [true, 0, 6], fzf.constrain(2, 10, 7, 10)
- end
-
- def test_invalid_utf8
- tmp = Tempfile.new('fzf')
- tmp << 'hello ' << [0xff].pack('C*') << ' world' << $/ << [0xff].pack('C*')
- tmp.close
- begin
- Timeout::timeout(0.5) do
- FZF.new(%w[-n..,1,2.. -q^ -x], File.open(tmp.path)).start
- end
- rescue Timeout::Error
- Curses.close_screen
- end
- ensure
- tmp.unlink
- end
-
- def test_with_nth_mock_tty
- # Manual selection with input
- assert_fzf_output ["--with-nth=2,1"], "hello world", "hello world" do |tty|
- tty << "world"
- tty << "hell"
- tty << "\r"
- end
-
- # Manual selection without input
- assert_fzf_output ["--with-nth=2,1"], "hello world", "hello world" do |tty|
- tty << "\r"
- end
-
- # Manual selection with input and --multi
- lines = "hello world\ngoodbye world"
- assert_fzf_output %w[-m --with-nth=2,1], lines, lines do |tty|
- tty << "o"
- tty << "\e[Z\e[Z"
- tty << "\r"
- end
-
- # Manual selection without input and --multi
- assert_fzf_output %w[-m --with-nth=2,1], lines, lines do |tty|
- tty << "\e[Z\e[Z"
- tty << "\r"
- end
-
- # ALT-D
- assert_fzf_output %w[--print-query], "", "hello baby = world" do |tty|
- tty << "hello world baby"
- tty << alt(:b) << alt(:b) << alt(:d)
- tty << ctrl(:e) << " = " << ctrl(:y)
- tty << "\r"
- end
-
- # ALT-BACKSPACE
- assert_fzf_output %w[--print-query], "", "hello baby = world " do |tty|
- tty << "hello world baby"
- tty << alt(:b) << alt(127.chr)
- tty << ctrl(:e) << " = " << ctrl(:y)
- tty << "\r"
- end
-
- # Word-movements
- assert_fzf_output %w[--print-query], "", "ello!_orld!~ foo=?" do |tty|
- tty << "hello_world==baby?"
- tty << alt(:b) << ctrl(:d)
- tty << alt(:b) << ctrl(:d)
- tty << alt(:b) << ctrl(:d)
- tty << alt(:f) << '!'
- tty << alt(:f) << '!'
- tty << alt(:d) << '~'
- tty << " foo=bar foo=bar"
- tty << ctrl(:w)
- tty << alt(127.chr)
- tty << "\r"
- end
- end
-
- def alt chr
- "\e#{chr}"
- end
-
- def ctrl char
- char.to_s.ord - 'a'.ord + 1
- end
-end
-
diff --git a/test/test_go.rb b/test/test_go.rb
new file mode 100644
index 0000000..e76b520
--- /dev/null
+++ b/test/test_go.rb
@@ -0,0 +1,1100 @@
+#!/usr/bin/env ruby
+# encoding: utf-8
+
+require 'minitest/autorun'
+require 'fileutils'
+
+DEFAULT_TIMEOUT = 20
+
+base = File.expand_path('../../', __FILE__)
+Dir.chdir base
+FZF = "#{base}/bin/fzf"
+
+class NilClass
+ def include? str
+ false
+ end
+
+ def start_with? str
+ false
+ end
+
+ def end_with? str
+ false
+ end
+end
+
+def wait
+ since = Time.now
+ while Time.now - since < DEFAULT_TIMEOUT
+ return if yield
+ sleep 0.05
+ end
+ throw 'timeout'
+end
+
+class Shell
+ class << self
+ def bash
+ 'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.bash'
+ end
+
+ def zsh
+ FileUtils.mkdir_p '/tmp/fzf-zsh'
+ FileUtils.cp File.expand_path('~/.fzf.zsh'), '/tmp/fzf-zsh/.zshrc'
+ 'PS1= PROMPT_COMMAND= HISTSIZE=100 ZDOTDIR=/tmp/fzf-zsh zsh'
+ end
+ end
+end
+
+class Tmux
+ TEMPNAME = '/tmp/fzf-test.txt'
+
+ attr_reader :win
+
+ def initialize shell = :bash
+ @win =
+ case shell
+ when :bash
+ go("new-window -d -P -F '#I' '#{Shell.bash}'").first
+ when :zsh
+ go("new-window -d -P -F '#I' '#{Shell.zsh}'").first
+ when :fish
+ go("new-window -d -P -F '#I' 'fish'").first
+ else
+ raise "Unknown shell: #{shell}"
+ end
+ @lines = `tput lines`.chomp.to_i
+
+ if shell == :fish
+ send_keys('function fish_prompt; end; clear', :Enter)
+ self.until { |lines| lines.empty? }
+ end
+ end
+
+ def kill
+ go("kill-window -t #{win} 2> /dev/null")
+ end
+
+ def send_keys *args
+ target =
+ if args.last.is_a?(Hash)
+ hash = args.pop
+ go("select-window -t #{win}")
+ "#{win}.#{hash[:pane]}"
+ else
+ win
+ end
+ args = args.map { |a| %{"#{a}"} }.join ' '
+ go("send-keys -t #{target} #{args}")
+ end
+
+ def capture pane = 0
+ File.unlink TEMPNAME while File.exists? TEMPNAME
+ wait do
+ go("capture-pane -t #{win}.#{pane} \\; save-buffer #{TEMPNAME} 2> /dev/null")
+ $?.exitstatus == 0
+ end
+ File.read(TEMPNAME).split($/)[0, @lines].reverse.drop_while(&:empty?).reverse
+ end
+
+ def until pane = 0
+ lines = nil
+ begin
+ 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
+ lines
+ end
+
+ def prepare
+ tries = 0
+ begin
+ self.send_keys 'C-u', 'hello', 'Right'
+ self.until { |lines| lines[-1].end_with?('hello') }
+ rescue Exception
+ (tries += 1) < 5 ? retry : raise
+ end
+ self.send_keys 'C-u'
+ end
+private
+ def go *args
+ %x[tmux #{args.join ' '}].split($/)
+ end
+end
+
+class TestBase < Minitest::Test
+ TEMPNAME = '/tmp/output'
+
+ attr_reader :tmux
+
+ def tempname
+ [TEMPNAME,
+ caller_locations.map(&:label).find { |l| l =~ /^test_/ }].join '-'
+ end
+
+ def setup
+ ENV.delete 'FZF_DEFAULT_OPTS'
+ ENV.delete 'FZF_CTRL_T_COMMAND'
+ ENV.delete 'FZF_DEFAULT_COMMAND'
+ end
+
+ def readonce
+ wait { File.exists?(tempname) }
+ File.read(tempname)
+ ensure
+ File.unlink tempname while File.exists?(tempname)
+ tmux.prepare
+ end
+
+ def fzf(*opts)
+ fzf!(*opts) + " > #{tempname}.tmp; mv #{tempname}.tmp #{tempname}"
+ end
+
+ def fzf!(*opts)
+ opts = opts.map { |o|
+ case o
+ when Symbol
+ o = o.to_s
+ o.length > 1 ? "--#{o.gsub('_', '-')}" : "-#{o}"
+ when String, Numeric
+ o.to_s
+ else
+ nil
+ end
+ }.compact
+ "#{FZF} #{opts.join ' '}"
+ end
+end
+
+class TestGoFZF < TestBase
+ def setup
+ super
+ @tmux = Tmux.new
+ end
+
+ def teardown
+ @tmux.kill
+ end
+
+ def test_vanilla
+ tmux.send_keys "seq 1 100000 | #{fzf}", :Enter
+ tmux.until { |lines| lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ }
+ lines = tmux.capture
+ assert_equal ' 2', lines[-4]
+ assert_equal '> 1', lines[-3]
+ assert_equal ' 100000/100000', lines[-2]
+ assert_equal '>', lines[-1]
+
+ # Testing basic key bindings
+ tmux.send_keys '99', 'C-a', '1', 'C-f', '3', 'C-b', 'C-h', 'C-u', 'C-e', 'C-y', 'C-k', 'Tab', 'BTab'
+ tmux.until { |lines| lines[-2] == ' 856/100000' }
+ lines = tmux.capture
+ assert_equal '> 1391', lines[-4]
+ assert_equal ' 391', lines[-3]
+ assert_equal ' 856/100000', lines[-2]
+ assert_equal '> 391', lines[-1]
+
+ tmux.send_keys :Enter
+ assert_equal '1391', readonce.chomp
+ end
+
+ def test_fzf_default_command
+ tmux.send_keys "FZF_DEFAULT_COMMAND='echo hello' #{fzf}", :Enter
+ tmux.until { |lines| lines.last =~ /^>/ }
+
+ tmux.send_keys :Enter
+ assert_equal 'hello', readonce.chomp
+ end
+
+ def test_key_bindings
+ tmux.send_keys "#{FZF} -q 'foo bar foo-bar'", :Enter
+ tmux.until { |lines| lines.last =~ /^>/ }
+
+ # CTRL-A
+ tmux.send_keys "C-A", "("
+ tmux.until { |lines| lines.last == '> (foo bar foo-bar' }
+
+ # META-F
+ tmux.send_keys :Escape, :f, ")"
+ tmux.until { |lines| lines.last == '> (foo) bar foo-bar' }
+
+ # CTRL-B
+ tmux.send_keys "C-B", "var"
+ tmux.until { |lines| lines.last == '> (foovar) bar foo-bar' }
+
+ # Left, CTRL-D
+ tmux.send_keys :Left, :Left, "C-D"
+ tmux.until { |lines| lines.last == '> (foovr) bar foo-bar' }
+
+ # META-BS
+ tmux.send_keys :Escape, :BSpace
+ tmux.until { |lines| lines.last == '> (r) bar foo-bar' }
+
+ # CTRL-Y
+ tmux.send_keys "C-Y", "C-Y"
+ tmux.until { |lines| lines.last == '> (foovfoovr) bar foo-bar' }
+
+ # META-B
+ tmux.send_keys :Escape, :b, :Space, :Space
+ tmux.until { |lines| lines.last == '> ( foovfoovr) bar foo-bar' }
+
+ # CTRL-F / Right
+ tmux.send_keys 'C-F', :Right, '/'
+ tmux.until { |lines| lines.last == '> ( fo/ovfoovr) bar foo-bar' }
+
+ # CTRL-H / BS
+ tmux.send_keys 'C-H', :BSpace
+ tmux.until { |lines| lines.last == '> ( fovfoovr) bar foo-bar' }
+
+ # CTRL-E
+ tmux.send_keys "C-E", 'baz'
+ tmux.until { |lines| lines.last == '> ( fovfoovr) bar foo-barbaz' }
+
+ # CTRL-U
+ tmux.send_keys "C-U"
+ tmux.until { |lines| lines.last == '>' }
+
+ # CTRL-Y
+ tmux.send_keys "C-Y"
+ tmux.until { |lines| lines.last == '> ( fovfoovr) bar foo-barbaz' }
+
+ # CTRL-W
+ tmux.send_keys "C-W", "bar-foo"
+ tmux.until { |lines| lines.last == '> ( fovfoovr) bar bar-foo' }
+
+ # META-D
+ tmux.send_keys :Escape, :b, :Escape, :b, :Escape, :d, "C-A", "C-Y"
+ tmux.until { |lines| lines.last == '> bar( fovfoovr) bar -foo' }
+
+ # CTRL-M
+ tmux.send_keys "C-M"
+ tmux.until { |lines| lines.last !~ /^>/ }
+ end
+
+ def test_multi_order
+ tmux.send_keys "seq 1 10 | #{fzf :multi}", :Enter
+ tmux.until { |lines| lines.last =~ /^>/ }
+
+ tmux.send_keys :Tab, :Up, :Up, :Tab, :Tab, :Tab, # 3, 2
+ 'C-K', 'C-K', 'C-K', 'C-K', :BTab, :BTab, # 5, 6
+ :PgUp, 'C-J', :Down, :Tab, :Tab # 8, 7
+ tmux.until { |lines| lines[-2].include? '(6)' }
+ tmux.send_keys "C-M"
+ assert_equal %w[3 2 5 6 8 7], readonce.split($/)
+ end
+
+ def test_with_nth
+ [true, false].each do |multi|
+ tmux.send_keys "(echo ' 1st 2nd 3rd/';
+ echo ' first second third/') |
+ #{fzf multi && :multi, :x, :nth, 2, :with_nth, '2,-1,1'}",
+ :Enter
+ tmux.until { |lines| lines[-2].include?('2/2') }
+
+ # Transformed list
+ lines = tmux.capture
+ assert_equal ' second third/first', lines[-4]
+ assert_equal '> 2nd 3rd/1st', lines[-3]
+
+ # However, the output must not be transformed
+ if multi
+ tmux.send_keys :BTab, :BTab
+ tmux.until { |lines| lines[-2].include?('(2)') }
+ tmux.send_keys :Enter
+ assert_equal [' 1st 2nd 3rd/', ' first second third/'], readonce.split($/)
+ else
+ tmux.send_keys '^', '3'
+ tmux.until { |lines| lines[-2].include?('1/2') }
+ tmux.send_keys :Enter
+ assert_equal [' 1st 2nd 3rd/'], readonce.split($/)
+ end
+ end
+ end
+
+ def test_scroll
+ [true, false].each do |rev|
+ tmux.send_keys "seq 1 100 | #{fzf rev && :reverse}", :Enter
+ tmux.until { |lines| lines.include? ' 100/100' }
+ tmux.send_keys *110.times.map { rev ? :Down : :Up }
+ tmux.until { |lines| lines.include? '> 100' }
+ tmux.send_keys :Enter
+ assert_equal '100', readonce.chomp
+ end
+ end
+
+ def test_select_1
+ tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 5555, :'1'}", :Enter
+ assert_equal ['5555', '55'], readonce.split($/)
+ end
+
+ def test_exit_0
+ tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 555555, :'0'}", :Enter
+ assert_equal ['555555'], readonce.split($/)
+ end
+
+ def test_select_1_exit_0_fail
+ [:'0', :'1', [:'1', :'0']].each do |opt|
+ tmux.send_keys "seq 1 100 | #{fzf :print_query, :multi, :q, 5, *opt}", :Enter
+ tmux.until { |lines| lines.last =~ /^> 5/ }
+ tmux.send_keys :BTab, :BTab, :BTab
+ tmux.until { |lines| lines[-2].include?('(3)') }
+ tmux.send_keys :Enter
+ assert_equal ['5', '5', '15', '25'], readonce.split($/)
+ end
+ end
+
+ def test_query_unicode
+ tmux.send_keys "(echo abc; echo 가나다) | #{fzf :query, '가다'}", :Enter
+ tmux.until { |lines| lines[-2].include? '1/2' }
+ tmux.send_keys :Enter
+ assert_equal ['가나다'], readonce.split($/)
+ end
+
+ def test_sync
+ tmux.send_keys "seq 1 100 | #{fzf! :multi} | awk '{print \\$1 \\$1}' | #{fzf :sync}", :Enter
+ tmux.until { |lines| lines[-1] == '>' }
+ tmux.send_keys 9
+ tmux.until { |lines| lines[-2] == ' 19/100' }
+ tmux.send_keys :BTab, :BTab, :BTab
+ tmux.until { |lines| lines[-2].include?('(3)') }
+ tmux.send_keys :Enter
+ tmux.until { |lines| lines[-1] == '>' }
+ tmux.send_keys 'C-K', :Enter
+ assert_equal ['1919'], readonce.split($/)
+ end
+
+ def test_tac
+ tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter
+ tmux.until { |lines| lines[-2].include? '1000/1000' }
+ tmux.send_keys :BTab, :BTab, :BTab
+ tmux.until { |lines| lines[-2].include?('(3)') }
+ tmux.send_keys :Enter
+ assert_equal %w[1000 999 998], readonce.split($/)
+ end
+
+ def test_tac_sort
+ tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter
+ tmux.until { |lines| lines[-2].include? '1000/1000' }
+ tmux.send_keys '99'
+ tmux.send_keys :BTab, :BTab, :BTab
+ tmux.until { |lines| lines[-2].include?('(3)') }
+ tmux.send_keys :Enter
+ assert_equal %w[99 999 998], readonce.split($/)
+ end
+
+ def test_tac_nosort
+ tmux.send_keys "seq 1 1000 | #{fzf :tac, :no_sort, :multi}", :Enter
+ tmux.until { |lines| lines[-2].include? '1000/1000' }
+ tmux.send_keys '00'
+ tmux.until { |lines| lines[-2].include? '10/1000' }
+ tmux.send_keys :BTab, :BTab, :BTab
+ tmux.until { |lines| lines[-2].include?('(3)') }
+ tmux.send_keys :Enter
+ assert_equal %w[1000 900 800], readonce.split($/)
+ end
+
+ def test_expect
+ test = lambda do |key, feed, expected = key|
+ tmux.send_keys "seq 1 100 | #{fzf :expect, key}", :Enter
+ tmux.until { |lines| lines[-2].include? '100/100' }
+ tmux.send_keys '55'
+ tmux.until { |lines| lines[-2].include? '1/100' }
+ tmux.send_keys *feed
+ assert_equal [expected, '55'], readonce.split($/)
+ end
+ test.call 'ctrl-t', 'C-T'
+ test.call 'ctrl-t', 'Enter', ''
+ test.call 'alt-c', [:Escape, :c]
+ test.call 'f1', 'f1'
+ test.call 'f2', 'f2'
+ test.call 'f3', 'f3'
+ test.call 'f2,f4', 'f2', 'f2'
+ test.call 'f2,f4', 'f4', 'f4'
+ test.call '@', '@'
+ end
+
+ def test_expect_print_query
+ tmux.send_keys "seq 1 100 | #{fzf '--expect=alt-z', :print_query}", :Enter
+ tmux.until { |lines| lines[-2].include? '100/100' }
+ tmux.send_keys '55'
+ tmux.until { |lines| lines[-2].include? '1/100' }
+ tmux.send_keys :Escape, :z
+ assert_equal ['55', 'alt-z', '55'], readonce.split($/)
+ end
+
+ def test_expect_print_query_select_1
+ tmux.send_keys "seq 1 100 | #{fzf '-q55 -1 --expect=alt-z --print-query'}", :Enter
+ assert_equal ['55', '', '55'], readonce.split($/)
+ end
+
+ def test_toggle_sort
+ ['--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.send_keys :Tab
+ tmux.until { |lines| lines[-2].include? '4/111 (1)' }
+ tmux.send_keys 'C-R'
+ tmux.until { |lines| lines[-3].include? '> 11' }
+ tmux.send_keys :Tab
+ tmux.until { |lines| lines[-2].include? '4/111/S (2)' }
+ tmux.send_keys :Enter
+ assert_equal ['111', '11'], readonce.split($/)
+ end
+ end
+
+ def test_unicode_case
+ writelines tempname, %w[строКА1 СТРОКА2 строка3 Строка4]
+ assert_equal %w[СТРОКА2 Строка4], `cat #{tempname} | #{FZF} -fС`.split($/)
+ assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `cat #{tempname} | #{FZF} -fс`.split($/)
+ end
+
+ def test_tiebreak
+ input = %w[
+ --foobar--------
+ -----foobar---
+ ----foobar--
+ -------foobar-
+ ]
+ writelines tempname, input
+
+ assert_equal input, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=index`.split($/)
+
+ by_length = %w[
+ ----foobar--
+ -----foobar---
+ -------foobar-
+ --foobar--------
+ ]
+ assert_equal by_length, `cat #{tempname} | #{FZF} -ffoobar`.split($/)
+ assert_equal by_length, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=length`.split($/)
+
+ by_begin = %w[
+ --foobar--------
+ ----foobar--
+ -----foobar---
+ -------foobar-
+ ]
+ assert_equal by_begin, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=begin`.split($/)
+ assert_equal by_begin, `cat #{tempname} | #{FZF} -f"!z foobar" -x --tiebreak begin`.split($/)
+
+ assert_equal %w[
+ -------foobar-
+ ----foobar--
+ -----foobar---
+ --foobar--------
+ ], `cat #{tempname} | #{FZF} -ffoobar --tiebreak end`.split($/)
+
+ assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/)
+ end
+
+ def test_tiebreak_length_with_nth
+ input = %w[
+ 1:hell
+ 123:hello
+ 12345:he
+ 1234567:h
+ ]
+ writelines tempname, input
+
+ output = %w[
+ 1:hell
+ 12345:he
+ 123:hello
+ 1234567:h
+ ]
+ assert_equal output, `cat #{tempname} | #{FZF} -fh`.split($/)
+
+ output = %w[
+ 1234567:h
+ 12345:he
+ 1:hell
+ 123:hello
+ ]
+ assert_equal output, `cat #{tempname} | #{FZF} -fh -n2 -d:`.split($/)
+ end
+
+ def test_tiebreak_end_backward_scan
+ input = %w[
+ foobar-fb
+ fubar
+ ]
+ writelines tempname, input
+
+ assert_equal input.reverse, `cat #{tempname} | #{FZF} -f fb`.split($/)
+ assert_equal input, `cat #{tempname} | #{FZF} -f fb --tiebreak=end`.split($/)
+ 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
+ data = '.' * 256 * 1024
+ File.open(tempname, 'w') do |f|
+ f << data
+ end
+ assert_equal data, `cat #{tempname} | #{FZF} -f .`.chomp
+ end
+
+ def test_read0
+ lines = `find .`.split($/)
+ assert_equal lines.last, `find . | #{FZF} -e -f "^#{lines.last}$"`.chomp
+ assert_equal lines.last, `find . -print0 | #{FZF} --read0 -e -f "^#{lines.last}$"`.chomp
+ end
+
+ def test_select_all_deselect_all_toggle_all
+ tmux.send_keys "seq 100 | #{fzf '--bind ctrl-a:select-all,ctrl-d:deselect-all,ctrl-t:toggle-all --multi'}", :Enter
+ tmux.until { |lines| lines[-2].include? '100/100' }
+ tmux.send_keys :BTab, :BTab, :BTab
+ tmux.until { |lines| lines[-2].include? '(3)' }
+ tmux.send_keys 'C-t'
+ tmux.until { |lines| lines[-2].include? '(97)' }
+ tmux.send_keys 'C-a'
+ tmux.until { |lines| lines[-2].include? '(100)' }
+ tmux.send_keys :Tab, :Tab
+ tmux.until { |lines| lines[-2].include? '(98)' }
+ tmux.send_keys 'C-d'
+ tmux.until { |lines| !lines[-2].include? '(' }
+ tmux.send_keys :Tab, :Tab
+ tmux.until { |lines| lines[-2].include? '(2)' }
+ tmux.send_keys 0
+ tmux.until { |lines| lines[-2].include? '10/100' }
+ tmux.send_keys 'C-a'
+ tmux.until { |lines| lines[-2].include? '(12)' }
+ tmux.send_keys :Enter
+ assert_equal %w[2 1 10 20 30 40 50 60 70 80 90 100], readonce.split($/)
+ end
+
+ def test_history
+ history_file = '/tmp/fzf-test-history'
+
+ # History with limited number of entries
+ File.unlink history_file rescue nil
+ opts = "--history=#{history_file} --history-size=4"
+ input = %w[00 11 22 33 44].map { |e| e + $/ }
+ input.each do |keys|
+ tmux.send_keys "seq 100 | #{fzf opts}", :Enter
+ tmux.until { |lines| lines[-2].include? '100/100' }
+ tmux.send_keys keys
+ tmux.until { |lines| lines[-2].include? '1/100' }
+ tmux.send_keys :Enter
+ readonce
+ end
+ assert_equal input[1..-1], File.readlines(history_file)
+
+ # Update history entries (not changed on disk)
+ tmux.send_keys "seq 100 | #{fzf opts}", :Enter
+ tmux.until { |lines| lines[-2].include? '100/100' }
+ tmux.send_keys 'C-p'
+ tmux.until { |lines| lines[-1].end_with? '> 44' }
+ tmux.send_keys 'C-p'
+ tmux.until { |lines| lines[-1].end_with? '> 33' }
+ tmux.send_keys :BSpace
+ tmux.until { |lines| lines[-1].end_with? '> 3' }
+ tmux.send_keys 1
+ tmux.until { |lines| lines[-1].end_with? '> 31' }
+ tmux.send_keys 'C-p'
+ tmux.until { |lines| lines[-1].end_with? '> 22' }
+ tmux.send_keys 'C-n'
+ tmux.until { |lines| lines[-1].end_with? '> 31' }
+ tmux.send_keys 0
+ tmux.until { |lines| lines[-1].end_with? '> 310' }
+ tmux.send_keys :Enter
+ readonce
+ assert_equal %w[22 33 44 310].map { |e| e + $/ }, File.readlines(history_file)
+
+ # Respect --bind option
+ tmux.send_keys "seq 100 | #{fzf opts + ' --bind ctrl-p:next-history,ctrl-n:previous-history'}", :Enter
+ tmux.until { |lines| lines[-2].include? '100/100' }
+ tmux.send_keys 'C-n', 'C-n', 'C-n', 'C-n', 'C-p'
+ tmux.until { |lines| lines[-1].end_with?('33') }
+ tmux.send_keys :Enter
+ ensure
+ File.unlink history_file
+ end
+
+ def test_execute
+ output = '/tmp/fzf-test-execute'
+ opts = %[--bind \\"alt-a:execute(echo '[{}]' >> #{output}),alt-b:execute[echo '({}), ({})' >> #{output}],C:execute:echo '({}), [{}], @{}@' >> #{output}\\"]
+ tmux.send_keys "seq 100 | #{fzf opts}", :Enter
+ tmux.until { |lines| lines[-2].include? '100/100' }
+ tmux.send_keys :Escape, :a, :Escape, :a
+ tmux.send_keys :Up
+ tmux.send_keys :Escape, :b, :Escape, :b
+ tmux.send_keys :Up
+ tmux.send_keys :C
+ tmux.send_keys 'foobar'
+ tmux.until { |lines| lines[-2].include? '0/100' }
+ tmux.send_keys :Escape, :a, :Escape, :b, :Escape, :c
+ tmux.send_keys :Enter
+ readonce
+ assert_equal ['["1"]', '["1"]', '("2"), ("2")', '("2"), ("2")', '("3"), ["3"], @"3"@'],
+ File.readlines(output).map(&:chomp)
+ ensure
+ File.unlink output rescue nil
+ end
+
+ def test_cycle
+ tmux.send_keys "seq 8 | #{fzf :cycle}", :Enter
+ tmux.until { |lines| lines[-2].include? '8/8' }
+ tmux.send_keys :Down
+ tmux.until { |lines| lines[-10].start_with? '>' }
+ tmux.send_keys :Down
+ tmux.until { |lines| lines[-9].start_with? '>' }
+ tmux.send_keys :PgUp
+ tmux.until { |lines| lines[-10].start_with? '>' }
+ tmux.send_keys :PgUp
+ tmux.until { |lines| lines[-3].start_with? '>' }
+ tmux.send_keys :Up
+ tmux.until { |lines| lines[-4].start_with? '>' }
+ tmux.send_keys :PgDn
+ tmux.until { |lines| lines[-3].start_with? '>' }
+ tmux.send_keys :PgDn
+ tmux.until { |lines| lines[-10].start_with? '>' }
+ end
+
+ def test_header_lines
+ tmux.send_keys "seq 100 | #{fzf '--header-lines=10 -q 5'}", :Enter
+ 2.times do
+ tmux.until do |lines|
+ lines[-2].include?('/90') &&
+ lines[-3] == ' 1' &&
+ lines[-4] == ' 2' &&
+ lines[-13] == '> 15'
+ end
+ tmux.send_keys :Down
+ end
+ tmux.send_keys :Enter
+ assert_equal '15', readonce.chomp
+ end
+
+ def test_header_lines_reverse
+ tmux.send_keys "seq 100 | #{fzf '--header-lines=10 -q 5 --reverse'}", :Enter
+ 2.times do
+ tmux.until do |lines|
+ lines[1].include?('/90') &&
+ lines[2] == ' 1' &&
+ lines[3] == ' 2' &&
+ lines[12] == '> 15'
+ end
+ tmux.send_keys :Up
+ end
+ tmux.send_keys :Enter
+ assert_equal '15', readonce.chomp
+ end
+
+ def test_header_lines_overflow
+ tmux.send_keys "seq 100 | #{fzf '--header-lines=200'}", :Enter
+ tmux.until do |lines|
+ lines[-2].include?('0/0') &&
+ lines[-3].include?(' 1')
+ end
+ tmux.send_keys :Enter
+ assert_equal '', readonce.chomp
+ end
+
+ def test_header_lines_with_nth
+ tmux.send_keys "seq 100 | #{fzf "--header-lines 5 --with-nth 1,1,1,1,1"}", :Enter
+ tmux.until do |lines|
+ lines[-2].include?('95/95') &&
+ lines[-3] == ' 11111' &&
+ lines[-7] == ' 55555' &&
+ lines[-8] == '> 66666'
+ end
+ tmux.send_keys :Enter
+ assert_equal '6', readonce.chomp
+ end
+
+ def test_header
+ tmux.send_keys "seq 100 | #{fzf "--header \\\"\\$(head -5 #{__FILE__})\\\""}", :Enter
+ header = File.readlines(__FILE__).take(5).map(&:strip)
+ tmux.until do |lines|
+ lines[-2].include?('100/100') &&
+ lines[-7..-3].map(&:strip) == header
+ end
+ end
+
+ def test_header_reverse
+ tmux.send_keys "seq 100 | #{fzf "--header=\\\"\\$(head -5 #{__FILE__})\\\" --reverse"}", :Enter
+ header = File.readlines(__FILE__).take(5).map(&:strip)
+ tmux.until do |lines|
+ lines[1].include?('100/100') &&
+ lines[2..6].map(&:strip) == header
+ end
+ end
+
+ def test_header_and_header_lines
+ tmux.send_keys "seq 100 | #{fzf "--header-lines 10 --header \\\"\\$(head -5 #{__FILE__})\\\""}", :Enter
+ header = File.readlines(__FILE__).take(5).map(&:strip)
+ tmux.until do |lines|
+ lines[-2].include?('90/90') &&
+ lines[-7...-2].map(&:strip) == header &&
+ lines[-17...-7].map(&:strip) == (1..10).map(&:to_s).reverse
+ end
+ end
+
+ def test_header_and_header_lines_reverse
+ tmux.send_keys "seq 100 | #{fzf "--reverse --header-lines 10 --header \\\"\\$(head -5 #{__FILE__})\\\""}", :Enter
+ header = File.readlines(__FILE__).take(5).map(&:strip)
+ tmux.until do |lines|
+ lines[1].include?('90/90') &&
+ lines[2...7].map(&:strip) == header &&
+ lines[7...17].map(&:strip) == (1..10).map(&:to_s)
+ end
+ end
+
+ def test_canel
+ tmux.send_keys "seq 10 | #{fzf "--bind 2:cancel"}", :Enter
+ tmux.until { |lines| lines[-2].include?('10/10') }
+ tmux.send_keys '123'
+ tmux.until { |lines| lines[-1] == '> 3' && lines[-2].include?('1/10') }
+ tmux.send_keys 'C-y', 'C-y'
+ tmux.until { |lines| lines[-1] == '> 311' }
+ tmux.send_keys 2
+ tmux.until { |lines| lines[-1] == '>' }
+ tmux.send_keys 2
+ tmux.prepare
+ end
+
+ def test_margin
+ tmux.send_keys "yes | head -1000 | #{fzf "--margin 5,3"}", :Enter
+ tmux.until { |lines| lines[4] == '' && lines[5] == ' y' }
+ tmux.send_keys :Enter
+ end
+
+ def test_margin_reverse
+ tmux.send_keys "seq 1000 | #{fzf "--margin 7,5 --reverse"}", :Enter
+ tmux.until { |lines| lines[1 + 7] == ' 1000/1000' }
+ tmux.send_keys :Enter
+ end
+
+ def test_with_nth
+ writelines tempname, ['hello world ', 'byebye']
+ assert_equal 'hello world ', `cat #{tempname} | #{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1`.chomp
+ end
+
+ def test_with_nth_ansi
+ writelines tempname, ["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye']
+ assert_equal 'hello world ', `cat #{tempname} | #{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi`.chomp
+ end
+
+ def test_with_nth_no_ansi
+ src = "\x1b[33mhello \x1b[34;1mworld\x1b[m "
+ writelines tempname, [src, 'byebye']
+ assert_equal src, `cat #{tempname} | #{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi`.chomp
+ end
+
+ def test_exit_0_exit_code
+ `echo foo | #{FZF} -q bar -0`
+ assert_equal 1, $?.exitstatus
+ end
+
+ def test_invalid_term
+ lines = `TERM=xxx #{FZF}`
+ assert_equal 2, $?.exitstatus
+ assert lines.include?('Invalid $TERM: xxx')
+ end
+
+ def test_invalid_option
+ lines = `#{FZF} --foobar 2>&1`
+ assert_equal 2, $?.exitstatus
+ assert lines.include?('unknown option: --foobar'), lines
+ end
+
+ def test_filter_exitstatus
+ # filter / streaming filter
+ ["", "--no-sort"].each do |opts|
+ assert `echo foo | #{FZF} -f foo #{opts}`.include?('foo')
+ assert_equal 0, $?.exitstatus
+
+ assert `echo foo | #{FZF} -f bar #{opts}`.empty?
+ assert_equal 1, $?.exitstatus
+ end
+ end
+
+ def test_exitstatus_empty
+ { '99' => '0', '999' => '1' }.each do |query, status|
+ tmux.send_keys "seq 100 | #{FZF} -q #{query}", :Enter
+ tmux.until { |lines| lines[-2] =~ %r{ [10]/100} }
+ tmux.send_keys :Enter
+
+ tmux.send_keys 'echo --\$?--'
+ tmux.until { |lines| lines.last.include? "echo --$?--" }
+ tmux.send_keys :Enter
+ tmux.until { |lines| lines.last.include? "--#{status}--" }
+ end
+ end
+
+private
+ def writelines path, lines
+ File.unlink path while File.exists? path
+ File.open(path, 'w') { |f| f << lines.join($/) }
+ end
+end
+
+module TestShell
+ def setup
+ super
+ end
+
+ def teardown
+ @tmux.kill
+ end
+
+ def set_var name, val
+ tmux.prepare
+ tmux.send_keys "export #{name}='#{val}'", :Enter
+ tmux.prepare
+ end
+
+ def test_ctrl_t
+ tmux.prepare
+ tmux.send_keys 'C-t', pane: 0
+ lines = tmux.until(1) { |lines| lines.item_count > 1 }
+ expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ')
+ tmux.send_keys :BTab, :BTab, pane: 1
+ tmux.until(1) { |lines| lines[-2].include?('(2)') }
+ tmux.send_keys :Enter, pane: 1
+ tmux.until(0) { |lines| lines[-1].include? expected }
+ tmux.send_keys 'C-c'
+
+ # FZF_TMUX=0
+ new_shell
+ tmux.send_keys 'C-t', pane: 0
+ lines = tmux.until(0) { |lines| lines.item_count > 1 }
+ expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ')
+ tmux.send_keys :BTab, :BTab, pane: 0
+ tmux.until(0) { |lines| lines[-2].include?('(2)') }
+ tmux.send_keys :Enter, pane: 0
+ tmux.until(0) { |lines| lines[-1].include? expected }
+ tmux.send_keys 'C-c', 'C-d'
+ end
+
+ def test_ctrl_t_command
+ set_var "FZF_CTRL_T_COMMAND", "seq 100"
+ tmux.send_keys 'C-t', pane: 0
+ lines = tmux.until(1) { |lines| lines.item_count == 100 }
+ tmux.send_keys :BTab, :BTab, :BTab, pane: 1
+ tmux.until(1) { |lines| lines[-2].include?('(3)') }
+ tmux.send_keys :Enter, pane: 1
+ tmux.until(0) { |lines| lines[-1].include? '1 2 3' }
+ end
+
+ def test_alt_c
+ tmux.prepare
+ tmux.send_keys :Escape, :c, pane: 0
+ lines = tmux.until(1) { |lines| lines.item_count > 0 && lines[-3][2..-1] }
+ expected = lines[-3][2..-1]
+ tmux.send_keys :Enter, pane: 1
+ tmux.prepare
+ tmux.send_keys :pwd, :Enter
+ tmux.until { |lines| lines[-1].end_with?(expected) }
+ end
+
+ def test_ctrl_r
+ tmux.prepare
+ tmux.send_keys 'echo 1st', :Enter; tmux.prepare
+ tmux.send_keys 'echo 2nd', :Enter; tmux.prepare
+ tmux.send_keys 'echo 3d', :Enter; tmux.prepare
+ tmux.send_keys 'echo 3rd', :Enter; tmux.prepare
+ tmux.send_keys 'echo 4th', :Enter; tmux.prepare
+ tmux.send_keys 'C-r', pane: 0
+ tmux.until(1) { |lines| lines.item_count > 0 }
+ tmux.send_keys '3d', pane: 1
+ tmux.until(1) { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort
+ tmux.send_keys :Enter, pane: 1
+ tmux.until { |lines| lines[-1] == 'echo 3rd' }
+ tmux.send_keys :Enter
+ tmux.until { |lines| lines[-1] == '3rd' }
+ end
+end
+
+module CompletionTest
+ def test_file_completion
+ 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.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0
+ tmux.until(1) { |lines| lines.item_count > 0 }
+ tmux.send_keys :BTab, :BTab
+ tmux.until(1) { |lines| lines[-2].include?('(2)') }
+ tmux.send_keys :Enter
+ tmux.until do |lines|
+ tmux.send_keys 'C-L'
+ lines[-1].include?('/tmp/fzf-test/10') &&
+ lines[-1].include?('/tmp/fzf-test/100')
+ end
+
+ # ~USERNAME**
+ 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**
+ 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**
+ 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
+
+ def test_dir_completion
+ tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}; touch /tmp/fzf-test/d55/xxx', :Enter
+ tmux.prepare
+ tmux.send_keys 'cd /tmp/fzf-test/**', :Tab, pane: 0
+ tmux.until(1) { |lines| lines.item_count > 0 }
+ tmux.send_keys :BTab, :BTab # BTab does not work here
+ tmux.send_keys 55
+ tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
+ tmux.send_keys :Enter
+ tmux.until do |lines|
+ tmux.send_keys 'C-L'
+ lines[-1] == 'cd /tmp/fzf-test/d55/'
+ end
+ tmux.send_keys :xx
+ tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' }
+
+ # Should not match regular files (bash-only)
+ if self.class == TestBash
+ tmux.send_keys :Tab
+ tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' }
+ end
+
+ # Fail back to plusdirs
+ tmux.send_keys :BSpace, :BSpace, :BSpace
+ tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55' }
+ tmux.send_keys :Tab
+ tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/' }
+ end
+
+ def test_process_completion
+ tmux.send_keys 'sleep 12345 &', :Enter
+ lines = tmux.until { |lines| lines[-1].start_with? '[1]' }
+ pid = lines[-1].split.last
+ tmux.prepare
+ tmux.send_keys 'kill ', :Tab, pane: 0
+ tmux.until(1) { |lines| lines.item_count > 0 }
+ tmux.send_keys 'sleep12345'
+ tmux.until(1) { |lines| lines[-3].include? 'sleep 12345' }
+ tmux.send_keys :Enter
+ tmux.until do |lines|
+ tmux.send_keys 'C-L'
+ lines[-1] == "kill #{pid}"
+ 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.prepare
+ tmux.send_keys "FZF_TMUX=0 #{Shell.bash}", :Enter
+ tmux.prepare
+ end
+
+ def setup
+ super
+ @tmux = Tmux.new :bash
+ end
+end
+
+class TestZsh < TestBase
+ include TestShell
+ include CompletionTest
+
+ def new_shell
+ tmux.send_keys "FZF_TMUX=0 #{Shell.zsh}", :Enter
+ tmux.prepare
+ end
+
+ def setup
+ super
+ @tmux = Tmux.new :zsh
+ end
+end
+
+class TestFish < TestBase
+ include TestShell
+
+ def new_shell
+ tmux.send_keys 'env FZF_TMUX=0 fish', :Enter
+ tmux.send_keys 'function fish_prompt; end; clear', :Enter
+ tmux.until { |lines| lines.empty? }
+ end
+
+ def set_var name, val
+ tmux.prepare
+ tmux.send_keys "set -g #{name} '#{val}'", :Enter
+ tmux.prepare
+ end
+
+ def setup
+ super
+ @tmux = Tmux.new :fish
+ end
+end
+
diff --git a/uninstall b/uninstall
index 1884c47..e2717a3 100755
--- a/uninstall
+++ b/uninstall
@@ -68,6 +68,7 @@ fi
if [ -d ~/.config/fish/functions ]; then
remove ~/.config/fish/functions/fzf.fish
+ remove ~/.config/fish/functions/fzf_key_bindings.fish
if [ "$(ls -A ~/.config/fish/functions)" ]; then
echo "Can't delete non-empty directory: \"~/.config/fish/functions\""