m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-15 06:43:47 -05:00

Compare commits

...

110 Commits

Author SHA1 Message Date
Junegunn Choi
2d61691bb2 0.16.7 2017-04-30 11:54:40 +09:00
Junegunn Choi
eba9e04e2e Export FZF_PREVIEW_HEIGHT instead of FZF_HEIGHT
https://github.com/junegunn/fzf.vim/issues/361
2017-04-30 11:36:23 +09:00
Junegunn Choi
33f32de690 Merge branch 'master' into devel 2017-04-30 11:23:42 +09:00
Junegunn Choi
93b8f61551 [vim] Export $FZF_HEIGHT for previewer scripts
Preview script cannot properly determine the height of fzf finder if
`--height` option is used.

https://github.com/junegunn/fzf.vim/issues/361
2017-04-30 11:18:56 +09:00
Junegunn Choi
7f17a9d1b4 Update mattn/go-shellwords 2017-04-30 00:47:44 +09:00
Junegunn Choi
d34e4cf698 Support CTRL-Z (SIGSTOP) 2017-04-28 22:58:08 +09:00
Junegunn Choi
6b592137b9 Add support for ctrl-alt-[a-z] key chords
Close #906
2017-04-28 02:36:36 +09:00
Junegunn Choi
d5e72bf55d Update README-VIM: options as list (#896) 2017-04-28 02:09:55 +09:00
Junegunn Choi
5677e5e133 [fish] Fix ~/.config/fish/functions/fish_user_key_bindings.fish
Install script will create the file with the proper function body only
if the file doesn't exist. If it already exists, it will try to append
`fzf_key_bindings` as before.

Close #851
2017-04-28 01:57:38 +09:00
Jan Edmund Lazo
7a11a06cbd [vim] Use backslash for Windows filepaths (#896)
- Fix shellescaping issues for filepaths
    - Supports both forward slashes or backslashes
    - Paths with spaces
- Use jobstart for neovim in s:execute (Windows)
    - https://github.com/neovim/neovim/pull/6497
- Make 2 s:fzf_shellescape functions
    - (Windows) Substitute \" with \\" to escape the last backslash
    - (Default) Regular shellescape
- Support list 'options'
- Add "@echo off" to the batchfile used to execute fzf
2017-04-22 11:30:51 +09:00
Junegunn Choi
a5862d4b9c [bash-completion] Use -o dirnames instead of -o plusdirs
Close #903
Related #135
2017-04-11 22:21:16 +09:00
Junegunn Choi
a50909e806 Correction: fzf no longer depends on ncurses 2017-04-06 02:08:49 +09:00
Kouki Higashikawa
7c8f7d3f20 [fzf-tmux] Close with exit code 130 when tmux pane is killed
Fix #796
2017-04-03 11:49:54 +09:00
Junegunn Choi
9078197446 Add --version to --help output and man page
Close #888
Close #894
2017-04-02 11:30:22 +09:00
Junegunn Choi
0fe07cf9fe Update README.md
Add PayPal donation button
2017-04-02 10:47:06 +09:00
Junegunn Choi
2216169ca1 Update doc/fzf.txt accordingly 2017-04-01 12:19:39 +09:00
Junegunn Choi
50e989ca85 Update example in README-VIM 2017-04-01 12:06:25 +09:00
Junegunn Choi
fa1fc3d855 Add vim doc
Close #893
2017-04-01 12:00:30 +09:00
五所和哉
bbe696e925 [fzf-tmux] Fix issue with zoomed pane on fish (#891) 2017-04-01 11:09:46 +09:00
Miodrag Milić
5d12f523a3 Add chocolatey upgrade instruction to Readme (#890) 2017-03-30 01:59:41 +09:00
Daniel Hahler
d295d20dc4 fzf#run: improve "is already running" message (#885)
This displays the buffer(s) in this case, which is useful when FZF got
stuck, and you have to manually remove the buffer.
2017-03-27 13:41:39 +09:00
Sam Van Den Berge
2ba10071c9 Add support for IPv6 addresses in ssh completion (#877)
Signed-off-by: Sam Van Den Berge <sam@drgt.net>
2017-03-21 01:06:13 +09:00
Christian Sturm
505dc0491b Make install script to work with non GNU tar (#871) 2017-03-10 23:22:37 +09:00
Junegunn Choi
54a4ab0f26 Add Chocolatey instruction
Thanks to @majkinetor. Close #869.
2017-03-07 23:03:14 +09:00
Junegunn Choi
e03e91477b 0.16.6 2017-03-05 03:05:06 +09:00
Junegunn Choi
88ac397158 Add test case for --no-clear 2017-03-04 14:26:47 +09:00
Junegunn Choi
6fd4be580b Use alternate screen only when the value of height is 100%
Do not automatically decide to use alternate screen when the value of
height exceeds the height of the terminal.

    # Use alternate screen
    fzf
    fzf --height 100%
    fzf --no-height

    # Still use current screen
    fzf --height 10000
2017-03-04 14:09:36 +09:00
Junegunn Choi
53348feb89 Add --no-clear option 2017-03-04 11:29:31 +09:00
Junegunn Choi
337cdbb37c [zsh] Use setopt noposixbuiltins instead of emulate -L zsh
Close #858
3a6af27586 (commitcomment-21135641)
2017-03-03 19:09:29 +09:00
Junegunn Choi
05fdf91fc5 Revert "[zsh] emulate -L zsh to avoid issues with incompatible options"
This reverts commit 3a6af27586.
2017-03-03 18:57:22 +09:00
Junegunn Choi
c387689d1c [shell] Enable sorting by default in CTRL-R
CTRL-R binding used to start with --no-sort to list the matched commands
in chronological order. However, it has been a constant source of
confusion. Let's enable it by default from now on. The sorted result
shouldn't be too confusing as we use --tiebreak=index.
2017-03-03 12:20:01 +09:00
Junegunn Choi
cb9238dc4e Display -S if sort is disabled and toggle-sort is used
This is to address a common confusion that one does not realize that
sorting is intentionally turned off by default and can be enabled by
a bind key.
2017-03-03 02:26:30 +09:00
Junegunn Choi
a484811f78 [vim] Capitalize exception messages 2017-03-02 14:17:59 +09:00
Junegunn Choi
111d1934c4 [vim] Throw error if g:fzf_layout is incorrectly used
https://github.com/junegunn/fzf.vim/issues/327
https://github.com/junegunn/fzf.vim/issues/317
2017-03-02 14:14:57 +09:00
Junegunn Choi
972fb1a29d Suppress ANSI colors in preview window if --no-color is set 2017-03-02 12:49:51 +09:00
Junegunn Choi
3a6af27586 [zsh] emulate -L zsh to avoid issues with incompatible options
Close #858
2017-03-01 16:07:04 +09:00
Junegunn Choi
c89ac341e4 Clear background even if background color is not set
This is needed when fzf is started from inside a program (e.g. Vim)
and it uses a different background color than the terminal.

- https://github.com/junegunn/fzf.vim/issues/325
- https://github.com/junegunn/fzf.vim/issues/300
2017-03-01 16:00:08 +09:00
Junegunn Choi
cd59e5d07b [neovim] Set 'dir' to the current direcotry
Close https://github.com/junegunn/fzf.vim/issues/308
2017-02-25 23:52:56 +09:00
Junegunn Choi
0b940e4b2b Redraw item if query string has changed 2017-02-24 02:30:11 +09:00
Junegunn Choi
b29375c844 [vim] Minor refactoring 2017-02-19 20:53:12 +09:00
Junegunn Choi
76d3f6d248 [vim] Escape ! when using :! to execute command
- call fzf#run({'source': "echo '!'"})
- call fzf#run({'source': "echo '!'", 'down': '40%'})

Close https://github.com/junegunn/fzf.vim/issues/315
2017-02-19 20:47:44 +09:00
Junegunn Choi
e87a85a179 0.16.5 2017-02-19 01:40:25 +09:00
Junegunn Choi
11407bf656 Exclude sysfs in find commands 2017-02-19 01:33:13 +09:00
Junegunn Choi
c82fb3c9b9 Add toggle-preview-wrap action 2017-02-18 23:49:00 +09:00
Junegunn Choi
309e1d8619 Properly truncate long query string 2017-02-18 23:17:29 +09:00
Junegunn Choi
c2db67c1c0 [vim] Prepend @echo off to $FZF_DEFAULT_COMMAND on Windows (#847) 2017-02-18 21:58:03 +09:00
Junegunn Choi
9526594905 [vim] Fix FZF_DEFAULT_COMMAND on Windows
Close #847. Patch submitted by @wontoncc.
2017-02-18 18:17:37 +09:00
Junegunn Choi
3d74d277aa Use cut instead of sed in the default command 2017-02-17 13:07:45 +09:00
Junegunn Choi
fc274c2ba4 [vim] Do not escape % when using system() instead of !
Close https://github.com/junegunn/fzf.vim/issues/309
2017-02-17 10:20:39 +09:00
Pierre Neidhardt
ce43ea9f42 [shell] Replace sed with -mindepth 1 and cut (#844) 2017-02-16 17:18:01 +09:00
Junegunn Choi
21da02fac2 Fix indentation 2017-02-14 22:30:09 +09:00
Junegunn Choi
19569bd5c5 Move cursor to the top-left when returning to alternate screen
Fix broken preview border. Reported by Thomas Sattler.

    fzf --bind 'enter:execute(date)' --preview=date --reverse
2017-02-14 22:28:04 +09:00
Daniel Gray
afa25d8c57 [zsh] Do not cd when cancelling alt+c keybind (#840) 2017-02-09 14:05:02 +09:00
Junegunn Choi
1ba7acf4bd [fzf-tmux] Fix race condition when using -l/-u on zoomed panes
Using a dummy command that works as the barrier.
2017-02-08 14:55:51 +09:00
Prabir Shrestha
a847fe8754 Use "type" instead of "cat" on windows (#836) 2017-02-07 14:42:08 +09:00
Junegunn Choi
5bb18b6441 Remove Dockerfiles and clean up Makefile
Due to the recent removal of ncurses dependency, we can cross-compile
binaries for different platforms without virtual machines.
2017-02-06 21:15:29 +09:00
Junegunn Choi
876c233a26 Remove Ruby version
Related #832
2017-02-06 21:06:12 +09:00
Junegunn Choi
ee5aeb80a4 0.16.4 2017-02-05 16:17:54 +09:00
Junegunn Choi
02ceae15a2 [vim] Download instruction for Windows 2017-02-05 02:07:54 +09:00
Junegunn Choi
e514739280 Fix failing test case 2017-02-04 22:49:17 +09:00
Junegunn Choi
72265298f9 [vim] Apply --no-height when running fzf in full screen mode
To override --height option in FZF_DEFAULT_OPTS
2017-02-04 21:52:05 +09:00
Junegunn Choi
4b700192c1 Add --border option to draw horizontal lines above and below the finder
Goes well with --height
2017-02-04 21:51:22 +09:00
Junegunn Choi
fe83589ade Add test case for --tiebreak=begin 2017-02-03 02:14:14 +09:00
Junegunn Choi
fcf63c74f1 Fix --tiebreak=begin with algo v2
Due to performance consideration, FuzzyMatchV2 does not return the exact
positions of the matching characters by default. However, the ommission
caused `--tiebreak=begin` to produce inaccurate result in some cases.

  (echo baz foo bar; echo foo bar baz) | fzf --tiebreak=begin -fbar | head -1

  # Expected: foo bar baz
  # Actual:   baz foo bar

This commit fixes the problem by using the end offset which is
guaranteed to be correct.
2017-02-02 13:46:46 +09:00
Junegunn Choi
c95bb109c8 Suppress CSI codes in the output 2017-02-02 13:14:27 +09:00
Junegunn Choi
bd9c46ee34 Update ANSI processor to strip ^H along with its preceding character 2017-02-02 13:00:41 +09:00
Junegunn Choi
736aeaa1d3 Update go-runewidth
https://github.com/junegunn/go-runewidth/pull/1

/cc @joshuarubin
2017-02-02 10:08:56 +09:00
Junegunn Choi
dd1f26522c Fix caching scheme when --exact is set and '-prefix is used 2017-02-01 02:06:56 +09:00
Kassio Borges
712b7b2188 [vim] Expose buffer variable with the current fzf setup (#828)
Exposing the `b:fzf` variable will be useful to get information about
which command is being executed on the current fzf window. With that,
now, it's possible to use the current command name on the statusline:

```viml
au User FzfStatusLine call <SID>fzf_statusline()

function! s:fzf_statusline()
  let fzf_cmd_name = get(b:fzf, 'name', 'FZF')
  let &l:statusline = '> '.fzf_cmd_name
endfunction
```
2017-02-01 01:06:52 +09:00
Junegunn Choi
5b749e2d5c Update documentation 2017-01-31 21:43:41 +09:00
Junegunn Choi
d85a69a709 0.16.3 2017-01-30 01:53:17 +09:00
Junegunn Choi
a425e96fb2 [vim] g:fzf_prefer_tmux for choosing fzf-tmux over --height
https://github.com/junegunn/fzf.vim/issues/296
2017-01-30 01:47:30 +09:00
Junegunn Choi
7763fdf6ba Update man pages 2017-01-30 01:27:12 +09:00
Junegunn Choi
dd156b59fc Fix display issues with execute action
- Move cursor to the top-left corner when starting a command in
  alternate screen
- Fix cursor position when returning to alternate screen when fzf is
  running in full screen mode
2017-01-30 01:08:07 +09:00
Junegunn Choi
36dceecd58 Add support for ctrl-space key
Close #825
2017-01-28 02:54:47 +09:00
Junegunn Choi
421b9b271a Add execute-silent action
Close #823
2017-01-27 18:56:41 +09:00
Junegunn Choi
ed57dcb924 Extend placeholder expression for multiple selections
Close #788
2017-01-27 16:38:42 +09:00
Junegunn Choi
95c77bfb98 Use --bind instead of --toggle-sort
Related #822
2017-01-26 11:54:08 +09:00
Junegunn Choi
2e3e721344 Merge branch 'devel' 2017-01-26 11:52:24 +09:00
Junegunn Choi
da2c28d5c2 Add --read0 and --print0 to --help output
Close #822
2017-01-26 11:41:20 +09:00
Junegunn Choi
dbddee9de9 [fish] Add toggle-sort back to CTRL-R (#759) 2017-01-25 10:21:14 +09:00
Junegunn Choi
8731d75607 Recalculate the width of trimmed line
Close #821
2017-01-25 02:39:49 +09:00
Junegunn Choi
f2ce233a6d 0.16.2 2017-01-24 00:37:47 +09:00
Junegunn Choi
6a75e30941 Allow invisible preview window (--preview-window 0)
Close #820
2017-01-24 00:23:16 +09:00
Junegunn Choi
a3244c4892 Delete every line below the cursor 2017-01-23 22:07:18 +09:00
Junegunn Choi
a5ad8fd3bd Minor refactoring 2017-01-23 12:55:13 +09:00
Junegunn Choi
deccdb1ec5 Cursor postition response can be preceded by user key strokes 2017-01-23 12:55:11 +09:00
Junegunn Choi
12a43b5e62 Disable mouse if failed to query cursor position 2017-01-23 12:55:04 +09:00
Junegunn Choi
e1291aa6d2 Fix make deps to see the right git dir 2017-01-23 12:10:43 +09:00
Junegunn Choi
bb26f32ac7 Allow build on OpenBSD/FreeBSD/Android
Close #497
2017-01-22 18:51:04 +09:00
Junegunn Choi
4d928001b8 Update release script to upload assets in parallel 2017-01-22 18:33:30 +09:00
Junegunn Choi
c4baa6a10c Update man page: 24-bit color 2017-01-22 18:33:03 +09:00
Junegunn Choi
71dec3dc5e Fix bug where screen is not properly cleared on toggle-preview 2017-01-22 17:43:27 +09:00
Junegunn Choi
e5017c0431 Remove unnecesasry test case 2017-01-22 17:41:47 +09:00
Junegunn Choi
cbb5134874 [vim] Use 24-bit colors if termguicolors is set 2017-01-22 14:40:30 +09:00
Junegunn Choi
ff248d566d Drop ncurses dependency
Close #818
2017-01-22 14:13:37 +09:00
Junegunn Choi
6ccc12c332 Use alternate screen if --height needs the entire screen
- Remove unnecessary scrolling
- Allow us to use `--height 100%` under Neovim terminal for 24-bit colors

Related:
- #789
- https://github.com/neovim/neovim/issues/4151
2017-01-22 05:26:38 +09:00
Junegunn Choi
2a669e9a17 Clear lines even when background color is not set
Also revert the workaround in Vim plugin introduced in fa7c897.

Related: #814
2017-01-22 03:19:50 +09:00
Junegunn Choi
5130abe76f Merge branch 'master' into devel 2017-01-22 03:10:06 +09:00
Junegunn Choi
fa7c8977a8 [vim] tput el to clear the last line
Close #814

Not grouping commands to avoid errors on non-standard shells.
2017-01-22 03:03:26 +09:00
Junegunn Choi
24fa183297 make deps 2017-01-22 02:54:19 +09:00
Junegunn Choi
131aa5dd15 Composable actions in --bind
Close #816
2017-01-22 02:32:49 +09:00
Junegunn Choi
a06ccc928f Fix flakies 2017-01-21 04:17:51 +09:00
Junegunn Choi
d09ad13208 [zsh] Workaround trailing esacped space bug in go-shellwords
https://github.com/mattn/go-shellwords/issues/3

Close #812
2017-01-21 03:59:36 +09:00
Junegunn Choi
8ac37d5927 [shell] Do not override --reverse in CTRL-R
Close #807
2017-01-17 18:09:29 +09:00
Junegunn Choi
7ef0e50507 [bash/zsh] Remove unused --reverse in CTRL-R binding
Related #807
2017-01-17 11:58:25 +09:00
Junegunn Choi
62ab8ece5e 0.16.1 2017-01-16 12:27:40 +09:00
Junegunn Choi
8e2e63f9b9 Propertly fill window with background color
Close #805
2017-01-16 12:27:32 +09:00
Junegunn Choi
f96173cbe4 Add -L flag to the default find command
Close #781
2017-01-16 12:01:58 +09:00
Amos Bird
11015df52f Add half-page-{up,down} actions (#784) 2017-01-16 11:58:13 +09:00
45 changed files with 1857 additions and 2446 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ gopath
pkg pkg
Gemfile.lock Gemfile.lock
.DS_Store .DS_Store
doc/tags

View File

@@ -25,65 +25,22 @@ make install
# Build 32-bit and 64-bit executables and tarballs # Build 32-bit and 64-bit executables and tarballs
make release make release
# Build executables and tarballs for Linux using Docker # Make release archives for all supported platforms
make linux make release-all
``` ```
### Using `go get` ### Using `go get`
Alternatively, you can build fzf directly with `go get` command without Alternatively, you can build fzf directly with `go get` command without
cloning the repository. manually cloning the repository.
```sh ```sh
go get -u github.com/junegunn/fzf/src/fzf go get -u github.com/junegunn/fzf/src/fzf
``` ```
Build options
-------------
### With ncurses 6
The official binaries of fzf are built with ncurses 5 because it's widely
supported by different platforms. However ncurses 5 is old and has a number of
limitations.
1. Does not support more than 256 color pairs (See [357][357])
2. Does not support italics
3. Does not support 24-bit color
[357]: https://github.com/junegunn/fzf/issues/357
But you can manually build fzf with ncurses 6 to overcome some of these
limitations. ncurses 6 supports up to 32767 color pairs (1), and supports
italics (2). To build fzf with ncurses 6, you have to install it first. On
macOS, you can use Homebrew to install it.
```sh
brew install homebrew/dupes/ncurses
LDFLAGS="-L/usr/local/opt/ncurses/lib" make install
```
### With tcell
[tcell][tcell] is a portable alternative to ncurses and we currently use it to
build Windows binaries. tcell has many benefits but most importantly, it
supports 24-bit colors. To build fzf with tcell:
```sh
TAGS=tcell make install
```
However, note that tcell has its own issues.
- Poor rendering performance compared to ncurses
- Does not support bracketed-paste mode
- Does not support italics unlike ncurses 6
- Some wide characters are not correctly displayed
Third-party libraries used Third-party libraries used
-------------------------- --------------------------
- [ncurses][ncurses]
- [mattn/go-runewidth](https://github.com/mattn/go-runewidth) - [mattn/go-runewidth](https://github.com/mattn/go-runewidth)
- Licensed under [MIT](http://mattn.mit-license.org) - Licensed under [MIT](http://mattn.mit-license.org)
- [mattn/go-shellwords](https://github.com/mattn/go-shellwords) - [mattn/go-shellwords](https://github.com/mattn/go-shellwords)
@@ -97,10 +54,3 @@ License
------- -------
[MIT](LICENSE) [MIT](LICENSE)
[install]: https://github.com/junegunn/fzf#installation
[go]: https://golang.org/
[gil]: http://en.wikipedia.org/wiki/Global_Interpreter_Lock
[ncurses]: https://www.gnu.org/software/ncurses/
[req]: http://golang.org/doc/install
[tcell]: https://github.com/gdamore/tcell

View File

@@ -1,6 +1,61 @@
CHANGELOG CHANGELOG
========= =========
0.16.7
------
- Added support for `ctrl-alt-[a-z]` key chords
- CTRL-Z (SIGSTOP) now works with fzf
- fzf will export `$FZF_PREVIEW_WINDOW` so that the scripts can use it
- Bug fixes and improvements in Vim plugin and shell extensions
0.16.6
------
- Minor bug fixes and improvements
- Added `--no-clear` option for scripting purposes
0.16.5
------
- Minor bug fixes
- Added `toggle-preview-wrap` action
- Built with Go 1.8
0.16.4
------
- Added `--border` option to draw border above and below the finder
- Bug fixes and improvements
0.16.3
------
- Fixed a bug where fzf incorrectly display the lines when straddling tab
characters are trimmed
- Placeholder expression used in `--preview` and `execute` action can
optionally take `+` flag to be used with multiple selections
- e.g. `git log --oneline | fzf --multi --preview 'git show {+1}'`
- Added `execute-silent` action for executing a command silently without
switching to the alternate screen. This is useful when the process is
short-lived and you're not interested in its output.
- e.g. `fzf --bind 'ctrl-y:execute!(echo -n {} | pbcopy)'`
- `ctrl-space` is allowed in `--bind`
0.16.2
------
- Dropped ncurses dependency
- Binaries for freebsd, openbsd, arm5, arm6, arm7, and arm8
- Official 24-bit color support
- Added support for composite actions in `--bind`. Multiple actions can be
chained using `+` separator.
- e.g. `fzf --bind 'ctrl-y:execute(echo -n {} | pbcopy)+abort'`
- `--preview-window` with size 0 is allowed. This is used to make fzf execute
preview command in the background without displaying the result.
- Minor bug fixes and improvements
0.16.1
------
- Fixed `--height` option to properly fill the window with the background
color
- Added `half-page-up` and `half-page-down` actions
- Added `-L` flag to the default find command
0.16.0 0.16.0
------ ------
- *Added `--height HEIGHT[%]` option* - *Added `--height HEIGHT[%]` option*

155
README-VIM.md Normal file
View File

@@ -0,0 +1,155 @@
FZF Vim integration
===================
This repository only enables basic integration with Vim. If you're looking for
more, check out [fzf.vim](https://github.com/junegunn/fzf.vim) project.
(Note: To use fzf in GVim, an external terminal emulator is required.)
`:FZF[!]`
---------
If you have set up fzf for Vim, `:FZF` command will be added.
```vim
" Look for files under current directory
:FZF
" Look for files under your home directory
:FZF ~
" With options
:FZF --no-sort --reverse --inline-info /tmp
" Bang version starts fzf in fullscreen mode
:FZF!
```
Similarly to [ctrlp.vim](https://github.com/kien/ctrlp.vim), use enter key,
`CTRL-T`, `CTRL-X` or `CTRL-V` to open selected files in the current window,
in new tabs, in horizontal splits, or in vertical splits respectively.
Note that the environment variables `FZF_DEFAULT_COMMAND` and
`FZF_DEFAULT_OPTS` also apply here.
### Configuration
- `g:fzf_action`
- Customizable extra key bindings for opening selected files in different ways
- `g:fzf_layout`
- Determines the size and position of fzf window (tmux pane or Neovim split)
- `g:fzf_colors`
- Customizes fzf colors to match the current color scheme
- `g:fzf_history_dir`
- Enables history feature
- `g:fzf_launcher`
- (Only in GVim) Terminal emulator to open fzf with
- `g:Fzf_launcher` for function reference
#### Examples
```vim
" This is the default extra key bindings
let g:fzf_action = {
\ 'ctrl-t': 'tab split',
\ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' }
" Default fzf layout
" - down / up / left / right
let g:fzf_layout = { 'down': '~40%' }
" In Neovim, you can set up fzf window using a Vim command
let g:fzf_layout = { 'window': 'enew' }
let g:fzf_layout = { 'window': '-tabnew' }
let g:fzf_layout = { 'window': '10split enew' }
" Customize fzf colors to match your color scheme
let g:fzf_colors =
\ { 'fg': ['fg', 'Normal'],
\ 'bg': ['bg', 'Normal'],
\ 'hl': ['fg', 'Comment'],
\ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'],
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
\ 'hl+': ['fg', 'Statement'],
\ 'info': ['fg', 'PreProc'],
\ 'prompt': ['fg', 'Conditional'],
\ 'pointer': ['fg', 'Exception'],
\ 'marker': ['fg', 'Keyword'],
\ 'spinner': ['fg', 'Label'],
\ 'header': ['fg', 'Comment'] }
" Enable per-command history.
" CTRL-N and CTRL-P will be automatically bound to next-history and
" previous-history instead of down and up. If you don't like the change,
" explicitly bind the keys to down and up in your $FZF_DEFAULT_OPTS.
let g:fzf_history_dir = '~/.local/share/fzf-history'
```
`fzf#run`
---------
For more advanced uses, you can use `fzf#run([options])` function with the
following options.
| Option name | Type | Description |
| -------------------------- | ------------- | ---------------------------------------------------------------- |
| `source` | string | External command to generate input to fzf (e.g. `find .`) |
| `source` | list | Vim list as input to fzf |
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
| `sink` | funcref | Reference to function to process each selected item |
| `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once |
| `options` | string/list | Options to fzf |
| `dir` | string | Working directory |
| `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) |
| `window` (*Neovim only*) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) |
| `launcher` | string | External terminal emulator to start fzf with (GVim only) |
| `launcher` | funcref | Function for generating `launcher` string (GVim only) |
`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
issues on different platforms.
```vim
call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'})
call fzf#run({'options': ['--reverse', '--prompt', 'C:\Program Files\']})
```
`fzf#wrap`
----------
`fzf#wrap([name string,] [opts dict,] [fullscreen boolean])` is a helper
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
`:FZF`.
```vim
command! -bang MyStuff
\ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, <bang>0))
```
GVim
----
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`.
```vim
" This is the default. %s is replaced with fzf command
let g:fzf_launcher = 'xterm -e bash -ic %s'
" 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][macvim-iterm2] to see how to set up.
[macvim-iterm2]: https://github.com/junegunn/fzf/wiki/On-MacVim-with-iTerm2
[License](LICENSE)
------------------
The MIT License (MIT)
Copyright (c) 2017 Junegunn Choi

129
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) <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)
=== ===
fzf is a general-purpose command-line fuzzy finder. fzf is a general-purpose command-line fuzzy finder.
@@ -53,7 +53,7 @@ brew install fzf
/usr/local/opt/fzf/install /usr/local/opt/fzf/install
``` ```
### Vim plugin ### As Vim plugin
You can manually add the directory to `&runtimepath` as follows, You can manually add the directory to `&runtimepath` as follows,
@@ -72,7 +72,25 @@ But it's recommended that you use a plugin manager like
Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' } Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' }
``` ```
### Upgrading fzf ### Windows
Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also
available as a [Chocolatey package][choco].
[choco]: https://chocolatey.org/packages/fzf
```sh
choco install fzf
```
However, other components of the project may not work on Windows. You might
want to consider installing fzf on [Windows Subsystem for Linux][wsl] where
everything runs flawlessly.
[wsl]: https://blogs.msdn.microsoft.com/wsl/
Upgrading fzf
-------------
fzf is being actively developed and you might want to upgrade it once in a fzf is being actively developed and you might want to upgrade it once in a
while. Please follow the instruction below depending on the installation while. Please follow the instruction below depending on the installation
@@ -80,17 +98,9 @@ method used.
- git: `cd ~/.fzf && git pull && ./install` - git: `cd ~/.fzf && git pull && ./install`
- brew: `brew update; brew reinstall fzf` - brew: `brew update; brew reinstall fzf`
- chocolatey: `choco upgrade fzf`
- vim-plug: `:PlugUpdate fzf` - vim-plug: `:PlugUpdate fzf`
### Windows
Pre-built binaries for Windows can be downloaded [here][bin]. However, other
components of the project may not work on Windows. You might want to consider
installing fzf on [Windows Subsystem for Linux][wsl] where everything runs
flawlessly.
[wsl]: https://blogs.msdn.microsoft.com/wsl/
Building fzf Building fzf
------------ ------------
@@ -99,7 +109,7 @@ See [BUILD.md](BUILD.md).
Usage Usage
----- -----
fzf will launch curses-based finder, read the list from STDIN, and write the fzf will launch interactive finder, read the list from STDIN, and write the
selected item to STDOUT. selected item to STDOUT.
```sh ```sh
@@ -140,10 +150,10 @@ vim $(fzf --height 40% --reverse)
``` ```
You can add these options to `$FZF_DEFAULT_OPTS` so that they're applied by You can add these options to `$FZF_DEFAULT_OPTS` so that they're applied by
default. default. For example,
```sh ```sh
export FZF_DEFAULT_OPTS='--height 40% --reverse' export FZF_DEFAULT_OPTS='--height 40% --reverse --border'
``` ```
#### Search syntax #### Search syntax
@@ -229,8 +239,8 @@ fish.
- 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
- Sort is disabled by default to respect chronological ordering - If you want to see the commands in chronological order, press `CTRL-R`
- Press `CTRL-R` again to toggle sort again which toggles sorting by relevance
- Set `FZF_CTRL_R_OPTS` to pass additional options - Set `FZF_CTRL_R_OPTS` to pass additional options
- `ALT-C` - cd into the selected directory - `ALT-C` - cd into the selected directory
- Set `FZF_ALT_C_COMMAND` to override the default command - Set `FZF_ALT_C_COMMAND` to override the default command
@@ -335,93 +345,14 @@ commands as well like follows.
complete -F _fzf_file_completion -o default -o bashdefault doge complete -F _fzf_file_completion -o default -o bashdefault doge
``` ```
Usage as Vim plugin Vim plugin
------------------- ----------
This repository only enables basic integration with Vim. If you're looking for See [README-VIM.md](README-VIM.md).
more, check out [fzf.vim](https://github.com/junegunn/fzf.vim) project.
(Note: To use fzf in GVim, an external terminal emulator is required.)
#### `:FZF[!]`
If you have set up fzf for Vim, `:FZF` command will be added.
```vim
" Look for files under current directory
:FZF
" Look for files under your home directory
:FZF ~
" With options
:FZF --no-sort --reverse --inline-info /tmp
" Bang version starts in fullscreen instead of using tmux pane or Neovim split
:FZF!
```
Similarly to [ctrlp.vim](https://github.com/kien/ctrlp.vim), use enter key,
`CTRL-T`, `CTRL-X` or `CTRL-V` to open selected files in the current window,
in new tabs, in horizontal splits, or in vertical splits respectively.
Note that the environment variables `FZF_DEFAULT_COMMAND` and
`FZF_DEFAULT_OPTS` also apply here. Refer to [the wiki page][fzf-config] for
customization.
[fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-Vim-plugin
#### `fzf#run`
For more advanced uses, you can use `fzf#run([options])` function with the
following options.
| Option name | Type | Description |
| -------------------------- | ------------- | ---------------------------------------------------------------- |
| `source` | string | External command to generate input to fzf (e.g. `find .`) |
| `source` | list | Vim list as input to fzf |
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
| `sink` | funcref | Reference to function to process each selected item |
| `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once |
| `options` | string | Options to fzf |
| `dir` | string | Working directory |
| `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) |
| `window` (*Neovim only*) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) |
| `launcher` | string | External terminal emulator to start fzf with (GVim only) |
| `launcher` | funcref | Function for generating `launcher` string (GVim only) |
Examples can be found on [the wiki
page](https://github.com/junegunn/fzf/wiki/Examples-(vim)).
#### `fzf#wrap`
`fzf#wrap([name string,] [opts dict,] [fullscreen boolean])` is a helper
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
`:FZF`.
```vim
command! -bang MyStuff
\ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, <bang>0))
```
Tips Tips
---- ----
#### Rendering issues
If you have any rendering issues, check the following:
1. Make sure `$TERM` is correctly set. fzf will use 256-color only if it
contains `256` (e.g. `xterm-256color`)
2. If you're on screen or tmux, `$TERM` should be either `screen` or
`screen-256color`
3. Some terminal emulators (e.g. mintty) have problem displaying default
background color and make some text unable to read. In that case, try
`--black` option. And if it solves your problem, I recommend including it
in `FZF_DEFAULT_OPTS` for further convenience.
4. If you still have problem, try `--no-256` option or even `--no-color`.
#### Respecting `.gitignore`, `.hgignore`, and `svn:ignore` #### Respecting `.gitignore`, `.hgignore`, and `svn:ignore`
[ag](https://github.com/ggreer/the_silver_searcher) or [ag](https://github.com/ggreer/the_silver_searcher) or

View File

@@ -121,7 +121,7 @@ args+=("--no-height")
if tmux list-panes -F '#F' | grep -q Z; then if tmux list-panes -F '#F' | grep -q Z; then
zoomed=1 zoomed=1
original_window=$(tmux display-message -p "#{window_id}") original_window=$(tmux display-message -p "#{window_id}")
tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - \\\\; do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'") tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - '\\;' do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'")
tmux swap-pane -t $tmp_window \; select-window -t $tmp_window tmux swap-pane -t $tmp_window \; select-window -t $tmp_window
fi fi
@@ -138,13 +138,18 @@ cleanup() {
# 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 swap-pane -t $original_window \; \ tmux swap-pane -t $original_window \; \
select-window -t $original_window \; \ select-window -t $original_window \; \
kill-window -t $tmp_window \; \ kill-window -t $tmp_window \; \
resize-pane -Z resize-pane -Z
fi fi
if [ $# -gt 0 ]; then
exit 130
fi
} }
trap cleanup EXIT SIGINT SIGTERM trap 'cleanup 1' SIGUSR1
envs="env TERM=$TERM " envs="env TERM=$TERM "
[[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" [[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
@@ -163,18 +168,22 @@ for arg in "${args[@]}"; do
opts="$opts \"$arg\"" opts="$opts \"$arg\""
done done
pppid=$$
trap_set="trap 'kill -SIGUSR1 $pppid' EXIT SIGINT SIGTERM"
trap_unset="trap - EXIT SIGINT SIGTERM"
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
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 "$trap_set;cd $(printf %q "$PWD");$envs bash $argsf;$trap_unset" $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 "$trap_set;$envs bash $argsf;$trap_unset" $swap \
> /dev/null 2>&1 > /dev/null 2>&1
cat <&0 > $fifo1 & cat <&0 > $fifo1 &
fi fi

182
doc/fzf.txt Normal file
View File

@@ -0,0 +1,182 @@
fzf.txt fzf Last change: April 28 2017
FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
==============================================================================
FZF Vim integration
:FZF[!]
Configuration
Examples
fzf#run
fzf#wrap
GVim
License
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.)
{1} https://github.com/junegunn/fzf.vim
:FZF[!]
==============================================================================
*:FZF*
If you have set up fzf for Vim, `:FZF` command will be added.
>
" Look for files under current directory
:FZF
" Look for files under your home directory
:FZF ~
" With options
:FZF --no-sort --reverse --inline-info /tmp
" Bang version starts fzf in fullscreen mode
:FZF!
<
Similarly to {ctrlp.vim}{2}, use enter key, CTRL-T, CTRL-X or CTRL-V to open
selected files in the current window, in new tabs, in horizontal splits, or in
vertical splits respectively.
Note that the environment variables `FZF_DEFAULT_COMMAND` and
`FZF_DEFAULT_OPTS` also apply here.
{2} https://github.com/kien/ctrlp.vim
< Configuration >_____________________________________________________________~
*fzf-configuration*
*g:fzf_action* *g:fzf_layout* *g:fzf_colors* *g:fzf_history_dir* *g:fzf_launcher*
*g:Fzf_launcher*
- `g:fzf_action`
- Customizable extra key bindings for opening selected files in different
ways
- `g:fzf_layout`
- Determines the size and position of fzf window (tmux pane or Neovim split)
- `g:fzf_colors`
- Customizes fzf colors to match the current color scheme
- `g:fzf_history_dir`
- Enables history feature
- `g:fzf_launcher`
- (Only in GVim) Terminal emulator to open fzf with
- `g:Fzf_launcher` for function reference
Examples~
*fzf-examples*
>
" This is the default extra key bindings
let g:fzf_action = {
\ 'ctrl-t': 'tab split',
\ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' }
" Default fzf layout
" - down / up / left / right
let g:fzf_layout = { 'down': '~40%' }
" In Neovim, you can set up fzf window using a Vim command
let g:fzf_layout = { 'window': 'enew' }
let g:fzf_layout = { 'window': '-tabnew' }
let g:fzf_layout = { 'window': '10split enew' }
" Customize fzf colors to match your color scheme
let g:fzf_colors =
\ { 'fg': ['fg', 'Normal'],
\ 'bg': ['bg', 'Normal'],
\ 'hl': ['fg', 'Comment'],
\ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'],
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
\ 'hl+': ['fg', 'Statement'],
\ 'info': ['fg', 'PreProc'],
\ 'prompt': ['fg', 'Conditional'],
\ 'pointer': ['fg', 'Exception'],
\ 'marker': ['fg', 'Keyword'],
\ 'spinner': ['fg', 'Label'],
\ 'header': ['fg', 'Comment'] }
" Enable per-command history.
" CTRL-N and CTRL-P will be automatically bound to next-history and
" previous-history instead of down and up. If you don't like the change,
" explicitly bind the keys to down and up in your $FZF_DEFAULT_OPTS.
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
following options.
---------------------------+---------------+--------------------------------------------------------------
Option name | Type | Description ~
---------------------------+---------------+--------------------------------------------------------------
`source` | string | External command to generate input to fzf (e.g. `find.` )
`source` | list | Vim list as input to fzf
`sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` )
`sink` | funcref | Reference to function to process each selected item
`sink*` | funcref | Similar to `sink` , but takes the list of output lines at once
`options` | string/list | Options to fzf
`dir` | string | Working directory
`up` / `down` / `left` / `right` | number/string | Use tmux pane with the given size (e.g. `20` , `50%` )
`window` (Neovim only) | string | Command to open fzf window (e.g. `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
should suffice, but prefer to use list type if you're concerned about escaping
issues on different platforms.
>
call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'})
call fzf#run({'options': ['--reverse', '--prompt', 'C:\Program Files\']})
<
FZF#WRAP *fzf#wrap*
==============================================================================
`fzf#wrap([namestring,][optsdict,][fullscreenboolean])` is a helper
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
`:FZF`.
>
command! -bang MyStuff
\ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, <bang>0))
<
GVIM *fzf-gvim*
==============================================================================
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`.
>
" This is the default. %s is replaced with fzf command
let g:fzf_launcher = 'xterm -e bash -ic %s'
" 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
LICENSE *fzf-license*
==============================================================================
The MIT License (MIT)
Copyright (c) 2017 Junegunn Choi
==============================================================================
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:

1348
fzf

File diff suppressed because it is too large Load Diff

141
install
View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.16.0 version=0.16.7
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
@@ -88,17 +88,6 @@ check_binary() {
return 1 return 1
} }
symlink() {
echo " - Creating symlink: bin/$1 -> bin/fzf"
(cd "$fzf_base"/bin &&
rm -f fzf &&
ln -sf $1 fzf)
if [ $? -ne 0 ]; then
binary_error="Failed to create symlink"
return 1
fi
}
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"
@@ -110,11 +99,11 @@ link_fzf_in_path() {
} }
try_curl() { try_curl() {
command -v curl > /dev/null && curl -fL $1 | tar -xz command -v curl > /dev/null && curl -fL $1 | tar -xzf -
} }
try_wget() { try_wget() {
command -v wget > /dev/null && wget -O - $1 | tar -xz command -v wget > /dev/null && wget -O - $1 | tar -xzf -
} }
download() { download() {
@@ -124,9 +113,6 @@ download() {
echo " - Already exists" echo " - Already exists"
check_binary && return check_binary && return
fi fi
if [ -x "$fzf_base"/bin/$1 ]; then
symlink $1 && check_binary && return
fi
link_fzf_in_path && return link_fzf_in_path && return
fi fi
mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin
@@ -147,12 +133,12 @@ download() {
fi fi
set +o pipefail set +o pipefail
if [ ! -f $1 ]; then if [ ! -f fzf ]; then
binary_error="Failed to download ${1}" binary_error="Failed to download ${1}"
return return
fi fi
chmod +x $1 && symlink $1 && check_binary chmod +x fzf && check_binary
} }
# Try to download binary executable # Try to download binary executable
@@ -160,84 +146,21 @@ archi=$(uname -sm)
binary_available=1 binary_available=1
binary_error="" binary_error=""
case "$archi" in case "$archi" in
Darwin\ x86_64) download fzf-$version-darwin_${binary_arch:-amd64} ;; Darwin\ *64) download fzf-$version-darwin_${binary_arch:-amd64} ;;
Darwin\ i*86) download fzf-$version-darwin_${binary_arch:-386} ;; Darwin\ *86) download fzf-$version-darwin_${binary_arch:-386} ;;
Linux\ x86_64) download fzf-$version-linux_${binary_arch:-amd64} ;; Linux\ *64) download fzf-$version-linux_${binary_arch:-amd64} ;;
Linux\ i*86) download fzf-$version-linux_${binary_arch:-386} ;; Linux\ *86) download fzf-$version-linux_${binary_arch:-386} ;;
*) binary_available=0 binary_error=1 ;; Linux\ armv5*) download fzf-$version-linux_${binary_arch:-arm5} ;;
Linux\ armv6*) download fzf-$version-linux_${binary_arch:-arm6} ;;
Linux\ armv7*) download fzf-$version-linux_${binary_arch:-arm7} ;;
Linux\ armv8*) download fzf-$version-linux_${binary_arch:-arm8} ;;
FreeBSD\ *64) download fzf-$version-freebsd_${binary_arch:-amd64} ;;
FreeBSD\ *86) download fzf-$version-freebsd_${binary_arch:-386} ;;
OpenBSD\ *64) download fzf-$version-openbsd_${binary_arch:-amd64} ;;
OpenBSD\ *86) download fzf-$version-openbsd_${binary_arch:-386} ;;
*) binary_available=0 binary_error=1 ;;
esac esac
install_ruby_fzf() {
if [ -z "$allow_legacy" ]; then
ask "Do you want to install legacy Ruby version instead?" && exit 1
fi
echo "Installing legacy Ruby version ..."
# ruby executable
echo -n "Checking Ruby executable ... "
ruby=$(command -v ruby)
if [ $? -ne 0 ]; then
echo "ruby executable not found !!!"
exit 1
fi
# System ruby is preferred
system_ruby=/usr/bin/ruby
if [ -x $system_ruby ] && [ $system_ruby != "$ruby" ]; then
$system_ruby --disable-gems -rcurses -e0 2> /dev/null
[ $? -eq 0 ] && ruby=$system_ruby
fi
echo "OK ($ruby)"
# Curses-support
echo -n "Checking Curses support ... "
"$ruby" -rcurses -e0 2> /dev/null
if [ $? -eq 0 ]; then
echo "OK"
else
echo "Not found"
echo "Installing 'curses' gem ... "
if (( EUID )); then
/usr/bin/env gem install curses --user-install
else
/usr/bin/env gem install curses
fi
if [ $? -ne 0 ]; then
echo
echo "Failed to install 'curses' gem."
if [[ $(uname -r) =~ 'ARCH' ]]; then
echo "Make sure that base-devel package group is installed."
fi
exit 1
fi
fi
# Ruby version
echo -n "Checking Ruby version ... "
"$ruby" -e 'exit RUBY_VERSION >= "1.9"'
if [ $? -eq 0 ]; then
echo ">= 1.9"
"$ruby" --disable-gems -rcurses -e0 2> /dev/null
if [ $? -eq 0 ]; then
fzf_cmd="$ruby --disable-gems $fzf_base/fzf"
else
fzf_cmd="$ruby $fzf_base/fzf"
fi
else
echo "< 1.9"
fzf_cmd="$ruby $fzf_base/fzf"
fi
# Create fzf script
echo -n "Creating wrapper script for fzf ... "
rm -f "$fzf_base"/bin/fzf
echo "#!/bin/sh" > "$fzf_base"/bin/fzf
echo "$fzf_cmd \"\$@\"" >> "$fzf_base"/bin/fzf
chmod +x "$fzf_base"/bin/fzf
echo "OK"
}
cd "$fzf_base" cd "$fzf_base"
if [ -n "$binary_error" ]; then if [ -n "$binary_error" ]; then
if [ $binary_available -eq 0 ]; then if [ $binary_available -eq 0 ]; then
@@ -255,12 +178,12 @@ if [ -n "$binary_error" ]; then
echo "OK" echo "OK"
cp "$GOPATH/bin/fzf" "$fzf_base/bin/" cp "$GOPATH/bin/fzf" "$fzf_base/bin/"
else else
echo "Failed to build binary ..." echo "Failed to build binary. Installation failed."
install_ruby_fzf exit 1
fi fi
else else
echo "go executable not found. Cannot build binary ..." echo "go executable not found. Installation failed."
install_ruby_fzf exit 1
fi fi
fi fi
@@ -374,6 +297,17 @@ append_line() {
set +e set +e
} }
create_file() {
local file="$1"
shift
echo "Create $file:"
for line in "$@"; do
echo " $line"
echo "$line" >> "$file"
done
echo
}
if [ $update_config -eq 2 ]; then if [ $update_config -eq 2 ]; then
echo echo
ask "Do you want to update your shell configuration files?" ask "Do you want to update your shell configuration files?"
@@ -387,7 +321,14 @@ done
if [ $key_bindings -eq 1 ] && [ $has_fish -eq 1 ]; then if [ $key_bindings -eq 1 ] && [ $has_fish -eq 1 ]; then
bind_file=~/.config/fish/functions/fish_user_key_bindings.fish bind_file=~/.config/fish/functions/fish_user_key_bindings.fish
append_line $update_config "fzf_key_bindings" "$bind_file" if [ ! -e "$bind_file" ]; then
create_file "$bind_file" \
'function fish_user_key_bindings' \
' fzf_key_bindings' \
'end'
else
append_line $update_config "fzf_key_bindings" "$bind_file"
fi
fi fi
if [ $update_config -eq 1 ]; then if [ $update_config -eq 1 ]; then

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf-tmux 1 "Jan 2017" "fzf 0.16.0" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "Apr 2017" "fzf 0.16.7" "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

@@ -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 "Jan 2017" "fzf 0.16.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "Apr 2017" "fzf 0.16.7" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -156,6 +156,9 @@ Ignored when \fB--height\fR is not specified.
.B "--reverse" .B "--reverse"
Reverse orientation Reverse orientation
.TP .TP
.B "--border"
Draw border above and below the finder
.TP
.BI "--margin=" MARGIN .BI "--margin=" MARGIN
Comma-separated expression for margins around the finder. Comma-separated expression for margins around the finder.
.br .br
@@ -209,8 +212,7 @@ Number of spaces for a tab character (default: 8)
Color configuration. The name of the base color scheme is followed by custom Color configuration. The name of the base color scheme is followed by custom
color mappings. Ansi color code of -1 denotes terminal default 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, but the support for 24-bit colors is experimental and only works when format.
\fB--height\fR option is used.
.RS .RS
e.g. \fBfzf --color=bg+:24\fR e.g. \fBfzf --color=bg+:24\fR
@@ -263,13 +265,21 @@ Execute the given command for the current line and display the result on the
preview window. \fB{}\fR in the command is the placeholder that is replaced to preview window. \fB{}\fR in the command is the placeholder that is replaced to
the single-quoted string of the current line. To transform the replacement the single-quoted string of the current line. To transform the replacement
string, specify field index expressions between the braces (See \fBFIELD INDEX string, specify field index expressions between the braces (See \fBFIELD INDEX
EXPRESSION\fR for the details). Also, \fB{q}\fR is replaced to the current EXPRESSION\fR for the details).
query string.
.RS .RS
e.g. \fBfzf --preview="head -$LINES {}"\fR e.g. \fBfzf --preview="head -$LINES {}"\fR
\fBls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR \fBls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR
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
was made) individually quoted.
e.g. \fBfzf --multi --preview="head -10 {+}"\fR
\fBgit log --oneline | fzf --multi --preview 'git show {+1}'\fR
Also, \fB{q}\fR is replaced to the current query string.
Note that you can escape a placeholder pattern by prepending a backslash. Note that you can escape a placeholder pattern by prepending a backslash.
.RE .RE
.TP .TP
@@ -279,6 +289,9 @@ Determine the layout of the preview window. If the argument ends with
\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.
If size is given as 0, preview window will not be visible, but fzf will still
execute the command in the background.
.RS .RS
.B POSITION: (default: right) .B POSITION: (default: right)
\fBup \fBup
@@ -321,10 +334,16 @@ e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR
.RE .RE
.TP .TP
.B "--read0" .B "--read0"
Read input delimited by ASCII NUL character instead of newline character Read input delimited by ASCII NUL characters instead of newline characters
.TP .TP
.B "--print0" .B "--print0"
Print output delimited by ASCII NUL character instead of newline character Print output delimited by ASCII NUL characters instead of newline characters
.TP
.B "--no-clear"
Do not clear finder interface on exit. If fzf was started in full screen mode,
it will not switch back to the original screen, so you'll have to manually run
\fBtput rmcup\fR to return. This option can be used to avoid flickering of the
screen when your application needs to start fzf multiple times in order.
.TP .TP
.B "--sync" .B "--sync"
Synchronous search for multi-staged filtering. If specified, fzf will launch Synchronous search for multi-staged filtering. If specified, fzf will launch
@@ -333,6 +352,9 @@ ncurses finder only after the input stream is complete.
.RS .RS
e.g. \fBfzf --multi | fzf --sync\fR e.g. \fBfzf --multi | fzf --sync\fR
.RE .RE
.TP
.B "--version"
Display version information and exit
.SH ENVIRONMENT VARIABLES .SH ENVIRONMENT VARIABLES
.TP .TP
@@ -416,6 +438,8 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
.B AVAILABLE KEYS: (SYNONYMS) .B AVAILABLE KEYS: (SYNONYMS)
\fIctrl-[a-z]\fR \fIctrl-[a-z]\fR
\fIctrl-space\fR
\fIctrl-alt-[a-z]\fR
\fIalt-[a-z]\fR \fIalt-[a-z]\fR
\fIalt-[0-9]\fR \fIalt-[0-9]\fR
\fIf[1-12]\fR \fIf[1-12]\fR
@@ -459,7 +483,8 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\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)
\fBexecute-multi(...)\fR (see below for the details) \fBexecute-silent(...)\fR (see below for the details)
\fRexecute-multi(...)\fR (deprecated in favor of \fB{+}\fR expression)
\fBforward-char\fR \fIctrl-f right\fR \fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR \fBforward-word\fR \fIalt-f shift-right\fR
\fBignore\fR \fBignore\fR
@@ -470,6 +495,8 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR) \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
\fBpage-down\fR \fIpgdn\fR \fBpage-down\fR \fIpgdn\fR
\fBpage-up\fR \fIpgup\fR \fBpage-up\fR \fIpgup\fR
\fBhalf-page-down\fR
\fBhalf-page-up\fR
\fBpreview-down\fR \fBpreview-down\fR
\fBpreview-up\fR \fBpreview-up\fR
\fBpreview-page-down\fR \fBpreview-page-down\fR
@@ -479,17 +506,22 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\fBselect-all\fR \fBselect-all\fR
\fBtoggle\fR \fBtoggle\fR
\fBtoggle-all\fR \fBtoggle-all\fR
\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--reverse\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
\fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle-down\fR : \fBtoggle-up\fR) \fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
\fBtoggle-preview\fR \fBtoggle-preview\fR
\fBtoggle-sort\fR (equivalent to \fB--toggle-sort\fR) \fBtoggle-preview-wrap\fR
\fBtoggle-up\fR \fIbtab (shift-tab)\fR \fBtoggle-sort\fR
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
\fBunix-line-discard\fR \fIctrl-u\fR \fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR \fBunix-word-rubout\fR \fIctrl-w\fR
\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
Multiple actions can be chained using \fB+\fR separator.
\fBfzf --bind 'ctrl-a:select-all+accept'\fR
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.
@@ -522,10 +554,12 @@ the closing character. The catch is that it should be the last one in the
comma-separated list of key-action pairs. comma-separated list of key-action pairs.
.RE .RE
\fBexecute-multi(...)\fR is an alternative action that executes the command fzf switches to the alternate screen when executing a command. However, if the
with the selected entries when multi-select is enabled (\fB--multi\fR). With command is expected to complete quickly, and you are not interested in its
this action, \fB{}\fR is replaced with the quoted strings of the selected output, you might want to use \fBexecute-silent\fR instead, which silently
entries separated by spaces. executes the command without the switching. Note that fzf will not be
responsible until the command is complete. For asynchronous execution, start
your command as a background process (i.e. appending \fB&\fR).
.SH AUTHOR .SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR) Junegunn Choi (\fIjunegunn.c@gmail.com\fR)

View File

@@ -1,4 +1,4 @@
" Copyright (c) 2016 Junegunn Choi " Copyright (c) 2017 Junegunn Choi
" "
" MIT License " MIT License
" "
@@ -26,12 +26,60 @@ if exists('g:loaded_fzf')
endif endif
let g:loaded_fzf = 1 let g:loaded_fzf = 1
let s:is_win = has('win32') || has('win64')
if s:is_win && &shellslash
set noshellslash
let s:base_dir = expand('<sfile>:h:h')
set shellslash
else
let s:base_dir = expand('<sfile>:h:h')
endif
if s:is_win
function! s:fzf_call(fn, ...)
let shellslash = &shellslash
try
set noshellslash
return call(a:fn, a:000)
finally
let &shellslash = shellslash
endtry
endfunction
function! s:fzf_shellescape(path)
return substitute(s:fzf_call('shellescape', a:path), '[^\\]\zs\\"$', '\\\\"', '')
endfunction
else
function! s:fzf_call(fn, ...)
return call(a:fn, a:000)
endfunction
function! s:fzf_shellescape(path)
return shellescape(a:path)
endfunction
endif
function! s:fzf_getcwd()
return s:fzf_call('getcwd')
endfunction
function! s:fzf_fnamemodify(fname, mods)
return s:fzf_call('fnamemodify', a:fname, a:mods)
endfunction
function! s:fzf_expand(fmt)
return s:fzf_call('expand', a:fmt)
endfunction
function! s:fzf_tempname()
return s:fzf_call('tempname')
endfunction
let s:default_layout = { 'down': '~40%' } let s:default_layout = { 'down': '~40%' }
let s:layout_keys = ['window', 'up', 'down', 'left', 'right'] let s:layout_keys = ['window', 'up', 'down', 'left', 'right']
let s:fzf_go = expand('<sfile>:h:h').'/bin/fzf' let s:fzf_go = s:base_dir.'/bin/fzf'
let s:install = expand('<sfile>:h:h').'/install' let s:fzf_tmux = s:base_dir.'/bin/fzf-tmux'
let s:install = s:base_dir.'/install'
let s:installed = 0 let s:installed = 0
let s:fzf_tmux = expand('<sfile>:h:h').'/bin/fzf-tmux'
let s:cpo_save = &cpo let s:cpo_save = &cpo
set cpo&vim set cpo&vim
@@ -39,9 +87,14 @@ set cpo&vim
function! s:fzf_exec() function! s:fzf_exec()
if !exists('s:exec') if !exists('s:exec')
if executable(s:fzf_go) if executable(s:fzf_go)
let s:exec = s:fzf_go let s:exec = s:fzf_expand(s:fzf_go)
elseif executable('fzf') elseif executable('fzf')
let s:exec = 'fzf' let s:exec = 'fzf'
elseif s:is_win
call s:warn('fzf executable not found.')
call s:warn('Download fzf binary for Windows from https://github.com/junegunn/fzf-bin/releases/')
call s:warn('and place it as '.s:base_dir.'\bin\fzf.exe')
throw 'fzf executable not found'
elseif !s:installed && executable(s:install) && elseif !s:installed && executable(s:install) &&
\ input('fzf executable not found. Download binary? (y/n) ') =~? '^y' \ input('fzf executable not found. Download binary? (y/n) ') =~? '^y'
redraw redraw
@@ -55,7 +108,7 @@ function! s:fzf_exec()
throw 'fzf executable not found' throw 'fzf executable not found'
endif endif
endif endif
return s:shellesc(s:exec) return s:is_win ? s:exec : s:shellesc(s:exec)
endfunction endfunction
function! s:tmux_enabled() function! s:tmux_enabled()
@@ -126,7 +179,7 @@ function! s:has_any(dict, keys)
endfunction endfunction
function! s:open(cmd, target) function! s:open(cmd, target)
if stridx('edit', a:cmd) == 0 && fnamemodify(a:target, ':p') ==# expand('%:p') if stridx('edit', a:cmd) == 0 && s:fzf_fnamemodify(a:target, ':p') ==# s:fzf_expand('%:p')
return return
endif endif
execute a:cmd s:escape(a:target) execute a:cmd s:escape(a:target)
@@ -141,11 +194,11 @@ function! s:common_sink(action, lines) abort
if len(a:lines) > 1 if len(a:lines) > 1
augroup fzf_swap augroup fzf_swap
autocmd SwapExists * let v:swapchoice='o' autocmd SwapExists * let v:swapchoice='o'
\| call s:warn('fzf: E325: swap file exists: '.expand('<afile>')) \| call s:warn('fzf: E325: swap file exists: '.s:fzf_expand('<afile>'))
augroup END augroup END
endif endif
try try
let empty = empty(expand('%')) && line('$') == 1 && empty(getline(1)) && !&modified let empty = empty(s:fzf_expand('%')) && line('$') == 1 && empty(getline(1)) && !&modified
let autochdir = &autochdir let autochdir = &autochdir
set noautochdir set noautochdir
for item in a:lines for item in a:lines
@@ -167,9 +220,12 @@ 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 fam = gui ? 'gui' : 'cterm'
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
for group in a:000 for group in a:000
let code = synIDattr(synIDtrans(hlID(group)), a:attr, 'cterm') let code = synIDattr(synIDtrans(hlID(group)), a:attr, fam)
if code =~ '^[0-9]\+$' if code =~? pat
return code return code
endif endif
endfor endfor
@@ -182,6 +238,21 @@ function! s:defaults()
return empty(colors) ? '' : ('--color='.colors) return empty(colors) ? '' : ('--color='.colors)
endfunction endfunction
function! s:validate_layout(layout)
for key in keys(a:layout)
if index(s:layout_keys, key) < 0
throw printf('Invalid entry in g:fzf_layout: %s (allowed: %s)%s',
\ key, join(s:layout_keys, ', '), key == 'options' ? '. Use $FZF_DEFAULT_OPTS.' : '')
endif
endfor
return a:layout
endfunction
function! s:evaluate_opts(options)
return type(a:options) == type([]) ?
\ join(map(copy(a:options), 's:fzf_shellescape(v:val)')) : a:options
endfunction
" [name string,] [opts dict,] [fullscreen boolean] " [name string,] [opts dict,] [fullscreen boolean]
function! fzf#wrap(...) function! fzf#wrap(...)
let args = ['', {}, 0] let args = ['', {}, 0]
@@ -190,7 +261,7 @@ function! fzf#wrap(...)
for arg in copy(a:000) for arg in copy(a:000)
let tidx = index(expects, type(arg), tidx) let tidx = index(expects, type(arg), tidx)
if tidx < 0 if tidx < 0
throw 'invalid arguments (expected: [name string] [opts dict] [fullscreen boolean])' throw 'Invalid arguments (expected: [name string] [opts dict] [fullscreen boolean])'
endif endif
let args[tidx] = arg let args[tidx] = arg
let tidx += 1 let tidx += 1
@@ -198,6 +269,10 @@ function! fzf#wrap(...)
endfor endfor
let [name, opts, bang] = args let [name, opts, bang] = args
if len(name)
let opts.name = name
end
" Layout: g:fzf_layout (and deprecated g:fzf_height) " Layout: g:fzf_layout (and deprecated g:fzf_height)
if bang if bang
for key in s:layout_keys for key in s:layout_keys
@@ -209,20 +284,21 @@ function! fzf#wrap(...)
if !exists('g:fzf_layout') && exists('g:fzf_height') if !exists('g:fzf_layout') && exists('g:fzf_height')
let opts.down = g:fzf_height let opts.down = g:fzf_height
else else
let opts = extend(opts, get(g:, 'fzf_layout', s:default_layout)) let opts = extend(opts, s:validate_layout(get(g:, 'fzf_layout', s:default_layout)))
endif endif
endif endif
" Colors: g:fzf_colors " Colors: g:fzf_colors
let opts.options = s:defaults() .' '. get(opts, 'options', '') let opts.options = s:defaults() .' '. s:evaluate_opts(get(opts, 'options', ''))
" History: g:fzf_history_dir " History: g:fzf_history_dir
if len(name) && len(get(g:, 'fzf_history_dir', '')) if len(name) && len(get(g:, 'fzf_history_dir', ''))
let dir = expand(g:fzf_history_dir) let dir = s:fzf_expand(g:fzf_history_dir)
if !isdirectory(dir) if !isdirectory(dir)
call mkdir(dir, 'p') call mkdir(dir, 'p')
endif endif
let opts.options = join(['--history', s:escape(dir.'/'.name), opts.options]) let history = s:is_win ? s:fzf_shellescape(dir.'\'.name) : s:escape(dir.'/'.name)
let opts.options = join(['--history', history, opts.options])
endif endif
" Action: g:fzf_action " Action: g:fzf_action
@@ -238,48 +314,42 @@ function! fzf#wrap(...)
return opts return opts
endfunction endfunction
function! fzf#shellescape(path)
if has('win32') || has('win64')
let shellslash = &shellslash
try
set noshellslash
return shellescape(a:path)
finally
let &shellslash = shellslash
endtry
endif
return shellescape(a:path)
endfunction
function! fzf#run(...) abort function! fzf#run(...) abort
try try
let oshell = &shell let oshell = &shell
let useshellslash = &shellslash let useshellslash = &shellslash
if has('win32') || has('win64') if s:is_win
set shell=cmd.exe set shell=cmd.exe
set noshellslash set noshellslash
else else
set shell=sh set shell=sh
endif endif
if has('nvim') && len(filter(range(1, bufnr('$')), 'bufname(v:val) =~# ";#FZF"')) if has('nvim')
call s:warn('FZF is already running!') let running = filter(range(1, bufnr('$')), "bufname(v:val) =~# ';#FZF'")
return [] if len(running)
call s:warn('FZF is already running (in buffer '.join(running, ', ').')!')
return []
endif
endif endif
let dict = exists('a:1') ? s:upgrade(a:1) : {} let dict = exists('a:1') ? s:upgrade(a:1) : {}
let temps = { 'result': tempname() } let temps = { 'result': s:fzf_tempname() }
let optstr = get(dict, 'options', '') let optstr = s:evaluate_opts(get(dict, 'options', ''))
try try
let fzf_exec = s:fzf_exec() let fzf_exec = s:fzf_exec()
catch catch
throw v:exception throw v:exception
endtry endtry
if has('nvim') && !has_key(dict, 'dir')
let dict.dir = s:fzf_getcwd()
endif
if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND)
let temps.source = tempname() let temps.source = s:fzf_tempname().(s:is_win ? '.bat' : '')
call writefile(split($FZF_DEFAULT_COMMAND, "\n"), temps.source) call writefile((s:is_win ? ['@echo off'] : []) + split($FZF_DEFAULT_COMMAND, "\n"), temps.source)
let dict.source = (empty($SHELL) ? &shell : $SHELL) . ' ' . s:shellesc(temps.source) let dict.source = (empty($SHELL) ? &shell : $SHELL) . (s:is_win ? ' /c ' : ' ') . s:shellesc(temps.source)
endif endif
if has_key(dict, 'source') if has_key(dict, 'source')
@@ -288,33 +358,39 @@ try
if type == 1 if type == 1
let prefix = source.'|' let prefix = source.'|'
elseif type == 3 elseif type == 3
let temps.input = tempname() let temps.input = s:fzf_tempname()
call writefile(source, temps.input) call writefile(source, temps.input)
let prefix = 'cat '.s:shellesc(temps.input).'|' let prefix = (s:is_win ? 'type ' : 'cat ').s:shellesc(temps.input).'|'
else else
throw 'invalid source type' throw 'Invalid source type'
endif endif
else else
let prefix = '' let prefix = ''
endif endif
let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0)
let use_height = has_key(dict, 'down') && let use_height = has_key(dict, 'down') &&
\ !(has('nvim') || has('win32') || has('win64') || s:present(dict, 'up', 'left', 'right')) && \ !(has('nvim') || s:is_win || s:present(dict, 'up', 'left', 'right')) &&
\ executable('tput') && filereadable('/dev/tty') \ executable('tput') && filereadable('/dev/tty')
let tmux = !use_height && (!has('nvim') || get(g:, 'fzf_prefer_tmux', 0)) && s:tmux_enabled() && s:splittable(dict) let use_term = has('nvim') && !s:is_win
let term = has('nvim') && !tmux let use_tmux = (!use_height && !use_term || prefer_tmux) && s:tmux_enabled() && s:splittable(dict)
if prefer_tmux && use_tmux
let use_height = 0
let use_term = 0
endif
if use_height if use_height
let optstr .= ' --height='.s:calc_size(&lines, dict.down, dict) let height = s:calc_size(&lines, dict.down, dict)
elseif term let optstr .= ' --height='.height
elseif use_term
let optstr .= ' --no-height' let optstr .= ' --no-height'
endif endif
let command = prefix.(tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
if term if use_term
return s:execute_term(dict, command, temps) return s:execute_term(dict, command, temps)
endif endif
let lines = tmux ? s:execute_tmux(dict, command, temps) let lines = use_tmux ? s:execute_tmux(dict, command, temps)
\ : s:execute(dict, command, use_height, temps) \ : s:execute(dict, command, use_height, temps)
call s:callback(dict, lines) call s:callback(dict, lines)
return lines return lines
@@ -358,13 +434,13 @@ endfunction
function! s:pushd(dict) function! s:pushd(dict)
if s:present(a:dict, 'dir') if s:present(a:dict, 'dir')
let cwd = getcwd() let cwd = s:fzf_getcwd()
if get(a:dict, 'prev_dir', '') ==# cwd if get(a:dict, 'prev_dir', '') ==# cwd
return 1 return 1
endif endif
let a:dict.prev_dir = cwd let a:dict.prev_dir = cwd
execute 'lcd' s:escape(a:dict.dir) execute 'lcd' s:escape(a:dict.dir)
let a:dict.dir = getcwd() let a:dict.dir = s:fzf_getcwd()
return 1 return 1
endif endif
return 0 return 0
@@ -393,7 +469,7 @@ function! s:xterm_launcher()
\ &columns, &lines/2, getwinposx(), getwinposy()) \ &columns, &lines/2, getwinposx(), getwinposy())
endfunction endfunction
unlet! s:launcher unlet! s:launcher
if has('win32') || has('win64') if s:is_win
let s:launcher = '%s' let s:launcher = '%s'
else else
let s:launcher = function('s:xterm_launcher') let s:launcher = function('s:xterm_launcher')
@@ -417,7 +493,7 @@ function! s:execute(dict, command, use_height, temps) abort
if has('unix') && !a:use_height if has('unix') && !a:use_height
silent! !clear 2> /dev/null silent! !clear 2> /dev/null
endif endif
let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#') let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#!')
if has('gui_running') if has('gui_running')
let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher))) let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher)))
let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher
@@ -426,7 +502,24 @@ function! s:execute(dict, command, use_height, temps) abort
endif endif
let command = printf(fmt, escaped) let command = printf(fmt, escaped)
else else
let command = escaped let command = a:use_height ? a:command : escaped
endif
if s:is_win
let batchfile = s:fzf_tempname().'.bat'
call writefile(['@echo off', command], batchfile)
let command = batchfile
if has('nvim')
let s:dict = a:dict
let s:temps = a:temps
let fzf = {}
function! fzf.on_exit(job_id, exit_status, event) dict
let lines = s:collect(s:temps)
call s:callback(s:dict, lines)
endfunction
let cmd = 'start /wait cmd /c '.command
call jobstart(cmd, fzf)
return []
endif
endif endif
if a:use_height if a:use_height
let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty' let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty'
@@ -513,6 +606,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)
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,
\ 'columns': &columns, 'command': a:command } \ 'columns': &columns, 'command': a:command }
@@ -638,19 +732,24 @@ let s:default_action = {
function! s:shortpath() function! s:shortpath()
let short = pathshorten(fnamemodify(getcwd(), ':~:.')) let short = pathshorten(fnamemodify(getcwd(), ':~:.'))
return empty(short) ? '~/' : short . (short =~ '/$' ? '' : '/') let slash = (s:is_win && !&shellslash) ? '\' : '/'
return empty(short) ? '~'.slash : short . (short =~ slash.'$' ? '' : slash)
endfunction endfunction
function! s:cmd(bang, ...) abort function! s:cmd(bang, ...) abort
let args = copy(a:000) let args = copy(a:000)
let opts = { 'options': '--multi ' } let opts = { 'options': ['--multi'] }
if len(args) && isdirectory(expand(args[-1])) if len(args) && isdirectory(expand(args[-1]))
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '') let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
let opts.options .= ' --prompt '.fzf#shellescape(opts.dir) if s:is_win && !&shellslash
let opts.dir = substitute(opts.dir, '/', '\\', 'g')
endif
let prompt = opts.dir
else else
let opts.options .= ' --prompt '.fzf#shellescape(s:shortpath()) let prompt = s:shortpath()
endif endif
let opts.options .= ' '.join(args) call extend(opts.options, ['--prompt', prompt])
call extend(opts.options, args)
call fzf#run(fzf#wrap('FZF', opts, a:bang)) call fzf#run(fzf#wrap('FZF', opts, a:bang))
endfunction endfunction

View File

@@ -234,7 +234,7 @@ _fzf_complete_telnet() {
_fzf_complete_ssh() { _fzf_complete_ssh() {
_fzf_complete '+m' "$@" < <( _fzf_complete '+m' "$@" < <(
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \ cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | 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' | 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
) )
@@ -304,7 +304,7 @@ done
# Directory # Directory
for cmd in $d_cmds; do for cmd in $d_cmds; do
_fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o plusdirs" _fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames"
done done
unset _fzf_defc unset _fzf_defc

View File

@@ -117,7 +117,7 @@ _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 '*') \ command cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | 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' | 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
) )
@@ -143,7 +143,7 @@ _fzf_complete_unalias() {
fzf-completion() { fzf-completion() {
local tokens cmd prefix trigger tail fzf matches lbuf d_cmds local tokens cmd prefix trigger tail fzf matches lbuf d_cmds
setopt localoptions noshwordsplit noksh_arrays setopt localoptions noshwordsplit noksh_arrays noposixbuiltins
# http://zsh.sourceforge.net/FAQ/zshfaq03.html # http://zsh.sourceforge.net/FAQ/zshfaq03.html
# http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags

View File

@@ -1,7 +1,7 @@
# Key bindings # Key bindings
# ------------ # ------------
__fzf_select__() { __fzf_select__() {
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ local cmd="${FZF_CTRL_T_COMMAND:-"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 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-"}"
@@ -46,8 +46,8 @@ fzf-file-widget() {
__fzf_cd__() { __fzf_cd__() {
local cmd dir local cmd dir
cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ 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 | sed 1d | cut -b3-"}" -o -type d -print 2> /dev/null | cut -b3-"}"
dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m) && printf 'cd %q' "$dir" dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m) && printf 'cd %q' "$dir"
} }
@@ -56,7 +56,7 @@ __fzf_history__() (
shopt -u nocaseglob nocasematch shopt -u nocaseglob nocasematch
line=$( line=$(
HISTTIMEFORMAT= history | HISTTIMEFORMAT= history |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS +s --tac --no-reverse -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m" $(__fzfcmd) | 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) |
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"

View File

@@ -2,7 +2,7 @@
# ------------ # ------------
function fzf_key_bindings function fzf_key_bindings
# Store last token in $dir as root for the 'find' command # Store current token in $dir as root for the 'find' command
function fzf-file-widget -d "List files and folders" function fzf-file-widget -d "List files and folders"
set -l dir (commandline -t) set -l dir (commandline -t)
# The commandline token might be escaped, we need to unescape it. # The commandline token might be escaped, we need to unescape it.
@@ -16,10 +16,10 @@ function fzf_key_bindings
# "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not # "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not
# $dir itself, even if hidden. # $dir itself, even if hidden.
set -q FZF_CTRL_T_COMMAND; or set -l FZF_CTRL_T_COMMAND " set -q FZF_CTRL_T_COMMAND; or set -l FZF_CTRL_T_COMMAND "
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | sed 's#^\./##'" -o -type l -print 2> /dev/null | cut -b3-"
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
begin begin
@@ -45,7 +45,7 @@ function fzf_key_bindings
function fzf-history-widget -d "Show command history" function fzf-history-widget -d "Show command history"
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS +s --no-reverse --tiebreak=index $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"
history | eval (__fzfcmd) -q '(commandline)' | read -l result history | eval (__fzfcmd) -q '(commandline)' | read -l result
and commandline -- $result and commandline -- $result
end end
@@ -54,8 +54,8 @@ function fzf_key_bindings
function fzf-cd-widget -d "Change directory" function fzf-cd-widget -d "Change directory"
set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND " set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND "
command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3-" -o -type d -print 2> /dev/null | cut -b3-"
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS"

View File

@@ -4,7 +4,7 @@ if [[ $- == *i* ]]; then
# CTRL-T - Paste the selected file path(s) into the command line # CTRL-T - Paste the selected file path(s) into the command line
__fsel() { __fsel() {
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ local cmd="${FZF_CTRL_T_COMMAND:-"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 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-"}"
@@ -38,10 +38,15 @@ bindkey '^T' fzf-file-widget
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
fzf-cd-widget() { fzf-cd-widget() {
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -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 | sed 1d | cut -b3-"}" -o -type d -print 2> /dev/null | cut -b3-"}"
setopt localoptions pipefail 2> /dev/null setopt localoptions pipefail 2> /dev/null
cd "${$(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
zle redisplay
return 0
fi
cd "$dir"
local ret=$? local ret=$?
zle reset-prompt zle reset-prompt
typeset -f zle-line-init >/dev/null && zle zle-line-init typeset -f zle-line-init >/dev/null && zle zle-line-init
@@ -53,9 +58,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 pipefail 2> /dev/null setopt localoptions noglobsubst noposixbuiltins pipefail 2> /dev/null
selected=( $(fc -l 1 | selected=( $(fc -l 1 |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS +s --tac --no-reverse -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m --query=${(q)LBUFFER}" $(__fzfcmd)) ) 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)) )
local ret=$? local ret=$?
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
num=$selected[1] num=$selected[1]
@@ -71,4 +76,3 @@ zle -N fzf-history-widget
bindkey '^R' fzf-history-widget bindkey '^R' fzf-history-widget
fi fi

View File

@@ -1,40 +0,0 @@
FROM ubuntu:14.04
MAINTAINER Junegunn Choi <junegunn.c@gmail.com>
# apt-get
RUN apt-get update && apt-get -y upgrade && \
apt-get install -y --force-yes git curl build-essential
# Install Go 1.4
RUN cd / && curl \
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
tar -xz && mv go go1.4 && \
sed -i 's@#define PTHREAD_KEYS_MAX 128@@' /go1.4/src/runtime/cgo/gcc_android_arm.c
ENV GOROOT /go1.4
ENV PATH /go1.4/bin:$PATH
RUN cd / && \
curl -O http://dl.google.com/android/ndk/android-ndk-r10e-linux-x86_64.bin && \
chmod 755 /android-ndk* && /android-ndk-r10e-linux-x86_64.bin && \
mv android-ndk-r10e /android-ndk
RUN cd /android-ndk && bash ./build/tools/make-standalone-toolchain.sh --platform=android-21 --install-dir=/ndk --arch=arm
ENV NDK_CC /ndk/bin/arm-linux-androideabi-gcc
RUN cd $GOROOT/src && \
CC_FOR_TARGET=$NDK_CC GOOS=android GOARCH=arm GOARM=7 ./make.bash
RUN cd / && curl \
http://ftp.gnu.org/gnu/ncurses/ncurses-5.9.tar.gz | \
tar -xz && cd /ncurses-5.9 && \
./configure CC=$NDK_CC CFLAGS="-fPIE -march=armv7-a -mfpu=neon -mhard-float -Wl,--no-warn-mismatch" LDFLAGS="-march=armv7-a -Wl,--no-warn-mismatch" --host=arm-linux --enable-overwrite --enable-const --without-cxx-binding --without-shared --without-debug --enable-widec --enable-ext-colors --enable-ext-mouse --enable-pc-files --with-pkg-config-libdir=$PKG_CONFIG_LIBDIR --without-manpages --without-ada --disable-shared --without-tests --prefix=/ndk/sysroot/usr --with-default-terminfo-dirs=/usr/share/terminfo --with-terminfo-dirs=/usr/share/terminfo ac_cv_header_locale_h=n ac_cv_func_getpwent=no ac_cv_func_getpwnam=no ac_cv_func_getpwuid=no && \
sed -i 's@#define HAVE_LOCALE_H 1@/* #undef HAVE_LOCALE_H */@' include/ncurses_cfg.h && \
make && \
sed -i '0,/echo.*/{s/echo.*/exit 0/}' misc/run_tic.sh && \
make install && \
mv /ndk/sysroot/usr/lib/libncursesw.a /ndk/sysroot/usr/lib/libncurses.a
# Default CMD
CMD cd /fzf/src && /bin/bash

View File

@@ -1,24 +0,0 @@
FROM base/archlinux:2014.07.03
MAINTAINER Junegunn Choi <junegunn.c@gmail.com>
# apt-get
RUN pacman-key --populate archlinux && pacman-key --refresh-keys
RUN pacman-db-upgrade && pacman -Syu --noconfirm base-devel git
# Install Go 1.4
RUN cd / && curl \
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
tar -xz && mv go go1.4
ENV GOROOT /go1.4
ENV PATH /go1.4/bin:$PATH
# For i386 build
RUN echo '[multilib]' >> /etc/pacman.conf && \
echo 'Include = /etc/pacman.d/mirrorlist' >> /etc/pacman.conf && \
pacman-db-upgrade && yes | pacman -Sy gcc-multilib lib32-ncurses && \
cd $GOROOT/src && GOARCH=386 ./make.bash
# Default CMD
CMD cd /fzf/src && /bin/bash

View File

@@ -1,32 +0,0 @@
FROM centos:centos6
MAINTAINER Junegunn Choi <junegunn.c@gmail.com>
# yum
RUN yum install -y git gcc make tar glibc-devel glibc-devel.i686 \
ncurses-devel ncurses-static ncurses-devel.i686 \
gpm-devel gpm-static libgcc.i686
# Install Go 1.4
RUN cd / && curl \
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
tar -xz && mv go go1.4
# Install Go 1.7
RUN cd / && curl \
https://storage.googleapis.com/golang/go1.7.linux-amd64.tar.gz | \
tar -xz && mv go go1.7
# Install RPMs for building static 32-bit binary
RUN curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.8/os/i386/Packages/ncurses-static-5.7-4.20090207.el6.i686.rpm -o rpm && rpm -i rpm && \
curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.8/os/i386/Packages/gpm-static-1.20.6-12.el6.i686.rpm -o rpm && rpm -i rpm
ENV GOROOT_BOOTSTRAP /go1.4
ENV GOROOT /go1.7
ENV PATH /go1.7/bin:$PATH
# For i386 build
RUN cd $GOROOT/src && GOARCH=386 ./make.bash
# Default CMD
CMD cd /fzf/src && /bin/bash

View File

@@ -1,22 +0,0 @@
FROM ubuntu:14.04
MAINTAINER Junegunn Choi <junegunn.c@gmail.com>
# apt-get
RUN apt-get update && apt-get -y upgrade && \
apt-get install -y --force-yes git curl build-essential libncurses-dev libgpm-dev
# Install Go 1.4
RUN cd / && curl \
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
tar -xz && mv go go1.4
ENV GOROOT /go1.4
ENV PATH /go1.4/bin:$PATH
# For i386 build
RUN apt-get install -y lib32ncurses5-dev && \
cd $GOROOT/src && GOARCH=386 ./make.bash
# Default CMD
CMD cd /fzf/src && /bin/bash

View File

@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2016 Junegunn Choi Copyright (c) 2017 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

View File

@@ -4,6 +4,8 @@ ifeq ($(UNAME_S),Darwin)
GOOS := darwin GOOS := darwin
else ifeq ($(UNAME_S),Linux) else ifeq ($(UNAME_S),Linux)
GOOS := linux GOOS := linux
else
$(error "$$GOOS is not defined.")
endif endif
endif endif
@@ -12,21 +14,37 @@ ROOTDIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
BINDIR := $(shell dirname $(ROOTDIR))/bin BINDIR := $(shell dirname $(ROOTDIR))/bin
GOPATH := $(shell dirname $(ROOTDIR))/gopath GOPATH := $(shell dirname $(ROOTDIR))/gopath
SRCDIR := $(GOPATH)/src/github.com/junegunn/fzf/src SRCDIR := $(GOPATH)/src/github.com/junegunn/fzf/src
DOCKEROPTS := -i -t -v $(ROOTDIR):/fzf/src
BINARY32 := fzf-$(GOOS)_386 BINARY32 := fzf-$(GOOS)_386
BINARY64 := fzf-$(GOOS)_amd64 BINARY64 := fzf-$(GOOS)_amd64
BINARYARM5 := fzf-$(GOOS)_arm5
BINARYARM6 := fzf-$(GOOS)_arm6
BINARYARM7 := fzf-$(GOOS)_arm7 BINARYARM7 := fzf-$(GOOS)_arm7
BINARYARM8 := fzf-$(GOOS)_arm8
VERSION := $(shell awk -F= '/version =/ {print $$2}' constants.go | tr -d "\" ") VERSION := $(shell awk -F= '/version =/ {print $$2}' constants.go | tr -d "\" ")
RELEASE32 := fzf-$(VERSION)-$(GOOS)_386 RELEASE32 := fzf-$(VERSION)-$(GOOS)_386
RELEASE64 := fzf-$(VERSION)-$(GOOS)_amd64 RELEASE64 := fzf-$(VERSION)-$(GOOS)_amd64
RELEASEARM5 := fzf-$(VERSION)-$(GOOS)_arm5
RELEASEARM6 := fzf-$(VERSION)-$(GOOS)_arm6
RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7 RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7
RELEASEARM8 := fzf-$(VERSION)-$(GOOS)_arm8
export GOPATH export GOPATH
# https://en.wikipedia.org/wiki/Uname
UNAME_M := $(shell uname -m) UNAME_M := $(shell uname -m)
ifeq ($(UNAME_M),x86_64) ifeq ($(UNAME_M),x86_64)
BINARY := $(BINARY64) BINARY := $(BINARY64)
else ifeq ($(UNAME_M),amd64)
BINARY := $(BINARY64)
else ifeq ($(UNAME_M),i686) else ifeq ($(UNAME_M),i686)
BINARY := $(BINARY32) BINARY := $(BINARY32)
else ifeq ($(UNAME_M),i386)
BINARY := $(BINARY32)
else ifeq ($(UNAME_M),armv5l)
BINARY := $(BINARYARM5)
else ifeq ($(UNAME_M),armv6l)
BINARY := $(BINARYARM6)
else ifeq ($(UNAME_M),armv7l)
BINARY := $(BINARYARM7)
else else
$(error "Build on $(UNAME_M) is not supported, yet.") $(error "Build on $(UNAME_M) is not supported, yet.")
endif endif
@@ -35,31 +53,42 @@ all: fzf/$(BINARY)
ifeq ($(GOOS),windows) ifeq ($(GOOS),windows)
release: fzf/$(BINARY32) fzf/$(BINARY64) release: fzf/$(BINARY32) fzf/$(BINARY64)
-cd fzf && cp $(BINARY32) $(RELEASE32).exe && zip $(RELEASE32).zip $(RELEASE32).exe cd fzf && cp -f $(BINARY32) fzf.exe && zip $(RELEASE32).zip fzf.exe
cd fzf && cp $(BINARY64) $(RELEASE64).exe && zip $(RELEASE64).zip $(RELEASE64).exe && \ cd fzf && cp -f $(BINARY64) fzf.exe && zip $(RELEASE64).zip fzf.exe
rm -f $(RELEASE32).exe $(RELEASE64).exe cd fzf && rm -f fzf.exe
else ifeq ($(GOOS),linux)
release: fzf/$(BINARY32) fzf/$(BINARY64) fzf/$(BINARYARM5) fzf/$(BINARYARM6) fzf/$(BINARYARM7) fzf/$(BINARYARM8)
cd fzf && cp -f $(BINARY32) fzf && tar -czf $(RELEASE32).tgz fzf
cd fzf && cp -f $(BINARY64) fzf && tar -czf $(RELEASE64).tgz fzf
cd fzf && cp -f $(BINARYARM5) fzf && tar -czf $(RELEASEARM5).tgz fzf
cd fzf && cp -f $(BINARYARM6) fzf && tar -czf $(RELEASEARM6).tgz fzf
cd fzf && cp -f $(BINARYARM7) fzf && tar -czf $(RELEASEARM7).tgz fzf
cd fzf && cp -f $(BINARYARM8) fzf && tar -czf $(RELEASEARM8).tgz fzf
cd fzf && rm -f fzf
else else
release: test fzf/$(BINARY32) fzf/$(BINARY64) release: fzf/$(BINARY32) fzf/$(BINARY64)
-cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) cd fzf && cp -f $(BINARY32) fzf && tar -czf $(RELEASE32).tgz fzf
cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \ cd fzf && cp -f $(BINARY64) fzf && tar -czf $(RELEASE64).tgz fzf
rm -f $(RELEASE32) $(RELEASE64) cd fzf && rm -f fzf
endif endif
release-all: clean test
GOOS=darwin make release
GOOS=linux make release
GOOS=freebsd make release
GOOS=openbsd make release
GOOS=windows make release
$(SRCDIR): $(SRCDIR):
mkdir -p $(shell dirname $(SRCDIR)) mkdir -p $(shell dirname $(SRCDIR))
ln -s $(ROOTDIR) $(SRCDIR) ln -s $(ROOTDIR) $(SRCDIR)
deps: $(SRCDIR) $(SOURCES) deps: $(SRCDIR) $(SOURCES)
cd $(SRCDIR) && go get -tags "$(TAGS)" cd $(SRCDIR) && go get -tags "$(TAGS)"
./deps
android-build: $(SRCDIR)
cd $(SRCDIR) && GOARCH=arm GOARM=7 CGO_ENABLED=1 go get
cd $(SRCDIR)/fzf && GOARCH=arm GOARM=7 CGO_ENABLED=1 go build -a -ldflags="-w -extldflags=-pie" -o $(BINARYARM7)
cd $(SRCDIR)/fzf && cp $(BINARYARM7) $(RELEASEARM7) && tar -czf $(RELEASEARM7).tgz $(RELEASEARM7) && \
rm -f $(RELEASEARM7)
test: deps test: deps
SHELL=/bin/sh GOOS=$(GOOS) go test -v -tags "$(TAGS)" ./... SHELL=/bin/sh GOOS= go test -v -tags "$(TAGS)" ./...
install: $(BINDIR)/fzf install: $(BINDIR)/fzf
@@ -70,10 +99,23 @@ clean:
cd fzf && rm -f fzf-* cd fzf && rm -f fzf-*
fzf/$(BINARY32): deps fzf/$(BINARY32): deps
cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY32) cd fzf && GOARCH=386 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY32)
fzf/$(BINARY64): deps fzf/$(BINARY64): deps
cd fzf && go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY64) cd fzf && GOARCH=amd64 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY64)
# https://github.com/golang/go/wiki/GoArm
fzf/$(BINARYARM5): deps
cd fzf && GOARCH=arm GOARM=5 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARYARM5)
fzf/$(BINARYARM6): deps
cd fzf && GOARCH=arm GOARM=6 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARYARM6)
fzf/$(BINARYARM7): deps
cd fzf && GOARCH=arm GOARM=7 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARYARM7)
fzf/$(BINARYARM8): deps
cd fzf && GOARCH=arm64 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARYARM8)
$(BINDIR)/fzf: fzf/$(BINARY) | $(BINDIR) $(BINDIR)/fzf: fzf/$(BINARY) | $(BINDIR)
cp -f fzf/$(BINARY) $(BINDIR) cp -f fzf/$(BINARY) $(BINDIR)
@@ -82,42 +124,4 @@ $(BINDIR)/fzf: fzf/$(BINARY) | $(BINDIR)
$(BINDIR): $(BINDIR):
mkdir -p $@ mkdir -p $@
docker-arch: .PHONY: all deps release release-all test install uninstall clean
docker build -t junegunn/arch-sandbox - < Dockerfile.arch
docker-ubuntu:
docker build -t junegunn/ubuntu-sandbox - < Dockerfile.ubuntu
docker-centos:
docker build -t junegunn/centos-sandbox - < Dockerfile.centos
docker-android:
docker build -t junegunn/android-sandbox - < Dockerfile.android
arch: docker-arch
docker run $(DOCKEROPTS) junegunn/$@-sandbox \
sh -c 'cd /fzf/src; /bin/bash'
ubuntu: docker-ubuntu
docker run $(DOCKEROPTS) junegunn/$@-sandbox \
sh -c 'cd /fzf/src; /bin/bash'
centos: docker-centos
docker run $(DOCKEROPTS) junegunn/$@-sandbox \
sh -c 'cd /fzf/src; /bin/bash'
linux: docker-centos
docker run $(DOCKEROPTS) junegunn/centos-sandbox \
/bin/bash -ci 'cd /fzf/src; make TAGS=static release'
ubuntu-android: docker-android
docker run $(DOCKEROPTS) junegunn/android-sandbox \
sh -c 'cd /fzf/src; /bin/bash'
android: docker-android
docker run $(DOCKEROPTS) junegunn/android-sandbox \
/bin/bash -ci 'cd /fzf/src; GOOS=android make android-build'
.PHONY: all deps release test install uninstall clean \
linux arch ubuntu centos docker-arch docker-ubuntu docker-centos \
android-build docker-android ubuntu-android android

View File

@@ -59,20 +59,31 @@ Unit tests can be run with `make test`. Integration tests are written in Ruby
script that should be run on tmux. script that should be run on tmux.
```sh ```sh
cd src
# Unit tests # Unit tests
make test make test
# Integration tests
ruby ../test/test_go.rb
# Build binary for the platform
make
# Install the executable to ../bin directory # Install the executable to ../bin directory
make install make install
# Integration tests # Make release archives
ruby ../test/test_go.rb make release
# Make release archives for all supported platforms
make release-all
``` ```
Third-party libraries used Third-party libraries used
-------------------------- --------------------------
- [ncurses][ncurses] - ~[ncurses][ncurses]~
- [mattn/go-runewidth](https://github.com/mattn/go-runewidth) - [mattn/go-runewidth](https://github.com/mattn/go-runewidth)
- Licensed under [MIT](http://mattn.mit-license.org) - Licensed under [MIT](http://mattn.mit-license.org)
- [mattn/go-shellwords](https://github.com/mattn/go-shellwords) - [mattn/go-shellwords](https://github.com/mattn/go-shellwords)

View File

@@ -44,7 +44,7 @@ func init() {
*/ */
// The following regular expression will include not all but most of the // The following regular expression will include not all but most of the
// frequently used ANSI sequences // frequently used ANSI sequences
ansiRegex = regexp.MustCompile("\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b.|[\x08\x0e\x0f]") ansiRegex = regexp.MustCompile("\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b.|[\x0e\x0f]|.\x08")
} }
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) { func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {

View File

@@ -8,7 +8,7 @@ import (
const ( const (
// Current version // Current version
version = "0.16.0" version = "0.16.7"
// Core // Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond coordinatorDelayMax time.Duration = 100 * time.Millisecond
@@ -18,10 +18,9 @@ const (
readerBufferSize = 64 * 1024 readerBufferSize = 64 * 1024
// Terminal // Terminal
initialDelay = 20 * time.Millisecond initialDelay = 20 * time.Millisecond
initialDelayTac = 100 * time.Millisecond initialDelayTac = 100 * time.Millisecond
spinnerDuration = 200 * time.Millisecond spinnerDuration = 200 * time.Millisecond
maxPatternLength = 100
// Matcher // Matcher
numPartitionsMultiplier = 8 numPartitionsMultiplier = 8

View File

@@ -4,5 +4,5 @@ package fzf
const ( const (
// Reader // Reader
defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//` 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-`
) )

View File

@@ -3,7 +3,7 @@ Package fzf implements fzf, a command-line fuzzy finder.
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2016 Junegunn Choi Copyright (c) 2017 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

18
src/deps Executable file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
if [ -z "$GOPATH" ]; then
echo '$GOPATH not defined'
exit 1
fi
reset() (
cd "$GOPATH/src/$1"
export GIT_DIR="$(pwd)/.git"
[ "$(git rev-parse HEAD)" = "$2" ] ||
(git fetch && git reset --hard "$2")
)
reset github.com/junegunn/go-isatty 66b8e73f3f5cda9f96b69efd03dd3d7fc4a5cdb8
reset github.com/junegunn/go-runewidth 14207d285c6c197daabb5c9793d63e7af9ab2d50
reset github.com/junegunn/go-shellwords 02e3cf038dcea8290e44424da473dd12be796a8a
reset golang.org/x/crypto abc5fa7ad02123a41f02bf1391c9760f7586e608

View File

@@ -54,6 +54,7 @@ const usage = `usage: fzf [options]
--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 --reverse Reverse orientation
--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 --inline-info Display finder info inline with the query
--prompt=STR Input prompt (default: '> ') --prompt=STR Input prompt (default: '> ')
@@ -82,7 +83,10 @@ const usage = `usage: fzf [options]
-f, --filter=STR Filter mode. Do not start interactive finder. -f, --filter=STR Filter mode. Do not start interactive finder.
--print-query Print query as the first line --print-query Print query as the first line
--expect=KEYS Comma-separated list of keys to complete fzf --expect=KEYS Comma-separated list of keys to complete fzf
--read0 Read input delimited by ASCII NUL characters
--print0 Print output delimited by ASCII NUL characters
--sync Synchronous search for multi-staged filtering --sync Synchronous search for multi-staged filtering
--version Display version information and exit
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
@@ -171,8 +175,7 @@ type Options struct {
Filter *string Filter *string
ToggleSort bool ToggleSort bool
Expect map[int]string Expect map[int]string
Keymap map[int]actionType Keymap map[int][]action
Execmap map[int]string
Preview previewOpts Preview previewOpts
PrintQuery bool PrintQuery bool
ReadZero bool ReadZero bool
@@ -182,7 +185,9 @@ type Options struct {
Header []string Header []string
HeaderLines int HeaderLines int
Margin [4]sizeSpec Margin [4]sizeSpec
Bordered bool
Tabstop int Tabstop int
ClearOnExit bool
Version bool Version bool
} }
@@ -220,8 +225,7 @@ func defaultOptions() *Options {
Filter: nil, Filter: nil,
ToggleSort: false, ToggleSort: false,
Expect: make(map[int]string), Expect: make(map[int]string),
Keymap: make(map[int]actionType), Keymap: make(map[int][]action),
Execmap: make(map[int]string),
Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false}, Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false},
PrintQuery: false, PrintQuery: false,
ReadZero: false, ReadZero: false,
@@ -232,6 +236,7 @@ func defaultOptions() *Options {
HeaderLines: 0, HeaderLines: 0,
Margin: defaultMargin(), Margin: defaultMargin(),
Tabstop: 8, Tabstop: 8,
ClearOnExit: true,
Version: false} Version: false}
} }
@@ -393,8 +398,10 @@ func parseKeyChords(str string, message string) map[int]string {
chord = tui.AltZ + int(' ') chord = tui.AltZ + int(' ')
case "bspace", "bs": case "bspace", "bs":
chord = tui.BSpace chord = tui.BSpace
case "ctrl-space":
chord = tui.CtrlSpace
case "alt-enter", "alt-return": case "alt-enter", "alt-return":
chord = tui.AltEnter chord = tui.CtrlAltM
case "alt-space": case "alt-space":
chord = tui.AltSpace chord = tui.AltSpace
case "alt-/": case "alt-/":
@@ -430,7 +437,9 @@ func parseKeyChords(str string, message string) map[int]string {
case "f12": case "f12":
chord = tui.F12 chord = tui.F12
default: default:
if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { if len(key) == 10 && strings.HasPrefix(lkey, "ctrl-alt-") && isAlphabet(lkey[9]) {
chord = tui.CtrlAltA + int(lkey[9]) - 'a'
} else if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
chord = tui.CtrlA + int(lkey[5]) - 'a' chord = tui.CtrlA + int(lkey[5]) - 'a'
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) { } else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) {
chord = tui.AltA + int(lkey[4]) - 'a' chord = tui.AltA + int(lkey[4]) - 'a'
@@ -578,23 +587,32 @@ func firstKey(keymap map[int]string) int {
const ( const (
escapedColon = 0 escapedColon = 0
escapedComma = 1 escapedComma = 1
escapedPlus = 2
) )
func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string) { func init() {
if executeRegexp == nil { // 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)?)(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)")
"(?s):execute(-multi)?:.*|:execute(-multi)?(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") }
}
func parseKeymap(keymap map[int][]action, str string) {
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
if strings.HasPrefix(src, ":execute-multi") { prefix := ":execute"
return ":execute-multi(" + strings.Repeat(" ", len(src)-len(":execute-multi()")) + ")" if src[len(prefix)] == '-' {
c := src[len(prefix)+1]
if c == 's' || c == 'S' {
prefix += "-silent"
} else {
prefix += "-multi"
}
} }
return ":execute(" + strings.Repeat(" ", len(src)-len(":execute()")) + ")" return prefix + "(" + strings.Repeat(" ", len(src)-len(prefix)-2) + ")"
}) })
masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1) masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1)
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1) masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1)
masked = strings.Replace(masked, "+:", string([]rune{escapedPlus, ':'}), -1)
idx := 0 idx := 0
for _, pairStr := range strings.Split(masked, ",") { for _, pairStr := range strings.Split(masked, ",") {
@@ -610,147 +628,176 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string)
key = ':' + tui.AltZ key = ':' + tui.AltZ
} else if len(pair[0]) == 1 && pair[0][0] == escapedComma { } else if len(pair[0]) == 1 && pair[0][0] == escapedComma {
key = ',' + tui.AltZ key = ',' + tui.AltZ
} else if len(pair[0]) == 1 && pair[0][0] == escapedPlus {
key = '+' + tui.AltZ
} else { } else {
keys := parseKeyChords(pair[0], "key name required") keys := parseKeyChords(pair[0], "key name required")
key = firstKey(keys) key = firstKey(keys)
} }
act := origPairStr[len(pair[0])+1 : len(origPairStr)] idx2 := len(pair[0]) + 1
actLower := strings.ToLower(act) specs := strings.Split(pair[1], "+")
switch actLower { actions := make([]action, 0, len(specs))
case "ignore": appendAction := func(types ...actionType) {
keymap[key] = actIgnore actions = append(actions, toActions(types...)...)
case "beginning-of-line": }
keymap[key] = actBeginningOfLine prevSpec := ""
case "abort": for specIndex, maskedSpec := range specs {
keymap[key] = actAbort spec := origPairStr[idx2 : idx2+len(maskedSpec)]
case "accept": idx2 += len(maskedSpec) + 1
keymap[key] = actAccept spec = prevSpec + spec
case "print-query": specLower := strings.ToLower(spec)
keymap[key] = actPrintQuery switch specLower {
case "backward-char": case "ignore":
keymap[key] = actBackwardChar appendAction(actIgnore)
case "backward-delete-char": case "beginning-of-line":
keymap[key] = actBackwardDeleteChar appendAction(actBeginningOfLine)
case "backward-word": case "abort":
keymap[key] = actBackwardWord appendAction(actAbort)
case "clear-screen": case "accept":
keymap[key] = actClearScreen appendAction(actAccept)
case "delete-char": case "print-query":
keymap[key] = actDeleteChar appendAction(actPrintQuery)
case "delete-char/eof": case "backward-char":
keymap[key] = actDeleteCharEOF appendAction(actBackwardChar)
case "end-of-line": case "backward-delete-char":
keymap[key] = actEndOfLine appendAction(actBackwardDeleteChar)
case "cancel": case "backward-word":
keymap[key] = actCancel appendAction(actBackwardWord)
case "forward-char": case "clear-screen":
keymap[key] = actForwardChar appendAction(actClearScreen)
case "forward-word": case "delete-char":
keymap[key] = actForwardWord appendAction(actDeleteChar)
case "jump": case "delete-char/eof":
keymap[key] = actJump appendAction(actDeleteCharEOF)
case "jump-accept": case "end-of-line":
keymap[key] = actJumpAccept appendAction(actEndOfLine)
case "kill-line": case "cancel":
keymap[key] = actKillLine appendAction(actCancel)
case "kill-word": case "forward-char":
keymap[key] = actKillWord appendAction(actForwardChar)
case "unix-line-discard", "line-discard": case "forward-word":
keymap[key] = actUnixLineDiscard appendAction(actForwardWord)
case "unix-word-rubout", "word-rubout": case "jump":
keymap[key] = actUnixWordRubout appendAction(actJump)
case "yank": case "jump-accept":
keymap[key] = actYank appendAction(actJumpAccept)
case "backward-kill-word": case "kill-line":
keymap[key] = actBackwardKillWord appendAction(actKillLine)
case "toggle-down": case "kill-word":
keymap[key] = actToggleDown appendAction(actKillWord)
case "toggle-up": case "unix-line-discard", "line-discard":
keymap[key] = actToggleUp appendAction(actUnixLineDiscard)
case "toggle-in": case "unix-word-rubout", "word-rubout":
keymap[key] = actToggleIn appendAction(actUnixWordRubout)
case "toggle-out": case "yank":
keymap[key] = actToggleOut appendAction(actYank)
case "toggle-all": case "backward-kill-word":
keymap[key] = actToggleAll appendAction(actBackwardKillWord)
case "select-all": case "toggle-down":
keymap[key] = actSelectAll appendAction(actToggle, actDown)
case "deselect-all": case "toggle-up":
keymap[key] = actDeselectAll appendAction(actToggle, actUp)
case "toggle": case "toggle-in":
keymap[key] = actToggle appendAction(actToggleIn)
case "down": case "toggle-out":
keymap[key] = actDown appendAction(actToggleOut)
case "up": case "toggle-all":
keymap[key] = actUp appendAction(actToggleAll)
case "page-up": case "select-all":
keymap[key] = actPageUp appendAction(actSelectAll)
case "page-down": case "deselect-all":
keymap[key] = actPageDown appendAction(actDeselectAll)
case "previous-history": case "toggle":
keymap[key] = actPreviousHistory appendAction(actToggle)
case "next-history": case "down":
keymap[key] = actNextHistory appendAction(actDown)
case "toggle-preview": case "up":
keymap[key] = actTogglePreview appendAction(actUp)
case "toggle-sort": case "page-up":
keymap[key] = actToggleSort appendAction(actPageUp)
case "preview-up": case "page-down":
keymap[key] = actPreviewUp appendAction(actPageDown)
case "preview-down": case "half-page-up":
keymap[key] = actPreviewDown appendAction(actHalfPageUp)
case "preview-page-up": case "half-page-down":
keymap[key] = actPreviewPageUp appendAction(actHalfPageDown)
case "preview-page-down": case "previous-history":
keymap[key] = actPreviewPageDown appendAction(actPreviousHistory)
default: case "next-history":
if isExecuteAction(actLower) { appendAction(actNextHistory)
var offset int case "toggle-preview":
if strings.HasPrefix(actLower, "execute-multi") { appendAction(actTogglePreview)
keymap[key] = actExecuteMulti case "toggle-preview-wrap":
offset = len("execute-multi") appendAction(actTogglePreviewWrap)
case "toggle-sort":
appendAction(actToggleSort)
case "preview-up":
appendAction(actPreviewUp)
case "preview-down":
appendAction(actPreviewDown)
case "preview-page-up":
appendAction(actPreviewPageUp)
case "preview-page-down":
appendAction(actPreviewPageDown)
default:
t := isExecuteAction(specLower)
if t == actIgnore {
errorExit("unknown action: " + spec)
} else { } else {
keymap[key] = actExecute var offset int
offset = len("execute") switch t {
case actExecuteSilent:
offset = len("execute-silent")
case actExecuteMulti:
offset = len("execute-multi")
default:
offset = len("execute")
}
if spec[offset] == ':' {
if specIndex == len(specs)-1 {
actions = append(actions, action{t: t, a: spec[offset+1:]})
} else {
prevSpec = spec + "+"
continue
}
} else {
actions = append(actions, action{t: t, a: spec[offset+1 : len(spec)-1]})
}
} }
if act[offset] == ':' {
execmap[key] = act[offset+1:]
} else {
execmap[key] = act[offset+1 : len(act)-1]
}
} else {
errorExit("unknown action: " + act)
} }
prevSpec = ""
} }
keymap[key] = actions
} }
} }
func isExecuteAction(str string) bool { func isExecuteAction(str string) actionType {
if !strings.HasPrefix(str, "execute") || len(str) < len("execute()") { matches := executeRegexp.FindAllStringSubmatch(":"+str, -1)
return false if matches == nil || len(matches) != 1 {
return actIgnore
} }
b := str[len("execute")] prefix := matches[0][1]
if strings.HasPrefix(str, "execute-multi") { if len(prefix) == 0 {
if len(str) < len("execute-multi()") { prefix = matches[0][2]
return false
}
b = str[len("execute-multi")]
} }
e := str[len(str)-1] switch prefix {
if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' || case "execute":
b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") { return actExecute
return true case "execute-silent":
return actExecuteSilent
case "execute-multi":
return actExecuteMulti
} }
return false return actIgnore
} }
func parseToggleSort(keymap map[int]actionType, str string) { func parseToggleSort(keymap map[int][]action, str string) {
keys := parseKeyChords(str, "key name required") keys := parseKeyChords(str, "key name required")
if len(keys) != 1 { if len(keys) != 1 {
errorExit("multiple keys specified") errorExit("multiple keys specified")
} }
keymap[firstKey(keys)] = actToggleSort keymap[firstKey(keys)] = toActions(actToggleSort)
} }
func strLines(str string) []string { func strLines(str string) []string {
@@ -797,7 +844,7 @@ func parsePreviewWindow(opts *previewOpts, input string) {
opts.wrap = false opts.wrap = false
tokens := strings.Split(input, ":") tokens := strings.Split(input, ":")
sizeRegex := regexp.MustCompile("^[1-9][0-9]*%?$") sizeRegex := regexp.MustCompile("^[0-9]+%?$")
for _, token := range tokens { for _, token := range tokens {
switch token { switch token {
case "hidden": case "hidden":
@@ -915,7 +962,7 @@ func parseOptions(opts *Options, allArgs []string) {
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":
parseKeymap(opts.Keymap, opts.Execmap, nextString(allArgs, &i, "bind expression required")) parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required"))
case "--color": case "--color":
spec := optionalNextString(allArgs, &i) spec := optionalNextString(allArgs, &i)
if len(spec) == 0 { if len(spec) == 0 {
@@ -1048,11 +1095,19 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Height = sizeSpec{} opts.Height = sizeSpec{}
case "--no-margin": case "--no-margin":
opts.Margin = defaultMargin() opts.Margin = defaultMargin()
case "--no-border":
opts.Bordered = false
case "--border":
opts.Bordered = 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)"))
case "--tabstop": case "--tabstop":
opts.Tabstop = nextInt(allArgs, &i, "tab stop required") opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
case "--clear":
opts.ClearOnExit = true
case "--no-clear":
opts.ClearOnExit = false
case "--version": case "--version":
opts.Version = true opts.Version = true
default: default:
@@ -1085,7 +1140,7 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--color="); match { } else if match, value := optString(arg, "--color="); match {
opts.Theme = parseTheme(opts.Theme, value) opts.Theme = parseTheme(opts.Theme, value)
} else if match, value := optString(arg, "--bind="); match { } else if match, value := optString(arg, "--bind="); match {
parseKeymap(opts.Keymap, opts.Execmap, value) parseKeymap(opts.Keymap, value)
} else if match, value := optString(arg, "--history="); match { } else if match, value := optString(arg, "--history="); match {
setHistory(value) setHistory(value)
} else if match, value := optString(arg, "--history-size="); match { } else if match, value := optString(arg, "--history-size="); match {
@@ -1141,20 +1196,22 @@ func postProcessOptions(opts *Options) {
// 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 {
opts.Keymap[tui.CtrlP] = actPreviousHistory opts.Keymap[tui.CtrlP] = toActions(actPreviousHistory)
} }
if _, prs := opts.Keymap[tui.CtrlN]; !prs { if _, prs := opts.Keymap[tui.CtrlN]; !prs {
opts.Keymap[tui.CtrlN] = actNextHistory opts.Keymap[tui.CtrlN] = toActions(actNextHistory)
} }
} }
// Extend the default key map // Extend the default key map
keymap := defaultKeymap() keymap := defaultKeymap()
for key, act := range opts.Keymap { for key, actions := range opts.Keymap {
if act == actToggleSort { for _, act := range actions {
opts.ToggleSort = true if act.t == actToggleSort {
opts.ToggleSort = true
}
} }
keymap[key] = act keymap[key] = actions
} }
opts.Keymap = keymap opts.Keymap = keymap

View File

@@ -125,14 +125,14 @@ func TestIrrelevantNth(t *testing.T) {
} }
func TestParseKeys(t *testing.T) { func TestParseKeys(t *testing.T) {
pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ALT-enter,alt-SPACE", "") pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "")
check := func(i int, s string) { check := func(i int, s string) {
if pairs[i] != s { if pairs[i] != s {
t.Errorf("%s != %s", pairs[i], s) t.Errorf("%s != %s", pairs[i], s)
} }
} }
if len(pairs) != 11 { if len(pairs) != 12 {
t.Error(11) t.Error(12)
} }
check(tui.CtrlZ, "ctrl-z") check(tui.CtrlZ, "ctrl-z")
check(tui.AltZ, "alt-z") check(tui.AltZ, "alt-z")
@@ -143,7 +143,8 @@ func TestParseKeys(t *testing.T) {
check(tui.CtrlA+'g'-'a', "ctrl-G") check(tui.CtrlA+'g'-'a', "ctrl-G")
check(tui.AltZ+'J', "J") check(tui.AltZ+'J', "J")
check(tui.AltZ+'g', "g") check(tui.AltZ+'g', "g")
check(tui.AltEnter, "ALT-enter") check(tui.CtrlAltA, "ctrl-alt-a")
check(tui.CtrlAltM, "ALT-enter")
check(tui.AltSpace, "alt-SPACE") check(tui.AltSpace, "alt-SPACE")
// Synonyms // Synonyms
@@ -225,49 +226,51 @@ func TestParseKeysWithComma(t *testing.T) {
} }
func TestBind(t *testing.T) { func TestBind(t *testing.T) {
check := func(action actionType, expected actionType) {
if action != expected {
t.Errorf("%d != %d", action, expected)
}
}
checkString := func(action string, expected string) {
if action != expected {
t.Errorf("%d != %d", action, expected)
}
}
keymap := defaultKeymap() keymap := defaultKeymap()
execmap := make(map[int]string) check := func(keyName int, arg1 string, types ...actionType) {
check(actBeginningOfLine, keymap[tui.CtrlA]) if len(keymap[keyName]) != len(types) {
parseKeymap(keymap, execmap, t.Errorf("invalid number of actions (%d != %d)", len(types), len(keymap[keyName]))
"ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+ return
"f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ }
"alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+ for idx, action := range keymap[keyName] {
",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)") if types[idx] != action.t {
check(actKillLine, keymap[tui.CtrlA]) t.Errorf("invalid action type (%d != %d)", types[idx], action.t)
check(actToggleSort, keymap[tui.CtrlB]) }
check(actPageUp, keymap[tui.AltZ+'c']) }
check(actAbort, keymap[tui.AltZ+',']) if len(arg1) > 0 && keymap[keyName][0].a != arg1 {
check(actAccept, keymap[tui.AltZ+':']) t.Errorf("invalid action argument: (%s != %s)", arg1, keymap[keyName][0].a)
check(actPageDown, keymap[tui.AltZ]) }
check(actExecute, keymap[tui.F1]) }
check(actExecute, keymap[tui.F2]) check(tui.CtrlA, "", actBeginningOfLine)
check(actExecute, keymap[tui.F3]) parseKeymap(keymap,
check(actExecute, keymap[tui.F4]) "ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
checkString("ls {}", execmap[tui.F1]) "f1:execute(ls {})+abort,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
checkString("echo {}, {}, {}", execmap[tui.F2]) "alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
checkString("echo '({})'", execmap[tui.F3]) "x:Execute(foo+bar),X:execute/bar+baz/"+
checkString("less {}", execmap[tui.F4]) ",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up")
checkString("echo (,),[,],/,:,;,%,{}", execmap[tui.AltA]) check(tui.CtrlA, "", actKillLine)
checkString("echo (,),[,],/,:,@,%,{}", execmap[tui.AltB]) check(tui.CtrlB, "", actToggleSort, actUp, actDown)
checkString("\nfoobar,Y:execute(baz)", execmap[tui.AltZ+'X']) check(tui.AltZ+'c', "", actPageUp)
check(tui.AltZ+',', "", actAbort)
check(tui.AltZ+':', "", actAccept)
check(tui.AltZ, "", actPageDown)
check(tui.F1, "ls {}", actExecute, actAbort)
check(tui.F2, "echo {}, {}, {}", actExecute)
check(tui.F3, "echo '({})'", actExecute)
check(tui.F4, "less {}", actExecute)
check(tui.AltZ+'x', "foo+bar", actExecute)
check(tui.AltZ+'X', "bar+baz", actExecute)
check(tui.AltA, "echo (,),[,],/,:,;,%,{}", actExecuteMulti)
check(tui.AltB, "echo (,),[,],/,:,@,%,{}", actExecute)
check(tui.AltZ+'+', "++\nfoobar,Y:execute(baz)+up", actExecute)
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
parseKeymap(keymap, execmap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char)) parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
checkString("foobar", execmap[tui.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])]) check(tui.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute)
} }
parseKeymap(keymap, execmap, "f1:abort") parseKeymap(keymap, "f1:abort")
check(actAbort, keymap[tui.F1]) check(tui.F1, "", actAbort)
} }
func TestColorSpec(t *testing.T) { func TestColorSpec(t *testing.T) {
@@ -327,7 +330,7 @@ func TestDefaultCtrlNP(t *testing.T) {
opts := defaultOptions() opts := defaultOptions()
parseOptions(opts, words) parseOptions(opts, words)
postProcessOptions(opts) postProcessOptions(opts)
if opts.Keymap[key] != expected { if opts.Keymap[key][0].t != expected {
t.Error() t.Error()
} }
} }

View File

@@ -101,7 +101,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
for idx, term := range termSet { for idx, term := range termSet {
// 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 { if !cacheable || idx > 0 || term.inv || !fuzzy && term.typ != termExact {
cacheable = false cacheable = false
break Loop break Loop
} }

View File

@@ -186,3 +186,21 @@ func TestCacheKey(t *testing.T) {
test(true, "foo | bar !baz", "", false) test(true, "foo | bar !baz", "", false)
test(true, "| | | foo", "foo", true) test(true, "| | | foo", "foo", true)
} }
func TestCacheable(t *testing.T) {
test := func(fuzzy bool, str string, cacheable bool) {
clearPatternCache()
pat := BuildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, true, []Range{}, Delimiter{}, []rune(str))
if cacheable != pat.cacheable {
t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable)
}
}
test(true, "foo bar", true)
test(true, "foo 'bar", true)
test(true, "foo !bar", false)
test(false, "foo bar", true)
test(false, "foo '", true)
test(false, "foo 'bar", false)
test(false, "foo !bar", false)
}

View File

@@ -37,12 +37,14 @@ func buildResult(item *Item, offsets []Offset, score int, trimLen int) *Result {
result := Result{item: item, rank: rank{index: item.index}} result := Result{item: item, rank: rank{index: item.index}}
numChars := item.text.Length() numChars := item.text.Length()
minBegin := math.MaxUint16 minBegin := math.MaxUint16
minEnd := math.MaxUint16
maxEnd := 0 maxEnd := 0
validOffsetFound := false validOffsetFound := false
for _, offset := range offsets { for _, offset := range offsets {
b, e := int(offset[0]), int(offset[1]) b, e := int(offset[0]), int(offset[1])
if b < e { if b < e {
minBegin = util.Min(b, minBegin) minBegin = util.Min(b, minBegin)
minEnd = util.Min(e, minEnd)
maxEnd = util.Max(e, maxEnd) maxEnd = util.Max(e, maxEnd)
validOffsetFound = true validOffsetFound = true
} }
@@ -68,7 +70,7 @@ func buildResult(item *Item, offsets []Offset, score int, trimLen int) *Result {
} }
} }
if criterion == byBegin { if criterion == byBegin {
val = util.AsUint16(minBegin - whitePrefixLen) val = util.AsUint16(minEnd - whitePrefixLen)
} else { } else {
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/trimLen) val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/trimLen)
} }

View File

@@ -24,7 +24,7 @@ import (
var placeholder *regexp.Regexp var placeholder *regexp.Regexp
func init() { func init() {
placeholder = regexp.MustCompile("\\\\?(?:{[0-9,-.]*}|{q})") placeholder = regexp.MustCompile("\\\\?(?:{\\+?[0-9,-.]*}|{q})")
} }
type jumpMode int type jumpMode int
@@ -46,6 +46,7 @@ type itemLine struct {
current bool current bool
selected bool selected bool
label string label string
queryLen int
width int width int
result Result result Result
} }
@@ -58,6 +59,7 @@ type Terminal struct {
inlineInfo bool inlineInfo bool
prompt string prompt string
reverse bool reverse bool
fullscreen bool
hscroll bool hscroll bool
hscrollOff int hscrollOff int
wordRubout string wordRubout string
@@ -72,8 +74,7 @@ type Terminal struct {
toggleSort bool toggleSort bool
delimiter Delimiter delimiter Delimiter
expect map[int]string expect map[int]string
keymap map[int]actionType keymap map[int][]action
execmap map[int]string
pressed string pressed string
printQuery bool printQuery bool
history *History history *History
@@ -84,8 +85,10 @@ type Terminal struct {
tabstop int tabstop int
margin [4]sizeSpec margin [4]sizeSpec
strong tui.Attr strong tui.Attr
bordered bool
border tui.Window
window tui.Window window tui.Window
bwindow tui.Window pborder tui.Window
pwindow tui.Window pwindow tui.Window
count int count int
progress int progress int
@@ -139,6 +142,7 @@ const (
reqList reqList
reqJump reqJump
reqRefresh reqRefresh
reqReinit
reqRedraw reqRedraw
reqClose reqClose
reqPrintQuery reqPrintQuery
@@ -148,6 +152,11 @@ const (
reqQuit reqQuit
) )
type action struct {
t actionType
a string
}
type actionType int type actionType int
const ( const (
@@ -186,11 +195,14 @@ const (
actUp actUp
actPageUp actPageUp
actPageDown actPageDown
actHalfPageUp
actHalfPageDown
actJump actJump
actJumpAccept actJumpAccept
actPrintQuery actPrintQuery
actToggleSort actToggleSort
actTogglePreview actTogglePreview
actTogglePreviewWrap
actPreviewUp actPreviewUp
actPreviewDown actPreviewDown
actPreviewPageUp actPreviewPageUp
@@ -198,57 +210,70 @@ const (
actPreviousHistory actPreviousHistory
actNextHistory actNextHistory
actExecute actExecute
actExecuteMulti actExecuteSilent
actExecuteMulti // Deprecated
actSigStop
) )
func defaultKeymap() map[int]actionType { func toActions(types ...actionType) []action {
keymap := make(map[int]actionType) actions := make([]action, len(types))
keymap[tui.Invalid] = actInvalid for idx, t := range types {
keymap[tui.Resize] = actClearScreen actions[idx] = action{t: t, a: ""}
keymap[tui.CtrlA] = actBeginningOfLine }
keymap[tui.CtrlB] = actBackwardChar return actions
keymap[tui.CtrlC] = actAbort }
keymap[tui.CtrlG] = actAbort
keymap[tui.CtrlQ] = actAbort
keymap[tui.ESC] = actAbort
keymap[tui.CtrlD] = actDeleteCharEOF
keymap[tui.CtrlE] = actEndOfLine
keymap[tui.CtrlF] = actForwardChar
keymap[tui.CtrlH] = actBackwardDeleteChar
keymap[tui.BSpace] = actBackwardDeleteChar
keymap[tui.Tab] = actToggleDown
keymap[tui.BTab] = actToggleUp
keymap[tui.CtrlJ] = actDown
keymap[tui.CtrlK] = actUp
keymap[tui.CtrlL] = actClearScreen
keymap[tui.CtrlM] = actAccept
keymap[tui.CtrlN] = actDown
keymap[tui.CtrlP] = actUp
keymap[tui.CtrlU] = actUnixLineDiscard
keymap[tui.CtrlW] = actUnixWordRubout
keymap[tui.CtrlY] = actYank
keymap[tui.AltB] = actBackwardWord func defaultKeymap() map[int][]action {
keymap[tui.SLeft] = actBackwardWord keymap := make(map[int][]action)
keymap[tui.AltF] = actForwardWord keymap[tui.Invalid] = toActions(actInvalid)
keymap[tui.SRight] = actForwardWord keymap[tui.Resize] = toActions(actClearScreen)
keymap[tui.AltD] = actKillWord keymap[tui.CtrlA] = toActions(actBeginningOfLine)
keymap[tui.AltBS] = actBackwardKillWord keymap[tui.CtrlB] = toActions(actBackwardChar)
keymap[tui.CtrlC] = toActions(actAbort)
keymap[tui.CtrlG] = toActions(actAbort)
keymap[tui.CtrlQ] = toActions(actAbort)
keymap[tui.ESC] = toActions(actAbort)
keymap[tui.CtrlD] = toActions(actDeleteCharEOF)
keymap[tui.CtrlE] = toActions(actEndOfLine)
keymap[tui.CtrlF] = toActions(actForwardChar)
keymap[tui.CtrlH] = toActions(actBackwardDeleteChar)
keymap[tui.BSpace] = toActions(actBackwardDeleteChar)
keymap[tui.Tab] = toActions(actToggleDown)
keymap[tui.BTab] = toActions(actToggleUp)
keymap[tui.CtrlJ] = toActions(actDown)
keymap[tui.CtrlK] = toActions(actUp)
keymap[tui.CtrlL] = toActions(actClearScreen)
keymap[tui.CtrlM] = toActions(actAccept)
keymap[tui.CtrlN] = toActions(actDown)
keymap[tui.CtrlP] = toActions(actUp)
keymap[tui.CtrlU] = toActions(actUnixLineDiscard)
keymap[tui.CtrlW] = toActions(actUnixWordRubout)
keymap[tui.CtrlY] = toActions(actYank)
if !util.IsWindows() {
keymap[tui.CtrlZ] = toActions(actSigStop)
}
keymap[tui.Up] = actUp keymap[tui.AltB] = toActions(actBackwardWord)
keymap[tui.Down] = actDown keymap[tui.SLeft] = toActions(actBackwardWord)
keymap[tui.Left] = actBackwardChar keymap[tui.AltF] = toActions(actForwardWord)
keymap[tui.Right] = actForwardChar keymap[tui.SRight] = toActions(actForwardWord)
keymap[tui.AltD] = toActions(actKillWord)
keymap[tui.AltBS] = toActions(actBackwardKillWord)
keymap[tui.Home] = actBeginningOfLine keymap[tui.Up] = toActions(actUp)
keymap[tui.End] = actEndOfLine keymap[tui.Down] = toActions(actDown)
keymap[tui.Del] = actDeleteChar keymap[tui.Left] = toActions(actBackwardChar)
keymap[tui.PgUp] = actPageUp keymap[tui.Right] = toActions(actForwardChar)
keymap[tui.PgDn] = actPageDown
keymap[tui.Rune] = actRune keymap[tui.Home] = toActions(actBeginningOfLine)
keymap[tui.Mouse] = actMouse keymap[tui.End] = toActions(actEndOfLine)
keymap[tui.DoubleClick] = actAccept keymap[tui.Del] = toActions(actDeleteChar)
keymap[tui.PgUp] = toActions(actPageUp)
keymap[tui.PgDn] = toActions(actPageDown)
keymap[tui.Rune] = toActions(actRune)
keymap[tui.Mouse] = toActions(actMouse)
keymap[tui.DoubleClick] = toActions(actAccept)
return keymap return keymap
} }
@@ -276,23 +301,36 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
strongAttr = tui.AttrRegular strongAttr = tui.AttrRegular
} }
var renderer tui.Renderer var renderer tui.Renderer
if opts.Height.size > 0 { fullscreen := opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100
if fullscreen {
if tui.HasFullscreenRenderer() {
renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse)
} else {
renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit,
true, func(h int) int { return h })
}
} else {
maxHeightFunc := func(termHeight int) int { maxHeightFunc := func(termHeight int) int {
var maxHeight int var maxHeight int
if opts.Height.percent { if opts.Height.percent {
maxHeight = util.Min(termHeight, maxHeight = util.Max(int(opts.Height.size*float64(termHeight)/100.0), opts.MinHeight)
util.Max(int(opts.Height.size*float64(termHeight)/100.0), opts.MinHeight))
} else { } else {
maxHeight = util.Min(termHeight, int(opts.Height.size)) maxHeight = int(opts.Height.size)
}
effectiveMinHeight := minHeight
if previewBox != nil && (opts.Preview.position == posUp || opts.Preview.position == posDown) {
effectiveMinHeight *= 2
} }
if opts.InlineInfo { if opts.InlineInfo {
return util.Max(maxHeight, minHeight-1) effectiveMinHeight -= 1
} }
return util.Max(maxHeight, minHeight) if opts.Bordered {
effectiveMinHeight += 2
}
return util.Min(termHeight, util.Max(maxHeight, effectiveMinHeight))
} }
renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, maxHeightFunc) renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, false, maxHeightFunc)
} else {
renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse)
} }
wordRubout := "[^[:alnum:]][[:alnum:]]" wordRubout := "[^[:alnum:]][[:alnum:]]"
wordNext := "[[:alnum:]][^[:alnum:]]|(.$)" wordNext := "[[:alnum:]][^[:alnum:]]|(.$)"
@@ -306,6 +344,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
inlineInfo: opts.InlineInfo, inlineInfo: opts.InlineInfo,
prompt: opts.Prompt, prompt: opts.Prompt,
reverse: opts.Reverse, reverse: opts.Reverse,
fullscreen: fullscreen,
hscroll: opts.Hscroll, hscroll: opts.Hscroll,
hscrollOff: opts.HscrollOff, hscrollOff: opts.HscrollOff,
wordRubout: wordRubout, wordRubout: wordRubout,
@@ -321,11 +360,11 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
delimiter: opts.Delimiter, delimiter: opts.Delimiter,
expect: opts.Expect, expect: opts.Expect,
keymap: opts.Keymap, keymap: opts.Keymap,
execmap: opts.Execmap,
pressed: "", pressed: "",
printQuery: opts.PrintQuery, printQuery: opts.PrintQuery,
history: opts.History, history: opts.History,
margin: opts.Margin, margin: opts.Margin,
bordered: opts.Bordered,
strong: strongAttr, strong: strongAttr,
cycle: opts.Cycle, cycle: opts.Cycle,
header: header, header: header,
@@ -420,9 +459,9 @@ func (t *Terminal) output() bool {
} }
found := len(t.selected) > 0 found := len(t.selected) > 0
if !found { if !found {
cnt := t.merger.Length() current := t.currentItem()
if cnt > 0 && cnt > t.cy { if current != nil {
t.printer(t.current()) t.printer(current.AsString(t.ansi))
found = true found = true
} }
} else { } else {
@@ -482,6 +521,9 @@ func (t *Terminal) resizeWindows() {
} else { } else {
marginInt[idx] = int(sizeSpec.size) marginInt[idx] = int(sizeSpec.size)
} }
if t.bordered && idx%2 == 0 {
marginInt[idx] += 1
}
} }
adjust := func(idx1 int, idx2 int, max int, min int) { adjust := func(idx1 int, idx2 int, max int, min int) {
if max >= min { if max >= min {
@@ -493,9 +535,11 @@ func (t *Terminal) resizeWindows() {
} }
} }
} }
previewVisible := t.isPreviewEnabled() && t.preview.size.size > 0
minAreaWidth := minWidth minAreaWidth := minWidth
minAreaHeight := minHeight minAreaHeight := minHeight
if t.isPreviewEnabled() { if previewVisible {
switch t.preview.position { switch t.preview.position {
case posUp, posDown: case posUp, posDown:
minAreaHeight *= 2 minAreaHeight *= 2
@@ -505,19 +549,29 @@ func (t *Terminal) resizeWindows() {
} }
adjust(1, 3, screenWidth, minAreaWidth) adjust(1, 3, screenWidth, minAreaWidth)
adjust(0, 2, screenHeight, minAreaHeight) adjust(0, 2, screenHeight, minAreaHeight)
if t.border != nil {
t.border.Close()
}
if t.window != nil { if t.window != nil {
t.window.Close() t.window.Close()
} }
if t.bwindow != nil { if t.pborder != nil {
t.bwindow.Close() t.pborder.Close()
t.pwindow.Close() t.pwindow.Close()
} }
width := screenWidth - marginInt[1] - marginInt[3] width := screenWidth - marginInt[1] - marginInt[3]
height := screenHeight - marginInt[0] - marginInt[2] height := screenHeight - marginInt[0] - marginInt[2]
if t.isPreviewEnabled() { if t.bordered {
t.border = t.tui.NewWindow(
marginInt[0]-1,
marginInt[3],
width,
height+2, tui.BorderHorizontal)
}
if previewVisible {
createPreviewWindow := func(y int, x int, w int, h int) { createPreviewWindow := func(y int, x int, w int, h int) {
t.bwindow = t.tui.NewWindow(y, x, w, h, true) t.pborder = t.tui.NewWindow(y, x, w, h, tui.BorderAround)
pwidth := w - 4 pwidth := w - 4
// ncurses auto-wraps the line when the cursor reaches the right-end of // ncurses auto-wraps the line when the cursor reaches the right-end of
// the window. To prevent unintended line-wraps, we use the width one // the window. To prevent unintended line-wraps, we use the width one
@@ -525,28 +579,29 @@ func (t *Terminal) resizeWindows() {
if !t.preview.wrap && t.tui.DoesAutoWrap() { if !t.preview.wrap && t.tui.DoesAutoWrap() {
pwidth += 1 pwidth += 1
} }
t.pwindow = t.tui.NewWindow(y+1, x+2, pwidth, h-2, false) t.pwindow = t.tui.NewWindow(y+1, x+2, pwidth, h-2, tui.BorderNone)
os.Setenv("FZF_PREVIEW_HEIGHT", strconv.Itoa(h-2))
} }
switch t.preview.position { switch t.preview.position {
case posUp: case posUp:
pheight := calculateSize(height, t.preview.size, minHeight, 3) pheight := calculateSize(height, t.preview.size, minHeight, 3)
t.window = t.tui.NewWindow( t.window = t.tui.NewWindow(
marginInt[0]+pheight, marginInt[3], width, height-pheight, false) marginInt[0]+pheight, marginInt[3], width, height-pheight, tui.BorderNone)
createPreviewWindow(marginInt[0], marginInt[3], width, pheight) createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
case posDown: case posDown:
pheight := calculateSize(height, t.preview.size, minHeight, 3) pheight := calculateSize(height, t.preview.size, minHeight, 3)
t.window = t.tui.NewWindow( t.window = t.tui.NewWindow(
marginInt[0], marginInt[3], width, height-pheight, false) marginInt[0], marginInt[3], width, height-pheight, tui.BorderNone)
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight) createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
case posLeft: case posLeft:
pwidth := calculateSize(width, t.preview.size, minWidth, 5) pwidth := calculateSize(width, t.preview.size, minWidth, 5)
t.window = t.tui.NewWindow( t.window = t.tui.NewWindow(
marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false) marginInt[0], marginInt[3]+pwidth, width-pwidth, height, tui.BorderNone)
createPreviewWindow(marginInt[0], marginInt[3], pwidth, height) createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
case posRight: case posRight:
pwidth := calculateSize(width, t.preview.size, minWidth, 5) pwidth := calculateSize(width, t.preview.size, minWidth, 5)
t.window = t.tui.NewWindow( t.window = t.tui.NewWindow(
marginInt[0], marginInt[3], width-pwidth, height, false) marginInt[0], marginInt[3], width-pwidth, height, tui.BorderNone)
createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height) createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height)
} }
} else { } else {
@@ -554,8 +609,14 @@ func (t *Terminal) resizeWindows() {
marginInt[0], marginInt[0],
marginInt[3], marginInt[3],
width, width,
height, false) height, tui.BorderNone)
} }
if !t.tui.IsOptimized() {
for i := 0; i < t.window.Height(); i++ {
t.window.MoveAndClear(i, 0)
}
}
t.truncateQuery()
} }
func (t *Terminal) move(y int, x int, clear bool) { func (t *Terminal) move(y int, x int, clear bool) {
@@ -581,13 +642,19 @@ func (t *Terminal) printPrompt() {
} }
func (t *Terminal) printInfo() { func (t *Terminal) printInfo() {
pos := 0
if t.inlineInfo { if t.inlineInfo {
t.move(0, t.displayWidth([]rune(t.prompt))+t.displayWidth(t.input)+1, true) pos = t.displayWidth([]rune(t.prompt)) + t.displayWidth(t.input) + 1
if pos+len(" < ") > t.window.Width() {
return
}
t.move(0, pos, true)
if t.reading { if t.reading {
t.window.CPrint(tui.ColSpinner, t.strong, " < ") t.window.CPrint(tui.ColSpinner, t.strong, " < ")
} else { } else {
t.window.CPrint(tui.ColPrompt, t.strong, " < ") t.window.CPrint(tui.ColPrompt, t.strong, " < ")
} }
pos += len(" < ")
} else { } else {
t.move(1, 0, true) t.move(1, 0, true)
if t.reading { if t.reading {
@@ -596,14 +663,15 @@ func (t *Terminal) printInfo() {
t.window.CPrint(tui.ColSpinner, t.strong, _spinner[idx]) t.window.CPrint(tui.ColSpinner, t.strong, _spinner[idx])
} }
t.move(1, 2, false) t.move(1, 2, false)
pos = 2
} }
output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count) output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count)
if t.toggleSort { if t.toggleSort {
if t.sort { if t.sort {
output += "/S" output += " +S"
} else { } else {
output += " " output += " -S"
} }
} }
if t.multi && len(t.selected) > 0 { if t.multi && len(t.selected) > 0 {
@@ -612,7 +680,9 @@ func (t *Terminal) printInfo() {
if t.progress > 0 && t.progress < 100 { if t.progress > 0 && t.progress < 100 {
output += fmt.Sprintf(" (%d%%)", t.progress) output += fmt.Sprintf(" (%d%%)", t.progress)
} }
t.window.CPrint(tui.ColInfo, 0, output) if pos+len(output) <= t.window.Width() {
t.window.CPrint(tui.ColInfo, 0, output)
}
} }
func (t *Terminal) printHeader() { func (t *Terminal) printHeader() {
@@ -679,11 +749,13 @@ func (t *Terminal) printItem(result *Result, line int, i int, current bool) {
} }
// Avoid unnecessary redraw // Avoid unnecessary redraw
newLine := itemLine{current: current, selected: selected, label: label, result: *result, width: 0} newLine := itemLine{current: current, selected: selected, label: label,
result: *result, queryLen: len(t.input), width: 0}
prevLine := t.prevLines[i] prevLine := t.prevLines[i]
if prevLine.current == newLine.current && if prevLine.current == newLine.current &&
prevLine.selected == newLine.selected && prevLine.selected == newLine.selected &&
prevLine.label == newLine.label && prevLine.label == newLine.label &&
prevLine.queryLen == newLine.queryLen &&
prevLine.result == newLine.result { prevLine.result == newLine.result {
return return
} }
@@ -824,6 +896,7 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo
offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth)) offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
} }
} }
displayWidth = t.displayWidthWithLimit(text, 0, displayWidth)
} }
var index int32 var index int32
@@ -868,7 +941,7 @@ func numLinesMax(str string, max int) int {
} }
func (t *Terminal) printPreview() { func (t *Terminal) printPreview() {
if !t.isPreviewEnabled() { if !t.hasPreviewWindow() {
return return
} }
t.pwindow.Erase() t.pwindow.Erase()
@@ -896,7 +969,7 @@ func (t *Terminal) printPreview() {
trimmed, _ = t.trimRight(trimmed, maxWidth-t.pwindow.X()) trimmed, _ = t.trimRight(trimmed, maxWidth-t.pwindow.X())
} }
str, _ = t.processTabs(trimmed, 0) str, _ = t.processTabs(trimmed, 0)
if ansi != nil && ansi.colored() { if t.theme != nil && ansi != nil && ansi.colored() {
fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str) fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str)
} else { } else {
fillRet = t.pwindow.Fill(str) fillRet = t.pwindow.Fill(str)
@@ -953,11 +1026,15 @@ func (t *Terminal) printAll() {
func (t *Terminal) refresh() { func (t *Terminal) refresh() {
if !t.suppress { if !t.suppress {
if t.isPreviewEnabled() { windows := make([]tui.Window, 0, 4)
t.tui.RefreshWindows([]tui.Window{t.bwindow, t.pwindow, t.window}) if t.bordered {
} else { windows = append(windows, t.border)
t.tui.RefreshWindows([]tui.Window{t.window})
} }
if t.hasPreviewWindow() {
windows = append(windows, t.pborder, t.pwindow)
}
windows = append(windows, t.window)
t.tui.RefreshWindows(windows)
} }
} }
@@ -1020,7 +1097,27 @@ func quoteEntry(entry string) string {
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'" return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
} }
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, query string, items []*Item) string { func hasPlusFlag(template string) bool {
for _, match := range placeholder.FindAllString(template, -1) {
if match[0] == '\\' {
continue
}
if match[1] == '+' {
return true
}
}
return false
}
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, forcePlus bool, query string, allItems []*Item) string {
current := allItems[:1]
selected := allItems[1:]
if current[0] == nil {
current = []*Item{}
}
if selected[0] == nil {
selected = []*Item{}
}
return placeholder.ReplaceAllStringFunc(template, func(match string) string { return placeholder.ReplaceAllStringFunc(template, func(match string) string {
// Escaped pattern // Escaped pattern
if match[0] == '\\' { if match[0] == '\\' {
@@ -1032,6 +1129,16 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, qu
return quoteEntry(query) return quoteEntry(query)
} }
plusFlag := forcePlus
if match[1] == '+' {
match = "{" + match[2:]
plusFlag = true
}
items := current
if plusFlag {
items = selected
}
replacements := make([]string, len(items)) replacements := make([]string, len(items))
if match == "{}" { if match == "{}" {
@@ -1072,34 +1179,70 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, qu
}) })
} }
func (t *Terminal) executeCommand(template string, items []*Item) { func (t *Terminal) redraw() {
command := replacePlaceholder(template, t.ansi, t.delimiter, string(t.input), items) t.tui.Clear()
cmd := util.ExecCommand(command) t.tui.Refresh()
cmd.Stdin = os.Stdin t.printAll()
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
t.tui.Pause()
cmd.Run()
if t.tui.Resume() {
t.printAll()
}
t.refresh()
} }
func (t *Terminal) hasPreviewWindow() bool { func (t *Terminal) executeCommand(template string, forcePlus bool, background bool) {
valid, list := t.buildPlusList(template, forcePlus)
if !valid {
return
}
command := replacePlaceholder(template, t.ansi, t.delimiter, forcePlus, string(t.input), list)
cmd := util.ExecCommand(command)
if !background {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
t.tui.Pause(true)
cmd.Run()
t.tui.Resume(true)
t.redraw()
t.refresh()
} else {
cmd.Run()
}
}
func (t *Terminal) hasPreviewer() bool {
return t.previewBox != nil return t.previewBox != nil
} }
func (t *Terminal) isPreviewEnabled() bool { func (t *Terminal) isPreviewEnabled() bool {
return t.previewBox != nil && t.previewer.enabled return t.hasPreviewer() && t.previewer.enabled
}
func (t *Terminal) hasPreviewWindow() bool {
return t.pwindow != nil && t.isPreviewEnabled()
} }
func (t *Terminal) currentItem() *Item { func (t *Terminal) currentItem() *Item {
return t.merger.Get(t.cy).item cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy {
return t.merger.Get(t.cy).item
}
return nil
} }
func (t *Terminal) current() string { func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item) {
return t.currentItem().AsString(t.ansi) current := t.currentItem()
if !forcePlus && !hasPlusFlag(template) || len(t.selected) == 0 {
return current != nil, []*Item{current, current}
}
sels := make([]*Item, len(t.selected)+1)
sels[0] = current
for i, sel := range t.sortSelected() {
sels[i+1] = sel.item
}
return true, sels
}
func (t *Terminal) truncateQuery() {
maxPatternLength := util.Max(1, t.window.Width()-t.displayWidth([]rune(t.prompt))-1)
t.input, _ = t.trimRight(t.input, maxPatternLength)
t.cx = util.Constrain(t.cx, 0, len(t.input))
} }
// Loop is called to start Terminal I/O // Loop is called to start Terminal I/O
@@ -1114,6 +1257,15 @@ func (t *Terminal) Loop() {
t.reqBox.Set(reqQuit, nil) t.reqBox.Set(reqQuit, nil)
}() }()
contChan := make(chan os.Signal, 1)
notifyOnCont(contChan)
go func() {
for {
<-contChan
t.reqBox.Set(reqReinit, nil)
}
}()
resizeChan := make(chan os.Signal, 1) resizeChan := make(chan os.Signal, 1)
notifyOnResize(resizeChan) // Non-portable notifyOnResize(resizeChan) // Non-portable
go func() { go func() {
@@ -1153,22 +1305,23 @@ func (t *Terminal) Loop() {
}() }()
} }
if t.hasPreviewWindow() { if t.hasPreviewer() {
go func() { go func() {
for { for {
var request *Item var request []*Item
t.previewBox.Wait(func(events *util.Events) { t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events { for req, value := range *events {
switch req { switch req {
case reqPreviewEnqueue: case reqPreviewEnqueue:
request = value.(*Item) request = value.([]*Item)
} }
} }
events.Clear() events.Clear()
}) })
if request != nil { // We don't display preview window if no match
if request[0] != nil {
command := replacePlaceholder(t.preview.command, command := replacePlaceholder(t.preview.command,
t.ansi, t.delimiter, string(t.input), []*Item{request}) t.ansi, t.delimiter, false, string(t.input), request)
cmd := util.ExecCommand(command) cmd := util.ExecCommand(command)
out, _ := cmd.CombinedOutput() out, _ := cmd.CombinedOutput()
t.reqBox.Set(reqPreviewDisplay, string(out)) t.reqBox.Set(reqPreviewDisplay, string(out))
@@ -1204,17 +1357,12 @@ func (t *Terminal) Loop() {
t.printInfo() t.printInfo()
case reqList: case reqList:
t.printList() t.printList()
cnt := t.merger.Length() currentFocus := t.currentItem()
var currentFocus *Item
if cnt > 0 && cnt > t.cy {
currentFocus = t.currentItem()
} else {
currentFocus = nil
}
if currentFocus != focused { if currentFocus != focused {
focused = currentFocus focused = currentFocus
if t.isPreviewEnabled() { if t.isPreviewEnabled() {
t.previewBox.Set(reqPreviewEnqueue, focused) _, list := t.buildPlusList(t.preview.command, false)
t.previewBox.Set(reqPreviewEnqueue, list)
} }
} }
case reqJump: case reqJump:
@@ -1226,10 +1374,11 @@ func (t *Terminal) Loop() {
t.printHeader() t.printHeader()
case reqRefresh: case reqRefresh:
t.suppress = false t.suppress = false
case reqReinit:
t.tui.Resume(t.fullscreen)
t.redraw()
case reqRedraw: case reqRedraw:
t.tui.Clear() t.redraw()
t.tui.Refresh()
t.printAll()
case reqClose: case reqClose:
t.tui.Close() t.tui.Close()
if t.output() { if t.output() {
@@ -1307,56 +1456,62 @@ func (t *Terminal) Loop() {
} }
} }
var doAction func(actionType, int) bool var doAction func(action, int) bool
doAction = func(action actionType, mapkey int) bool { doActions := func(actions []action, mapkey int) bool {
switch action { for _, action := range actions {
if !doAction(action, mapkey) {
return false
}
}
return true
}
doAction = func(a action, mapkey int) bool {
switch a.t {
case actIgnore: case actIgnore:
case actExecute: case actExecute, actExecuteSilent:
if t.cy >= 0 && t.cy < t.merger.Length() { t.executeCommand(a.a, false, a.t == actExecuteSilent)
t.executeCommand(t.execmap[mapkey], []*Item{t.currentItem()})
}
case actExecuteMulti: case actExecuteMulti:
if len(t.selected) > 0 { t.executeCommand(a.a, true, false)
sels := make([]*Item, len(t.selected))
for i, sel := range t.sortSelected() {
sels[i] = sel.item
}
t.executeCommand(t.execmap[mapkey], sels)
} else {
return doAction(actExecute, mapkey)
}
case actInvalid: case actInvalid:
t.mutex.Unlock() t.mutex.Unlock()
return false return false
case actTogglePreview: case actTogglePreview:
if t.hasPreviewWindow() { if t.hasPreviewer() {
t.previewer.enabled = !t.previewer.enabled t.previewer.enabled = !t.previewer.enabled
t.tui.Clear()
t.resizeWindows() t.resizeWindows()
cnt := t.merger.Length() if t.previewer.enabled {
if t.previewer.enabled && cnt > 0 && cnt > t.cy { valid, list := t.buildPlusList(t.preview.command, false)
t.previewBox.Set(reqPreviewEnqueue, t.currentItem()) if valid {
t.previewBox.Set(reqPreviewEnqueue, list)
}
} }
req(reqList, reqInfo, reqHeader) req(reqList, reqInfo, reqHeader)
} }
case actTogglePreviewWrap:
if t.hasPreviewWindow() {
t.preview.wrap = !t.preview.wrap
req(reqPreviewRefresh)
}
case actToggleSort: case actToggleSort:
t.sort = !t.sort t.sort = !t.sort
t.eventBox.Set(EvtSearchNew, t.sort) t.eventBox.Set(EvtSearchNew, t.sort)
t.mutex.Unlock() t.mutex.Unlock()
return false return false
case actPreviewUp: case actPreviewUp:
if t.isPreviewEnabled() { if t.hasPreviewWindow() {
scrollPreview(-1) scrollPreview(-1)
} }
case actPreviewDown: case actPreviewDown:
if t.isPreviewEnabled() { if t.hasPreviewWindow() {
scrollPreview(1) scrollPreview(1)
} }
case actPreviewPageUp: case actPreviewPageUp:
if t.isPreviewEnabled() { if t.hasPreviewWindow() {
scrollPreview(-t.pwindow.Height()) scrollPreview(-t.pwindow.Height())
} }
case actPreviewPageDown: case actPreviewPageDown:
if t.isPreviewEnabled() { if t.hasPreviewWindow() {
scrollPreview(t.pwindow.Height()) scrollPreview(t.pwindow.Height())
} }
case actBeginningOfLine: case actBeginningOfLine:
@@ -1424,14 +1579,14 @@ func (t *Terminal) Loop() {
} }
case actToggleIn: case actToggleIn:
if t.reverse { if t.reverse {
return doAction(actToggleUp, mapkey) return doAction(action{t: actToggleUp}, mapkey)
} }
return doAction(actToggleDown, mapkey) return doAction(action{t: actToggleDown}, mapkey)
case actToggleOut: case actToggleOut:
if t.reverse { if t.reverse {
return doAction(actToggleDown, mapkey) return doAction(action{t: actToggleDown}, mapkey)
} }
return doAction(actToggleUp, mapkey) return doAction(action{t: actToggleUp}, mapkey)
case actToggleDown: case actToggleDown:
if t.multi && t.merger.Length() > 0 { if t.multi && t.merger.Length() > 0 {
toggle() toggle()
@@ -1478,6 +1633,12 @@ func (t *Terminal) Loop() {
case actPageDown: case actPageDown:
t.vmove(-(t.maxItems() - 1)) t.vmove(-(t.maxItems() - 1))
req(reqList) req(reqList)
case actHalfPageUp:
t.vmove(t.maxItems() / 2)
req(reqList)
case actHalfPageDown:
t.vmove(-(t.maxItems() / 2))
req(reqList)
case actJump: case actJump:
t.jumping = jumpEnabled t.jumping = jumpEnabled
req(reqJump) req(reqJump)
@@ -1516,6 +1677,15 @@ func (t *Terminal) Loop() {
t.input = []rune(t.history.next()) t.input = []rune(t.history.next())
t.cx = len(t.input) t.cx = len(t.input)
} }
case actSigStop:
p, err := os.FindProcess(os.Getpid())
if err == nil {
t.tui.Clear()
t.tui.Pause(t.fullscreen)
notifyStop(p)
t.mutex.Unlock()
return false
}
case actMouse: case actMouse:
me := event.MouseEvent me := event.MouseEvent
mx, my := me.X, me.Y mx, my := me.X, me.Y
@@ -1527,7 +1697,7 @@ func (t *Terminal) Loop() {
} }
t.vmove(me.S) t.vmove(me.S)
req(reqList) req(reqList)
} else if t.isPreviewEnabled() && t.pwindow.Enclose(my, mx) { } else if t.hasPreviewWindow() && t.pwindow.Enclose(my, mx) {
scrollPreview(-me.S) scrollPreview(-me.S)
} }
} else if t.window.Enclose(my, mx) { } else if t.window.Enclose(my, mx) {
@@ -1545,7 +1715,7 @@ func (t *Terminal) Loop() {
// Double-click // Double-click
if my >= min { if my >= min {
if t.vset(t.offset+my-min) && t.cy < t.merger.Length() { if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
return doAction(t.keymap[tui.DoubleClick], tui.DoubleClick) return doActions(t.keymap[tui.DoubleClick], tui.DoubleClick)
} }
} }
} else if me.Down { } else if me.Down {
@@ -1567,21 +1737,17 @@ func (t *Terminal) Loop() {
changed := false changed := false
mapkey := event.Type mapkey := event.Type
if t.jumping == jumpDisabled { if t.jumping == jumpDisabled {
action := t.keymap[mapkey] actions := t.keymap[mapkey]
if mapkey == tui.Rune { if mapkey == tui.Rune {
mapkey = int(event.Char) + int(tui.AltZ) mapkey = int(event.Char) + int(tui.AltZ)
if act, prs := t.keymap[mapkey]; prs { if act, prs := t.keymap[mapkey]; prs {
action = act actions = act
} }
} }
if !doAction(action, mapkey) { if !doActions(actions, mapkey) {
continue continue
} }
// Truncate the query if it's too long t.truncateQuery()
if len(t.input) > maxPatternLength {
t.input = t.input[:maxPatternLength]
t.cx = util.Constrain(t.cx, 0, maxPatternLength)
}
changed = string(previousInput) != string(t.input) changed = string(previousInput) != string(t.input)
} else { } else {
if mapkey == tui.Rune { if mapkey == tui.Rune {

View File

@@ -14,8 +14,10 @@ func newItem(str string) *Item {
} }
func TestReplacePlaceholder(t *testing.T) { func TestReplacePlaceholder(t *testing.T) {
items1 := []*Item{newItem(" foo'bar \x1b[31mbaz\x1b[m")} item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
items1 := []*Item{item1, item1}
items2 := []*Item{ items2 := []*Item{
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")} newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}
@@ -27,47 +29,65 @@ func TestReplacePlaceholder(t *testing.T) {
} }
// {}, preserve ansi // {}, preserve ansi
result = replacePlaceholder("echo {}", false, Delimiter{}, "query", items1) result = replacePlaceholder("echo {}", false, Delimiter{}, 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{}, "query", items1) result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items1)
check("echo ' foo'\\''bar baz'") check("echo ' foo'\\''bar baz'")
// {}, with multiple items // {}, with multiple items
result = replacePlaceholder("echo {}", true, Delimiter{}, "query", items2) result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items2)
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ'") check("echo 'foo'\\''bar baz'")
// {..}, strip leading whitespaces, preserve ansi // {..}, strip leading whitespaces, preserve ansi
result = replacePlaceholder("echo {..}", false, Delimiter{}, "query", items1) result = replacePlaceholder("echo {..}", false, Delimiter{}, 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{}, "query", items1) result = replacePlaceholder("echo {..}", true, Delimiter{}, false, "query", items1)
check("echo 'foo'\\''bar baz'") check("echo 'foo'\\''bar baz'")
// {q} // {q}
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, "query", items1) result = replacePlaceholder("echo {} {q}", true, Delimiter{}, 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{}, "query 'string'", items2) result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, 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 {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, "query", items1) result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, false, "query 'string'", items2)
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)
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{}, "query", items2) result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, false, "query", items2)
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)
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
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, 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}/'' ''")
// No match
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, nil})
check("echo /")
// No match, but with selections
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, item1})
check("echo /' foo'\\''bar baz'")
// String delimiter // String delimiter
delim := "'" delim := "'"
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, "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}, "query", items1) result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, false, "query", items1)
check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'") check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'")
} }

View File

@@ -11,3 +11,11 @@ import (
func notifyOnResize(resizeChan chan<- os.Signal) { func notifyOnResize(resizeChan chan<- os.Signal) {
signal.Notify(resizeChan, syscall.SIGWINCH) signal.Notify(resizeChan, syscall.SIGWINCH)
} }
func notifyStop(p *os.Process) {
p.Signal(syscall.SIGSTOP)
}
func notifyOnCont(resizeChan chan<- os.Signal) {
signal.Notify(resizeChan, syscall.SIGCONT)
}

View File

@@ -9,3 +9,11 @@ import (
func notifyOnResize(resizeChan chan<- os.Signal) { func notifyOnResize(resizeChan chan<- os.Signal) {
// TODO // TODO
} }
func notifyStop(p *os.Process) {
// NOOP
}
func notifyOnCont(resizeChan chan<- os.Signal) {
// NOOP
}

45
src/tui/dummy.go Normal file
View File

@@ -0,0 +1,45 @@
// +build !ncurses
// +build !tcell
// +build !windows
package tui
type Attr int
func HasFullscreenRenderer() bool {
return false
}
func (a Attr) Merge(b Attr) Attr {
return a | b
}
const (
AttrRegular Attr = Attr(0)
Bold = Attr(1)
Dim = Attr(1 << 1)
Italic = Attr(1 << 2)
Underline = Attr(1 << 3)
Blink = Attr(1 << 4)
Blink2 = Attr(1 << 5)
Reverse = Attr(1 << 6)
)
func (r *FullscreenRenderer) Init() {}
func (r *FullscreenRenderer) Pause(bool) {}
func (r *FullscreenRenderer) Resume(bool) {}
func (r *FullscreenRenderer) Clear() {}
func (r *FullscreenRenderer) Refresh() {}
func (r *FullscreenRenderer) Close() {}
func (r *FullscreenRenderer) DoesAutoWrap() bool { return false }
func (r *FullscreenRenderer) IsOptimized() bool { return false }
func (r *FullscreenRenderer) GetChar() Event { return Event{} }
func (r *FullscreenRenderer) MaxX() int { return 0 }
func (r *FullscreenRenderer) MaxY() int { return 0 }
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {}
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
return nil
}

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"regexp"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
@@ -19,11 +20,15 @@ const (
defaultWidth = 80 defaultWidth = 80
defaultHeight = 24 defaultHeight = 24
defaultEscDelay = 100
escPollInterval = 5 escPollInterval = 5
offsetPollTries = 10
) )
const consoleDevice string = "/dev/tty" const consoleDevice string = "/dev/tty"
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 {
@@ -69,6 +74,7 @@ type LightRenderer struct {
theme *ColorTheme theme *ColorTheme
mouse bool mouse bool
forceBlack bool forceBlack bool
clearOnExit bool
prevDownTime time.Time prevDownTime time.Time
clickY []int clickY []int
ttyin *os.File ttyin *os.File
@@ -79,6 +85,7 @@ type LightRenderer struct {
yoffset int yoffset int
tabstop int tabstop int
escDelay int escDelay int
fullscreen bool
upOneLine bool upOneLine bool
queued string queued string
y int y int
@@ -89,7 +96,7 @@ type LightRenderer struct {
type LightWindow struct { type LightWindow struct {
renderer *LightRenderer renderer *LightRenderer
colored bool colored bool
border bool border BorderStyle
top int top int
left int left int
width int width int
@@ -100,14 +107,16 @@ type LightWindow struct {
bg Color bg Color
} }
func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, maxHeightFunc func(int) int) Renderer { func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) Renderer {
r := LightRenderer{ r := LightRenderer{
theme: theme, theme: theme,
forceBlack: forceBlack, forceBlack: forceBlack,
mouse: mouse, mouse: mouse,
clearOnExit: clearOnExit,
ttyin: openTtyIn(), ttyin: openTtyIn(),
yoffset: -1, yoffset: 0,
tabstop: tabstop, tabstop: tabstop,
fullscreen: fullscreen,
upOneLine: false, upOneLine: false,
maxHeightFunc: maxHeightFunc} maxHeightFunc: maxHeightFunc}
return &r return &r
@@ -131,18 +140,14 @@ func (r *LightRenderer) defaultTheme() *ColorTheme {
func (r *LightRenderer) findOffset() (row int, col int) { func (r *LightRenderer) findOffset() (row int, col int) {
r.csi("6n") r.csi("6n")
r.flush() r.flush()
bytes := r.getBytesInternal([]byte{}) bytes := []byte{}
for tries := 0; tries < offsetPollTries; tries++ {
// ^[[*;*R bytes = r.getBytesInternal(bytes, tries > 0)
if len(bytes) > 5 && bytes[0] == 27 && bytes[1] == 91 && bytes[len(bytes)-1] == 'R' { offsets := offsetRegexp.FindSubmatch(bytes)
nums := strings.Split(string(bytes[2:len(bytes)-1]), ";") if len(offsets) > 2 {
if len(nums) == 2 { return atoi(string(offsets[1]), 0) - 1, atoi(string(offsets[2]), 0) - 1
return atoi(nums[0], 0) - 1, atoi(nums[1], 0) - 1
} }
return -1, -1
} }
// No idea
return -1, -1 return -1, -1
} }
@@ -162,15 +167,7 @@ func atoi(s string, defaultValue int) int {
} }
func (r *LightRenderer) Init() { func (r *LightRenderer) Init() {
delay := 100 r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay)
delayEnv := os.Getenv("ESCDELAY")
if len(delayEnv) > 0 {
num, err := strconv.Atoi(delayEnv)
if err == nil && num >= 0 {
delay = num
}
}
r.escDelay = delay
fd := r.fd() fd := r.fd()
origState, err := terminal.GetState(fd) origState, err := terminal.GetState(fd)
@@ -182,14 +179,19 @@ func (r *LightRenderer) Init() {
r.updateTerminalSize() r.updateTerminalSize()
initTheme(r.theme, r.defaultTheme(), r.forceBlack) initTheme(r.theme, r.defaultTheme(), r.forceBlack)
_, x := r.findOffset() if r.fullscreen {
if x > 0 { r.smcup()
r.upOneLine = true } else {
r.stderr("\n") r.csi("J")
} y, x := r.findOffset()
for i := 1; i < r.MaxY(); i++ { r.mouse = r.mouse && y >= 0
r.stderr("\n") if x > 0 {
r.csi("G") r.upOneLine = true
r.makeSpace()
}
for i := 1; i < r.MaxY(); i++ {
r.makeSpace()
}
} }
if r.mouse { if r.mouse {
@@ -197,12 +199,18 @@ func (r *LightRenderer) Init() {
} }
r.csi(fmt.Sprintf("%dA", r.MaxY()-1)) r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
r.csi("G") r.csi("G")
r.csi("K")
// r.csi("s") // r.csi("s")
if r.mouse { if !r.fullscreen && r.mouse {
r.yoffset, _ = r.findOffset() r.yoffset, _ = r.findOffset()
} }
} }
func (r *LightRenderer) makeSpace() {
r.stderr("\n")
r.csi("G")
}
func (r *LightRenderer) move(y int, x int) { func (r *LightRenderer) move(y int, x int) {
// w.csi("u") // w.csi("u")
if r.y < y { if r.y < y {
@@ -252,18 +260,18 @@ func (r *LightRenderer) getch(nonblock bool) (int, bool) {
} }
func (r *LightRenderer) getBytes() []byte { func (r *LightRenderer) getBytes() []byte {
return r.getBytesInternal(r.buffer) return r.getBytesInternal(r.buffer, false)
} }
func (r *LightRenderer) getBytesInternal(buffer []byte) []byte { func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
c, ok := r.getch(false) c, ok := r.getch(nonblock)
if !ok { if !nonblock && !ok {
r.Close() r.Close()
errorExit("Failed to read " + consoleDevice) errorExit("Failed to read " + consoleDevice)
} }
retries := 0 retries := 0
if c == ESC { if c == ESC || nonblock {
retries = r.escDelay / escPollInterval retries = r.escDelay / escPollInterval
} }
buffer = append(buffer, byte(c)) buffer = append(buffer, byte(c))
@@ -307,6 +315,8 @@ func (r *LightRenderer) GetChar() Event {
return Event{CtrlQ, 0, nil} return Event{CtrlQ, 0, nil}
case 127: case 127:
return Event{BSpace, 0, nil} return Event{BSpace, 0, nil}
case 0:
return Event{CtrlSpace, 0, nil}
case ESC: case ESC:
ev := r.escSequence(&sz) ev := r.escSequence(&sz)
// Second chance // Second chance
@@ -334,9 +344,10 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return Event{ESC, 0, nil} return Event{ESC, 0, nil}
} }
*sz = 2 *sz = 2
if r.buffer[1] >= 1 && r.buffer[1] <= 'z'-'a'+1 {
return Event{int(CtrlAltA + r.buffer[1] - 1), 0, nil}
}
switch r.buffer[1] { switch r.buffer[1] {
case 13:
return Event{AltEnter, 0, nil}
case 32: case 32:
return Event{AltSpace, 0, nil} return Event{AltSpace, 0, nil}
case 47: case 47:
@@ -464,7 +475,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
} }
func (r *LightRenderer) mouseSequence(sz *int) Event { func (r *LightRenderer) mouseSequence(sz *int) Event {
if len(r.buffer) < 6 || r.yoffset < 0 { if len(r.buffer) < 6 || !r.mouse {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
*sz = 6 *sz = 6
@@ -503,21 +514,49 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
func (r *LightRenderer) Pause() { func (r *LightRenderer) smcup() {
terminal.Restore(r.fd(), r.origState)
r.csi("?1049h") r.csi("?1049h")
r.flush()
} }
func (r *LightRenderer) Resume() bool { func (r *LightRenderer) rmcup() {
terminal.MakeRaw(r.fd())
r.csi("?1049l") r.csi("?1049l")
r.flush() }
// Should redraw
return true func (r *LightRenderer) Pause(clear bool) {
terminal.Restore(r.fd(), r.origState)
if clear {
if r.fullscreen {
r.rmcup()
} else {
r.smcup()
r.csi("H")
}
r.flush()
}
}
func (r *LightRenderer) Resume(clear bool) {
terminal.MakeRaw(r.fd())
if clear {
if r.fullscreen {
r.smcup()
} else {
r.rmcup()
}
r.flush()
} else if !r.fullscreen && r.mouse {
// NOTE: Resume(false) is only called on SIGCONT after SIGSTOP.
// And It's highly likely that the offset we obtained at the beginning will
// no longer be correct, so we simply disable mouse input.
r.csi("?1000l")
r.mouse = false
}
} }
func (r *LightRenderer) Clear() { func (r *LightRenderer) Clear() {
if r.fullscreen {
r.csi("H")
}
// r.csi("u") // r.csi("u")
r.origin() r.origin()
r.csi("J") r.csi("J")
@@ -534,14 +573,24 @@ func (r *LightRenderer) Refresh() {
func (r *LightRenderer) Close() { func (r *LightRenderer) Close() {
// r.csi("u") // r.csi("u")
r.origin() if r.clearOnExit {
r.csi("J") if r.fullscreen {
r.rmcup()
} else {
r.origin()
if r.upOneLine {
r.csi("A")
}
r.csi("J")
}
} else if r.fullscreen {
r.csi("G")
} else {
r.move(r.height, 0)
}
if r.mouse { if r.mouse {
r.csi("?1000l") r.csi("?1000l")
} }
if r.upOneLine {
r.csi("A")
}
r.flush() r.flush()
terminal.Restore(r.fd(), r.origState) terminal.Restore(r.fd(), r.origState)
} }
@@ -555,18 +604,18 @@ func (r *LightRenderer) MaxY() int {
} }
func (r *LightRenderer) DoesAutoWrap() bool { func (r *LightRenderer) DoesAutoWrap() bool {
return true return false
} }
func (r *LightRenderer) IsOptimized() bool { func (r *LightRenderer) IsOptimized() bool {
return false return false
} }
func (r *LightRenderer) NewWindow(top int, left int, width int, height int, border bool) Window { 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,
border: border, border: borderStyle,
top: top, top: top,
left: left, left: left,
width: width, width: width,
@@ -576,13 +625,27 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, bord
if r.theme != nil { if r.theme != nil {
w.bg = r.theme.Bg w.bg = r.theme.Bg
} }
if w.border { w.drawBorder()
w.drawBorder()
}
return w return w
} }
func (w *LightWindow) drawBorder() { func (w *LightWindow) drawBorder() {
switch w.border {
case BorderAround:
w.drawBorderAround()
case BorderHorizontal:
w.drawBorderHorizontal()
}
}
func (w *LightWindow) drawBorderHorizontal() {
w.Move(0, 0)
w.CPrint(ColBorder, AttrRegular, repeat("─", w.width))
w.Move(w.height-1, 0)
w.CPrint(ColBorder, AttrRegular, repeat("─", w.width))
}
func (w *LightWindow) drawBorderAround() {
w.Move(0, 0) w.Move(0, 0)
w.CPrint(ColBorder, AttrRegular, "┌"+repeat("─", w.width-2)+"┐") w.CPrint(ColBorder, AttrRegular, "┌"+repeat("─", w.width-2)+"┐")
for y := 1; y < w.height-1; y++ { for y := 1; y < w.height-1; y++ {
@@ -710,13 +773,17 @@ func (w *LightWindow) Print(text string) {
w.cprint2(colDefault, w.bg, AttrRegular, text) w.cprint2(colDefault, w.bg, AttrRegular, text)
} }
func cleanse(str string) string {
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) {
if !w.colored { if !w.colored {
w.csiColor(colDefault, colDefault, attrFor(pair, attr)) w.csiColor(colDefault, colDefault, attrFor(pair, attr))
} else { } else {
w.csiColor(pair.Fg(), pair.Bg(), attr) w.csiColor(pair.Fg(), pair.Bg(), attr)
} }
w.stderrInternal(text, false) w.stderrInternal(cleanse(text), false)
w.csi("m") w.csi("m")
} }
@@ -724,7 +791,7 @@ func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
if w.csiColor(fg, bg, attr) { if w.csiColor(fg, bg, attr) {
defer w.csi("m") defer w.csi("m")
} }
w.stderrInternal(text, false) w.stderrInternal(cleanse(text), false)
} }
type wrappedLine struct { type wrappedLine struct {
@@ -812,9 +879,7 @@ func (w *LightWindow) FinishFill() {
} }
func (w *LightWindow) Erase() { func (w *LightWindow) Erase() {
if w.border { w.drawBorder()
w.drawBorder()
}
// We don't erase the window here to avoid flickering during scroll // We don't erase the window here to avoid flickering during scroll
w.Move(0, 0) w.Move(0, 0)
} }

View File

@@ -1,3 +1,4 @@
// +build ncurses
// +build !windows // +build !windows
// +build !tcell // +build !tcell
@@ -32,6 +33,10 @@ import (
"unicode/utf8" "unicode/utf8"
) )
func HasFullscreenRenderer() bool {
return true
}
type Attr C.uint type Attr C.uint
type CursesWindow struct { type CursesWindow struct {
@@ -171,12 +176,11 @@ func initPairs(theme *ColorTheme) {
} }
} }
func (r *FullscreenRenderer) Pause() { func (r *FullscreenRenderer) Pause(bool) {
C.endwin() C.endwin()
} }
func (r *FullscreenRenderer) Resume() bool { func (r *FullscreenRenderer) Resume(bool) {
return false
} }
func (r *FullscreenRenderer) Close() { func (r *FullscreenRenderer) Close() {
@@ -184,12 +188,13 @@ func (r *FullscreenRenderer) Close() {
C.delscreen(_screen) C.delscreen(_screen)
} }
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, border bool) Window { 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)) win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left))
if r.theme != nil { if r.theme != nil {
C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal.index())))) C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal.index()))))
} }
if border { // FIXME Does not implement BorderHorizontal
if borderStyle != BorderNone {
pair, attr := _colorFn(ColBorder, 0) pair, attr := _colorFn(ColBorder, 0)
C.wcolor_set(win, pair, nil) C.wcolor_set(win, pair, nil)
C.wattron(win, attr) C.wattron(win, attr)
@@ -347,7 +352,7 @@ func escSequence() Event {
case C.ERR: case C.ERR:
return Event{ESC, 0, nil} return Event{ESC, 0, nil}
case CtrlM: case CtrlM:
return Event{AltEnter, 0, nil} return Event{CtrlAltM, 0, nil}
case '/': case '/':
return Event{AltSlash, 0, nil} return Event{AltSlash, 0, nil}
case ' ': case ' ':
@@ -470,6 +475,8 @@ func (r *FullscreenRenderer) GetChar() Event {
return escSequence() return escSequence()
case 127: case 127:
return Event{BSpace, 0, nil} return Event{BSpace, 0, nil}
case 0:
return Event{CtrlSpace, 0, nil}
} }
// CTRL-A ~ CTRL-Z // CTRL-A ~ CTRL-Z
if c >= CtrlA && c <= CtrlZ { if c >= CtrlA && c <= CtrlZ {

View File

@@ -15,6 +15,10 @@ import (
"github.com/junegunn/go-runewidth" "github.com/junegunn/go-runewidth"
) )
func HasFullscreenRenderer() bool {
return true
}
func (p ColorPair) style() tcell.Style { func (p ColorPair) style() tcell.Style {
style := tcell.StyleDefault style := tcell.StyleDefault
return style.Foreground(tcell.Color(p.Fg())).Background(tcell.Color(p.Bg())) return style.Foreground(tcell.Color(p.Fg())).Background(tcell.Color(p.Bg()))
@@ -23,15 +27,15 @@ func (p ColorPair) style() tcell.Style {
type Attr tcell.Style type Attr tcell.Style
type TcellWindow struct { type TcellWindow struct {
color bool color bool
top int top int
left int left int
width int width int
height int height int
lastX int lastX int
lastY int lastY int
moveCursor bool moveCursor bool
border bool borderStyle BorderStyle
} }
func (w *TcellWindow) Top() int { func (w *TcellWindow) Top() int {
@@ -57,8 +61,11 @@ func (w *TcellWindow) Refresh() {
} }
w.lastX = 0 w.lastX = 0
w.lastY = 0 w.lastY = 0
if w.border { switch w.borderStyle {
w.drawBorder() case BorderAround:
w.drawBorder(true)
case BorderHorizontal:
w.drawBorder(false)
} }
} }
@@ -214,59 +221,68 @@ func (r *FullscreenRenderer) GetChar() Event {
// process keyboard: // process keyboard:
case *tcell.EventKey: case *tcell.EventKey:
alt := (ev.Modifiers() & tcell.ModAlt) > 0 alt := (ev.Modifiers() & tcell.ModAlt) > 0
keyfn := func(r rune) int {
if alt {
return CtrlAltA - 'a' + int(r)
}
return CtrlA - 'a' + int(r)
}
switch ev.Key() { switch ev.Key() {
case tcell.KeyCtrlA: case tcell.KeyCtrlA:
return Event{CtrlA, 0, nil} return Event{keyfn('a'), 0, nil}
case tcell.KeyCtrlB: case tcell.KeyCtrlB:
return Event{CtrlB, 0, nil} return Event{keyfn('b'), 0, nil}
case tcell.KeyCtrlC: case tcell.KeyCtrlC:
return Event{CtrlC, 0, nil} return Event{keyfn('c'), 0, nil}
case tcell.KeyCtrlD: case tcell.KeyCtrlD:
return Event{CtrlD, 0, nil} return Event{keyfn('d'), 0, nil}
case tcell.KeyCtrlE: case tcell.KeyCtrlE:
return Event{CtrlE, 0, nil} return Event{keyfn('e'), 0, nil}
case tcell.KeyCtrlF: case tcell.KeyCtrlF:
return Event{CtrlF, 0, nil} return Event{keyfn('f'), 0, nil}
case tcell.KeyCtrlG: case tcell.KeyCtrlG:
return Event{CtrlG, 0, nil} return Event{keyfn('g'), 0, nil}
case tcell.KeyCtrlH:
return Event{keyfn('h'), 0, nil}
case tcell.KeyCtrlI:
return Event{keyfn('i'), 0, nil}
case tcell.KeyCtrlJ: case tcell.KeyCtrlJ:
return Event{CtrlJ, 0, nil} return Event{keyfn('j'), 0, nil}
case tcell.KeyCtrlK: case tcell.KeyCtrlK:
return Event{CtrlK, 0, nil} return Event{keyfn('k'), 0, nil}
case tcell.KeyCtrlL: case tcell.KeyCtrlL:
return Event{CtrlL, 0, nil} return Event{keyfn('l'), 0, nil}
case tcell.KeyCtrlM: case tcell.KeyCtrlM:
if alt { return Event{keyfn('m'), 0, nil}
return Event{AltEnter, 0, nil}
}
return Event{CtrlM, 0, nil}
case tcell.KeyCtrlN: case tcell.KeyCtrlN:
return Event{CtrlN, 0, nil} return Event{keyfn('n'), 0, nil}
case tcell.KeyCtrlO: case tcell.KeyCtrlO:
return Event{CtrlO, 0, nil} return Event{keyfn('o'), 0, nil}
case tcell.KeyCtrlP: case tcell.KeyCtrlP:
return Event{CtrlP, 0, nil} return Event{keyfn('p'), 0, nil}
case tcell.KeyCtrlQ: case tcell.KeyCtrlQ:
return Event{CtrlQ, 0, nil} return Event{keyfn('q'), 0, nil}
case tcell.KeyCtrlR: case tcell.KeyCtrlR:
return Event{CtrlR, 0, nil} return Event{keyfn('r'), 0, nil}
case tcell.KeyCtrlS: case tcell.KeyCtrlS:
return Event{CtrlS, 0, nil} return Event{keyfn('s'), 0, nil}
case tcell.KeyCtrlT: case tcell.KeyCtrlT:
return Event{CtrlT, 0, nil} return Event{keyfn('t'), 0, nil}
case tcell.KeyCtrlU: case tcell.KeyCtrlU:
return Event{CtrlU, 0, nil} return Event{keyfn('u'), 0, nil}
case tcell.KeyCtrlV: case tcell.KeyCtrlV:
return Event{CtrlV, 0, nil} return Event{keyfn('v'), 0, nil}
case tcell.KeyCtrlW: case tcell.KeyCtrlW:
return Event{CtrlW, 0, nil} return Event{keyfn('w'), 0, nil}
case tcell.KeyCtrlX: case tcell.KeyCtrlX:
return Event{CtrlX, 0, nil} return Event{keyfn('x'), 0, nil}
case tcell.KeyCtrlY: case tcell.KeyCtrlY:
return Event{CtrlY, 0, nil} return Event{keyfn('y'), 0, nil}
case tcell.KeyCtrlZ: case tcell.KeyCtrlZ:
return Event{CtrlZ, 0, nil} return Event{keyfn('z'), 0, nil}
case tcell.KeyBackspace, tcell.KeyBackspace2: case tcell.KeyCtrlSpace:
return Event{CtrlSpace, 0, nil}
case tcell.KeyBackspace2:
if alt { if alt {
return Event{AltBS, 0, nil} return Event{AltBS, 0, nil}
} }
@@ -292,8 +308,6 @@ func (r *FullscreenRenderer) GetChar() Event {
case tcell.KeyPgDn: case tcell.KeyPgDn:
return Event{PgDn, 0, nil} return Event{PgDn, 0, nil}
case tcell.KeyTab:
return Event{Tab, 0, nil}
case tcell.KeyBacktab: case tcell.KeyBacktab:
return Event{BTab, 0, nil} return Event{BTab, 0, nil}
@@ -350,13 +364,12 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
func (r *FullscreenRenderer) Pause() { func (r *FullscreenRenderer) Pause(bool) {
_screen.Fini() _screen.Fini()
} }
func (r *FullscreenRenderer) Resume() bool { func (r *FullscreenRenderer) Resume(bool) {
r.initScreen() r.initScreen()
return true
} }
func (r *FullscreenRenderer) Close() { func (r *FullscreenRenderer) Close() {
@@ -371,15 +384,15 @@ func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
_screen.Show() _screen.Show()
} }
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, border bool) Window { func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
// TODO // TODO
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,
border: border} borderStyle: borderStyle}
} }
func (w *TcellWindow) Close() { func (w *TcellWindow) Close() {
@@ -530,7 +543,7 @@ func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
return w.fillString(str, ColorPair{fg, bg, -1}, a) return w.fillString(str, ColorPair{fg, bg, -1}, a)
} }
func (w *TcellWindow) drawBorder() { 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
@@ -548,13 +561,15 @@ func (w *TcellWindow) drawBorder() {
_screen.SetContent(x, bot-1, tcell.RuneHLine, nil, style) _screen.SetContent(x, bot-1, tcell.RuneHLine, nil, style)
} }
for y := top; y < bot; y++ { if around {
_screen.SetContent(left, y, tcell.RuneVLine, nil, style) for y := top; y < bot; y++ {
_screen.SetContent(right-1, y, tcell.RuneVLine, nil, style) _screen.SetContent(left, y, tcell.RuneVLine, nil, style)
} _screen.SetContent(right-1, y, tcell.RuneVLine, nil, style)
}
_screen.SetContent(left, top, tcell.RuneULCorner, nil, style) _screen.SetContent(left, top, tcell.RuneULCorner, nil, style)
_screen.SetContent(right-1, top, tcell.RuneURCorner, nil, style) _screen.SetContent(right-1, top, tcell.RuneURCorner, nil, style)
_screen.SetContent(left, bot-1, tcell.RuneLLCorner, nil, style) _screen.SetContent(left, bot-1, tcell.RuneLLCorner, nil, style)
_screen.SetContent(right-1, bot-1, tcell.RuneLRCorner, nil, style) _screen.SetContent(right-1, bot-1, tcell.RuneLRCorner, nil, style)
}
} }

View File

@@ -38,6 +38,7 @@ const (
CtrlY CtrlY
CtrlZ CtrlZ
ESC ESC
CtrlSpace
Invalid Invalid
Resize Resize
@@ -74,7 +75,6 @@ const (
F11 F11
F12 F12
AltEnter
AltSpace AltSpace
AltSlash AltSlash
AltBS AltBS
@@ -89,7 +89,9 @@ const ( // Reset iota
AltD AltD
AltE AltE
AltF AltF
AltZ = AltA + 'z' - 'a' AltZ = AltA + 'z' - 'a'
CtrlAltA = AltZ + 1
CtrlAltM = CtrlAltA + 'm' - 'a'
) )
const ( const (
@@ -175,6 +177,10 @@ 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
@@ -190,10 +196,18 @@ type MouseEvent struct {
Mod bool Mod bool
} }
type BorderStyle int
const (
BorderNone BorderStyle = iota
BorderAround
BorderHorizontal
)
type Renderer interface { type Renderer interface {
Init() Init()
Pause() Pause(clear bool)
Resume() bool Resume(clear bool)
Clear() Clear()
RefreshWindows(windows []Window) RefreshWindows(windows []Window)
Refresh() Refresh()
@@ -206,7 +220,7 @@ type Renderer interface {
DoesAutoWrap() bool DoesAutoWrap() bool
IsOptimized() bool IsOptimized() bool
NewWindow(top int, left int, width int, height int, border bool) Window NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window
} }
type Window interface { type Window interface {

View File

@@ -23,21 +23,23 @@ end
# List assets # List assets
assets = Hash[rel['assets'].map { |a| a.values_at *%w[name id] }] assets = Hash[rel['assets'].map { |a| a.values_at *%w[name id] }]
files.select { |f| File.exists? f }.each do |file| files.select { |f| File.exists? f }.map do |file|
name = File.basename file Thread.new do
name = File.basename file
if asset_id = assets[name] if asset_id = assets[name]
puts "#{name} found. Deleting asset id #{asset_id}." puts "#{name} found. Deleting asset id #{asset_id}."
RestClient.delete "#{base}/assets/#{asset_id}", RestClient.delete "#{base}/assets/#{asset_id}",
:authorization => "token #{token}" :authorization => "token #{token}"
else else
puts "#{name} not found" puts "#{name} not found"
end
puts "Uploading #{name}"
RestClient.post(
"#{base.sub 'api', 'uploads'}/#{rel['id']}/assets?name=#{name}",
File.read(file),
:authorization => "token #{token}",
:content_type => "application/octet-stream")
end end
end.each(&:join)
puts "Uploading #{name}"
RestClient.post(
"#{base.sub 'api', 'uploads'}/#{rel['id']}/assets?name=#{name}",
File.read(file),
:authorization => "token #{token}",
:content_type => "application/octet-stream")
end

View File

@@ -111,7 +111,7 @@ class Tmux
File.read(TEMPNAME).split($/)[0, @lines].reverse.drop_while(&:empty?).reverse File.read(TEMPNAME).split($/)[0, @lines].reverse.drop_while(&:empty?).reverse
end end
def until pane = 0 def until refresh = false, pane = 0
lines = nil lines = nil
begin begin
wait do wait do
@@ -141,7 +141,9 @@ class Tmux
self.select { |line| line.send method, val }.first self.select { |line| line.send method, val }.first
end end
end end
yield lines yield(lines).tap do |ok|
send_keys 'C-l' if refresh && !ok
end
end end
rescue Exception rescue Exception
puts $!.backtrace puts $!.backtrace
@@ -511,11 +513,11 @@ class TestGoFZF < TestBase
tmux.send_keys "seq 1 111 | #{fzf "-m +s --tac #{opt} -q11"}", :Enter tmux.send_keys "seq 1 111 | #{fzf "-m +s --tac #{opt} -q11"}", :Enter
tmux.until { |lines| lines[-3].include? '> 111' } tmux.until { |lines| lines[-3].include? '> 111' }
tmux.send_keys :Tab tmux.send_keys :Tab
tmux.until { |lines| lines[-2].include? '4/111 (1)' } tmux.until { |lines| lines[-2].include? '4/111 -S (1)' }
tmux.send_keys 'C-R' tmux.send_keys 'C-R'
tmux.until { |lines| lines[-3].include? '> 11' } tmux.until { |lines| lines[-3].include? '> 11' }
tmux.send_keys :Tab tmux.send_keys :Tab
tmux.until { |lines| lines[-2].include? '4/111/S (2)' } tmux.until { |lines| lines[-2].include? '4/111 +S (2)' }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal ['111', '11'], readonce.split($/) assert_equal ['111', '11'], readonce.split($/)
end end
@@ -615,6 +617,17 @@ class TestGoFZF < TestBase
], `#{FZF} -foo --tiebreak=begin,length < #{tempname}`.split($/) ], `#{FZF} -foo --tiebreak=begin,length < #{tempname}`.split($/)
end end
def test_tiebreak_begin_algo_v2
writelines tempname, [
'baz foo bar',
'foo bar baz',
]
assert_equal [
'foo bar baz',
'baz foo bar',
], `#{FZF} -fbar --tiebreak=begin --algo=v2 < #{tempname}`.split($/)
end
def test_tiebreak_end def test_tiebreak_end
writelines tempname, [ writelines tempname, [
'xoxxxxxxxx', 'xoxxxxxxxx',
@@ -877,7 +890,7 @@ class TestGoFZF < TestBase
def test_execute_multi def test_execute_multi
output = '/tmp/fzf-test-execute-multi' output = '/tmp/fzf-test-execute-multi'
opts = %[--multi --bind \\"alt-a:execute-multi(echo {}/{} >> #{output}; sync)\\"] opts = %[--multi --bind \\"alt-a:execute-multi(echo {}/{+} >> #{output}; sync)\\"]
writelines tempname, %w[foo'bar foo"bar foo$bar foobar] writelines tempname, %w[foo'bar foo"bar foo$bar foobar]
tmux.send_keys "cat #{tempname} | #{fzf opts}", :Enter tmux.send_keys "cat #{tempname} | #{fzf opts}", :Enter
tmux.until { |lines| lines[-2].include? '4/4' } tmux.until { |lines| lines[-2].include? '4/4' }
@@ -900,6 +913,43 @@ class TestGoFZF < TestBase
File.unlink output rescue nil File.unlink output rescue nil
end end
def test_execute_plus_flag
output = tempname + ".tmp"
File.unlink output rescue nil
writelines tempname, ["foo bar", "123 456"]
tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter
execute = lambda do
tmux.send_keys 'x', 'y'
tmux.until { |lines| lines[-2].include? '0/2' }
tmux.send_keys :BSpace
tmux.until { |lines| lines[-2].include? '2/2' }
end
tmux.until { |lines| lines[-2].include? '2/2' }
execute.call
tmux.send_keys :Up
tmux.send_keys :Tab
execute.call
tmux.send_keys :Tab
execute.call
tmux.send_keys :Enter
tmux.prepare
readonce
assert_equal [
%[foo bar/foo bar/bar/bar],
%[123 456/foo bar/456/bar],
%[123 456 foo bar/foo bar/456 bar/bar]
], File.readlines(output).map(&:chomp)
rescue
File.unlink output rescue nil
end
def test_execute_shell def test_execute_shell
# Custom script to use as $SHELL # Custom script to use as $SHELL
output = tempname + '.out' output = tempname + '.out'
@@ -1067,7 +1117,7 @@ class TestGoFZF < TestBase
}.each do |ts, exp| }.each do |ts, exp|
tmux.prepare tmux.prepare
tmux.send_keys %[cat #{tempname} | fzf --tabstop=#{ts}], :Enter tmux.send_keys %[cat #{tempname} | fzf --tabstop=#{ts}], :Enter
tmux.until { |lines| exp.start_with? lines[-3].to_s.strip.sub(/\.\.$/, '') } tmux.until(true) { |lines| exp.start_with? lines[-3].to_s.strip.sub(/\.\.$/, '') }
tmux.send_keys :Enter tmux.send_keys :Enter
end end
end end
@@ -1093,12 +1143,6 @@ class TestGoFZF < TestBase
assert_equal 1, $?.exitstatus assert_equal 1, $?.exitstatus
end end
def test_invalid_term
lines = `TERM=xxx #{FZF} 2>&1`
assert_equal 2, $?.exitstatus
assert lines.include?('Invalid $TERM: xxx') || lines.include?('terminal entry not found')
end
def test_invalid_option def test_invalid_option
lines = `#{FZF} --foobar 2>&1` lines = `#{FZF} --foobar 2>&1`
assert_equal 2, $?.exitstatus assert_equal 2, $?.exitstatus
@@ -1202,7 +1246,7 @@ class TestGoFZF < TestBase
end end
def test_preview def test_preview
tmux.send_keys %[seq 1000 | sed s/^2$// | #{FZF} --preview 'sleep 0.2; echo {{}-{}}' --bind ?:toggle-preview], :Enter tmux.send_keys %[seq 1000 | sed s/^2$// | #{FZF} -m --preview 'sleep 0.2; echo {{}-{+}}' --bind ?:toggle-preview], :Enter
tmux.until { |lines| lines[1].include?(' {1-1}') } tmux.until { |lines| lines[1].include?(' {1-1}') }
tmux.send_keys :Up tmux.send_keys :Up
tmux.until { |lines| lines[1].include?(' {-}') } tmux.until { |lines| lines[1].include?(' {-}') }
@@ -1216,6 +1260,17 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-2].start_with? ' 28/1000' } tmux.until { |lines| lines[-2].start_with? ' 28/1000' }
tmux.send_keys 'foobar' tmux.send_keys 'foobar'
tmux.until { |lines| !lines[1].include?('{') } tmux.until { |lines| !lines[1].include?('{') }
tmux.send_keys 'C-u'
tmux.until { |lines| lines.match_count == 1000 }
tmux.until { |lines| lines[1].include?(' {1-1}') }
tmux.send_keys :BTab
tmux.until { |lines| lines[1].include?(' {-1}') }
tmux.send_keys :BTab
tmux.until { |lines| lines[1].include?(' {3-1 }') }
tmux.send_keys :BTab
tmux.until { |lines| lines[1].include?(' {4-1 3}') }
tmux.send_keys :BTab
tmux.until { |lines| lines[1].include?(' {5-1 3 4}') }
end end
def test_preview_hidden def test_preview_hidden
@@ -1228,6 +1283,27 @@ class TestGoFZF < TestBase
tmux.send_keys '?' tmux.send_keys '?'
tmux.until { |lines| lines[-1] == '> 555' } tmux.until { |lines| lines[-1] == '> 555' }
end end
def test_preview_size_0
File.unlink tempname rescue nil
tmux.send_keys %[seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0], :Enter
tmux.until { |lines| lines.item_count == 100 && lines[1] == ' 100/100' && lines[2] == '> 1' }
tmux.until { |_| %w[1] == File.readlines(tempname).map(&:chomp) }
tmux.send_keys :Down
tmux.until { |lines| lines[3] == '> 2' }
tmux.until { |_| %w[1 2] == File.readlines(tempname).map(&:chomp) }
tmux.send_keys :Down
tmux.until { |lines| lines[4] == '> 3' }
tmux.until { |_| %w[1 2 3] == File.readlines(tempname).map(&:chomp) }
end
def test_no_clear
tmux.send_keys 'seq 100 | fzf --no-clear --inline-info --height 5', :Enter
prompt = '> < 100/100'
tmux.until { |lines| lines[-1] == prompt }
tmux.send_keys :Enter
tmux.until { |lines| lines[-2] == prompt && lines[-1] == '1' }
end
end end
module TestShell module TestShell
@@ -1340,6 +1416,7 @@ module TestShell
tmux.send_keys 'C-r' tmux.send_keys 'C-r'
tmux.until { |lines| lines.item_count > 0 } tmux.until { |lines| lines.item_count > 0 }
end end
tmux.send_keys 'C-r'
tmux.send_keys '3d' tmux.send_keys '3d'
tmux.until { |lines| lines[-3].end_with? 'echo 3rd' } tmux.until { |lines| lines[-3].end_with? 'echo 3rd' }
tmux.send_keys :Enter tmux.send_keys :Enter
@@ -1375,8 +1452,7 @@ module CompletionTest
tmux.send_keys :Tab, :Tab tmux.send_keys :Tab, :Tab
tmux.until { |lines| lines.select_count == 2 } tmux.until { |lines| lines.select_count == 2 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until(true) do |lines|
tmux.send_keys 'C-L'
lines[-1].include?('/tmp/fzf-test/10') && lines[-1].include?('/tmp/fzf-test/10') &&
lines[-1].include?('/tmp/fzf-test/100') lines[-1].include?('/tmp/fzf-test/100')
end end
@@ -1388,20 +1464,16 @@ module CompletionTest
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
tmux.until do |lines| tmux.until(true) do |lines|
tmux.send_keys 'C-L'
lines[-1].end_with?('.fzf-home') lines[-1].end_with?('.fzf-home')
end end
# ~INVALID_USERNAME**<TAB> # ~INVALID_USERNAME**<TAB>
tmux.send_keys 'C-u' tmux.send_keys 'C-u'
tmux.send_keys "cat ~such**", :Tab tmux.send_keys "cat ~such**", :Tab
tmux.until { |lines| lines.any_include? 'no~such~user' } tmux.until(true) { |lines| lines.any_include? 'no~such~user' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until(true) { |lines| lines[-1].end_with?('no~such~user') }
tmux.send_keys 'C-L'
lines[-1].end_with?('no~such~user')
end
# /tmp/fzf\ test**<TAB> # /tmp/fzf\ test**<TAB>
tmux.send_keys 'C-u' tmux.send_keys 'C-u'
@@ -1410,19 +1482,13 @@ module CompletionTest
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
tmux.until do |lines| tmux.until(true) { |lines| lines[-1].end_with?('/tmp/fzf\ test/foobar') }
tmux.send_keys 'C-L'
lines[-1].end_with?('/tmp/fzf\ test/foobar')
end
# Should include hidden files # Should include hidden files
(1..100).each { |i| FileUtils.touch "/tmp/fzf-test/.hidden-#{i}" } (1..100).each { |i| FileUtils.touch "/tmp/fzf-test/.hidden-#{i}" }
tmux.send_keys 'C-u' tmux.send_keys 'C-u'
tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab
tmux.until do |lines| tmux.until(true) { |lines| lines.match_count == 100 && lines.any_include?('/tmp/fzf-test/.hidden-') }
tmux.send_keys 'C-L'
lines.match_count == 100 && lines.any_include?('/tmp/fzf-test/.hidden-')
end
tmux.send_keys :Enter tmux.send_keys :Enter
ensure ensure
['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f| ['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f|
@@ -1448,10 +1514,7 @@ module CompletionTest
tmux.send_keys 55 tmux.send_keys 55
tmux.until { |lines| lines.match_count == 1 } tmux.until { |lines| lines.match_count == 1 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until(true) { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/' }
tmux.send_keys 'C-L'
lines[-1] == 'cd /tmp/fzf-test/d55/'
end
tmux.send_keys :xx tmux.send_keys :xx
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' } tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' }
@@ -1479,10 +1542,7 @@ module CompletionTest
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
tmux.until do |lines| tmux.until(true) { |lines| lines[-1].include? "kill #{pid}" }
tmux.send_keys 'C-L'
lines[-1].include? "kill #{pid}"
end
ensure ensure
Process.kill 'KILL', pid.to_i rescue nil if pid Process.kill 'KILL', pid.to_i rescue nil if pid
end end
@@ -1495,10 +1555,7 @@ module CompletionTest
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
tmux.until do |lines| tmux.until(true) { |lines| lines[-1] == "ls /tmp 1 2" }
tmux.send_keys 'C-L'
lines[-1] == "ls /tmp 1 2"
end
end end
def test_unset_completion def test_unset_completion
@@ -1506,7 +1563,7 @@ module CompletionTest
tmux.prepare tmux.prepare
# Using tmux # Using tmux
tmux.send_keys 'unset FZFFOO**', :Tab tmux.send_keys 'unset FZFFOOBR**', :Tab
tmux.until { |lines| lines.match_count == 1 } tmux.until { |lines| lines.match_count == 1 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' } tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' }
@@ -1514,8 +1571,8 @@ module CompletionTest
# FZF_TMUX=1 # FZF_TMUX=1
new_shell new_shell
tmux.send_keys 'unset FZFFO**', :Tab, pane: 0 tmux.send_keys 'unset FZFFOOBR**', :Tab, pane: 0
tmux.until(1) { |lines| lines.match_count == 1 } tmux.until(false, 1) { |lines| lines.match_count == 1 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' } tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' }
end end
@@ -1541,10 +1598,7 @@ module CompletionTest
tmux.until { |lines| lines.select_count == 2 } tmux.until { |lines| lines.select_count == 2 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until(true) { |lines| lines.any_include? 'cat' }
tmux.send_keys 'C-l'
lines.any_include? 'cat'
end
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include? 'test3test4' } tmux.until { |lines| lines[-1].include? 'test3test4' }
end end