mirror of
https://github.com/junegunn/fzf.git
synced 2025-08-12 18:55:49 -07:00
Compare commits
146 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
e4c3ecc57e | ||
|
673c5d886d | ||
|
f799b568d1 | ||
|
7bff4661f6 | ||
|
ffd8bef808 | ||
|
02cee2234d | ||
|
e0dd2be3fb | ||
|
a33c011c21 | ||
|
7c3f42bbba | ||
|
edac9820b5 | ||
|
84a47f7102 | ||
|
97ae8afb6f | ||
|
4138333f5c | ||
|
61339a8ae2 | ||
|
50eb2e3855 | ||
|
5fc78e4584 | ||
|
2736a2f69e | ||
|
179993f0cd | ||
|
b734f657f9 | ||
|
c29d7d02c2 | ||
|
3df6b2a58c | ||
|
b8aa2d2c32 | ||
|
0ff885461b | ||
|
ca43f95fb1 | ||
|
09700f676b | ||
|
4271e9cffa | ||
|
f3dc8a10d5 | ||
|
00fb486f6a | ||
|
4173e94c6f | ||
|
261d3d3340 | ||
|
15e20fcae1 | ||
|
f4f47f5fe3 | ||
|
71d11de7ca | ||
|
88d74a15aa | ||
|
0f02fc0c77 | ||
|
3f90fb42d8 | ||
|
9bd8988300 | ||
|
3c804bcfec | ||
|
cca4cdc4f1 | ||
|
8f899aaf8a | ||
|
e53b4bb439 | ||
|
ab247a1309 | ||
|
c21e9edad4 | ||
|
9c21a20f8b | ||
|
7191ebb615 | ||
|
a74731d7f5 | ||
|
e086f0b3fe | ||
|
8255aa23f4 | ||
|
a4bc08f5a3 | ||
|
7e5aa1e2a5 | ||
|
0818dbc36a | ||
|
347c4b2625 | ||
|
34f0d4d0c4 | ||
|
cbedb57511 | ||
|
9ef825d2fd | ||
|
85ae745910 | ||
|
7411da8d5a | ||
|
3f75a8369f | ||
|
4cd621e877 | ||
|
6e3a2fe0bf | ||
|
8b0e1f941a | ||
|
c7c5e7670a | ||
|
f6c621ef1b | ||
|
faf32d451d | ||
|
252fd7ecb1 | ||
|
7fa89dddb4 | ||
|
fefdb8c84e | ||
|
a6cc05936e | ||
|
b209843545 | ||
|
19759ed36e | ||
|
1a7ae8e7b9 | ||
|
da1f645670 | ||
|
3a2015ee26 | ||
|
c440418ce6 | ||
|
3d37a5ba1d | ||
|
15f4cfb6d9 | ||
|
be36de2482 | ||
|
391237f7df | ||
|
977e5effd9 | ||
|
8b36a4cb19 | ||
|
c8cd94a772 | ||
|
764316a53d | ||
|
2048fd4042 | ||
|
f84b3de24b | ||
|
6a1f3ec08b | ||
|
2e353aee96 | ||
|
8edfd14a37 | ||
|
1a191ec6f7 | ||
|
e7171e94b4 | ||
|
398d937419 | ||
|
34fe5ab143 | ||
|
1b08f43f82 | ||
|
b24a2e2fdc | ||
|
4c4c6e626e | ||
|
7310370a31 | ||
|
8ae94f0059 | ||
|
8fccf20892 | ||
|
5a874ae241 | ||
|
f4e1ed25f2 | ||
|
cbfbb49ab4 | ||
|
489b16efce | ||
|
b82c1693c0 | ||
|
019bfc4e35 | ||
|
dfda5c054a | ||
|
f657169616 | ||
|
4c06da8b70 | ||
|
9fe2393a00 | ||
|
e2e8d94b14 | ||
|
4f9a7f8c87 | ||
|
bb0502ff44 | ||
|
c256442245 | ||
|
1137404190 | ||
|
d57c6d0284 | ||
|
76bbf57b3d | ||
|
806a47a7cc | ||
|
29851c18aa | ||
|
dea950c2c8 | ||
|
a367dfb22e | ||
|
9fe1a7b373 | ||
|
8e2d21c548 | ||
|
bedf1cd357 | ||
|
13f180a70c | ||
|
6654239c94 | ||
|
1b61e5e9e9 | ||
|
43b3b907f8 | ||
|
fcd896508b | ||
|
f55c990e86 | ||
|
d110372f99 | ||
|
c862af09f2 | ||
|
1cfeec0ca3 | ||
|
a0649edc1e | ||
|
0e0bcb3e10 | ||
|
686528d627 | ||
|
3afa920151 | ||
|
32c493e994 | ||
|
1a76bdf891 | ||
|
af48b3df29 | ||
|
58ac1fb2fa | ||
|
e922704f72 | ||
|
c6115735c7 | ||
|
9ddf5c72be | ||
|
cc5640326b | ||
|
bf447d7703 | ||
|
cbb008c938 | ||
|
eaa0c52b45 | ||
|
82791f7efc |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
custom: ["https://paypal.me/junegunn", "https://www.buymeacoffee.com/junegunn"]
|
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "gomod"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
37
.github/workflows/codeql-analysis.yml
vendored
Normal file
37
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning
|
||||||
|
name: CodeQL
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master, devel ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: ['go']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v1
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v1
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v1
|
45
.github/workflows/linux.yml
vendored
Normal file
45
.github/workflows/linux.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
name: Test fzf on Linux
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master, devel ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go: [1.14, 1.16]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
|
- name: Setup Ruby
|
||||||
|
uses: ruby/setup-ruby@v1.62.0
|
||||||
|
with:
|
||||||
|
ruby-version: 3.0.0
|
||||||
|
|
||||||
|
- name: Install packages
|
||||||
|
run: sudo apt-get install --yes zsh fish tmux
|
||||||
|
|
||||||
|
- name: Install Ruby gems
|
||||||
|
run: sudo gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1
|
||||||
|
|
||||||
|
- name: Rubocop
|
||||||
|
run: rubocop --require rubocop-minitest --require rubocop-performance
|
||||||
|
|
||||||
|
- name: Unit test
|
||||||
|
run: make test
|
||||||
|
|
||||||
|
- name: Integration test
|
||||||
|
run: make install && ./install --all && LC_ALL=C tmux new-session -d && ruby test/test_go.rb --verbose
|
45
.github/workflows/macos.yml
vendored
Normal file
45
.github/workflows/macos.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
name: Test fzf on macOS
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master, devel ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: macos-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go: [1.14, 1.16]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
|
- name: Setup Ruby
|
||||||
|
uses: ruby/setup-ruby@v1.62.0
|
||||||
|
with:
|
||||||
|
ruby-version: 3.0.0
|
||||||
|
|
||||||
|
- name: Install packages
|
||||||
|
run: HOMEBREW_NO_INSTALL_CLEANUP=1 brew install fish zsh tmux
|
||||||
|
|
||||||
|
- name: Install Ruby gems
|
||||||
|
run: gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1
|
||||||
|
|
||||||
|
- name: Rubocop
|
||||||
|
run: rubocop --require rubocop-minitest --require rubocop-performance
|
||||||
|
|
||||||
|
- name: Unit test
|
||||||
|
run: make test
|
||||||
|
|
||||||
|
- name: Integration test
|
||||||
|
run: make install && ./install --all && LC_ALL=C tmux new-session -d && ruby test/test_go.rb --verbose
|
11
.gon.hcl
11
.gon.hcl
@@ -1,11 +0,0 @@
|
|||||||
source = ["./dist/fzf-macos_darwin_amd64/fzf"]
|
|
||||||
bundle_id = "kr.junegunn.fzf"
|
|
||||||
|
|
||||||
apple_id {
|
|
||||||
username = "junegunn.c@gmail.com"
|
|
||||||
password = "@env:AC_PASSWORD"
|
|
||||||
}
|
|
||||||
|
|
||||||
sign {
|
|
||||||
application_identity = "Apple Development: junegunn.c@gmail.com"
|
|
||||||
}
|
|
@@ -15,9 +15,55 @@ builds:
|
|||||||
ldflags:
|
ldflags:
|
||||||
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
|
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
|
||||||
hooks:
|
hooks:
|
||||||
post: gon .gon.hcl
|
post: |
|
||||||
|
sh -c '
|
||||||
|
cat > /tmp/fzf-gon-amd64.hcl << EOF
|
||||||
|
source = ["./dist/fzf-macos_darwin_amd64/fzf"]
|
||||||
|
bundle_id = "kr.junegunn.fzf"
|
||||||
|
apple_id {
|
||||||
|
username = "junegunn.c@gmail.com"
|
||||||
|
password = "@env:AC_PASSWORD"
|
||||||
|
}
|
||||||
|
sign {
|
||||||
|
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
|
||||||
|
}
|
||||||
|
zip {
|
||||||
|
output_path = "./dist/fzf-{{ .Version }}-darwin_amd64.zip"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
gon /tmp/fzf-gon-amd64.hcl
|
||||||
|
'
|
||||||
|
|
||||||
- goos:
|
- id: fzf-macos-arm
|
||||||
|
binary: fzf
|
||||||
|
goos:
|
||||||
|
- darwin
|
||||||
|
goarch:
|
||||||
|
- arm64
|
||||||
|
ldflags:
|
||||||
|
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
|
||||||
|
hooks:
|
||||||
|
post: |
|
||||||
|
sh -c '
|
||||||
|
cat > /tmp/fzf-gon-arm64.hcl << EOF
|
||||||
|
source = ["./dist/fzf-macos-arm_darwin_arm64/fzf"]
|
||||||
|
bundle_id = "kr.junegunn.fzf"
|
||||||
|
apple_id {
|
||||||
|
username = "junegunn.c@gmail.com"
|
||||||
|
password = "@env:AC_PASSWORD"
|
||||||
|
}
|
||||||
|
sign {
|
||||||
|
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
|
||||||
|
}
|
||||||
|
zip {
|
||||||
|
output_path = "./dist/fzf-{{ .Version }}-darwin_arm64.zip"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
gon /tmp/fzf-gon-arm64.hcl
|
||||||
|
'
|
||||||
|
|
||||||
|
- id: fzf
|
||||||
|
goos:
|
||||||
- linux
|
- linux
|
||||||
- windows
|
- windows
|
||||||
- freebsd
|
- freebsd
|
||||||
@@ -44,6 +90,8 @@ builds:
|
|||||||
|
|
||||||
archives:
|
archives:
|
||||||
- name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
- name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||||
|
builds:
|
||||||
|
- fzf
|
||||||
format: tar.gz
|
format: tar.gz
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
@@ -57,6 +105,8 @@ release:
|
|||||||
name: fzf
|
name: fzf
|
||||||
prerelease: auto
|
prerelease: auto
|
||||||
name_template: '{{ .Tag }}'
|
name_template: '{{ .Tag }}'
|
||||||
|
extra_files:
|
||||||
|
- glob: ./dist/fzf-*darwin*.zip
|
||||||
|
|
||||||
snapshot:
|
snapshot:
|
||||||
name_template: "{{ .Tag }}-devel"
|
name_template: "{{ .Tag }}-devel"
|
||||||
|
28
.travis.yml
28
.travis.yml
@@ -1,28 +0,0 @@
|
|||||||
language: go
|
|
||||||
go:
|
|
||||||
- "1.14"
|
|
||||||
env: GO111MODULE=on
|
|
||||||
os:
|
|
||||||
- linux
|
|
||||||
- osx
|
|
||||||
dist: bionic
|
|
||||||
osx_image: xcode12.2
|
|
||||||
addons:
|
|
||||||
apt:
|
|
||||||
packages:
|
|
||||||
- fish
|
|
||||||
- zsh
|
|
||||||
sources:
|
|
||||||
sourceline: ppa:fish-shell/release-3
|
|
||||||
homebrew:
|
|
||||||
packages:
|
|
||||||
- fish
|
|
||||||
- tmux
|
|
||||||
install: gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1
|
|
||||||
script:
|
|
||||||
- make test
|
|
||||||
# LC_ALL=C to avoid escape codes in
|
|
||||||
# printf %q $'\355\205\214\354\212\244\355\212\270' on macOS. Bash on
|
|
||||||
# macOS is built without HANDLE_MULTIBYTE?
|
|
||||||
- make install && ./install --all && LC_ALL=C tmux new-session -d && ruby test/test_go.rb --verbose
|
|
||||||
- rubocop --require rubocop-minitest --require rubocop-performance
|
|
565
ADVANCED.md
Normal file
565
ADVANCED.md
Normal file
@@ -0,0 +1,565 @@
|
|||||||
|
Advanced fzf examples
|
||||||
|
======================
|
||||||
|
|
||||||
|
*(Last update: 2021/05/22)*
|
||||||
|
|
||||||
|
<!-- vim-markdown-toc GFM -->
|
||||||
|
|
||||||
|
* [Introduction](#introduction)
|
||||||
|
* [Screen Layout](#screen-layout)
|
||||||
|
* [`--height`](#--height)
|
||||||
|
* [`fzf-tmux`](#fzf-tmux)
|
||||||
|
* [Popup window support](#popup-window-support)
|
||||||
|
* [Dynamic reloading of the list](#dynamic-reloading-of-the-list)
|
||||||
|
* [Updating the list of processes by pressing CTRL-R](#updating-the-list-of-processes-by-pressing-ctrl-r)
|
||||||
|
* [Toggling between data sources](#toggling-between-data-sources)
|
||||||
|
* [Ripgrep integration](#ripgrep-integration)
|
||||||
|
* [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter)
|
||||||
|
* [Using fzf as interative Ripgrep launcher](#using-fzf-as-interative-ripgrep-launcher)
|
||||||
|
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
|
||||||
|
* [Log tailing](#log-tailing)
|
||||||
|
* [Key bindings for git objects](#key-bindings-for-git-objects)
|
||||||
|
* [Files listed in `git status`](#files-listed-in-git-status)
|
||||||
|
* [Branches](#branches)
|
||||||
|
* [Commit hashes](#commit-hashes)
|
||||||
|
* [Color themes](#color-themes)
|
||||||
|
* [Generating fzf color theme from Vim color schemes](#generating-fzf-color-theme-from-vim-color-schemes)
|
||||||
|
|
||||||
|
<!-- vim-markdown-toc -->
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
------------
|
||||||
|
|
||||||
|
fzf is an interactive [Unix filter][filter] program that is designed to be
|
||||||
|
used with other Unix tools. It reads a list of items from the standard input,
|
||||||
|
allows you to select a subset of the items, and prints the selected ones to
|
||||||
|
the standard output. You can think of it as an interactive version of *grep*,
|
||||||
|
and it's already useful even if you don't know any of its options.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 1. ps: Feed the list of processes to fzf
|
||||||
|
# 2. fzf: Interactively select a process using fuzzy matching algorithm
|
||||||
|
# 3. awk: Take the PID from the selected line
|
||||||
|
# 3. kill: Kill the process with the PID
|
||||||
|
ps -ef | fzf | awk '{print $2}' | xargs kill -9
|
||||||
|
```
|
||||||
|
|
||||||
|
[filter]: https://en.wikipedia.org/wiki/Filter_(software)
|
||||||
|
|
||||||
|
While the above example succinctly summarizes the fundamental concept of fzf,
|
||||||
|
you can build much more sophisticated interactive workflows using fzf once you
|
||||||
|
learn its wide variety of features.
|
||||||
|
|
||||||
|
- To see the full list of options and features, see `man fzf`
|
||||||
|
- To see the latest additions, see [CHANGELOG.md](CHANGELOG.md)
|
||||||
|
|
||||||
|
This document will guide you through some examples that will familiarize you
|
||||||
|
with the advanced features of fzf.
|
||||||
|
|
||||||
|
Screen Layout
|
||||||
|
-------------
|
||||||
|
|
||||||
|
### `--height`
|
||||||
|
|
||||||
|
fzf by default opens in fullscreen mode, but it's not always desirable.
|
||||||
|
Oftentimes, you want to see the current context of the terminal while using
|
||||||
|
fzf. `--height` is an option for opening fzf below the cursor in
|
||||||
|
non-fullscreen mode so you can still see the previous commands and their
|
||||||
|
results above it.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
fzf --height=40%
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You might also want to experiment with other layout options such as
|
||||||
|
`--layout=reverse`, `--info=inline`, `--border`, `--margin`, etc.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
fzf --height=40% --layout=reverse
|
||||||
|
fzf --height=40% --layout=reverse --info=inline
|
||||||
|
fzf --height=40% --layout=reverse --info=inline --border
|
||||||
|
fzf --height=40% --layout=reverse --info=inline --border --margin=1
|
||||||
|
fzf --height=40% --layout=reverse --info=inline --border --margin=1 --padding=1
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*(See `Layout` section of the man page to see the full list of options)*
|
||||||
|
|
||||||
|
But you definitely don't want to repeat `--height=40% --layout=reverse
|
||||||
|
--info=inline --border --margin=1 --padding=1` every time you use fzf. You
|
||||||
|
could write a wrapper script or shell alias, but there is an easier option.
|
||||||
|
Define `$FZF_DEFAULT_OPTS` like so:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export FZF_DEFAULT_OPTS="--height=40% --layout=reverse --info=inline --border --margin=1 --padding=1"
|
||||||
|
```
|
||||||
|
|
||||||
|
### `fzf-tmux`
|
||||||
|
|
||||||
|
Before fzf had `--height` option, we would open fzf in a tmux split pane not
|
||||||
|
to take up the whole screen. This is done using `fzf-tmux` script.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Open fzf on a tmux split pane below the current pane.
|
||||||
|
# Takes the same set of options.
|
||||||
|
fzf-tmux --layout=reverse
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The limitation of `fzf-tmux` is that it only works when you're on tmux unlike
|
||||||
|
`--height` option. But the advantage of it is that it's more flexible.
|
||||||
|
(See `man fzf-tmux` for available options.)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# On the right (50%)
|
||||||
|
fzf-tmux -r
|
||||||
|
|
||||||
|
# On the left (30%)
|
||||||
|
fzf-tmux -l30%
|
||||||
|
|
||||||
|
# Above the cursor
|
||||||
|
fzf-tmux -u30%
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Popup window support
|
||||||
|
|
||||||
|
But here's the really cool part; tmux 3.2 added support for popup windows. So
|
||||||
|
you can open fzf in a popup window, which is quite useful if you frequently
|
||||||
|
use split panes.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Open tmux in a tmux popup window (default size: 50% of the screen)
|
||||||
|
fzf-tmux -p
|
||||||
|
|
||||||
|
# 80% width, 60% height
|
||||||
|
fzf-tmux -p 80%,60%
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> You might also want to check out my tmux plugins which support this popup
|
||||||
|
> window layout.
|
||||||
|
>
|
||||||
|
> - https://github.com/junegunn/tmux-fzf-url
|
||||||
|
> - https://github.com/junegunn/tmux-fzf-maccy
|
||||||
|
|
||||||
|
Dynamic reloading of the list
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
fzf can dynamically update the candidate list using an arbitrary program with
|
||||||
|
`reload` bindings (The design document for `reload` can be found
|
||||||
|
[here][reload]).
|
||||||
|
|
||||||
|
[reload]: https://github.com/junegunn/fzf/issues/1750
|
||||||
|
|
||||||
|
### Updating the list of processes by pressing CTRL-R
|
||||||
|
|
||||||
|
This example shows how you can set up a binding for dynamically updating the
|
||||||
|
list without restarting fzf.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
(date; ps -ef) |
|
||||||
|
fzf --bind='ctrl-r:reload(date; ps -ef)' \
|
||||||
|
--header=$'Press CTRL-R to reload\n\n' --header-lines=2 \
|
||||||
|
--preview='echo {}' --preview-window=down,3,wrap \
|
||||||
|
--layout=reverse --height=80% | awk '{print $2}' | xargs kill -9
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- The initial command is `(date; ps -ef)`. It prints the current date and
|
||||||
|
time, and the list of the processes.
|
||||||
|
- With `--header` option, you can show any message as the fixed header.
|
||||||
|
- To disallow selecting the first two lines (`date` and `ps` header), we use
|
||||||
|
`--header-lines=2` option.
|
||||||
|
- `--bind='ctrl-r:reload(date; ps -ef)'` binds CTRL-R to `reload` action that
|
||||||
|
runs `date; ps -ef`, so we can update the list of the processes by pressing
|
||||||
|
CTRL-R.
|
||||||
|
- We use simple `echo {}` preview option, so we can see the entire line on the
|
||||||
|
preview window below even if it's too long
|
||||||
|
|
||||||
|
### Toggling between data sources
|
||||||
|
|
||||||
|
You're not limited to just one reload binding. Set up multiple bindings so
|
||||||
|
you can switch between data sources.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
find * | fzf --prompt 'All> ' \
|
||||||
|
--header 'CTRL-D: Directories / CTRL-F: Files' \
|
||||||
|
--bind 'ctrl-d:change-prompt(Directories> )+reload(find * -type d)' \
|
||||||
|
--bind 'ctrl-f:change-prompt(Files> )+reload(find * -type f)'
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Ripgrep integration
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
### Using fzf as the secondary filter
|
||||||
|
|
||||||
|
* Requires [bat][bat]
|
||||||
|
* Requires [Ripgrep][rg]
|
||||||
|
|
||||||
|
[bat]: https://github.com/sharkdp/bat
|
||||||
|
[rg]: https://github.com/BurntSushi/ripgrep
|
||||||
|
|
||||||
|
fzf is pretty fast for filtering a list that you will rarely have to think
|
||||||
|
about its performance. But it is not the right tool for searching for text
|
||||||
|
inside many large files, and in that case you should definitely use something
|
||||||
|
like [Ripgrep][rg].
|
||||||
|
|
||||||
|
In the next example, Ripgrep is the primary filter that searches for the given
|
||||||
|
text in files, and fzf is used as the secondary fuzzy filter that adds
|
||||||
|
interactivity to the workflow. And we use [bat][bat] to show the matching line in
|
||||||
|
the preview window.
|
||||||
|
|
||||||
|
This is a bash script and it will not run as expected on other non-compliant
|
||||||
|
shells. To avoid the compatibility issue, let's save this snippet as a script
|
||||||
|
file called `rfv`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# 1. Search for text in files using Ripgrep
|
||||||
|
# 2. Interactively narrow down the list using fzf
|
||||||
|
# 3. Open the file in Vim
|
||||||
|
IFS=: read -ra selected < <(
|
||||||
|
rg --color=always --line-number --no-heading --smart-case "${*:-}" |
|
||||||
|
fzf --ansi \
|
||||||
|
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||||
|
--delimiter : \
|
||||||
|
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||||
|
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
|
||||||
|
)
|
||||||
|
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
|
||||||
|
```
|
||||||
|
|
||||||
|
And run it with an initial query string.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Make the script executable
|
||||||
|
chmod +x rfv
|
||||||
|
|
||||||
|
# Run it with the initial query "algo"
|
||||||
|
./rfv algo
|
||||||
|
```
|
||||||
|
|
||||||
|
> Ripgrep will perform the initial search and list all the lines that contain
|
||||||
|
`algo`. Then we further narrow down the list on fzf.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
I know it's a lot to digest, let's try to break down the code.
|
||||||
|
|
||||||
|
- Ripgrep prints the matching lines in the following format
|
||||||
|
```
|
||||||
|
man/man1/fzf.1:54:.BI "--algo=" TYPE
|
||||||
|
man/man1/fzf.1:55:Fuzzy matching algorithm (default: v2)
|
||||||
|
man/man1/fzf.1:58:.BR v2 " Optimal scoring algorithm (quality)"
|
||||||
|
src/pattern_test.go:7: "github.com/junegunn/fzf/src/algo"
|
||||||
|
```
|
||||||
|
The first token delimited by `:` is the file path, and the second token is
|
||||||
|
the line number of the matching line. They respectively correspond to `{1}`
|
||||||
|
and `{2}` in the preview command.
|
||||||
|
- `--preview 'bat --color=always {1} --highlight-line {2}'`
|
||||||
|
- As we run `rg` with `--color=always` option, we should tell fzf to parse
|
||||||
|
ANSI color codes in the input by setting `--ansi`.
|
||||||
|
- We customize how fzf colors various text elements using `--color` option.
|
||||||
|
`-1` tells fzf to keep the original color from the input. See `man fzf` for
|
||||||
|
available color options.
|
||||||
|
- The value of `--preview-window` option consists of 5 components delimited
|
||||||
|
by `,`
|
||||||
|
1. `up` — Position of the preview window
|
||||||
|
1. `60%` — Size of the preview window
|
||||||
|
1. `border-bottom` — Preview window border only on the bottom side
|
||||||
|
1. `+{2}+3/3` — Scroll offset of the preview contents
|
||||||
|
1. `~3` — Fixed header
|
||||||
|
- Let's break down the latter two. We want to display the bat output in the
|
||||||
|
preview window with a certain scroll offset so that the matching line is
|
||||||
|
positioned near the center of the preview window.
|
||||||
|
- `+{2}` — The base offset is extracted from the second token
|
||||||
|
- `+3` — We add 3 lines to the base offset to compensate for the header
|
||||||
|
part of `bat` output
|
||||||
|
- ```
|
||||||
|
───────┬──────────────────────────────────────────────────────────
|
||||||
|
│ File: CHANGELOG.md
|
||||||
|
───────┼──────────────────────────────────────────────────────────
|
||||||
|
1 │ CHANGELOG
|
||||||
|
2 │ =========
|
||||||
|
3 │
|
||||||
|
4 │ 0.26.0
|
||||||
|
5 │ ------
|
||||||
|
```
|
||||||
|
- `/3` adjusts the offset so that the matching line is shown at a third
|
||||||
|
position in the window
|
||||||
|
- `~3` makes the top three lines fixed header so that they are always
|
||||||
|
visible regardless of the scroll offset
|
||||||
|
- Once we selected a line, we open the file with `vim` (`vim
|
||||||
|
"${selected[0]}"`) and move the cursor to the line (`+${selected[1]}`).
|
||||||
|
|
||||||
|
### Using fzf as interative Ripgrep launcher
|
||||||
|
|
||||||
|
We have learned that we can bind `reload` action to a key (e.g.
|
||||||
|
`--bind=ctrl-r:execute(ps -ef)`). In the next example, we are going to **bind
|
||||||
|
`reload` action to `change` event** so that whenever the user *changes* the
|
||||||
|
query string on fzf, `reload` action is triggered.
|
||||||
|
|
||||||
|
Here is a variation of the above `rfv` script. fzf will restart Ripgrep every
|
||||||
|
time the user updates the query string on fzf. Searching and filtering is
|
||||||
|
completely done by Ripgrep, and fzf merely provides the interactive interface.
|
||||||
|
So we lose the "fuzziness", but the performance will be better on larger
|
||||||
|
projects, and it will free up memory as you narrow down the results.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# 1. Search for text in files using Ripgrep
|
||||||
|
# 2. Interactively restart Ripgrep with reload action
|
||||||
|
# 3. Open the file in Vim
|
||||||
|
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||||
|
INITIAL_QUERY="${*:-}"
|
||||||
|
IFS=: read -ra selected < <(
|
||||||
|
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
|
||||||
|
fzf --ansi \
|
||||||
|
--disabled --query "$INITIAL_QUERY" \
|
||||||
|
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||||
|
--delimiter : \
|
||||||
|
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||||
|
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
|
||||||
|
)
|
||||||
|
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- 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.
|
||||||
|
- 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
|
||||||
|
reduce the number of intermediate Ripgrep processes while we're typing in
|
||||||
|
a query.
|
||||||
|
|
||||||
|
### Switching to fzf-only search mode
|
||||||
|
|
||||||
|
*(Requires fzf 0.27.1 or above)*
|
||||||
|
|
||||||
|
In the previous example, we lost fuzzy matching capability as we completely
|
||||||
|
delegated search functionality to Ripgrep. But we can dynamically switch to
|
||||||
|
fzf-only search mode by *"unbinding"* `reload` action from `change` event.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Two-phase filtering with Ripgrep and fzf
|
||||||
|
#
|
||||||
|
# 1. Search for text in files using Ripgrep
|
||||||
|
# 2. Interactively restart Ripgrep with reload action
|
||||||
|
# * Press alt-enter to switch to fzf-only filtering
|
||||||
|
# 3. Open the file in Vim
|
||||||
|
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||||
|
INITIAL_QUERY="${*:-}"
|
||||||
|
IFS=: read -ra selected < <(
|
||||||
|
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
|
||||||
|
fzf --ansi \
|
||||||
|
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||||
|
--disabled --query "$INITIAL_QUERY" \
|
||||||
|
--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" \
|
||||||
|
--prompt '1. ripgrep> ' \
|
||||||
|
--delimiter : \
|
||||||
|
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||||
|
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
|
||||||
|
)
|
||||||
|
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
|
||||||
|
```
|
||||||
|
|
||||||
|
* Phase 1. Filtering with Ripgrep
|
||||||
|

|
||||||
|
* Phase 2. Filtering with fzf
|
||||||
|

|
||||||
|
|
||||||
|
- We added `--prompt` option to show that fzf is initially running in "Ripgrep
|
||||||
|
launcher mode".
|
||||||
|
- We added `alt-enter` binding that
|
||||||
|
1. unbinds `change` event, so Ripgrep is no longer restarted on key press
|
||||||
|
2. changes the prompt to `2. fzf>`
|
||||||
|
3. enables search functionality of fzf
|
||||||
|
4. clears the current query string that was used to start Ripgrep process
|
||||||
|
5. and unbinds `alt-enter` itself as this is a one-off event
|
||||||
|
- We reverted `--color` option for customizing how the matching chunks are
|
||||||
|
displayed in the second phase
|
||||||
|
|
||||||
|
Log tailing
|
||||||
|
-----------
|
||||||
|
|
||||||
|
fzf can run long-running preview commands and render partial results before
|
||||||
|
completion. And when you specify `follow` flag in `--preview-window` option,
|
||||||
|
fzf will "`tail -f`" the result, automatically scrolling to the bottom.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# With "follow", preview window will automatically scroll to the bottom.
|
||||||
|
# "\033[2J" is an ANSI escape sequence for clearing the screen.
|
||||||
|
# When fzf reads this code it clears the previous preview contents.
|
||||||
|
fzf --preview-window follow --preview 'for i in $(seq 100000); do
|
||||||
|
echo "$i"
|
||||||
|
sleep 0.01
|
||||||
|
(( i % 300 == 0 )) && printf "\033[2J"
|
||||||
|
done'
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Admittedly, that was a silly example. Here's a practical one for browsing
|
||||||
|
Kubernetes pods.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
read -ra tokens < <(
|
||||||
|
kubectl get pods --all-namespaces |
|
||||||
|
fzf --info=inline --layout=reverse --header-lines=1 --border \
|
||||||
|
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
|
||||||
|
--header $'Press CTRL-O to open log in editor\n\n' \
|
||||||
|
--bind ctrl-/:toggle-preview \
|
||||||
|
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --namespace {1} {2}) > /dev/tty' \
|
||||||
|
--preview-window up,follow \
|
||||||
|
--preview 'kubectl logs --follow --tail=100000 --namespace {1} {2}' "$@"
|
||||||
|
)
|
||||||
|
[ ${#tokens} -gt 1 ] &&
|
||||||
|
kubectl exec -it --namespace "${tokens[0]}" "${tokens[1]}" -- bash
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- The preview window will *"log tail"* the pod
|
||||||
|
- Holding on to a large amount of log will consume a lot of memory. So we
|
||||||
|
limited the initial log amount with `--tail=100000`.
|
||||||
|
- With `execute` binding, you can press CTRL-O to open the log in your editor
|
||||||
|
without leaving fzf
|
||||||
|
- Select a pod (with an enter key) to `kubectl exec` into it
|
||||||
|
|
||||||
|
Key bindings for git objects
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
I have [blogged](https://junegunn.kr/2016/07/fzf-git) about my fzf+git key
|
||||||
|
bindings a few years ago. I'm going to show them here again, because they are
|
||||||
|
seriously useful.
|
||||||
|
|
||||||
|
### Files listed in `git status`
|
||||||
|
|
||||||
|
<kbd>CTRL-G</kbd><kbd>CTRL-F</kbd>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Branches
|
||||||
|
|
||||||
|
<kbd>CTRL-G</kbd><kbd>CTRL-B</kbd>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Commit hashes
|
||||||
|
|
||||||
|
<kbd>CTRL-G</kbd><kbd>CTRL-H</kbd>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
The full source code can be found [here](https://gist.github.com/junegunn/8b572b8d4b5eddd8b85e5f4d40f17236).
|
||||||
|
|
||||||
|
Color themes
|
||||||
|
------------
|
||||||
|
|
||||||
|
You can customize how fzf colors the text elements with `--color` option. Here
|
||||||
|
are a few color themes. Note that you need a terminal emulator that can
|
||||||
|
display 24-bit colors.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# junegunn/seoul256.vim (dark)
|
||||||
|
export FZF_DEFAULT_OPTS='--color=bg+:#3F3F3F,bg:#4B4B4B,border:#6B6B6B,spinner:#98BC99,hl:#719872,fg:#D9D9D9,header:#719872,info:#BDBB72,pointer:#E12672,marker:#E17899,fg+:#D9D9D9,preview-bg:#3F3F3F,prompt:#98BEDE,hl+:#98BC99'
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```sh
|
||||||
|
# junegunn/seoul256.vim (light)
|
||||||
|
export FZF_DEFAULT_OPTS='--color=bg+:#D9D9D9,bg:#E1E1E1,border:#C8C8C8,spinner:#719899,hl:#719872,fg:#616161,header:#719872,info:#727100,pointer:#E12672,marker:#E17899,fg+:#616161,preview-bg:#D9D9D9,prompt:#0099BD,hl+:#719899'
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```sh
|
||||||
|
# morhetz/gruvbox
|
||||||
|
export FZF_DEFAULT_OPTS='--color=bg+:#3c3836,bg:#32302f,spinner:#fb4934,hl:#928374,fg:#ebdbb2,header:#928374,info:#8ec07c,pointer:#fb4934,marker:#fb4934,fg+:#ebdbb2,prompt:#fb4934,hl+:#fb4934'
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```sh
|
||||||
|
# arcticicestudio/nord-vim
|
||||||
|
export FZF_DEFAULT_OPTS='--color=bg+:#3B4252,bg:#2E3440,spinner:#81A1C1,hl:#616E88,fg:#D8DEE9,header:#616E88,info:#81A1C1,pointer:#81A1C1,marker:#81A1C1,fg+:#D8DEE9,prompt:#81A1C1,hl+:#81A1C1'
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```sh
|
||||||
|
# tomasr/molokai
|
||||||
|
export FZF_DEFAULT_OPTS='--color=bg+:#293739,bg:#1B1D1E,border:#808080,spinner:#E6DB74,hl:#7E8E91,fg:#F8F8F2,header:#7E8E91,info:#A6E22E,pointer:#A6E22E,marker:#F92672,fg+:#F8F8F2,prompt:#F92672,hl+:#F92672'
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Generating fzf color theme from Vim color schemes
|
||||||
|
|
||||||
|
The Vim plugin of fzf can generate `--color` option from the current color
|
||||||
|
scheme according to `g:fzf_colors` variable. You can find the detailed
|
||||||
|
explanation [here](https://github.com/junegunn/fzf/blob/master/README-VIM.md#explanation-of-gfzf_colors).
|
||||||
|
|
||||||
|
Here is an example. Add this to your Vim configuration file.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
let g:fzf_colors =
|
||||||
|
\ { 'fg': ['fg', 'Normal'],
|
||||||
|
\ 'bg': ['bg', 'Normal'],
|
||||||
|
\ 'preview-bg': ['bg', 'NormalFloat'],
|
||||||
|
\ 'hl': ['fg', 'Comment'],
|
||||||
|
\ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'],
|
||||||
|
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
|
||||||
|
\ 'hl+': ['fg', 'Statement'],
|
||||||
|
\ 'info': ['fg', 'PreProc'],
|
||||||
|
\ 'border': ['fg', 'Ignore'],
|
||||||
|
\ 'prompt': ['fg', 'Conditional'],
|
||||||
|
\ 'pointer': ['fg', 'Exception'],
|
||||||
|
\ 'marker': ['fg', 'Keyword'],
|
||||||
|
\ 'spinner': ['fg', 'Label'],
|
||||||
|
\ 'header': ['fg', 'Comment'] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can see how the `--color` option is generated by printing the result
|
||||||
|
of `fzf#wrap()`.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
:echo fzf#wrap()
|
||||||
|
```
|
||||||
|
|
||||||
|
Use this command to append `export FZF_DEFAULT_OPTS="..."` line to the end of
|
||||||
|
the current file.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
:call append('$', printf('export FZF_DEFAULT_OPTS="%s"', matchstr(fzf#wrap().options, "--color[^']*")))
|
||||||
|
```
|
2
BUILD.md
2
BUILD.md
@@ -6,7 +6,7 @@ Build instructions
|
|||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Go 1.11 or above
|
- Go 1.13 or above
|
||||||
|
|
||||||
### Using Makefile
|
### Using Makefile
|
||||||
|
|
||||||
|
119
CHANGELOG.md
119
CHANGELOG.md
@@ -1,6 +1,124 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
0.28.0
|
||||||
|
------
|
||||||
|
- Added `--header-first` option to print header before the prompt line
|
||||||
|
```sh
|
||||||
|
fzf --header $'Welcome to fzf\n▔▔▔▔▔▔▔▔▔▔▔▔▔▔' --reverse --height 30% --border --header-first
|
||||||
|
```
|
||||||
|
- Added `--scroll-off=LINES` option (similar to `scrolloff` option of Vim)
|
||||||
|
- You can set it to a very large number so that the cursor stays in the
|
||||||
|
middle of the screen while scrolling
|
||||||
|
```sh
|
||||||
|
fzf --scroll-off=5
|
||||||
|
fzf --scroll-off=999
|
||||||
|
```
|
||||||
|
- Fixed bug where preview window is not updated on `reload` (#2644)
|
||||||
|
- fzf on Windows will also use `$SHELL` to execute external programs
|
||||||
|
- See #2638 and #2647
|
||||||
|
- Thanks to @rashil2000, @vovcacik, and @janlazo
|
||||||
|
|
||||||
|
0.27.3
|
||||||
|
------
|
||||||
|
- Preview window is `hidden` by default when there are `preview` bindings but
|
||||||
|
`--preview` command is not given
|
||||||
|
- Fixed bug where `{n}` is not properly reset on `reload`
|
||||||
|
- Fixed bug where spinner is not displayed on `reload`
|
||||||
|
- Enhancements in tcell renderer for Windows (#2616)
|
||||||
|
- Vim plugin
|
||||||
|
- `sinklist` is added as a synonym to `sink*` so that it's easier to add
|
||||||
|
a function to a spec dictionary
|
||||||
|
```vim
|
||||||
|
let spec = { 'source': 'ls', 'options': ['--multi', '--preview', 'cat {}'] }
|
||||||
|
function spec.sinklist(matches)
|
||||||
|
echom string(a:matches)
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
call fzf#run(fzf#wrap(spec))
|
||||||
|
```
|
||||||
|
- Vim 7 compatibility
|
||||||
|
|
||||||
|
0.27.2
|
||||||
|
------
|
||||||
|
- 16 base ANSI colors can be specified by their names
|
||||||
|
```sh
|
||||||
|
fzf --color fg:3,fg+:11
|
||||||
|
fzf --color fg:yellow,fg+:bright-yellow
|
||||||
|
```
|
||||||
|
- Fix bug where `--read0` not properly displaying long lines
|
||||||
|
|
||||||
|
0.27.1
|
||||||
|
------
|
||||||
|
- Added `unbind` action. In the following Ripgrep launcher example, you can
|
||||||
|
use `unbind(reload)` to switch to fzf-only filtering mode.
|
||||||
|
- See https://github.com/junegunn/fzf/blob/master/ADVANCED.md#switching-to-fzf-only-search-mode
|
||||||
|
- Vim plugin
|
||||||
|
- Vim plugin will stop immediately even when the source command hasn't finished
|
||||||
|
```vim
|
||||||
|
" fzf will read the stream file while allowing other processes to append to it
|
||||||
|
call fzf#run({'source': 'cat /dev/null > /tmp/stream; tail -f /tmp/stream'})
|
||||||
|
```
|
||||||
|
- It is now possible to open popup window relative to the current window
|
||||||
|
```vim
|
||||||
|
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true, 'yoffset': 1.0 } }
|
||||||
|
```
|
||||||
|
|
||||||
|
0.27.0
|
||||||
|
------
|
||||||
|
- More border options for `--preview-window`
|
||||||
|
```sh
|
||||||
|
fzf --preview 'cat {}' --preview-window border-left
|
||||||
|
fzf --preview 'cat {}' --preview-window border-left --border horizontal
|
||||||
|
fzf --preview 'cat {}' --preview-window top:border-bottom
|
||||||
|
fzf --preview 'cat {}' --preview-window top:border-horizontal
|
||||||
|
```
|
||||||
|
- Automatically set `/dev/tty` as STDIN on execute action
|
||||||
|
```sh
|
||||||
|
# Redirect /dev/tty to suppress "Vim: Warning: Input is not from a terminal"
|
||||||
|
# ls | fzf --bind "enter:execute(vim {} < /dev/tty)"
|
||||||
|
|
||||||
|
# "< /dev/tty" part is no longer needed
|
||||||
|
ls | fzf --bind "enter:execute(vim {})"
|
||||||
|
```
|
||||||
|
- Bug fixes and improvements
|
||||||
|
- Signed and notarized macOS binaries
|
||||||
|
(Huge thanks to [BACKERS.md](https://github.com/junegunn/junegunn/blob/main/BACKERS.md)!)
|
||||||
|
|
||||||
|
0.26.0
|
||||||
|
------
|
||||||
|
- Added support for fixed header in preview window
|
||||||
|
```sh
|
||||||
|
# Display top 3 lines as the fixed header
|
||||||
|
fzf --preview 'bat --style=header,grid --color=always {}' --preview-window '~3'
|
||||||
|
```
|
||||||
|
- More advanced preview offset expression to better support the fixed header
|
||||||
|
```sh
|
||||||
|
# Preview with bat, matching line in the middle of the window below
|
||||||
|
# the fixed header of the top 3 lines
|
||||||
|
#
|
||||||
|
# ~3 Top 3 lines as the fixed header
|
||||||
|
# +{2} Base scroll offset extracted from the second field
|
||||||
|
# +3 Extra offset to compensate for the 3-line header
|
||||||
|
# /2 Put in the middle of the preview area
|
||||||
|
#
|
||||||
|
git grep --line-number '' |
|
||||||
|
fzf --delimiter : \
|
||||||
|
--preview 'bat --style=full --color=always --highlight-line {2} {1}' \
|
||||||
|
--preview-window '~3:+{2}+3/2'
|
||||||
|
```
|
||||||
|
- Added `select` and `deselect` action for unconditionally selecting or
|
||||||
|
deselecting a single item in `--multi` mode. Complements `toggle` action.
|
||||||
|
- Significant performance improvement in ANSI code processing
|
||||||
|
- Bug fixes and improvements
|
||||||
|
- Built with Go 1.16
|
||||||
|
|
||||||
|
0.25.1
|
||||||
|
------
|
||||||
|
- Added `close` action
|
||||||
|
- Close preview window if open, abort fzf otherwise
|
||||||
|
- Bug fixes and improvements
|
||||||
|
|
||||||
0.25.0
|
0.25.0
|
||||||
------
|
------
|
||||||
- Text attributes set in `--color` are not reset when fzf sees another
|
- Text attributes set in `--color` are not reset when fzf sees another
|
||||||
@@ -1073,4 +1191,3 @@ add `--sync` option to re-enable buffering.
|
|||||||
### Improvements
|
### Improvements
|
||||||
- `--select-1` and `--exit-0` will start finder immediately when the condition
|
- `--select-1` and `--exit-0` will start finder immediately when the condition
|
||||||
cannot be met
|
cannot be met
|
||||||
|
|
||||||
|
21
Makefile
21
Makefile
@@ -27,12 +27,14 @@ $(error Not on git repository; cannot determine $$FZF_REVISION)
|
|||||||
endif
|
endif
|
||||||
BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision=$(REVISION)" -tags "$(TAGS)"
|
BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision=$(REVISION)" -tags "$(TAGS)"
|
||||||
|
|
||||||
|
BINARY32 := fzf-$(GOOS)_386
|
||||||
BINARY64 := fzf-$(GOOS)_amd64
|
BINARY64 := fzf-$(GOOS)_amd64
|
||||||
BINARYARM5 := fzf-$(GOOS)_arm5
|
BINARYARM5 := fzf-$(GOOS)_arm5
|
||||||
BINARYARM6 := fzf-$(GOOS)_arm6
|
BINARYARM6 := fzf-$(GOOS)_arm6
|
||||||
BINARYARM7 := fzf-$(GOOS)_arm7
|
BINARYARM7 := fzf-$(GOOS)_arm7
|
||||||
BINARYARM8 := fzf-$(GOOS)_arm8
|
BINARYARM8 := fzf-$(GOOS)_arm8
|
||||||
BINARYPPC64LE := fzf-$(GOOS)_ppc64le
|
BINARYPPC64LE := fzf-$(GOOS)_ppc64le
|
||||||
|
BINARYRISCV64 := fzf-$(GOOS)_riscv64
|
||||||
|
|
||||||
# https://en.wikipedia.org/wiki/Uname
|
# https://en.wikipedia.org/wiki/Uname
|
||||||
UNAME_M := $(shell uname -m)
|
UNAME_M := $(shell uname -m)
|
||||||
@@ -40,6 +42,10 @@ ifeq ($(UNAME_M),x86_64)
|
|||||||
BINARY := $(BINARY64)
|
BINARY := $(BINARY64)
|
||||||
else ifeq ($(UNAME_M),amd64)
|
else ifeq ($(UNAME_M),amd64)
|
||||||
BINARY := $(BINARY64)
|
BINARY := $(BINARY64)
|
||||||
|
else ifeq ($(UNAME_M),i686)
|
||||||
|
BINARY := $(BINARY32)
|
||||||
|
else ifeq ($(UNAME_M),i386)
|
||||||
|
BINARY := $(BINARY32)
|
||||||
else ifeq ($(UNAME_M),armv5l)
|
else ifeq ($(UNAME_M),armv5l)
|
||||||
BINARY := $(BINARYARM5)
|
BINARY := $(BINARYARM5)
|
||||||
else ifeq ($(UNAME_M),armv6l)
|
else ifeq ($(UNAME_M),armv6l)
|
||||||
@@ -54,6 +60,8 @@ else ifeq ($(UNAME_M),aarch64)
|
|||||||
BINARY := $(BINARYARM8)
|
BINARY := $(BINARYARM8)
|
||||||
else ifeq ($(UNAME_M),ppc64le)
|
else ifeq ($(UNAME_M),ppc64le)
|
||||||
BINARY := $(BINARYPPC64LE)
|
BINARY := $(BINARYPPC64LE)
|
||||||
|
else ifeq ($(UNAME_M),riscv64)
|
||||||
|
BINARY := $(BINARYRISCV64)
|
||||||
else
|
else
|
||||||
$(error Build on $(UNAME_M) is not supported, yet.)
|
$(error Build on $(UNAME_M) is not supported, yet.)
|
||||||
endif
|
endif
|
||||||
@@ -61,12 +69,16 @@ endif
|
|||||||
all: target/$(BINARY)
|
all: target/$(BINARY)
|
||||||
|
|
||||||
test: $(SOURCES)
|
test: $(SOURCES)
|
||||||
|
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
|
||||||
SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \
|
SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \
|
||||||
github.com/junegunn/fzf/src \
|
github.com/junegunn/fzf/src \
|
||||||
github.com/junegunn/fzf/src/algo \
|
github.com/junegunn/fzf/src/algo \
|
||||||
github.com/junegunn/fzf/src/tui \
|
github.com/junegunn/fzf/src/tui \
|
||||||
github.com/junegunn/fzf/src/util
|
github.com/junegunn/fzf/src/util
|
||||||
|
|
||||||
|
bench:
|
||||||
|
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
|
||||||
|
|
||||||
install: bin/fzf
|
install: bin/fzf
|
||||||
|
|
||||||
build:
|
build:
|
||||||
@@ -90,6 +102,7 @@ endif
|
|||||||
grep -qF $(VERSION) install.ps1
|
grep -qF $(VERSION) install.ps1
|
||||||
|
|
||||||
# Make release note out of CHANGELOG.md
|
# Make release note out of CHANGELOG.md
|
||||||
|
mkdir -p tmp
|
||||||
sed -n '/^$(VERSION_REGEX)$$/,/^[0-9]/p' CHANGELOG.md | tail -r | \
|
sed -n '/^$(VERSION_REGEX)$$/,/^[0-9]/p' CHANGELOG.md | tail -r | \
|
||||||
sed '1,/^ *$$/d' | tail -r | sed 1,2d | tee tmp/release-note
|
sed '1,/^ *$$/d' | tail -r | sed 1,2d | tee tmp/release-note
|
||||||
|
|
||||||
@@ -110,6 +123,9 @@ endif
|
|||||||
clean:
|
clean:
|
||||||
$(RM) -r dist target
|
$(RM) -r dist target
|
||||||
|
|
||||||
|
target/$(BINARY32): $(SOURCES)
|
||||||
|
GOARCH=386 $(GO) build $(BUILD_FLAGS) -o $@
|
||||||
|
|
||||||
target/$(BINARY64): $(SOURCES)
|
target/$(BINARY64): $(SOURCES)
|
||||||
GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@
|
GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@
|
||||||
|
|
||||||
@@ -129,6 +145,9 @@ target/$(BINARYARM8): $(SOURCES)
|
|||||||
target/$(BINARYPPC64LE): $(SOURCES)
|
target/$(BINARYPPC64LE): $(SOURCES)
|
||||||
GOARCH=ppc64le $(GO) build $(BUILD_FLAGS) -o $@
|
GOARCH=ppc64le $(GO) build $(BUILD_FLAGS) -o $@
|
||||||
|
|
||||||
|
target/$(BINARYRISCV64): $(SOURCES)
|
||||||
|
GOARCH=riscv64 $(GO) build $(BUILD_FLAGS) -o $@
|
||||||
|
|
||||||
bin/fzf: target/$(BINARY) | bin
|
bin/fzf: target/$(BINARY) | bin
|
||||||
cp -f target/$(BINARY) bin/fzf
|
cp -f target/$(BINARY) bin/fzf
|
||||||
|
|
||||||
@@ -144,4 +163,4 @@ update:
|
|||||||
$(GO) get -u
|
$(GO) get -u
|
||||||
$(GO) mod tidy
|
$(GO) mod tidy
|
||||||
|
|
||||||
.PHONY: all build release test install clean docker docker-test update
|
.PHONY: all build release test bench install clean docker docker-test update
|
||||||
|
@@ -127,9 +127,15 @@ let g:fzf_action = {
|
|||||||
\ 'ctrl-v': 'vsplit' }
|
\ 'ctrl-v': 'vsplit' }
|
||||||
|
|
||||||
" Default fzf layout
|
" Default fzf layout
|
||||||
" - Popup window
|
" - Popup window (center of the screen)
|
||||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||||
|
|
||||||
|
" - Popup window (center of the current window)
|
||||||
|
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true } }
|
||||||
|
|
||||||
|
" - Popup window (anchored to the bottom of the current window)
|
||||||
|
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true, 'yoffset': 1.0 } }
|
||||||
|
|
||||||
" - down / up / left / right
|
" - down / up / left / right
|
||||||
let g:fzf_layout = { 'down': '40%' }
|
let g:fzf_layout = { 'down': '40%' }
|
||||||
|
|
||||||
@@ -277,7 +283,7 @@ The following table summarizes the available options.
|
|||||||
| `source` | list | Vim list as input to fzf |
|
| `source` | list | Vim list as input to fzf |
|
||||||
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
|
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
|
||||||
| `sink` | funcref | Reference to function to process each selected item |
|
| `sink` | funcref | Reference to function to process each selected item |
|
||||||
| `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once |
|
| `sinklist` (or `sink*`) | funcref | Similar to `sink`, but takes the list of output lines at once |
|
||||||
| `options` | string/list | Options to fzf |
|
| `options` | string/list | Options to fzf |
|
||||||
| `dir` | string | Working directory |
|
| `dir` | string | Working directory |
|
||||||
| `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) |
|
| `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) |
|
||||||
@@ -302,6 +308,7 @@ following options are allowed:
|
|||||||
- Optional:
|
- Optional:
|
||||||
- `yoffset` [float default 0.5 range [0 ~ 1]]
|
- `yoffset` [float default 0.5 range [0 ~ 1]]
|
||||||
- `xoffset` [float default 0.5 range [0 ~ 1]]
|
- `xoffset` [float default 0.5 range [0 ~ 1]]
|
||||||
|
- `relative` [boolean default v:false]
|
||||||
- `border` [string default `rounded`]: Border style
|
- `border` [string default `rounded`]: Border style
|
||||||
- `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]`
|
- `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]`
|
||||||
|
|
||||||
@@ -362,7 +369,7 @@ Our `:LS` command will be much more useful if we can pass a directory argument
|
|||||||
to it, so that something like `:LS /tmp` is possible.
|
to it, so that something like `:LS /tmp` is possible.
|
||||||
|
|
||||||
```vim
|
```vim
|
||||||
command! -bang -complete=dir -nargs=* LS
|
command! -bang -complete=dir -nargs=? LS
|
||||||
\ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0))
|
\ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0))
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -372,7 +379,7 @@ a unique name to our command and pass it as the first argument to `fzf#wrap`.
|
|||||||
```vim
|
```vim
|
||||||
" The query history for this command will be stored as 'ls' inside g:fzf_history_dir.
|
" The query history for this command will be stored as 'ls' inside g:fzf_history_dir.
|
||||||
" The name is ignored if g:fzf_history_dir is not defined.
|
" The name is ignored if g:fzf_history_dir is not defined.
|
||||||
command! -bang -complete=dir -nargs=* LS
|
command! -bang -complete=dir -nargs=? LS
|
||||||
\ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': <q-args>}, <bang>0))
|
\ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': <q-args>}, <bang>0))
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -380,7 +387,7 @@ command! -bang -complete=dir -nargs=* LS
|
|||||||
|
|
||||||
- `g:fzf_layout`
|
- `g:fzf_layout`
|
||||||
- `g:fzf_action`
|
- `g:fzf_action`
|
||||||
- **Works only when no custom `sink` (or `sink*`) is provided**
|
- **Works only when no custom `sink` (or `sinklist`) is provided**
|
||||||
- Having custom sink usually means that each entry is not an ordinary
|
- Having custom sink usually means that each entry is not an ordinary
|
||||||
file path (e.g. name of color scheme), so we can't blindly apply the
|
file path (e.g. name of color scheme), so we can't blindly apply the
|
||||||
same strategy (i.e. `tabedit some-color-scheme` doesn't make sense)
|
same strategy (i.e. `tabedit some-color-scheme` doesn't make sense)
|
||||||
@@ -392,15 +399,41 @@ Tips
|
|||||||
|
|
||||||
### fzf inside terminal buffer
|
### fzf inside terminal buffer
|
||||||
|
|
||||||
The latest versions of Vim and Neovim include builtin terminal emulator
|
On the latest versions of Vim and Neovim, fzf will start in a terminal buffer.
|
||||||
(`:terminal`) and fzf will start in a terminal buffer in the following cases:
|
If you find the default ANSI colors to be different, consider configuring the
|
||||||
|
colors using `g:terminal_ansi_colors` in regular Vim or `g:terminal_color_x`
|
||||||
|
in Neovim.
|
||||||
|
|
||||||
- On Neovim
|
```vim
|
||||||
- On GVim
|
" Terminal colors for seoul256 color scheme
|
||||||
- On Terminal Vim with a non-default layout
|
if has('nvim')
|
||||||
- `call fzf#run({'left': '30%'})` or `let g:fzf_layout = {'left': '30%'}`
|
let g:terminal_color_0 = '#4e4e4e'
|
||||||
|
let g:terminal_color_1 = '#d68787'
|
||||||
|
let g:terminal_color_2 = '#5f865f'
|
||||||
|
let g:terminal_color_3 = '#d8af5f'
|
||||||
|
let g:terminal_color_4 = '#85add4'
|
||||||
|
let g:terminal_color_5 = '#d7afaf'
|
||||||
|
let g:terminal_color_6 = '#87afaf'
|
||||||
|
let g:terminal_color_7 = '#d0d0d0'
|
||||||
|
let g:terminal_color_8 = '#626262'
|
||||||
|
let g:terminal_color_9 = '#d75f87'
|
||||||
|
let g:terminal_color_10 = '#87af87'
|
||||||
|
let g:terminal_color_11 = '#ffd787'
|
||||||
|
let g:terminal_color_12 = '#add4fb'
|
||||||
|
let g:terminal_color_13 = '#ffafaf'
|
||||||
|
let g:terminal_color_14 = '#87d7d7'
|
||||||
|
let g:terminal_color_15 = '#e4e4e4'
|
||||||
|
else
|
||||||
|
let g:terminal_ansi_colors = [
|
||||||
|
\ '#4e4e4e', '#d68787', '#5f865f', '#d8af5f',
|
||||||
|
\ '#85add4', '#d7afaf', '#87afaf', '#d0d0d0',
|
||||||
|
\ '#626262', '#d75f87', '#87af87', '#ffd787',
|
||||||
|
\ '#add4fb', '#ffafaf', '#87d7d7', '#e4e4e4'
|
||||||
|
\ ]
|
||||||
|
endif
|
||||||
|
```
|
||||||
|
|
||||||
#### Starting fzf in a popup window
|
### Starting fzf in a popup window
|
||||||
|
|
||||||
```vim
|
```vim
|
||||||
" Required:
|
" Required:
|
||||||
@@ -410,6 +443,7 @@ The latest versions of Vim and Neovim include builtin terminal emulator
|
|||||||
" Optional:
|
" Optional:
|
||||||
" - xoffset [float default 0.5 range [0 ~ 1]]
|
" - xoffset [float default 0.5 range [0 ~ 1]]
|
||||||
" - yoffset [float default 0.5 range [0 ~ 1]]
|
" - yoffset [float default 0.5 range [0 ~ 1]]
|
||||||
|
" - relative [boolean default v:false]
|
||||||
" - border [string default 'rounded']: Border style
|
" - border [string default 'rounded']: Border style
|
||||||
" - 'rounded' / 'sharp' / 'horizontal' / 'vertical' / 'top' / 'bottom' / 'left' / 'right'
|
" - 'rounded' / 'sharp' / 'horizontal' / 'vertical' / 'top' / 'bottom' / 'left' / 'right'
|
||||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||||
@@ -427,21 +461,21 @@ else
|
|||||||
endif
|
endif
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Hide statusline
|
### Hide statusline
|
||||||
|
|
||||||
When fzf starts in a terminal buffer, the file type of the buffer is set to
|
When fzf starts in a terminal buffer, the file type of the buffer is set to
|
||||||
`fzf`. So you can set up `FileType fzf` autocmd to customize the settings of
|
`fzf`. So you can set up `FileType fzf` autocmd to customize the settings of
|
||||||
the window.
|
the window.
|
||||||
|
|
||||||
For example, if you use a non-popup layout (e.g. `{'down': '40%'}`) on Neovim,
|
For example, if you open fzf on the bottom on the screen (e.g. `{'down':
|
||||||
you might want to temporarily disable the statusline for a cleaner look.
|
'40%'}`), you might want to temporarily disable the statusline for a cleaner
|
||||||
|
look.
|
||||||
|
|
||||||
```vim
|
```vim
|
||||||
if has('nvim') && !exists('g:fzf_layout')
|
let g:fzf_layout = { 'down': '30%' }
|
||||||
autocmd! FileType fzf
|
autocmd! FileType fzf
|
||||||
autocmd FileType fzf set laststatus=0 noshowmode noruler
|
autocmd FileType fzf set laststatus=0 noshowmode noruler
|
||||||
\| autocmd BufLeave <buffer> set laststatus=2 showmode ruler
|
\| autocmd BufLeave <buffer> set laststatus=2 showmode ruler
|
||||||
endif
|
|
||||||
```
|
```
|
||||||
|
|
||||||
[License](LICENSE)
|
[License](LICENSE)
|
||||||
|
69
README.md
69
README.md
@@ -1,4 +1,4 @@
|
|||||||
<img src="https://raw.githubusercontent.com/junegunn/i/master/fzf.png" height="170" alt="fzf - a command-line fuzzy finder"> [](https://travis-ci.org/junegunn/fzf)
|
<img src="https://raw.githubusercontent.com/junegunn/i/master/fzf.png" height="170" alt="fzf - a command-line fuzzy finder"> [](https://github.com/junegunn/fzf/actions)
|
||||||
===
|
===
|
||||||
|
|
||||||
fzf is a general-purpose command-line fuzzy finder.
|
fzf is a general-purpose command-line fuzzy finder.
|
||||||
@@ -17,7 +17,7 @@ Pros
|
|||||||
- The most comprehensive feature set
|
- The most comprehensive feature set
|
||||||
- Flexible layout
|
- Flexible layout
|
||||||
- Batteries included
|
- Batteries included
|
||||||
- Vim/Neovim plugin, key bindings and fuzzy auto-completion
|
- Vim/Neovim plugin, key bindings, and fuzzy auto-completion
|
||||||
|
|
||||||
Table of Contents
|
Table of Contents
|
||||||
-----------------
|
-----------------
|
||||||
@@ -25,7 +25,7 @@ Table of Contents
|
|||||||
<!-- vim-markdown-toc GFM -->
|
<!-- vim-markdown-toc GFM -->
|
||||||
|
|
||||||
* [Installation](#installation)
|
* [Installation](#installation)
|
||||||
* [Using Homebrew or Linuxbrew](#using-homebrew-or-linuxbrew)
|
* [Using Homebrew](#using-homebrew)
|
||||||
* [Using git](#using-git)
|
* [Using git](#using-git)
|
||||||
* [Using Linux package managers](#using-linux-package-managers)
|
* [Using Linux package managers](#using-linux-package-managers)
|
||||||
* [Windows](#windows)
|
* [Windows](#windows)
|
||||||
@@ -84,9 +84,9 @@ stuff.
|
|||||||
|
|
||||||
[bin]: https://github.com/junegunn/fzf/releases
|
[bin]: https://github.com/junegunn/fzf/releases
|
||||||
|
|
||||||
### Using Homebrew or Linuxbrew
|
### Using Homebrew
|
||||||
|
|
||||||
You can use [Homebrew](http://brew.sh/) or [Linuxbrew](http://linuxbrew.sh/)
|
You can use [Homebrew](http://brew.sh/) (on macOS or Linux)
|
||||||
to install fzf.
|
to install fzf.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -121,6 +121,7 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
|
|||||||
| Nix | NixOS, etc. | `nix-env -iA nixpkgs.fzf` |
|
| Nix | NixOS, etc. | `nix-env -iA nixpkgs.fzf` |
|
||||||
| Pacman | Arch Linux | `sudo pacman -S fzf` |
|
| Pacman | Arch Linux | `sudo pacman -S fzf` |
|
||||||
| pkg | FreeBSD | `pkg install fzf` |
|
| pkg | FreeBSD | `pkg install fzf` |
|
||||||
|
| pkgin | NetBSD | `pkgin install fzf` |
|
||||||
| pkg_add | OpenBSD | `pkg_add fzf` |
|
| pkg_add | OpenBSD | `pkg_add fzf` |
|
||||||
| XBPS | Void Linux | `sudo xbps-install -S fzf` |
|
| XBPS | Void Linux | `sudo xbps-install -S fzf` |
|
||||||
| Zypper | openSUSE | `sudo zypper install fzf` |
|
| Zypper | openSUSE | `sudo zypper install fzf` |
|
||||||
@@ -166,12 +167,12 @@ For more installation options, see [README-VIM.md](README-VIM.md).
|
|||||||
Upgrading fzf
|
Upgrading fzf
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
fzf is being actively developed and you might want to upgrade it once in a
|
fzf is being actively developed, and you might want to upgrade it once in a
|
||||||
while. Please follow the instruction below depending on the installation
|
while. Please follow the instruction below depending on the installation
|
||||||
method used.
|
method used.
|
||||||
|
|
||||||
- git: `cd ~/.fzf && git pull && ./install`
|
- git: `cd ~/.fzf && git pull && ./install`
|
||||||
- brew: `brew update; brew reinstall fzf`
|
- brew: `brew update; brew upgrade fzf`
|
||||||
- macports: `sudo port upgrade fzf`
|
- macports: `sudo port upgrade fzf`
|
||||||
- chocolatey: `choco upgrade fzf`
|
- chocolatey: `choco upgrade fzf`
|
||||||
- vim-plug: `:PlugUpdate fzf`
|
- vim-plug: `:PlugUpdate fzf`
|
||||||
@@ -201,7 +202,7 @@ vim $(fzf)
|
|||||||
|
|
||||||
#### Using the finder
|
#### Using the finder
|
||||||
|
|
||||||
- `CTRL-J` / `CTRL-K` (or `CTRL-N` / `CTRL-P`) to move cursor up and down
|
- `CTRL-K` / `CTRL-J` (or `CTRL-P` / `CTRL-N`) to move cursor up and down
|
||||||
- `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit
|
- `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit
|
||||||
- On multi-select mode (`-m`), `TAB` and `Shift-TAB` to mark multiple items
|
- On multi-select mode (`-m`), `TAB` and `Shift-TAB` to mark multiple items
|
||||||
- Emacs style key bindings
|
- Emacs style key bindings
|
||||||
@@ -217,7 +218,7 @@ cursor with `--height` option.
|
|||||||
vim $(fzf --height 40%)
|
vim $(fzf --height 40%)
|
||||||
```
|
```
|
||||||
|
|
||||||
Also check out `--reverse` and `--layout` options if you prefer
|
Also, check out `--reverse` and `--layout` options if you prefer
|
||||||
"top-down" layout instead of the default "bottom-up" layout.
|
"top-down" layout instead of the default "bottom-up" layout.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -264,6 +265,13 @@ or `py`.
|
|||||||
- `FZF_DEFAULT_COMMAND`
|
- `FZF_DEFAULT_COMMAND`
|
||||||
- Default command to use when input is tty
|
- Default command to use when input is tty
|
||||||
- e.g. `export FZF_DEFAULT_COMMAND='fd --type f'`
|
- e.g. `export FZF_DEFAULT_COMMAND='fd --type f'`
|
||||||
|
- > :warning: This variable is not used by shell extensions due to the
|
||||||
|
> slight difference in requirements.
|
||||||
|
>
|
||||||
|
> (e.g. `CTRL-T` runs `$FZF_CTRL_T_COMMAND` instead, `vim **<tab>` runs
|
||||||
|
> `_fzf_compgen_path()`, and `cd **<tab>` runs `_fzf_compgen_dir()`)
|
||||||
|
>
|
||||||
|
> The available options are described later in this document.
|
||||||
- `FZF_DEFAULT_OPTS`
|
- `FZF_DEFAULT_OPTS`
|
||||||
- Default options
|
- Default options
|
||||||
- e.g. `export FZF_DEFAULT_OPTS="--layout=reverse --inline-info"`
|
- e.g. `export FZF_DEFAULT_OPTS="--layout=reverse --inline-info"`
|
||||||
@@ -282,9 +290,10 @@ If you learn by watching videos, check out this screencast by [@samoshkin](https
|
|||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
|
||||||
Many useful examples can be found on [the wiki
|
* [Wiki page of examples](https://github.com/junegunn/fzf/wiki/examples)
|
||||||
page](https://github.com/junegunn/fzf/wiki/examples). Feel free to add your
|
* *Disclaimer: The examples on this page are maintained by the community
|
||||||
own as well.
|
and are not thoroughly tested*
|
||||||
|
* [Advanced fzf examples](https://github.com/junegunn/fzf/blob/master/ADVANCED.md)
|
||||||
|
|
||||||
`fzf-tmux` script
|
`fzf-tmux` script
|
||||||
-----------------
|
-----------------
|
||||||
@@ -331,7 +340,7 @@ fish.
|
|||||||
- Set `FZF_ALT_C_COMMAND` to override the default command
|
- Set `FZF_ALT_C_COMMAND` to override the default command
|
||||||
- Set `FZF_ALT_C_OPTS` to pass additional options
|
- Set `FZF_ALT_C_OPTS` to pass additional options
|
||||||
|
|
||||||
If you're on a tmux session, you can start fzf in a tmux split pane or in
|
If you're on a tmux session, you can start fzf in a tmux split-pane or in
|
||||||
a tmux popup window by setting `FZF_TMUX_OPTS` (e.g. `-d 40%`).
|
a tmux popup window by setting `FZF_TMUX_OPTS` (e.g. `-d 40%`).
|
||||||
See `fzf-tmux --help` for available options.
|
See `fzf-tmux --help` for available options.
|
||||||
|
|
||||||
@@ -343,12 +352,12 @@ Fuzzy completion for bash and zsh
|
|||||||
#### Files and directories
|
#### Files and directories
|
||||||
|
|
||||||
Fuzzy completion for files and directories can be triggered if the word before
|
Fuzzy completion for files and directories can be triggered if the word before
|
||||||
the cursor ends with the trigger sequence which is by default `**`.
|
the cursor ends with the trigger sequence, which is by default `**`.
|
||||||
|
|
||||||
- `COMMAND [DIRECTORY/][FUZZY_PATTERN]**<TAB>`
|
- `COMMAND [DIRECTORY/][FUZZY_PATTERN]**<TAB>`
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Files under current directory
|
# Files under the current directory
|
||||||
# - You can select multiple items with TAB key
|
# - You can select multiple items with TAB key
|
||||||
vim **<TAB>
|
vim **<TAB>
|
||||||
|
|
||||||
@@ -372,7 +381,7 @@ cd ~/github/fzf**<TAB>
|
|||||||
#### Process IDs
|
#### Process IDs
|
||||||
|
|
||||||
Fuzzy completion for PIDs is provided for kill command. In this case,
|
Fuzzy completion for PIDs is provided for kill command. In this case,
|
||||||
there is no trigger sequence, just press tab key after kill command.
|
there is no trigger sequence; just press the tab key after the kill command.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Can select multiple processes with <TAB> or <Shift-TAB> keys
|
# Can select multiple processes with <TAB> or <Shift-TAB> keys
|
||||||
@@ -381,7 +390,7 @@ kill -9 <TAB>
|
|||||||
|
|
||||||
#### Host names
|
#### Host names
|
||||||
|
|
||||||
For ssh and telnet commands, fuzzy completion for host names is provided. The
|
For ssh and telnet commands, fuzzy completion for hostnames is provided. The
|
||||||
names are extracted from /etc/hosts and ~/.ssh/config.
|
names are extracted from /etc/hosts and ~/.ssh/config.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -404,7 +413,7 @@ unalias **<TAB>
|
|||||||
export FZF_COMPLETION_TRIGGER='~~'
|
export FZF_COMPLETION_TRIGGER='~~'
|
||||||
|
|
||||||
# Options to fzf command
|
# Options to fzf command
|
||||||
export FZF_COMPLETION_OPTS='+c -x'
|
export FZF_COMPLETION_OPTS='--border --info=inline'
|
||||||
|
|
||||||
# Use fd (https://github.com/sharkdp/fd) instead of the default find
|
# Use fd (https://github.com/sharkdp/fd) instead of the default find
|
||||||
# command for listing path candidates.
|
# command for listing path candidates.
|
||||||
@@ -469,11 +478,11 @@ _fzf_complete_doge() {
|
|||||||
|
|
||||||
- The arguments before `--` are the options to fzf.
|
- The arguments before `--` are the options to fzf.
|
||||||
- After `--`, simply pass the original completion arguments unchanged (`"$@"`).
|
- After `--`, simply pass the original completion arguments unchanged (`"$@"`).
|
||||||
- Then write a set of commands that generates the completion candidates and
|
- Then, write a set of commands that generates the completion candidates and
|
||||||
feed its output to the function using process substitution (`< <(...)`).
|
feed its output to the function using process substitution (`< <(...)`).
|
||||||
|
|
||||||
zsh will automatically pick up the function using the naming convention but in
|
zsh will automatically pick up the function using the naming convention but in
|
||||||
bash you have to manually associate the function with the command using
|
bash you have to manually associate the function with the command using the
|
||||||
`complete` command.
|
`complete` command.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -509,12 +518,12 @@ Advanced topics
|
|||||||
|
|
||||||
fzf is fast and is [getting even faster][perf]. Performance should not be
|
fzf is fast and is [getting even faster][perf]. Performance should not be
|
||||||
a problem in most use cases. However, you might want to be aware of the
|
a problem in most use cases. However, you might want to be aware of the
|
||||||
options that affect the performance.
|
options that affect performance.
|
||||||
|
|
||||||
- `--ansi` tells fzf to extract and parse ANSI color codes in the input and it
|
- `--ansi` tells fzf to extract and parse ANSI color codes in the input, and it
|
||||||
makes the initial scanning slower. So it's not recommended that you add it
|
makes the initial scanning slower. So it's not recommended that you add it
|
||||||
to your `$FZF_DEFAULT_OPTS`.
|
to your `$FZF_DEFAULT_OPTS`.
|
||||||
- `--nth` makes fzf slower as fzf has to tokenize each line.
|
- `--nth` makes fzf slower because it has to tokenize each line.
|
||||||
- `--with-nth` makes fzf slower as fzf has to tokenize and reassemble each
|
- `--with-nth` makes fzf slower as fzf has to tokenize and reassemble each
|
||||||
line.
|
line.
|
||||||
- If you absolutely need better performance, you can consider using
|
- If you absolutely need better performance, you can consider using
|
||||||
@@ -563,17 +572,17 @@ FZF_DEFAULT_COMMAND='find . -type f' \
|
|||||||
#### 3. Interactive ripgrep integration
|
#### 3. Interactive ripgrep integration
|
||||||
|
|
||||||
The following example uses fzf as the selector interface for ripgrep. We bound
|
The following example uses fzf as the selector interface for ripgrep. We bound
|
||||||
`reload` action to `change` event, so every time you type on fzf, ripgrep
|
`reload` action to `change` event, so every time you type on fzf, the ripgrep
|
||||||
process will restart with the updated query string denoted by the placeholder
|
process will restart with the updated query string denoted by the placeholder
|
||||||
expression `{q}`. Also, note that we used `--phony` option so that fzf doesn't
|
expression `{q}`. Also, note that we used `--disabled` option so that fzf
|
||||||
perform any secondary filtering.
|
doesn't perform any secondary filtering.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
INITIAL_QUERY=""
|
INITIAL_QUERY=""
|
||||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||||
FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \
|
FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \
|
||||||
fzf --bind "change:reload:$RG_PREFIX {q} || true" \
|
fzf --bind "change:reload:$RG_PREFIX {q} || true" \
|
||||||
--ansi --phony --query "$INITIAL_QUERY" \
|
--ansi --disabled --query "$INITIAL_QUERY" \
|
||||||
--height=50% --layout=reverse
|
--height=50% --layout=reverse
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -589,7 +598,7 @@ Your `$SHELL` is used to execute the command with `$SHELL -c COMMAND`.
|
|||||||
The window can be scrolled using the mouse or custom key bindings.
|
The window can be scrolled using the mouse or custom key bindings.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# {} is replaced to the single-quoted string of the focused line
|
# {} is replaced with the single-quoted string of the focused line
|
||||||
fzf --preview 'cat {}'
|
fzf --preview 'cat {}'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -608,7 +617,7 @@ You can customize the size, position, and border of the preview window using
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
fzf --height 40% --layout reverse --info inline --border \
|
fzf --height 40% --layout reverse --info inline --border \
|
||||||
--preview 'file {}' --preview-window down:1:noborder \
|
--preview 'file {}' --preview-window up,1,border-horizontal \
|
||||||
--color 'fg:#bbccdd,fg+:#ddeeff,bg:#334455,preview-bg:#223344,border:#778899'
|
--color 'fg:#bbccdd,fg+:#ddeeff,bg:#334455,preview-bg:#223344,border:#778899'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -661,7 +670,7 @@ fzf
|
|||||||
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
|
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want the command to follow symbolic links, and don't want it to exclude
|
If you want the command to follow symbolic links and don't want it to exclude
|
||||||
hidden files, use the following command:
|
hidden files, use the following command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
13
bin/fzf-tmux
13
bin/fzf-tmux
@@ -172,7 +172,7 @@ cleanup() {
|
|||||||
resize-pane -Z
|
resize-pane -Z
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ $# -gt 0 ]; then
|
if [[ $# -gt 0 ]]; then
|
||||||
trap - EXIT
|
trap - EXIT
|
||||||
exit 130
|
exit 130
|
||||||
fi
|
fi
|
||||||
@@ -204,7 +204,16 @@ if [[ "$opt" =~ "-K -E" ]]; then
|
|||||||
cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; out=\$? $close; exit \$out" >> $argsf
|
cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; out=\$? $close; exit \$out" >> $argsf
|
||||||
cat <&0 > $fifo1 &
|
cat <&0 > $fifo1 &
|
||||||
fi
|
fi
|
||||||
tmux popup -d "$PWD" "${tmux_args[@]}" $opt -R "bash $argsf" > /dev/null 2>&1
|
|
||||||
|
# tmux dropped the support for `-K`, `-R` to popup command
|
||||||
|
# TODO: We can remove this once tmux 3.2 is released
|
||||||
|
if [[ ! "$(tmux popup --help 2>&1)" =~ '-R shell-command' ]]; then
|
||||||
|
opt="${opt/-K/}"
|
||||||
|
else
|
||||||
|
opt="${opt} -R"
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmux popup -d "$PWD" "${tmux_args[@]}" $opt "bash $argsf" > /dev/null 2>&1
|
||||||
exit $?
|
exit $?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
68
doc/fzf.txt
68
doc/fzf.txt
@@ -1,4 +1,4 @@
|
|||||||
fzf.txt fzf Last change: January 3 2021
|
fzf.txt fzf Last change: May 19 2021
|
||||||
FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
|
FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
|
||||||
==============================================================================
|
==============================================================================
|
||||||
|
|
||||||
@@ -155,9 +155,15 @@ Examples~
|
|||||||
\ 'ctrl-v': 'vsplit' }
|
\ 'ctrl-v': 'vsplit' }
|
||||||
|
|
||||||
" Default fzf layout
|
" Default fzf layout
|
||||||
" - Popup window
|
" - Popup window (center of the screen)
|
||||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||||
|
|
||||||
|
" - Popup window (center of the current window)
|
||||||
|
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true } }
|
||||||
|
|
||||||
|
" - Popup window (anchored to the bottom of the current window)
|
||||||
|
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true, 'yoffset': 1.0 } }
|
||||||
|
|
||||||
" - down / up / left / right
|
" - down / up / left / right
|
||||||
let g:fzf_layout = { 'down': '40%' }
|
let g:fzf_layout = { 'down': '40%' }
|
||||||
|
|
||||||
@@ -294,7 +300,7 @@ The following table summarizes the available options.
|
|||||||
`source` | list | Vim list as input to fzf
|
`source` | list | Vim list as input to fzf
|
||||||
`sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` )
|
`sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` )
|
||||||
`sink` | funcref | Reference to function to process each selected item
|
`sink` | funcref | Reference to function to process each selected item
|
||||||
`sink*` | funcref | Similar to `sink` , but takes the list of output lines at once
|
`sinklist` (or `sink*` ) | funcref | Similar to `sink` , but takes the list of output lines at once
|
||||||
`options` | string/list | Options to fzf
|
`options` | string/list | Options to fzf
|
||||||
`dir` | string | Working directory
|
`dir` | string | Working directory
|
||||||
`up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` )
|
`up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` )
|
||||||
@@ -318,6 +324,7 @@ following options are allowed:
|
|||||||
- Optional:
|
- Optional:
|
||||||
- `yoffset` [float default 0.5 range [0 ~ 1]]
|
- `yoffset` [float default 0.5 range [0 ~ 1]]
|
||||||
- `xoffset` [float default 0.5 range [0 ~ 1]]
|
- `xoffset` [float default 0.5 range [0 ~ 1]]
|
||||||
|
- `relative` [boolean default v:false]
|
||||||
- `border` [string default `rounded`]: Border style
|
- `border` [string default `rounded`]: Border style
|
||||||
- `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]`
|
- `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]`
|
||||||
|
|
||||||
@@ -372,7 +379,7 @@ last `fullscreen` argument of `fzf#wrap` (see :help <bang>).
|
|||||||
Our `:LS` command will be much more useful if we can pass a directory argument
|
Our `:LS` command will be much more useful if we can pass a directory argument
|
||||||
to it, so that something like `:LS /tmp` is possible.
|
to it, so that something like `:LS /tmp` is possible.
|
||||||
>
|
>
|
||||||
command! -bang -complete=dir -nargs=* LS
|
command! -bang -complete=dir -nargs=? LS
|
||||||
\ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0))
|
\ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0))
|
||||||
<
|
<
|
||||||
Lastly, if you have enabled `g:fzf_history_dir`, you might want to assign a
|
Lastly, if you have enabled `g:fzf_history_dir`, you might want to assign a
|
||||||
@@ -380,7 +387,7 @@ unique name to our command and pass it as the first argument to `fzf#wrap`.
|
|||||||
>
|
>
|
||||||
" The query history for this command will be stored as 'ls' inside g:fzf_history_dir.
|
" The query history for this command will be stored as 'ls' inside g:fzf_history_dir.
|
||||||
" The name is ignored if g:fzf_history_dir is not defined.
|
" The name is ignored if g:fzf_history_dir is not defined.
|
||||||
command! -bang -complete=dir -nargs=* LS
|
command! -bang -complete=dir -nargs=? LS
|
||||||
\ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': <q-args>}, <bang>0))
|
\ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': <q-args>}, <bang>0))
|
||||||
<
|
<
|
||||||
|
|
||||||
@@ -412,8 +419,46 @@ The latest versions of Vim and Neovim include builtin terminal emulator
|
|||||||
- On Terminal Vim with a non-default layout
|
- On Terminal Vim with a non-default layout
|
||||||
- `call fzf#run({'left': '30%'})` or `let g:fzf_layout = {'left': '30%'}`
|
- `call fzf#run({'left': '30%'})` or `let g:fzf_layout = {'left': '30%'}`
|
||||||
|
|
||||||
|
On the latest versions of Vim and Neovim, fzf will start in a terminal buffer.
|
||||||
|
If you find the default ANSI colors to be different, consider configuring the
|
||||||
|
colors using `g:terminal_ansi_colors` in regular Vim or `g:terminal_color_x`
|
||||||
|
in Neovim.
|
||||||
|
|
||||||
Starting fzf in a popup window~
|
*g:terminal_color_15* *g:terminal_color_14* *g:terminal_color_13*
|
||||||
|
*g:terminal_color_12* *g:terminal_color_11* *g:terminal_color_10* *g:terminal_color_9*
|
||||||
|
*g:terminal_color_8* *g:terminal_color_7* *g:terminal_color_6* *g:terminal_color_5*
|
||||||
|
*g:terminal_color_4* *g:terminal_color_3* *g:terminal_color_2* *g:terminal_color_1*
|
||||||
|
*g:terminal_color_0*
|
||||||
|
>
|
||||||
|
" Terminal colors for seoul256 color scheme
|
||||||
|
if has('nvim')
|
||||||
|
let g:terminal_color_0 = '#4e4e4e'
|
||||||
|
let g:terminal_color_1 = '#d68787'
|
||||||
|
let g:terminal_color_2 = '#5f865f'
|
||||||
|
let g:terminal_color_3 = '#d8af5f'
|
||||||
|
let g:terminal_color_4 = '#85add4'
|
||||||
|
let g:terminal_color_5 = '#d7afaf'
|
||||||
|
let g:terminal_color_6 = '#87afaf'
|
||||||
|
let g:terminal_color_7 = '#d0d0d0'
|
||||||
|
let g:terminal_color_8 = '#626262'
|
||||||
|
let g:terminal_color_9 = '#d75f87'
|
||||||
|
let g:terminal_color_10 = '#87af87'
|
||||||
|
let g:terminal_color_11 = '#ffd787'
|
||||||
|
let g:terminal_color_12 = '#add4fb'
|
||||||
|
let g:terminal_color_13 = '#ffafaf'
|
||||||
|
let g:terminal_color_14 = '#87d7d7'
|
||||||
|
let g:terminal_color_15 = '#e4e4e4'
|
||||||
|
else
|
||||||
|
let g:terminal_ansi_colors = [
|
||||||
|
\ '#4e4e4e', '#d68787', '#5f865f', '#d8af5f',
|
||||||
|
\ '#85add4', '#d7afaf', '#87afaf', '#d0d0d0',
|
||||||
|
\ '#626262', '#d75f87', '#87af87', '#ffd787',
|
||||||
|
\ '#add4fb', '#ffafaf', '#87d7d7', '#e4e4e4'
|
||||||
|
\ ]
|
||||||
|
endif
|
||||||
|
<
|
||||||
|
|
||||||
|
< Starting fzf in a popup window >____________________________________________~
|
||||||
*fzf-starting-fzf-in-a-popup-window*
|
*fzf-starting-fzf-in-a-popup-window*
|
||||||
>
|
>
|
||||||
" Required:
|
" Required:
|
||||||
@@ -423,6 +468,7 @@ Starting fzf in a popup window~
|
|||||||
" Optional:
|
" Optional:
|
||||||
" - xoffset [float default 0.5 range [0 ~ 1]]
|
" - xoffset [float default 0.5 range [0 ~ 1]]
|
||||||
" - yoffset [float default 0.5 range [0 ~ 1]]
|
" - yoffset [float default 0.5 range [0 ~ 1]]
|
||||||
|
" - relative [boolean default v:false]
|
||||||
" - border [string default 'rounded']: Border style
|
" - border [string default 'rounded']: Border style
|
||||||
" - 'rounded' / 'sharp' / 'horizontal' / 'vertical' / 'top' / 'bottom' / 'left' / 'right'
|
" - 'rounded' / 'sharp' / 'horizontal' / 'vertical' / 'top' / 'bottom' / 'left' / 'right'
|
||||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||||
@@ -438,21 +484,21 @@ or above) by putting fzf-tmux options in `tmux` key.
|
|||||||
endif
|
endif
|
||||||
<
|
<
|
||||||
|
|
||||||
Hide statusline~
|
< Hide statusline >___________________________________________________________~
|
||||||
*fzf-hide-statusline*
|
*fzf-hide-statusline*
|
||||||
|
|
||||||
When fzf starts in a terminal buffer, the file type of the buffer is set to
|
When fzf starts in a terminal buffer, the file type of the buffer is set to
|
||||||
`fzf`. So you can set up `FileType fzf` autocmd to customize the settings of
|
`fzf`. So you can set up `FileType fzf` autocmd to customize the settings of
|
||||||
the window.
|
the window.
|
||||||
|
|
||||||
For example, if you use a non-popup layout (e.g. `{'down': '40%'}`) on Neovim,
|
For example, if you open fzf on the bottom on the screen (e.g. `{'down':
|
||||||
you might want to temporarily disable the statusline for a cleaner look.
|
'40%'}`), you might want to temporarily disable the statusline for a cleaner
|
||||||
|
look.
|
||||||
>
|
>
|
||||||
if has('nvim') && !exists('g:fzf_layout')
|
let g:fzf_layout = { 'down': '30%' }
|
||||||
autocmd! FileType fzf
|
autocmd! FileType fzf
|
||||||
autocmd FileType fzf set laststatus=0 noshowmode noruler
|
autocmd FileType fzf set laststatus=0 noshowmode noruler
|
||||||
\| autocmd BufLeave <buffer> set laststatus=2 showmode ruler
|
\| autocmd BufLeave <buffer> set laststatus=2 showmode ruler
|
||||||
endif
|
|
||||||
<
|
<
|
||||||
|
|
||||||
LICENSE *fzf-license*
|
LICENSE *fzf-license*
|
||||||
|
18
go.mod
18
go.mod
@@ -2,14 +2,16 @@ module github.com/junegunn/fzf
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gdamore/tcell v1.4.0
|
github.com/gdamore/tcell v1.4.0
|
||||||
github.com/mattn/go-isatty v0.0.12
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.9
|
github.com/mattn/go-isatty v0.0.14
|
||||||
github.com/mattn/go-shellwords v1.0.10
|
github.com/mattn/go-runewidth v0.0.13
|
||||||
github.com/saracen/walker v0.1.1
|
github.com/mattn/go-shellwords v1.0.12
|
||||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
|
github.com/rivo/uniseg v0.2.0
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 // indirect
|
github.com/saracen/walker v0.1.2
|
||||||
golang.org/x/sys v0.0.0-20201026173827-119d4633e4d1
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||||
golang.org/x/text v0.3.3 // indirect
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
|
||||||
|
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6
|
||||||
|
golang.org/x/text v0.3.6 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
go 1.13
|
go 1.13
|
||||||
|
46
go.sum
46
go.sum
@@ -2,34 +2,30 @@ github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdk
|
|||||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||||
github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=
|
github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=
|
||||||
github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0=
|
github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0=
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw=
|
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||||
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||||
github.com/saracen/walker v0.1.1 h1:Ou2QIKTWqo0QxhtuHVmtObbmhjMCEUyJ82xp0uV+MGI=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/saracen/walker v0.1.1/go.mod h1:0oKYMsKVhSJ+ful4p/XbjvXbMgLEkLITZaxozsl4CGE=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
github.com/saracen/walker v0.1.2 h1:/o1TxP82n8thLvmL4GpJXduYaRmJ7qXp8u9dSlV0zmo=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
github.com/saracen/walker v0.1.2/go.mod h1:0oKYMsKVhSJ+ful4p/XbjvXbMgLEkLITZaxozsl4CGE=
|
||||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
|
|
||||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
|
|
||||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201026173827-119d4633e4d1 h1:/DtoiOYKoQCcIFXQjz07RnWNPRCbqmSXSpgEzhC9ZHM=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
||||||
golang.org/x/sys v0.0.0-20201026173827-119d4633e4d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
|
||||||
|
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
5
install
5
install
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
version=0.25.0
|
version=0.28.0
|
||||||
auto_completion=
|
auto_completion=
|
||||||
key_bindings=
|
key_bindings=
|
||||||
update_config=2
|
update_config=2
|
||||||
@@ -168,7 +168,8 @@ archi=$(uname -sm)
|
|||||||
binary_available=1
|
binary_available=1
|
||||||
binary_error=""
|
binary_error=""
|
||||||
case "$archi" in
|
case "$archi" in
|
||||||
Darwin\ *64) download fzf-$version-darwin_amd64.tar.gz ;;
|
Darwin\ arm64) download fzf-$version-darwin_arm64.zip ;;
|
||||||
|
Darwin\ x86_64) download fzf-$version-darwin_amd64.zip ;;
|
||||||
Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;;
|
Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;;
|
||||||
Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;;
|
Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;;
|
||||||
Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;;
|
Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;;
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
$version="0.25.0"
|
$version="0.28.0"
|
||||||
|
|
||||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
|
||||||
@@ -43,7 +43,11 @@ function download {
|
|||||||
$url="https://github.com/junegunn/fzf/releases/download/$version/$file"
|
$url="https://github.com/junegunn/fzf/releases/download/$version/$file"
|
||||||
$temp=$env:TMP + "\fzf.zip"
|
$temp=$env:TMP + "\fzf.zip"
|
||||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
|
if ($PSVersionTable.PSVersion.Major -ge 3) {
|
||||||
|
Invoke-WebRequest -Uri $url -OutFile $temp
|
||||||
|
} else {
|
||||||
(New-Object Net.WebClient).DownloadFile($url, $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath("$temp"))
|
(New-Object Net.WebClient).DownloadFile($url, $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath("$temp"))
|
||||||
|
}
|
||||||
if ($?) {
|
if ($?) {
|
||||||
(Microsoft.PowerShell.Archive\Expand-Archive -Path $temp -DestinationPath .); (Remove-Item $temp)
|
(Microsoft.PowerShell.Archive\Expand-Archive -Path $temp -DestinationPath .); (Remove-Item $temp)
|
||||||
} else {
|
} else {
|
||||||
@@ -53,7 +57,7 @@ function download {
|
|||||||
$binary_error="Failed to download $file"
|
$binary_error="Failed to download $file"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
check_binary >$null
|
echo y | icacls $fzf_base\bin\fzf.exe /grant Administrator:F ; check_binary >$null
|
||||||
}
|
}
|
||||||
|
|
||||||
download "fzf-$version-windows_amd64.zip"
|
download "fzf-$version-windows_amd64.zip"
|
||||||
|
4
main.go
4
main.go
@@ -1,11 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/junegunn/fzf/src"
|
fzf "github.com/junegunn/fzf/src"
|
||||||
"github.com/junegunn/fzf/src/protector"
|
"github.com/junegunn/fzf/src/protector"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version string = "0.24"
|
var version string = "0.28"
|
||||||
var revision string = "devel"
|
var revision string = "devel"
|
||||||
|
|
||||||
func main() {
|
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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
..
|
..
|
||||||
.TH fzf-tmux 1 "Jan 2021" "fzf 0.25.0" "fzf-tmux - open fzf in tmux split pane"
|
.TH fzf-tmux 1 "Nov 2021" "fzf 0.28.0" "fzf-tmux - open fzf in tmux split pane"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
fzf-tmux - open fzf in tmux split pane
|
fzf-tmux - open fzf in tmux split pane
|
||||||
|
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
..
|
..
|
||||||
.TH fzf 1 "Jan 2021" "fzf 0.25.0" "fzf - a command-line fuzzy finder"
|
.TH fzf 1 "Nov 2021" "fzf 0.28.0" "fzf - a command-line fuzzy finder"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
fzf - a command-line fuzzy finder
|
fzf - a command-line fuzzy finder
|
||||||
@@ -74,7 +74,7 @@ Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style)
|
|||||||
.BI "--disabled"
|
.BI "--disabled"
|
||||||
Do not perform search. With this option, fzf becomes a simple selector
|
Do not perform search. With this option, fzf becomes a simple selector
|
||||||
interface rather than a "fuzzy finder". You can later enable the search using
|
interface rather than a "fuzzy finder". You can later enable the search using
|
||||||
\fBenable-search\fR or `\fBtoggle-search\R action.
|
\fBenable-search\fR or \fBtoggle-search\fR action.
|
||||||
.SS Search result
|
.SS Search result
|
||||||
.TP
|
.TP
|
||||||
.B "+s, --no-sort"
|
.B "+s, --no-sort"
|
||||||
@@ -135,10 +135,14 @@ Enable cyclic scroll
|
|||||||
Keep the right end of the line visible when it's too long. Effective only when
|
Keep the right end of the line visible when it's too long. Effective only when
|
||||||
the query string is empty.
|
the query string is empty.
|
||||||
.TP
|
.TP
|
||||||
|
.BI "--scroll-off=" "LINES"
|
||||||
|
Number of screen lines to keep above or below when scrolling to the top or to
|
||||||
|
the bottom (default: 0).
|
||||||
|
.TP
|
||||||
.B "--no-hscroll"
|
.B "--no-hscroll"
|
||||||
Disable horizontal scroll
|
Disable horizontal scroll
|
||||||
.TP
|
.TP
|
||||||
.BI "--hscroll-off=" "COL"
|
.BI "--hscroll-off=" "COLS"
|
||||||
Number of screen columns to keep to the right of the highlighted substring
|
Number of screen columns to keep to the right of the highlighted substring
|
||||||
(default: 10). Setting it to a large value will cause the text to be positioned
|
(default: 10). Setting it to a large value will cause the text to be positioned
|
||||||
on the center of the screen.
|
on the center of the screen.
|
||||||
@@ -203,6 +207,8 @@ Draw border around the finder
|
|||||||
.br
|
.br
|
||||||
.BR right
|
.BR right
|
||||||
.br
|
.br
|
||||||
|
.BR none
|
||||||
|
.br
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B "--no-unicode"
|
.B "--no-unicode"
|
||||||
@@ -293,6 +299,9 @@ are not affected by \fB--with-nth\fR. ANSI color codes are processed even when
|
|||||||
The first N lines of the input are treated as the sticky header. When
|
The first N lines of the input are treated as the sticky header. When
|
||||||
\fB--with-nth\fR is set, the lines are transformed just like the other
|
\fB--with-nth\fR is set, the lines are transformed just like the other
|
||||||
lines that follow.
|
lines that follow.
|
||||||
|
.TP
|
||||||
|
.B "--header-first"
|
||||||
|
Print header before the prompt line
|
||||||
.SS Display
|
.SS Display
|
||||||
.TP
|
.TP
|
||||||
.B "--ansi"
|
.B "--ansi"
|
||||||
@@ -338,6 +347,22 @@ color mappings.
|
|||||||
\fB-1 \fRDefault terminal foreground/background color
|
\fB-1 \fRDefault terminal foreground/background color
|
||||||
\fB \fR(or the original color of the text)
|
\fB \fR(or the original color of the text)
|
||||||
\fB0 ~ 15 \fR16 base colors
|
\fB0 ~ 15 \fR16 base colors
|
||||||
|
\fBblack\fR
|
||||||
|
\fBred\fR
|
||||||
|
\fBgreen\fR
|
||||||
|
\fByellow\fR
|
||||||
|
\fBblue\fR
|
||||||
|
\fBmagenta\fR
|
||||||
|
\fBcyan\fR
|
||||||
|
\fBwhite\fR
|
||||||
|
\fBbright-black\fR (gray | grey)
|
||||||
|
\fBbright-red\fR
|
||||||
|
\fBbright-green\fR
|
||||||
|
\fBbright-yellow\fR
|
||||||
|
\fBbright-blue\fR
|
||||||
|
\fBbright-magenta\fR
|
||||||
|
\fBbright-cyan\fR
|
||||||
|
\fBbright-white\fR
|
||||||
\fB16 ~ 255 \fRANSI 256 colors
|
\fB16 ~ 255 \fRANSI 256 colors
|
||||||
\fB#rrggbb \fR24-bit colors
|
\fB#rrggbb \fR24-bit colors
|
||||||
|
|
||||||
@@ -442,7 +467,7 @@ e.g.
|
|||||||
done'\fR
|
done'\fR
|
||||||
.RE
|
.RE
|
||||||
.TP
|
.TP
|
||||||
.BI "--preview-window=" "[POSITION][:SIZE[%]][:rounded|sharp|noborder][:[no]wrap][:[no]follow][:[no]cycle][:[no]hidden][:+SCROLL[-OFFSET]][:default]"
|
.BI "--preview-window=" "[POSITION][,SIZE[%]][,border-BORDER_OPT][,[no]wrap][,[no]follow][,[no]cycle][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]"
|
||||||
|
|
||||||
.RS
|
.RS
|
||||||
.B POSITION: (default: right)
|
.B POSITION: (default: right)
|
||||||
@@ -477,35 +502,51 @@ e.g.
|
|||||||
* Cyclic scrolling is enabled with \fB:cycle\fR flag.
|
* Cyclic scrolling is enabled with \fB:cycle\fR flag.
|
||||||
|
|
||||||
* To change the style of the border of the preview window, specify one of
|
* To change the style of the border of the preview window, specify one of
|
||||||
\fBrounded\fR (border with rounded edges, default), \fBsharp\fR (border with
|
the options for \fB--border\fR with \fBborder-\fR prefix.
|
||||||
sharp edges), or \fBnoborder\fR (no border).
|
e.g. \fBborder-rounded\fR (border with rounded edges, default),
|
||||||
|
\fBborder-sharp\fR (border with sharp edges), \fBborder-left\fR,
|
||||||
|
\fBborder-none\fR, etc.
|
||||||
|
|
||||||
* \fB+SCROLL[-OFFSET]\fR determines the initial scroll offset of the preview
|
* \fB[:+SCROLL[OFFSETS][/DENOM]]\fR determines the initial scroll offset of the
|
||||||
window. \fBSCROLL\fR can be either a numeric integer or a single-field index
|
preview window.
|
||||||
expression that refers to a numeric integer. The optional \fB-OFFSET\fR part is
|
|
||||||
for adjusting the base offset so that you can see the text above it. It should
|
- \fBSCROLL\fR can be either a numeric integer or a single-field index expression that refers to a numeric integer.
|
||||||
be given as a numeric integer (\fB-INTEGER\fR), or as a denominator form
|
|
||||||
(\fB-/INTEGER\fR) for specifying a fraction of the preview window height.
|
- The optional \fBOFFSETS\fR part is for adjusting the base offset. It should be given as a series of signed integers (\fB-INTEGER\fR or \fB+INTEGER\fR).
|
||||||
|
|
||||||
|
- The final \fB/DENOM\fR part is for specifying a fraction of the preview window height.
|
||||||
|
|
||||||
|
* \fB~HEADER_LINES\fR keeps the top N lines as the fixed header so that they
|
||||||
|
are always visible.
|
||||||
|
|
||||||
* \fBdefault\fR resets all options previously set to the default.
|
* \fBdefault\fR resets all options previously set to the default.
|
||||||
|
|
||||||
.RS
|
.RS
|
||||||
e.g.
|
e.g.
|
||||||
\fB# Non-default scroll window positions and sizes
|
\fB# Non-default scroll window positions and sizes
|
||||||
fzf --preview="head {}" --preview-window=up:30%
|
fzf --preview="head {}" --preview-window=up,30%
|
||||||
fzf --preview="file {}" --preview-window=down:1
|
fzf --preview="file {}" --preview-window=down,1
|
||||||
|
|
||||||
# Initial scroll offset is set to the line number of each line of
|
# Initial scroll offset is set to the line number of each line of
|
||||||
# git grep output *minus* 5 lines (-5)
|
# git grep output *minus* 5 lines (-5)
|
||||||
git grep --line-number '' |
|
git grep --line-number '' |
|
||||||
fzf --delimiter : --preview 'nl {1}' --preview-window +{2}-5
|
fzf --delimiter : --preview 'nl {1}' --preview-window '+{2}-5'
|
||||||
|
|
||||||
# Preview with bat, matching line in the middle of the window (-/2)
|
# Preview with bat, matching line in the middle of the window below
|
||||||
|
# the fixed header of the top 3 lines
|
||||||
|
#
|
||||||
|
# ~3 Top 3 lines as the fixed header
|
||||||
|
# +{2} Base scroll offset extracted from the second field
|
||||||
|
# +3 Extra offset to compensate for the 3-line header
|
||||||
|
# /2 Put in the middle of the preview area
|
||||||
|
#
|
||||||
git grep --line-number '' |
|
git grep --line-number '' |
|
||||||
fzf --delimiter : \\
|
fzf --delimiter : \\
|
||||||
--preview 'bat --style=numbers --color=always --highlight-line {2} {1}' \\
|
--preview 'bat --style=full --color=always --highlight-line {2} {1}' \\
|
||||||
--preview-window +{2}-/2\fR
|
--preview-window '~3,+{2}+3/2'
|
||||||
|
|
||||||
|
# Display top 3 lines as the fixed header
|
||||||
|
fzf --preview 'bat --style=full --color=always {}' --preview-window '~3'\fR
|
||||||
.RE
|
.RE
|
||||||
|
|
||||||
.SS Scripting
|
.SS Scripting
|
||||||
@@ -514,10 +555,12 @@ e.g.
|
|||||||
Start the finder with the given query
|
Start the finder with the given query
|
||||||
.TP
|
.TP
|
||||||
.B "-1, --select-1"
|
.B "-1, --select-1"
|
||||||
Automatically select the only match
|
If there is only one match for the initial query (\fB--query\fR), do not start
|
||||||
|
interactive finder and automatically select the only match
|
||||||
.TP
|
.TP
|
||||||
.B "-0, --exit-0"
|
.B "-0, --exit-0"
|
||||||
Exit immediately when there's no match
|
If there is no match for the initial query (\fB--query\fR), do not start
|
||||||
|
interactive finder and exit immediately
|
||||||
.TP
|
.TP
|
||||||
.BI "-f, --filter=" "STR"
|
.BI "-f, --filter=" "STR"
|
||||||
Filter mode. Do not start interactive finder. When used with \fB--no-sort\fR,
|
Filter mode. Do not start interactive finder. When used with \fB--no-sort\fR,
|
||||||
@@ -570,7 +613,8 @@ Note that most options have the opposite versions with \fB--no-\fR prefix.
|
|||||||
.TP
|
.TP
|
||||||
.B FZF_DEFAULT_COMMAND
|
.B FZF_DEFAULT_COMMAND
|
||||||
Default command to use when input is tty. On *nix systems, fzf runs the command
|
Default command to use when input is tty. On *nix systems, fzf runs the command
|
||||||
with \fBsh -c\fR, so make sure that it's POSIX-compliant.
|
with \fB$SHELL -c\fR if \fBSHELL\fR is set, otherwise with \fBsh -c\fR, so in
|
||||||
|
this case make sure that the command is POSIX-compliant.
|
||||||
.TP
|
.TP
|
||||||
.B FZF_DEFAULT_OPTS
|
.B FZF_DEFAULT_OPTS
|
||||||
Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR
|
Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR
|
||||||
@@ -779,9 +823,11 @@ A key or an event can be bound to one or more of the following actions.
|
|||||||
\fBchange-prompt(...)\fR (change prompt to the given string)
|
\fBchange-prompt(...)\fR (change prompt to the given string)
|
||||||
\fBclear-screen\fR \fIctrl-l\fR
|
\fBclear-screen\fR \fIctrl-l\fR
|
||||||
\fBclear-selection\fR (clear multi-selection)
|
\fBclear-selection\fR (clear multi-selection)
|
||||||
|
\fBclose\fR (close preview window if open, abort fzf otherwise)
|
||||||
\fBclear-query\fR (clear query string)
|
\fBclear-query\fR (clear query string)
|
||||||
\fBdelete-char\fR \fIdel\fR
|
\fBdelete-char\fR \fIdel\fR
|
||||||
\fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty)
|
\fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty)
|
||||||
|
\fBdeselect\fR
|
||||||
\fBdeselect-all\fR (deselect all matches)
|
\fBdeselect-all\fR (deselect all matches)
|
||||||
\fBdisable-search\fR (disable search functionality)
|
\fBdisable-search\fR (disable search functionality)
|
||||||
\fBdown\fR \fIctrl-j ctrl-n down\fR
|
\fBdown\fR \fIctrl-j ctrl-n down\fR
|
||||||
@@ -814,9 +860,11 @@ A key or an event can be bound to one or more of the following actions.
|
|||||||
\fBpreview-top\fR
|
\fBpreview-top\fR
|
||||||
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
|
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
|
||||||
\fBprint-query\fR (print query and exit)
|
\fBprint-query\fR (print query and exit)
|
||||||
|
\fBput\fR (put the character to the prompt)
|
||||||
\fBrefresh-preview\fR
|
\fBrefresh-preview\fR
|
||||||
\fBreload(...)\fR (see below for the details)
|
\fBreload(...)\fR (see below for the details)
|
||||||
\fBreplace-query\fR (replace query string with the current selection)
|
\fBreplace-query\fR (replace query string with the current selection)
|
||||||
|
\fBselect\fR
|
||||||
\fBselect-all\fR (select all matches)
|
\fBselect-all\fR (select all matches)
|
||||||
\fBtoggle\fR (\fIright-click\fR)
|
\fBtoggle\fR (\fIright-click\fR)
|
||||||
\fBtoggle-all\fR (toggle all matches)
|
\fBtoggle-all\fR (toggle all matches)
|
||||||
@@ -828,6 +876,7 @@ A key or an event can be bound to one or more of the following actions.
|
|||||||
\fBtoggle-search\fR (toggle search functionality)
|
\fBtoggle-search\fR (toggle search functionality)
|
||||||
\fBtoggle-sort\fR
|
\fBtoggle-sort\fR
|
||||||
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
|
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
|
||||||
|
\fBunbind(...)\fR (unbind bindings)
|
||||||
\fBunix-line-discard\fR \fIctrl-u\fR
|
\fBunix-line-discard\fR \fIctrl-u\fR
|
||||||
\fBunix-word-rubout\fR \fIctrl-w\fR
|
\fBunix-word-rubout\fR \fIctrl-w\fR
|
||||||
\fBup\fR \fIctrl-k ctrl-p up\fR
|
\fBup\fR \fIctrl-k ctrl-p up\fR
|
||||||
@@ -890,6 +939,10 @@ executes the command without the switching. Note that fzf will not be
|
|||||||
responsive until the command is complete. For asynchronous execution, start
|
responsive until the command is complete. For asynchronous execution, start
|
||||||
your command as a background process (i.e. appending \fB&\fR).
|
your command as a background process (i.e. appending \fB&\fR).
|
||||||
|
|
||||||
|
On *nix systems, fzf runs the command with \fB$SHELL -c\fR if \fBSHELL\fR is
|
||||||
|
set, otherwise with \fBsh -c\fR, so in this case make sure that the command is
|
||||||
|
POSIX-compliant.
|
||||||
|
|
||||||
.SS RELOAD INPUT
|
.SS RELOAD INPUT
|
||||||
|
|
||||||
\fBreload(...)\fR action is used to dynamically update the input list
|
\fBreload(...)\fR action is used to dynamically update the input list
|
||||||
|
164
plugin/fzf.vim
164
plugin/fzf.vim
@@ -154,26 +154,51 @@ function! fzf#install()
|
|||||||
endif
|
endif
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
function! s:version_requirement(val, min)
|
let s:versions = {}
|
||||||
let val = split(a:val, '\.')
|
function s:get_version(bin)
|
||||||
let min = split(a:min, '\.')
|
if has_key(s:versions, a:bin)
|
||||||
for idx in range(0, len(min) - 1)
|
return s:versions[a:bin]
|
||||||
let v = get(val, idx, 0)
|
end
|
||||||
if v < min[idx] | return 0
|
let command = a:bin . ' --version --no-height'
|
||||||
elseif v > min[idx] | return 1
|
let output = systemlist(command)
|
||||||
|
if v:shell_error || empty(output)
|
||||||
|
return ''
|
||||||
|
endif
|
||||||
|
let ver = matchstr(output[-1], '[0-9.]\+')
|
||||||
|
let s:versions[a:bin] = ver
|
||||||
|
return ver
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
function! s:compare_versions(a, b)
|
||||||
|
let a = split(a:a, '\.')
|
||||||
|
let b = split(a:b, '\.')
|
||||||
|
for idx in range(0, max([len(a), len(b)]) - 1)
|
||||||
|
let v1 = str2nr(get(a, idx, 0))
|
||||||
|
let v2 = str2nr(get(b, idx, 0))
|
||||||
|
if v1 < v2 | return -1
|
||||||
|
elseif v1 > v2 | return 1
|
||||||
endif
|
endif
|
||||||
endfor
|
endfor
|
||||||
return 1
|
return 0
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
function! s:compare_binary_versions(a, b)
|
||||||
|
return s:compare_versions(s:get_version(a:a), s:get_version(a:b))
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
let s:checked = {}
|
let s:checked = {}
|
||||||
function! fzf#exec(...)
|
function! fzf#exec(...)
|
||||||
if !exists('s:exec')
|
if !exists('s:exec')
|
||||||
|
let binaries = []
|
||||||
|
if executable('fzf')
|
||||||
|
call add(binaries, 'fzf')
|
||||||
|
endif
|
||||||
if executable(s:fzf_go)
|
if executable(s:fzf_go)
|
||||||
let s:exec = s:fzf_go
|
call add(binaries, s:fzf_go)
|
||||||
elseif executable('fzf')
|
endif
|
||||||
let s:exec = 'fzf'
|
|
||||||
elseif input('fzf executable not found. Download binary? (y/n) ') =~? '^y'
|
if empty(binaries)
|
||||||
|
if input('fzf executable not found. Download binary? (y/n) ') =~? '^y'
|
||||||
redraw
|
redraw
|
||||||
call fzf#install()
|
call fzf#install()
|
||||||
return fzf#exec()
|
return fzf#exec()
|
||||||
@@ -181,19 +206,27 @@ function! fzf#exec(...)
|
|||||||
redraw
|
redraw
|
||||||
throw 'fzf executable not found'
|
throw 'fzf executable not found'
|
||||||
endif
|
endif
|
||||||
|
elseif len(binaries) > 1
|
||||||
|
call sort(binaries, 's:compare_binary_versions')
|
||||||
|
endif
|
||||||
|
|
||||||
|
let s:exec = binaries[-1]
|
||||||
endif
|
endif
|
||||||
|
|
||||||
if a:0 && !has_key(s:checked, a:1)
|
if a:0 && !has_key(s:checked, a:1)
|
||||||
let command = s:exec . ' --version'
|
let fzf_version = s:get_version(s:exec)
|
||||||
let output = systemlist(command)
|
if empty(fzf_version)
|
||||||
if v:shell_error || empty(output)
|
let message = printf('Failed to run "%s --version"', s:exec)
|
||||||
throw printf('Failed to run "%s": %s', command, output)
|
unlet s:exec
|
||||||
endif
|
throw message
|
||||||
let fzf_version = matchstr(output[-1], '[0-9.]\+')
|
end
|
||||||
if s:version_requirement(fzf_version, a:1)
|
|
||||||
|
if s:compare_versions(fzf_version, a:1) >= 0
|
||||||
let s:checked[a:1] = 1
|
let s:checked[a:1] = 1
|
||||||
return s:exec
|
return s:exec
|
||||||
elseif a:0 < 2 && input(printf('You need fzf %s or above. Found: %s. Download binary? (y/n) ', a:1, fzf_version)) =~? '^y'
|
elseif a:0 < 2 && input(printf('You need fzf %s or above. Found: %s. Download binary? (y/n) ', a:1, fzf_version)) =~? '^y'
|
||||||
|
let s:versions = {}
|
||||||
|
unlet s:exec
|
||||||
redraw
|
redraw
|
||||||
call fzf#install()
|
call fzf#install()
|
||||||
return fzf#exec(a:1, 1)
|
return fzf#exec(a:1, 1)
|
||||||
@@ -386,13 +419,13 @@ function! fzf#wrap(...)
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
" Action: g:fzf_action
|
" Action: g:fzf_action
|
||||||
if !s:has_any(opts, ['sink', 'sink*'])
|
if !s:has_any(opts, ['sink', 'sinklist', 'sink*'])
|
||||||
let opts._action = get(g:, 'fzf_action', s:default_action)
|
let opts._action = get(g:, 'fzf_action', s:default_action)
|
||||||
let opts.options .= ' --expect='.join(keys(opts._action), ',')
|
let opts.options .= ' --expect='.join(keys(opts._action), ',')
|
||||||
function! opts.sink(lines) abort
|
function! opts.sinklist(lines) abort
|
||||||
return s:common_sink(self._action, a:lines)
|
return s:common_sink(self._action, a:lines)
|
||||||
endfunction
|
endfunction
|
||||||
let opts['sink*'] = remove(opts, 'sink')
|
let opts['sink*'] = opts.sinklist " For backward compatibility
|
||||||
endif
|
endif
|
||||||
|
|
||||||
return opts
|
return opts
|
||||||
@@ -424,27 +457,27 @@ try
|
|||||||
throw v:exception
|
throw v:exception
|
||||||
endtry
|
endtry
|
||||||
|
|
||||||
if !has_key(dict, 'dir')
|
if !s:present(dict, 'dir')
|
||||||
let dict.dir = s:fzf_getcwd()
|
let dict.dir = s:fzf_getcwd()
|
||||||
endif
|
endif
|
||||||
if has('win32unix') && has_key(dict, 'dir')
|
if has('win32unix') && s:present(dict, 'dir')
|
||||||
let dict.dir = fnamemodify(dict.dir, ':p')
|
let dict.dir = fnamemodify(dict.dir, ':p')
|
||||||
endif
|
endif
|
||||||
|
|
||||||
if has_key(dict, 'source')
|
if has_key(dict, 'source')
|
||||||
let source = dict.source
|
let source = remove(dict, 'source')
|
||||||
let type = type(source)
|
let type = type(source)
|
||||||
if type == 1
|
if type == 1
|
||||||
let prefix = '( '.source.' )|'
|
let source_command = source
|
||||||
elseif type == 3
|
elseif type == 3
|
||||||
let temps.input = s:fzf_tempname()
|
let temps.input = s:fzf_tempname()
|
||||||
call writefile(source, temps.input)
|
call writefile(source, temps.input)
|
||||||
let prefix = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input).'|'
|
let source_command = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input)
|
||||||
else
|
else
|
||||||
throw 'Invalid source type'
|
throw 'Invalid source type'
|
||||||
endif
|
endif
|
||||||
else
|
else
|
||||||
let prefix = ''
|
let source_command = ''
|
||||||
endif
|
endif
|
||||||
|
|
||||||
let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0) || has_key(dict, 'tmux')
|
let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0) || has_key(dict, 'tmux')
|
||||||
@@ -454,20 +487,24 @@ try
|
|||||||
let has_vim8_term = has('terminal') && has('patch-8.0.995')
|
let has_vim8_term = has('terminal') && has('patch-8.0.995')
|
||||||
let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win
|
let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win
|
||||||
let use_term = has_nvim_term ||
|
let use_term = has_nvim_term ||
|
||||||
\ has_vim8_term && !has('win32unix') && (has('gui_running') || s:is_win || !use_height && s:present(dict, 'down', 'up', 'left', 'right', 'window'))
|
\ has_vim8_term && !has('win32unix') && (has('gui_running') || s:is_win || s:present(dict, 'down', 'up', 'left', 'right', 'window'))
|
||||||
let use_tmux = (has_key(dict, 'tmux') || (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:splittable(dict)) && s:tmux_enabled()
|
let use_tmux = (has_key(dict, 'tmux') || (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:splittable(dict)) && s:tmux_enabled()
|
||||||
if prefer_tmux && use_tmux
|
if prefer_tmux && use_tmux
|
||||||
let use_height = 0
|
let use_height = 0
|
||||||
let use_term = 0
|
let use_term = 0
|
||||||
endif
|
endif
|
||||||
if use_height
|
if use_term
|
||||||
|
let optstr .= ' --no-height'
|
||||||
|
elseif use_height
|
||||||
let height = s:calc_size(&lines, dict.down, dict)
|
let height = s:calc_size(&lines, dict.down, dict)
|
||||||
let optstr .= ' --height='.height
|
let optstr .= ' --height='.height
|
||||||
elseif use_term
|
|
||||||
let optstr .= ' --no-height'
|
|
||||||
endif
|
endif
|
||||||
let optstr .= s:border_opt(get(dict, 'window', 0))
|
let optstr .= s:border_opt(get(dict, 'window', 0))
|
||||||
let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
|
let prev_default_command = $FZF_DEFAULT_COMMAND
|
||||||
|
if len(source_command)
|
||||||
|
let $FZF_DEFAULT_COMMAND = source_command
|
||||||
|
endif
|
||||||
|
let command = (use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
|
||||||
|
|
||||||
if use_term
|
if use_term
|
||||||
return s:execute_term(dict, command, temps)
|
return s:execute_term(dict, command, temps)
|
||||||
@@ -478,6 +515,14 @@ try
|
|||||||
call s:callback(dict, lines)
|
call s:callback(dict, lines)
|
||||||
return lines
|
return lines
|
||||||
finally
|
finally
|
||||||
|
if len(source_command)
|
||||||
|
if len(prev_default_command)
|
||||||
|
let $FZF_DEFAULT_COMMAND = prev_default_command
|
||||||
|
else
|
||||||
|
let $FZF_DEFAULT_COMMAND = ''
|
||||||
|
silent! execute 'unlet $FZF_DEFAULT_COMMAND'
|
||||||
|
endif
|
||||||
|
endif
|
||||||
let [&shell, &shellslash, &shellcmdflag, &shellxquote] = [shell, shellslash, shellcmdflag, shellxquote]
|
let [&shell, &shellslash, &shellcmdflag, &shellxquote] = [shell, shellslash, shellcmdflag, shellxquote]
|
||||||
endtry
|
endtry
|
||||||
endfunction
|
endfunction
|
||||||
@@ -507,8 +552,8 @@ function! s:fzf_tmux(dict)
|
|||||||
endif
|
endif
|
||||||
endfor
|
endfor
|
||||||
endif
|
endif
|
||||||
return printf('LINES=%d COLUMNS=%d %s %s %s --',
|
return printf('LINES=%d COLUMNS=%d %s %s - --',
|
||||||
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
|
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size)
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
function! s:splittable(dict)
|
function! s:splittable(dict)
|
||||||
@@ -638,8 +683,7 @@ function! s:execute(dict, command, use_height, temps) abort
|
|||||||
let a:temps.shellscript = shellscript
|
let a:temps.shellscript = shellscript
|
||||||
endif
|
endif
|
||||||
if a:use_height
|
if a:use_height
|
||||||
let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty'
|
call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s < /dev/tty 2> /dev/tty', &lines, command))
|
||||||
call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin))
|
|
||||||
else
|
else
|
||||||
execute 'silent !'.command
|
execute 'silent !'.command
|
||||||
endif
|
endif
|
||||||
@@ -764,6 +808,13 @@ function! s:split(dict)
|
|||||||
endtry
|
endtry
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
|
nnoremap <silent> <Plug>(fzf-insert) i
|
||||||
|
nnoremap <silent> <Plug>(fzf-normal) <Nop>
|
||||||
|
if exists(':tnoremap')
|
||||||
|
tnoremap <silent> <Plug>(fzf-insert) <C-\><C-n>i
|
||||||
|
tnoremap <silent> <Plug>(fzf-normal) <C-\><C-n>
|
||||||
|
endif
|
||||||
|
|
||||||
function! s:execute_term(dict, command, temps) abort
|
function! s:execute_term(dict, command, temps) abort
|
||||||
let winrest = winrestcmd()
|
let winrest = winrestcmd()
|
||||||
let pbuf = bufnr('')
|
let pbuf = bufnr('')
|
||||||
@@ -776,7 +827,7 @@ function! s:execute_term(dict, command, temps) abort
|
|||||||
function! fzf.switch_back(inplace)
|
function! fzf.switch_back(inplace)
|
||||||
if a:inplace && bufnr('') == self.buf
|
if a:inplace && bufnr('') == self.buf
|
||||||
if bufexists(self.pbuf)
|
if bufexists(self.pbuf)
|
||||||
execute 'keepalt b' self.pbuf
|
execute 'keepalt keepjumps b' self.pbuf
|
||||||
endif
|
endif
|
||||||
" No other listed buffer
|
" No other listed buffer
|
||||||
if bufnr('') == self.buf
|
if bufnr('') == self.buf
|
||||||
@@ -816,6 +867,10 @@ function! s:execute_term(dict, command, temps) abort
|
|||||||
call s:pushd(self.dict)
|
call s:pushd(self.dict)
|
||||||
call s:callback(self.dict, lines)
|
call s:callback(self.dict, lines)
|
||||||
call self.switch_back(s:getpos() == self.ppos)
|
call self.switch_back(s:getpos() == self.ppos)
|
||||||
|
|
||||||
|
if &buftype == 'terminal'
|
||||||
|
call feedkeys(&filetype == 'fzf' ? "\<Plug>(fzf-insert)" : "\<Plug>(fzf-normal)")
|
||||||
|
endif
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -831,16 +886,16 @@ function! s:execute_term(dict, command, temps) abort
|
|||||||
if has('nvim')
|
if has('nvim')
|
||||||
call termopen(command, fzf)
|
call termopen(command, fzf)
|
||||||
else
|
else
|
||||||
let term_opts = {'exit_cb': function(fzf.on_exit), 'term_kill': 'term'}
|
let term_opts = {'exit_cb': function(fzf.on_exit)}
|
||||||
|
if v:version >= 802
|
||||||
|
let term_opts.term_kill = 'term'
|
||||||
|
endif
|
||||||
if is_popup
|
if is_popup
|
||||||
let term_opts.hidden = 1
|
let term_opts.hidden = 1
|
||||||
else
|
else
|
||||||
let term_opts.curwin = 1
|
let term_opts.curwin = 1
|
||||||
endif
|
endif
|
||||||
let fzf.buf = term_start([&shell, &shellcmdflag, command], term_opts)
|
let fzf.buf = term_start([&shell, &shellcmdflag, command], term_opts)
|
||||||
if exists('&termwinkey')
|
|
||||||
call setbufvar(fzf.buf, '&termwinkey', '<c-z>')
|
|
||||||
endif
|
|
||||||
if is_popup && exists('#TerminalWinOpen')
|
if is_popup && exists('#TerminalWinOpen')
|
||||||
doautocmd <nomodeline> TerminalWinOpen
|
doautocmd <nomodeline> TerminalWinOpen
|
||||||
endif
|
endif
|
||||||
@@ -849,6 +904,9 @@ function! s:execute_term(dict, command, temps) abort
|
|||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
tnoremap <buffer> <c-z> <nop>
|
tnoremap <buffer> <c-z> <nop>
|
||||||
|
if exists('&termwinkey') && (empty(&termwinkey) || &termwinkey =~? '<c-w>')
|
||||||
|
tnoremap <buffer> <c-w> <c-w>.
|
||||||
|
endif
|
||||||
finally
|
finally
|
||||||
call s:dopopd()
|
call s:dopopd()
|
||||||
endtry
|
endtry
|
||||||
@@ -886,6 +944,8 @@ function! s:callback(dict, lines) abort
|
|||||||
endif
|
endif
|
||||||
if has_key(a:dict, 'sink*')
|
if has_key(a:dict, 'sink*')
|
||||||
call a:dict['sink*'](a:lines)
|
call a:dict['sink*'](a:lines)
|
||||||
|
elseif has_key(a:dict, 'sinklist')
|
||||||
|
call a:dict['sinklist'](a:lines)
|
||||||
endif
|
endif
|
||||||
catch
|
catch
|
||||||
if stridx(v:exception, ':E325:') < 0
|
if stridx(v:exception, ':E325:') < 0
|
||||||
@@ -904,13 +964,9 @@ if has('nvim')
|
|||||||
function s:create_popup(hl, opts) abort
|
function s:create_popup(hl, opts) abort
|
||||||
let buf = nvim_create_buf(v:false, v:true)
|
let buf = nvim_create_buf(v:false, v:true)
|
||||||
let opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts)
|
let opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts)
|
||||||
let border = has_key(opts, 'border') ? remove(opts, 'border') : []
|
|
||||||
let win = nvim_open_win(buf, v:true, opts)
|
let win = nvim_open_win(buf, v:true, opts)
|
||||||
call setwinvar(win, '&winhighlight', 'NormalFloat:'..a:hl)
|
call setwinvar(win, '&winhighlight', 'NormalFloat:'..a:hl)
|
||||||
call setwinvar(win, '&colorcolumn', '')
|
call setwinvar(win, '&colorcolumn', '')
|
||||||
if !empty(border)
|
|
||||||
call nvim_buf_set_lines(buf, 0, -1, v:true, border)
|
|
||||||
endif
|
|
||||||
return buf
|
return buf
|
||||||
endfunction
|
endfunction
|
||||||
else
|
else
|
||||||
@@ -929,11 +985,19 @@ else
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
function! s:popup(opts) abort
|
function! s:popup(opts) abort
|
||||||
|
let xoffset = get(a:opts, 'xoffset', 0.5)
|
||||||
|
let yoffset = get(a:opts, 'yoffset', 0.5)
|
||||||
|
let relative = get(a:opts, 'relative', 0)
|
||||||
|
|
||||||
|
" Use current window size for positioning relatively positioned popups
|
||||||
|
let columns = relative ? winwidth(0) : &columns
|
||||||
|
let lines = relative ? winheight(0) : (&lines - has('nvim'))
|
||||||
|
|
||||||
" Size and position
|
" Size and position
|
||||||
let width = min([max([8, a:opts.width > 1 ? a:opts.width : float2nr(&columns * a:opts.width)]), &columns])
|
let width = min([max([8, a:opts.width > 1 ? a:opts.width : float2nr(columns * a:opts.width)]), columns])
|
||||||
let height = min([max([4, a:opts.height > 1 ? a:opts.height : float2nr(&lines * a:opts.height)]), &lines - has('nvim')])
|
let height = min([max([4, a:opts.height > 1 ? a:opts.height : float2nr(lines * a:opts.height)]), lines])
|
||||||
let row = float2nr(get(a:opts, 'yoffset', 0.5) * (&lines - height))
|
let row = float2nr(yoffset * (lines - height)) + (relative ? win_screenpos(0)[0] - 1 : 0)
|
||||||
let col = float2nr(get(a:opts, 'xoffset', 0.5) * (&columns - width))
|
let col = float2nr(xoffset * (columns - width)) + (relative ? win_screenpos(0)[1] - 1 : 0)
|
||||||
|
|
||||||
" Managing the differences
|
" Managing the differences
|
||||||
let row = min([max([0, row]), &lines - has('nvim') - height])
|
let row = min([max([0, row]), &lines - has('nvim') - height])
|
||||||
|
@@ -32,12 +32,12 @@ fi
|
|||||||
###########################################################
|
###########################################################
|
||||||
|
|
||||||
# To redraw line after fzf closes (printf '\e[5n')
|
# To redraw line after fzf closes (printf '\e[5n')
|
||||||
bind '"\e[0n": redraw-current-line'
|
bind '"\e[0n": redraw-current-line' 2> /dev/null
|
||||||
|
|
||||||
__fzf_comprun() {
|
__fzf_comprun() {
|
||||||
if [ "$(type -t _fzf_comprun 2>&1)" = function ]; then
|
if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then
|
||||||
_fzf_comprun "$@"
|
_fzf_comprun "$@"
|
||||||
elif [ -n "$TMUX_PANE" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "$FZF_TMUX_OPTS" ]; }; then
|
elif [[ -n "$TMUX_PANE" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "$FZF_TMUX_OPTS" ]]; }; then
|
||||||
shift
|
shift
|
||||||
fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- "$@"
|
fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- "$@"
|
||||||
else
|
else
|
||||||
@@ -140,14 +140,14 @@ _fzf_handle_dynamic_completion() {
|
|||||||
orig_cmd="$1"
|
orig_cmd="$1"
|
||||||
orig_var="_fzf_orig_completion_$cmd"
|
orig_var="_fzf_orig_completion_$cmd"
|
||||||
orig="${!orig_var##*#}"
|
orig="${!orig_var##*#}"
|
||||||
if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then
|
if [[ -n "$orig" ]] && type "$orig" > /dev/null 2>&1; then
|
||||||
$orig "$@"
|
$orig "$@"
|
||||||
elif [ -n "$_fzf_completion_loader" ]; then
|
elif [[ -n "$_fzf_completion_loader" ]]; then
|
||||||
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
|
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
|
||||||
_completion_loader "$@"
|
_completion_loader "$@"
|
||||||
ret=$?
|
ret=$?
|
||||||
# _completion_loader may not have updated completion for the command
|
# _completion_loader may not have updated completion for the command
|
||||||
if [ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]; then
|
if [[ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]]; then
|
||||||
__fzf_orig_completion < <(complete -p "$orig_cmd" 2> /dev/null)
|
__fzf_orig_completion < <(complete -p "$orig_cmd" 2> /dev/null)
|
||||||
if [[ "$__fzf_nospace_commands" = *" $orig_cmd "* ]]; then
|
if [[ "$__fzf_nospace_commands" = *" $orig_cmd "* ]]; then
|
||||||
eval "${orig_complete/ -F / -o nospace -F }"
|
eval "${orig_complete/ -F / -o nospace -F }"
|
||||||
@@ -171,17 +171,17 @@ __fzf_generic_path_completion() {
|
|||||||
|
|
||||||
[[ $base = *"/"* ]] && dir="$base"
|
[[ $base = *"/"* ]] && dir="$base"
|
||||||
while true; do
|
while true; do
|
||||||
if [ -z "$dir" ] || [ -d "$dir" ]; then
|
if [[ -z "$dir" ]] || [[ -d "$dir" ]]; then
|
||||||
leftover=${base/#"$dir"}
|
leftover=${base/#"$dir"}
|
||||||
leftover=${leftover/#\/}
|
leftover=${leftover/#\/}
|
||||||
[ -z "$dir" ] && dir='.'
|
[[ -z "$dir" ]] && dir='.'
|
||||||
[ "$dir" != "/" ] && dir="${dir/%\//}"
|
[[ "$dir" != "/" ]] && dir="${dir/%\//}"
|
||||||
matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $2" __fzf_comprun "$4" -q "$leftover" | while read -r item; do
|
matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $2" __fzf_comprun "$4" -q "$leftover" | while read -r item; do
|
||||||
printf "%q$3 " "$item"
|
printf "%q$3 " "$item"
|
||||||
done)
|
done)
|
||||||
matches=${matches% }
|
matches=${matches% }
|
||||||
[[ -z "$3" ]] && [[ "$__fzf_nospace_commands" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
|
[[ -z "$3" ]] && [[ "$__fzf_nospace_commands" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
|
||||||
if [ -n "$matches" ]; then
|
if [[ -n "$matches" ]]; then
|
||||||
COMPREPLY=( "$matches" )
|
COMPREPLY=( "$matches" )
|
||||||
else
|
else
|
||||||
COMPREPLY=( "$cur" )
|
COMPREPLY=( "$cur" )
|
||||||
@@ -234,7 +234,7 @@ _fzf_complete() {
|
|||||||
|
|
||||||
selected=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $str_arg" __fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | tr '\n' ' ')
|
selected=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $str_arg" __fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | tr '\n' ' ')
|
||||||
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
|
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
|
||||||
if [ -n "$selected" ]; then
|
if [[ -n "$selected" ]]; then
|
||||||
COMPREPLY=("$selected")
|
COMPREPLY=("$selected")
|
||||||
else
|
else
|
||||||
COMPREPLY=("$cur")
|
COMPREPLY=("$cur")
|
||||||
@@ -330,7 +330,7 @@ __fzf_defc() {
|
|||||||
opts="$3"
|
opts="$3"
|
||||||
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
|
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
|
||||||
orig="${!orig_var}"
|
orig="${!orig_var}"
|
||||||
if [ -n "$orig" ]; then
|
if [[ -n "$orig" ]]; then
|
||||||
printf -v def "$orig" "$func"
|
printf -v def "$orig" "$func"
|
||||||
eval "$def"
|
eval "$def"
|
||||||
else
|
else
|
||||||
|
@@ -211,7 +211,6 @@ _fzf_complete() {
|
|||||||
if [ -n "$matches" ]; then
|
if [ -n "$matches" ]; then
|
||||||
LBUFFER="$lbuf$matches"
|
LBUFFER="$lbuf$matches"
|
||||||
fi
|
fi
|
||||||
zle reset-prompt
|
|
||||||
command rm -f "$fifo"
|
command rm -f "$fifo"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,6 +301,7 @@ fzf-completion() {
|
|||||||
|
|
||||||
if eval "type _fzf_complete_${cmd} > /dev/null"; then
|
if eval "type _fzf_complete_${cmd} > /dev/null"; then
|
||||||
prefix="$prefix" eval _fzf_complete_${cmd} ${(q)lbuf}
|
prefix="$prefix" eval _fzf_complete_${cmd} ${(q)lbuf}
|
||||||
|
zle reset-prompt
|
||||||
elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
|
elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
|
||||||
_fzf_dir_completion "$prefix" "$lbuf"
|
_fzf_dir_completion "$prefix" "$lbuf"
|
||||||
else
|
else
|
||||||
|
@@ -27,7 +27,7 @@ __fzf_select__() {
|
|||||||
if [[ $- =~ i ]]; then
|
if [[ $- =~ i ]]; then
|
||||||
|
|
||||||
__fzfcmd() {
|
__fzfcmd() {
|
||||||
[ -n "$TMUX_PANE" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "$FZF_TMUX_OPTS" ]; } &&
|
[[ -n "$TMUX_PANE" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "$FZF_TMUX_OPTS" ]]; } &&
|
||||||
echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf"
|
echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ __fzf_history__() {
|
|||||||
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS +m --read0" $(__fzfcmd) --query "$READLINE_LINE"
|
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS +m --read0" $(__fzfcmd) --query "$READLINE_LINE"
|
||||||
) || return
|
) || return
|
||||||
READLINE_LINE=${output#*$'\t'}
|
READLINE_LINE=${output#*$'\t'}
|
||||||
if [ -z "$READLINE_POINT" ]; then
|
if [[ -z "$READLINE_POINT" ]]; then
|
||||||
echo "$READLINE_LINE"
|
echo "$READLINE_LINE"
|
||||||
else
|
else
|
||||||
READLINE_POINT=0x7fffffff
|
READLINE_POINT=0x7fffffff
|
||||||
@@ -66,7 +66,7 @@ bind -m vi-command '"\C-z": emacs-editing-mode'
|
|||||||
bind -m vi-insert '"\C-z": emacs-editing-mode'
|
bind -m vi-insert '"\C-z": emacs-editing-mode'
|
||||||
bind -m emacs-standard '"\C-z": vi-editing-mode'
|
bind -m emacs-standard '"\C-z": vi-editing-mode'
|
||||||
|
|
||||||
if [ "${BASH_VERSINFO[0]}" -lt 4 ]; then
|
if (( BASH_VERSINFO[0] < 4 )); then
|
||||||
# CTRL-T - Paste the selected file path into the command line
|
# CTRL-T - Paste the selected file path into the command line
|
||||||
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"'
|
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"'
|
||||||
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
|
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
|
||||||
|
@@ -20,6 +20,7 @@ function fzf_key_bindings
|
|||||||
set -l commandline (__fzf_parse_commandline)
|
set -l commandline (__fzf_parse_commandline)
|
||||||
set -l dir $commandline[1]
|
set -l dir $commandline[1]
|
||||||
set -l fzf_query $commandline[2]
|
set -l fzf_query $commandline[2]
|
||||||
|
set -l prefix $commandline[3]
|
||||||
|
|
||||||
# "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not
|
# "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not
|
||||||
# $dir itself, even if hidden.
|
# $dir itself, even if hidden.
|
||||||
@@ -42,6 +43,7 @@ function fzf_key_bindings
|
|||||||
commandline -t ""
|
commandline -t ""
|
||||||
end
|
end
|
||||||
for i in $result
|
for i in $result
|
||||||
|
commandline -it -- $prefix
|
||||||
commandline -it -- (string escape $i)
|
commandline -it -- (string escape $i)
|
||||||
commandline -it -- ' '
|
commandline -it -- ' '
|
||||||
end
|
end
|
||||||
@@ -74,6 +76,7 @@ function fzf_key_bindings
|
|||||||
set -l commandline (__fzf_parse_commandline)
|
set -l commandline (__fzf_parse_commandline)
|
||||||
set -l dir $commandline[1]
|
set -l dir $commandline[1]
|
||||||
set -l fzf_query $commandline[2]
|
set -l fzf_query $commandline[2]
|
||||||
|
set -l prefix $commandline[3]
|
||||||
|
|
||||||
test -n "$FZF_ALT_C_COMMAND"; or set -l FZF_ALT_C_COMMAND "
|
test -n "$FZF_ALT_C_COMMAND"; or set -l FZF_ALT_C_COMMAND "
|
||||||
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
|
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
|
||||||
@@ -88,6 +91,7 @@ function fzf_key_bindings
|
|||||||
|
|
||||||
# Remove last token from commandline.
|
# Remove last token from commandline.
|
||||||
commandline -t ""
|
commandline -t ""
|
||||||
|
commandline -it -- $prefix
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -116,9 +120,15 @@ function fzf_key_bindings
|
|||||||
bind -M insert \ec fzf-cd-widget
|
bind -M insert \ec fzf-cd-widget
|
||||||
end
|
end
|
||||||
|
|
||||||
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath and rest of token'
|
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
||||||
|
set -l commandline (commandline -t)
|
||||||
|
|
||||||
|
# strip -option= from token if present
|
||||||
|
set -l prefix (string match -r -- '^-[^\s=]+=' $commandline)
|
||||||
|
set commandline (string replace -- "$prefix" '' $commandline)
|
||||||
|
|
||||||
# eval is used to do shell expansion on paths
|
# eval is used to do shell expansion on paths
|
||||||
set -l commandline (eval "printf '%s' "(commandline -t))
|
eval set commandline $commandline
|
||||||
|
|
||||||
if [ -z $commandline ]
|
if [ -z $commandline ]
|
||||||
# Default to current directory with no --query
|
# Default to current directory with no --query
|
||||||
@@ -138,6 +148,7 @@ function fzf_key_bindings
|
|||||||
|
|
||||||
echo $dir
|
echo $dir
|
||||||
echo $fzf_query
|
echo $fzf_query
|
||||||
|
echo $prefix
|
||||||
end
|
end
|
||||||
|
|
||||||
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
|
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
|
||||||
|
@@ -68,16 +68,6 @@ fzf-file-widget() {
|
|||||||
zle -N fzf-file-widget
|
zle -N fzf-file-widget
|
||||||
bindkey '^T' fzf-file-widget
|
bindkey '^T' fzf-file-widget
|
||||||
|
|
||||||
# Ensure precmds are run after cd
|
|
||||||
fzf-redraw-prompt() {
|
|
||||||
local precmd
|
|
||||||
for precmd in $precmd_functions; do
|
|
||||||
$precmd
|
|
||||||
done
|
|
||||||
zle reset-prompt
|
|
||||||
}
|
|
||||||
zle -N fzf-redraw-prompt
|
|
||||||
|
|
||||||
# ALT-C - cd into the selected directory
|
# ALT-C - cd into the selected directory
|
||||||
fzf-cd-widget() {
|
fzf-cd-widget() {
|
||||||
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||||
@@ -88,16 +78,12 @@ fzf-cd-widget() {
|
|||||||
zle redisplay
|
zle redisplay
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
if [ -z "$BUFFER" ]; then
|
zle push-line # Clear buffer. Auto-restored on next prompt.
|
||||||
BUFFER="cd ${(q)dir}"
|
BUFFER="cd ${(q)dir}"
|
||||||
zle accept-line
|
zle accept-line
|
||||||
else
|
|
||||||
print -sr "cd ${(q)dir}"
|
|
||||||
cd "$dir"
|
|
||||||
fi
|
|
||||||
local ret=$?
|
local ret=$?
|
||||||
unset dir # ensure this doesn't end up appearing in prompt expansion
|
unset dir # ensure this doesn't end up appearing in prompt expansion
|
||||||
zle fzf-redraw-prompt
|
zle reset-prompt
|
||||||
return $ret
|
return $ret
|
||||||
}
|
}
|
||||||
zle -N fzf-cd-widget
|
zle -N fzf-cd-widget
|
||||||
|
@@ -107,7 +107,7 @@ type Result struct {
|
|||||||
const (
|
const (
|
||||||
scoreMatch = 16
|
scoreMatch = 16
|
||||||
scoreGapStart = -3
|
scoreGapStart = -3
|
||||||
scoreGapExtention = -1
|
scoreGapExtension = -1
|
||||||
|
|
||||||
// We prefer matches at the beginning of a word, but the bonus should not be
|
// We prefer matches at the beginning of a word, but the bonus should not be
|
||||||
// too great to prevent the longer acronym matches from always winning over
|
// too great to prevent the longer acronym matches from always winning over
|
||||||
@@ -125,16 +125,16 @@ const (
|
|||||||
// Edge-triggered bonus for matches in camelCase words.
|
// Edge-triggered bonus for matches in camelCase words.
|
||||||
// Compared to word-boundary case, they don't accompany single-character gaps
|
// Compared to word-boundary case, they don't accompany single-character gaps
|
||||||
// (e.g. FooBar vs. foo-bar), so we deduct bonus point accordingly.
|
// (e.g. FooBar vs. foo-bar), so we deduct bonus point accordingly.
|
||||||
bonusCamel123 = bonusBoundary + scoreGapExtention
|
bonusCamel123 = bonusBoundary + scoreGapExtension
|
||||||
|
|
||||||
// Minimum bonus point given to characters in consecutive chunks.
|
// Minimum bonus point given to characters in consecutive chunks.
|
||||||
// Note that bonus points for consecutive matches shouldn't have needed if we
|
// Note that bonus points for consecutive matches shouldn't have needed if we
|
||||||
// used fixed match score as in the original algorithm.
|
// used fixed match score as in the original algorithm.
|
||||||
bonusConsecutive = -(scoreGapStart + scoreGapExtention)
|
bonusConsecutive = -(scoreGapStart + scoreGapExtension)
|
||||||
|
|
||||||
// The first character in the typed pattern usually has more significance
|
// The first character in the typed pattern usually has more significance
|
||||||
// than the rest so it's important that it appears at special positions where
|
// than the rest so it's important that it appears at special positions where
|
||||||
// bonus points are given. e.g. "to-go" vs. "ongoing" on "og" or on "ogo".
|
// bonus points are given, e.g. "to-go" vs. "ongoing" on "og" or on "ogo".
|
||||||
// The amount of the extra bonus should be limited so that the gap penalty is
|
// The amount of the extra bonus should be limited so that the gap penalty is
|
||||||
// still respected.
|
// still respected.
|
||||||
bonusFirstCharMultiplier = 2
|
bonusFirstCharMultiplier = 2
|
||||||
@@ -424,7 +424,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
|||||||
inGap = false
|
inGap = false
|
||||||
} else {
|
} else {
|
||||||
if inGap {
|
if inGap {
|
||||||
H0sub[off] = util.Max16(prevH0+scoreGapExtention, 0)
|
H0sub[off] = util.Max16(prevH0+scoreGapExtension, 0)
|
||||||
} else {
|
} else {
|
||||||
H0sub[off] = util.Max16(prevH0+scoreGapStart, 0)
|
H0sub[off] = util.Max16(prevH0+scoreGapStart, 0)
|
||||||
}
|
}
|
||||||
@@ -477,7 +477,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
|||||||
var s1, s2, consecutive int16
|
var s1, s2, consecutive int16
|
||||||
|
|
||||||
if inGap {
|
if inGap {
|
||||||
s2 = Hleft[off] + scoreGapExtention
|
s2 = Hleft[off] + scoreGapExtension
|
||||||
} else {
|
} else {
|
||||||
s2 = Hleft[off] + scoreGapStart
|
s2 = Hleft[off] + scoreGapStart
|
||||||
}
|
}
|
||||||
@@ -598,7 +598,7 @@ func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, patter
|
|||||||
pidx++
|
pidx++
|
||||||
} else {
|
} else {
|
||||||
if inGap {
|
if inGap {
|
||||||
score += scoreGapExtention
|
score += scoreGapExtension
|
||||||
} else {
|
} else {
|
||||||
score += scoreGapStart
|
score += scoreGapStart
|
||||||
}
|
}
|
||||||
|
@@ -43,10 +43,10 @@ func TestFuzzyMatch(t *testing.T) {
|
|||||||
for _, fn := range []Algo{FuzzyMatchV1, FuzzyMatchV2} {
|
for _, fn := range []Algo{FuzzyMatchV1, FuzzyMatchV2} {
|
||||||
for _, forward := range []bool{true, false} {
|
for _, forward := range []bool{true, false} {
|
||||||
assertMatch(t, fn, false, forward, "fooBarbaz1", "oBZ", 2, 9,
|
assertMatch(t, fn, false, forward, "fooBarbaz1", "oBZ", 2, 9,
|
||||||
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtention*3)
|
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3)
|
||||||
assertMatch(t, fn, false, forward, "foo bar baz", "fbb", 0, 9,
|
assertMatch(t, fn, false, forward, "foo bar baz", "fbb", 0, 9,
|
||||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
|
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
|
||||||
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtention)
|
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtension)
|
||||||
assertMatch(t, fn, false, forward, "/AutomatorDocument.icns", "rdoc", 9, 13,
|
assertMatch(t, fn, false, forward, "/AutomatorDocument.icns", "rdoc", 9, 13,
|
||||||
scoreMatch*4+bonusCamel123+bonusConsecutive*2)
|
scoreMatch*4+bonusCamel123+bonusConsecutive*2)
|
||||||
assertMatch(t, fn, false, forward, "/man1/zshcompctl.1", "zshc", 6, 10,
|
assertMatch(t, fn, false, forward, "/man1/zshcompctl.1", "zshc", 6, 10,
|
||||||
@@ -54,18 +54,18 @@ func TestFuzzyMatch(t *testing.T) {
|
|||||||
assertMatch(t, fn, false, forward, "/.oh-my-zsh/cache", "zshc", 8, 13,
|
assertMatch(t, fn, false, forward, "/.oh-my-zsh/cache", "zshc", 8, 13,
|
||||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3+scoreGapStart)
|
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3+scoreGapStart)
|
||||||
assertMatch(t, fn, false, forward, "ab0123 456", "12356", 3, 10,
|
assertMatch(t, fn, false, forward, "ab0123 456", "12356", 3, 10,
|
||||||
scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtention)
|
scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtension)
|
||||||
assertMatch(t, fn, false, forward, "abc123 456", "12356", 3, 10,
|
assertMatch(t, fn, false, forward, "abc123 456", "12356", 3, 10,
|
||||||
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtention)
|
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtension)
|
||||||
assertMatch(t, fn, false, forward, "foo/bar/baz", "fbb", 0, 9,
|
assertMatch(t, fn, false, forward, "foo/bar/baz", "fbb", 0, 9,
|
||||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
|
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
|
||||||
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtention)
|
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtension)
|
||||||
assertMatch(t, fn, false, forward, "fooBarBaz", "fbb", 0, 7,
|
assertMatch(t, fn, false, forward, "fooBarBaz", "fbb", 0, 7,
|
||||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
|
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
|
||||||
bonusCamel123*2+2*scoreGapStart+2*scoreGapExtention)
|
bonusCamel123*2+2*scoreGapStart+2*scoreGapExtension)
|
||||||
assertMatch(t, fn, false, forward, "foo barbaz", "fbb", 0, 8,
|
assertMatch(t, fn, false, forward, "foo barbaz", "fbb", 0, 8,
|
||||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary+
|
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary+
|
||||||
scoreGapStart*2+scoreGapExtention*3)
|
scoreGapStart*2+scoreGapExtension*3)
|
||||||
assertMatch(t, fn, false, forward, "fooBar Baz", "foob", 0, 4,
|
assertMatch(t, fn, false, forward, "fooBar Baz", "foob", 0, 4,
|
||||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3)
|
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3)
|
||||||
assertMatch(t, fn, false, forward, "xFoo-Bar Baz", "foo-b", 1, 6,
|
assertMatch(t, fn, false, forward, "xFoo-Bar Baz", "foo-b", 1, 6,
|
||||||
@@ -73,13 +73,13 @@ func TestFuzzyMatch(t *testing.T) {
|
|||||||
bonusNonWord+bonusBoundary)
|
bonusNonWord+bonusBoundary)
|
||||||
|
|
||||||
assertMatch(t, fn, true, forward, "fooBarbaz", "oBz", 2, 9,
|
assertMatch(t, fn, true, forward, "fooBarbaz", "oBz", 2, 9,
|
||||||
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtention*3)
|
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3)
|
||||||
assertMatch(t, fn, true, forward, "Foo/Bar/Baz", "FBB", 0, 9,
|
assertMatch(t, fn, true, forward, "Foo/Bar/Baz", "FBB", 0, 9,
|
||||||
scoreMatch*3+bonusBoundary*(bonusFirstCharMultiplier+2)+
|
scoreMatch*3+bonusBoundary*(bonusFirstCharMultiplier+2)+
|
||||||
scoreGapStart*2+scoreGapExtention*4)
|
scoreGapStart*2+scoreGapExtension*4)
|
||||||
assertMatch(t, fn, true, forward, "FooBarBaz", "FBB", 0, 7,
|
assertMatch(t, fn, true, forward, "FooBarBaz", "FBB", 0, 7,
|
||||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusCamel123*2+
|
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusCamel123*2+
|
||||||
scoreGapStart*2+scoreGapExtention*2)
|
scoreGapStart*2+scoreGapExtension*2)
|
||||||
assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4,
|
assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4,
|
||||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+
|
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+
|
||||||
util.Max(bonusCamel123, bonusBoundary))
|
util.Max(bonusCamel123, bonusBoundary))
|
||||||
@@ -99,7 +99,7 @@ func TestFuzzyMatch(t *testing.T) {
|
|||||||
func TestFuzzyMatchBackward(t *testing.T) {
|
func TestFuzzyMatchBackward(t *testing.T) {
|
||||||
assertMatch(t, FuzzyMatchV1, false, true, "foobar fb", "fb", 0, 4,
|
assertMatch(t, FuzzyMatchV1, false, true, "foobar fb", "fb", 0, 4,
|
||||||
scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+
|
scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+
|
||||||
scoreGapStart+scoreGapExtention)
|
scoreGapStart+scoreGapExtension)
|
||||||
assertMatch(t, FuzzyMatchV1, false, false, "foobar fb", "fb", 7, 9,
|
assertMatch(t, FuzzyMatchV1, false, false, "foobar fb", "fb", 7, 9,
|
||||||
scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary)
|
scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary)
|
||||||
}
|
}
|
||||||
|
253
src/ansi.go
253
src/ansi.go
@@ -1,8 +1,6 @@
|
|||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
@@ -82,73 +80,154 @@ func toAnsiString(color tui.Color, offset int) string {
|
|||||||
return ret + ";"
|
return ret + ";"
|
||||||
}
|
}
|
||||||
|
|
||||||
var ansiRegex *regexp.Regexp
|
func isPrint(c uint8) bool {
|
||||||
|
return '\x20' <= c && c <= '\x7e'
|
||||||
func init() {
|
|
||||||
/*
|
|
||||||
References:
|
|
||||||
- https://github.com/gnachman/iTerm2
|
|
||||||
- http://ascii-table.com/ansi-escape-sequences.php
|
|
||||||
- http://ascii-table.com/ansi-escape-sequences-vt-100.php
|
|
||||||
- http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
|
|
||||||
- https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
|
||||||
*/
|
|
||||||
// The following regular expression will include not all but most of the
|
|
||||||
// frequently used ANSI sequences
|
|
||||||
ansiRegex = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func findAnsiStart(str string) int {
|
func matchOperatingSystemCommand(s string) int {
|
||||||
idx := 0
|
// `\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)`
|
||||||
for ; idx < len(str); idx++ {
|
// ^ match starting here
|
||||||
b := str[idx]
|
//
|
||||||
if b == 0x1b || b == 0x0e || b == 0x0f {
|
i := 5 // prefix matched in nextAnsiEscapeSequence()
|
||||||
return idx
|
for ; i < len(s) && isPrint(s[i]); i++ {
|
||||||
}
|
}
|
||||||
if b == 0x08 && idx > 0 {
|
if i < len(s) {
|
||||||
return idx - 1
|
if s[i] == '\x07' {
|
||||||
|
return i + 1
|
||||||
|
}
|
||||||
|
if s[i] == '\x1b' && i < len(s)-1 && s[i+1] == '\\' {
|
||||||
|
return i + 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return idx
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchControlSequence(s string) int {
|
||||||
|
// `\x1b[\\[()][0-9;?]*[a-zA-Z@]`
|
||||||
|
// ^ match starting here
|
||||||
|
//
|
||||||
|
i := 2 // prefix matched in nextAnsiEscapeSequence()
|
||||||
|
for ; i < len(s) && (isNumeric(s[i]) || s[i] == ';' || s[i] == '?'); i++ {
|
||||||
|
}
|
||||||
|
if i < len(s) {
|
||||||
|
c := s[i]
|
||||||
|
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '@' {
|
||||||
|
return i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCtrlSeqStart(c uint8) bool {
|
||||||
|
return c == '\\' || c == '[' || c == '(' || c == ')'
|
||||||
|
}
|
||||||
|
|
||||||
|
// nextAnsiEscapeSequence returns the ANSI escape sequence and is equivalent to
|
||||||
|
// calling FindStringIndex() on the below regex (which was originally used):
|
||||||
|
//
|
||||||
|
// "(?:\x1b[\\[()][0-9;?]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)"
|
||||||
|
//
|
||||||
|
func nextAnsiEscapeSequence(s string) (int, int) {
|
||||||
|
// fast check for ANSI escape sequences
|
||||||
|
i := 0
|
||||||
|
for ; i < len(s); i++ {
|
||||||
|
switch s[i] {
|
||||||
|
case '\x0e', '\x0f', '\x1b', '\x08':
|
||||||
|
// We ignore the fact that '\x08' cannot be the first char
|
||||||
|
// in the string and be an escape sequence for the sake of
|
||||||
|
// speed and simplicity.
|
||||||
|
goto Loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1, -1
|
||||||
|
|
||||||
|
Loop:
|
||||||
|
for ; i < len(s); i++ {
|
||||||
|
switch s[i] {
|
||||||
|
case '\x08':
|
||||||
|
// backtrack to match: `.\x08`
|
||||||
|
if i > 0 && s[i-1] != '\n' {
|
||||||
|
if s[i-1] < utf8.RuneSelf {
|
||||||
|
return i - 1, i + 1
|
||||||
|
}
|
||||||
|
_, n := utf8.DecodeLastRuneInString(s[:i])
|
||||||
|
return i - n, i + 1
|
||||||
|
}
|
||||||
|
case '\x1b':
|
||||||
|
// match: `\x1b[\\[()][0-9;?]*[a-zA-Z@]`
|
||||||
|
if i+2 < len(s) && isCtrlSeqStart(s[i+1]) {
|
||||||
|
if j := matchControlSequence(s[i:]); j != -1 {
|
||||||
|
return i, i + j
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// match: `\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)`
|
||||||
|
if i+5 < len(s) && s[i+1] == ']' && isNumeric(s[i+2]) &&
|
||||||
|
s[i+3] == ';' && isPrint(s[i+4]) {
|
||||||
|
|
||||||
|
if j := matchOperatingSystemCommand(s[i:]); j != -1 {
|
||||||
|
return i, i + j
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// match: `\x1b.`
|
||||||
|
if i+1 < len(s) && s[i+1] != '\n' {
|
||||||
|
if s[i+1] < utf8.RuneSelf {
|
||||||
|
return i, i + 2
|
||||||
|
}
|
||||||
|
_, n := utf8.DecodeRuneInString(s[i+1:])
|
||||||
|
return i, i + n + 1
|
||||||
|
}
|
||||||
|
case '\x0e', '\x0f':
|
||||||
|
// match: `[\x0e\x0f]`
|
||||||
|
return i, i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
|
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
|
||||||
var offsets []ansiOffset
|
// We append to a stack allocated variable that we'll
|
||||||
var output bytes.Buffer
|
// later copy and return, to save on allocations.
|
||||||
|
offsets := make([]ansiOffset, 0, 32)
|
||||||
|
|
||||||
if state != nil {
|
if state != nil {
|
||||||
offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state})
|
offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state})
|
||||||
}
|
}
|
||||||
|
|
||||||
prevIdx := 0
|
var (
|
||||||
runeCount := 0
|
pstate *ansiState // lazily allocated
|
||||||
|
output strings.Builder
|
||||||
|
prevIdx int
|
||||||
|
runeCount int
|
||||||
|
)
|
||||||
for idx := 0; idx < len(str); {
|
for idx := 0; idx < len(str); {
|
||||||
idx += findAnsiStart(str[idx:])
|
// Make sure that we found an ANSI code
|
||||||
if idx == len(str) {
|
start, end := nextAnsiEscapeSequence(str[idx:])
|
||||||
|
if start == -1 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
start += idx
|
||||||
// Make sure that we found an ANSI code
|
idx += end
|
||||||
offset := ansiRegex.FindStringIndex(str[idx:])
|
|
||||||
if len(offset) < 2 {
|
|
||||||
idx++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
offset[0] += idx
|
|
||||||
offset[1] += idx
|
|
||||||
idx = offset[1]
|
|
||||||
|
|
||||||
// Check if we should continue
|
// Check if we should continue
|
||||||
prev := str[prevIdx:offset[0]]
|
prev := str[prevIdx:start]
|
||||||
if proc != nil && !proc(prev, state) {
|
if proc != nil && !proc(prev, state) {
|
||||||
return "", nil, nil
|
return "", nil, nil
|
||||||
}
|
}
|
||||||
|
prevIdx = idx
|
||||||
|
|
||||||
prevIdx = offset[1]
|
if len(prev) != 0 {
|
||||||
runeCount += utf8.RuneCountInString(prev)
|
runeCount += utf8.RuneCountInString(prev)
|
||||||
|
// Grow the buffer size to the maximum possible length (string length
|
||||||
|
// containing ansi codes) to avoid repetitive allocation
|
||||||
|
if output.Cap() == 0 {
|
||||||
|
output.Grow(len(str))
|
||||||
|
}
|
||||||
output.WriteString(prev)
|
output.WriteString(prev)
|
||||||
|
}
|
||||||
|
|
||||||
newState := interpretCode(str[offset[0]:offset[1]], state)
|
newState := interpretCode(str[start:idx], state)
|
||||||
if !newState.equals(state) {
|
if !newState.equals(state) {
|
||||||
if state != nil {
|
if state != nil {
|
||||||
// Update last offset
|
// Update last offset
|
||||||
@@ -157,8 +236,15 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
|
|||||||
|
|
||||||
if newState.colored() {
|
if newState.colored() {
|
||||||
// Append new offset
|
// Append new offset
|
||||||
state = newState
|
if pstate == nil {
|
||||||
offsets = append(offsets, ansiOffset{[2]int32{int32(runeCount), int32(runeCount)}, *state})
|
pstate = &ansiState{}
|
||||||
|
}
|
||||||
|
*pstate = newState
|
||||||
|
state = pstate
|
||||||
|
offsets = append(offsets, ansiOffset{
|
||||||
|
[2]int32{int32(runeCount), int32(runeCount)},
|
||||||
|
newState,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
// Discard state
|
// Discard state
|
||||||
state = nil
|
state = nil
|
||||||
@@ -168,7 +254,6 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
|
|||||||
|
|
||||||
var rest string
|
var rest string
|
||||||
var trimmed string
|
var trimmed string
|
||||||
|
|
||||||
if prevIdx == 0 {
|
if prevIdx == 0 {
|
||||||
// No ANSI code found
|
// No ANSI code found
|
||||||
rest = str
|
rest = str
|
||||||
@@ -178,51 +263,75 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
|
|||||||
output.WriteString(rest)
|
output.WriteString(rest)
|
||||||
trimmed = output.String()
|
trimmed = output.String()
|
||||||
}
|
}
|
||||||
|
if proc != nil {
|
||||||
|
proc(rest, state)
|
||||||
|
}
|
||||||
|
if len(offsets) > 0 {
|
||||||
if len(rest) > 0 && state != nil {
|
if len(rest) > 0 && state != nil {
|
||||||
// Update last offset
|
// Update last offset
|
||||||
runeCount += utf8.RuneCountInString(rest)
|
runeCount += utf8.RuneCountInString(rest)
|
||||||
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
|
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
|
||||||
}
|
}
|
||||||
if proc != nil {
|
// Return a copy of the offsets slice
|
||||||
proc(rest, state)
|
a := make([]ansiOffset, len(offsets))
|
||||||
|
copy(a, offsets)
|
||||||
|
return trimmed, &a, state
|
||||||
}
|
}
|
||||||
if len(offsets) == 0 {
|
|
||||||
return trimmed, nil, state
|
return trimmed, nil, state
|
||||||
}
|
|
||||||
return trimmed, &offsets, state
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
|
func parseAnsiCode(s string) (int, string) {
|
||||||
// State
|
var remaining string
|
||||||
var state *ansiState
|
if i := strings.IndexByte(s, ';'); i >= 0 {
|
||||||
|
remaining = s[i+1:]
|
||||||
|
s = s[:i]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s) > 0 {
|
||||||
|
// Inlined version of strconv.Atoi() that only handles positive
|
||||||
|
// integers and does not allocate on error.
|
||||||
|
code := 0
|
||||||
|
for _, ch := range []byte(s) {
|
||||||
|
ch -= '0'
|
||||||
|
if ch > 9 {
|
||||||
|
return -1, remaining
|
||||||
|
}
|
||||||
|
code = code*10 + int(ch)
|
||||||
|
}
|
||||||
|
return code, remaining
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1, remaining
|
||||||
|
}
|
||||||
|
|
||||||
|
func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||||
|
var state ansiState
|
||||||
if prevState == nil {
|
if prevState == nil {
|
||||||
state = &ansiState{-1, -1, 0, -1}
|
state = ansiState{-1, -1, 0, -1}
|
||||||
} else {
|
} else {
|
||||||
state = &ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg}
|
state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg}
|
||||||
}
|
}
|
||||||
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
|
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
|
||||||
if strings.HasSuffix(ansiCode, "0K") {
|
if prevState != nil && strings.HasSuffix(ansiCode, "0K") {
|
||||||
state.lbg = prevState.bg
|
state.lbg = prevState.bg
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
ptr := &state.fg
|
if len(ansiCode) <= 3 {
|
||||||
state256 := 0
|
|
||||||
|
|
||||||
init := func() {
|
|
||||||
state.fg = -1
|
state.fg = -1
|
||||||
state.bg = -1
|
state.bg = -1
|
||||||
state.attr = 0
|
state.attr = 0
|
||||||
state256 = 0
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
ansiCode = ansiCode[2 : len(ansiCode)-1]
|
ansiCode = ansiCode[2 : len(ansiCode)-1]
|
||||||
if len(ansiCode) == 0 {
|
|
||||||
init()
|
state256 := 0
|
||||||
}
|
ptr := &state.fg
|
||||||
for _, code := range strings.Split(ansiCode, ";") {
|
|
||||||
if num, err := strconv.Atoi(code); err == nil {
|
for len(ansiCode) != 0 {
|
||||||
|
var num int
|
||||||
|
if num, ansiCode = parseAnsiCode(ansiCode); num != -1 {
|
||||||
switch state256 {
|
switch state256 {
|
||||||
case 0:
|
case 0:
|
||||||
switch num {
|
switch num {
|
||||||
@@ -253,7 +362,10 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
|
|||||||
case 24: // tput rmul
|
case 24: // tput rmul
|
||||||
state.attr = state.attr &^ tui.Underline
|
state.attr = state.attr &^ tui.Underline
|
||||||
case 0:
|
case 0:
|
||||||
init()
|
state.fg = -1
|
||||||
|
state.bg = -1
|
||||||
|
state.attr = 0
|
||||||
|
state256 = 0
|
||||||
default:
|
default:
|
||||||
if num >= 30 && num <= 37 {
|
if num >= 30 && num <= 37 {
|
||||||
state.fg = tui.Color(num - 30)
|
state.fg = tui.Color(num - 30)
|
||||||
@@ -289,6 +401,7 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if state256 > 0 {
|
if state256 > 0 {
|
||||||
*ptr = -1
|
*ptr = -1
|
||||||
}
|
}
|
||||||
|
244
src/ansi_test.go
244
src/ansi_test.go
@@ -1,13 +1,192 @@
|
|||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"math/rand"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/tui"
|
"github.com/junegunn/fzf/src/tui"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// The following regular expression will include not all but most of the
|
||||||
|
// frequently used ANSI sequences. This regex is used as a reference for
|
||||||
|
// testing nextAnsiEscapeSequence().
|
||||||
|
//
|
||||||
|
// References:
|
||||||
|
// - https://github.com/gnachman/iTerm2
|
||||||
|
// - https://web.archive.org/web/20090204053813/http://ascii-table.com/ansi-escape-sequences.php
|
||||||
|
// (archived from http://ascii-table.com/ansi-escape-sequences.php)
|
||||||
|
// - https://web.archive.org/web/20090227051140/http://ascii-table.com/ansi-escape-sequences-vt-100.php
|
||||||
|
// (archived from http://ascii-table.com/ansi-escape-sequences-vt-100.php)
|
||||||
|
// - http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
|
||||||
|
// - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||||
|
var ansiRegexReference = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)")
|
||||||
|
|
||||||
|
func testParserReference(t testing.TB, str string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
toSlice := func(start, end int) []int {
|
||||||
|
if start == -1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []int{start, end}
|
||||||
|
}
|
||||||
|
|
||||||
|
s := str
|
||||||
|
for i := 0; ; i++ {
|
||||||
|
got := toSlice(nextAnsiEscapeSequence(s))
|
||||||
|
exp := ansiRegexReference.FindStringIndex(s)
|
||||||
|
|
||||||
|
equal := len(got) == len(exp)
|
||||||
|
if equal {
|
||||||
|
for i := 0; i < len(got); i++ {
|
||||||
|
if got[i] != exp[i] {
|
||||||
|
equal = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !equal {
|
||||||
|
var exps, gots []rune
|
||||||
|
if len(got) == 2 {
|
||||||
|
gots = []rune(s[got[0]:got[1]])
|
||||||
|
}
|
||||||
|
if len(exp) == 2 {
|
||||||
|
exps = []rune(s[exp[0]:exp[1]])
|
||||||
|
}
|
||||||
|
t.Errorf("%d: %q: got: %v (%q) want: %v (%q)", i, s, got, gots, exp, exps)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(exp) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s = s[exp[1]:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextAnsiEscapeSequence(t *testing.T) {
|
||||||
|
testStrs := []string{
|
||||||
|
"\x1b[0mhello world",
|
||||||
|
"\x1b[1mhello world",
|
||||||
|
"椙\x1b[1m椙",
|
||||||
|
"椙\x1b[1椙m椙",
|
||||||
|
"\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d",
|
||||||
|
"\x1b[1mhello \x1b[Kworld",
|
||||||
|
"hello \x1b[34;45;1mworld",
|
||||||
|
"hello \x1b[34;45;1mwor\x1b[34;45;1mld",
|
||||||
|
"hello \x1b[34;45;1mwor\x1b[0mld",
|
||||||
|
"hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md",
|
||||||
|
"hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md",
|
||||||
|
"hello \x1b[32;1mworld",
|
||||||
|
"hello world",
|
||||||
|
"hello \x1b[0;38;5;200;48;5;100mworld",
|
||||||
|
"\x1b椙",
|
||||||
|
"椙\x08",
|
||||||
|
"\n\x08",
|
||||||
|
"X\x08",
|
||||||
|
"",
|
||||||
|
"\x1b]4;3;rgb:aa/bb/cc\x07 ",
|
||||||
|
"\x1b]4;3;rgb:aa/bb/cc\x1b\\ ",
|
||||||
|
ansiBenchmarkString,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range testStrs {
|
||||||
|
testParserReference(t, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextAnsiEscapeSequence_Fuzz_Modified(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("short test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testStrs := []string{
|
||||||
|
"\x1b[0mhello world",
|
||||||
|
"\x1b[1mhello world",
|
||||||
|
"椙\x1b[1m椙",
|
||||||
|
"椙\x1b[1椙m椙",
|
||||||
|
"\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d",
|
||||||
|
"\x1b[1mhello \x1b[Kworld",
|
||||||
|
"hello \x1b[34;45;1mworld",
|
||||||
|
"hello \x1b[34;45;1mwor\x1b[34;45;1mld",
|
||||||
|
"hello \x1b[34;45;1mwor\x1b[0mld",
|
||||||
|
"hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md",
|
||||||
|
"hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md",
|
||||||
|
"hello \x1b[32;1mworld",
|
||||||
|
"hello world",
|
||||||
|
"hello \x1b[0;38;5;200;48;5;100mworld",
|
||||||
|
ansiBenchmarkString,
|
||||||
|
}
|
||||||
|
|
||||||
|
replacementBytes := [...]rune{'\x0e', '\x0f', '\x1b', '\x08'}
|
||||||
|
|
||||||
|
modifyString := func(s string, rr *rand.Rand) string {
|
||||||
|
n := rr.Intn(len(s))
|
||||||
|
b := []rune(s)
|
||||||
|
for ; n >= 0 && len(b) != 0; n-- {
|
||||||
|
i := rr.Intn(len(b))
|
||||||
|
switch x := rr.Intn(4); x {
|
||||||
|
case 0:
|
||||||
|
b = append(b[:i], b[i+1:]...)
|
||||||
|
case 1:
|
||||||
|
j := rr.Intn(len(replacementBytes) - 1)
|
||||||
|
b[i] = replacementBytes[j]
|
||||||
|
case 2:
|
||||||
|
x := rune(rr.Intn(utf8.MaxRune))
|
||||||
|
for !utf8.ValidRune(x) {
|
||||||
|
x = rune(rr.Intn(utf8.MaxRune))
|
||||||
|
}
|
||||||
|
b[i] = x
|
||||||
|
case 3:
|
||||||
|
b[i] = rune(rr.Intn(utf8.MaxRune)) // potentially invalid
|
||||||
|
default:
|
||||||
|
t.Fatalf("unsupported value: %d", x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
rr := rand.New(rand.NewSource(1))
|
||||||
|
for _, s := range testStrs {
|
||||||
|
for i := 1_000; i >= 0; i-- {
|
||||||
|
testParserReference(t, modifyString(s, rr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextAnsiEscapeSequence_Fuzz_Random(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("short test")
|
||||||
|
}
|
||||||
|
|
||||||
|
randomString := func(rr *rand.Rand) string {
|
||||||
|
numChars := rand.Intn(50)
|
||||||
|
codePoints := make([]rune, numChars)
|
||||||
|
for i := 0; i < len(codePoints); i++ {
|
||||||
|
var r rune
|
||||||
|
for n := 0; n < 1000; n++ {
|
||||||
|
r = rune(rr.Intn(utf8.MaxRune))
|
||||||
|
// Allow 10% of runes to be invalid
|
||||||
|
if utf8.ValidRune(r) || rr.Float64() < 0.10 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
codePoints[i] = r
|
||||||
|
}
|
||||||
|
return string(codePoints)
|
||||||
|
}
|
||||||
|
|
||||||
|
rr := rand.New(rand.NewSource(1))
|
||||||
|
for i := 0; i < 100_000; i++ {
|
||||||
|
testParserReference(t, randomString(rr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExtractColor(t *testing.T) {
|
func TestExtractColor(t *testing.T) {
|
||||||
assert := func(offset ansiOffset, b int32, e int32, fg tui.Color, bg tui.Color, bold bool) {
|
assert := func(offset ansiOffset, b int32, e int32, fg tui.Color, bg tui.Color, bold bool) {
|
||||||
var attr tui.Attr
|
var attr tui.Attr
|
||||||
@@ -29,7 +208,7 @@ func TestExtractColor(t *testing.T) {
|
|||||||
if output != "hello world" {
|
if output != "hello world" {
|
||||||
t.Errorf("Invalid output: %s %v", output, []rune(output))
|
t.Errorf("Invalid output: %s %v", output, []rune(output))
|
||||||
}
|
}
|
||||||
fmt.Println(src, ansiOffsets, clean)
|
t.Log(src, ansiOffsets, clean)
|
||||||
assertion(ansiOffsets, state)
|
assertion(ansiOffsets, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,3 +364,64 @@ func TestAnsiCodeStringConversion(t *testing.T) {
|
|||||||
&ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1},
|
&ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1},
|
||||||
"\x1b[2;3;7;38;2;10;20;30;48;5;100m")
|
"\x1b[2;3;7;38;2;10;20;30;48;5;100m")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseAnsiCode(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
In, Exp string
|
||||||
|
N int
|
||||||
|
}{
|
||||||
|
{"123", "", 123},
|
||||||
|
{"1a", "", -1},
|
||||||
|
{"1a;12", "12", -1},
|
||||||
|
{"12;a", "a", 12},
|
||||||
|
{"-2", "", -1},
|
||||||
|
}
|
||||||
|
for _, x := range tests {
|
||||||
|
n, s := parseAnsiCode(x.In)
|
||||||
|
if n != x.N || s != x.Exp {
|
||||||
|
t.Fatalf("%q: got: (%d %q) want: (%d %q)", x.In, n, s, x.N, x.Exp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// kernel/bpf/preload/iterators/README
|
||||||
|
const ansiBenchmarkString = "\x1b[38;5;81m\x1b[01;31m\x1b[Kkernel/\x1b[0m\x1b[38;5;81mbpf/" +
|
||||||
|
"\x1b[0m\x1b[38;5;81mpreload/\x1b[0m\x1b[38;5;81miterators/" +
|
||||||
|
"\x1b[0m\x1b[38;5;149mMakefile\x1b[m\x1b[K\x1b[0m"
|
||||||
|
|
||||||
|
func BenchmarkNextAnsiEscapeSequence(b *testing.B) {
|
||||||
|
b.SetBytes(int64(len(ansiBenchmarkString)))
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
s := ansiBenchmarkString
|
||||||
|
for {
|
||||||
|
_, o := nextAnsiEscapeSequence(s)
|
||||||
|
if o == -1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
s = s[o:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Baseline test to compare the speed of nextAnsiEscapeSequence() to the
|
||||||
|
// previously used regex based implementation.
|
||||||
|
func BenchmarkNextAnsiEscapeSequence_Regex(b *testing.B) {
|
||||||
|
b.SetBytes(int64(len(ansiBenchmarkString)))
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
s := ansiBenchmarkString
|
||||||
|
for {
|
||||||
|
a := ansiRegexReference.FindStringIndex(s)
|
||||||
|
if len(a) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
s = s[a[1]:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkExtractColor(b *testing.B) {
|
||||||
|
b.SetBytes(int64(len(ansiBenchmarkString)))
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
extractColor(ansiBenchmarkString, nil, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -6,8 +6,8 @@ func TestChunkCache(t *testing.T) {
|
|||||||
cache := NewChunkCache()
|
cache := NewChunkCache()
|
||||||
chunk1p := &Chunk{}
|
chunk1p := &Chunk{}
|
||||||
chunk2p := &Chunk{count: chunkSize}
|
chunk2p := &Chunk{count: chunkSize}
|
||||||
items1 := []Result{Result{}}
|
items1 := []Result{{}}
|
||||||
items2 := []Result{Result{}, Result{}}
|
items2 := []Result{{}, {}}
|
||||||
cache.Add(chunk1p, "foo", items1)
|
cache.Add(chunk1p, "foo", items1)
|
||||||
cache.Add(chunk2p, "foo", items1)
|
cache.Add(chunk2p, "foo", items1)
|
||||||
cache.Add(chunk2p, "bar", items2)
|
cache.Add(chunk2p, "bar", items2)
|
||||||
|
@@ -73,6 +73,7 @@ const (
|
|||||||
EvtSearchFin
|
EvtSearchFin
|
||||||
EvtHeader
|
EvtHeader
|
||||||
EvtReady
|
EvtReady
|
||||||
|
EvtQuit
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@@ -233,6 +233,7 @@ func Run(opts *Options, version string, revision string) {
|
|||||||
clearCache = util.Once(true)
|
clearCache = util.Once(true)
|
||||||
clearSelection = util.Once(true)
|
clearSelection = util.Once(true)
|
||||||
chunkList.Clear()
|
chunkList.Clear()
|
||||||
|
itemIndex = 0
|
||||||
header = make([]string, 0, opts.HeaderLines)
|
header = make([]string, 0, opts.HeaderLines)
|
||||||
go reader.restart(command)
|
go reader.restart(command)
|
||||||
}
|
}
|
||||||
@@ -254,7 +255,11 @@ func Run(opts *Options, version string, revision string) {
|
|||||||
}
|
}
|
||||||
for evt, value := range *events {
|
for evt, value := range *events {
|
||||||
switch evt {
|
switch evt {
|
||||||
|
case EvtQuit:
|
||||||
|
if reading {
|
||||||
|
reader.terminate()
|
||||||
|
}
|
||||||
|
os.Exit(value.(int))
|
||||||
case EvtReadNew, EvtReadFin:
|
case EvtReadNew, EvtReadFin:
|
||||||
if evt == EvtReadFin && nextCommand != nil {
|
if evt == EvtReadFin && nextCommand != nil {
|
||||||
restart(*nextCommand)
|
restart(*nextCommand)
|
||||||
|
@@ -3,7 +3,6 @@ package fzf
|
|||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -12,16 +11,12 @@ func TestHistory(t *testing.T) {
|
|||||||
maxHistory := 50
|
maxHistory := 50
|
||||||
|
|
||||||
// Invalid arguments
|
// Invalid arguments
|
||||||
user, _ := user.Current()
|
|
||||||
var paths []string
|
var paths []string
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
// GOPATH should exist, so we shouldn't be able to override it
|
// GOPATH should exist, so we shouldn't be able to override it
|
||||||
paths = []string{os.Getenv("GOPATH")}
|
paths = []string{os.Getenv("GOPATH")}
|
||||||
} else {
|
} else {
|
||||||
paths = []string{"/etc", "/proc"}
|
paths = []string{"/etc", "/proc"}
|
||||||
if user.Name != "root" {
|
|
||||||
paths = append(paths, "/etc/sudoers")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, path := range paths {
|
for _, path := range paths {
|
||||||
|
@@ -106,7 +106,6 @@ func (mg *Merger) mergedGet(idx int) Result {
|
|||||||
minIdx = listIdx
|
minIdx = listIdx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mg.cursors[listIdx] = cursor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if minIdx >= 0 {
|
if minIdx >= 0 {
|
||||||
|
146
src/options.go
146
src/options.go
@@ -44,8 +44,10 @@ const usage = `usage: fzf [options]
|
|||||||
--bind=KEYBINDS Custom key bindings. Refer to the man page.
|
--bind=KEYBINDS Custom key bindings. Refer to the man page.
|
||||||
--cycle Enable cyclic scroll
|
--cycle Enable cyclic scroll
|
||||||
--keep-right Keep the right end of the line visible on overflow
|
--keep-right Keep the right end of the line visible on overflow
|
||||||
|
--scroll-off=LINES Number of screen lines to keep above or below when
|
||||||
|
scrolling to the top or to the bottom (default: 0)
|
||||||
--no-hscroll Disable horizontal scroll
|
--no-hscroll Disable horizontal scroll
|
||||||
--hscroll-off=COL Number of screen columns to keep to the right of the
|
--hscroll-off=COLS Number of screen columns to keep to the right of the
|
||||||
highlighted substring (default: 10)
|
highlighted substring (default: 10)
|
||||||
--filepath-word Make word-wise movements respect path separators
|
--filepath-word Make word-wise movements respect path separators
|
||||||
--jump-labels=CHARS Label characters for jump and jump-accept
|
--jump-labels=CHARS Label characters for jump and jump-accept
|
||||||
@@ -58,7 +60,7 @@ const usage = `usage: fzf [options]
|
|||||||
--layout=LAYOUT Choose layout: [default|reverse|reverse-list]
|
--layout=LAYOUT Choose layout: [default|reverse|reverse-list]
|
||||||
--border[=STYLE] Draw border around the finder
|
--border[=STYLE] Draw border around the finder
|
||||||
[rounded|sharp|horizontal|vertical|
|
[rounded|sharp|horizontal|vertical|
|
||||||
top|bottom|left|right] (default: rounded)
|
top|bottom|left|right|none] (default: rounded)
|
||||||
--margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L)
|
--margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L)
|
||||||
--padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
|
--padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
|
||||||
--info=STYLE Finder info style [default|inline|hidden]
|
--info=STYLE Finder info style [default|inline|hidden]
|
||||||
@@ -67,6 +69,7 @@ const usage = `usage: fzf [options]
|
|||||||
--marker=STR Multi-select marker (default: '>')
|
--marker=STR Multi-select marker (default: '>')
|
||||||
--header=STR String to print as header
|
--header=STR String to print as header
|
||||||
--header-lines=N The first N lines of the input are treated as header
|
--header-lines=N The first N lines of the input are treated as header
|
||||||
|
--header-first Print header before the prompt line
|
||||||
|
|
||||||
Display
|
Display
|
||||||
--ansi Enable processing of ANSI color codes
|
--ansi Enable processing of ANSI color codes
|
||||||
@@ -81,11 +84,11 @@ const usage = `usage: fzf [options]
|
|||||||
Preview
|
Preview
|
||||||
--preview=COMMAND Command to preview highlighted line ({})
|
--preview=COMMAND Command to preview highlighted line ({})
|
||||||
--preview-window=OPT Preview window layout (default: right:50%)
|
--preview-window=OPT Preview window layout (default: right:50%)
|
||||||
[up|down|left|right][:SIZE[%]]
|
[up|down|left|right][,SIZE[%]]
|
||||||
[:[no]wrap][:[no]cycle][:[no]follow][:[no]hidden]
|
[,[no]wrap][,[no]cycle][,[no]follow][,[no]hidden]
|
||||||
[:rounded|sharp|noborder]
|
[,border-BORDER_OPT]
|
||||||
[:+SCROLL[-OFFSET]]
|
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
|
||||||
[:default]
|
[,default]
|
||||||
|
|
||||||
Scripting
|
Scripting
|
||||||
-q, --query=STR Start the finder with the given query
|
-q, --query=STR Start the finder with the given query
|
||||||
@@ -170,6 +173,7 @@ type previewOpts struct {
|
|||||||
cycle bool
|
cycle bool
|
||||||
follow bool
|
follow bool
|
||||||
border tui.BorderShape
|
border tui.BorderShape
|
||||||
|
headerLines int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options stores the values of command-line options
|
// Options stores the values of command-line options
|
||||||
@@ -199,6 +203,7 @@ type Options struct {
|
|||||||
KeepRight bool
|
KeepRight bool
|
||||||
Hscroll bool
|
Hscroll bool
|
||||||
HscrollOff int
|
HscrollOff int
|
||||||
|
ScrollOff int
|
||||||
FileWord bool
|
FileWord bool
|
||||||
InfoStyle infoStyle
|
InfoStyle infoStyle
|
||||||
JumpLabels string
|
JumpLabels string
|
||||||
@@ -221,6 +226,7 @@ type Options struct {
|
|||||||
History *History
|
History *History
|
||||||
Header []string
|
Header []string
|
||||||
HeaderLines int
|
HeaderLines int
|
||||||
|
HeaderFirst bool
|
||||||
Margin [4]sizeSpec
|
Margin [4]sizeSpec
|
||||||
Padding [4]sizeSpec
|
Padding [4]sizeSpec
|
||||||
BorderShape tui.BorderShape
|
BorderShape tui.BorderShape
|
||||||
@@ -231,7 +237,7 @@ type Options struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func defaultPreviewOpts(command string) previewOpts {
|
func defaultPreviewOpts(command string) previewOpts {
|
||||||
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded}
|
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded, 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultOptions() *Options {
|
func defaultOptions() *Options {
|
||||||
@@ -260,6 +266,7 @@ func defaultOptions() *Options {
|
|||||||
KeepRight: false,
|
KeepRight: false,
|
||||||
Hscroll: true,
|
Hscroll: true,
|
||||||
HscrollOff: 10,
|
HscrollOff: 10,
|
||||||
|
ScrollOff: 0,
|
||||||
FileWord: false,
|
FileWord: false,
|
||||||
InfoStyle: infoDefault,
|
InfoStyle: infoDefault,
|
||||||
JumpLabels: defaultJumpLabels,
|
JumpLabels: defaultJumpLabels,
|
||||||
@@ -282,6 +289,7 @@ func defaultOptions() *Options {
|
|||||||
History: nil,
|
History: nil,
|
||||||
Header: make([]string, 0),
|
Header: make([]string, 0),
|
||||||
HeaderLines: 0,
|
HeaderLines: 0,
|
||||||
|
HeaderFirst: false,
|
||||||
Margin: defaultMargin(),
|
Margin: defaultMargin(),
|
||||||
Padding: defaultMargin(),
|
Padding: defaultMargin(),
|
||||||
Unicode: true,
|
Unicode: true,
|
||||||
@@ -435,11 +443,13 @@ func parseBorder(str string, optional bool) tui.BorderShape {
|
|||||||
return tui.BorderLeft
|
return tui.BorderLeft
|
||||||
case "right":
|
case "right":
|
||||||
return tui.BorderRight
|
return tui.BorderRight
|
||||||
|
case "none":
|
||||||
|
return tui.BorderNone
|
||||||
default:
|
default:
|
||||||
if optional && str == "" {
|
if optional && str == "" {
|
||||||
return tui.BorderRounded
|
return tui.BorderRounded
|
||||||
}
|
}
|
||||||
errorExit("invalid border style (expected: rounded|sharp|horizontal|vertical|top|bottom|left|right)")
|
errorExit("invalid border style (expected: rounded|sharp|horizontal|vertical|top|bottom|left|right|none)")
|
||||||
}
|
}
|
||||||
return tui.BorderNone
|
return tui.BorderNone
|
||||||
}
|
}
|
||||||
@@ -449,7 +459,7 @@ func parseKeyChords(str string, message string) map[tui.Event]string {
|
|||||||
errorExit(message)
|
errorExit(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
str = regexp.MustCompile("(?i)(alt-),").ReplaceAllString(str, "$1"+string(escapedComma))
|
str = regexp.MustCompile("(?i)(alt-),").ReplaceAllString(str, "$1"+string([]rune{escapedComma}))
|
||||||
tokens := strings.Split(str, ",")
|
tokens := strings.Split(str, ",")
|
||||||
if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Contains(str, ",,,") {
|
if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Contains(str, ",,,") {
|
||||||
tokens = append(tokens, ",")
|
tokens = append(tokens, ",")
|
||||||
@@ -460,7 +470,7 @@ func parseKeyChords(str string, message string) map[tui.Event]string {
|
|||||||
if len(key) == 0 {
|
if len(key) == 0 {
|
||||||
continue // ignore
|
continue // ignore
|
||||||
}
|
}
|
||||||
key = strings.ReplaceAll(key, string(escapedComma), ",")
|
key = strings.ReplaceAll(key, string([]rune{escapedComma}), ",")
|
||||||
lkey := strings.ToLower(key)
|
lkey := strings.ToLower(key)
|
||||||
add := func(e tui.EventType) {
|
add := func(e tui.EventType) {
|
||||||
chords[e.AsEvent()] = key
|
chords[e.AsEvent()] = key
|
||||||
@@ -667,6 +677,38 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
|
|||||||
cattr.Attr |= tui.Blink
|
cattr.Attr |= tui.Blink
|
||||||
case "reverse":
|
case "reverse":
|
||||||
cattr.Attr |= tui.Reverse
|
cattr.Attr |= tui.Reverse
|
||||||
|
case "black":
|
||||||
|
cattr.Color = tui.Color(0)
|
||||||
|
case "red":
|
||||||
|
cattr.Color = tui.Color(1)
|
||||||
|
case "green":
|
||||||
|
cattr.Color = tui.Color(2)
|
||||||
|
case "yellow":
|
||||||
|
cattr.Color = tui.Color(3)
|
||||||
|
case "blue":
|
||||||
|
cattr.Color = tui.Color(4)
|
||||||
|
case "magenta":
|
||||||
|
cattr.Color = tui.Color(5)
|
||||||
|
case "cyan":
|
||||||
|
cattr.Color = tui.Color(6)
|
||||||
|
case "white":
|
||||||
|
cattr.Color = tui.Color(7)
|
||||||
|
case "bright-black", "gray", "grey":
|
||||||
|
cattr.Color = tui.Color(8)
|
||||||
|
case "bright-red":
|
||||||
|
cattr.Color = tui.Color(9)
|
||||||
|
case "bright-green":
|
||||||
|
cattr.Color = tui.Color(10)
|
||||||
|
case "bright-yellow":
|
||||||
|
cattr.Color = tui.Color(11)
|
||||||
|
case "bright-blue":
|
||||||
|
cattr.Color = tui.Color(12)
|
||||||
|
case "bright-magenta":
|
||||||
|
cattr.Color = tui.Color(13)
|
||||||
|
case "bright-cyan":
|
||||||
|
cattr.Color = tui.Color(14)
|
||||||
|
case "bright-white":
|
||||||
|
cattr.Color = tui.Color(15)
|
||||||
case "":
|
case "":
|
||||||
default:
|
default:
|
||||||
if rrggbb.MatchString(component) {
|
if rrggbb.MatchString(component) {
|
||||||
@@ -745,7 +787,7 @@ func init() {
|
|||||||
// Backreferences are not supported.
|
// Backreferences are not supported.
|
||||||
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
|
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
|
||||||
executeRegexp = regexp.MustCompile(
|
executeRegexp = regexp.MustCompile(
|
||||||
`(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
|
`(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|unbind):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|unbind)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseKeymap(keymap map[tui.Event][]action, str string) {
|
func parseKeymap(keymap map[tui.Event][]action, str string) {
|
||||||
@@ -759,6 +801,8 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
|
|||||||
prefix = symbol + "reload"
|
prefix = symbol + "reload"
|
||||||
} else if strings.HasPrefix(src[1:], "preview") {
|
} else if strings.HasPrefix(src[1:], "preview") {
|
||||||
prefix = symbol + "preview"
|
prefix = symbol + "preview"
|
||||||
|
} else if strings.HasPrefix(src[1:], "unbind") {
|
||||||
|
prefix = symbol + "unbind"
|
||||||
} else if strings.HasPrefix(src[1:], "change-prompt") {
|
} else if strings.HasPrefix(src[1:], "change-prompt") {
|
||||||
prefix = symbol + "change-prompt"
|
prefix = symbol + "change-prompt"
|
||||||
} else if src[len(prefix)] == '-' {
|
} else if src[len(prefix)] == '-' {
|
||||||
@@ -839,6 +883,8 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
|
|||||||
appendAction(actDeleteChar)
|
appendAction(actDeleteChar)
|
||||||
case "delete-char/eof":
|
case "delete-char/eof":
|
||||||
appendAction(actDeleteCharEOF)
|
appendAction(actDeleteCharEOF)
|
||||||
|
case "deselect":
|
||||||
|
appendAction(actDeselect)
|
||||||
case "end-of-line":
|
case "end-of-line":
|
||||||
appendAction(actEndOfLine)
|
appendAction(actEndOfLine)
|
||||||
case "cancel":
|
case "cancel":
|
||||||
@@ -879,10 +925,14 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
|
|||||||
appendAction(actToggleAll)
|
appendAction(actToggleAll)
|
||||||
case "toggle-search":
|
case "toggle-search":
|
||||||
appendAction(actToggleSearch)
|
appendAction(actToggleSearch)
|
||||||
|
case "select":
|
||||||
|
appendAction(actSelect)
|
||||||
case "select-all":
|
case "select-all":
|
||||||
appendAction(actSelectAll)
|
appendAction(actSelectAll)
|
||||||
case "deselect-all":
|
case "deselect-all":
|
||||||
appendAction(actDeselectAll)
|
appendAction(actDeselectAll)
|
||||||
|
case "close":
|
||||||
|
appendAction(actClose)
|
||||||
case "toggle":
|
case "toggle":
|
||||||
appendAction(actToggle)
|
appendAction(actToggle)
|
||||||
case "down":
|
case "down":
|
||||||
@@ -931,6 +981,12 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
|
|||||||
appendAction(actEnableSearch)
|
appendAction(actEnableSearch)
|
||||||
case "disable-search":
|
case "disable-search":
|
||||||
appendAction(actDisableSearch)
|
appendAction(actDisableSearch)
|
||||||
|
case "put":
|
||||||
|
if key.Type == tui.Rune && unicode.IsGraphic(key.Char) {
|
||||||
|
appendAction(actRune)
|
||||||
|
} else {
|
||||||
|
errorExit("unable to put non-printable character: " + pair[0])
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
t := isExecuteAction(specLower)
|
t := isExecuteAction(specLower)
|
||||||
if t == actIgnore {
|
if t == actIgnore {
|
||||||
@@ -948,6 +1004,8 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
|
|||||||
offset = len("preview")
|
offset = len("preview")
|
||||||
case actChangePrompt:
|
case actChangePrompt:
|
||||||
offset = len("change-prompt")
|
offset = len("change-prompt")
|
||||||
|
case actUnbind:
|
||||||
|
offset = len("unbind")
|
||||||
case actExecuteSilent:
|
case actExecuteSilent:
|
||||||
offset = len("execute-silent")
|
offset = len("execute-silent")
|
||||||
case actExecuteMulti:
|
case actExecuteMulti:
|
||||||
@@ -955,15 +1013,21 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
|
|||||||
default:
|
default:
|
||||||
offset = len("execute")
|
offset = len("execute")
|
||||||
}
|
}
|
||||||
|
var actionArg string
|
||||||
if spec[offset] == ':' {
|
if spec[offset] == ':' {
|
||||||
if specIndex == len(specs)-1 {
|
if specIndex == len(specs)-1 {
|
||||||
actions = append(actions, action{t: t, a: spec[offset+1:]})
|
actionArg = spec[offset+1:]
|
||||||
|
actions = append(actions, action{t: t, a: actionArg})
|
||||||
} else {
|
} else {
|
||||||
prevSpec = spec + "+"
|
prevSpec = spec + "+"
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
actions = append(actions, action{t: t, a: spec[offset+1 : len(spec)-1]})
|
actionArg = spec[offset+1 : len(spec)-1]
|
||||||
|
actions = append(actions, action{t: t, a: actionArg})
|
||||||
|
}
|
||||||
|
if t == actUnbind {
|
||||||
|
parseKeyChords(actionArg, "unbind target required")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -985,6 +1049,8 @@ func isExecuteAction(str string) actionType {
|
|||||||
switch prefix {
|
switch prefix {
|
||||||
case "reload":
|
case "reload":
|
||||||
return actReload
|
return actReload
|
||||||
|
case "unbind":
|
||||||
|
return actUnbind
|
||||||
case "preview":
|
case "preview":
|
||||||
return actPreview
|
return actPreview
|
||||||
case "change-prompt":
|
case "change-prompt":
|
||||||
@@ -1069,9 +1135,11 @@ func parseInfoStyle(str string) infoStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parsePreviewWindow(opts *previewOpts, input string) {
|
func parsePreviewWindow(opts *previewOpts, input string) {
|
||||||
tokens := strings.Split(input, ":")
|
delimRegex := regexp.MustCompile("[:,]") // : for backward compatibility
|
||||||
sizeRegex := regexp.MustCompile("^[0-9]+%?$")
|
sizeRegex := regexp.MustCompile("^[0-9]+%?$")
|
||||||
offsetRegex := regexp.MustCompile("^\\+([0-9]+|{-?[0-9]+})(-[0-9]+|-/[1-9][0-9]*)?$")
|
offsetRegex := regexp.MustCompile(`^(\+{-?[0-9]+})?([+-][0-9]+)*(-?/[1-9][0-9]*)?$`)
|
||||||
|
headerRegex := regexp.MustCompile("^~(0|[1-9][0-9]*)$")
|
||||||
|
tokens := delimRegex.Split(input, -1)
|
||||||
for _, token := range tokens {
|
for _, token := range tokens {
|
||||||
switch token {
|
switch token {
|
||||||
case "":
|
case "":
|
||||||
@@ -1097,21 +1165,35 @@ func parsePreviewWindow(opts *previewOpts, input string) {
|
|||||||
opts.position = posLeft
|
opts.position = posLeft
|
||||||
case "right":
|
case "right":
|
||||||
opts.position = posRight
|
opts.position = posRight
|
||||||
case "rounded", "border":
|
case "rounded", "border", "border-rounded":
|
||||||
opts.border = tui.BorderRounded
|
opts.border = tui.BorderRounded
|
||||||
case "sharp":
|
case "sharp", "border-sharp":
|
||||||
opts.border = tui.BorderSharp
|
opts.border = tui.BorderSharp
|
||||||
case "noborder":
|
case "noborder", "border-none":
|
||||||
opts.border = tui.BorderNone
|
opts.border = tui.BorderNone
|
||||||
|
case "border-horizontal":
|
||||||
|
opts.border = tui.BorderHorizontal
|
||||||
|
case "border-vertical":
|
||||||
|
opts.border = tui.BorderVertical
|
||||||
|
case "border-top":
|
||||||
|
opts.border = tui.BorderTop
|
||||||
|
case "border-bottom":
|
||||||
|
opts.border = tui.BorderBottom
|
||||||
|
case "border-left":
|
||||||
|
opts.border = tui.BorderLeft
|
||||||
|
case "border-right":
|
||||||
|
opts.border = tui.BorderRight
|
||||||
case "follow":
|
case "follow":
|
||||||
opts.follow = true
|
opts.follow = true
|
||||||
case "nofollow":
|
case "nofollow":
|
||||||
opts.follow = false
|
opts.follow = false
|
||||||
default:
|
default:
|
||||||
if sizeRegex.MatchString(token) {
|
if headerRegex.MatchString(token) {
|
||||||
|
opts.headerLines = atoi(token[1:])
|
||||||
|
} else if sizeRegex.MatchString(token) {
|
||||||
opts.size = parseSize(token, 99, "window size")
|
opts.size = parseSize(token, 99, "window size")
|
||||||
} else if offsetRegex.MatchString(token) {
|
} else if offsetRegex.MatchString(token) {
|
||||||
opts.scroll = token[1:]
|
opts.scroll = token
|
||||||
} else {
|
} else {
|
||||||
errorExit("invalid preview window option: " + token)
|
errorExit("invalid preview window option: " + token)
|
||||||
}
|
}
|
||||||
@@ -1285,6 +1367,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
opts.Hscroll = false
|
opts.Hscroll = false
|
||||||
case "--hscroll-off":
|
case "--hscroll-off":
|
||||||
opts.HscrollOff = nextInt(allArgs, &i, "hscroll offset required")
|
opts.HscrollOff = nextInt(allArgs, &i, "hscroll offset required")
|
||||||
|
case "--scroll-off":
|
||||||
|
opts.ScrollOff = nextInt(allArgs, &i, "scroll offset required")
|
||||||
case "--filepath-word":
|
case "--filepath-word":
|
||||||
opts.FileWord = true
|
opts.FileWord = true
|
||||||
case "--no-filepath-word":
|
case "--no-filepath-word":
|
||||||
@@ -1352,13 +1436,17 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
case "--header-lines":
|
case "--header-lines":
|
||||||
opts.HeaderLines = atoi(
|
opts.HeaderLines = atoi(
|
||||||
nextString(allArgs, &i, "number of header lines required"))
|
nextString(allArgs, &i, "number of header lines required"))
|
||||||
|
case "--header-first":
|
||||||
|
opts.HeaderFirst = true
|
||||||
|
case "--no-header-first":
|
||||||
|
opts.HeaderFirst = false
|
||||||
case "--preview":
|
case "--preview":
|
||||||
opts.Preview.command = nextString(allArgs, &i, "preview command required")
|
opts.Preview.command = nextString(allArgs, &i, "preview command required")
|
||||||
case "--no-preview":
|
case "--no-preview":
|
||||||
opts.Preview.command = ""
|
opts.Preview.command = ""
|
||||||
case "--preview-window":
|
case "--preview-window":
|
||||||
parsePreviewWindow(&opts.Preview,
|
parsePreviewWindow(&opts.Preview,
|
||||||
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:rounded|sharp|noborder][:wrap][:cycle][:hidden][:+SCROLL[-OFFSET]][:default]"))
|
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][,SIZE[%]][,border-BORDER_OPT][,wrap][,cycle][,hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]"))
|
||||||
case "--height":
|
case "--height":
|
||||||
opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]"))
|
opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]"))
|
||||||
case "--min-height":
|
case "--min-height":
|
||||||
@@ -1461,6 +1549,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
opts.Tabstop = atoi(value)
|
opts.Tabstop = atoi(value)
|
||||||
} else if match, value := optString(arg, "--hscroll-off="); match {
|
} else if match, value := optString(arg, "--hscroll-off="); match {
|
||||||
opts.HscrollOff = atoi(value)
|
opts.HscrollOff = atoi(value)
|
||||||
|
} else if match, value := optString(arg, "--scroll-off="); match {
|
||||||
|
opts.ScrollOff = atoi(value)
|
||||||
} else if match, value := optString(arg, "--jump-labels="); match {
|
} else if match, value := optString(arg, "--jump-labels="); match {
|
||||||
opts.JumpLabels = value
|
opts.JumpLabels = value
|
||||||
validateJumpLabels = true
|
validateJumpLabels = true
|
||||||
@@ -1478,6 +1568,10 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
errorExit("hscroll offset must be a non-negative integer")
|
errorExit("hscroll offset must be a non-negative integer")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.ScrollOff < 0 {
|
||||||
|
errorExit("scroll offset must be a non-negative integer")
|
||||||
|
}
|
||||||
|
|
||||||
if opts.Tabstop < 1 {
|
if opts.Tabstop < 1 {
|
||||||
errorExit("tab stop must be a positive integer")
|
errorExit("tab stop must be a positive integer")
|
||||||
}
|
}
|
||||||
@@ -1511,21 +1605,19 @@ func validateSign(sign string, signOptName string) error {
|
|||||||
if sign == "" {
|
if sign == "" {
|
||||||
return fmt.Errorf("%v cannot be empty", signOptName)
|
return fmt.Errorf("%v cannot be empty", signOptName)
|
||||||
}
|
}
|
||||||
widthSum := 0
|
|
||||||
for _, r := range sign {
|
for _, r := range sign {
|
||||||
if !unicode.IsGraphic(r) {
|
if !unicode.IsGraphic(r) {
|
||||||
return fmt.Errorf("invalid character in %v", signOptName)
|
return fmt.Errorf("invalid character in %v", signOptName)
|
||||||
}
|
}
|
||||||
widthSum += runewidth.RuneWidth(r)
|
|
||||||
if widthSum > 2 {
|
|
||||||
return fmt.Errorf("%v display width should be up to 2", signOptName)
|
|
||||||
}
|
}
|
||||||
|
if runewidth.StringWidth(sign) > 2 {
|
||||||
|
return fmt.Errorf("%v display width should be up to 2", signOptName)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func postProcessOptions(opts *Options) {
|
func postProcessOptions(opts *Options) {
|
||||||
if !tui.IsLightRendererSupported() && opts.Height.size > 0 {
|
if !opts.Version && !tui.IsLightRendererSupported() && opts.Height.size > 0 {
|
||||||
errorExit("--height option is currently not supported on this platform")
|
errorExit("--height option is currently not supported on this platform")
|
||||||
}
|
}
|
||||||
// Default actions for CTRL-N / CTRL-P when --history is set
|
// Default actions for CTRL-N / CTRL-P when --history is set
|
||||||
|
@@ -102,7 +102,7 @@ func TestIrrelevantNth(t *testing.T) {
|
|||||||
t.Errorf("nth should be empty: %v", opts.Nth)
|
t.Errorf("nth should be empty: %v", opts.Nth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, words := range [][]string{[]string{"--nth", "..,3", "+x"}, []string{"--nth", "3,1..", "+x"}, []string{"--nth", "..-1,1", "+x"}} {
|
for _, words := range [][]string{{"--nth", "..,3", "+x"}, {"--nth", "3,1..", "+x"}, {"--nth", "..-1,1", "+x"}} {
|
||||||
{
|
{
|
||||||
opts := defaultOptions()
|
opts := defaultOptions()
|
||||||
parseOptions(opts, words)
|
parseOptions(opts, words)
|
||||||
@@ -384,23 +384,23 @@ func TestPreviewOpts(t *testing.T) {
|
|||||||
opts.Preview.size.size == 50) {
|
opts.Preview.size.size == 50) {
|
||||||
t.Error()
|
t.Error()
|
||||||
}
|
}
|
||||||
opts = optsFor("--preview", "cat {}", "--preview-window=left:15:hidden:wrap:+{1}-/2")
|
opts = optsFor("--preview", "cat {}", "--preview-window=left:15,hidden,wrap:+{1}-/2")
|
||||||
if !(opts.Preview.command == "cat {}" &&
|
if !(opts.Preview.command == "cat {}" &&
|
||||||
opts.Preview.hidden == true &&
|
opts.Preview.hidden == true &&
|
||||||
opts.Preview.wrap == true &&
|
opts.Preview.wrap == true &&
|
||||||
opts.Preview.position == posLeft &&
|
opts.Preview.position == posLeft &&
|
||||||
opts.Preview.scroll == "{1}-/2" &&
|
opts.Preview.scroll == "+{1}-/2" &&
|
||||||
opts.Preview.size.percent == false &&
|
opts.Preview.size.percent == false &&
|
||||||
opts.Preview.size.size == 15) {
|
opts.Preview.size.size == 15) {
|
||||||
t.Error(opts.Preview)
|
t.Error(opts.Preview)
|
||||||
}
|
}
|
||||||
opts = optsFor("--preview-window=up:15:wrap:hidden:+{1}-/2", "--preview-window=down", "--preview-window=cycle")
|
opts = optsFor("--preview-window=up,15,wrap,hidden,+{1}+3-1-2/2", "--preview-window=down", "--preview-window=cycle")
|
||||||
if !(opts.Preview.command == "" &&
|
if !(opts.Preview.command == "" &&
|
||||||
opts.Preview.hidden == true &&
|
opts.Preview.hidden == true &&
|
||||||
opts.Preview.wrap == true &&
|
opts.Preview.wrap == true &&
|
||||||
opts.Preview.cycle == true &&
|
opts.Preview.cycle == true &&
|
||||||
opts.Preview.position == posDown &&
|
opts.Preview.position == posDown &&
|
||||||
opts.Preview.scroll == "{1}-/2" &&
|
opts.Preview.scroll == "+{1}+3-1-2/2" &&
|
||||||
opts.Preview.size.percent == false &&
|
opts.Preview.size.percent == false &&
|
||||||
opts.Preview.size.size == 15) {
|
opts.Preview.size.size == 15) {
|
||||||
t.Error(opts.Preview.size.size)
|
t.Error(opts.Preview.size.size)
|
||||||
|
@@ -337,7 +337,7 @@ func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result,
|
|||||||
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
|
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
|
||||||
var input []Token
|
var input []Token
|
||||||
if len(p.nth) == 0 {
|
if len(p.nth) == 0 {
|
||||||
input = []Token{Token{text: &item.text, prefixLength: 0}}
|
input = []Token{{text: &item.text, prefixLength: 0}}
|
||||||
} else {
|
} else {
|
||||||
input = p.transformInput(item)
|
input = p.transformInput(item)
|
||||||
}
|
}
|
||||||
@@ -350,7 +350,7 @@ func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset,
|
|||||||
func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, *[]int) {
|
func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, *[]int) {
|
||||||
var input []Token
|
var input []Token
|
||||||
if len(p.nth) == 0 {
|
if len(p.nth) == 0 {
|
||||||
input = []Token{Token{text: &item.text, prefixLength: 0}}
|
input = []Token{{text: &item.text, prefixLength: 0}}
|
||||||
} else {
|
} else {
|
||||||
input = p.transformInput(item)
|
input = p.transformInput(item)
|
||||||
}
|
}
|
||||||
|
@@ -131,7 +131,7 @@ func TestCaseSensitivity(t *testing.T) {
|
|||||||
func TestOrigTextAndTransformed(t *testing.T) {
|
func TestOrigTextAndTransformed(t *testing.T) {
|
||||||
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg"))
|
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg"))
|
||||||
tokens := Tokenize("junegunn", Delimiter{})
|
tokens := Tokenize("junegunn", Delimiter{})
|
||||||
trans := Transform(tokens, []Range{Range{1, 1}})
|
trans := Transform(tokens, []Range{{1, 1}})
|
||||||
|
|
||||||
origBytes := []byte("junegunn.choi")
|
origBytes := []byte("junegunn.choi")
|
||||||
for _, extended := range []bool{false, true} {
|
for _, extended := range []bool{false, true} {
|
||||||
|
@@ -22,7 +22,7 @@ func TestReadFromCommand(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Normal command
|
// Normal command
|
||||||
reader.fin(reader.readFromCommand(nil, `echo abc && echo def`))
|
reader.fin(reader.readFromCommand(nil, `echo abc&&echo def`))
|
||||||
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" {
|
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" {
|
||||||
t.Errorf("%s", strs)
|
t.Errorf("%s", strs)
|
||||||
}
|
}
|
||||||
|
@@ -164,7 +164,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
|||||||
// combination of either [hl and bg] or [hl+ and bg+].
|
// combination of either [hl and bg] or [hl+ and bg+].
|
||||||
//
|
//
|
||||||
// If the original text already has background color, and the
|
// If the original text already has background color, and the
|
||||||
// forground color of colMatch is -1, we shouldn't only apply the
|
// foreground color of colMatch is -1, we shouldn't only apply the
|
||||||
// background color of colMatch.
|
// background color of colMatch.
|
||||||
// e.g. echo -e "\x1b[32;7mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline
|
// e.g. echo -e "\x1b[32;7mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline
|
||||||
// echo -e "\x1b[42mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline
|
// echo -e "\x1b[42mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline
|
||||||
|
@@ -18,8 +18,8 @@ func withIndex(i *Item, index int) *Item {
|
|||||||
|
|
||||||
func TestOffsetSort(t *testing.T) {
|
func TestOffsetSort(t *testing.T) {
|
||||||
offsets := []Offset{
|
offsets := []Offset{
|
||||||
Offset{3, 5}, Offset{2, 7},
|
{3, 5}, {2, 7},
|
||||||
Offset{1, 3}, Offset{2, 9}}
|
{1, 3}, {2, 9}}
|
||||||
sort.Sort(ByOrder(offsets))
|
sort.Sort(ByOrder(offsets))
|
||||||
|
|
||||||
if offsets[0][0] != 1 || offsets[0][1] != 3 ||
|
if offsets[0][0] != 1 || offsets[0][1] != 3 ||
|
||||||
@@ -84,13 +84,13 @@ func TestResultRank(t *testing.T) {
|
|||||||
|
|
||||||
// Sort by relevance
|
// Sort by relevance
|
||||||
item3 := buildResult(
|
item3 := buildResult(
|
||||||
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 3)
|
withIndex(&Item{}, 2), []Offset{{1, 3}, {5, 7}}, 3)
|
||||||
item4 := buildResult(
|
item4 := buildResult(
|
||||||
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 4)
|
withIndex(&Item{}, 2), []Offset{{1, 2}, {6, 7}}, 4)
|
||||||
item5 := buildResult(
|
item5 := buildResult(
|
||||||
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 5)
|
withIndex(&Item{}, 2), []Offset{{1, 3}, {5, 7}}, 5)
|
||||||
item6 := buildResult(
|
item6 := buildResult(
|
||||||
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 6)
|
withIndex(&Item{}, 2), []Offset{{1, 2}, {6, 7}}, 6)
|
||||||
items = []Result{item1, item2, item3, item4, item5, item6}
|
items = []Result{item1, item2, item3, item4, item5, item6}
|
||||||
sort.Sort(ByRelevance(items))
|
sort.Sort(ByRelevance(items))
|
||||||
if !(items[0] == item6 && items[1] == item5 &&
|
if !(items[0] == item6 && items[1] == item5 &&
|
||||||
|
488
src/terminal.go
488
src/terminal.go
@@ -2,7 +2,6 @@ package fzf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
@@ -15,15 +14,39 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mattn/go-runewidth"
|
||||||
|
"github.com/rivo/uniseg"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/tui"
|
"github.com/junegunn/fzf/src/tui"
|
||||||
"github.com/junegunn/fzf/src/util"
|
"github.com/junegunn/fzf/src/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// import "github.com/pkg/profile"
|
// import "github.com/pkg/profile"
|
||||||
|
|
||||||
|
/*
|
||||||
|
Placeholder regex is used to extract placeholders from fzf's template
|
||||||
|
strings. Acts as input validation for parsePlaceholder function.
|
||||||
|
Describes the syntax, but it is fairly lenient.
|
||||||
|
|
||||||
|
The following pseudo regex has been reverse engineered from the
|
||||||
|
implementation. It is overly strict, but better describes whats possible.
|
||||||
|
As such it is not useful for validation, but rather to generate test
|
||||||
|
cases for example.
|
||||||
|
|
||||||
|
\\?(?: # escaped type
|
||||||
|
{\+?s?f?RANGE(?:,RANGE)*} # token type
|
||||||
|
|{q} # query type
|
||||||
|
|{\+?n?f?} # item type (notice no mandatory element inside brackets)
|
||||||
|
)
|
||||||
|
RANGE = (?:
|
||||||
|
(?:-?[0-9]+)?\.\.(?:-?[0-9]+)? # ellipsis syntax for token range (x..y)
|
||||||
|
|-?[0-9]+ # shorthand syntax (x..x)
|
||||||
|
)
|
||||||
|
*/
|
||||||
var placeholder *regexp.Regexp
|
var placeholder *regexp.Regexp
|
||||||
var numericPrefix *regexp.Regexp
|
|
||||||
var whiteSuffix *regexp.Regexp
|
var whiteSuffix *regexp.Regexp
|
||||||
|
var offsetComponentRegex *regexp.Regexp
|
||||||
|
var offsetTrimCharsRegex *regexp.Regexp
|
||||||
var activeTempFiles []string
|
var activeTempFiles []string
|
||||||
|
|
||||||
const ellipsis string = ".."
|
const ellipsis string = ".."
|
||||||
@@ -31,8 +54,9 @@ const clearCode string = "\x1b[2J"
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\+?f?nf?})`)
|
placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\+?f?nf?})`)
|
||||||
numericPrefix = regexp.MustCompile(`^[[:punct:]]*([0-9]+)`)
|
|
||||||
whiteSuffix = regexp.MustCompile(`\s*$`)
|
whiteSuffix = regexp.MustCompile(`\s*$`)
|
||||||
|
offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
|
||||||
|
offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
|
||||||
activeTempFiles = []string{}
|
activeTempFiles = []string{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +121,7 @@ type Terminal struct {
|
|||||||
keepRight bool
|
keepRight bool
|
||||||
hscroll bool
|
hscroll bool
|
||||||
hscrollOff int
|
hscrollOff int
|
||||||
|
scrollOff int
|
||||||
wordRubout string
|
wordRubout string
|
||||||
wordNext string
|
wordNext string
|
||||||
cx int
|
cx int
|
||||||
@@ -115,6 +140,8 @@ type Terminal struct {
|
|||||||
printQuery bool
|
printQuery bool
|
||||||
history *History
|
history *History
|
||||||
cycle bool
|
cycle bool
|
||||||
|
headerFirst bool
|
||||||
|
headerLines int
|
||||||
header []string
|
header []string
|
||||||
header0 []string
|
header0 []string
|
||||||
ansi bool
|
ansi bool
|
||||||
@@ -133,6 +160,7 @@ type Terminal struct {
|
|||||||
count int
|
count int
|
||||||
progress int
|
progress int
|
||||||
reading bool
|
reading bool
|
||||||
|
running bool
|
||||||
failed *string
|
failed *string
|
||||||
jumping jumpMode
|
jumping jumpMode
|
||||||
jumpLabels string
|
jumpLabels string
|
||||||
@@ -157,6 +185,7 @@ type Terminal struct {
|
|||||||
slab *util.Slab
|
slab *util.Slab
|
||||||
theme *tui.ColorTheme
|
theme *tui.ColorTheme
|
||||||
tui tui.Renderer
|
tui tui.Renderer
|
||||||
|
executing *util.AtomicBool
|
||||||
}
|
}
|
||||||
|
|
||||||
type selectedItem struct {
|
type selectedItem struct {
|
||||||
@@ -221,6 +250,7 @@ const (
|
|||||||
actClearScreen
|
actClearScreen
|
||||||
actClearQuery
|
actClearQuery
|
||||||
actClearSelection
|
actClearSelection
|
||||||
|
actClose
|
||||||
actDeleteChar
|
actDeleteChar
|
||||||
actDeleteCharEOF
|
actDeleteCharEOF
|
||||||
actEndOfLine
|
actEndOfLine
|
||||||
@@ -275,6 +305,9 @@ const (
|
|||||||
actReload
|
actReload
|
||||||
actDisableSearch
|
actDisableSearch
|
||||||
actEnableSearch
|
actEnableSearch
|
||||||
|
actSelect
|
||||||
|
actDeselect
|
||||||
|
actUnbind
|
||||||
)
|
)
|
||||||
|
|
||||||
type placeholderFlags struct {
|
type placeholderFlags struct {
|
||||||
@@ -415,6 +448,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
|||||||
delay = initialDelay
|
delay = initialDelay
|
||||||
}
|
}
|
||||||
var previewBox *util.EventBox
|
var previewBox *util.EventBox
|
||||||
|
showPreviewWindow := len(opts.Preview.command) > 0 && !opts.Preview.hidden
|
||||||
if len(opts.Preview.command) > 0 || hasPreviewAction(opts) {
|
if len(opts.Preview.command) > 0 || hasPreviewAction(opts) {
|
||||||
previewBox = util.NewEventBox()
|
previewBox = util.NewEventBox()
|
||||||
}
|
}
|
||||||
@@ -471,6 +505,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
|||||||
keepRight: opts.KeepRight,
|
keepRight: opts.KeepRight,
|
||||||
hscroll: opts.Hscroll,
|
hscroll: opts.Hscroll,
|
||||||
hscrollOff: opts.HscrollOff,
|
hscrollOff: opts.HscrollOff,
|
||||||
|
scrollOff: opts.ScrollOff,
|
||||||
wordRubout: wordRubout,
|
wordRubout: wordRubout,
|
||||||
wordNext: wordNext,
|
wordNext: wordNext,
|
||||||
cx: len(input),
|
cx: len(input),
|
||||||
@@ -496,11 +531,14 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
|||||||
paused: opts.Phony,
|
paused: opts.Phony,
|
||||||
strong: strongAttr,
|
strong: strongAttr,
|
||||||
cycle: opts.Cycle,
|
cycle: opts.Cycle,
|
||||||
|
headerFirst: opts.HeaderFirst,
|
||||||
|
headerLines: opts.HeaderLines,
|
||||||
header: header,
|
header: header,
|
||||||
header0: header,
|
header0: header,
|
||||||
ansi: opts.Ansi,
|
ansi: opts.Ansi,
|
||||||
tabstop: opts.Tabstop,
|
tabstop: opts.Tabstop,
|
||||||
reading: true,
|
reading: true,
|
||||||
|
running: true,
|
||||||
failed: nil,
|
failed: nil,
|
||||||
jumping: jumpDisabled,
|
jumping: jumpDisabled,
|
||||||
jumpLabels: opts.JumpLabels,
|
jumpLabels: opts.JumpLabels,
|
||||||
@@ -510,7 +548,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
|||||||
selected: make(map[int32]selectedItem),
|
selected: make(map[int32]selectedItem),
|
||||||
reqBox: util.NewEventBox(),
|
reqBox: util.NewEventBox(),
|
||||||
previewOpts: opts.Preview,
|
previewOpts: opts.Preview,
|
||||||
previewer: previewer{0, []string{}, 0, previewBox != nil && !opts.Preview.hidden, false, true, false, ""},
|
previewer: previewer{0, []string{}, 0, showPreviewWindow, false, true, false, ""},
|
||||||
previewed: previewed{0, 0, 0, false},
|
previewed: previewed{0, 0, 0, false},
|
||||||
previewBox: previewBox,
|
previewBox: previewBox,
|
||||||
eventBox: eventBox,
|
eventBox: eventBox,
|
||||||
@@ -522,7 +560,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
|||||||
startChan: make(chan bool, 1),
|
startChan: make(chan bool, 1),
|
||||||
killChan: make(chan int),
|
killChan: make(chan int),
|
||||||
tui: renderer,
|
tui: renderer,
|
||||||
initFunc: func() { renderer.Init() }}
|
initFunc: func() { renderer.Init() },
|
||||||
|
executing: util.NewAtomicBool(false)}
|
||||||
t.prompt, t.promptLen = t.parsePrompt(opts.Prompt)
|
t.prompt, t.promptLen = t.parsePrompt(opts.Prompt)
|
||||||
t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0)
|
t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0)
|
||||||
t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0)
|
t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0)
|
||||||
@@ -664,11 +703,8 @@ func (t *Terminal) sortSelected() []selectedItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) displayWidth(runes []rune) int {
|
func (t *Terminal) displayWidth(runes []rune) int {
|
||||||
l := 0
|
width, _ := util.RunesWidth(runes, 0, t.tabstop, 0)
|
||||||
for _, r := range runes {
|
return width
|
||||||
l += util.RuneWidth(r, l, t.tabstop)
|
|
||||||
}
|
|
||||||
return l
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -822,16 +858,33 @@ func (t *Terminal) resizeWindows() {
|
|||||||
createPreviewWindow := func(y int, x int, w int, h int) {
|
createPreviewWindow := func(y int, x int, w int, h int) {
|
||||||
pwidth := w
|
pwidth := w
|
||||||
pheight := h
|
pheight := h
|
||||||
if t.previewOpts.border != tui.BorderNone {
|
var previewBorder tui.BorderStyle
|
||||||
previewBorder := tui.MakeBorderStyle(t.previewOpts.border, t.unicode)
|
if t.previewOpts.border == tui.BorderNone {
|
||||||
|
previewBorder = tui.MakeTransparentBorder()
|
||||||
|
} else {
|
||||||
|
previewBorder = tui.MakeBorderStyle(t.previewOpts.border, t.unicode)
|
||||||
|
}
|
||||||
t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder)
|
t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder)
|
||||||
|
switch t.previewOpts.border {
|
||||||
|
case tui.BorderSharp, tui.BorderRounded:
|
||||||
pwidth -= 4
|
pwidth -= 4
|
||||||
pheight -= 2
|
pheight -= 2
|
||||||
x += 2
|
x += 2
|
||||||
y += 1
|
y += 1
|
||||||
} else {
|
case tui.BorderLeft:
|
||||||
previewBorder := tui.MakeTransparentBorder()
|
pwidth -= 2
|
||||||
t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder)
|
x += 2
|
||||||
|
case tui.BorderRight:
|
||||||
|
pwidth -= 2
|
||||||
|
case tui.BorderTop:
|
||||||
|
pheight -= 1
|
||||||
|
y += 1
|
||||||
|
case tui.BorderBottom:
|
||||||
|
pheight -= 1
|
||||||
|
case tui.BorderHorizontal:
|
||||||
|
pheight -= 2
|
||||||
|
y += 1
|
||||||
|
case tui.BorderVertical:
|
||||||
pwidth -= 4
|
pwidth -= 4
|
||||||
x += 2
|
x += 2
|
||||||
}
|
}
|
||||||
@@ -839,9 +892,13 @@ func (t *Terminal) resizeWindows() {
|
|||||||
}
|
}
|
||||||
verticalPad := 2
|
verticalPad := 2
|
||||||
minPreviewHeight := 3
|
minPreviewHeight := 3
|
||||||
if t.previewOpts.border == tui.BorderNone {
|
switch t.previewOpts.border {
|
||||||
|
case tui.BorderNone, tui.BorderVertical, tui.BorderLeft, tui.BorderRight:
|
||||||
verticalPad = 0
|
verticalPad = 0
|
||||||
minPreviewHeight = 1
|
minPreviewHeight = 1
|
||||||
|
case tui.BorderTop, tui.BorderBottom:
|
||||||
|
verticalPad = 1
|
||||||
|
minPreviewHeight = 2
|
||||||
}
|
}
|
||||||
switch t.previewOpts.position {
|
switch t.previewOpts.position {
|
||||||
case posUp:
|
case posUp:
|
||||||
@@ -923,12 +980,23 @@ func (t *Terminal) updatePromptOffset() ([]rune, []rune) {
|
|||||||
return before, after
|
return before, after
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) promptLine() int {
|
||||||
|
if t.headerFirst {
|
||||||
|
max := t.window.Height() - 1
|
||||||
|
if !t.noInfoLine() {
|
||||||
|
max--
|
||||||
|
}
|
||||||
|
return util.Min(len(t.header0)+t.headerLines, max)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Terminal) placeCursor() {
|
func (t *Terminal) placeCursor() {
|
||||||
t.move(0, t.promptLen+t.queryLen[0], false)
|
t.move(t.promptLine(), t.promptLen+t.queryLen[0], false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) printPrompt() {
|
func (t *Terminal) printPrompt() {
|
||||||
t.move(0, 0, true)
|
t.move(t.promptLine(), 0, true)
|
||||||
t.prompt()
|
t.prompt()
|
||||||
|
|
||||||
before, after := t.updatePromptOffset()
|
before, after := t.updatePromptOffset()
|
||||||
@@ -950,22 +1018,23 @@ func (t *Terminal) trimMessage(message string, maxWidth int) string {
|
|||||||
|
|
||||||
func (t *Terminal) printInfo() {
|
func (t *Terminal) printInfo() {
|
||||||
pos := 0
|
pos := 0
|
||||||
|
line := t.promptLine()
|
||||||
switch t.infoStyle {
|
switch t.infoStyle {
|
||||||
case infoDefault:
|
case infoDefault:
|
||||||
t.move(1, 0, true)
|
t.move(line+1, 0, true)
|
||||||
if t.reading {
|
if t.reading {
|
||||||
duration := int64(spinnerDuration)
|
duration := int64(spinnerDuration)
|
||||||
idx := (time.Now().UnixNano() % (duration * int64(len(t.spinner)))) / duration
|
idx := (time.Now().UnixNano() % (duration * int64(len(t.spinner)))) / duration
|
||||||
t.window.CPrint(tui.ColSpinner, t.spinner[idx])
|
t.window.CPrint(tui.ColSpinner, t.spinner[idx])
|
||||||
}
|
}
|
||||||
t.move(1, 2, false)
|
t.move(line+1, 2, false)
|
||||||
pos = 2
|
pos = 2
|
||||||
case infoInline:
|
case infoInline:
|
||||||
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
|
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
|
||||||
if pos+len(" < ") > t.window.Width() {
|
if pos+len(" < ") > t.window.Width() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t.move(0, pos, true)
|
t.move(line, pos, true)
|
||||||
if t.reading {
|
if t.reading {
|
||||||
t.window.CPrint(tui.ColSpinner, " < ")
|
t.window.CPrint(tui.ColSpinner, " < ")
|
||||||
} else {
|
} else {
|
||||||
@@ -1008,11 +1077,20 @@ func (t *Terminal) printHeader() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
max := t.window.Height()
|
max := t.window.Height()
|
||||||
|
if t.headerFirst {
|
||||||
|
max--
|
||||||
|
if !t.noInfoLine() {
|
||||||
|
max--
|
||||||
|
}
|
||||||
|
}
|
||||||
var state *ansiState
|
var state *ansiState
|
||||||
for idx, lineStr := range t.header {
|
for idx, lineStr := range t.header {
|
||||||
line := idx + 2
|
line := idx
|
||||||
if t.noInfoLine() {
|
if !t.headerFirst {
|
||||||
line--
|
line++
|
||||||
|
if !t.noInfoLine() {
|
||||||
|
line++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if line >= max {
|
if line >= max {
|
||||||
continue
|
continue
|
||||||
@@ -1111,36 +1189,26 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool) {
|
|||||||
t.prevLines[i] = newLine
|
t.prevLines[i] = newLine
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) trimRight(runes []rune, width int) ([]rune, int) {
|
func (t *Terminal) trimRight(runes []rune, width int) ([]rune, bool) {
|
||||||
// We start from the beginning to handle tab characters
|
// We start from the beginning to handle tab characters
|
||||||
l := 0
|
width, overflowIdx := util.RunesWidth(runes, 0, t.tabstop, width)
|
||||||
for idx, r := range runes {
|
if overflowIdx >= 0 {
|
||||||
l += util.RuneWidth(r, l, t.tabstop)
|
return runes[:overflowIdx], true
|
||||||
if l > width {
|
|
||||||
return runes[:idx], len(runes) - idx
|
|
||||||
}
|
}
|
||||||
}
|
return runes, false
|
||||||
return runes, 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int {
|
func (t *Terminal) displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int {
|
||||||
l := 0
|
width, _ := util.RunesWidth(runes, prefixWidth, t.tabstop, limit)
|
||||||
for _, r := range runes {
|
return width
|
||||||
l += util.RuneWidth(r, l+prefixWidth, t.tabstop)
|
|
||||||
if l > limit {
|
|
||||||
// Early exit
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return l
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) trimLeft(runes []rune, width int) ([]rune, int32) {
|
func (t *Terminal) trimLeft(runes []rune, width int) ([]rune, int32) {
|
||||||
width = util.Max(0, width)
|
width = util.Max(0, width)
|
||||||
var trimmed int32
|
var trimmed int32
|
||||||
// Assume that each rune takes at least one column on screen
|
// Assume that each rune takes at least one column on screen
|
||||||
if len(runes) > width {
|
if len(runes) > width+2 {
|
||||||
diff := len(runes) - width
|
diff := len(runes) - width - 2
|
||||||
trimmed = int32(diff)
|
trimmed = int32(diff)
|
||||||
runes = runes[diff:]
|
runes = runes[diff:]
|
||||||
}
|
}
|
||||||
@@ -1288,18 +1356,37 @@ func (t *Terminal) renderPreviewSpinner() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) renderPreviewText(unchanged bool) {
|
func (t *Terminal) renderPreviewArea(unchanged bool) {
|
||||||
maxWidth := t.pwindow.Width()
|
|
||||||
lineNo := -t.previewer.offset
|
|
||||||
height := t.pwindow.Height()
|
|
||||||
if unchanged {
|
if unchanged {
|
||||||
t.pwindow.MoveAndClear(0, 0)
|
t.pwindow.MoveAndClear(0, 0) // Clear scroll offset display
|
||||||
} else {
|
} else {
|
||||||
t.previewed.filled = false
|
t.previewed.filled = false
|
||||||
t.pwindow.Erase()
|
t.pwindow.Erase()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
height := t.pwindow.Height()
|
||||||
|
header := []string{}
|
||||||
|
body := t.previewer.lines
|
||||||
|
headerLines := t.previewOpts.headerLines
|
||||||
|
// Do not enable preview header lines if it's value is too large
|
||||||
|
if headerLines > 0 && headerLines < util.Min(len(body), height) {
|
||||||
|
header = t.previewer.lines[0:headerLines]
|
||||||
|
body = t.previewer.lines[headerLines:]
|
||||||
|
// Always redraw header
|
||||||
|
t.renderPreviewText(height, header, 0, false)
|
||||||
|
t.pwindow.MoveAndClear(t.pwindow.Y(), 0)
|
||||||
|
}
|
||||||
|
t.renderPreviewText(height, body, -t.previewer.offset+headerLines, unchanged)
|
||||||
|
|
||||||
|
if !unchanged {
|
||||||
|
t.pwindow.FinishFill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unchanged bool) {
|
||||||
|
maxWidth := t.pwindow.Width()
|
||||||
var ansi *ansiState
|
var ansi *ansiState
|
||||||
for _, line := range t.previewer.lines {
|
for _, line := range lines {
|
||||||
var lbg tui.Color = -1
|
var lbg tui.Color = -1
|
||||||
if ansi != nil {
|
if ansi != nil {
|
||||||
ansi.lbg = -1
|
ansi.lbg = -1
|
||||||
@@ -1313,8 +1400,9 @@ func (t *Terminal) renderPreviewText(unchanged bool) {
|
|||||||
prefixWidth := 0
|
prefixWidth := 0
|
||||||
_, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool {
|
_, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool {
|
||||||
trimmed := []rune(str)
|
trimmed := []rune(str)
|
||||||
|
isTrimmed := false
|
||||||
if !t.previewOpts.wrap {
|
if !t.previewOpts.wrap {
|
||||||
trimmed, _ = t.trimRight(trimmed, maxWidth-t.pwindow.X())
|
trimmed, isTrimmed = t.trimRight(trimmed, maxWidth-t.pwindow.X())
|
||||||
}
|
}
|
||||||
str, width := t.processTabs(trimmed, prefixWidth)
|
str, width := t.processTabs(trimmed, prefixWidth)
|
||||||
prefixWidth += width
|
prefixWidth += width
|
||||||
@@ -1324,7 +1412,8 @@ func (t *Terminal) renderPreviewText(unchanged bool) {
|
|||||||
} else {
|
} else {
|
||||||
fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str)
|
fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str)
|
||||||
}
|
}
|
||||||
return fillRet == tui.FillContinue
|
return !isTrimmed &&
|
||||||
|
(fillRet == tui.FillContinue || t.previewOpts.wrap && fillRet == tui.FillNextLine)
|
||||||
})
|
})
|
||||||
t.previewer.scrollable = t.previewer.scrollable || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width()
|
t.previewer.scrollable = t.previewer.scrollable || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width()
|
||||||
if fillRet == tui.FillNextLine {
|
if fillRet == tui.FillNextLine {
|
||||||
@@ -1345,9 +1434,6 @@ func (t *Terminal) renderPreviewText(unchanged bool) {
|
|||||||
}
|
}
|
||||||
lineNo++
|
lineNo++
|
||||||
}
|
}
|
||||||
if !unchanged {
|
|
||||||
t.pwindow.FinishFill()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) printPreview() {
|
func (t *Terminal) printPreview() {
|
||||||
@@ -1360,7 +1446,7 @@ func (t *Terminal) printPreview() {
|
|||||||
t.previewer.version == t.previewed.version &&
|
t.previewer.version == t.previewed.version &&
|
||||||
t.previewer.offset == t.previewed.offset
|
t.previewer.offset == t.previewed.offset
|
||||||
t.previewer.scrollable = t.previewer.offset > 0 || numLines > height
|
t.previewer.scrollable = t.previewer.offset > 0 || numLines > height
|
||||||
t.renderPreviewText(unchanged)
|
t.renderPreviewArea(unchanged)
|
||||||
t.renderPreviewSpinner()
|
t.renderPreviewSpinner()
|
||||||
t.previewed.numLines = numLines
|
t.previewed.numLines = numLines
|
||||||
t.previewed.version = t.previewer.version
|
t.previewed.version = t.previewer.version
|
||||||
@@ -1373,7 +1459,7 @@ func (t *Terminal) printPreviewDelayed() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.previewer.scrollable = false
|
t.previewer.scrollable = false
|
||||||
t.renderPreviewText(true)
|
t.renderPreviewArea(true)
|
||||||
|
|
||||||
message := t.trimMessage("Loading ..", t.pwindow.Width())
|
message := t.trimMessage("Loading ..", t.pwindow.Width())
|
||||||
pos := t.pwindow.Width() - len(message)
|
pos := t.pwindow.Width() - len(message)
|
||||||
@@ -1382,16 +1468,21 @@ func (t *Terminal) printPreviewDelayed() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) {
|
func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) {
|
||||||
var strbuf bytes.Buffer
|
var strbuf strings.Builder
|
||||||
l := prefixWidth
|
l := prefixWidth
|
||||||
for _, r := range runes {
|
gr := uniseg.NewGraphemes(string(runes))
|
||||||
w := util.RuneWidth(r, l, t.tabstop)
|
for gr.Next() {
|
||||||
l += w
|
rs := gr.Runes()
|
||||||
if r == '\t' {
|
str := string(rs)
|
||||||
|
var w int
|
||||||
|
if len(rs) == 1 && rs[0] == '\t' {
|
||||||
|
w = t.tabstop - l%t.tabstop
|
||||||
strbuf.WriteString(strings.Repeat(" ", w))
|
strbuf.WriteString(strings.Repeat(" ", w))
|
||||||
} else {
|
} else {
|
||||||
strbuf.WriteRune(r)
|
w = runewidth.StringWidth(str)
|
||||||
|
strbuf.WriteString(str)
|
||||||
}
|
}
|
||||||
|
l += w
|
||||||
}
|
}
|
||||||
return strbuf.String(), l
|
return strbuf.String(), l
|
||||||
}
|
}
|
||||||
@@ -1476,22 +1567,6 @@ func keyMatch(key tui.Event, event tui.Event) bool {
|
|||||||
key.Type == tui.DoubleClick && event.Type == tui.Mouse && event.MouseEvent.Double
|
key.Type == tui.DoubleClick && event.Type == tui.Mouse && event.MouseEvent.Double
|
||||||
}
|
}
|
||||||
|
|
||||||
func quoteEntryCmd(entry string) string {
|
|
||||||
escaped := strings.Replace(entry, `\`, `\\`, -1)
|
|
||||||
escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"`
|
|
||||||
r, _ := regexp.Compile(`[&|<>()@^%!"]`)
|
|
||||||
return r.ReplaceAllStringFunc(escaped, func(match string) string {
|
|
||||||
return "^" + match
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func quoteEntry(entry string) string {
|
|
||||||
if util.IsWindows() {
|
|
||||||
return quoteEntryCmd(entry)
|
|
||||||
}
|
|
||||||
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
|
|
||||||
}
|
|
||||||
|
|
||||||
func parsePlaceholder(match string) (bool, string, placeholderFlags) {
|
func parsePlaceholder(match string) (bool, string, placeholderFlags) {
|
||||||
flags := placeholderFlags{}
|
flags := placeholderFlags{}
|
||||||
|
|
||||||
@@ -1517,6 +1592,7 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
|
|||||||
skipChars++
|
skipChars++
|
||||||
case 'q':
|
case 'q':
|
||||||
flags.query = true
|
flags.query = true
|
||||||
|
// query flag is not skipped
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -1566,43 +1642,33 @@ func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input str
|
|||||||
template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list)
|
template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ascii to positive integer
|
func (t *Terminal) evaluateScrollOffset(list []*Item, height int) int {
|
||||||
func atopi(s string) int {
|
offsetExpr := offsetTrimCharsRegex.ReplaceAllString(
|
||||||
matches := numericPrefix.FindStringSubmatch(s)
|
t.replacePlaceholder(t.previewOpts.scroll, false, "", list), "")
|
||||||
if len(matches) < 2 {
|
|
||||||
return 0
|
atoi := func(s string) int {
|
||||||
}
|
n, e := strconv.Atoi(s)
|
||||||
n, e := strconv.Atoi(matches[1])
|
if e != nil {
|
||||||
if e != nil || n < 1 {
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return n
|
return n
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Terminal) evaluateScrollOffset(list []*Item, height int) int {
|
|
||||||
offsetExpr := t.replacePlaceholder(t.previewOpts.scroll, false, "", list)
|
|
||||||
nums := strings.Split(offsetExpr, "-")
|
|
||||||
switch len(nums) {
|
|
||||||
case 0:
|
|
||||||
return 0
|
|
||||||
case 1, 2:
|
|
||||||
base := atopi(nums[0])
|
|
||||||
if base == 0 {
|
|
||||||
return 0
|
|
||||||
} else if len(nums) == 1 {
|
|
||||||
return base - 1
|
|
||||||
}
|
}
|
||||||
if nums[1][0] == '/' {
|
|
||||||
denom := atopi(nums[1][1:])
|
base := -1
|
||||||
|
for _, component := range offsetComponentRegex.FindAllString(offsetExpr, -1) {
|
||||||
|
if strings.HasPrefix(component, "-/") {
|
||||||
|
component = component[1:]
|
||||||
|
}
|
||||||
|
if component[0] == '/' {
|
||||||
|
denom := atoi(component[1:])
|
||||||
if denom == 0 {
|
if denom == 0 {
|
||||||
return base
|
return base
|
||||||
}
|
}
|
||||||
return base - height/denom
|
return base - height/denom
|
||||||
}
|
}
|
||||||
return base - atopi(nums[1]) - 1
|
base += atoi(component)
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
return base
|
||||||
}
|
}
|
||||||
|
|
||||||
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
|
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
|
||||||
@@ -1614,50 +1680,41 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
|
|||||||
if selected[0] == nil {
|
if selected[0] == nil {
|
||||||
selected = []*Item{}
|
selected = []*Item{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// replace placeholders one by one
|
||||||
return placeholder.ReplaceAllStringFunc(template, func(match string) string {
|
return placeholder.ReplaceAllStringFunc(template, func(match string) string {
|
||||||
escaped, match, flags := parsePlaceholder(match)
|
escaped, match, flags := parsePlaceholder(match)
|
||||||
|
|
||||||
if escaped {
|
// this function implements the effects a placeholder has on items
|
||||||
|
var replace func(*Item) string
|
||||||
|
|
||||||
|
// placeholder types (escaped, query type, item type, token type)
|
||||||
|
switch {
|
||||||
|
case escaped:
|
||||||
return match
|
return match
|
||||||
}
|
case match == "{q}":
|
||||||
|
|
||||||
// Current query
|
|
||||||
if match == "{q}" {
|
|
||||||
return quoteEntry(query)
|
return quoteEntry(query)
|
||||||
}
|
case match == "{}":
|
||||||
|
replace = func(item *Item) string {
|
||||||
items := current
|
switch {
|
||||||
if flags.plus || forcePlus {
|
case flags.number:
|
||||||
items = selected
|
|
||||||
}
|
|
||||||
|
|
||||||
replacements := make([]string, len(items))
|
|
||||||
|
|
||||||
if match == "{}" {
|
|
||||||
for idx, item := range items {
|
|
||||||
if flags.number {
|
|
||||||
n := int(item.text.Index)
|
n := int(item.text.Index)
|
||||||
if n < 0 {
|
if n < 0 {
|
||||||
replacements[idx] = ""
|
return ""
|
||||||
} else {
|
|
||||||
replacements[idx] = strconv.Itoa(n)
|
|
||||||
}
|
}
|
||||||
} else if flags.file {
|
return strconv.Itoa(n)
|
||||||
replacements[idx] = item.AsString(stripAnsi)
|
case flags.file:
|
||||||
} else {
|
return item.AsString(stripAnsi)
|
||||||
replacements[idx] = quoteEntry(item.AsString(stripAnsi))
|
default:
|
||||||
|
return quoteEntry(item.AsString(stripAnsi))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if flags.file {
|
default:
|
||||||
return writeTemporaryFile(replacements, printsep)
|
// token type and also failover (below)
|
||||||
}
|
rangeExpressions := strings.Split(match[1:len(match)-1], ",")
|
||||||
return strings.Join(replacements, " ")
|
ranges := make([]Range, len(rangeExpressions))
|
||||||
}
|
for idx, s := range rangeExpressions {
|
||||||
|
r, ok := ParseRange(&s) // ellipsis (x..y) and shorthand (x..x) range syntax
|
||||||
tokens := strings.Split(match[1:len(match)-1], ",")
|
|
||||||
ranges := make([]Range, len(tokens))
|
|
||||||
for idx, s := range tokens {
|
|
||||||
r, ok := ParseRange(&s)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
// Invalid expression, just return the original string in the template
|
// Invalid expression, just return the original string in the template
|
||||||
return match
|
return match
|
||||||
@@ -1665,26 +1722,44 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
|
|||||||
ranges[idx] = r
|
ranges[idx] = r
|
||||||
}
|
}
|
||||||
|
|
||||||
for idx, item := range items {
|
replace = func(item *Item) string {
|
||||||
tokens := Tokenize(item.AsString(stripAnsi), delimiter)
|
tokens := Tokenize(item.AsString(stripAnsi), delimiter)
|
||||||
trans := Transform(tokens, ranges)
|
trans := Transform(tokens, ranges)
|
||||||
str := joinTokens(trans)
|
str := joinTokens(trans)
|
||||||
|
|
||||||
|
// trim the last delimiter
|
||||||
if delimiter.str != nil {
|
if delimiter.str != nil {
|
||||||
str = strings.TrimSuffix(str, *delimiter.str)
|
str = strings.TrimSuffix(str, *delimiter.str)
|
||||||
} else if delimiter.regex != nil {
|
} else if delimiter.regex != nil {
|
||||||
delims := delimiter.regex.FindAllStringIndex(str, -1)
|
delims := delimiter.regex.FindAllStringIndex(str, -1)
|
||||||
|
// make sure the delimiter is at the very end of the string
|
||||||
if len(delims) > 0 && delims[len(delims)-1][1] == len(str) {
|
if len(delims) > 0 && delims[len(delims)-1][1] == len(str) {
|
||||||
str = str[:delims[len(delims)-1][0]]
|
str = str[:delims[len(delims)-1][0]]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !flags.preserveSpace {
|
if !flags.preserveSpace {
|
||||||
str = strings.TrimSpace(str)
|
str = strings.TrimSpace(str)
|
||||||
}
|
}
|
||||||
if !flags.file {
|
if !flags.file {
|
||||||
str = quoteEntry(str)
|
str = quoteEntry(str)
|
||||||
}
|
}
|
||||||
replacements[idx] = str
|
return str
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply 'replace' function over proper set of items and return result
|
||||||
|
|
||||||
|
items := current
|
||||||
|
if flags.plus || forcePlus {
|
||||||
|
items = selected
|
||||||
|
}
|
||||||
|
replacements := make([]string, len(items))
|
||||||
|
|
||||||
|
for idx, item := range items {
|
||||||
|
replacements[idx] = replace(item)
|
||||||
|
}
|
||||||
|
|
||||||
if flags.file {
|
if flags.file {
|
||||||
return writeTemporaryFile(replacements, printsep)
|
return writeTemporaryFile(replacements, printsep)
|
||||||
}
|
}
|
||||||
@@ -1705,8 +1780,9 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
|
|||||||
}
|
}
|
||||||
command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
|
command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
|
||||||
cmd := util.ExecCommand(command, false)
|
cmd := util.ExecCommand(command, false)
|
||||||
|
t.executing.Set(true)
|
||||||
if !background {
|
if !background {
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = tui.TtyIn()
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
t.tui.Pause(true)
|
t.tui.Pause(true)
|
||||||
@@ -1719,6 +1795,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
|
|||||||
cmd.Run()
|
cmd.Run()
|
||||||
t.tui.Resume(false, false)
|
t.tui.Resume(false, false)
|
||||||
}
|
}
|
||||||
|
t.executing.Set(false)
|
||||||
cleanTemporaryFiles()
|
cleanTemporaryFiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1784,11 +1861,26 @@ func (t *Terminal) selectItem(item *Item) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) selectItemChanged(item *Item) bool {
|
||||||
|
if _, found := t.selected[item.Index()]; found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return t.selectItem(item)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Terminal) deselectItem(item *Item) {
|
func (t *Terminal) deselectItem(item *Item) {
|
||||||
delete(t.selected, item.Index())
|
delete(t.selected, item.Index())
|
||||||
t.version++
|
t.version++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) deselectItemChanged(item *Item) bool {
|
||||||
|
if _, found := t.selected[item.Index()]; found {
|
||||||
|
t.deselectItem(item)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Terminal) toggleItem(item *Item) bool {
|
func (t *Terminal) toggleItem(item *Item) bool {
|
||||||
if _, found := t.selected[item.Index()]; !found {
|
if _, found := t.selected[item.Index()]; !found {
|
||||||
return t.selectItem(item)
|
return t.selectItem(item)
|
||||||
@@ -1802,7 +1894,7 @@ func (t *Terminal) killPreview(code int) {
|
|||||||
case t.killChan <- code:
|
case t.killChan <- code:
|
||||||
default:
|
default:
|
||||||
if code != exitCancel {
|
if code != exitCancel {
|
||||||
os.Exit(code)
|
t.eventBox.Set(EvtQuit, code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1819,8 +1911,12 @@ func (t *Terminal) Loop() {
|
|||||||
intChan := make(chan os.Signal, 1)
|
intChan := make(chan os.Signal, 1)
|
||||||
signal.Notify(intChan, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(intChan, os.Interrupt, syscall.SIGTERM)
|
||||||
go func() {
|
go func() {
|
||||||
<-intChan
|
for s := range intChan {
|
||||||
|
// Don't quit by SIGINT while executing because it should be for the executing command and not for fzf itself
|
||||||
|
if !(s == os.Interrupt && t.executing.Get()) {
|
||||||
t.reqBox.Set(reqQuit, nil)
|
t.reqBox.Set(reqQuit, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
contChan := make(chan os.Signal, 1)
|
contChan := make(chan os.Signal, 1)
|
||||||
@@ -1897,7 +1993,7 @@ func (t *Terminal) Loop() {
|
|||||||
cmd := util.ExecCommand(command, true)
|
cmd := util.ExecCommand(command, true)
|
||||||
if pwindow != nil {
|
if pwindow != nil {
|
||||||
height := pwindow.Height()
|
height := pwindow.Height()
|
||||||
initialOffset = util.Max(0, t.evaluateScrollOffset(items, height))
|
initialOffset = util.Max(0, t.evaluateScrollOffset(items, util.Max(0, height-t.previewOpts.headerLines)))
|
||||||
env := os.Environ()
|
env := os.Environ()
|
||||||
lines := fmt.Sprintf("LINES=%d", height)
|
lines := fmt.Sprintf("LINES=%d", height)
|
||||||
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
|
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
|
||||||
@@ -1930,6 +2026,7 @@ func (t *Terminal) Loop() {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Goroutine 2 periodically requests rendering
|
// Goroutine 2 periodically requests rendering
|
||||||
|
rendered := util.NewAtomicBool(false)
|
||||||
go func(version int64) {
|
go func(version int64) {
|
||||||
lines := []string{}
|
lines := []string{}
|
||||||
spinner := makeSpinner(t.unicode)
|
spinner := makeSpinner(t.unicode)
|
||||||
@@ -1944,6 +2041,7 @@ func (t *Terminal) Loop() {
|
|||||||
if spinnerIndex >= 0 {
|
if spinnerIndex >= 0 {
|
||||||
spin := spinner[spinnerIndex%len(spinner)]
|
spin := spinner[spinnerIndex%len(spinner)]
|
||||||
t.reqBox.Set(reqPreviewDisplay, previewResult{version, lines, offset, spin})
|
t.reqBox.Set(reqPreviewDisplay, previewResult{version, lines, offset, spin})
|
||||||
|
rendered.Set(true)
|
||||||
offset = -1
|
offset = -1
|
||||||
}
|
}
|
||||||
spinnerIndex++
|
spinnerIndex++
|
||||||
@@ -1963,6 +2061,7 @@ func (t *Terminal) Loop() {
|
|||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.reqBox.Set(reqPreviewDisplay, previewResult{version, lines, offset, ""})
|
t.reqBox.Set(reqPreviewDisplay, previewResult{version, lines, offset, ""})
|
||||||
|
rendered.Set(true)
|
||||||
break Loop
|
break Loop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1982,9 +2081,15 @@ func (t *Terminal) Loop() {
|
|||||||
case code := <-t.killChan:
|
case code := <-t.killChan:
|
||||||
if code != exitCancel {
|
if code != exitCancel {
|
||||||
util.KillCommand(cmd)
|
util.KillCommand(cmd)
|
||||||
os.Exit(code)
|
t.eventBox.Set(EvtQuit, code)
|
||||||
} else {
|
} else {
|
||||||
timer := time.NewTimer(previewCancelWait)
|
// We can immediately kill a long-running preview program
|
||||||
|
// once we started rendering its partial output
|
||||||
|
delay := previewCancelWait
|
||||||
|
if rendered.Get() {
|
||||||
|
delay = 0
|
||||||
|
}
|
||||||
|
timer := time.NewTimer(delay)
|
||||||
select {
|
select {
|
||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
util.KillCommand(cmd)
|
util.KillCommand(cmd)
|
||||||
@@ -2019,16 +2124,6 @@ func (t *Terminal) Loop() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
exit := func(getCode func() int) {
|
|
||||||
t.tui.Close()
|
|
||||||
code := getCode()
|
|
||||||
if code <= exitNoMatch && t.history != nil {
|
|
||||||
t.history.append(string(t.input))
|
|
||||||
}
|
|
||||||
// prof.Stop()
|
|
||||||
t.killPreview(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshPreview := func(command string) {
|
refreshPreview := func(command string) {
|
||||||
if len(command) > 0 && t.isPreviewEnabled() {
|
if len(command) > 0 && t.isPreviewEnabled() {
|
||||||
_, list := t.buildPlusList(command, false)
|
_, list := t.buildPlusList(command, false)
|
||||||
@@ -2040,7 +2135,19 @@ func (t *Terminal) Loop() {
|
|||||||
go func() {
|
go func() {
|
||||||
var focusedIndex int32 = minItem.Index()
|
var focusedIndex int32 = minItem.Index()
|
||||||
var version int64 = -1
|
var version int64 = -1
|
||||||
for {
|
running := true
|
||||||
|
code := exitError
|
||||||
|
exit := func(getCode func() int) {
|
||||||
|
t.tui.Close()
|
||||||
|
code = getCode()
|
||||||
|
if code <= exitNoMatch && t.history != nil {
|
||||||
|
t.history.append(string(t.input))
|
||||||
|
}
|
||||||
|
running = false
|
||||||
|
t.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
for running {
|
||||||
t.reqBox.Wait(func(events *util.Events) {
|
t.reqBox.Wait(func(events *util.Events) {
|
||||||
defer events.Clear()
|
defer events.Clear()
|
||||||
t.mutex.Lock()
|
t.mutex.Lock()
|
||||||
@@ -2086,6 +2193,7 @@ func (t *Terminal) Loop() {
|
|||||||
}
|
}
|
||||||
return exitNoMatch
|
return exitNoMatch
|
||||||
})
|
})
|
||||||
|
return
|
||||||
case reqPreviewDisplay:
|
case reqPreviewDisplay:
|
||||||
result := value.(previewResult)
|
result := value.(previewResult)
|
||||||
if t.previewer.version != result.version {
|
if t.previewer.version != result.version {
|
||||||
@@ -2097,7 +2205,7 @@ func (t *Terminal) Loop() {
|
|||||||
if t.previewer.following {
|
if t.previewer.following {
|
||||||
t.previewer.offset = len(t.previewer.lines) - t.pwindow.Height()
|
t.previewer.offset = len(t.previewer.lines) - t.pwindow.Height()
|
||||||
} else if result.offset >= 0 {
|
} else if result.offset >= 0 {
|
||||||
t.previewer.offset = util.Constrain(result.offset, 0, len(t.previewer.lines)-1)
|
t.previewer.offset = util.Constrain(result.offset, t.previewOpts.headerLines, len(t.previewer.lines)-1)
|
||||||
}
|
}
|
||||||
t.printPreview()
|
t.printPreview()
|
||||||
case reqPreviewRefresh:
|
case reqPreviewRefresh:
|
||||||
@@ -2110,14 +2218,18 @@ func (t *Terminal) Loop() {
|
|||||||
t.printer(string(t.input))
|
t.printer(string(t.input))
|
||||||
return exitOk
|
return exitOk
|
||||||
})
|
})
|
||||||
|
return
|
||||||
case reqQuit:
|
case reqQuit:
|
||||||
exit(func() int { return exitInterrupt })
|
exit(func() int { return exitInterrupt })
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.refresh()
|
t.refresh()
|
||||||
t.mutex.Unlock()
|
t.mutex.Unlock()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// prof.Stop()
|
||||||
|
t.killPreview(code)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
looping := true
|
looping := true
|
||||||
@@ -2166,7 +2278,7 @@ func (t *Terminal) Loop() {
|
|||||||
if t.previewOpts.cycle {
|
if t.previewOpts.cycle {
|
||||||
newOffset = (newOffset + numLines) % numLines
|
newOffset = (newOffset + numLines) % numLines
|
||||||
}
|
}
|
||||||
newOffset = util.Constrain(newOffset, 0, numLines-1)
|
newOffset = util.Constrain(newOffset, t.previewOpts.headerLines, numLines-1)
|
||||||
if t.previewer.offset != newOffset {
|
if t.previewer.offset != newOffset {
|
||||||
t.previewer.offset = newOffset
|
t.previewer.offset = newOffset
|
||||||
req(reqPreviewRefresh)
|
req(reqPreviewRefresh)
|
||||||
@@ -2222,6 +2334,8 @@ func (t *Terminal) Loop() {
|
|||||||
case actTogglePreviewWrap:
|
case actTogglePreviewWrap:
|
||||||
if t.hasPreviewWindow() {
|
if t.hasPreviewWindow() {
|
||||||
t.previewOpts.wrap = !t.previewOpts.wrap
|
t.previewOpts.wrap = !t.previewOpts.wrap
|
||||||
|
// Reset preview version so that full redraw occurs
|
||||||
|
t.previewed.version = 0
|
||||||
req(reqPreviewRefresh)
|
req(reqPreviewRefresh)
|
||||||
}
|
}
|
||||||
case actToggleSort:
|
case actToggleSort:
|
||||||
@@ -2332,6 +2446,22 @@ func (t *Terminal) Loop() {
|
|||||||
}
|
}
|
||||||
req(reqList, reqInfo)
|
req(reqList, reqInfo)
|
||||||
}
|
}
|
||||||
|
case actClose:
|
||||||
|
if t.isPreviewEnabled() {
|
||||||
|
togglePreview(false)
|
||||||
|
} else {
|
||||||
|
req(reqQuit)
|
||||||
|
}
|
||||||
|
case actSelect:
|
||||||
|
current := t.currentItem()
|
||||||
|
if t.multi > 0 && current != nil && t.selectItemChanged(current) {
|
||||||
|
req(reqList, reqInfo)
|
||||||
|
}
|
||||||
|
case actDeselect:
|
||||||
|
current := t.currentItem()
|
||||||
|
if t.multi > 0 && current != nil && t.deselectItemChanged(current) {
|
||||||
|
req(reqList, reqInfo)
|
||||||
|
}
|
||||||
case actToggle:
|
case actToggle:
|
||||||
if t.multi > 0 && t.merger.Length() > 0 && toggle() {
|
if t.multi > 0 && t.merger.Length() > 0 && toggle() {
|
||||||
req(reqList)
|
req(reqList)
|
||||||
@@ -2539,7 +2669,7 @@ func (t *Terminal) Loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if me.Down {
|
} else if me.Down {
|
||||||
if my == 0 && mx >= 0 {
|
if my == t.promptLine() && mx >= 0 {
|
||||||
// Prompt
|
// Prompt
|
||||||
t.cx = mx + t.xoffset
|
t.cx = mx + t.xoffset
|
||||||
} else if my >= min {
|
} else if my >= min {
|
||||||
@@ -2569,6 +2699,13 @@ func (t *Terminal) Loop() {
|
|||||||
if valid {
|
if valid {
|
||||||
command := t.replacePlaceholder(a.a, false, string(t.input), list)
|
command := t.replacePlaceholder(a.a, false, string(t.input), list)
|
||||||
newCommand = &command
|
newCommand = &command
|
||||||
|
t.reading = true
|
||||||
|
t.version++
|
||||||
|
}
|
||||||
|
case actUnbind:
|
||||||
|
keys := parseKeyChords(a.a, "PANIC")
|
||||||
|
for key := range keys {
|
||||||
|
delete(t.keymap, key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -2632,18 +2769,33 @@ func (t *Terminal) Loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) constrain() {
|
func (t *Terminal) constrain() {
|
||||||
|
// count of items to display allowed by filtering
|
||||||
count := t.merger.Length()
|
count := t.merger.Length()
|
||||||
|
// count of lines can be displayed
|
||||||
height := t.maxItems()
|
height := t.maxItems()
|
||||||
diffpos := t.cy - t.offset
|
|
||||||
|
|
||||||
t.cy = util.Constrain(t.cy, 0, count-1)
|
t.cy = util.Constrain(t.cy, 0, count-1)
|
||||||
t.offset = util.Constrain(t.offset, t.cy-height+1, t.cy)
|
|
||||||
// Adjustment
|
minOffset := util.Max(t.cy-height+1, 0)
|
||||||
if count-t.offset < height {
|
maxOffset := util.Max(util.Min(count-height, t.cy), 0)
|
||||||
t.offset = util.Max(0, count-height)
|
t.offset = util.Constrain(t.offset, minOffset, maxOffset)
|
||||||
t.cy = util.Constrain(t.offset+diffpos, 0, count-1)
|
if t.scrollOff == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollOff := util.Min(height/2, t.scrollOff)
|
||||||
|
for {
|
||||||
|
prevOffset := t.offset
|
||||||
|
if t.cy-t.offset < scrollOff {
|
||||||
|
t.offset = util.Max(minOffset, t.offset-1)
|
||||||
|
}
|
||||||
|
if t.cy-t.offset >= height-scrollOff {
|
||||||
|
t.offset = util.Min(maxOffset, t.offset+1)
|
||||||
|
}
|
||||||
|
if t.offset == prevOffset {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
t.offset = util.Max(0, t.offset)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) vmove(o int, allowCycle bool) {
|
func (t *Terminal) vmove(o int, allowCycle bool) {
|
||||||
|
@@ -1,18 +1,17 @@
|
|||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/util"
|
"github.com/junegunn/fzf/src/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newItem(str string) *Item {
|
|
||||||
bytes := []byte(str)
|
|
||||||
trimmed, _, _ := extractColor(str, nil, nil)
|
|
||||||
return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReplacePlaceholder(t *testing.T) {
|
func TestReplacePlaceholder(t *testing.T) {
|
||||||
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
|
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
|
||||||
items1 := []*Item{item1, item1}
|
items1 := []*Item{item1, item1}
|
||||||
@@ -30,75 +29,96 @@ func TestReplacePlaceholder(t *testing.T) {
|
|||||||
t.Errorf("expected: %s, actual: %s", expected, result)
|
t.Errorf("expected: %s, actual: %s", expected, result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// helper function that converts template format into string and carries out the check()
|
||||||
|
checkFormat := func(format string) {
|
||||||
|
type quotes struct{ O, I, S string } // outer, inner quotes, print separator
|
||||||
|
unixStyle := quotes{`'`, `'\''`, "\n"}
|
||||||
|
windowsStyle := quotes{`^"`, `'`, "\n"}
|
||||||
|
var effectiveStyle quotes
|
||||||
|
|
||||||
|
if util.IsWindows() {
|
||||||
|
effectiveStyle = windowsStyle
|
||||||
|
} else {
|
||||||
|
effectiveStyle = unixStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := templateToString(format, effectiveStyle)
|
||||||
|
check(expected)
|
||||||
|
}
|
||||||
printsep := "\n"
|
printsep := "\n"
|
||||||
|
|
||||||
|
/*
|
||||||
|
Test multiple placeholders and the function parameters.
|
||||||
|
*/
|
||||||
|
|
||||||
// {}, preserve ansi
|
// {}, preserve ansi
|
||||||
result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1)
|
result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1)
|
||||||
check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'")
|
checkFormat("echo {{.O}} foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
|
||||||
|
|
||||||
// {}, strip ansi
|
// {}, strip ansi
|
||||||
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1)
|
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1)
|
||||||
check("echo ' foo'\\''bar baz'")
|
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
||||||
|
|
||||||
// {}, with multiple items
|
// {}, with multiple items
|
||||||
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2)
|
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2)
|
||||||
check("echo 'foo'\\''bar baz'")
|
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
|
||||||
|
|
||||||
// {..}, strip leading whitespaces, preserve ansi
|
// {..}, strip leading whitespaces, preserve ansi
|
||||||
result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1)
|
result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1)
|
||||||
check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'")
|
checkFormat("echo {{.O}}foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
|
||||||
|
|
||||||
// {..}, strip leading whitespaces, strip ansi
|
// {..}, strip leading whitespaces, strip ansi
|
||||||
result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
|
result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
|
||||||
check("echo 'foo'\\''bar baz'")
|
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
|
||||||
|
|
||||||
// {q}
|
// {q}
|
||||||
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1)
|
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1)
|
||||||
check("echo ' foo'\\''bar baz' 'query'")
|
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}} {{.O}}query{{.O}}")
|
||||||
|
|
||||||
// {q}, multiple items
|
// {q}, multiple items
|
||||||
result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
||||||
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ''query '\\''string'\\''''foo'\\''bar baz' 'FOO'\\''BAR BAZ'")
|
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}")
|
||||||
|
|
||||||
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
||||||
check("echo 'foo'\\''bar baz''query '\\''string'\\''''foo'\\''bar baz'")
|
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}}")
|
||||||
|
|
||||||
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1)
|
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1)
|
||||||
check("echo 'foo'\\''bar'/'baz'/'bazfoo'\\''bar'/'baz'/'foo'\\''bar'/' foo'\\''bar baz'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
|
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}bazfoo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
|
||||||
|
|
||||||
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
|
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
|
||||||
check("echo 'foo'\\''bar'/'baz'/'baz'/'foo'\\''bar'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
|
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
|
||||||
|
|
||||||
result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2)
|
result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2)
|
||||||
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
|
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
|
||||||
|
|
||||||
// forcePlus
|
// forcePlus
|
||||||
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2)
|
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2)
|
||||||
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
|
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
|
||||||
|
|
||||||
// Whitespace preserving flag with "'" delimiter
|
// Whitespace preserving flag with "'" delimiter
|
||||||
result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||||
check("echo ' foo'")
|
checkFormat("echo {{.O}} foo{{.O}}")
|
||||||
|
|
||||||
result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||||
check("echo 'bar baz'")
|
checkFormat("echo {{.O}}bar baz{{.O}}")
|
||||||
|
|
||||||
result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||||
check("echo ' foo'\\''bar baz'")
|
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
||||||
|
|
||||||
result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||||
check("echo ' foo'\\''bar baz'")
|
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
||||||
|
|
||||||
// Whitespace preserving flag with regex delimiter
|
// Whitespace preserving flag with regex delimiter
|
||||||
regex = regexp.MustCompile(`\w+`)
|
regex = regexp.MustCompile(`\w+`)
|
||||||
|
|
||||||
result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||||
check("echo ' '")
|
checkFormat("echo {{.O}} {{.O}}")
|
||||||
|
|
||||||
result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||||
check("echo ''\\'''")
|
checkFormat("echo {{.O}}{{.I}}{{.O}}")
|
||||||
|
|
||||||
result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||||
check("echo ' '")
|
checkFormat("echo {{.O}} {{.O}}")
|
||||||
|
|
||||||
// No match
|
// No match
|
||||||
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
|
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
|
||||||
@@ -106,34 +126,513 @@ func TestReplacePlaceholder(t *testing.T) {
|
|||||||
|
|
||||||
// No match, but with selections
|
// No match, but with selections
|
||||||
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
|
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
|
||||||
check("echo /' foo'\\''bar baz'")
|
checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}")
|
||||||
|
|
||||||
// String delimiter
|
// String delimiter
|
||||||
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||||
check("echo ' foo'\\''bar baz'/'foo'/'bar baz'")
|
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.O}}/{{.O}}bar baz{{.O}}")
|
||||||
|
|
||||||
// Regex delimiter
|
// Regex delimiter
|
||||||
regex = regexp.MustCompile("[oa]+")
|
regex = regexp.MustCompile("[oa]+")
|
||||||
// foo'bar baz
|
// foo'bar baz
|
||||||
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||||
check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'")
|
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}f{{.O}}/{{.O}}r b{{.O}}/{{.O}}{{.I}}bar b{{.O}}")
|
||||||
|
|
||||||
|
/*
|
||||||
|
Test single placeholders, but focus on the placeholders' parameters (e.g. flags).
|
||||||
|
see: TestParsePlaceholder
|
||||||
|
*/
|
||||||
|
items3 := []*Item{
|
||||||
|
// single line
|
||||||
|
newItem("1a 1b 1c 1d 1e 1f"),
|
||||||
|
// multi line
|
||||||
|
newItem("1a 1b 1c 1d 1e 1f"),
|
||||||
|
newItem("2a 2b 2c 2d 2e 2f"),
|
||||||
|
newItem("3a 3b 3c 3d 3e 3f"),
|
||||||
|
newItem("4a 4b 4c 4d 4e 4f"),
|
||||||
|
newItem("5a 5b 5c 5d 5e 5f"),
|
||||||
|
newItem("6a 6b 6c 6d 6e 6f"),
|
||||||
|
newItem("7a 7b 7c 7d 7e 7f"),
|
||||||
|
}
|
||||||
|
stripAnsi := false
|
||||||
|
printsep = "\n"
|
||||||
|
forcePlus := false
|
||||||
|
query := "sample query"
|
||||||
|
|
||||||
|
templateToOutput := make(map[string]string)
|
||||||
|
templateToFile := make(map[string]string) // same as above, but the file contents will be matched
|
||||||
|
// I. item type placeholder
|
||||||
|
templateToOutput[`{}`] = `{{.O}}1a 1b 1c 1d 1e 1f{{.O}}`
|
||||||
|
templateToOutput[`{+}`] = `{{.O}}1a 1b 1c 1d 1e 1f{{.O}} {{.O}}2a 2b 2c 2d 2e 2f{{.O}} {{.O}}3a 3b 3c 3d 3e 3f{{.O}} {{.O}}4a 4b 4c 4d 4e 4f{{.O}} {{.O}}5a 5b 5c 5d 5e 5f{{.O}} {{.O}}6a 6b 6c 6d 6e 6f{{.O}} {{.O}}7a 7b 7c 7d 7e 7f{{.O}}`
|
||||||
|
templateToOutput[`{n}`] = `0`
|
||||||
|
templateToOutput[`{+n}`] = `0 0 0 0 0 0 0`
|
||||||
|
templateToFile[`{f}`] = `1a 1b 1c 1d 1e 1f{{.S}}`
|
||||||
|
templateToFile[`{+f}`] = `1a 1b 1c 1d 1e 1f{{.S}}2a 2b 2c 2d 2e 2f{{.S}}3a 3b 3c 3d 3e 3f{{.S}}4a 4b 4c 4d 4e 4f{{.S}}5a 5b 5c 5d 5e 5f{{.S}}6a 6b 6c 6d 6e 6f{{.S}}7a 7b 7c 7d 7e 7f{{.S}}`
|
||||||
|
templateToFile[`{nf}`] = `0{{.S}}`
|
||||||
|
templateToFile[`{+nf}`] = `0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}`
|
||||||
|
|
||||||
|
// II. token type placeholders
|
||||||
|
templateToOutput[`{..}`] = templateToOutput[`{}`]
|
||||||
|
templateToOutput[`{1..}`] = templateToOutput[`{}`]
|
||||||
|
templateToOutput[`{..2}`] = `{{.O}}1a 1b{{.O}}`
|
||||||
|
templateToOutput[`{1..2}`] = templateToOutput[`{..2}`]
|
||||||
|
templateToOutput[`{-2..-1}`] = `{{.O}}1e 1f{{.O}}`
|
||||||
|
// shorthand for x..x range
|
||||||
|
templateToOutput[`{1}`] = `{{.O}}1a{{.O}}`
|
||||||
|
templateToOutput[`{1..1}`] = templateToOutput[`{1}`]
|
||||||
|
templateToOutput[`{-6}`] = templateToOutput[`{1}`]
|
||||||
|
// multiple ranges
|
||||||
|
templateToOutput[`{1,2}`] = templateToOutput[`{1..2}`]
|
||||||
|
templateToOutput[`{1,2,4}`] = `{{.O}}1a 1b 1d{{.O}}`
|
||||||
|
templateToOutput[`{1,2..4}`] = `{{.O}}1a 1b 1c 1d{{.O}}`
|
||||||
|
templateToOutput[`{1..2,-4..-3}`] = `{{.O}}1a 1b 1c 1d{{.O}}`
|
||||||
|
// flags
|
||||||
|
templateToOutput[`{+1}`] = `{{.O}}1a{{.O}} {{.O}}2a{{.O}} {{.O}}3a{{.O}} {{.O}}4a{{.O}} {{.O}}5a{{.O}} {{.O}}6a{{.O}} {{.O}}7a{{.O}}`
|
||||||
|
templateToOutput[`{+-1}`] = `{{.O}}1f{{.O}} {{.O}}2f{{.O}} {{.O}}3f{{.O}} {{.O}}4f{{.O}} {{.O}}5f{{.O}} {{.O}}6f{{.O}} {{.O}}7f{{.O}}`
|
||||||
|
templateToOutput[`{s1}`] = `{{.O}}1a {{.O}}`
|
||||||
|
templateToFile[`{f1}`] = `1a{{.S}}`
|
||||||
|
templateToOutput[`{+s1..2}`] = `{{.O}}1a 1b {{.O}} {{.O}}2a 2b {{.O}} {{.O}}3a 3b {{.O}} {{.O}}4a 4b {{.O}} {{.O}}5a 5b {{.O}} {{.O}}6a 6b {{.O}} {{.O}}7a 7b {{.O}}`
|
||||||
|
templateToFile[`{+sf1..2}`] = `1a 1b {{.S}}2a 2b {{.S}}3a 3b {{.S}}4a 4b {{.S}}5a 5b {{.S}}6a 6b {{.S}}7a 7b {{.S}}`
|
||||||
|
|
||||||
|
// III. query type placeholder
|
||||||
|
// query flag is not removed after parsing, so it gets doubled
|
||||||
|
// while the double q is invalid, it is useful here for testing purposes
|
||||||
|
templateToOutput[`{q}`] = "{{.O}}" + query + "{{.O}}"
|
||||||
|
|
||||||
|
// IV. escaping placeholder
|
||||||
|
templateToOutput[`\{}`] = `{}`
|
||||||
|
templateToOutput[`\{++}`] = `{++}`
|
||||||
|
templateToOutput[`{++}`] = templateToOutput[`{+}`]
|
||||||
|
|
||||||
|
for giveTemplate, wantOutput := range templateToOutput {
|
||||||
|
result = replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
|
||||||
|
checkFormat(wantOutput)
|
||||||
|
}
|
||||||
|
for giveTemplate, wantOutput := range templateToFile {
|
||||||
|
path := replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
|
||||||
|
|
||||||
|
data, err := readFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Cannot read the content of the temp file %s.", path)
|
||||||
|
}
|
||||||
|
result = string(data)
|
||||||
|
|
||||||
|
checkFormat(wantOutput)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestQuoteEntryCmd(t *testing.T) {
|
func TestQuoteEntry(t *testing.T) {
|
||||||
|
type quotes struct{ E, O, SQ, DQ, BS string } // standalone escape, outer, single and double quotes, backslash
|
||||||
|
unixStyle := quotes{``, `'`, `'\''`, `"`, `\`}
|
||||||
|
windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`}
|
||||||
|
var effectiveStyle quotes
|
||||||
|
|
||||||
|
if util.IsWindows() {
|
||||||
|
effectiveStyle = windowsStyle
|
||||||
|
} else {
|
||||||
|
effectiveStyle = unixStyle
|
||||||
|
}
|
||||||
|
|
||||||
tests := map[string]string{
|
tests := map[string]string{
|
||||||
`"`: `^"\^"^"`,
|
`'`: `{{.O}}{{.SQ}}{{.O}}`,
|
||||||
`\`: `^"\\^"`,
|
`"`: `{{.O}}{{.DQ}}{{.O}}`,
|
||||||
`\"`: `^"\\\^"^"`,
|
`\`: `{{.O}}{{.BS}}{{.O}}`,
|
||||||
`"\\\"`: `^"\^"\\\\\\\^"^"`,
|
`\"`: `{{.O}}{{.BS}}{{.DQ}}{{.O}}`,
|
||||||
`&|<>()@^%!`: `^"^&^|^<^>^(^)^@^^^%^!^"`,
|
`"\\\"`: `{{.O}}{{.DQ}}{{.BS}}{{.BS}}{{.BS}}{{.DQ}}{{.O}}`,
|
||||||
`%USERPROFILE%`: `^"^%USERPROFILE^%^"`,
|
|
||||||
`C:\Program Files (x86)\`: `^"C:\\Program Files ^(x86^)\\^"`,
|
`$`: `{{.O}}${{.O}}`,
|
||||||
|
`$HOME`: `{{.O}}$HOME{{.O}}`,
|
||||||
|
`'$HOME'`: `{{.O}}{{.SQ}}$HOME{{.SQ}}{{.O}}`,
|
||||||
|
|
||||||
|
`&`: `{{.O}}{{.E}}&{{.O}}`,
|
||||||
|
`|`: `{{.O}}{{.E}}|{{.O}}`,
|
||||||
|
`<`: `{{.O}}{{.E}}<{{.O}}`,
|
||||||
|
`>`: `{{.O}}{{.E}}>{{.O}}`,
|
||||||
|
`(`: `{{.O}}{{.E}}({{.O}}`,
|
||||||
|
`)`: `{{.O}}{{.E}}){{.O}}`,
|
||||||
|
`@`: `{{.O}}{{.E}}@{{.O}}`,
|
||||||
|
`^`: `{{.O}}{{.E}}^{{.O}}`,
|
||||||
|
`%`: `{{.O}}{{.E}}%{{.O}}`,
|
||||||
|
`!`: `{{.O}}{{.E}}!{{.O}}`,
|
||||||
|
`%USERPROFILE%`: `{{.O}}{{.E}}%USERPROFILE{{.E}}%{{.O}}`,
|
||||||
|
`C:\Program Files (x86)\`: `{{.O}}C:{{.BS}}Program Files {{.E}}(x86{{.E}}){{.BS}}{{.O}}`,
|
||||||
|
`"C:\Program Files"`: `{{.O}}{{.DQ}}C:{{.BS}}Program Files{{.DQ}}{{.O}}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
for input, expected := range tests {
|
for input, expected := range tests {
|
||||||
escaped := quoteEntryCmd(input)
|
escaped := quoteEntry(input)
|
||||||
|
expected = templateToString(expected, effectiveStyle)
|
||||||
if escaped != expected {
|
if escaped != expected {
|
||||||
t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped)
|
t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Unix
|
||||||
|
func TestUnixCommands(t *testing.T) {
|
||||||
|
if util.IsWindows() {
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
tests := []testCase{
|
||||||
|
// reference: give{template, query, items}, want{output OR match}
|
||||||
|
|
||||||
|
// 1) working examples
|
||||||
|
|
||||||
|
// paths that does not have to evaluated will work fine, when quoted
|
||||||
|
{give{`grep foo {}`, ``, newItems(`test`)}, want{output: `grep foo 'test'`}},
|
||||||
|
{give{`grep foo {}`, ``, newItems(`/home/user/test`)}, want{output: `grep foo '/home/user/test'`}},
|
||||||
|
{give{`grep foo {}`, ``, newItems(`./test`)}, want{output: `grep foo './test'`}},
|
||||||
|
|
||||||
|
// only placeholders are escaped as data, this will lookup tilde character in a test file in your home directory
|
||||||
|
// quoting the tilde is required (to be treated as string)
|
||||||
|
{give{`grep {} ~/test`, ``, newItems(`~`)}, want{output: `grep '~' ~/test`}},
|
||||||
|
|
||||||
|
// 2) problematic examples
|
||||||
|
// (not necessarily unexpected)
|
||||||
|
|
||||||
|
// paths that need to expand some part of it won't work (special characters and variables)
|
||||||
|
{give{`cat {}`, ``, newItems(`~/test`)}, want{output: `cat '~/test'`}},
|
||||||
|
{give{`cat {}`, ``, newItems(`$HOME/test`)}, want{output: `cat '$HOME/test'`}},
|
||||||
|
}
|
||||||
|
testCommands(t, tests)
|
||||||
|
}
|
||||||
|
|
||||||
|
// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows
|
||||||
|
func TestWindowsCommands(t *testing.T) {
|
||||||
|
if !util.IsWindows() {
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
tests := []testCase{
|
||||||
|
// reference: give{template, query, items}, want{output OR match}
|
||||||
|
|
||||||
|
// 1) working examples
|
||||||
|
|
||||||
|
// example of redundantly escaped backslash in the output, besides looking bit ugly, it won't cause any issue
|
||||||
|
{give{`type {}`, ``, newItems(`C:\test.txt`)}, want{output: `type ^"C:\\test.txt^"`}},
|
||||||
|
{give{`rg -- "package" {}`, ``, newItems(`.\test.go`)}, want{output: `rg -- "package" ^".\\test.go^"`}},
|
||||||
|
// example of mandatorily escaped backslash in the output, otherwise `rg -- "C:\test.txt"` is matching for tabulator
|
||||||
|
{give{`rg -- {}`, ``, newItems(`C:\test.txt`)}, want{output: `rg -- ^"C:\\test.txt^"`}},
|
||||||
|
// example of mandatorily escaped double quote in the output, otherwise `rg -- ""C:\\test.txt""` is not matching for the double quotes around the path
|
||||||
|
{give{`rg -- {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `rg -- ^"\^"C:\\test.txt\^"^"`}},
|
||||||
|
|
||||||
|
// 2) problematic examples
|
||||||
|
// (not necessarily unexpected)
|
||||||
|
|
||||||
|
// notepad++'s parser can't handle `-n"12"` generate by fzf, expects `-n12`
|
||||||
|
{give{`notepad++ -n{1} {2}`, ``, newItems(`12 C:\Work\Test Folder\File.txt`)}, want{output: `notepad++ -n^"12^" ^"C:\\Work\\Test Folder\\File.txt^"`}},
|
||||||
|
|
||||||
|
// cat is parsing `\"` as a part of the file path, double quote is illegal character for paths on Windows
|
||||||
|
// cat: "C:\\test.txt: Invalid argument
|
||||||
|
{give{`cat {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `cat ^"\^"C:\\test.txt\^"^"`}},
|
||||||
|
// cat: "C:\\test.txt": Invalid argument
|
||||||
|
{give{`cmd /c {}`, ``, newItems(`cat "C:\test.txt"`)}, want{output: `cmd /c ^"cat \^"C:\\test.txt\^"^"`}},
|
||||||
|
|
||||||
|
// the "file" flag in the pattern won't create *.bat or *.cmd file so the command in the output tries to edit the file, instead of executing it
|
||||||
|
// the temp file contains: `cat "C:\test.txt"`
|
||||||
|
// TODO this should actually work
|
||||||
|
{give{`cmd /c {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^cmd /c .*\fzf-preview-[0-9]{9}$`}},
|
||||||
|
}
|
||||||
|
testCommands(t, tests)
|
||||||
|
}
|
||||||
|
|
||||||
|
// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows in Powershell
|
||||||
|
func TestPowershellCommands(t *testing.T) {
|
||||||
|
if !util.IsWindows() {
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []testCase{
|
||||||
|
// reference: give{template, query, items}, want{output OR match}
|
||||||
|
|
||||||
|
/*
|
||||||
|
You can read each line in the following table as a pipeline that
|
||||||
|
consist of series of parsers that act upon your input (col. 1) and
|
||||||
|
each cell represents the output value.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
- exec.Command("program.exe", `\''`)
|
||||||
|
- goes to win32 api which will process it transparently as it contains no special characters, see [CommandLineToArgvW][].
|
||||||
|
- powershell command will receive it as is, that is two arguments: a literal backslash and empty string in single quotes
|
||||||
|
- native command run via/from powershell will receive only one argument: a literal backslash. Because extra parsing rules apply, see [NativeCallsFromPowershell][].
|
||||||
|
- some¹ apps have internal parser, that requires one more level of escaping (yes, this is completely application-specific, but see terminal_test.go#TestWindowsCommands)
|
||||||
|
|
||||||
|
Character⁰ CommandLineToArgvW Powershell commands Native commands from Powershell Apps requiring escapes¹ | Being tested below
|
||||||
|
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
|
||||||
|
" empty string² missing argument error ... ... |
|
||||||
|
\" literal " unbalanced quote error ... ... |
|
||||||
|
'\"' literal '"' literal " empty string empty string (match all) | yes
|
||||||
|
'\\\"' literal '\"' literal \" literal " literal " |
|
||||||
|
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
|
||||||
|
\ transparent transparent transparent regex error |
|
||||||
|
'\' transparent literal \ literal \ regex error | yes
|
||||||
|
\\ transparent transparent transparent literal \ |
|
||||||
|
'\\' transparent literal \\ literal \\ literal \ |
|
||||||
|
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
|
||||||
|
' transparent unbalanced quote error ... ... |
|
||||||
|
\' transparent literal \ and unb. quote error ... ... |
|
||||||
|
\'' transparent literal \ and empty string literal \ regex error | no, but given as example above
|
||||||
|
''' transparent unbalanced quote error ... ... |
|
||||||
|
'''' transparent literal ' literal ' literal ' | yes
|
||||||
|
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
|
||||||
|
|
||||||
|
⁰: charatecter or characters 'x' as an argument to a program in go's call: exec.Command("program.exe", `x`)
|
||||||
|
¹: native commands like grep, git grep, ripgrep
|
||||||
|
²: interpreted as a grouping quote, affects argument parser and gets removed from the result
|
||||||
|
|
||||||
|
[CommandLineToArgvW]: https://docs.microsoft.com/en-gb/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw#remarks
|
||||||
|
[NativeCallsFromPowershell]: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing?view=powershell-7.1#passing-arguments-that-contain-quote-characters
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 1) working examples
|
||||||
|
|
||||||
|
{give{`Get-Content {}`, ``, newItems(`C:\test.txt`)}, want{output: `Get-Content 'C:\test.txt'`}},
|
||||||
|
{give{`rg -- "package" {}`, ``, newItems(`.\test.go`)}, want{output: `rg -- "package" '.\test.go'`}},
|
||||||
|
|
||||||
|
// example of escaping single quotes
|
||||||
|
{give{`rg -- {}`, ``, newItems(`'foobar'`)}, want{output: `rg -- '''foobar'''`}},
|
||||||
|
|
||||||
|
// chaining powershells
|
||||||
|
{give{`powershell -NoProfile -Command {}`, ``, newItems(`cat "C:\test.txt"`)}, want{output: `powershell -NoProfile -Command 'cat \"C:\test.txt\"'`}},
|
||||||
|
|
||||||
|
// 2) problematic examples
|
||||||
|
// (not necessarily unexpected)
|
||||||
|
|
||||||
|
// looking for a path string will only work with escaped backslashes
|
||||||
|
{give{`rg -- {}`, ``, newItems(`C:\test.txt`)}, want{output: `rg -- 'C:\test.txt'`}},
|
||||||
|
// looking for a literal double quote will only work with triple escaped double quotes
|
||||||
|
{give{`rg -- {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `rg -- '\"C:\test.txt\"'`}},
|
||||||
|
|
||||||
|
// Get-Content (i.e. cat alias) is parsing `"` as a part of the file path, returns an error:
|
||||||
|
// Get-Content : Cannot find drive. A drive with the name '"C:' does not exist.
|
||||||
|
{give{`cat {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `cat '\"C:\test.txt\"'`}},
|
||||||
|
|
||||||
|
// the "file" flag in the pattern won't create *.ps1 file so the powershell will offload this "unknown" filetype
|
||||||
|
// to explorer, which will prompt user to pick editing program for the fzf-preview file
|
||||||
|
// the temp file contains: `cat "C:\test.txt"`
|
||||||
|
// TODO this should actually work
|
||||||
|
{give{`powershell -NoProfile -Command {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^powershell -NoProfile -Command .*\fzf-preview-[0-9]{9}$`}},
|
||||||
|
}
|
||||||
|
|
||||||
|
// to force powershell-style escaping we temporarily set environment variable that fzf honors
|
||||||
|
shellBackup := os.Getenv("SHELL")
|
||||||
|
os.Setenv("SHELL", "powershell")
|
||||||
|
testCommands(t, tests)
|
||||||
|
os.Setenv("SHELL", shellBackup)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Test typical valid placeholders and parsing of them.
|
||||||
|
|
||||||
|
Also since the parser assumes the input is matched with `placeholder` regex,
|
||||||
|
the regex is tested here as well.
|
||||||
|
*/
|
||||||
|
func TestParsePlaceholder(t *testing.T) {
|
||||||
|
// give, want pairs
|
||||||
|
templates := map[string]string{
|
||||||
|
// I. item type placeholder
|
||||||
|
`{}`: `{}`,
|
||||||
|
`{+}`: `{+}`,
|
||||||
|
`{n}`: `{n}`,
|
||||||
|
`{+n}`: `{+n}`,
|
||||||
|
`{f}`: `{f}`,
|
||||||
|
`{+nf}`: `{+nf}`,
|
||||||
|
|
||||||
|
// II. token type placeholders
|
||||||
|
`{..}`: `{..}`,
|
||||||
|
`{1..}`: `{1..}`,
|
||||||
|
`{..2}`: `{..2}`,
|
||||||
|
`{1..2}`: `{1..2}`,
|
||||||
|
`{-2..-1}`: `{-2..-1}`,
|
||||||
|
// shorthand for x..x range
|
||||||
|
`{1}`: `{1}`,
|
||||||
|
`{1..1}`: `{1..1}`,
|
||||||
|
`{-6}`: `{-6}`,
|
||||||
|
// multiple ranges
|
||||||
|
`{1,2}`: `{1,2}`,
|
||||||
|
`{1,2,4}`: `{1,2,4}`,
|
||||||
|
`{1,2..4}`: `{1,2..4}`,
|
||||||
|
`{1..2,-4..-3}`: `{1..2,-4..-3}`,
|
||||||
|
// flags
|
||||||
|
`{+1}`: `{+1}`,
|
||||||
|
`{+-1}`: `{+-1}`,
|
||||||
|
`{s1}`: `{s1}`,
|
||||||
|
`{f1}`: `{f1}`,
|
||||||
|
`{+s1..2}`: `{+s1..2}`,
|
||||||
|
`{+sf1..2}`: `{+sf1..2}`,
|
||||||
|
|
||||||
|
// III. query type placeholder
|
||||||
|
// query flag is not removed after parsing, so it gets doubled
|
||||||
|
// while the double q is invalid, it is useful here for testing purposes
|
||||||
|
`{q}`: `{qq}`,
|
||||||
|
|
||||||
|
// IV. escaping placeholder
|
||||||
|
`\{}`: `{}`,
|
||||||
|
`\{++}`: `{++}`,
|
||||||
|
`{++}`: `{+}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for giveTemplate, wantTemplate := range templates {
|
||||||
|
if !placeholder.MatchString(giveTemplate) {
|
||||||
|
t.Errorf(`given placeholder %s does not match placeholder regex, so attempt to parse it is unexpected`, giveTemplate)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, placeholderWithoutFlags, flags := parsePlaceholder(giveTemplate)
|
||||||
|
gotTemplate := placeholderWithoutFlags[:1] + flags.encodePlaceholder() + placeholderWithoutFlags[1:]
|
||||||
|
|
||||||
|
if gotTemplate != wantTemplate {
|
||||||
|
t.Errorf(`parsed placeholder "%s" into "%s", but want "%s"`, giveTemplate, gotTemplate, wantTemplate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* utilities section */
|
||||||
|
|
||||||
|
// Item represents one line in fzf UI. Usually it is relative path to files and folders.
|
||||||
|
func newItem(str string) *Item {
|
||||||
|
bytes := []byte(str)
|
||||||
|
trimmed, _, _ := extractColor(str, nil, nil)
|
||||||
|
return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Functions tested in this file require array of items (allItems). The array needs
|
||||||
|
// to consist of at least two nils. This is helper function.
|
||||||
|
func newItems(str ...string) []*Item {
|
||||||
|
result := make([]*Item, util.Max(len(str), 2))
|
||||||
|
for i, s := range str {
|
||||||
|
result[i] = newItem(s)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// (for logging purposes)
|
||||||
|
func (item *Item) String() string {
|
||||||
|
return item.AsString(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to parse, execute and convert "text/template" to string. Panics on error.
|
||||||
|
func templateToString(format string, data interface{}) string {
|
||||||
|
bb := &bytes.Buffer{}
|
||||||
|
|
||||||
|
err := template.Must(template.New("").Parse(format)).Execute(bb, data)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ad hoc types for test cases
|
||||||
|
type give struct {
|
||||||
|
template string
|
||||||
|
query string
|
||||||
|
allItems []*Item
|
||||||
|
}
|
||||||
|
type want struct {
|
||||||
|
/*
|
||||||
|
Unix:
|
||||||
|
The `want.output` string is supposed to be formatted for evaluation by
|
||||||
|
`sh -c command` system call.
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
The `want.output` string is supposed to be formatted for evaluation by
|
||||||
|
`cmd.exe /s /c "command"` system call. The `/s` switch enables so called old
|
||||||
|
behaviour, which is more favourable for nesting (possibly escaped)
|
||||||
|
special characters. This is the relevant section of `help cmd`:
|
||||||
|
|
||||||
|
...old behavior is to see if the first character is
|
||||||
|
a quote character and if so, strip the leading character and
|
||||||
|
remove the last quote character on the command line, preserving
|
||||||
|
any text after the last quote character.
|
||||||
|
*/
|
||||||
|
output string // literal output
|
||||||
|
match string // output is matched against this regex (when output is empty string)
|
||||||
|
}
|
||||||
|
type testCase struct {
|
||||||
|
give
|
||||||
|
want
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCommands(t *testing.T, tests []testCase) {
|
||||||
|
// common test parameters
|
||||||
|
delim := "\t"
|
||||||
|
delimiter := Delimiter{str: &delim}
|
||||||
|
printsep := ""
|
||||||
|
stripAnsi := false
|
||||||
|
forcePlus := false
|
||||||
|
|
||||||
|
// evaluate the test cases
|
||||||
|
for idx, test := range tests {
|
||||||
|
gotOutput := replacePlaceholder(
|
||||||
|
test.give.template, stripAnsi, delimiter, printsep, forcePlus,
|
||||||
|
test.give.query,
|
||||||
|
test.give.allItems)
|
||||||
|
switch {
|
||||||
|
case test.want.output != "":
|
||||||
|
if gotOutput != test.want.output {
|
||||||
|
t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
|
||||||
|
idx,
|
||||||
|
test.give.template, test.give.query, test.give.allItems,
|
||||||
|
gotOutput, test.want.output)
|
||||||
|
}
|
||||||
|
case test.want.match != "":
|
||||||
|
wantMatch := strings.ReplaceAll(test.want.match, `\`, `\\`)
|
||||||
|
wantRegex := regexp.MustCompile(wantMatch)
|
||||||
|
if !wantRegex.MatchString(gotOutput) {
|
||||||
|
t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
|
||||||
|
idx,
|
||||||
|
test.give.template, test.give.query, test.give.allItems,
|
||||||
|
gotOutput, test.want.match)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Errorf("tests[%v]: test case does not describe 'want' property", idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// naive encoder of placeholder flags
|
||||||
|
func (flags placeholderFlags) encodePlaceholder() string {
|
||||||
|
encoded := ""
|
||||||
|
if flags.plus {
|
||||||
|
encoded += "+"
|
||||||
|
}
|
||||||
|
if flags.preserveSpace {
|
||||||
|
encoded += "s"
|
||||||
|
}
|
||||||
|
if flags.number {
|
||||||
|
encoded += "n"
|
||||||
|
}
|
||||||
|
if flags.file {
|
||||||
|
encoded += "f"
|
||||||
|
}
|
||||||
|
if flags.query {
|
||||||
|
encoded += "q"
|
||||||
|
}
|
||||||
|
return encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
// can be replaced with os.ReadFile() in go 1.16+
|
||||||
|
func readFile(path string) ([]byte, error) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
data := make([]byte, 0, 128)
|
||||||
|
for {
|
||||||
|
if len(data) >= cap(data) {
|
||||||
|
d := append(data[:cap(data)], 0)
|
||||||
|
data = d[:len(data)]
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := file.Read(data[len(data):cap(data)])
|
||||||
|
data = data[:len(data)+n]
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -5,6 +5,7 @@ package fzf
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,3 +20,7 @@ func notifyStop(p *os.Process) {
|
|||||||
func notifyOnCont(resizeChan chan<- os.Signal) {
|
func notifyOnCont(resizeChan chan<- os.Signal) {
|
||||||
signal.Notify(resizeChan, syscall.SIGCONT)
|
signal.Notify(resizeChan, syscall.SIGCONT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func quoteEntry(entry string) string {
|
||||||
|
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
|
||||||
|
}
|
||||||
|
@@ -4,6 +4,8 @@ package fzf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func notifyOnResize(resizeChan chan<- os.Signal) {
|
func notifyOnResize(resizeChan chan<- os.Signal) {
|
||||||
@@ -17,3 +19,27 @@ func notifyStop(p *os.Process) {
|
|||||||
func notifyOnCont(resizeChan chan<- os.Signal) {
|
func notifyOnCont(resizeChan chan<- os.Signal) {
|
||||||
// NOOP
|
// NOOP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func quoteEntry(entry string) string {
|
||||||
|
shell := os.Getenv("SHELL")
|
||||||
|
if len(shell) == 0 {
|
||||||
|
shell = "cmd"
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(shell, "cmd") {
|
||||||
|
// backslash escaping is done here for applications
|
||||||
|
// (see ripgrep test case in terminal_test.go#TestWindowsCommands)
|
||||||
|
escaped := strings.Replace(entry, `\`, `\\`, -1)
|
||||||
|
escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"`
|
||||||
|
// caret is the escape character for cmd shell
|
||||||
|
r, _ := regexp.Compile(`[&|<>()@^%!"]`)
|
||||||
|
return r.ReplaceAllStringFunc(escaped, func(match string) string {
|
||||||
|
return "^" + match
|
||||||
|
})
|
||||||
|
} else if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") {
|
||||||
|
escaped := strings.Replace(entry, `"`, `\"`, -1)
|
||||||
|
return "'" + strings.Replace(escaped, "'", "''", -1) + "'"
|
||||||
|
} else {
|
||||||
|
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -35,7 +35,7 @@ type Delimiter struct {
|
|||||||
str *string
|
str *string
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns the string representation of a Delimeter.
|
// String returns the string representation of a Delimiter.
|
||||||
func (d Delimiter) String() string {
|
func (d Delimiter) String() string {
|
||||||
return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str)
|
return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str)
|
||||||
}
|
}
|
||||||
|
@@ -10,9 +10,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/util"
|
"github.com/mattn/go-runewidth"
|
||||||
|
"github.com/rivo/uniseg"
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh/terminal"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -50,7 +51,7 @@ func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) {
|
|||||||
}
|
}
|
||||||
bytes = bytes[sz:]
|
bytes = bytes[sz:]
|
||||||
}
|
}
|
||||||
r.queued += string(runes)
|
r.queued.WriteString(string(runes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) csi(code string) {
|
func (r *LightRenderer) csi(code string) {
|
||||||
@@ -58,9 +59,9 @@ func (r *LightRenderer) csi(code string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) flush() {
|
func (r *LightRenderer) flush() {
|
||||||
if len(r.queued) > 0 {
|
if r.queued.Len() > 0 {
|
||||||
fmt.Fprint(os.Stderr, r.queued)
|
fmt.Fprint(os.Stderr, r.queued.String())
|
||||||
r.queued = ""
|
r.queued.Reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ type LightRenderer struct {
|
|||||||
clickY []int
|
clickY []int
|
||||||
ttyin *os.File
|
ttyin *os.File
|
||||||
buffer []byte
|
buffer []byte
|
||||||
origState *terminal.State
|
origState *term.State
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
yoffset int
|
yoffset int
|
||||||
@@ -82,7 +83,7 @@ type LightRenderer struct {
|
|||||||
escDelay int
|
escDelay int
|
||||||
fullscreen bool
|
fullscreen bool
|
||||||
upOneLine bool
|
upOneLine bool
|
||||||
queued string
|
queued strings.Builder
|
||||||
y int
|
y int
|
||||||
x int
|
x int
|
||||||
maxHeightFunc func(int) int
|
maxHeightFunc func(int) int
|
||||||
@@ -693,13 +694,17 @@ func (w *LightWindow) drawBorder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
|
func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
|
||||||
|
color := ColBorder
|
||||||
|
if w.preview {
|
||||||
|
color = ColPreviewBorder
|
||||||
|
}
|
||||||
if top {
|
if top {
|
||||||
w.Move(0, 0)
|
w.Move(0, 0)
|
||||||
w.CPrint(ColBorder, repeat(w.border.horizontal, w.width))
|
w.CPrint(color, repeat(w.border.horizontal, w.width))
|
||||||
}
|
}
|
||||||
if bottom {
|
if bottom {
|
||||||
w.Move(w.height-1, 0)
|
w.Move(w.height-1, 0)
|
||||||
w.CPrint(ColBorder, repeat(w.border.horizontal, w.width))
|
w.CPrint(color, repeat(w.border.horizontal, w.width))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -708,14 +713,18 @@ func (w *LightWindow) drawBorderVertical(left, right bool) {
|
|||||||
if !left || !right {
|
if !left || !right {
|
||||||
width++
|
width++
|
||||||
}
|
}
|
||||||
|
color := ColBorder
|
||||||
|
if w.preview {
|
||||||
|
color = ColPreviewBorder
|
||||||
|
}
|
||||||
for y := 0; y < w.height; y++ {
|
for y := 0; y < w.height; y++ {
|
||||||
w.Move(y, 0)
|
w.Move(y, 0)
|
||||||
if left {
|
if left {
|
||||||
w.CPrint(ColBorder, string(w.border.vertical))
|
w.CPrint(color, string(w.border.vertical))
|
||||||
}
|
}
|
||||||
w.CPrint(ColBorder, repeat(' ', width))
|
w.CPrint(color, repeat(' ', width))
|
||||||
if right {
|
if right {
|
||||||
w.CPrint(ColBorder, string(w.border.vertical))
|
w.CPrint(color, string(w.border.vertical))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -881,20 +890,26 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
|
|||||||
lines := []wrappedLine{}
|
lines := []wrappedLine{}
|
||||||
width := 0
|
width := 0
|
||||||
line := ""
|
line := ""
|
||||||
for _, r := range input {
|
gr := uniseg.NewGraphemes(input)
|
||||||
w := util.RuneWidth(r, prefixLength+width, 8)
|
for gr.Next() {
|
||||||
width += w
|
rs := gr.Runes()
|
||||||
str := string(r)
|
str := string(rs)
|
||||||
if r == '\t' {
|
var w int
|
||||||
|
if len(rs) == 1 && rs[0] == '\t' {
|
||||||
|
w = tabstop - (prefixLength+width)%tabstop
|
||||||
str = repeat(' ', w)
|
str = repeat(' ', w)
|
||||||
|
} else {
|
||||||
|
w = runewidth.StringWidth(str)
|
||||||
}
|
}
|
||||||
|
width += w
|
||||||
|
|
||||||
if prefixLength+width <= max {
|
if prefixLength+width <= max {
|
||||||
line += str
|
line += str
|
||||||
} else {
|
} else {
|
||||||
lines = append(lines, wrappedLine{string(line), width - w})
|
lines = append(lines, wrappedLine{string(line), width - w})
|
||||||
line = str
|
line = str
|
||||||
prefixLength = 0
|
prefixLength = 0
|
||||||
width = util.RuneWidth(r, prefixLength, 8)
|
width = w
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines = append(lines, wrappedLine{string(line), width})
|
lines = append(lines, wrappedLine{string(line), width})
|
||||||
@@ -906,12 +921,6 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
|
|||||||
for i, line := range allLines {
|
for i, line := range allLines {
|
||||||
lines := wrapLine(line, w.posx, w.width, w.tabstop)
|
lines := wrapLine(line, w.posx, w.width, w.tabstop)
|
||||||
for j, wl := range lines {
|
for j, wl := range lines {
|
||||||
if w.posx >= w.Width()-1 && wl.displayWidth == 0 {
|
|
||||||
if w.posy < w.height-1 {
|
|
||||||
w.Move(w.posy+1, 0)
|
|
||||||
}
|
|
||||||
return FillNextLine
|
|
||||||
}
|
|
||||||
w.stderrInternal(wl.text, false)
|
w.stderrInternal(wl.text, false)
|
||||||
w.posx += wl.displayWidth
|
w.posx += wl.displayWidth
|
||||||
|
|
||||||
@@ -926,6 +935,14 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if w.posx+1 >= w.Width() {
|
||||||
|
if w.posy+1 >= w.height {
|
||||||
|
return FillSuspend
|
||||||
|
}
|
||||||
|
w.Move(w.posy+1, 0)
|
||||||
|
onMove()
|
||||||
|
return FillNextLine
|
||||||
|
}
|
||||||
return FillContinue
|
return FillContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -10,7 +10,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/util"
|
"github.com/junegunn/fzf/src/util"
|
||||||
"golang.org/x/crypto/ssh/terminal"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
func IsLightRendererSupported() bool {
|
func IsLightRendererSupported() bool {
|
||||||
@@ -34,12 +34,12 @@ func (r *LightRenderer) fd() int {
|
|||||||
|
|
||||||
func (r *LightRenderer) initPlatform() error {
|
func (r *LightRenderer) initPlatform() error {
|
||||||
fd := r.fd()
|
fd := r.fd()
|
||||||
origState, err := terminal.GetState(fd)
|
origState, err := term.GetState(fd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
r.origState = origState
|
r.origState = origState
|
||||||
terminal.MakeRaw(fd)
|
term.MakeRaw(fd)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,15 +63,15 @@ func openTtyIn() *os.File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) setupTerminal() {
|
func (r *LightRenderer) setupTerminal() {
|
||||||
terminal.MakeRaw(r.fd())
|
term.MakeRaw(r.fd())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) restoreTerminal() {
|
func (r *LightRenderer) restoreTerminal() {
|
||||||
terminal.Restore(r.fd(), r.origState)
|
term.Restore(r.fd(), r.origState)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) updateTerminalSize() {
|
func (r *LightRenderer) updateTerminalSize() {
|
||||||
width, height, err := terminal.GetSize(r.fd())
|
width, height, err := term.GetSize(r.fd())
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
r.width = width
|
r.width = width
|
||||||
|
@@ -5,11 +5,16 @@ package tui
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/util"
|
"github.com/junegunn/fzf/src/util"
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
timeoutInterval = 10
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
consoleFlagsInput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_EXTENDED_FLAGS)
|
consoleFlagsInput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_EXTENDED_FLAGS)
|
||||||
consoleFlagsOutput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING | windows.ENABLE_PROCESSED_OUTPUT | windows.DISABLE_NEWLINE_AUTO_RETURN)
|
consoleFlagsOutput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING | windows.ENABLE_PROCESSED_OUTPUT | windows.DISABLE_NEWLINE_AUTO_RETURN)
|
||||||
@@ -60,7 +65,7 @@ func (r *LightRenderer) initPlatform() error {
|
|||||||
|
|
||||||
// channel for non-blocking reads. Buffer to make sure
|
// channel for non-blocking reads. Buffer to make sure
|
||||||
// we get the ESC sets:
|
// we get the ESC sets:
|
||||||
r.ttyinChannel = make(chan byte, 12)
|
r.ttyinChannel = make(chan byte, 1024)
|
||||||
|
|
||||||
// the following allows for non-blocking IO.
|
// the following allows for non-blocking IO.
|
||||||
// syscall.SetNonblock() is a NOOP under Windows.
|
// syscall.SetNonblock() is a NOOP under Windows.
|
||||||
@@ -130,7 +135,7 @@ func (r *LightRenderer) getch(nonblock bool) (int, bool) {
|
|||||||
select {
|
select {
|
||||||
case bc := <-r.ttyinChannel:
|
case bc := <-r.ttyinChannel:
|
||||||
return int(bc), true
|
return int(bc), true
|
||||||
default:
|
case <-time.After(timeoutInterval * time.Millisecond):
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
143
src/tui/tcell.go
143
src/tui/tcell.go
@@ -5,7 +5,6 @@ package tui
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
@@ -13,6 +12,7 @@ import (
|
|||||||
"github.com/gdamore/tcell/encoding"
|
"github.com/gdamore/tcell/encoding"
|
||||||
|
|
||||||
"github.com/mattn/go-runewidth"
|
"github.com/mattn/go-runewidth"
|
||||||
|
"github.com/rivo/uniseg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HasFullscreenRenderer() bool {
|
func HasFullscreenRenderer() bool {
|
||||||
@@ -120,8 +120,11 @@ func (a Attr) Merge(b Attr) Attr {
|
|||||||
return a | b
|
return a | b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handle the following as private members of FullscreenRenderer instance
|
||||||
|
// they are declared here to prevent introducing tcell library in non-windows builds
|
||||||
var (
|
var (
|
||||||
_screen tcell.Screen
|
_screen tcell.Screen
|
||||||
|
_prevMouseButton tcell.ButtonMask
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *FullscreenRenderer) initScreen() {
|
func (r *FullscreenRenderer) initScreen() {
|
||||||
@@ -185,14 +188,48 @@ func (r *FullscreenRenderer) GetChar() Event {
|
|||||||
|
|
||||||
// process mouse events:
|
// process mouse events:
|
||||||
case *tcell.EventMouse:
|
case *tcell.EventMouse:
|
||||||
|
// mouse down events have zeroed buttons, so we can't use them
|
||||||
|
// mouse up event consists of two events, 1. (main) event with modifier and other metadata, 2. event with zeroed buttons
|
||||||
|
// so mouse click is three consecutive events, but the first and last are indistinguishable from movement events (with released buttons)
|
||||||
|
// dragging has same structure, it only repeats the middle (main) event appropriately
|
||||||
x, y := ev.Position()
|
x, y := ev.Position()
|
||||||
button := ev.Buttons()
|
|
||||||
mod := ev.Modifiers() != 0
|
mod := ev.Modifiers() != 0
|
||||||
if button&tcell.WheelDown != 0 {
|
|
||||||
|
// since we dont have mouse down events (unlike LightRenderer), we need to track state in prevButton
|
||||||
|
prevButton, button := _prevMouseButton, ev.Buttons()
|
||||||
|
_prevMouseButton = button
|
||||||
|
drag := prevButton == button
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case button&tcell.WheelDown != 0:
|
||||||
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}}
|
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}}
|
||||||
} else if button&tcell.WheelUp != 0 {
|
case button&tcell.WheelUp != 0:
|
||||||
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}}
|
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}}
|
||||||
} else if runtime.GOOS != "windows" {
|
case button&tcell.Button1 != 0 && !drag:
|
||||||
|
// all potential double click events put their 'line' coordinate in the clickY array
|
||||||
|
// double click event has two conditions, temporal and spatial, the first is checked here
|
||||||
|
now := time.Now()
|
||||||
|
if now.Sub(r.prevDownTime) < doubleClickDuration {
|
||||||
|
r.clickY = append(r.clickY, y)
|
||||||
|
} else {
|
||||||
|
r.clickY = []int{y}
|
||||||
|
}
|
||||||
|
r.prevDownTime = now
|
||||||
|
|
||||||
|
// detect double clicks (also check for spatial condition)
|
||||||
|
n := len(r.clickY)
|
||||||
|
double := n > 1 && r.clickY[n-2] == r.clickY[n-1]
|
||||||
|
if double {
|
||||||
|
// make sure two consecutive double clicks require four clicks
|
||||||
|
r.clickY = []int{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fire single or double click event
|
||||||
|
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, mod}}
|
||||||
|
case button&tcell.Button2 != 0 && !drag:
|
||||||
|
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, mod}}
|
||||||
|
case runtime.GOOS != "windows":
|
||||||
|
|
||||||
// double and single taps on Windows don't quite work due to
|
// double and single taps on Windows don't quite work due to
|
||||||
// the console acting on the events and not allowing us
|
// the console acting on the events and not allowing us
|
||||||
// to consume them.
|
// to consume them.
|
||||||
@@ -223,9 +260,13 @@ func (r *FullscreenRenderer) GetChar() Event {
|
|||||||
// process keyboard:
|
// process keyboard:
|
||||||
case *tcell.EventKey:
|
case *tcell.EventKey:
|
||||||
mods := ev.Modifiers()
|
mods := ev.Modifiers()
|
||||||
|
none := mods == tcell.ModNone
|
||||||
alt := (mods & tcell.ModAlt) > 0
|
alt := (mods & tcell.ModAlt) > 0
|
||||||
|
ctrl := (mods & tcell.ModCtrl) > 0
|
||||||
shift := (mods & tcell.ModShift) > 0
|
shift := (mods & tcell.ModShift) > 0
|
||||||
|
ctrlAlt := ctrl && alt
|
||||||
altShift := alt && shift
|
altShift := alt && shift
|
||||||
|
|
||||||
keyfn := func(r rune) Event {
|
keyfn := func(r rune) Event {
|
||||||
if alt {
|
if alt {
|
||||||
return CtrlAltKey(r)
|
return CtrlAltKey(r)
|
||||||
@@ -233,6 +274,7 @@ func (r *FullscreenRenderer) GetChar() Event {
|
|||||||
return EventType(CtrlA.Int() - 'a' + int(r)).AsEvent()
|
return EventType(CtrlA.Int() - 'a' + int(r)).AsEvent()
|
||||||
}
|
}
|
||||||
switch ev.Key() {
|
switch ev.Key() {
|
||||||
|
// section 1: Ctrl+(Alt)+[a-z]
|
||||||
case tcell.KeyCtrlA:
|
case tcell.KeyCtrlA:
|
||||||
return keyfn('a')
|
return keyfn('a')
|
||||||
case tcell.KeyCtrlB:
|
case tcell.KeyCtrlB:
|
||||||
@@ -248,7 +290,21 @@ func (r *FullscreenRenderer) GetChar() Event {
|
|||||||
case tcell.KeyCtrlG:
|
case tcell.KeyCtrlG:
|
||||||
return keyfn('g')
|
return keyfn('g')
|
||||||
case tcell.KeyCtrlH:
|
case tcell.KeyCtrlH:
|
||||||
|
switch ev.Rune() {
|
||||||
|
case 0:
|
||||||
|
if ctrl {
|
||||||
|
return Event{BSpace, 0, nil}
|
||||||
|
}
|
||||||
|
case rune(tcell.KeyCtrlH):
|
||||||
|
switch {
|
||||||
|
case ctrl:
|
||||||
return keyfn('h')
|
return keyfn('h')
|
||||||
|
case alt:
|
||||||
|
return Event{AltBS, 0, nil}
|
||||||
|
case none, shift:
|
||||||
|
return Event{BSpace, 0, nil}
|
||||||
|
}
|
||||||
|
}
|
||||||
case tcell.KeyCtrlI:
|
case tcell.KeyCtrlI:
|
||||||
return keyfn('i')
|
return keyfn('i')
|
||||||
case tcell.KeyCtrlJ:
|
case tcell.KeyCtrlJ:
|
||||||
@@ -285,20 +341,25 @@ func (r *FullscreenRenderer) GetChar() Event {
|
|||||||
return keyfn('y')
|
return keyfn('y')
|
||||||
case tcell.KeyCtrlZ:
|
case tcell.KeyCtrlZ:
|
||||||
return keyfn('z')
|
return keyfn('z')
|
||||||
|
// section 2: Ctrl+[ \]_]
|
||||||
case tcell.KeyCtrlSpace:
|
case tcell.KeyCtrlSpace:
|
||||||
return Event{CtrlSpace, 0, nil}
|
return Event{CtrlSpace, 0, nil}
|
||||||
case tcell.KeyCtrlBackslash:
|
case tcell.KeyCtrlBackslash:
|
||||||
return Event{CtrlBackSlash, 0, nil}
|
return Event{CtrlBackSlash, 0, nil}
|
||||||
case tcell.KeyCtrlRightSq:
|
case tcell.KeyCtrlRightSq:
|
||||||
return Event{CtrlRightBracket, 0, nil}
|
return Event{CtrlRightBracket, 0, nil}
|
||||||
|
case tcell.KeyCtrlCarat:
|
||||||
|
return Event{CtrlCaret, 0, nil}
|
||||||
case tcell.KeyCtrlUnderscore:
|
case tcell.KeyCtrlUnderscore:
|
||||||
return Event{CtrlSlash, 0, nil}
|
return Event{CtrlSlash, 0, nil}
|
||||||
|
// section 3: (Alt)+Backspace2
|
||||||
case tcell.KeyBackspace2:
|
case tcell.KeyBackspace2:
|
||||||
if alt {
|
if alt {
|
||||||
return Event{AltBS, 0, nil}
|
return Event{AltBS, 0, nil}
|
||||||
}
|
}
|
||||||
return Event{BSpace, 0, nil}
|
return Event{BSpace, 0, nil}
|
||||||
|
|
||||||
|
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
|
||||||
case tcell.KeyUp:
|
case tcell.KeyUp:
|
||||||
if altShift {
|
if altShift {
|
||||||
return Event{AltSUp, 0, nil}
|
return Event{AltSUp, 0, nil}
|
||||||
@@ -344,6 +405,7 @@ func (r *FullscreenRenderer) GetChar() Event {
|
|||||||
}
|
}
|
||||||
return Event{Right, 0, nil}
|
return Event{Right, 0, nil}
|
||||||
|
|
||||||
|
// section 5: (Insert|Home|Delete|End|PgUp|PgDn|BackTab|F1-F12)
|
||||||
case tcell.KeyInsert:
|
case tcell.KeyInsert:
|
||||||
return Event{Insert, 0, nil}
|
return Event{Insert, 0, nil}
|
||||||
case tcell.KeyHome:
|
case tcell.KeyHome:
|
||||||
@@ -356,10 +418,8 @@ func (r *FullscreenRenderer) GetChar() Event {
|
|||||||
return Event{PgUp, 0, nil}
|
return Event{PgUp, 0, nil}
|
||||||
case tcell.KeyPgDn:
|
case tcell.KeyPgDn:
|
||||||
return Event{PgDn, 0, nil}
|
return Event{PgDn, 0, nil}
|
||||||
|
|
||||||
case tcell.KeyBacktab:
|
case tcell.KeyBacktab:
|
||||||
return Event{BTab, 0, nil}
|
return Event{BTab, 0, nil}
|
||||||
|
|
||||||
case tcell.KeyF1:
|
case tcell.KeyF1:
|
||||||
return Event{F1, 0, nil}
|
return Event{F1, 0, nil}
|
||||||
case tcell.KeyF2:
|
case tcell.KeyF2:
|
||||||
@@ -385,20 +445,31 @@ func (r *FullscreenRenderer) GetChar() Event {
|
|||||||
case tcell.KeyF12:
|
case tcell.KeyF12:
|
||||||
return Event{F12, 0, nil}
|
return Event{F12, 0, nil}
|
||||||
|
|
||||||
// ev.Ch doesn't work for some reason for space:
|
// section 6: (Ctrl+Alt)+'rune'
|
||||||
case tcell.KeyRune:
|
case tcell.KeyRune:
|
||||||
r := ev.Rune()
|
r := ev.Rune()
|
||||||
if alt {
|
|
||||||
return AltKey(r)
|
|
||||||
}
|
|
||||||
return Event{Rune, r, nil}
|
|
||||||
|
|
||||||
|
switch {
|
||||||
|
// translate native key events to ascii control characters
|
||||||
|
case r == ' ' && ctrl:
|
||||||
|
return Event{CtrlSpace, 0, nil}
|
||||||
|
// handle AltGr characters
|
||||||
|
case ctrlAlt:
|
||||||
|
return Event{Rune, r, nil} // dropping modifiers
|
||||||
|
// simple characters (possibly with modifier)
|
||||||
|
case alt:
|
||||||
|
return AltKey(r)
|
||||||
|
default:
|
||||||
|
return Event{Rune, r, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
// section 7: Esc
|
||||||
case tcell.KeyEsc:
|
case tcell.KeyEsc:
|
||||||
return Event{ESC, 0, nil}
|
return Event{ESC, 0, nil}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// section 8: Invalid
|
||||||
return Event{Invalid, 0, nil}
|
return Event{Invalid, 0, nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,7 +553,6 @@ func (w *TcellWindow) Print(text string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) printString(text string, pair ColorPair) {
|
func (w *TcellWindow) printString(text string, pair ColorPair) {
|
||||||
t := text
|
|
||||||
lx := 0
|
lx := 0
|
||||||
a := pair.Attr()
|
a := pair.Attr()
|
||||||
|
|
||||||
@@ -496,33 +566,28 @@ func (w *TcellWindow) printString(text string, pair ColorPair) {
|
|||||||
Dim(a&Attr(tcell.AttrDim) != 0)
|
Dim(a&Attr(tcell.AttrDim) != 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
gr := uniseg.NewGraphemes(text)
|
||||||
if len(t) == 0 {
|
for gr.Next() {
|
||||||
break
|
rs := gr.Runes()
|
||||||
}
|
|
||||||
r, size := utf8.DecodeRuneInString(t)
|
|
||||||
t = t[size:]
|
|
||||||
|
|
||||||
|
if len(rs) == 1 {
|
||||||
|
r := rs[0]
|
||||||
if r < rune(' ') { // ignore control characters
|
if r < rune(' ') { // ignore control characters
|
||||||
continue
|
continue
|
||||||
}
|
} else if r == '\n' {
|
||||||
|
|
||||||
if r == '\n' {
|
|
||||||
w.lastY++
|
w.lastY++
|
||||||
lx = 0
|
lx = 0
|
||||||
} else {
|
continue
|
||||||
|
} else if r == '\u000D' { // skip carriage return
|
||||||
if r == '\u000D' { // skip carriage return
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
}
|
||||||
var xPos = w.left + w.lastX + lx
|
var xPos = w.left + w.lastX + lx
|
||||||
var yPos = w.top + w.lastY
|
var yPos = w.top + w.lastY
|
||||||
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
|
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
|
||||||
_screen.SetContent(xPos, yPos, r, nil, style)
|
_screen.SetContent(xPos, yPos, rs[0], rs[1:], style)
|
||||||
}
|
|
||||||
lx += runewidth.RuneWidth(r)
|
|
||||||
}
|
}
|
||||||
|
lx += runewidth.StringWidth(string(rs))
|
||||||
}
|
}
|
||||||
w.lastX += lx
|
w.lastX += lx
|
||||||
}
|
}
|
||||||
@@ -549,30 +614,32 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
|||||||
Underline(a&Attr(tcell.AttrUnderline) != 0).
|
Underline(a&Attr(tcell.AttrUnderline) != 0).
|
||||||
Italic(a&Attr(tcell.AttrItalic) != 0)
|
Italic(a&Attr(tcell.AttrItalic) != 0)
|
||||||
|
|
||||||
for _, r := range text {
|
gr := uniseg.NewGraphemes(text)
|
||||||
if r == '\n' {
|
for gr.Next() {
|
||||||
|
rs := gr.Runes()
|
||||||
|
if len(rs) == 1 && rs[0] == '\n' {
|
||||||
w.lastY++
|
w.lastY++
|
||||||
w.lastX = 0
|
w.lastX = 0
|
||||||
lx = 0
|
lx = 0
|
||||||
} else {
|
continue
|
||||||
var xPos = w.left + w.lastX + lx
|
}
|
||||||
|
|
||||||
// word wrap:
|
// word wrap:
|
||||||
|
xPos := w.left + w.lastX + lx
|
||||||
if xPos >= (w.left + w.width) {
|
if xPos >= (w.left + w.width) {
|
||||||
w.lastY++
|
w.lastY++
|
||||||
w.lastX = 0
|
w.lastX = 0
|
||||||
lx = 0
|
lx = 0
|
||||||
xPos = w.left
|
xPos = w.left
|
||||||
}
|
}
|
||||||
var yPos = w.top + w.lastY
|
|
||||||
|
|
||||||
|
yPos := w.top + w.lastY
|
||||||
if yPos >= (w.top + w.height) {
|
if yPos >= (w.top + w.height) {
|
||||||
return FillSuspend
|
return FillSuspend
|
||||||
}
|
}
|
||||||
|
|
||||||
_screen.SetContent(xPos, yPos, r, nil, style)
|
_screen.SetContent(xPos, yPos, rs[0], rs[1:], style)
|
||||||
lx += runewidth.RuneWidth(r)
|
lx += runewidth.StringWidth(string(rs))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
w.lastX += lx
|
w.lastX += lx
|
||||||
if w.lastX == w.width {
|
if w.lastX == w.width {
|
||||||
|
392
src/tui/tcell_test.go
Normal file
392
src/tui/tcell_test.go
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
// +build tcell windows
|
||||||
|
|
||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
"github.com/junegunn/fzf/src/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func assert(t *testing.T, context string, got interface{}, want interface{}) bool {
|
||||||
|
if got == want {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
t.Errorf("%s = (%T)%v, want (%T)%v", context, got, got, want, want)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the handling of the tcell keyboard events.
|
||||||
|
func TestGetCharEventKey(t *testing.T) {
|
||||||
|
if util.ToTty() {
|
||||||
|
// This test is skipped when output goes to terminal, because it causes
|
||||||
|
// some glitches:
|
||||||
|
// - output lines may not start at the beginning of a row which makes
|
||||||
|
// the output unreadable
|
||||||
|
// - terminal may get cleared which prevents you from seeing results of
|
||||||
|
// previous tests
|
||||||
|
// Good ways to prevent the glitches are piping the output to a pager
|
||||||
|
// or redirecting to a file. I've found `less +G` to be trouble-free.
|
||||||
|
t.Skip("Skipped because this test misbehaves in terminal, pipe to a pager or redirect output to a file to run it safely.")
|
||||||
|
} else if testing.Verbose() {
|
||||||
|
// I have observed a behaviour when this test outputted more than 8192
|
||||||
|
// bytes (32*256) into the 'less' pager, both the go's test executable
|
||||||
|
// and the pager hanged. The go's executable was blocking on printing.
|
||||||
|
// I was able to create minimal working example of that behaviour, but
|
||||||
|
// that example hanged after 12256 bytes (32*(256+127)).
|
||||||
|
t.Log("If you are piping this test to a pager and it hangs, make the pager greedy for input, e.g. 'less +G'.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !HasFullscreenRenderer() {
|
||||||
|
t.Skip("Can't test FullscreenRenderer.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// construct test cases
|
||||||
|
type giveKey struct {
|
||||||
|
Type tcell.Key
|
||||||
|
Char rune
|
||||||
|
Mods tcell.ModMask
|
||||||
|
}
|
||||||
|
type wantKey = Event
|
||||||
|
type testCase struct {
|
||||||
|
giveKey
|
||||||
|
wantKey
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
Some test cases are marked "fabricated". It means that giveKey value
|
||||||
|
is valid, but it is not what you get when you press the keys. For
|
||||||
|
example Ctrl+C will NOT give you tcell.KeyCtrlC, but tcell.KeyETX
|
||||||
|
(End-Of-Text character, causing SIGINT).
|
||||||
|
I was trying to accompany the fabricated test cases with real ones.
|
||||||
|
|
||||||
|
Some test cases are marked "unhandled". It means that giveKey.Type
|
||||||
|
is not present in tcell.go source code. It can still be handled via
|
||||||
|
implicit or explicit alias.
|
||||||
|
|
||||||
|
If not said otherwise, test cases are for US keyboard.
|
||||||
|
|
||||||
|
(tabstop=44)
|
||||||
|
*/
|
||||||
|
tests := []testCase{
|
||||||
|
|
||||||
|
// section 1: Ctrl+(Alt)+[a-z]
|
||||||
|
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl}, wantKey{CtrlA, 0, nil}},
|
||||||
|
{giveKey{tcell.KeyCtrlC, rune(tcell.KeyCtrlC), tcell.ModCtrl}, wantKey{CtrlC, 0, nil}}, // fabricated
|
||||||
|
{giveKey{tcell.KeyETX, rune(tcell.KeyETX), tcell.ModCtrl}, wantKey{CtrlC, 0, nil}}, // this is SIGINT (Ctrl+C)
|
||||||
|
{giveKey{tcell.KeyCtrlZ, rune(tcell.KeyCtrlZ), tcell.ModCtrl}, wantKey{CtrlZ, 0, nil}}, // fabricated
|
||||||
|
// KeyTab is alias for KeyTAB
|
||||||
|
{giveKey{tcell.KeyCtrlI, rune(tcell.KeyCtrlI), tcell.ModCtrl}, wantKey{Tab, 0, nil}}, // fabricated
|
||||||
|
{giveKey{tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, wantKey{Tab, 0, nil}}, // unhandled, actual "Tab" keystroke
|
||||||
|
{giveKey{tcell.KeyTAB, rune(tcell.KeyTAB), tcell.ModNone}, wantKey{Tab, 0, nil}}, // fabricated, unhandled
|
||||||
|
// KeyEnter is alias for KeyCR
|
||||||
|
{giveKey{tcell.KeyCtrlM, rune(tcell.KeyCtrlM), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // actual "Enter" keystroke
|
||||||
|
{giveKey{tcell.KeyCR, rune(tcell.KeyCR), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
|
||||||
|
{giveKey{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
|
||||||
|
// Ctrl+Alt keys
|
||||||
|
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
|
||||||
|
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
|
||||||
|
|
||||||
|
// section 2: Ctrl+[ \]_]
|
||||||
|
{giveKey{tcell.KeyCtrlSpace, rune(tcell.KeyCtrlSpace), tcell.ModCtrl}, wantKey{CtrlSpace, 0, nil}}, // fabricated
|
||||||
|
{giveKey{tcell.KeyNUL, rune(tcell.KeyNUL), tcell.ModNone}, wantKey{CtrlSpace, 0, nil}}, // fabricated, unhandled
|
||||||
|
{giveKey{tcell.KeyRune, ' ', tcell.ModCtrl}, wantKey{CtrlSpace, 0, nil}}, // actual Ctrl+' '
|
||||||
|
{giveKey{tcell.KeyCtrlBackslash, rune(tcell.KeyCtrlBackslash), tcell.ModCtrl}, wantKey{CtrlBackSlash, 0, nil}},
|
||||||
|
{giveKey{tcell.KeyCtrlRightSq, rune(tcell.KeyCtrlRightSq), tcell.ModCtrl}, wantKey{CtrlRightBracket, 0, nil}},
|
||||||
|
{giveKey{tcell.KeyCtrlCarat, rune(tcell.KeyCtrlCarat), tcell.ModShift | tcell.ModCtrl}, wantKey{CtrlCaret, 0, nil}}, // fabricated
|
||||||
|
{giveKey{tcell.KeyRS, rune(tcell.KeyRS), tcell.ModShift | tcell.ModCtrl}, wantKey{CtrlCaret, 0, nil}}, // actual Ctrl+Shift+6 (i.e. Ctrl+^) keystroke
|
||||||
|
{giveKey{tcell.KeyCtrlUnderscore, rune(tcell.KeyCtrlUnderscore), tcell.ModShift | tcell.ModCtrl}, wantKey{CtrlSlash, 0, nil}},
|
||||||
|
|
||||||
|
// section 3: (Alt)+Backspace2
|
||||||
|
// KeyBackspace2 is alias for KeyDEL = 0x7F (ASCII) (allegedly unused by Windows)
|
||||||
|
// KeyDelete = 0x2E (VK_DELETE constant in Windows)
|
||||||
|
// KeyBackspace is alias for KeyBS = 0x08 (ASCII) (implicit alias with KeyCtrlH)
|
||||||
|
{giveKey{tcell.KeyBackspace2, 0, tcell.ModNone}, wantKey{BSpace, 0, nil}}, // fabricated
|
||||||
|
{giveKey{tcell.KeyBackspace2, 0, tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // fabricated
|
||||||
|
{giveKey{tcell.KeyDEL, 0, tcell.ModNone}, wantKey{BSpace, 0, nil}}, // fabricated, unhandled
|
||||||
|
{giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Del, 0, nil}},
|
||||||
|
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{Del, 0, nil}},
|
||||||
|
{giveKey{tcell.KeyBackspace, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||||
|
{giveKey{tcell.KeyBS, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||||
|
{giveKey{tcell.KeyCtrlH, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||||
|
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModNone}, wantKey{BSpace, 0, nil}}, // actual "Backspace" keystroke
|
||||||
|
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // actual "Alt+Backspace" keystroke
|
||||||
|
{giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Backspace" keystroke
|
||||||
|
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Shift+Backspace" keystroke
|
||||||
|
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke
|
||||||
|
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Shift+Backspace" keystroke
|
||||||
|
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift | tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // actual "Shift+Alt+Backspace" keystroke
|
||||||
|
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke
|
||||||
|
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+H" keystroke
|
||||||
|
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Alt+H" keystroke
|
||||||
|
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+Shift+H" keystroke
|
||||||
|
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Shift+Alt+H" keystroke
|
||||||
|
|
||||||
|
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
|
||||||
|
{giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}},
|
||||||
|
{giveKey{tcell.KeyDown, 0, tcell.ModAlt}, wantKey{AltDown, 0, nil}},
|
||||||
|
{giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{SLeft, 0, nil}},
|
||||||
|
{giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltSRight, 0, nil}},
|
||||||
|
{giveKey{tcell.KeyUpLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||||
|
{giveKey{tcell.KeyUpRight, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||||
|
{giveKey{tcell.KeyDownLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||||
|
{giveKey{tcell.KeyDownRight, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||||
|
{giveKey{tcell.KeyCenter, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||||
|
// section 5: (Insert|Home|Delete|End|PgUp|PgDn|BackTab|F1-F12)
|
||||||
|
{giveKey{tcell.KeyInsert, 0, tcell.ModNone}, wantKey{Insert, 0, nil}},
|
||||||
|
{giveKey{tcell.KeyF1, 0, tcell.ModNone}, wantKey{F1, 0, nil}},
|
||||||
|
// section 6: (Ctrl+Alt)+'rune'
|
||||||
|
{giveKey{tcell.KeyRune, 'a', tcell.ModNone}, wantKey{Rune, 'a', nil}},
|
||||||
|
{giveKey{tcell.KeyRune, 'a', tcell.ModCtrl}, wantKey{Rune, 'a', nil}}, // fabricated
|
||||||
|
{giveKey{tcell.KeyRune, 'a', tcell.ModAlt}, wantKey{Alt, 'a', nil}},
|
||||||
|
{giveKey{tcell.KeyRune, 'A', tcell.ModAlt}, wantKey{Alt, 'A', nil}},
|
||||||
|
{giveKey{tcell.KeyRune, '`', tcell.ModAlt}, wantKey{Alt, '`', nil}},
|
||||||
|
/*
|
||||||
|
"Input method" in Windows Language options:
|
||||||
|
US: "US Keyboard" does not generate any characters (and thus any events) in Ctrl+Alt+[a-z] range
|
||||||
|
CS: "Czech keyboard"
|
||||||
|
DE: "German keyboard"
|
||||||
|
|
||||||
|
Note that right Alt is not just `tcell.ModAlt` on foreign language keyboards, but it is the AltGr `tcell.ModCtrl|tcell.ModAlt`.
|
||||||
|
*/
|
||||||
|
{giveKey{tcell.KeyRune, '{', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '{', nil}}, // CS: Ctrl+Alt+b = "{" // Note that this does not interfere with CtrlB, since the "b" is replaced with "{" on OS level
|
||||||
|
{giveKey{tcell.KeyRune, '$', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '$', nil}}, // CS: Ctrl+Alt+ů = "$"
|
||||||
|
{giveKey{tcell.KeyRune, '~', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '~', nil}}, // CS: Ctrl+Alt++ = "~"
|
||||||
|
{giveKey{tcell.KeyRune, '`', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '`', nil}}, // CS: Ctrl+Alt+ý,Space = "`" // this is dead key, space is required to emit the char
|
||||||
|
|
||||||
|
{giveKey{tcell.KeyRune, '{', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '{', nil}}, // DE: Ctrl+Alt+7 = "{"
|
||||||
|
{giveKey{tcell.KeyRune, '@', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '@', nil}}, // DE: Ctrl+Alt+q = "@"
|
||||||
|
{giveKey{tcell.KeyRune, 'µ', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, 'µ', nil}}, // DE: Ctrl+Alt+m = "µ"
|
||||||
|
|
||||||
|
// section 7: Esc
|
||||||
|
// KeyEsc and KeyEscape are aliases for KeyESC
|
||||||
|
{giveKey{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone}, wantKey{ESC, 0, nil}}, // fabricated
|
||||||
|
{giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModNone}, wantKey{ESC, 0, nil}}, // unhandled
|
||||||
|
{giveKey{tcell.KeyEscape, rune(tcell.KeyEscape), tcell.ModNone}, wantKey{ESC, 0, nil}}, // fabricated, unhandled
|
||||||
|
{giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModCtrl}, wantKey{ESC, 0, nil}}, // actual Ctrl+[ keystroke
|
||||||
|
{giveKey{tcell.KeyCtrlLeftSq, rune(tcell.KeyCtrlLeftSq), tcell.ModCtrl}, wantKey{ESC, 0, nil}}, // fabricated, unhandled
|
||||||
|
|
||||||
|
// section 8: Invalid
|
||||||
|
{giveKey{tcell.KeyRune, 'a', tcell.ModMeta}, wantKey{Rune, 'a', nil}}, // fabricated
|
||||||
|
{giveKey{tcell.KeyF24, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}},
|
||||||
|
{giveKey{tcell.KeyHelp, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||||
|
{giveKey{tcell.KeyExit, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||||
|
{giveKey{tcell.KeyClear, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // unhandled, actual keystroke Numpad_5 with Numlock OFF
|
||||||
|
{giveKey{tcell.KeyCancel, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||||
|
{giveKey{tcell.KeyPrint, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||||
|
{giveKey{tcell.KeyPause, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // unhandled
|
||||||
|
|
||||||
|
}
|
||||||
|
r := NewFullscreenRenderer(&ColorTheme{}, false, false)
|
||||||
|
r.Init()
|
||||||
|
|
||||||
|
// run and evaluate the tests
|
||||||
|
for _, test := range tests {
|
||||||
|
// generate key event
|
||||||
|
giveEvent := tcell.NewEventKey(test.giveKey.Type, test.giveKey.Char, test.giveKey.Mods)
|
||||||
|
_screen.PostEventWait(giveEvent)
|
||||||
|
t.Logf("giveEvent = %T{key: %v, ch: %q (%[3]v), mod: %#04b}\n", giveEvent, giveEvent.Key(), giveEvent.Rune(), giveEvent.Modifiers())
|
||||||
|
|
||||||
|
// process the event in fzf and evaluate the test
|
||||||
|
gotEvent := r.GetChar()
|
||||||
|
// skip Resize events, those are sometimes put in the buffer outside of this test
|
||||||
|
for gotEvent.Type == Resize {
|
||||||
|
t.Logf("Resize swallowed")
|
||||||
|
gotEvent = r.GetChar()
|
||||||
|
}
|
||||||
|
t.Logf("wantEvent = %T{Type: %v, Char: %q (%[3]v)}\n", test.wantKey, test.wantKey.Type, test.wantKey.Char)
|
||||||
|
t.Logf("gotEvent = %T{Type: %v, Char: %q (%[3]v)}\n", gotEvent, gotEvent.Type, gotEvent.Char)
|
||||||
|
|
||||||
|
assert(t, "r.GetChar().Type", gotEvent.Type, test.wantKey.Type)
|
||||||
|
assert(t, "r.GetChar().Char", gotEvent.Char, test.wantKey.Char)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Quick reference
|
||||||
|
---------------
|
||||||
|
|
||||||
|
(tabstop=18)
|
||||||
|
(this is not mapping table, it merely puts multiple constants ranges in one table)
|
||||||
|
|
||||||
|
¹) the two columns are each other implicit alias
|
||||||
|
²) explicit aliases here
|
||||||
|
|
||||||
|
%v section # tcell ctrl key¹ tcell ctrl char¹ tcell alias² tui constants tcell named keys tcell mods
|
||||||
|
-- --------- -------------- --------------- ----------- ------------- ---------------- ----------
|
||||||
|
0 2 KeyCtrlSpace KeyNUL = ^@ Rune ModNone
|
||||||
|
1 1 KeyCtrlA KeySOH = ^A CtrlA ModShift
|
||||||
|
2 1 KeyCtrlB KeySTX = ^B CtrlB ModCtrl
|
||||||
|
3 1 KeyCtrlC KeyETX = ^C CtrlC
|
||||||
|
4 1 KeyCtrlD KeyEOT = ^D CtrlD ModAlt
|
||||||
|
5 1 KeyCtrlE KeyENQ = ^E CtrlE
|
||||||
|
6 1 KeyCtrlF KeyACK = ^F CtrlF
|
||||||
|
7 1 KeyCtrlG KeyBEL = ^G CtrlG
|
||||||
|
8 1 KeyCtrlH KeyBS = ^H KeyBackspace CtrlH ModMeta
|
||||||
|
9 1 KeyCtrlI KeyTAB = ^I KeyTab Tab
|
||||||
|
10 1 KeyCtrlJ KeyLF = ^J CtrlJ
|
||||||
|
11 1 KeyCtrlK KeyVT = ^K CtrlK
|
||||||
|
12 1 KeyCtrlL KeyFF = ^L CtrlL
|
||||||
|
13 1 KeyCtrlM KeyCR = ^M KeyEnter CtrlM
|
||||||
|
14 1 KeyCtrlN KeySO = ^N CtrlN
|
||||||
|
15 1 KeyCtrlO KeySI = ^O CtrlO
|
||||||
|
16 1 KeyCtrlP KeyDLE = ^P CtrlP
|
||||||
|
17 1 KeyCtrlQ KeyDC1 = ^Q CtrlQ
|
||||||
|
18 1 KeyCtrlR KeyDC2 = ^R CtrlR
|
||||||
|
19 1 KeyCtrlS KeyDC3 = ^S CtrlS
|
||||||
|
20 1 KeyCtrlT KeyDC4 = ^T CtrlT
|
||||||
|
21 1 KeyCtrlU KeyNAK = ^U CtrlU
|
||||||
|
22 1 KeyCtrlV KeySYN = ^V CtrlV
|
||||||
|
23 1 KeyCtrlW KeyETB = ^W CtrlW
|
||||||
|
24 1 KeyCtrlX KeyCAN = ^X CtrlX
|
||||||
|
25 1 KeyCtrlY KeyEM = ^Y CtrlY
|
||||||
|
26 1 KeyCtrlZ KeySUB = ^Z CtrlZ
|
||||||
|
27 7 KeyCtrlLeftSq KeyESC = ^[ KeyEsc, KeyEscape ESC
|
||||||
|
28 2 KeyCtrlBackslash KeyFS = ^\ CtrlSpace
|
||||||
|
29 2 KeyCtrlRightSq KeyGS = ^] CtrlBackSlash
|
||||||
|
30 2 KeyCtrlCarat KeyRS = ^^ CtrlRightBracket
|
||||||
|
31 2 KeyCtrlUnderscore KeyUS = ^_ CtrlCaret
|
||||||
|
32 CtrlSlash
|
||||||
|
33 Invalid
|
||||||
|
34 Resize
|
||||||
|
35 Mouse
|
||||||
|
36 DoubleClick
|
||||||
|
37 LeftClick
|
||||||
|
38 RightClick
|
||||||
|
39 BTab
|
||||||
|
40 BSpace
|
||||||
|
41 Del
|
||||||
|
42 PgUp
|
||||||
|
43 PgDn
|
||||||
|
44 Up
|
||||||
|
45 Down
|
||||||
|
46 Left
|
||||||
|
47 Right
|
||||||
|
48 Home
|
||||||
|
49 End
|
||||||
|
50 Insert
|
||||||
|
51 SUp
|
||||||
|
52 SDown
|
||||||
|
53 SLeft
|
||||||
|
54 SRight
|
||||||
|
55 F1
|
||||||
|
56 F2
|
||||||
|
57 F3
|
||||||
|
58 F4
|
||||||
|
59 F5
|
||||||
|
60 F6
|
||||||
|
61 F7
|
||||||
|
62 F8
|
||||||
|
63 F9
|
||||||
|
64 F10
|
||||||
|
65 F11
|
||||||
|
66 F12
|
||||||
|
67 Change
|
||||||
|
68 BackwardEOF
|
||||||
|
69 AltBS
|
||||||
|
70 AltUp
|
||||||
|
71 AltDown
|
||||||
|
72 AltLeft
|
||||||
|
73 AltRight
|
||||||
|
74 AltSUp
|
||||||
|
75 AltSDown
|
||||||
|
76 AltSLeft
|
||||||
|
77 AltSRight
|
||||||
|
78 Alt
|
||||||
|
79 CtrlAlt
|
||||||
|
..
|
||||||
|
127 3 KeyDEL KeyBackspace2
|
||||||
|
..
|
||||||
|
256 6 KeyRune
|
||||||
|
257 4 KeyUp
|
||||||
|
258 4 KeyDown
|
||||||
|
259 4 KeyRight
|
||||||
|
260 4 KeyLeft
|
||||||
|
261 8 KeyUpLeft
|
||||||
|
262 8 KeyUpRight
|
||||||
|
263 8 KeyDownLeft
|
||||||
|
264 8 KeyDownRight
|
||||||
|
265 8 KeyCenter
|
||||||
|
266 5 KeyPgUp
|
||||||
|
267 5 KeyPgDn
|
||||||
|
268 5 KeyHome
|
||||||
|
269 5 KeyEnd
|
||||||
|
270 5 KeyInsert
|
||||||
|
271 5 KeyDelete
|
||||||
|
272 8 KeyHelp
|
||||||
|
273 8 KeyExit
|
||||||
|
274 8 KeyClear
|
||||||
|
275 8 KeyCancel
|
||||||
|
276 8 KeyPrint
|
||||||
|
277 8 KeyPause
|
||||||
|
278 5 KeyBacktab
|
||||||
|
279 5 KeyF1
|
||||||
|
280 5 KeyF2
|
||||||
|
281 5 KeyF3
|
||||||
|
282 5 KeyF4
|
||||||
|
283 5 KeyF5
|
||||||
|
284 5 KeyF6
|
||||||
|
285 5 KeyF7
|
||||||
|
286 5 KeyF8
|
||||||
|
287 5 KeyF9
|
||||||
|
288 5 KeyF10
|
||||||
|
289 5 KeyF11
|
||||||
|
290 5 KeyF12
|
||||||
|
291 8 KeyF13
|
||||||
|
292 8 KeyF14
|
||||||
|
293 8 KeyF15
|
||||||
|
294 8 KeyF16
|
||||||
|
295 8 KeyF17
|
||||||
|
296 8 KeyF18
|
||||||
|
297 8 KeyF19
|
||||||
|
298 8 KeyF20
|
||||||
|
299 8 KeyF21
|
||||||
|
300 8 KeyF22
|
||||||
|
301 8 KeyF23
|
||||||
|
302 8 KeyF24
|
||||||
|
303 8 KeyF25
|
||||||
|
304 8 KeyF26
|
||||||
|
305 8 KeyF27
|
||||||
|
306 8 KeyF28
|
||||||
|
307 8 KeyF29
|
||||||
|
308 8 KeyF30
|
||||||
|
309 8 KeyF31
|
||||||
|
310 8 KeyF32
|
||||||
|
311 8 KeyF33
|
||||||
|
312 8 KeyF34
|
||||||
|
313 8 KeyF35
|
||||||
|
314 8 KeyF36
|
||||||
|
315 8 KeyF37
|
||||||
|
316 8 KeyF38
|
||||||
|
317 8 KeyF39
|
||||||
|
318 8 KeyF40
|
||||||
|
319 8 KeyF41
|
||||||
|
320 8 KeyF42
|
||||||
|
321 8 KeyF43
|
||||||
|
322 8 KeyF44
|
||||||
|
323 8 KeyF45
|
||||||
|
324 8 KeyF46
|
||||||
|
325 8 KeyF47
|
||||||
|
326 8 KeyF48
|
||||||
|
327 8 KeyF49
|
||||||
|
328 8 KeyF50
|
||||||
|
329 8 KeyF51
|
||||||
|
330 8 KeyF52
|
||||||
|
331 8 KeyF53
|
||||||
|
332 8 KeyF54
|
||||||
|
333 8 KeyF55
|
||||||
|
334 8 KeyF56
|
||||||
|
335 8 KeyF57
|
||||||
|
336 8 KeyF58
|
||||||
|
337 8 KeyF59
|
||||||
|
338 8 KeyF60
|
||||||
|
339 8 KeyF61
|
||||||
|
340 8 KeyF62
|
||||||
|
341 8 KeyF63
|
||||||
|
342 8 KeyF64
|
||||||
|
-- --------- -------------- --------------- ----------- ------------- ---------------- ----------
|
||||||
|
%v section # tcell ctrl key tcell ctrl char tcell alias tui constants tcell named keys tcell mods
|
||||||
|
*/
|
@@ -4,6 +4,7 @@ package tui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,3 +30,18 @@ func ttyname() string {
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TtyIn returns terminal device to be used as STDIN, falls back to os.Stdin
|
||||||
|
func TtyIn() *os.File {
|
||||||
|
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
|
||||||
|
if err != nil {
|
||||||
|
tty := ttyname()
|
||||||
|
if len(tty) > 0 {
|
||||||
|
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return os.Stdin
|
||||||
|
}
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
package tui
|
package tui
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
func ttyname() string {
|
func ttyname() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TtyIn on Windows returns os.Stdin
|
||||||
|
func TtyIn() *os.File {
|
||||||
|
return os.Stdin
|
||||||
|
}
|
||||||
|
@@ -3,26 +3,35 @@ package util
|
|||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mattn/go-isatty"
|
"github.com/mattn/go-isatty"
|
||||||
"github.com/mattn/go-runewidth"
|
"github.com/mattn/go-runewidth"
|
||||||
|
"github.com/rivo/uniseg"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _runeWidths = make(map[rune]int)
|
// RunesWidth returns runes width
|
||||||
|
func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) {
|
||||||
// RuneWidth returns rune width
|
width := 0
|
||||||
func RuneWidth(r rune, prefixWidth int, tabstop int) int {
|
gr := uniseg.NewGraphemes(string(runes))
|
||||||
if r == '\t' {
|
idx := 0
|
||||||
return tabstop - prefixWidth%tabstop
|
for gr.Next() {
|
||||||
} else if w, found := _runeWidths[r]; found {
|
rs := gr.Runes()
|
||||||
return w
|
var w int
|
||||||
} else if r == '\n' || r == '\r' {
|
if len(rs) == 1 && rs[0] == '\t' {
|
||||||
return 1
|
w = tabstop - (prefixWidth+width)%tabstop
|
||||||
|
} else {
|
||||||
|
s := string(rs)
|
||||||
|
w = runewidth.StringWidth(s) + strings.Count(s, "\n")
|
||||||
}
|
}
|
||||||
w := runewidth.RuneWidth(r)
|
width += w
|
||||||
_runeWidths[r] = w
|
if limit > 0 && width > limit {
|
||||||
return w
|
return width, idx
|
||||||
|
}
|
||||||
|
idx += len(rs)
|
||||||
|
}
|
||||||
|
return width, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Max returns the largest integer
|
// Max returns the largest integer
|
||||||
@@ -108,11 +117,16 @@ func DurWithin(
|
|||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTty returns true is stdin is a terminal
|
// IsTty returns true if stdin is a terminal
|
||||||
func IsTty() bool {
|
func IsTty() bool {
|
||||||
return isatty.IsTerminal(os.Stdin.Fd())
|
return isatty.IsTerminal(os.Stdin.Fd())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToTty returns true if stdout is a terminal
|
||||||
|
func ToTty() bool {
|
||||||
|
return isatty.IsTerminal(os.Stdout.Fd())
|
||||||
|
}
|
||||||
|
|
||||||
// Once returns a function that returns the specified boolean value only once
|
// Once returns a function that returns the specified boolean value only once
|
||||||
func Once(nextResponse bool) func() bool {
|
func Once(nextResponse bool) func() bool {
|
||||||
state := nextResponse
|
state := nextResponse
|
||||||
|
@@ -6,26 +6,60 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExecCommand executes the given command with cmd
|
var shellPath atomic.Value
|
||||||
|
|
||||||
|
// ExecCommand executes the given command with $SHELL
|
||||||
func ExecCommand(command string, setpgid bool) *exec.Cmd {
|
func ExecCommand(command string, setpgid bool) *exec.Cmd {
|
||||||
return ExecCommandWith("cmd", command, setpgid)
|
var shell string
|
||||||
|
if cached := shellPath.Load(); cached != nil {
|
||||||
|
shell = cached.(string)
|
||||||
|
} else {
|
||||||
|
shell = os.Getenv("SHELL")
|
||||||
|
if len(shell) == 0 {
|
||||||
|
shell = "cmd"
|
||||||
|
} else if strings.Contains(shell, "/") {
|
||||||
|
out, err := exec.Command("cygpath", "-w", shell).Output()
|
||||||
|
if err == nil {
|
||||||
|
shell = strings.Trim(string(out), "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shellPath.Store(shell)
|
||||||
|
}
|
||||||
|
return ExecCommandWith(shell, command, setpgid)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecCommandWith executes the given command with cmd. _shell parameter is
|
// ExecCommandWith executes the given command with the specified shell
|
||||||
// ignored on Windows.
|
|
||||||
// FIXME: setpgid is unused. We set it in the Unix implementation so that we
|
// FIXME: setpgid is unused. We set it in the Unix implementation so that we
|
||||||
// can kill preview process with its child processes at once.
|
// can kill preview process with its child processes at once.
|
||||||
func ExecCommandWith(_shell string, command string, setpgid bool) *exec.Cmd {
|
// NOTE: For "powershell", we should ideally set output encoding to UTF8,
|
||||||
cmd := exec.Command("cmd")
|
// but it is left as is now because no adverse effect has been observed.
|
||||||
|
func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if strings.Contains(shell, "cmd") {
|
||||||
|
cmd = exec.Command(shell)
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
HideWindow: false,
|
HideWindow: false,
|
||||||
CmdLine: fmt.Sprintf(` /v:on/s/c "%s"`, command),
|
CmdLine: fmt.Sprintf(` /v:on/s/c "%s"`, command),
|
||||||
CreationFlags: 0,
|
CreationFlags: 0,
|
||||||
}
|
}
|
||||||
return cmd
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") {
|
||||||
|
cmd = exec.Command(shell, "-NoProfile", "-Command", command)
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command(shell, "-c", command)
|
||||||
|
}
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
HideWindow: false,
|
||||||
|
CreationFlags: 0,
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// KillCommand kills the process for the given command
|
// KillCommand kills the process for the given command
|
||||||
|
289
test/test_go.rb
289
test/test_go.rb
@@ -148,14 +148,15 @@ class Tmux
|
|||||||
def prepare
|
def prepare
|
||||||
tries = 0
|
tries = 0
|
||||||
begin
|
begin
|
||||||
self.until do |lines|
|
self.until(true) do |lines|
|
||||||
send_keys ' ', 'C-u', :Enter, 'hello', :Left, :Right
|
message = "Prepare[#{tries}]"
|
||||||
lines[-1] == 'hello'
|
send_keys ' ', 'C-u', :Enter, message, :Left, :Right
|
||||||
|
lines[-1] == message
|
||||||
end
|
end
|
||||||
rescue Minitest::Assertion
|
rescue Minitest::Assertion
|
||||||
(tries += 1) < 5 ? retry : raise
|
(tries += 1) < 5 ? retry : raise
|
||||||
end
|
end
|
||||||
send_keys 'C-u'
|
send_keys 'C-u', 'C-l'
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -1863,6 +1864,284 @@ class TestGoFZF < TestBase
|
|||||||
tmux.send_keys "#{FZF} --preview 'seq 1000 | nl' --preview-window down:noborder:follow", :Enter
|
tmux.send_keys "#{FZF} --preview 'seq 1000 | nl' --preview-window down:noborder:follow", :Enter
|
||||||
tmux.until { |lines| assert_equal '1000 1000', lines[-1].strip }
|
tmux.until { |lines| assert_equal '1000 1000', lines[-1].strip }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_toggle_preview_wrap
|
||||||
|
tmux.send_keys "#{FZF} --preview 'for i in $(seq $FZF_PREVIEW_COLUMNS); do echo -n .; done; echo wrapped; echo 2nd line' --bind ctrl-w:toggle-preview-wrap", :Enter
|
||||||
|
2.times do
|
||||||
|
tmux.until { |lines| assert_includes lines[2], '2nd line' }
|
||||||
|
tmux.send_keys 'C-w'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines[2], 'wrapped'
|
||||||
|
assert_includes lines[3], '2nd line'
|
||||||
|
end
|
||||||
|
tmux.send_keys 'C-w'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_close
|
||||||
|
tmux.send_keys "seq 100 | #{FZF} --preview 'echo foo' --bind ctrl-c:close", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||||
|
tmux.until { |lines| assert_includes lines[1], 'foo' }
|
||||||
|
tmux.send_keys 'C-c'
|
||||||
|
tmux.until { |lines| refute_includes lines[1], 'foo' }
|
||||||
|
tmux.send_keys '10'
|
||||||
|
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||||
|
tmux.send_keys 'C-c'
|
||||||
|
tmux.send_keys 'C-l', 'closed'
|
||||||
|
tmux.until { |lines| assert_includes lines[0], 'closed' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_select_deselect
|
||||||
|
tmux.send_keys "seq 3 | #{FZF} --multi --bind up:deselect+up,down:select+down", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||||
|
tmux.send_keys :Tab
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.select_count }
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until { |lines| assert_equal 0, lines.select_count }
|
||||||
|
tmux.send_keys :Down, :Down
|
||||||
|
tmux.until { |lines| assert_equal 2, lines.select_count }
|
||||||
|
tmux.send_keys :Tab
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.select_count }
|
||||||
|
tmux.send_keys :Down, :Down
|
||||||
|
tmux.until { |lines| assert_equal 2, lines.select_count }
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.select_count }
|
||||||
|
tmux.send_keys :Down
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.select_count }
|
||||||
|
tmux.send_keys :Down
|
||||||
|
tmux.until { |lines| assert_equal 2, lines.select_count }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_interrupt_execute
|
||||||
|
tmux.send_keys "seq 100 | #{FZF} --bind 'ctrl-l:execute:echo executing {}; sleep 100'", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||||
|
tmux.send_keys 'C-l'
|
||||||
|
tmux.until { |lines| assert lines.any_include?('executing 1') }
|
||||||
|
tmux.send_keys 'C-c'
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||||
|
tmux.send_keys 99
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_kill_default_command_on_abort
|
||||||
|
script = tempname + '.sh'
|
||||||
|
writelines(script,
|
||||||
|
['#!/usr/bin/env bash',
|
||||||
|
"echo 'Started'",
|
||||||
|
'while :; do sleep 1; done'])
|
||||||
|
system("chmod +x #{script}")
|
||||||
|
|
||||||
|
tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{script}"), :Enter
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.item_count }
|
||||||
|
tmux.send_keys 'C-c'
|
||||||
|
tmux.send_keys 'C-l', 'closed'
|
||||||
|
tmux.until { |lines| assert_includes lines[0], 'closed' }
|
||||||
|
wait { refute system("pgrep -f #{script}") }
|
||||||
|
ensure
|
||||||
|
system("pkill -9 -f #{script}")
|
||||||
|
begin
|
||||||
|
File.unlink(script)
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_kill_default_command_on_accept
|
||||||
|
script = tempname + '.sh'
|
||||||
|
writelines(script,
|
||||||
|
['#!/usr/bin/env bash',
|
||||||
|
"echo 'Started'",
|
||||||
|
'while :; do sleep 1; done'])
|
||||||
|
system("chmod +x #{script}")
|
||||||
|
|
||||||
|
tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{script}"), :Enter
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.item_count }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
assert_equal 'Started', readonce.chomp
|
||||||
|
wait { refute system("pgrep -f #{script}") }
|
||||||
|
ensure
|
||||||
|
system("pkill -9 -f #{script}")
|
||||||
|
begin
|
||||||
|
File.unlink(script)
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_kill_reload_command_on_abort
|
||||||
|
script = tempname + '.sh'
|
||||||
|
writelines(script,
|
||||||
|
['#!/usr/bin/env bash',
|
||||||
|
"echo 'Started'",
|
||||||
|
'while :; do sleep 1; done'])
|
||||||
|
system("chmod +x #{script}")
|
||||||
|
|
||||||
|
tmux.send_keys "seq 1 3 | #{fzf("--bind 'ctrl-r:reload(#{script})'")}", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 3, lines.item_count }
|
||||||
|
tmux.send_keys 'C-r'
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.item_count }
|
||||||
|
tmux.send_keys 'C-c'
|
||||||
|
tmux.send_keys 'C-l', 'closed'
|
||||||
|
tmux.until { |lines| assert_includes lines[0], 'closed' }
|
||||||
|
wait { refute system("pgrep -f #{script}") }
|
||||||
|
ensure
|
||||||
|
system("pkill -9 -f #{script}")
|
||||||
|
begin
|
||||||
|
File.unlink(script)
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_kill_reload_command_on_accept
|
||||||
|
script = tempname + '.sh'
|
||||||
|
writelines(script,
|
||||||
|
['#!/usr/bin/env bash',
|
||||||
|
"echo 'Started'",
|
||||||
|
'while :; do sleep 1; done'])
|
||||||
|
system("chmod +x #{script}")
|
||||||
|
|
||||||
|
tmux.send_keys "seq 1 3 | #{fzf("--bind 'ctrl-r:reload(#{script})'")}", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 3, lines.item_count }
|
||||||
|
tmux.send_keys 'C-r'
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.item_count }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
assert_equal 'Started', readonce.chomp
|
||||||
|
wait { refute system("pgrep -f #{script}") }
|
||||||
|
ensure
|
||||||
|
system("pkill -9 -f #{script}")
|
||||||
|
begin
|
||||||
|
File.unlink(script)
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_header
|
||||||
|
tmux.send_keys "seq 100 | #{FZF} --bind ctrl-k:preview-up+preview-up,ctrl-j:preview-down+preview-down+preview-down --preview 'seq 1000' --preview-window 'top:+{1}:~3'", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||||
|
top5 = ->(lines) { lines.drop(1).take(5).map { |s| s[/[0-9]+/] } }
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines[1], '4/1000'
|
||||||
|
assert_equal(%w[1 2 3 4 5], top5[lines])
|
||||||
|
end
|
||||||
|
tmux.send_keys '55'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert_equal(%w[1 2 3 55 56], top5[lines])
|
||||||
|
end
|
||||||
|
tmux.send_keys 'C-J'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal(%w[1 2 3 58 59], top5[lines])
|
||||||
|
end
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 19, lines.match_count
|
||||||
|
assert_equal(%w[1 2 3 5 6], top5[lines])
|
||||||
|
end
|
||||||
|
tmux.send_keys 'C-K'
|
||||||
|
tmux.until { |lines| assert_equal(%w[1 2 3 4 5], top5[lines]) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_unbind
|
||||||
|
tmux.send_keys "seq 100 | #{FZF} --bind 'c:clear-query,d:unbind(c,d)'", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||||
|
tmux.send_keys 'ab'
|
||||||
|
tmux.until { |lines| assert_equal '> ab', lines[-1] }
|
||||||
|
tmux.send_keys 'c'
|
||||||
|
tmux.until { |lines| assert_equal '>', lines[-1] }
|
||||||
|
tmux.send_keys 'dabcd'
|
||||||
|
tmux.until { |lines| assert_equal '> abcd', lines[-1] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_item_index_reset_on_reload
|
||||||
|
tmux.send_keys "seq 10 | #{FZF} --preview 'echo [[{n}]]' --bind 'up:last,down:first,space:reload:seq 100'", :Enter
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '[[0]]' }
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '[[9]]' }
|
||||||
|
tmux.send_keys :Down
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '[[0]]' }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 100, lines.item_count
|
||||||
|
assert_includes lines[1], '[[0]]'
|
||||||
|
end
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '[[99]]' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_reload_should_update_preview
|
||||||
|
tmux.send_keys "seq 3 | #{FZF} --bind 'ctrl-t:reload:echo 4' --preview 'echo {}' --preview-window 'nohidden'", :Enter
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '1' }
|
||||||
|
tmux.send_keys 'C-t'
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '4' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_scroll_off
|
||||||
|
tmux.send_keys "seq 1000 | #{FZF} --scroll-off=3 --bind l:last", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 1000, lines.item_count }
|
||||||
|
height = tmux.until { |lines| lines }.first.to_i
|
||||||
|
tmux.send_keys :PgUp
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal height + 3, lines.first.to_i
|
||||||
|
assert_equal "> #{height}", lines[3].strip
|
||||||
|
end
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until { |lines| assert_equal "> #{height + 1}", lines[3].strip }
|
||||||
|
tmux.send_keys 'l'
|
||||||
|
tmux.until { |lines| assert_equal '> 1000', lines.first.strip }
|
||||||
|
tmux.send_keys :PgDn
|
||||||
|
tmux.until { |lines| assert_equal "> #{1000 - height + 1}", lines.reverse[5].strip }
|
||||||
|
tmux.send_keys :Down
|
||||||
|
tmux.until { |lines| assert_equal "> #{1000 - height}", lines.reverse[5].strip }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_scroll_off_large
|
||||||
|
tmux.send_keys "seq 1000 | #{FZF} --scroll-off=9999", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 1000, lines.item_count }
|
||||||
|
height = tmux.until { |lines| lines }.first.to_i
|
||||||
|
tmux.send_keys :PgUp
|
||||||
|
tmux.until { |lines| assert_equal "> #{height}", lines[height / 2].strip }
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until { |lines| assert_equal "> #{height + 1}", lines[height / 2].strip }
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until { |lines| assert_equal "> #{height + 2}", lines[height / 2].strip }
|
||||||
|
tmux.send_keys :Down
|
||||||
|
tmux.until { |lines| assert_equal "> #{height + 1}", lines[height / 2].strip }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_header_first
|
||||||
|
tmux.send_keys "seq 1000 | #{FZF} --header foobar --header-lines 3 --header-first", :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
> 4
|
||||||
|
997/997
|
||||||
|
>
|
||||||
|
3
|
||||||
|
2
|
||||||
|
1
|
||||||
|
foobar
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
assert_equal expected.chomp, lines.reverse.take(7).reverse.join("\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_header_first_reverse
|
||||||
|
tmux.send_keys "seq 1000 | #{FZF} --header foobar --header-lines 3 --header-first --reverse --inline-info", :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
foobar
|
||||||
|
1
|
||||||
|
2
|
||||||
|
3
|
||||||
|
> < 997/997
|
||||||
|
> 4
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
assert_equal expected.chomp, lines.take(6).join("\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
module TestShell
|
module TestShell
|
||||||
@@ -2033,7 +2312,7 @@ module CompletionTest
|
|||||||
end
|
end
|
||||||
|
|
||||||
# ~USERNAME**<TAB>
|
# ~USERNAME**<TAB>
|
||||||
user = ENV['USER']
|
user = `whoami`.chomp
|
||||||
tmux.send_keys 'C-u'
|
tmux.send_keys 'C-u'
|
||||||
tmux.send_keys "cat ~#{user}**", :Tab
|
tmux.send_keys "cat ~#{user}**", :Tab
|
||||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
|
@@ -104,10 +104,10 @@ if [ -d "${fish_dir}/functions" ]; then
|
|||||||
remove "${fish_dir}/functions/fzf.fish"
|
remove "${fish_dir}/functions/fzf.fish"
|
||||||
remove "${fish_dir}/functions/fzf_key_bindings.fish"
|
remove "${fish_dir}/functions/fzf_key_bindings.fish"
|
||||||
|
|
||||||
if [ "$(ls -A "${fish_dir}/functions")" ]; then
|
if [ -z "$(ls -A "${fish_dir}/functions")" ]; then
|
||||||
echo "Can't delete non-empty directory: \"${fish_dir}/functions\""
|
|
||||||
else
|
|
||||||
rmdir "${fish_dir}/functions"
|
rmdir "${fish_dir}/functions"
|
||||||
|
else
|
||||||
|
echo "Can't delete non-empty directory: \"${fish_dir}/functions\""
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user