mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-16 23:33:39 -05:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6eb1874c5a | ||
|
|
4c70745cc1 | ||
|
|
7795748a3f | ||
|
|
098ef4d7cf | ||
|
|
e3f91bfe1b | ||
|
|
7374fe73a3 | ||
|
|
d2bde205f0 | ||
|
|
5620f70f9a | ||
|
|
37f258b1bf | ||
|
|
e2dd2a133e | ||
|
|
7514644e07 | ||
|
|
16b0aeda7d | ||
|
|
86e4f4a841 | ||
|
|
607eacf8c7 | ||
|
|
7a049644a8 | ||
|
|
17a13f00f8 | ||
|
|
43436e48e0 | ||
|
|
5a39102405 | ||
|
|
94999101e3 | ||
|
|
e619b7c4f4 | ||
|
|
b7c2e8cb67 | ||
|
|
fb76893e18 | ||
|
|
88d812fe82 | ||
|
|
77f9f4664a | ||
|
|
5c2f85c39e | ||
|
|
ac4d22cd12 | ||
|
|
cf95e44cb4 | ||
|
|
65dd2bb429 | ||
|
|
6be855be6a | ||
|
|
b6e3f4423b | ||
|
|
0c61d81713 | ||
|
|
7c6f5dba63 | ||
|
|
44cfc7e62a | ||
|
|
96670d5f16 | ||
|
|
36b971ee4e | ||
|
|
f1a9629652 |
2
.github/workflows/typos.yml
vendored
2
.github/workflows/typos.yml
vendored
@@ -7,4 +7,4 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: crate-ci/typos@v1.13.16
|
||||
- uses: crate-ci/typos@v1.14.10
|
||||
|
||||
@@ -1 +1 @@
|
||||
golang 1.20.2
|
||||
golang 1.20.4
|
||||
|
||||
55
ADVANCED.md
55
ADVANCED.md
@@ -1,8 +1,8 @@
|
||||
Advanced fzf examples
|
||||
======================
|
||||
|
||||
* *Last update: 2023/02/15*
|
||||
* *Requires fzf 0.38.0 or above*
|
||||
* *Last update: 2023/05/26*
|
||||
* *Requires fzf 0.41.0 or above*
|
||||
|
||||
---
|
||||
|
||||
@@ -336,9 +336,8 @@ projects, and it will free up memory as you narrow down the results.
|
||||
# 3. Open the file in Vim
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
|
||||
fzf --ansi \
|
||||
--disabled --query "$INITIAL_QUERY" \
|
||||
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload:$RG_PREFIX {q}" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--delimiter : \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
@@ -348,11 +347,11 @@ fzf --ansi \
|
||||
|
||||

