Compare commits

...

47 Commits

Author SHA1 Message Date
Junegunn Choi
355d004895 [neovim] Fix error with {'window': 'enew'} (#274) 2015-06-21 21:45:10 +09:00
Junegunn Choi
a336494f5d 0.10.0 2015-06-21 17:40:36 +09:00
Junegunn Choi
8270f7f0ca Rename --null to --read0 and undocument the option
`--null` is ambiguous. For completeness' sake, we need both `--read0`
and `--print0`.

`--read0` only makes sense when the input contains multiline entries.
However, fzf currently cannot correctly display multiline entries,
I'm going to make `--read0` an undocumented feature.
2015-06-21 17:29:58 +09:00
Junegunn Choi
638a956a9e Merge pull request #272 from okapia/zsh-simplify
Use vi-fetch-history on zsh to get history line
2015-06-21 16:57:14 +09:00
Oliver Kiddle
d395ebd28f use vi-fetch-history on zsh to get history line
In addition to being simpler, it allows subsequent up/down history
or accept-line-and-down-history widgets to work.
Also allow for find being and alias if alias expansion
after command is enabled.
2015-06-21 09:21:35 +02:00
Junegunn Choi
c0d3faa84f Hide --toggle-sort from --help output
Since the same can be now achieved with --bind KEY:toggle-sort
2015-06-19 01:06:56 +09:00
Junegunn Choi
3492c8b780 Rename --history-max to --history-size
Considering HISTSIZE and HISTFILESIZE of bash
2015-06-19 01:03:25 +09:00
Junegunn Choi
a8b2c257cd Improve handling of key names
Remember the exact string given as the key name so that it's possible to
correctly handle synonyms and print the original string.
2015-06-19 00:31:48 +09:00
Junegunn Choi
5e8d8dab82 More key names for --bind 2015-06-18 02:27:50 +09:00
Junegunn Choi
b504c6eb39 Avoid intermittent test failures
by making sure that we're back on shell command-line
2015-06-18 02:09:44 +09:00
Junegunn Choi
d261c36cde Keep the spinner spinning even when the source stream is idle 2015-06-18 00:42:38 +09:00
Junegunn Choi
fe4e452d68 Add --cycle option for cyclic scrolling
Close #266
2015-06-16 23:16:34 +09:00
Junegunn Choi
d54a4fa223 Add key name "bspace" for --bind (bspace != ctrl-h) 2015-06-16 02:18:49 +09:00
Junegunn Choi
45bd323cab Allow binding CTRL-G and CTRL-Q 2015-06-16 02:17:06 +09:00
Junegunn Choi
8677dbded1 Change alternative notation for execute action (#265)
e.g. fzf --bind "ctrl-m:execute:COMMAND..." --bind ctrl-j:accept
2015-06-15 23:27:05 +09:00
Junegunn Choi
794ad5785d Fix . to match newlines as well (#265) 2015-06-15 23:11:22 +09:00
Junegunn Choi
fa5b58968e Add alternative execute notation that does not require closing char
This can be used to avoid parse errors that can happen when the command
contains the closing character. Since the command does not finish at
a certain character, the key binding should be the last one in the
group. Suggested by @tiziano88. (#265)

  e.g. fzf --bind "ctrl-m:execute=COMMAND..." --bind ctrl-j:accept
2015-06-15 23:00:38 +09:00
Junegunn Choi
e720f56ea8 Fix test code for docker build 2015-06-15 22:45:31 +09:00
Junegunn Choi
7db53e6459 Add synonyms for some keys to be used with --bind and --toggle-sort
enter (return), space, tab, btab, esc, up, down, left, right
2015-06-15 01:26:18 +09:00
Junegunn Choi
e287bd7f04 Fix Travis CI build 2015-06-14 23:44:42 +09:00
Junegunn Choi
022435a90a More alternative notations for execute action
execute(...)
    execute[...]
    execute~...~
    execute!...!
    execute@...@
    execute#...#
    execute$...$
    execute%...%
    execute^...^
    execute&...&
    execute*...*
    execute:...:
    execute;...;
    execute/.../
    execute|...|
2015-06-14 23:36:49 +09:00
Junegunn Choi
6c99cc1700 Add bind action for executing arbitrary command (#265)
e.g. fzf --bind "ctrl-m:execute(less {})"
     fzf --bind "ctrl-t:execute[tmux new-window -d 'vim {}']"
2015-06-14 12:25:08 +09:00
Junegunn Choi
fe5b190a7d Remove unnecessary regexp matches
This change does have positive effect on startup time of fzf when many
number of options are provided.

    time fzf --query=____ --filter=____ --delimiter=q --prompt=________ \
    --nth=1,2,3,4 --with-nth=1,2,3,4 --toggle-sort=ctrl-r \
    --expect=ctrl-x --tiebreak=index --color=light --bind=ctrl-t:accept \
    --history=/tmp/xxx --history-max=1000 --help

    0m0.013s -> 0m0.008s
2015-06-14 11:23:07 +09:00
Junegunn Choi
77bab51696 GoLint fix 2015-06-14 03:19:18 +09:00
Junegunn Choi
77048f3e3b Fix Travis CI build 2015-06-14 02:51:45 +09:00
Junegunn Choi
8b618f7439 Test refactoring 2015-06-14 02:44:22 +09:00
Junegunn Choi
8973207bb4 Fix Travis CI build 2015-06-14 02:13:02 +09:00
Junegunn Choi
6ad1736832 Fix ignore action 2015-06-14 02:11:27 +09:00
Junegunn Choi
9fca611c4a Add ignore action for --bind 2015-06-14 01:54:56 +09:00
Junegunn Choi
8e7164553f Add missing files from the previous commit
:(
2015-06-14 00:53:45 +09:00
Junegunn Choi
3b52811796 Add support for search history
- Add `--history` option (e.g. fzf --history ~/.fzf.history)
- Add `--history-max` option for limiting the size of the file (default 1000)
- Add `previous-history` and `next-history` actions for `--bind`
    - CTRL-P and CTRL-N are automatically remapped to these actions when
      `--history` is used

Closes #249, #251
2015-06-14 00:48:48 +09:00
Junegunn Choi
2e84b1db64 Merge pull request #264 from kassio/master
Do not rename terminal buffer
2015-06-14 00:11:10 +09:00
Kassio Borges
9f33068ab3 Avoid conflict with other neoterm plugins.
To avoid conflict with other neoterm plugins that manage terminals,
prefer named terminals.
2015-06-13 11:13:33 -03:00
Junegunn Choi
eaa3c67a5e Add actions for --bind: select-all / deselect-all / toggle-all
Close #257
2015-06-09 23:44:54 +09:00
Junegunn Choi
1b9b1d15bc Adjust --help output 2015-06-08 23:28:41 +09:00
Junegunn Choi
97f433a274 Merge branch 'dullgiulio-121-accept-nil-input' 2015-06-08 23:28:06 +09:00
Junegunn Choi
45a3655eaf Add test case for --null option 2015-06-08 23:27:50 +09:00
Junegunn Choi
81ffde92fb Merge branch '121-accept-nil-input' of https://github.com/dullgiulio/fzf into dullgiulio-121-accept-nil-input 2015-06-08 23:21:16 +09:00
Junegunn Choi
0be4cead20 Allow ^EqualMatch$ 2015-06-08 23:17:24 +09:00
Giulio Iotti
f6dd32046e add support to nil-byte separated input strings, closes #121 2015-06-08 08:38:40 +00:00
Junegunn Choi
443a80f254 Always use the same color for multi-select markers 2015-06-07 23:32:07 +09:00
Junegunn Choi
8017635a71 Merge pull request #252 from dominikh/portable-swapOutput
Use ncurses's newterm instead of swapping stdout and stderr
2015-06-07 14:31:44 +09:00
Dominik Honnef
98f62b191a Use ncurses's newterm instead of swapping stdout and stderr 2015-06-07 07:26:26 +02:00
Junegunn Choi
52771a6226 0.9.13 2015-06-03 02:09:07 +09:00
Junegunn Choi
b00bcf506e Fix #248 - Premature termination of Reader on long input 2015-06-03 01:48:02 +09:00
Junegunn Choi
fdbfe36c0b Color customization (#245) 2015-06-03 01:46:03 +09:00
Junegunn Choi
446e822723 Update CHANGELOG 2015-05-22 02:37:38 +09:00
21 changed files with 1319 additions and 341 deletions

View File

@@ -1,6 +1,68 @@
CHANGELOG
=========
0.10.0
------
### New features
- More actions for `--bind`
- `select-all`
- `deselect-all`
- `toggle-all`
- `ignore`
- `execute(...)` action for running arbitrary command without leaving fzf
- `fzf --bind "ctrl-m:execute(less {})"`
- `fzf --bind "ctrl-t:execute(tmux new-window -d 'vim {}')"`
- If the command contains parentheses, use any of the follows alternative
notations to avoid parse errors
- `execute[...]`
- `execute~...~`
- `execute!...!`
- `execute@...@`
- `execute#...#`
- `execute$...$`
- `execute%...%`
- `execute^...^`
- `execute&...&`
- `execute*...*`
- `execute;...;`
- `execute/.../`
- `execute|...|`
- `execute:...`
- This is the special form that frees you from parse errors as it
does not expect the closing character
- The catch is that it should be the last one in the
comma-separated list
- Added support for optional search history
- `--history HISTORY_FILE`
- When used, `CTRL-N` and `CTRL-P` are automatically remapped to
`next-history` and `previous-history`
- `--history-size MAX_ENTRIES` (default: 1000)
- Cyclic scrolling can be enabled with `--cycle`
- Fixed the bug where the spinner was not spinning on idle input stream
- e.g. `sleep 100 | fzf`
### Minor improvements/fixes
- Added synonyms for key names that can be specified for `--bind`,
`--toggle-sort`, and `--expect`
- Fixed the color of multi-select marker on the current line
- Fixed to allow `^pattern$` in extended-search mode
0.9.13
------
### New features
- Color customization with the extended `--color` option
### Bug fixes
- Fixed premature termination of Reader in the presence of a long line which
is longer than 64KB
0.9.12
------
@@ -11,6 +73,7 @@ CHANGELOG
### Bug fixes
- Fixed to update "inline-info" immediately after terminal resize
- Fixed ANSI code offset calculation
0.9.11
------

6
fzf
View File

@@ -206,11 +206,11 @@ class FZF
@expect = true
when /^--expect=(.*)$/
@expect = true
when '--toggle-sort', '--tiebreak', '--color', '--bind'
when '--toggle-sort', '--tiebreak', '--color', '--bind', '--history', '--history-size'
argv.shift
when '--tac', '--no-tac', '--sync', '--no-sync', '--hscroll', '--no-hscroll',
'--inline-info', '--no-inline-info', /^--bind=(.*)$/,
/^--color=(.*)$/, /^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/
'--inline-info', '--no-inline-info', '--read0', '--cycle', /^--bind=(.*)$/,
/^--color=(.*)$/, /^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/, /^--history(-max)?=(.*)$/
# XXX
else
usage 1, "illegal option: #{o}"

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
version=0.9.12
version=0.10.0
cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd)

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
THE SOFTWARE.
..
.TH fzf 1 "May 2015" "fzf 0.9.12" "fzf - a command-line fuzzy finder"
.TH fzf 1 "June 2015" "fzf 0.10.0" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -52,10 +52,10 @@ Comma-separated list of field index expressions for limiting search scope.
See \fBFIELD INDEX EXPRESSION\fR for details.
.TP
.BI "--with-nth=" "N[,..]"
Transform the item using the list of index expressions for search
Transform each item using index expressions within finder
.TP
.BI "-d, --delimiter=" "STR"
Field delimiter regex for \fI--nth\fR and \fI--with-nth\fR (default: AWK-style)
Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style)
.SS Search result
.TP
.B "+s, --no-sort"
@@ -91,21 +91,38 @@ Enable processing of ANSI color codes
.B "--no-mouse"
Disable mouse
.TP
.B "--color=COL"
Color scheme: [dark|light|16|bw]
.br
(default: dark on 256-color terminal, otherwise 16)
.br
.R ""
.br
.BR dark " Color scheme for dark 256-color terminal"
.br
.BR light " Color scheme for light 256-color terminal"
.br
.BR 16 " Color scheme for 16-color terminal"
.br
.BR bw " No colors"
.br
.BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]"
Color configuration. The name of the base color scheme is followed by custom
color mappings. Ansi color code of -1 denotes terminal default
foreground/background color.
.RS
e.g. \fBfzf --color=bg+:24\fR
\fBfzf --color=light,fg:232,bg:255,bg+:116,info:27\fR
.RE
.RS
.B BASE SCHEME:
(default: dark on 256-color terminal, otherwise 16)
\fBdark \fRColor scheme for dark 256-color terminal
\fBlight \fRColor scheme for light 256-color terminal
\fB16 \fRColor scheme for 16-color terminal
\fBbw \fRNo colors
.B COLOR:
\fBfg \fRText
\fBbg \fRBackground
\fBhl \fRHighlighted substrings
\fBfg+ \fRText (current line)
\fBbg+ \fRBackground (current line)
\fBhl+ \fRHighlighted substrings (current line)
\fBinfo \fRInfo
\fBprompt \fRPrompt
\fBpointer \fRPointer to the current line
\fBmarker \fRMulti-select marker
\fBspinner \fRStreaming input indicator
.RE
.TP
.B "--black"
Use black background
@@ -113,6 +130,9 @@ Use black background
.B "--reverse"
Reverse orientation
.TP
.B "--cycle"
Enable cyclic scroll
.TP
.B "--no-hscroll"
Disable horizontal scroll
.TP
@@ -123,8 +143,7 @@ Display finder info inline with the query
Input prompt (default: '> ')
.TP
.BI "--toggle-sort=" "KEY"
Key to toggle sort (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR,
or any single character)
Key to toggle sort. For the list of the allowed key names, see \fB--bind\fR.
.TP
.BI "--bind=" "KEYBINDS"
Comma-separated list of custom key bindings. Each key binding expression
@@ -134,38 +153,109 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
.RE
.RS
.B KEY:
\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR, or any single character
.B AVAILABLE KEYS:
\fIctrl-[a-z]\fR
\fIalt-[a-z]\fR
\fIf[1-4]\fR
\fIenter\fR (\fIreturn\fR)
\fIspace\fR
\fIbspace\fR (\fIbs\fR)
\fIalt-bspace\fR (\fIalt-bs\fR)
\fItab\fR
\fIbtab\fR (\fIshift-tab\fR)
\fIesc\fR
\fIdel\fR
\fIup\fR
\fIdown\fR
\fIleft\fR
\fIright\fR
\fIhome\fR
\fIend\fR
\fIpgup\fR (\fIpage-up\fR)
\fIpgdn\fR (\fIpage-down\fR)
\fIshift-left\fR
\fIshift-right\fR
or any single character
.RE
.RS
.B ACTION:
abort
accept
backward-char
backward-delete-char
backward-kill-word
backward-word
beginning-of-line
clear-screen
delete-char
down
end-of-line
forward-char
forward-word
kill-line (not bound)
kill-word
page-down
page-up
toggle (not bound)
toggle-down
toggle-sort (not bound; equivalent to \fB--toggle-sort\fR)
toggle-up
unix-line-discard
unix-word-rubout
up
yank
\fBACTION: DEFAULT BINDINGS:
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIctrl-m (enter)\fR
\fBbackward-char\fR \fIctrl-b left\fR
\fBbackward-delete-char\fR \fIctrl-h bspace\fR
\fBbackward-kill-word\fR \fIalt-bs\fR
\fBbackward-word\fR \fIalt-b shift-left\fR
\fBbeginning-of-line\fR \fIctrl-a home\fR
\fBclear-screen\fR \fIctrl-l\fR
\fBdelete-char\fR \fIctrl-d del\fR
\fBdeselect-all\fR
\fBdown\fR \fIctrl-j ctrl-n down\fR
\fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details)
\fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR
\fBignore\fR
\fBkill-line\fR
\fBkill-word\fR \fIalt-d\fR
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
\fBpage-down\fR \fIpgdn\fR
\fBpage-up\fR \fIpgup\fR
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBselect-all\fR
\fBtoggle\fR
\fBtoggle-all\fR
\fBtoggle-down\fR \fIctrl-i (tab)\fR
\fBtoggle-sort\fR (equivalent to \fB--toggle-sort\fR)
\fBtoggle-up\fR \fIbtab (shift-tab)\fR
\fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR
\fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR
.RE
.RS
With \fBexecute(...)\fR action, you can execute arbitrary commands without
leaving fzf. For example, you can turn fzf into a simple file browser by
binding \fBenter\fR key to \fBless\fR command like follows.
.RS
\fBfzf --bind "enter:execute(less {})"\fR
.RE
\fB{}\fR is the placeholder for the double-quoted string of the current line.
If the command contains parentheses, you can use any of the following
alternative notations to avoid parse errors.
\fBexecute[...]\fR
\fBexecute~...~\fR
\fBexecute!...!\fR
\fBexecute@...@\fR
\fBexecute#...#\fR
\fBexecute$...$\fR
\fBexecute%...%\fR
\fBexecute^...^\fR
\fBexecute&...&\fR
\fBexecute*...*\fR
\fBexecute;...;\fR
\fBexecute/.../\fR
\fBexecute|...|\fR
\fBexecute:...\fR
.RS
This is the special form that frees you from parse errors as it does not expect
the closing character. The catch is that it should be the last one in the
comma-separated list.
.RE
.RE
.TP
.BI "--history=" "HISTORY_FILE"
Load search history from the specified file and update the file on completion.
When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically remapped to
\fBnext-history\fR and \fBprevious-history\fR.
.TP
.BI "--history-size=" "N"
Maximum number of entries in the history file (default: 1000). The file is
automatically truncated when the number of the lines exceeds the value.
.SS Scripting
.TP
.BI "-q, --query=" "STR"
@@ -185,10 +275,9 @@ fzf becomes a fuzzy-version of grep.
Print query as the first line
.TP
.BI "--expect=" "KEY[,..]"
Comma-separated list of keys (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR,
or any single character) that can be used to complete fzf in addition to the
default enter key. When this option is set, fzf will print the name of the key
pressed as the first line of its output (or as the second line if
Comma-separated list of keys that can be used to complete fzf in addition to
the default enter key. When this option is set, fzf will print the name of the
key pressed as the first line of its output (or as the second line if
\fB--print-query\fR is also used). The line will be empty if fzf is completed
with the default enter key.
.RS
@@ -218,7 +307,7 @@ Default options. e.g. \fB--extended --ansi\fR
.SH FIELD INDEX EXPRESSION
A field index expression can be a non-zero integer or a range expression
([BEGIN]..[END]). \fI--nth\fR and \fI--with-nth\fR take a comma-separated list
([BEGIN]..[END]). \fB--nth\fR and \fB--with-nth\fR take a comma-separated list
of field index expressions.
.SS Examples
@@ -241,7 +330,7 @@ of field index expressions.
.SH EXTENDED SEARCH MODE
With \fI-x\fR or \fI--extended\fR option, fzf will start in "extended-search
With \fB-x\fR or \fB--extended\fR option, fzf will start in "extended-search
mode". In this mode, you can specify multiple patterns delimited by spaces,
such as: \fB'wild ^music .mp3$ sbtrkt !rmx\fR
@@ -261,8 +350,8 @@ from the result.
.SS Extended-exact mode
If you don't need fuzzy matching at all and do not wish to "quote" (prefixing
with ') every word, start fzf with \fI-e\fR or \fI--extended-exact\fR option
(instead of \fI-x\fR or \fI--extended\fR).
with ') every word, start fzf with \fB-e\fR or \fB--extended-exact\fR option
(instead of \fB-x\fR or \fB--extended\fR).
.SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR)

View File

@@ -98,7 +98,7 @@ function! fzf#run(...) abort
try
let oshell = &shell
set shell=sh
if has('nvim') && bufexists('[FZF]')
if has('nvim') && bufexists('term://*:FZF')
echohl WarningMsg
echomsg 'FZF is already running!'
echohl None
@@ -248,13 +248,17 @@ function! s:calc_size(max, val)
endif
endfunction
function! s:getpos()
return {'tab': tabpagenr(), 'win': winnr()}
endfunction
function! s:split(dict)
let directions = {
\ 'up': ['topleft', 'resize', &lines],
\ 'down': ['botright', 'resize', &lines],
\ 'left': ['vertical topleft', 'vertical resize', &columns],
\ 'right': ['vertical botright', 'vertical resize', &columns] }
let s:ptab = tabpagenr()
let s:ppos = s:getpos()
try
for [dir, triple] in items(directions)
let val = get(a:dict, dir, '')
@@ -280,32 +284,38 @@ function! s:execute_term(dict, command, temps)
call s:split(a:dict)
call s:pushd(a:dict)
let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps }
let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps, 'name': 'FZF' }
function! fzf.on_exit(id, code)
let tab = tabpagenr()
if bufnr('') == self.buf
" We use close instead of bd! since Vim does not close the split when
" there's no other listed buffer
close
" FIXME This should be unnecessary due to `bufhidden=wipe` but in some
" cases Neovim fails to clean up the buffer and `bufexists('[FZF]')
" returns 1 even when it cannot be seen anywhere else. e.g. `FZF!`
silent! execute 'bd!' self.buf
endif
if s:ptab == tab
wincmd p
let pos = s:getpos()
let inplace = pos == s:ppos " {'window': 'enew'}
if !inplace
if bufnr('') == self.buf
" We use close instead of bd! since Vim does not close the split when
" there's no other listed buffer (nvim +'set nobuflisted')
close
endif
if pos.tab == s:ppos.tab
wincmd p
endif
endif
call s:pushd(self.dict)
try
redraw!
call s:callback(self.dict, self.temps)
if inplace && bufnr('') == self.buf
execute "normal! \<c-^>"
" No other listed buffer
if bufnr('') == self.buf
bd!
endif
endif
finally
call s:popd(self.dict)
endtry
endfunction
call termopen(a:command, fzf)
silent file [FZF]
startinsert
return []
endfunction

View File

@@ -45,7 +45,10 @@ _fzf_opts_completion() {
--print-query
--expect
--toggle-sort
--sync"
--sync
--cycle
--history
--history-size"
case "${prev}" in
--tiebreak)
@@ -56,6 +59,10 @@ _fzf_opts_completion() {
COMPREPLY=( $(compgen -W "dark light 16 bw" -- ${cur}) )
return 0
;;
--history)
COMPREPLY=()
return 0
;;
esac
if [[ ${cur} =~ ^-|\+ ]]; then
@@ -207,7 +214,7 @@ EOF
}
# fzf options
complete -F _fzf_opts_completion fzf
complete -o default -F _fzf_opts_completion fzf
d_cmds="cd pushd rmdir"
f_cmds="

