Compare commits

...

243 Commits

Author SHA1 Message Date
Junegunn Choi
30577b0c17 0.20.0 2019-12-18 01:07:25 +09:00
Junegunn Choi
212de25409 Fix incorrect header array mutation 2019-12-16 18:47:05 +09:00
Jan Edmund Lazo
5da8bbf45a [vim] Encode list source to codepage (#1794) 2019-12-16 14:41:03 +09:00
Jan Edmund Lazo
aa0e10ead7 [vim] Use cterm colors on Windows (#1793)
Truecolor does not work on default Windows terminal.
It is a problem in neovim GUIs.

https://github.com/sainnhe/edge/issues/5#issuecomment-565748240
2019-12-15 22:17:24 +09:00
msr1k
a9906c7c29 Add MSYS2 support as a vim plugin (#1677)
* Add MSYS2 support as a vim plugin

Add &shellcmdflag and TERM environment variable treatment.

- Make &shellcmdflag `/C` when &shell turns into `cmd.exe`
- Delete %TERM% environment variable before fzf execution

* Change shellescape default value depending on s:is_win flag

* Make TERM environment empty only when gui is running

* Stop checking &shell in fzf#shellescape function

This funcion's behavior is controlled by only if it is Windows or not.
So there is no need to check &shell.

* Take neovim into consideration when to set shellcmdflag

* Add &shellxquote control
2019-12-15 18:25:58 +09:00
Junegunn Choi
9fefe08b3f Revert README as preview-{fg,bg} are only available on master 2019-12-13 12:46:27 +09:00
Junegunn Choi
684bfff713 Update README/CHANGELOG 2019-12-13 12:43:34 +09:00
Junegunn Choi
3db6b88d82 Add preview-fg and preview-bg for --color
Close #1776
2019-12-12 23:03:17 +09:00
Junegunn Choi
8ae96774df Gutter color of 16-color theme should be undefined by default 2019-12-12 22:53:28 +09:00
Junegunn Choi
f68017d21e [windows/vim] Encode batchfile in current codepage
Backport https://github.com/junegunn/vim-plug/pull/913
2019-12-12 12:29:14 +09:00
Junegunn Choi
2b725a4db5 Defer resetting multi-selection on reload 2019-12-09 21:32:58 +09:00
Junegunn Choi
af1a5f130b Add clear-query and clear-selection
Close #1787
Related #1364
2019-12-07 14:44:24 +09:00
Junegunn Choi
86e3994e87 Properly clear list when --header-lines not filled on reload 2019-12-06 22:34:45 +09:00
Junegunn Choi
1e6ac5590e 'reload' action should be allowed even where there's no match
If the command template doesn't have any placeholder expressions.

    : | fzf --bind 'space:reload:seq 10'
2019-12-06 22:34:30 +09:00
Henré Botha
5e42b1c9f8 [ssh completion] Skip only aliases matching * (#1788)
This commit fixes a bug where lines that declare multiple hostnames get
omitted from completion entirely if one of the hostnames matches *. For
example:

	Host foo.com bar.dev baz.*
2019-12-06 17:58:53 +09:00
Junegunn Choi
9d842630c9 Mention _fzf_setup_completion helper function for bash 2019-12-06 12:03:34 +09:00
David Gray
77cb906dfe [completion] Add support for HostName lines in ~/.ssh/config (#1785)
Close #1783
2019-12-06 10:14:15 +09:00
Junegunn Choi
a59e846f74 Update installation instruction
Close #1707
Close #1779
2019-12-05 23:10:41 +09:00
infokiller
6e6340a0c9 Ignore zcompile output files (*.zwc files) (#1775) 2019-12-05 22:27:30 +09:00
John Purnell
357e82e51b [completion] Ignore hg repos (#1777)
* Update completion.bash
* Update completion.zsh
2019-12-05 22:27:04 +09:00
Junegunn Choi
394d8cfd18 Remove immediate flickering on reload action 2019-12-05 22:27:18 +09:00
Junegunn Choi
ef80bd401f Update installation instruction using Linux package managers
Added NixOS instruction. Close #1731
2019-12-01 23:48:48 +09:00
midchildan
f51d61d57a [zsh] Prevent the current directory from appearing as ~dir in prompts (#1774)
The zsh version of the cd widget sets the variable `dir` to the path of
the target directory before invoking `cd`. This causes zsh to treat the
target directory as a named directory, which has the effect of zsh
substituting '%~' with '~dir' instead of the proper path when it
performs prompt expansion.

This commit will cause the widget to unset `dir` before redrawing the
prompt to fix this issue.

Details of zsh prompt expansion can be found in:
http://zsh.sourceforge.net/Doc/Release/Prompt-Expansion.html
2019-12-01 23:20:26 +09:00
Junegunn Choi
1dd256a68a Update README-VIM 2019-11-29 18:32:49 +09:00
Junegunn Choi
85644aa3fb Revamp README-VIM.md 2019-11-23 01:54:52 +09:00
Junegunn Choi
effbc258bb Update CHANGELOG 2019-11-21 23:06:14 +09:00
Junegunn Choi
e615600ff1 Allow action composition over multiple --bind
# Note + prefix in the second bind expression
  fzf --bind u:up --bind u:+up
  fzf --bind u:up+up
2019-11-21 23:06:14 +09:00
Junegunn Choi
60465c4664 Fix parse error of --bind expression 2019-11-21 23:06:13 +09:00
Jan Edmund Lazo
c03c058bd5 [install] Support busybox uname on Windows (#1758) 2019-11-16 22:23:42 +09:00
Junegunn Choi
7238c8944d Update CHANGELOG 2019-11-16 01:23:01 +09:00
Junegunn Choi
9a41fd5327 0.19.0 2019-11-15 22:54:55 +09:00
Junegunn Choi
b471042037 Merge branch 'devel' 2019-11-15 22:53:08 +09:00
Junegunn Choi
2886f06977 Fix --preview-window noborder with non-default background color 2019-11-15 18:27:08 +09:00
Junegunn Choi
d630484eeb Update error message for --preview-window 2019-11-15 17:20:47 +09:00
Junegunn Choi
e24299239e Add --preview-window noborder option to disable preview border
Close #1699
2019-11-15 11:39:51 +09:00
Junegunn Choi
d2fa470165 Add --info=STYLE [default|inline|hidden]
Close #1738
2019-11-15 00:39:29 +09:00
Junegunn Choi
168453da71 More key chords for --bind
Close #1752
2019-11-14 22:39:35 +09:00
Junegunn Choi
23a06d63ac Update CHANGELOG and man pages 2019-11-13 01:27:39 +09:00
Junegunn Choi
751aa1944a Remove trailing whitespaces when using --with-nth 2019-11-12 23:20:09 +09:00
Junegunn Choi
05b5f3f845 'reload' action should reset multi-selection 2019-11-12 00:57:19 +09:00
Marco Hinz
16fc6862a8 [nvim] Handle SIGHUP in exit handler (#1749)
In recent Nvim versions, an "Error running ..." message is shown even for normal
use cases, such as:

    :Files
    <c-\><c-n>
    :close

Closing the window will :bwipeout! the terminal buffer, because fzf sets
bufhiden=wipe.

When deleting the terminal buffer while fzf is still running, Nvim sends SIGHUP.
This happens for quite some time already, but the bug only manifests since this
commit:

  https://github.com/neovim/neovim/commit/939d9053b

It's The Right Thing to do when the application exited due to a signal.

Before that commit, no "Error running ..." message was shown, because 1 (instead
of 128 + 1 == SIGHUP) was returned which the exit handler in fzf.vim treats as
"NO MATCH".
2019-11-12 00:47:05 +09:00
Junegunn Choi
7e1c0f39e7 'reload' action should reset --header-lines 2019-11-12 00:10:24 +09:00
Junegunn Choi
deccf20a35 Fix regression of select-all 2019-11-11 23:31:31 +09:00
Junegunn Choi
73c0a645e0 Remove unnecessary reader barrier on --filter mode 2019-11-11 12:53:03 +09:00
Junegunn Choi
e975bd0c8d Add test cases for --phony and reload action 2019-11-10 13:13:45 +09:00
Junegunn Choi
78da928727 Experimental implementation of "reload" action
# Reload input list with different sources
  seq 10 | fzf --bind 'ctrl-a:reload(seq 100),ctrl-b:reload(seq 1000)'

  # Reload as you type
  seq 10 | fzf --bind 'change:reload:seq {q}' --phony

  # Integration with ripgrep
  RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
  INITIAL_QUERY=""
  FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \
    fzf --bind "change:reload:$RG_PREFIX {q} || true" \
        --ansi --phony --query "$INITIAL_QUERY"

Close #751
Close #965
Close #974
Close #1736
Related #1723
2019-11-10 11:43:37 +09:00
Junegunn Choi
11962dabba Add --phony option for disabling search
With --phony, fzf becomes a simply selector interface without its own
search functionality. The query string is only used for building the
command for preview or execute action.

Close #1723
2019-11-10 11:36:55 +09:00
Junegunn Choi
dceb5d09cd RTFM, please 2019-11-08 14:09:49 +09:00
Alexandr
b4cccf23d4 Improvements to code quality and readability (#1737)
* Remove 1 unused field and 3 unused functions

unused elements fount by running
golangci-lint run --disable-all --enable unused

src/result.go:19:2: field `index` is unused (unused)
        index  int32
        ^
src/tui/light.go:716:23: func `(*LightWindow).stderr` is unused (unused)
func (w *LightWindow) stderr(str string) {
                      ^
src/terminal.go:1015:6: func `numLinesMax` is unused (unused)
func numLinesMax(str string, max int) int {
     ^
src/tui/tui.go:167:20: func `ColorPair.is24` is unused (unused)
func (p ColorPair) is24() bool {
                   ^

* Address warnings from "gosimple" linter

src/options.go:389:83: S1003: should use strings.Contains(str, ",,,") instead (gosimple)
        if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Index(str, ",,,") >= 0 {
                                                                                         ^
src/options.go:630:18: S1007: should use raw string (`...`) with regexp.MustCompile to avoid having to escape twice (gosimple)
        executeRegexp = regexp.MustCompile(
                        ^
src/terminal.go:29:16: S1007: should use raw string (`...`) with regexp.MustCompile to avoid having to escape twice (gosimple)
        placeholder = regexp.MustCompile("\\\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\\+?f?nf?})")
                      ^
src/terminal_test.go:92:10: S1007: should use raw string (`...`) with regexp.MustCompile to avoid having to escape twice (gosimple)
        regex = regexp.MustCompile("\\w+")
                ^

* Address warnings from "staticcheck" linter

src/algo/algo.go:374:2: SA4006: this value of `offset32` is never used (staticcheck)
        offset32, T := alloc32(offset32, slab, N)
        ^
src/algo/algo.go:456:2: SA4006: this value of `offset16` is never used (staticcheck)
        offset16, C := alloc16(offset16, slab, width*M)
        ^
src/tui/tui.go:119:2: SA9004: only the first constant in this group has an explicit type (staticcheck)
        colUndefined Color = -2
        ^
2019-11-05 09:46:51 +09:00
zhaoyunfeng
b911af200c [zsh-completion] Fix prefix extraction when triggers start with ';' 2019-11-03 00:33:30 +09:00
Junegunn Choi
68683c444f Fix argument parser for -m
/cc @tessus
2019-11-02 20:44:21 +09:00
Junegunn Choi
a185593d65 Remove unnecessary map lookup 2019-11-02 19:55:05 +09:00
Junegunn Choi
525040238e Fix behavior of 'deselect-all' to only deselect matches
To make it consistent with select-all and toggle-all.

Close #1364
2019-11-02 19:41:59 +09:00
Junegunn Choi
33f89a08f3 Build with Go 1.13 2019-11-02 14:59:22 +09:00
Junegunn Choi
11645e1fac Fix flaky test case 2019-11-02 14:55:13 +09:00
Junegunn Choi
6390140539 [vim/windows] Use chcp only if sed is in PATH
https://github.com/junegunn/vim-plug/pull/891
2019-11-02 14:35:21 +09:00
Junegunn Choi
072066c49c --multi to take optional argument to limit the number of selection
Close #1718
Related #688
2019-11-02 14:25:12 +09:00
Junegunn Choi
a2e9366c84 Fix flaky test case 2019-11-02 13:15:01 +09:00
Simon Fraser
391669a451 Add 'f' flag for placeholder expression (#1733)
If present the contents of the selection will be placed in a temporary file,
and the filename will be placed into the string instead.
2019-10-27 23:50:12 +09:00
Junegunn Choi
0c6c76e081 [zsh] Suppress global alias expansion in widget functions
Close #1708
2019-10-17 14:51:57 +09:00
stiletto
f1520bdde6 Support building on machines with uname -m == "aarch64" (#1710) 2019-10-11 22:11:06 +09:00
Junegunn Choi
3089880f18 [vim/windows] Fix chcp parsing for the current codepage
https://github.com/junegunn/vim-plug/pull/888
2019-10-08 09:41:22 +09:00
Junegunn Choi
ab11b74be4 [vim] Output of chcp was not parsed correctly
By @gh4w and @janlazo

See 68b31a4a66
2019-09-29 14:53:45 +09:00
Junegunn Choi
a5a97be017 [bash-completion] Properly handle exit event
Related #1704
2019-09-29 14:45:58 +09:00
Junegunn Choi
80b5bc1b68 [vim] Shell-escape --color option generated by fzf#wrap
Fix https://github.com/junegunn/fzf.vim/issues/855
2019-09-09 12:12:42 +09:00
Junegunn Choi
5c7dcaffe8 [bash-completion] _fzf_setup_completion to retain previous options 2019-08-09 11:11:29 +09:00
Junegunn Choi
5095899245 [bash-completion] Add _fzf_setup_completion to enable fuzzy completion
While we can attach `_fzf_path_completion` or `_fzf_dir_completion` to
any command using the standard bash complete command, the functionality
of the existing completion function is lost.

Use _fzf_setup_completion if you want to extend the existing function
with fuzzy completion instead of completely replacing it.

e.g. _fzf_setup_completion path kubectl
2019-08-08 15:35:52 +09:00
Ross Smith II
4800e5d2ae Add scoop mention (#1646) 2019-08-06 14:11:28 +09:00
Junegunn Choi
3b1e37f718 Fix #1657: alt-0 to alt-9 2019-08-06 14:06:58 +09:00
Christian Muehlhaeuser
6577388250 os.Kill signal cannot be trapped (#1641) 2019-07-19 13:24:46 +09:00
Christian Muehlhaeuser
3b9dbd4146 Code cleanup: remove unnecessary string conversions (#1642) 2019-07-19 13:23:18 +09:00
Christian Muehlhaeuser
a1260feeed Code cleanup (#1640)
- Replaced time.Now().Sub() with time.Since()
- Replaced unnecessary string/byte slice conversions
- Removed obsolete return and value assignment in range loop
2019-07-19 13:22:35 +09:00
Dan Čermák
7322504ad0 Add installation instructions for openSUSE (#1631) 2019-07-17 11:19:13 +09:00
miclill
de569f0052 Add Debian install instructions (#1620) 2019-07-17 11:18:07 +09:00
ssjhv
e7097a9d25 [fish] Remove perl from fish key bindings (#1635)
Perl was used to remove the trailing newline character, but fzf already
has --print0 to use null character as terminators, and fish read -z is
expecting null character as terminators. There is no reason to depend on
perl if --print0 is passed to fzf invocation.
2019-07-13 14:47:51 +09:00
charlton1
c1dbc800e5 [vim] Fix name-based colors for GVim/8.0 w/o builtin terminal (#1634)
(i.e. spawn xterm)
2019-07-09 11:08:36 +09:00
Junegunn Choi
951746297e Fix invalid layout example 2019-06-08 23:29:04 +09:00
Junegunn Choi
984304568d Remove outdated GVim instruction
The section is no longer relevant since (G)Vim 8 or above has builtin
terminal emulator.
2019-06-08 23:22:05 +09:00
Junegunn Choi
723217bdea Add fzf#run tutorial to README-VIM.md 2019-06-08 23:17:30 +09:00
Mateusz Piotrowski
0fdb71f7e4 Add FreeBSD installation instructions (#1569) 2019-06-05 09:26:04 +09:00
Junegunn Choi
12ce76b56a [bash] Make sure to execute builtin history
Fix #1592
2019-06-03 18:46:01 +09:00
Michael Kelley
0030d18448 Update sys module to newer version (#1582)
A newer version of the sys module is needed for pull request #1341
2019-05-26 11:06:46 +09:00
Junegunn Choi
0e3e6ac442 Disallow preview scroll when the content just fits the window 2019-05-21 16:35:45 +09:00
Dominik Reller
430e8193e0 [install] Remove unused variable in install script (#1571) 2019-05-07 11:34:55 +09:00
Jesus Briales
03e8ed4d88 [bash-completion] Fix custom completion with dynamic loader enabled for non-standard command names (#1564)
Related to #1170.

Fix the solution for commands with non-standard names
where `$cmd` and `$orig_cmd` differ. e.g. `s.foo` -> `s_foo`
2019-05-01 02:35:51 +09:00
Junegunn Choi
ef492f6178 Output --help message to standard output
Close #1554
2019-04-21 18:02:34 +09:00
Alexey Samoshkin
8eea45ef50 Add demo screencast video to README (#1557) 2019-04-18 18:49:30 +09:00
Junegunn Choi
ff951341c9 0.18.0 2019-03-31 11:22:38 +09:00
Junegunn Choi
df570afd52 [docker] Fix gem install option in Dockerfile 2019-03-31 11:22:12 +09:00
Junegunn Choi
07d755df11 Fix regression of prompt display 2019-03-30 02:07:09 +09:00
Junegunn Choi
37585bd5a5 Disable preview scroll if the content fits on the screen
Close #1540
2019-03-29 18:28:45 +09:00
Junegunn Choi
89e24bf8f2 Fix ineffective break statement 2019-03-29 17:29:24 +09:00
Junegunn Choi
8d2fcd3518 Avoid unnecessary redraw of the preview window 2019-03-29 15:12:46 +09:00
Junegunn Choi
f39ab3875e Redraw prompt only when necessary 2019-03-29 15:02:31 +09:00
AnakinXL
82efe6c60d [doc] Add bat for preview syntax highlighting example (#1538)
Similar to this PR from fzf.vim:
https://github.com/junegunn/fzf.vim/pull/712
2019-03-29 10:31:17 +09:00
Junegunn Choi
75972d59a8 Add --no-unicode option to draw borders in ASCII characters
Close ##1533
2019-03-29 02:11:03 +09:00
Junegunn Choi
e7d60aac9c [vim] Do not restore cwd when autochdir is set and buffer changed
Close #1539
2019-03-28 13:57:04 +09:00
Junegunn Choi
a0bfbdd49c [vim] Increase window height by 2 when --border is set
Close #1535
2019-03-26 16:42:35 +09:00
Junegunn Choi
ba594982f0 Add MacPorts instruction
Close #1521
2019-03-18 18:45:57 +09:00
Junegunn Choi
2157f4f193 Add color option for gutter
fzf --color gutter:-1

Close #1529
Close #1468
2019-03-17 15:52:38 +09:00
Junegunn Choi
309bae423c [zsh-completion] Suppress "no matches found" message 2019-03-14 01:10:51 +09:00
Junegunn Choi
4f8bf2ae78 [install] Avoid generating empty component in $PATH
Fix #1517
2019-03-08 12:38:36 +09:00
Junegunn Choi
85c1f8a9e0 Always prepend ANSI reset code before re-assembling tokens 2019-03-07 15:29:57 +09:00
Junegunn Choi
e00e7e1e56 Remove unnecessary ANSI code injection 2019-03-07 02:02:02 +09:00
Junegunn Choi
1a6defdbcc Use simple string concatenation instead of using fmt.Sprintf 2019-03-07 02:00:31 +09:00
Junegunn Choi
ef577a6509 Preserve the original color of each token when using --with-nth with --ansi
Close #1500
2019-03-06 19:05:05 +09:00
Junegunn Choi
b7c6838e45 [install] Fix symlink log
Related #1466
2019-03-05 14:45:29 +09:00
Junegunn Choi
91d04cec5c [install] Print better error message when fzf --version failed
Related #1466
2019-03-05 14:44:29 +09:00
Rui Coelho
3bd8441079 [completion] Look up on ~/.ssh/config.d/* files when doing ssh host complete (#1420) 2019-02-28 16:40:41 +09:00
Junegunn Choi
8cf45a5197 [shell] Skip loading completion code on non-interactive shell
This change is not required if you use the install script to generate
~/.fzf.bash or ~/.fzf.zsh which already has the proper guard statement.

Close #1474
2019-02-28 16:13:59 +09:00
Junegunn Choi
8dc1377efb Export FZF_PREVIEW_LINES and FZF_PREVIEW_COLUMNS to preview process
fzf will still override LINES and COLUMNS as before but they may not
hold the correct values depending on the default shell.

Close #1314
2019-02-22 14:36:30 +09:00
Junegunn Choi
6c32148f90 Add placeholder expression for zero-based item index: {n} and {+n}
Close #1482
2019-02-19 01:12:57 +09:00
Junegunn Choi
315e568de0 Update build instruction
Close #1485
2019-01-31 18:43:10 +09:00
Junegunn Choi
5d16b28869 Fix tab width after ANSI reset code in preview window
Close #1423
2018-12-22 11:52:18 +09:00
Junegunn Choi
5624a89231 Inverse-only matches should not reorder the remaining results
Fix #1458
2018-12-19 23:05:29 +09:00
Junegunn Choi
63c42b14f2 Remove trailing spaces in Makefile 2018-12-13 15:17:30 +09:00
Stefan Tatschner
6f1eaa9b39 Use go modules and simplify build (#1444)
* Update .travis.yml and use stages

This updates the .travis.yml configuration to use separate stages for
unittests and CLI tests. The output is now clearer, since for unittests
and CLI tests separate web pages are available.

* Use go modules and simplify build
2018-12-13 14:36:15 +09:00
Junegunn Choi
ca42e5e00a Avoid unnecessary redraw of preview window
Close #1455
2018-12-13 10:58:57 +09:00
Junegunn Choi
61feee690c Render preview window when the initial query fails to match
Only if preview template contains {q}

Fix #1452
Related #1307
2018-12-05 18:45:55 +09:00
Christian Muehlhaeuser
d4ed955aee Typo & grammar fixes in README (#1413) 2018-11-09 16:50:16 +09:00
Junegunn Choi
b46227dcb6 0.17.5 2018-10-07 01:46:29 +09:00
Paul Frybarger
fd8d371ac7 [zsh] Fix multiline prompt issue with 'zle reset-prompt' (#1397)
Close #867 
Close #1256
2018-10-05 10:56:26 +09:00
Junegunn Choi
0e06e298d4 [man] Document that FZF_DEFAULT_COMMAND should be POSIX-compliant
Close #1379
2018-09-30 22:20:46 +09:00
Junegunn Choi
72df905902 Do not wait for more keystrokes after double escape characters
Close #1393
2018-09-28 10:33:53 +09:00
Junegunn Choi
0d748a0699 Kill running preview process after 500ms when focus has changed
Close #1383
Close #1384
2018-09-28 10:33:52 +09:00
Junegunn Choi
27c40dc6b0 Restore STDIN during execute-silent
This allows users to terminate the process with CTRL-C when it hangs.
2018-09-27 15:54:13 +09:00
Junegunn Choi
8e34e6fbb4 [install] Escape spaces in installation directory
Close #1390
2018-09-27 10:15:22 +09:00
Junegunn Choi
3bc98ed623 Add link to related projects page 2018-09-27 02:52:52 +09:00
Tim Cuthbertson
70a92a858a Don't drop buffered input data in findOffset() (#1392) 2018-09-27 02:35:44 +09:00
Jan Edmund Lazo
49d04374a4 [install] Detect MSYS on Windows (#1391) 2018-09-25 23:03:36 +09:00
Junegunn Choi
8540902a35 Add link to git key bindings gist 2018-09-04 12:24:46 +09:00
Junegunn Choi
8c6fcee3ca [vim] Fix directory switching around sink function
Close #1356

Related:
- #612
- https://github.com/junegunn/fzf.vim/issues/308
2018-08-20 15:31:41 +09:00
Junegunn Choi
13803d0dbb [vim] Clear temporary window-local working directory
Close #1085
Close #1086
Close https://github.com/junegunn/fzf.vim/issues/678
2018-08-10 18:24:18 +09:00
Michael Kelley
423986996a Handle incomplete ESC sequence in typeahead buffer (#1350)
If an ESC char is found while processing characters,
continue to check for characters. This prevents fzf from
prematurely exiting.

Close #1349
2018-08-08 15:43:55 +09:00
Younes Manton
1c9e7b7ea6 Update Makefile to build ppc64le binary (#1326)
* Add ppc64le support to Makefile

* Update crypt libs to fix tty ioctls on ppc64le

The hardcoded tty ioctl commands in the terminal package were not
correct for ppc64le and caused the ioctls to return ENOTTY for
commands like TCGETS and so on. The bug is fixed in later versions.
2018-07-16 18:55:06 +09:00
Jay
6de1ad9d3d [completion] Filter out non-hostnames in SSH config file (#1329)
* Correctly exclude SSH config options with Host

SSH config files have 14 options containing 'Host'.
Previously The zsh and bash completion scripts would include lines
containing these options when doing command-line completion of SSH hosts
with `ssh **`.

This commit fixes that problem by only including lines with 'host '.

* Don't autocomplete SSH hostnames using ?

SSH config files support ? as well as * for wildcards in Host lines.
This commit excludes lines containing ? for zsh/bash command line
completeion using `ssh **`
2018-07-06 11:29:39 +09:00
Oliver Schrenk
5004ae3457 [fish] Use $version instead of $FISH_VERSION (#1100)
$FISH_VERSION is dropped in 2.7, but every version has $version

- https://github.com/fish-shell/fish-shell/issues/4414
- fb8ae04f80

Comment from @faho in #1316:

Unfortunately, $FISH_VERSION was only ever a thing from fish 2.0 to fish 2.7.1.

All fish versions from the very beginning though used a variable called simply "$version" to store their version, so that is the one that should be used.
2018-06-27 19:02:16 +09:00
做梦专业户
e67cc75063 Update Makefile to support armv8l (#1321) 2018-06-27 18:56:02 +09:00
Junegunn Choi
0edbcbdf19 Allow search query longer than the screen width
By implementing horizontal scrolling of the prompt line.
Maximum length is hard-coded to 300-chars.

Close #1312
Fix #1225
2018-06-25 19:07:47 +09:00
Junegunn Choi
f0fe79dd3b 0.17.4 2018-06-10 10:35:52 +09:00
Akinori MUSHA
daa1958f86 Provide an option to reverse items only (#1267) 2018-06-10 01:41:50 +09:00
Junegunn Choi
2c26f02f5c Improve preview window update events
- Update preview window even if there is no match for the query string
  if any of the placeholder expressions evaluates to a non-empty string.
- Also, if the command template contains {q}, preview window will be
  updated if the query string changes even though the focus remains on
  the same item.

An example:

    git log --oneline --color=always |
       fzf --reverse --ansi --preview \
       '[ -n {1} ] && git show --color=always {1} || git show --color=always {q}'

Close #1307
2018-06-10 01:40:22 +09:00
Junegunn Choi
af87650bc4 [docker] Build binary from source 2018-06-08 19:42:29 +09:00
ptzz
2b19c0bc68 [bash/zsh] Fix missing fuzzy completions (#1303)
* [bash/zsh] Fix missing fuzzy completions

`cat foo**<TAB>` did not display the file `foobar` if there was a directory
named `foo`.

Fixes #1301

* [zsh] Evaluate completion prefix

  cat $HOME**
  cat ~username**
  cat ~username/foo**
2018-06-02 10:40:33 +09:00
Junegunn Choi
76a2dcb5a9 Add Dockerfile for running tests
make docker
make docker-test
2018-06-01 18:23:25 +09:00
Junegunn Choi
68ec3d1c10 Fix flaky test cases 2018-06-01 18:21:34 +09:00
Mark
2ff19084ca [install] Support for XDG Base Directory Specification (#1282)
Add --xdg option which makes the installer generate files under $XDG_CONFIG_HOME/fzf.
2018-06-01 11:54:58 +09:00
Daniel Gray
62f062ecfa Remove -y flag from Arch Linux installation (#1290)
https://wiki.archlinux.org/index.php/Partial_upgrades#Partial_upgrades_are_unsupported

You should never `pacman -Sy <pkg>`, Arch users are expected
to keep their system already up-to-date before installing anything.
2018-05-14 17:24:32 +09:00
Jan Edmund Lazo
cce17ad0a0 [vim] Use CRLF in batchfile for multibyte codepage (#1289)
Close #1288
2018-05-13 16:24:28 +09:00
Junegunn Choi
b8296a91b9 Clarify Vim plugin instruction
Close #1251

@amaravora
2018-05-04 16:01:42 +09:00
Junegunn Choi
6e9452b06e Add Arch Linux installation instruction
Close #1273

@codingCoffee
2018-05-04 16:00:40 +09:00
Junegunn Choi
888fd35689 [fzf-tmux] Avoid unnecessary recovery of window options
fzf-tmux temporarily turns off remain-on-exit and synchronize-panes
options. We don't have to try to restore the values of the options if
they were already turned off when fzf-tmux was started.
2018-05-04 15:38:27 +09:00
ptzz
1fb0fbca58 [bash] Do not print error when falling back to default completion (#1279)
Fixes #1278
2018-05-04 14:55:48 +09:00
Heinrich Kruger
ddd2a109e4 [fzf-tmux] Restore tmux window options (#1272)
Restore the original values of 'remain-on-exit' and 'synchronize-panes'
options when exiting 'fzf-tmux'.
2018-05-04 04:25:15 +09:00
Junegunn Choi
87504a528e [bash] Fix infinite loop on tab completion
awk may not set OFS to match FS depending on the implementation.

Close #1227
2018-04-30 12:58:10 +09:00
Junegunn Choi
6eac4af7db [vim] Ignore Vim:Interrupt when "Abort" selected on E325
Close #1268
2018-04-26 10:23:18 +09:00
Junegunn Choi
89de1340af [bash] Add --sync to the default CTRL-R options
This compensates the use of --tac. fzf will not render on the screen
until the complete list of commands are loaded.
2018-04-25 18:47:56 +09:00
Junegunn Choi
9e753a0d44 Implement ttyname() in case /dev/tty is not available
Close #1266
Close #447
2018-04-25 17:50:47 +09:00
Junegunn Choi
f57920ad90 Do not print non-displayable characters
fzf used to print non-displayable characters (ascii code < 32) as '?',
but we will simply ignore those characters with this patch, just like
our terminals do.

\n and \r are exceptions. They will be printed as a space character.

TODO: \H should delete the preceding character, but this is not implemented.

Related: #1253
2018-04-12 17:49:52 +09:00
Junegunn Choi
7dbbbef51a Add support for alt-{up,down,left,right} keys
Close #1234
2018-04-12 17:42:48 +09:00
Avindra Goolcharan
7add75126d ZSH and Bash completion: remove shebang (#1248)
Shebangs are only for files that are directly executable. In cases
where files are only sourced (such as completion scripts), these
are unneeded.
2018-04-12 17:21:56 +09:00
Akinori MUSHA
d207672bd5 Parse the output of go version to get the value of GOOS (#1260) 2018-04-12 17:09:08 +09:00
Robert Orzanna
851fa38251 Add reference to Fedora package documentation (#1255) 2018-04-06 14:10:10 +09:00
ZDNoFYVe
43345fb642 Implement flag for preserving whitespace around field (#1242) 2018-03-30 11:47:46 +09:00
xalexalex
9ff33814ea Fix typo in README (#1243) 2018-03-27 17:53:20 +09:00
Ryan Boehning
21b94d2de5 Make fzf pass go vet
Add String() methods to types, so they can be printed with %s. Change
some %s format specifiers to %v, when the default string representation
is good enough. In Go 1.10, `go test` triggers a parallel `go vet`. So
this also makes fzf pass `go test`.

Close #1236
Close #1219
2018-03-13 14:56:55 +09:00
Jesse Leite
24236860c8 Document inverse prefix exact match search syntax (#1224)
* Document inverse prefix exact match search syntax.

* Reorder search syntax table to explain basic exact match first.
2018-03-06 16:33:33 +09:00
Junegunn Choi
3f868fd792 [bash] Fix CTRL-R to preserve the latest yank
Close #1216

1. Append a single space so that step 3 won't fail
2. CTRL-E to move to the end of the line
3. CTRL-U to delete the whole line before the cursor
4. CTRL-Y to paste the deleted line
5. ESC+Y to rotate the kill ring and bring back the previous yank before step 3
6. CTRL-U to delete the whole line again
7. Paste `__fzf_history__`
8. ESC+CTRL-E to expand the command substitution
9. ESC+R to redraw the line
10. ESC+^ to expand the history entry (!NUMBER)
2018-02-16 21:55:23 +09:00
Junegunn Choi
417bca03df Add shift-up and shift-down
For now, they are respectively bound to preview-up and preview-down
by default (TBD).

Not available on tcell build.

Close #1201
2018-02-15 19:57:21 +09:00
Junegunn Choi
cce6aef557 [bash] Fix extra space issue of dynamic completion with 'nospace'
Close #1203
2018-02-15 18:21:02 +09:00
Junegunn Choi
eb3afc03b5 [vim] Make list options compatible with layout options
Fix #1205
2018-01-26 13:48:05 +09:00
Jan Edmund Lazo
7f0caf0683 Update Windows default command to print relative paths (#1200) 2018-01-17 22:02:50 +09:00
Pierre P
7f606665cb [install] Make default answer "y" (#1195) 2018-01-14 03:19:33 +09:00
Junegunn Choi
202872c2dc Remove PayPal donation button
I've decided not to take more donations.

Thanks to everyone who has supported my projects.

Edgar Hipp
Eyal Levin
Philip Stewart
James O'Beirne
Minh Triet Ly
Victor Alvarez
Max Hung
Gearoid Murphy
Aaron Taylor
Brett Bender
Phil Thompson
Anders Damsgaard
2018-01-06 02:47:42 +09:00
Junegunn Choi
93aeae1985 [bash] Trigger redraw-current-line before history-expand-line
Close #681
2017-12-07 23:33:01 +09:00
Junegunn Choi
5c34ab6692 [vim] Fix terminal buffer cleanup on Vim 8
Close #1172
2017-12-05 23:50:55 +09:00
Junegunn Choi
390b49653b 0.17.3 2017-12-03 23:55:24 +09:00
Junegunn Choi
b877c385f0 Fix assertions in test_dynamic_completion_loader 2017-12-03 23:54:58 +09:00
Junegunn Choi
9c47739c0e Fix panic when replace-query is triggered on empty result set 2017-12-03 23:48:59 +09:00
Junegunn Choi
04aa2992e7 Revert "0.17.2"
This reverts commit 2f1edeff78.
2017-12-03 23:42:38 +09:00
Junegunn Choi
2f1edeff78 0.17.2 2017-12-03 23:34:37 +09:00
Junegunn Choi
306d51cdcf Update tcell to fix double-enter problem on Windows GVim
- Close #1169
- https://github.com/gdamore/tcell/pull/159
2017-12-03 23:32:45 +09:00
Junegunn Choi
54a026525a [vim] Remove unnecessary term_wait workaround
The issue is fixed in 1232624ae5
2017-12-03 23:32:43 +09:00
Junegunn Choi
d6588fc835 [bash-completion] Fix custom completion with dynamic loader enabled
After _completion_loader is called, instead of loading the entire
completion.bash file, just restore the fzf completion for the current
command. `_fzf_orig_completion_$cmd` is only set if _completion_loader
actually changed the completion options to avoid infinite loop.

Close #1170
2017-12-03 23:32:41 +09:00
Junegunn Choi
5a7b41a2cf Add accept-non-empty action
'accept-non-empty' is similar to 'accept' (which is bound to 'enter' and
'double-click' by default) but it prevents fzf from exiting without any
selection.

Close #1162
2017-12-02 02:28:36 +09:00
Junegunn Choi
338a73d764 [man] Describe 'cancel' action 2017-12-01 19:13:51 +09:00
Junegunn Choi
c20954f020 Add replace-query action
replace-query action replaces the query string with the current
selection. If the selection is too long, it will be truncated.

If the line contains meta-characters of fzf search syntax, it is
possible that the line is no longer included in the updated result.

e.g.

  echo '!hello' | fzf --bind ctrl-v:replace-query

Close #1137
2017-12-01 13:08:55 +09:00
Junegunn Choi
1e8e1d3c9d Fix test case on older versions of Ruby 2017-12-01 13:03:02 +09:00
Junegunn Choi
f6b1962056 Inject $LINES and $COLUMNS when running preview command
Close #1168
2017-12-01 03:28:10 +09:00
Junegunn Choi
b3b101a89c Support binding of left-click and right-click
left-click and right-click are respectively bound to "ignore" and
"toggle" (after implicitly moving the cursor) by default.

Close #1130
2017-12-01 03:28:08 +09:00
Junegunn Choi
9615c4edf1 Fix test case for invalid FZF_DEFAULT_COMMAND 2017-12-01 02:22:36 +09:00
Junegunn Choi
85a75ee035 Revert default command: find with -fstype required
In #1061 we changed the default command to retry with a simpler find
command with fewer arguments if the first find command failed. This was
to support stripped-down verions of find that do not support -fstype
argument.

However, this caused an unwanted side-effect of yielding duplicate
entries when the first command failed after producing some lines.

We revert the change in this commit, so the default command will not
work with find without -fstype support. But we now print better error
message in that case so that the user can set up a working
$FZF_DEFAULT_COMMAND.

Close #1120 #1167
2017-12-01 01:40:42 +09:00
Junegunn Choi
1e5bd55672 [install] Change the order of case patterns for $archi (#1060)
/cc @ehandal
2017-11-27 15:44:19 +09:00
Jan Edmund Lazo
37d4015d56 [vim] Don't use :terminal on msys2 or Cygwin (#1155)
Close #1152

msys2 terminal Vim assumes that it runs in mintty
so `:terminal` uses `TERM=xterm`.
fzf doesn't support `TERM=xterm` on Windows.
2017-11-22 13:34:02 +09:00
Junegunn Choi
6b27554cdb Clarify installation instructions 2017-11-22 03:10:20 +09:00
Junegunn Choi
fc1b119159 [vim] Add instruction to hide statusline of terminal buffer (#1143) 2017-11-19 12:07:54 +09:00
Aaron Jensen
2cd0d4a9f7 [zsh] Fire zsh precmd functions after cd (#1136)
Fixes #915
2017-11-14 12:43:52 +09:00
Elliott Sales de Andrade
fd03aabeb2 Add Fedora installation information (#1141) 2017-11-14 12:40:17 +09:00
Justin Toniazzo
8068c975c2 Fix broken link in readme TOC (#1131)
The `Respecting .gitignore` link pointed to a section of the readme which no longer exists.
2017-11-08 23:54:46 +09:00
Junegunn Choi
a6d2ab3360 Update README: Examples using fd
- https://github.com/sharkdp/fd
- https://mike.place/2017/fzf-fd/

/cc @williamsmj
2017-10-29 23:48:55 +09:00
Adam Dinwoodie
fe7b91dfd9 Add bin/fzf.exe to .gitignore (#1111)
On Cygwin and MinGW, the fzf binary will have a .exe extension, so
ignore that binary if it exists as well as the bare binary.
2017-10-27 09:12:12 +09:00
Junegunn Choi
5784101bea Suggest ripgrep instead of the silver searcher
Since https://github.com/BurntSushi/ripgrep/issues/200 is fixed in
0.7.1, we can safely suggest ripgrep as the candidate generator as it
has a more precise implementation of gitignore filtering than the silver
searcher.
2017-10-23 13:19:11 +09:00
Igor Urazov
eaf6eb8978 [completion] Ensure ps called as command (#1098)
When `ps` is aliased for something uncommon, like `alias ps=grc ps` which colorizes ps output, the output of `ps` can be unexpected and/or undesired.

This change makes ps to be always executed as command, even if it's aliased.
2017-10-21 10:31:34 +09:00
Daniel Schaffrath
3af63bcf1f [zsh] Use fc -r instead of fzf --tac to speed up loadtime (#1097)
Reference: http://zsh.sourceforge.net/Doc/Release/Shell-Builtin-Commands.html

> The flag -r reverses the order of the events
2017-10-20 12:56:02 +09:00
Andrey Chernih
80a21f7a75 [completion] Fix known_hosts completion for custom port number (#1092)
Handles records like "[20.20.7.168]:9722 ssh-rsa ..."

This is a standard format for servers running on custom port according to http://man.openbsd.org/sshd.8#SSH_KNOWN_HOSTS_FILE_FORMAT

    A hostname or address may optionally be enclosed within ‘[’ and ‘]’
    brackets then followed by ‘:’ and a non-standard port number.
2017-10-19 22:04:32 +09:00
Junegunn Choi
0b33dc6ce1 0.17.1 2017-10-16 01:58:57 +09:00
Junegunn Choi
64a6ced62e Do not immediately check --height option on Windows (#1082) 2017-10-15 19:02:05 +09:00
Junegunn Choi
438f6c96cd Fix compilation error of Windows binary 2017-10-15 18:32:59 +09:00
Junegunn Choi
6ae085f974 Add link to Windows wiki page
Related: #1072
/cc @janlazo
2017-10-15 17:44:25 +09:00
Junegunn Choi
cb8e97274e Update README to add an example of _fzf_compgen_dir
/cc @chrisjohnson

Close #1067
Close #1083
2017-10-14 16:18:46 +09:00
Jan Edmund Lazo
c4185e81e8 Fix ExecCommandWith for cmd.exe in Windows (#1072)
Close #1018

Run the command as is in cmd.exe with no parsing and escaping.
Explicity set cmd.SysProcAttr so execCommand does not escape the command.
Technically, the command should be escaped with ^ for special characters,
including ". This allows cmd.exe commands to be chained together.

See https://github.com/neovim/neovim/pull/7343#issuecomment-333350201

This commit also updates quoteEntry to use strings.Replace instead of
strconv.Quote which escapes more than \ and ".
2017-10-14 15:26:37 +09:00
Ionel Cristian Mărieș
0580fe9046 Don't do shell quoting for weird chars (#1079)
* Don't do shell quoting for weird chars

This would prevent tabs from being escaped as `$'\t'` (definitely not what I would want to see as initial value in the search).

* Do different escape.
2017-10-10 12:27:01 +09:00
Junegunn Choi
1b1bc9ea36 [install] Download arm8 binaries on Linux aarch64
Close #1060
2017-10-08 03:52:55 +09:00
Junegunn Choi
c2614467cf [neovim] Fix Neovim plugin to use terminal instead of --height
Close #1066
Close #1068
2017-09-30 22:13:43 +09:00
Junegunn Choi
077ae51f05 [vim] Use Vim 8 terminal when appropriate
Close #1055
2017-09-29 01:11:00 +09:00
Junegunn Choi
ee40212e97 Update FZF_DEFAULT_COMMAND
- Use bash for `set -o pipefail`
- Fall back to simpler find command when the original command failed

Related: #1061
2017-09-28 23:05:02 +09:00
Ricardo González
7f5f6efbac [fzf-tmux] Executes fzf from fzf-tmux with a process name (#1056) 2017-09-28 22:23:44 +09:00
Josh Pencheon
45d4c57d91 [completion] Include host aliases in ssh completion (#1062) 2017-09-27 00:18:01 +09:00
Robert Orzanna
41e0208335 Update Homebrew/Linuxbrew instructions (#1052) 2017-09-17 17:12:20 +09:00
Lawrence Wu
2f8238342b [install] Don't touch dotfiles if not requested (#1048) 2017-09-11 09:18:26 +09:00
Junegunn Choi
e1582b8323 Clean up renderer code
Remove code that is no longer relevant after the removal of ncurses
renderer. This commit also fixes background color issue on tcell-based
FullscreenRenderer (Windows).
2017-09-09 23:20:42 +09:00
Junegunn Choi
7cfa6f0265 Fix custom foreground color inside preview window (addendum)
This fixes foreground color inside preview window when the text has ANSI
attributes except for foreground color.

Close #1046
2017-09-08 18:33:17 +09:00
Junegunn Choi
e3973c74e7 Fix custom foreground color inside preview window
Close #1046
2017-09-08 18:18:54 +09:00
Junegunn Choi
a8deca2dd9 [vim] Update README-VIM: fzf can run inside GVim 2017-09-07 12:42:40 +09:00
Junegunn Choi
a78ade1771 Update link to performance chart 2017-09-07 12:38:34 +09:00
Jan Edmund Lazo
79d2ef4616 [vim] Do not pathshorten prompt in cygwin (#1043)
Prevents the following case:
before pathshorten - /usr/bin
after pathshorten - /u/bin
piped to cmd.exe - U:/bin
2017-09-07 11:03:26 +09:00
Junegunn Choi
5edc3f755c [vim] Update FZF command not set up lengthy prompt on narrow screen
Port of e7928d154a

Since :FZF does not enable preview window, we determine based on full
&columns instead of &columns / 2.
2017-09-07 11:01:40 +09:00
Junegunn Choi
288976310b Update g:fzf_colors example 2017-09-06 10:44:25 +09:00
Junegunn Choi
58b5be8ab6 0.17.0-2 2017-09-05 13:40:58 +09:00
Jan Edmund Lazo
26d7896877 [vim] Bind Ctrl-J in Vim terminal to fix enter key
Temporary workaround for non-Windows environment

Reference:
https://github.com/vim/vim/issues/1998
https://github.com/junegunn/fzf/pull/1019#issuecomment-327008348
2017-09-05 13:29:46 +09:00
Jan Edmund Lazo
fd6bc7308f [vim] Use s:execute_term in Windows
IMPORTANT:
cmd.exe and powershell are fine in default Windows terminal.
cmd.exe prompt is broken on ConEmu because it natively supports ucs-2 only.
utf-16 support is exclusive to .Net (ie. powershell).
utf-8 supports requires chcp, external program, but does not fix the cmd.exe prompt.
Use powershell on ConEmu to avoid corrupted text on display
2017-09-05 13:29:46 +09:00
Jan Edmund Lazo
6c41c95f28 [vim] s:execute_term works in GVim on Windows
Requirements:
- compiled with +terminal
- has patch-8.0.995
- has('gui_running') returns 1
2017-09-05 13:29:46 +09:00
Jan Edmund Lazo
446e04469d [neovim] use batchfile for s:execute_term in Windows 2017-09-05 13:29:46 +09:00
Michael Smith
5097e563df [neovim] Fix terminal buffer marker on Windows
Original Patch: a9bf29b65e
2017-09-05 13:29:46 +09:00
Jan Edmund Lazo
c7ad97c641 [neovim] use terminal in Windows for v0.2.1+ 2017-09-05 13:29:46 +09:00
Junegunn Choi
9516fe3324 [install] Add --no-{bash,zsh,fish}
Close #1040
2017-09-03 11:45:22 +09:00
Junegunn Choi
20cdbac8c3 [install] Ignore user-defined grep aliases 2017-09-03 11:38:22 +09:00
Junegunn Choi
e3e7b3360c Delete ncurses implementation 2017-09-02 03:19:50 +09:00
Junegunn Choi
655dfb8328 [fzf-tmux] Remove cat command
Close #1039
2017-09-01 18:46:00 +09:00
Mike Hearn
9b9c67b768 [fzf-tmux] Add pane_height/pane_width fallback (#1037) 2017-09-01 11:16:00 +09:00
Junegunn Choi
5b7457ff08 [install] Wait for a linefeed when asking for confirmation
Close #1035
2017-09-01 02:45:48 +09:00
Junegunn Choi
48adad5454 [neovim] Set &shell to sh (again) after opening a new window
Close #1031
2017-08-30 18:58:28 +09:00
Jack O'Connor
b27dc3eb17 [vim] Add parens around piped source commands (#1029)
Previously a command like `echo a && echo b` would get transformed into
`echo a && echo b | fzf`, which only pipes the output of the second
command. Adding parentheses around the source command avoids this issue,
and works on both Unix and Windows.
2017-08-28 22:32:13 +09:00
58 changed files with 3376 additions and 1693 deletions

View File

@@ -1,30 +1,22 @@
<!-- ISSUES NOT FOLLOWING THIS TEMPLATE WILL BE CLOSED AND DELETED -->
<!-- Check all that apply [x] --> <!-- Check all that apply [x] -->
- Category
- [ ] fzf binary - [ ] I have read through the manual page (`man fzf`)
- [ ] fzf-tmux script - [ ] I have the latest version of fzf
- [ ] Key bindings - [ ] I have searched through the existing issues
- [ ] Completion
- [ ] Vim ## Info
- [ ] Neovim
- [ ] Etc.
- OS - OS
- [ ] Linux - [ ] Linux
- [ ] Mac OS X - [ ] Mac OS X
- [ ] Windows - [ ] Windows
- [ ] Windows Subsystem for Linux
- [ ] Etc. - [ ] Etc.
- Shell - Shell
- [ ] bash - [ ] bash
- [ ] zsh - [ ] zsh
- [ ] fish - [ ] fish
<!-- ## Problem / Steps to reproduce
### Before submitting
- Make sure that you have the latest version of fzf
- If you use tmux, make sure $TERM is set to screen or screen-256color
- For more Vim stuff, check out https://github.com/junegunn/fzf.vim
Describe your problem or suggestion from here ...
-->

2
.gitignore vendored
View File

@@ -1,4 +1,5 @@
bin/fzf bin/fzf
bin/fzf.exe
target target
pkg pkg
Gemfile.lock Gemfile.lock
@@ -6,3 +7,4 @@ Gemfile.lock
doc/tags doc/tags
vendor vendor
gopath gopath
*.zwc

View File

@@ -1,20 +1,27 @@
language: ruby language: go
dist: trusty dist: xenial
sudo: required addons:
matrix: apt:
sources:
- sourceline: "ppa:pi-rho/dev"
- sourceline: "ppa:fish-shell/release-2"
packages:
- tmux
- zsh
- fish
env:
- GO111MODULE=on
jobs:
include: include:
- env: TAGS= - stage: unittest
rvm: 2.3.3 go: "1.13.x"
# - env: TAGS=tcell script: make && make test
# rvm: 2.3.3
install:
- sudo add-apt-repository -y ppa:pi-rho/dev
- sudo apt-add-repository -y ppa:fish-shell/release-2
- sudo apt-get update
- sudo apt-get install -y tmux zsh fish
- stage: cli
go: "1.13.x"
rvm: "2.5"
script: | script: |
make test install && make install && ./install --all && tmux new "ruby test/test_go.rb > out && touch ok" && cat out && [ -e ok ]
./install --all &&
tmux new "ruby test/test_go.rb > out && touch ok" && cat out && [ -e ok ]

View File

@@ -6,12 +6,10 @@ Build instructions
### Prerequisites ### Prerequisites
- `go` executable in $PATH - Go 1.11 or above
### Using Makefile ### Using Makefile
Makefile will set up and use its own `$GOPATH` under the project root.
```sh ```sh
# Build fzf binary for your platform in target # Build fzf binary for your platform in target
make make

View File

@@ -1,6 +1,175 @@
CHANGELOG CHANGELOG
========= =========
0.20.0
------
- Customizable preview window color (`preview-fg` and `preview-bg` for `--color`)
```sh
fzf --preview 'cat {}' \
--color 'fg:#bbccdd,fg+:#ddeeff,bg:#334455,preview-bg:#223344,border:#778899' \
--border --height 20 --layout reverse --info inline
```
- Removed the immediate flicking of the screen on `reload` action.
```sh
: | fzf --bind 'change:reload:seq {q}' --phony
```
- Added `clear-query` and `clear-selection` actions for `--bind`
- It is now possible to split a composite bind action over multiple `--bind`
expressions by prefixing the later ones with `+`.
```sh
fzf --bind 'ctrl-a:up+up'
# Can be now written as
fzf --bind 'ctrl-a:up' --bind 'ctrl-a:+up'
# This is useful when you need to write special execute/reload form (i.e. `execute:...`)
# to avoid parse errors and add more actions to the same key
fzf --multi --bind 'ctrl-l:select-all+execute:less {+f}' --bind 'ctrl-l:+deselect-all'
```
- Fixed parse error of `--bind` expression where concatenated execute/reload
action contains `+` character.
```sh
fzf --multi --bind 'ctrl-l:select-all+execute(less {+f})+deselect-all'
```
- Fixed bugs of reload action
- Not triggered when there's no match even when the command doesn't have
any placeholder expressions
- Screen not properly cleared when `--header-lines` not filled on reload
0.19.0
------
- Added `--phony` option which completely disables search functionality.
Useful when you want to use fzf only as a selector interface. See below.
- Added "reload" action for dynamically updating the input list without
restarting fzf. See https://github.com/junegunn/fzf/issues/1750 to learn
more about it.
```sh
# Using fzf as the selector interface for ripgrep
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="foo"
FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY' || true" \
fzf --bind "change:reload:$RG_PREFIX {q} || true" \
--ansi --phony --query "$INITIAL_QUERY"
```
- `--multi` now takes an optional integer argument which indicates the maximum
number of items that can be selected
```sh
seq 100 | fzf --multi 3 --reverse --height 50%
```
- If a placeholder expression for `--preview` and `execute` action (and the
new `reload` action) contains `f` flag, it is replaced to the
path of a temporary file that holds the evaluated list. This is useful
when you multi-select a large number of items and the length of the
evaluated string may exceed [`ARG_MAX`][argmax].
```sh
# Press CTRL-A to select 100K items and see the sum of all the numbers
seq 100000 | fzf --multi --bind ctrl-a:select-all \
--preview "awk '{sum+=\$1} END {print sum}' {+f}"
```
- `deselect-all` no longer deselects unmatched items. It is now consistent
with `select-all` and `toggle-all` in that it only affects matched items.
- Due to the limitation of bash, fuzzy completion is enabled by default for
a fixed set of commands. A helper function for easily setting up fuzzy
completion for any command is now provided.
```sh
# usage: _fzf_setup_completion path|dir COMMANDS...
_fzf_setup_completion path git kubectl
```
- Info line style can be changed by `--info=STYLE`
- `--info=default`
- `--info=inline` (same as old `--inline-info`)
- `--info=hidden`
- Preview window border can be disabled by adding `noborder` to
`--preview-window`.
- When you transform the input with `--with-nth`, the trailing white spaces
are removed.
- `ctrl-\`, `ctrl-]`, `ctrl-^`, and `ctrl-/` can now be used with `--bind`
- See https://github.com/junegunn/fzf/milestone/15?closed=1 for more details
[argmax]: https://unix.stackexchange.com/questions/120642/what-defines-the-maximum-size-for-a-command-single-argument
0.18.0
------
- Added placeholder expression for zero-based item index: `{n}` and `{+n}`
- `fzf --preview 'echo {n}: {}'`
- Added color option for the gutter: `--color gutter:-1`
- Added `--no-unicode` option for drawing borders in non-Unicode, ASCII
characters
- `FZF_PREVIEW_LINES` and `FZF_PREVIEW_COLUMNS` are exported to preview process
- fzf still overrides `LINES` and `COLUMNS` as before, but they may be
reset by the default shell.
- Bug fixes and improvements
- See https://github.com/junegunn/fzf/milestone/14?closed=1
- Built with Go 1.12.1
0.17.5
------
- Bug fixes and improvements
- See https://github.com/junegunn/fzf/milestone/13?closed=1
- Search query longer than the screen width is allowed (up to 300 chars)
- Built with Go 1.11.1
0.17.4
------
- Added `--layout` option with a new layout called `reverse-list`.
- `--layout=reverse` is a synonym for `--reverse`
- `--layout=default` is a synonym for `--no-reverse`
- Preview window will be updated even when there is no match for the query
if any of the placeholder expressions (e.g. `{q}`, `{+}`) evaluates to
a non-empty string.
- More keys for binding: `shift-{up,down}`, `alt-{up,down,left,right}`
- fzf can now start even when `/dev/tty` is not available by making an
educated guess.
- Updated the default command for Windows.
- Fixes and improvements on bash/zsh completion
- install and uninstall scripts now supports generating files under
`XDG_CONFIG_HOME` on `--xdg` flag.
See https://github.com/junegunn/fzf/milestone/12?closed=1 for the full list of
changes.
0.17.3
------
- `$LINES` and `$COLUMNS` are exported to preview command so that the command
knows the exact size of the preview window.
- Better error messages when the default command or `$FZF_DEFAULT_COMMAND`
fails.
- Reverted #1061 to avoid having duplicate entries in the list when find
command detected a file system loop (#1120). The default command now
requires that find supports `-fstype` option.
- fzf now distinguishes mouse left click and right click (#1130)
- Right click is now bound to `toggle` action by default
- `--bind` understands `left-click` and `right-click`
- Added `replace-query` action (#1137)
- Replaces query string with the current selection
- Added `accept-non-empty` action (#1162)
- Same as accept, except that it prevents fzf from exiting without any
selection
0.17.1
------
- Fixed custom background color of preview window (#1046)
- Fixed background color issues of Windows binary
- Fixed Windows binary to execute command using cmd.exe with no parsing and
escaping (#1072)
- Added support for `window` layout on Vim 8 using Vim 8 terminal (#1055)
0.17.0-2
--------
A maintenance release for auxiliary scripts. fzf binaries are not updated.
- Experimental support for the builtin terminal of Vim 8
- fzf can now run inside GVim
- Updated Vim plugin to better handle `&shell` issue on fish
- Fixed a bug of fzf-tmux where invalid output is generated
- Fixed fzf-tmux to work even when `tput` does not work
0.17.0 0.17.0
------ ------
- Performance optimization - Performance optimization

11
Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM archlinux/base:latest
RUN pacman -Sy && pacman --noconfirm -S awk git tmux zsh fish ruby procps go make
RUN gem install --no-document minitest
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc
RUN echo '. ~/.bashrc' >> ~/.bash_profile
# Do not set default PS1
RUN rm -f /etc/bash.bashrc
COPY . /fzf
RUN cd /fzf && make install && ./install --all
CMD tmux new 'set -o pipefail; ruby /fzf/test/test_go.rb | tee out && touch ok' && cat out && [ -e ok ]

View File

@@ -1,24 +1,9 @@
ifndef GOOS GO ?= go
UNAME_S := $(shell uname -s) GOOS ?= $(word 1, $(subst /, " ", $(word 4, $(shell go version))))
ifeq ($(UNAME_S),Darwin)
GOOS := darwin
else ifeq ($(UNAME_S),Linux)
GOOS := linux
else
$(error "$$GOOS is not defined.")
endif
endif
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST))) MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
ROOT_DIR := $(shell dirname $(MAKEFILE)) ROOT_DIR := $(shell dirname $(MAKEFILE))
GOPATH := $(ROOT_DIR)/gopath SOURCES := $(wildcard *.go src/*.go src/*/*.go) $(MAKEFILE)
SRC_LINK := $(GOPATH)/src/github.com/junegunn/fzf/src
VENDOR_LINK := $(GOPATH)/src/github.com/junegunn/fzf/vendor
export GOPATH
GLIDE_YAML := glide.yaml
GLIDE_LOCK := glide.lock
SOURCES := $(wildcard *.go src/*.go src/*/*.go) $(SRC_LINK) $(VENDOR_LINK) $(GLIDE_LOCK) $(MAKEFILE)
REVISION := $(shell git log -n 1 --pretty=format:%h -- $(SOURCES)) REVISION := $(shell git log -n 1 --pretty=format:%h -- $(SOURCES))
BUILD_FLAGS := -a -ldflags "-X main.revision=$(REVISION) -w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" BUILD_FLAGS := -a -ldflags "-X main.revision=$(REVISION) -w -extldflags=$(LDFLAGS)" -tags "$(TAGS)"
@@ -29,6 +14,7 @@ BINARYARM5 := fzf-$(GOOS)_arm5
BINARYARM6 := fzf-$(GOOS)_arm6 BINARYARM6 := fzf-$(GOOS)_arm6
BINARYARM7 := fzf-$(GOOS)_arm7 BINARYARM7 := fzf-$(GOOS)_arm7
BINARYARM8 := fzf-$(GOOS)_arm8 BINARYARM8 := fzf-$(GOOS)_arm8
BINARYPPC64LE := fzf-$(GOOS)_ppc64le
VERSION := $(shell awk -F= '/version =/ {print $$2}' src/constants.go | tr -d "\" ") VERSION := $(shell awk -F= '/version =/ {print $$2}' src/constants.go | tr -d "\" ")
RELEASE32 := fzf-$(VERSION)-$(GOOS)_386 RELEASE32 := fzf-$(VERSION)-$(GOOS)_386
RELEASE64 := fzf-$(VERSION)-$(GOOS)_amd64 RELEASE64 := fzf-$(VERSION)-$(GOOS)_amd64
@@ -36,6 +22,7 @@ RELEASEARM5 := fzf-$(VERSION)-$(GOOS)_arm5
RELEASEARM6 := fzf-$(VERSION)-$(GOOS)_arm6 RELEASEARM6 := fzf-$(VERSION)-$(GOOS)_arm6
RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7 RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7
RELEASEARM8 := fzf-$(VERSION)-$(GOOS)_arm8 RELEASEARM8 := fzf-$(VERSION)-$(GOOS)_arm8
RELEASEPPC64LE := fzf-$(VERSION)-$(GOOS)_ppc64le
# https://en.wikipedia.org/wiki/Uname # https://en.wikipedia.org/wiki/Uname
UNAME_M := $(shell uname -m) UNAME_M := $(shell uname -m)
@@ -53,6 +40,12 @@ else ifeq ($(UNAME_M),armv6l)
BINARY := $(BINARYARM6) BINARY := $(BINARYARM6)
else ifeq ($(UNAME_M),armv7l) else ifeq ($(UNAME_M),armv7l)
BINARY := $(BINARYARM7) BINARY := $(BINARYARM7)
else ifeq ($(UNAME_M),armv8l)
BINARY := $(BINARYARM8)
else ifeq ($(UNAME_M),aarch64)
BINARY := $(BINARYARM8)
else ifeq ($(UNAME_M),ppc64le)
BINARY := $(BINARYPPC64LE)
else else
$(error "Build on $(UNAME_M) is not supported, yet.") $(error "Build on $(UNAME_M) is not supported, yet.")
endif endif
@@ -68,13 +61,14 @@ release: target/$(BINARY32) target/$(BINARY64)
cd target && cp -f $(BINARY64) fzf.exe && zip $(RELEASE64).zip fzf.exe cd target && cp -f $(BINARY64) fzf.exe && zip $(RELEASE64).zip fzf.exe
cd target && rm -f fzf.exe cd target && rm -f fzf.exe
else ifeq ($(GOOS),linux) else ifeq ($(GOOS),linux)
release: target/$(BINARY32) target/$(BINARY64) target/$(BINARYARM5) target/$(BINARYARM6) target/$(BINARYARM7) target/$(BINARYARM8) release: target/$(BINARY32) target/$(BINARY64) target/$(BINARYARM5) target/$(BINARYARM6) target/$(BINARYARM7) target/$(BINARYARM8) target/$(BINARYPPC64LE)
cd target && cp -f $(BINARY32) fzf && tar -czf $(RELEASE32).tgz fzf cd target && cp -f $(BINARY32) fzf && tar -czf $(RELEASE32).tgz fzf
cd target && cp -f $(BINARY64) fzf && tar -czf $(RELEASE64).tgz fzf cd target && cp -f $(BINARY64) fzf && tar -czf $(RELEASE64).tgz fzf
cd target && cp -f $(BINARYARM5) fzf && tar -czf $(RELEASEARM5).tgz fzf cd target && cp -f $(BINARYARM5) fzf && tar -czf $(RELEASEARM5).tgz fzf
cd target && cp -f $(BINARYARM6) fzf && tar -czf $(RELEASEARM6).tgz fzf cd target && cp -f $(BINARYARM6) fzf && tar -czf $(RELEASEARM6).tgz fzf
cd target && cp -f $(BINARYARM7) fzf && tar -czf $(RELEASEARM7).tgz fzf cd target && cp -f $(BINARYARM7) fzf && tar -czf $(RELEASEARM7).tgz fzf
cd target && cp -f $(BINARYARM8) fzf && tar -czf $(RELEASEARM8).tgz fzf cd target && cp -f $(BINARYARM8) fzf && tar -czf $(RELEASEARM8).tgz fzf
cd target && cp -f $(BINARYPPC64LE) fzf && tar -czf $(RELEASEPPC64LE).tgz fzf
cd target && rm -f fzf cd target && rm -f fzf
else else
release: target/$(BINARY32) target/$(BINARY64) release: target/$(BINARY32) target/$(BINARY64)
@@ -90,19 +84,8 @@ release-all: clean test
GOOS=openbsd make release GOOS=openbsd make release
GOOS=windows make release GOOS=windows make release
$(SRC_LINK): test: $(SOURCES)
mkdir -p $(shell dirname $(SRC_LINK)) SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \
ln -sf $(ROOT_DIR)/src $(SRC_LINK)
$(VENDOR_LINK):
mkdir -p $(shell dirname $(VENDOR_LINK))
ln -sf $(ROOT_DIR)/vendor $(VENDOR_LINK)
vendor: $(GLIDE_YAML)
go get -u github.com/Masterminds/glide && $(GOPATH)/bin/glide install && touch $@
test: $(SOURCES) vendor
SHELL=/bin/sh GOOS= go test -v -tags "$(TAGS)" \
github.com/junegunn/fzf/src \ github.com/junegunn/fzf/src \
github.com/junegunn/fzf/src/algo \ github.com/junegunn/fzf/src/algo \
github.com/junegunn/fzf/src/tui \ github.com/junegunn/fzf/src/tui \
@@ -111,28 +94,43 @@ test: $(SOURCES) vendor
install: bin/fzf install: bin/fzf
clean: clean:
rm -rf target $(RM) -r target
target/$(BINARY32): $(SOURCES) vendor target/$(BINARY32): $(SOURCES)
GOARCH=386 go build $(BUILD_FLAGS) -o $@ GOARCH=386 $(GO) build $(BUILD_FLAGS) -o $@
target/$(BINARY64): $(SOURCES) vendor target/$(BINARY64): $(SOURCES)
GOARCH=amd64 go build $(BUILD_FLAGS) -o $@ GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@
# https://github.com/golang/go/wiki/GoArm # https://github.com/golang/go/wiki/GoArm
target/$(BINARYARM5): $(SOURCES) vendor target/$(BINARYARM5): $(SOURCES)
GOARCH=arm GOARM=5 go build $(BUILD_FLAGS) -o $@ GOARCH=arm GOARM=5 $(GO) build $(BUILD_FLAGS) -o $@
target/$(BINARYARM6): $(SOURCES) vendor target/$(BINARYARM6): $(SOURCES)
GOARCH=arm GOARM=6 go build $(BUILD_FLAGS) -o $@ GOARCH=arm GOARM=6 $(GO) build $(BUILD_FLAGS) -o $@
target/$(BINARYARM7): $(SOURCES) vendor target/$(BINARYARM7): $(SOURCES)
GOARCH=arm GOARM=7 go build $(BUILD_FLAGS) -o $@ GOARCH=arm GOARM=7 $(GO) build $(BUILD_FLAGS) -o $@
target/$(BINARYARM8): $(SOURCES) vendor target/$(BINARYARM8): $(SOURCES)
GOARCH=arm64 go build $(BUILD_FLAGS) -o $@ GOARCH=arm64 $(GO) build $(BUILD_FLAGS) -o $@
target/$(BINARYPPC64LE): $(SOURCES)
GOARCH=ppc64le $(GO) build $(BUILD_FLAGS) -o $@
bin/fzf: target/$(BINARY) | bin bin/fzf: target/$(BINARY) | bin
cp -f target/$(BINARY) bin/fzf cp -f target/$(BINARY) bin/fzf
.PHONY: all release release-all test install clean docker:
docker build -t fzf-arch .
docker run -it fzf-arch tmux
docker-test:
docker build -t fzf-arch .
docker run -it fzf-arch
update:
$(GO) get -u
$(GO) mod tidy
.PHONY: all release release-all test install clean docker docker-test update

View File

@@ -1,16 +1,33 @@
FZF Vim integration FZF Vim integration
=================== ===================
This repository only enables basic integration with Vim. If you're looking for Summary
more, check out [fzf.vim](https://github.com/junegunn/fzf.vim) project. -------
(Note: To use fzf in GVim, an external terminal emulator is required.) The Vim plugin of fzf provides two core functions, and `:FZF` command which is
the basic file selector command built on top of them.
1. **`fzf#run([spec dict])`**
- Starts fzf inside Vim with the given spec
- `:call fzf#run({'source': 'ls'})`
2. **`fzf#wrap([spec dict]) -> (dict)`**
- Takes a spec for `fzf#run` and returns an extended version of it with
additional options for addressing global preferences (`g:fzf_xxx`)
- `:echo fzf#wrap({'source': 'ls'})`
- We usually *wrap* a spec with `fzf#wrap` before passing it to `fzf#run`
- `:call fzf#run(fzf#wrap({'source': 'ls'}))`
3. **`:FZF [fzf_options string] [path string]`**
- Basic fuzzy file selector
- A reference implementation for those who don't want to write VimScript
to implement custom commands
- If you're looking for more such commands, check out [fzf.vim](https://github.com/junegunn/fzf.vim) project.
The most important of all is `fzf#run`, but it would be easier to understand
the whole if we start off with `:FZF` command.
`:FZF[!]` `:FZF[!]`
--------- ---------
If you have set up fzf for Vim, `:FZF` command will be added.
```vim ```vim
" Look for files under current directory " Look for files under current directory
:FZF :FZF
@@ -18,8 +35,8 @@ If you have set up fzf for Vim, `:FZF` command will be added.
" Look for files under your home directory " Look for files under your home directory
:FZF ~ :FZF ~
" With options " With fzf command-line options
:FZF --no-sort --reverse --inline-info /tmp :FZF --reverse --info=inline /tmp
" Bang version starts fzf in fullscreen mode " Bang version starts fzf in fullscreen mode
:FZF! :FZF!
@@ -37,14 +54,11 @@ Note that the environment variables `FZF_DEFAULT_COMMAND` and
- `g:fzf_action` - `g:fzf_action`
- Customizable extra key bindings for opening selected files in different ways - Customizable extra key bindings for opening selected files in different ways
- `g:fzf_layout` - `g:fzf_layout`
- Determines the size and position of fzf window (tmux pane or Neovim split) - Determines the size and position of fzf window
- `g:fzf_colors` - `g:fzf_colors`
- Customizes fzf colors to match the current color scheme - Customizes fzf colors to match the current color scheme
- `g:fzf_history_dir` - `g:fzf_history_dir`
- Enables history feature - Enables history feature
- `g:fzf_launcher`
- (Only in GVim) Terminal emulator to open fzf with
- `g:Fzf_launcher` for function reference
#### Examples #### Examples
@@ -72,12 +86,13 @@ let g:fzf_action = {
" - down / up / left / right " - down / up / left / right
let g:fzf_layout = { 'down': '~40%' } let g:fzf_layout = { 'down': '~40%' }
" In Neovim, you can set up fzf window using a Vim command " You can set up fzf window using a Vim command (Neovim or latest Vim 8 required)
let g:fzf_layout = { 'window': 'enew' } let g:fzf_layout = { 'window': 'enew' }
let g:fzf_layout = { 'window': '-tabnew' } let g:fzf_layout = { 'window': '-tabnew' }
let g:fzf_layout = { 'window': '10split enew' } let g:fzf_layout = { 'window': '10new' }
" Customize fzf colors to match your color scheme " Customize fzf colors to match your color scheme
" - fzf#wrap translates this to a set of `--color` options
let g:fzf_colors = let g:fzf_colors =
\ { 'fg': ['fg', 'Normal'], \ { 'fg': ['fg', 'Normal'],
\ 'bg': ['bg', 'Normal'], \ 'bg': ['bg', 'Normal'],
@@ -86,24 +101,77 @@ let g:fzf_colors =
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'], \ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
\ 'hl+': ['fg', 'Statement'], \ 'hl+': ['fg', 'Statement'],
\ 'info': ['fg', 'PreProc'], \ 'info': ['fg', 'PreProc'],
\ 'border': ['fg', 'Ignore'],
\ 'prompt': ['fg', 'Conditional'], \ 'prompt': ['fg', 'Conditional'],
\ 'pointer': ['fg', 'Exception'], \ 'pointer': ['fg', 'Exception'],
\ 'marker': ['fg', 'Keyword'], \ 'marker': ['fg', 'Keyword'],
\ 'spinner': ['fg', 'Label'], \ 'spinner': ['fg', 'Label'],
\ 'header': ['fg', 'Comment'] } \ 'header': ['fg', 'Comment'] }
" Enable per-command history. " Enable per-command history
" CTRL-N and CTRL-P will be automatically bound to next-history and " - History files will be stored in the specified directory
" previous-history instead of down and up. If you don't like the change, " - When set, CTRL-N and CTRL-P will be bound to 'next-history' and
" explicitly bind the keys to down and up in your $FZF_DEFAULT_OPTS. " 'previous-history' instead of 'down' and 'up'.
let g:fzf_history_dir = '~/.local/share/fzf-history' let g:fzf_history_dir = '~/.local/share/fzf-history'
``` ```
`fzf#run` `fzf#run`
--------- ---------
For more advanced uses, you can use `fzf#run([options])` function with the `fzf#run()` function is the core of Vim integration. It takes a single
following options. dictionary argument, *a spec*, and starts fzf process accordingly. At the very
least, specify `sink` option to tell what it should do with the selected
entry.
```vim
call fzf#run({'sink': 'e'})
```
We haven't specified the `source`, so this is equivalent to starting fzf on
command line without standard input pipe; fzf will use find command (or
`$FZF_DEFAULT_COMMAND` if defined) to list the files under the current
directory. When you select one, it will open it with the sink, `:e` command.
If you want to open it in a new tab, you can pass `:tabedit` command instead
as the sink.
```vim
call fzf#run({'sink': 'tabedit'})
```
Instead of using the default find command, you can use any shell command as
the source. The following example will list the files managed by git. It's
equivalent to running `git ls-files | fzf` on shell.
```vim
call fzf#run({'source': 'git ls-files', 'sink': 'e'})
```
fzf options can be specified as `options` entry in spec dictionary.
```vim
call fzf#run({'sink': 'tabedit', 'options': '--multi --reverse'})
```
You can also pass a layout option if you don't want fzf window to take up the
entire screen.
```vim
" up / down / left / right / window are allowed
call fzf#run({'source': 'git ls-files', 'sink': 'e', 'left': '40%'})
call fzf#run({'source': 'git ls-files', 'sink': 'e', 'window': '30vnew'})
```
`source` doesn't have to be an external shell command, you can pass a Vim
array as the source. In the next example, we pass the names of color
schemes as the source to implement a color scheme selector.
```vim
call fzf#run({'source': map(split(globpath(&rtp, 'colors/*.vim')),
\ 'fnamemodify(v:val, ":t:r")'),
\ 'sink': 'colo', 'left': '25%'})
```
The following table summarizes the available options.
| Option name | Type | Description | | Option name | Type | Description |
| -------------------------- | ------------- | ---------------------------------------------------------------- | | -------------------------- | ------------- | ---------------------------------------------------------------- |
@@ -114,14 +182,11 @@ following options.
| `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once | | `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once |
| `options` | string/list | Options to fzf | | `options` | string/list | Options to fzf |
| `dir` | string | Working directory | | `dir` | string | Working directory |
| `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) | | `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) |
| `window` (*Neovim only*) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) | | `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new`) |
| `launcher` | string | External terminal emulator to start fzf with (GVim only) |
| `launcher` | funcref | Function for generating `launcher` string (GVim only) |
`options` entry can be either a string or a list. For simple cases, string `options` entry can be either a string or a list. For simple cases, string
should suffice, but prefer to use list type if you're concerned about escaping should suffice, but prefer to use list type to avoid escaping issues.
issues on different platforms.
```vim ```vim
call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'}) call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'})
@@ -131,38 +196,131 @@ call fzf#run({'options': ['--reverse', '--prompt', 'C:\Program Files\']})
`fzf#wrap` `fzf#wrap`
---------- ----------
`fzf#wrap([name string,] [opts dict,] [fullscreen boolean])` is a helper We have seen that several aspects of `:FZF` command can be configured with
function that decorates the options dictionary so that it understands a set of global option variables; different ways to open files
`g:fzf_layout`, `g:fzf_action`, `g:fzf_colors`, and `g:fzf_history_dir` like (`g:fzf_action`), window position and size (`g:fzf_layout`), color palette
`:FZF`. (`g:fzf_colors`), etc.
So how can we make our custom `fzf#run` calls also respect those variables?
Simply by *"wrapping"* the spec dictionary with `fzf#wrap` before passing it
to `fzf#run`.
- **`fzf#wrap([name string], [spec dict], [fullscreen bool]) -> (dict)`**
- All arguments are optional. Usually we only need to pass a spec dictionary.
- `name` is for managing history files. It is ignored if
`g:fzf_history_dir` is not defined.
- `fullscreen` can be either `0` or `1` (default: 0).
`fzf#wrap` takes a spec and returns an extended version of it (also
a dictionary) with additional options for addressing global preferences. You
can examine the return value of it like so:
```vim ```vim
command! -bang MyStuff echo fzf#wrap({'source': 'ls'})
\ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, <bang>0))
``` ```
GVim After we *"wrap"* our spec, we pass it to `fzf#run`.
```vim
call fzf#run(fzf#wrap({'source': 'ls'}))
```
Now it supports `CTRL-T`, `CTRL-V`, and `CTRL-X` key bindings and it opens fzf
window according to `g:fzf_layout` setting.
To make it easier to use, let's define `LS` command.
```vim
command! LS call fzf#run(fzf#wrap({'source': 'ls'}))
```
Type `:LS` and see how it works.
We would like to make `:LS!` (bang version) open fzf in fullscreen, just like
`:FZF!`. Add `-bang` to command definition, and use `<bang>` value to set
the last `fullscreen` argument of `fzf#wrap` (see `:help <bang>`).
```vim
" On :LS!, <bang> evaluates to '!', and '!0' becomes 1
command! -bang LS call fzf#run(fzf#wrap({'source': 'ls'}, <bang>0))
```
Our `:LS` command will be much more useful if we can pass a directory argument
to it, so that something like `:LS /tmp` is possible.
```vim
command! -bang -complete=dir -nargs=* LS
\ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0))
```
Lastly, if you have enabled `g:fzf_history_dir`, you might want to assign
a unique name to our command and pass it as the first argument to `fzf#wrap`.
```vim
" The query history for this command will be stored as 'ls' inside g:fzf_history_dir.
" The name is ignored if g:fzf_history_dir is not defined.
command! -bang -complete=dir -nargs=* LS
\ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': <q-args>}, <bang>0))
```
Tips
---- ----
In GVim, you need an external terminal emulator to start fzf with. `xterm` ### fzf inside terminal buffer
command is used by default, but you can customize it with `g:fzf_launcher`.
The latest versions of Vim and Neovim include builtin terminal emulator
(`:terminal`) and fzf will start in a terminal buffer in the following cases:
- On Neovim
- On GVim
- On Terminal Vim with a non-default layout
- `call fzf#run({'left': '30%'})` or `let g:fzf_layout = {'left': '30%'}`
#### Starting fzf in Neovim floating window
```vim ```vim
" This is the default. %s is replaced with fzf command " Using floating windows of Neovim to start fzf
let g:fzf_launcher = 'xterm -e bash -ic %s' if has('nvim')
let $FZF_DEFAULT_OPTS .= ' --border --margin=0,2'
function! FloatingFZF()
let width = float2nr(&columns * 0.9)
let height = float2nr(&lines * 0.6)
let opts = { 'relative': 'editor',
\ 'row': (&lines - height) / 2,
\ 'col': (&columns - width) / 2,
\ 'width': width,
\ 'height': height }
let win = nvim_open_win(nvim_create_buf(v:false, v:true), v:true, opts)
call setwinvar(win, '&winhighlight', 'NormalFloat:Normal')
endfunction
let g:fzf_layout = { 'window': 'call FloatingFZF()' }
endif
" Use urxvt instead
let g:fzf_launcher = 'urxvt -geometry 120x30 -e sh -c %s'
``` ```
If you're running MacVim on OSX, I recommend you to use iTerm2 as the #### Hide statusline
launcher. Refer to the [this wiki page][macvim-iterm2] to see how to set up.
[macvim-iterm2]: https://github.com/junegunn/fzf/wiki/On-MacVim-with-iTerm2 When fzf starts in a terminal buffer, the file type of the buffer is set to
`fzf`. So you can set up `FileType fzf` autocmd to customize the settings of
the window.
For example, if you use the default layout (`{'down': '~40%'}`) on Neovim, you
might want to temporarily disable the statusline for a cleaner look.
```vim
if has('nvim') && !exists('g:fzf_layout')
autocmd! FileType fzf
autocmd FileType fzf set laststatus=0 noshowmode noruler
\| autocmd BufLeave <buffer> set laststatus=2 showmode ruler
endif
```
[License](LICENSE) [License](LICENSE)
------------------ ------------------
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2017 Junegunn Choi Copyright (c) 2019 Junegunn Choi

294
README.md
View File

@@ -1,4 +1,4 @@
<img src="https://raw.githubusercontent.com/junegunn/i/master/fzf.png" height="170" alt="fzf - a command-line fuzzy finder"> [![travis-ci](https://travis-ci.org/junegunn/fzf.svg?branch=master)](https://travis-ci.org/junegunn/fzf) [![Donate via PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=EKYAW9PGKPD2N) <img src="https://raw.githubusercontent.com/junegunn/i/master/fzf.png" height="170" alt="fzf - a command-line fuzzy finder"> [![travis-ci](https://travis-ci.org/junegunn/fzf.svg?branch=master)](https://travis-ci.org/junegunn/fzf)
=== ===
fzf is a general-purpose command-line fuzzy finder. fzf is a general-purpose command-line fuzzy finder.
@@ -23,10 +23,11 @@ Table of Contents
----------------- -----------------
* [Installation](#installation) * [Installation](#installation)
* [Using git](#using-git)
* [Using Homebrew or Linuxbrew](#using-homebrew-or-linuxbrew) * [Using Homebrew or Linuxbrew](#using-homebrew-or-linuxbrew)
* [As Vim plugin](#as-vim-plugin) * [Using git](#using-git)
* [Using Linux package managers](#using-linux-package-managers)
* [Windows](#windows) * [Windows](#windows)
* [As Vim plugin](#as-vim-plugin)
* [Upgrading fzf](#upgrading-fzf) * [Upgrading fzf](#upgrading-fzf)
* [Building fzf](#building-fzf) * [Building fzf](#building-fzf)
* [Usage](#usage) * [Usage](#usage)
@@ -35,6 +36,7 @@ Table of Contents
* [Search syntax](#search-syntax) * [Search syntax](#search-syntax)
* [Environment variables](#environment-variables) * [Environment variables](#environment-variables)
* [Options](#options) * [Options](#options)
* [Demo](#demo)
* [Examples](#examples) * [Examples](#examples)
* [fzf-tmux script](#fzf-tmux-script) * [fzf-tmux script](#fzf-tmux-script)
* [Key bindings for command line](#key-bindings-for-command-line) * [Key bindings for command line](#key-bindings-for-command-line)
@@ -51,9 +53,9 @@ Table of Contents
* [Executing external programs](#executing-external-programs) * [Executing external programs](#executing-external-programs)
* [Preview window](#preview-window) * [Preview window](#preview-window)
* [Tips](#tips) * [Tips](#tips)
* [Respecting .gitignore, <code>.hgignore</code>, and <code>svn:ignore</code>](#respecting-gitignore-hgignore-and-svnignore) * [Respecting .gitignore](#respecting-gitignore)
* [git ls-tree for fast traversal](#git-ls-tree-for-fast-traversal)
* [Fish shell](#fish-shell) * [Fish shell](#fish-shell)
* [Related projects](#related-projects)
* [<a href="LICENSE">License</a>](#license) * [<a href="LICENSE">License</a>](#license)
Installation Installation
@@ -73,9 +75,25 @@ stuff.
[bin]: https://github.com/junegunn/fzf-bin/releases [bin]: https://github.com/junegunn/fzf-bin/releases
### Using Homebrew or Linuxbrew
You can use [Homebrew](http://brew.sh/) or [Linuxbrew](http://linuxbrew.sh/)
to install fzf.
```sh
brew install fzf
# To install useful key bindings and fuzzy completion:
$(brew --prefix)/opt/fzf/install
```
fzf is also available [via MacPorts][portfile]: `sudo port install fzf`
[portfile]: https://github.com/macports/macports-ports/blob/master/sysutils/fzf/Portfile
### Using git ### Using git
Clone this repository and run Alternatively, you can "git clone" this repository to any directory and run
[install](https://github.com/junegunn/fzf/blob/master/install) script. [install](https://github.com/junegunn/fzf/blob/master/install) script.
```sh ```sh
@@ -83,53 +101,75 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install ~/.fzf/install
``` ```
### Using Homebrew or Linuxbrew ### Using Linux package managers
Alternatively, you can use [Homebrew](http://brew.sh/) or | Distro | Command |
[Linuxbrew](http://linuxbrew.sh/) to install fzf. | --- | --- |
| Alpine Linux | `sudo apk add fzf` |
| Arch Linux | `sudo pacman -S fzf` |
| Debian | `sudo apt-get install fzf` |
| Fedora | `sudo dnf install fzf` |
| FreeBSD | `pkg install fzf` |
| NixOS | `nix-env -iA nixpkgs.fzf` |
| openSUSE | `sudo zypper install fzf` |
```sh Shell extensions (key bindings and fuzzy auto-completion) and Vim/Neovim
brew install fzf plugin may or may not be enabled by default depending on the package manager.
Refer to the package documentation for more information.
# Install shell extensions
/usr/local/opt/fzf/install
```
### As Vim plugin
You can manually add the directory to `&runtimepath` as follows,
```vim
" If installed using git
set rtp+=~/.fzf
" If installed using Homebrew
set rtp+=/usr/local/opt/fzf
```
But it's recommended that you use a plugin manager like
[vim-plug](https://github.com/junegunn/vim-plug).
```vim
Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' }
```
### Windows ### Windows
Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also
available as a [Chocolatey package][choco]. available via [Chocolatey][choco] and [Scoop][scoop]:
| Package manager | Command |
| --- | --- |
| Chocolatey | `choco install fzf` |
| Scoop | `scoop install fzf` |
[choco]: https://chocolatey.org/packages/fzf [choco]: https://chocolatey.org/packages/fzf
[scoop]: https://github.com/ScoopInstaller/Main/blob/master/bucket/fzf.json
```sh Known issues and limitations on Windows can be found on [the wiki
choco install fzf page][windows-wiki].
[windows-wiki]: https://github.com/junegunn/fzf/wiki/Windows
### As Vim plugin
Once you have fzf installed, you can enable it inside Vim simply by adding the
directory to `&runtimepath` in your Vim configuration file. The path may
differ depending on the package manager.
```vim
" If installed using Homebrew
set rtp+=/usr/local/opt/fzf
" If installed using git
set rtp+=~/.fzf
``` ```
However, other components of the project may not work on Windows. You might If you use [vim-plug](https://github.com/junegunn/vim-plug), the same can be
want to consider installing fzf on [Windows Subsystem for Linux][wsl] where written as:
everything runs flawlessly.
[wsl]: https://blogs.msdn.microsoft.com/wsl/ ```vim
" If installed using Homebrew
Plug '/usr/local/opt/fzf'
" If installed using git
Plug '~/.fzf'
```
But instead of separately installing fzf on your system (using Homebrew or
"git clone") and enabling it on Vim (adding it to `&runtimepath`), you can use
vim-plug to do both.
```vim
" PlugInstall and PlugUpdate will clone fzf in ~/.fzf and run the install script
Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' }
" Both options are optional. You don't have to install fzf in ~/.fzf
" and you don't have to run the install script if you use fzf only in Vim.
```
Upgrading fzf Upgrading fzf
------------- -------------
@@ -184,8 +224,8 @@ cursor with `--height` option.
vim $(fzf --height 40%) vim $(fzf --height 40%)
``` ```
Also check out `--reverse` option if you prefer "top-down" layout instead of Also check out `--reverse` and `--layout` options if you prefer
the default "bottom-up" layout. "top-down" layout instead of the default "bottom-up" layout.
```sh ```sh
vim $(fzf --height 40% --reverse) vim $(fzf --height 40% --reverse)
@@ -195,7 +235,7 @@ You can add these options to `$FZF_DEFAULT_OPTS` so that they're applied by
default. For example, default. For example,
```sh ```sh
export FZF_DEFAULT_OPTS='--height 40% --reverse --border' export FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border'
``` ```
#### Search syntax #### Search syntax
@@ -205,12 +245,13 @@ type in multiple search terms delimited by spaces. e.g. `^music .mp3$ sbtrkt
!fire` !fire`
| Token | Match type | Description | | Token | Match type | Description |
| -------- | -------------------------- | --------------------------------- | | --------- | -------------------------- | ------------------------------------ |
| `sbtrkt` | fuzzy-match | Items that match `sbtrkt` | | `sbtrkt` | fuzzy-match | Items that match `sbtrkt` |
| `'wild` | exact-match (quoted) | Items that include `wild` |
| `^music` | prefix-exact-match | Items that start with `music` | | `^music` | prefix-exact-match | Items that start with `music` |
| `.mp3$` | suffix-exact-match | Items that end with `.mp3` | | `.mp3$` | suffix-exact-match | Items that end with `.mp3` |
| `'wild` | exact-match (quoted) | Items that include `wild` |
| `!fire` | inverse-exact-match | Items that do not include `fire` | | `!fire` | inverse-exact-match | Items that do not include `fire` |
| `!^music` | inverse-prefix-exact-match | Items that do not start with `music` |
| `!.mp3$` | inverse-suffix-exact-match | Items that do not end with `.mp3` | | `!.mp3$` | inverse-suffix-exact-match | Items that do not end with `.mp3` |
If you don't prefer fuzzy matching and do not wish to "quote" every word, If you don't prefer fuzzy matching and do not wish to "quote" every word,
@@ -229,15 +270,22 @@ or `py`.
- `FZF_DEFAULT_COMMAND` - `FZF_DEFAULT_COMMAND`
- Default command to use when input is tty - Default command to use when input is tty
- e.g. `export FZF_DEFAULT_COMMAND='ag -g ""'` - e.g. `export FZF_DEFAULT_COMMAND='fd --type f'`
- `FZF_DEFAULT_OPTS` - `FZF_DEFAULT_OPTS`
- Default options - Default options
- e.g. `export FZF_DEFAULT_OPTS="--reverse --inline-info"` - e.g. `export FZF_DEFAULT_OPTS="--layout=reverse --inline-info"`
#### Options #### Options
See the man page (`man fzf`) for the full list of options. See the man page (`man fzf`) for the full list of options.
#### Demo
If you learn by watching videos, check out this screencast by [@samoshkin](https://github.com/samoshkin) to explore `fzf` features.
<a title="fzf - command-line fuzzy finder" href="https://www.youtube.com/watch?v=qgG5Jhi_Els">
<img src="https://i.imgur.com/vtG8olE.png" width="640">
</a>
Examples Examples
-------- --------
@@ -271,16 +319,16 @@ fullscreen mode.
fzf --height 40% fzf --height 40%
``` ```
Key bindings for command line Key bindings for command-line
----------------------------- -----------------------------
The install script will setup the following key bindings for bash, zsh, and The install script will setup the following key bindings for bash, zsh, and
fish. fish.
- `CTRL-T` - Paste the selected files and directories onto the command line - `CTRL-T` - Paste the selected files and directories onto the command-line
- Set `FZF_CTRL_T_COMMAND` to override the default command - Set `FZF_CTRL_T_COMMAND` to override the default command
- Set `FZF_CTRL_T_OPTS` to pass additional options - Set `FZF_CTRL_T_OPTS` to pass additional options
- `CTRL-R` - Paste the selected command from history onto the command line - `CTRL-R` - Paste the selected command from history onto the command-line
- If you want to see the commands in chronological order, press `CTRL-R` - If you want to see the commands in chronological order, press `CTRL-R`
again which toggles sorting by relevance again which toggles sorting by relevance
- Set `FZF_CTRL_R_OPTS` to pass additional options - Set `FZF_CTRL_R_OPTS` to pass additional options
@@ -332,7 +380,7 @@ cd ~/github/fzf**<TAB>
#### Process IDs #### Process IDs
Fuzzy completion for PIDs is provided for kill command. In this case Fuzzy completion for PIDs is provided for kill command. In this case,
there is no trigger sequence, just press tab key after kill command. there is no trigger sequence, just press tab key after kill command.
```sh ```sh
@@ -367,12 +415,17 @@ export FZF_COMPLETION_TRIGGER='~~'
# Options to fzf command # Options to fzf command
export FZF_COMPLETION_OPTS='+c -x' export FZF_COMPLETION_OPTS='+c -x'
# Use ag instead of the default find command for listing candidates. # Use fd (https://github.com/sharkdp/fd) instead of the default find
# - The first argument to the function is the base path to start traversal # command for listing path candidates.
# - Note that ag only lists files not directories # - The first argument to the function ($1) is the base path to start traversal
# - See the source code (completion.{bash,zsh}) for the details. # - See the source code (completion.{bash,zsh}) for the details.
_fzf_compgen_path() { _fzf_compgen_path() {
ag -g "" "$1" fd --hidden --follow --exclude ".git" . "$1"
}
# Use fd to generate the list for directory completion
_fzf_compgen_dir() {
fd --type d --hidden --follow --exclude ".git" . "$1"
} }
``` ```
@@ -380,11 +433,12 @@ _fzf_compgen_path() {
On bash, fuzzy completion is enabled only for a predefined set of commands On bash, fuzzy completion is enabled only for a predefined set of commands
(`complete | grep _fzf` to see the list). But you can enable it for other (`complete | grep _fzf` to see the list). But you can enable it for other
commands as well like follows. commands as well by using `_fzf_setup_completion` helper function.
```sh ```sh
complete -F _fzf_path_completion -o default -o bashdefault ag # usage: _fzf_setup_completion path|dir COMMANDS...
complete -F _fzf_dir_completion -o default -o bashdefault tree _fzf_setup_completion path ag git kubectl
_fzf_setup_completion dir tree
``` ```
Vim plugin Vim plugin
@@ -397,7 +451,7 @@ Advanced topics
### Performance ### Performance
fzf is fast, and is [getting even faster][perf]. Performance should not be fzf is fast and is [getting even faster][perf]. Performance should not be
a problem in most use cases. However, you might want to be aware of the a problem in most use cases. However, you might want to be aware of the
options that affect the performance. options that affect the performance.
@@ -408,11 +462,11 @@ options that affect the performance.
- `--with-nth` makes fzf slower as fzf has to tokenize and reassemble each - `--with-nth` makes fzf slower as fzf has to tokenize and reassemble each
line. line.
- If you absolutely need better performance, you can consider using - If you absolutely need better performance, you can consider using
`--algo=v1` (the default being `v2`) to make fzf use faster greedy `--algo=v1` (the default being `v2`) to make fzf use a faster greedy
algorithm. However, this algorithm is not guaranteed to find the optimal algorithm. However, this algorithm is not guaranteed to find the optimal
ordering of the matches and is not recommended. ordering of the matches and is not recommended.
[perf]: https://junegunn.kr/images/fzf-0.16.11.png [perf]: https://junegunn.kr/images/fzf-0.17.0.png
### Executing external programs ### Executing external programs
@@ -429,7 +483,7 @@ See *KEY BINDINGS* section of the man page for details.
### Preview window ### Preview window
When `--preview` option is set, fzf automatically starts external process with When `--preview` option is set, fzf automatically starts an external process with
the current line as the argument and shows the result in the split window. the current line as the argument and shows the result in the split window.
```bash ```bash
@@ -437,7 +491,7 @@ the current line as the argument and shows the result in the split window.
fzf --preview 'cat {}' fzf --preview 'cat {}'
``` ```
Since preview window is updated only after the process is complete, it's Since the preview window is updated only after the process is complete, it's
important that the command finishes quickly. important that the command finishes quickly.
```bash ```bash
@@ -445,102 +499,93 @@ important that the command finishes quickly.
fzf --preview 'head -100 {}' fzf --preview 'head -100 {}'
``` ```
Preview window supports ANSI colors, so you can use programs that Preview window supports ANSI colors, so you can use any program that
syntax-highlights the content of a file. syntax-highlights the content of a file.
- Bat: https://github.com/sharkdp/bat
- Highlight: http://www.andre-simon.de/doku/highlight/en/highlight.php - Highlight: http://www.andre-simon.de/doku/highlight/en/highlight.php
- CodeRay: http://coderay.rubychan.de/
- Rouge: https://github.com/jneen/rouge
```bash ```bash
# Try highlight, coderay, rougify in turn, then fall back to cat fzf --preview 'bat --style=numbers --color=always {} | head -500'
fzf --preview '[[ $(file --mime {}) =~ binary ]] &&
echo {} is a binary file ||
(highlight -O ansi -l {} ||
coderay {} ||
rougify {} ||
cat {}) 2> /dev/null | head -500'
``` ```
You can customize the size and position of the preview window using You can customize the size, position, and border of the preview window using
`--preview-window` option. For example, `--preview-window` option, and the foreground and background color of it with
`--color` option. For example,
```bash ```bash
fzf --height 40% --reverse --preview 'file {}' --preview-window down:1 fzf --height 40% --layout reverse --info inline --border \
--preview 'file {}' --preview-window down:1:noborder \
--color 'fg:#bbccdd,fg+:#ddeeff,bg:#334455,preview-bg:#223344,border:#778899'
``` ```
For more advanced examples, see [Key bindings for git with fzf][fzf-git]. See the man page (`man fzf`) for the full list of options.
For more advanced examples, see [Key bindings for git with fzf][fzf-git]
([code](https://gist.github.com/junegunn/8b572b8d4b5eddd8b85e5f4d40f17236)).
[fzf-git]: https://junegunn.kr/2016/07/fzf-git/ [fzf-git]: https://junegunn.kr/2016/07/fzf-git/
----
Since fzf is a general-purpose text filter rather than a file finder, **it is
not a good idea to add `--preview` option to your `$FZF_DEFAULT_OPTS`**.
```sh
# *********************
# ** DO NOT DO THIS! **
# *********************
export FZF_DEFAULT_OPTS='--preview "bat --style=numbers --color=always {} | head -500"'
# bat doesn't work with any input other than the list of files
ps -ef | fzf
seq 100 | fzf
history | fzf
```
Tips Tips
---- ----
#### Respecting `.gitignore`, `.hgignore`, and `svn:ignore` #### Respecting `.gitignore`
[ag](https://github.com/ggreer/the_silver_searcher) or You can use [fd](https://github.com/sharkdp/fd),
[rg](https://github.com/BurntSushi/ripgrep) will do the [ripgrep](https://github.com/BurntSushi/ripgrep), or [the silver
filtering: searcher](https://github.com/ggreer/the_silver_searcher) instead of the
default find command to traverse the file system while respecting
`.gitignore`.
```sh ```sh
# Feed the output of ag into fzf # Feed the output of fd into fzf
ag -g "" | fzf fd --type f | fzf
# Setting ag as the default source for fzf # Setting fd as the default source for fzf
export FZF_DEFAULT_COMMAND='ag -g ""' export FZF_DEFAULT_COMMAND='fd --type f'
# Now fzf (w/o pipe) will use ag instead of find # Now fzf (w/o pipe) will use fd instead of find
fzf fzf
# To apply the command to CTRL-T as well # To apply the command to CTRL-T as well
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND" export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
``` ```
If you don't want to exclude hidden files, use the following command: If you want the command to follow symbolic links, and don't want it to exclude
hidden files, use the following command:
```sh ```sh
export FZF_DEFAULT_COMMAND='ag --hidden --ignore .git -g ""' export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git'
```
#### `git ls-tree` for fast traversal
If you're running fzf in a large git repository, `git ls-tree` can boost up the
speed of the traversal.
```sh
export FZF_DEFAULT_COMMAND='
(git ls-tree -r --name-only HEAD ||
find . -path "*/\.*" -prune -o -type f -print -o -type l -print |
sed s/^..//) 2> /dev/null'
``` ```
#### Fish shell #### Fish shell
Fish shell before version 2.6.0 [doesn't allow](https://github.com/fish-shell/fish-shell/issues/1362) `CTRL-T` key binding of fish, unlike those of bash and zsh, will use the last
reading from STDIN in command substitution, which means simple `vim (fzf)` token on the command-line as the root directory for the recursive search. For
doesn't work as expected. The workaround for fish 2.5.0 and earlier is to use instance, hitting `CTRL-T` at the end of the following command-line
the `read` fish command:
```sh
fzf | read -l result; and vim $result
```
or, for multiple results:
```sh
fzf -m | while read -l r; set result $result $r; end; and vim $result
```
The globbing system is different in fish and thus `**` completion will not work.
However, the `CTRL-T` command will use the last token on the commandline as the
root folder for the recursive search. For instance, hitting `CTRL-T` at the end
of the following commandline
```sh ```sh
ls /var/ ls /var/
``` ```
will list all files and folders under `/var/`. will list all files and directories under `/var/`.
When using a custom `FZF_CTRL_T_COMMAND`, use the unexpanded `$dir` variable to When using a custom `FZF_CTRL_T_COMMAND`, use the unexpanded `$dir` variable to
make use of this feature. `$dir` defaults to `.` when the last token is not a make use of this feature. `$dir` defaults to `.` when the last token is not a
@@ -550,9 +595,14 @@ valid directory. Example:
set -g FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'" set -g FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'"
``` ```
Related projects
----------------
https://github.com/junegunn/fzf/wiki/Related-projects
[License](LICENSE) [License](LICENSE)
------------------ ------------------
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2017 Junegunn Choi Copyright (c) 2019 Junegunn Choi

View File

@@ -16,8 +16,8 @@ skip=""
swap="" swap=""
close="" close=""
term="" term=""
[[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines) [[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines) || lines=$(tmux display-message -p "#{pane_height}")
[[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols) [[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols) || columns=$(tmux display-message -p "#{pane_width}")
help() { help() {
>&2 echo 'usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS] >&2 echo 'usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
@@ -136,6 +136,11 @@ fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id"
cleanup() { cleanup() {
\rm -f $argsf $fifo1 $fifo2 $fifo3 \rm -f $argsf $fifo1 $fifo2 $fifo3
# Restore tmux window options
if [[ "${#tmux_win_opts[@]}" -gt 0 ]]; then
eval "tmux ${tmux_win_opts[@]}"
fi
# Remove temp window if we were zoomed # Remove temp window if we were zoomed
if [[ -n "$zoomed" ]]; then if [[ -n "$zoomed" ]]; then
tmux display-message -p "#{window_id}" > /dev/null tmux display-message -p "#{window_id}" > /dev/null
@@ -174,19 +179,20 @@ pppid=$$
echo -n "trap 'kill -SIGUSR1 -$pppid' EXIT SIGINT SIGTERM;" > $argsf echo -n "trap 'kill -SIGUSR1 -$pppid' EXIT SIGINT SIGTERM;" > $argsf
close="; trap - EXIT SIGINT SIGTERM $close" close="; trap - EXIT SIGINT SIGTERM $close"
tmux_win_opts=( $(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/') )
if [[ -n "$term" ]] || [[ -t 0 ]]; then if [[ -n "$term" ]] || [[ -t 0 ]]; then
cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" >> $argsf cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" >> $argsf
cat $argsf
TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\ TMUX=$(echo $TMUX | cut -d , -f 1,2) 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 "$envs bash -c 'cd $(printf %q "$PWD"); exec -a fzf 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=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\ TMUX=$(echo $TMUX | cut -d , -f 1,2) 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 -c 'exec -a fzf bash $argsf'" $swap \
> /dev/null 2>&1 > /dev/null 2>&1
cat <&0 > $fifo1 & cat <&0 > $fifo1 &
fi fi

View File

@@ -1,23 +1,47 @@
fzf.txt fzf Last change: August 14 2017 fzf.txt fzf Last change: November 23 2019
FZF - TABLE OF CONTENTS *fzf* *fzf-toc* FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
============================================================================== ==============================================================================
FZF Vim integration FZF Vim integration
Summary
:FZF[!] :FZF[!]
Configuration Configuration
Examples Examples
fzf#run fzf#run
fzf#wrap fzf#wrap
GVim Tips
fzf inside terminal buffer
Starting fzf in Neovim floating window
Hide statusline
License License
FZF VIM INTEGRATION *fzf-vim-integration* FZF VIM INTEGRATION *fzf-vim-integration*
============================================================================== ==============================================================================
This repository only enables basic integration with Vim. If you're looking for
more, check out {fzf.vim}{1} project.
(Note: To use fzf in GVim, an external terminal emulator is required.) SUMMARY *fzf-summary*
==============================================================================
The Vim plugin of fzf provides two core functions, and `:FZF` command which is
the basic file selector command built on top of them.
1. `fzf#run([specdict])`
- Starts fzf inside Vim with the given spec
- `:callfzf#run({'source':'ls'})`
2. `fzf#wrap([specdict])->(dict)`
- Takes a spec for `fzf#run` and returns an extended version of it with
additional options for addressing global preferences (`g:fzf_xxx`)
- `:echofzf#wrap({'source':'ls'})`
- We usually wrap a spec with `fzf#wrap` before passing it to `fzf#run`
- `:callfzf#run(fzf#wrap({'source':'ls'}))`
3. `:FZF[fzf_optionsstring][pathstring]`
- Basic fuzzy file selector
- A reference implementation for those who don't want to write VimScript to
implement custom commands
- If you're looking for more such commands, check out {fzf.vim}{1} project.
The most important of all is `fzf#run`, but it would be easier to understand
the whole if we start off with `:FZF` command.
{1} https://github.com/junegunn/fzf.vim {1} https://github.com/junegunn/fzf.vim
@@ -26,8 +50,6 @@ more, check out {fzf.vim}{1} project.
============================================================================== ==============================================================================
*:FZF* *:FZF*
If you have set up fzf for Vim, `:FZF` command will be added.
> >
" Look for files under current directory " Look for files under current directory
:FZF :FZF
@@ -35,8 +57,8 @@ If you have set up fzf for Vim, `:FZF` command will be added.
" Look for files under your home directory " Look for files under your home directory
:FZF ~ :FZF ~
" With options " With fzf command-line options
:FZF --no-sort --reverse --inline-info /tmp :FZF --reverse --info=inline /tmp
" Bang version starts fzf in fullscreen mode " Bang version starts fzf in fullscreen mode
:FZF! :FZF!
@@ -54,21 +76,17 @@ Note that the environment variables `FZF_DEFAULT_COMMAND` and
< Configuration >_____________________________________________________________~ < Configuration >_____________________________________________________________~
*fzf-configuration* *fzf-configuration*
*g:fzf_action* *g:fzf_layout* *g:fzf_colors* *g:fzf_history_dir* *g:fzf_launcher* *g:fzf_action* *g:fzf_layout* *g:fzf_colors* *g:fzf_history_dir*
*g:Fzf_launcher*
- `g:fzf_action` - `g:fzf_action`
- Customizable extra key bindings for opening selected files in different - Customizable extra key bindings for opening selected files in different
ways ways
- `g:fzf_layout` - `g:fzf_layout`
- Determines the size and position of fzf window (tmux pane or Neovim split) - Determines the size and position of fzf window
- `g:fzf_colors` - `g:fzf_colors`
- Customizes fzf colors to match the current color scheme - Customizes fzf colors to match the current color scheme
- `g:fzf_history_dir` - `g:fzf_history_dir`
- Enables history feature - Enables history feature
- `g:fzf_launcher`
- (Only in GVim) Terminal emulator to open fzf with
- `g:Fzf_launcher` for function reference
Examples~ Examples~
@@ -97,12 +115,13 @@ Examples~
" - down / up / left / right " - down / up / left / right
let g:fzf_layout = { 'down': '~40%' } let g:fzf_layout = { 'down': '~40%' }
" In Neovim, you can set up fzf window using a Vim command " You can set up fzf window using a Vim command (Neovim or latest Vim 8 required)
let g:fzf_layout = { 'window': 'enew' } let g:fzf_layout = { 'window': 'enew' }
let g:fzf_layout = { 'window': '-tabnew' } let g:fzf_layout = { 'window': '-tabnew' }
let g:fzf_layout = { 'window': '10split enew' } let g:fzf_layout = { 'window': '10new' }
" Customize fzf colors to match your color scheme " Customize fzf colors to match your color scheme
" - fzf#wrap translates this to a set of `--color` options
let g:fzf_colors = let g:fzf_colors =
\ { 'fg': ['fg', 'Normal'], \ { 'fg': ['fg', 'Normal'],
\ 'bg': ['bg', 'Normal'], \ 'bg': ['bg', 'Normal'],
@@ -111,28 +130,71 @@ Examples~
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'], \ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
\ 'hl+': ['fg', 'Statement'], \ 'hl+': ['fg', 'Statement'],
\ 'info': ['fg', 'PreProc'], \ 'info': ['fg', 'PreProc'],
\ 'border': ['fg', 'Ignore'],
\ 'prompt': ['fg', 'Conditional'], \ 'prompt': ['fg', 'Conditional'],
\ 'pointer': ['fg', 'Exception'], \ 'pointer': ['fg', 'Exception'],
\ 'marker': ['fg', 'Keyword'], \ 'marker': ['fg', 'Keyword'],
\ 'spinner': ['fg', 'Label'], \ 'spinner': ['fg', 'Label'],
\ 'header': ['fg', 'Comment'] } \ 'header': ['fg', 'Comment'] }
" Enable per-command history. " Enable per-command history
" CTRL-N and CTRL-P will be automatically bound to next-history and " - History files will be stored in the specified directory
" previous-history instead of down and up. If you don't like the change, " - When set, CTRL-N and CTRL-P will be bound to 'next-history' and
" explicitly bind the keys to down and up in your $FZF_DEFAULT_OPTS. " 'previous-history' instead of 'down' and 'up'.
let g:fzf_history_dir = '~/.local/share/fzf-history' let g:fzf_history_dir = '~/.local/share/fzf-history'
< <
FZF#RUN *fzf#run* FZF#RUN
============================================================================== ==============================================================================
For more advanced uses, you can use `fzf#run([options])` function with the *fzf#run*
following options.
---------------------------+---------------+-------------------------------------------------------------- `fzf#run()` function is the core of Vim integration. It takes a single
dictionary argument, a spec, and starts fzf process accordingly. At the very
least, specify `sink` option to tell what it should do with the selected
entry.
>
call fzf#run({'sink': 'e'})
<
We haven't specified the `source`, so this is equivalent to starting fzf on
command line without standard input pipe; fzf will use find command (or
`$FZF_DEFAULT_COMMAND` if defined) to list the files under the current
directory. When you select one, it will open it with the sink, `:e` command.
If you want to open it in a new tab, you can pass `:tabedit` command instead
as the sink.
>
call fzf#run({'sink': 'tabedit'})
<
Instead of using the default find command, you can use any shell command as
the source. The following example will list the files managed by git. It's
equivalent to running `gitls-files|fzf` on shell.
>
call fzf#run({'source': 'git ls-files', 'sink': 'e'})
<
fzf options can be specified as `options` entry in spec dictionary.
>
call fzf#run({'sink': 'tabedit', 'options': '--multi --reverse'})
<
You can also pass a layout option if you don't want fzf window to take up the
entire screen.
>
" up / down / left / right / window are allowed
call fzf#run({'source': 'git ls-files', 'sink': 'e', 'left': '40%'})
call fzf#run({'source': 'git ls-files', 'sink': 'e', 'window': '30vnew'})
<
`source` doesn't have to be an external shell command, you can pass a Vim
array as the source. In the next example, we pass the names of color schemes
as the source to implement a color scheme selector.
>
call fzf#run({'source': map(split(globpath(&rtp, 'colors/*.vim')),
\ 'fnamemodify(v:val, ":t:r")'),
\ 'sink': 'colo', 'left': '25%'})
<
The following table summarizes the available options.
---------------------------+---------------+----------------------------------------------------------------------
Option name | Type | Description ~ Option name | Type | Description ~
---------------------------+---------------+-------------------------------------------------------------- ---------------------------+---------------+----------------------------------------------------------------------
`source` | string | External command to generate input to fzf (e.g. `find.` ) `source` | string | External command to generate input to fzf (e.g. `find.` )
`source` | list | Vim list as input to fzf `source` | list | Vim list as input to fzf
`sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` ) `sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` )
@@ -140,56 +202,143 @@ following options.
`sink*` | funcref | Similar to `sink` , but takes the list of output lines at once `sink*` | funcref | Similar to `sink` , but takes the list of output lines at once
`options` | string/list | Options to fzf `options` | string/list | Options to fzf
`dir` | string | Working directory `dir` | string | Working directory
`up` / `down` / `left` / `right` | number/string | Use tmux pane with the given size (e.g. `20` , `50%` ) `up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` )
`window` (Neovim only) | string | Command to open fzf window (e.g. `verticalaboveleft30new` ) `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `verticalaboveleft30new` )
`launcher` | string | External terminal emulator to start fzf with (GVim only) ---------------------------+---------------+----------------------------------------------------------------------
`launcher` | funcref | Function for generating `launcher` string (GVim only)
---------------------------+---------------+--------------------------------------------------------------
`options` entry can be either a string or a list. For simple cases, string `options` entry can be either a string or a list. For simple cases, string
should suffice, but prefer to use list type if you're concerned about escaping should suffice, but prefer to use list type to avoid escaping issues.
issues on different platforms.
> >
call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'}) call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'})
call fzf#run({'options': ['--reverse', '--prompt', 'C:\Program Files\']}) call fzf#run({'options': ['--reverse', '--prompt', 'C:\Program Files\']})
< <
FZF#WRAP *fzf#wrap* FZF#WRAP
============================================================================== ==============================================================================
`fzf#wrap([namestring,][optsdict,][fullscreenboolean])` is a helper *fzf#wrap*
function that decorates the options dictionary so that it understands
`g:fzf_layout`, `g:fzf_action`, `g:fzf_colors`, and `g:fzf_history_dir` like We have seen that several aspects of `:FZF` command can be configured with a
`:FZF`. set of global option variables; different ways to open files (`g:fzf_action`),
window position and size (`g:fzf_layout`), color palette (`g:fzf_colors`),
etc.
So how can we make our custom `fzf#run` calls also respect those variables?
Simply by "wrapping" the spec dictionary with `fzf#wrap` before passing it to
`fzf#run`.
- `fzf#wrap([namestring],[specdict],[fullscreenbool])->(dict)`
- All arguments are optional. Usually we only need to pass a spec
dictionary.
- `name` is for managing history files. It is ignored if `g:fzf_history_dir`
is not defined.
- `fullscreen` can be either `0` or `1` (default: 0).
`fzf#wrap` takes a spec and returns an extended version of it (also a
dictionary) with additional options for addressing global preferences. You can
examine the return value of it like so:
> >
command! -bang MyStuff echo fzf#wrap({'source': 'ls'})
\ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, <bang>0)) <
After we "wrap" our spec, we pass it to `fzf#run`.
>
call fzf#run(fzf#wrap({'source': 'ls'}))
<
Now it supports CTRL-T, CTRL-V, and CTRL-X key bindings and it opens fzf
window according to `g:fzf_layout` setting.
To make it easier to use, let's define `LS` command.
>
command! LS call fzf#run(fzf#wrap({'source': 'ls'}))
<
Type `:LS` and see how it works.
We would like to make `:LS!` (bang version) open fzf in fullscreen, just like
`:FZF!`. Add `-bang` to command definition, and use <bang> value to set the
last `fullscreen` argument of `fzf#wrap` (see :help <bang>).
>
" On :LS!, <bang> evaluates to '!', and '!0' becomes 1
command! -bang LS call fzf#run(fzf#wrap({'source': 'ls'}, <bang>0))
<
Our `:LS` command will be much more useful if we can pass a directory argument
to it, so that something like `:LS/tmp` is possible.
>
command! -bang -complete=dir -nargs=* LS
\ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0))
<
Lastly, if you have enabled `g:fzf_history_dir`, you might want to assign a
unique name to our command and pass it as the first argument to `fzf#wrap`.
>
" The query history for this command will be stored as 'ls' inside g:fzf_history_dir.
" The name is ignored if g:fzf_history_dir is not defined.
command! -bang -complete=dir -nargs=* LS
\ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': <q-args>}, <bang>0))
< <
GVIM *fzf-gvim* TIPS *fzf-tips*
============================================================================== ==============================================================================
In GVim, you need an external terminal emulator to start fzf with. `xterm`
command is used by default, but you can customize it with `g:fzf_launcher`. < fzf inside terminal buffer >________________________________________________~
*fzf-inside-terminal-buffer*
The latest versions of Vim and Neovim include builtin terminal emulator
(`:terminal`) and fzf will start in a terminal buffer in the following cases:
- On Neovim
- On GVim
- On Terminal Vim with a non-default layout
- `callfzf#run({'left':'30%'})` or `letg:fzf_layout={'left':'30%'}`
Starting fzf in Neovim floating window~
*fzf-starting-fzf-in-neovim-floating-window*
> >
" This is the default. %s is replaced with fzf command " Using floating windows of Neovim to start fzf
let g:fzf_launcher = 'xterm -e bash -ic %s' if has('nvim')
let $FZF_DEFAULT_OPTS .= ' --border --margin=0,2'
function! FloatingFZF()
let width = float2nr(&columns * 0.9)
let height = float2nr(&lines * 0.6)
let opts = { 'relative': 'editor',
\ 'row': (&lines - height) / 2,
\ 'col': (&columns - width) / 2,
\ 'width': width,
\ 'height': height }
let win = nvim_open_win(nvim_create_buf(v:false, v:true), v:true, opts)
call setwinvar(win, '&winhighlight', 'NormalFloat:Normal')
endfunction
let g:fzf_layout = { 'window': 'call FloatingFZF()' }
endif
" Use urxvt instead
let g:fzf_launcher = 'urxvt -geometry 120x30 -e sh -c %s'
< <
If you're running MacVim on OSX, I recommend you to use iTerm2 as the
launcher. Refer to the {this wiki page}{3} to see how to set up.
{3} https://github.com/junegunn/fzf/wiki/On-MacVim-with-iTerm2 Hide statusline~
*fzf-hide-statusline*
When fzf starts in a terminal buffer, the file type of the buffer is set to
`fzf`. So you can set up `FileTypefzf` autocmd to customize the settings of
the window.
For example, if you use the default layout (`{'down':'~40%'}`) on Neovim, you
might want to temporarily disable the statusline for a cleaner look.
>
if has('nvim') && !exists('g:fzf_layout')
autocmd! FileType fzf
autocmd FileType fzf set laststatus=0 noshowmode noruler
\| autocmd BufLeave <buffer> set laststatus=2 showmode ruler
endif
<
LICENSE *fzf-license* LICENSE *fzf-license*
============================================================================== ==============================================================================
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2017 Junegunn Choi Copyright (c) 2019 Junegunn Choi
============================================================================== ==============================================================================
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap: vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:

38
glide.lock generated
View File

@@ -1,38 +0,0 @@
hash: d68dd0bd779ac4ffca1e0c49ca38d85f90d5d68fa8e2d5d7db70a8ce8c662ec1
updated: 2017-06-01T15:48:41.653745249-07:00
imports:
- name: github.com/gdamore/encoding
version: b23993cbb6353f0e6aa98d0ee318a34728f628b9
- name: github.com/gdamore/tcell
version: 44772c121bb7838819d3ba4a7e84c0c2d617328e
subpackages:
- encoding
- name: github.com/lucasb-eyer/go-colorful
version: c900de9dbbc73129068f5af6a823068fc5f2308c
- name: github.com/mattn/go-isatty
version: 66b8e73f3f5cda9f96b69efd03dd3d7fc4a5cdb8
- name: github.com/mattn/go-runewidth
version: 14207d285c6c197daabb5c9793d63e7af9ab2d50
- name: github.com/mattn/go-shellwords
version: 02e3cf038dcea8290e44424da473dd12be796a8a
- name: golang.org/x/crypto
version: e1a4589e7d3ea14a3352255d04b6f1a418845e5e
subpackages:
- ssh/terminal
- name: golang.org/x/sys
version: b90f89a1e7a9c1f6b918820b3daa7f08488c8594
subpackages:
- unix
- name: golang.org/x/text
version: 4ee4af566555f5fbe026368b75596286a312663a
subpackages:
- encoding
- encoding/charmap
- encoding/internal
- encoding/internal/identifier
- encoding/japanese
- encoding/korean
- encoding/simplifiedchinese
- encoding/traditionalchinese
- transform
testImports: []

View File

@@ -1,16 +0,0 @@
package: github.com/junegunn/fzf
import:
- package: github.com/mattn/go-isatty
version: 66b8e73f3f5cda9f96b69efd03dd3d7fc4a5cdb8
- package: github.com/mattn/go-runewidth
version: 14207d285c6c197daabb5c9793d63e7af9ab2d50
- package: github.com/mattn/go-shellwords
version: 02e3cf038dcea8290e44424da473dd12be796a8a
- package: github.com/gdamore/tcell
version: 44772c121bb7838819d3ba4a7e84c0c2d617328e
subpackages:
- encoding
- package: golang.org/x/crypto
version: e1a4589e7d3ea14a3352255d04b6f1a418845e5e
subpackages:
- ssh/terminal

19
go.mod Normal file
View File

@@ -0,0 +1,19 @@
module github.com/junegunn/fzf
require (
github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635 // indirect
github.com/gdamore/tcell v0.0.0-20170915061752-0a0db94084df
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
github.com/jtolds/gls v4.2.1+incompatible // indirect
github.com/lucasb-eyer/go-colorful v0.0.0-20170223221042-c900de9dbbc7 // indirect
github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c
github.com/mattn/go-shellwords v1.0.3
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
golang.org/x/crypto v0.0.0-20170728183002-558b6879de74
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 // indirect
golang.org/x/text v0.0.0-20170530162606-4ee4af566555 // indirect
)
go 1.13

26
go.sum Normal file
View File

@@ -0,0 +1,26 @@
github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635 h1:hheUEMzaOie/wKeIc1WPa7CDVuIO5hqQxjS+dwTQEnI=
github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635/go.mod h1:yrQYJKKDTrHmbYxI7CYi+/hbdiDT2m4Hj+t0ikCjsrQ=
github.com/gdamore/tcell v0.0.0-20170915061752-0a0db94084df h1:tLS1QD2puA1USLvkEnGfOt+Zp2ijtNIK3z2YFaIZZR4=
github.com/gdamore/tcell v0.0.0-20170915061752-0a0db94084df/go.mod h1:tqyG50u7+Ctv1w5VX67kLzKcj9YXR/JSBZQq/+mLl1A=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/lucasb-eyer/go-colorful v0.0.0-20170223221042-c900de9dbbc7 h1:G52I+Gk/wPD4HKvKT0Vxxp9OUPxqKs3OK6rffSPtNkA=
github.com/lucasb-eyer/go-colorful v0.0.0-20170223221042-c900de9dbbc7/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4=
github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c h1:3nKFouDdpgGUV/uerJcYWH45ZbJzX0SiVWfTgmUeTzc=
github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c h1:eFzthqtg3W6Pihj3DMTXLAF4f+ge5r5Ie5g6HLIZAF0=
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-shellwords v1.0.3 h1:K/VxK7SZ+cvuPgFSLKi5QPI9Vr/ipOf4C1gN+ntueUk=
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
golang.org/x/crypto v0.0.0-20170728183002-558b6879de74 h1:/0jf0Cx3u07Ma4EzUA6NIGuvk9hb3Br6i9V8atthkwk=
golang.org/x/crypto v0.0.0-20170728183002-558b6879de74/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 h1:rM0ROo5vb9AdYJi1110yjWGMej9ITfKddS89P3Fkhug=
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170530162606-4ee4af566555 h1:pjwO9HxObpgZBurDvTLFbSinaf3+cKpTAjVfiGaHwrI=
golang.org/x/text v0.0.0-20170530162606-4ee4af566555/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

120
install
View File

@@ -2,12 +2,15 @@
set -u set -u
version=0.17.0 version=0.20.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
binary_arch= binary_arch=
allow_legacy= shells="bash zsh fish"
prefix='~/.fzf'
prefix_expand=~/.fzf
fish_dir=${XDG_CONFIG_HOME:-$HOME/.config}/fish
help() { help() {
cat << EOF cat << EOF
@@ -17,10 +20,15 @@ usage: $0 [OPTIONS]
--bin Download fzf binary only; Do not generate ~/.fzf.{bash,zsh} --bin Download fzf binary only; Do not generate ~/.fzf.{bash,zsh}
--all Download fzf binary and update configuration files --all Download fzf binary and update configuration files
to enable key bindings and fuzzy completion to enable key bindings and fuzzy completion
--xdg Generate files under \$XDG_CONFIG_HOME/fzf
--[no-]key-bindings Enable/disable key bindings (CTRL-T, CTRL-R, ALT-C) --[no-]key-bindings Enable/disable key bindings (CTRL-T, CTRL-R, ALT-C)
--[no-]completion Enable/disable fuzzy completion (bash & zsh) --[no-]completion Enable/disable fuzzy completion (bash & zsh)
--[no-]update-rc Whether or not to update shell configuration files --[no-]update-rc Whether or not to update shell configuration files
--no-bash Do not set up bash configuration
--no-zsh Do not set up zsh configuration
--no-fish Do not set up fish configuration
--32 Download 32-bit binary --32 Download 32-bit binary
--64 Download 64-bit binary --64 Download 64-bit binary
EOF EOF
@@ -36,7 +44,11 @@ for opt in "$@"; do
auto_completion=1 auto_completion=1
key_bindings=1 key_bindings=1
update_config=1 update_config=1
allow_legacy=1 ;;
--xdg)
prefix='"${XDG_CONFIG_HOME:-$HOME/.config}"/fzf/fzf'
prefix_expand=${XDG_CONFIG_HOME:-$HOME/.config}/fzf/fzf
mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/fzf"
;; ;;
--key-bindings) key_bindings=1 ;; --key-bindings) key_bindings=1 ;;
--no-key-bindings) key_bindings=0 ;; --no-key-bindings) key_bindings=0 ;;
@@ -47,6 +59,9 @@ for opt in "$@"; do
--32) binary_arch=386 ;; --32) binary_arch=386 ;;
--64) binary_arch=amd64 ;; --64) binary_arch=amd64 ;;
--bin) ;; --bin) ;;
--no-bash) shells=${shells/bash/} ;;
--no-zsh) shells=${shells/zsh/} ;;
--no-fish) shells=${shells/fish/} ;;
*) *)
echo "unknown option: $opt" echo "unknown option: $opt"
help help
@@ -56,27 +71,31 @@ for opt in "$@"; do
done done
cd "$(dirname "${BASH_SOURCE[0]}")" cd "$(dirname "${BASH_SOURCE[0]}")"
fzf_base="$(pwd)" fzf_base=$(pwd)
fzf_base_esc=$(printf %q "$fzf_base")
ask() { ask() {
# If stdin is a tty, we are "interactive". while true; do
# non-interactive shell: wait for a linefeed read -p "$1 ([y]/n) " -r
# interactive shell: continue after a single keypress REPLY=${REPLY:-"y"}
read_n=$([ -t 0 ] && echo "-n 1") if [[ $REPLY =~ ^[Yy]$ ]]; then
return 1
read -p "$1 ([y]/n) " $read_n -r elif [[ $REPLY =~ ^[Nn]$ ]]; then
echo return 0
[[ $REPLY =~ ^[Nn]$ ]] fi
done
} }
check_binary() { check_binary() {
echo -n " - Checking fzf executable ... " echo -n " - Checking fzf executable ... "
local output local output
output=$("$fzf_base"/bin/fzf --version 2>&1 | awk '{print $1}') output=$("$fzf_base"/bin/fzf --version 2>&1)
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "Error: $output" echo "Error: $output"
binary_error="Invalid binary" binary_error="Invalid binary"
elif [ "$version" != "$output" ]; then else
output=${output/ */}
if [ "$version" != "$output" ]; then
echo "$output != $version" echo "$output != $version"
binary_error="Invalid version" binary_error="Invalid version"
else else
@@ -84,6 +103,7 @@ check_binary() {
binary_error="" binary_error=""
return 0 return 0
fi fi
fi
rm -f "$fzf_base"/bin/fzf rm -f "$fzf_base"/bin/fzf
return 1 return 1
} }
@@ -91,7 +111,7 @@ check_binary() {
link_fzf_in_path() { link_fzf_in_path() {
if which_fzf="$(command -v fzf)"; then if which_fzf="$(command -v fzf)"; then
echo " - Found in \$PATH" echo " - Found in \$PATH"
echo " - Creating symlink: $which_fzf -> bin/fzf" echo " - Creating symlink: bin/fzf -> $which_fzf"
(cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf) (cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf)
check_binary && return check_binary && return
fi fi
@@ -160,12 +180,13 @@ binary_error=""
case "$archi" in case "$archi" in
Darwin\ *64) download fzf-$version-darwin_${binary_arch:-amd64}.tgz ;; Darwin\ *64) download fzf-$version-darwin_${binary_arch:-amd64}.tgz ;;
Darwin\ *86) download fzf-$version-darwin_${binary_arch:-386}.tgz ;; Darwin\ *86) download fzf-$version-darwin_${binary_arch:-386}.tgz ;;
Linux\ *64) download fzf-$version-linux_${binary_arch:-amd64}.tgz ;;
Linux\ *86) download fzf-$version-linux_${binary_arch:-386}.tgz ;;
Linux\ armv5*) download fzf-$version-linux_${binary_arch:-arm5}.tgz ;; Linux\ armv5*) download fzf-$version-linux_${binary_arch:-arm5}.tgz ;;
Linux\ armv6*) download fzf-$version-linux_${binary_arch:-arm6}.tgz ;; Linux\ armv6*) download fzf-$version-linux_${binary_arch:-arm6}.tgz ;;
Linux\ armv7*) download fzf-$version-linux_${binary_arch:-arm7}.tgz ;; Linux\ armv7*) download fzf-$version-linux_${binary_arch:-arm7}.tgz ;;
Linux\ armv8*) download fzf-$version-linux_${binary_arch:-arm8}.tgz ;; Linux\ armv8*) download fzf-$version-linux_${binary_arch:-arm8}.tgz ;;
Linux\ aarch64*) download fzf-$version-linux_${binary_arch:-arm8}.tgz ;;
Linux\ *64) download fzf-$version-linux_${binary_arch:-amd64}.tgz ;;
Linux\ *86) download fzf-$version-linux_${binary_arch:-386}.tgz ;;
FreeBSD\ *64) download fzf-$version-freebsd_${binary_arch:-amd64}.tgz ;; FreeBSD\ *64) download fzf-$version-freebsd_${binary_arch:-amd64}.tgz ;;
FreeBSD\ *86) download fzf-$version-freebsd_${binary_arch:-386}.tgz ;; FreeBSD\ *86) download fzf-$version-freebsd_${binary_arch:-386}.tgz ;;
OpenBSD\ *64) download fzf-$version-openbsd_${binary_arch:-amd64}.tgz ;; OpenBSD\ *64) download fzf-$version-openbsd_${binary_arch:-amd64}.tgz ;;
@@ -173,6 +194,10 @@ case "$archi" in
CYGWIN*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;; CYGWIN*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;;
MINGW*\ *86) download fzf-$version-windows_${binary_arch:-386}.zip ;; MINGW*\ *86) download fzf-$version-windows_${binary_arch:-386}.zip ;;
MINGW*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;; MINGW*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;;
MSYS*\ *86) download fzf-$version-windows_${binary_arch:-386}.zip ;;
MSYS*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;;
Windows*\ *86) download fzf-$version-windows_${binary_arch:-386}.zip ;;
Windows*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;;
*) binary_available=0 binary_error=1 ;; *) binary_available=0 binary_error=1 ;;
esac esac
@@ -204,6 +229,17 @@ fi
[[ "$*" =~ "--bin" ]] && exit 0 [[ "$*" =~ "--bin" ]] && exit 0
for s in $shells; do
if ! command -v "$s" > /dev/null; then
shells=${shells/$s/}
fi
done
if [[ ${#shells} -lt 3 ]]; then
echo "No shell configuration to be updated."
exit 0
fi
# Auto-completion # Auto-completion
if [ -z "$auto_completion" ]; then if [ -z "$auto_completion" ]; then
ask "Do you want to enable fuzzy auto-completion?" ask "Do you want to enable fuzzy auto-completion?"
@@ -217,11 +253,10 @@ if [ -z "$key_bindings" ]; then
fi fi
echo echo
has_zsh=$(command -v zsh > /dev/null && echo 1 || echo 0)
shells=$([ $has_zsh -eq 1 ] && echo "bash zsh" || echo "bash")
for shell in $shells; do for shell in $shells; do
echo -n "Generate ~/.fzf.$shell ... " [[ "$shell" = fish ]] && continue
src=~/.fzf.${shell} src=${prefix_expand}.${shell}
echo -n "Generate $src ... "
fzf_completion="[[ \$- == *i* ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null" fzf_completion="[[ \$- == *i* ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null"
if [ $auto_completion -eq 0 ]; then if [ $auto_completion -eq 0 ]; then
@@ -233,11 +268,11 @@ for shell in $shells; do
fzf_key_bindings="# $fzf_key_bindings" fzf_key_bindings="# $fzf_key_bindings"
fi fi
cat > $src << EOF cat > "$src" << EOF
# Setup fzf # Setup fzf
# --------- # ---------
if [[ ! "\$PATH" == *$fzf_base/bin* ]]; then if [[ ! "\$PATH" == *$fzf_base_esc/bin* ]]; then
export PATH="\$PATH:$fzf_base/bin" export PATH="\${PATH:+\${PATH}:}$fzf_base/bin"
fi fi
# Auto-completion # Auto-completion
@@ -247,28 +282,26 @@ $fzf_completion
# Key bindings # Key bindings
# ------------ # ------------
$fzf_key_bindings $fzf_key_bindings
EOF EOF
echo "OK" echo "OK"
done done
# fish # fish
has_fish=$(command -v fish > /dev/null && echo 1 || echo 0) if [[ "$shells" =~ fish ]]; then
if [ $has_fish -eq 1 ]; then
echo -n "Update fish_user_paths ... " echo -n "Update fish_user_paths ... "
fish << EOF fish << EOF
echo \$fish_user_paths | grep $fzf_base/bin > /dev/null echo \$fish_user_paths | \grep "$fzf_base"/bin > /dev/null
or set --universal fish_user_paths \$fish_user_paths $fzf_base/bin or set --universal fish_user_paths \$fish_user_paths "$fzf_base"/bin
EOF EOF
[ $? -eq 0 ] && echo "OK" || echo "Failed" [ $? -eq 0 ] && echo "OK" || echo "Failed"
mkdir -p ~/.config/fish/functions mkdir -p "${fish_dir}/functions"
if [ -e ~/.config/fish/functions/fzf.fish ]; then if [ -e "${fish_dir}/functions/fzf.fish" ]; then
echo -n "Remove unnecessary ~/.config/fish/functions/fzf.fish ... " echo -n "Remove unnecessary ${fish_dir}/functions/fzf.fish ... "
rm -f ~/.config/fish/functions/fzf.fish && echo "OK" || echo "Failed" rm -f "${fish_dir}/functions/fzf.fish" && echo "OK" || echo "Failed"
fi fi
fish_binding=~/.config/fish/functions/fzf_key_bindings.fish fish_binding="${fish_dir}/functions/fzf_key_bindings.fish"
if [ $key_bindings -ne 0 ]; then if [ $key_bindings -ne 0 ]; then
echo -n "Symlink $fish_binding ... " echo -n "Symlink $fish_binding ... "
ln -sf "$fzf_base/shell/key-bindings.fish" \ ln -sf "$fzf_base/shell/key-bindings.fish" \
@@ -288,20 +321,22 @@ append_line() {
line="$2" line="$2"
file="$3" file="$3"
pat="${4:-}" pat="${4:-}"
lno=""
echo "Update $file:" echo "Update $file:"
echo " - $line" echo " - $line"
[ -f "$file" ] || touch "$file" if [ -f "$file" ]; then
if [ $# -lt 4 ]; then if [ $# -lt 4 ]; then
lno=$(\grep -nF "$line" "$file" | sed 's/:.*//' | tr '\n' ' ') lno=$(\grep -nF "$line" "$file" | sed 's/:.*//' | tr '\n' ' ')
else else
lno=$(\grep -nF "$pat" "$file" | sed 's/:.*//' | tr '\n' ' ') lno=$(\grep -nF "$pat" "$file" | sed 's/:.*//' | tr '\n' ' ')
fi fi
fi
if [ -n "$lno" ]; then if [ -n "$lno" ]; then
echo " - Already exists: line #$lno" echo " - Already exists: line #$lno"
else else
if [ $update -eq 1 ]; then if [ $update -eq 1 ]; then
echo >> "$file" [ -f "$file" ] && echo >> "$file"
echo "$line" >> "$file" echo "$line" >> "$file"
echo " + Added" echo " + Added"
else else
@@ -330,12 +365,13 @@ if [ $update_config -eq 2 ]; then
fi fi
echo echo
for shell in $shells; do for shell in $shells; do
[[ "$shell" = fish ]] && continue
[ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc [ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc
append_line $update_config "[ -f ~/.fzf.${shell} ] && source ~/.fzf.${shell}" "$dest" "~/.fzf.${shell}" append_line $update_config "[ -f ${prefix}.${shell} ] && source ${prefix}.${shell}" "$dest" "${prefix}.${shell}"
done done
if [ $key_bindings -eq 1 ] && [ $has_fish -eq 1 ]; then if [ $key_bindings -eq 1 ] && [[ "$shells" =~ fish ]]; then
bind_file=~/.config/fish/functions/fish_user_key_bindings.fish bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
if [ ! -e "$bind_file" ]; then if [ ! -e "$bind_file" ]; then
create_file "$bind_file" \ create_file "$bind_file" \
'function fish_user_key_bindings' \ 'function fish_user_key_bindings' \
@@ -348,9 +384,9 @@ fi
if [ $update_config -eq 1 ]; then if [ $update_config -eq 1 ]; then
echo 'Finished. Restart your shell or reload config file.' echo 'Finished. Restart your shell or reload config file.'
echo ' source ~/.bashrc # bash' [[ "$shells" =~ bash ]] && echo ' source ~/.bashrc # bash'
[ $has_zsh -eq 1 ] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh" [[ "$shells" =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh"
[ $has_fish -eq 1 ] && [ $key_bindings -eq 1 ] && echo ' fzf_key_bindings # fish' [[ "$shells" =~ fish ]] && [ $key_bindings -eq 1 ] && echo ' fzf_key_bindings # fish'
echo echo
echo 'Use uninstall script to remove fzf.' echo 'Use uninstall script to remove fzf.'
echo echo

View File

@@ -1,7 +1,7 @@
.ig .ig
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2017 Junegunn Choi Copyright (c) 2019 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -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-tmux 1 "Aug 2017" "fzf 0.17.0" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "Dec 2019" "fzf 0.20.0" "fzf-tmux - open fzf in tmux split pane"
.SH NAME .SH NAME
fzf-tmux - open fzf in tmux split pane fzf-tmux - open fzf in tmux split pane

View File

@@ -1,7 +1,7 @@
.ig .ig
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2017 Junegunn Choi Copyright (c) 2019 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -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 "Aug 2017" "fzf 0.17.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "Dec 2019" "fzf 0.20.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -70,6 +70,10 @@ 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)
.TP
.BI "--phony"
Do not perform search. With this option, fzf becomes a simple selector
interface rather than a "fuzzy finder".
.SS Search result .SS Search result
.TP .TP
.B "+s, --no-sort" .B "+s, --no-sort"
@@ -79,7 +83,8 @@ Do not sort the result
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
.TP .TP
.BI "--tiebreak=" "CRI[,..]" .BI "--tiebreak=" "CRI[,..]"
@@ -109,7 +114,8 @@ Comma-separated list of sort criteria to apply when the scores are tied.
.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. It optionally takes an integer argument
which denotes the maximum number of items that can be selected.
.TP .TP
.B "+m, --no-multi" .B "+m, --no-multi"
Disable multi-select Disable multi-select
@@ -118,8 +124,8 @@ Disable multi-select
Disable mouse Disable mouse
.TP .TP
.BI "--bind=" "KEYBINDS" .BI "--bind=" "KEYBINDS"
Comma-separated list of custom key bindings. See \fBKEY BINDINGS\fR for the Comma-separated list of custom key bindings. See \fBKEY/EVENT BINDINGS\fR for
details. the details.
.TP .TP
.B "--cycle" .B "--cycle"
Enable cyclic scroll Enable cyclic scroll
@@ -155,12 +161,30 @@ the full screen.
.BI "--min-height=" "HEIGHT" .BI "--min-height=" "HEIGHT"
Minimum height when \fB--height\fR is given in percent (default: 10). Minimum height when \fB--height\fR is given in percent (default: 10).
Ignored when \fB--height\fR is not specified. Ignored when \fB--height\fR is not specified.
.TP
.BI "--layout=" "LAYOUT"
Choose the layout (default: default)
.br
.BR default " Display from the bottom of the screen"
.br
.BR reverse " Display from the top of the screen"
.br
.BR reverse-list " Display from the top of the screen, prompt at the bottom"
.br
.TP .TP
.B "--reverse" .B "--reverse"
Reverse orientation A synonym for \fB--layout=reverse\fB
.TP .TP
.B "--border" .B "--border"
Draw border above and below the finder Draw border above and below the finder
.TP
.B "--no-unicode"
Use ASCII characters instead of Unicode box drawing characters to draw border
.TP .TP
.BI "--margin=" MARGIN .BI "--margin=" MARGIN
Comma-separated expression for margins around the finder. Comma-separated expression for margins around the finder.
@@ -183,19 +207,33 @@ terminal size with \fB%\fR suffix.
.br .br
.br .br
e.g. \fBfzf --margin 10%\fR e.g.
\fBfzf --margin 1,5%\fR \fBfzf --margin 10%
fzf --margin 1,5%\fR
.RE .RE
.TP .TP
.B "--inline-info" .BI "--info=" "STYLE"
Display finder info inline with the query Determines the display style of finder info.
.br
.BR default " Display on the next line to the prompt"
.br
.BR inline " Display on the same line"
.br
.BR hidden " Do not display finder info"
.br
.TP
.B "--no-info"
A synonym for \fB--info=hidden\fB
.TP .TP
.BI "--prompt=" "STR" .BI "--prompt=" "STR"
Input prompt (default: '> ') Input prompt (default: '> ')
.TP .TP
.BI "--header=" "STR" .BI "--header=" "STR"
The given string will be printed as the sticky header. The lines are displayed 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 in the given order from top to bottom regardless of \fB--layout\fR option, and
are not affected by \fB--with-nth\fR. ANSI color codes are processed even when are not affected by \fB--with-nth\fR. ANSI color codes are processed even when
\fB--ansi\fR is not set. \fB--ansi\fR is not set.
.TP .TP
@@ -217,11 +255,6 @@ color mappings. Ansi color code of -1 denotes terminal default
foreground/background color. You can also specify 24-bit color in \fB#rrggbb\fR foreground/background color. You can also specify 24-bit color in \fB#rrggbb\fR
format. format.
.RS
e.g. \fBfzf --color=bg+:24\fR
\fBfzf --color=light,fg:232,bg:255,bg+:116,info:27\fR
.RE
.RS .RS
.B BASE SCHEME: .B BASE SCHEME:
(default: dark on 256-color terminal, otherwise 16) (default: dark on 256-color terminal, otherwise 16)
@@ -229,14 +262,17 @@ e.g. \fBfzf --color=bg+:24\fR
\fBdark \fRColor scheme for dark 256-color terminal \fBdark \fRColor scheme for dark 256-color terminal
\fBlight \fRColor scheme for light 256-color terminal \fBlight \fRColor scheme for light 256-color terminal
\fB16 \fRColor scheme for 16-color terminal \fB16 \fRColor scheme for 16-color terminal
\fBbw \fRNo colors \fBbw \fRNo colors (equivalent to \fB--no-color\fR)
.B COLOR: .B COLOR:
\fBfg \fRText \fBfg \fRText
\fBbg \fRBackground \fBbg \fRBackground
\fBpreview-fg \fRPreview window text
\fBpreview-bg \fRPreview window background
\fBhl \fRHighlighted substrings \fBhl \fRHighlighted substrings
\fBfg+ \fRText (current line) \fBfg+ \fRText (current line)
\fBbg+ \fRBackground (current line) \fBbg+ \fRBackground (current line)
\fBgutter \fRGutter on the left (defaults to \fBbg+\fR)
\fBhl+ \fRHighlighted substrings (current line) \fBhl+ \fRHighlighted substrings (current line)
\fBinfo \fRInfo \fBinfo \fRInfo
\fBborder \fRBorder of the preview window and horizontal separators (\fB--border\fR) \fBborder \fRBorder of the preview window and horizontal separators (\fB--border\fR)
@@ -245,6 +281,19 @@ e.g. \fBfzf --color=bg+:24\fR
\fBmarker \fRMulti-select marker \fBmarker \fRMulti-select marker
\fBspinner \fRStreaming input indicator \fBspinner \fRStreaming input indicator
\fBheader \fRHeader \fBheader \fRHeader
.B EXAMPLES:
\fB# Seoul256 theme with 8-bit colors
# (https://github.com/junegunn/seoul256.vim)
fzf --color='bg:237,bg+:236,info:143,border:240,spinner:108' \\
--color='hl:65,fg:252,header:65,fg+:252' \\
--color='pointer:161,marker:168,prompt:110,hl+:108'
# Seoul256 theme with 24-bit colors
fzf --color='bg:#4B4B4B,bg+:#3F3F3F,info:#BDBB72,border:#6B6B6B,spinner:#98BC99' \\
--color='hl:#719872,fg:#D9D9D9,header:#719872,fg+:#D9D9D9' \\
--color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'\fR
.RE .RE
.TP .TP
.B "--no-bold" .B "--no-bold"
@@ -272,23 +321,50 @@ string, specify field index expressions between the braces (See \fBFIELD INDEX
EXPRESSION\fR for the details). EXPRESSION\fR for the details).
.RS .RS
e.g. \fBfzf --preview="head -$LINES {}"\fR e.g.
\fBls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR \fBfzf --preview='head -$LINES {}'
ls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR
fzf exports \fB$FZF_PREVIEW_LINES\fR and \fB$FZF_PREVIEW_COLUMNS\fR so that
they represent the exact size of the preview window. (It also overrides
\fB$LINES\fR and \fB$COLUMNS\fR with the same values but they can be reset
by the default shell, so prefer to refer to the ones with \fBFZF_PREVIEW_\fR
prefix.)
A placeholder expression starting with \fB+\fR flag will be replaced to the A placeholder expression starting with \fB+\fR flag will be replaced to the
space-separated list of the selected lines (or the current line if no selection space-separated list of the selected lines (or the current line if no selection
was made) individually quoted. was made) individually quoted.
e.g. \fBfzf --multi --preview="head -10 {+}"\fR e.g.
\fBgit log --oneline | fzf --multi --preview 'git show {+1}'\fR \fBfzf --multi --preview='head -10 {+}'
git log --oneline | fzf --multi --preview 'git show {+1}'\fR
Also, \fB{q}\fR is replaced to the current query string. When using a field index expression, leading and trailing whitespace is stripped
from the replacement string. To preserve the whitespace, use the \fBs\fR flag.
Also, \fB{q}\fR is replaced to the current query string, and \fB{n}\fR is
replaced to zero-based ordinal index of the line. Use \fB{+n}\fR if you want
all index numbers when multiple lines are selected.
A placeholder expression with \fBf\fR flag is replaced to the path of
a temporary file that holds the evaluated list. This is useful when you
multi-select a large number of items and the length of the evaluated string may
exceed \fBARG_MAX\fR.
e.g.
\fB# Press CTRL-A to select 100K items and see the sum of all the numbers.
# This won't work properly without 'f' flag due to ARG_MAX limit.
seq 100000 | fzf --multi --bind ctrl-a:select-all \\
--preview "awk '{sum+=\$1} END {print sum}' {+f}"\fR
Note that you can escape a placeholder pattern by prepending a backslash. Note that you can escape a placeholder pattern by prepending a backslash.
Preview window will be updated even when there is no match for the current
query if any of the placeholder expressions evaluates to a non-empty string.
.RE .RE
.TP .TP
.BI "--preview-window=" "[POSITION][:SIZE[%]][:wrap][:hidden]" .BI "--preview-window=" "[POSITION][:SIZE[%]][:noborder][:wrap][:hidden]"
Determine the layout of the preview window. If the argument ends with Determines the layout of the preview window. If the argument contains
\fB:hidden\fR, the preview window will be hidden by default until \fB:hidden\fR, the preview window will be hidden by default until
\fBtoggle-preview\fR action is triggered. Long lines are truncated by default. \fBtoggle-preview\fR action is triggered. Long lines are truncated by default.
Line wrap can be enabled with \fB:wrap\fR flag. Line wrap can be enabled with \fB:wrap\fR flag.
@@ -305,8 +381,9 @@ execute the command in the background.
.RE .RE
.RS .RS
e.g. \fBfzf --preview="head {}" --preview-window=up:30%\fR e.g.
\fBfzf --preview="file {}" --preview-window=down:1\fR \fBfzf --preview="head {}" --preview-window=up:30%
fzf --preview="file {}" --preview-window=down:1\fR
.RE .RE
.SS Scripting .SS Scripting
.TP .TP
@@ -336,7 +413,8 @@ times, fzf will expect the union of the keys. \fB--no-expect\fR will clear the
list. list.
.RS .RS
e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s --expect=f1,f2,~,@\fR e.g.
\fBfzf --expect=ctrl-v,ctrl-t,alt-s --expect=f1,f2,~,@\fR
.RE .RE
.TP .TP
.B "--read0" .B "--read0"
@@ -368,7 +446,8 @@ Note that most options have the opposite versions with \fB--no-\fR prefix.
.SH ENVIRONMENT VARIABLES .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. On *nix systems, fzf runs the command
with \fBsh -c\fR, so make sure that it's POSIX-compliant.
.TP .TP
.B FZF_DEFAULT_OPTS .B FZF_DEFAULT_OPTS
Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR
@@ -441,62 +520,127 @@ 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 .SH KEY/EVENT BINDINGS
You can customize key bindings of fzf with \fB--bind\fR option which takes \fB--bind\fR option allows you to bind \fBa key\fR or \fBan event\fR to one or
a comma-separated list of key binding expressions. Each key binding expression more \fBactions\fR. You can use it to customize key bindings or implement
follows the following format: \fBKEY:ACTION\fR dynamic behaviors.
e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR \fB--bind\fR takes a comma-separated list of binding expressions. Each binding
expression is \fBKEY:ACTION\fR or \fBEVENT:ACTION\fR.
.B AVAILABLE KEYS: (SYNONYMS) e.g.
\fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
.SS AVAILABLE KEYS: (SYNONYMS)
\fIctrl-[a-z]\fR \fIctrl-[a-z]\fR
.br
\fIctrl-space\fR \fIctrl-space\fR
.br
\fIctrl-\\\fR
.br
\fIctrl-]\fR
.br
\fIctrl-^\fR (\fIctrl-6\fR)
.br
\fIctrl-/\fR (\fIctrl-_\fR)
.br
\fIctrl-alt-[a-z]\fR \fIctrl-alt-[a-z]\fR
.br
\fIalt-[a-z]\fR \fIalt-[a-z]\fR
.br
\fIalt-[0-9]\fR \fIalt-[0-9]\fR
.br
\fIf[1-12]\fR \fIf[1-12]\fR
.br
\fIenter\fR (\fIreturn\fR \fIctrl-m\fR) \fIenter\fR (\fIreturn\fR \fIctrl-m\fR)
.br
\fIspace\fR \fIspace\fR
.br
\fIbspace\fR (\fIbs\fR) \fIbspace\fR (\fIbs\fR)
.br
\fIalt-up\fR
.br
\fIalt-down\fR
.br
\fIalt-left\fR
.br
\fIalt-right\fR
.br
\fIalt-enter\fR \fIalt-enter\fR
.br
\fIalt-space\fR \fIalt-space\fR
.br
\fIalt-bspace\fR (\fIalt-bs\fR) \fIalt-bspace\fR (\fIalt-bs\fR)
.br
\fIalt-/\fR \fIalt-/\fR
.br
\fItab\fR \fItab\fR
.br
\fIbtab\fR (\fIshift-tab\fR) \fIbtab\fR (\fIshift-tab\fR)
.br
\fIesc\fR \fIesc\fR
.br
\fIdel\fR \fIdel\fR
.br
\fIup\fR \fIup\fR
.br
\fIdown\fR \fIdown\fR
.br
\fIleft\fR \fIleft\fR
.br
\fIright\fR \fIright\fR
.br
\fIhome\fR \fIhome\fR
.br
\fIend\fR \fIend\fR
.br
\fIpgup\fR (\fIpage-up\fR) \fIpgup\fR (\fIpage-up\fR)
.br
\fIpgdn\fR (\fIpage-down\fR) \fIpgdn\fR (\fIpage-down\fR)
.br
\fIshift-up\fR
.br
\fIshift-down\fR
.br
\fIshift-left\fR \fIshift-left\fR
.br
\fIshift-right\fR \fIshift-right\fR
.br
\fIleft-click\fR
.br
\fIright-click\fR
.br
\fIdouble-click\fR \fIdouble-click\fR
.br
or any single character or any single character
Additionally, a special event named \fIchange\fR is available which is .SS AVAILABLE EVENTS:
triggered whenever the query string is changed. \fIchange\fR (triggered whenever the query string is changed)
.br
e.g. \fBfzf --bind change:top\fR e.g.
\fB# Moves cursor to the top (or bottom depending on --layout) whenever the query is changed
fzf --bind change:top\fR
.SS AVAILABLE ACTIONS:
A key or an event can be bound to one or more of the following actions.
\fBACTION: DEFAULT BINDINGS (NOTES): \fBACTION: DEFAULT BINDINGS (NOTES):
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIenter double-click\fR \fBaccept\fR \fIenter double-click\fR
\fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
\fBbackward-char\fR \fIctrl-b left\fR \fBbackward-char\fR \fIctrl-b left\fR
\fBbackward-delete-char\fR \fIctrl-h bspace\fR \fBbackward-delete-char\fR \fIctrl-h bspace\fR
\fBbackward-kill-word\fR \fIalt-bs\fR \fBbackward-kill-word\fR \fIalt-bs\fR
\fBbackward-word\fR \fIalt-b shift-left\fR \fBbackward-word\fR \fIalt-b shift-left\fR
\fBbeginning-of-line\fR \fIctrl-a home\fR \fBbeginning-of-line\fR \fIctrl-a home\fR
\fBcancel\fR \fBcancel\fR (clear query string if not empty, abort fzf otherwise)
\fBclear-screen\fR \fIctrl-l\fR \fBclear-screen\fR \fIctrl-l\fR
\fBclear-selection\fR (clear multi-selection)
\fBclear-query\fR (clear query string)
\fBdelete-char\fR \fIdel\fR \fBdelete-char\fR \fIdel\fR
\fBdelete-char/eof\fR \fIctrl-d\fR \fBdelete-char/eof\fR \fIctrl-d\fR
\fBdeselect-all\fR \fBdeselect-all\fR (deselect all matches)
\fBdown\fR \fIctrl-j ctrl-n down\fR \fBdown\fR \fIctrl-j ctrl-n down\fR
\fBend-of-line\fR \fIctrl-e end\fR \fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details) \fBexecute(...)\fR (see below for the details)
@@ -514,18 +658,20 @@ triggered whenever the query string is changed.
\fBpage-up\fR \fIpgup\fR \fBpage-up\fR \fIpgup\fR
\fBhalf-page-down\fR \fBhalf-page-down\fR
\fBhalf-page-up\fR \fBhalf-page-up\fR
\fBpreview-down\fR \fBpreview-down\fR \fIshift-down\fR
\fBpreview-up\fR \fBpreview-up\fR \fIshift-up\fR
\fBpreview-page-down\fR \fBpreview-page-down\fR
\fBpreview-page-up\fR \fBpreview-page-up\fR
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR) \fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBprint-query\fR (print query and exit) \fBprint-query\fR (print query and exit)
\fBselect-all\fR \fBreload(...)\fR (see below for the details)
\fBtoggle\fR \fBreplace-query\fR (replace query string with the current selection)
\fBtoggle-all\fR \fBselect-all\fR (select all matches)
\fBtoggle\fR (\fIright-click\fR)
\fBtoggle-all\fR (toggle all matches)
\fBtoggle+down\fR \fIctrl-i (tab)\fR \fBtoggle+down\fR \fIctrl-i (tab)\fR
\fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR) \fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
\fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR) \fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
\fBtoggle-preview\fR \fBtoggle-preview\fR
\fBtoggle-preview-wrap\fR \fBtoggle-preview-wrap\fR
\fBtoggle-sort\fR \fBtoggle-sort\fR
@@ -536,10 +682,15 @@ triggered whenever the query string is changed.
\fBup\fR \fIctrl-k ctrl-p up\fR \fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR \fByank\fR \fIctrl-y\fR
.SS ACTION COMPOSITION
Multiple actions can be chained using \fB+\fR separator. Multiple actions can be chained using \fB+\fR separator.
e.g.
\fBfzf --bind 'ctrl-a:select-all+accept'\fR \fBfzf --bind 'ctrl-a:select-all+accept'\fR
.SS COMMAND EXECUTION
With \fBexecute(...)\fR action, you can execute arbitrary commands without With \fBexecute(...)\fR action, you can execute arbitrary commands without
leaving fzf. For example, you can turn fzf into a simple file browser by leaving fzf. For example, you can turn fzf into a simple file browser by
binding \fBenter\fR key to \fBless\fR command like follows. binding \fBenter\fR key to \fBless\fR command like follows.
@@ -567,18 +718,38 @@ parse errors.
\fBexecute|...|\fR \fBexecute|...|\fR
\fBexecute:...\fR \fBexecute:...\fR
.RS .RS
This is the special form that frees you from parse errors as it does not expect The last one is the special form that frees you from parse errors as it does
the closing character. The catch is that it should be the last one in the not expect the closing character. The catch is that it should be the last one
comma-separated list of key-action pairs. in the comma-separated list of key-action pairs.
.RE .RE
fzf switches to the alternate screen when executing a command. However, if the fzf switches to the alternate screen when executing a command. However, if the
command is expected to complete quickly, and you are not interested in its command is expected to complete quickly, and you are not interested in its
output, you might want to use \fBexecute-silent\fR instead, which silently output, you might want to use \fBexecute-silent\fR instead, which silently
executes the command without the switching. Note that fzf will not be executes the command without the switching. Note that fzf will not be
responsible until the command is complete. For asynchronous execution, start responsive until the command is complete. For asynchronous execution, start
your command as a background process (i.e. appending \fB&\fR). your command as a background process (i.e. appending \fB&\fR).
.SS RELOAD INPUT
\fBreload(...)\fR action is used to dynamically update the input list
without restarting fzf. It takes the same command template with placeholder
expressions as \fBexecute(...)\fR.
See \fIhttps://github.com/junegunn/fzf/issues/1750\fR for more info.
e.g.
\fB# Update the list of processes by pressing CTRL-R
ps -ef | fzf --bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \\
--header-lines=1 --layout=reverse
# Integration with ripgrep
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="foobar"
FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \\
fzf --bind "change:reload:$RG_PREFIX {q} || true" \\
--ansi --phony --query "$INITIAL_QUERY"\fR
.SH AUTHOR .SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR) Junegunn Choi (\fIjunegunn.c@gmail.com\fR)

View File

@@ -35,6 +35,8 @@ else
let s:base_dir = expand('<sfile>:h:h') let s:base_dir = expand('<sfile>:h:h')
endif endif
if s:is_win if s:is_win
let s:term_marker = '&::FZF'
function! s:fzf_call(fn, ...) function! s:fzf_call(fn, ...)
let shellslash = &shellslash let shellslash = &shellslash
try try
@@ -47,12 +49,22 @@ if s:is_win
" Use utf-8 for fzf.vim commands " Use utf-8 for fzf.vim commands
" Return array of shell commands for cmd.exe " Return array of shell commands for cmd.exe
let s:codepage = libcallnr('kernel32.dll', 'GetACP', 0)
function! s:enc_to_cp(str)
return iconv(a:str, &encoding, 'cp'.s:codepage)
endfunction
function! s:wrap_cmds(cmds) function! s:wrap_cmds(cmds)
return ['@echo off', 'for /f "tokens=4" %%a in (''chcp'') do set origchcp=%%a', 'chcp 65001 > nul'] + return map([
\ (type(a:cmds) == type([]) ? a:cmds : [a:cmds]) + \ '@echo off',
\ ['chcp %origchcp% > nul'] \ 'setlocal enabledelayedexpansion']
\ + (has('gui_running') ? ['set TERM= > nul'] : [])
\ + (type(a:cmds) == type([]) ? a:cmds : [a:cmds])
\ + ['endlocal'],
\ '<SID>enc_to_cp(v:val."\r")')
endfunction endfunction
else else
let s:term_marker = ";#FZF"
function! s:fzf_call(fn, ...) function! s:fzf_call(fn, ...)
return call(a:fn, a:000) return call(a:fn, a:000)
endfunction endfunction
@@ -60,6 +72,10 @@ else
function! s:wrap_cmds(cmds) function! s:wrap_cmds(cmds)
return a:cmds return a:cmds
endfunction endfunction
function! s:enc_to_cp(str)
return a:str
endfunction
endif endif
function! s:shellesc_cmd(arg) function! s:shellesc_cmd(arg)
@@ -71,7 +87,7 @@ function! s:shellesc_cmd(arg)
endfunction endfunction
function! fzf#shellescape(arg, ...) function! fzf#shellescape(arg, ...)
let shell = get(a:000, 0, &shell) let shell = get(a:000, 0, s:is_win ? 'cmd.exe' : 'sh')
if shell =~# 'cmd.exe$' if shell =~# 'cmd.exe$'
return s:shellesc_cmd(a:arg) return s:shellesc_cmd(a:arg)
endif endif
@@ -227,6 +243,7 @@ function! s:common_sink(action, lines) abort
doautocmd BufEnter doautocmd BufEnter
endif endif
endfor endfor
catch /^Vim:Interrupt$/
finally finally
let &autochdir = autochdir let &autochdir = autochdir
silent! autocmd! fzf_swap silent! autocmd! fzf_swap
@@ -234,7 +251,7 @@ function! s:common_sink(action, lines) abort
endfunction endfunction
function! s:get_color(attr, ...) function! s:get_color(attr, ...)
let gui = has('termguicolors') && &termguicolors let gui = !s:is_win && !has('win32unix') && has('termguicolors') && &termguicolors
let fam = gui ? 'gui' : 'cterm' let fam = gui ? 'gui' : 'cterm'
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$' let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
for group in a:000 for group in a:000
@@ -249,7 +266,7 @@ endfunction
function! s:defaults() function! s:defaults()
let rules = copy(get(g:, 'fzf_colors', {})) let rules = copy(get(g:, 'fzf_colors', {}))
let colors = join(map(items(filter(map(rules, 'call("s:get_color", v:val)'), '!empty(v:val)')), 'join(v:val, ":")'), ',') let colors = join(map(items(filter(map(rules, 'call("s:get_color", v:val)'), '!empty(v:val)')), 'join(v:val, ":")'), ',')
return empty(colors) ? '' : ('--color='.colors) return empty(colors) ? '' : fzf#shellescape('--color='.colors)
endfunction endfunction
function! s:validate_layout(layout) function! s:validate_layout(layout)
@@ -328,17 +345,22 @@ function! fzf#wrap(...)
return opts return opts
endfunction endfunction
function! fzf#run(...) abort function! s:use_sh()
try let [shell, shellslash, shellcmdflag, shellxquote] = [&shell, &shellslash, &shellcmdflag, &shellxquote]
let oshell = &shell
let useshellslash = &shellslash
if s:is_win if s:is_win
set shell=cmd.exe set shell=cmd.exe
set noshellslash set noshellslash
let &shellcmdflag = has('nvim') ? '/s /c' : '/c'
let &shellxquote = has('nvim') ? '"' : '('
else else
set shell=sh set shell=sh
endif endif
return [shell, shellslash, shellcmdflag, shellxquote]
endfunction
function! fzf#run(...) abort
try
let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
let dict = exists('a:1') ? s:upgrade(a:1) : {} let dict = exists('a:1') ? s:upgrade(a:1) : {}
let temps = { 'result': s:fzf_tempname() } let temps = { 'result': s:fzf_tempname() }
@@ -349,7 +371,7 @@ try
throw v:exception throw v:exception
endtry endtry
if has('nvim') && !has_key(dict, 'dir') if !has_key(dict, 'dir')
let dict.dir = s:fzf_getcwd() let dict.dir = s:fzf_getcwd()
endif endif
if has('win32unix') && has_key(dict, 'dir') if has('win32unix') && has_key(dict, 'dir')
@@ -366,10 +388,10 @@ try
let source = dict.source let source = dict.source
let type = type(source) let type = type(source)
if type == 1 if type == 1
let prefix = source.'|' let prefix = '( '.source.' )|'
elseif type == 3 elseif type == 3
let temps.input = s:fzf_tempname() let temps.input = s:fzf_tempname()
call writefile(source, temps.input) call writefile(map(source, '<SID>enc_to_cp(v:val)'), temps.input)
let prefix = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input).'|' let prefix = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input).'|'
else else
throw 'Invalid source type' throw 'Invalid source type'
@@ -379,10 +401,13 @@ try
endif endif
let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0) let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0)
let use_height = has_key(dict, 'down') && let use_height = has_key(dict, 'down') && !has('gui_running') &&
\ !(has('nvim') || s:is_win || has('win32unix') || s:present(dict, 'up', 'left', 'right')) && \ !(has('nvim') || s:is_win || has('win32unix') || s:present(dict, 'up', 'left', 'right', 'window')) &&
\ executable('tput') && filereadable('/dev/tty') \ executable('tput') && filereadable('/dev/tty')
let use_term = has('nvim') && !s:is_win let has_vim8_term = has('terminal') && has('patch-8.0.995')
let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win
let use_term = has_nvim_term ||
\ has_vim8_term && !has('win32unix') && (has('gui_running') || s:is_win || !use_height && s:present(dict, 'down', 'up', 'left', 'right', 'window'))
let use_tmux = (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:tmux_enabled() && s:splittable(dict) let use_tmux = (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:tmux_enabled() && s:splittable(dict)
if prefer_tmux && use_tmux if prefer_tmux && use_tmux
let use_height = 0 let use_height = 0
@@ -405,8 +430,7 @@ try
call s:callback(dict, lines) call s:callback(dict, lines)
return lines return lines
finally finally
let &shell = oshell let [&shell, &shellslash, &shellcmdflag, &shellxquote] = [shell, shellslash, shellcmdflag, shellxquote]
let &shellslash = useshellslash
endtry endtry
endfunction endfunction
@@ -445,15 +469,18 @@ endfunction
function! s:pushd(dict) function! s:pushd(dict)
if s:present(a:dict, 'dir') if s:present(a:dict, 'dir')
let cwd = s:fzf_getcwd() let cwd = s:fzf_getcwd()
if get(a:dict, 'prev_dir', '') ==# cwd let w:fzf_pushd = {
return 1 \ 'command': haslocaldir() ? 'lcd' : (exists(':tcd') && haslocaldir(-1) ? 'tcd' : 'cd'),
endif \ 'origin': cwd,
let a:dict.prev_dir = cwd \ 'bufname': bufname('')
\ }
execute 'lcd' s:escape(a:dict.dir) execute 'lcd' s:escape(a:dict.dir)
let a:dict.dir = s:fzf_getcwd() let cwd = s:fzf_getcwd()
return 1 let w:fzf_pushd.dir = cwd
let a:dict.pushd = w:fzf_pushd
return cwd
endif endif
return 0 return ''
endfunction endfunction
augroup fzf_popd augroup fzf_popd
@@ -462,20 +489,38 @@ augroup fzf_popd
augroup END augroup END
function! s:dopopd() function! s:dopopd()
if !exists('w:fzf_dir') || s:fzf_getcwd() != w:fzf_dir[1] if !exists('w:fzf_pushd')
return return
endif endif
execute 'lcd' s:escape(w:fzf_dir[0])
unlet w:fzf_dir " FIXME: We temporarily change the working directory to 'dir' entry
" of options dictionary (set to the current working directory if not given)
" before running fzf.
"
" e.g. call fzf#run({'dir': '/tmp', 'source': 'ls', 'sink': 'e'})
"
" After processing the sink function, we have to restore the current working
" directory. But doing so may not be desirable if the function changed the
" working directory on purpose.
"
" So how can we tell if we should do it or not? A simple heuristic we use
" here is that we change directory only if the current working directory
" matches 'dir' entry. However, it is possible that the sink function did
" change the directory to 'dir'. In that case, the user will have an
" unexpected result.
if s:fzf_getcwd() ==# w:fzf_pushd.dir && (!&autochdir || w:fzf_pushd.bufname ==# bufname(''))
execute w:fzf_pushd.command s:escape(w:fzf_pushd.origin)
endif
unlet w:fzf_pushd
endfunction endfunction
function! s:xterm_launcher() function! s:xterm_launcher()
let fmt = 'xterm -T "[fzf]" -bg "\%s" -fg "\%s" -geometry %dx%d+%d+%d -e bash -ic %%s' let fmt = 'xterm -T "[fzf]" -bg "%s" -fg "%s" -geometry %dx%d+%d+%d -e bash -ic %%s'
if has('gui_macvim') if has('gui_macvim')
let fmt .= '&& osascript -e "tell application \"MacVim\" to activate"' let fmt .= '&& osascript -e "tell application \"MacVim\" to activate"'
endif endif
return printf(fmt, return printf(fmt,
\ synIDattr(hlID("Normal"), "bg"), synIDattr(hlID("Normal"), "fg"), \ escape(synIDattr(hlID("Normal"), "bg"), '#'), escape(synIDattr(hlID("Normal"), "fg"), '#'),
\ &columns, &lines/2, getwinposx(), getwinposy()) \ &columns, &lines/2, getwinposx(), getwinposy())
endfunction endfunction
unlet! s:launcher unlet! s:launcher
@@ -488,6 +533,10 @@ endif
function! s:exit_handler(code, command, ...) function! s:exit_handler(code, command, ...)
if a:code == 130 if a:code == 130
return 0 return 0
elseif has('nvim') && a:code == 129
" When deleting the terminal buffer while fzf is still running,
" Nvim sends SIGHUP.
return 0
elseif a:code > 1 elseif a:code > 1
call s:error('Error running ' . a:command) call s:error('Error running ' . a:command)
if !empty(a:000) if !empty(a:000)
@@ -524,9 +573,7 @@ function! s:execute(dict, command, use_height, temps) abort
let fzf.dict = a:dict let fzf.dict = a:dict
let fzf.temps = a:temps let fzf.temps = a:temps
function! fzf.on_exit(job_id, exit_status, event) dict function! fzf.on_exit(job_id, exit_status, event) dict
if s:present(self.dict, 'dir') call s:pushd(self.dict)
execute 'lcd' s:escape(self.dict.dir)
endif
let lines = s:collect(self.temps) let lines = s:collect(self.temps)
call s:callback(self.dict, lines) call s:callback(self.dict, lines)
endfunction endfunction
@@ -553,9 +600,10 @@ endfunction
function! s:execute_tmux(dict, command, temps) abort function! s:execute_tmux(dict, command, temps) abort
let command = a:command let command = a:command
if s:pushd(a:dict) let cwd = s:pushd(a:dict)
if len(cwd)
" -c '#{pane_current_path}' is only available on tmux 1.9 or above " -c '#{pane_current_path}' is only available on tmux 1.9 or above
let command = join(['cd', fzf#shellescape(a:dict.dir), '&&', command]) let command = join(['cd', fzf#shellescape(cwd), '&&', command])
endif endif
call system(command) call system(command)
@@ -577,8 +625,9 @@ function! s:calc_size(max, val, dict)
let srcsz = len(a:dict.source) let srcsz = len(a:dict.source)
endif endif
let opts = get(a:dict, 'options', '').$FZF_DEFAULT_OPTS let opts = $FZF_DEFAULT_OPTS.' '.s:evaluate_opts(get(a:dict, 'options', ''))
let margin = stridx(opts, '--inline-info') > stridx(opts, '--no-inline-info') ? 1 : 2 let margin = stridx(opts, '--inline-info') > stridx(opts, '--no-inline-info') ? 1 : 2
let margin += stridx(opts, '--border') > stridx(opts, '--no-border') ? 2 : 0
let margin += stridx(opts, '--header') > stridx(opts, '--no-header') let margin += stridx(opts, '--header') > stridx(opts, '--no-header')
return srcsz >= 0 ? min([srcsz + margin, size]) : size return srcsz >= 0 ? min([srcsz + margin, size]) : size
endfunction endfunction
@@ -625,6 +674,7 @@ function! s:execute_term(dict, command, temps) abort
let winrest = winrestcmd() let winrest = winrestcmd()
let pbuf = bufnr('') let pbuf = bufnr('')
let [ppos, winopts] = s:split(a:dict) let [ppos, winopts] = s:split(a:dict)
call s:use_sh()
let b:fzf = a:dict let b:fzf = a:dict
let fzf = { 'buf': bufnr(''), 'pbuf': pbuf, 'ppos': ppos, 'dict': a:dict, 'temps': a:temps, let fzf = { 'buf': bufnr(''), 'pbuf': pbuf, 'ppos': ppos, 'dict': a:dict, 'temps': a:temps,
\ 'winopts': winopts, 'winrest': winrest, 'lines': &lines, \ 'winopts': winopts, 'winrest': winrest, 'lines': &lines,
@@ -640,7 +690,7 @@ function! s:execute_term(dict, command, temps) abort
endif endif
endif endif
endfunction endfunction
function! fzf.on_exit(id, code, _event) function! fzf.on_exit(id, code, ...)
if s:getpos() == self.ppos " {'window': 'enew'} if s:getpos() == self.ppos " {'window': 'enew'}
for [opt, val] in items(self.winopts) for [opt, val] in items(self.winopts)
execute 'let' opt '=' val execute 'let' opt '=' val
@@ -675,14 +725,25 @@ function! s:execute_term(dict, command, temps) abort
endfunction endfunction
try try
if s:present(a:dict, 'dir') call s:pushd(a:dict)
execute 'lcd' s:escape(a:dict.dir) if s:is_win
let fzf.temps.batchfile = s:fzf_tempname().'.bat'
call writefile(s:wrap_cmds(a:command), fzf.temps.batchfile)
let command = fzf.temps.batchfile
else
let command = a:command
endif
let command .= s:term_marker
if has('nvim')
call termopen(command, fzf)
else
let fzf.buf = term_start([&shell, &shellcmdflag, command], {'curwin': 1, 'exit_cb': function(fzf.on_exit)})
if !has('patch-8.0.1261') && !has('nvim') && !s:is_win
call term_wait(fzf.buf, 20)
endif
endif endif
call termopen(a:command . ';#FZF', fzf)
finally finally
if s:present(a:dict, 'dir') call s:dopopd()
lcd -
endif
endtry endtry
setlocal nospell bufhidden=wipe nobuflisted nonumber setlocal nospell bufhidden=wipe nobuflisted nonumber
setf fzf setf fzf
@@ -701,21 +762,9 @@ function! s:collect(temps) abort
endfunction endfunction
function! s:callback(dict, lines) abort function! s:callback(dict, lines) abort
" Since anything can be done in the sink function, there is no telling that let popd = has_key(a:dict, 'pushd')
" the change of the working directory was made by &autochdir setting.
"
" We use the following heuristic to determine whether to restore CWD:
" - Always restore the current directory when &autochdir is disabled.
" FIXME This makes it impossible to change directory from inside the sink
" function when &autochdir is not used.
" - In case of an error or an interrupt, a:lines will be empty.
" And it will be an array of a single empty string when fzf was finished
" without a match. In these cases, we presume that the change of the
" directory is not expected and should be undone.
let popd = has_key(a:dict, 'prev_dir') &&
\ (!&autochdir || (empty(a:lines) || len(a:lines) == 1 && empty(a:lines[0])))
if popd if popd
let w:fzf_dir = [a:dict.prev_dir, a:dict.dir] let w:fzf_pushd = a:dict.pushd
endif endif
try try
@@ -739,7 +788,7 @@ function! s:callback(dict, lines) abort
" We may have opened a new window or tab " We may have opened a new window or tab
if popd if popd
let w:fzf_dir = [a:dict.prev_dir, a:dict.dir] let w:fzf_pushd = a:dict.pushd
call s:dopopd() call s:dopopd()
endif endif
endfunction endfunction
@@ -750,7 +799,10 @@ let s:default_action = {
\ 'ctrl-v': 'vsplit' } \ 'ctrl-v': 'vsplit' }
function! s:shortpath() function! s:shortpath()
let short = pathshorten(fnamemodify(getcwd(), ':~:.')) let short = fnamemodify(getcwd(), ':~:.')
if !has('win32unix')
let short = pathshorten(short)
endif
let slash = (s:is_win && !&shellslash) ? '\' : '/' let slash = (s:is_win && !&shellslash) ? '\' : '/'
return empty(short) ? '~'.slash : short . (short =~ escape(slash, '\').'$' ? '' : slash) return empty(short) ? '~'.slash : short . (short =~ escape(slash, '\').'$' ? '' : slash)
endfunction endfunction
@@ -767,6 +819,7 @@ function! s:cmd(bang, ...) abort
else else
let prompt = s:shortpath() let prompt = s:shortpath()
endif endif
let prompt = strwidth(prompt) < &columns - 20 ? prompt : '> '
call extend(opts.options, ['--prompt', prompt]) call extend(opts.options, ['--prompt', prompt])
call extend(opts.options, args) call extend(opts.options, args)
call fzf#run(fzf#wrap('FZF', opts, a:bang)) call fzf#run(fzf#wrap('FZF', opts, a:bang))

View File

@@ -1,4 +1,3 @@
#!/bin/bash
# ____ ____ # ____ ____
# / __/___ / __/ # / __/___ / __/
# / /_/_ / / /_ # / /_/_ / / /_
@@ -10,12 +9,14 @@
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
if [[ $- =~ i ]]; then
# To use custom commands instead of find, override _fzf_compgen_{path,dir} # To use custom commands instead of find, override _fzf_compgen_{path,dir}
if ! declare -f _fzf_compgen_path > /dev/null; then if ! declare -f _fzf_compgen_path > /dev/null; then
_fzf_compgen_path() { _fzf_compgen_path() {
echo "$1" echo "$1"
command find -L "$1" \ command find -L "$1" \
-name .git -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \ -name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
} }
fi fi
@@ -23,7 +24,7 @@ fi
if ! declare -f _fzf_compgen_dir > /dev/null; then if ! declare -f _fzf_compgen_dir > /dev/null; then
_fzf_compgen_dir() { _fzf_compgen_dir() {
command find -L "$1" \ command find -L "$1" \
-name .git -prune -o -name .svn -prune -o -type d \ -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
} }
fi fi
@@ -38,9 +39,9 @@ __fzfcmd_complete() {
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
} }
_fzf_orig_completion_filter() { __fzf_orig_completion_filter() {
sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2";/' | sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2"; [[ "\1" = *" -o nospace "* ]] \&\& [[ ! "$__fzf_nospace_commands" = *" \3 "* ]] \&\& __fzf_nospace_commands="$__fzf_nospace_commands \3 ";/' |
awk -F= '{gsub(/[^A-Za-z0-9_= ;]/, "_", $1); print $1"="$2}' awk -F= '{OFS = FS} {gsub(/[^A-Za-z0-9_= ;]/, "_", $1);}1'
} }
_fzf_opts_completion() { _fzf_opts_completion() {
@@ -113,7 +114,7 @@ _fzf_opts_completion() {
} }
_fzf_handle_dynamic_completion() { _fzf_handle_dynamic_completion() {
local cmd orig_var orig ret orig_cmd local cmd orig_var orig ret orig_cmd orig_complete
cmd="$1" cmd="$1"
shift shift
orig_cmd="$1" orig_cmd="$1"
@@ -122,10 +123,18 @@ _fzf_handle_dynamic_completion() {
if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then
$orig "$@" $orig "$@"
elif [ -n "$_fzf_completion_loader" ]; then elif [ -n "$_fzf_completion_loader" ]; then
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
_completion_loader "$@" _completion_loader "$@"
ret=$? ret=$?
eval "$(complete | command grep "\-F.* $orig_cmd$" | _fzf_orig_completion_filter)" # _completion_loader may not have updated completion for the command
source "${BASH_SOURCE[0]}" if [ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]; then
eval "$(complete | command grep " -F.* $orig_cmd$" | __fzf_orig_completion_filter)"
if [[ "$__fzf_nospace_commands" = *" $orig_cmd "* ]]; then
eval "${orig_complete/ -F / -o nospace -F }"
else
eval "$orig_complete"
fi
fi
return $ret return $ret
fi fi
} }
@@ -141,7 +150,7 @@ __fzf_generic_path_completion() {
base=${cur:0:${#cur}-${#trigger}} base=${cur:0:${#cur}-${#trigger}}
eval "base=$base" eval "base=$base"
dir="$base" [[ $base = *"/"* ]] && dir="$base"
while true; do while true; do
if [ -z "$dir" ] || [ -d "$dir" ]; then if [ -z "$dir" ] || [ -d "$dir" ]; then
leftover=${base/#"$dir"} leftover=${base/#"$dir"}
@@ -152,6 +161,7 @@ __fzf_generic_path_completion() {
printf "%q$3 " "$item" printf "%q$3 " "$item"
done) done)
matches=${matches% } matches=${matches% }
[[ -z "$3" ]] && [[ "$__fzf_nospace_commands" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
if [ -n "$matches" ]; then if [ -n "$matches" ]; then
COMPREPLY=( "$matches" ) COMPREPLY=( "$matches" )
else else
@@ -185,12 +195,13 @@ _fzf_complete() {
selected=$(cat | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $1 -q "$cur" | $post | tr '\n' ' ') selected=$(cat | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $1 -q "$cur" | $post | tr '\n' ' ')
selected=${selected% } # Strip trailing space not to repeat "-o nospace" selected=${selected% } # Strip trailing space not to repeat "-o nospace"
printf '\e[5n'
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
COMPREPLY=("$selected") COMPREPLY=("$selected")
return 0 else
COMPREPLY=("$cur")
fi fi
printf '\e[5n'
return 0
else else
shift shift
_fzf_handle_dynamic_completion "$cmd" "$@" _fzf_handle_dynamic_completion "$cmd" "$@"
@@ -215,7 +226,7 @@ _fzf_complete_kill() {
local selected fzf local selected fzf
fzf="$(__fzfcmd_complete)" fzf="$(__fzfcmd_complete)"
selected=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" $fzf -m | awk '{print $2}' | tr '\n' ' ') selected=$(command ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" $fzf -m | awk '{print $2}' | tr '\n' ' ')
printf '\e[5n' printf '\e[5n'
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
@@ -233,8 +244,8 @@ _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 | command grep -i '^host' | command grep -v '*') \ cat <(cat ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \
<(command grep -oE '^[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ <(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
awk '{if (length($2) > 0) {print $2}}' | sort -u awk '{if (length($2) > 0) {print $2}}' | sort -u
) )
@@ -274,15 +285,15 @@ a_cmds="
x_cmds="kill ssh telnet unset unalias export" x_cmds="kill ssh telnet unset unalias export"
# Preserve existing completion # Preserve existing completion
eval $(complete | eval "$(complete |
sed -E '/-F/!d; / _fzf/d; '"/ ($(echo $d_cmds $a_cmds $x_cmds | sed 's/ /|/g; s/+/\\+/g'))$/"'!d' | sed -E '/-F/!d; / _fzf/d; '"/ ($(echo $d_cmds $a_cmds $x_cmds | sed 's/ /|/g; s/+/\\+/g'))$/"'!d' |
_fzf_orig_completion_filter) __fzf_orig_completion_filter)"
if type _completion_loader > /dev/null 2>&1; then if type _completion_loader > /dev/null 2>&1; then
_fzf_completion_loader=1 _fzf_completion_loader=1
fi fi
_fzf_defc() { __fzf_defc() {
local cmd func opts orig_var orig def local cmd func opts orig_var orig def
cmd="$1" cmd="$1"
func="$2" func="$2"
@@ -299,16 +310,14 @@ _fzf_defc() {
# Anything # Anything
for cmd in $a_cmds; do for cmd in $a_cmds; do
_fzf_defc "$cmd" _fzf_path_completion "-o default -o bashdefault" __fzf_defc "$cmd" _fzf_path_completion "-o default -o bashdefault"
done done
# Directory # Directory
for cmd in $d_cmds; do for cmd in $d_cmds; do
_fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames" __fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames"
done done
unset _fzf_defc
# Kill completion # Kill completion
complete -F _fzf_complete_kill -o nospace -o default -o bashdefault kill complete -F _fzf_complete_kill -o nospace -o default -o bashdefault kill
@@ -322,3 +331,23 @@ complete -F _fzf_complete_export -o default -o bashdefault export
complete -F _fzf_complete_unalias -o default -o bashdefault unalias complete -F _fzf_complete_unalias -o default -o bashdefault unalias
unset cmd d_cmds a_cmds x_cmds unset cmd d_cmds a_cmds x_cmds
_fzf_setup_completion() {
local kind fn cmd
kind=$1
fn=_fzf_${1}_completion
if [[ $# -lt 2 ]] || ! type -t "$fn" > /dev/null; then
echo "usage: ${FUNCNAME[0]} path|dir COMMANDS..."
return 1
fi
shift
for cmd in "$@"; do
eval "$(complete -p "$cmd" 2> /dev/null | grep -v "$fn" | __fzf_orig_completion_filter)"
case "$kind" in
dir) __fzf_defc "$cmd" "$fn" "-o nospace -o dirnames" ;;
*) __fzf_defc "$cmd" "$fn" "-o default -o bashdefault" ;;
esac
done
}
fi

View File

@@ -1,4 +1,3 @@
#!/bin/zsh
# ____ ____ # ____ ____
# / __/___ / __/ # / __/___ / __/
# / /_/_ / / /_ # / /_/_ / / /_
@@ -10,12 +9,14 @@
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
if [[ $- =~ i ]]; then
# To use custom commands instead of find, override _fzf_compgen_{path,dir} # To use custom commands instead of find, override _fzf_compgen_{path,dir}
if ! declare -f _fzf_compgen_path > /dev/null; then if ! declare -f _fzf_compgen_path > /dev/null; then
_fzf_compgen_path() { _fzf_compgen_path() {
echo "$1" echo "$1"
command find -L "$1" \ command find -L "$1" \
-name .git -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \ -name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
} }
fi fi
@@ -23,7 +24,7 @@ fi
if ! declare -f _fzf_compgen_dir > /dev/null; then if ! declare -f _fzf_compgen_dir > /dev/null; then
_fzf_compgen_dir() { _fzf_compgen_dir() {
command find -L "$1" \ command find -L "$1" \
-name .git -prune -o -name .svn -prune -o -type d \ -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
} }
fi fi
@@ -37,8 +38,7 @@ __fzfcmd_complete() {
__fzf_generic_path_completion() { __fzf_generic_path_completion() {
local base lbuf compgen fzf_opts suffix tail fzf dir leftover matches local base lbuf compgen fzf_opts suffix tail fzf dir leftover matches
# (Q) flag removes a quoting level: "foo\ bar" => "foo bar" base=$1
base=${(Q)1}
lbuf=$2 lbuf=$2
compgen=$3 compgen=$3
fzf_opts=$4 fzf_opts=$4
@@ -47,14 +47,14 @@ __fzf_generic_path_completion() {
fzf="$(__fzfcmd_complete)" fzf="$(__fzfcmd_complete)"
setopt localoptions nonomatch setopt localoptions nonomatch
dir="$base" eval "base=$base"
[[ $base = *"/"* ]] && dir="$base"
while [ 1 ]; do while [ 1 ]; do
if [[ -z "$dir" || -d ${~dir} ]]; then if [[ -z "$dir" || -d ${dir} ]]; then
leftover=${base/#"$dir"} leftover=${base/#"$dir"}
leftover=${leftover/#\/} leftover=${leftover/#\/}
[ -z "$dir" ] && dir='.' [ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}" [ "$dir" != "/" ] && dir="${dir/%\//}"
dir=${~dir}
matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "$leftover" | while read item; do matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "$leftover" | while read item; do
echo -n "${(q)item}$suffix " echo -n "${(q)item}$suffix "
done) done)
@@ -62,8 +62,7 @@ __fzf_generic_path_completion() {
if [ -n "$matches" ]; then if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches$tail" LBUFFER="$lbuf$matches$tail"
fi fi
zle redisplay zle reset-prompt
typeset -f zle-line-init >/dev/null && zle zle-line-init
break break
fi fi
dir=$(dirname "$dir") dir=$(dirname "$dir")
@@ -102,8 +101,7 @@ _fzf_complete() {
if [ -n "$matches" ]; then if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches" LBUFFER="$lbuf$matches"
fi fi
zle redisplay zle reset-prompt
typeset -f zle-line-init >/dev/null && zle zle-line-init
command rm -f "$fifo" command rm -f "$fifo"
} }
@@ -116,8 +114,9 @@ _fzf_complete_telnet() {
_fzf_complete_ssh() { _fzf_complete_ssh() {
_fzf_complete '+m' "$@" < <( _fzf_complete '+m' "$@" < <(
command cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \ setopt localoptions nonomatch
<(command grep -oE '^[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ command cat <(cat ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
awk '{if (length($2) > 0) {print $2}}' | sort -u awk '{if (length($2) > 0) {print $2}}' | sort -u
) )
@@ -159,16 +158,21 @@ fzf-completion() {
trigger=${FZF_COMPLETION_TRIGGER-'**'} trigger=${FZF_COMPLETION_TRIGGER-'**'}
[ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("") [ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("")
# When the trigger starts with ';', it becomes a separate token
if [[ ${LBUFFER} = *"${tokens[-2]}${tokens[-1]}" ]]; then
tokens[-2]="${tokens[-2]}${tokens[-1]}"
tokens=(${tokens[0,-2]})
fi
tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))} tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))}
# Kill completion (do not require trigger sequence) # Kill completion (do not require trigger sequence)
if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then
fzf="$(__fzfcmd_complete)" fzf="$(__fzfcmd_complete)"
matches=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" ${=fzf} -m | awk '{print $2}' | tr '\n' ' ') matches=$(command ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" ${=fzf} -m | awk '{print $2}' | tr '\n' ' ')
if [ -n "$matches" ]; then if [ -n "$matches" ]; then
LBUFFER="$LBUFFER$matches" LBUFFER="$LBUFFER$matches"
fi fi
zle redisplay zle reset-prompt
typeset -f zle-line-init >/dev/null && zle zle-line-init
# Trigger sequence given # Trigger sequence given
elif [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then elif [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}) d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir})
@@ -197,3 +201,5 @@ fzf-completion() {
zle -N fzf-completion zle -N fzf-completion
bindkey '^I' fzf-completion bindkey '^I' fzf-completion
fi

View File

@@ -55,8 +55,8 @@ __fzf_history__() (
local line local line
shopt -u nocaseglob nocasematch shopt -u nocaseglob nocasematch
line=$( line=$(
HISTTIMEFORMAT= history | HISTTIMEFORMAT= builtin history |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m" $(__fzfcmd) | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS --tac --sync -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m" $(__fzfcmd) |
command grep '^ *[0-9]') && command grep '^ *[0-9]') &&
if [[ $- =~ H ]]; then if [[ $- =~ H ]]; then
sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line" sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line"
@@ -80,7 +80,7 @@ if [[ ! -o vi ]]; 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\C-y\ey\C-u`__fzf_history__`\e\C-e\er\e^"'
# 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"'
@@ -110,7 +110,7 @@ else
bind -m vi-command '"\C-t": "i\C-t"' bind -m vi-command '"\C-t": "i\C-t"'
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": "\C-x\C-addi`__fzf_history__`\C-x\C-e\C-x^\C-x\C-a$a\C-x\C-r"' bind '"\C-r": "\C-x\C-addi`__fzf_history__`\C-x\C-e\C-x\C-r\C-x^\C-x\C-a$a"'
bind -m vi-command '"\C-r": "i\C-r"' bind -m vi-command '"\C-r": "i\C-r"'
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory

View File

@@ -40,14 +40,14 @@ function fzf_key_bindings
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m" set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m"
set -l FISH_MAJOR (echo $FISH_VERSION | cut -f1 -d.) set -l FISH_MAJOR (echo $version | cut -f1 -d.)
set -l FISH_MINOR (echo $FISH_VERSION | cut -f2 -d.) set -l FISH_MINOR (echo $version | cut -f2 -d.)
# history's -z flag is needed for multi-line support. # history's -z flag is needed for multi-line support.
# history's -z flag was added in fish 2.4.0, so don't use it for versions # history's -z flag was added in fish 2.4.0, so don't use it for versions
# before 2.4.0. # before 2.4.0.
if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ]; if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ];
history -z | eval (__fzfcmd) --read0 -q '(commandline)' | perl -pe 'chomp if eof' | read -lz result history -z | eval (__fzfcmd) --read0 --print0 -q '(commandline)' | read -lz result
and commandline -- $result and commandline -- $result
else else
history | eval (__fzfcmd) -q '(commandline)' | read -l result history | eval (__fzfcmd) -q '(commandline)' | read -l result

View File

@@ -8,7 +8,7 @@ __fsel() {
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | cut -b3-"}" -o -type l -print 2> /dev/null | cut -b3-"}"
setopt localoptions pipefail 2> /dev/null setopt localoptions pipefail no_aliases 2> /dev/null
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read item; do eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read item; do
echo -n "${(q)item} " echo -n "${(q)item} "
done done
@@ -29,27 +29,36 @@ __fzfcmd() {
fzf-file-widget() { fzf-file-widget() {
LBUFFER="${LBUFFER}$(__fsel)" LBUFFER="${LBUFFER}$(__fsel)"
local ret=$? local ret=$?
zle redisplay zle reset-prompt
typeset -f zle-line-init >/dev/null && zle zle-line-init
return $ret return $ret
} }
zle -N fzf-file-widget zle -N fzf-file-widget
bindkey '^T' fzf-file-widget bindkey '^T' fzf-file-widget
# Ensure precmds are run after cd
fzf-redraw-prompt() {
local precmd
for precmd in $precmd_functions; do
$precmd
done
zle reset-prompt
}
zle -N fzf-redraw-prompt
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
fzf-cd-widget() { fzf-cd-widget() {
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type d -print 2> /dev/null | cut -b3-"}" -o -type d -print 2> /dev/null | cut -b3-"}"
setopt localoptions pipefail 2> /dev/null setopt localoptions pipefail no_aliases 2> /dev/null
local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m)" local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m)"
if [[ -z "$dir" ]]; then if [[ -z "$dir" ]]; then
zle redisplay zle redisplay
return 0 return 0
fi fi
cd "$dir" cd "$dir"
unset dir # ensure this doesn't end up appearing in prompt expansion
local ret=$? local ret=$?
zle reset-prompt zle fzf-redraw-prompt
typeset -f zle-line-init >/dev/null && zle zle-line-init
return $ret return $ret
} }
zle -N fzf-cd-widget zle -N fzf-cd-widget
@@ -58,9 +67,9 @@ 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
setopt localoptions noglobsubst noposixbuiltins pipefail 2> /dev/null setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
selected=( $(fc -l 1 | selected=( $(fc -rl 1 |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(q)LBUFFER} +m" $(__fzfcmd)) ) FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) )
local ret=$? local ret=$?
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
num=$selected[1] num=$selected[1]
@@ -68,8 +77,7 @@ fzf-history-widget() {
zle vi-fetch-history -n $num zle vi-fetch-history -n $num
fi fi
fi fi
zle redisplay zle reset-prompt
typeset -f zle-line-init >/dev/null && zle zle-line-init
return $ret return $ret
} }
zle -N fzf-history-widget zle -N fzf-history-widget

View File

@@ -371,7 +371,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
// The first occurrence of each character in the pattern // The first occurrence of each character in the pattern
offset32, F := alloc32(offset32, slab, M) offset32, F := alloc32(offset32, slab, M)
// Rune array // Rune array
offset32, T := alloc32(offset32, slab, N) _, T := alloc32(offset32, slab, N)
input.CopyRunes(T) input.CopyRunes(T)
// Phase 2. Calculate bonus for each point // Phase 2. Calculate bonus for each point
@@ -453,7 +453,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
copy(H, H0[f0:lastIdx+1]) copy(H, H0[f0:lastIdx+1])
// Possible length of consecutive chunk at each position. // Possible length of consecutive chunk at each position.
offset16, C := alloc16(offset16, slab, width*M) _, C := alloc16(offset16, slab, width*M)
copy(C, C0[f0:lastIdx+1]) copy(C, C0[f0:lastIdx+1])
Fsub := F[1:] Fsub := F[1:]

View File

@@ -32,6 +32,55 @@ func (s *ansiState) equals(t *ansiState) bool {
return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr
} }
func (s *ansiState) ToString() string {
if !s.colored() {
return ""
}
ret := ""
if s.attr&tui.Bold > 0 {
ret += "1;"
}
if s.attr&tui.Dim > 0 {
ret += "2;"
}
if s.attr&tui.Italic > 0 {
ret += "3;"
}
if s.attr&tui.Underline > 0 {
ret += "4;"
}
if s.attr&tui.Blink > 0 {
ret += "5;"
}
if s.attr&tui.Reverse > 0 {
ret += "7;"
}
ret += toAnsiString(s.fg, 30) + toAnsiString(s.bg, 40)
return "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
}
func toAnsiString(color tui.Color, offset int) string {
col := int(color)
ret := ""
if col == -1 {
ret += strconv.Itoa(offset + 9)
} else if col < 8 {
ret += strconv.Itoa(offset + col)
} else if col < 16 {
ret += strconv.Itoa(offset - 30 + 90 + col - 8)
} else if col < 256 {
ret += strconv.Itoa(offset+8) + ";5;" + strconv.Itoa(col)
} else if col >= (1 << 24) {
r := strconv.Itoa((col >> 16) & 0xff)
g := strconv.Itoa((col >> 8) & 0xff)
b := strconv.Itoa(col & 0xff)
ret += strconv.Itoa(offset+8) + ";2;" + r + ";" + g + ";" + b
}
return ret + ";"
}
var ansiRegex *regexp.Regexp var ansiRegex *regexp.Regexp
func init() { func init() {

View File

@@ -2,6 +2,7 @@ package fzf
import ( import (
"fmt" "fmt"
"strings"
"testing" "testing"
"github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/tui"
@@ -26,7 +27,7 @@ func TestExtractColor(t *testing.T) {
output, ansiOffsets, newState := extractColor(src, state, nil) output, ansiOffsets, newState := extractColor(src, state, nil)
state = newState state = newState
if output != "hello world" { if output != "hello world" {
t.Errorf("Invalid output: %s %s", output, []rune(output)) t.Errorf("Invalid output: %s %v", output, []rune(output))
} }
fmt.Println(src, ansiOffsets, clean) fmt.Println(src, ansiOffsets, clean)
assertion(ansiOffsets, state) assertion(ansiOffsets, state)
@@ -156,3 +157,31 @@ func TestExtractColor(t *testing.T) {
assert((*offsets)[1], 6, 11, 200, 100, false) assert((*offsets)[1], 6, 11, 200, 100, false)
}) })
} }
func TestAnsiCodeStringConversion(t *testing.T) {
assert := func(code string, prevState *ansiState, expected string) {
state := interpretCode(code, prevState)
if expected != state.ToString() {
t.Errorf("expected: %s, actual: %s",
strings.Replace(expected, "\x1b[", "\\x1b[", -1),
strings.Replace(state.ToString(), "\x1b[", "\\x1b[", -1))
}
}
assert("\x1b[m", nil, "")
assert("\x1b[m", &ansiState{attr: tui.Blink}, "")
assert("\x1b[31m", nil, "\x1b[31;49m")
assert("\x1b[41m", nil, "\x1b[39;41m")
assert("\x1b[92m", nil, "\x1b[92;49m")
assert("\x1b[102m", nil, "\x1b[39;102m")
assert("\x1b[31m", &ansiState{fg: 4, bg: 4}, "\x1b[31;44m")
assert("\x1b[1;2;31m", &ansiState{fg: 2, bg: -1, attr: tui.Reverse}, "\x1b[1;2;7;31;49m")
assert("\x1b[38;5;100;48;5;200m", nil, "\x1b[38;5;100;48;5;200m")
assert("\x1b[48;5;100;38;5;200m", nil, "\x1b[38;5;200;48;5;100m")
assert("\x1b[48;5;100;38;2;10;20;30;1m", nil, "\x1b[1;38;2;10;20;30;48;5;100m")
assert("\x1b[48;5;100;38;2;10;20;30;7m",
&ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1},
"\x1b[2;3;7;38;2;10;20;30;48;5;100m")
}

View File

@@ -64,6 +64,13 @@ func (cl *ChunkList) Push(data []byte) bool {
return ret return ret
} }
// Clear clears the data
func (cl *ChunkList) Clear() {
cl.mutex.Lock()
cl.chunks = nil
cl.mutex.Unlock()
}
// Snapshot returns immutable snapshot of the ChunkList // Snapshot returns immutable snapshot of the ChunkList
func (cl *ChunkList) Snapshot() ([]*Chunk, int) { func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
cl.mutex.Lock() cl.mutex.Lock()

View File

@@ -1,6 +1,7 @@
package fzf package fzf
import ( import (
"math"
"os" "os"
"time" "time"
@@ -9,7 +10,7 @@ import (
const ( const (
// Current version // Current version
version = "0.17.0" version = "0.20.0"
// Core // Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond coordinatorDelayMax time.Duration = 100 * time.Millisecond
@@ -25,6 +26,9 @@ const (
initialDelay = 20 * time.Millisecond initialDelay = 20 * time.Millisecond
initialDelayTac = 100 * time.Millisecond initialDelayTac = 100 * time.Millisecond
spinnerDuration = 200 * time.Millisecond spinnerDuration = 200 * time.Millisecond
previewCancelWait = 500 * time.Millisecond
maxPatternLength = 300
maxMulti = math.MaxInt32
// Matcher // Matcher
numPartitionsMultiplier = 8 numPartitionsMultiplier = 8
@@ -55,11 +59,11 @@ var defaultCommand string
func init() { func init() {
if !util.IsWindows() { if !util.IsWindows() {
defaultCommand = `command find -L . -mindepth 1 \( -path '*/\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \) -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-` defaultCommand = `set -o pipefail; command find -L . -mindepth 1 \( -path '*/\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \) -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-`
} else if os.Getenv("TERM") == "cygwin" { } else if os.Getenv("TERM") == "cygwin" {
defaultCommand = `sh -c "command find -L . -mindepth 1 -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-"` defaultCommand = `sh -c "command find -L . -mindepth 1 -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-"`
} else { } else {
defaultCommand = `dir /s/b` defaultCommand = `for /r %P in (*) do @(set "_curfile=%P" & set "_curfile=!_curfile:%__CD__%=!" & echo !_curfile!)`
} }
} }
@@ -75,6 +79,7 @@ const (
) )
const ( const (
exitCancel = -1
exitOk = 0 exitOk = 0
exitNoMatch = 1 exitNoMatch = 1
exitError = 2 exitError = 2

View File

@@ -63,12 +63,14 @@ func Run(opts *Options, revision string) {
ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) { ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) {
return util.ToChars(data), nil return util.ToChars(data), nil
} }
var lineAnsiState, prevLineAnsiState *ansiState
if opts.Ansi { if opts.Ansi {
if opts.Theme != nil { if opts.Theme != nil {
var state *ansiState
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) { ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, offsets, newState := extractColor(string(data), state, nil) prevLineAnsiState = lineAnsiState
state = newState trimmed, offsets, newState := extractColor(string(data), lineAnsiState, nil)
lineAnsiState = newState
return util.ToChars([]byte(trimmed)), offsets return util.ToChars([]byte(trimmed)), offsets
} }
} else { } else {
@@ -100,6 +102,22 @@ func Run(opts *Options, revision string) {
} else { } else {
chunkList = NewChunkList(func(item *Item, data []byte) bool { chunkList = NewChunkList(func(item *Item, data []byte) bool {
tokens := Tokenize(string(data), opts.Delimiter) tokens := Tokenize(string(data), opts.Delimiter)
if opts.Ansi && opts.Theme != nil && len(tokens) > 1 {
var ansiState *ansiState
if prevLineAnsiState != nil {
ansiStateDup := *prevLineAnsiState
ansiState = &ansiStateDup
}
for _, token := range tokens {
prevAnsiState := ansiState
_, _, ansiState = extractColor(token.text.ToString(), ansiState, nil)
if prevAnsiState != nil {
token.text.Prepend("\x1b[m" + prevAnsiState.ToString())
} else {
token.text.Prepend("\x1b[m")
}
}
}
trans := Transform(tokens, opts.WithNth) trans := Transform(tokens, opts.WithNth)
transformed := joinTokens(trans) transformed := joinTokens(trans)
if len(header) < opts.HeaderLines { if len(header) < opts.HeaderLines {
@@ -108,6 +126,7 @@ func Run(opts *Options, revision string) {
return false return false
} }
item.text, item.colors = ansiProcessor([]byte(transformed)) item.text, item.colors = ansiProcessor([]byte(transformed))
item.text.TrimTrailingWhitespaces()
item.text.Index = itemIndex item.text.Index = itemIndex
item.origText = &data item.origText = &data
itemIndex++ itemIndex++
@@ -117,10 +136,11 @@ func Run(opts *Options, revision string) {
// Reader // Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
var reader *Reader
if !streamingFilter { if !streamingFilter {
reader := NewReader(func(data []byte) bool { reader = NewReader(func(data []byte) bool {
return chunkList.Push(data) return chunkList.Push(data)
}, eventBox, opts.ReadZero) }, eventBox, opts.ReadZero, opts.Filter == nil)
go reader.ReadSource() go reader.ReadSource()
} }
@@ -149,6 +169,7 @@ func Run(opts *Options, revision string) {
} }
pattern := patternBuilder([]rune(*opts.Filter)) pattern := patternBuilder([]rune(*opts.Filter))
matcher.sort = pattern.sortable
found := false found := false
if streamingFilter { if streamingFilter {
@@ -163,7 +184,7 @@ func Run(opts *Options, revision string) {
} }
} }
return false return false
}, eventBox, opts.ReadZero) }, eventBox, opts.ReadZero, false)
reader.ReadSource() reader.ReadSource()
} else { } else {
eventBox.Unwatch(EvtReadNew) eventBox.Unwatch(EvtReadNew)
@@ -203,11 +224,28 @@ func Run(opts *Options, revision string) {
// Event coordination // Event coordination
reading := true reading := true
clearCache := util.Once(false)
clearSelection := util.Once(false)
ticks := 0 ticks := 0
var nextCommand *string
restart := func(command string) {
reading = true
clearCache = util.Once(true)
clearSelection = util.Once(true)
chunkList.Clear()
header = make([]string, 0, opts.HeaderLines)
go reader.restart(command)
}
eventBox.Watch(EvtReadNew) eventBox.Watch(EvtReadNew)
for { for {
delay := true delay := true
ticks++ ticks++
input := func() []rune {
if opts.Phony {
return []rune{}
}
return []rune(terminal.Input())
}
eventBox.Wait(func(events *util.Events) { eventBox.Wait(func(events *util.Events) {
if _, fin := (*events)[EvtReadFin]; fin { if _, fin := (*events)[EvtReadFin]; fin {
delete(*events, EvtReadNew) delete(*events, EvtReadNew)
@@ -216,21 +254,39 @@ func Run(opts *Options, revision string) {
switch evt { switch evt {
case EvtReadNew, EvtReadFin: case EvtReadNew, EvtReadFin:
if evt == EvtReadFin && nextCommand != nil {
restart(*nextCommand)
nextCommand = nil
break
} else {
reading = reading && evt == EvtReadNew reading = reading && evt == EvtReadNew
snapshot, count := chunkList.Snapshot()
terminal.UpdateCount(count, !reading, value.(bool))
if opts.Sync {
terminal.UpdateList(PassMerger(&snapshot, opts.Tac))
} }
matcher.Reset(snapshot, terminal.Input(), false, !reading, sort) snapshot, count := chunkList.Snapshot()
terminal.UpdateCount(count, !reading, value.(*string))
if opts.Sync {
opts.Sync = false
terminal.UpdateList(PassMerger(&snapshot, opts.Tac), false)
}
matcher.Reset(snapshot, input(), false, !reading, sort, clearCache())
case EvtSearchNew: case EvtSearchNew:
var command *string
switch val := value.(type) { switch val := value.(type) {
case bool: case searchRequest:
sort = val sort = val.sort
command = val.command
}
if command != nil {
if reading {
reader.terminate()
nextCommand = command
} else {
restart(*command)
}
break
} }
snapshot, _ := chunkList.Snapshot() snapshot, _ := chunkList.Snapshot()
matcher.Reset(snapshot, terminal.Input(), true, !reading, sort) matcher.Reset(snapshot, input(), true, !reading, sort, clearCache())
delay = false delay = false
case EvtSearchProgress: case EvtSearchProgress:
@@ -240,7 +296,9 @@ func Run(opts *Options, revision string) {
} }
case EvtHeader: case EvtHeader:
terminal.UpdateHeader(value.([]string)) headerPadded := make([]string, opts.HeaderLines)
copy(headerPadded, value.([]string))
terminal.UpdateHeader(headerPadded)
case EvtSearchFin: case EvtSearchFin:
switch val := value.(type) { switch val := value.(type) {
@@ -270,7 +328,7 @@ func Run(opts *Options, revision string) {
terminal.startChan <- true terminal.startChan <- true
} }
} }
terminal.UpdateList(val) terminal.UpdateList(val, clearSelection())
} }
} }
} }

View File

@@ -59,7 +59,7 @@ func (h *History) append(line string) error {
lines := append(h.lines[:len(h.lines)-1], line) lines := append(h.lines[:len(h.lines)-1], line)
if len(lines) > h.maxSize { if len(lines) > h.maxSize {
lines = lines[len(lines)-h.maxSize : len(lines)] lines = lines[len(lines)-h.maxSize:]
} }
h.lines = append(lines, "") h.lines = append(lines, "")
return ioutil.WriteFile(h.path, []byte(strings.Join(h.lines, "\n")), 0600) return ioutil.WriteFile(h.path, []byte(strings.Join(h.lines, "\n")), 0600)

View File

@@ -16,6 +16,7 @@ type MatchRequest struct {
pattern *Pattern pattern *Pattern
final bool final bool
sort bool sort bool
clearCache bool
} }
// Matcher is responsible for performing search // Matcher is responsible for performing search
@@ -69,7 +70,7 @@ func (m *Matcher) Loop() {
events.Clear() events.Clear()
}) })
if request.sort != m.sort { if request.sort != m.sort || request.clearCache {
m.sort = request.sort m.sort = request.sort
m.mergerCache = make(map[string]*Merger) m.mergerCache = make(map[string]*Merger)
clearChunkCache() clearChunkCache()
@@ -207,13 +208,13 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
return nil, wait() return nil, wait()
} }
if time.Now().Sub(startedAt) > progressMinDuration { if time.Since(startedAt) > progressMinDuration {
m.eventBox.Set(EvtSearchProgress, float32(count)/float32(numChunks)) m.eventBox.Set(EvtSearchProgress, float32(count)/float32(numChunks))
} }
} }
partialResults := make([][]Result, numSlices) partialResults := make([][]Result, numSlices)
for _ = range slices { for range slices {
partialResult := <-resultChan partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches partialResults[partialResult.index] = partialResult.matches
} }
@@ -221,7 +222,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
} }
// Reset is called to interrupt/signal the ongoing search // Reset is called to interrupt/signal the ongoing search
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool) { func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, clearCache bool) {
pattern := m.patternBuilder(patternRunes) pattern := m.patternBuilder(patternRunes)
var event util.EventType var event util.EventType
@@ -230,5 +231,5 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
} else { } else {
event = reqRetry event = reqRetry
} }
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort}) m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, clearCache})
} }

View File

@@ -33,12 +33,13 @@ const usage = `usage: fzf [options]
-d, --delimiter=STR Field delimiter regex (default: AWK-style) -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
--phony Do not perform search
--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 [length|begin|end|index] when the scores are tied [length|begin|end|index]
(default: length) (default: length)
Interface Interface
-m, --multi Enable multi-select with tab/shift-tab -m, --multi[=MAX] Enable multi-select with tab/shift-tab
--no-mouse Disable mouse --no-mouse Disable mouse
--bind=KEYBINDS Custom key bindings. Refer to the man page. --bind=KEYBINDS Custom key bindings. Refer to the man page.
--cycle Enable cyclic scroll --cycle Enable cyclic scroll
@@ -53,10 +54,10 @@ const usage = `usage: fzf [options]
height instead of using fullscreen height instead of using fullscreen
--min-height=HEIGHT Minimum height when --height is given in percent --min-height=HEIGHT Minimum height when --height is given in percent
(default: 10) (default: 10)
--reverse Reverse orientation --layout=LAYOUT Choose layout: [default|reverse|reverse-list]
--border Draw border above and below the finder --border Draw border above and below the finder
--margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L) --margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
--inline-info Display finder info inline with the query --info=STYLE Finder info style [default|inline|hidden]
--prompt=STR Input prompt (default: '> ') --prompt=STR Input prompt (default: '> ')
--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
@@ -90,7 +91,8 @@ const usage = `usage: fzf [options]
Environment variables Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty FZF_DEFAULT_COMMAND Default command to use when input is tty
FZF_DEFAULT_OPTS Default options (e.g. '--reverse --inline-info') FZF_DEFAULT_OPTS Default options
(e.g. '--layout=reverse --inline-info')
` `
@@ -132,12 +134,29 @@ const (
posRight posRight
) )
type layoutType int
const (
layoutDefault layoutType = iota
layoutReverse
layoutReverseList
)
type infoStyle int
const (
infoDefault infoStyle = iota
infoInline
infoHidden
)
type previewOpts struct { type previewOpts struct {
command string command string
position windowPosition position windowPosition
size sizeSpec size sizeSpec
hidden bool hidden bool
wrap bool wrap bool
border bool
} }
// Options stores the values of command-line options // Options stores the values of command-line options
@@ -145,6 +164,7 @@ type Options struct {
Fuzzy bool Fuzzy bool
FuzzyAlgo algo.Algo FuzzyAlgo algo.Algo
Extended bool Extended bool
Phony bool
Case Case Case Case
Normalize bool Normalize bool
Nth []Range Nth []Range
@@ -153,7 +173,7 @@ type Options struct {
Sort int Sort int
Tac bool Tac bool
Criteria []criterion Criteria []criterion
Multi bool Multi int
Ansi bool Ansi bool
Mouse bool Mouse bool
Theme *tui.ColorTheme Theme *tui.ColorTheme
@@ -161,12 +181,12 @@ type Options struct {
Bold bool Bold bool
Height sizeSpec Height sizeSpec
MinHeight int MinHeight int
Reverse bool Layout layoutType
Cycle bool Cycle bool
Hscroll bool Hscroll bool
HscrollOff int HscrollOff int
FileWord bool FileWord bool
InlineInfo bool InfoStyle infoStyle
JumpLabels string JumpLabels string
Prompt string Prompt string
Query string Query string
@@ -180,12 +200,14 @@ type Options struct {
PrintQuery bool PrintQuery bool
ReadZero bool ReadZero bool
Printer func(string) Printer func(string)
PrintSep string
Sync bool Sync bool
History *History History *History
Header []string Header []string
HeaderLines int HeaderLines int
Margin [4]sizeSpec Margin [4]sizeSpec
Bordered bool Bordered bool
Unicode bool
Tabstop int Tabstop int
ClearOnExit bool ClearOnExit bool
Version bool Version bool
@@ -196,6 +218,7 @@ func defaultOptions() *Options {
Fuzzy: true, Fuzzy: true,
FuzzyAlgo: algo.FuzzyMatchV2, FuzzyAlgo: algo.FuzzyMatchV2,
Extended: true, Extended: true,
Phony: false,
Case: CaseSmart, Case: CaseSmart,
Normalize: true, Normalize: true,
Nth: make([]Range, 0), Nth: make([]Range, 0),
@@ -204,19 +227,19 @@ func defaultOptions() *Options {
Sort: 1000, Sort: 1000,
Tac: false, Tac: false,
Criteria: []criterion{byScore, byLength}, Criteria: []criterion{byScore, byLength},
Multi: false, Multi: 0,
Ansi: false, Ansi: false,
Mouse: true, Mouse: true,
Theme: tui.EmptyTheme(), Theme: tui.EmptyTheme(),
Black: false, Black: false,
Bold: true, Bold: true,
MinHeight: 10, MinHeight: 10,
Reverse: false, Layout: layoutDefault,
Cycle: false, Cycle: false,
Hscroll: true, Hscroll: true,
HscrollOff: 10, HscrollOff: 10,
FileWord: false, FileWord: false,
InlineInfo: false, InfoStyle: infoDefault,
JumpLabels: defaultJumpLabels, JumpLabels: defaultJumpLabels,
Prompt: "> ", Prompt: "> ",
Query: "", Query: "",
@@ -226,22 +249,24 @@ func defaultOptions() *Options {
ToggleSort: false, ToggleSort: false,
Expect: make(map[int]string), Expect: make(map[int]string),
Keymap: make(map[int][]action), Keymap: make(map[int][]action),
Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false}, Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false, true},
PrintQuery: false, PrintQuery: false,
ReadZero: false, ReadZero: false,
Printer: func(str string) { fmt.Println(str) }, Printer: func(str string) { fmt.Println(str) },
PrintSep: "\n",
Sync: false, Sync: false,
History: nil, History: nil,
Header: make([]string, 0), Header: make([]string, 0),
HeaderLines: 0, HeaderLines: 0,
Margin: defaultMargin(), Margin: defaultMargin(),
Unicode: true,
Tabstop: 8, Tabstop: 8,
ClearOnExit: true, ClearOnExit: true,
Version: false} Version: false}
} }
func help(code int) { func help(code int) {
os.Stderr.WriteString(usage) os.Stdout.WriteString(usage)
os.Exit(code) os.Exit(code)
} }
@@ -301,13 +326,14 @@ func nextInt(args []string, i *int, message string) int {
return atoi(args[*i]) return atoi(args[*i])
} }
func optionalNumeric(args []string, i *int) int { func optionalNumeric(args []string, i *int, defaultValue int) int {
if len(args) > *i+1 { if len(args) > *i+1 {
if strings.IndexAny(args[*i+1], "0123456789") == 0 { if strings.IndexAny(args[*i+1], "0123456789") == 0 {
*i++ *i++
return atoi(args[*i])
} }
} }
return 1 // Don't care return defaultValue
} }
func splitNth(str string) []Range { func splitNth(str string) []Range {
@@ -372,7 +398,7 @@ func parseKeyChords(str string, message string) map[int]string {
} }
tokens := strings.Split(str, ",") tokens := strings.Split(str, ",")
if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Index(str, ",,,") >= 0 { if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Contains(str, ",,,") {
tokens = append(tokens, ",") tokens = append(tokens, ",")
} }
@@ -400,6 +426,14 @@ func parseKeyChords(str string, message string) map[int]string {
chord = tui.BSpace chord = tui.BSpace
case "ctrl-space": case "ctrl-space":
chord = tui.CtrlSpace chord = tui.CtrlSpace
case "ctrl-^", "ctrl-6":
chord = tui.CtrlCaret
case "ctrl-/", "ctrl-_":
chord = tui.CtrlSlash
case "ctrl-\\":
chord = tui.CtrlBackSlash
case "ctrl-]":
chord = tui.CtrlRightBracket
case "change": case "change":
chord = tui.Change chord = tui.Change
case "alt-enter", "alt-return": case "alt-enter", "alt-return":
@@ -410,6 +444,14 @@ func parseKeyChords(str string, message string) map[int]string {
chord = tui.AltSlash chord = tui.AltSlash
case "alt-bs", "alt-bspace": case "alt-bs", "alt-bspace":
chord = tui.AltBS chord = tui.AltBS
case "alt-up":
chord = tui.AltUp
case "alt-down":
chord = tui.AltDown
case "alt-left":
chord = tui.AltLeft
case "alt-right":
chord = tui.AltRight
case "tab": case "tab":
chord = tui.Tab chord = tui.Tab
case "btab", "shift-tab": case "btab", "shift-tab":
@@ -426,10 +468,18 @@ func parseKeyChords(str string, message string) map[int]string {
chord = tui.PgUp chord = tui.PgUp
case "pgdn", "page-down": case "pgdn", "page-down":
chord = tui.PgDn chord = tui.PgDn
case "shift-up":
chord = tui.SUp
case "shift-down":
chord = tui.SDown
case "shift-left": case "shift-left":
chord = tui.SLeft chord = tui.SLeft
case "shift-right": case "shift-right":
chord = tui.SRight chord = tui.SRight
case "left-click":
chord = tui.LeftClick
case "right-click":
chord = tui.RightClick
case "double-click": case "double-click":
chord = tui.DoubleClick chord = tui.DoubleClick
case "f10": case "f10":
@@ -547,10 +597,16 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
theme.Fg = ansi theme.Fg = ansi
case "bg": case "bg":
theme.Bg = ansi theme.Bg = ansi
case "preview-fg":
theme.PreviewFg = ansi
case "preview-bg":
theme.PreviewBg = ansi
case "fg+": case "fg+":
theme.Current = ansi theme.Current = ansi
case "bg+": case "bg+":
theme.DarkBg = ansi theme.DarkBg = ansi
case "gutter":
theme.Gutter = ansi
case "hl": case "hl":
theme.Match = ansi theme.Match = ansi
case "hl+": case "hl+":
@@ -596,13 +652,19 @@ func init() {
// Backreferences are not supported. // Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
executeRegexp = regexp.MustCompile( executeRegexp = regexp.MustCompile(
"(?si):(execute(?:-multi|-silent)?):.+|:(execute(?:-multi|-silent)?)(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") `(?si)[:+](execute(?:-multi|-silent)?|reload):.+|[:+](execute(?:-multi|-silent)?|reload)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
} }
func parseKeymap(keymap map[int][]action, str string) { func parseKeymap(keymap map[int][]action, str string) {
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
prefix := ":execute" symbol := ":"
if src[len(prefix)] == '-' { if strings.HasPrefix(src, "+") {
symbol = "+"
}
prefix := symbol + "execute"
if strings.HasPrefix(src[1:], "reload") {
prefix = symbol + "reload"
} else if src[len(prefix)] == '-' {
c := src[len(prefix)+1] c := src[len(prefix)+1]
if c == 's' || c == 'S' { if c == 's' || c == 'S' {
prefix += "-silent" prefix += "-silent"
@@ -658,8 +720,12 @@ func parseKeymap(keymap map[int][]action, str string) {
appendAction(actAbort) appendAction(actAbort)
case "accept": case "accept":
appendAction(actAccept) appendAction(actAccept)
case "accept-non-empty":
appendAction(actAcceptNonEmpty)
case "print-query": case "print-query":
appendAction(actPrintQuery) appendAction(actPrintQuery)
case "replace-query":
appendAction(actReplaceQuery)
case "backward-char": case "backward-char":
appendAction(actBackwardChar) appendAction(actBackwardChar)
case "backward-delete-char": case "backward-delete-char":
@@ -676,6 +742,10 @@ func parseKeymap(keymap map[int][]action, str string) {
appendAction(actEndOfLine) appendAction(actEndOfLine)
case "cancel": case "cancel":
appendAction(actCancel) appendAction(actCancel)
case "clear-query":
appendAction(actClearQuery)
case "clear-selection":
appendAction(actClearSelection)
case "forward-char": case "forward-char":
appendAction(actForwardChar) appendAction(actForwardChar)
case "forward-word": case "forward-word":
@@ -747,10 +817,16 @@ func parseKeymap(keymap map[int][]action, str string) {
default: default:
t := isExecuteAction(specLower) t := isExecuteAction(specLower)
if t == actIgnore { if t == actIgnore {
if specIndex == 0 && specLower == "" {
actions = append(keymap[key], actions...)
} else {
errorExit("unknown action: " + spec) errorExit("unknown action: " + spec)
}
} else { } else {
var offset int var offset int
switch t { switch t {
case actReload:
offset = len("reload")
case actExecuteSilent: case actExecuteSilent:
offset = len("execute-silent") offset = len("execute-silent")
case actExecuteMulti: case actExecuteMulti:
@@ -786,6 +862,8 @@ func isExecuteAction(str string) actionType {
prefix = matches[0][2] prefix = matches[0][2]
} }
switch prefix { switch prefix {
case "reload":
return actReload
case "execute": case "execute":
return actExecute return actExecute
case "execute-silent": case "execute-silent":
@@ -833,13 +911,38 @@ func parseSize(str string, maxPercent float64, label string) sizeSpec {
} }
func parseHeight(str string) sizeSpec { func parseHeight(str string) sizeSpec {
if util.IsWindows() {
errorExit("--height options is currently not supported on Windows")
}
size := parseSize(str, 100, "height") size := parseSize(str, 100, "height")
return size return size
} }
func parseLayout(str string) layoutType {
switch str {
case "default":
return layoutDefault
case "reverse":
return layoutReverse
case "reverse-list":
return layoutReverseList
default:
errorExit("invalid layout (expected: default / reverse / reverse-list)")
}
return layoutDefault
}
func parseInfoStyle(str string) infoStyle {
switch str {
case "default":
return infoDefault
case "inline":
return infoInline
case "hidden":
return infoHidden
default:
errorExit("invalid info style (expected: default / inline / hidden)")
}
return infoDefault
}
func parsePreviewWindow(opts *previewOpts, input string) { func parsePreviewWindow(opts *previewOpts, input string) {
// Default // Default
opts.position = posRight opts.position = posRight
@@ -851,6 +954,7 @@ func parsePreviewWindow(opts *previewOpts, input string) {
sizeRegex := regexp.MustCompile("^[0-9]+%?$") sizeRegex := regexp.MustCompile("^[0-9]+%?$")
for _, token := range tokens { for _, token := range tokens {
switch token { switch token {
case "":
case "hidden": case "hidden":
opts.hidden = true opts.hidden = true
case "wrap": case "wrap":
@@ -863,6 +967,10 @@ func parsePreviewWindow(opts *previewOpts, input string) {
opts.position = posLeft opts.position = posLeft
case "right": case "right":
opts.position = posRight opts.position = posRight
case "border":
opts.border = true
case "noborder":
opts.border = false
default: default:
if sizeRegex.MatchString(token) { if sizeRegex.MatchString(token) {
opts.size = parseSize(token, 99, "window size") opts.size = parseSize(token, 99, "window size")
@@ -967,6 +1075,10 @@ func parseOptions(opts *Options, allArgs []string) {
} }
case "--no-expect": case "--no-expect":
opts.Expect = make(map[int]string) opts.Expect = make(map[int]string)
case "--no-phony":
opts.Phony = false
case "--phony":
opts.Phony = true
case "--tiebreak": case "--tiebreak":
opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
case "--bind": case "--bind":
@@ -987,7 +1099,7 @@ func parseOptions(opts *Options, allArgs []string) {
case "--with-nth": case "--with-nth":
opts.WithNth = splitNth(nextString(allArgs, &i, "nth expression required")) opts.WithNth = splitNth(nextString(allArgs, &i, "nth expression required"))
case "-s", "--sort": case "-s", "--sort":
opts.Sort = optionalNumeric(allArgs, &i) opts.Sort = optionalNumeric(allArgs, &i, 1)
case "+s", "--no-sort": case "+s", "--no-sort":
opts.Sort = 0 opts.Sort = 0
case "--tac": case "--tac":
@@ -999,9 +1111,9 @@ func parseOptions(opts *Options, allArgs []string) {
case "+i": case "+i":
opts.Case = CaseRespect opts.Case = CaseRespect
case "-m", "--multi": case "-m", "--multi":
opts.Multi = true opts.Multi = optionalNumeric(allArgs, &i, maxMulti)
case "+m", "--no-multi": case "+m", "--no-multi":
opts.Multi = false opts.Multi = 0
case "--ansi": case "--ansi":
opts.Ansi = true opts.Ansi = true
case "--no-ansi": case "--no-ansi":
@@ -1020,10 +1132,13 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Bold = true opts.Bold = true
case "--no-bold": case "--no-bold":
opts.Bold = false opts.Bold = false
case "--layout":
opts.Layout = parseLayout(
nextString(allArgs, &i, "layout required (default / reverse / reverse-list)"))
case "--reverse": case "--reverse":
opts.Reverse = true opts.Layout = layoutReverse
case "--no-reverse": case "--no-reverse":
opts.Reverse = false opts.Layout = layoutDefault
case "--cycle": case "--cycle":
opts.Cycle = true opts.Cycle = true
case "--no-cycle": case "--no-cycle":
@@ -1038,10 +1153,15 @@ func parseOptions(opts *Options, allArgs []string) {
opts.FileWord = true opts.FileWord = true
case "--no-filepath-word": case "--no-filepath-word":
opts.FileWord = false opts.FileWord = false
case "--info":
opts.InfoStyle = parseInfoStyle(
nextString(allArgs, &i, "info style required"))
case "--no-info":
opts.InfoStyle = infoHidden
case "--inline-info": case "--inline-info":
opts.InlineInfo = true opts.InfoStyle = infoInline
case "--no-inline-info": case "--no-inline-info":
opts.InlineInfo = false opts.InfoStyle = infoDefault
case "--jump-labels": case "--jump-labels":
opts.JumpLabels = nextString(allArgs, &i, "label characters required") opts.JumpLabels = nextString(allArgs, &i, "label characters required")
validateJumpLabels = true validateJumpLabels = true
@@ -1059,8 +1179,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.ReadZero = false opts.ReadZero = false
case "--print0": case "--print0":
opts.Printer = func(str string) { fmt.Print(str, "\x00") } opts.Printer = func(str string) { fmt.Print(str, "\x00") }
opts.PrintSep = "\x00"
case "--no-print0": case "--no-print0":
opts.Printer = func(str string) { fmt.Println(str) } opts.Printer = func(str string) { fmt.Println(str) }
opts.PrintSep = "\n"
case "--print-query": case "--print-query":
opts.PrintQuery = true opts.PrintQuery = true
case "--no-print-query": case "--no-print-query":
@@ -1094,7 +1216,7 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Preview.command = "" opts.Preview.command = ""
case "--preview-window": case "--preview-window":
parsePreviewWindow(&opts.Preview, parsePreviewWindow(&opts.Preview,
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:wrap][:hidden]")) nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:noborder][:wrap][:hidden]"))
case "--height": case "--height":
opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]")) opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]"))
case "--min-height": case "--min-height":
@@ -1107,6 +1229,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Bordered = false opts.Bordered = false
case "--border": case "--border":
opts.Bordered = true opts.Bordered = true
case "--no-unicode":
opts.Unicode = false
case "--unicode":
opts.Unicode = true
case "--margin": case "--margin":
opts.Margin = parseMargin( opts.Margin = parseMargin(
nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)")) nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
@@ -1135,10 +1261,16 @@ func parseOptions(opts *Options, allArgs []string) {
opts.WithNth = splitNth(value) opts.WithNth = splitNth(value)
} else if match, _ := optString(arg, "-s", "--sort="); match { } else if match, _ := optString(arg, "-s", "--sort="); match {
opts.Sort = 1 // Don't care opts.Sort = 1 // Don't care
} else if match, value := optString(arg, "-m", "--multi="); match {
opts.Multi = atoi(value)
} else if match, value := optString(arg, "--height="); match { } else if match, value := optString(arg, "--height="); match {
opts.Height = parseHeight(value) opts.Height = parseHeight(value)
} else if match, value := optString(arg, "--min-height="); match { } else if match, value := optString(arg, "--min-height="); match {
opts.MinHeight = atoi(value) opts.MinHeight = atoi(value)
} else if match, value := optString(arg, "--layout="); match {
opts.Layout = parseLayout(value)
} else if match, value := optString(arg, "--info="); match {
opts.InfoStyle = parseInfoStyle(value)
} else if match, value := optString(arg, "--toggle-sort="); match { } else if match, value := optString(arg, "--toggle-sort="); match {
parseToggleSort(opts.Keymap, value) parseToggleSort(opts.Keymap, value)
} else if match, value := optString(arg, "--expect="); match { } else if match, value := optString(arg, "--expect="); match {
@@ -1203,6 +1335,9 @@ func parseOptions(opts *Options, allArgs []string) {
} }
func postProcessOptions(opts *Options) { func postProcessOptions(opts *Options) {
if util.IsWindows() && opts.Height.size > 0 {
errorExit("--height option is currently not supported on Windows")
}
// Default actions for CTRL-N / CTRL-P when --history is set // Default actions for CTRL-N / CTRL-P when --history is set
if opts.History != nil { if opts.History != nil {
if _, prs := opts.Keymap[tui.CtrlP]; !prs { if _, prs := opts.Keymap[tui.CtrlP]; !prs {

View File

@@ -50,7 +50,7 @@ func TestDelimiterRegexString(t *testing.T) {
tokens[2].text.ToString() != "---*" || tokens[2].text.ToString() != "---*" ||
tokens[3].text.ToString() != "*" || tokens[3].text.ToString() != "*" ||
tokens[4].text.ToString() != "---" { tokens[4].text.ToString() != "---" {
t.Errorf("%s %s %d", delim, tokens, len(tokens)) t.Errorf("%s %v %d", delim, tokens, len(tokens))
} }
} }
@@ -71,7 +71,7 @@ func TestSplitNth(t *testing.T) {
if len(ranges) != 1 || if len(ranges) != 1 ||
ranges[0].begin != rangeEllipsis || ranges[0].begin != rangeEllipsis ||
ranges[0].end != rangeEllipsis { ranges[0].end != rangeEllipsis {
t.Errorf("%s", ranges) t.Errorf("%v", ranges)
} }
} }
{ {
@@ -87,7 +87,7 @@ func TestSplitNth(t *testing.T) {
ranges[7].begin != -2 || ranges[7].end != -2 || ranges[7].begin != -2 || ranges[7].end != -2 ||
ranges[8].begin != 2 || ranges[8].end != -2 || ranges[8].begin != 2 || ranges[8].end != -2 ||
ranges[9].begin != rangeEllipsis || ranges[9].end != rangeEllipsis { ranges[9].begin != rangeEllipsis || ranges[9].end != rangeEllipsis {
t.Errorf("%s", ranges) t.Errorf("%v", ranges)
} }
} }
} }
@@ -99,7 +99,7 @@ func TestIrrelevantNth(t *testing.T) {
parseOptions(opts, words) parseOptions(opts, words)
postProcessOptions(opts) postProcessOptions(opts)
if len(opts.Nth) != 0 { if len(opts.Nth) != 0 {
t.Errorf("nth should be empty: %s", opts.Nth) t.Errorf("nth should be empty: %v", opts.Nth)
} }
} }
for _, words := range [][]string{[]string{"--nth", "..,3", "+x"}, []string{"--nth", "3,1..", "+x"}, []string{"--nth", "..-1,1", "+x"}} { for _, words := range [][]string{[]string{"--nth", "..,3", "+x"}, []string{"--nth", "3,1..", "+x"}, []string{"--nth", "..-1,1", "+x"}} {
@@ -108,7 +108,7 @@ func TestIrrelevantNth(t *testing.T) {
parseOptions(opts, words) parseOptions(opts, words)
postProcessOptions(opts) postProcessOptions(opts)
if len(opts.Nth) != 0 { if len(opts.Nth) != 0 {
t.Errorf("nth should be empty: %s", opts.Nth) t.Errorf("nth should be empty: %v", opts.Nth)
} }
} }
{ {
@@ -117,7 +117,7 @@ func TestIrrelevantNth(t *testing.T) {
parseOptions(opts, words) parseOptions(opts, words)
postProcessOptions(opts) postProcessOptions(opts)
if len(opts.Nth) != 2 { if len(opts.Nth) != 2 {
t.Errorf("nth should not be empty: %s", opts.Nth) t.Errorf("nth should not be empty: %v", opts.Nth)
} }
} }
} }
@@ -243,9 +243,10 @@ func TestBind(t *testing.T) {
check(tui.CtrlA, "", actBeginningOfLine) check(tui.CtrlA, "", actBeginningOfLine)
parseKeymap(keymap, parseKeymap(keymap,
"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+ "ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
"f1:execute(ls {})+abort,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ "f1:execute(ls {+})+abort+execute(echo {+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
"alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+ "alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
"x:Execute(foo+bar),X:execute/bar+baz/"+ "x:Execute(foo+bar),X:execute/bar+baz/"+
",f1:+top,f1:+top"+
",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up") ",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up")
check(tui.CtrlA, "", actKillLine) check(tui.CtrlA, "", actKillLine)
check(tui.CtrlB, "", actToggleSort, actUp, actDown) check(tui.CtrlB, "", actToggleSort, actUp, actDown)
@@ -253,7 +254,7 @@ func TestBind(t *testing.T) {
check(tui.AltZ+',', "", actAbort) check(tui.AltZ+',', "", actAbort)
check(tui.AltZ+':', "", actAccept) check(tui.AltZ+':', "", actAccept)
check(tui.AltZ, "", actPageDown) check(tui.AltZ, "", actPageDown)
check(tui.F1, "ls {}", actExecute, actAbort) check(tui.F1, "ls {+}", actExecute, actAbort, actExecute, actSelectAll, actTop, actTop)
check(tui.F2, "echo {}, {}, {}", actExecute) check(tui.F2, "echo {}, {}, {}", actExecute)
check(tui.F3, "echo '({})'", actExecute) check(tui.F3, "echo '({})'", actExecute)
check(tui.F4, "less {}", actExecute) check(tui.F4, "less {}", actExecute)

View File

@@ -1,6 +1,7 @@
package fzf package fzf
import ( import (
"fmt"
"regexp" "regexp"
"strings" "strings"
@@ -34,6 +35,11 @@ type term struct {
caseSensitive bool caseSensitive bool
} }
// String returns the string representation of a term.
func (t term) String() string {
return fmt.Sprintf("term{typ: %d, inv: %v, text: []rune(%q), caseSensitive: %v}", t.typ, t.inv, string(t.text), t.caseSensitive)
}
type termSet []term type termSet []term
// Pattern represents search pattern // Pattern represents search pattern
@@ -46,6 +52,7 @@ type Pattern struct {
forward bool forward bool
text []rune text []rune
termSets []termSet termSets []termSet
sortable bool
cacheable bool cacheable bool
cacheKey string cacheKey string
delimiter Delimiter delimiter Delimiter
@@ -95,21 +102,30 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
} }
caseSensitive := true caseSensitive := true
sortable := true
termSets := []termSet{} termSets := []termSet{}
if extended { if extended {
termSets = parseTerms(fuzzy, caseMode, normalize, asString) termSets = parseTerms(fuzzy, caseMode, normalize, asString)
// We should not sort the result if there are only inverse search terms
sortable = false
Loop: Loop:
for _, termSet := range termSets { for _, termSet := range termSets {
for idx, term := range termSet { for idx, term := range termSet {
if !term.inv {
sortable = true
}
// If the query contains inverse search terms or OR operators, // If the query contains inverse search terms or OR operators,
// we cannot cache the search scope // we cannot cache the search scope
if !cacheable || idx > 0 || term.inv || fuzzy && term.typ != termFuzzy || !fuzzy && term.typ != termExact { if !cacheable || idx > 0 || term.inv || fuzzy && term.typ != termFuzzy || !fuzzy && term.typ != termExact {
cacheable = false cacheable = false
if sortable {
// Can't break until we see at least one non-inverse term
break Loop break Loop
} }
} }
} }
}
} else { } else {
lowerString := strings.ToLower(asString) lowerString := strings.ToLower(asString)
caseSensitive = caseMode == CaseRespect || caseSensitive = caseMode == CaseRespect ||
@@ -128,6 +144,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
forward: forward, forward: forward,
text: []rune(asString), text: []rune(asString),
termSets: termSets, termSets: termSets,
sortable: sortable,
cacheable: cacheable, cacheable: cacheable,
nth: nth, nth: nth,
delimiter: delimiter, delimiter: delimiter,

View File

@@ -31,12 +31,12 @@ func TestParseTermsExtended(t *testing.T) {
terms[8][1].typ != termExact || terms[8][1].inv || terms[8][1].typ != termExact || terms[8][1].inv ||
terms[8][2].typ != termSuffix || terms[8][2].inv || terms[8][2].typ != termSuffix || terms[8][2].inv ||
terms[8][3].typ != termExact || !terms[8][3].inv { terms[8][3].typ != termExact || !terms[8][3].inv {
t.Errorf("%s", terms) t.Errorf("%v", terms)
} }
for _, termSet := range terms[:8] { for _, termSet := range terms[:8] {
term := termSet[0] term := termSet[0]
if len(term.text) != 3 { if len(term.text) != 3 {
t.Errorf("%s", term) t.Errorf("%v", term)
} }
} }
} }
@@ -53,14 +53,14 @@ func TestParseTermsExtendedExact(t *testing.T) {
terms[5][0].typ != termFuzzy || !terms[5][0].inv || len(terms[5][0].text) != 3 || terms[5][0].typ != termFuzzy || !terms[5][0].inv || len(terms[5][0].text) != 3 ||
terms[6][0].typ != termPrefix || !terms[6][0].inv || len(terms[6][0].text) != 3 || terms[6][0].typ != termPrefix || !terms[6][0].inv || len(terms[6][0].text) != 3 ||
terms[7][0].typ != termSuffix || !terms[7][0].inv || len(terms[7][0].text) != 3 { terms[7][0].typ != termSuffix || !terms[7][0].inv || len(terms[7][0].text) != 3 {
t.Errorf("%s", terms) t.Errorf("%v", terms)
} }
} }
func TestParseTermsEmpty(t *testing.T) { func TestParseTermsEmpty(t *testing.T) {
terms := parseTerms(true, CaseSmart, false, "' ^ !' !^") terms := parseTerms(true, CaseSmart, false, "' ^ !' !^")
if len(terms) != 0 { if len(terms) != 0 {
t.Errorf("%s", terms) t.Errorf("%v", terms)
} }
} }
@@ -73,7 +73,7 @@ func TestExact(t *testing.T) {
res, pos := algo.ExactMatchNaive( res, pos := algo.ExactMatchNaive(
pattern.caseSensitive, pattern.normalize, pattern.forward, &chars, pattern.termSets[0][0].text, true, nil) pattern.caseSensitive, pattern.normalize, pattern.forward, &chars, pattern.termSets[0][0].text, true, nil)
if res.Start != 7 || res.End != 10 { if res.Start != 7 || res.End != 10 {
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) t.Errorf("%v / %d / %d", pattern.termSets, res.Start, res.End)
} }
if pos != nil { if pos != nil {
t.Errorf("pos is expected to be nil") t.Errorf("pos is expected to be nil")
@@ -90,7 +90,7 @@ func TestEqual(t *testing.T) {
res, pos := algo.EqualMatch( res, pos := algo.EqualMatch(
pattern.caseSensitive, pattern.normalize, pattern.forward, &chars, pattern.termSets[0][0].text, true, nil) pattern.caseSensitive, pattern.normalize, pattern.forward, &chars, pattern.termSets[0][0].text, true, nil)
if res.Start != sidxExpected || res.End != eidxExpected { if res.Start != sidxExpected || res.End != eidxExpected {
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) t.Errorf("%v / %d / %d", pattern.termSets, res.Start, res.End)
} }
if pos != nil { if pos != nil {
t.Errorf("pos is expected to be nil") t.Errorf("pos is expected to be nil")

View File

@@ -4,6 +4,8 @@ import (
"bufio" "bufio"
"io" "io"
"os" "os"
"os/exec"
"sync"
"sync/atomic" "sync/atomic"
"time" "time"
@@ -16,11 +18,17 @@ type Reader struct {
eventBox *util.EventBox eventBox *util.EventBox
delimNil bool delimNil bool
event int32 event int32
finChan chan bool
mutex sync.Mutex
exec *exec.Cmd
command *string
killed bool
wait bool
} }
// NewReader returns new Reader object // NewReader returns new Reader object
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool) *Reader { func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool, wait bool) *Reader {
return &Reader{pusher, eventBox, delimNil, int32(EvtReady)} return &Reader{pusher, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait}
} }
func (r *Reader) startEventPoller() { func (r *Reader) startEventPoller() {
@@ -29,9 +37,12 @@ func (r *Reader) startEventPoller() {
pollInterval := readerPollIntervalMin pollInterval := readerPollIntervalMin
for { for {
if atomic.CompareAndSwapInt32(ptr, int32(EvtReadNew), int32(EvtReady)) { if atomic.CompareAndSwapInt32(ptr, int32(EvtReadNew), int32(EvtReady)) {
r.eventBox.Set(EvtReadNew, true) r.eventBox.Set(EvtReadNew, (*string)(nil))
pollInterval = readerPollIntervalMin pollInterval = readerPollIntervalMin
} else if atomic.LoadInt32(ptr) == int32(EvtReadFin) { } else if atomic.LoadInt32(ptr) == int32(EvtReadFin) {
if r.wait {
r.finChan <- true
}
return return
} else { } else {
pollInterval += readerPollIntervalStep pollInterval += readerPollIntervalStep
@@ -46,7 +57,37 @@ func (r *Reader) startEventPoller() {
func (r *Reader) fin(success bool) { func (r *Reader) fin(success bool) {
atomic.StoreInt32(&r.event, int32(EvtReadFin)) atomic.StoreInt32(&r.event, int32(EvtReadFin))
r.eventBox.Set(EvtReadFin, success) if r.wait {
<-r.finChan
}
r.mutex.Lock()
ret := r.command
if success || r.killed {
ret = nil
}
r.mutex.Unlock()
r.eventBox.Set(EvtReadFin, ret)
}
func (r *Reader) terminate() {
r.mutex.Lock()
defer func() { r.mutex.Unlock() }()
r.killed = true
if r.exec != nil && r.exec.Process != nil {
util.KillCommand(r.exec)
} else {
os.Stdin.Close()
}
}
func (r *Reader) restart(command string) {
r.event = int32(EvtReady)
r.startEventPoller()
success := r.readFromCommand(nil, command)
r.fin(success)
} }
// ReadSource reads data from the default command or from standard input // ReadSource reads data from the default command or from standard input
@@ -54,11 +95,14 @@ func (r *Reader) ReadSource() {
r.startEventPoller() r.startEventPoller()
var success bool var success bool
if util.IsTty() { if util.IsTty() {
// The default command for *nix requires bash
shell := "bash"
cmd := os.Getenv("FZF_DEFAULT_COMMAND") cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 { if len(cmd) == 0 {
cmd = defaultCommand success = r.readFromCommand(&shell, defaultCommand)
} else {
success = r.readFromCommand(nil, cmd)
} }
success = r.readFromCommand(cmd)
} else { } else {
success = r.readFromStdin() success = r.readFromStdin()
} }
@@ -100,16 +144,25 @@ func (r *Reader) readFromStdin() bool {
return true return true
} }
func (r *Reader) readFromCommand(cmd string) bool { func (r *Reader) readFromCommand(shell *string, command string) bool {
listCommand := util.ExecCommand(cmd) r.mutex.Lock()
out, err := listCommand.StdoutPipe() r.killed = false
r.command = &command
if shell != nil {
r.exec = util.ExecCommandWith(*shell, command, true)
} else {
r.exec = util.ExecCommand(command, true)
}
out, err := r.exec.StdoutPipe()
if err != nil { if err != nil {
r.mutex.Unlock()
return false return false
} }
err = listCommand.Start() err = r.exec.Start()
r.mutex.Unlock()
if err != nil { if err != nil {
return false return false
} }
r.feed(out) r.feed(out)
return listCommand.Wait() == nil return r.exec.Wait() == nil
} }

View File

@@ -10,10 +10,9 @@ import (
func TestReadFromCommand(t *testing.T) { func TestReadFromCommand(t *testing.T) {
strs := []string{} strs := []string{}
eb := util.NewEventBox() eb := util.NewEventBox()
reader := Reader{ reader := NewReader(
pusher: func(s []byte) bool { strs = append(strs, string(s)); return true }, func(s []byte) bool { strs = append(strs, string(s)); return true },
eventBox: eb, eb, false, true)
event: int32(EvtReady)}
reader.startEventPoller() reader.startEventPoller()
@@ -23,7 +22,7 @@ func TestReadFromCommand(t *testing.T) {
} }
// Normal command // Normal command
reader.fin(reader.readFromCommand(`echo abc && echo def`)) reader.fin(reader.readFromCommand(nil, `echo abc && echo def`))
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" { if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" {
t.Errorf("%s", strs) t.Errorf("%s", strs)
} }
@@ -48,7 +47,7 @@ func TestReadFromCommand(t *testing.T) {
reader.startEventPoller() reader.startEventPoller()
// Failing command // Failing command
reader.fin(reader.readFromCommand(`no-such-command`)) reader.fin(reader.readFromCommand(nil, `no-such-command`))
strs = []string{} strs = []string{}
if len(strs) > 0 { if len(strs) > 0 {
t.Errorf("%s", strs) t.Errorf("%s", strs)

View File

@@ -16,7 +16,6 @@ type colorOffset struct {
offset [2]int32 offset [2]int32
color tui.ColorPair color tui.ColorPair
attr tui.Attr attr tui.Attr
index int32
} }
type Result struct { type Result struct {

File diff suppressed because it is too large Load Diff

View File

@@ -21,73 +21,119 @@ func TestReplacePlaceholder(t *testing.T) {
newItem("foo'bar \x1b[31mbaz\x1b[m"), newItem("foo'bar \x1b[31mbaz\x1b[m"),
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")} newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}
delim := "'"
var regex *regexp.Regexp
var result string var result string
check := func(expected string) { check := func(expected string) {
if result != expected { if result != expected {
t.Errorf("expected: %s, actual: %s", expected, result) t.Errorf("expected: %s, actual: %s", expected, result)
} }
} }
printsep := "\n"
// {}, preserve ansi // {}, preserve ansi
result = replacePlaceholder("echo {}", false, Delimiter{}, false, "query", items1) result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1)
check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'") check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'")
// {}, strip ansi // {}, strip ansi
result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items1) result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'") check("echo ' foo'\\''bar baz'")
// {}, with multiple items // {}, with multiple items
result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items2) result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2)
check("echo 'foo'\\''bar baz'") check("echo 'foo'\\''bar baz'")
// {..}, strip leading whitespaces, preserve ansi // {..}, strip leading whitespaces, preserve ansi
result = replacePlaceholder("echo {..}", false, Delimiter{}, false, "query", items1) result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1)
check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'") check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'")
// {..}, strip leading whitespaces, strip ansi // {..}, strip leading whitespaces, strip ansi
result = replacePlaceholder("echo {..}", true, Delimiter{}, false, "query", items1) result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
check("echo 'foo'\\''bar baz'") check("echo 'foo'\\''bar baz'")
// {q} // {q}
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, false, "query", items1) result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz' 'query'") check("echo ' foo'\\''bar baz' 'query'")
// {q}, multiple items // {q}, multiple items
result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, false, "query 'string'", items2) result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2)
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ''query '\\''string'\\''''foo'\\''bar baz' 'FOO'\\''BAR BAZ'") check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ''query '\\''string'\\''''foo'\\''bar baz' 'FOO'\\''BAR BAZ'")
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, false, "query 'string'", items2) result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
check("echo 'foo'\\''bar baz''query '\\''string'\\''''foo'\\''bar baz'") check("echo 'foo'\\''bar baz''query '\\''string'\\''''foo'\\''bar baz'")
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, false, "query", items1) result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1)
check("echo 'foo'\\''bar'/'baz'/'bazfoo'\\''bar'/'baz'/'foo'\\''bar'/' foo'\\''bar baz'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''") check("echo 'foo'\\''bar'/'baz'/'bazfoo'\\''bar'/'baz'/'foo'\\''bar'/' foo'\\''bar baz'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, false, "query", items2) result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
check("echo 'foo'\\''bar'/'baz'/'baz'/'foo'\\''bar'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''") check("echo 'foo'\\''bar'/'baz'/'baz'/'foo'\\''bar'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, false, "query", items2) result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2)
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''") check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
// forcePlus // forcePlus
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, true, "query", items2) result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2)
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''") check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
// Whitespace preserving flag with "'" delimiter
result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo ' foo'")
result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo 'bar baz'")
result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'")
result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'")
// Whitespace preserving flag with regex delimiter
regex = regexp.MustCompile(`\w+`)
result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
check("echo ' '")
result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
check("echo ''\\'''")
result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
check("echo ' '")
// No match // No match
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, nil}) result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
check("echo /") check("echo /")
// No match, but with selections // No match, but with selections
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, item1}) result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
check("echo /' foo'\\''bar baz'") check("echo /' foo'\\''bar baz'")
// String delimiter // String delimiter
delim := "'" result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, false, "query", items1)
check("echo ' foo'\\''bar baz'/'foo'/'bar baz'") check("echo ' foo'\\''bar baz'/'foo'/'bar baz'")
// Regex delimiter // Regex delimiter
regex := regexp.MustCompile("[oa]+") regex = regexp.MustCompile("[oa]+")
// foo'bar baz // foo'bar baz
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, false, "query", items1) result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'") check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'")
} }
func TestQuoteEntryCmd(t *testing.T) {
tests := map[string]string{
`"`: `^"\^"^"`,
`\`: `^"\\^"`,
`\"`: `^"\\\^"^"`,
`"\\\"`: `^"\^"\\\\\\\^"^"`,
`&|<>()@^%!`: `^"^&^|^<^>^(^)^@^^^%^!^"`,
`%USERPROFILE%`: `^"^%USERPROFILE^%^"`,
`C:\Program Files (x86)\`: `^"C:\\Program Files ^(x86^)\\^"`,
}
for input, expected := range tests {
escaped := quoteEntryCmd(input)
if escaped != expected {
t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped)
}
}
}

View File

@@ -2,6 +2,7 @@ package fzf
import ( import (
"bytes" "bytes"
"fmt"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@@ -23,12 +24,22 @@ type Token struct {
prefixLength int32 prefixLength int32
} }
// String returns the string representation of a Token.
func (t Token) String() string {
return fmt.Sprintf("Token{text: %s, prefixLength: %d}", t.text, t.prefixLength)
}
// Delimiter for tokenizing the input // Delimiter for tokenizing the input
type Delimiter struct { type Delimiter struct {
regex *regexp.Regexp regex *regexp.Regexp
str *string str *string
} }
// String returns the string representation of a Delimeter.
func (d Delimiter) String() string {
return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str)
}
func newRange(begin int, end int) Range { func newRange(begin int, end int) Range {
if begin == 1 { if begin == 1 {
begin = rangeEllipsis begin = rangeEllipsis
@@ -227,7 +238,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
for _, part := range parts { for _, part := range parts {
output.WriteString(part.ToString()) output.WriteString(part.ToString())
} }
merged = util.ToChars([]byte(output.String())) merged = util.ToChars(output.Bytes())
} }
var prefixLength int32 var prefixLength int32

View File

@@ -9,35 +9,35 @@ func TestParseRange(t *testing.T) {
i := ".." i := ".."
r, _ := ParseRange(&i) r, _ := ParseRange(&i)
if r.begin != rangeEllipsis || r.end != rangeEllipsis { if r.begin != rangeEllipsis || r.end != rangeEllipsis {
t.Errorf("%s", r) t.Errorf("%v", r)
} }
} }
{ {
i := "3.." i := "3.."
r, _ := ParseRange(&i) r, _ := ParseRange(&i)
if r.begin != 3 || r.end != rangeEllipsis { if r.begin != 3 || r.end != rangeEllipsis {
t.Errorf("%s", r) t.Errorf("%v", r)
} }
} }
{ {
i := "3..5" i := "3..5"
r, _ := ParseRange(&i) r, _ := ParseRange(&i)
if r.begin != 3 || r.end != 5 { if r.begin != 3 || r.end != 5 {
t.Errorf("%s", r) t.Errorf("%v", r)
} }
} }
{ {
i := "-3..-5" i := "-3..-5"
r, _ := ParseRange(&i) r, _ := ParseRange(&i)
if r.begin != -3 || r.end != -5 { if r.begin != -3 || r.end != -5 {
t.Errorf("%s", r) t.Errorf("%v", r)
} }
} }
{ {
i := "3" i := "3"
r, _ := ParseRange(&i) r, _ := ParseRange(&i)
if r.begin != 3 || r.end != 3 { if r.begin != 3 || r.end != 3 {
t.Errorf("%s", r) t.Errorf("%v", r)
} }
} }
} }
@@ -73,7 +73,7 @@ func TestTransform(t *testing.T) {
{ {
ranges := splitNth("1,2,3") ranges := splitNth("1,2,3")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if string(joinTokens(tx)) != "abc: def: ghi: " { if joinTokens(tx) != "abc: def: ghi: " {
t.Errorf("%s", tx) t.Errorf("%s", tx)
} }
} }
@@ -95,7 +95,7 @@ func TestTransform(t *testing.T) {
{ {
ranges := splitNth("1..2,3,2..,1") ranges := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if string(joinTokens(tx)) != " abc: def: ghi: def: ghi: jkl abc:" || if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
len(tx) != 4 || len(tx) != 4 ||
tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 || tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 || tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||

View File

@@ -33,13 +33,12 @@ func (r *FullscreenRenderer) Refresh() {}
func (r *FullscreenRenderer) Close() {} func (r *FullscreenRenderer) Close() {}
func (r *FullscreenRenderer) DoesAutoWrap() bool { return false } func (r *FullscreenRenderer) DoesAutoWrap() bool { return false }
func (r *FullscreenRenderer) IsOptimized() bool { return false }
func (r *FullscreenRenderer) GetChar() Event { return Event{} } func (r *FullscreenRenderer) GetChar() Event { return Event{} }
func (r *FullscreenRenderer) MaxX() int { return 0 } func (r *FullscreenRenderer) MaxX() int { return 0 }
func (r *FullscreenRenderer) MaxY() int { return 0 } func (r *FullscreenRenderer) MaxY() int { return 0 }
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {} func (r *FullscreenRenderer) RefreshWindows(windows []Window) {}
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window { func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
return nil return nil
} }

View File

@@ -27,11 +27,17 @@ const (
const consoleDevice string = "/dev/tty" const consoleDevice string = "/dev/tty"
var offsetRegexp *regexp.Regexp = regexp.MustCompile("\x1b\\[([0-9]+);([0-9]+)R") var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
func openTtyIn() *os.File { func openTtyIn() *os.File {
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
if err != nil { if err != nil {
tty := ttyname()
if len(tty) > 0 {
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
return in
}
}
fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice) fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice)
os.Exit(2) os.Exit(2)
} }
@@ -48,12 +54,14 @@ func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) {
runes := []rune{} runes := []rune{}
for len(bytes) > 0 { for len(bytes) > 0 {
r, sz := utf8.DecodeRune(bytes) r, sz := utf8.DecodeRune(bytes)
if r == utf8.RuneError || r < 32 && nlcr := r == '\n' || r == '\r'
r != '\x1b' && (!allowNLCR || r != '\n' && r != '\r') { if r >= 32 || r == '\x1b' || nlcr {
runes = append(runes, '?') if r == utf8.RuneError || nlcr && !allowNLCR {
runes = append(runes, ' ')
} else { } else {
runes = append(runes, r) runes = append(runes, r)
} }
}
bytes = bytes[sz:] bytes = bytes[sz:]
} }
r.queued += string(runes) r.queued += string(runes)
@@ -105,6 +113,7 @@ type LightWindow struct {
posx int posx int
posy int posy int
tabstop int tabstop int
fg Color
bg Color bg Color
} }
@@ -145,16 +154,18 @@ func (r *LightRenderer) findOffset() (row int, col int) {
for tries := 0; tries < offsetPollTries; tries++ { for tries := 0; tries < offsetPollTries; tries++ {
bytes = r.getBytesInternal(bytes, tries > 0) bytes = r.getBytesInternal(bytes, tries > 0)
offsets := offsetRegexp.FindSubmatch(bytes) offsets := offsetRegexp.FindSubmatch(bytes)
if len(offsets) > 2 { if len(offsets) > 3 {
return atoi(string(offsets[1]), 0) - 1, atoi(string(offsets[2]), 0) - 1 // add anything we skipped over to the input buffer
r.buffer = append(r.buffer, offsets[1]...)
return atoi(string(offsets[2]), 0) - 1, atoi(string(offsets[3]), 0) - 1
} }
} }
return -1, -1 return -1, -1
} }
func repeat(s string, times int) string { func repeat(r rune, times int) string {
if times > 0 { if times > 0 {
return strings.Repeat(s, times) return strings.Repeat(string(r), times)
} }
return "" return ""
} }
@@ -288,6 +299,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
} }
buffer = append(buffer, byte(c)) buffer = append(buffer, byte(c))
pc := c
for { for {
c, ok = r.getch(true) c, ok = r.getch(true)
if !ok { if !ok {
@@ -297,9 +309,13 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
continue continue
} }
break break
} } else if c == ESC && pc != c {
retries = r.escDelay / escPollInterval
} else {
retries = 0 retries = 0
}
buffer = append(buffer, byte(c)) buffer = append(buffer, byte(c))
pc = c
} }
return buffer return buffer
@@ -329,6 +345,14 @@ func (r *LightRenderer) GetChar() Event {
return Event{BSpace, 0, nil} return Event{BSpace, 0, nil}
case 0: case 0:
return Event{CtrlSpace, 0, nil} return Event{CtrlSpace, 0, nil}
case 28:
return Event{CtrlBackSlash, 0, nil}
case 29:
return Event{CtrlRightBracket, 0, nil}
case 30:
return Event{CtrlCaret, 0, nil}
case 31:
return Event{CtrlSlash, 0, nil}
case ESC: case ESC:
ev := r.escSequence(&sz) ev := r.escSequence(&sz)
// Second chance // Second chance
@@ -359,7 +383,14 @@ func (r *LightRenderer) escSequence(sz *int) Event {
if r.buffer[1] >= 1 && r.buffer[1] <= 'z'-'a'+1 { if r.buffer[1] >= 1 && r.buffer[1] <= 'z'-'a'+1 {
return Event{int(CtrlAltA + r.buffer[1] - 1), 0, nil} return Event{int(CtrlAltA + r.buffer[1] - 1), 0, nil}
} }
alt := false
if len(r.buffer) > 2 && r.buffer[1] == ESC {
r.buffer = r.buffer[1:]
alt = true
}
switch r.buffer[1] { switch r.buffer[1] {
case ESC:
return Event{ESC, 0, nil}
case 32: case 32:
return Event{AltSpace, 0, nil} return Event{AltSpace, 0, nil}
case 47: case 47:
@@ -379,12 +410,25 @@ func (r *LightRenderer) escSequence(sz *int) Event {
*sz = 3 *sz = 3
switch r.buffer[2] { switch r.buffer[2] {
case 68: case 68:
if alt {
return Event{AltLeft, 0, nil}
}
return Event{Left, 0, nil} return Event{Left, 0, nil}
case 67: case 67:
if alt {
// Ugh..
return Event{AltRight, 0, nil}
}
return Event{Right, 0, nil} return Event{Right, 0, nil}
case 66: case 66:
if alt {
return Event{AltDown, 0, nil}
}
return Event{Down, 0, nil} return Event{Down, 0, nil}
case 65: case 65:
if alt {
return Event{AltUp, 0, nil}
}
return Event{Up, 0, nil} return Event{Up, 0, nil}
case 90: case 90:
return Event{BTab, 0, nil} return Event{BTab, 0, nil}
@@ -457,25 +501,22 @@ func (r *LightRenderer) escSequence(sz *int) Event {
} }
} }
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
case 59: case ';':
if len(r.buffer) != 6 { if len(r.buffer) != 6 {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
*sz = 6 *sz = 6
switch r.buffer[4] { switch r.buffer[4] {
case 50: case '2', '5':
switch r.buffer[5] { switch r.buffer[5] {
case 68: case 'A':
return Event{Home, 0, nil} return Event{SUp, 0, nil}
case 67: case 'B':
return Event{End, 0, nil} return Event{SDown, 0, nil}
} case 'C':
case 53:
switch r.buffer[5] {
case 68:
return Event{SLeft, 0, nil}
case 67:
return Event{SRight, 0, nil} return Event{SRight, 0, nil}
case 'D':
return Event{SLeft, 0, nil}
} }
} // r.buffer[4] } // r.buffer[4]
} // r.buffer[3] } // r.buffer[3]
@@ -485,6 +526,9 @@ func (r *LightRenderer) escSequence(sz *int) Event {
if r.buffer[1] >= 'a' && r.buffer[1] <= 'z' { if r.buffer[1] >= 'a' && r.buffer[1] <= 'z' {
return Event{AltA + int(r.buffer[1]) - 'a', 0, nil} return Event{AltA + int(r.buffer[1]) - 'a', 0, nil}
} }
if r.buffer[1] >= '0' && r.buffer[1] <= '9' {
return Event{Alt0 + int(r.buffer[1]) - '0', 0, nil}
}
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
@@ -494,16 +538,19 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
} }
*sz = 6 *sz = 6
switch r.buffer[3] { switch r.buffer[3] {
case 32, 36, 40, 48, // mouse-down / shift / cmd / ctrl case 32, 34, 36, 40, 48, // mouse-down / shift / cmd / ctrl
35, 39, 43, 51: // mouse-up / shift / cmd / ctrl 35, 39, 43, 51: // mouse-up / shift / cmd / ctrl
mod := r.buffer[3] >= 36 mod := r.buffer[3] >= 36
left := r.buffer[3] == 32
down := r.buffer[3]%2 == 0 down := r.buffer[3]%2 == 0
x := int(r.buffer[4] - 33) x := int(r.buffer[4] - 33)
y := int(r.buffer[5]-33) - r.yoffset y := int(r.buffer[5]-33) - r.yoffset
double := false double := false
if down { if down {
now := time.Now() now := time.Now()
if now.Sub(r.prevDownTime) < doubleClickDuration { if !left { // Right double click is not allowed
r.clickY = []int{}
} else if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clickY = append(r.clickY, y) r.clickY = append(r.clickY, y)
} else { } else {
r.clickY = []int{y} r.clickY = []int{y}
@@ -511,19 +558,19 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
r.prevDownTime = now r.prevDownTime = now
} else { } else {
if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] && if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
time.Now().Sub(r.prevDownTime) < doubleClickDuration { time.Since(r.prevDownTime) < doubleClickDuration {
double = true double = true
} }
} }
return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}} return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl
97, 101, 105, 113: // scroll-down / shift / cmd / ctrl 97, 101, 105, 113: // scroll-down / shift / cmd / ctrl
mod := r.buffer[3] >= 100 mod := r.buffer[3] >= 100
s := 1 - int(r.buffer[3]%2)*2 s := 1 - int(r.buffer[3]%2)*2
x := int(r.buffer[4] - 33) x := int(r.buffer[4] - 33)
y := int(r.buffer[5]-33) - r.yoffset y := int(r.buffer[5]-33) - r.yoffset
return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, mod}} return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, false, mod}}
} }
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
@@ -619,11 +666,7 @@ func (r *LightRenderer) DoesAutoWrap() bool {
return false return false
} }
func (r *LightRenderer) IsOptimized() bool { func (r *LightRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
return false
}
func (r *LightRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
w := &LightWindow{ w := &LightWindow{
renderer: r, renderer: r,
colored: r.theme != nil, colored: r.theme != nil,
@@ -633,16 +676,23 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, bord
width: width, width: width,
height: height, height: height,
tabstop: r.tabstop, tabstop: r.tabstop,
fg: colDefault,
bg: colDefault} bg: colDefault}
if r.theme != nil { if r.theme != nil {
if preview {
w.fg = r.theme.PreviewFg
w.bg = r.theme.PreviewBg
} else {
w.fg = r.theme.Fg
w.bg = r.theme.Bg w.bg = r.theme.Bg
} }
}
w.drawBorder() w.drawBorder()
return w return w
} }
func (w *LightWindow) drawBorder() { func (w *LightWindow) drawBorder() {
switch w.border { switch w.border.shape {
case BorderAround: case BorderAround:
w.drawBorderAround() w.drawBorderAround()
case BorderHorizontal: case BorderHorizontal:
@@ -652,32 +702,30 @@ func (w *LightWindow) drawBorder() {
func (w *LightWindow) drawBorderHorizontal() { func (w *LightWindow) drawBorderHorizontal() {
w.Move(0, 0) w.Move(0, 0)
w.CPrint(ColBorder, AttrRegular, repeat("─", w.width)) w.CPrint(ColBorder, AttrRegular, repeat(w.border.horizontal, w.width))
w.Move(w.height-1, 0) w.Move(w.height-1, 0)
w.CPrint(ColBorder, AttrRegular, repeat("─", w.width)) w.CPrint(ColBorder, AttrRegular, repeat(w.border.horizontal, w.width))
} }
func (w *LightWindow) drawBorderAround() { func (w *LightWindow) drawBorderAround() {
w.Move(0, 0) w.Move(0, 0)
w.CPrint(ColBorder, AttrRegular, "┌"+repeat("─", w.width-2)+"┐") w.CPrint(ColPreviewBorder, AttrRegular,
string(w.border.topLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.topRight))
for y := 1; y < w.height-1; y++ { for y := 1; y < w.height-1; y++ {
w.Move(y, 0) w.Move(y, 0)
w.CPrint(ColBorder, AttrRegular, "│") w.CPrint(ColPreviewBorder, AttrRegular, string(w.border.vertical))
w.cprint2(colDefault, w.bg, AttrRegular, repeat(" ", w.width-2)) w.CPrint(ColPreviewBorder, AttrRegular, repeat(' ', w.width-2))
w.CPrint(ColBorder, AttrRegular, "│") w.CPrint(ColPreviewBorder, AttrRegular, string(w.border.vertical))
} }
w.Move(w.height-1, 0) w.Move(w.height-1, 0)
w.CPrint(ColBorder, AttrRegular, "└"+repeat("─", w.width-2)+"┘") w.CPrint(ColPreviewBorder, AttrRegular,
string(w.border.bottomLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.bottomRight))
} }
func (w *LightWindow) csi(code string) { func (w *LightWindow) csi(code string) {
w.renderer.csi(code) w.renderer.csi(code)
} }
func (w *LightWindow) stderr(str string) {
w.renderer.stderr(str)
}
func (w *LightWindow) stderrInternal(str string, allowNLCR bool) { func (w *LightWindow) stderrInternal(str string, allowNLCR bool) {
w.renderer.stderrInternal(str, allowNLCR) w.renderer.stderrInternal(str, allowNLCR)
} }
@@ -728,7 +776,7 @@ func (w *LightWindow) MoveAndClear(y int, x int) {
w.Move(y, x) w.Move(y, x)
// We should not delete preview window on the right // We should not delete preview window on the right
// csi("K") // csi("K")
w.Print(repeat(" ", w.width-x)) w.Print(repeat(' ', w.width-x))
w.Move(y, x) w.Move(y, x)
} }
@@ -790,7 +838,7 @@ func (w *LightWindow) Print(text string) {
} }
func cleanse(str string) string { func cleanse(str string) string {
return strings.Replace(str, "\x1b", "?", -1) return strings.Replace(str, "\x1b", "", -1)
} }
func (w *LightWindow) CPrint(pair ColorPair, attr Attr, text string) { func (w *LightWindow) CPrint(pair ColorPair, attr Attr, text string) {
@@ -824,7 +872,7 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
width += w width += w
str := string(r) str := string(r)
if r == '\t' { if r == '\t' {
str = repeat(" ", w) str = repeat(' ', w)
} }
if prefixLength+width <= max { if prefixLength+width <= max {
line += str line += str
@@ -881,6 +929,9 @@ func (w *LightWindow) Fill(text string) FillReturn {
func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn { func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn {
w.Move(w.posy, w.posx) w.Move(w.posy, w.posx)
if fg == colDefault {
fg = w.fg
}
if bg == colDefault { if bg == colDefault {
bg = w.bg bg = w.bg
} }

View File

@@ -1,505 +0,0 @@
// +build ncurses
// +build !windows
// +build !tcell
package tui
/*
#include <ncurses.h>
#include <locale.h>
#cgo !static LDFLAGS: -lncurses
#cgo static LDFLAGS: -l:libncursesw.a -l:libtinfo.a -l:libgpm.a -ldl
#cgo android static LDFLAGS: -l:libncurses.a -fPIE -march=armv7-a -mfpu=neon -mhard-float -Wl,--no-warn-mismatch
FILE* c_tty() {
return fopen("/dev/tty", "r");
}
SCREEN* c_newterm(FILE* tty) {
return newterm(NULL, stderr, tty);
}
int c_getcurx(WINDOW* win) {
return getcurx(win);
}
*/
import "C"
import (
"os"
"strconv"
"strings"
"time"
"unicode/utf8"
)
func HasFullscreenRenderer() bool {
return true
}
type Attr C.uint
type CursesWindow struct {
impl *C.WINDOW
top int
left int
width int
height int
}
func (w *CursesWindow) Top() int {
return w.top
}
func (w *CursesWindow) Left() int {
return w.left
}
func (w *CursesWindow) Width() int {
return w.width
}
func (w *CursesWindow) Height() int {
return w.height
}
func (w *CursesWindow) Refresh() {
C.wnoutrefresh(w.impl)
}
func (w *CursesWindow) FinishFill() {
// NO-OP
}
const (
Bold Attr = C.A_BOLD
Dim = C.A_DIM
Blink = C.A_BLINK
Reverse = C.A_REVERSE
Underline = C.A_UNDERLINE
)
var Italic Attr = C.A_VERTICAL << 1 // FIXME
const (
AttrRegular Attr = 0
)
var (
_screen *C.SCREEN
_colorMap map[int]int16
_colorFn func(ColorPair, Attr) (C.short, C.int)
)
func init() {
_colorMap = make(map[int]int16)
if strings.HasPrefix(C.GoString(C.curses_version()), "ncurses 5") {
Italic = C.A_NORMAL
}
}
func (a Attr) Merge(b Attr) Attr {
return a | b
}
func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
if C.tigetnum(C.CString("colors")) >= 256 {
return Dark256
}
return Default16
}
func (r *FullscreenRenderer) Init() {
C.setlocale(C.LC_ALL, C.CString(""))
tty := C.c_tty()
if tty == nil {
errorExit("Failed to open /dev/tty")
}
_screen = C.c_newterm(tty)
if _screen == nil {
errorExit("Invalid $TERM: " + os.Getenv("TERM"))
}
C.set_term(_screen)
if r.mouse {
C.mousemask(C.ALL_MOUSE_EVENTS, nil)
C.mouseinterval(0)
}
C.noecho()
C.raw() // stty dsusp undef
C.nonl()
C.keypad(C.stdscr, true)
delay := 50
delayEnv := os.Getenv("ESCDELAY")
if len(delayEnv) > 0 {
num, err := strconv.Atoi(delayEnv)
if err == nil && num >= 0 {
delay = num
}
}
C.set_escdelay(C.int(delay))
if r.theme != nil {
C.start_color()
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
initPairs(r.theme)
C.bkgd(C.chtype(C.COLOR_PAIR(C.int(ColNormal.index()))))
_colorFn = attrColored
} else {
initTheme(r.theme, nil, r.forceBlack)
_colorFn = attrMono
}
C.nodelay(C.stdscr, true)
ch := C.getch()
if ch != C.ERR {
C.ungetch(ch)
}
C.nodelay(C.stdscr, false)
}
func initPairs(theme *ColorTheme) {
C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg))
for _, pair := range []ColorPair{
ColNormal,
ColPrompt,
ColMatch,
ColCurrent,
ColCurrentMatch,
ColSpinner,
ColInfo,
ColCursor,
ColSelected,
ColHeader,
ColBorder} {
C.init_pair(C.short(pair.index()), C.short(pair.Fg()), C.short(pair.Bg()))
}
}
func (r *FullscreenRenderer) Pause(bool) {
C.endwin()
}
func (r *FullscreenRenderer) Resume(bool) {
}
func (r *FullscreenRenderer) Close() {
C.endwin()
C.delscreen(_screen)
}
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left))
if r.theme != nil {
C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal.index()))))
}
// FIXME Does not implement BorderHorizontal
if borderStyle != BorderNone {
pair, attr := _colorFn(ColBorder, 0)
C.wcolor_set(win, pair, nil)
C.wattron(win, attr)
C.box(win, 0, 0)
C.wattroff(win, attr)
C.wcolor_set(win, 0, nil)
}
return &CursesWindow{
impl: win,
top: top,
left: left,
width: width,
height: height,
}
}
func attrColored(color ColorPair, a Attr) (C.short, C.int) {
return C.short(color.index()), C.int(a)
}
func attrMono(color ColorPair, a Attr) (C.short, C.int) {
return 0, C.int(attrFor(color, a))
}
func (r *FullscreenRenderer) MaxX() int {
return int(C.COLS)
}
func (r *FullscreenRenderer) MaxY() int {
return int(C.LINES)
}
func (w *CursesWindow) Close() {
C.delwin(w.impl)
}
func (w *CursesWindow) Enclose(y int, x int) bool {
return bool(C.wenclose(w.impl, C.int(y), C.int(x)))
}
func (w *CursesWindow) Move(y int, x int) {
C.wmove(w.impl, C.int(y), C.int(x))
}
func (w *CursesWindow) MoveAndClear(y int, x int) {
w.Move(y, x)
C.wclrtoeol(w.impl)
}
func (w *CursesWindow) Print(text string) {
C.waddstr(w.impl, C.CString(strings.Map(func(r rune) rune {
if r < 32 {
return -1
}
return r
}, text)))
}
func (w *CursesWindow) CPrint(color ColorPair, attr Attr, text string) {
p, a := _colorFn(color, attr)
C.wcolor_set(w.impl, p, nil)
C.wattron(w.impl, a)
w.Print(text)
C.wattroff(w.impl, a)
C.wcolor_set(w.impl, 0, nil)
}
func (r *FullscreenRenderer) Clear() {
C.clear()
C.endwin()
}
func (r *FullscreenRenderer) Refresh() {
C.refresh()
}
func (w *CursesWindow) Erase() {
C.werase(w.impl)
}
func (w *CursesWindow) X() int {
return int(C.c_getcurx(w.impl))
}
func (r *FullscreenRenderer) DoesAutoWrap() bool {
return true
}
func (r *FullscreenRenderer) IsOptimized() bool {
return true
}
func (w *CursesWindow) Fill(str string) FillReturn {
if C.waddstr(w.impl, C.CString(str)) == C.OK {
return FillContinue
}
return FillSuspend
}
func (w *CursesWindow) CFill(fg Color, bg Color, attr Attr, str string) FillReturn {
index := ColorPair{fg, bg, -1}.index()
C.wcolor_set(w.impl, C.short(index), nil)
C.wattron(w.impl, C.int(attr))
ret := w.Fill(str)
C.wattroff(w.impl, C.int(attr))
C.wcolor_set(w.impl, 0, nil)
return ret
}
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
for _, w := range windows {
w.Refresh()
}
C.doupdate()
}
func (p ColorPair) index() int16 {
if p.id >= 0 {
return p.id
}
// ncurses does not support 24-bit colors
if p.is24() {
return ColDefault.index()
}
key := p.key()
if found, prs := _colorMap[key]; prs {
return found
}
id := int16(len(_colorMap)) + ColUser.id
C.init_pair(C.short(id), C.short(p.Fg()), C.short(p.Bg()))
_colorMap[key] = id
return id
}
func consume(expects ...rune) bool {
for _, r := range expects {
if int(C.getch()) != int(r) {
return false
}
}
return true
}
func escSequence() Event {
C.nodelay(C.stdscr, true)
defer func() {
C.nodelay(C.stdscr, false)
}()
c := C.getch()
switch c {
case C.ERR:
return Event{ESC, 0, nil}
case CtrlM:
return Event{CtrlAltM, 0, nil}
case '/':
return Event{AltSlash, 0, nil}
case ' ':
return Event{AltSpace, 0, nil}
case 127, C.KEY_BACKSPACE:
return Event{AltBS, 0, nil}
case '[':
// Bracketed paste mode (printf "\e[?2004h")
// \e[200~ TEXT \e[201~
if consume('2', '0', '0', '~') {
return Event{Invalid, 0, nil}
}
}
if c >= 'a' && c <= 'z' {
return Event{AltA + int(c) - 'a', 0, nil}
}
if c >= '0' && c <= '9' {
return Event{Alt0 + int(c) - '0', 0, nil}
}
// Don't care. Ignore the rest.
for ; c != C.ERR; c = C.getch() {
}
return Event{Invalid, 0, nil}
}
func (r *FullscreenRenderer) GetChar() Event {
c := C.getch()
switch c {
case C.ERR:
// Unexpected error from blocking read
r.Close()
errorExit("Failed to read /dev/tty")
case C.KEY_UP:
return Event{Up, 0, nil}
case C.KEY_DOWN:
return Event{Down, 0, nil}
case C.KEY_LEFT:
return Event{Left, 0, nil}
case C.KEY_RIGHT:
return Event{Right, 0, nil}
case C.KEY_HOME:
return Event{Home, 0, nil}
case C.KEY_END:
return Event{End, 0, nil}
case C.KEY_BACKSPACE:
return Event{BSpace, 0, nil}
case C.KEY_F0 + 1:
return Event{F1, 0, nil}
case C.KEY_F0 + 2:
return Event{F2, 0, nil}
case C.KEY_F0 + 3:
return Event{F3, 0, nil}
case C.KEY_F0 + 4:
return Event{F4, 0, nil}
case C.KEY_F0 + 5:
return Event{F5, 0, nil}
case C.KEY_F0 + 6:
return Event{F6, 0, nil}
case C.KEY_F0 + 7:
return Event{F7, 0, nil}
case C.KEY_F0 + 8:
return Event{F8, 0, nil}
case C.KEY_F0 + 9:
return Event{F9, 0, nil}
case C.KEY_F0 + 10:
return Event{F10, 0, nil}
case C.KEY_F0 + 11:
return Event{F11, 0, nil}
case C.KEY_F0 + 12:
return Event{F12, 0, nil}
case C.KEY_DC:
return Event{Del, 0, nil}
case C.KEY_PPAGE:
return Event{PgUp, 0, nil}
case C.KEY_NPAGE:
return Event{PgDn, 0, nil}
case C.KEY_BTAB:
return Event{BTab, 0, nil}
case C.KEY_ENTER:
return Event{CtrlM, 0, nil}
case C.KEY_SLEFT:
return Event{SLeft, 0, nil}
case C.KEY_SRIGHT:
return Event{SRight, 0, nil}
case C.KEY_MOUSE:
var me C.MEVENT
if C.getmouse(&me) != C.ERR {
mod := ((me.bstate & C.BUTTON_SHIFT) | (me.bstate & C.BUTTON_CTRL) | (me.bstate & C.BUTTON_ALT)) > 0
x := int(me.x)
y := int(me.y)
/* Cannot use BUTTON1_DOUBLE_CLICKED due to mouseinterval(0) */
if (me.bstate & C.BUTTON1_PRESSED) > 0 {
now := time.Now()
if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clickY = append(r.clickY, y)
} else {
r.clickY = []int{y}
r.prevDownTime = now
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, false, mod}}
} else if (me.bstate & C.BUTTON1_RELEASED) > 0 {
double := false
if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
time.Now().Sub(r.prevDownTime) < doubleClickDuration {
double = true
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, double, mod}}
} else if (me.bstate&0x8000000) > 0 || (me.bstate&0x80) > 0 {
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, mod}}
} else if (me.bstate & C.BUTTON4_PRESSED) > 0 {
return Event{Mouse, 0, &MouseEvent{y, x, 1, false, false, mod}}
}
}
return Event{Invalid, 0, nil}
case C.KEY_RESIZE:
return Event{Resize, 0, nil}
case ESC:
return escSequence()
case 127:
return Event{BSpace, 0, nil}
case 0:
return Event{CtrlSpace, 0, nil}
}
// CTRL-A ~ CTRL-Z
if c >= CtrlA && c <= CtrlZ {
return Event{int(c), 0, nil}
}
// Multi-byte character
buffer := []byte{byte(c)}
for {
r, _ := utf8.DecodeRune(buffer)
if r != utf8.RuneError {
return Event{Rune, r, nil}
}
c := C.getch()
if c == C.ERR {
break
}
if c >= C.KEY_CODE_YES {
C.ungetch(c)
break
}
buffer = append(buffer, byte(c))
}
return Event{Invalid, 0, nil}
}

View File

@@ -32,6 +32,7 @@ type TcellWindow struct {
left int left int
width int width int
height int height int
normal ColorPair
lastX int lastX int
lastY int lastY int
moveCursor bool moveCursor bool
@@ -61,12 +62,8 @@ func (w *TcellWindow) Refresh() {
} }
w.lastX = 0 w.lastX = 0
w.lastY = 0 w.lastY = 0
switch w.borderStyle {
case BorderAround: w.drawBorder()
w.drawBorder(true)
case BorderHorizontal:
w.drawBorder(false)
}
} }
func (w *TcellWindow) FinishFill() { func (w *TcellWindow) FinishFill() {
@@ -172,10 +169,6 @@ func (r *FullscreenRenderer) DoesAutoWrap() bool {
return false return false
} }
func (r *FullscreenRenderer) IsOptimized() bool {
return false
}
func (r *FullscreenRenderer) Clear() { func (r *FullscreenRenderer) Clear() {
_screen.Sync() _screen.Sync()
_screen.Clear() _screen.Clear()
@@ -197,19 +190,22 @@ func (r *FullscreenRenderer) GetChar() Event {
button := ev.Buttons() button := ev.Buttons()
mod := ev.Modifiers() != 0 mod := ev.Modifiers() != 0
if button&tcell.WheelDown != 0 { if button&tcell.WheelDown != 0 {
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, mod}} return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}}
} else if button&tcell.WheelUp != 0 { } else if button&tcell.WheelUp != 0 {
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, mod}} return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}}
} else if runtime.GOOS != "windows" { } else if runtime.GOOS != "windows" {
// double and single taps on Windows don't quite work due to // double and single taps on Windows don't quite work due to
// the console acting on the events and not allowing us // the console acting on the events and not allowing us
// to consume them. // to consume them.
down := button&tcell.Button1 != 0 // left left := button&tcell.Button1 != 0
down := left || button&tcell.Button3 != 0
double := false double := false
if down { if down {
now := time.Now() now := time.Now()
if now.Sub(r.prevDownTime) < doubleClickDuration { if !left {
r.clickY = []int{}
} else if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clickY = append(r.clickY, x) r.clickY = append(r.clickY, x)
} else { } else {
r.clickY = []int{x} r.clickY = []int{x}
@@ -222,7 +218,7 @@ func (r *FullscreenRenderer) GetChar() Event {
} }
} }
return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}} return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
} }
// process keyboard: // process keyboard:
@@ -289,6 +285,12 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{keyfn('z'), 0, nil} return Event{keyfn('z'), 0, nil}
case tcell.KeyCtrlSpace: case tcell.KeyCtrlSpace:
return Event{CtrlSpace, 0, nil} return Event{CtrlSpace, 0, nil}
case tcell.KeyCtrlBackslash:
return Event{CtrlBackSlash, 0, nil}
case tcell.KeyCtrlRightSq:
return Event{CtrlRightBracket, 0, nil}
case tcell.KeyCtrlUnderscore:
return Event{CtrlSlash, 0, nil}
case tcell.KeyBackspace2: case tcell.KeyBackspace2:
if alt { if alt {
return Event{AltBS, 0, nil} return Event{AltBS, 0, nil}
@@ -296,12 +298,24 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{BSpace, 0, nil} return Event{BSpace, 0, nil}
case tcell.KeyUp: case tcell.KeyUp:
if alt {
return Event{AltUp, 0, nil}
}
return Event{Up, 0, nil} return Event{Up, 0, nil}
case tcell.KeyDown: case tcell.KeyDown:
if alt {
return Event{AltDown, 0, nil}
}
return Event{Down, 0, nil} return Event{Down, 0, nil}
case tcell.KeyLeft: case tcell.KeyLeft:
if alt {
return Event{AltLeft, 0, nil}
}
return Event{Left, 0, nil} return Event{Left, 0, nil}
case tcell.KeyRight: case tcell.KeyRight:
if alt {
return Event{AltRight, 0, nil}
}
return Event{Right, 0, nil} return Event{Right, 0, nil}
case tcell.KeyHome: case tcell.KeyHome:
@@ -371,13 +385,17 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
func (r *FullscreenRenderer) Pause(bool) { func (r *FullscreenRenderer) Pause(clear bool) {
if clear {
_screen.Fini() _screen.Fini()
} }
}
func (r *FullscreenRenderer) Resume(bool) { func (r *FullscreenRenderer) Resume(clear bool) {
if clear {
r.initScreen() r.initScreen()
} }
}
func (r *FullscreenRenderer) Close() { func (r *FullscreenRenderer) Close() {
_screen.Fini() _screen.Fini()
@@ -391,14 +409,18 @@ func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
_screen.Show() _screen.Show()
} }
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window { func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
// TODO normal := ColNormal
if preview {
normal = ColPreview
}
return &TcellWindow{ return &TcellWindow{
color: r.theme != nil, color: r.theme != nil,
top: top, top: top,
left: left, left: left,
width: width, width: width,
height: height, height: height,
normal: normal,
borderStyle: borderStyle} borderStyle: borderStyle}
} }
@@ -406,17 +428,16 @@ func (w *TcellWindow) Close() {
// TODO // TODO
} }
func fill(x, y, w, h int, r rune) { func fill(x, y, w, h int, n ColorPair, r rune) {
for ly := 0; ly <= h; ly++ { for ly := 0; ly <= h; ly++ {
for lx := 0; lx <= w; lx++ { for lx := 0; lx <= w; lx++ {
_screen.SetContent(x+lx, y+ly, r, nil, ColDefault.style()) _screen.SetContent(x+lx, y+ly, r, nil, n.style())
} }
} }
} }
func (w *TcellWindow) Erase() { func (w *TcellWindow) Erase() {
// TODO fill(w.left-1, w.top, w.width+1, w.height, w.normal, ' ')
fill(w.left, w.top, w.width, w.height, ' ')
} }
func (w *TcellWindow) Enclose(y int, x int) bool { func (w *TcellWindow) Enclose(y int, x int) bool {
@@ -433,13 +454,13 @@ func (w *TcellWindow) Move(y int, x int) {
func (w *TcellWindow) MoveAndClear(y int, x int) { func (w *TcellWindow) MoveAndClear(y int, x int) {
w.Move(y, x) w.Move(y, x)
for i := w.lastX; i < w.width; i++ { for i := w.lastX; i < w.width; i++ {
_screen.SetContent(i+w.left, w.lastY+w.top, rune(' '), nil, ColDefault.style()) _screen.SetContent(i+w.left, w.lastY+w.top, rune(' '), nil, w.normal.style())
} }
w.lastX = x w.lastX = x
} }
func (w *TcellWindow) Print(text string) { func (w *TcellWindow) Print(text string) {
w.printString(text, ColDefault, 0) w.printString(text, w.normal, 0)
} }
func (w *TcellWindow) printString(text string, pair ColorPair, a Attr) { func (w *TcellWindow) printString(text string, pair ColorPair, a Attr) {
@@ -452,7 +473,7 @@ func (w *TcellWindow) printString(text string, pair ColorPair, a Attr) {
Reverse(a&Attr(tcell.AttrReverse) != 0). Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0) Underline(a&Attr(tcell.AttrUnderline) != 0)
} else { } else {
style = ColDefault.style(). style = w.normal.style().
Reverse(a&Attr(tcell.AttrReverse) != 0 || pair == ColCurrent || pair == ColCurrentMatch). Reverse(a&Attr(tcell.AttrReverse) != 0 || pair == ColCurrent || pair == ColCurrentMatch).
Underline(a&Attr(tcell.AttrUnderline) != 0 || pair == ColMatch || pair == ColCurrentMatch) Underline(a&Attr(tcell.AttrUnderline) != 0 || pair == ColMatch || pair == ColCurrentMatch)
} }
@@ -503,7 +524,7 @@ func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) FillReturn
if w.color { if w.color {
style = pair.style() style = pair.style()
} else { } else {
style = ColDefault.style() style = w.normal.style()
} }
style = style. style = style.
Blink(a&Attr(tcell.AttrBlink) != 0). Blink(a&Attr(tcell.AttrBlink) != 0).
@@ -543,14 +564,24 @@ func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) FillReturn
} }
func (w *TcellWindow) Fill(str string) FillReturn { func (w *TcellWindow) Fill(str string) FillReturn {
return w.fillString(str, ColDefault, 0) return w.fillString(str, w.normal, 0)
} }
func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn { func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
return w.fillString(str, ColorPair{fg, bg, -1}, a) if fg == colDefault {
fg = w.normal.Fg()
}
if bg == colDefault {
bg = w.normal.Bg()
}
return w.fillString(str, NewColorPair(fg, bg), a)
}
func (w *TcellWindow) drawBorder() {
if w.borderStyle.shape == BorderNone {
return
} }
func (w *TcellWindow) drawBorder(around bool) {
left := w.left left := w.left
right := left + w.width right := left + w.width
top := w.top top := w.top
@@ -558,25 +589,29 @@ func (w *TcellWindow) drawBorder(around bool) {
var style tcell.Style var style tcell.Style
if w.color { if w.color {
style = ColBorder.style() if w.borderStyle.shape == BorderAround {
style = ColPreviewBorder.style()
} else { } else {
style = ColDefault.style() style = ColBorder.style()
}
} else {
style = w.normal.style()
} }
for x := left; x < right; x++ { for x := left; x < right; x++ {
_screen.SetContent(x, top, tcell.RuneHLine, nil, style) _screen.SetContent(x, top, w.borderStyle.horizontal, nil, style)
_screen.SetContent(x, bot-1, tcell.RuneHLine, nil, style) _screen.SetContent(x, bot-1, w.borderStyle.horizontal, nil, style)
} }
if around { if w.borderStyle.shape == BorderAround {
for y := top; y < bot; y++ { for y := top; y < bot; y++ {
_screen.SetContent(left, y, tcell.RuneVLine, nil, style) _screen.SetContent(left, y, w.borderStyle.vertical, nil, style)
_screen.SetContent(right-1, y, tcell.RuneVLine, nil, style) _screen.SetContent(right-1, y, w.borderStyle.vertical, nil, style)
} }
_screen.SetContent(left, top, tcell.RuneULCorner, nil, style) _screen.SetContent(left, top, w.borderStyle.topLeft, nil, style)
_screen.SetContent(right-1, top, tcell.RuneURCorner, nil, style) _screen.SetContent(right-1, top, w.borderStyle.topRight, nil, style)
_screen.SetContent(left, bot-1, tcell.RuneLLCorner, nil, style) _screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style)
_screen.SetContent(right-1, bot-1, tcell.RuneLRCorner, nil, style) _screen.SetContent(right-1, bot-1, w.borderStyle.bottomRight, nil, style)
} }
} }

31
src/tui/ttyname_unix.go Normal file
View File

@@ -0,0 +1,31 @@
// +build !windows
package tui
import (
"io/ioutil"
"syscall"
)
var devPrefixes = [...]string{"/dev/pts/", "/dev/"}
func ttyname() string {
var stderr syscall.Stat_t
if syscall.Fstat(2, &stderr) != nil {
return ""
}
for _, prefix := range devPrefixes {
files, err := ioutil.ReadDir(prefix)
if err != nil {
continue
}
for _, file := range files {
if stat, ok := file.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev {
return prefix + file.Name()
}
}
}
return ""
}

View File

@@ -0,0 +1,7 @@
// +build windows
package tui
func ttyname() string {
return ""
}

View File

@@ -40,10 +40,18 @@ const (
ESC ESC
CtrlSpace CtrlSpace
// https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal
CtrlBackSlash
CtrlRightBracket
CtrlCaret
CtrlSlash
Invalid Invalid
Resize Resize
Mouse Mouse
DoubleClick DoubleClick
LeftClick
RightClick
BTab BTab
BSpace BSpace
@@ -59,6 +67,8 @@ const (
Home Home
End End
SUp
SDown
SLeft SLeft
SRight SRight
@@ -81,6 +91,11 @@ const (
AltSlash AltSlash
AltBS AltBS
AltUp
AltDown
AltLeft
AltRight
Alt0 Alt0
) )
@@ -108,7 +123,7 @@ func (c Color) is24() bool {
const ( const (
colUndefined Color = -2 colUndefined Color = -2
colDefault = -1 colDefault Color = -1
) )
const ( const (
@@ -133,7 +148,7 @@ const (
type ColorPair struct { type ColorPair struct {
fg Color fg Color
bg Color bg Color
id int16 id int
} }
func HexToColor(rrggbb string) Color { func HexToColor(rrggbb string) Color {
@@ -155,18 +170,13 @@ func (p ColorPair) Bg() Color {
return p.bg return p.bg
} }
func (p ColorPair) key() int {
return (int(p.Fg()) << 8) + int(p.Bg())
}
func (p ColorPair) is24() bool {
return p.Fg().is24() || p.Bg().is24()
}
type ColorTheme struct { type ColorTheme struct {
Fg Color Fg Color
Bg Color Bg Color
PreviewFg Color
PreviewBg Color
DarkBg Color DarkBg Color
Gutter Color
Prompt Color Prompt Color
Match Color Match Color
Current Color Current Color
@@ -179,10 +189,6 @@ type ColorTheme struct {
Border Color Border Color
} }
func (t *ColorTheme) HasBg() bool {
return t.Bg != colDefault
}
type Event struct { type Event struct {
Type int Type int
Char rune Char rune
@@ -193,19 +199,66 @@ type MouseEvent struct {
Y int Y int
X int X int
S int S int
Left bool
Down bool Down bool
Double bool Double bool
Mod bool Mod bool
} }
type BorderStyle int type BorderShape int
const ( const (
BorderNone BorderStyle = iota BorderNone BorderShape = iota
BorderAround BorderAround
BorderHorizontal BorderHorizontal
) )
type BorderStyle struct {
shape BorderShape
horizontal rune
vertical rune
topLeft rune
topRight rune
bottomLeft rune
bottomRight rune
}
type BorderCharacter int
func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
if unicode {
return BorderStyle{
shape: shape,
horizontal: '─',
vertical: '│',
topLeft: '┌',
topRight: '┐',
bottomLeft: '└',
bottomRight: '┘',
}
}
return BorderStyle{
shape: shape,
horizontal: '-',
vertical: '|',
topLeft: '+',
topRight: '+',
bottomLeft: '+',
bottomRight: '+',
}
}
func MakeTransparentBorder() BorderStyle {
return BorderStyle{
shape: BorderAround,
horizontal: ' ',
vertical: ' ',
topLeft: ' ',
topRight: ' ',
bottomLeft: ' ',
bottomRight: ' '}
}
type Renderer interface { type Renderer interface {
Init() Init()
Pause(clear bool) Pause(clear bool)
@@ -220,9 +273,8 @@ type Renderer interface {
MaxX() int MaxX() int
MaxY() int MaxY() int
DoesAutoWrap() bool DoesAutoWrap() bool
IsOptimized() bool
NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window
} }
type Window interface { type Window interface {
@@ -271,26 +323,31 @@ var (
Dark256 *ColorTheme Dark256 *ColorTheme
Light256 *ColorTheme Light256 *ColorTheme
ColDefault ColorPair
ColNormal ColorPair
ColPrompt ColorPair ColPrompt ColorPair
ColNormal ColorPair
ColMatch ColorPair ColMatch ColorPair
ColCurrent ColorPair
ColCurrentMatch ColorPair
ColSpinner ColorPair
ColInfo ColorPair
ColCursor ColorPair ColCursor ColorPair
ColSelected ColorPair ColSelected ColorPair
ColCurrent ColorPair
ColCurrentMatch ColorPair
ColCurrentCursor ColorPair
ColCurrentSelected ColorPair
ColSpinner ColorPair
ColInfo ColorPair
ColHeader ColorPair ColHeader ColorPair
ColBorder ColorPair ColBorder ColorPair
ColUser ColorPair ColPreview ColorPair
ColPreviewBorder ColorPair
) )
func EmptyTheme() *ColorTheme { func EmptyTheme() *ColorTheme {
return &ColorTheme{ return &ColorTheme{
Fg: colUndefined, Fg: colUndefined,
Bg: colUndefined, Bg: colUndefined,
PreviewFg: colUndefined,
PreviewBg: colUndefined,
DarkBg: colUndefined, DarkBg: colUndefined,
Gutter: colUndefined,
Prompt: colUndefined, Prompt: colUndefined,
Match: colUndefined, Match: colUndefined,
Current: colUndefined, Current: colUndefined,
@@ -312,7 +369,10 @@ func init() {
Default16 = &ColorTheme{ Default16 = &ColorTheme{
Fg: colDefault, Fg: colDefault,
Bg: colDefault, Bg: colDefault,
PreviewFg: colUndefined,
PreviewBg: colUndefined,
DarkBg: colBlack, DarkBg: colBlack,
Gutter: colUndefined,
Prompt: colBlue, Prompt: colBlue,
Match: colGreen, Match: colGreen,
Current: colYellow, Current: colYellow,
@@ -326,7 +386,10 @@ func init() {
Dark256 = &ColorTheme{ Dark256 = &ColorTheme{
Fg: colDefault, Fg: colDefault,
Bg: colDefault, Bg: colDefault,
PreviewFg: colUndefined,
PreviewBg: colUndefined,
DarkBg: 236, DarkBg: 236,
Gutter: colUndefined,
Prompt: 110, Prompt: 110,
Match: 108, Match: 108,
Current: 254, Current: 254,
@@ -340,7 +403,10 @@ func init() {
Light256 = &ColorTheme{ Light256 = &ColorTheme{
Fg: colDefault, Fg: colDefault,
Bg: colDefault, Bg: colDefault,
PreviewFg: colUndefined,
PreviewBg: colUndefined,
DarkBg: 251, DarkBg: 251,
Gutter: colUndefined,
Prompt: 25, Prompt: 25,
Match: 66, Match: 66,
Current: 237, Current: 237,
@@ -371,7 +437,10 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
} }
theme.Fg = o(baseTheme.Fg, theme.Fg) theme.Fg = o(baseTheme.Fg, theme.Fg)
theme.Bg = o(baseTheme.Bg, theme.Bg) theme.Bg = o(baseTheme.Bg, theme.Bg)
theme.PreviewFg = o(theme.Fg, o(baseTheme.PreviewFg, theme.PreviewFg))
theme.PreviewBg = o(theme.Bg, o(baseTheme.PreviewBg, theme.PreviewBg))
theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg) theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg)
theme.Gutter = o(theme.DarkBg, o(baseTheme.Gutter, theme.Gutter))
theme.Prompt = o(baseTheme.Prompt, theme.Prompt) theme.Prompt = o(baseTheme.Prompt, theme.Prompt)
theme.Match = o(baseTheme.Match, theme.Match) theme.Match = o(baseTheme.Match, theme.Match)
theme.Current = o(baseTheme.Current, theme.Current) theme.Current = o(baseTheme.Current, theme.Current)
@@ -387,33 +456,44 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
} }
func initPalette(theme *ColorTheme) { func initPalette(theme *ColorTheme) {
ColDefault = ColorPair{colDefault, colDefault, 0} idx := 0
if theme != nil { pair := func(fg, bg Color) ColorPair {
ColNormal = ColorPair{theme.Fg, theme.Bg, 1} idx++
ColPrompt = ColorPair{theme.Prompt, theme.Bg, 2} return ColorPair{fg, bg, idx}
ColMatch = ColorPair{theme.Match, theme.Bg, 3} }
ColCurrent = ColorPair{theme.Current, theme.DarkBg, 4} if theme != nil {
ColCurrentMatch = ColorPair{theme.CurrentMatch, theme.DarkBg, 5} ColPrompt = pair(theme.Prompt, theme.Bg)
ColSpinner = ColorPair{theme.Spinner, theme.Bg, 6} ColNormal = pair(theme.Fg, theme.Bg)
ColInfo = ColorPair{theme.Info, theme.Bg, 7} ColMatch = pair(theme.Match, theme.Bg)
ColCursor = ColorPair{theme.Cursor, theme.DarkBg, 8} ColCursor = pair(theme.Cursor, theme.Gutter)
ColSelected = ColorPair{theme.Selected, theme.DarkBg, 9} ColSelected = pair(theme.Selected, theme.Gutter)
ColHeader = ColorPair{theme.Header, theme.Bg, 10} ColCurrent = pair(theme.Current, theme.DarkBg)
ColBorder = ColorPair{theme.Border, theme.Bg, 11} ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg)
} else { ColCurrentCursor = pair(theme.Cursor, theme.DarkBg)
ColNormal = ColorPair{colDefault, colDefault, 1} ColCurrentSelected = pair(theme.Selected, theme.DarkBg)
ColPrompt = ColorPair{colDefault, colDefault, 2} ColSpinner = pair(theme.Spinner, theme.Bg)
ColMatch = ColorPair{colDefault, colDefault, 3} ColInfo = pair(theme.Info, theme.Bg)
ColCurrent = ColorPair{colDefault, colDefault, 4} ColHeader = pair(theme.Header, theme.Bg)
ColCurrentMatch = ColorPair{colDefault, colDefault, 5} ColBorder = pair(theme.Border, theme.Bg)
ColSpinner = ColorPair{colDefault, colDefault, 6} ColPreview = pair(theme.PreviewFg, theme.PreviewBg)
ColInfo = ColorPair{colDefault, colDefault, 7} ColPreviewBorder = pair(theme.Border, theme.PreviewBg)
ColCursor = ColorPair{colDefault, colDefault, 8} } else {
ColSelected = ColorPair{colDefault, colDefault, 9} ColPrompt = pair(colDefault, colDefault)
ColHeader = ColorPair{colDefault, colDefault, 10} ColNormal = pair(colDefault, colDefault)
ColBorder = ColorPair{colDefault, colDefault, 11} ColMatch = pair(colDefault, colDefault)
ColCursor = pair(colDefault, colDefault)
ColSelected = pair(colDefault, colDefault)
ColCurrent = pair(colDefault, colDefault)
ColCurrentMatch = pair(colDefault, colDefault)
ColCurrentCursor = pair(colDefault, colDefault)
ColCurrentSelected = pair(colDefault, colDefault)
ColSpinner = pair(colDefault, colDefault)
ColInfo = pair(colDefault, colDefault)
ColHeader = pair(colDefault, colDefault)
ColBorder = pair(colDefault, colDefault)
ColPreview = pair(colDefault, colDefault)
ColPreviewBorder = pair(colDefault, colDefault)
} }
ColUser = ColorPair{colDefault, colDefault, 12}
} }
func attrFor(color ColorPair, attr Attr) Attr { func attrFor(color ColorPair, attr Attr) Attr {

View File

@@ -1,6 +1,7 @@
package util package util
import ( import (
"fmt"
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
"unsafe" "unsafe"
@@ -94,6 +95,11 @@ func (chars *Chars) Length() int {
return len(chars.slice) return len(chars.slice)
} }
// String returns the string representation of a Chars object.
func (chars *Chars) String() string {
return fmt.Sprintf("Chars{slice: []byte(%q), inBytes: %v, trimLengthKnown: %v, trimLength: %d, Index: %d}", chars.slice, chars.inBytes, chars.trimLengthKnown, chars.trimLength, chars.Index)
}
// TrimLength returns the length after trimming leading and trailing whitespaces // TrimLength returns the length after trimming leading and trailing whitespaces
func (chars *Chars) TrimLength() uint16 { func (chars *Chars) TrimLength() uint16 {
if chars.trimLengthKnown { if chars.trimLengthKnown {
@@ -136,6 +142,11 @@ func (chars *Chars) TrailingWhitespaces() int {
return whitespaces return whitespaces
} }
func (chars *Chars) TrimTrailingWhitespaces() {
whitespaces := chars.TrailingWhitespaces()
chars.slice = chars.slice[0 : len(chars.slice)-whitespaces]
}
func (chars *Chars) ToString() string { func (chars *Chars) ToString() string {
if runes := chars.optionalRunes(); runes != nil { if runes := chars.optionalRunes(); runes != nil {
return string(runes) return string(runes)
@@ -163,5 +174,13 @@ func (chars *Chars) CopyRunes(dest []rune) {
for idx, b := range chars.slice[:len(dest)] { for idx, b := range chars.slice[:len(dest)] {
dest[idx] = rune(b) dest[idx] = rune(b)
} }
return }
func (chars *Chars) Prepend(prefix string) {
if runes := chars.optionalRunes(); runes != nil {
runes = append([]rune(prefix), runes...)
chars.slice = *(*[]byte)(unsafe.Pointer(&runes))
} else {
chars.slice = append([]byte(prefix), chars.slice...)
}
} }

View File

@@ -17,12 +17,13 @@ func RuneWidth(r rune, prefixWidth int, tabstop int) int {
return tabstop - prefixWidth%tabstop return tabstop - prefixWidth%tabstop
} else if w, found := _runeWidths[r]; found { } else if w, found := _runeWidths[r]; found {
return w return w
} else { } else if r == '\n' || r == '\r' {
w := Max(runewidth.RuneWidth(r), 1) return 1
}
w := runewidth.RuneWidth(r)
_runeWidths[r] = w _runeWidths[r] = w
return w return w
} }
}
// Max returns the largest integer // Max returns the largest integer
func Max(first int, second int) int { func Max(first int, second int) int {
@@ -111,3 +112,13 @@ func DurWithin(
func IsTty() bool { func IsTty() bool {
return isatty.IsTerminal(os.Stdin.Fd()) return isatty.IsTerminal(os.Stdin.Fd())
} }
// Once returns a function that returns the specified boolean value only once
func Once(nextResponse bool) func() bool {
state := nextResponse
return func() bool {
prevState := state
state = false
return prevState
}
}

View File

@@ -20,3 +20,21 @@ func TestContrain(t *testing.T) {
t.Error("Expected", 3) t.Error("Expected", 3)
} }
} }
func TestOnce(t *testing.T) {
o := Once(false)
if o() {
t.Error("Expected: false")
}
if o() {
t.Error("Expected: false")
}
o = Once(true)
if !o() {
t.Error("Expected: true")
}
if o() {
t.Error("Expected: false")
}
}

View File

@@ -9,12 +9,26 @@ import (
) )
// ExecCommand executes the given command with $SHELL // ExecCommand executes the given command with $SHELL
func ExecCommand(command string) *exec.Cmd { func ExecCommand(command string, setpgid bool) *exec.Cmd {
shell := os.Getenv("SHELL") shell := os.Getenv("SHELL")
if len(shell) == 0 { if len(shell) == 0 {
shell = "sh" shell = "sh"
} }
return exec.Command(shell, "-c", command) return ExecCommandWith(shell, command, setpgid)
}
// ExecCommandWith executes the given command with the specified shell
func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd {
cmd := exec.Command(shell, "-c", command)
if setpgid {
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
}
return cmd
}
// KillCommand kills the process for the given command
func KillCommand(cmd *exec.Cmd) error {
return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
} }
// IsWindows returns true on Windows // IsWindows returns true on Windows
@@ -22,7 +36,7 @@ func IsWindows() bool {
return false return false
} }
// SetNonBlock executes syscall.SetNonblock on file descriptor // SetNonblock executes syscall.SetNonblock on file descriptor
func SetNonblock(file *os.File, nonblock bool) { func SetNonblock(file *os.File, nonblock bool) {
syscall.SetNonblock(int(file.Fd()), nonblock) syscall.SetNonblock(int(file.Fd()), nonblock)
} }

View File

@@ -3,20 +3,34 @@
package util package util
import ( import (
"fmt"
"os" "os"
"os/exec" "os/exec"
"syscall" "syscall"
"github.com/mattn/go-shellwords"
) )
// ExecCommand executes the given command with $SHELL // ExecCommand executes the given command with cmd
func ExecCommand(command string) *exec.Cmd { func ExecCommand(command string, setpgid bool) *exec.Cmd {
args, _ := shellwords.Parse(command) return ExecCommandWith("cmd", command, setpgid)
allArgs := make([]string, len(args)+1) }
allArgs[0] = "/c"
copy(allArgs[1:], args) // ExecCommandWith executes the given command with cmd. _shell parameter is
return exec.Command("cmd", allArgs...) // ignored on Windows.
// FIXME: setpgid is unused. We set it in the Unix implementation so that we
// can kill preview process with its child processes at once.
func ExecCommandWith(_shell string, command string, setpgid bool) *exec.Cmd {
cmd := exec.Command("cmd")
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: false,
CmdLine: fmt.Sprintf(` /v:on/s/c "%s"`, command),
CreationFlags: 0,
}
return cmd
}
// KillCommand kills the process for the given command
func KillCommand(cmd *exec.Cmd) error {
return cmd.Process.Kill()
} }
// IsWindows returns true on Windows // IsWindows returns true on Windows
@@ -24,7 +38,7 @@ func IsWindows() bool {
return true return true
} }
// SetNonBlock executes syscall.SetNonblock on file descriptor // SetNonblock executes syscall.SetNonblock on file descriptor
func SetNonblock(file *os.File, nonblock bool) { func SetNonblock(file *os.File, nonblock bool) {
syscall.SetNonblock(syscall.Handle(file.Fd()), nonblock) syscall.SetNonblock(syscall.Handle(file.Fd()), nonblock)
} }

View File

@@ -8,10 +8,13 @@ Execute (fzf#run with dir option):
let cwd = getcwd() let cwd = getcwd()
let result = fzf#run({ 'source': 'git ls-files', 'options': '--filter=vdr', 'dir': g:dir }) let result = fzf#run({ 'source': 'git ls-files', 'options': '--filter=vdr', 'dir': g:dir })
AssertEqual ['fzf.vader'], result AssertEqual ['fzf.vader'], result
AssertEqual 0, haslocaldir()
AssertEqual getcwd(), cwd AssertEqual getcwd(), cwd
execute 'lcd' fnameescape(cwd)
let result = sort(fzf#run({ 'source': 'git ls-files', 'options': '--filter e', 'dir': g:dir })) let result = sort(fzf#run({ 'source': 'git ls-files', 'options': '--filter e', 'dir': g:dir }))
AssertEqual ['fzf.vader', 'test_go.rb'], result AssertEqual ['fzf.vader', 'test_go.rb'], result
AssertEqual 1, haslocaldir()
AssertEqual getcwd(), cwd AssertEqual getcwd(), cwd
Execute (fzf#run with Funcref command): Execute (fzf#run with Funcref command):
@@ -56,11 +59,11 @@ Execute (Incomplete fzf#run with dir option and autochdir):
" No change in working directory even if &acd is set " No change in working directory even if &acd is set
AssertEqual cwd, getcwd() AssertEqual cwd, getcwd()
Execute (fzf#run with dir option and autochdir): Execute (FIXME: fzf#run with dir option and autochdir):
set acd set acd
let cwd = getcwd()
call fzf#run({'source': ['/foobar'], 'sink': 'e', 'dir': '/tmp', 'options': '-1'}) call fzf#run({'source': ['/foobar'], 'sink': 'e', 'dir': '/tmp', 'options': '-1'})
" Working directory changed due to &acd " Working directory changed due to &acd
AssertEqual '/foobar', expand('%')
AssertEqual '/', getcwd() AssertEqual '/', getcwd()
Execute (fzf#run with dir option and autochdir when final cwd is same as dir): Execute (fzf#run with dir option and autochdir when final cwd is same as dir):

348
test/test_go.rb Normal file → Executable file
View File

@@ -256,12 +256,12 @@ class TestGoFZF < TestBase
# Testing basic key bindings # Testing basic key bindings
tmux.send_keys '99', 'C-a', '1', 'C-f', '3', 'C-b', 'C-h', 'C-u', 'C-e', 'C-y', 'C-k', 'Tab', 'BTab' tmux.send_keys '99', 'C-a', '1', 'C-f', '3', 'C-b', 'C-h', 'C-u', 'C-e', 'C-y', 'C-k', 'Tab', 'BTab'
tmux.until { |lines| lines[-2] == ' 856/100000' } tmux.until do |lines|
lines = tmux.capture '> 3910' == lines[-4] &&
assert_equal '> 3910', lines[-4] ' 391' == lines[-3] &&
assert_equal ' 391', lines[-3] ' 856/100000' == lines[-2] &&
assert_equal ' 856/100000', lines[-2] '> 391' == lines[-1]
assert_equal '> 391', lines[-1] end
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal '3910', readonce.chomp assert_equal '3910', readonce.chomp
@@ -277,7 +277,7 @@ class TestGoFZF < TestBase
def test_fzf_default_command_failure def test_fzf_default_command_failure
tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', 'FZF_DEFAULT_COMMAND=false'), :Enter tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', 'FZF_DEFAULT_COMMAND=false'), :Enter
tmux.until { |lines| lines[-2].include?('ERROR') } tmux.until { |lines| lines[-2].include?('Command failed: false') }
tmux.send_keys :Enter tmux.send_keys :Enter
end end
@@ -371,6 +371,65 @@ class TestGoFZF < TestBase
assert_equal %w[3 2 5 6 8 7], readonce.split($INPUT_RECORD_SEPARATOR) assert_equal %w[3 2 5 6 8 7], readonce.split($INPUT_RECORD_SEPARATOR)
end end
def test_multi_max
tmux.send_keys "seq 1 10 | #{FZF} -m 3 --bind A:select-all,T:toggle-all --preview 'echo [{+}]/{}'", :Enter
tmux.until { |lines| lines.item_count == 10 }
tmux.send_keys '1'
tmux.until do |lines|
lines[1].include?('[1]/1') && lines[-2].include?('2/10')
end
tmux.send_keys 'A'
tmux.until do |lines|
lines[1].include?('[1 10]/1') && lines[-2].include?('2/10 (2/3)')
end
tmux.send_keys :BSpace
tmux.until { |lines| lines[-2].include?('10/10 (2/3)') }
tmux.send_keys 'T'
tmux.until do |lines|
lines[1].include?('[2 3 4]/1') && lines[-2].include?('10/10 (3/3)')
end
%w[T A].each do |key|
tmux.send_keys key
tmux.until do |lines|
lines[1].include?('[1 5 6]/1') && lines[-2].include?('10/10 (3/3)')
end
end
tmux.send_keys :BTab
tmux.until do |lines|
lines[1].include?('[5 6]/2') && lines[-2].include?('10/10 (2/3)')
end
[:BTab, :BTab, 'A'].each do |key|
tmux.send_keys key
tmux.until do |lines|
lines[1].include?('[5 6 2]/3') && lines[-2].include?('10/10 (3/3)')
end
end
tmux.send_keys '2'
tmux.until { |lines| lines[-2].include?('1/10 (3/3)') }
tmux.send_keys 'T'
tmux.until do |lines|
lines[1].include?('[5 6]/2') && lines[-2].include?('1/10 (2/3)')
end
tmux.send_keys :BSpace
tmux.until { |lines| lines[-2].include?('10/10 (2/3)') }
tmux.send_keys 'A'
tmux.until do |lines|
lines[1].include?('[5 6 1]/1') && lines[-2].include?('10/10 (3/3)')
end
end
def test_with_nth def test_with_nth
[true, false].each do |multi| [true, false].each do |multi|
tmux.send_keys "(echo ' 1st 2nd 3rd/'; tmux.send_keys "(echo ' 1st 2nd 3rd/';
@@ -769,6 +828,15 @@ class TestGoFZF < TestBase
assert_equal %w[print-my-query], readonce.split($INPUT_RECORD_SEPARATOR) assert_equal %w[print-my-query], readonce.split($INPUT_RECORD_SEPARATOR)
end end
def test_bind_replace_query
tmux.send_keys "seq 1 1000 | #{fzf '--print-query --bind=ctrl-j:replace-query'}", :Enter
tmux.send_keys '1'
tmux.until { |lines| lines[-2].end_with? '272/1000' }
tmux.send_keys 'C-k', 'C-j'
tmux.until { |lines| lines[-2].end_with? '29/1000' }
tmux.until { |lines| lines[-1].end_with? '> 10' }
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|
@@ -797,16 +865,22 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-2].include? '(100)' } tmux.until { |lines| lines[-2].include? '(100)' }
tmux.send_keys :Tab, :Tab tmux.send_keys :Tab, :Tab
tmux.until { |lines| lines[-2].include? '(98)' } tmux.until { |lines| lines[-2].include? '(98)' }
tmux.send_keys '100'
tmux.until { |lines| lines.match_count == 1 }
tmux.send_keys 'C-d'
tmux.until { |lines| lines[-2].include? '(97)' }
tmux.send_keys 'C-u'
tmux.until { |lines| lines.match_count == 100 }
tmux.send_keys 'C-d' tmux.send_keys 'C-d'
tmux.until { |lines| !lines[-2].include? '(' } tmux.until { |lines| !lines[-2].include? '(' }
tmux.send_keys :Tab, :Tab tmux.send_keys :BTab, :BTab
tmux.until { |lines| lines[-2].include? '(2)' } tmux.until { |lines| lines[-2].include? '(2)' }
tmux.send_keys 0 tmux.send_keys 0
tmux.until { |lines| lines[-2].include? '10/100' } tmux.until { |lines| lines[-2].include? '10/100' }
tmux.send_keys 'C-a' tmux.send_keys 'C-a'
tmux.until { |lines| lines[-2].include? '(12)' } tmux.until { |lines| lines[-2].include? '(12)' }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal %w[2 1 10 20 30 40 50 60 70 80 90 100], assert_equal %w[1 2 10 20 30 40 50 60 70 80 90 100],
readonce.split($INPUT_RECORD_SEPARATOR) readonce.split($INPUT_RECORD_SEPARATOR)
end end
@@ -864,7 +938,7 @@ 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}\\"]
wait = ->(exp) { tmux.until { |lines| lines[-2].include? exp } } wait = ->(exp) { tmux.until { |lines| lines[-2].include? exp } }
writelines tempname, %w[foo'bar foo"bar foo$bar] writelines tempname, %w[foo'bar foo"bar foo$bar]
tmux.send_keys "cat #{tempname} | #{fzf opts}; sync", :Enter tmux.send_keys "cat #{tempname} | #{fzf opts}; sync", :Enter
@@ -889,7 +963,7 @@ class TestGoFZF < TestBase
wait['/3'] wait['/3']
tmux.send_keys :Enter tmux.send_keys :Enter
readonce readonce
assert_equal %w[[foo'bar] [foo'bar] assert_equal %w[/foo'bar/ /foo'bar/
/foo"barfoo"bar/ /foo"barfoo"bar/ /foo"barfoo"bar/ /foo"barfoo"bar/
/foo$barfoo$barfoo$bar/], /foo$barfoo$barfoo$bar/],
File.readlines(output).map(&:chomp) File.readlines(output).map(&:chomp)
@@ -1051,6 +1125,21 @@ class TestGoFZF < TestBase
assert_equal '50', readonce.chomp assert_equal '50', readonce.chomp
end end
def test_header_lines_reverse_list
tmux.send_keys "seq 100 | #{fzf '--header-lines=10 -q 5 --layout=reverse-list'}", :Enter
2.times do
tmux.until do |lines|
lines[0] == '> 50' &&
lines[-4] == ' 2' &&
lines[-3] == ' 1' &&
lines[-2].include?('/90')
end
tmux.send_keys :Up
end
tmux.send_keys :Enter
assert_equal '50', readonce.chomp
end
def test_header_lines_overflow def test_header_lines_overflow
tmux.send_keys "seq 100 | #{fzf '--header-lines=200'}", :Enter tmux.send_keys "seq 100 | #{fzf '--header-lines=200'}", :Enter
tmux.until do |lines| tmux.until do |lines|
@@ -1078,7 +1167,8 @@ class TestGoFZF < TestBase
header = File.readlines(FILE).take(5).map(&:strip) header = File.readlines(FILE).take(5).map(&:strip)
tmux.until do |lines| tmux.until do |lines|
lines[-2].include?('100/100') && lines[-2].include?('100/100') &&
lines[-7..-3].map(&:strip) == header lines[-7..-3].map(&:strip) == header &&
lines[-8] == '> 1'
end end
end end
@@ -1087,7 +1177,18 @@ class TestGoFZF < TestBase
header = File.readlines(FILE).take(5).map(&:strip) header = File.readlines(FILE).take(5).map(&:strip)
tmux.until do |lines| tmux.until do |lines|
lines[1].include?('100/100') && lines[1].include?('100/100') &&
lines[2..6].map(&:strip) == header lines[2..6].map(&:strip) == header &&
lines[7] == '> 1'
end
end
def test_header_reverse_list
tmux.send_keys "seq 100 | #{fzf "--header=\\\"\\$(head -5 #{FILE})\\\" --layout=reverse-list"}", :Enter
header = File.readlines(FILE).take(5).map(&:strip)
tmux.until do |lines|
lines[-2].include?('100/100') &&
lines[-7..-3].map(&:strip) == header &&
lines[0] == '> 1'
end end
end end
@@ -1111,6 +1212,16 @@ class TestGoFZF < TestBase
end end
end end
def test_header_and_header_lines_reverse_list
tmux.send_keys "seq 100 | #{fzf "--layout=reverse-list --header-lines 10 --header \\\"\\$(head -5 #{FILE})\\\""}", :Enter
header = File.readlines(FILE).take(5).map(&:strip)
tmux.until do |lines|
lines[-2].include?('90/90') &&
lines[-7...-2].map(&:strip) == header &&
lines[-17...-7].map(&:strip) == (1..10).map(&:to_s).reverse
end
end
def test_cancel 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') }
@@ -1136,6 +1247,12 @@ class TestGoFZF < TestBase
tmux.send_keys :Enter tmux.send_keys :Enter
end end
def test_margin_reverse_list
tmux.send_keys "yes | head -1000 | #{fzf '--margin 5,3 --layout=reverse-list'}", :Enter
tmux.until { |lines| lines[4] == '' && lines[5] == ' > y' }
tmux.send_keys :Enter
end
def test_tabstop def test_tabstop
writelines tempname, ["f\too\tba\tr\tbaz\tbarfooq\tux"] writelines tempname, ["f\too\tba\tr\tbaz\tbarfooq\tux"]
{ {
@@ -1319,12 +1436,12 @@ class TestGoFZF < TestBase
end end
def test_preview_hidden def test_preview_hidden
tmux.send_keys %(seq 1000 | #{FZF} --preview 'echo {{}-{}}' --preview-window down:1:hidden --bind ?:toggle-preview), :Enter tmux.send_keys %(seq 1000 | #{FZF} --preview 'echo {{}-{}-\\$FZF_PREVIEW_LINES-\\$FZF_PREVIEW_COLUMNS}' --preview-window down:1:hidden --bind ?:toggle-preview), :Enter
tmux.until { |lines| lines[-1] == '>' } tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys '?' tmux.send_keys '?'
tmux.until { |lines| lines[-2].include?(' {1-1}') } tmux.until { |lines| lines[-2] =~ / {1-1-1-[0-9]+}/ }
tmux.send_keys '555' tmux.send_keys '555'
tmux.until { |lines| lines[-2].include?(' {555-555}') } tmux.until { |lines| lines[-2] =~ / {555-555-1-[0-9]+}/ }
tmux.send_keys '?' tmux.send_keys '?'
tmux.until { |lines| lines[-1] == '> 555' } tmux.until { |lines| lines[-1] == '> 555' }
end end
@@ -1346,6 +1463,50 @@ class TestGoFZF < TestBase
tmux.until { |_| %w[1 2 3] == File.readlines(tempname).map(&:chomp) } tmux.until { |_| %w[1 2 3] == File.readlines(tempname).map(&:chomp) }
end end
def test_preview_flags
tmux.send_keys %(seq 10 | sed 's/^/:: /; s/$/ /' |
#{FZF} --multi --preview 'echo {{2}/{s2}/{+2}/{+s2}/{q}/{n}/{+n}}'), :Enter
tmux.until { |lines| lines[1].include?('{1/1 /1/1 //0/0}') }
tmux.send_keys '123'
tmux.until { |lines| lines[1].include?('{////123//}') }
tmux.send_keys 'C-u', '1'
tmux.until { |lines| lines.match_count == 2 }
tmux.until { |lines| lines[1].include?('{1/1 /1/1 /1/0/0}') }
tmux.send_keys :BTab
tmux.until { |lines| lines[1].include?('{10/10 /1/1 /1/9/0}') }
tmux.send_keys :BTab
tmux.until { |lines| lines[1].include?('{10/10 /1 10/1 10 /1/9/0 9}') }
tmux.send_keys '2'
tmux.until { |lines| lines[1].include?('{//1 10/1 10 /12//0 9}') }
tmux.send_keys '3'
tmux.until { |lines| lines[1].include?('{//1 10/1 10 /123//0 9}') }
end
def test_preview_file
tmux.send_keys %[(echo foo bar; echo bar foo) | #{FZF} --multi --preview 'cat {+f} {+f2} {+nf} {+fn}' --print0], :Enter
tmux.until { |lines| lines[1].include?('foo barbar00') }
tmux.send_keys :BTab
tmux.until { |lines| lines[1].include?('foo barbar00') }
tmux.send_keys :BTab
tmux.until { |lines| lines[1].include?('foo barbar foobarfoo0101') }
end
def test_preview_q_no_match
tmux.send_keys %(: | #{FZF} --preview 'echo foo {q}'), :Enter
tmux.until { |lines| lines.match_count == 0 }
tmux.until { |lines| !lines[1].include?('foo') }
tmux.send_keys 'bar'
tmux.until { |lines| lines[1].include?('foo bar') }
tmux.send_keys 'C-u'
tmux.until { |lines| !lines[1].include?('foo') }
end
def test_preview_q_no_match_with_initial_query
tmux.send_keys %(: | #{FZF} --preview 'echo foo {q}{q}' --query foo), :Enter
tmux.until { |lines| lines.match_count == 0 }
tmux.until { |lines| lines[1].include?('foofoo') }
end
def test_no_clear def test_no_clear
tmux.send_keys "seq 10 | fzf --no-clear --inline-info --height 5 > #{tempname}", :Enter tmux.send_keys "seq 10 | fzf --no-clear --inline-info --height 5 > #{tempname}", :Enter
prompt = '> < 10/10' prompt = '> < 10/10'
@@ -1355,6 +1516,11 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-1] == prompt } tmux.until { |lines| lines[-1] == prompt }
end end
def test_info_hidden
tmux.send_keys 'seq 10 | fzf --info=hidden', :Enter
tmux.until { |lines| lines[-2] == '> 1' }
end
def test_change_top def test_change_top
tmux.send_keys %(seq 1000 | #{FZF} --bind change:top), :Enter tmux.send_keys %(seq 1000 | #{FZF} --bind change:top), :Enter
tmux.until { |lines| lines.match_count == 1000 } tmux.until { |lines| lines.match_count == 1000 }
@@ -1369,6 +1535,42 @@ class TestGoFZF < TestBase
tmux.send_keys :Enter tmux.send_keys :Enter
end end
def test_accept_non_empty
tmux.send_keys %(seq 1000 | #{fzf '--print-query --bind enter:accept-non-empty'}), :Enter
tmux.until { |lines| lines.match_count == 1000 }
tmux.send_keys 'foo'
tmux.until { |lines| lines[-2].include? '0/1000' }
# fzf doesn't exit since there's no selection
tmux.send_keys :Enter
tmux.until { |lines| lines[-2].include? '0/1000' }
tmux.send_keys 'C-u'
tmux.until { |lines| lines[-2].include? '1000/1000' }
tmux.send_keys '999'
tmux.until { |lines| lines[-2].include? '1/1000' }
tmux.send_keys :Enter
assert_equal %w[999 999], readonce.split($INPUT_RECORD_SEPARATOR)
end
def test_accept_non_empty_with_multi_selection
tmux.send_keys %(seq 1000 | #{fzf '-m --print-query --bind enter:accept-non-empty'}), :Enter
tmux.until { |lines| lines.match_count == 1000 }
tmux.send_keys :Tab
tmux.until { |lines| lines[-2].include? '1000/1000 (1)' }
tmux.send_keys 'foo'
tmux.until { |lines| lines[-2].include? '0/1000' }
# fzf will exit in this case even though there's no match for the current query
tmux.send_keys :Enter
assert_equal %w[foo 1], readonce.split($INPUT_RECORD_SEPARATOR)
end
def test_accept_non_empty_with_empty_list
tmux.send_keys %(: | #{fzf '-q foo --print-query --bind enter:accept-non-empty'}), :Enter
tmux.until { |lines| lines[-2].strip == '0/0' }
tmux.send_keys :Enter
# fzf will exit anyway since input list is empty
assert_equal %w[foo], readonce.split($INPUT_RECORD_SEPARATOR)
end
def test_preview_update_on_select def test_preview_update_on_select
tmux.send_keys(%(seq 10 | fzf -m --preview 'echo {+}' --bind a:toggle-all), tmux.send_keys(%(seq 10 | fzf -m --preview 'echo {+}' --bind a:toggle-all),
:Enter) :Enter)
@@ -1396,6 +1598,85 @@ class TestGoFZF < TestBase
assert_equal ['foo bar'], `#{FZF} -f'^foo\\ bar$' < #{tempname}`.lines.map(&:chomp) assert_equal ['foo bar'], `#{FZF} -f'^foo\\ bar$' < #{tempname}`.lines.map(&:chomp)
assert_equal input.lines.count - 1, `#{FZF} -f'!^foo\\ bar$' < #{tempname}`.lines.count assert_equal input.lines.count - 1, `#{FZF} -f'!^foo\\ bar$' < #{tempname}`.lines.count
end end
def test_inverse_only_search_should_not_sort_the_result
# Filter
assert_equal(%w[aaaaa b ccc],
`printf '%s\n' aaaaa b ccc BAD | #{FZF} -f '!bad'`.lines.map(&:chomp))
# Interactive
tmux.send_keys(%[printf '%s\n' aaaaa b ccc BAD | #{FZF} -q '!bad'], :Enter)
tmux.until { |lines| lines.item_count == 4 && lines.match_count == 3 }
tmux.until { |lines| lines[-3] == '> aaaaa' }
tmux.until { |lines| lines[-4] == ' b' }
tmux.until { |lines| lines[-5] == ' ccc' }
end
def test_preview_correct_tab_width_after_ansi_reset_code
writelines tempname, ["\x1b[31m+\x1b[m\t\x1b[32mgreen"]
tmux.send_keys "#{FZF} --preview 'cat #{tempname}'", :Enter
tmux.until { |lines| lines[1].include?('+ green') }
end
def test_phony
tmux.send_keys %(seq 1000 | #{FZF} --query 333 --phony --preview 'echo {} {q}'), :Enter
tmux.until { |lines| lines.match_count == 1000 }
tmux.until { |lines| lines[1].include?('1 333') }
tmux.send_keys 'foo'
tmux.until { |lines| lines.match_count == 1000 }
tmux.until { |lines| lines[1].include?('1 333foo') }
end
def test_reload
tmux.send_keys %(seq 1000 | #{FZF} --bind 'change:reload(seq {q}),a:reload(seq 100),b:reload:seq 200' --header-lines 2 --multi 2), :Enter
tmux.until { |lines| lines.match_count == 998 }
tmux.send_keys 'a'
tmux.until { |lines| lines.item_count == 98 && lines.match_count == 98 }
tmux.send_keys 'b'
tmux.until { |lines| lines.item_count == 198 && lines.match_count == 198 }
tmux.send_keys :Tab
tmux.until { |lines| lines[-2].include?('(1/2)') }
tmux.send_keys '555'
tmux.until { |lines| lines.item_count == 553 && lines.match_count == 1 }
tmux.until { |lines| !lines[-2].include?('(1/2)') }
end
def test_reload_even_when_theres_no_match
tmux.send_keys %(: | #{FZF} --bind 'space:reload(seq 10)'), :Enter
tmux.until { |lines| lines.item_count.zero? }
tmux.send_keys :Space
tmux.until { |lines| lines.item_count == 10 }
end
def test_clear_list_when_header_lines_changed_due_to_reload
tmux.send_keys %(seq 10 | #{FZF} --header 0 --header-lines 3 --bind 'space:reload(seq 1)'), :Enter
tmux.until { |lines| lines.any? { |line| line.include?('9') } }
tmux.send_keys :Space
tmux.until { |lines| lines.none? { |line| line.include?('9') } }
end
def test_clear_query
tmux.send_keys %(: | #{FZF} --query foo --bind space:clear-query), :Enter
tmux.until { |lines| lines.item_count.zero? }
tmux.until { |lines| lines.last.include?('> foo') }
tmux.send_keys 'C-a', 'bar'
tmux.until { |lines| lines.last.include?('> barfoo') }
tmux.send_keys :Space
tmux.until { |lines| lines.last == '>' }
end
def test_clear_selection
tmux.send_keys %(seq 100 | #{FZF} --multi --bind space:clear-selection), :Enter
tmux.until { |lines| lines.match_count == 100 }
tmux.send_keys :Tab
tmux.until { |lines| lines[-2].include?('(1)') }
tmux.send_keys 'foo'
tmux.until { |lines| lines.match_count.zero? }
tmux.until { |lines| lines[-2].include?('(1)') }
tmux.send_keys :Space
tmux.until { |lines| lines.match_count.zero? }
tmux.until { |lines| !lines[-2].include?('(1)') }
end
end end
module TestShell module TestShell
@@ -1469,7 +1750,7 @@ module TestShell
lines = retries do lines = retries do
tmux.prepare tmux.prepare
tmux.send_keys :Escape, :c tmux.send_keys :Escape, :c
tmux.until { |lines| lines.item_count.positive? } tmux.until { |lines| lines.match_count.positive? }
end end
expected = lines.reverse.select { |l| l.start_with? '>' }.first[2..-1] expected = lines.reverse.select { |l| l.start_with? '>' }.first[2..-1]
tmux.send_keys :Enter tmux.send_keys :Enter
@@ -1506,7 +1787,7 @@ module TestShell
retries do retries do
tmux.prepare tmux.prepare
tmux.send_keys 'C-r' tmux.send_keys 'C-r'
tmux.until { |lines| lines.item_count.positive? } tmux.until { |lines| lines.match_count.positive? }
end end
tmux.send_keys 'C-r' tmux.send_keys 'C-r'
tmux.send_keys '3d' tmux.send_keys '3d'
@@ -1538,7 +1819,7 @@ module CompletionTest
end end
tmux.prepare tmux.prepare
tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab
tmux.until { |lines| lines.item_count.positive? } tmux.until { |lines| lines.match_count.positive? }
tmux.send_keys ' !d' tmux.send_keys ' !d'
tmux.until { |lines| lines.match_count == 2 } tmux.until { |lines| lines.match_count == 2 }
tmux.send_keys :Tab, :Tab tmux.send_keys :Tab, :Tab
@@ -1552,7 +1833,7 @@ module CompletionTest
# ~USERNAME**<TAB> # ~USERNAME**<TAB>
tmux.send_keys 'C-u' tmux.send_keys 'C-u'
tmux.send_keys "cat ~#{ENV['USER']}**", :Tab tmux.send_keys "cat ~#{ENV['USER']}**", :Tab
tmux.until { |lines| lines.item_count.positive? } tmux.until { |lines| lines.match_count.positive? }
tmux.send_keys "'.fzf-home" tmux.send_keys "'.fzf-home"
tmux.until { |lines| lines.select { |l| l.include? '.fzf-home' }.count > 1 } tmux.until { |lines| lines.select { |l| l.include? '.fzf-home' }.count > 1 }
tmux.send_keys :Enter tmux.send_keys :Enter
@@ -1570,7 +1851,7 @@ module CompletionTest
# /tmp/fzf\ test**<TAB> # /tmp/fzf\ test**<TAB>
tmux.send_keys 'C-u' tmux.send_keys 'C-u'
tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab
tmux.until { |lines| lines.item_count.positive? } tmux.until { |lines| lines.match_count.positive? }
tmux.send_keys 'foobar$' tmux.send_keys 'foobar$'
tmux.until { |lines| lines.match_count == 1 } tmux.until { |lines| lines.match_count == 1 }
tmux.send_keys :Enter tmux.send_keys :Enter
@@ -1590,7 +1871,7 @@ module CompletionTest
def test_file_completion_root def test_file_completion_root
tmux.send_keys 'ls /**', :Tab tmux.send_keys 'ls /**', :Tab
tmux.until { |lines| lines.item_count.positive? } tmux.until { |lines| lines.match_count.positive? }
tmux.send_keys :Enter tmux.send_keys :Enter
end end
@@ -1601,7 +1882,7 @@ module CompletionTest
FileUtils.touch '/tmp/fzf-test/d55/xxx' FileUtils.touch '/tmp/fzf-test/d55/xxx'
tmux.prepare tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test/**', :Tab tmux.send_keys 'cd /tmp/fzf-test/**', :Tab
tmux.until { |lines| lines.item_count.positive? } tmux.until { |lines| lines.match_count.positive? }
tmux.send_keys :Tab, :Tab # Tab does not work here tmux.send_keys :Tab, :Tab # Tab does not work here
tmux.send_keys 55 tmux.send_keys 55
tmux.until { |lines| lines.match_count == 1 } tmux.until { |lines| lines.match_count == 1 }
@@ -1630,7 +1911,7 @@ module CompletionTest
tmux.prepare tmux.prepare
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
tmux.send_keys 'kill ', :Tab tmux.send_keys 'kill ', :Tab
tmux.until { |lines| lines.item_count.positive? } tmux.until { |lines| lines.match_count.positive? }
tmux.send_keys 'sleep12345' tmux.send_keys 'sleep12345'
tmux.until { |lines| lines.any_include? 'sleep 12345' } tmux.until { |lines| lines.any_include? 'sleep 12345' }
tmux.send_keys :Enter tmux.send_keys :Enter
@@ -1649,7 +1930,7 @@ module CompletionTest
tmux.send_keys '_fzf_compgen_path() { echo "\$1"; seq 10; }', :Enter tmux.send_keys '_fzf_compgen_path() { echo "\$1"; seq 10; }', :Enter
tmux.prepare tmux.prepare
tmux.send_keys 'ls /tmp/**', :Tab tmux.send_keys 'ls /tmp/**', :Tab
tmux.until { |lines| lines.item_count == 11 } tmux.until { |lines| lines.match_count == 11 }
tmux.send_keys :Tab, :Tab, :Tab tmux.send_keys :Tab, :Tab, :Tab
tmux.until { |lines| lines.select_count == 3 } tmux.until { |lines| lines.select_count == 3 }
tmux.send_keys :Enter tmux.send_keys :Enter
@@ -1716,6 +1997,23 @@ class TestBash < TestBase
super super
@tmux = Tmux.new :bash @tmux = Tmux.new :bash
end end
def test_dynamic_completion_loader
tmux.paste 'touch /tmp/foo; _fzf_completion_loader=1'
tmux.paste '_completion_loader() { complete -o default fake; }'
tmux.paste 'complete -F _fzf_path_completion -o default -o bashdefault fake'
tmux.send_keys 'fake /tmp/foo**', :Tab
tmux.until { |lines| lines.match_count.positive? }
tmux.send_keys 'C-c'
tmux.prepare
tmux.send_keys 'fake /tmp/foo'
tmux.send_keys :Tab , 'C-u'
tmux.prepare
tmux.send_keys 'fake /tmp/foo**', :Tab
tmux.until { |lines| lines.match_count.positive? }
end
end end
class TestZsh < TestBase class TestZsh < TestBase

View File

@@ -1,12 +1,45 @@
#!/usr/bin/env bash #!/usr/bin/env bash
confirm() { xdg=0
while [ 1 ]; do prefix='~/.fzf'
read -p "$1" -n 1 -r prefix_expand=~/.fzf
echo fish_dir=${XDG_CONFIG_HOME:-$HOME/.config}/fish
if [[ "$REPLY" =~ ^[Yy] ]]; then
help() {
cat << EOF
usage: $0 [OPTIONS]
--help Show this message
--xdg Remove files generated under \$XDG_CONFIG_HOME/fzf
EOF
}
for opt in "$@"; do
case $opt in
--help)
help
exit 0
;;
--xdg)
xdg=1
prefix='"${XDG_CONFIG_HOME:-$HOME/.config}"/fzf/fzf'
prefix_expand=${XDG_CONFIG_HOME:-$HOME/.config}/fzf/fzf
;;
*)
echo "unknown option: $opt"
help
exit 1
;;
esac
done
ask() {
while true; do
read -p "$1 ([y]/n) " -r
REPLY=${REPLY:-"y"}
if [[ $REPLY =~ ^[Yy]$ ]]; then
return 0 return 0
elif [[ "$REPLY" =~ ^[Nn] ]]; then elif [[ $REPLY =~ ^[Nn]$ ]]; then
return 1 return 1
fi fi
done done
@@ -40,7 +73,7 @@ remove_line() {
content=$(sed 's/^[0-9]*://' <<< "$line") content=$(sed 's/^[0-9]*://' <<< "$line")
match=1 match=1
echo " - Line #$line_no: $content" echo " - Line #$line_no: $content"
[ "$content" = "$1" ] || confirm " - Remove (y/n) ? " [ "$content" = "$1" ] || ask " - Remove?"
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
awk -v n=$line_no 'NR == n {next} {print}' "$src" > "$src.bak" && awk -v n=$line_no 'NR == n {next} {print}' "$src" > "$src.bak" &&
mv "$src.bak" "$src" || break mv "$src.bak" "$src" || break
@@ -55,25 +88,30 @@ remove_line() {
} }
for shell in bash zsh; do for shell in bash zsh; do
remove ~/.fzf.${shell} shell_config=${prefix_expand}.${shell}
remove "${shell_config}"
remove_line ~/.${shell}rc \ remove_line ~/.${shell}rc \
"[ -f ~/.fzf.${shell} ] && source ~/.fzf.${shell}" \ "[ -f ${prefix}.${shell} ] && source ${prefix}.${shell}" \
"source ~/.fzf.${shell}" "source ${prefix}.${shell}"
done done
bind_file=~/.config/fish/functions/fish_user_key_bindings.fish bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
if [ -f "$bind_file" ]; then if [ -f "$bind_file" ]; then
remove_line "$bind_file" "fzf_key_bindings" remove_line "$bind_file" "fzf_key_bindings"
fi fi
if [ -d ~/.config/fish/functions ]; then if [ -d "${fish_dir}/functions" ]; then
remove ~/.config/fish/functions/fzf.fish remove "${fish_dir}/functions/fzf.fish"
remove ~/.config/fish/functions/fzf_key_bindings.fish remove "${fish_dir}/functions/fzf_key_bindings.fish"
if [ "$(ls -A ~/.config/fish/functions)" ]; then if [ "$(ls -A "${fish_dir}/functions")" ]; then
echo "Can't delete non-empty directory: \"~/.config/fish/functions\"" echo "Can't delete non-empty directory: \"${fish_dir}/functions\""
else else
rmdir ~/.config/fish/functions rmdir "${fish_dir}/functions"
fi fi
fi fi
config_dir=$(dirname "$prefix_expand")
if [[ "$xdg" = 1 ]] && [[ "$config_dir" = */fzf ]] && [[ -d "$config_dir" ]]; then
rmdir "$config_dir"
fi