|
||||
|
||||
- Instead of starting fzf in `rg ... | fzf` form, we start fzf without an
|
||||
explicit input, but with a custom `FZF_DEFAULT_COMMAND` variable. This way
|
||||
fzf can kill the initial Ripgrep process it starts with the initial query.
|
||||
Otherwise, the initial Ripgrep process will keep consuming system resources
|
||||
even after `reload` is triggered.
|
||||
- Instead of starting fzf in the usual `rg ... | fzf` form, we start fzf with
|
||||
an empty input (`: | fzf`), then we make it start the initial Ripgrep
|
||||
process immediately via `start:reload` binding. This way, fzf owns the
|
||||
initial Ripgrep process so it can kill it on the next `reload`. Otherwise,
|
||||
the process will keep running in the background.
|
||||
- Filtering is no longer a responsibility of fzf; hence `--disabled`
|
||||
- `{q}` in the reload command evaluates to the query string on fzf prompt.
|
||||
- `sleep 0.1` in the reload command is for "debouncing". This small delay will
|
||||
@@ -376,12 +375,11 @@ fzf-only search mode by *"unbinding"* `reload` action from `change` event.
|
||||
# 3. Open the file in Vim
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
|
||||
fzf --ansi \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--disabled --query "$INITIAL_QUERY" \
|
||||
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload:$RG_PREFIX {q}" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--bind "alt-enter:unbind(change,alt-enter)+change-prompt(2. fzf> )+enable-search+clear-query" \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--prompt '1. ripgrep> ' \
|
||||
--delimiter : \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
@@ -421,14 +419,12 @@ CTRL-F.
|
||||
rm -f /tmp/rg-fzf-{r,f}
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
|
||||
fzf --ansi \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--disabled --query "$INITIAL_QUERY" \
|
||||
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload($RG_PREFIX {q})+unbind(ctrl-r)" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+rebind(ctrl-r)+transform-query(echo {q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f)" \
|
||||
--bind "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || true)+rebind(change,ctrl-f)+transform-query(echo {q} > /tmp/rg-fzf-f; cat /tmp/rg-fzf-r)" \
|
||||
--bind "start:unbind(ctrl-r)" \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--prompt '1. ripgrep> ' \
|
||||
--delimiter : \
|
||||
--header '╱ CTRL-R (ripgrep mode) ╱ CTRL-F (fzf mode) ╱' \
|
||||
@@ -471,16 +467,17 @@ Kubernetes pods.
|
||||
|
||||
```bash
|
||||
pods() {
|
||||
FZF_DEFAULT_COMMAND="kubectl get pods --all-namespaces" \
|
||||
fzf --info=inline --layout=reverse --header-lines=1 \
|
||||
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
|
||||
--header $'╱ Enter (kubectl exec) ╱ CTRL-O (open log in editor) ╱ CTRL-R (reload) ╱\n\n' \
|
||||
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
|
||||
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \
|
||||
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \
|
||||
--bind 'ctrl-r:reload:$FZF_DEFAULT_COMMAND' \
|
||||
--preview-window up:follow \
|
||||
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
|
||||
: | command='kubectl get pods --all-namespaces' fzf \
|
||||
--info=inline --layout=reverse --header-lines=1 \
|
||||
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
|
||||
--header $'╱ Enter (kubectl exec) ╱ CTRL-O (open log in editor) ╱ CTRL-R (reload) ╱\n\n' \
|
||||
--bind 'start:reload:$command' \
|
||||
--bind 'ctrl-r:reload:$command' \
|
||||
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
|
||||
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \
|
||||
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \
|
||||
--preview-window up:follow \
|
||||
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
54
CHANGELOG.md
54
CHANGELOG.md
@@ -1,6 +1,58 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.41.1
|
||||
------
|
||||
- Fixed a bug where preview window is not updated when `--disabled` is set and
|
||||
a reload is triggered by `change:reload` binding
|
||||
|
||||
0.41.0
|
||||
------
|
||||
- Added color name `preview-border` and `preview-scrollbar`
|
||||
- Added new border style `block` which uses [block elements](https://en.wikipedia.org/wiki/Block_Elements)
|
||||
- `--scrollbar` can take two characters, one for the main window, the other
|
||||
for the preview window
|
||||
- Putting it altogether:
|
||||
```sh
|
||||
fzf-tmux -p 80% --padding 1,2 --preview 'bat --style=plain --color=always {}' \
|
||||
--color 'bg:237,bg+:235,gutter:237,border:238,scrollbar:236' \
|
||||
--color 'preview-bg:235,preview-border:236,preview-scrollbar:234' \
|
||||
--preview-window 'border-block' --border block --scrollbar '▌▐'
|
||||
```
|
||||
- Bug fixes and improvements
|
||||
|
||||
0.40.0
|
||||
------
|
||||
- Added `zero` event that is triggered when there's no match
|
||||
```sh
|
||||
# Reload the candidate list when there's no match
|
||||
echo $RANDOM | fzf --bind 'zero:reload(echo $RANDOM)+clear-query' --height 3
|
||||
```
|
||||
- New actions
|
||||
- Added `track` action which makes fzf track the current item when the
|
||||
search result is updated. If the user manually moves the cursor, or the
|
||||
item is not in the updated search result, tracking is automatically
|
||||
disabled. Tracking is useful when you want to see the surrounding items
|
||||
by deleting the query string.
|
||||
```sh
|
||||
# Narrow down the list with a query, point to a command,
|
||||
# and hit CTRL-T to see its surrounding commands.
|
||||
export FZF_CTRL_R_OPTS="
|
||||
--preview 'echo {}' --preview-window up:3:hidden:wrap
|
||||
--bind 'ctrl-/:toggle-preview'
|
||||
--bind 'ctrl-t:track+clear-query'
|
||||
--bind 'ctrl-y:execute-silent(echo -n {2..} | pbcopy)+abort'
|
||||
--color header:italic
|
||||
--header 'Press CTRL-Y to copy command into clipboard'"
|
||||
```
|
||||
- Added `change-header(...)`
|
||||
- Added `transform-header(...)`
|
||||
- Added `toggle-track` action
|
||||
- Fixed `--track` behavior when used with `--tac`
|
||||
- However, using `--track` with `--tac` is not recommended. The resulting
|
||||
behavior can be very confusing.
|
||||
- Bug fixes and improvements
|
||||
|
||||
0.39.0
|
||||
------
|
||||
- Added `one` event that is triggered when there's only one match
|
||||
@@ -176,7 +228,7 @@ CHANGELOG
|
||||
- Added color name `preview-label` for `--preview-label` (defaults to `label`
|
||||
for `--border-label`)
|
||||
- Better support for (Windows) terminals where each box-drawing character
|
||||
takes 2 columns. Set `RUNEWIDTH_EASTASIAN` environment variable to `1`.
|
||||
takes 2 columns. Set `RUNEWIDTH_EASTASIAN` environment variable to `0` or `1`.
|
||||
- On Vim, the variable will be automatically set if `&ambiwidth` is `double`
|
||||
- Behavior changes
|
||||
- fzf will always execute the preview command if the command template
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM archlinux
|
||||
FROM --platform=linux/amd64 archlinux
|
||||
RUN pacman -Sy && pacman --noconfirm -S awk git tmux zsh fish ruby procps go make gcc
|
||||
RUN gem install --no-document -v 5.14.2 minitest
|
||||
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc
|
||||
|
||||
2
Makefile
2
Makefile
@@ -88,7 +88,7 @@ bench:
|
||||
install: bin/fzf
|
||||
|
||||
build:
|
||||
goreleaser --rm-dist --snapshot
|
||||
goreleaser build --rm-dist --snapshot --skip-post-hooks
|
||||
|
||||
release:
|
||||
ifndef GITHUB_TOKEN
|
||||
|
||||
18
README.md
18
README.md
@@ -630,8 +630,8 @@ more details.
|
||||
#### 1. Update the list of processes by pressing CTRL-R
|
||||
|
||||
```sh
|
||||
FZF_DEFAULT_COMMAND='ps -ef' \
|
||||
fzf --bind 'ctrl-r:reload(eval "$FZF_DEFAULT_COMMAND")' \
|
||||
ps -ef |
|
||||
fzf --bind 'ctrl-r:reload(ps -ef)' \
|
||||
--header 'Press CTRL-R to reload' --header-lines=1 \
|
||||
--height=50% --layout=reverse
|
||||
```
|
||||
@@ -653,12 +653,12 @@ expression `{q}`. Also, note that we used `--disabled` option so that fzf
|
||||
doesn't perform any secondary filtering.
|
||||
|
||||
```sh
|
||||
INITIAL_QUERY=""
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \
|
||||
fzf --bind "change:reload:$RG_PREFIX {q} || true" \
|
||||
--ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--height=50% --layout=reverse
|
||||
: | rg_prefix='rg --column --line-number --no-heading --color=always --smart-case' \
|
||||
fzf --bind 'start:reload:$rg_prefix ""' \
|
||||
--bind 'change:reload:$rg_prefix {q} || true' \
|
||||
--bind 'enter:become(vim {1} +{2})' \
|
||||
--ansi --disabled \
|
||||
--height=50% --layout=reverse
|
||||
```
|
||||
|
||||
If ripgrep doesn't find any matches, it will exit with a non-zero exit status,
|
||||
@@ -666,7 +666,7 @@ and fzf will warn you about it. To suppress the warning message, we added
|
||||
`|| true` to the command, so that it always exits with 0.
|
||||
|
||||
See ["Using fzf as interactive Ripgrep launcher"](https://github.com/junegunn/fzf/blob/master/ADVANCED.md#using-fzf-as-interactive-ripgrep-launcher)
|
||||
for a fuller example with preview window options.
|
||||
for more sophisticated examples.
|
||||
|
||||
### Preview window
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ trap 'cleanup' EXIT
|
||||
envs="export TERM=$TERM "
|
||||
if [[ "$opt" =~ "-E" ]]; then
|
||||
tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
|
||||
if [[ $(bc -l <<< "$tmux_version > 3.2") = 1 ]]; then
|
||||
if [[ $(awk '{print ($1 > 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version > 3.2") = 1 ]]; then
|
||||
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
|
||||
opt="-B $opt"
|
||||
elif [[ $tmux_version = 3.2 ]]; then
|
||||
|
||||
4
go.mod
4
go.mod
@@ -7,8 +7,8 @@ require (
|
||||
github.com/mattn/go-shellwords v1.0.12
|
||||
github.com/rivo/uniseg v0.4.4
|
||||
github.com/saracen/walker v0.1.3
|
||||
golang.org/x/sys v0.6.0
|
||||
golang.org/x/term v0.6.0
|
||||
golang.org/x/sys v0.8.0
|
||||
golang.org/x/term v0.8.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
8
go.sum
8
go.sum
@@ -32,12 +32,12 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
|
||||
2
install
2
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.39.0
|
||||
version=0.41.1
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
$version="0.39.0"
|
||||
$version="0.41.1"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
|
||||
2
main.go
2
main.go
@@ -5,7 +5,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/protector"
|
||||
)
|
||||
|
||||
var version string = "0.39"
|
||||
var version string = "0.41"
|
||||
var revision string = "devel"
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -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 "Apr 2023" "fzf 0.39.0" "fzf-tmux - open fzf in tmux split pane"
|
||||
.TH fzf-tmux 1 "May 2023" "fzf 0.41.1" "fzf-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf-tmux - open fzf in tmux split pane
|
||||
|
||||
@@ -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 "Apr 2023" "fzf 0.39.0" "fzf - a command-line fuzzy finder"
|
||||
.TH fzf 1 "May 2023" "fzf 0.41.1" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
@@ -94,7 +94,10 @@ Do not sort the result
|
||||
.TP
|
||||
.B "--track"
|
||||
Make fzf track the current selection when the result list is updated.
|
||||
This can be useful when browsing logs using fzf with sorting disabled.
|
||||
This can be useful when browsing logs using fzf with sorting disabled. It is
|
||||
not recommended to use this option with \fB--tac\fR as the resulting behavior
|
||||
can be confusing. Also, consider using \fBtrack\fR action instead of this
|
||||
option.
|
||||
|
||||
.RS
|
||||
e.g.
|
||||
@@ -241,8 +244,9 @@ Draw border around the finder
|
||||
.br
|
||||
|
||||
If you use a terminal emulator where each box-drawing character takes
|
||||
2 columns, try setting \fBRUNEWIDTH_EASTASIAN\fR to \fB1\fR. If the border is
|
||||
still not properly rendered, set \fB--no-unicode\fR.
|
||||
2 columns, try setting \fBRUNEWIDTH_EASTASIAN\fR environment variable to
|
||||
\fB0\fR or \fB1\fR. If the border is still not properly rendered, set
|
||||
\fB--no-unicode\fR.
|
||||
|
||||
.TP
|
||||
.BI "--border-label" [=LABEL]
|
||||
@@ -373,9 +377,10 @@ Do not display horizontal separator on the info line. A synonym for
|
||||
\fB--separator=''\fB
|
||||
|
||||
.TP
|
||||
.BI "--scrollbar=" "CHAR"
|
||||
.BI "--scrollbar=" "CHAR1[CHAR2]"
|
||||
Use the given character to render scrollbar. (default: '│' or ':' depending on
|
||||
\fB--no-unicode\fR).
|
||||
\fB--no-unicode\fR). The optional \fBCHAR2\fR is used to render scrollbar of
|
||||
the preview window.
|
||||
|
||||
.TP
|
||||
.B "--no-scrollbar"
|
||||
@@ -429,28 +434,30 @@ color mappings.
|
||||
\fBbw \fRNo colors (equivalent to \fB--no-color\fR)
|
||||
|
||||
.B COLOR NAMES:
|
||||
\fBfg \fRText
|
||||
\fBpreview-fg \fRPreview window text
|
||||
\fBbg \fRBackground
|
||||
\fBpreview-bg \fRPreview window background
|
||||
\fBhl \fRHighlighted substrings
|
||||
\fBfg+ \fRText (current line)
|
||||
\fBbg+ \fRBackground (current line)
|
||||
\fBgutter \fRGutter on the left
|
||||
\fBhl+ \fRHighlighted substrings (current line)
|
||||
\fBquery \fRQuery string
|
||||
\fBdisabled \fRQuery string when search is disabled (\fB--disabled\fR)
|
||||
\fBinfo \fRInfo line (match counters)
|
||||
\fBborder \fRBorder around the window (\fB--border\fR and \fB--preview\fR)
|
||||
\fBseparator \fRHorizontal separator on info line
|
||||
\fBscrollbar \fRScrollbar
|
||||
\fBlabel \fRBorder label (\fB--border-label\fR and \fB--preview-label\fR)
|
||||
\fBpreview-label \fRBorder label of the preview window (\fB--preview-label\fR)
|
||||
\fBprompt \fRPrompt
|
||||
\fBpointer \fRPointer to the current line
|
||||
\fBmarker \fRMulti-select marker
|
||||
\fBspinner \fRStreaming input indicator
|
||||
\fBheader \fRHeader
|
||||
\fBfg \fRText
|
||||
\fBpreview-fg \fRPreview window text
|
||||
\fBbg \fRBackground
|
||||
\fBpreview-bg \fRPreview window background
|
||||
\fBhl \fRHighlighted substrings
|
||||
\fBfg+ \fRText (current line)
|
||||
\fBbg+ \fRBackground (current line)
|
||||
\fBgutter \fRGutter on the left
|
||||
\fBhl+ \fRHighlighted substrings (current line)
|
||||
\fBquery \fRQuery string
|
||||
\fBdisabled \fRQuery string when search is disabled (\fB--disabled\fR)
|
||||
\fBinfo \fRInfo line (match counters)
|
||||
\fBborder \fRBorder around the window (\fB--border\fR and \fB--preview\fR)
|
||||
\fBscrollbar \fRScrollbar
|
||||
\fBpreview-border \fRBorder around the preview window (\fB--preview\fR)
|
||||
\fBpreview-scrollbar \fRScrollbar
|
||||
\fBseparator \fRHorizontal separator on info line
|
||||
\fBlabel \fRBorder label (\fB--border-label\fR and \fB--preview-label\fR)
|
||||
\fBpreview-label \fRBorder label of the preview window (\fB--preview-label\fR)
|
||||
\fBprompt \fRPrompt
|
||||
\fBpointer \fRPointer to the current line
|
||||
\fBmarker \fRMulti-select marker
|
||||
\fBspinner \fRStreaming input indicator
|
||||
\fBheader \fRHeader
|
||||
|
||||
.B ANSI COLORS:
|
||||
\fB-1 \fRDefault terminal foreground/background color
|
||||
@@ -866,6 +873,8 @@ e.g.
|
||||
.br
|
||||
\fIctrl-space\fR
|
||||
.br
|
||||
\fIctrl-delete\fR
|
||||
.br
|
||||
\fIctrl-\\\fR
|
||||
.br
|
||||
\fIctrl-]\fR
|
||||
@@ -934,6 +943,8 @@ e.g.
|
||||
.br
|
||||
\fIshift-right\fR
|
||||
.br
|
||||
\fIshift-delete\fR
|
||||
.br
|
||||
\fIalt-shift-up\fR
|
||||
.br
|
||||
\fIalt-shift-down\fR
|
||||
@@ -1004,6 +1015,17 @@ e.g.
|
||||
\fB# Automatically select the only match
|
||||
seq 10 | fzf --bind one:accept\fR
|
||||
.RE
|
||||
\fIzero\fR
|
||||
.RS
|
||||
Triggered when there's no match. \fBzero:abort\fR binding is comparable to
|
||||
\fB--exit-0\fR option, but the difference is that \fB--exit-0\fR is only
|
||||
effective before the interactive finder starts but \fBzero\fR event is
|
||||
triggered by the interactive finder.
|
||||
|
||||
e.g.
|
||||
\fB# Reload the candidate list when there's no match
|
||||
echo $RANDOM | fzf --bind 'zero:reload(echo $RANDOM)+clear-query' --height 3\fR
|
||||
.RE
|
||||
|
||||
\fIbackward-eof\fR
|
||||
.RS
|
||||
@@ -1030,6 +1052,7 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBbeginning-of-line\fR \fIctrl-a home\fR
|
||||
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
|
||||
\fBchange-border-label(...)\fR (change \fB--border-label\fR to the given string)
|
||||
\fBchange-header(...)\fR (change header to the given string; doesn't affect \fB--header-lines\fR)
|
||||
\fBchange-preview(...)\fR (change \fB--preview\fR option)
|
||||
\fBchange-preview-label(...)\fR (change \fB--preview-label\fR to the given string)
|
||||
\fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|')
|
||||
@@ -1097,8 +1120,11 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBtoggle-preview-wrap\fR
|
||||
\fBtoggle-search\fR (toggle search functionality)
|
||||
\fBtoggle-sort\fR
|
||||
\fBtoggle-track\fR
|
||||
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
|
||||
\fBtrack\fR (track the current item; automatically disabled if focus changes)
|
||||
\fBtransform-border-label(...)\fR (transform border label using an external command)
|
||||
\fBtransform-header(...)\fR (transform header using an external command)
|
||||
\fBtransform-preview-label(...)\fR (transform preview label using an external command)
|
||||
\fBtransform-prompt(...)\fR (transform prompt string using an external command)
|
||||
\fBtransform-query(...)\fR (transform query string using an external command)
|
||||
|
||||
@@ -164,7 +164,7 @@ function s:get_version(bin)
|
||||
if has_key(s:versions, a:bin)
|
||||
return s:versions[a:bin]
|
||||
end
|
||||
let command = (&shell =~ 'powershell' ? '&' : '') . s:fzf_call('shellescape', a:bin) . ' --version --no-height'
|
||||
let command = (&shell =~ 'powershell\|pwsh' ? '&' : '') . s:fzf_call('shellescape', a:bin) . ' --version --no-height'
|
||||
let output = systemlist(command)
|
||||
if v:shell_error || empty(output)
|
||||
return ''
|
||||
|
||||
42
src/core.go
42
src/core.go
@@ -138,7 +138,9 @@ func Run(opts *Options, version string, revision string) {
|
||||
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
|
||||
opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
|
||||
}
|
||||
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)
|
||||
inputRevision := 0
|
||||
snapshotRevision := 0
|
||||
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox, inputRevision)
|
||||
|
||||
// Filtering mode
|
||||
if opts.Filter != nil {
|
||||
@@ -209,8 +211,6 @@ func Run(opts *Options, version string, revision string) {
|
||||
|
||||
// Event coordination
|
||||
reading := true
|
||||
clearCache := util.Once(false)
|
||||
clearSelection := util.Once(false)
|
||||
ticks := 0
|
||||
var nextCommand *string
|
||||
eventBox.Watch(EvtReadNew)
|
||||
@@ -231,26 +231,20 @@ func Run(opts *Options, version string, revision string) {
|
||||
|
||||
useSnapshot := false
|
||||
var snapshot []*Chunk
|
||||
var prevSnapshot []*Chunk
|
||||
var count int
|
||||
restart := func(command string) {
|
||||
reading = true
|
||||
clearCache = util.Once(true)
|
||||
clearSelection = util.Once(true)
|
||||
// We should not update snapshot if reload is triggered again while
|
||||
// the previous reload is in progress
|
||||
if useSnapshot && prevSnapshot != nil {
|
||||
snapshot, count = chunkList.Snapshot()
|
||||
}
|
||||
chunkList.Clear()
|
||||
itemIndex = 0
|
||||
inputRevision++
|
||||
header = make([]string, 0, opts.HeaderLines)
|
||||
go reader.restart(command)
|
||||
}
|
||||
for {
|
||||
delay := true
|
||||
ticks++
|
||||
input := func(reloaded bool) []rune {
|
||||
input := func() []rune {
|
||||
reloaded := snapshotRevision != inputRevision
|
||||
paused, input := terminal.Input()
|
||||
if reloaded && paused {
|
||||
query = []rune{}
|
||||
@@ -280,29 +274,30 @@ func Run(opts *Options, version string, revision string) {
|
||||
}
|
||||
if useSnapshot && evt == EvtReadFin {
|
||||
useSnapshot = false
|
||||
prevSnapshot = nil
|
||||
}
|
||||
if !useSnapshot {
|
||||
snapshot, count = chunkList.Snapshot()
|
||||
snapshotRevision = inputRevision
|
||||
}
|
||||
total = count
|
||||
terminal.UpdateCount(total, !reading, value.(*string))
|
||||
if opts.Sync {
|
||||
opts.Sync = false
|
||||
terminal.UpdateList(PassMerger(&snapshot, opts.Tac), false)
|
||||
terminal.UpdateList(PassMerger(&snapshot, opts.Tac, snapshotRevision))
|
||||
}
|
||||
if heightUnknown && !deferred {
|
||||
determine(!reading)
|
||||
}
|
||||
reset := !useSnapshot && clearCache()
|
||||
matcher.Reset(snapshot, input(reset), false, !reading, sort, reset)
|
||||
matcher.Reset(snapshot, input(), false, !reading, sort, snapshotRevision)
|
||||
|
||||
case EvtSearchNew:
|
||||
var command *string
|
||||
var changed bool
|
||||
switch val := value.(type) {
|
||||
case searchRequest:
|
||||
sort = val.sort
|
||||
command = val.command
|
||||
changed = val.changed
|
||||
if command != nil {
|
||||
useSnapshot = val.sync
|
||||
}
|
||||
@@ -314,13 +309,20 @@ func Run(opts *Options, version string, revision string) {
|
||||
} else {
|
||||
restart(*command)
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
break
|
||||
}
|
||||
if !useSnapshot {
|
||||
snapshot, _ = chunkList.Snapshot()
|
||||
newSnapshot, _ := chunkList.Snapshot()
|
||||
// We want to avoid showing empty list when reload is triggered
|
||||
// and the query string is changed at the same time i.e. command != nil && changed
|
||||
if command == nil || len(newSnapshot) > 0 {
|
||||
snapshot = newSnapshot
|
||||
snapshotRevision = inputRevision
|
||||
}
|
||||
}
|
||||
reset := !useSnapshot && clearCache()
|
||||
matcher.Reset(snapshot, input(reset), true, !reading, sort, reset)
|
||||
matcher.Reset(snapshot, input(), true, !reading, sort, snapshotRevision)
|
||||
delay = false
|
||||
|
||||
case EvtSearchProgress:
|
||||
@@ -360,7 +362,7 @@ func Run(opts *Options, version string, revision string) {
|
||||
determine(val.final)
|
||||
}
|
||||
}
|
||||
terminal.UpdateList(val, clearSelection())
|
||||
terminal.UpdateList(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,11 @@ import (
|
||||
|
||||
// MatchRequest represents a search request
|
||||
type MatchRequest struct {
|
||||
chunks []*Chunk
|
||||
pattern *Pattern
|
||||
final bool
|
||||
sort bool
|
||||
clearCache bool
|
||||
chunks []*Chunk
|
||||
pattern *Pattern
|
||||
final bool
|
||||
sort bool
|
||||
revision int
|
||||
}
|
||||
|
||||
// Matcher is responsible for performing search
|
||||
@@ -29,6 +29,7 @@ type Matcher struct {
|
||||
partitions int
|
||||
slab []*util.Slab
|
||||
mergerCache map[string]*Merger
|
||||
revision int
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -38,7 +39,7 @@ const (
|
||||
|
||||
// NewMatcher returns a new Matcher
|
||||
func NewMatcher(patternBuilder func([]rune) *Pattern,
|
||||
sort bool, tac bool, eventBox *util.EventBox) *Matcher {
|
||||
sort bool, tac bool, eventBox *util.EventBox, revision int) *Matcher {
|
||||
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
|
||||
return &Matcher{
|
||||
patternBuilder: patternBuilder,
|
||||
@@ -48,7 +49,8 @@ func NewMatcher(patternBuilder func([]rune) *Pattern,
|
||||
reqBox: util.NewEventBox(),
|
||||
partitions: partitions,
|
||||
slab: make([]*util.Slab, partitions),
|
||||
mergerCache: make(map[string]*Merger)}
|
||||
mergerCache: make(map[string]*Merger),
|
||||
revision: revision}
|
||||
}
|
||||
|
||||
// Loop puts Matcher in action
|
||||
@@ -70,8 +72,9 @@ func (m *Matcher) Loop() {
|
||||
events.Clear()
|
||||
})
|
||||
|
||||
if request.sort != m.sort || request.clearCache {
|
||||
if request.sort != m.sort || request.revision != m.revision {
|
||||
m.sort = request.sort
|
||||
m.revision = request.revision
|
||||
m.mergerCache = make(map[string]*Merger)
|
||||
clearChunkCache()
|
||||
}
|
||||
@@ -140,11 +143,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
|
||||
numChunks := len(request.chunks)
|
||||
if numChunks == 0 {
|
||||
return EmptyMerger, false
|
||||
return EmptyMerger(request.revision), false
|
||||
}
|
||||
pattern := request.pattern
|
||||
if pattern.IsEmpty() {
|
||||
return PassMerger(&request.chunks, m.tac), false
|
||||
return PassMerger(&request.chunks, m.tac, request.revision), false
|
||||
}
|
||||
|
||||
cancelled := util.NewAtomicBool(false)
|
||||
@@ -218,11 +221,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
partialResult := <-resultChan
|
||||
partialResults[partialResult.index] = partialResult.matches
|
||||
}
|
||||
return NewMerger(pattern, partialResults, m.sort, m.tac), false
|
||||
return NewMerger(pattern, partialResults, m.sort, m.tac, request.revision), false
|
||||
}
|
||||
|
||||
// Reset is called to interrupt/signal the ongoing search
|
||||
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, clearCache bool) {
|
||||
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, revision int) {
|
||||
pattern := m.patternBuilder(patternRunes)
|
||||
|
||||
var event util.EventType
|
||||
@@ -231,5 +234,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, clearCache})
|
||||
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, revision})
|
||||
}
|
||||
|
||||
@@ -3,32 +3,36 @@ package fzf
|
||||
import "fmt"
|
||||
|
||||
// EmptyMerger is a Merger with no data
|
||||
var EmptyMerger = NewMerger(nil, [][]Result{}, false, false)
|
||||
func EmptyMerger(revision int) *Merger {
|
||||
return NewMerger(nil, [][]Result{}, false, false, revision)
|
||||
}
|
||||
|
||||
// Merger holds a set of locally sorted lists of items and provides the view of
|
||||
// a single, globally-sorted list
|
||||
type Merger struct {
|
||||
pattern *Pattern
|
||||
lists [][]Result
|
||||
merged []Result
|
||||
chunks *[]*Chunk
|
||||
cursors []int
|
||||
sorted bool
|
||||
tac bool
|
||||
final bool
|
||||
count int
|
||||
pass bool
|
||||
pattern *Pattern
|
||||
lists [][]Result
|
||||
merged []Result
|
||||
chunks *[]*Chunk
|
||||
cursors []int
|
||||
sorted bool
|
||||
tac bool
|
||||
final bool
|
||||
count int
|
||||
pass bool
|
||||
revision int
|
||||
}
|
||||
|
||||
// PassMerger returns a new Merger that simply returns the items in the
|
||||
// original order
|
||||
func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
|
||||
func PassMerger(chunks *[]*Chunk, tac bool, revision int) *Merger {
|
||||
mg := Merger{
|
||||
pattern: nil,
|
||||
chunks: chunks,
|
||||
tac: tac,
|
||||
count: 0,
|
||||
pass: true}
|
||||
pattern: nil,
|
||||
chunks: chunks,
|
||||
tac: tac,
|
||||
count: 0,
|
||||
pass: true,
|
||||
revision: revision}
|
||||
|
||||
for _, chunk := range *mg.chunks {
|
||||
mg.count += chunk.count
|
||||
@@ -37,17 +41,18 @@ func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
|
||||
}
|
||||
|
||||
// NewMerger returns a new Merger
|
||||
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merger {
|
||||
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision int) *Merger {
|
||||
mg := Merger{
|
||||
pattern: pattern,
|
||||
lists: lists,
|
||||
merged: []Result{},
|
||||
chunks: nil,
|
||||
cursors: make([]int, len(lists)),
|
||||
sorted: sorted,
|
||||
tac: tac,
|
||||
final: false,
|
||||
count: 0}
|
||||
pattern: pattern,
|
||||
lists: lists,
|
||||
merged: []Result{},
|
||||
chunks: nil,
|
||||
cursors: make([]int, len(lists)),
|
||||
sorted: sorted,
|
||||
tac: tac,
|
||||
final: false,
|
||||
count: 0,
|
||||
revision: revision}
|
||||
|
||||
for _, list := range mg.lists {
|
||||
mg.count += len(list)
|
||||
@@ -55,22 +60,40 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merge
|
||||
return &mg
|
||||
}
|
||||
|
||||
// Revision returns revision number
|
||||
func (mg *Merger) Revision() int {
|
||||
return mg.revision
|
||||
}
|
||||
|
||||
// Length returns the number of items
|
||||
func (mg *Merger) Length() int {
|
||||
return mg.count
|
||||
}
|
||||
|
||||
func (mg *Merger) First() Result {
|
||||
if mg.tac && !mg.sorted {
|
||||
return mg.Get(mg.count - 1)
|
||||
}
|
||||
return mg.Get(0)
|
||||
}
|
||||
|
||||
// FindIndex returns the index of the item with the given item index
|
||||
func (mg *Merger) FindIndex(itemIndex int32) int {
|
||||
index := -1
|
||||
if mg.pass {
|
||||
return int(itemIndex)
|
||||
}
|
||||
for i := 0; i < mg.count; i++ {
|
||||
if mg.Get(i).item.Index() == itemIndex {
|
||||
return i
|
||||
index = int(itemIndex)
|
||||
if mg.tac {
|
||||
index = mg.count - index - 1
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < mg.count; i++ {
|
||||
if mg.Get(i).item.Index() == itemIndex {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
return index
|
||||
}
|
||||
|
||||
// Get returns the pointer to the Result object indexed by the given integer
|
||||
|
||||
@@ -57,7 +57,7 @@ func TestMergerUnsorted(t *testing.T) {
|
||||
cnt := len(items)
|
||||
|
||||
// Not sorted: same order
|
||||
mg := NewMerger(nil, lists, false, false)
|
||||
mg := NewMerger(nil, lists, false, false, 0)
|
||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||
for i := 0; i < cnt; i++ {
|
||||
assert(t, items[i] == mg.Get(i), "Invalid Get")
|
||||
@@ -69,7 +69,7 @@ func TestMergerSorted(t *testing.T) {
|
||||
cnt := len(items)
|
||||
|
||||
// Sorted sorted order
|
||||
mg := NewMerger(nil, lists, true, false)
|
||||
mg := NewMerger(nil, lists, true, false, 0)
|
||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||
sort.Sort(ByRelevance(items))
|
||||
for i := 0; i < cnt; i++ {
|
||||
@@ -79,7 +79,7 @@ func TestMergerSorted(t *testing.T) {
|
||||
}
|
||||
|
||||
// Inverse order
|
||||
mg2 := NewMerger(nil, lists, true, false)
|
||||
mg2 := NewMerger(nil, lists, true, false, 0)
|
||||
for i := cnt - 1; i >= 0; i-- {
|
||||
if items[i] != mg2.Get(i) {
|
||||
t.Error("Not sorted", items[i], mg2.Get(i))
|
||||
|
||||
@@ -63,7 +63,7 @@ const usage = `usage: fzf [options]
|
||||
(default: 10)
|
||||
--layout=LAYOUT Choose layout: [default|reverse|reverse-list]
|
||||
--border[=STYLE] Draw border around the finder
|
||||
[rounded|sharp|horizontal|vertical|
|
||||
[rounded|sharp|bold|block|double|horizontal|vertical|
|
||||
top|bottom|left|right|none] (default: rounded)
|
||||
--border-label=LABEL Label to print on the border
|
||||
--border-label-pos=COL Position of the border label
|
||||
@@ -75,7 +75,7 @@ const usage = `usage: fzf [options]
|
||||
--info=STYLE Finder info style [default|hidden|inline|inline:SEPARATOR]
|
||||
--separator=STR String to form horizontal separator on info line
|
||||
--no-separator Hide info line separator
|
||||
--scrollbar[=CHAR] Scrollbar character
|
||||
--scrollbar[=C1[C2]] Scrollbar character(s) (each for main and preview window)
|
||||
--no-scrollbar Hide scrollbar
|
||||
--prompt=STR Input prompt (default: '> ')
|
||||
--pointer=STR Pointer to the current line (default: '>')
|
||||
@@ -165,6 +165,14 @@ func defaultMargin() [4]sizeSpec {
|
||||
return [4]sizeSpec{}
|
||||
}
|
||||
|
||||
type trackOption int
|
||||
|
||||
const (
|
||||
trackDisabled trackOption = iota
|
||||
trackEnabled
|
||||
trackCurrent
|
||||
)
|
||||
|
||||
type windowPosition int
|
||||
|
||||
const (
|
||||
@@ -267,7 +275,7 @@ type Options struct {
|
||||
WithNth []Range
|
||||
Delimiter Delimiter
|
||||
Sort int
|
||||
Track bool
|
||||
Track trackOption
|
||||
Tac bool
|
||||
Criteria []criterion
|
||||
Multi int
|
||||
@@ -340,7 +348,7 @@ func defaultOptions() *Options {
|
||||
WithNth: make([]Range, 0),
|
||||
Delimiter: Delimiter{},
|
||||
Sort: 1000,
|
||||
Track: false,
|
||||
Track: trackDisabled,
|
||||
Tac: false,
|
||||
Criteria: []criterion{byScore, byLength},
|
||||
Multi: 0,
|
||||
@@ -536,6 +544,8 @@ func parseBorder(str string, optional bool) tui.BorderShape {
|
||||
return tui.BorderSharp
|
||||
case "bold":
|
||||
return tui.BorderBold
|
||||
case "block":
|
||||
return tui.BorderBlock
|
||||
case "double":
|
||||
return tui.BorderDouble
|
||||
case "horizontal":
|
||||
@@ -556,7 +566,7 @@ func parseBorder(str string, optional bool) tui.BorderShape {
|
||||
if optional && str == "" {
|
||||
return tui.DefaultBorderShape
|
||||
}
|
||||
errorExit("invalid border style (expected: rounded|sharp|bold|double|horizontal|vertical|top|bottom|left|right|none)")
|
||||
errorExit("invalid border style (expected: rounded|sharp|bold|block|double|horizontal|vertical|top|bottom|left|right|none)")
|
||||
}
|
||||
return tui.BorderNone
|
||||
}
|
||||
@@ -604,6 +614,8 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
|
||||
add(tui.BSpace)
|
||||
case "ctrl-space":
|
||||
add(tui.CtrlSpace)
|
||||
case "ctrl-delete":
|
||||
add(tui.CtrlDelete)
|
||||
case "ctrl-^", "ctrl-6":
|
||||
add(tui.CtrlCaret)
|
||||
case "ctrl-/", "ctrl-_":
|
||||
@@ -624,6 +636,8 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
|
||||
add(tui.Focus)
|
||||
case "one":
|
||||
add(tui.One)
|
||||
case "zero":
|
||||
add(tui.Zero)
|
||||
case "alt-enter", "alt-return":
|
||||
chords[tui.CtrlAltKey('m')] = key
|
||||
case "alt-space":
|
||||
@@ -672,6 +686,8 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
|
||||
add(tui.SLeft)
|
||||
case "shift-right":
|
||||
add(tui.SRight)
|
||||
case "shift-delete":
|
||||
add(tui.SDelete)
|
||||
case "left-click":
|
||||
add(tui.LeftClick)
|
||||
case "right-click":
|
||||
@@ -878,10 +894,14 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
|
||||
mergeAttr(&theme.CurrentMatch)
|
||||
case "border":
|
||||
mergeAttr(&theme.Border)
|
||||
case "preview-border":
|
||||
mergeAttr(&theme.PreviewBorder)
|
||||
case "separator":
|
||||
mergeAttr(&theme.Separator)
|
||||
case "scrollbar":
|
||||
mergeAttr(&theme.Scrollbar)
|
||||
case "preview-scrollbar":
|
||||
mergeAttr(&theme.PreviewScrollbar)
|
||||
case "label":
|
||||
mergeAttr(&theme.BorderLabel)
|
||||
case "preview-label":
|
||||
@@ -927,7 +947,7 @@ const (
|
||||
|
||||
func init() {
|
||||
executeRegexp = regexp.MustCompile(
|
||||
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`)
|
||||
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`)
|
||||
splitRegexp = regexp.MustCompile("[,:]+")
|
||||
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
|
||||
}
|
||||
@@ -1083,6 +1103,10 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
appendAction(actToggleAll)
|
||||
case "toggle-search":
|
||||
appendAction(actToggleSearch)
|
||||
case "toggle-track":
|
||||
appendAction(actToggleTrack)
|
||||
case "track":
|
||||
appendAction(actTrack)
|
||||
case "select":
|
||||
appendAction(actSelect)
|
||||
case "select-all":
|
||||
@@ -1247,6 +1271,8 @@ func isExecuteAction(str string) actionType {
|
||||
return actPreview
|
||||
case "change-border-label":
|
||||
return actChangeBorderLabel
|
||||
case "change-header":
|
||||
return actChangeHeader
|
||||
case "change-preview-label":
|
||||
return actChangePreviewLabel
|
||||
case "change-preview-window":
|
||||
@@ -1271,6 +1297,8 @@ func isExecuteAction(str string) actionType {
|
||||
return actTransformBorderLabel
|
||||
case "transform-preview-label":
|
||||
return actTransformPreviewLabel
|
||||
case "transform-header":
|
||||
return actTransformHeader
|
||||
case "transform-prompt":
|
||||
return actTransformPrompt
|
||||
case "transform-query":
|
||||
@@ -1408,6 +1436,8 @@ func parsePreviewWindowImpl(opts *previewOpts, input string, exit func(string))
|
||||
opts.border = tui.BorderSharp
|
||||
case "border-bold":
|
||||
opts.border = tui.BorderBold
|
||||
case "border-block":
|
||||
opts.border = tui.BorderBlock
|
||||
case "border-double":
|
||||
opts.border = tui.BorderDouble
|
||||
case "noborder", "border-none":
|
||||
@@ -1568,9 +1598,9 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
case "+s", "--no-sort":
|
||||
opts.Sort = 0
|
||||
case "--track":
|
||||
opts.Track = true
|
||||
opts.Track = trackEnabled
|
||||
case "--no-track":
|
||||
opts.Track = false
|
||||
opts.Track = trackDisabled
|
||||
case "--tac":
|
||||
opts.Tac = true
|
||||
case "--no-tac":
|
||||
@@ -1934,8 +1964,16 @@ func postProcessOptions(opts *Options) {
|
||||
errorExit("--height option is currently not supported on this platform")
|
||||
}
|
||||
|
||||
if opts.Scrollbar != nil && runewidth.StringWidth(*opts.Scrollbar) > 1 {
|
||||
errorExit("scrollbar display width should be 1")
|
||||
if opts.Scrollbar != nil {
|
||||
runes := []rune(*opts.Scrollbar)
|
||||
if len(runes) > 2 {
|
||||
errorExit("--scrollbar should be given one or two characters")
|
||||
}
|
||||
for _, r := range runes {
|
||||
if runewidth.RuneWidth(r) != 1 {
|
||||
errorExit("scrollbar display width should be 1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default actions for CTRL-N / CTRL-P when --history is set
|
||||
@@ -1951,25 +1989,25 @@ func postProcessOptions(opts *Options) {
|
||||
// Extend the default key map
|
||||
keymap := defaultKeymap()
|
||||
for key, actions := range opts.Keymap {
|
||||
var lastChangePreviewWindow *action
|
||||
reordered := []*action{}
|
||||
for _, act := range actions {
|
||||
switch act.t {
|
||||
case actToggleSort:
|
||||
// To display "+S"/"-S" on info line
|
||||
opts.ToggleSort = true
|
||||
case actChangePreviewWindow:
|
||||
lastChangePreviewWindow = act
|
||||
case actTogglePreview, actShowPreview, actHidePreview, actChangePreviewWindow:
|
||||
reordered = append(reordered, act)
|
||||
}
|
||||
}
|
||||
|
||||
// Re-organize actions so that we only keep the last change-preview-window
|
||||
// and it comes first in the list.
|
||||
// Re-organize actions so that we put actions that change the preview window first in the list.
|
||||
// * change-preview-window(up,+10)+preview(sleep 3; cat {})+change-preview-window(up,+20)
|
||||
// -> change-preview-window(up,+20)+preview(sleep 3; cat {})
|
||||
if lastChangePreviewWindow != nil {
|
||||
reordered := []*action{lastChangePreviewWindow}
|
||||
// -> change-preview-window(up,+10)+change-preview-window(up,+20)+preview(sleep 3; cat {})
|
||||
if len(reordered) > 0 {
|
||||
for _, act := range actions {
|
||||
if act.t != actChangePreviewWindow {
|
||||
switch act.t {
|
||||
case actTogglePreview, actShowPreview, actHidePreview, actChangePreviewWindow:
|
||||
default:
|
||||
reordered = append(reordered, act)
|
||||
}
|
||||
}
|
||||
|
||||
191
src/terminal.go
191
src/terminal.go
@@ -3,6 +3,7 @@ package fzf
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
@@ -183,7 +184,7 @@ type Terminal struct {
|
||||
multi int
|
||||
sort bool
|
||||
toggleSort bool
|
||||
track bool
|
||||
track trackOption
|
||||
delimiter Delimiter
|
||||
expect map[tui.Event]string
|
||||
keymap map[tui.Event][]*action
|
||||
@@ -198,6 +199,7 @@ type Terminal struct {
|
||||
header0 []string
|
||||
ellipsis string
|
||||
scrollbar string
|
||||
previewScrollbar string
|
||||
ansi bool
|
||||
tabstop int
|
||||
margin [4]sizeSpec
|
||||
@@ -226,6 +228,7 @@ type Terminal struct {
|
||||
merger *Merger
|
||||
selected map[int32]selectedItem
|
||||
version int64
|
||||
revision int
|
||||
reqBox *util.EventBox
|
||||
initialPreviewOpts previewOpts
|
||||
previewOpts previewOpts
|
||||
@@ -310,6 +313,7 @@ const (
|
||||
actBackwardWord
|
||||
actCancel
|
||||
actChangeBorderLabel
|
||||
actChangeHeader
|
||||
actChangePreviewLabel
|
||||
actChangePrompt
|
||||
actChangeQuery
|
||||
@@ -337,6 +341,8 @@ const (
|
||||
actToggleUp
|
||||
actToggleIn
|
||||
actToggleOut
|
||||
actToggleTrack
|
||||
actTrack
|
||||
actDown
|
||||
actUp
|
||||
actPageUp
|
||||
@@ -355,6 +361,7 @@ const (
|
||||
actTogglePreview
|
||||
actTogglePreviewWrap
|
||||
actTransformBorderLabel
|
||||
actTransformHeader
|
||||
actTransformPreviewLabel
|
||||
actTransformPrompt
|
||||
actTransformQuery
|
||||
@@ -403,6 +410,7 @@ type searchRequest struct {
|
||||
sort bool
|
||||
sync bool
|
||||
command *string
|
||||
changed bool
|
||||
}
|
||||
|
||||
type previewRequest struct {
|
||||
@@ -623,7 +631,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
cycle: opts.Cycle,
|
||||
headerFirst: opts.HeaderFirst,
|
||||
headerLines: opts.HeaderLines,
|
||||
header: header,
|
||||
header: []string{},
|
||||
header0: header,
|
||||
ellipsis: opts.Ellipsis,
|
||||
ansi: opts.Ansi,
|
||||
@@ -637,7 +645,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
jumpLabels: opts.JumpLabels,
|
||||
printer: opts.Printer,
|
||||
printsep: opts.PrintSep,
|
||||
merger: EmptyMerger,
|
||||
merger: EmptyMerger(0),
|
||||
selected: make(map[int32]selectedItem),
|
||||
reqBox: util.NewEventBox(),
|
||||
initialPreviewOpts: opts.Preview,
|
||||
@@ -684,8 +692,16 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
} else {
|
||||
t.scrollbar = "|"
|
||||
}
|
||||
t.previewScrollbar = t.scrollbar
|
||||
} else {
|
||||
t.scrollbar = *opts.Scrollbar
|
||||
runes := []rune(*opts.Scrollbar)
|
||||
if len(runes) > 0 {
|
||||
t.scrollbar = string(runes[0])
|
||||
t.previewScrollbar = t.scrollbar
|
||||
if len(runes) > 1 {
|
||||
t.previewScrollbar = string(runes[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
|
||||
@@ -711,7 +727,7 @@ func (t *Terminal) environ() []string {
|
||||
|
||||
func borderLines(shape tui.BorderShape) int {
|
||||
switch shape {
|
||||
case tui.BorderHorizontal, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderDouble:
|
||||
case tui.BorderHorizontal, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderDouble:
|
||||
return 2
|
||||
case tui.BorderTop, tui.BorderBottom:
|
||||
return 1
|
||||
@@ -882,10 +898,21 @@ func reverseStringArray(input []string) []string {
|
||||
return reversed
|
||||
}
|
||||
|
||||
func (t *Terminal) changeHeader(header string) bool {
|
||||
lines := strings.Split(strings.TrimSuffix(header, "\n"), "\n")
|
||||
switch t.layout {
|
||||
case layoutDefault, layoutReverseList:
|
||||
lines = reverseStringArray(lines)
|
||||
}
|
||||
needFullRedraw := len(t.header0) != len(lines)
|
||||
t.header0 = lines
|
||||
return needFullRedraw
|
||||
}
|
||||
|
||||
// UpdateHeader updates the header
|
||||
func (t *Terminal) UpdateHeader(header []string) {
|
||||
t.mutex.Lock()
|
||||
t.header = append(append([]string{}, t.header0...), header...)
|
||||
t.header = header
|
||||
t.mutex.Unlock()
|
||||
t.reqBox.Set(reqHeader, nil)
|
||||
}
|
||||
@@ -904,19 +931,25 @@ func (t *Terminal) UpdateProgress(progress float32) {
|
||||
}
|
||||
|
||||
// UpdateList updates Merger to display the list
|
||||
func (t *Terminal) UpdateList(merger *Merger, reset bool) {
|
||||
func (t *Terminal) UpdateList(merger *Merger) {
|
||||
t.mutex.Lock()
|
||||
var prevIndex int32 = -1
|
||||
if !reset && t.track && t.merger.Length() > 0 {
|
||||
prevIndex = t.merger.Get(t.cy).item.Index()
|
||||
reset := t.revision != merger.Revision()
|
||||
if !reset && t.track != trackDisabled {
|
||||
if t.merger.Length() > 0 {
|
||||
prevIndex = t.merger.Get(t.cy).item.Index()
|
||||
} else if merger.Length() > 0 {
|
||||
prevIndex = merger.First().item.Index()
|
||||
}
|
||||
}
|
||||
t.progress = 100
|
||||
t.merger = merger
|
||||
if reset {
|
||||
t.selected = make(map[int32]selectedItem)
|
||||
t.revision = merger.Revision()
|
||||
t.version++
|
||||
}
|
||||
if t.hasLoadActions && t.triggerLoad {
|
||||
if t.triggerLoad {
|
||||
t.triggerLoad = false
|
||||
t.eventChan <- tui.Load.AsEvent()
|
||||
}
|
||||
@@ -927,15 +960,27 @@ func (t *Terminal) UpdateList(merger *Merger, reset bool) {
|
||||
if i >= 0 {
|
||||
t.cy = i
|
||||
t.offset = t.cy - pos
|
||||
} else if t.track == trackCurrent {
|
||||
t.track = trackDisabled
|
||||
t.cy = pos
|
||||
t.offset = 0
|
||||
} else if t.cy > count {
|
||||
// Try to keep the vertical position when the list shrinks
|
||||
t.cy = count - util.Min(count, t.maxItems()) + pos
|
||||
}
|
||||
}
|
||||
if !t.reading && t.merger.Length() == 1 {
|
||||
one := tui.One.AsEvent()
|
||||
if _, prs := t.keymap[one]; prs {
|
||||
t.eventChan <- one
|
||||
if !t.reading {
|
||||
switch t.merger.Length() {
|
||||
case 0:
|
||||
zero := tui.Zero.AsEvent()
|
||||
if _, prs := t.keymap[zero]; prs {
|
||||
t.eventChan <- zero
|
||||
}
|
||||
case 1:
|
||||
one := tui.One.AsEvent()
|
||||
if _, prs := t.keymap[one]; prs {
|
||||
t.eventChan <- one
|
||||
}
|
||||
}
|
||||
}
|
||||
t.mutex.Unlock()
|
||||
@@ -1040,7 +1085,7 @@ func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) {
|
||||
if idx == 3 {
|
||||
extraMargin[idx] += 1 + bw
|
||||
}
|
||||
case tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderDouble:
|
||||
case tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderDouble:
|
||||
extraMargin[idx] += 1 + bw*(idx%2)
|
||||
}
|
||||
marginInt[idx] = sizeSpecToInt(idx, sizeSpec) + extraMargin[idx]
|
||||
@@ -1133,7 +1178,7 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
|
||||
t.border = t.tui.NewWindow(
|
||||
marginInt[0], marginInt[3], width+(1+bw), height,
|
||||
false, tui.MakeBorderStyle(tui.BorderRight, t.unicode))
|
||||
case tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderDouble:
|
||||
case tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderDouble:
|
||||
t.border = t.tui.NewWindow(
|
||||
marginInt[0]-1, marginInt[3]-(1+bw), width+(1+bw)*2, height+2,
|
||||
false, tui.MakeBorderStyle(t.borderShape, t.unicode))
|
||||
@@ -1167,7 +1212,7 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
|
||||
}
|
||||
t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder)
|
||||
switch previewOpts.border {
|
||||
case tui.BorderSharp, tui.BorderRounded, tui.BorderBold, tui.BorderDouble:
|
||||
case tui.BorderSharp, tui.BorderRounded, tui.BorderBold, tui.BorderBlock, tui.BorderDouble:
|
||||
pwidth -= (1 + bw) * 2
|
||||
pheight -= 2
|
||||
x += 1 + bw
|
||||
@@ -1193,6 +1238,8 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
|
||||
// Need a column to show scrollbar
|
||||
pwidth -= 1
|
||||
}
|
||||
pwidth = util.Max(0, pwidth)
|
||||
pheight = util.Max(0, pheight)
|
||||
t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, true, noBorder)
|
||||
}
|
||||
verticalPad := 2
|
||||
@@ -1309,7 +1356,7 @@ func (t *Terminal) printLabel(window tui.Window, render labelPrinter, opts label
|
||||
}
|
||||
|
||||
switch borderShape {
|
||||
case tui.BorderHorizontal, tui.BorderTop, tui.BorderBottom, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderDouble:
|
||||
case tui.BorderHorizontal, tui.BorderTop, tui.BorderBottom, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderDouble:
|
||||
if redrawBorder {
|
||||
window.DrawHBorder()
|
||||
}
|
||||
@@ -1340,7 +1387,7 @@ func (t *Terminal) move(y int, x int, clear bool) {
|
||||
case layoutDefault:
|
||||
y = h - y - 1
|
||||
case layoutReverseList:
|
||||
n := 2 + len(t.header)
|
||||
n := 2 + len(t.header0) + len(t.header)
|
||||
if t.noInfoLine() {
|
||||
n--
|
||||
}
|
||||
@@ -1460,6 +1507,9 @@ func (t *Terminal) printInfo() {
|
||||
output += " -S"
|
||||
}
|
||||
}
|
||||
if t.track != trackDisabled {
|
||||
output += " +T"
|
||||
}
|
||||
if t.multi > 0 {
|
||||
if t.multi == maxMulti {
|
||||
output += fmt.Sprintf(" (%d)", len(t.selected))
|
||||
@@ -1485,7 +1535,7 @@ func (t *Terminal) printInfo() {
|
||||
}
|
||||
|
||||
func (t *Terminal) printHeader() {
|
||||
if len(t.header) == 0 {
|
||||
if len(t.header0)+len(t.header) == 0 {
|
||||
return
|
||||
}
|
||||
max := t.window.Height()
|
||||
@@ -1496,7 +1546,7 @@ func (t *Terminal) printHeader() {
|
||||
}
|
||||
}
|
||||
var state *ansiState
|
||||
for idx, lineStr := range t.header {
|
||||
for idx, lineStr := range append(append([]string{}, t.header0...), t.header...) {
|
||||
line := idx
|
||||
if !t.headerFirst {
|
||||
line++
|
||||
@@ -1530,7 +1580,7 @@ func (t *Terminal) printList() {
|
||||
if t.layout == layoutDefault {
|
||||
i = maxy - 1 - j
|
||||
}
|
||||
line := i + 2 + len(t.header)
|
||||
line := i + 2 + len(t.header0) + len(t.header)
|
||||
if t.noInfoLine() {
|
||||
line--
|
||||
}
|
||||
@@ -1836,7 +1886,7 @@ func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unc
|
||||
if ansi != nil {
|
||||
ansi.lbg = -1
|
||||
}
|
||||
line = strings.TrimSuffix(line, "\n")
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
if lineNo >= height || t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
|
||||
t.previewed.filled = true
|
||||
t.previewer.scrollable = true
|
||||
@@ -1894,7 +1944,9 @@ func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unc
|
||||
func (t *Terminal) renderPreviewScrollbar(yoff int, barLength int, barStart int) {
|
||||
height := t.pwindow.Height()
|
||||
w := t.pborder.Width()
|
||||
redraw := false
|
||||
if len(t.previewer.bar) != height {
|
||||
redraw = true
|
||||
t.previewer.bar = make([]bool, height)
|
||||
}
|
||||
xshift := -1 - t.borderWidth
|
||||
@@ -1911,22 +1963,22 @@ func (t *Terminal) renderPreviewScrollbar(yoff int, barLength int, barStart int)
|
||||
|
||||
// Avoid unnecessary redraws
|
||||
bar := i >= yoff+barStart && i < yoff+barStart+barLength
|
||||
if bar == t.previewer.bar[i] && !t.tui.NeedScrollbarRedraw() {
|
||||
if !redraw && bar == t.previewer.bar[i] && !t.tui.NeedScrollbarRedraw() {
|
||||
continue
|
||||
}
|
||||
|
||||
t.previewer.bar[i] = bar
|
||||
t.pborder.Move(y, x)
|
||||
if i >= yoff+barStart && i < yoff+barStart+barLength {
|
||||
t.pborder.CPrint(tui.ColScrollbar, t.scrollbar)
|
||||
t.pborder.CPrint(tui.ColPreviewScrollbar, t.previewScrollbar)
|
||||
} else {
|
||||
t.pborder.Print(" ")
|
||||
t.pborder.CPrint(tui.ColPreviewScrollbar, " ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Terminal) printPreview() {
|
||||
if !t.hasPreviewWindow() {
|
||||
if !t.hasPreviewWindow() || t.pwindow.Height() == 0 {
|
||||
return
|
||||
}
|
||||
numLines := len(t.previewer.lines)
|
||||
@@ -2268,12 +2320,12 @@ func (t *Terminal) redraw() {
|
||||
t.printAll()
|
||||
}
|
||||
|
||||
func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, captureFirstLine bool) string {
|
||||
func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, capture bool, firstLineOnly bool) string {
|
||||
line := ""
|
||||
valid, list := t.buildPlusList(template, forcePlus)
|
||||
// captureFirstLine is used for transform-{prompt,query} and we don't want to
|
||||
// 'capture' is used for transform-* and we don't want to
|
||||
// return an empty string in those cases
|
||||
if !valid && !captureFirstLine {
|
||||
if !valid && !capture {
|
||||
return line
|
||||
}
|
||||
command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
|
||||
@@ -2290,12 +2342,17 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
|
||||
t.redraw()
|
||||
t.refresh()
|
||||
} else {
|
||||
if captureFirstLine {
|
||||
if capture {
|
||||
out, _ := cmd.StdoutPipe()
|
||||
reader := bufio.NewReader(out)
|
||||
cmd.Start()
|
||||
line, _ = reader.ReadString('\n')
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
if firstLineOnly {
|
||||
line, _ = reader.ReadString('\n')
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
} else {
|
||||
bytes, _ := io.ReadAll(reader)
|
||||
line = string(bytes)
|
||||
}
|
||||
cmd.Wait()
|
||||
} else {
|
||||
cmd.Run()
|
||||
@@ -2665,11 +2722,6 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
}
|
||||
|
||||
var onFocus []*action
|
||||
if actions, prs := t.keymap[tui.Focus.AsEvent()]; prs {
|
||||
onFocus = actions
|
||||
}
|
||||
|
||||
go func() {
|
||||
var focusedIndex int32 = minItem.Index()
|
||||
var version int64 = -1
|
||||
@@ -2706,7 +2758,11 @@ func (t *Terminal) Loop() {
|
||||
currentIndex = currentItem.Index()
|
||||
}
|
||||
focusChanged := focusedIndex != currentIndex
|
||||
if onFocus != nil && focusChanged {
|
||||
if focusChanged && t.track == trackCurrent {
|
||||
t.track = trackDisabled
|
||||
t.printInfo()
|
||||
}
|
||||
if onFocus, prs := t.keymap[tui.Focus.AsEvent()]; prs && focusChanged {
|
||||
t.serverChan <- onFocus
|
||||
}
|
||||
if focusChanged || version != t.version {
|
||||
@@ -2818,7 +2874,7 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
select {
|
||||
case event = <-t.eventChan:
|
||||
needBarrier = event != tui.Load.AsEvent()
|
||||
needBarrier = !event.Is(tui.Load, tui.One, tui.Zero)
|
||||
case actions = <-t.serverChan:
|
||||
event = tui.Invalid.AsEvent()
|
||||
needBarrier = false
|
||||
@@ -2908,14 +2964,22 @@ func (t *Terminal) Loop() {
|
||||
if t.history != nil {
|
||||
t.history.append(string(t.input))
|
||||
}
|
||||
/*
|
||||
FIXME: It is not at all clear why this is required.
|
||||
The following command will report 'not a tty', unless we open
|
||||
/dev/tty *twice* after closing the standard input for 'reload'
|
||||
in Reader.terminate().
|
||||
: | fzf --bind 'start:reload:ls' --bind 'enter:become:tty'
|
||||
*/
|
||||
tui.TtyIn()
|
||||
util.SetStdin(tui.TtyIn())
|
||||
syscall.Exec(shellPath, []string{shell, "-c", command}, os.Environ())
|
||||
}
|
||||
}
|
||||
case actExecute, actExecuteSilent:
|
||||
t.executeCommand(a.a, false, a.t == actExecuteSilent, false)
|
||||
t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false)
|
||||
case actExecuteMulti:
|
||||
t.executeCommand(a.a, true, false, false)
|
||||
t.executeCommand(a.a, true, false, false, false)
|
||||
case actInvalid:
|
||||
t.mutex.Unlock()
|
||||
return false
|
||||
@@ -2949,11 +3013,11 @@ func (t *Terminal) Loop() {
|
||||
req(reqPreviewRefresh)
|
||||
}
|
||||
case actTransformPrompt:
|
||||
prompt := t.executeCommand(a.a, false, true, true)
|
||||
prompt := t.executeCommand(a.a, false, true, true, true)
|
||||
t.prompt, t.promptLen = t.parsePrompt(prompt)
|
||||
req(reqPrompt)
|
||||
case actTransformQuery:
|
||||
query := t.executeCommand(a.a, false, true, true)
|
||||
query := t.executeCommand(a.a, false, true, true, true)
|
||||
t.input = []rune(query)
|
||||
t.cx = len(t.input)
|
||||
case actToggleSort:
|
||||
@@ -3002,6 +3066,19 @@ func (t *Terminal) Loop() {
|
||||
case actChangeQuery:
|
||||
t.input = []rune(a.a)
|
||||
t.cx = len(t.input)
|
||||
case actTransformHeader:
|
||||
header := t.executeCommand(a.a, false, true, true, false)
|
||||
if t.changeHeader(header) {
|
||||
req(reqFullRedraw)
|
||||
} else {
|
||||
req(reqHeader)
|
||||
}
|
||||
case actChangeHeader:
|
||||
if t.changeHeader(a.a) {
|
||||
req(reqFullRedraw)
|
||||
} else {
|
||||
req(reqHeader)
|
||||
}
|
||||
case actChangeBorderLabel:
|
||||
if t.border != nil {
|
||||
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(a.a, &tui.ColBorderLabel, false)
|
||||
@@ -3014,13 +3091,13 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
case actTransformBorderLabel:
|
||||
if t.border != nil {
|
||||
label := t.executeCommand(a.a, false, true, true)
|
||||
label := t.executeCommand(a.a, false, true, true, true)
|
||||
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false)
|
||||
req(reqRedrawBorderLabel)
|
||||
}
|
||||
case actTransformPreviewLabel:
|
||||
if t.pborder != nil {
|
||||
label := t.executeCommand(a.a, false, true, true)
|
||||
label := t.executeCommand(a.a, false, true, true, true)
|
||||
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false)
|
||||
req(reqRedrawPreviewLabel)
|
||||
}
|
||||
@@ -3270,6 +3347,19 @@ func (t *Terminal) Loop() {
|
||||
t.paused = !t.paused
|
||||
changed = !t.paused
|
||||
req(reqPrompt)
|
||||
case actToggleTrack:
|
||||
switch t.track {
|
||||
case trackEnabled:
|
||||
t.track = trackDisabled
|
||||
case trackDisabled:
|
||||
t.track = trackEnabled
|
||||
}
|
||||
req(reqInfo)
|
||||
case actTrack:
|
||||
if t.track == trackDisabled {
|
||||
t.track = trackCurrent
|
||||
}
|
||||
req(reqInfo)
|
||||
case actEnableSearch:
|
||||
t.paused = false
|
||||
changed = true
|
||||
@@ -3347,7 +3437,7 @@ func (t *Terminal) Loop() {
|
||||
// Translate coordinates
|
||||
mx -= t.window.Left()
|
||||
my -= t.window.Top()
|
||||
min := 2 + len(t.header)
|
||||
min := 2 + len(t.header0) + len(t.header)
|
||||
if t.noInfoLine() {
|
||||
min--
|
||||
}
|
||||
@@ -3452,6 +3542,9 @@ func (t *Terminal) Loop() {
|
||||
|
||||
// Split window options
|
||||
tokens := strings.Split(a.a, "|")
|
||||
if len(tokens[0]) > 0 && t.initialPreviewOpts.hidden {
|
||||
t.previewOpts.hidden = false
|
||||
}
|
||||
parsePreviewWindow(&t.previewOpts, tokens[0])
|
||||
if len(tokens) > 1 {
|
||||
a.a = strings.Join(append(tokens[1:], tokens[0]), "|")
|
||||
@@ -3552,7 +3645,7 @@ func (t *Terminal) Loop() {
|
||||
t.mutex.Unlock() // Must be unlocked before touching reqBox
|
||||
|
||||
if changed || newCommand != nil {
|
||||
t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, sync: reloadSync, command: newCommand})
|
||||
t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, sync: reloadSync, command: newCommand, changed: changed})
|
||||
}
|
||||
for _, event := range events {
|
||||
t.reqBox.Set(event, nil)
|
||||
@@ -3616,7 +3709,7 @@ func (t *Terminal) vset(o int) bool {
|
||||
}
|
||||
|
||||
func (t *Terminal) maxItems() int {
|
||||
max := t.window.Height() - 2 - len(t.header)
|
||||
max := t.window.Height() - 2 - len(t.header0) - len(t.header)
|
||||
if t.noInfoLine() {
|
||||
max++
|
||||
}
|
||||
|
||||
@@ -430,7 +430,19 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
}
|
||||
return Event{Invalid, 0, nil} // INS
|
||||
case '3':
|
||||
return Event{Del, 0, nil}
|
||||
if r.buffer[3] == '~' {
|
||||
return Event{Del, 0, nil}
|
||||
}
|
||||
if len(r.buffer) == 6 && r.buffer[5] == '~' {
|
||||
*sz = 6
|
||||
switch r.buffer[4] {
|
||||
case '5':
|
||||
return Event{CtrlDelete, 0, nil}
|
||||
case '2':
|
||||
return Event{SDelete, 0, nil}
|
||||
}
|
||||
}
|
||||
return Event{Invalid, 0, nil}
|
||||
case '4':
|
||||
return Event{End, 0, nil}
|
||||
case '5':
|
||||
@@ -745,7 +757,7 @@ func (w *LightWindow) DrawHBorder() {
|
||||
|
||||
func (w *LightWindow) drawBorder(onlyHorizontal bool) {
|
||||
switch w.border.shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderDouble:
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderDouble:
|
||||
w.drawBorderAround(onlyHorizontal)
|
||||
case BorderHorizontal:
|
||||
w.drawBorderHorizontal(true, true)
|
||||
@@ -776,14 +788,14 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
|
||||
if w.preview {
|
||||
color = ColPreviewBorder
|
||||
}
|
||||
hw := runewidth.RuneWidth(w.border.horizontal)
|
||||
hw := runewidth.RuneWidth(w.border.top)
|
||||
if top {
|
||||
w.Move(0, 0)
|
||||
w.CPrint(color, repeat(w.border.horizontal, w.width/hw))
|
||||
w.CPrint(color, repeat(w.border.top, w.width/hw))
|
||||
}
|
||||
if bottom {
|
||||
w.Move(w.height-1, 0)
|
||||
w.CPrint(color, repeat(w.border.horizontal, w.width/hw))
|
||||
w.CPrint(color, repeat(w.border.bottom, w.width/hw))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -799,11 +811,11 @@ func (w *LightWindow) drawBorderVertical(left, right bool) {
|
||||
for y := 0; y < w.height; y++ {
|
||||
w.Move(y, 0)
|
||||
if left {
|
||||
w.CPrint(color, string(w.border.vertical))
|
||||
w.CPrint(color, string(w.border.left))
|
||||
}
|
||||
w.CPrint(color, repeat(' ', width))
|
||||
if right {
|
||||
w.CPrint(color, string(w.border.vertical))
|
||||
w.CPrint(color, string(w.border.right))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -814,23 +826,23 @@ func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
|
||||
if w.preview {
|
||||
color = ColPreviewBorder
|
||||
}
|
||||
hw := runewidth.RuneWidth(w.border.horizontal)
|
||||
hw := runewidth.RuneWidth(w.border.top)
|
||||
tcw := runewidth.RuneWidth(w.border.topLeft) + runewidth.RuneWidth(w.border.topRight)
|
||||
bcw := runewidth.RuneWidth(w.border.bottomLeft) + runewidth.RuneWidth(w.border.bottomRight)
|
||||
rem := (w.width - tcw) % hw
|
||||
w.CPrint(color, string(w.border.topLeft)+repeat(w.border.horizontal, (w.width-tcw)/hw)+repeat(' ', rem)+string(w.border.topRight))
|
||||
w.CPrint(color, string(w.border.topLeft)+repeat(w.border.top, (w.width-tcw)/hw)+repeat(' ', rem)+string(w.border.topRight))
|
||||
if !onlyHorizontal {
|
||||
vw := runewidth.RuneWidth(w.border.vertical)
|
||||
vw := runewidth.RuneWidth(w.border.left)
|
||||
for y := 1; y < w.height-1; y++ {
|
||||
w.Move(y, 0)
|
||||
w.CPrint(color, string(w.border.vertical))
|
||||
w.CPrint(color, string(w.border.left))
|
||||
w.CPrint(color, repeat(' ', w.width-vw*2))
|
||||
w.CPrint(color, string(w.border.vertical))
|
||||
w.CPrint(color, string(w.border.right))
|
||||
}
|
||||
}
|
||||
w.Move(w.height-1, 0)
|
||||
rem = (w.width - bcw) % hw
|
||||
w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.horizontal, (w.width-bcw)/hw)+repeat(' ', rem)+string(w.border.bottomRight))
|
||||
w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.bottom, (w.width-bcw)/hw)+repeat(' ', rem)+string(w.border.bottomRight))
|
||||
}
|
||||
|
||||
func (w *LightWindow) csi(code string) string {
|
||||
|
||||
@@ -413,6 +413,12 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
case tcell.KeyHome:
|
||||
return Event{Home, 0, nil}
|
||||
case tcell.KeyDelete:
|
||||
if ctrl {
|
||||
return Event{CtrlDelete, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{SDelete, 0, nil}
|
||||
}
|
||||
return Event{Del, 0, nil}
|
||||
case tcell.KeyEnd:
|
||||
return Event{End, 0, nil}
|
||||
@@ -707,9 +713,9 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
|
||||
style = w.normal.style()
|
||||
}
|
||||
|
||||
hw := runewidth.RuneWidth(w.borderStyle.horizontal)
|
||||
hw := runewidth.RuneWidth(w.borderStyle.top)
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderHorizontal, BorderTop:
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderDouble, BorderHorizontal, BorderTop:
|
||||
max := right - 2*hw
|
||||
if shape == BorderHorizontal || shape == BorderTop {
|
||||
max = right - hw
|
||||
@@ -720,36 +726,36 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
|
||||
// ==================
|
||||
// ( HH ) => TR is ignored
|
||||
for x := left; x <= max; x += hw {
|
||||
_screen.SetContent(x, top, w.borderStyle.horizontal, nil, style)
|
||||
_screen.SetContent(x, top, w.borderStyle.top, nil, style)
|
||||
}
|
||||
}
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderHorizontal, BorderBottom:
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderDouble, BorderHorizontal, BorderBottom:
|
||||
max := right - 2*hw
|
||||
if shape == BorderHorizontal || shape == BorderBottom {
|
||||
max = right - hw
|
||||
}
|
||||
for x := left; x <= max; x += hw {
|
||||
_screen.SetContent(x, bot-1, w.borderStyle.horizontal, nil, style)
|
||||
_screen.SetContent(x, bot-1, w.borderStyle.bottom, nil, style)
|
||||
}
|
||||
}
|
||||
if !onlyHorizontal {
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderLeft:
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderDouble, BorderVertical, BorderLeft:
|
||||
for y := top; y < bot; y++ {
|
||||
_screen.SetContent(left, y, w.borderStyle.vertical, nil, style)
|
||||
_screen.SetContent(left, y, w.borderStyle.left, nil, style)
|
||||
}
|
||||
}
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderRight:
|
||||
vw := runewidth.RuneWidth(w.borderStyle.vertical)
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderDouble, BorderVertical, BorderRight:
|
||||
vw := runewidth.RuneWidth(w.borderStyle.right)
|
||||
for y := top; y < bot; y++ {
|
||||
_screen.SetContent(right-vw, y, w.borderStyle.vertical, nil, style)
|
||||
_screen.SetContent(right-vw, y, w.borderStyle.right, nil, style)
|
||||
}
|
||||
}
|
||||
}
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderDouble:
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderDouble:
|
||||
_screen.SetContent(left, top, w.borderStyle.topLeft, nil, style)
|
||||
_screen.SetContent(right-runewidth.RuneWidth(w.borderStyle.topRight), top, w.borderStyle.topRight, nil, style)
|
||||
_screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style)
|
||||
|
||||
364
src/tui/tui.go
364
src/tui/tui.go
@@ -41,6 +41,7 @@ const (
|
||||
CtrlZ
|
||||
ESC
|
||||
CtrlSpace
|
||||
CtrlDelete
|
||||
|
||||
// https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal
|
||||
CtrlBackSlash
|
||||
@@ -74,6 +75,7 @@ const (
|
||||
SDown
|
||||
SLeft
|
||||
SRight
|
||||
SDelete
|
||||
|
||||
F1
|
||||
F2
|
||||
@@ -94,6 +96,7 @@ const (
|
||||
Load
|
||||
Focus
|
||||
One
|
||||
Zero
|
||||
|
||||
AltBS
|
||||
|
||||
@@ -252,29 +255,31 @@ func (p ColorPair) MergeNonDefault(other ColorPair) ColorPair {
|
||||
}
|
||||
|
||||
type ColorTheme struct {
|
||||
Colored bool
|
||||
Input ColorAttr
|
||||
Disabled ColorAttr
|
||||
Fg ColorAttr
|
||||
Bg ColorAttr
|
||||
PreviewFg ColorAttr
|
||||
PreviewBg ColorAttr
|
||||
DarkBg ColorAttr
|
||||
Gutter ColorAttr
|
||||
Prompt ColorAttr
|
||||
Match ColorAttr
|
||||
Current ColorAttr
|
||||
CurrentMatch ColorAttr
|
||||
Spinner ColorAttr
|
||||
Info ColorAttr
|
||||
Cursor ColorAttr
|
||||
Selected ColorAttr
|
||||
Header ColorAttr
|
||||
Separator ColorAttr
|
||||
Scrollbar ColorAttr
|
||||
Border ColorAttr
|
||||
BorderLabel ColorAttr
|
||||
PreviewLabel ColorAttr
|
||||
Colored bool
|
||||
Input ColorAttr
|
||||
Disabled ColorAttr
|
||||
Fg ColorAttr
|
||||
Bg ColorAttr
|
||||
PreviewFg ColorAttr
|
||||
PreviewBg ColorAttr
|
||||
DarkBg ColorAttr
|
||||
Gutter ColorAttr
|
||||
Prompt ColorAttr
|
||||
Match ColorAttr
|
||||
Current ColorAttr
|
||||
CurrentMatch ColorAttr
|
||||
Spinner ColorAttr
|
||||
Info ColorAttr
|
||||
Cursor ColorAttr
|
||||
Selected ColorAttr
|
||||
Header ColorAttr
|
||||
Separator ColorAttr
|
||||
Scrollbar ColorAttr
|
||||
Border ColorAttr
|
||||
PreviewBorder ColorAttr
|
||||
PreviewScrollbar ColorAttr
|
||||
BorderLabel ColorAttr
|
||||
PreviewLabel ColorAttr
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
@@ -283,6 +288,15 @@ type Event struct {
|
||||
MouseEvent *MouseEvent
|
||||
}
|
||||
|
||||
func (e Event) Is(types ...EventType) bool {
|
||||
for _, t := range types {
|
||||
if e.Type == t {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type MouseEvent struct {
|
||||
Y int
|
||||
X int
|
||||
@@ -300,6 +314,7 @@ const (
|
||||
BorderRounded
|
||||
BorderSharp
|
||||
BorderBold
|
||||
BorderBlock
|
||||
BorderDouble
|
||||
BorderHorizontal
|
||||
BorderVertical
|
||||
@@ -327,8 +342,10 @@ func (s BorderShape) HasTop() bool {
|
||||
|
||||
type BorderStyle struct {
|
||||
shape BorderShape
|
||||
horizontal rune
|
||||
vertical rune
|
||||
top rune
|
||||
bottom rune
|
||||
left rune
|
||||
right rune
|
||||
topLeft rune
|
||||
topRight rune
|
||||
bottomLeft rune
|
||||
@@ -341,8 +358,10 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
if !unicode {
|
||||
return BorderStyle{
|
||||
shape: shape,
|
||||
horizontal: '-',
|
||||
vertical: '|',
|
||||
top: '-',
|
||||
bottom: '-',
|
||||
left: '|',
|
||||
right: '|',
|
||||
topLeft: '+',
|
||||
topRight: '+',
|
||||
bottomLeft: '+',
|
||||
@@ -353,8 +372,10 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
case BorderSharp:
|
||||
return BorderStyle{
|
||||
shape: shape,
|
||||
horizontal: '─',
|
||||
vertical: '│',
|
||||
top: '─',
|
||||
bottom: '─',
|
||||
left: '│',
|
||||
right: '│',
|
||||
topLeft: '┌',
|
||||
topRight: '┐',
|
||||
bottomLeft: '└',
|
||||
@@ -363,18 +384,37 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
case BorderBold:
|
||||
return BorderStyle{
|
||||
shape: shape,
|
||||
horizontal: '━',
|
||||
vertical: '┃',
|
||||
top: '━',
|
||||
bottom: '━',
|
||||
left: '┃',
|
||||
right: '┃',
|
||||
topLeft: '┏',
|
||||
topRight: '┓',
|
||||
bottomLeft: '┗',
|
||||
bottomRight: '┛',
|
||||
}
|
||||
case BorderBlock:
|
||||
// ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜
|
||||
// ▌ ▐
|
||||
// ▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟
|
||||
return BorderStyle{
|
||||
shape: shape,
|
||||
top: '▀',
|
||||
bottom: '▄',
|
||||
left: '▌',
|
||||
right: '▐',
|
||||
topLeft: '▛',
|
||||
topRight: '▜',
|
||||
bottomLeft: '▙',
|
||||
bottomRight: '▟',
|
||||
}
|
||||
case BorderDouble:
|
||||
return BorderStyle{
|
||||
shape: shape,
|
||||
horizontal: '═',
|
||||
vertical: '║',
|
||||
top: '═',
|
||||
bottom: '═',
|
||||
left: '║',
|
||||
right: '║',
|
||||
topLeft: '╔',
|
||||
topRight: '╗',
|
||||
bottomLeft: '╚',
|
||||
@@ -383,8 +423,10 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
}
|
||||
return BorderStyle{
|
||||
shape: shape,
|
||||
horizontal: '─',
|
||||
vertical: '│',
|
||||
top: '─',
|
||||
bottom: '─',
|
||||
left: '│',
|
||||
right: '│',
|
||||
topLeft: '╭',
|
||||
topRight: '╮',
|
||||
bottomLeft: '╰',
|
||||
@@ -395,8 +437,10 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
func MakeTransparentBorder() BorderStyle {
|
||||
return BorderStyle{
|
||||
shape: BorderRounded,
|
||||
horizontal: ' ',
|
||||
vertical: ' ',
|
||||
top: ' ',
|
||||
bottom: ' ',
|
||||
left: ' ',
|
||||
right: ' ',
|
||||
topLeft: ' ',
|
||||
topRight: ' ',
|
||||
bottomLeft: ' ',
|
||||
@@ -493,61 +537,66 @@ var (
|
||||
ColPreviewBorder ColorPair
|
||||
ColBorderLabel ColorPair
|
||||
ColPreviewLabel ColorPair
|
||||
ColPreviewScrollbar ColorPair
|
||||
)
|
||||
|
||||
func EmptyTheme() *ColorTheme {
|
||||
return &ColorTheme{
|
||||
Colored: true,
|
||||
Input: ColorAttr{colUndefined, AttrUndefined},
|
||||
Fg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Bg: ColorAttr{colUndefined, AttrUndefined},
|
||||
DarkBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Prompt: ColorAttr{colUndefined, AttrUndefined},
|
||||
Match: ColorAttr{colUndefined, AttrUndefined},
|
||||
Current: ColorAttr{colUndefined, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||
Spinner: ColorAttr{colUndefined, AttrUndefined},
|
||||
Info: ColorAttr{colUndefined, AttrUndefined},
|
||||
Cursor: ColorAttr{colUndefined, AttrUndefined},
|
||||
Selected: ColorAttr{colUndefined, AttrUndefined},
|
||||
Header: ColorAttr{colUndefined, AttrUndefined},
|
||||
Border: ColorAttr{colUndefined, AttrUndefined},
|
||||
BorderLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
Colored: true,
|
||||
Input: ColorAttr{colUndefined, AttrUndefined},
|
||||
Fg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Bg: ColorAttr{colUndefined, AttrUndefined},
|
||||
DarkBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Prompt: ColorAttr{colUndefined, AttrUndefined},
|
||||
Match: ColorAttr{colUndefined, AttrUndefined},
|
||||
Current: ColorAttr{colUndefined, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||
Spinner: ColorAttr{colUndefined, AttrUndefined},
|
||||
Info: ColorAttr{colUndefined, AttrUndefined},
|
||||
Cursor: ColorAttr{colUndefined, AttrUndefined},
|
||||
Selected: ColorAttr{colUndefined, AttrUndefined},
|
||||
Header: ColorAttr{colUndefined, AttrUndefined},
|
||||
Border: ColorAttr{colUndefined, AttrUndefined},
|
||||
BorderLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
}
|
||||
}
|
||||
|
||||
func NoColorTheme() *ColorTheme {
|
||||
return &ColorTheme{
|
||||
Colored: false,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
DarkBg: ColorAttr{colDefault, AttrUndefined},
|
||||
Prompt: ColorAttr{colDefault, AttrUndefined},
|
||||
Match: ColorAttr{colDefault, Underline},
|
||||
Current: ColorAttr{colDefault, Reverse},
|
||||
CurrentMatch: ColorAttr{colDefault, Reverse | Underline},
|
||||
Spinner: ColorAttr{colDefault, AttrUndefined},
|
||||
Info: ColorAttr{colDefault, AttrUndefined},
|
||||
Cursor: ColorAttr{colDefault, AttrUndefined},
|
||||
Selected: ColorAttr{colDefault, AttrUndefined},
|
||||
Header: ColorAttr{colDefault, AttrUndefined},
|
||||
Border: ColorAttr{colDefault, AttrUndefined},
|
||||
BorderLabel: ColorAttr{colDefault, AttrUndefined},
|
||||
Disabled: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colDefault, AttrUndefined},
|
||||
Gutter: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colDefault, AttrUndefined},
|
||||
Separator: ColorAttr{colDefault, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colDefault, AttrUndefined},
|
||||
Colored: false,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
DarkBg: ColorAttr{colDefault, AttrUndefined},
|
||||
Prompt: ColorAttr{colDefault, AttrUndefined},
|
||||
Match: ColorAttr{colDefault, Underline},
|
||||
Current: ColorAttr{colDefault, Reverse},
|
||||
CurrentMatch: ColorAttr{colDefault, Reverse | Underline},
|
||||
Spinner: ColorAttr{colDefault, AttrUndefined},
|
||||
Info: ColorAttr{colDefault, AttrUndefined},
|
||||
Cursor: ColorAttr{colDefault, AttrUndefined},
|
||||
Selected: ColorAttr{colDefault, AttrUndefined},
|
||||
Header: ColorAttr{colDefault, AttrUndefined},
|
||||
Border: ColorAttr{colDefault, AttrUndefined},
|
||||
BorderLabel: ColorAttr{colDefault, AttrUndefined},
|
||||
Disabled: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colDefault, AttrUndefined},
|
||||
Gutter: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewBorder: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewScrollbar: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colDefault, AttrUndefined},
|
||||
Separator: ColorAttr{colDefault, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colDefault, AttrUndefined},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,79 +607,85 @@ func errorExit(message string) {
|
||||
|
||||
func init() {
|
||||
Default16 = &ColorTheme{
|
||||
Colored: true,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
DarkBg: ColorAttr{colBlack, AttrUndefined},
|
||||
Prompt: ColorAttr{colBlue, AttrUndefined},
|
||||
Match: ColorAttr{colGreen, AttrUndefined},
|
||||
Current: ColorAttr{colYellow, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{colGreen, AttrUndefined},
|
||||
Spinner: ColorAttr{colGreen, AttrUndefined},
|
||||
Info: ColorAttr{colWhite, AttrUndefined},
|
||||
Cursor: ColorAttr{colRed, AttrUndefined},
|
||||
Selected: ColorAttr{colMagenta, AttrUndefined},
|
||||
Header: ColorAttr{colCyan, AttrUndefined},
|
||||
Border: ColorAttr{colBlack, AttrUndefined},
|
||||
BorderLabel: ColorAttr{colWhite, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
Colored: true,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
DarkBg: ColorAttr{colBlack, AttrUndefined},
|
||||
Prompt: ColorAttr{colBlue, AttrUndefined},
|
||||
Match: ColorAttr{colGreen, AttrUndefined},
|
||||
Current: ColorAttr{colYellow, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{colGreen, AttrUndefined},
|
||||
Spinner: ColorAttr{colGreen, AttrUndefined},
|
||||
Info: ColorAttr{colWhite, AttrUndefined},
|
||||
Cursor: ColorAttr{colRed, AttrUndefined},
|
||||
Selected: ColorAttr{colMagenta, AttrUndefined},
|
||||
Header: ColorAttr{colCyan, AttrUndefined},
|
||||
Border: ColorAttr{colBlack, AttrUndefined},
|
||||
BorderLabel: ColorAttr{colWhite, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
}
|
||||
Dark256 = &ColorTheme{
|
||||
Colored: true,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
DarkBg: ColorAttr{236, AttrUndefined},
|
||||
Prompt: ColorAttr{110, AttrUndefined},
|
||||
Match: ColorAttr{108, AttrUndefined},
|
||||
Current: ColorAttr{254, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{151, AttrUndefined},
|
||||
Spinner: ColorAttr{148, AttrUndefined},
|
||||
Info: ColorAttr{144, AttrUndefined},
|
||||
Cursor: ColorAttr{161, AttrUndefined},
|
||||
Selected: ColorAttr{168, AttrUndefined},
|
||||
Header: ColorAttr{109, AttrUndefined},
|
||||
Border: ColorAttr{59, AttrUndefined},
|
||||
BorderLabel: ColorAttr{145, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
Colored: true,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
DarkBg: ColorAttr{236, AttrUndefined},
|
||||
Prompt: ColorAttr{110, AttrUndefined},
|
||||
Match: ColorAttr{108, AttrUndefined},
|
||||
Current: ColorAttr{254, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{151, AttrUndefined},
|
||||
Spinner: ColorAttr{148, AttrUndefined},
|
||||
Info: ColorAttr{144, AttrUndefined},
|
||||
Cursor: ColorAttr{161, AttrUndefined},
|
||||
Selected: ColorAttr{168, AttrUndefined},
|
||||
Header: ColorAttr{109, AttrUndefined},
|
||||
Border: ColorAttr{59, AttrUndefined},
|
||||
BorderLabel: ColorAttr{145, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
}
|
||||
Light256 = &ColorTheme{
|
||||
Colored: true,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
DarkBg: ColorAttr{251, AttrUndefined},
|
||||
Prompt: ColorAttr{25, AttrUndefined},
|
||||
Match: ColorAttr{66, AttrUndefined},
|
||||
Current: ColorAttr{237, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{23, AttrUndefined},
|
||||
Spinner: ColorAttr{65, AttrUndefined},
|
||||
Info: ColorAttr{101, AttrUndefined},
|
||||
Cursor: ColorAttr{161, AttrUndefined},
|
||||
Selected: ColorAttr{168, AttrUndefined},
|
||||
Header: ColorAttr{31, AttrUndefined},
|
||||
Border: ColorAttr{145, AttrUndefined},
|
||||
BorderLabel: ColorAttr{59, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
Colored: true,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
DarkBg: ColorAttr{251, AttrUndefined},
|
||||
Prompt: ColorAttr{25, AttrUndefined},
|
||||
Match: ColorAttr{66, AttrUndefined},
|
||||
Current: ColorAttr{237, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{23, AttrUndefined},
|
||||
Spinner: ColorAttr{65, AttrUndefined},
|
||||
Info: ColorAttr{101, AttrUndefined},
|
||||
Cursor: ColorAttr{161, AttrUndefined},
|
||||
Selected: ColorAttr{168, AttrUndefined},
|
||||
Header: ColorAttr{31, AttrUndefined},
|
||||
Border: ColorAttr{145, AttrUndefined},
|
||||
BorderLabel: ColorAttr{59, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,8 +726,10 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
|
||||
theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
|
||||
theme.PreviewBg = o(theme.Bg, theme.PreviewBg)
|
||||
theme.PreviewLabel = o(theme.BorderLabel, theme.PreviewLabel)
|
||||
theme.PreviewBorder = o(theme.Border, theme.PreviewBorder)
|
||||
theme.Separator = o(theme.Border, theme.Separator)
|
||||
theme.Scrollbar = o(theme.Border, theme.Scrollbar)
|
||||
theme.PreviewScrollbar = o(theme.PreviewBorder, theme.PreviewScrollbar)
|
||||
|
||||
initPalette(theme)
|
||||
}
|
||||
@@ -710,5 +767,6 @@ func initPalette(theme *ColorTheme) {
|
||||
ColBorderLabel = pair(theme.BorderLabel, theme.Bg)
|
||||
ColPreviewLabel = pair(theme.PreviewLabel, theme.PreviewBg)
|
||||
ColPreview = pair(theme.PreviewFg, theme.PreviewBg)
|
||||
ColPreviewBorder = pair(theme.Border, theme.PreviewBg)
|
||||
ColPreviewBorder = pair(theme.PreviewBorder, theme.PreviewBg)
|
||||
ColPreviewScrollbar = pair(theme.PreviewScrollbar, theme.PreviewBg)
|
||||
}
|
||||
|
||||
228
test/test_go.rb
228
test/test_go.rb
@@ -1865,6 +1865,67 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| assert_equal '>', lines.last }
|
||||
end
|
||||
|
||||
def test_change_and_transform_header
|
||||
[
|
||||
'space:change-header:$(seq 4)',
|
||||
'space:transform-header:seq 4'
|
||||
].each_with_index do |binding, i|
|
||||
tmux.send_keys %(seq 3 | #{FZF} --header-lines 2 --header bar --bind "#{binding}"), :Enter
|
||||
expected = <<~OUTPUT
|
||||
> 3
|
||||
2
|
||||
1
|
||||
bar
|
||||
1/1
|
||||
>
|
||||
OUTPUT
|
||||
tmux.until { assert_block(expected, _1) }
|
||||
tmux.send_keys :Space
|
||||
expected = <<~OUTPUT
|
||||
> 3
|
||||
2
|
||||
1
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
1/1
|
||||
>
|
||||
OUTPUT
|
||||
tmux.until { assert_block(expected, _1) }
|
||||
next unless i.zero?
|
||||
|
||||
teardown
|
||||
setup
|
||||
end
|
||||
end
|
||||
|
||||
def test_change_header
|
||||
tmux.send_keys %(seq 3 | #{FZF} --header-lines 2 --header bar --bind "space:change-header:$(seq 4)"), :Enter
|
||||
expected = <<~OUTPUT
|
||||
> 3
|
||||
2
|
||||
1
|
||||
bar
|
||||
1/1
|
||||
>
|
||||
OUTPUT
|
||||
tmux.until { assert_block(expected, _1) }
|
||||
tmux.send_keys :Space
|
||||
expected = <<~OUTPUT
|
||||
> 3
|
||||
2
|
||||
1
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
1/1
|
||||
>
|
||||
OUTPUT
|
||||
tmux.until { assert_block(expected, _1) }
|
||||
end
|
||||
|
||||
def test_change_query
|
||||
tmux.send_keys %(: | #{FZF} --query foo --bind space:change-query:foobar), :Enter
|
||||
tmux.until { |lines| assert_equal 0, lines.item_count }
|
||||
@@ -2432,6 +2493,39 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
end
|
||||
|
||||
def test_change_preview_window_rotate_hidden
|
||||
tmux.send_keys "seq 100 | #{FZF} --preview-window hidden --preview 'echo =={}==' --bind '" \
|
||||
"a:change-preview-window(nohidden||down,1|)'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
tmux.until { |lines| refute_includes lines[1], '==1==' }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| assert_includes lines[1], '==1==' }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| refute_includes lines[1], '==1==' }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| assert_includes lines[-2], '==1==' }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| refute_includes lines[-2], '==1==' }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| assert_includes lines[1], '==1==' }
|
||||
end
|
||||
|
||||
def test_change_preview_window_rotate_hidden_down
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind '?:change-preview-window:up||down|' --preview 'echo =={}==' --preview-window hidden,down,1", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
tmux.until { |lines| refute_includes lines[1], '==1==' }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| assert_includes lines[1], '==1==' }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| refute_includes lines[1], '==1==' }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| assert_includes lines[-2], '==1==' }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| refute_includes lines[-2], '==1==' }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| assert_includes lines[1], '==1==' }
|
||||
end
|
||||
|
||||
def test_ellipsis
|
||||
tmux.send_keys 'seq 1000 | tr "\n" , | fzf --ellipsis=SNIPSNIP -e -q500', :Enter
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
@@ -2534,12 +2628,16 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_focus_event
|
||||
tmux.send_keys 'seq 100 | fzf --bind "focus:transform-prompt(echo [[{}]])"', :Enter
|
||||
tmux.send_keys 'seq 100 | fzf --bind "focus:transform-prompt(echo [[{}]]),?:unbind(focus)"', :Enter
|
||||
tmux.until { |lines| assert_includes(lines[-1], '[[1]]') }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes(lines[-1], '[[2]]') }
|
||||
tmux.send_keys :X
|
||||
tmux.until { |lines| assert_includes(lines[-1], '[[]]') }
|
||||
tmux.send_keys '?'
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
tmux.until { |lines| refute_includes(lines[-1], '[[1]]') }
|
||||
end
|
||||
|
||||
def test_labels_center
|
||||
@@ -2681,7 +2779,7 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_track
|
||||
tmux.send_keys "seq 1000 | #{FZF} --query 555 --track", :Enter
|
||||
tmux.send_keys "seq 1000 | #{FZF} --query 555 --track --bind t:toggle-track", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert_includes lines, '> 555'
|
||||
@@ -2701,20 +2799,97 @@ class TestGoFZF < TestBase
|
||||
assert_equal 1000, lines.match_count
|
||||
assert_equal '> 555', lines[index]
|
||||
end
|
||||
tmux.send_keys '555'
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert_includes lines, '> 555'
|
||||
assert_includes lines[-2], '+T'
|
||||
end
|
||||
tmux.send_keys 't'
|
||||
tmux.until do |lines|
|
||||
refute_includes lines[-2], '+T'
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 28, lines.match_count
|
||||
assert_includes lines, '> 55'
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 271, lines.match_count
|
||||
assert_includes lines, '> 5'
|
||||
end
|
||||
tmux.send_keys 't'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[-2], '+T'
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 1000, lines.match_count
|
||||
assert_includes lines, '> 5'
|
||||
end
|
||||
end
|
||||
|
||||
def test_one
|
||||
tmux.send_keys "seq 10 | #{FZF} --bind 'one:preview:echo {} is the only match'", :Enter
|
||||
def test_track_action
|
||||
tmux.send_keys "seq 1000 | #{FZF} --query 555 --bind t:track", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert_includes lines, '> 555'
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 28, lines.match_count
|
||||
assert_includes lines, '> 55'
|
||||
end
|
||||
tmux.send_keys :t
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[-2], '+T'
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 271, lines.match_count
|
||||
assert_includes lines, '> 55'
|
||||
end
|
||||
|
||||
# Automatically disabled when the tracking item is no longer visible
|
||||
tmux.send_keys '4'
|
||||
tmux.until do |lines|
|
||||
assert_equal 28, lines.match_count
|
||||
refute_includes lines[-2], '+T'
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 271, lines.match_count
|
||||
assert_includes lines, '> 5'
|
||||
end
|
||||
tmux.send_keys :t
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[-2], '+T'
|
||||
end
|
||||
tmux.send_keys :Up
|
||||
tmux.until do |lines|
|
||||
refute_includes lines[-2], '+T'
|
||||
end
|
||||
end
|
||||
|
||||
def test_one_and_zero
|
||||
tmux.send_keys "seq 10 | #{FZF} --bind 'zero:preview(echo no match),one:preview(echo {} is the only match)'", :Enter
|
||||
tmux.send_keys '1'
|
||||
tmux.until do |lines|
|
||||
assert_equal 2, lines.match_count
|
||||
refute(lines.any? { _1.include?('only match') })
|
||||
refute(lines.any? { _1.include?('no match') })
|
||||
end
|
||||
tmux.send_keys '0'
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert(lines.any? { _1.include?('only match') })
|
||||
end
|
||||
tmux.send_keys '0'
|
||||
tmux.until do |lines|
|
||||
assert_equal 0, lines.match_count
|
||||
assert(lines.any? { _1.include?('no match') })
|
||||
end
|
||||
end
|
||||
|
||||
def test_height_range_with_exit_0
|
||||
@@ -2723,6 +2898,51 @@ class TestGoFZF < TestBase
|
||||
tmux.send_keys :c
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
end
|
||||
|
||||
def test_reload_and_change
|
||||
tmux.send_keys "(echo foo; echo bar) | #{FZF} --bind 'load:reload-sync(sleep 60)+change-query(bar)'", :Enter
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
end
|
||||
|
||||
def test_reload_and_change_cache
|
||||
tmux.send_keys "echo bar | #{FZF} --bind 'zero:change-header(foo)+reload(echo foo)+clear-query'", :Enter
|
||||
expected = <<~OUTPUT
|
||||
> bar
|
||||
1/1
|
||||
>
|
||||
OUTPUT
|
||||
tmux.until { assert_block(expected, _1) }
|
||||
tmux.send_keys :z
|
||||
expected = <<~OUTPUT
|
||||
> foo
|
||||
foo
|
||||
1/1
|
||||
>
|
||||
OUTPUT
|
||||
tmux.until { assert_block(expected, _1) }
|
||||
end
|
||||
|
||||
def test_delete_with_modifiers
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind 'ctrl-delete:up+up,shift-delete:down,focus:transform-prompt:echo [{}]'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.send_keys 'C-Delete'
|
||||
tmux.until { |lines| assert_equal '[3]', lines[-1] }
|
||||
tmux.send_keys 'S-Delete'
|
||||
tmux.until { |lines| assert_equal '[2]', lines[-1] }
|
||||
end
|
||||
|
||||
def test_become_tty
|
||||
tmux.send_keys "sleep 0.5 | #{FZF} --bind 'start:reload:ls' --bind 'load:become:tty'", :Enter
|
||||
tmux.until { |lines| assert_includes lines, '/dev/tty' }
|
||||
end
|
||||
|
||||
def test_disabled_preview_update
|
||||
tmux.send_keys "echo bar | #{FZF} --disabled --bind 'change:reload:echo foo' --preview 'echo [{q}-{}]'", :Enter
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.until { |lines| assert(lines.any? { |line| line.include?('[-bar]') }) }
|
||||
tmux.send_keys :x
|
||||
tmux.until { |lines| assert(lines.any? { |line| line.include?('[x-foo]') }) }
|
||||
end
|
||||
end
|
||||
|
||||
module TestShell
|
||||
|
||||
Reference in New Issue
Block a user