summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2019-11-15 22:53:08 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2019-11-15 22:53:08 +0900
commitb47104203747b2c1af17c07df4eadd6928f5cebc (patch)
treed1efb3c538471a8497dcdf9dcea1ad10b7679e68
parent16fc6862a89eef0f02d32ab8b365887522719da8 (diff)
parent2886f06977579276d0c1a8273d29d5bdad23d628 (diff)
downloadfzf-b47104203747b2c1af17c07df4eadd6928f5cebc.tar.gz
Merge branch 'devel'
-rw-r--r--CHANGELOG.md51
-rw-r--r--man/man1/fzf-tmux.14
-rw-r--r--man/man1/fzf.1279
-rw-r--r--src/chunklist.go7
-rw-r--r--src/core.go50
-rw-r--r--src/matcher.go15
-rw-r--r--src/options.go74
-rw-r--r--src/reader.go75
-rw-r--r--src/reader_test.go11
-rw-r--r--src/terminal.go138
-rw-r--r--src/tui/light.go8
-rw-r--r--src/tui/tcell.go6
-rw-r--r--src/tui/tui.go19
-rw-r--r--src/util/chars.go5
-rwxr-xr-xtest/test_go.rb30
15 files changed, 595 insertions, 177 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 70293d82..b8eb3f5e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,57 @@
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
------
diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1
index 2bad0241..6a5ee467 100644
--- a/man/man1/fzf-tmux.1
+++ b/man/man1/fzf-tmux.1
@@ -1,7 +1,7 @@
.ig
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
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
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
fzf-tmux - open fzf in tmux split pane
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 3a994f65..877330cd 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -1,7 +1,7 @@
.ig
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
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
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
fzf - a command-line fuzzy finder
@@ -70,6 +70,10 @@ Transform the presentation of each line using field index expressions
.TP
.BI "-d, --delimiter=" "STR"
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
.TP
.B "+s, --no-sort"
@@ -79,7 +83,8 @@ Do not sort the result
Reverse the order of the input
.RS
-e.g. \fBhistory | fzf --tac --no-sort\fR
+e.g.
+ \fBhistory | fzf --tac --no-sort\fR
.RE
.TP
.BI "--tiebreak=" "CRI[,..]"
@@ -109,7 +114,8 @@ Comma-separated list of sort criteria to apply when the scores are tied.
.SS Interface
.TP
.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
.B "+m, --no-multi"
Disable multi-select
@@ -118,8 +124,8 @@ Disable multi-select
Disable mouse
.TP
.BI "--bind=" "KEYBINDS"
-Comma-separated list of custom key bindings. See \fBKEY BINDINGS\fR for the
-details.
+Comma-separated list of custom key bindings. See \fBKEY/EVENT BINDINGS\fR for
+the details.
.TP
.B "--cycle"
Enable cyclic scroll
@@ -201,12 +207,26 @@ terminal size with \fB%\fR suffix.
.br
.br
-e.g. \fBfzf --margin 10%\fR
- \fBfzf --margin 1,5%\fR
+e.g.
+ \fBfzf --margin 10%
+ fzf --margin 1,5%\fR
.RE
.TP
-.B "--inline-info"
-Display finder info inline with the query
+.BI "--info=" "STYLE"
+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
.BI "--prompt=" "STR"
Input prompt (default: '> ')
@@ -236,11 +256,6 @@ foreground/background color. You can also specify 24-bit color in \fB#rrggbb\fR
format.
.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)
@@ -264,6 +279,19 @@ e.g. \fBfzf --color=bg+:24\fR
\fBmarker \fRMulti-select marker
\fBspinner \fRStreaming input indicator
\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
.TP
.B "--no-bold"
@@ -291,8 +319,9 @@ string, specify field index expressions between the braces (See \fBFIELD INDEX
EXPRESSION\fR for the details).
.RS
-e.g. \fBfzf --preview='head -$LINES {}'\fR
- \fBls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR
+e.g.
+ \fBfzf --preview='head -$LINES {}'
+ ls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR
fzf exports \fB$FZF_PREVIEW_LINES\fR and \fB$FZF_PREVIEW_COLUMNS\fR so that
they represent the exact size of the preview window. (It also overrides
@@ -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
was made) individually quoted.
-e.g. \fBfzf --multi --preview='head -10 {+}'\fR
- \fBgit log --oneline | fzf --multi --preview 'git show {+1}'\fR
+e.g.
+ \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
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
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.
Preview window will be updated even when there is no match for the current
query if any of the placeholder expressions evaluates to a non-empty string.
.RE
.TP
-.BI "--preview-window=" "[POSITION][:SIZE[%]][:wrap][:hidden]"
-Determine the layout of the preview window. If the argument ends with
+.BI "--preview-window=" "[POSITION][:SIZE[%]][:noborder][:wrap][:hidden]"
+Determines the layout of the preview window. If the argument contains
\fB:hidden\fR, the preview window will be hidden by default until
\fBtoggle-preview\fR action is triggered. Long lines are truncated by default.
Line wrap can be enabled with \fB:wrap\fR flag.
@@ -338,8 +379,9 @@ execute the command in the background.
.RE
.RS
-e.g. \fBfzf --preview="head {}" --preview-window=up:30%\fR
- \fBfzf --preview="file {}" --preview-window=down:1\fR
+e.g.
+ \fBfzf --preview="head {}" --preview-window=up:30%
+ fzf --preview="file {}" --preview-window=down:1\fR
.RE
.SS Scripting
.TP
@@ -369,7 +411,8 @@ times, fzf will expect the union of the keys. \fB--no-expect\fR will clear the
list.
.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
.TP
.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
-.SH KEY BINDINGS
-You can customize key bindings of fzf with \fB--bind\fR option which takes
-a comma-separated list of key binding expressions. Each key binding expression
-follows the following format: \fBKEY:ACTION\fR
-
-e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
-
-.B AVAILABLE KEYS: (SYNONYMS)
- \fIctrl-[a-z]\fR
- \fIctrl-space\fR
- \fIctrl-alt-[a-z]\fR
- \fIalt-[a-z]\fR
- \fIalt-[0-9]\fR
- \fIf[1-12]\fR
- \fIenter\fR (\fIreturn\fR \fIctrl-m\fR)
- \fIspace\fR
- \fIbspace\fR (\fIbs\fR)
- \fIalt-up\fR
- \fIalt-down\fR
- \fIalt-left\fR
- \fIalt-right\fR
- \fIalt-enter\fR
- \fIalt-space\fR
- \fIalt-bspace\fR (\fIalt-bs\fR)
- \fIalt-/\fR
- \fItab\fR
- \fIbtab\fR (\fIshift-tab\fR)
- \fIesc\fR
- \fIdel\fR
- \fIup\fR
- \fIdown\fR
- \fIleft\fR
- \fIright\fR
- \fIhome\fR
- \fIend\fR
- \fIpgup\fR (\fIpage-up\fR)
- \fIpgdn\fR (\fIpage-down\fR)
- \fIshift-up\fR
- \fIshift-down\fR
- \fIshift-left\fR
- \fIshift-right\fR
- \fIleft-click\fR
- \fIright-click\fR
- \fIdouble-click\fR
- or any single character
-
-Additionally, a special event named \fIchange\fR is available which is
-triggered whenever the query string is changed.
-
- e.g. \fBfzf --bind change:top\fR
+.SH KEY/EVENT BINDINGS
+\fB--bind\fR option allows you to bind \fBa key\fR or \fBan event\fR to one or
+more \fBactions\fR. You can use it to customize key bindings or implement
+dynamic behaviors.
+
+\fB--bind\fR takes a comma-separated list of binding expressions. Each binding
+expression is \fBKEY:ACTION\fR or \fBEVENT:ACTION\fR.
+
+e.g.
+ \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
+
+.SS AVAILABLE KEYS: (SYNONYMS)
+\fIctrl-[a-z]\fR
+.br
+\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
+.br
+\fIalt-[a-z]\fR
+.br
+\fIalt-[0-9]\fR
+.br
+\fIf[1-12]\fR
+.br
+\fIenter\fR (\fIreturn\fR \fIctrl-m\fR)
+.br
+\fIspace\fR
+.br
+\fIbspace\fR (\fIbs\fR)
+.br
+\fIalt-up\fR
+.br
+\fIalt-down\fR
+.br
+\fIalt-left\fR
+.br
+\fIalt-right\fR
+.br
+\fIalt-enter\fR
+.br
+\fIalt-space\fR
+.br
+\fIalt-bspace\fR (\fIalt-bs\fR)
+.br
+\fIalt-/\fR
+.br
+\fItab\fR
+.br
+\fIbtab\fR (\fIshift-tab\fR)
+.br
+\fIesc\fR
+.br
+\fIdel\fR
+.br
+\fIup\fR
+.br
+\fIdown\fR
+.br
+\fIleft\fR
+.br
+\fIright\fR
+.br
+\fIhome\fR
+.br
+\fIend\fR
+.br
+\fIpgup\fR (\fIpage-up\fR)
+.br
+\fIpgdn\fR (\fIpage-down\fR)
+.br
+\fIshift-up\fR
+.br
+\fIshift-down\fR
+.br
+\fIshift-left\fR
+.br
+\fIshift-right\fR
+.br
+\fIleft-click\fR
+.br
+\fIright-click\fR
+.br
+\fIdouble-click\fR
+.br
+or any single character
+
+.SS AVAILABLE EVENTS:
+\fIchange\fR (triggered whenever the query string is changed)
+.br
+
+ 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):
\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
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBprint-query\fR (print query and exit)
+ \fBreload(...)\fR (see below for the details)
\fBreplace-query\fR (replace query string with the current selection)
\fBselect-all\fR
\fBtoggle\fR (\fIright-click\fR)
@@ -580,9 +678,14 @@ triggered whenever the query string is changed.
\fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR
+.SS ACTION COMPOSITION
+
Multiple actions can be chained using \fB+\fR separator.
- \fBfzf --bind 'ctrl-a:select-all+accept'\fR
+e.g.
+ \fBfzf --bind 'ctrl-a:select-all+accept'\fR
+
+.SS COMMAND EXECUTION
With \fBexecute(...)\fR action, you can execute arbitrary commands without
leaving fzf. For example, you can turn fzf into a simple file browser by
@@ -611,9 +714,9 @@ parse errors.
\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 of key-action pairs.
+The last one 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 of key-action pairs.
.RE
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
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
Junegunn Choi (\fIjunegunn.c@gmail.com\fR)
diff --git a/src/chunklist.go b/src/chunklist.go
index 510cd734..cd635c25 100644
--- a/src/chunklist.go
+++ b/src/chunklist.go
@@ -64,6 +64,13 @@ func (cl *ChunkList) Push(data []byte) bool {
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
func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
cl.mutex.Lock()
diff --git a/src/core.go b/src/core.go
index 2db5b3ae..9d118a49 100644
--- a/src/core.go
+++ b/src/core.go
@@ -126,6 +126,7 @@ func Run(opts *Options, revision string) {
return false
}
item.text, item.colors = ansiProcessor([]byte(transformed))
+ item.text.TrimTrailingWhitespaces()
item.text.Index = itemIndex
item.origText = &data
itemIndex++
@@ -135,10 +136,11 @@ func Run(opts *Options, revision string) {
// Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
+ var reader *Reader
if !streamingFilter {
- reader := NewReader(func(data []byte) bool {
+ reader = NewReader(func(data []byte) bool {
return chunkList.Push(data)
- }, eventBox, opts.ReadZero)
+ }, eventBox, opts.ReadZero, opts.Filter == nil)
go reader.ReadSource()
}
@@ -182,7 +184,7 @@ func Run(opts *Options, revision string) {
}
}
return false
- }, eventBox, opts.ReadZero)
+ }, eventBox, opts.ReadZero, false)
reader.ReadSource()
} else {
eventBox.Unwatch(EvtReadNew)
@@ -223,10 +225,23 @@ func Run(opts *Options, revision string) {
// Event coordination
reading := true
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)
for {
delay := true
ticks++
+ input := func() []rune {
+ if opts.Phony {
+ return []rune{}
+ }
+ return []rune(terminal.Input())
+ }
eventBox.Wait(func(events *util.Events) {
if _, fin := (*events)[EvtReadFin]; fin {
delete(*events, EvtReadNew)
@@ -235,21 +250,38 @@ func Run(opts *Options, revision string) {
switch evt {
case EvtReadNew, EvtReadFin:
- reading = reading && evt == EvtReadNew
+ clearCache := false
+ if evt == EvtReadFin && nextCommand != nil {
+ clearCache = true
+ restart(*nextCommand)
+ nextCommand = nil
+ } else {
+ reading = reading && evt == EvtReadNew
+ }
snapshot, count := chunkList.Snapshot()
- terminal.UpdateCount(count, !reading, value.(bool))
+ terminal.UpdateCount(count, !reading, value.(*string))
if opts.Sync {
terminal.UpdateList(PassMerger(&snapshot, opts.Tac))
}
- matcher.Reset(snapshot, terminal.Input(), false, !reading, sort)
+ matcher.Reset(snapshot, input(), false, !reading, sort, clearCache)
case EvtSearchNew:
+ var command *string
switch val := value.(type) {
- case bool:
- sort = val
+ case searchRequest:
+ sort = val.sort
+ command = val.command
+ }
+ if command != nil {
+ if reading {
+ reader.terminate()
+ nextCommand = command
+ } else {
+ restart(*command)
+ }
}
snapshot, _ := chunkList.Snapshot()
- matcher.Reset(snapshot, terminal.Input(), true, !reading, sort)
+ matcher.Reset(snapshot, input(), true, !reading, sort, command != nil)
delay = false
case EvtSearchProgress:
diff --git a/src/matcher.go b/src/matcher.go
index 69250873..22aa819c 100644
--- a/src/matcher.go
+++ b/src/matcher.go
@@ -12,10 +12,11 @@ import (
// MatchRequest represents a search request
type MatchRequest struct {
- chunks []*Chunk
- pattern *Pattern
- final bool
- sort bool
+ chunks []*Chunk
+ pattern *Pattern
+ final bool
+ sort bool
+ clearCache bool
}
// Matcher is responsible for performing search
@@ -69,7 +70,7 @@ func (m *Matcher) Loop() {
events.Clear()
})
- if request.sort != m.sort {
+ if request.sort != m.sort || request.clearCache {
m.sort = request.sort
m.mergerCache = make(map[string]*Merger)
clearChunkCache()
@@ -221,7 +222,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
}
// 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)
var event util.EventType
@@ -230,5 +231,5 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
} else {
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})
}
diff --git a/src/options.go b/src/options.go
index 2ab3a896..9b20e55a 100644
--- a/src/options.go
+++ b/src/options.go
@@ -33,6 +33,7 @@ const usage = `usage: fzf [options]
-d, --delimiter=STR Field delimiter regex (default: AWK-style)
+s, --no-sort Do not sort the result
--tac Reverse the order of the input
+ --phony Do not perform search
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
when the scores are tied [length|begin|end|index]
(default: length)
@@ -56,7 +57,7 @@ const usage = `usage: fzf [options]
--layout=LAYOUT Choose layout: [default|reverse|reverse-list]
--border Draw border above and below the finder
--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: '> ')
--header=STR String to print as header
--header-lines=N The first N lines of the input are treated as header
@@ -141,12 +142,21 @@ const (
layoutReverseList
)
+type infoStyle int
+
+const (
+ infoDefault infoStyle = iota
+ infoInline
+ infoHidden
+)
+
type previewOpts struct {
command string
position windowPosition
size sizeSpec
hidden bool
wrap bool
+ border bool
}
// Options stores the values of command-line options
@@ -154,6 +164,7 @@ type Options struct {
Fuzzy bool
FuzzyAlgo algo.Algo
Extended bool
+ Phony bool
Case Case
Normalize bool
Nth []Range
@@ -175,7 +186,7 @@ type Options struct {
Hscroll bool
HscrollOff int
FileWord bool
- InlineInfo bool
+ InfoStyle infoStyle
JumpLabels string
Prompt string
Query string
@@ -207,6 +218,7 @@ func defaultOptions() *Options {
Fuzzy: true,
FuzzyAlgo: algo.FuzzyMatchV2,
Extended: true,
+ Phony: false,
Case: CaseSmart,
Normalize: true,
Nth: make([]Range, 0),
@@ -227,7 +239,7 @@ func defaultOptions() *Options {
Hscroll: true,
HscrollOff: 10,
FileWord: false,
- InlineInfo: false,
+ InfoStyle: infoDefault,
JumpLabels: defaultJumpLabels,
Prompt: "> ",
Query: "",
@@ -237,7 +249,7 @@ func defaultOptions() *Options {
ToggleSort: false,
Expect: make(map[int]string),
Keymap: make(map[int][]action),
- Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false},
+ Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false, true},
PrintQuery: false,
ReadZero: false,
Printer: func(str string) { fmt.Println(str) },
@@ -414,6 +426,14 @@ func parseKeyChords(str string, message string) map[int]string {
chord = tui.BSpace
case "ctrl-space":
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":
chord = tui.Change
case "alt-enter", "alt-return":
@@ -628,13 +648,15 @@ func init() {
// Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
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) {
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
prefix := ":execute"
- if src[len(prefix)] == '-' {
+ if strings.HasPrefix(src, ":reload") {
+ prefix = ":reload"
+ } else if src[len(prefix)] == '-' {
c := src[len(prefix)+1]
if c == 's' || c == 'S' {
prefix += "-silent"
@@ -787,6 +809,8 @@ func parseKeymap(keymap map[int][]action, str string) {
} else {
var offset int
switch t {
+ case actReload:
+ offset = len("reload")
case actExecuteSilent:
offset = len("execute-silent")
case actExecuteMulti:
@@ -822,6 +846,8 @@ func isExecuteAction(str string) actionType {
prefix = matches[0][2]
}
switch prefix {
+ case "reload":
+ return actReload
case "execute":
return actExecute
case "execute-silent":
@@ -887,6 +913,20 @@ func parseLayout(str string) layoutType {
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) {
// Default
opts.position = posRight
@@ -898,6 +938,7 @@ func parsePreviewWindow(opts *previewOpts, input string) {
sizeRegex := regexp.MustCompile("^[0-9]+%?$")
for _, token := range tokens {
switch token {
+ case "":
case "hidden":
opts.hidden = true
case "wrap":
@@ -910,6 +951,10 @@ func parsePreviewWindow(opts *previewOpts, input string) {
opts.position = posLeft
case "right":
opts.position = posRight
+ case "border":
+ opts.border = true
+ case "noborder":
+ opts.border = false
default:
if sizeRegex.MatchString(token) {
opts.size = parseSize(token, 99, "window size")
@@ -1014,6 +1059,10 @@ func parseOptions(opts *Options, allArgs []string) {
}
case "--no-expect":
opts.Expect = make(map[int]string)
+ case "--no-phony":
+ opts.Phony = false
+ case "--phony":
+ opts.Phony = true
case "--tiebreak":
opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
case "--bind":
@@ -1088,10 +1137,15 @@ func parseOptions(opts *Options, allArgs []string) {
opts.FileWord = true
case "--no-filepath-word":
opts.FileWord = false
+ case "--info":
+ opts.InfoStyle = parseInfoStyle(
+ nextString(allArgs, &i, "info style required"))
+ case "--no-info":
+ opts.InfoStyle = infoHidden
case "--inline-info":
- opts.InlineInfo = true
+ opts.InfoStyle = infoInline
case "--no-inline-info":
- opts.InlineInfo = false
+ opts.InfoStyle = infoDefault
case "--jump-labels":
opts.JumpLabels = nextString(allArgs, &i, "label characters required")
validateJumpLabels = true
@@ -1146,7 +1200,7 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Preview.command = ""
case "--preview-window":
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":
opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]"))
case "--min-height":
@@ -1199,6 +1253,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.MinHeight = atoi(value)
} else if match, value := optString(arg, "--layout="); match {
opts.Layout = parseLayout(value)
+ } else if match, value := optString(arg, "--info="); match {
+ opts.InfoStyle = parseInfoStyle(value)
} else if match, value := optString(arg, "--toggle-sort="); match {
parseToggleSort(opts.Keymap, value)
} else if match, value := optString(arg, "--expect="); match {
diff --git a/src/reader.go b/src/reader.go
index b418f549..b388411b 100644
--- a/src/reader.go
+++ b/src/reader.go
@@ -4,6 +4,8 @@ import (
"bufio"
"io"
"os"
+ "os/exec"
+ "sync"
"sync/atomic"
"time"
@@ -16,11 +18,17 @@ type Reader struct {
eventBox *util.EventBox
delimNil bool
event int32
+ finChan chan bool
+ mutex sync.Mutex
+ exec *exec.Cmd
+ command *string
+ killed bool
+ wait bool
}
// NewReader returns new Reader object
-func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool) *Reader {
- return &Reader{pusher, eventBox, delimNil, int32(EvtReady)}
+func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool, wait bool) *Reader {
+ return &Reader{pusher, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait}
}
func (r *Reader) startEventPoller() {
@@ -29,9 +37,12 @@ func (r *Reader) startEventPoller() {
pollInterval := readerPollIntervalMin
for {
if atomic.CompareAndSwapInt32(ptr, int32(EvtReadNew), int32(EvtReady)) {
- r.eventBox.Set(EvtReadNew, true)
+ r.eventBox.Set(EvtReadNew, (*string)(nil))
pollInterval = readerPollIntervalMin
} else if atomic.LoadInt32(ptr) == int32(EvtReadFin) {
+ if r.wait {
+ r.finChan <- true
+ }
return
} else {
pollInterval += readerPollIntervalStep
@@ -46,7 +57,37 @@ func (r *Reader) startEventPoller() {
func (r *Reader) fin(success bool) {
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
@@ -54,12 +95,13 @@ func (r *Reader) ReadSource() {
r.startEventPoller()
var success bool
if util.IsTty() {
+ // The default command for *nix requires bash
+ shell := "bash"
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 {
- // The default command for *nix requires bash
- success = r.readFromCommand("bash", defaultCommand)
+ success = r.readFromCommand(&shell, defaultCommand)
} else {
- success = r.readFromCommand("sh", cmd)
+ success = r.readFromCommand(nil, cmd)
}
} else {
success = r.readFromStdin()
@@ -102,16 +144,25 @@ func (r *Reader) readFromStdin() bool {
return true
}
-func (r *Reader) readFromCommand(shell string, cmd string) bool {
- listCommand := util.ExecCommandWith(shell, cmd, false)
- out, err := listCommand.StdoutPipe()
+func (r *Reader) readFromCommand(shell *string, command string) bool {
+ r.mutex.Lock()
+ 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 {
+ r.mutex.Unlock()
return false
}
- err = listCommand.Start()
+ err = r.exec.Start()
+ r.mutex.Unlock()
if err != nil {
return false
}
r.feed(out)
- return listCommand.Wait() == nil
+ return r.exec.Wait() == nil
}
diff --git a/src/reader_test.go b/src/reader_test.go
index c29936ce..8bbb488e 100644
--- a/src/reader_test.go
+++ b/src/reader_test.go
@@ -10,10 +10,9 @@ import (
func TestReadFromCommand(t *testing.T) {
strs := []string{}
eb := util.NewEventBox()
- reader := Reader{
- pusher: func(s []byte) bool { strs = append(strs, string(s)); return true },
- eventBox: eb,
- event: int32(EvtReady)}
+ reader := NewReader(
+ func(s []byte) bool { strs = append(strs, string(s)); return true },
+ eb, false, true)
reader.startEventPoller()
@@ -23,7 +22,7 @@ func TestReadFromCommand(t *testing.T) {
}
// 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" {
t.Errorf("%s", strs)
}
@@ -48,7 +47,7 @@ func TestReadFromCommand(t *testing.T) {
reader.startEventPoller()
// Failing command
- reader.fin(reader.readFromCommand("sh", `no-such-command`))
+ reader.fin(reader.readFromCommand(nil, `no-such-command`))
strs = []string{}
if len(strs) > 0 {
t.Errorf("%s", strs)
diff --git a/src/terminal.go b/src/terminal.go
index 3c656cce..4cd507a3 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -60,7 +60,7 @@ var emptyLine = itemLine{}
// Terminal represents terminal input/output
type Terminal struct {
initDelay time.Duration
- inlineInfo bool
+ infoStyle infoStyle
prompt string
promptLen int
queryLen [2]int
@@ -102,7 +102,7 @@ type Terminal struct {
count int
progress int
reading bool
- success bool
+ failed *string
jumping jumpMode
jumpLabels string
printer func(string)
@@ -228,6 +228,7 @@ const (
actExecuteMulti // Deprecated
actSigStop
actTop
+ actReload
)
type placeholderFlags struct {
@@ -238,6 +239,11 @@ type placeholderFlags struct {
file bool
}
+type searchRequest struct {
+ sort bool
+ command *string
+}
+
func toActions(types ...actionType) []action {
actions := make([]action, len(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) {
effectiveMinHeight *= 2
}
- if opts.InlineInfo {
+ if opts.InfoStyle != infoDefault {
effectiveMinHeight -= 1
}
if opts.Bordered {
@@ -374,7 +380,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
}
t := Terminal{
initDelay: delay,
- inlineInfo: opts.InlineInfo,
+ infoStyle: opts.InfoStyle,
queryLen: [2]int{0, 0},
layout: opts.Layout,
fullscreen: fullscreen,
@@ -408,7 +414,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
ansi: opts.Ansi,
tabstop: opts.Tabstop,
reading: true,
- success: true,
+ failed: nil,
jumping: jumpDisabled,
jumpLabels: opts.JumpLabels,
printer: opts.Printer,
@@ -432,6 +438,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
return &t
}
+func (t *Terminal) noInfoLine() bool {
+ return t.infoStyle != infoDefault
+}
+
// Input returns current query string
func (t *Terminal) Input() []rune {
t.mutex.Lock()
@@ -440,11 +450,11 @@ func (t *Terminal) Input() []rune {
}
// 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.count = cnt
t.reading = !final
- t.success = success
+ t.failed = failedCommand
t.mutex.Unlock()
t.reqBox.Set(reqInfo, nil)
if final {
@@ -614,7 +624,11 @@ func (t *Terminal) resizeWindows() {
noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
if previewVisible {
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
// 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
@@ -666,7 +680,7 @@ func (t *Terminal) move(y int, x int, clear bool) {
y = h - y - 1
case layoutReverseList:
n := 2 + len(t.header)
- if t.inlineInfo {
+ if t.noInfoLine() {
n--
}
if y < n {
@@ -719,7 +733,17 @@ func (t *Terminal) printPrompt() {
func (t *Terminal) printInfo() {
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
if pos+len(" < ") > t.window.Width() {
return
@@ -731,18 +755,13 @@ func (t *Terminal) printInfo() {
t.window.CPrint(tui.ColPrompt, t.strong, " < ")
}
pos += len(" < ")
- } else {
- 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 infoHidden:
+ return
}
- 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.sort {
output += " +S"
@@ -760,16 +779,15 @@ func (t *Terminal) printInfo() {
if t.progress > 0 && t.progress < 100 {
output += fmt.Sprintf(" (%d%%)", t.progress)
}
- if !t.success && t.count == 0 {
- if len(os.Getenv("FZF_DEFAULT_COMMAND")) > 0 {
- output = "[$FZF_DEFAULT_COMMAND failed]"
- } else {
- output = "[default command failed - $FZF_DEFAULT_COMMAND required]"
- }
+ if t.failed != nil && t.count == 0 {
+ output = fmt.Sprintf("[Command failed: %s]", *t.failed)
}
- if pos+len(output) <= t.window.Width() {
- t.window.CPrint(tui.ColInfo, 0, output)
+ maxWidth := t.window.Width() - pos
+ if len(output) > maxWidth {
+ outputRunes, _ := t.trimRight([]rune(output), maxWidth-2)
+ output = string(outputRunes) + ".."
}
+ t.window.CPrint(tui.ColInfo, 0, output)
}
func (t *Terminal) printHeader() {
@@ -780,7 +798,7 @@ func (t *Terminal) printHeader() {
var state *ansiState
for idx, lineStr := range t.header {
line := idx + 2
- if t.inlineInfo {
+ if t.noInfoLine() {
line--
}
if line >= max {
@@ -809,7 +827,7 @@ func (t *Terminal) printList() {
i = maxy - 1 - j
}
line := i + 2 + len(t.header)
- if t.inlineInfo {
+ if t.noInfoLine() {
line--
}
if i < count {
@@ -1383,7 +1401,7 @@ func (t *Terminal) hasPreviewWindow() bool {
func (t *Terminal) currentItem() *Item {
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 nil
@@ -1422,7 +1440,7 @@ func (t *Terminal) selectItem(item *Item) bool {
return false
}
if _, found := t.selected[item.Index()]; found {
- return false
+ return true
}
t.selected[item.Index()] = selectedItem{time.Now(), item}
@@ -1508,11 +1526,10 @@ func (t *Terminal) Loop() {
t.mutex.Lock()
reading := t.reading
t.mutex.Unlock()
- if !reading {
- break
- }
time.Sleep(spinnerDuration)
- t.reqBox.Set(reqInfo, nil)
+ if reading {
+ t.reqBox.Set(reqInfo, nil)
+ }
}
}()
}
@@ -1533,7 +1550,7 @@ func (t *Terminal) Loop() {
// We don't display preview window if no match
if request[0] != nil {
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)
if t.pwindow != nil {
env := os.Environ()
@@ -1584,9 +1601,6 @@ func (t *Terminal) Loop() {
}
exit := func(getCode func() int) {
- if !t.cleanExit && t.fullscreen && t.inlineInfo {
- t.placeCursor()
- }
t.tui.Close()
code := getCode()
if code <= exitNoMatch && t.history != nil {
@@ -1607,7 +1621,7 @@ func (t *Terminal) Loop() {
switch req {
case reqPrompt:
t.printPrompt()
- if t.inlineInfo {
+ if t.noInfoLine() {
t.printInfo()
}
case reqInfo:
@@ -1673,6 +1687,10 @@ func (t *Terminal) Loop() {
looping := true
for looping {
+ var newCommand *string
+ changed := false
+ queryChanged := false
+
event := t.tui.GetChar()
t.mutex.Lock()
@@ -1754,9 +1772,7 @@ func (t *Terminal) Loop() {
}
case actToggleSort:
t.sort = !t.sort
- t.eventBox.Set(EvtSearchNew, t.sort)
- t.mutex.Unlock()
- return false
+ changed = true
case actPreviewUp:
if t.hasPreviewWindow() {
scrollPreview(-1)
@@ -1987,7 +2003,7 @@ func (t *Terminal) Loop() {
my -= t.window.Top()
mx = util.Constrain(mx-t.promptLen, 0, len(t.input))
min := 2 + len(t.header)
- if t.inlineInfo {
+ if t.noInfoLine() {
min--
}
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
}
- changed := false
mapkey := event.Type
if t.jumping == jumpDisabled {
actions := t.keymap[mapkey]
@@ -2042,8 +2073,9 @@ func (t *Terminal) Loop() {
continue
}
t.truncateQuery()
- changed = string(previousInput) != string(t.input)
- if onChanges, prs := t.keymap[tui.Change]; changed && prs {
+ queryChanged = string(previousInput) != string(t.input)
+ changed = changed || queryChanged
+ if onChanges, prs := t.keymap[tui.Change]; queryChanged && prs {
if !doActions(onChanges, tui.Change) {
continue
}
@@ -2061,7 +2093,7 @@ func (t *Terminal) Loop() {
req(reqList)
}
- if changed {
+ if queryChanged {
if t.isPreviewEnabled() {
_, q := hasPreviewFlags(t.preview.command)
if q {
@@ -2070,14 +2102,14 @@ func (t *Terminal) Loop() {
}
}
- if changed || t.cx != previousCx {
+ if queryChanged || t.cx != previousCx {
req(reqPrompt)
}
t.mutex.Unlock() // Must be unlocked before touching reqBox
- if changed {
- t.eventBox.Set(EvtSearchNew, t.sort)
+ if changed || newCommand != nil {
+ t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, command: newCommand})
}
for _, event := range events {
t.reqBox.Set(event, nil)
@@ -2127,7 +2159,7 @@ func (t *Terminal) vset(o int) bool {
func (t *Terminal) maxItems() int {
max := t.window.Height() - 2 - len(t.header)
- if t.inlineInfo {
+ if t.noInfoLine() {
max++
}
return util.Max(max, 0)
diff --git a/src/tui/light.go b/src/tui/light.go
index 43d7efee..d1020a99 100644
--- a/src/tui/light.go
+++ b/src/tui/light.go
@@ -345,6 +345,14 @@ func (r *LightRenderer) GetChar() Event {
return Event{BSpace, 0, nil}
case 0:
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:
ev := r.escSequence(&sz)
// Second chance
diff --git a/src/tui/tcell.go b/src/tui/tcell.go
index 098e8a18..4bd7c812 100644
--- a/src/tui/tcell.go
+++ b/src/tui/tcell.go
@@ -284,6 +284,12 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{keyfn('z'), 0, nil}
case tcell.KeyCtrlSpace:
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:
if alt {
return Event{AltBS, 0, nil}
diff --git a/src/tui/tui.go b/src/tui/tui.go
index 9b821940..5a2e8d1b 100644
--- a/src/tui/tui.go
+++ b/src/tui/tui.go
@@ -40,6 +40,12 @@ const (
ESC
CtrlSpace
+ // https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal
+ CtrlBackSlash
+ CtrlRightBracket
+ CtrlCaret
+ CtrlSlash
+
Invalid
Resize
Mouse
@@ -215,6 +221,8 @@ type BorderStyle struct {
bottomRight rune
}
+type BorderCharacter int
+
func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
if unicode {
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 {
Init()
Pause(clear bool)
diff --git a/src/util/chars.go b/src/util/chars.go
index e36ab769..a57ba4bb 100644
--- a/src/util/chars.go
+++ b/src/util/chars.go
@@ -142,6 +142,11 @@ func (chars *Chars) TrailingWhitespaces() int {
return whitespaces
}
+func (chars *Chars) TrimTrailingWhitespaces() {
+ whitespaces := chars.TrailingWhitespaces()
+ chars.slice = chars.slice[0 : len(chars.slice)-whitespaces]
+}
+
func (chars *Chars) ToString() string {
if runes := chars.optionalRunes(); runes != nil {
return string(runes)
diff --git a/test/test_go.rb b/test/test_go.rb
index 9282f9ee..501641e2 100755
--- a/test/test_go.rb
+++ b/test/test_go.rb
@@ -277,7 +277,7 @@ class TestGoFZF < TestBase
def test_fzf_default_command_failure
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
end
@@ -1516,6 +1516,11 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-1] == prompt }
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
tmux.send_keys %(seq 1000 | #{FZF} --bind change:top), :Enter
tmux.until { |lines| lines.match_count == 1000 }
@@ -1612,6 +1617,29 @@ class TestGoFZF < TestBase
tmux.send_keys "#{FZF} --preview 'cat #{tempname}'", :Enter
tmux.until { |lines| lines[1].include?('+ green') }
end
+
+ def test_phony
+ tmux.send_keys %(seq 1000 | #{FZF} --query 333 --phony --preview 'echo {} {q}'), :Enter
+ tmux.until { |lines| lines.match_count == 1000 }
+ tmux.until { |lines| lines[1].include?('1 333') }
+ tmux.send_keys 'foo'
+ tmux.until { |lines| lines.match_count == 1000 }
+ tmux.until { |lines| lines[1].include?('1 333foo') }
+ end
+
+ def test_reload
+ tmux.send_keys %(seq 1000 | #{FZF} --bind 'change:reload(seq {q}),a:reload(seq 100),b:reload:seq 200' --header-lines 2 --multi 2), :Enter
+ tmux.until { |lines| lines.match_count == 998 }
+ tmux.send_keys 'a'
+ tmux.until { |lines| lines.item_count == 98 && lines.match_count == 98 }
+ tmux.send_keys 'b'
+ tmux.until { |lines| lines.item_count == 198 && lines.match_count == 198 }
+ tmux.send_keys :Tab
+ tmux.until { |lines| lines[-2].include?('(1/2)') }
+ tmux.send_keys '555'
+ tmux.until { |lines| lines.item_count == 553 && lines.match_count == 1 }
+ tmux.until { |lines| !lines[-2].include?('(1/2)') }
+ end
end
module TestShell