Compare commits

..

45 Commits

Author SHA1 Message Date
Junegunn Choi
3e1d6a7bcf 0.13.0 2016-06-12 02:15:11 +09:00
Junegunn Choi
2bbc12063c Add --preview and --preview-window
Close #587
2016-06-11 19:59:12 +09:00
Junegunn Choi
b8737b724b Ignore controls chars for bracketed paste mode
Close #594
2016-06-11 12:14:34 +09:00
Junegunn Choi
d91c3a2f5e Merge pull request #593 from edi9999/master
Add fzf_prefer_tmux option
2016-06-10 22:58:58 +09:00
Edgar Hipp
fe5db5aadc Add fzf_prefer_tmux option 2016-06-10 09:05:05 +02:00
Junegunn Choi
cf9c957c66 Update test_execute_shell (#590) 2016-06-08 02:16:07 +09:00
Junegunn Choi
68b60c6d19 Update test_execute_multi (#590) 2016-06-08 02:15:22 +09:00
Junegunn Choi
3a644b16a4 Update test_execute (#590) 2016-06-08 02:04:40 +09:00
Junegunn Choi
95b34de339 [bash/zsh] Fix $FZF_CTRL_R_OPTS with option values with spaces 2016-06-08 01:30:26 +09:00
Junegunn Choi
6a431cbf49 [fzf-tmux] Escape $ in arguments
e.g. fzf-tmux -q '$PATH'

Related: #343
2016-06-08 01:27:22 +09:00
Junegunn Choi
56fb2f00b3 Use single-quoted strings in execute action
Close #590
2016-06-08 00:54:21 +09:00
Junegunn Choi
1c86aaf342 [vim/fzf-tmux] Handle fzf project directory with spaces
Close #583
2016-06-03 12:09:31 +09:00
Junegunn Choi
cfc0b18eaa Revert "Change tmux pane title for fzf splits"
This reverts commit f074709fc9.

Close #586. /cc @akashin
2016-06-03 12:02:21 +09:00
Junegunn Choi
412c211655 [vim] Use lcd instead of chdir
https://github.com/junegunn/fzf.vim/issues/147
2016-06-02 22:24:47 +09:00
Junegunn Choi
923feb69ab [zsh] Fix indentation 2016-06-02 22:01:26 +09:00
Junegunn Choi
92dba7035a Merge pull request #584 from jimbocoder/master
Take SSH completion hints from known_hosts
2016-06-02 21:58:59 +09:00
Jim Howell
b8a3ba16a2 [bash/zsh] Take SSH completion hints from known_hosts
Signed-off-by: Junegunn Choi <junegunn.c@gmail.com>
2016-06-02 21:58:01 +09:00
Junegunn Choi
cd5e4d9402 Merge pull request #582 from akashin/master
[fzf-tmux] Change tmux pane title for fzf splits
2016-06-01 17:07:07 +09:00
Andrey Kashin
f074709fc9 Change tmux pane title for fzf splits 2016-06-01 10:19:26 +03:00
Junegunn Choi
e0b29e437b [bash] Use backticks to avoid delay with blink-matching-paren
Close #580
2016-05-29 02:11:50 +09:00
Junegunn Choi
bdb94fba7d [zsh] Fix #579 - Locally unset globsubst 2016-05-26 00:52:06 +09:00
Junegunn Choi
2f364c62f4 0.12.2 2016-05-19 01:55:54 +09:00
Junegunn Choi
7ed9f83662 Validate jump label characters
Also extend default jump labels
2016-05-19 01:46:22 +09:00
Junegunn Choi
f498a9b3fb Revert version number 2016-05-18 22:47:57 +09:00
Junegunn Choi
13330738b8 Do not match jump labels beyond the screen limit 2016-05-18 22:45:34 +09:00
Junegunn Choi
e53535cc61 Update default jump labels 2016-05-18 22:44:31 +09:00
Junegunn Choi
c62fc5e75c More named keys: F5 ~ F10, ALT-/ 2016-05-18 22:25:09 +09:00
Junegunn Choi
70245ad98c [make] Reduce the size of the binaries with -ldflags -w
Related: #555
2016-05-18 13:29:27 +09:00
Junegunn Choi
6d235bceee Add jump and jump-accept actions for --bind
jump and jump-accept implement EasyMotion-like movement in fzf.
Suggested by @mhrebenyuk. Close #569.
2016-05-18 02:10:03 +09:00
Junegunn Choi
4adebfc856 [install] go get -u github.com/junegunn/fzf/src/fzf 2016-05-17 01:41:59 +09:00
Junegunn Choi
faccc0a410 [fzf-tmux] Escape backslash in command-line arguments 2016-05-15 17:07:34 +09:00
Junegunn Choi
9078688baf Add print-query action for --bind
Close #571
2016-05-13 00:51:15 +09:00
Junegunn Choi
9bd8b1d25f Fix typo 2016-05-13 00:44:33 +09:00
Junegunn Choi
dd4be1da38 Allow alt-enter and alt-space for --bind (#571) 2016-05-13 00:43:50 +09:00
Junegunn Choi
66f86e1870 [fzf-tmux] Fix #562 - Check $TMUX instead of $TMUX_PANE 2016-05-11 22:08:14 +09:00
Junegunn Choi
4ab75b68dc Fix flaky test case: test_execute
Should wait until execute action completes
2016-05-11 01:40:49 +09:00
Junegunn Choi
73cb70dbb3 Fix flaky test case: test_file_completion_unicode 2016-05-11 01:25:17 +09:00
Junegunn Choi
d082cccb6d Fix flaky test case: test_ctrl_t_unicode
The width of the pseudo-terminal on Travis CI environment can be small
and cause the line to be wrapped.
2016-05-11 01:18:26 +09:00
Junegunn Choi
88a80e3c2c Determine 256-color capability using tigetnum("colors")
Close #570
2016-05-11 01:07:06 +09:00
Junegunn Choi
24516bcf4d [install] Set a temporary GOPATH 2016-05-09 02:03:08 +09:00
Junegunn Choi
b4c4a642ed Update README
Close #560, #561
2016-05-03 00:07:53 +09:00
Junegunn Choi
0231617857 [neovim] Fix issues with enew and tabnew layouts
Related: #559
2016-04-28 01:25:24 +09:00
Junegunn Choi
7f64fba80f Update Makefile to allow build on i686 (#555) 2016-04-26 01:49:02 +09:00
Junegunn Choi
633aec38f5 Merge pull request #554 from gene-pavlovsky/patch-1
Fix missing reference to UNAME_M
2016-04-25 23:04:29 +09:00
Gene Pavlovsky
d1b402a23c Fix missing reference to UNAME_M
The `Build on $(UNAME_M) is not supported, yet` message was referencing an undefined UNAME_M. Fixed that.
2016-04-24 21:24:10 +03:00
18 changed files with 1109 additions and 461 deletions

View File

@@ -1,6 +1,29 @@
CHANGELOG CHANGELOG
========= =========
0.13.0
------
- Added preview feature
- `--preview CMD`
- `--preview-window POS[:SIZE][:hidden]`
- `{}` in execute action is now replaced to the single-quoted (instead of
double-quoted) string of the current line
- Fixed to ignore control characters for bracketed paste mode
0.12.2
------
- 256-color capability detection does not require `256` in `$TERM`
- Added `print-query` action
- More named keys for binding; <kbd>F1</kbd> ~ <kbd>F10</kbd>,
<kbd>ALT-/</kbd>, <kbd>ALT-space</kbd>, and <kbd>ALT-enter</kbd>
- Added `jump` and `jump-accept` actions that implement [EasyMotion][em]-like
movement
![][jump]
[em]: https://github.com/easymotion/vim-easymotion
[jump]: https://cloud.githubusercontent.com/assets/700826/15367574/b3999dc4-1d64-11e6-85da-28ceeb1a9bc2.png
0.12.1 0.12.1
------ ------

View File

@@ -151,27 +151,6 @@ Many useful examples can be found on [the wiki
page](https://github.com/junegunn/fzf/wiki/examples). Feel free to add your page](https://github.com/junegunn/fzf/wiki/examples). Feel free to add your
own as well. own as well.
Key bindings for command line
-----------------------------
The install script will setup the following key bindings for bash, zsh, and
fish.
- `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, 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.
`fzf-tmux` script `fzf-tmux` script
----------------- -----------------
@@ -191,6 +170,28 @@ cat /usr/share/dict/words | fzf-tmux -l 20% --multi --reverse
It will still work even when you're not on tmux, silently ignoring `-[udlr]` It will still work even when you're not on tmux, silently ignoring `-[udlr]`
options, so you can invariably use `fzf-tmux` in your scripts. options, so you can invariably use `fzf-tmux` in your scripts.
Key bindings for command line
-----------------------------
The install script will setup the following key bindings for bash, zsh, and
fish.
- `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
- Set `FZF_ALT_C_COMMAND` to override the default command
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.
Fuzzy completion for bash and zsh Fuzzy completion for bash and zsh
--------------------------------- ---------------------------------

View File

@@ -83,7 +83,7 @@ while [ $# -gt 0 ]; do
shift shift
done done
if ! [ -n "$TMUX_PANE" -a $lines -gt 15 ]; then if ! [ -n "$TMUX" -a "$lines" -gt 15 ]; then
fzf "${args[@]}" fzf "${args[@]}"
exit $? exit $?
fi fi
@@ -134,20 +134,22 @@ mkfifo -m o+w $fifo3
# Build arguments to fzf # Build arguments to fzf
opts="" opts=""
for arg in "${args[@]}"; do for arg in "${args[@]}"; do
arg="${arg//\\/\\\\}"
arg="${arg//\"/\\\"}" arg="${arg//\"/\\\"}"
arg="${arg//\`/\\\`}" arg="${arg//\`/\\\`}"
arg="${arg//$/\\$}"
opts="$opts \"$arg\"" opts="$opts \"$arg\""
done done
if [ -n "$term" -o -t 0 ]; then if [ -n "$term" -o -t 0 ]; then
cat <<< "$fzf $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf
tmux set-window-option synchronize-panes off \;\ tmux set-window-option synchronize-panes off \;\
set-window-option remain-on-exit off \;\ set-window-option remain-on-exit off \;\
split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap \ split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap \
> /dev/null 2>&1 > /dev/null 2>&1
else else
mkfifo $fifo1 mkfifo $fifo1
cat <<< "$fzf $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf
tmux set-window-option synchronize-panes off \;\ tmux set-window-option synchronize-panes off \;\
set-window-option remain-on-exit off \;\ set-window-option remain-on-exit off \;\
split-window $opt "$envs bash $argsf" $swap \ split-window $opt "$envs bash $argsf" $swap \

14
install
View File

@@ -2,8 +2,8 @@
set -u set -u
[[ "$@" =~ --pre ]] && version=0.12.1 pre=1 || [[ "$@" =~ --pre ]] && version=0.13.0 pre=1 ||
version=0.12.1 pre=0 version=0.13.0 pre=0
auto_completion= auto_completion=
key_bindings= key_bindings=
@@ -230,10 +230,14 @@ if [ -n "$binary_error" ]; then
if [ $binary_available -eq 0 ]; then if [ $binary_available -eq 0 ]; then
echo "No prebuilt binary for $archi ..." echo "No prebuilt binary for $archi ..."
if command -v go > /dev/null; then if command -v go > /dev/null; then
echo -n "Building binary (go get github.com/junegunn/fzf/src/fzf) ... " echo -n "Building binary (go get -u github.com/junegunn/fzf/src/fzf) ... "
if go get github.com/junegunn/fzf/src/fzf; then if [ -z "${GOPATH-}" ]; then
export GOPATH="${TMPDIR:-/tmp}/fzf-gopath"
mkdir -p "$GOPATH"
fi
if go get -u github.com/junegunn/fzf/src/fzf; then
echo "OK" echo "OK"
link_fzf_in_path cp "$GOPATH/bin/fzf" "$fzf_base/bin/"
else else
echo "Failed to build binary ..." echo "Failed to build binary ..."
install_ruby_fzf install_ruby_fzf

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Apr 2016" "fzf 0.12.1" "fzf - a command-line fuzzy finder" .TH fzf 1 "Jun 2016" "fzf 0.13.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -50,10 +50,10 @@ Case-sensitive match
.TP .TP
.BI "-n, --nth=" "N[,..]" .BI "-n, --nth=" "N[,..]"
Comma-separated list of field index expressions for limiting search scope. Comma-separated list of field index expressions for limiting search scope.
See \fBFIELD INDEX EXPRESSION\fR for details. See \fBFIELD INDEX EXPRESSION\fR for the details.
.TP .TP
.BI "--with-nth=" "N[,..]" .BI "--with-nth=" "N[,..]"
Transform each item using index expressions within finder Transform the presentation of each line using field index expressions
.TP .TP
.BI "-d, --delimiter=" "STR" .BI "-d, --delimiter=" "STR"
Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style) Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style)
@@ -64,6 +64,7 @@ Do not sort the result
.TP .TP
.B "--tac" .B "--tac"
Reverse the order of the input Reverse the order of the input
.RS .RS
e.g. \fBhistory | fzf --tac --no-sort\fR e.g. \fBhistory | fzf --tac --no-sort\fR
.RE .RE
@@ -73,13 +74,13 @@ Comma-separated list of sort criteria to apply when the scores are tied.
.br .br
.br .br
.BR length " Prefers item with shorter length" .BR length " Prefers line with shorter length"
.br .br
.BR begin " Prefers item with matched substring closer to the beginning" .BR begin " Prefers line with matched substring closer to the beginning"
.br .br
.BR end " Prefers item with matched substring closer to the end" .BR end " Prefers line with matched substring closer to the end"
.br .br
.BR index " Prefers item that appeared earlier in the input stream" .BR index " Prefers line that appeared earlier in the input stream"
.br .br
.br .br
@@ -90,16 +91,86 @@ Comma-separated list of sort criteria to apply when the scores are tied.
- \fBindex\fR is implicitly appended to the list when not specified - \fBindex\fR is implicitly appended to the list when not specified
.br .br
- Default is \fBlength\fR (or equivalently \fBlength\fR,index) - Default is \fBlength\fR (or equivalently \fBlength\fR,index)
.br
- If \fBend\fR is found in the list, fzf will scan each line backwards
.SS Interface .SS Interface
.TP .TP
.B "-m, --multi" .B "-m, --multi"
Enable multi-select with tab/shift-tab Enable multi-select with tab/shift-tab
.TP .TP
.B "--no-mouse"
Disable mouse
.TP
.BI "--bind=" "KEYBINDS"
Comma-separated list of custom key bindings. See \fBKEY BINDINGS\fR for the
details.
.TP
.B "--cycle"
Enable cyclic scroll
.TP
.B "--no-hscroll"
Disable horizontal scroll
.TP
.BI "--hscroll-off=" "COL"
Number of screen columns to keep to the right of the highlighted substring
(default: 10). Setting it to a large value will cause the text to be positioned
on the center of the screen.
.TP
.BI "--jump-labels=" "CHARS"
Label characters for \fBjump\fR and \fBjump-accept\fR
.SS Layout
.TP
.B "--reverse"
Reverse orientation
.TP
.BI "--margin=" MARGIN
Comma-separated expression for margins around the finder.
.br
.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
.br
Each part can be given in absolute number or in percentage relative to the
terminal size with \fB%\fR suffix.
.br
.br
e.g. \fBfzf --margin 10%\fR
\fBfzf --margin 1,5%\fR
.RE
.TP
.B "--inline-info"
Display finder info inline with the query
.TP
.BI "--prompt=" "STR"
Input prompt (default: '> ')
.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 Display
.TP
.B "--ansi" .B "--ansi"
Enable processing of ANSI color codes Enable processing of ANSI color codes
.TP .TP
.B "--no-mouse" .BI "--tabstop=" SPACES
Disable mouse Number of spaces for a tab character (default: 8)
.TP .TP
.BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]" .BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]"
Color configuration. The name of the base color scheme is followed by custom Color configuration. The name of the base color scheme is followed by custom
@@ -137,172 +208,7 @@ e.g. \fBfzf --color=bg+:24\fR
.TP .TP
.B "--black" .B "--black"
Use black background Use black background
.TP .SS History
.B "--reverse"
Reverse orientation
.TP
.BI "--margin=" MARGIN
Comma-separated expression for margins around the finder.
.br
.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
.br
Each part can be given in absolute number or in percentage relative to the
terminal size with \fB%\fR suffix.
.br
.br
e.g. \fBfzf --margin 10%\fR
\fBfzf --margin 1,5%\fR
.RE
.TP
.BI "--tabstop=" SPACES
Number of spaces for a tab character (default: 8)
.TP
.B "--cycle"
Enable cyclic scroll
.TP
.B "--no-hscroll"
Disable horizontal scroll
.TP
.BI "--hscroll-off=" "COL"
Number of screen columns to keep to the right of the highlighted substring
(default: 10). Setting it to a large value will cause the text to be positioned
on the center of the screen.
.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: (SYNONYMS)
\fIctrl-[a-z]\fR
\fIalt-[a-z]\fR
\fIf[1-4]\fR
\fIenter\fR (\fIreturn\fR \fIctrl-m\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
\fIdouble-click\fR
or any single character
.RE
.RS
\fBACTION: DEFAULT BINDINGS (NOTES):
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIenter double-click\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)
\fBexecute-multi(...)\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-in\fR (\fB--reverse\fR ? \fBtoggle-up\fR : \fBtoggle-down\fR)
\fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle-down\fR : \fBtoggle-up\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
\fBexecute-multi(...)\fR is an alternative action that executes the command
with the selected entries when multi-select is enabled (\fB--multi\fR). With
this action, \fB{}\fR is replaced with the double-quoted strings of the
selected entries separated by spaces.
.RE
.TP .TP
.BI "--history=" "HISTORY_FILE" .BI "--history=" "HISTORY_FILE"
Load search history from the specified file and update the file on completion. Load search history from the specified file and update the file on completion.
@@ -312,17 +218,34 @@ When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically remapped to
.BI "--history-size=" "N" .BI "--history-size=" "N"
Maximum number of entries in the history file (default: 1000). The file is Maximum number of entries in the history file (default: 1000). The file is
automatically truncated when the number of the lines exceeds the value. automatically truncated when the number of the lines exceeds the value.
.SS Preview
.TP .TP
.BI "--header=" "STR" .BI "--preview=" "COMMAND"
The given string will be printed as the sticky header. The lines are displayed Execute the given command for the current line and display the result on the
in the given order from top to bottom regardless of \fB--reverse\fR option, and preview window. \fB{}\fR is the placeholder for the quoted string of the
are not affected by \fB--with-nth\fR. ANSI color codes are processed even when current line.
\fB--ansi\fR is not set.
.RS
e.g. \fBfzf --preview="head -$LINES {}"\fR
.RE
.TP .TP
.BI "--header-lines=" "N" .BI "--preview-window=" "[POSITION][:SIZE[%]][:hidden]"
The first N lines of the input are treated as the sticky header. When Determine the layout of the preview window. If the argument ends with
\fB--with-nth\fR is set, the lines are transformed just like the other \fB:hidden\fR, the preview window will be hidden by default until
lines that follow. \fBtoggle-preview\fR action is triggered.
.RS
.B POSITION: (default: right)
\fBup
\fBdown
\fBleft
\fBright
.RE
.RS
e.g. \fBfzf --preview="head {}" --preview-window=up:30%\fR
\fBfzf --preview="file {}" --preview-window=down:1\fR
.RE
.SS Scripting .SS Scripting
.TP .TP
.BI "-q, --query=" "STR" .BI "-q, --query=" "STR"
@@ -347,6 +270,7 @@ 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 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 \fB--print-query\fR is also used). The line will be empty if fzf is completed
with the default enter key. with the default enter key.
.RS .RS
e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR
.RE .RE
@@ -354,11 +278,12 @@ e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR
.B "--sync" .B "--sync"
Synchronous search for multi-staged filtering. If specified, fzf will launch Synchronous search for multi-staged filtering. If specified, fzf will launch
ncurses finder only after the input stream is complete. ncurses finder only after the input stream is complete.
.RS .RS
e.g. \fBfzf --multi | fzf --sync\fR e.g. \fBfzf --multi | fzf --sync\fR
.RE .RE
.SH ENVIRONMENT .SH ENVIRONMENT VARIABLES
.TP .TP
.B FZF_DEFAULT_COMMAND .B FZF_DEFAULT_COMMAND
Default command to use when input is tty Default command to use when input is tty
@@ -412,11 +337,11 @@ occurrences of the string.
.SS Anchored-match .SS Anchored-match
A term can be prefixed by \fB^\fR, or suffixed by \fB$\fR to become an 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 anchored-match term. Then fzf will search for the lines that start with or end
with the given string. An anchored-match term is also an exact-match term. with the given string. An anchored-match term is also an exact-match term.
.SS Negation .SS Negation
If a term is prefixed by \fB!\fR, fzf will exclude the items that satisfy the If a term is prefixed by \fB!\fR, fzf will exclude the lines that satisfy the
term from the result. term from the result.
.SS Exact-match by default .SS Exact-match by default
@@ -431,6 +356,119 @@ query matches entries that start with \fBcore\fR and end with either \fBgo\fR,
e.g. \fB^core go$ | rb$ | py$\fR e.g. \fB^core go$ | rb$ | py$\fR
.SH KEY BINDINGS
You can customize key bindings of fzf with \fB--bind\fR option which takes
a comma-separated list of key binding expressions. Each key binding expression
follows the following format: \fBKEY:ACTION\fR
e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
.B AVAILABLE KEYS: (SYNONYMS)
\fIctrl-[a-z]\fR
\fIalt-[a-z]\fR
\fIf[1-10]\fR
\fIenter\fR (\fIreturn\fR \fIctrl-m\fR)
\fIspace\fR
\fIbspace\fR (\fIbs\fR)
\fIalt-enter\fR
\fIalt-space\fR
\fIalt-bspace\fR (\fIalt-bs\fR)
\fIalt-/\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
\fIdouble-click\fR
or any single character
\fBACTION: DEFAULT BINDINGS (NOTES):
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIenter double-click\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)
\fBexecute-multi(...)\fR (see below for the details)
\fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR
\fBignore\fR
\fBjump\fR (EasyMotion-like 2-keystroke movement)
\fBjump-accept\fR (jump and accept)
\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)
\fBprint-query\fR (print query and exit)
\fBselect-all\fR
\fBtoggle\fR
\fBtoggle-all\fR
\fBtoggle-down\fR \fIctrl-i (tab)\fR
\fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle-up\fR : \fBtoggle-down\fR)
\fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle-down\fR : \fBtoggle-up\fR)
\fBtoggle-preview\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
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.
\fBfzf --bind "enter:execute(less {})"\fR
\fB{}\fR is the placeholder for the 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
\fBexecute-multi(...)\fR is an alternative action that executes the command
with the selected entries when multi-select is enabled (\fB--multi\fR). With
this action, \fB{}\fR is replaced with the quoted strings of the selected
entries separated by spaces.
.SH AUTHOR .SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR) Junegunn Choi (\fIjunegunn.c@gmail.com\fR)