View File

@@ -26,7 +26,7 @@ bindkey '^T' fzf-file-widget
# ALT-C - cd into the selected directory
fzf-cd-widget() {
cd "${$(command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
cd "${$(command \find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) +m):-.}"
zle reset-prompt
}
@@ -36,16 +36,10 @@ bindkey '\ec' fzf-cd-widget
# CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() {
local selected restore_no_bang_hist
if selected=$(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER"); then
num=$(echo "$selected" | head -n1 | awk '{print $1}' | sed 's/[^0-9]//g')
if selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER") ); then
num=$selected[1]
if [ -n "$num" ]; then
LBUFFER=!$num
if setopt | grep nobanghist > /dev/null; then
restore_no_bang_hist=1
unsetopt no_bang_hist
fi
zle expand-history
[ -n "$restore_no_bang_hist" ] && setopt no_bang_hist
zle vi-fetch-history -n $num
fi
fi
zle redisplay

View File

@@ -1,6 +1,7 @@
package algo
import (
"strings"
"unicode"
"github.com/junegunn/fzf/src/util"
@@ -159,3 +160,17 @@ func SuffixMatch(caseSensitive bool, input *[]rune, pattern []rune) (int, int) {
}
return trimmedLen - len(pattern), trimmedLen
}
func EqualMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
if len(*runes) != len(pattern) {
return -1, -1
}
runesStr := string(*runes)
if !caseSensitive {
runesStr = strings.ToLower(runesStr)
}
if runesStr == string(pattern) {
return 0, len(pattern)
}
return -1, -1
}

