Merge branch 'devel'

This commit is contained in:
Junegunn Choi 2019-11-15 22:53:08 +09:00
commit b471042037
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
15 changed files with 591 additions and 173 deletions

View File

@ -1,6 +1,57 @@
CHANGELOG CHANGELOG
========= =========
0.19.0
------
- Added "reload" action for dynamically updating the input list without
restarting fzf. See https://github.com/junegunn/fzf/issues/1750 to learn
more about it.
```sh
# Using fzf as the selector interface for ripgrep
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="foo"
FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \
fzf --bind "change:reload:$RG_PREFIX {q} || true" \
--ansi --phony --query "$INITIAL_QUERY"
```
- `--multi` now takes an optional integer argument which indicates the maximum
number of items that can be selected
```sh
seq 100 | fzf --multi 3 --reverse --height 50%
```
- If a placeholder expression for `--preview` and `execute` action (and the
new `reload` action) contains `f` flag, it is replaced to the
path of a temporary file that holds the evaluated list. This is useful
when you multi-select a large number of items and the length of the
evaluated string may exceed [`ARG_MAX`][argmax].
```sh
# Press CTRL-A to select 100K items and see the sum of all the numbers
seq 100000 | fzf --multi --bind ctrl-a:select-all \
--preview "awk '{sum+=\$1} END {print sum}' {+f}"
```
- `deselect-all` no longer deselects unmatched items. It is now consistent
with `select-all` and `toggle-all` in that it only affects matched items.
- Due to the limitation of bash, fuzzy completion is enabled by default for
a fixed set of commands. A helper function for easily setting up fuzzy
completion for any command is now provided.
```sh
# usage: _fzf_setup_completion path|dir COMMANDS...
_fzf_setup_completion path git kubectl
```
- Info line style can be changed by `--info=STYLE`
- `--info=default`
- `--info=inline` (same as old `--inline-info`)
- `--info=hidden`
- Preview window border can be disabled by adding `noborder` to
`--preview-window`.
- When you transform the input with `--with-nth`, the trailing white spaces
are removed.
- `ctrl-\`, `ctrl-]`, `ctrl-^`, and `ctrl-/` can now be used with `--bind`
- See https://github.com/junegunn/fzf/milestone/15?closed=1 for more details
[argmax]: https://unix.stackexchange.com/questions/120642/what-defines-the-maximum-size-for-a-command-single-argument
0.18.0 0.18.0
------ ------

View File

@ -1,7 +1,7 @@
.ig .ig
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2017 Junegunn Choi Copyright (c) 2019 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf-tmux 1 "Mar 2019" "fzf 0.18.0" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "Nov 2019" "fzf 0.19.0" "fzf-tmux - open fzf in tmux split pane"
.SH NAME .SH NAME
fzf-tmux - open fzf in tmux split pane fzf-tmux - open fzf in tmux split pane

View File

@ -1,7 +1,7 @@
.ig .ig
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2017 Junegunn Choi Copyright (c) 2019 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Mar 2019" "fzf 0.18.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "Nov 2019" "fzf 0.19.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@ -70,6 +70,10 @@ Transform the presentation of each line using field index expressions
.TP .TP
.BI "-d, --delimiter=" "STR" .BI "-d, --delimiter=" "STR"
Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style) Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style)
.TP
.BI "--phony"
Do not perform search. With this option, fzf becomes a simple selector
interface rather than a "fuzzy finder".
.SS Search result .SS Search result
.TP .TP
.B "+s, --no-sort" .B "+s, --no-sort"
@ -79,7 +83,8 @@ Do not sort the result
Reverse the order of the input Reverse the order of the input
.RS .RS
e.g. \fBhistory | fzf --tac --no-sort\fR e.g.
\fBhistory | fzf --tac --no-sort\fR
.RE .RE
.TP .TP
.BI "--tiebreak=" "CRI[,..]" .BI "--tiebreak=" "CRI[,..]"
@ -109,7 +114,8 @@ Comma-separated list of sort criteria to apply when the scores are tied.
.SS Interface .SS Interface
.TP .TP
.B "-m, --multi" .B "-m, --multi"
Enable multi-select with tab/shift-tab Enable multi-select with tab/shift-tab. It optionally takes an integer argument
which denotes the maximum number of items that can be selected.
.TP .TP
.B "+m, --no-multi" .B "+m, --no-multi"
Disable multi-select Disable multi-select
@ -118,8 +124,8 @@ Disable multi-select
Disable mouse Disable mouse
.TP .TP
.BI "--bind=" "KEYBINDS" .BI "--bind=" "KEYBINDS"
Comma-separated list of custom key bindings. See \fBKEY BINDINGS\fR for the Comma-separated list of custom key bindings. See \fBKEY/EVENT BINDINGS\fR for
details. the details.
.TP .TP
.B "--cycle" .B "--cycle"
Enable cyclic scroll Enable cyclic scroll
@ -201,12 +207,26 @@ terminal size with \fB%\fR suffix.
.br .br
.br .br
e.g. \fBfzf --margin 10%\fR e.g.
\fBfzf --margin 1,5%\fR \fBfzf --margin 10%
fzf --margin 1,5%\fR
.RE .RE
.TP .TP
.B "--inline-info" .BI "--info=" "STYLE"
Display finder info inline with the query Determines the display style of finder info.
.br
.BR default " Display on the next line to the prompt"
.br
.BR inline " Display on the same line"
.br
.BR hidden " Do not display finder info"
.br
.TP
.B "--no-info"
A synonym for \fB--info=hidden\fB
.TP .TP
.BI "--prompt=" "STR" .BI "--prompt=" "STR"
Input prompt (default: '> ') Input prompt (default: '> ')
@ -235,11 +255,6 @@ color mappings. Ansi color code of -1 denotes terminal default
foreground/background color. You can also specify 24-bit color in \fB#rrggbb\fR foreground/background color. You can also specify 24-bit color in \fB#rrggbb\fR
format. format.
.RS
e.g. \fBfzf --color=bg+:24\fR
\fBfzf --color=light,fg:232,bg:255,bg+:116,info:27\fR
.RE
.RS .RS
.B BASE SCHEME: .B BASE SCHEME:
(default: dark on 256-color terminal, otherwise 16) (default: dark on 256-color terminal, otherwise 16)
@ -264,6 +279,19 @@ e.g. \fBfzf --color=bg+:24\fR
\fBmarker \fRMulti-select marker \fBmarker \fRMulti-select marker
\fBspinner \fRStreaming input indicator \fBspinner \fRStreaming input indicator
\fBheader \fRHeader \fBheader \fRHeader
.B EXAMPLES:
\fB# Seoul256 theme with 8-bit colors
# (https://github.com/junegunn/seoul256.vim)
fzf --color='bg:237,bg+:236,info:143,border:240,spinner:108' \\
--color='hl:65,fg:252,header:65,fg+:252' \\
--color='pointer:161,marker:168,prompt:110,hl+:108'
# Seoul256 theme with 24-bit colors
fzf --color='bg:#4B4B4B,bg+:#3F3F3F,info:#BDBB72,border:#6B6B6B,spinner:#98BC99' \\
--color='hl:#719872,fg:#D9D9D9,header:#719872,fg+:#D9D9D9' \\
--color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'\fR
.RE .RE
.TP .TP
.B "--no-bold" .B "--no-bold"
@ -291,8 +319,9 @@ string, specify field index expressions between the braces (See \fBFIELD INDEX
EXPRESSION\fR for the details). EXPRESSION\fR for the details).
.RS .RS
e.g. \fBfzf --preview='head -$LINES {}'\fR e.g.
\fBls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR \fBfzf --preview='head -$LINES {}'
ls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR
fzf exports \fB$FZF_PREVIEW_LINES\fR and \fB$FZF_PREVIEW_COLUMNS\fR so that fzf exports \fB$FZF_PREVIEW_LINES\fR and \fB$FZF_PREVIEW_COLUMNS\fR so that
they represent the exact size of the preview window. (It also overrides they represent the exact size of the preview window. (It also overrides
@ -304,8 +333,9 @@ A placeholder expression starting with \fB+\fR flag will be replaced to the
space-separated list of the selected lines (or the current line if no selection space-separated list of the selected lines (or the current line if no selection
was made) individually quoted. was made) individually quoted.
e.g. \fBfzf --multi --preview='head -10 {+}'\fR e.g.
\fBgit log --oneline | fzf --multi --preview 'git show {+1}'\fR \fBfzf --multi --preview='head -10 {+}'
git log --oneline | fzf --multi --preview 'git show {+1}'\fR
When using a field index expression, leading and trailing whitespace is stripped When using a field index expression, leading and trailing whitespace is stripped
from the replacement string. To preserve the whitespace, use the \fBs\fR flag. from the replacement string. To preserve the whitespace, use the \fBs\fR flag.
@ -314,14 +344,25 @@ Also, \fB{q}\fR is replaced to the current query string, and \fB{n}\fR is
replaced to zero-based ordinal index of the line. Use \fB{+n}\fR if you want replaced to zero-based ordinal index of the line. Use \fB{+n}\fR if you want
all index numbers when multiple lines are selected. all index numbers when multiple lines are selected.
A placeholder expression with \fBf\fR flag is replaced to the path of
a temporary file that holds the evaluated list. This is useful when you
multi-select a large number of items and the length of the evaluated string may
exceed \fBARG_MAX\fR.
e.g.
\fB# Press CTRL-A to select 100K items and see the sum of all the numbers.
# This won't work properly without 'f' flag due to ARG_MAX limit.
seq 100000 | fzf --multi --bind ctrl-a:select-all \\
--preview "awk '{sum+=\$1} END {print sum}' {+f}"\fR
Note that you can escape a placeholder pattern by prepending a backslash. Note that you can escape a placeholder pattern by prepending a backslash.
Preview window will be updated even when there is no match for the current Preview window will be updated even when there is no match for the current
query if any of the placeholder expressions evaluates to a non-empty string. query if any of the placeholder expressions evaluates to a non-empty string.
.RE .RE
.TP .TP
.BI "--preview-window=" "[POSITION][:SIZE[%]][:wrap][:hidden]" .BI "--preview-window=" "[POSITION][:SIZE[%]][:noborder][:wrap][:hidden]"
Determine the layout of the preview window. If the argument ends with Determines the layout of the preview window. If the argument contains
\fB:hidden\fR, the preview window will be hidden by default until \fB:hidden\fR, the preview window will be hidden by default until
\fBtoggle-preview\fR action is triggered. Long lines are truncated by default. \fBtoggle-preview\fR action is triggered. Long lines are truncated by default.
Line wrap can be enabled with \fB:wrap\fR flag. Line wrap can be enabled with \fB:wrap\fR flag.
@ -338,8 +379,9 @@ execute the command in the background.
.RE .RE
.RS .RS
e.g. \fBfzf --preview="head {}" --preview-window=up:30%\fR e.g.
\fBfzf --preview="file {}" --preview-window=down:1\fR \fBfzf --preview="head {}" --preview-window=up:30%
fzf --preview="file {}" --preview-window=down:1\fR
.RE .RE
.SS Scripting .SS Scripting
.TP .TP
@ -369,7 +411,8 @@ times, fzf will expect the union of the keys. \fB--no-expect\fR will clear the
list. list.
.RS .RS
e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s --expect=f1,f2,~,@\fR e.g.
\fBfzf --expect=ctrl-v,ctrl-t,alt-s --expect=f1,f2,~,@\fR
.RE .RE
.TP .TP
.B "--read0" .B "--read0"
@ -475,56 +518,110 @@ query matches entries that start with \fBcore\fR and end with either \fBgo\fR,
e.g. \fB^core go$ | rb$ | py$\fR e.g. \fB^core go$ | rb$ | py$\fR
.SH KEY BINDINGS .SH KEY/EVENT BINDINGS
You can customize key bindings of fzf with \fB--bind\fR option which takes \fB--bind\fR option allows you to bind \fBa key\fR or \fBan event\fR to one or
a comma-separated list of key binding expressions. Each key binding expression more \fBactions\fR. You can use it to customize key bindings or implement
follows the following format: \fBKEY:ACTION\fR dynamic behaviors.
e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR \fB--bind\fR takes a comma-separated list of binding expressions. Each binding
expression is \fBKEY:ACTION\fR or \fBEVENT:ACTION\fR.
.B AVAILABLE KEYS: (SYNONYMS) e.g.
\fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
.SS AVAILABLE KEYS: (SYNONYMS)
\fIctrl-[a-z]\fR \fIctrl-[a-z]\fR
.br
\fIctrl-space\fR \fIctrl-space\fR
.br
\fIctrl-\\\fR
.br
\fIctrl-]\fR
.br
\fIctrl-^\fR (\fIctrl-6\fR)
.br
\fIctrl-/\fR (\fIctrl-_\fR)
.br
\fIctrl-alt-[a-z]\fR \fIctrl-alt-[a-z]\fR
.br
\fIalt-[a-z]\fR \fIalt-[a-z]\fR
.br
\fIalt-[0-9]\fR \fIalt-[0-9]\fR
.br
\fIf[1-12]\fR \fIf[1-12]\fR
.br
\fIenter\fR (\fIreturn\fR \fIctrl-m\fR) \fIenter\fR (\fIreturn\fR \fIctrl-m\fR)
.br
\fIspace\fR \fIspace\fR
.br
\fIbspace\fR (\fIbs\fR) \fIbspace\fR (\fIbs\fR)
.br
\fIalt-up\fR \fIalt-up\fR
.br
\fIalt-down\fR \fIalt-down\fR
.br
\fIalt-left\fR \fIalt-left\fR
.br
\fIalt-right\fR \fIalt-right\fR
.br
\fIalt-enter\fR \fIalt-enter\fR
.br
\fIalt-space\fR \fIalt-space\fR
.br
\fIalt-bspace\fR (\fIalt-bs\fR) \fIalt-bspace\fR (\fIalt-bs\fR)
.br
\fIalt-/\fR \fIalt-/\fR
.br
\fItab\fR \fItab\fR
.br
\fIbtab\fR (\fIshift-tab\fR) \fIbtab\fR (\fIshift-tab\fR)
.br
\fIesc\fR \fIesc\fR
.br
\fIdel\fR \fIdel\fR
.br
\fIup\fR \fIup\fR
.br
\fIdown\fR \fIdown\fR
.br
\fIleft\fR \fIleft\fR
.br
\fIright\fR \fIright\fR
.br
\fIhome\fR \fIhome\fR
.br
\fIend\fR \fIend\fR
.br
\fIpgup\fR (\fIpage-up\fR) \fIpgup\fR (\fIpage-up\fR)
.br
\fIpgdn\fR (\fIpage-down\fR) \fIpgdn\fR (\fIpage-down\fR)
.br
\fIshift-up\fR \fIshift-up\fR
.br
\fIshift-down\fR \fIshift-down\fR
.br
\fIshift-left\fR \fIshift-left\fR
.br
\fIshift-right\fR \fIshift-right\fR
.br
\fIleft-click\fR \fIleft-click\fR
.br
\fIright-click\fR \fIright-click\fR
.br
\fIdouble-click\fR \fIdouble-click\fR
.br
or any single character or any single character
Additionally, a special event named \fIchange\fR is available which is .SS AVAILABLE EVENTS:
triggered whenever the query string is changed. \fIchange\fR (triggered whenever the query string is changed)
.br
e.g. \fBfzf --bind change:top\fR e.g.
\fB# Moves cursor to the top (or bottom depending on --layout) whenever the query is changed
fzf --bind change:top\fR
.SS AVAILABLE ACTIONS:
A key or an event can be bound to one or more of the following actions.
\fBACTION: DEFAULT BINDINGS (NOTES): \fBACTION: DEFAULT BINDINGS (NOTES):
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
@ -563,6 +660,7 @@ triggered whenever the query string is changed.
\fBpreview-page-up\fR \fBpreview-page-up\fR
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR) \fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBprint-query\fR (print query and exit) \fBprint-query\fR (print query and exit)
\fBreload(...)\fR (see below for the details)
\fBreplace-query\fR (replace query string with the current selection) \fBreplace-query\fR (replace query string with the current selection)
\fBselect-all\fR \fBselect-all\fR
\fBtoggle\fR (\fIright-click\fR) \fBtoggle\fR (\fIright-click\fR)
@ -580,10 +678,15 @@ triggered whenever the query string is changed.
\fBup\fR \fIctrl-k ctrl-p up\fR \fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR \fByank\fR \fIctrl-y\fR
.SS ACTION COMPOSITION
Multiple actions can be chained using \fB+\fR separator. Multiple actions can be chained using \fB+\fR separator.
e.g.
\fBfzf --bind 'ctrl-a:select-all+accept'\fR \fBfzf --bind 'ctrl-a:select-all+accept'\fR
.SS COMMAND EXECUTION
With \fBexecute(...)\fR action, you can execute arbitrary commands without With \fBexecute(...)\fR action, you can execute arbitrary commands without
leaving fzf. For example, you can turn fzf into a simple file browser by leaving fzf. For example, you can turn fzf into a simple file browser by
binding \fBenter\fR key to \fBless\fR command like follows. binding \fBenter\fR key to \fBless\fR command like follows.
@ -611,9 +714,9 @@ parse errors.
\fBexecute|...|\fR \fBexecute|...|\fR
\fBexecute:...\fR \fBexecute:...\fR
.RS .RS
This is the special form that frees you from parse errors as it does not expect The last one is the special form that frees you from parse errors as it does
the closing character. The catch is that it should be the last one in the not expect the closing character. The catch is that it should be the last one
comma-separated list of key-action pairs. in the comma-separated list of key-action pairs.
.RE .RE
fzf switches to the alternate screen when executing a command. However, if the fzf switches to the alternate screen when executing a command. However, if the
@ -623,6 +726,26 @@ executes the command without the switching. Note that fzf will not be
responsive until the command is complete. For asynchronous execution, start responsive until the command is complete. For asynchronous execution, start
your command as a background process (i.e. appending \fB&\fR). your command as a background process (i.e. appending \fB&\fR).
.SS RELOAD INPUT
\fBreload(...)\fR action is used to dynamically update the input list
without restarting fzf. It takes the same command template with placeholder
expressions as \fBexecute(...)\fR.
See \fIhttps://github.com/junegunn/fzf/issues/1750\fR for more info.
e.g.
\fB# Update the list of processes by pressing CTRL-R
ps -ef | fzf --bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \\
--header-lines=1 --layout=reverse
# Integration with ripgrep
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="foobar"
FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \\
fzf --bind "change:reload:$RG_PREFIX {q} || true" \\
--ansi --phony --query "$INITIAL_QUERY"\fR
.SH AUTHOR .SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR) Junegunn Choi (\fIjunegunn.c@gmail.com\fR)

View File

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

View File

@ -126,6 +126,7 @@ func Run(opts *Options, revision string) {
return false return false
} }
item.text, item.colors = ansiProcessor([]byte(transformed)) item.text, item.colors = ansiProcessor([]byte(transformed))
item.text.TrimTrailingWhitespaces()
item.text.Index = itemIndex item.text.Index = itemIndex
item.origText = &data item.origText = &data
itemIndex++ itemIndex++
@ -135,10 +136,11 @@ func Run(opts *Options, revision string) {
// Reader // Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
var reader *Reader
if !streamingFilter { if !streamingFilter {
reader := NewReader(func(data []byte) bool { reader = NewReader(func(data []byte) bool {
return chunkList.Push(data) return chunkList.Push(data)
}, eventBox, opts.ReadZero) }, eventBox, opts.ReadZero, opts.Filter == nil)
go reader.ReadSource() go reader.ReadSource()
} }
@ -182,7 +184,7 @@ func Run(opts *Options, revision string) {
} }
} }
return false return false
}, eventBox, opts.ReadZero) }, eventBox, opts.ReadZero, false)
reader.ReadSource() reader.ReadSource()
} else { } else {
eventBox.Unwatch(EvtReadNew) eventBox.Unwatch(EvtReadNew)
@ -223,10 +225,23 @@ func Run(opts *Options, revision string) {
// Event coordination // Event coordination
reading := true reading := true
ticks := 0 ticks := 0
var nextCommand *string
restart := func(command string) {
reading = true
chunkList.Clear()
header = make([]string, 0, opts.HeaderLines)
go reader.restart(command)
}
eventBox.Watch(EvtReadNew) eventBox.Watch(EvtReadNew)
for { for {
delay := true delay := true
ticks++ ticks++
input := func() []rune {
if opts.Phony {
return []rune{}
}
return []rune(terminal.Input())
}
eventBox.Wait(func(events *util.Events) { eventBox.Wait(func(events *util.Events) {
if _, fin := (*events)[EvtReadFin]; fin { if _, fin := (*events)[EvtReadFin]; fin {
delete(*events, EvtReadNew) delete(*events, EvtReadNew)
@ -235,21 +250,38 @@ func Run(opts *Options, revision string) {
switch evt { switch evt {
case EvtReadNew, EvtReadFin: case EvtReadNew, EvtReadFin:
clearCache := false
if evt == EvtReadFin && nextCommand != nil {
clearCache = true
restart(*nextCommand)
nextCommand = nil
} else {
reading = reading && evt == EvtReadNew reading = reading && evt == EvtReadNew
}
snapshot, count := chunkList.Snapshot() snapshot, count := chunkList.Snapshot()
terminal.UpdateCount(count, !reading, value.(bool)) terminal.UpdateCount(count, !reading, value.(*string))
if opts.Sync { if opts.Sync {
terminal.UpdateList(PassMerger(&snapshot, opts.Tac)) terminal.UpdateList(PassMerger(&snapshot, opts.Tac))
} }
matcher.Reset(snapshot, terminal.Input(), false, !reading, sort) matcher.Reset(snapshot, input(), false, !reading, sort, clearCache)
case EvtSearchNew: case EvtSearchNew:
var command *string
switch val := value.(type) { switch val := value.(type) {
case bool: case searchRequest:
sort = val sort = val.sort
command = val.command
}
if command != nil {
if reading {
reader.terminate()
nextCommand = command
} else {
restart(*command)
}
} }
snapshot, _ := chunkList.Snapshot() snapshot, _ := chunkList.Snapshot()
matcher.Reset(snapshot, terminal.Input(), true, !reading, sort) matcher.Reset(snapshot, input(), true, !reading, sort, command != nil)
delay = false delay = false
case EvtSearchProgress: case EvtSearchProgress:

View File

@ -16,6 +16,7 @@ type MatchRequest struct {
pattern *Pattern pattern *Pattern
final bool final bool
sort bool sort bool
clearCache bool
} }
// Matcher is responsible for performing search // Matcher is responsible for performing search
@ -69,7 +70,7 @@ func (m *Matcher) Loop() {
events.Clear() events.Clear()
}) })
if request.sort != m.sort { if request.sort != m.sort || request.clearCache {
m.sort = request.sort m.sort = request.sort
m.mergerCache = make(map[string]*Merger) m.mergerCache = make(map[string]*Merger)
clearChunkCache() clearChunkCache()
@ -221,7 +222,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
} }
// Reset is called to interrupt/signal the ongoing search // Reset is called to interrupt/signal the ongoing search
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool) { func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, clearCache bool) {
pattern := m.patternBuilder(patternRunes) pattern := m.patternBuilder(patternRunes)
var event util.EventType var event util.EventType
@ -230,5 +231,5 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
} else { } else {
event = reqRetry event = reqRetry
} }
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable}) m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, clearCache})
} }

View File

@ -33,6 +33,7 @@ const usage = `usage: fzf [options]
-d, --delimiter=STR Field delimiter regex (default: AWK-style) -d, --delimiter=STR Field delimiter regex (default: AWK-style)
+s, --no-sort Do not sort the result +s, --no-sort Do not sort the result
--tac Reverse the order of the input --tac Reverse the order of the input
--phony Do not perform search
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
when the scores are tied [length|begin|end|index] when the scores are tied [length|begin|end|index]
(default: length) (default: length)
@ -56,7 +57,7 @@ const usage = `usage: fzf [options]
--layout=LAYOUT Choose layout: [default|reverse|reverse-list] --layout=LAYOUT Choose layout: [default|reverse|reverse-list]
--border Draw border above and below the finder --border Draw border above and below the finder
--margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L) --margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
--inline-info Display finder info inline with the query --info=STYLE Finder info style [default|inline|hidden]
--prompt=STR Input prompt (default: '> ') --prompt=STR Input prompt (default: '> ')
--header=STR String to print as header --header=STR String to print as header
--header-lines=N The first N lines of the input are treated as header --header-lines=N The first N lines of the input are treated as header
@ -141,12 +142,21 @@ const (
layoutReverseList layoutReverseList
) )
type infoStyle int
const (
infoDefault infoStyle = iota
infoInline
infoHidden
)
type previewOpts struct { type previewOpts struct {
command string command string
position windowPosition position windowPosition
size sizeSpec size sizeSpec
hidden bool hidden bool
wrap bool wrap bool
border bool
} }
// Options stores the values of command-line options // Options stores the values of command-line options
@ -154,6 +164,7 @@ type Options struct {
Fuzzy bool Fuzzy bool
FuzzyAlgo algo.Algo FuzzyAlgo algo.Algo
Extended bool Extended bool
Phony bool
Case Case Case Case
Normalize bool Normalize bool
Nth []Range Nth []Range
@ -175,7 +186,7 @@ type Options struct {
Hscroll bool Hscroll bool
HscrollOff int HscrollOff int
FileWord bool FileWord bool
InlineInfo bool InfoStyle infoStyle
JumpLabels string JumpLabels string
Prompt string Prompt string
Query string Query string
@ -207,6 +218,7 @@ func defaultOptions() *Options {
Fuzzy: true, Fuzzy: true,
FuzzyAlgo: algo.FuzzyMatchV2, FuzzyAlgo: algo.FuzzyMatchV2,
Extended: true, Extended: true,
Phony: false,
Case: CaseSmart, Case: CaseSmart,
Normalize: true, Normalize: true,
Nth: make([]Range, 0), Nth: make([]Range, 0),
@ -227,7 +239,7 @@ func defaultOptions() *Options {
Hscroll: true, Hscroll: true,
HscrollOff: 10, HscrollOff: 10,
FileWord: false, FileWord: false,
InlineInfo: false, InfoStyle: infoDefault,
JumpLabels: defaultJumpLabels, JumpLabels: defaultJumpLabels,
Prompt: "> ", Prompt: "> ",
Query: "", Query: "",
@ -237,7 +249,7 @@ func defaultOptions() *Options {
ToggleSort: false, ToggleSort: false,
Expect: make(map[int]string), Expect: make(map[int]string),
Keymap: make(map[int][]action), Keymap: make(map[int][]action),
Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false}, Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false, true},
PrintQuery: false, PrintQuery: false,
ReadZero: false, ReadZero: false,
Printer: func(str string) { fmt.Println(str) }, Printer: func(str string) { fmt.Println(str) },
@ -414,6 +426,14 @@ func parseKeyChords(str string, message string) map[int]string {
chord = tui.BSpace chord = tui.BSpace
case "ctrl-space": case "ctrl-space":
chord = tui.CtrlSpace chord = tui.CtrlSpace
case "ctrl-^", "ctrl-6":
chord = tui.CtrlCaret
case "ctrl-/", "ctrl-_":
chord = tui.CtrlSlash
case "ctrl-\\":
chord = tui.CtrlBackSlash
case "ctrl-]":
chord = tui.CtrlRightBracket
case "change": case "change":
chord = tui.Change chord = tui.Change
case "alt-enter", "alt-return": case "alt-enter", "alt-return":
@ -628,13 +648,15 @@ func init() {
// Backreferences are not supported. // Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
executeRegexp = regexp.MustCompile( executeRegexp = regexp.MustCompile(
`(?si):(execute(?:-multi|-silent)?):.+|:(execute(?:-multi|-silent)?)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`) `(?si):(execute(?:-multi|-silent)?|reload):.+|:(execute(?:-multi|-silent)?|reload)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
} }
func parseKeymap(keymap map[int][]action, str string) { func parseKeymap(keymap map[int][]action, str string) {
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
prefix := ":execute" prefix := ":execute"
if src[len(prefix)] == '-' { if strings.HasPrefix(src, ":reload") {
prefix = ":reload"
} else if src[len(prefix)] == '-' {
c := src[len(prefix)+1] c := src[len(prefix)+1]
if c == 's' || c == 'S' { if c == 's' || c == 'S' {
prefix += "-silent" prefix += "-silent"
@ -787,6 +809,8 @@ func parseKeymap(keymap map[int][]action, str string) {
} else { } else {
var offset int var offset int
switch t { switch t {
case actReload:
offset = len("reload")
case actExecuteSilent: case actExecuteSilent:
offset = len("execute-silent") offset = len("execute-silent")
case actExecuteMulti: case actExecuteMulti:
@ -822,6 +846,8 @@ func isExecuteAction(str string) actionType {
prefix = matches[0][2] prefix = matches[0][2]
} }
switch prefix { switch prefix {
case "reload":
return actReload
case "execute": case "execute":
return actExecute return actExecute
case "execute-silent": case "execute-silent":
@ -887,6 +913,20 @@ func parseLayout(str string) layoutType {
return layoutDefault return layoutDefault
} }
func parseInfoStyle(str string) infoStyle {
switch str {
case "default":
return infoDefault
case "inline":
return infoInline
case "hidden":
return infoHidden
default:
errorExit("invalid info style (expected: default / inline / hidden)")
}
return infoDefault
}
func parsePreviewWindow(opts *previewOpts, input string) { func parsePreviewWindow(opts *previewOpts, input string) {
// Default // Default
opts.position = posRight opts.position = posRight
@ -898,6 +938,7 @@ func parsePreviewWindow(opts *previewOpts, input string) {
sizeRegex := regexp.MustCompile("^[0-9]+%?$") sizeRegex := regexp.MustCompile("^[0-9]+%?$")
for _, token := range tokens { for _, token := range tokens {
switch token { switch token {
case "":
case "hidden": case "hidden":
opts.hidden = true opts.hidden = true
case "wrap": case "wrap":
@ -910,6 +951,10 @@ func parsePreviewWindow(opts *previewOpts, input string) {
opts.position = posLeft opts.position = posLeft
case "right": case "right":
opts.position = posRight opts.position = posRight
case "border":
opts.border = true
case "noborder":
opts.border = false
default: default:
if sizeRegex.MatchString(token) { if sizeRegex.MatchString(token) {
opts.size = parseSize(token, 99, "window size") opts.size = parseSize(token, 99, "window size")
@ -1014,6 +1059,10 @@ func parseOptions(opts *Options, allArgs []string) {
} }
case "--no-expect": case "--no-expect":
opts.Expect = make(map[int]string) opts.Expect = make(map[int]string)
case "--no-phony":
opts.Phony = false
case "--phony":
opts.Phony = true
case "--tiebreak": case "--tiebreak":
opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
case "--bind": case "--bind":
@ -1088,10 +1137,15 @@ func parseOptions(opts *Options, allArgs []string) {
opts.FileWord = true opts.FileWord = true
case "--no-filepath-word": case "--no-filepath-word":
opts.FileWord = false opts.FileWord = false
case "--info":
opts.InfoStyle = parseInfoStyle(
nextString(allArgs, &i, "info style required"))
case "--no-info":
opts.InfoStyle = infoHidden
case "--inline-info": case "--inline-info":
opts.InlineInfo = true opts.InfoStyle = infoInline
case "--no-inline-info": case "--no-inline-info":
opts.InlineInfo = false opts.InfoStyle = infoDefault
case "--jump-labels": case "--jump-labels":
opts.JumpLabels = nextString(allArgs, &i, "label characters required") opts.JumpLabels = nextString(allArgs, &i, "label characters required")
validateJumpLabels = true validateJumpLabels = true
@ -1146,7 +1200,7 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Preview.command = "" opts.Preview.command = ""
case "--preview-window": case "--preview-window":
parsePreviewWindow(&opts.Preview, parsePreviewWindow(&opts.Preview,
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:wrap][:hidden]")) nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:noborder][:wrap][:hidden]"))
case "--height": case "--height":
opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]")) opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]"))
case "--min-height": case "--min-height":
@ -1199,6 +1253,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.MinHeight = atoi(value) opts.MinHeight = atoi(value)
} else if match, value := optString(arg, "--layout="); match { } else if match, value := optString(arg, "--layout="); match {
opts.Layout = parseLayout(value) opts.Layout = parseLayout(value)
} else if match, value := optString(arg, "--info="); match {
opts.InfoStyle = parseInfoStyle(value)
} else if match, value := optString(arg, "--toggle-sort="); match { } else if match, value := optString(arg, "--toggle-sort="); match {
parseToggleSort(opts.Keymap, value) parseToggleSort(opts.Keymap, value)
} else if match, value := optString(arg, "--expect="); match { } else if match, value := optString(arg, "--expect="); match {

View File

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

View File

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

View File

@ -60,7 +60,7 @@ var emptyLine = itemLine{}
// Terminal represents terminal input/output // Terminal represents terminal input/output
type Terminal struct { type Terminal struct {
initDelay time.Duration initDelay time.Duration
inlineInfo bool infoStyle infoStyle
prompt string prompt string
promptLen int promptLen int
queryLen [2]int queryLen [2]int
@ -102,7 +102,7 @@ type Terminal struct {
count int count int
progress int progress int
reading bool reading bool
success bool failed *string
jumping jumpMode jumping jumpMode
jumpLabels string jumpLabels string
printer func(string) printer func(string)
@ -228,6 +228,7 @@ const (
actExecuteMulti // Deprecated actExecuteMulti // Deprecated
actSigStop actSigStop
actTop actTop
actReload
) )
type placeholderFlags struct { type placeholderFlags struct {
@ -238,6 +239,11 @@ type placeholderFlags struct {
file bool file bool
} }
type searchRequest struct {
sort bool
command *string
}
func toActions(types ...actionType) []action { func toActions(types ...actionType) []action {
actions := make([]action, len(types)) actions := make([]action, len(types))
for idx, t := range types { for idx, t := range types {
@ -355,7 +361,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
if previewBox != nil && (opts.Preview.position == posUp || opts.Preview.position == posDown) { if previewBox != nil && (opts.Preview.position == posUp || opts.Preview.position == posDown) {
effectiveMinHeight *= 2 effectiveMinHeight *= 2
} }
if opts.InlineInfo { if opts.InfoStyle != infoDefault {
effectiveMinHeight -= 1 effectiveMinHeight -= 1
} }
if opts.Bordered { if opts.Bordered {
@ -374,7 +380,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
} }
t := Terminal{ t := Terminal{
initDelay: delay, initDelay: delay,
inlineInfo: opts.InlineInfo, infoStyle: opts.InfoStyle,
queryLen: [2]int{0, 0}, queryLen: [2]int{0, 0},
layout: opts.Layout, layout: opts.Layout,
fullscreen: fullscreen, fullscreen: fullscreen,
@ -408,7 +414,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
ansi: opts.Ansi, ansi: opts.Ansi,
tabstop: opts.Tabstop, tabstop: opts.Tabstop,
reading: true, reading: true,
success: true, failed: nil,
jumping: jumpDisabled, jumping: jumpDisabled,
jumpLabels: opts.JumpLabels, jumpLabels: opts.JumpLabels,
printer: opts.Printer, printer: opts.Printer,
@ -432,6 +438,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
return &t return &t
} }
func (t *Terminal) noInfoLine() bool {
return t.infoStyle != infoDefault
}
// Input returns current query string // Input returns current query string
func (t *Terminal) Input() []rune { func (t *Terminal) Input() []rune {
t.mutex.Lock() t.mutex.Lock()
@ -440,11 +450,11 @@ func (t *Terminal) Input() []rune {
} }
// UpdateCount updates the count information // UpdateCount updates the count information
func (t *Terminal) UpdateCount(cnt int, final bool, success bool) { func (t *Terminal) UpdateCount(cnt int, final bool, failedCommand *string) {
t.mutex.Lock() t.mutex.Lock()
t.count = cnt t.count = cnt
t.reading = !final t.reading = !final
t.success = success t.failed = failedCommand
t.mutex.Unlock() t.mutex.Unlock()
t.reqBox.Set(reqInfo, nil) t.reqBox.Set(reqInfo, nil)
if final { if final {
@ -614,7 +624,11 @@ func (t *Terminal) resizeWindows() {
noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode) noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
if previewVisible { if previewVisible {
createPreviewWindow := func(y int, x int, w int, h int) { createPreviewWindow := func(y int, x int, w int, h int) {
t.pborder = t.tui.NewWindow(y, x, w, h, tui.MakeBorderStyle(tui.BorderAround, t.unicode)) previewBorder := tui.MakeBorderStyle(tui.BorderAround, t.unicode)
if !t.preview.border {
previewBorder = tui.MakeTransparentBorder()
}
t.pborder = t.tui.NewWindow(y, x, w, h, previewBorder)
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
@ -666,7 +680,7 @@ func (t *Terminal) move(y int, x int, clear bool) {
y = h - y - 1 y = h - y - 1
case layoutReverseList: case layoutReverseList:
n := 2 + len(t.header) n := 2 + len(t.header)
if t.inlineInfo { if t.noInfoLine() {
n-- n--
} }
if y < n { if y < n {
@ -719,7 +733,17 @@ func (t *Terminal) printPrompt() {
func (t *Terminal) printInfo() { func (t *Terminal) printInfo() {
pos := 0 pos := 0
if t.inlineInfo { switch t.infoStyle {
case infoDefault:
t.move(1, 0, true)
if t.reading {
duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
t.window.CPrint(tui.ColSpinner, t.strong, _spinner[idx])
}
t.move(1, 2, false)
pos = 2
case infoInline:
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1 pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
if pos+len(" < ") > t.window.Width() { if pos+len(" < ") > t.window.Width() {
return return
@ -731,18 +755,13 @@ func (t *Terminal) printInfo() {
t.window.CPrint(tui.ColPrompt, t.strong, " < ") t.window.CPrint(tui.ColPrompt, t.strong, " < ")
} }
pos += len(" < ") pos += len(" < ")
} else { case infoHidden:
t.move(1, 0, true) return
if t.reading {
duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
t.window.CPrint(tui.ColSpinner, t.strong, _spinner[idx])
}
t.move(1, 2, false)
pos = 2
} }
output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count) found := t.merger.Length()
total := util.Max(found, t.count)
output := fmt.Sprintf("%d/%d", found, total)
if t.toggleSort { if t.toggleSort {
if t.sort { if t.sort {
output += " +S" output += " +S"
@ -760,17 +779,16 @@ 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)
} }
if !t.success && t.count == 0 { if t.failed != nil && t.count == 0 {
if len(os.Getenv("FZF_DEFAULT_COMMAND")) > 0 { output = fmt.Sprintf("[Command failed: %s]", *t.failed)
output = "[$FZF_DEFAULT_COMMAND failed]"
} else {
output = "[default command failed - $FZF_DEFAULT_COMMAND required]"
} }
maxWidth := t.window.Width() - pos
if len(output) > maxWidth {
outputRunes, _ := t.trimRight([]rune(output), maxWidth-2)
output = string(outputRunes) + ".."
} }
if pos+len(output) <= t.window.Width() {
t.window.CPrint(tui.ColInfo, 0, output) t.window.CPrint(tui.ColInfo, 0, output)
} }
}
func (t *Terminal) printHeader() { func (t *Terminal) printHeader() {
if len(t.header) == 0 { if len(t.header) == 0 {
@ -780,7 +798,7 @@ func (t *Terminal) printHeader() {
var state *ansiState var state *ansiState
for idx, lineStr := range t.header { for idx, lineStr := range t.header {
line := idx + 2 line := idx + 2
if t.inlineInfo { if t.noInfoLine() {
line-- line--
} }
if line >= max { if line >= max {
@ -809,7 +827,7 @@ func (t *Terminal) printList() {
i = maxy - 1 - j i = maxy - 1 - j
} }
line := i + 2 + len(t.header) line := i + 2 + len(t.header)
if t.inlineInfo { if t.noInfoLine() {
line-- line--
} }
if i < count { if i < count {
@ -1383,7 +1401,7 @@ func (t *Terminal) hasPreviewWindow() bool {
func (t *Terminal) currentItem() *Item { func (t *Terminal) currentItem() *Item {
cnt := t.merger.Length() cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy { if t.cy >= 0 && cnt > 0 && cnt > t.cy {
return t.merger.Get(t.cy).item return t.merger.Get(t.cy).item
} }
return nil return nil
@ -1422,7 +1440,7 @@ func (t *Terminal) selectItem(item *Item) bool {
return false return false
} }
if _, found := t.selected[item.Index()]; found { if _, found := t.selected[item.Index()]; found {
return false return true
} }
t.selected[item.Index()] = selectedItem{time.Now(), item} t.selected[item.Index()] = selectedItem{time.Now(), item}
@ -1508,12 +1526,11 @@ func (t *Terminal) Loop() {
t.mutex.Lock() t.mutex.Lock()
reading := t.reading reading := t.reading
t.mutex.Unlock() t.mutex.Unlock()
if !reading {
break
}
time.Sleep(spinnerDuration) time.Sleep(spinnerDuration)
if reading {
t.reqBox.Set(reqInfo, nil) t.reqBox.Set(reqInfo, nil)
} }
}
}() }()
} }
@ -1533,7 +1550,7 @@ func (t *Terminal) Loop() {
// We don't display preview window if no match // We don't display preview window if no match
if request[0] != nil { if request[0] != nil {
command := replacePlaceholder(t.preview.command, command := replacePlaceholder(t.preview.command,
t.ansi, t.delimiter, t.printsep, false, string(t.input), request) t.ansi, t.delimiter, t.printsep, false, string(t.Input()), request)
cmd := util.ExecCommand(command, true) cmd := util.ExecCommand(command, true)
if t.pwindow != nil { if t.pwindow != nil {
env := os.Environ() env := os.Environ()
@ -1584,9 +1601,6 @@ func (t *Terminal) Loop() {
} }
exit := func(getCode func() int) { exit := func(getCode func() int) {
if !t.cleanExit && t.fullscreen && t.inlineInfo {
t.placeCursor()
}
t.tui.Close() t.tui.Close()
code := getCode() code := getCode()
if code <= exitNoMatch && t.history != nil { if code <= exitNoMatch && t.history != nil {
@ -1607,7 +1621,7 @@ func (t *Terminal) Loop() {
switch req { switch req {
case reqPrompt: case reqPrompt:
t.printPrompt() t.printPrompt()
if t.inlineInfo { if t.noInfoLine() {
t.printInfo() t.printInfo()
} }
case reqInfo: case reqInfo:
@ -1673,6 +1687,10 @@ func (t *Terminal) Loop() {
looping := true looping := true
for looping { for looping {
var newCommand *string
changed := false
queryChanged := false
event := t.tui.GetChar() event := t.tui.GetChar()
t.mutex.Lock() t.mutex.Lock()
@ -1754,9 +1772,7 @@ func (t *Terminal) Loop() {
} }
case actToggleSort: case actToggleSort:
t.sort = !t.sort t.sort = !t.sort
t.eventBox.Set(EvtSearchNew, t.sort) changed = true
t.mutex.Unlock()
return false
case actPreviewUp: case actPreviewUp:
if t.hasPreviewWindow() { if t.hasPreviewWindow() {
scrollPreview(-1) scrollPreview(-1)
@ -1987,7 +2003,7 @@ func (t *Terminal) Loop() {
my -= t.window.Top() my -= t.window.Top()
mx = util.Constrain(mx-t.promptLen, 0, len(t.input)) mx = util.Constrain(mx-t.promptLen, 0, len(t.input))
min := 2 + len(t.header) min := 2 + len(t.header)
if t.inlineInfo { if t.noInfoLine() {
min-- min--
} }
h := t.window.Height() h := t.window.Height()
@ -2025,10 +2041,25 @@ func (t *Terminal) Loop() {
} }
} }
} }
case actReload:
t.failed = nil
valid, list := t.buildPlusList(a.a, false)
// If the command template has {q}, we run the command even when the
// query string is empty.
if !valid {
_, query := hasPreviewFlags(a.a)
valid = query
}
if valid {
command := replacePlaceholder(a.a,
t.ansi, t.delimiter, t.printsep, false, string(t.input), list)
newCommand = &command
t.selected = make(map[int32]selectedItem)
}
} }
return true return true
} }
changed := false
mapkey := event.Type mapkey := event.Type
if t.jumping == jumpDisabled { if t.jumping == jumpDisabled {
actions := t.keymap[mapkey] actions := t.keymap[mapkey]
@ -2042,8 +2073,9 @@ func (t *Terminal) Loop() {
continue continue
} }
t.truncateQuery() t.truncateQuery()
changed = string(previousInput) != string(t.input) queryChanged = string(previousInput) != string(t.input)
if onChanges, prs := t.keymap[tui.Change]; changed && prs { changed = changed || queryChanged
if onChanges, prs := t.keymap[tui.Change]; queryChanged && prs {
if !doActions(onChanges, tui.Change) { if !doActions(onChanges, tui.Change) {
continue continue
} }
@ -2061,7 +2093,7 @@ func (t *Terminal) Loop() {
req(reqList) req(reqList)
} }
if changed { if queryChanged {
if t.isPreviewEnabled() { if t.isPreviewEnabled() {
_, q := hasPreviewFlags(t.preview.command) _, q := hasPreviewFlags(t.preview.command)
if q { if q {
@ -2070,14 +2102,14 @@ func (t *Terminal) Loop() {
} }
} }
if changed || t.cx != previousCx { if queryChanged || t.cx != previousCx {
req(reqPrompt) req(reqPrompt)
} }
t.mutex.Unlock() // Must be unlocked before touching reqBox t.mutex.Unlock() // Must be unlocked before touching reqBox
if changed { if changed || newCommand != nil {
t.eventBox.Set(EvtSearchNew, t.sort) t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, command: newCommand})
} }
for _, event := range events { for _, event := range events {
t.reqBox.Set(event, nil) t.reqBox.Set(event, nil)
@ -2127,7 +2159,7 @@ func (t *Terminal) vset(o int) bool {
func (t *Terminal) maxItems() int { func (t *Terminal) maxItems() int {
max := t.window.Height() - 2 - len(t.header) max := t.window.Height() - 2 - len(t.header)
if t.inlineInfo { if t.noInfoLine() {
max++ max++
} }
return util.Max(max, 0) return util.Max(max, 0)

View File

@ -345,6 +345,14 @@ func (r *LightRenderer) GetChar() Event {
return Event{BSpace, 0, nil} return Event{BSpace, 0, nil}
case 0: case 0:
return Event{CtrlSpace, 0, nil} return Event{CtrlSpace, 0, nil}
case 28:
return Event{CtrlBackSlash, 0, nil}
case 29:
return Event{CtrlRightBracket, 0, nil}
case 30:
return Event{CtrlCaret, 0, nil}
case 31:
return Event{CtrlSlash, 0, nil}
case ESC: case ESC:
ev := r.escSequence(&sz) ev := r.escSequence(&sz)
// Second chance // Second chance

View File

@ -284,6 +284,12 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{keyfn('z'), 0, nil} return Event{keyfn('z'), 0, nil}
case tcell.KeyCtrlSpace: case tcell.KeyCtrlSpace:
return Event{CtrlSpace, 0, nil} return Event{CtrlSpace, 0, nil}
case tcell.KeyCtrlBackslash:
return Event{CtrlBackSlash, 0, nil}
case tcell.KeyCtrlRightSq:
return Event{CtrlRightBracket, 0, nil}
case tcell.KeyCtrlUnderscore:
return Event{CtrlSlash, 0, nil}
case tcell.KeyBackspace2: case tcell.KeyBackspace2:
if alt { if alt {
return Event{AltBS, 0, nil} return Event{AltBS, 0, nil}

View File

@ -40,6 +40,12 @@ const (
ESC ESC
CtrlSpace CtrlSpace
// https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal
CtrlBackSlash
CtrlRightBracket
CtrlCaret
CtrlSlash
Invalid Invalid
Resize Resize
Mouse Mouse
@ -215,6 +221,8 @@ type BorderStyle struct {
bottomRight rune bottomRight rune
} }
type BorderCharacter int
func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle { func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
if unicode { if unicode {
return BorderStyle{ return BorderStyle{
@ -238,6 +246,17 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
} }
} }
func MakeTransparentBorder() BorderStyle {
return BorderStyle{
shape: BorderAround,
horizontal: ' ',
vertical: ' ',
topLeft: ' ',
topRight: ' ',
bottomLeft: ' ',
bottomRight: ' '}
}
type Renderer interface { type Renderer interface {
Init() Init()
Pause(clear bool) Pause(clear bool)

View File

@ -142,6 +142,11 @@ func (chars *Chars) TrailingWhitespaces() int {
return whitespaces return whitespaces
} }
func (chars *Chars) TrimTrailingWhitespaces() {
whitespaces := chars.TrailingWhitespaces()
chars.slice = chars.slice[0 : len(chars.slice)-whitespaces]
}
func (chars *Chars) ToString() string { func (chars *Chars) ToString() string {
if runes := chars.optionalRunes(); runes != nil { if runes := chars.optionalRunes(); runes != nil {
return string(runes) return string(runes)

View File

@ -277,7 +277,7 @@ class TestGoFZF < TestBase
def test_fzf_default_command_failure def test_fzf_default_command_failure
tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', 'FZF_DEFAULT_COMMAND=false'), :Enter tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', 'FZF_DEFAULT_COMMAND=false'), :Enter
tmux.until { |lines| lines[-2].include?('FZF_DEFAULT_COMMAND failed') } tmux.until { |lines| lines[-2].include?('Command failed: false') }
tmux.send_keys :Enter tmux.send_keys :Enter
end end
@ -1516,6 +1516,11 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-1] == prompt } tmux.until { |lines| lines[-1] == prompt }
end end
def test_info_hidden
tmux.send_keys 'seq 10 | fzf --info=hidden', :Enter
tmux.until { |lines| lines[-2] == '> 1' }
end
def test_change_top def test_change_top
tmux.send_keys %(seq 1000 | #{FZF} --bind change:top), :Enter tmux.send_keys %(seq 1000 | #{FZF} --bind change:top), :Enter
tmux.until { |lines| lines.match_count == 1000 } tmux.until { |lines| lines.match_count == 1000 }
@ -1612,6 +1617,29 @@ class TestGoFZF < TestBase
tmux.send_keys "#{FZF} --preview 'cat #{tempname}'", :Enter tmux.send_keys "#{FZF} --preview 'cat #{tempname}'", :Enter
tmux.until { |lines| lines[1].include?('+ green') } tmux.until { |lines| lines[1].include?('+ green') }
end end
def test_phony
tmux.send_keys %(seq 1000 | #{FZF} --query 333 --phony --preview 'echo {} {q}'), :Enter
tmux.until { |lines| lines.match_count == 1000 }
tmux.until { |lines| lines[1].include?('1 333') }
tmux.send_keys 'foo'
tmux.until { |lines| lines.match_count == 1000 }
tmux.until { |lines| lines[1].include?('1 333foo') }
end
def test_reload
tmux.send_keys %(seq 1000 | #{FZF} --bind 'change:reload(seq {q}),a:reload(seq 100),b:reload:seq 200' --header-lines 2 --multi 2), :Enter
tmux.until { |lines| lines.match_count == 998 }
tmux.send_keys 'a'
tmux.until { |lines| lines.item_count == 98 && lines.match_count == 98 }
tmux.send_keys 'b'
tmux.until { |lines| lines.item_count == 198 && lines.match_count == 198 }
tmux.send_keys :Tab
tmux.until { |lines| lines[-2].include?('(1/2)') }
tmux.send_keys '555'
tmux.until { |lines| lines.item_count == 553 && lines.match_count == 1 }
tmux.until { |lines| !lines[-2].include?('(1/2)') }
end
end end
module TestShell module TestShell