View File

@@ -49,7 +49,7 @@ function! s:fzf_exec()
throw 'fzf executable not found' throw 'fzf executable not found'
endif endif
endif endif
return s:exec return s:shellesc(s:exec)
endfunction endfunction
function! s:tmux_enabled() function! s:tmux_enabled()
@@ -140,10 +140,10 @@ try
else else
let prefix = '' let prefix = ''
endif endif
let tmux = !has('nvim') && s:tmux_enabled() && s:splittable(dict) let tmux = (!has('nvim') || get(g:, 'fzf_prefer_tmux', 0)) && s:tmux_enabled() && s:splittable(dict)
let command = prefix.(tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result let command = prefix.(tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
if has('nvim') if has('nvim') && !tmux
return s:execute_term(dict, command, temps) return s:execute_term(dict, command, temps)
endif endif
@@ -179,7 +179,7 @@ function! s:fzf_tmux(dict)
endif endif
endfor endfor
return printf('LINES=%d COLUMNS=%d %s %s %s --', return printf('LINES=%d COLUMNS=%d %s %s %s --',
\ &lines, &columns, s:fzf_tmux, size, (has_key(a:dict, 'source') ? '' : '-')) \ &lines, &columns, s:shellesc(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
endfunction endfunction
function! s:splittable(dict) function! s:splittable(dict)
@@ -193,7 +193,7 @@ function! s:pushd(dict)
return 1 return 1
endif endif
let a:dict.prev_dir = cwd let a:dict.prev_dir = cwd
execute 'chdir' s:escape(a:dict.dir) execute 'lcd' s:escape(a:dict.dir)
let a:dict.dir = getcwd() let a:dict.dir = getcwd()
return 1 return 1
endif endif
@@ -214,7 +214,7 @@ function! s:popd(dict, lines)
" directory is not expected and should be undone. " directory is not expected and should be undone.
if has_key(a:dict, 'prev_dir') && if has_key(a:dict, 'prev_dir') &&
\ (!&autochdir || (empty(a:lines) || len(a:lines) == 1 && empty(a:lines[0]))) \ (!&autochdir || (empty(a:lines) || len(a:lines) == 1 && empty(a:lines[0])))
execute 'chdir' s:escape(remove(a:dict, 'prev_dir')) execute 'lcd' s:escape(remove(a:dict, 'prev_dir'))
endif endif
endfunction endfunction
@@ -298,7 +298,7 @@ function! s:split(dict)
\ 'down': ['botright', 'resize', &lines], \ 'down': ['botright', 'resize', &lines],
\ 'left': ['vertical topleft', 'vertical resize', &columns], \ 'left': ['vertical topleft', 'vertical resize', &columns],
\ 'right': ['vertical botright', 'vertical resize', &columns] } \ 'right': ['vertical botright', 'vertical resize', &columns] }
let s:ppos = s:getpos() let ppos = s:getpos()
try try
for [dir, triple] in items(directions) for [dir, triple] in items(directions)
let val = get(a:dict, dir, '') let val = get(a:dict, dir, '')
@@ -311,7 +311,7 @@ function! s:split(dict)
endif endif
execute cmd sz.'new' execute cmd sz.'new'
execute resz sz execute resz sz
return {} return [ppos, {}]
endif endif
endfor endfor
if s:present(a:dict, 'window') if s:present(a:dict, 'window')
@@ -319,36 +319,44 @@ function! s:split(dict)
else else
execute (tabpagenr()-1).'tabnew' execute (tabpagenr()-1).'tabnew'
endif endif
return { '&l:wfw': &l:wfw, '&l:wfh': &l:wfh } return [ppos, { '&l:wfw': &l:wfw, '&l:wfh': &l:wfh }]
finally finally
setlocal winfixwidth winfixheight setlocal winfixwidth winfixheight
endtry endtry
endfunction endfunction
function! s:execute_term(dict, command, temps) abort function! s:execute_term(dict, command, temps) abort
let winopts = s:split(a:dict) let [ppos, winopts] = s:split(a:dict)
let fzf = { 'buf': bufnr('%'), 'ppos': ppos, 'dict': a:dict, 'temps': a:temps,
let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps, 'name': 'FZF', 'winopts': winopts } \ 'name': 'FZF', 'winopts': winopts, 'command': a:command }
let s:command = a:command function! fzf.switch_back(inplace)
if a:inplace && bufnr('') == self.buf
" FIXME: Can't re-enter normal mode from terminal mode
" execute "normal! \<c-^>"
b #
" No other listed buffer
if bufnr('') == self.buf
enew
endif
endif
endfunction
function! fzf.on_exit(id, code) function! fzf.on_exit(id, code)
let pos = s:getpos() if s:getpos() == self.ppos " {'window': 'enew'}
let inplace = pos == s:ppos " {'window': 'enew'}
if inplace
for [opt, val] in items(self.winopts) for [opt, val] in items(self.winopts)
execute 'let' opt '=' val execute 'let' opt '=' val
endfor endfor
call self.switch_back(1)
else else
if bufnr('') == self.buf if bufnr('') == self.buf
" We use close instead of bd! since Vim does not close the split when " We use close instead of bd! since Vim does not close the split when
" there's no other listed buffer (nvim +'set nobuflisted') " there's no other listed buffer (nvim +'set nobuflisted')
close close
endif endif
if pos.tab == s:ppos.tab execute 'tabnext' self.ppos.tab
wincmd p execute self.ppos.win.'wincmd w'
endif
endif endif
if !s:exit_handler(a:code, s:command, 1) if !s:exit_handler(a:code, self.command, 1)
return return
endif endif
@@ -356,14 +364,7 @@ function! s:execute_term(dict, command, temps) abort
let ret = [] let ret = []
try try
let ret = s:callback(self.dict, self.temps) let ret = s:callback(self.dict, self.temps)
call self.switch_back(s:getpos() == self.ppos)
if inplace && bufnr('') == self.buf
execute "normal! \<c-^>"
" No other listed buffer
if bufnr('') == self.buf
bd!
endif
endif
finally finally
call s:popd(self.dict, ret) call s:popd(self.dict, ret)
endtry endtry

View File

@@ -221,6 +221,7 @@ _fzf_complete_telnet() {
_fzf_complete_ssh() { _fzf_complete_ssh() {
_fzf_complete '+m' "$@" < <( _fzf_complete '+m' "$@" < <(
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') \ cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') \
<(\grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \
<(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') |
awk '{if (length($2) > 0) {print $2}}' | sort -u awk '{if (length($2) > 0) {print $2}}' | sort -u
) )

View File

@@ -31,7 +31,7 @@ fi
########################################################### ###########################################################
__fzf_generic_path_completion() { __fzf_generic_path_completion() {
local base lbuf compgen fzf_opts suffix tail fzf dir leftover matches nnm local base lbuf compgen fzf_opts suffix tail fzf dir leftover matches
# (Q) flag removes a quoting level: "foo\ bar" => "foo bar" # (Q) flag removes a quoting level: "foo\ bar" => "foo bar"
base=${(Q)1} base=${(Q)1}
lbuf=$2 lbuf=$2
@@ -41,10 +41,7 @@ __fzf_generic_path_completion() {
tail=$6 tail=$6
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
if ! setopt | \grep nonomatch > /dev/null; then setopt localoptions nonomatch
nnm=1
setopt nonomatch
fi
dir="$base" dir="$base"
while [ 1 ]; do while [ 1 ]; do
if [ -z "$dir" -o -d ${~dir} ]; then if [ -z "$dir" -o -d ${~dir} ]; then
@@ -66,7 +63,6 @@ __fzf_generic_path_completion() {
dir=$(dirname "$dir") dir=$(dirname "$dir")
dir=${dir%/}/ dir=${dir%/}/
done done
[ -n "$nnm" ] && unsetopt nonomatch
} }
_fzf_path_completion() { _fzf_path_completion() {
@@ -114,6 +110,7 @@ _fzf_complete_telnet() {
_fzf_complete_ssh() { _fzf_complete_ssh() {
_fzf_complete '+m' "$@" < <( _fzf_complete '+m' "$@" < <(
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') \ cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') \
<(\grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \
<(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') |
awk '{if (length($2) > 0) {print $2}}' | sort -u awk '{if (length($2) > 0) {print $2}}' | sort -u
) )

View File

@@ -51,7 +51,7 @@ __fzf_history__() (
shopt -u nocaseglob nocasematch shopt -u nocaseglob nocasematch
line=$( line=$(
HISTTIMEFORMAT= history | HISTTIMEFORMAT= history |
$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS | eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS" |
\grep '^ *[0-9]') && \grep '^ *[0-9]') &&
if [[ $- =~ H ]]; then if [[ $- =~ H ]]; then
sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line" sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line"
@@ -67,7 +67,7 @@ __fzf_use_tmux__() {
[ $BASH_VERSINFO -gt 3 ] && __use_bind_x=1 || __use_bind_x=0 [ $BASH_VERSINFO -gt 3 ] && __use_bind_x=1 || __use_bind_x=0
__fzf_use_tmux__ && __use_tmux=1 || __use_tmux=0 __fzf_use_tmux__ && __use_tmux=1 || __use_tmux=0
if [[ $'\n'$(set -o) != *$'\n'vi*on* ]]; then if [[ ! -o vi ]]; then
# Required to refresh the prompt after fzf # Required to refresh the prompt after fzf
bind '"\er": redraw-current-line' bind '"\er": redraw-current-line'
bind '"\e^": history-expand-line' bind '"\e^": history-expand-line'
@@ -82,10 +82,10 @@ if [[ $'\n'$(set -o) != *$'\n'vi*on* ]]; then
fi fi
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": " \C-e\C-u$(__fzf_history__)\e\C-e\e^\er"' bind '"\C-r": " \C-e\C-u`__fzf_history__`\e\C-e\e^\er"'
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
bind '"\ec": " \C-e\C-u$(__fzf_cd__)\e\C-e\er\C-m"' bind '"\ec": " \C-e\C-u`__fzf_cd__`\e\C-e\er\C-m"'
else else
# We'd usually use "\e" to enter vi-movement-mode so we can do our magic, # We'd usually use "\e" to enter vi-movement-mode so we can do our magic,
# but this incurs a very noticeable delay of a half second or so, # but this incurs a very noticeable delay of a half second or so,

View File

@@ -38,7 +38,8 @@ bindkey '\ec' fzf-cd-widget
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() { fzf-history-widget() {
local selected num local selected num
selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r ${=FZF_CTRL_R_OPTS} -q "${LBUFFER//$/\\$}") ) setopt localoptions noglobsubst
selected=( $(fc -l 1 | eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q ${(q)LBUFFER}") )
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
num=$selected[1] num=$selected[1]
if [ -n "$num" ]; then if [ -n "$num" ]; then

View File

@@ -7,10 +7,6 @@ else ifeq ($(UNAME_S),Linux)
endif endif
endif endif
ifneq ($(shell uname -m),x86_64)
$(error "Build on $(UNAME_M) is not supported, yet.")
endif
SOURCES := $(wildcard *.go */*.go) SOURCES := $(wildcard *.go */*.go)
ROOTDIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) ROOTDIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
BINDIR := $(shell dirname $(ROOTDIR))/bin BINDIR := $(shell dirname $(ROOTDIR))/bin
@@ -26,15 +22,22 @@ RELEASE64 := fzf-$(VERSION)-$(GOOS)_amd64
RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7 RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7
export GOPATH export GOPATH
all: release UNAME_M := $(shell uname -m)
ifeq ($(UNAME_M),x86_64)
BINARY := $(BINARY64)
else ifeq ($(UNAME_M),i686)
BINARY := $(BINARY32)
else
$(error "Build on $(UNAME_M) is not supported, yet.")
endif
release: test build all: fzf/$(BINARY)
release: test fzf/$(BINARY32) fzf/$(BINARY64)
-cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) -cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32)
cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \ cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \
rm -f $(RELEASE32) $(RELEASE64) rm -f $(RELEASE32) $(RELEASE64)
build: fzf/$(BINARY32) fzf/$(BINARY64)
$(SRCDIR): $(SRCDIR):
mkdir -p $(shell dirname $(SRCDIR)) mkdir -p $(shell dirname $(SRCDIR))
ln -s $(ROOTDIR) $(SRCDIR) ln -s $(ROOTDIR) $(SRCDIR)
@@ -44,7 +47,7 @@ deps: $(SRCDIR) $(SOURCES)
android-build: $(SRCDIR) android-build: $(SRCDIR)
cd $(SRCDIR) && GOARCH=arm GOARM=7 CGO_ENABLED=1 go get cd $(SRCDIR) && GOARCH=arm GOARM=7 CGO_ENABLED=1 go get
cd $(SRCDIR)/fzf && GOARCH=arm GOARM=7 CGO_ENABLED=1 go build -a -ldflags="-extldflags=-pie" -o $(BINARYARM7) cd $(SRCDIR)/fzf && GOARCH=arm GOARM=7 CGO_ENABLED=1 go build -a -ldflags="-w -extldflags=-pie" -o $(BINARYARM7)
cd $(SRCDIR)/fzf && cp $(BINARYARM7) $(RELEASEARM7) && tar -czf $(RELEASEARM7).tgz $(RELEASEARM7) && \ cd $(SRCDIR)/fzf && cp $(BINARYARM7) $(RELEASEARM7) && tar -czf $(RELEASEARM7).tgz $(RELEASEARM7) && \
rm -f $(RELEASEARM7) rm -f $(RELEASEARM7)
@@ -54,20 +57,20 @@ test: deps
install: $(BINDIR)/fzf install: $(BINDIR)/fzf
uninstall: uninstall:
rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY64) rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY)
clean: clean:
cd fzf && rm -f fzf-* cd fzf && rm -f fzf-*
fzf/$(BINARY32): deps fzf/$(BINARY32): deps
cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -tags "$(TAGS)" -o $(BINARY32) cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -ldflags -w -tags "$(TAGS)" -o $(BINARY32)
fzf/$(BINARY64): deps fzf/$(BINARY64): deps
cd fzf && go build -a -tags "$(TAGS)" -o $(BINARY64) cd fzf && go build -a -ldflags -w -tags "$(TAGS)" -o $(BINARY64)
$(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR) $(BINDIR)/fzf: fzf/$(BINARY) | $(BINDIR)
cp -f fzf/$(BINARY64) $(BINDIR) cp -f fzf/$(BINARY) $(BINDIR)
cd $(BINDIR) && ln -sf $(BINARY64) fzf cd $(BINDIR) && ln -sf $(BINARY) fzf
$(BINDIR): $(BINDIR):
mkdir -p $@ mkdir -p $@
@@ -98,7 +101,7 @@ centos: docker-centos
linux: docker-centos linux: docker-centos
docker run $(DOCKEROPTS) junegunn/centos-sandbox \ docker run $(DOCKEROPTS) junegunn/centos-sandbox \
/bin/bash -ci 'cd /fzf/src; make TAGS=static' /bin/bash -ci 'cd /fzf/src; make TAGS=static release'
ubuntu-android: docker-android ubuntu-android: docker-android
docker run $(DOCKEROPTS) junegunn/android-sandbox \ docker run $(DOCKEROPTS) junegunn/android-sandbox \
@@ -108,6 +111,6 @@ android: docker-android
docker run $(DOCKEROPTS) junegunn/android-sandbox \ docker run $(DOCKEROPTS) junegunn/android-sandbox \
/bin/bash -ci 'cd /fzf/src; GOOS=android make android-build' /bin/bash -ci 'cd /fzf/src; GOOS=android make android-build'
.PHONY: all build deps release test install uninstall clean \ .PHONY: all deps release test install uninstall clean \
linux arch ubuntu centos docker-arch docker-ubuntu docker-centos \ linux arch ubuntu centos docker-arch docker-ubuntu docker-centos \
android-build docker-android ubuntu-android android android-build docker-android ubuntu-android android

View File

@@ -79,7 +79,7 @@ Build
```sh ```sh
# Build fzf executables and tarballs # Build fzf executables and tarballs
make make release
# Install the executable to ../bin directory # Install the executable to ../bin directory
make install make install

View File

@@ -8,7 +8,7 @@ import (
const ( const (
// Current version // Current version
version = "0.12.1" version = "0.13.0"
// Core // Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond coordinatorDelayMax time.Duration = 100 * time.Millisecond
@@ -36,6 +36,9 @@ const (
// History // History
defaultHistoryMax int = 1000 defaultHistoryMax int = 1000
// Jump labels
defaultJumpLabels string = "asdfghjklqwertyuiopzxcvbnm1234567890ASDFGHJKLQWERTYUIOPZXCVBNM`~;:,<.>/?'\"!@#$%^&*()[{]}-_=+"
) )
// fzf events // fzf events

View File

@@ -80,7 +80,16 @@ const (
F2 F2
F3 F3
F4 F4
F5
F6
F7
F8
F9
F10
AltEnter
AltSpace
AltSlash
AltBS AltBS
AltA AltA
AltB AltB
@@ -104,11 +113,14 @@ const (
ColCursor ColCursor
ColSelected ColSelected
ColHeader ColHeader
ColUser ColBorder
ColUser // Should be the last entry
) )
const ( const (
doubleClickDuration = 500 * time.Millisecond doubleClickDuration = 500 * time.Millisecond
colDefault = -1
colUndefined = -2
) )
type ColorTheme struct { type ColorTheme struct {
@@ -125,6 +137,7 @@ type ColorTheme struct {
Cursor int16 Cursor int16
Selected int16 Selected int16
Header int16 Header int16
Border int16
} }
type Event struct { type Event struct {
@@ -159,6 +172,49 @@ var (
DarkBG int DarkBG int
) )
type Window struct {
win *C.WINDOW
Top int
Left int
Width int
Height int
}
func NewWindow(top int, left int, width int, height int, border bool) *Window {
win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left))
if border {
attr := _color(ColBorder, false)
C.wattron(win, attr)
C.box(win, 0, 0)
C.wattroff(win, attr)
}
return &Window{
win: win,
Top: top,
Left: left,
Width: width,
Height: height,
}
}
func EmptyTheme() *ColorTheme {
return &ColorTheme{
UseDefault: true,
Fg: colUndefined,
Bg: colUndefined,
DarkBg: colUndefined,
Prompt: colUndefined,
Match: colUndefined,
Current: colUndefined,
CurrentMatch: colUndefined,
Spinner: colUndefined,
Info: colUndefined,
Cursor: colUndefined,
Selected: colUndefined,
Header: colUndefined,
Border: colUndefined}
}
func init() { func init() {
_prevDownTime = time.Unix(0, 0) _prevDownTime = time.Unix(0, 0)
_clickY = []int{} _clickY = []int{}
@@ -176,7 +232,8 @@ func init() {
Info: C.COLOR_WHITE, Info: C.COLOR_WHITE,
Cursor: C.COLOR_RED, Cursor: C.COLOR_RED,
Selected: C.COLOR_MAGENTA, Selected: C.COLOR_MAGENTA,
Header: C.COLOR_CYAN} Header: C.COLOR_CYAN,
Border: C.COLOR_BLACK}
Dark256 = &ColorTheme{ Dark256 = &ColorTheme{
UseDefault: true, UseDefault: true,
Fg: 15, Fg: 15,
@@ -190,7 +247,8 @@ func init() {
Info: 144, Info: 144,
Cursor: 161, Cursor: 161,
Selected: 168, Selected: 168,
Header: 109} Header: 109,
Border: 59}
Light256 = &ColorTheme{ Light256 = &ColorTheme{
UseDefault: true, UseDefault: true,
Fg: 15, Fg: 15,
@@ -204,7 +262,8 @@ func init() {
Info: 101, Info: 101,
Cursor: 161, Cursor: 161,
Selected: 168, Selected: 168,
Header: 31} Header: 31,
Border: 145}
} }
func attrColored(pair int, bold bool) C.int { func attrColored(pair int, bold bool) C.int {
@@ -280,44 +339,59 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
if theme != nil { if theme != nil {
C.start_color() C.start_color()
initPairs(theme, black) var baseTheme *ColorTheme
if C.tigetnum(C.CString("colors")) >= 256 {
baseTheme = Dark256
} else {
baseTheme = Default16
}
initPairs(baseTheme, theme, black)
_color = attrColored _color = attrColored
} else { } else {
_color = attrMono _color = attrMono
} }
} }
func initPairs(theme *ColorTheme, black bool) { func override(a int16, b int16) C.short {
fg := C.short(theme.Fg) if b == colUndefined {
bg := C.short(theme.Bg) return C.short(a)
}
return C.short(b)
}
func initPairs(baseTheme *ColorTheme, theme *ColorTheme, black bool) {
fg := override(baseTheme.Fg, theme.Fg)
bg := override(baseTheme.Bg, theme.Bg)
if black { if black {
bg = C.COLOR_BLACK bg = C.COLOR_BLACK
} else if theme.UseDefault { } else if theme.UseDefault {
fg = -1 fg = colDefault
bg = -1 bg = colDefault
C.use_default_colors() C.use_default_colors()
} }
if theme.UseDefault { if theme.UseDefault {
FG = -1 FG = colDefault
BG = -1 BG = colDefault
} else { } else {
FG = int(fg) FG = int(fg)
BG = int(bg) BG = int(bg)
C.assume_default_colors(C.int(theme.Fg), C.int(bg)) C.assume_default_colors(C.int(override(baseTheme.Fg, theme.Fg)), C.int(bg))
} }
CurrentFG = int(theme.Current) currentFG := override(baseTheme.Current, theme.Current)
DarkBG = int(theme.DarkBg) darkBG := override(baseTheme.DarkBg, theme.DarkBg)
darkBG := C.short(DarkBG) CurrentFG = int(currentFG)
C.init_pair(ColPrompt, C.short(theme.Prompt), bg) DarkBG = int(darkBG)
C.init_pair(ColMatch, C.short(theme.Match), bg) C.init_pair(ColPrompt, override(baseTheme.Prompt, theme.Prompt), bg)
C.init_pair(ColCurrent, C.short(theme.Current), darkBG) C.init_pair(ColMatch, override(baseTheme.Match, theme.Match), bg)
C.init_pair(ColCurrentMatch, C.short(theme.CurrentMatch), darkBG) C.init_pair(ColCurrent, currentFG, darkBG)
C.init_pair(ColSpinner, C.short(theme.Spinner), bg) C.init_pair(ColCurrentMatch, override(baseTheme.CurrentMatch, theme.CurrentMatch), darkBG)
C.init_pair(ColInfo, C.short(theme.Info), bg) C.init_pair(ColSpinner, override(baseTheme.Spinner, theme.Spinner), bg)
C.init_pair(ColCursor, C.short(theme.Cursor), darkBG) C.init_pair(ColInfo, override(baseTheme.Info, theme.Info), bg)
C.init_pair(ColSelected, C.short(theme.Selected), darkBG) C.init_pair(ColCursor, override(baseTheme.Cursor, theme.Cursor), darkBG)
C.init_pair(ColHeader, C.short(theme.Header), bg) C.init_pair(ColSelected, override(baseTheme.Selected, theme.Selected), darkBG)
C.init_pair(ColHeader, override(baseTheme.Header, theme.Header), bg)
C.init_pair(ColBorder, override(baseTheme.Border, theme.Border), bg)
} }
func Close() { func Close() {
@@ -373,7 +447,9 @@ func mouseSequence(sz *int) Event {
97, 101, 105, 113: // scroll-down / shift / cmd / ctrl 97, 101, 105, 113: // scroll-down / shift / cmd / ctrl
mod := _buf[3] >= 100 mod := _buf[3] >= 100
s := 1 - int(_buf[3]%2)*2 s := 1 - int(_buf[3]%2)*2
return Event{Mouse, 0, &MouseEvent{0, 0, s, false, false, mod}} x := int(_buf[4] - 33)
y := int(_buf[5] - 33)
return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, mod}}
} }
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
@@ -384,6 +460,12 @@ func escSequence(sz *int) Event {
} }
*sz = 2 *sz = 2
switch _buf[1] { switch _buf[1] {
case 13:
return Event{AltEnter, 0, nil}
case 32:
return Event{AltSpace, 0, nil}
case 47:
return Event{AltSlash, 0, nil}
case 98: case 98:
return Event{AltB, 0, nil} return Event{AltB, 0, nil}
case 100: case 100:
@@ -429,6 +511,20 @@ func escSequence(sz *int) Event {
*sz = 4 *sz = 4
switch _buf[2] { switch _buf[2] {
case 50: case 50:
if len(_buf) == 5 && _buf[4] == 126 {
*sz = 5
switch _buf[3] {
case 48:
return Event{F9, 0, nil}
case 49:
return Event{F10, 0, nil}
}
}
// Bracketed paste mode \e[200~ / \e[201
if _buf[3] == 48 && (_buf[4] == 48 || _buf[4] == 49) && _buf[5] == 126 {
*sz = 6
return Event{Invalid, 0, nil}
}
return Event{Invalid, 0, nil} // INS return Event{Invalid, 0, nil} // INS
case 51: case 51:
return Event{Del, 0, nil} return Event{Del, 0, nil}
@@ -442,6 +538,21 @@ func escSequence(sz *int) Event {
switch _buf[3] { switch _buf[3] {
case 126: case 126:
return Event{Home, 0, nil} return Event{Home, 0, nil}
case 53, 55, 56, 57:
if len(_buf) == 5 && _buf[4] == 126 {
*sz = 5
switch _buf[3] {
case 53:
return Event{F5, 0, nil}
case 55:
return Event{F6, 0, nil}
case 56:
return Event{F7, 0, nil}
case 57:
return Event{F8, 0, nil}
}
}
return Event{Invalid, 0, nil}
case 59: case 59:
if len(_buf) != 6 { if len(_buf) != 6 {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
@@ -511,17 +622,25 @@ func GetChar() Event {
return Event{Rune, r, nil} return Event{Rune, r, nil}
} }
func Move(y int, x int) { func (w *Window) Close() {
C.move(C.int(y), C.int(x)) C.delwin(w.win)
} }
func MoveAndClear(y int, x int) { func (w *Window) Enclose(y int, x int) bool {
Move(y, x) return bool(C.wenclose(w.win, C.int(y), C.int(x)))
C.clrtoeol()
} }
func Print(text string) { func (w *Window) Move(y int, x int) {
C.addstr(C.CString(strings.Map(func(r rune) rune { C.wmove(w.win, C.int(y), C.int(x))
}
func (w *Window) MoveAndClear(y int, x int) {
w.Move(y, x)
C.wclrtoeol(w.win)
}
func (w *Window) Print(text string) {
C.waddstr(w.win, C.CString(strings.Map(func(r rune) rune {
if r < 32 { if r < 32 {
return -1 return -1
} }
@@ -529,11 +648,11 @@ func Print(text string) {
}, text))) }, text)))
} }
func CPrint(pair int, bold bool, text string) { func (w *Window) CPrint(pair int, bold bool, text string) {
attr := _color(pair, bold) attr := _color(pair, bold)
C.attron(attr) C.wattron(w.win, attr)
Print(text) w.Print(text)
C.attroff(attr) C.wattroff(w.win, attr)
} }
func Clear() { func Clear() {
@@ -548,6 +667,30 @@ func Refresh() {
C.refresh() C.refresh()
} }
func (w *Window) Erase() {
C.werase(w.win)
}
func (w *Window) Fill(str string) bool {
return C.waddstr(w.win, C.CString(str)) == C.OK
}
func (w *Window) CFill(str string, fg int, bg int, bold bool) bool {
attr := _color(PairFor(fg, bg), bold)
C.wattron(w.win, attr)
ret := w.Fill(str)
C.wattroff(w.win, attr)
return ret
}
func (w *Window) Refresh() {
C.wnoutrefresh(w.win)
}
func DoUpdate() {
C.doupdate()
}
func PairFor(fg int, bg int) int { func PairFor(fg int, bg int) int {
key := (fg << 8) + bg key := (fg << 8) + bg
if found, prs := _colorMap[key]; prs { if found, prs := _colorMap[key]; prs {

View File

@@ -1,6 +1,7 @@
package fzf package fzf
import ( import (
"fmt"
"os" "os"
"regexp" "regexp"
"strconv" "strconv"
@@ -23,35 +24,47 @@ const usage = `usage: fzf [options]
-n, --nth=N[,..] Comma-separated list of field index expressions -n, --nth=N[,..] Comma-separated list of field index expressions
for limiting search scope. Each can be a non-zero for limiting search scope. Each can be a non-zero
integer or a range expression ([BEGIN]..[END]). integer or a range expression ([BEGIN]..[END]).
--with-nth=N[,..] Transform item using index expressions within finder --with-nth=N[,..] Transform the presentation of each line using
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) field index expressions
-d, --delimiter=STR Field delimiter regex (default: AWK-style)
+s, --no-sort Do not sort the result +s, --no-sort Do not sort the result
--tac Reverse the order of the input --tac Reverse the order of the input
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
when the scores are tied; when the scores are tied [length|begin|end|index]
[length|begin|end|index] (default: length) (default: length)
Interface Interface
-m, --multi Enable multi-select with tab/shift-tab -m, --multi Enable multi-select with tab/shift-tab
--ansi Enable processing of ANSI color codes
--no-mouse Disable mouse --no-mouse Disable mouse
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors --bind=KEYBINDS Custom key bindings. Refer to the man page.
--black Use black background
--reverse Reverse orientation
--margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
--tabstop=SPACES Number of spaces for a tab character (default: 8)
--cycle Enable cyclic scroll --cycle Enable cyclic scroll
--no-hscroll Disable horizontal scroll --no-hscroll Disable horizontal scroll
--hscroll-off=COL Number of screen columns to keep to the right of the --hscroll-off=COL Number of screen columns to keep to the right of the
highlighted substring (default: 10) highlighted substring (default: 10)
--jump-labels=CHARS Label characters for jump and jump-accept
Layout
--reverse Reverse orientation
--margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
--inline-info Display finder info inline with the query --inline-info Display finder info inline with the query
--prompt=STR Input prompt (default: '> ') --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=STR String to print as header
--header-lines=N The first N lines of the input are treated as header --header-lines=N The first N lines of the input are treated as header
Display
--ansi Enable processing of ANSI color codes
--tabstop=SPACES Number of spaces for a tab character (default: 8)
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
History
--history=FILE History file
--history-size=N Maximum number of history entries (default: 1000)
Preview
--preview=COMMAND Command to preview highlighted line ({})
--preview-window=OPT Preview window layout (default: right:50%)
[up|down|left|right][:SIZE[%]][:hidden]
Scripting Scripting
-q, --query=STR Start the finder with the given query -q, --query=STR Start the finder with the given query
-1, --select-1 Automatically select the only match -1, --select-1 Automatically select the only match
@@ -87,8 +100,29 @@ const (
byEnd byEnd
) )
func defaultMargin() [4]string { type sizeSpec struct {
return [4]string{"0", "0", "0", "0"} size float64
percent bool
}
func defaultMargin() [4]sizeSpec {
return [4]sizeSpec{}
}
type windowPosition int
const (
posUp windowPosition = iota
posDown
posLeft
posRight
)
type previewOpts struct {
command string
position windowPosition
size sizeSpec
hidden bool
} }
// Options stores the values of command-line options // Options stores the values of command-line options
@@ -112,6 +146,7 @@ type Options struct {
Hscroll bool Hscroll bool
HscrollOff int HscrollOff int
InlineInfo bool InlineInfo bool
JumpLabels string
Prompt string Prompt string
Query string Query string
Select1 bool Select1 bool
@@ -121,24 +156,18 @@ type Options struct {
Expect map[int]string Expect map[int]string
Keymap map[int]actionType Keymap map[int]actionType
Execmap map[int]string Execmap map[int]string
Preview previewOpts
PrintQuery bool PrintQuery bool
ReadZero bool ReadZero bool
Sync bool Sync bool
History *History History *History
Header []string Header []string
HeaderLines int HeaderLines int
Margin [4]string Margin [4]sizeSpec
Tabstop int Tabstop int
Version bool Version bool
} }
func defaultTheme() *curses.ColorTheme {
if strings.Contains(os.Getenv("TERM"), "256") {
return curses.Dark256
}
return curses.Default16
}
func defaultOptions() *Options { func defaultOptions() *Options {
return &Options{ return &Options{
Fuzzy: true, Fuzzy: true,
@@ -153,13 +182,14 @@ func defaultOptions() *Options {
Multi: false, Multi: false,
Ansi: false, Ansi: false,
Mouse: true, Mouse: true,
Theme: defaultTheme(), Theme: curses.EmptyTheme(),
Black: false, Black: false,
Reverse: false, Reverse: false,
Cycle: false, Cycle: false,
Hscroll: true, Hscroll: true,
HscrollOff: 10, HscrollOff: 10,
InlineInfo: false, InlineInfo: false,
JumpLabels: defaultJumpLabels,
Prompt: "> ", Prompt: "> ",
Query: "", Query: "",
Select1: false, Select1: false,
@@ -169,6 +199,7 @@ func defaultOptions() *Options {
Expect: make(map[int]string), Expect: make(map[int]string),
Keymap: make(map[int]actionType), Keymap: make(map[int]actionType),
Execmap: make(map[int]string), Execmap: make(map[int]string),
Preview: previewOpts{"", posRight, sizeSpec{50, true}, false},
PrintQuery: false, PrintQuery: false,
ReadZero: false, ReadZero: false,
Sync: false, Sync: false,
@@ -322,6 +353,12 @@ func parseKeyChords(str string, message string) map[int]string {
chord = curses.AltZ + int(' ') chord = curses.AltZ + int(' ')
case "bspace", "bs": case "bspace", "bs":
chord = curses.BSpace chord = curses.BSpace
case "alt-enter", "alt-return":
chord = curses.AltEnter
case "alt-space":
chord = curses.AltSpace
case "alt-/":
chord = curses.AltSlash
case "alt-bs", "alt-bspace": case "alt-bs", "alt-bspace":
chord = curses.AltBS chord = curses.AltBS
case "tab": case "tab":
@@ -346,12 +383,14 @@ func parseKeyChords(str string, message string) map[int]string {
chord = curses.SRight chord = curses.SRight
case "double-click": case "double-click":
chord = curses.DoubleClick chord = curses.DoubleClick
case "f10":
chord = curses.F10
default: default:
if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
chord = curses.CtrlA + int(lkey[5]) - 'a' chord = curses.CtrlA + int(lkey[5]) - 'a'
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) { } else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) {
chord = curses.AltA + int(lkey[4]) - 'a' chord = curses.AltA + int(lkey[4]) - 'a'
} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '4' { } else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' {
chord = curses.F1 + int(key[1]) - '1' chord = curses.F1 + int(key[1]) - '1'
} else if utf8.RuneCountInString(key) == 1 { } else if utf8.RuneCountInString(key) == 1 {
chord = curses.AltZ + int([]rune(key)[0]) chord = curses.AltZ + int([]rune(key)[0])
@@ -454,6 +493,8 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme
theme.Match = ansi theme.Match = ansi
case "hl+": case "hl+":
theme.CurrentMatch = ansi theme.CurrentMatch = ansi
case "border":
theme.Border = ansi
case "prompt": case "prompt":
theme.Prompt = ansi theme.Prompt = ansi
case "spinner": case "spinner":
@@ -534,6 +575,8 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string)
keymap[key] = actAbort keymap[key] = actAbort
case "accept": case "accept":
keymap[key] = actAccept keymap[key] = actAccept
case "print-query":
keymap[key] = actPrintQuery
case "backward-char": case "backward-char":
keymap[key] = actBackwardChar keymap[key] = actBackwardChar
case "backward-delete-char": case "backward-delete-char":
@@ -554,6 +597,10 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string)
keymap[key] = actForwardChar keymap[key] = actForwardChar
case "forward-word": case "forward-word":
keymap[key] = actForwardWord keymap[key] = actForwardWord
case "jump":
keymap[key] = actJump
case "jump-accept":
keymap[key] = actJumpAccept
case "kill-line": case "kill-line":
keymap[key] = actKillLine keymap[key] = actKillLine
case "kill-word": case "kill-word":
@@ -594,6 +641,8 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string)
keymap[key] = actPreviousHistory keymap[key] = actPreviousHistory
case "next-history": case "next-history":
keymap[key] = actNextHistory keymap[key] = actNextHistory
case "toggle-preview":
keymap[key] = actTogglePreview
case "toggle-sort": case "toggle-sort":
keymap[key] = actToggleSort keymap[key] = actToggleSort
default: default:
@@ -649,40 +698,86 @@ func strLines(str string) []string {
return strings.Split(strings.TrimSuffix(str, "\n"), "\n") return strings.Split(strings.TrimSuffix(str, "\n"), "\n")
} }
func parseMargin(margin string) [4]string { func parseSize(str string, maxPercent float64, label string) sizeSpec {
margins := strings.Split(margin, ",") var val float64
checked := func(str string) string { percent := strings.HasSuffix(str, "%")
if strings.HasSuffix(str, "%") { if percent {
val := atof(str[:len(str)-1]) val = atof(str[:len(str)-1])
if val < 0 { if val < 0 {
errorExit("margin must be non-negative") errorExit(label + " must be non-negative")
} }
if val > 100 { if val > maxPercent {
errorExit("margin too large") errorExit(fmt.Sprintf("%s too large (max: %d%%)", label, int(maxPercent)))
} }
} else { } else {
val := atoi(str) if strings.Contains(str, ".") {
errorExit(label + " (without %) must be a non-negative integer")
}
val = float64(atoi(str))
if val < 0 { if val < 0 {
errorExit("margin must be non-negative") errorExit(label + " must be non-negative")
} }
} }
return str return sizeSpec{val, percent}
}
func parsePreviewWindow(opts *previewOpts, input string) {
layout := input
if strings.HasSuffix(layout, ":hidden") {
opts.hidden = true
layout = strings.TrimSuffix(layout, ":hidden")
}
tokens := strings.Split(layout, ":")
if len(tokens) == 0 || len(tokens) > 2 {
errorExit("invalid window layout: " + input)
}
if len(tokens) > 1 {
opts.size = parseSize(tokens[1], 99, "window size")
} else {
opts.size = sizeSpec{50, true}
}
if !opts.size.percent && opts.size.size > 0 {
// Adjust size for border
opts.size.size += 2
}
switch tokens[0] {
case "up":
opts.position = posUp
case "down":
opts.position = posDown
case "left":
opts.position = posLeft
case "right":
opts.position = posRight
default:
errorExit("invalid window position: " + input)
}
}
func parseMargin(margin string) [4]sizeSpec {
margins := strings.Split(margin, ",")
checked := func(str string) sizeSpec {
return parseSize(str, 49, "margin")
} }
switch len(margins) { switch len(margins) {
case 1: case 1:
m := checked(margins[0]) m := checked(margins[0])
return [4]string{m, m, m, m} return [4]sizeSpec{m, m, m, m}
case 2: case 2:
tb := checked(margins[0]) tb := checked(margins[0])
rl := checked(margins[1]) rl := checked(margins[1])
return [4]string{tb, rl, tb, rl} return [4]sizeSpec{tb, rl, tb, rl}
case 3: case 3:
t := checked(margins[0]) t := checked(margins[0])
rl := checked(margins[1]) rl := checked(margins[1])
b := checked(margins[2]) b := checked(margins[2])
return [4]string{t, rl, b, rl} return [4]sizeSpec{t, rl, b, rl}
case 4: case 4:
return [4]string{ return [4]sizeSpec{
checked(margins[0]), checked(margins[1]), checked(margins[0]), checked(margins[1]),
checked(margins[2]), checked(margins[3])} checked(margins[2]), checked(margins[3])}
default: default:
@@ -714,6 +809,7 @@ func parseOptions(opts *Options, allArgs []string) {
opts.History.maxSize = historyMax opts.History.maxSize = historyMax
} }
} }
validateJumpLabels := false
for i := 0; i < len(allArgs); i++ { for i := 0; i < len(allArgs); i++ {
arg := allArgs[i] arg := allArgs[i]
switch arg { switch arg {
@@ -745,7 +841,7 @@ func parseOptions(opts *Options, allArgs []string) {
case "--color": case "--color":
spec := optionalNextString(allArgs, &i) spec := optionalNextString(allArgs, &i)
if len(spec) == 0 { if len(spec) == 0 {
opts.Theme = defaultTheme() opts.Theme = curses.EmptyTheme()
} else { } else {
opts.Theme = parseTheme(opts.Theme, spec) opts.Theme = parseTheme(opts.Theme, spec)
} }
@@ -805,6 +901,9 @@ func parseOptions(opts *Options, allArgs []string) {
opts.InlineInfo = true opts.InlineInfo = true
case "--no-inline-info": case "--no-inline-info":
opts.InlineInfo = false opts.InlineInfo = false
case "--jump-labels":
opts.JumpLabels = nextString(allArgs, &i, "label characters required")
validateJumpLabels = true
case "-1", "--select-1": case "-1", "--select-1":
opts.Select1 = true opts.Select1 = true
case "+1", "--no-select-1": case "+1", "--no-select-1":
@@ -844,6 +943,13 @@ func parseOptions(opts *Options, allArgs []string) {
case "--header-lines": case "--header-lines":
opts.HeaderLines = atoi( opts.HeaderLines = atoi(
nextString(allArgs, &i, "number of header lines required")) nextString(allArgs, &i, "number of header lines required"))
case "--preview":
opts.Preview.command = nextString(allArgs, &i, "preview command required")
case "--no-preview":
opts.Preview.command = ""
case "--preview-window":
parsePreviewWindow(&opts.Preview,
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]]"))
case "--no-margin": case "--no-margin":
opts.Margin = defaultMargin() opts.Margin = defaultMargin()
case "--margin": case "--margin":
@@ -886,12 +992,18 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Header = strLines(value) opts.Header = strLines(value)
} else if match, value := optString(arg, "--header-lines="); match { } else if match, value := optString(arg, "--header-lines="); match {
opts.HeaderLines = atoi(value) opts.HeaderLines = atoi(value)
} else if match, value := optString(arg, "--preview="); match {
opts.Preview.command = value
} else if match, value := optString(arg, "--preview-window="); match {
parsePreviewWindow(&opts.Preview, value)
} else if match, value := optString(arg, "--margin="); match { } else if match, value := optString(arg, "--margin="); match {
opts.Margin = parseMargin(value) opts.Margin = parseMargin(value)
} else if match, value := optString(arg, "--tabstop="); match { } else if match, value := optString(arg, "--tabstop="); match {
opts.Tabstop = atoi(value) opts.Tabstop = atoi(value)
} else if match, value := optString(arg, "--hscroll-off="); match { } else if match, value := optString(arg, "--hscroll-off="); match {
opts.HscrollOff = atoi(value) opts.HscrollOff = atoi(value)
} else if match, value := optString(arg, "--jump-labels="); match {
opts.JumpLabels = value
} else { } else {
errorExit("unknown option: " + arg) errorExit("unknown option: " + arg)
} }
@@ -909,6 +1021,18 @@ func parseOptions(opts *Options, allArgs []string) {
if opts.Tabstop < 1 { if opts.Tabstop < 1 {
errorExit("tab stop must be a positive integer") errorExit("tab stop must be a positive integer")
} }
if len(opts.JumpLabels) == 0 {
errorExit("empty jump labels")
}
if validateJumpLabels {
for _, r := range opts.JumpLabels {
if r < 32 || r > 126 {
errorExit("non-ascii jump labels are not allowed")
}
}
}
} }
func postProcessOptions(opts *Options) { func postProcessOptions(opts *Options) {

View File

@@ -123,14 +123,14 @@ func TestIrrelevantNth(t *testing.T) {
} }
func TestParseKeys(t *testing.T) { func TestParseKeys(t *testing.T) {
pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g", "") pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ALT-enter,alt-SPACE", "")
check := func(i int, s string) { check := func(i int, s string) {
if pairs[i] != s { if pairs[i] != s {
t.Errorf("%s != %s", pairs[i], s) t.Errorf("%s != %s", pairs[i], s)
} }
} }
if len(pairs) != 9 { if len(pairs) != 11 {
t.Error(9) t.Error(11)
} }
check(curses.CtrlZ, "ctrl-z") check(curses.CtrlZ, "ctrl-z")
check(curses.AltZ, "alt-z") check(curses.AltZ, "alt-z")
@@ -141,6 +141,8 @@ func TestParseKeys(t *testing.T) {
check(curses.CtrlA+'g'-'a', "ctrl-G") check(curses.CtrlA+'g'-'a', "ctrl-G")
check(curses.AltZ+'J', "J") check(curses.AltZ+'J', "J")
check(curses.AltZ+'g', "g") check(curses.AltZ+'g', "g")
check(curses.AltEnter, "ALT-enter")
check(curses.AltSpace, "alt-SPACE")
// Synonyms // Synonyms
pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "") pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")

View File

@@ -7,7 +7,6 @@ import (
"os/signal" "os/signal"
"regexp" "regexp"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
@@ -19,6 +18,14 @@ import (
"github.com/junegunn/go-runewidth" "github.com/junegunn/go-runewidth"
) )
type jumpMode int
const (
jumpDisabled jumpMode = iota
jumpEnabled
jumpAcceptEnabled
)
// Terminal represents terminal input/output // Terminal represents terminal input/output
type Terminal struct { type Terminal struct {
initDelay time.Duration initDelay time.Duration
@@ -45,14 +52,22 @@ type Terminal struct {
header []string header []string
header0 []string header0 []string
ansi bool ansi bool
margin [4]string margin [4]sizeSpec
marginInt [4]int window *C.Window
bwindow *C.Window
pwindow *C.Window
count int count int
progress int progress int
reading bool reading bool
jumping jumpMode
jumpLabels string
merger *Merger merger *Merger
selected map[int32]selectedItem selected map[int32]selectedItem
reqBox *util.EventBox reqBox *util.EventBox
preview previewOpts
previewing bool
previewTxt string
previewBox *util.EventBox
eventBox *util.EventBox eventBox *util.EventBox
mutex sync.Mutex mutex sync.Mutex
initFunc func() initFunc func()
@@ -88,9 +103,13 @@ const (
reqInfo reqInfo
reqHeader reqHeader
reqList reqList
reqJump
reqRefresh reqRefresh
reqRedraw reqRedraw
reqClose reqClose
reqPrintQuery
reqPreviewEnqueue
reqPreviewDisplay
reqQuit reqQuit
) )
@@ -132,7 +151,11 @@ const (
actUp actUp
actPageUp actPageUp
actPageDown actPageDown
actJump
actJumpAccept
actPrintQuery
actToggleSort actToggleSort
actTogglePreview
actPreviousHistory actPreviousHistory
actNextHistory actNextHistory
actExecute actExecute
@@ -205,6 +228,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
} else { } else {
delay = initialDelay delay = initialDelay
} }
var previewBox *util.EventBox
if len(opts.Preview.command) > 0 {
previewBox = util.NewEventBox()
}
return &Terminal{ return &Terminal{
initDelay: delay, initDelay: delay,
inlineInfo: opts.InlineInfo, inlineInfo: opts.InlineInfo,
@@ -227,15 +254,20 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
printQuery: opts.PrintQuery, printQuery: opts.PrintQuery,
history: opts.History, history: opts.History,
margin: opts.Margin, margin: opts.Margin,
marginInt: [4]int{0, 0, 0, 0},
cycle: opts.Cycle, cycle: opts.Cycle,
header: header, header: header,
header0: header, header0: header,
ansi: opts.Ansi, ansi: opts.Ansi,
reading: true, reading: true,
jumping: jumpDisabled,
jumpLabels: opts.JumpLabels,
merger: EmptyMerger, merger: EmptyMerger,
selected: make(map[int32]selectedItem), selected: make(map[int32]selectedItem),
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
preview: opts.Preview,
previewing: previewBox != nil && !opts.Preview.hidden,
previewTxt: "",
previewBox: previewBox,
eventBox: eventBox, eventBox: eventBox,
mutex: sync.Mutex{}, mutex: sync.Mutex{},
suppress: true, suppress: true,
@@ -315,7 +347,7 @@ func (t *Terminal) output() bool {
if !found { if !found {
cnt := t.merger.Length() cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy { if cnt > 0 && cnt > t.cy {
fmt.Println(t.merger.Get(t.cy).AsString(t.ansi)) fmt.Println(t.current())
found = true found = true
} }
} else { } else {
@@ -355,56 +387,113 @@ func displayWidth(runes []rune) int {
return l return l
} }
const minWidth = 16 const (
const minHeight = 4 minWidth = 16
minHeight = 4
)
func (t *Terminal) calculateMargins() { func calculateSize(base int, size sizeSpec, margin int, minSize int) int {
max := base - margin
if size.percent {
return util.Constrain(int(float64(base)*0.01*size.size), minSize, max)
}
return util.Constrain(int(size.size), minSize, max)
}
func (t *Terminal) resizeWindows() {
screenWidth := C.MaxX() screenWidth := C.MaxX()
screenHeight := C.MaxY() screenHeight := C.MaxY()
for idx, str := range t.margin { marginInt := [4]int{}
if str == "0" { for idx, sizeSpec := range t.margin {
t.marginInt[idx] = 0 if sizeSpec.percent {
} else if strings.HasSuffix(str, "%") { var max float64
num, _ := strconv.ParseFloat(str[:len(str)-1], 64)
var val float64
if idx%2 == 0 { if idx%2 == 0 {
val = float64(screenHeight) max = float64(screenHeight)
} else { } else {
val = float64(screenWidth) max = float64(screenWidth)
} }
t.marginInt[idx] = int(val * num * 0.01) marginInt[idx] = int(max * sizeSpec.size * 0.01)
} else { } else {
num, _ := strconv.Atoi(str) marginInt[idx] = int(sizeSpec.size)
t.marginInt[idx] = num
} }
} }
adjust := func(idx1 int, idx2 int, max int, min int) { adjust := func(idx1 int, idx2 int, max int, min int) {
if max >= min { if max >= min {
margin := t.marginInt[idx1] + t.marginInt[idx2] margin := marginInt[idx1] + marginInt[idx2]
if max-margin < min { if max-margin < min {
desired := max - min desired := max - min
t.marginInt[idx1] = desired * t.marginInt[idx1] / margin marginInt[idx1] = desired * marginInt[idx1] / margin
t.marginInt[idx2] = desired * t.marginInt[idx2] / margin marginInt[idx2] = desired * marginInt[idx2] / margin
} }
} }
} }
adjust(1, 3, screenWidth, minWidth) minAreaWidth := minWidth
adjust(0, 2, screenHeight, minHeight) minAreaHeight := minHeight
if t.isPreviewEnabled() {
switch t.preview.position {
case posUp, posDown:
minAreaHeight *= 2
case posLeft, posRight:
minAreaWidth *= 2
}
}
adjust(1, 3, screenWidth, minAreaWidth)
adjust(0, 2, screenHeight, minAreaHeight)
if t.window != nil {
t.window.Close()
}
if t.bwindow != nil {
t.bwindow.Close()
t.pwindow.Close()
}
width := screenWidth - marginInt[1] - marginInt[3]
height := screenHeight - marginInt[0] - marginInt[2]
if t.isPreviewEnabled() {
createPreviewWindow := func(y int, x int, w int, h int) {
t.bwindow = C.NewWindow(y, x, w, h, true)
t.pwindow = C.NewWindow(y+1, x+2, w-4, h-2, false)
}
switch t.preview.position {
case posUp:
pheight := calculateSize(height, t.preview.size, minHeight, 3)
t.window = C.NewWindow(
marginInt[0]+pheight, marginInt[3], width, height-pheight, false)
createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
case posDown:
pheight := calculateSize(height, t.preview.size, minHeight, 3)
t.window = C.NewWindow(
marginInt[0], marginInt[3], width, height-pheight, false)
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
case posLeft:
pwidth := calculateSize(width, t.preview.size, minWidth, 5)
t.window = C.NewWindow(
marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false)
createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
case posRight:
pwidth := calculateSize(width, t.preview.size, minWidth, 5)
t.window = C.NewWindow(
marginInt[0], marginInt[3], width-pwidth, height, false)
createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height)
}
} else {
t.window = C.NewWindow(
marginInt[0],
marginInt[3],
width,
height, false)
}
} }
func (t *Terminal) move(y int, x int, clear bool) { func (t *Terminal) move(y int, x int, clear bool) {
x += t.marginInt[3]
maxy := C.MaxY()
if !t.reverse { if !t.reverse {
y = maxy - y - 1 - t.marginInt[2] y = t.window.Height - y - 1
} else {
y += t.marginInt[0]
} }
if clear { if clear {
C.MoveAndClear(y, x) t.window.MoveAndClear(y, x)
} else { } else {
C.Move(y, x) t.window.Move(y, x)
} }
} }
@@ -414,24 +503,24 @@ func (t *Terminal) placeCursor() {
func (t *Terminal) printPrompt() { func (t *Terminal) printPrompt() {
t.move(0, 0, true) t.move(0, 0, true)
C.CPrint(C.ColPrompt, true, t.prompt) t.window.CPrint(C.ColPrompt, true, t.prompt)
C.CPrint(C.ColNormal, true, string(t.input)) t.window.CPrint(C.ColNormal, true, string(t.input))
} }
func (t *Terminal) printInfo() { func (t *Terminal) printInfo() {
if t.inlineInfo { if t.inlineInfo {
t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input)+1, true) t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input)+1, true)
if t.reading { if t.reading {
C.CPrint(C.ColSpinner, true, " < ") t.window.CPrint(C.ColSpinner, true, " < ")
} else { } else {
C.CPrint(C.ColPrompt, true, " < ") t.window.CPrint(C.ColPrompt, true, " < ")
} }
} else { } else {
t.move(1, 0, true) t.move(1, 0, true)
if t.reading { if t.reading {
duration := int64(spinnerDuration) duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
C.CPrint(C.ColSpinner, true, _spinner[idx]) t.window.CPrint(C.ColSpinner, true, _spinner[idx])
} }
t.move(1, 2, false) t.move(1, 2, false)
} }
@@ -450,18 +539,14 @@ func (t *Terminal) printInfo() {
if t.progress > 0 && t.progress < 100 { if t.progress > 0 && t.progress < 100 {
output += fmt.Sprintf(" (%d%%)", t.progress) output += fmt.Sprintf(" (%d%%)", t.progress)
} }
C.CPrint(C.ColInfo, false, output) t.window.CPrint(C.ColInfo, false, output)
}
func (t *Terminal) maxHeight() int {
return C.MaxY() - t.marginInt[0] - t.marginInt[2]
} }
func (t *Terminal) printHeader() { func (t *Terminal) printHeader() {
if len(t.header) == 0 { if len(t.header) == 0 {
return return
} }
max := t.maxHeight() max := t.window.Height
var state *ansiState var state *ansiState
for idx, lineStr := range t.header { for idx, lineStr := range t.header {
line := idx + 2 line := idx + 2
@@ -495,27 +580,36 @@ func (t *Terminal) printList() {
} }
t.move(line, 0, true) t.move(line, 0, true)
if i < count { if i < count {
t.printItem(t.merger.Get(i+t.offset), i == t.cy-t.offset) t.printItem(t.merger.Get(i+t.offset), i, i == t.cy-t.offset)
} }
} }
} }
func (t *Terminal) printItem(item *Item, current bool) { func (t *Terminal) printItem(item *Item, i int, current bool) {
_, selected := t.selected[item.Index()] _, selected := t.selected[item.Index()]
label := " "
if t.jumping != jumpDisabled {
if i < len(t.jumpLabels) {
// Striped
current = i%2 == 0
label = t.jumpLabels[i : i+1]
}
} else if current {
label = ">"
}
t.window.CPrint(C.ColCursor, true, label)
if current { if current {
C.CPrint(C.ColCursor, true, ">")
if selected { if selected {
C.CPrint(C.ColSelected, true, ">") t.window.CPrint(C.ColSelected, true, ">")
} else { } else {
C.CPrint(C.ColCurrent, true, " ") t.window.CPrint(C.ColCurrent, true, " ")
} }
t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch, true) t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch, true)
} else { } else {
C.CPrint(C.ColCursor, true, " ")
if selected { if selected {
C.CPrint(C.ColSelected, true, ">") t.window.CPrint(C.ColSelected, true, ">")
} else { } else {
C.Print(" ") t.window.Print(" ")
} }
t.printHighlighted(item, false, 0, C.ColMatch, false) t.printHighlighted(item, false, 0, C.ColMatch, false)
} }
@@ -567,7 +661,7 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c
text := make([]rune, len(item.text)) text := make([]rune, len(item.text))
copy(text, item.text) copy(text, item.text)
offsets := item.colorOffsets(col2, bold, current) offsets := item.colorOffsets(col2, bold, current)
maxWidth := C.MaxX() - 3 - t.marginInt[1] - t.marginInt[3] maxWidth := t.window.Width - 3
maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text))
fullWidth := displayWidth(text) fullWidth := displayWidth(text)
if fullWidth > maxWidth { if fullWidth > maxWidth {
@@ -617,11 +711,11 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c
e := util.Constrain32(offset.offset[1], index, maxOffset) e := util.Constrain32(offset.offset[1], index, maxOffset)
substr, prefixWidth = processTabs(text[index:b], prefixWidth) substr, prefixWidth = processTabs(text[index:b], prefixWidth)
C.CPrint(col1, bold, substr) t.window.CPrint(col1, bold, substr)
if b < e { if b < e {
substr, prefixWidth = processTabs(text[b:e], prefixWidth) substr, prefixWidth = processTabs(text[b:e], prefixWidth)
C.CPrint(offset.color, offset.bold, substr) t.window.CPrint(offset.color, offset.bold, substr)
} }
index = e index = e
@@ -631,7 +725,29 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c
} }
if index < maxOffset { if index < maxOffset {
substr, _ = processTabs(text[index:], prefixWidth) substr, _ = processTabs(text[index:], prefixWidth)
C.CPrint(col1, bold, substr) t.window.CPrint(col1, bold, substr)
}
}
func (t *Terminal) printPreview() {
trimmed, ansiOffsets, _ := extractColor(t.previewTxt, nil)
var index int32
t.pwindow.Erase()
for _, o := range ansiOffsets {
b := o.offset[0]
e := o.offset[1]
if b > index {
if !t.pwindow.Fill(trimmed[index:b]) {
return
}
}
if !t.pwindow.CFill(trimmed[b:e], o.color.fg, o.color.bg, o.color.bold) {
return
}
index = e
}
if int(index) < len(trimmed) {
t.pwindow.Fill(trimmed[index:])
} }
} }
@@ -651,16 +767,24 @@ func processTabs(runes []rune, prefixWidth int) (string, int) {
} }
func (t *Terminal) printAll() { func (t *Terminal) printAll() {
t.calculateMargins() t.resizeWindows()
t.printList() t.printList()
t.printPrompt() t.printPrompt()
t.printInfo() t.printInfo()
t.printHeader() t.printHeader()
if t.isPreviewEnabled() {
t.printPreview()
}
} }
func (t *Terminal) refresh() { func (t *Terminal) refresh() {
if !t.suppress { if !t.suppress {
C.Refresh() if t.isPreviewEnabled() {
t.bwindow.Refresh()
t.pwindow.Refresh()
}
t.window.Refresh()
C.DoUpdate()
} }
} }
@@ -717,10 +841,10 @@ func keyMatch(key int, event C.Event) bool {
} }
func quoteEntry(entry string) string { func quoteEntry(entry string) string {
return fmt.Sprintf("%q", entry) return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
} }
func executeCommand(template string, replacement string) { func (t *Terminal) executeCommand(template string, replacement string) {
command := strings.Replace(template, "{}", replacement, -1) command := strings.Replace(template, "{}", replacement, -1)
cmd := util.ExecCommand(command) cmd := util.ExecCommand(command)
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
@@ -728,7 +852,19 @@ func executeCommand(template string, replacement string) {
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
C.Endwin() C.Endwin()
cmd.Run() cmd.Run()
C.Refresh() t.refresh()
}
func (t *Terminal) hasPreviewWindow() bool {
return t.previewBox != nil
}
func (t *Terminal) isPreviewEnabled() bool {
return t.previewBox != nil && t.previewing
}
func (t *Terminal) current() string {
return t.merger.Get(t.cy).AsString(t.ansi)
} }
// Loop is called to start Terminal I/O // Loop is called to start Terminal I/O
@@ -753,10 +889,10 @@ func (t *Terminal) Loop() {
t.mutex.Lock() t.mutex.Lock()
t.initFunc() t.initFunc()
t.calculateMargins() t.resizeWindows()
t.printPrompt() t.printPrompt()
t.placeCursor() t.placeCursor()
C.Refresh() t.refresh()
t.printInfo() t.printInfo()
t.printHeader() t.printHeader()
t.mutex.Unlock() t.mutex.Unlock()
@@ -781,6 +917,29 @@ func (t *Terminal) Loop() {
}() }()
} }
if t.hasPreviewWindow() {
go func() {
for {
focused := ""
t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events {
switch req {
case reqPreviewEnqueue:
focused = value.(string)
}
}
events.Clear()
})
if len(focused) > 0 {
command := strings.Replace(t.preview.command, "{}", quoteEntry(focused), -1)
cmd := util.ExecCommand(command)
out, _ := cmd.CombinedOutput()
t.reqBox.Set(reqPreviewDisplay, string(out))
}
}
}()
}
exit := func(code int) { exit := func(code int) {
if code <= exitNoMatch && t.history != nil { if code <= exitNoMatch && t.history != nil {
t.history.append(string(t.input)) t.history.append(string(t.input))
@@ -789,11 +948,12 @@ func (t *Terminal) Loop() {
} }
go func() { go func() {
focused := ""
for { for {
t.reqBox.Wait(func(events *util.Events) { t.reqBox.Wait(func(events *util.Events) {
defer events.Clear() defer events.Clear()
t.mutex.Lock() t.mutex.Lock()
for req := range *events { for req, value := range *events {
switch req { switch req {
case reqPrompt: case reqPrompt:
t.printPrompt() t.printPrompt()
@@ -804,6 +964,26 @@ func (t *Terminal) Loop() {
t.printInfo() t.printInfo()
case reqList: case reqList:
t.printList() t.printList()
cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy {
currentFocus := t.current()
if currentFocus != focused {
focused = currentFocus
if t.isPreviewEnabled() {
t.previewBox.Set(reqPreviewEnqueue, focused)
}
}
} else {
if focused != "" && t.isPreviewEnabled() {
t.pwindow.Erase()
}
focused = ""
}
case reqJump:
if t.merger.Length() == 0 {
t.jumping = jumpDisabled
}
t.printList()
case reqHeader: case reqHeader:
t.printHeader() t.printHeader()
case reqRefresh: case reqRefresh:
@@ -819,6 +999,13 @@ func (t *Terminal) Loop() {
exit(exitOk) exit(exitOk)
} }
exit(exitNoMatch) exit(exitNoMatch)
case reqPreviewDisplay:
t.previewTxt = value.(string)
t.printPreview()
case reqPrintQuery:
C.Close()
fmt.Println(string(t.input))
exit(exitOk)
case reqQuit: case reqQuit:
C.Close() C.Close()
exit(exitInterrupt) exit(exitInterrupt)
@@ -880,7 +1067,7 @@ func (t *Terminal) Loop() {
case actExecute: case actExecute:
if t.cy >= 0 && t.cy < t.merger.Length() { if t.cy >= 0 && t.cy < t.merger.Length() {
item := t.merger.Get(t.cy) item := t.merger.Get(t.cy)
executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi))) t.executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi)))
} }
case actExecuteMulti: case actExecuteMulti:
if len(t.selected) > 0 { if len(t.selected) > 0 {
@@ -888,13 +1075,23 @@ func (t *Terminal) Loop() {
for i, sel := range t.sortSelected() { for i, sel := range t.sortSelected() {
sels[i] = quoteEntry(*sel.text) sels[i] = quoteEntry(*sel.text)
} }
executeCommand(t.execmap[mapkey], strings.Join(sels, " ")) t.executeCommand(t.execmap[mapkey], strings.Join(sels, " "))
} else { } else {
return doAction(actExecute, mapkey) return doAction(actExecute, mapkey)
} }
case actInvalid: case actInvalid:
t.mutex.Unlock() t.mutex.Unlock()
return false return false
case actTogglePreview:
if t.hasPreviewWindow() {
t.previewing = !t.previewing
t.resizeWindows()
cnt := t.merger.Length()
if t.previewing && cnt > 0 && cnt > t.cy {
t.previewBox.Set(reqPreviewEnqueue, t.current())
}
req(reqList, reqInfo)
}
case actToggleSort: case actToggleSort:
t.sort = !t.sort t.sort = !t.sort
t.eventBox.Set(EvtSearchNew, t.sort) t.eventBox.Set(EvtSearchNew, t.sort)
@@ -906,6 +1103,8 @@ func (t *Terminal) Loop() {
if t.cx > 0 { if t.cx > 0 {
t.cx-- t.cx--
} }
case actPrintQuery:
req(reqPrintQuery)
case actAbort: case actAbort:
req(reqQuit) req(reqQuit)
case actDeleteChar: case actDeleteChar:
@@ -1017,6 +1216,12 @@ func (t *Terminal) Loop() {
case actPageDown: case actPageDown:
t.vmove(-(t.maxItems() - 1)) t.vmove(-(t.maxItems() - 1))
req(reqList) req(reqList)
case actJump:
t.jumping = jumpEnabled
req(reqJump)
case actJumpAccept:
t.jumping = jumpAcceptEnabled
req(reqJump)
case actBackwardWord: case actBackwardWord:
t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1 t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1
case actForwardWord: case actForwardWord:
@@ -1054,20 +1259,19 @@ func (t *Terminal) Loop() {
mx, my := me.X, me.Y mx, my := me.X, me.Y
if me.S != 0 { if me.S != 0 {
// Scroll // Scroll
if t.merger.Length() > 0 { if t.window.Enclose(my, mx) && t.merger.Length() > 0 {
if t.multi && me.Mod { if t.multi && me.Mod {
toggle() toggle()
} }
t.vmove(me.S) t.vmove(me.S)
req(reqList) req(reqList)
} }
} else if mx >= t.marginInt[3] && mx < C.MaxX()-t.marginInt[1] && } else if t.window.Enclose(my, mx) {
my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] { mx -= t.window.Left
mx -= t.marginInt[3] my -= t.window.Top
my -= t.marginInt[0]
mx = util.Constrain(mx-displayWidth([]rune(t.prompt)), 0, len(t.input)) mx = util.Constrain(mx-displayWidth([]rune(t.prompt)), 0, len(t.input))
if !t.reverse { if !t.reverse {
my = t.maxHeight() - my - 1 my = t.window.Height - my - 1
} }
min := 2 + len(t.header) min := 2 + len(t.header)
if t.inlineInfo { if t.inlineInfo {
@@ -1096,9 +1300,11 @@ func (t *Terminal) Loop() {
} }
return true return true
} }
action := t.keymap[event.Type] changed := false
mapkey := event.Type mapkey := event.Type
if event.Type == C.Rune { if t.jumping == jumpDisabled {
action := t.keymap[mapkey]
if mapkey == C.Rune {
mapkey = int(event.Char) + int(C.AltZ) mapkey = int(event.Char) + int(C.AltZ)
if act, prs := t.keymap[mapkey]; prs { if act, prs := t.keymap[mapkey]; prs {
action = act action = act
@@ -1107,7 +1313,19 @@ func (t *Terminal) Loop() {
if !doAction(action, mapkey) { if !doAction(action, mapkey) {
continue continue
} }
changed := string(previousInput) != string(t.input) changed = string(previousInput) != string(t.input)
} else {
if mapkey == C.Rune {
if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() {
t.cy = idx + t.offset
if t.jumping == jumpAcceptEnabled {
req(reqClose)
}
}
}
t.jumping = jumpDisabled
req(reqList)
}
t.mutex.Unlock() // Must be unlocked before touching reqBox t.mutex.Unlock() // Must be unlocked before touching reqBox
if changed { if changed {
@@ -1160,7 +1378,7 @@ func (t *Terminal) vset(o int) bool {
} }
func (t *Terminal) maxItems() int { func (t *Terminal) maxItems() int {
max := t.maxHeight() - 2 - len(t.header) max := t.window.Height - 2 - len(t.header)
if t.inlineInfo { if t.inlineInfo {
max++ max++
} }

View File

@@ -430,6 +430,10 @@ class TestGoFZF < TestBase
test.call 'f3', 'f3' test.call 'f3', 'f3'
test.call 'f2,f4', 'f2', 'f2' test.call 'f2,f4', 'f2', 'f2'
test.call 'f2,f4', 'f4', 'f4' test.call 'f2,f4', 'f4', 'f4'
test.call 'alt-/', [:Escape, :/]
%w[f5 f6 f7 f8 f9 f10].each do |key|
test.call 'f5,f6,f7,f8,f9,f10', key, key
end
test.call '@', '@' test.call '@', '@'
end end
@@ -771,6 +775,13 @@ class TestGoFZF < TestBase
assert_equal %w[4 5 6 9], readonce.split($/) assert_equal %w[4 5 6 9], readonce.split($/)
end end
def test_bind_print_query
tmux.send_keys "seq 1 1000 | #{fzf '-m --bind=ctrl-j:print-query'}", :Enter
tmux.until { |lines| lines[-2].end_with? '/1000' }
tmux.send_keys 'print-my-query', 'C-j'
assert_equal %w[print-my-query], readonce.split($/)
end
def test_long_line def test_long_line
data = '.' * 256 * 1024 data = '.' * 256 * 1024
File.open(tempname, 'w') do |f| File.open(tempname, 'w') do |f|
@@ -858,20 +869,34 @@ class TestGoFZF < TestBase
def test_execute def test_execute
output = '/tmp/fzf-test-execute' output = '/tmp/fzf-test-execute'
opts = %[--bind \\"alt-a:execute(echo '[{}]' >> #{output}),alt-b:execute[echo '({}), ({})' >> #{output}],C:execute:echo '({}), [{}], @{}@' >> #{output}\\"] opts = %[--bind \\"alt-a:execute(echo [{}] >> #{output}),alt-b:execute[echo /{}{}/ >> #{output}],C:execute:echo /{}{}{}/ >> #{output}\\"]
tmux.send_keys "seq 100 | #{fzf opts}", :Enter wait = lambda { |exp| tmux.until { |lines| lines[-2].include? exp } }
tmux.until { |lines| lines[-2].include? '100/100' } writelines tempname, %w[foo'bar foo"bar foo$bar]
tmux.send_keys :Escape, :a, :Escape, :a tmux.send_keys "cat #{tempname} | #{fzf opts}; sync", :Enter
wait['3/3']
tmux.send_keys :Escape, :a
wait['/3']
tmux.send_keys :Escape, :a
wait['/3']
tmux.send_keys :Up tmux.send_keys :Up
tmux.send_keys :Escape, :b, :Escape, :b tmux.send_keys :Escape, :b
wait['/3']
tmux.send_keys :Escape, :b
wait['/3']
tmux.send_keys :Up tmux.send_keys :Up
tmux.send_keys :C tmux.send_keys :C
tmux.send_keys 'foobar' wait['3/3']
tmux.until { |lines| lines[-2].include? '0/100' } tmux.send_keys 'barfoo'
tmux.send_keys :Escape, :a, :Escape, :b, :Escape, :c wait['0/3']
tmux.send_keys :Escape, :a
wait['/3']
tmux.send_keys :Escape, :b
wait['/3']
tmux.send_keys :Enter tmux.send_keys :Enter
readonce readonce
assert_equal ['["1"]', '["1"]', '("2"), ("2")', '("2"), ("2")', '("3"), ["3"], @"3"@'], assert_equal %w[[foo'bar] [foo'bar]
/foo"barfoo"bar/ /foo"barfoo"bar/
/foo$barfoo$barfoo$bar/],
File.readlines(output).map(&:chomp) File.readlines(output).map(&:chomp)
ensure ensure
File.unlink output rescue nil File.unlink output rescue nil
@@ -879,21 +904,24 @@ class TestGoFZF < TestBase
def test_execute_multi def test_execute_multi
output = '/tmp/fzf-test-execute-multi' output = '/tmp/fzf-test-execute-multi'
opts = %[--multi --bind \\"alt-a:execute-multi(echo '[{}], @{}@' >> #{output}; sync)\\"] opts = %[--multi --bind \\"alt-a:execute-multi(echo {}/{} >> #{output}; sync)\\"]
tmux.send_keys "seq 100 | #{fzf opts}", :Enter writelines tempname, %w[foo'bar foo"bar foo$bar foobar]
tmux.until { |lines| lines[-2].include? '100/100' } tmux.send_keys "cat #{tempname} | #{fzf opts}", :Enter
tmux.until { |lines| lines[-2].include? '4/4' }
tmux.send_keys :Escape, :a tmux.send_keys :Escape, :a
tmux.until { |lines| lines[-2].include? '/100' } tmux.until { |lines| lines[-2].include? '/4' }
tmux.send_keys :BTab, :BTab, :BTab tmux.send_keys :BTab, :BTab, :BTab
tmux.send_keys :Escape, :a tmux.send_keys :Escape, :a
tmux.until { |lines| lines[-2].include? '/100' } tmux.until { |lines| lines[-2].include? '/4' }
tmux.send_keys :Tab, :Tab tmux.send_keys :Tab, :Tab
tmux.send_keys :Escape, :a tmux.send_keys :Escape, :a
tmux.until { |lines| lines[-2].include? '/100' } tmux.until { |lines| lines[-2].include? '/4' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.prepare tmux.prepare
readonce readonce
assert_equal ['["1"], @"1"@', '["1" "2" "3"], @"1" "2" "3"@', '["1" "2" "4"], @"1" "2" "4"@'], assert_equal [%[foo'bar/foo'bar],
%[foo'bar foo"bar foo$bar/foo'bar foo"bar foo$bar],
%[foo'bar foo"bar foobar/foo'bar foo"bar foobar]],
File.readlines(output).map(&:chomp) File.readlines(output).map(&:chomp)
ensure ensure
File.unlink output rescue nil File.unlink output rescue nil
@@ -912,7 +940,7 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-2].include? '1/1' } tmux.until { |lines| lines[-2].include? '1/1' }
tmux.send_keys 'C-c' tmux.send_keys 'C-c'
tmux.prepare tmux.prepare
assert_equal ['-c / "foo"bar'], File.readlines(output).map(&:chomp) assert_equal ["-c / 'foo'bar"], File.readlines(output).map(&:chomp)
ensure ensure
File.unlink output rescue nil File.unlink output rescue nil
end end
@@ -1026,7 +1054,7 @@ class TestGoFZF < TestBase
end end
end end
def test_canel def test_cancel
tmux.send_keys "seq 10 | #{fzf "--bind 2:cancel"}", :Enter tmux.send_keys "seq 10 | #{fzf "--bind 2:cancel"}", :Enter
tmux.until { |lines| lines[-2].include?('10/10') } tmux.until { |lines| lines[-2].include?('10/10') }
tmux.send_keys '123' tmux.send_keys '123'
@@ -1163,6 +1191,65 @@ class TestGoFZF < TestBase
tmux.send_keys :Enter tmux.send_keys :Enter
end end
def test_jump
tmux.send_keys "seq 1000 | #{fzf "--multi --jump-labels 12345 --bind 'ctrl-j:jump'"}", :Enter
tmux.until { |lines| lines[-2] == ' 1000/1000' }
tmux.send_keys 'C-j'
tmux.until { |lines| lines[-7] == '5 5' }
tmux.until { |lines| lines[-8] == ' 6' }
tmux.send_keys '5'
tmux.until { |lines| lines[-7] == '> 5' }
tmux.send_keys :Tab
tmux.until { |lines| lines[-7] == ' >5' }
tmux.send_keys 'C-j'
tmux.until { |lines| lines[-7] == '5>5' }
tmux.send_keys '2'
tmux.until { |lines| lines[-4] == '> 2' }
tmux.send_keys :Tab
tmux.until { |lines| lines[-4] == ' >2' }
tmux.send_keys 'C-j'
tmux.until { |lines| lines[-7] == '5>5' }
# Press any key other than jump labels to cancel jump
tmux.send_keys '6'
tmux.until { |lines| lines[-3] == '> 1' }
tmux.send_keys :Tab
tmux.until { |lines| lines[-3] == '>>1' }
tmux.send_keys :Enter
assert_equal %w[5 2 1], readonce.split($/)
end
def test_jump_accept
tmux.send_keys "seq 1000 | #{fzf "--multi --jump-labels 12345 --bind 'ctrl-j:jump-accept'"}", :Enter
tmux.until { |lines| lines[-2] == ' 1000/1000' }
tmux.send_keys 'C-j'
tmux.until { |lines| lines[-7] == '5 5' }
tmux.send_keys '3'
assert_equal '3', readonce.chomp
end
def test_preview
tmux.send_keys %[seq 1000 | #{FZF} --preview 'echo {{}-{}}' --bind ?:toggle-preview], :Enter
tmux.until { |lines| lines[1].include?(' {1-1}') }
tmux.send_keys '555'
tmux.until { |lines| lines[1].include?(' {555-555}') }
tmux.send_keys '?'
tmux.until { |lines| !lines[1].include?(' {555-555}') }
tmux.send_keys '?'
tmux.until { |lines| lines[1].include?(' {555-555}') }
end
def test_preview_hidden
tmux.send_keys %[seq 1000 | #{FZF} --preview 'echo {{}-{}}' --preview-window down:1:hidden --bind ?:toggle-preview], :Enter
tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys '?'
tmux.until { |lines| lines[-2].include?(' {1-1}') }
tmux.send_keys '555'
tmux.until { |lines| lines[-2].include?(' {555-555}') }
tmux.send_keys '?'
tmux.until { |lines| lines[-1] == '> 555' }
end
private private
def writelines path, lines def writelines path, lines
File.unlink path while File.exists? path File.unlink path while File.exists? path
@@ -1229,7 +1316,7 @@ module TestShell
tmux.send_keys :BTab, :BTab, pane: 1 tmux.send_keys :BTab, :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include? '(2)' } tmux.until(1) { |lines| lines[-2].include? '(2)' }
tmux.send_keys :Enter, pane: 1 tmux.send_keys :Enter, pane: 1
tmux.until { |lines| lines[-1].include? 'cat' } tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include? 'test1test2' } tmux.until { |lines| lines[-1].include? 'test1test2' }
end end
@@ -1444,7 +1531,7 @@ module CompletionTest
tmux.send_keys :BTab, :BTab, pane: 1 tmux.send_keys :BTab, :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include? '(2)' } tmux.until(1) { |lines| lines[-2].include? '(2)' }
tmux.send_keys :Enter, pane: 1 tmux.send_keys :Enter, pane: 1
tmux.until { |lines| lines[-1].include? 'cat' } tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include? 'test3test4' } tmux.until { |lines| lines[-1].include? 'test3test4' }
end end