View File

@@ -8,7 +8,7 @@ import (
const (
// Current version
Version = "0.9.12"
Version = "0.10.0"
// Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond
@@ -32,6 +32,9 @@ const (
// Not to cache mergers with large lists
mergerCacheMax int = 100000
// History
defaultHistoryMax int = 1000
)
// fzf events

View File

@@ -113,7 +113,7 @@ func Run(opts *Options) {
// Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
if !streamingFilter {
reader := Reader{func(str string) { chunkList.Push(str) }, eventBox}
reader := Reader{func(str string) { chunkList.Push(str) }, eventBox, opts.ReadZero}
go reader.ReadSource()
}
@@ -139,7 +139,7 @@ func Run(opts *Options) {
if pattern.MatchItem(item) {
fmt.Println(*item.text)
}
}, eventBox}
}, eventBox, opts.ReadZero}
reader.ReadSource()
} else {
eventBox.Unwatch(EvtReadNew)

View File

@@ -4,11 +4,6 @@ package curses
#include <ncurses.h>
#include <locale.h>
#cgo LDFLAGS: -lncurses
void swapOutput() {
FILE* temp = stdout;
stdout = stderr;
stderr = temp;
}
*/
import "C"
@@ -56,6 +51,7 @@ const (
Mouse
BTab
BSpace
Del
PgUp
@@ -106,15 +102,18 @@ const (
)
type ColorTheme struct {
darkBg C.short
prompt C.short
match C.short
current C.short
currentMatch C.short
spinner C.short
info C.short
cursor C.short
selected C.short
UseDefault bool
Fg int16
Bg int16
DarkBg int16
Prompt int16
Match int16
Current int16
CurrentMatch int16
Spinner int16
Info int16
Cursor int16
Selected int16
}
type Event struct {
@@ -139,10 +138,14 @@ var (
_colorMap map[int]int
_prevDownTime time.Time
_clickY []int
_screen *C.SCREEN
Default16 *ColorTheme
Dark256 *ColorTheme
Light256 *ColorTheme
DarkBG C.short
FG int
CurrentFG int
BG int
DarkBG int
)
func init() {
@@ -150,35 +153,44 @@ func init() {
_clickY = []int{}
_colorMap = make(map[int]int)
Default16 = &ColorTheme{
darkBg: C.COLOR_BLACK,
prompt: C.COLOR_BLUE,
match: C.COLOR_GREEN,
current: C.COLOR_YELLOW,
currentMatch: C.COLOR_GREEN,
spinner: C.COLOR_GREEN,
info: C.COLOR_WHITE,
cursor: C.COLOR_RED,
selected: C.COLOR_MAGENTA}
UseDefault: true,
Fg: 15,
Bg: 0,
DarkBg: C.COLOR_BLACK,
Prompt: C.COLOR_BLUE,
Match: C.COLOR_GREEN,
Current: C.COLOR_YELLOW,
CurrentMatch: C.COLOR_GREEN,
Spinner: C.COLOR_GREEN,
Info: C.COLOR_WHITE,
Cursor: C.COLOR_RED,
Selected: C.COLOR_MAGENTA}
Dark256 = &ColorTheme{
darkBg: 236,
prompt: 110,
match: 108,
current: 254,
currentMatch: 151,
spinner: 148,
info: 144,
cursor: 161,
selected: 168}
UseDefault: true,
Fg: 15,
Bg: 0,
DarkBg: 236,
Prompt: 110,
Match: 108,
Current: 254,
CurrentMatch: 151,
Spinner: 148,
Info: 144,
Cursor: 161,
Selected: 168}
Light256 = &ColorTheme{
darkBg: 251,
prompt: 25,
match: 66,
current: 237,
currentMatch: 23,
spinner: 65,
info: 101,
cursor: 161,
selected: 168}
UseDefault: true,
Fg: 15,
Bg: 0,
DarkBg: 251,
Prompt: 25,
Match: 66,
Current: 237,
CurrentMatch: 23,
Spinner: 65,
Info: 101,
Cursor: 161,
Selected: 168}
}
func attrColored(pair int, bold bool) C.int {
@@ -239,10 +251,9 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
// syscall.Dup2(int(in.Fd()), int(os.Stdin.Fd()))
}
C.swapOutput()
C.setlocale(C.LC_ALL, C.CString(""))
C.initscr()
_screen = C.newterm(nil, C.stderr, C.stdin)
C.set_term(_screen)
if mouse {
C.mousemask(C.ALL_MOUSE_EVENTS, nil)
}
@@ -268,28 +279,40 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
}
func initPairs(theme *ColorTheme, black bool) {
var bg C.short
fg := C.short(theme.Fg)
bg := C.short(theme.Bg)
if black {
bg = C.COLOR_BLACK
} else {
C.use_default_colors()
} else if theme.UseDefault {
fg = -1
bg = -1
C.use_default_colors()
}
if theme.UseDefault {
FG = -1
BG = -1
} else {
FG = int(fg)
BG = int(bg)
C.assume_default_colors(C.int(theme.Fg), C.int(bg))
}
DarkBG = theme.darkBg
C.init_pair(ColPrompt, theme.prompt, bg)
C.init_pair(ColMatch, theme.match, bg)
C.init_pair(ColCurrent, theme.current, DarkBG)
C.init_pair(ColCurrentMatch, theme.currentMatch, DarkBG)
C.init_pair(ColSpinner, theme.spinner, bg)
C.init_pair(ColInfo, theme.info, bg)
C.init_pair(ColCursor, theme.cursor, DarkBG)
C.init_pair(ColSelected, theme.selected, DarkBG)
CurrentFG = int(theme.Current)
DarkBG = int(theme.DarkBg)
darkBG := C.short(DarkBG)
C.init_pair(ColPrompt, C.short(theme.Prompt), bg)
C.init_pair(ColMatch, C.short(theme.Match), bg)
C.init_pair(ColCurrent, C.short(theme.Current), darkBG)
C.init_pair(ColCurrentMatch, C.short(theme.CurrentMatch), darkBG)
C.init_pair(ColSpinner, C.short(theme.Spinner), bg)
C.init_pair(ColInfo, C.short(theme.Info), bg)
C.init_pair(ColCursor, C.short(theme.Cursor), darkBG)
C.init_pair(ColSelected, C.short(theme.Selected), darkBG)
}
func Close() {
C.endwin()
C.swapOutput()
C.delscreen(_screen)
}
func GetBytes() []byte {
@@ -454,10 +477,14 @@ func GetChar() Event {
}()
switch _buf[0] {
case CtrlC, CtrlG, CtrlQ:
case CtrlC:
return Event{CtrlC, 0, nil}
case CtrlG:
return Event{CtrlG, 0, nil}
case CtrlQ:
return Event{CtrlQ, 0, nil}
case 127:
return Event{CtrlH, 0, nil}
return Event{BSpace, 0, nil}
case ESC:
return escSequence(&sz)
}

94
src/history.go Normal file
View File

@@ -0,0 +1,94 @@
package fzf
import (
"errors"
"io/ioutil"
"os"
"strings"
)
type History struct {
path string
lines []string
modified map[int]string
maxSize int
cursor int
}
func NewHistory(path string, maxSize int) (*History, error) {
fmtError := func(e error) error {
if os.IsPermission(e) {
return errors.New("permission denied: " + path)
}
return errors.New("invalid history file: " + e.Error())
}
// Read history file
data, err := ioutil.ReadFile(path)
if err != nil {
// If it doesn't exist, check if we can create a file with the name
if os.IsNotExist(err) {
data = []byte{}
if err := ioutil.WriteFile(path, data, 0600); err != nil {
return nil, fmtError(err)
}
} else {
return nil, fmtError(err)
}
}
// Split lines and limit the maximum number of lines
lines := strings.Split(strings.Trim(string(data), "\n"), "\n")
if len(lines[len(lines)-1]) > 0 {
lines = append(lines, "")
}
return &History{
path: path,
maxSize: maxSize,
lines: lines,
modified: make(map[int]string),
cursor: len(lines) - 1}, nil
}
func (h *History) append(line string) error {
// We don't append empty lines
if len(line) == 0 {
return nil
}
lines := append(h.lines[:len(h.lines)-1], line)
if len(lines) > h.maxSize {
lines = lines[len(lines)-h.maxSize : len(lines)]
}
h.lines = append(lines, "")
return ioutil.WriteFile(h.path, []byte(strings.Join(h.lines, "\n")), 0600)
}
func (h *History) override(str string) {
// You can update the history but they're not written to the file
if h.cursor == len(h.lines)-1 {
h.lines[h.cursor] = str
} else if h.cursor < len(h.lines)-1 {
h.modified[h.cursor] = str
}
}
func (h *History) current() string {
if str, prs := h.modified[h.cursor]; prs {
return str
}
return h.lines[h.cursor]
}
func (h *History) previous() string {
if h.cursor > 0 {
h.cursor--
}
return h.current()
}
func (h *History) next() string {
if h.cursor < len(h.lines)-1 {
h.cursor++
}
return h.current()
}

59
src/history_test.go Normal file
View File

@@ -0,0 +1,59 @@
package fzf
import (
"os/user"
"testing"
)
func TestHistory(t *testing.T) {
maxHistory := 50
// Invalid arguments
user, _ := user.Current()
paths := []string{"/etc", "/proc"}
if user.Name != "root" {
paths = append(paths, "/etc/sudoers")
}
for _, path := range paths {
if _, e := NewHistory(path, maxHistory); e == nil {
t.Error("Error expected for: " + path)
}
}
{ // Append lines
h, _ := NewHistory("/tmp/fzf-history", maxHistory)
for i := 0; i < maxHistory+10; i++ {
h.append("foobar")
}
}
{ // Read lines
h, _ := NewHistory("/tmp/fzf-history", maxHistory)
if len(h.lines) != maxHistory+1 {
t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
}
for i := 0; i < maxHistory; i++ {
if h.lines[i] != "foobar" {
t.Error("Expected: foobar, actual: " + h.lines[i])
}
}
}
{ // Append lines
h, _ := NewHistory("/tmp/fzf-history", maxHistory)
h.append("barfoo")
h.append("")
h.append("foobarbaz")
}
{ // Read lines again
h, _ := NewHistory("/tmp/fzf-history", maxHistory)
if len(h.lines) != maxHistory+1 {
t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
}
compare := func(idx int, exp string) {
if h.lines[idx] != exp {
t.Errorf("Expected: %s, actual: %s\n", exp, h.lines[idx])
}
}
compare(maxHistory-3, "foobar")
compare(maxHistory-2, "barfoo")
compare(maxHistory-1, "foobarbaz")
}
}

View File

@@ -86,10 +86,15 @@ func (i *Item) Rank(cache bool) Rank {
// AsString returns the original string
func (i *Item) AsString() string {
return *i.StringPtr()
}
// StringPtr returns the pointer to the original string
func (i *Item) StringPtr() *string {
if i.origText != nil {
return *i.origText
return i.origText
}
return *i.text
return i.text
}
func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset {
@@ -143,13 +148,25 @@ func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset
offset: Offset{int32(start), int32(idx)}, color: color, bold: bold})
} else {
ansi := item.colors[curr-1]
fg := ansi.color.fg
if fg == -1 {
if current {
fg = curses.CurrentFG
} else {
fg = curses.FG
}
}
bg := ansi.color.bg
if current && bg == -1 {
bg = int(curses.DarkBG)
if bg == -1 {
if current {
bg = curses.DarkBG
} else {
bg = curses.BG
}
}
offsets = append(offsets, colorOffset{
offset: Offset{int32(start), int32(idx)},
color: curses.PairFor(ansi.color.fg, bg),
color: curses.PairFor(fg, bg),
bold: ansi.color.bold || bold})
}
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"regexp"
"strconv"
"strings"
"unicode/utf8"
@@ -14,7 +15,7 @@ import (
const usage = `usage: fzf [options]
Search mode
Search
-x, --extended Extended-search mode
-e, --extended-exact Extended-search mode (exact match)
-i Case-insensitive match (default: smart-case match)
@@ -22,10 +23,8 @@ const usage = `usage: fzf [options]
-n, --nth=N[,..] Comma-separated list of field index expressions
for limiting search scope. Each can be a non-zero
integer or a range expression ([BEGIN]..[END])
--with-nth=N[,..] Transform the item using index expressions for search
--with-nth=N[,..] Transform item using index expressions within finder
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
Search result
+s, --no-sort Do not sort the result
--tac Reverse the order of the input
--tiebreak=CRI Sort criterion when the scores are tied;
@@ -35,15 +34,16 @@ const usage = `usage: fzf [options]
-m, --multi Enable multi-select with tab/shift-tab
--ansi Enable processing of ANSI color codes
--no-mouse Disable mouse
--color=COL Color scheme; [dark|light|16|bw]
(default: dark on 256-color terminal, otherwise 16)
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
--black Use black background
--reverse Reverse orientation
--cycle Enable cyclic scroll
--no-hscroll Disable horizontal scroll
--inline-info Display finder info inline with the query
--prompt=STR Input prompt (default: '> ')
--toggle-sort=KEY Key to toggle sort
--bind=KEYBINDS Custom key bindings. Refer to the man page.
--history=FILE History file
--history-size=N Maximum number of history entries (default: 1000)
Scripting
-q, --query=STR Start the finder with the given query
@@ -106,6 +106,7 @@ type Options struct {
Theme *curses.ColorTheme
Black bool
Reverse bool
Cycle bool
Hscroll bool
InlineInfo bool
Prompt string
@@ -114,21 +115,24 @@ type Options struct {
Exit0 bool
Filter *string
ToggleSort bool
Expect []int
Expect map[int]string
Keymap map[int]actionType
Execmap map[int]string
PrintQuery bool
ReadZero bool
Sync bool
History *History
Version bool
}
func defaultOptions() *Options {
var defaultTheme *curses.ColorTheme
func defaultTheme() *curses.ColorTheme {
if strings.Contains(os.Getenv("TERM"), "256") {
defaultTheme = curses.Dark256
} else {
defaultTheme = curses.Default16
return curses.Dark256
}
return curses.Default16
}
func defaultOptions() *Options {
return &Options{
Mode: ModeFuzzy,
Case: CaseSmart,
@@ -141,9 +145,10 @@ func defaultOptions() *Options {
Multi: false,
Ansi: false,
Mouse: true,
Theme: defaultTheme,
Theme: defaultTheme(),
Black: false,
Reverse: false,
Cycle: false,
Hscroll: true,
InlineInfo: false,
Prompt: "> ",
@@ -152,10 +157,13 @@ func defaultOptions() *Options {
Exit0: false,
Filter: nil,
ToggleSort: false,
Expect: []int{},
Expect: make(map[int]string),
Keymap: defaultKeymap(),
Execmap: make(map[int]string),
PrintQuery: false,
ReadZero: false,
Sync: false,
History: nil,
Version: false}
}
@@ -169,11 +177,11 @@ func errorExit(msg string) {
help(1)
}
func optString(arg string, prefix string) (bool, string) {
rx, _ := regexp.Compile(fmt.Sprintf("^(?:%s)(.*)$", prefix))
matches := rx.FindStringSubmatch(arg)
if len(matches) > 1 {
return true, matches[1]
func optString(arg string, prefixes ...string) (bool, string) {
for _, prefix := range prefixes {
if strings.HasPrefix(arg, prefix) {
return true, arg[len(prefix):]
}
}
return false, ""
}
@@ -187,6 +195,31 @@ func nextString(args []string, i *int, message string) string {
return args[*i]
}
func optionalNextString(args []string, i *int) string {
if len(args) > *i+1 {
*i++
return args[*i]
}
return ""
}
func atoi(str string) int {
num, err := strconv.Atoi(str)
if err != nil {
errorExit("not a valid integer: " + str)
}
return num
}
func nextInt(args []string, i *int, message string) int {
if len(args) > *i+1 {
*i++
} else {
errorExit(message)
}
return atoi(args[*i])
}
func optionalNumeric(args []string, i *int) int {
if len(args) > *i+1 {
if strings.IndexAny(args[*i+1], "0123456789") == 0 {
@@ -230,7 +263,7 @@ func isAlphabet(char uint8) bool {
return char >= 'a' && char <= 'z'
}
func parseKeyChords(str string, message string) []int {
func parseKeyChords(str string, message string) map[int]string {
if len(str) == 0 {
errorExit(message)
}
@@ -240,22 +273,65 @@ func parseKeyChords(str string, message string) []int {
tokens = append(tokens, ",")
}
var chords []int
chords := make(map[int]string)
for _, key := range tokens {
if len(key) == 0 {
continue // ignore
}
lkey := strings.ToLower(key)
if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
chords = append(chords, curses.CtrlA+int(lkey[5])-'a')
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) {
chords = append(chords, curses.AltA+int(lkey[4])-'a')
} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '4' {
chords = append(chords, curses.F1+int(key[1])-'1')
} else if utf8.RuneCountInString(key) == 1 {
chords = append(chords, curses.AltZ+int([]rune(key)[0]))
} else {
errorExit("unsupported key: " + key)
chord := 0
switch lkey {
case "up":
chord = curses.Up
case "down":
chord = curses.Down
case "left":
chord = curses.Left
case "right":
chord = curses.Right
case "enter", "return":
chord = curses.CtrlM
case "space":
chord = curses.AltZ + int(' ')
case "bspace", "bs":
chord = curses.BSpace
case "alt-bs", "alt-bspace":
chord = curses.AltBS
case "tab":
chord = curses.Tab
case "btab", "shift-tab":
chord = curses.BTab
case "esc":
chord = curses.ESC
case "del":
chord = curses.Del
case "home":
chord = curses.Home
case "end":
chord = curses.End
case "pgup", "page-up":
chord = curses.PgUp
case "pgdn", "page-down":
chord = curses.PgDn
case "shift-left":
chord = curses.SLeft
case "shift-right":
chord = curses.SRight
default:
if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
chord = curses.CtrlA + int(lkey[5]) - 'a'
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) {
chord = curses.AltA + int(lkey[4]) - 'a'
} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '4' {
chord = curses.F1 + int(key[1]) - '1'
} else if utf8.RuneCountInString(key) == 1 {
chord = curses.AltZ + int([]rune(key)[0])
} else {
errorExit("unsupported key: " + key)
}
}
if chord > 0 {
chords[chord] = key
}
}
return chords
@@ -277,28 +353,103 @@ func parseTiebreak(str string) tiebreak {
return byLength
}
func parseTheme(str string) *curses.ColorTheme {
switch strings.ToLower(str) {
case "dark":
return curses.Dark256
case "light":
return curses.Light256
case "16":
return curses.Default16
case "bw", "no":
return nil
default:
errorExit("invalid color scheme: " + str)
}
return nil
func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme {
dupe := *theme
return &dupe
}
func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[int]actionType, bool) {
for _, pairStr := range strings.Split(str, ",") {
func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme {
theme := dupeTheme(defaultTheme)
for _, str := range strings.Split(strings.ToLower(str), ",") {
switch str {
case "dark":
theme = dupeTheme(curses.Dark256)
case "light":
theme = dupeTheme(curses.Light256)
case "16":
theme = dupeTheme(curses.Default16)
case "bw", "no":
theme = nil
default:
fail := func() {
errorExit("invalid color specification: " + str)
}
// Color is disabled
if theme == nil {
errorExit("colors disabled; cannot customize colors")
}
pair := strings.Split(str, ":")
if len(pair) != 2 {
fail()
}
ansi32, err := strconv.Atoi(pair[1])
if err != nil || ansi32 < -1 || ansi32 > 255 {
fail()
}
ansi := int16(ansi32)
switch pair[0] {
case "fg":
theme.Fg = ansi
theme.UseDefault = theme.UseDefault && ansi < 0
case "bg":
theme.Bg = ansi
theme.UseDefault = theme.UseDefault && ansi < 0
case "fg+":
theme.Current = ansi
case "bg+":
theme.DarkBg = ansi
case "hl":
theme.Match = ansi
case "hl+":
theme.CurrentMatch = ansi
case "prompt":
theme.Prompt = ansi
case "spinner":
theme.Spinner = ansi
case "info":
theme.Info = ansi
case "pointer":
theme.Cursor = ansi
case "marker":
theme.Selected = ansi
default:
fail()
}
}
}
return theme
}
var executeRegexp *regexp.Regexp
func firstKey(keymap map[int]string) int {
for k := range keymap {
return k
}
return 0
}
func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort bool, str string) (map[int]actionType, map[int]string, bool) {
if executeRegexp == nil {
// Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
executeRegexp = regexp.MustCompile(
"(?s):execute:.*|:execute(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)")
}
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
return ":execute(" + strings.Repeat(" ", len(src)-10) + ")"
})
idx := 0
for _, pairStr := range strings.Split(masked, ",") {
pairStr = str[idx : idx+len(pairStr)]
idx += len(pairStr) + 1
fail := func() {
errorExit("invalid key binding: " + pairStr)
}
pair := strings.Split(pairStr, ":")
pair := strings.SplitN(pairStr, ":", 2)
if len(pair) != 2 {
fail()
}
@@ -306,9 +457,11 @@ func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[in
if len(keys) != 1 {
fail()
}
key := keys[0]
key := firstKey(keys)
act := strings.ToLower(pair[1])
switch act {
case "ignore":
keymap[key] = actIgnore
case "beginning-of-line":
keymap[key] = actBeginningOfLine
case "abort":
@@ -347,6 +500,12 @@ func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[in
keymap[key] = actToggleDown
case "toggle-up":
keymap[key] = actToggleUp
case "toggle-all":
keymap[key] = actToggleAll
case "select-all":
keymap[key] = actSelectAll
case "deselect-all":
keymap[key] = actDeselectAll
case "toggle":
keymap[key] = actToggle
case "down":
@@ -357,14 +516,40 @@ func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[in
keymap[key] = actPageUp
case "page-down":
keymap[key] = actPageDown
case "previous-history":
keymap[key] = actPreviousHistory
case "next-history":
keymap[key] = actNextHistory
case "toggle-sort":
keymap[key] = actToggleSort
toggleSort = true
default:
errorExit("unknown action: " + act)
if isExecuteAction(act) {
keymap[key] = actExecute
if pair[1][7] == ':' {
execmap[key] = pair[1][8:]
} else {
execmap[key] = pair[1][8 : len(act)-1]
}
} else {
errorExit("unknown action: " + act)
}
}
}
return keymap, toggleSort
return keymap, execmap, toggleSort
}
func isExecuteAction(str string) bool {
if !strings.HasPrefix(str, "execute") || len(str) < 9 {
return false
}
b := str[7]
e := str[len(str)-1]
if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' ||
b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") {
return true
}
return false
}
func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType {
@@ -372,11 +557,34 @@ func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType {
if len(keys) != 1 {
errorExit("multiple keys specified")
}
keymap[keys[0]] = actToggleSort
keymap[firstKey(keys)] = actToggleSort
return keymap
}
func parseOptions(opts *Options, allArgs []string) {
keymap := make(map[int]actionType)
var historyMax int
if opts.History == nil {
historyMax = defaultHistoryMax
} else {
historyMax = opts.History.maxSize
}
setHistory := func(path string) {
h, e := NewHistory(path, historyMax)
if e != nil {
errorExit(e.Error())
}
opts.History = h
}
setHistoryMax := func(max int) {
historyMax = max
if historyMax < 1 {
errorExit("history max must be a positive integer")
}
if opts.History != nil {
opts.History.maxSize = historyMax
}
}
for i := 0; i < len(allArgs); i++ {
arg := allArgs[i]
switch arg {
@@ -398,11 +606,17 @@ func parseOptions(opts *Options, allArgs []string) {
case "--tiebreak":
opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
case "--bind":
opts.Keymap, opts.ToggleSort = parseKeymap(opts.Keymap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required"))
keymap, opts.Execmap, opts.ToggleSort =
parseKeymap(keymap, opts.Execmap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required"))
case "--color":
opts.Theme = parseTheme(nextString(allArgs, &i, "color scheme name required"))
spec := optionalNextString(allArgs, &i)
if len(spec) == 0 {
opts.Theme = defaultTheme()
} else {
opts.Theme = parseTheme(opts.Theme, spec)
}
case "--toggle-sort":
opts.Keymap = checkToggleSort(opts.Keymap, nextString(allArgs, &i, "key name required"))
keymap = checkToggleSort(keymap, nextString(allArgs, &i, "key name required"))
opts.ToggleSort = true
case "-d", "--delimiter":
opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required"))
@@ -444,6 +658,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Reverse = true
case "--no-reverse":
opts.Reverse = false
case "--cycle":
opts.Cycle = true
case "--no-cycle":
opts.Cycle = false
case "--hscroll":
opts.Hscroll = true
case "--no-hscroll":
@@ -460,6 +678,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Exit0 = true
case "+0", "--no-exit-0":
opts.Exit0 = false
case "--read0":
opts.ReadZero = true
case "--no-read0":
opts.ReadZero = false
case "--print-query":
opts.PrintQuery = true
case "--no-print-query":
@@ -472,40 +694,66 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Sync = false
case "--async":
opts.Sync = false
case "--no-history":
opts.History = nil
case "--history":
setHistory(nextString(allArgs, &i, "history file path required"))
case "--history-size":
setHistoryMax(nextInt(allArgs, &i, "history max size required"))
case "--version":
opts.Version = true
default:
if match, value := optString(arg, "-q|--query="); match {
if match, value := optString(arg, "-q", "--query="); match {
opts.Query = value
} else if match, value := optString(arg, "-f|--filter="); match {
} else if match, value := optString(arg, "-f", "--filter="); match {
opts.Filter = &value
} else if match, value := optString(arg, "-d|--delimiter="); match {
} else if match, value := optString(arg, "-d", "--delimiter="); match {
opts.Delimiter = delimiterRegexp(value)
} else if match, value := optString(arg, "--prompt="); match {
opts.Prompt = value
} else if match, value := optString(arg, "-n|--nth="); match {
} else if match, value := optString(arg, "-n", "--nth="); match {
opts.Nth = splitNth(value)
} else if match, value := optString(arg, "--with-nth="); match {
opts.WithNth = splitNth(value)
} else if match, _ := optString(arg, "-s|--sort="); match {
} else if match, _ := optString(arg, "-s", "--sort="); match {
opts.Sort = 1 // Don't care
} else if match, value := optString(arg, "--toggle-sort="); match {
opts.Keymap = checkToggleSort(opts.Keymap, value)
keymap = checkToggleSort(keymap, value)
opts.ToggleSort = true
} else if match, value := optString(arg, "--expect="); match {
opts.Expect = parseKeyChords(value, "key names required")
} else if match, value := optString(arg, "--tiebreak="); match {
opts.Tiebreak = parseTiebreak(value)
} else if match, value := optString(arg, "--color="); match {
opts.Theme = parseTheme(value)
opts.Theme = parseTheme(opts.Theme, value)
} else if match, value := optString(arg, "--bind="); match {
opts.Keymap, opts.ToggleSort = parseKeymap(opts.Keymap, opts.ToggleSort, value)
keymap, opts.Execmap, opts.ToggleSort =
parseKeymap(keymap, opts.Execmap, opts.ToggleSort, value)
} else if match, value := optString(arg, "--history="); match {
setHistory(value)
} else if match, value := optString(arg, "--history-size="); match {
setHistoryMax(atoi(value))
} else {
errorExit("unknown option: " + arg)
}
}
}
// Change default actions for CTRL-N / CTRL-P when --history is used
if opts.History != nil {
if _, prs := keymap[curses.CtrlP]; !prs {
keymap[curses.CtrlP] = actPreviousHistory
}
if _, prs := keymap[curses.CtrlN]; !prs {
keymap[curses.CtrlN] = actNextHistory
}
}
// Override default key bindings
for key, act := range keymap {
opts.Keymap[key] = act
}
// If we're not using extended search mode, --nth option becomes irrelevant
// if it contains the whole range
if opts.Mode == ModeFuzzy || len(opts.Nth) == 1 {

View File

@@ -1,6 +1,7 @@
package fzf
import (
"fmt"
"testing"
"github.com/junegunn/fzf/src/curses"
@@ -71,63 +72,101 @@ func TestIrrelevantNth(t *testing.T) {
}
func TestParseKeys(t *testing.T) {
keys := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g", "")
check := func(key int, expected int) {
if key != expected {
t.Errorf("%d != %d", key, expected)
pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g", "")
check := func(i int, s string) {
if pairs[i] != s {
t.Errorf("%s != %s", pairs[i], s)
}
}
check(len(keys), 9)
check(keys[0], curses.CtrlZ)
check(keys[1], curses.AltZ)
check(keys[2], curses.F2)
check(keys[3], curses.AltZ+'@')
check(keys[4], curses.AltA)
check(keys[5], curses.AltZ+'!')
check(keys[6], curses.CtrlA+'g'-'a')
check(keys[7], curses.AltZ+'J')
check(keys[8], curses.AltZ+'g')
if len(pairs) != 9 {
t.Error(9)
}
check(curses.CtrlZ, "ctrl-z")
check(curses.AltZ, "alt-z")
check(curses.F2, "f2")
check(curses.AltZ+'@', "@")
check(curses.AltA, "Alt-a")
check(curses.AltZ+'!', "!")
check(curses.CtrlA+'g'-'a', "ctrl-G")
check(curses.AltZ+'J', "J")
check(curses.AltZ+'g', "g")
// Synonyms
pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
if len(pairs) != 9 {
t.Error(9)
}
check(curses.CtrlM, "Return")
check(curses.AltZ+' ', "space")
check(curses.Tab, "tab")
check(curses.BTab, "btab")
check(curses.ESC, "esc")
check(curses.Up, "up")
check(curses.Down, "down")
check(curses.Left, "left")
check(curses.Right, "right")
pairs = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
if len(pairs) != 11 {
t.Error(11)
}
check(curses.Tab, "Ctrl-I")
check(curses.PgUp, "page-up")
check(curses.PgDn, "Page-Down")
check(curses.Home, "Home")
check(curses.End, "End")
check(curses.AltBS, "Alt-BSpace")
check(curses.SLeft, "shift-left")
check(curses.SRight, "shift-right")
check(curses.BTab, "shift-tab")
check(curses.CtrlM, "Enter")
check(curses.BSpace, "bspace")
}
func TestParseKeysWithComma(t *testing.T) {
check := func(key int, expected int) {
if key != expected {
t.Errorf("%d != %d", key, expected)
checkN := func(a int, b int) {
if a != b {
t.Errorf("%d != %d", a, b)
}
}
check := func(pairs map[int]string, i int, s string) {
if pairs[i] != s {
t.Errorf("%s != %s", pairs[i], s)
}
}
keys := parseKeyChords(",", "")
check(len(keys), 1)
check(keys[0], curses.AltZ+',')
pairs := parseKeyChords(",", "")
checkN(len(pairs), 1)
check(pairs, curses.AltZ+',', ",")
keys = parseKeyChords(",,a,b", "")
check(len(keys), 3)
check(keys[0], curses.AltZ+'a')
check(keys[1], curses.AltZ+'b')
check(keys[2], curses.AltZ+',')
pairs = parseKeyChords(",,a,b", "")
checkN(len(pairs), 3)
check(pairs, curses.AltZ+'a', "a")
check(pairs, curses.AltZ+'b', "b")
check(pairs, curses.AltZ+',', ",")
keys = parseKeyChords("a,b,,", "")
check(len(keys), 3)
check(keys[0], curses.AltZ+'a')
check(keys[1], curses.AltZ+'b')
check(keys[2], curses.AltZ+',')
pairs = parseKeyChords("a,b,,", "")
checkN(len(pairs), 3)
check(pairs, curses.AltZ+'a', "a")
check(pairs, curses.AltZ+'b', "b")
check(pairs, curses.AltZ+',', ",")
keys = parseKeyChords("a,,,b", "")
check(len(keys), 3)
check(keys[0], curses.AltZ+'a')
check(keys[1], curses.AltZ+'b')
check(keys[2], curses.AltZ+',')
pairs = parseKeyChords("a,,,b", "")
checkN(len(pairs), 3)
check(pairs, curses.AltZ+'a', "a")
check(pairs, curses.AltZ+'b', "b")
check(pairs, curses.AltZ+',', ",")
keys = parseKeyChords("a,,,b,c", "")
check(len(keys), 4)
check(keys[0], curses.AltZ+'a')
check(keys[1], curses.AltZ+'b')
check(keys[2], curses.AltZ+'c')
check(keys[3], curses.AltZ+',')
pairs = parseKeyChords("a,,,b,c", "")
checkN(len(pairs), 4)
check(pairs, curses.AltZ+'a', "a")
check(pairs, curses.AltZ+'b', "b")
check(pairs, curses.AltZ+'c', "c")
check(pairs, curses.AltZ+',', ",")
keys = parseKeyChords(",,,", "")
check(len(keys), 1)
check(keys[0], curses.AltZ+',')
pairs = parseKeyChords(",,,", "")
checkN(len(pairs), 1)
check(pairs, curses.AltZ+',', ",")
}
func TestBind(t *testing.T) {
@@ -136,11 +175,20 @@ func TestBind(t *testing.T) {
t.Errorf("%d != %d", action, expected)
}
}
checkString := func(action string, expected string) {
if action != expected {
t.Errorf("%d != %d", action, expected)
}
}
keymap := defaultKeymap()
execmap := make(map[int]string)
check(actBeginningOfLine, keymap[curses.CtrlA])
keymap, toggleSort :=
parseKeymap(keymap, false,
"ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down")
keymap, execmap, toggleSort :=
parseKeymap(keymap, execmap, false,
"ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+
"f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
"alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+
",X:execute:\nfoobar,Y:execute(baz)")
if !toggleSort {
t.Errorf("toggleSort not set")
}
@@ -148,10 +196,73 @@ func TestBind(t *testing.T) {
check(actToggleSort, keymap[curses.CtrlB])
check(actPageUp, keymap[curses.AltZ+'c'])
check(actPageDown, keymap[curses.AltZ])
check(actExecute, keymap[curses.F1])
check(actExecute, keymap[curses.F2])
check(actExecute, keymap[curses.F3])
check(actExecute, keymap[curses.F4])
checkString("ls {}", execmap[curses.F1])
checkString("echo {}, {}, {}", execmap[curses.F2])
checkString("echo '({})'", execmap[curses.F3])
checkString("less {}", execmap[curses.F4])
checkString("echo (,),[,],/,:,;,%,{}", execmap[curses.AltA])
checkString("echo (,),[,],/,:,@,%,{}", execmap[curses.AltB])
checkString("\nfoobar,Y:execute(baz)", execmap[curses.AltZ+'X'])
keymap, toggleSort = parseKeymap(keymap, false, "f1:abort")
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
keymap, execmap, toggleSort =
parseKeymap(keymap, execmap, false, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
checkString("foobar", execmap[curses.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])])
}
keymap, execmap, toggleSort = parseKeymap(keymap, execmap, false, "f1:abort")
if toggleSort {
t.Errorf("toggleSort set")
}
check(actAbort, keymap[curses.F1])
}
func TestColorSpec(t *testing.T) {
theme := curses.Dark256
dark := parseTheme(theme, "dark")
if *dark != *theme {
t.Errorf("colors should be equivalent")
}
if dark == theme {
t.Errorf("point should not be equivalent")
}
light := parseTheme(theme, "dark,light")
if *light == *theme {
t.Errorf("should not be equivalent")
}
if *light != *curses.Light256 {
t.Errorf("colors should be equivalent")
}
if light == theme {
t.Errorf("point should not be equivalent")
}
customized := parseTheme(theme, "fg:231,bg:232")
if customized.Fg != 231 || customized.Bg != 232 {
t.Errorf("color not customized")
}
if *curses.Dark256 == *customized {
t.Errorf("colors should not be equivalent")
}
customized.Fg = curses.Dark256.Fg
customized.Bg = curses.Dark256.Bg
if *curses.Dark256 == *customized {
t.Errorf("colors should now be equivalent")
}
customized = parseTheme(theme, "fg:231,dark,bg:232")
if customized.Fg != curses.Dark256.Fg || customized.Bg == curses.Dark256.Bg {
t.Errorf("color not customized")
}
if customized.UseDefault {
t.Errorf("not using default colors")
}
if !curses.Dark256.UseDefault {
t.Errorf("using default colors")
}
}

View File

@@ -24,6 +24,7 @@ const (
termExact
termPrefix
termSuffix
termEqual
)
type term struct {
@@ -116,6 +117,7 @@ func BuildPattern(mode Mode, caseMode Case,
procFun: make(map[termType]func(bool, *[]rune, []rune) (int, int))}
ptr.procFun[termFuzzy] = algo.FuzzyMatch
ptr.procFun[termEqual] = algo.EqualMatch
ptr.procFun[termExact] = algo.ExactMatchNaive
ptr.procFun[termPrefix] = algo.PrefixMatch
ptr.procFun[termSuffix] = algo.SuffixMatch
@@ -151,8 +153,13 @@ func parseTerms(mode Mode, caseMode Case, str string) []term {
text = text[1:]
}
} else if strings.HasPrefix(text, "^") {
typ = termPrefix
text = text[1:]
if strings.HasSuffix(text, "$") {
typ = termEqual
text = text[1 : len(text)-1]
} else {
typ = termPrefix
text = text[1:]
}
} else if strings.HasSuffix(text, "$") {
typ = termSuffix
text = text[:len(text)-1]

View File

@@ -8,8 +8,8 @@ import (
func TestParseTermsExtended(t *testing.T) {
terms := parseTerms(ModeExtended, CaseSmart,
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$")
if len(terms) != 8 ||
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ ^iii$")
if len(terms) != 9 ||
terms[0].typ != termFuzzy || terms[0].inv ||
terms[1].typ != termExact || terms[1].inv ||
terms[2].typ != termPrefix || terms[2].inv ||
@@ -17,7 +17,8 @@ func TestParseTermsExtended(t *testing.T) {
terms[4].typ != termFuzzy || !terms[4].inv ||
terms[5].typ != termExact || !terms[5].inv ||
terms[6].typ != termPrefix || !terms[6].inv ||
terms[7].typ != termSuffix || !terms[7].inv {
terms[7].typ != termSuffix || !terms[7].inv ||
terms[8].typ != termEqual || terms[8].inv {
t.Errorf("%s", terms)
}
for idx, term := range terms {
@@ -65,6 +66,22 @@ func TestExact(t *testing.T) {
}
}
func TestEqual(t *testing.T) {
defer clearPatternCache()
clearPatternCache()
pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, nil, []rune("^AbC$"))
match := func(str string, sidxExpected int, eidxExpected int) {
runes := []rune(str)
sidx, eidx := algo.EqualMatch(pattern.caseSensitive, &runes, pattern.terms[0].text)
if sidx != sidxExpected || eidx != eidxExpected {
t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx)
}
}
match("ABC", -1, -1)
match("AbC", 0, 3)
}
func TestCaseSensitivity(t *testing.T) {
defer clearPatternCache()
clearPatternCache()

View File

@@ -13,6 +13,7 @@ import (
type Reader struct {
pusher func(string)
eventBox *util.EventBox
delimNil bool
}
// ReadSource reads data from the default command or from standard input
@@ -30,11 +31,24 @@ func (r *Reader) ReadSource() {
}
func (r *Reader) feed(src io.Reader) {
if scanner := bufio.NewScanner(src); scanner != nil {
for scanner.Scan() {
r.pusher(scanner.Text())
delim := byte('\n')
if r.delimNil {
delim = '\000'
}
reader := bufio.NewReader(src)
for {
line, err := reader.ReadString(delim)
if line != "" {
// "ReadString returns err != nil if and only if the returned data does not end in delim."
if err == nil {
line = line[:len(line)-1]
}
r.pusher(line)
r.eventBox.Set(EvtReadNew, nil)
}
if err != nil {
break
}
}
}

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"os"
"os/exec"
"os/signal"
"regexp"
"sort"
@@ -32,10 +33,13 @@ type Terminal struct {
multi bool
sort bool
toggleSort bool
expect []int
expect map[int]string
keymap map[int]actionType
pressed int
execmap map[int]string
pressed string
printQuery bool
history *History
cycle bool
count int
progress int
reading bool
@@ -105,7 +109,10 @@ const (
actUnixWordRubout
actYank
actBackwardKillWord
actSelectAll
actDeselectAll
actToggle
actToggleAll
actToggleDown
actToggleUp
actDown
@@ -113,6 +120,9 @@ const (
actPageUp
actPageDown
actToggleSort
actPreviousHistory
actNextHistory
actExecute
)
func defaultKeymap() map[int]actionType {
@@ -128,6 +138,7 @@ func defaultKeymap() map[int]actionType {
keymap[C.CtrlE] = actEndOfLine
keymap[C.CtrlF] = actForwardChar
keymap[C.CtrlH] = actBackwardDeleteChar
keymap[C.BSpace] = actBackwardDeleteChar
keymap[C.Tab] = actToggleDown
keymap[C.BTab] = actToggleUp
keymap[C.CtrlJ] = actDown
@@ -181,8 +192,12 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
toggleSort: opts.ToggleSort,
expect: opts.Expect,
keymap: opts.Keymap,
pressed: 0,
execmap: opts.Execmap,
pressed: "",
printQuery: opts.PrintQuery,
history: opts.History,
cycle: opts.Cycle,
reading: true,
merger: EmptyMerger,
selected: make(map[uint32]selectedItem),
reqBox: util.NewEventBox(),
@@ -242,17 +257,7 @@ func (t *Terminal) output() {
fmt.Println(string(t.input))
}
if len(t.expect) > 0 {
if t.pressed == 0 {
fmt.Println()
} else if util.Between(t.pressed, C.AltA, C.AltZ) {
fmt.Printf("alt-%c\n", t.pressed+'a'-C.AltA)
} else if util.Between(t.pressed, C.F1, C.F4) {
fmt.Printf("f%c\n", t.pressed+'1'-C.F1)
} else if util.Between(t.pressed, C.CtrlA, C.CtrlZ) {
fmt.Printf("ctrl-%c\n", t.pressed+'a'-C.CtrlA)
} else {
fmt.Printf("%c\n", t.pressed-C.AltZ)
}
fmt.Println(t.pressed)
}
if len(t.selected) == 0 {
cnt := t.merger.Length()
@@ -373,7 +378,7 @@ func (t *Terminal) printItem(item *Item, current bool) {
if current {
C.CPrint(C.ColCursor, true, ">")
if selected {
C.CPrint(C.ColCurrent, true, ">")
C.CPrint(C.ColSelected, true, ">")
} else {
C.CPrint(C.ColCurrent, true, " ")
}
@@ -580,6 +585,17 @@ func keyMatch(key int, event C.Event) bool {
return event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ
}
func executeCommand(template string, current string) {
command := strings.Replace(template, "{}", fmt.Sprintf("%q", current), -1)
cmd := exec.Command("sh", "-c", command)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
C.Endwin()
cmd.Run()
C.Refresh()
}
// Loop is called to start Terminal I/O
func (t *Terminal) Loop() {
<-t.startChan
@@ -605,6 +621,27 @@ func (t *Terminal) Loop() {
t.reqBox.Set(reqRedraw, nil)
}
}()
// Keep the spinner spinning
go func() {
for {
t.mutex.Lock()
reading := t.reading
t.mutex.Unlock()
if !reading {
break
}
time.Sleep(spinnerDuration)
t.reqBox.Set(reqInfo, nil)
}
}()
}
exit := func(code int) {
if code == 0 && t.history != nil {
t.history.append(string(t.input))
}
os.Exit(code)
}
go func() {
@@ -633,10 +670,10 @@ func (t *Terminal) Loop() {
case reqClose:
C.Close()
t.output()
os.Exit(0)
exit(0)
case reqQuit:
C.Close()
os.Exit(1)
exit(1)
}
}
t.placeCursor()
@@ -661,39 +698,48 @@ func (t *Terminal) Loop() {
}
}
}
selectItem := func(item *Item) bool {
if _, found := t.selected[item.index]; !found {
t.selected[item.index] = selectedItem{time.Now(), item.StringPtr()}
return true
}
return false
}
toggleY := func(y int) {
item := t.merger.Get(y)
if !selectItem(item) {
delete(t.selected, item.index)
}
}
toggle := func() {
if t.cy < t.merger.Length() {
item := t.merger.Get(t.cy)
if _, found := t.selected[item.index]; !found {
var strptr *string
if item.origText != nil {
strptr = item.origText
} else {
strptr = item.text
}
t.selected[item.index] = selectedItem{time.Now(), strptr}
} else {
delete(t.selected, item.index)
}
toggleY(t.cy)
req(reqInfo)
}
}
for _, key := range t.expect {
for key, ret := range t.expect {
if keyMatch(key, event) {
t.pressed = key
t.pressed = ret
req(reqClose)
break
}
}
action := t.keymap[event.Type]
mapkey := event.Type
if event.Type == C.Rune {
code := int(event.Char) + int(C.AltZ)
if act, prs := t.keymap[code]; prs {
mapkey = int(event.Char) + int(C.AltZ)
if act, prs := t.keymap[mapkey]; prs {
action = act
}
}
switch action {
case actIgnore:
case actExecute:
if t.cy >= 0 && t.cy < t.merger.Length() {
item := t.merger.Get(t.cy)
executeCommand(t.execmap[mapkey], item.AsString())
}
case actInvalid:
t.mutex.Unlock()
continue
@@ -725,11 +771,34 @@ func (t *Terminal) Loop() {
t.input = append(t.input[:t.cx-1], t.input[t.cx:]...)
t.cx--
}
case actSelectAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
item := t.merger.Get(i)
selectItem(item)
}
req(reqList, reqInfo)
}
case actDeselectAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
item := t.merger.Get(i)
delete(t.selected, item.index)
}
req(reqList, reqInfo)
}
case actToggle:
if t.multi && t.merger.Length() > 0 {
toggle()
req(reqList)
}
case actToggleAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
toggleY(i)
}
req(reqList, reqInfo)
}
case actToggleDown:
if t.multi && t.merger.Length() > 0 {
toggle()
@@ -796,6 +865,18 @@ func (t *Terminal) Loop() {
prefix := copySlice(t.input[:t.cx])
t.input = append(append(prefix, event.Char), t.input[t.cx:]...)
t.cx++
case actPreviousHistory:
if t.history != nil {
t.history.override(string(t.input))
t.input = []rune(t.history.previous())
t.cx = len(t.input)
}
case actNextHistory:
if t.history != nil {
t.history.override(string(t.input))
t.input = []rune(t.history.next())
t.cx = len(t.input)
}
case actMouse:
me := event.MouseEvent
mx, my := util.Constrain(me.X-len(t.prompt), 0, len(t.input)), me.Y
@@ -871,10 +952,22 @@ func (t *Terminal) constrain() {
func (t *Terminal) vmove(o int) {
if t.reverse {
t.vset(t.cy - o)
} else {
t.vset(t.cy + o)
o *= -1
}
dest := t.cy + o
if t.cycle {
max := t.merger.Length() - 1
if dest > max {
if t.cy == max {
dest = 0
}
} else if dest < 0 {
if t.cy == 0 {
dest = max
}
}
}
t.vset(dest)
}
func (t *Terminal) vset(o int) bool {
@@ -885,7 +978,6 @@ func (t *Terminal) vset(o int) bool {
func (t *Terminal) maxItems() int {
if t.inlineInfo {
return C.MaxY() - 1
} else {
return C.MaxY() - 2
}
return C.MaxY() - 2
}

View File

@@ -72,17 +72,6 @@ class Tmux
end
end
def closed?
!go("list-window -F '#I'").include?(win)
end
def close
wait do
send_keys 'C-c', 'C-u', 'exit', :Enter
closed?
end
end
def kill
go("kill-window -t #{win} 2> /dev/null")
end
@@ -152,21 +141,26 @@ class TestBase < Minitest::Test
attr_reader :tmux
def tempname
[TEMPNAME,
caller_locations.map(&:label).find { |l| l =~ /^test_/ }].join '-'
end
def setup
ENV.delete 'FZF_DEFAULT_OPTS'
ENV.delete 'FZF_DEFAULT_COMMAND'
File.unlink TEMPNAME while File.exists?(TEMPNAME)
end
def readonce
wait { File.exists?(TEMPNAME) }
File.read(TEMPNAME)
wait { File.exists?(tempname) }
File.read(tempname)
ensure
File.unlink TEMPNAME while File.exists?(TEMPNAME)
File.unlink tempname while File.exists?(tempname)
tmux.prepare
end
def fzf(*opts)
fzf!(*opts) + " > #{TEMPNAME}.tmp; mv #{TEMPNAME}.tmp #{TEMPNAME}"
fzf!(*opts) + " > #{tempname}.tmp; mv #{tempname}.tmp #{tempname}"
end
def fzf!(*opts)
@@ -214,7 +208,6 @@ class TestGoFZF < TestBase
assert_equal '> 391', lines[-1]
tmux.send_keys :Enter
tmux.close
assert_equal '1391', readonce.chomp
end
@@ -223,7 +216,6 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines.last =~ /^>/ }
tmux.send_keys :Enter
tmux.close
assert_equal 'hello', readonce.chomp
end
@@ -290,7 +282,6 @@ class TestGoFZF < TestBase
# CTRL-M
tmux.send_keys "C-M"
tmux.until { |lines| lines.last !~ /^>/ }
tmux.close
end
def test_multi_order
@@ -303,7 +294,6 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-2].include? '(6)' }
tmux.send_keys "C-M"
assert_equal %w[3 2 5 6 8 7], readonce.split($/)
tmux.close
end
def test_with_nth
@@ -454,16 +444,12 @@ class TestGoFZF < TestBase
end
def test_unicode_case
tempname = TEMPNAME + Time.now.to_f.to_s
writelines tempname, %w[строКА1 СТРОКА2 строка3 Строка4]
assert_equal %w[СТРОКА2 Строка4], `cat #{tempname} | #{FZF} -fС`.split($/)
assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `cat #{tempname} | #{FZF} -fс`.split($/)
rescue
File.unlink tempname
end
def test_tiebreak
tempname = TEMPNAME + Time.now.to_f.to_s
input = %w[
--foobar--------
-----foobar---
@@ -500,8 +486,6 @@ class TestGoFZF < TestBase
], `cat #{tempname} | #{FZF} -ffoobar --tiebreak end`.split($/)
assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/)
rescue
File.unlink tempname
end
def test_invalid_cache
@@ -525,6 +509,132 @@ class TestGoFZF < TestBase
tmux.send_keys 'uuu', 'TTT', 'tt', 'uu', 'ttt', 'C-j'
assert_equal %w[4 5 6 9], readonce.split($/)
end
def test_long_line
data = '.' * 256 * 1024
File.open(tempname, 'w') do |f|
f << data
end
assert_equal data, `cat #{tempname} | #{FZF} -f .`.chomp
end
def test_read0
lines = `find .`.split($/)
assert_equal lines.last, `find . | #{FZF} -e -f "^#{lines.last}$"`.chomp
assert_equal lines.last, `find . -print0 | #{FZF} --read0 -e -f "^#{lines.last}$"`.chomp
end
def test_select_all_deselect_all_toggle_all
tmux.send_keys "seq 100 | #{fzf '--bind ctrl-a:select-all,ctrl-d:deselect-all,ctrl-t:toggle-all --multi'}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| lines[-2].include? '(3)' }
tmux.send_keys 'C-t'
tmux.until { |lines| lines[-2].include? '(97)' }
tmux.send_keys 'C-a'
tmux.until { |lines| lines[-2].include? '(100)' }
tmux.send_keys :Tab, :Tab
tmux.until { |lines| lines[-2].include? '(98)' }
tmux.send_keys 'C-d'
tmux.until { |lines| !lines[-2].include? '(' }
tmux.send_keys :Tab, :Tab
tmux.until { |lines| lines[-2].include? '(2)' }
tmux.send_keys 0
tmux.until { |lines| lines[-2].include? '10/100' }
tmux.send_keys 'C-a'
tmux.until { |lines| lines[-2].include? '(12)' }
tmux.send_keys :Enter
assert_equal %w[2 1 10 20 30 40 50 60 70 80 90 100], readonce.split($/)
end
def test_history
history_file = '/tmp/fzf-test-history'
# History with limited number of entries
File.unlink history_file rescue nil
opts = "--history=#{history_file} --history-size=4"
input = %w[00 11 22 33 44].map { |e| e + $/ }
input.each do |keys|
tmux.send_keys "seq 100 | #{fzf opts}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys keys
tmux.until { |lines| lines[-2].include? '1/100' }
tmux.send_keys :Enter
readonce
end
assert_equal input[1..-1], File.readlines(history_file)
# Update history entries (not changed on disk)
tmux.send_keys "seq 100 | #{fzf opts}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys 'C-p'
tmux.until { |lines| lines[-1].end_with? '> 44' }
tmux.send_keys 'C-p'
tmux.until { |lines| lines[-1].end_with? '> 33' }
tmux.send_keys :BSpace
tmux.until { |lines| lines[-1].end_with? '> 3' }
tmux.send_keys 1
tmux.until { |lines| lines[-1].end_with? '> 31' }
tmux.send_keys 'C-p'
tmux.until { |lines| lines[-1].end_with? '> 22' }
tmux.send_keys 'C-n'
tmux.until { |lines| lines[-1].end_with? '> 31' }
tmux.send_keys 0
tmux.until { |lines| lines[-1].end_with? '> 310' }
tmux.send_keys :Enter
readonce
assert_equal %w[22 33 44 310].map { |e| e + $/ }, File.readlines(history_file)
# Respect --bind option
tmux.send_keys "seq 100 | #{fzf opts + ' --bind ctrl-p:next-history,ctrl-n:previous-history'}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys 'C-n', 'C-n', 'C-n', 'C-n', 'C-p'
tmux.until { |lines| lines[-1].end_with?('33') }
tmux.send_keys :Enter
ensure
File.unlink history_file
end
def test_execute
output = '/tmp/fzf-test-execute'
opts = %[--bind \\"alt-a:execute(echo '[{}]' >> #{output}),alt-b:execute[echo '({}), ({})' >> #{output}],C:execute:echo '({}), [{}], @{}@' >> #{output}\\"]
tmux.send_keys "seq 100 | #{fzf opts}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys :Escape, :a, :Escape, :a
tmux.send_keys :Up
tmux.send_keys :Escape, :b, :Escape, :b
tmux.send_keys :Up
tmux.send_keys :C
tmux.send_keys 'foobar'
tmux.until { |lines| lines[-2].include? '0/100' }
tmux.send_keys :Escape, :a, :Escape, :b, :Escape, :c
tmux.send_keys :Enter
readonce
assert_equal ['["1"]', '["1"]', '("2"), ("2")', '("2"), ("2")', '("3"), ["3"], @"3"@'],
File.readlines(output).map(&:chomp)
ensure
File.unlink output rescue nil
end
def test_cycle
tmux.send_keys "seq 8 | #{fzf :cycle}", :Enter
tmux.until { |lines| lines[-2].include? '8/8' }
tmux.send_keys :Down
tmux.until { |lines| lines[-10].start_with? '>' }
tmux.send_keys :Down
tmux.until { |lines| lines[-9].start_with? '>' }
tmux.send_keys :PgUp
tmux.until { |lines| lines[-10].start_with? '>' }
tmux.send_keys :PgUp
tmux.until { |lines| lines[-3].start_with? '>' }
tmux.send_keys :Up
tmux.until { |lines| lines[-4].start_with? '>' }
tmux.send_keys :PgDn
tmux.until { |lines| lines[-3].start_with? '>' }
tmux.send_keys :PgDn
tmux.until { |lines| lines[-10].start_with? '>' }
end
private
def writelines path, lines
File.unlink path while File.exists? path
@@ -563,7 +673,7 @@ module TestShell
def test_alt_c
tmux.prepare
tmux.send_keys :Escape, :c, pane: 0
lines = tmux.until(1) { |lines| lines.item_count > 0 }
lines = tmux.until(1) { |lines| lines.item_count > 0 && lines[-3][2..-1] }
expected = lines[-3][2..-1]
tmux.send_keys :Enter, pane: 1
tmux.prepare
@@ -697,6 +807,7 @@ class TestBash < TestBase
include CompletionTest
def new_shell
tmux.prepare
tmux.send_keys "FZF_TMUX=0 #{Shell.bash}", :Enter
tmux.prepare
end