mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-17 15:53:39 -05:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20230402d0 | ||
|
|
5c2c3a6c88 | ||
|
|
fb019d43bf | ||
|
|
025aa33773 | ||
|
|
302e21fd58 | ||
|
|
211512ae64 | ||
|
|
8ec917b1c3 | ||
|
|
1c7534f009 | ||
|
|
ae745d9397 | ||
|
|
60f37aae2f | ||
|
|
d7daf5f724 | ||
|
|
e5103d9429 | ||
|
|
8fecb29848 | ||
|
|
290ea6179d | ||
|
|
9695a40fc9 | ||
|
|
1913b95227 | ||
|
|
a874aea692 | ||
|
|
69c52099e7 | ||
|
|
cfc0747d5d | ||
|
|
fcd7e8768d | ||
|
|
3c34dd8275 | ||
|
|
1116e481be | ||
|
|
63cf9d04de | ||
|
|
3364d4d147 | ||
|
|
57ad21e4bd | ||
|
|
414f87981f | ||
|
|
b1459c79cf |
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.19
|
||||
|
||||
|
||||
2
.github/workflows/macos.yml
vendored
2
.github/workflows/macos.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.18
|
||||
|
||||
|
||||
10
.github/workflows/typos.yml
vendored
Normal file
10
.github/workflows/typos.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
name: "Spell Check"
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
typos:
|
||||
name: Spell Check with Typos
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: crate-ci/typos@v1.13.16
|
||||
@@ -74,6 +74,7 @@ builds:
|
||||
- arm64
|
||||
- loong64
|
||||
- ppc64le
|
||||
- s390x
|
||||
goarm:
|
||||
- 5
|
||||
- 6
|
||||
|
||||
@@ -1 +1 @@
|
||||
golang 1.19
|
||||
golang 1.20.2
|
||||
|
||||
36
CHANGELOG.md
36
CHANGELOG.md
@@ -1,6 +1,42 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.39.0
|
||||
------
|
||||
- Added `one` event that is triggered when there's only one match
|
||||
```sh
|
||||
# Automatically select the only match
|
||||
seq 10 | fzf --bind one:accept
|
||||
```
|
||||
- Added `--track` option that makes fzf track the current selection when the
|
||||
result list is updated. This can be useful when browsing logs using fzf with
|
||||
sorting disabled.
|
||||
```sh
|
||||
git log --oneline --graph --color=always | nl |
|
||||
fzf --ansi --track --no-sort --layout=reverse-list
|
||||
```
|
||||
- If you use `--listen` option without a port number fzf will automatically
|
||||
allocate an available port and export it as `$FZF_PORT` environment
|
||||
variable.
|
||||
```sh
|
||||
# Automatic port assignment
|
||||
fzf --listen --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'
|
||||
|
||||
# Say hello
|
||||
curl "localhost:$(cat /tmp/fzf-port)" -d 'preview:echo Hello, fzf is listening on $FZF_PORT.'
|
||||
```
|
||||
- A carriage return and a line feed character will be rendered as dim ␍ and
|
||||
␊ respectively.
|
||||
```sh
|
||||
printf "foo\rbar\nbaz" | fzf --read0 --preview 'echo {}'
|
||||
```
|
||||
- fzf will stop rendering a non-displayable characters as a space. This will
|
||||
likely cause less glitches in the preview window.
|
||||
```sh
|
||||
fzf --preview 'head -1000 /dev/random'
|
||||
```
|
||||
- Bug fixes and improvements
|
||||
|
||||
0.38.0
|
||||
------
|
||||
- New actions
|
||||
|
||||
7
Makefile
7
Makefile
@@ -20,7 +20,7 @@ VERSION_REGEX := $(subst .,\.,$(VERSION_TRIM))
|
||||
ifdef FZF_REVISION
|
||||
REVISION := $(FZF_REVISION)
|
||||
else
|
||||
REVISION := $(shell git log -n 1 --pretty=format:%h -- $(SOURCES) 2> /dev/null)
|
||||
REVISION := $(shell git log -n 1 --pretty=format:%h --abbrev=8 -- $(SOURCES) 2> /dev/null)
|
||||
endif
|
||||
ifeq ($(REVISION),)
|
||||
$(error Not on git repository; cannot determine $$FZF_REVISION)
|
||||
@@ -29,6 +29,7 @@ BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision
|
||||
|
||||
BINARY32 := fzf-$(GOOS)_386
|
||||
BINARY64 := fzf-$(GOOS)_amd64
|
||||
BINARYS390 := fzf-$(GOOS)_s390x
|
||||
BINARYARM5 := fzf-$(GOOS)_arm5
|
||||
BINARYARM6 := fzf-$(GOOS)_arm6
|
||||
BINARYARM7 := fzf-$(GOOS)_arm7
|
||||
@@ -43,6 +44,8 @@ ifeq ($(UNAME_M),x86_64)
|
||||
BINARY := $(BINARY64)
|
||||
else ifeq ($(UNAME_M),amd64)
|
||||
BINARY := $(BINARY64)
|
||||
else ifeq ($(UNAME_M),s390x)
|
||||
BINARY := $(BINARYS390)
|
||||
else ifeq ($(UNAME_M),i686)
|
||||
BINARY := $(BINARY32)
|
||||
else ifeq ($(UNAME_M),i386)
|
||||
@@ -132,6 +135,8 @@ target/$(BINARY32): $(SOURCES)
|
||||
target/$(BINARY64): $(SOURCES)
|
||||
GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@
|
||||
|
||||
target/$(BINARYS390): $(SOURCES)
|
||||
GOARCH=s390x $(GO) build $(BUILD_FLAGS) -o $@
|
||||
# https://github.com/golang/go/wiki/GoArm
|
||||
target/$(BINARYARM5): $(SOURCES)
|
||||
GOARCH=arm GOARM=5 $(GO) build $(BUILD_FLAGS) -o $@
|
||||
|
||||
@@ -15,7 +15,7 @@ set rtp+=/usr/local/opt/fzf
|
||||
" If installed using Homebrew on Apple Silicon
|
||||
set rtp+=/opt/homebrew/opt/fzf
|
||||
|
||||
" If installed using git
|
||||
" If you have cloned fzf on ~/.fzf directory
|
||||
set rtp+=~/.fzf
|
||||
```
|
||||
|
||||
@@ -26,7 +26,7 @@ written as:
|
||||
" If installed using Homebrew
|
||||
Plug '/usr/local/opt/fzf'
|
||||
|
||||
" If installed using git
|
||||
" If you have cloned fzf on ~/.fzf directory
|
||||
Plug '~/.fzf'
|
||||
```
|
||||
|
||||
@@ -118,7 +118,7 @@ let g:fzf_action = {
|
||||
|
||||
" An action can be a reference to a function that processes selected lines
|
||||
function! s:build_quickfix_list(lines)
|
||||
call setqflist(map(copy(a:lines), '{ "filename": v:val }'))
|
||||
call setqflist(map(copy(a:lines), '{ "filename": v:val, "lnum": 1 }'))
|
||||
copen
|
||||
cc
|
||||
endfunction
|
||||
|
||||
@@ -61,8 +61,8 @@ Table of Contents
|
||||
* [3. Interactive ripgrep integration](#3-interactive-ripgrep-integration)
|
||||
* [Preview window](#preview-window)
|
||||
* [Tips](#tips)
|
||||
* [Respecting `.gitignore`](#respecting-gitignore)
|
||||
* [Fish shell](#fish-shell)
|
||||
* [Respecting `.gitignore`](#respecting-gitignore)
|
||||
* [Fish shell](#fish-shell)
|
||||
* [Related projects](#related-projects)
|
||||
* [License](#license)
|
||||
|
||||
@@ -124,6 +124,7 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
|
||||
| pkg | FreeBSD | `pkg install fzf` |
|
||||
| pkgin | NetBSD | `pkgin install fzf` |
|
||||
| pkg_add | OpenBSD | `pkg_add fzf` |
|
||||
| Portage | Gentoo | `emerge --ask app-shells/fzf` |
|
||||
| XBPS | Void Linux | `sudo xbps-install -S fzf` |
|
||||
| Zypper | openSUSE | `sudo zypper install fzf` |
|
||||
|
||||
@@ -723,7 +724,7 @@ history | fzf
|
||||
Tips
|
||||
----
|
||||
|
||||
#### Respecting `.gitignore`
|
||||
### Respecting `.gitignore`
|
||||
|
||||
You can use [fd](https://github.com/sharkdp/fd),
|
||||
[ripgrep](https://github.com/BurntSushi/ripgrep), or [the silver
|
||||
@@ -752,7 +753,7 @@ hidden files, use the following command:
|
||||
export FZF_DEFAULT_COMMAND='fd --type f --strip-cwd-prefix --hidden --follow --exclude .git'
|
||||
```
|
||||
|
||||
#### Fish shell
|
||||
### Fish shell
|
||||
|
||||
`CTRL-T` key binding of fish, unlike those of bash and zsh, will use the last
|
||||
token on the command-line as the root directory for the recursive search. For
|
||||
|
||||
11
bin/fzf-tmux
11
bin/fzf-tmux
@@ -179,12 +179,15 @@ trap 'cleanup' EXIT
|
||||
|
||||
envs="export TERM=$TERM "
|
||||
if [[ "$opt" =~ "-E" ]]; then
|
||||
tmux_version=$(tmux -V)
|
||||
if [[ $tmux_version =~ ^tmux\ 3\.2[a-z]?$ ]]; then
|
||||
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
|
||||
else
|
||||
tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
|
||||
if [[ $(bc -l <<< "$tmux_version > 3.2") = 1 ]]; then
|
||||
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
|
||||
opt="-B $opt"
|
||||
elif [[ $tmux_version = 3.2 ]]; then
|
||||
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
|
||||
else
|
||||
echo "fzf-tmux: tmux 3.2 or above is required for popup mode" >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
[[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
fzf.txt fzf Last change: May 19 2021
|
||||
fzf.txt fzf Last change: Mar 20 2023
|
||||
FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
|
||||
==============================================================================
|
||||
|
||||
@@ -32,7 +32,7 @@ depending on the package manager.
|
||||
" If installed using Homebrew
|
||||
set rtp+=/usr/local/opt/fzf
|
||||
|
||||
" If installed using git
|
||||
" If you have cloned fzf on ~/.fzf directory
|
||||
set rtp+=~/.fzf
|
||||
<
|
||||
If you use {vim-plug}{1}, the same can be written as:
|
||||
@@ -40,7 +40,7 @@ If you use {vim-plug}{1}, the same can be written as:
|
||||
" If installed using Homebrew
|
||||
Plug '/usr/local/opt/fzf'
|
||||
|
||||
" If installed using git
|
||||
" If you have cloned fzf on ~/.fzf directory
|
||||
Plug '~/.fzf'
|
||||
<
|
||||
But if you want the latest Vim plugin file from GitHub rather than the one
|
||||
@@ -143,7 +143,7 @@ Examples~
|
||||
|
||||
" An action can be a reference to a function that processes selected lines
|
||||
function! s:build_quickfix_list(lines)
|
||||
call setqflist(map(copy(a:lines), '{ "filename": v:val }'))
|
||||
call setqflist(map(copy(a:lines), '{ "filename": v:val, "lnum": 1 }'))
|
||||
copen
|
||||
cc
|
||||
endfunction
|
||||
|
||||
6
go.mod
6
go.mod
@@ -5,10 +5,10 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.17
|
||||
github.com/mattn/go-runewidth v0.0.14
|
||||
github.com/mattn/go-shellwords v1.0.12
|
||||
github.com/rivo/uniseg v0.4.2
|
||||
github.com/rivo/uniseg v0.4.4
|
||||
github.com/saracen/walker v0.1.3
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
golang.org/x/sys v0.6.0
|
||||
golang.org/x/term v0.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
10
go.sum
10
go.sum
@@ -11,8 +11,8 @@ github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
|
||||
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
|
||||
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/saracen/walker v0.1.3 h1:YtcKKmpRPy6XJTHJ75J2QYXXZYWnZNQxPCVqZSHVV/g=
|
||||
github.com/saracen/walker v0.1.3/go.mod h1:FU+7qU8DeQQgSZDmmThMJi93kPkLFgy0oVAcLxurjIk=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
@@ -31,11 +31,13 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
|
||||
3
install
3
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.38.0
|
||||
version=0.39.0
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
@@ -178,6 +178,7 @@ case "$archi" in
|
||||
Linux\ loongarch64) download fzf-$version-linux_loong64.tar.gz ;;
|
||||
Linux\ ppc64le) download fzf-$version-linux_ppc64le.tar.gz ;;
|
||||
Linux\ *64) download fzf-$version-linux_amd64.tar.gz ;;
|
||||
Linux\ s390x) download fzf-$version-linux_s390x.tar.gz ;;
|
||||
FreeBSD\ *64) download fzf-$version-freebsd_amd64.tar.gz ;;
|
||||
OpenBSD\ *64) download fzf-$version-openbsd_amd64.tar.gz ;;
|
||||
CYGWIN*\ *64) download fzf-$version-windows_amd64.zip ;;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
$version="0.38.0"
|
||||
$version="0.39.0"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
|
||||
2
main.go
2
main.go
@@ -5,7 +5,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/protector"
|
||||
)
|
||||
|
||||
var version string = "0.38"
|
||||
var version string = "0.39"
|
||||
var revision string = "devel"
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf-tmux 1 "Feb 2023" "fzf 0.38.0" "fzf-tmux - open fzf in tmux split pane"
|
||||
.TH fzf-tmux 1 "Apr 2023" "fzf 0.39.0" "fzf-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf-tmux - open fzf in tmux split pane
|
||||
|
||||
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf 1 "Feb 2023" "fzf 0.38.0" "fzf - a command-line fuzzy finder"
|
||||
.TH fzf 1 "Apr 2023" "fzf 0.39.0" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
@@ -92,6 +92,16 @@ interface rather than a "fuzzy finder". You can later enable the search using
|
||||
.B "+s, --no-sort"
|
||||
Do not sort the result
|
||||
.TP
|
||||
.B "--track"
|
||||
Make fzf track the current selection when the result list is updated.
|
||||
This can be useful when browsing logs using fzf with sorting disabled.
|
||||
|
||||
.RS
|
||||
e.g.
|
||||
\fBgit log --oneline --graph --color=always | nl |
|
||||
fzf --ansi --track --no-sort --layout=reverse-list\fR
|
||||
.RE
|
||||
.TP
|
||||
.B "--tac"
|
||||
Reverse the order of the input
|
||||
|
||||
@@ -738,9 +748,12 @@ ncurses finder only after the input stream is complete.
|
||||
e.g. \fBfzf --multi | fzf --sync\fR
|
||||
.RE
|
||||
.TP
|
||||
.B "--listen=HTTP_PORT"
|
||||
.B "--listen[=HTTP_PORT]"
|
||||
Start HTTP server on the given port. It allows external processes to send
|
||||
actions to perform via POST method.
|
||||
actions to perform via POST method. If the port number is omitted or given as
|
||||
0, fzf will choose the port automatically and export it as \fBFZF_PORT\fR
|
||||
environment variable to the child processes started via \fBexecute\fR and
|
||||
\fBexecute-silent\fR actions.
|
||||
|
||||
e.g.
|
||||
\fB# Start HTTP server on port 6266
|
||||
@@ -748,6 +761,9 @@ e.g.
|
||||
|
||||
# Send action to the server
|
||||
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
|
||||
|
||||
# Choose port automatically and export it as $FZF_PORT to the child process
|
||||
fzf --listen --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'
|
||||
\fR
|
||||
.TP
|
||||
.B "--version"
|
||||
@@ -977,6 +993,17 @@ e.g.
|
||||
# Beware not to introduce an infinite loop
|
||||
seq 10 | fzf --bind 'focus:up' --cycle\fR
|
||||
.RE
|
||||
\fIone\fR
|
||||
.RS
|
||||
Triggered when there's only one match. \fBone:accept\fR binding is comparable
|
||||
to \fB--select-1\fR option, but the difference is that \fB--select-1\fR is only
|
||||
effective before the interactive finder starts but \fBone\fR event is triggered
|
||||
by the interactive finder.
|
||||
|
||||
e.g.
|
||||
\fB# Automatically select the only match
|
||||
seq 10 | fzf --bind one:accept\fR
|
||||
.RE
|
||||
|
||||
\fIbackward-eof\fR
|
||||
.RS
|
||||
@@ -1145,7 +1172,7 @@ set, otherwise with \fBsh -c\fR, so in this case make sure that the command is
|
||||
POSIX-compliant.
|
||||
|
||||
\fBbecome(...)\fR action is similar to \fBexecute(...)\fR, but it replaces the
|
||||
current fzf process with the specifed command using \fBexecve(2)\fR system
|
||||
current fzf process with the specified command using \fBexecve(2)\fR system
|
||||
call.
|
||||
|
||||
\fBfzf --bind "enter:become(vim {})"\fR
|
||||
|
||||
@@ -270,8 +270,9 @@ _fzf_complete_kill() {
|
||||
}
|
||||
|
||||
_fzf_proc_completion() {
|
||||
_fzf_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||
command ps -ef | sed 1d
|
||||
_fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||
command ps -eo user,pid,ppid,time,args # For BusyBox
|
||||
)
|
||||
}
|
||||
|
||||
@@ -309,7 +310,7 @@ complete -o default -F _fzf_opts_completion fzf-tmux
|
||||
|
||||
d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}"
|
||||
a_cmds="
|
||||
awk cat diff diff3
|
||||
awk bat cat diff diff3
|
||||
emacs emacsclient ex file ftp g++ gcc gvim head hg hx java
|
||||
javac ld less more mvim nvim patch perl python ruby
|
||||
sed sftp sort source tail tee uniq vi view vim wc xdg-open
|
||||
|
||||
@@ -251,8 +251,9 @@ _fzf_complete_unalias() {
|
||||
}
|
||||
|
||||
_fzf_complete_kill() {
|
||||
_fzf_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||
command ps -ef | sed 1d
|
||||
_fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||
command ps -eo user,pid,ppid,time,args # For BusyBox
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -219,6 +219,7 @@ func Run(opts *Options, version string, revision string) {
|
||||
determine := func(final bool) {
|
||||
if heightUnknown {
|
||||
if total >= maxFit || final {
|
||||
deferred = false
|
||||
heightUnknown = false
|
||||
terminal.startChan <- fitpad{util.Min(total, maxFit), padHeight}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ type Merger struct {
|
||||
tac bool
|
||||
final bool
|
||||
count int
|
||||
pass bool
|
||||
}
|
||||
|
||||
// PassMerger returns a new Merger that simply returns the items in the
|
||||
@@ -26,7 +27,8 @@ func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
|
||||
pattern: nil,
|
||||
chunks: chunks,
|
||||
tac: tac,
|
||||
count: 0}
|
||||
count: 0,
|
||||
pass: true}
|
||||
|
||||
for _, chunk := range *mg.chunks {
|
||||
mg.count += chunk.count
|
||||
@@ -58,6 +60,19 @@ func (mg *Merger) Length() int {
|
||||
return mg.count
|
||||
}
|
||||
|
||||
// FindIndex returns the index of the item with the given item index
|
||||
func (mg *Merger) FindIndex(itemIndex int32) int {
|
||||
if mg.pass {
|
||||
return int(itemIndex)
|
||||
}
|
||||
for i := 0; i < mg.count; i++ {
|
||||
if mg.Get(i).item.Index() == itemIndex {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// Get returns the pointer to the Result object indexed by the given integer
|
||||
func (mg *Merger) Get(idx int) Result {
|
||||
if mg.chunks != nil {
|
||||
|
||||
@@ -33,6 +33,7 @@ const usage = `usage: fzf [options]
|
||||
field index expressions
|
||||
-d, --delimiter=STR Field delimiter regex (default: AWK-style)
|
||||
+s, --no-sort Do not sort the result
|
||||
--track Track the current selection when the result is updated
|
||||
--tac Reverse the order of the input
|
||||
--disabled Do not perform search
|
||||
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
|
||||
@@ -116,7 +117,7 @@ const usage = `usage: fzf [options]
|
||||
--read0 Read input delimited by ASCII NUL characters
|
||||
--print0 Print output delimited by ASCII NUL characters
|
||||
--sync Synchronous search for multi-staged filtering
|
||||
--listen=HTTP_PORT Start HTTP server to receive actions (POST /)
|
||||
--listen[=HTTP_PORT] Start HTTP server to receive actions (POST /)
|
||||
--version Display version information and exit
|
||||
|
||||
Environment variables
|
||||
@@ -266,6 +267,7 @@ type Options struct {
|
||||
WithNth []Range
|
||||
Delimiter Delimiter
|
||||
Sort int
|
||||
Track bool
|
||||
Tac bool
|
||||
Criteria []criterion
|
||||
Multi int
|
||||
@@ -316,7 +318,7 @@ type Options struct {
|
||||
PreviewLabel labelOpts
|
||||
Unicode bool
|
||||
Tabstop int
|
||||
ListenPort int
|
||||
ListenPort *int
|
||||
ClearOnExit bool
|
||||
Version bool
|
||||
}
|
||||
@@ -338,6 +340,7 @@ func defaultOptions() *Options {
|
||||
WithNth: make([]Range, 0),
|
||||
Delimiter: Delimiter{},
|
||||
Sort: 1000,
|
||||
Track: false,
|
||||
Tac: false,
|
||||
Criteria: []criterion{byScore, byLength},
|
||||
Multi: 0,
|
||||
@@ -619,6 +622,8 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
|
||||
add(tui.Load)
|
||||
case "focus":
|
||||
add(tui.Focus)
|
||||
case "one":
|
||||
add(tui.One)
|
||||
case "alt-enter", "alt-return":
|
||||
chords[tui.CtrlAltKey('m')] = key
|
||||
case "alt-space":
|
||||
@@ -1562,6 +1567,10 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
opts.Sort = optionalNumeric(allArgs, &i, 1)
|
||||
case "+s", "--no-sort":
|
||||
opts.Sort = 0
|
||||
case "--track":
|
||||
opts.Track = true
|
||||
case "--no-track":
|
||||
opts.Track = false
|
||||
case "--tac":
|
||||
opts.Tac = true
|
||||
case "--no-tac":
|
||||
@@ -1756,9 +1765,10 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
case "--tabstop":
|
||||
opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
|
||||
case "--listen":
|
||||
opts.ListenPort = nextInt(allArgs, &i, "listen port required")
|
||||
port := optionalNumeric(allArgs, &i, 0)
|
||||
opts.ListenPort = &port
|
||||
case "--no-listen":
|
||||
opts.ListenPort = 0
|
||||
opts.ListenPort = nil
|
||||
case "--clear":
|
||||
opts.ClearOnExit = true
|
||||
case "--no-clear":
|
||||
@@ -1849,7 +1859,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
} else if match, value := optString(arg, "--tabstop="); match {
|
||||
opts.Tabstop = atoi(value)
|
||||
} else if match, value := optString(arg, "--listen="); match {
|
||||
opts.ListenPort = atoi(value)
|
||||
port := atoi(value)
|
||||
opts.ListenPort = &port
|
||||
} else if match, value := optString(arg, "--hscroll-off="); match {
|
||||
opts.HscrollOff = atoi(value)
|
||||
} else if match, value := optString(arg, "--scroll-off="); match {
|
||||
@@ -1879,7 +1890,7 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
errorExit("tab stop must be a positive integer")
|
||||
}
|
||||
|
||||
if opts.ListenPort < 0 || opts.ListenPort > 65535 {
|
||||
if opts.ListenPort != nil && (*opts.ListenPort < 0 || *opts.ListenPort > 65535) {
|
||||
errorExit("invalid listen port")
|
||||
}
|
||||
|
||||
@@ -2001,9 +2012,7 @@ func postProcessOptions(opts *Options) {
|
||||
theme := opts.Theme
|
||||
boldify := func(c tui.ColorAttr) tui.ColorAttr {
|
||||
dup := c
|
||||
if !theme.Colored {
|
||||
dup.Attr |= tui.Bold
|
||||
} else if (c.Attr & tui.AttrRegular) == 0 {
|
||||
if (c.Attr & tui.AttrRegular) == 0 {
|
||||
dup.Attr |= tui.Bold
|
||||
}
|
||||
return dup
|
||||
|
||||
@@ -19,14 +19,26 @@ const (
|
||||
maxContentLength = 1024 * 1024
|
||||
)
|
||||
|
||||
func startHttpServer(port int, channel chan []*action) error {
|
||||
if port == 0 {
|
||||
return nil
|
||||
func startHttpServer(port int, channel chan []*action) (error, int) {
|
||||
if port < 0 {
|
||||
return nil, port
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
|
||||
if err != nil {
|
||||
return fmt.Errorf("port not available: %d", port)
|
||||
return fmt.Errorf("port not available: %d", port), port
|
||||
}
|
||||
if port == 0 {
|
||||
addr := listener.Addr().String()
|
||||
parts := strings.SplitN(addr, ":", 2)
|
||||
if len(parts) < 2 {
|
||||
return fmt.Errorf("cannot extract port: %s", addr), port
|
||||
}
|
||||
var err error
|
||||
port, err = strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return err, port
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
@@ -45,7 +57,7 @@ func startHttpServer(port int, channel chan []*action) error {
|
||||
listener.Close()
|
||||
}()
|
||||
|
||||
return nil
|
||||
return nil, port
|
||||
}
|
||||
|
||||
// Here we are writing a simplistic HTTP server without using net/http
|
||||
|
||||
@@ -183,6 +183,7 @@ type Terminal struct {
|
||||
multi int
|
||||
sort bool
|
||||
toggleSort bool
|
||||
track bool
|
||||
delimiter Delimiter
|
||||
expect map[tui.Event]string
|
||||
keymap map[tui.Event][]*action
|
||||
@@ -201,9 +202,8 @@ type Terminal struct {
|
||||
tabstop int
|
||||
margin [4]sizeSpec
|
||||
padding [4]sizeSpec
|
||||
strong tui.Attr
|
||||
unicode bool
|
||||
listenPort int
|
||||
listenPort *int
|
||||
borderShape tui.BorderShape
|
||||
cleanExit bool
|
||||
paused bool
|
||||
@@ -538,13 +538,9 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
}
|
||||
var previewBox *util.EventBox
|
||||
// We need to start previewer if HTTP server is enabled even when --preview option is not specified
|
||||
if len(opts.Preview.command) > 0 || hasPreviewAction(opts) || opts.ListenPort > 0 {
|
||||
if len(opts.Preview.command) > 0 || hasPreviewAction(opts) || opts.ListenPort != nil {
|
||||
previewBox = util.NewEventBox()
|
||||
}
|
||||
strongAttr := tui.Bold
|
||||
if !opts.Bold {
|
||||
strongAttr = tui.AttrRegular
|
||||
}
|
||||
var renderer tui.Renderer
|
||||
fullscreen := !opts.Height.auto && (opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100)
|
||||
if fullscreen {
|
||||
@@ -604,6 +600,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
multi: opts.Multi,
|
||||
sort: opts.Sort > 0,
|
||||
toggleSort: opts.ToggleSort,
|
||||
track: opts.Track,
|
||||
delimiter: opts.Delimiter,
|
||||
expect: opts.Expect,
|
||||
keymap: opts.Keymap,
|
||||
@@ -623,7 +620,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
previewLabelOpts: opts.PreviewLabel,
|
||||
cleanExit: opts.ClearOnExit,
|
||||
paused: opts.Phony,
|
||||
strong: strongAttr,
|
||||
cycle: opts.Cycle,
|
||||
headerFirst: opts.HeaderFirst,
|
||||
headerLines: opts.HeaderLines,
|
||||
@@ -694,13 +690,25 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
|
||||
_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
|
||||
|
||||
if err := startHttpServer(t.listenPort, t.serverChan); err != nil {
|
||||
errorExit(err.Error())
|
||||
if t.listenPort != nil {
|
||||
err, port := startHttpServer(*t.listenPort, t.serverChan)
|
||||
if err != nil {
|
||||
errorExit(err.Error())
|
||||
}
|
||||
t.listenPort = &port
|
||||
}
|
||||
|
||||
return &t
|
||||
}
|
||||
|
||||
func (t *Terminal) environ() []string {
|
||||
env := os.Environ()
|
||||
if t.listenPort != nil {
|
||||
env = append(env, fmt.Sprintf("FZF_PORT=%d", *t.listenPort))
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
func borderLines(shape tui.BorderShape) int {
|
||||
switch shape {
|
||||
case tui.BorderHorizontal, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderDouble:
|
||||
@@ -740,7 +748,7 @@ func (t *Terminal) ansiLabelPrinter(str string, color *tui.ColorPair, fill bool)
|
||||
|
||||
// Simpler printer for strings without ANSI colors or tab characters
|
||||
if colors == nil && strings.IndexRune(str, '\t') < 0 {
|
||||
length := runewidth.StringWidth(str)
|
||||
length := util.StringWidth(str)
|
||||
if length == 0 {
|
||||
return nil, 0
|
||||
}
|
||||
@@ -898,6 +906,10 @@ func (t *Terminal) UpdateProgress(progress float32) {
|
||||
// UpdateList updates Merger to display the list
|
||||
func (t *Terminal) UpdateList(merger *Merger, reset bool) {
|
||||
t.mutex.Lock()
|
||||
var prevIndex int32 = -1
|
||||
if !reset && t.track && t.merger.Length() > 0 {
|
||||
prevIndex = t.merger.Get(t.cy).item.Index()
|
||||
}
|
||||
t.progress = 100
|
||||
t.merger = merger
|
||||
if reset {
|
||||
@@ -908,6 +920,24 @@ func (t *Terminal) UpdateList(merger *Merger, reset bool) {
|
||||
t.triggerLoad = false
|
||||
t.eventChan <- tui.Load.AsEvent()
|
||||
}
|
||||
if prevIndex >= 0 {
|
||||
pos := t.cy - t.offset
|
||||
count := t.merger.Length()
|
||||
i := t.merger.FindIndex(prevIndex)
|
||||
if i >= 0 {
|
||||
t.cy = i
|
||||
t.offset = t.cy - pos
|
||||
} else if t.cy > count {
|
||||
// Try to keep the vertical position when the list shrinks
|
||||
t.cy = count - util.Min(count, t.maxItems()) + pos
|
||||
}
|
||||
}
|
||||
if !t.reading && t.merger.Length() == 1 {
|
||||
one := tui.One.AsEvent()
|
||||
if _, prs := t.keymap[one]; prs {
|
||||
t.eventChan <- one
|
||||
}
|
||||
}
|
||||
t.mutex.Unlock()
|
||||
t.reqBox.Set(reqInfo, nil)
|
||||
t.reqBox.Set(reqList, nil)
|
||||
@@ -1338,8 +1368,7 @@ func (t *Terminal) updatePromptOffset() ([]rune, []rune) {
|
||||
|
||||
_, overflow := t.trimLeft(t.input[:t.cx], maxWidth)
|
||||
minOffset := int(overflow)
|
||||
maxOffset := util.Min(util.Min(len(t.input), minOffset+maxWidth), t.cx)
|
||||
|
||||
maxOffset := minOffset + (maxWidth-util.Max(0, maxWidth-t.cx))/2
|
||||
t.xoffset = util.Constrain(t.xoffset, minOffset, maxOffset)
|
||||
before, _ := t.trimLeft(t.input[t.xoffset:t.cx], maxWidth)
|
||||
beforeLen := t.displayWidth(before)
|
||||
@@ -1404,7 +1433,7 @@ func (t *Terminal) printInfo() {
|
||||
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
|
||||
str := t.infoSep
|
||||
maxWidth := t.window.Width() - pos
|
||||
width := runewidth.StringWidth(str)
|
||||
width := util.StringWidth(str)
|
||||
if width > maxWidth {
|
||||
trimmed, _ := t.trimRight([]rune(str), maxWidth)
|
||||
str = string(trimmed)
|
||||
@@ -1829,12 +1858,14 @@ func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unc
|
||||
trimmed, isTrimmed = t.trimRight(trimmed, maxWidth-t.pwindow.X())
|
||||
}
|
||||
str, width := t.processTabs(trimmed, prefixWidth)
|
||||
prefixWidth += width
|
||||
if t.theme.Colored && ansi != nil && ansi.colored() {
|
||||
lbg = ansi.lbg
|
||||
fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str)
|
||||
} else {
|
||||
fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str)
|
||||
if width > prefixWidth {
|
||||
prefixWidth = width
|
||||
if t.theme.Colored && ansi != nil && ansi.colored() {
|
||||
lbg = ansi.lbg
|
||||
fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str)
|
||||
} else {
|
||||
fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str)
|
||||
}
|
||||
}
|
||||
return !isTrimmed &&
|
||||
(fillRet == tui.FillContinue || t.previewOpts.wrap && fillRet == tui.FillNextLine)
|
||||
@@ -1937,7 +1968,7 @@ func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) {
|
||||
w = t.tabstop - l%t.tabstop
|
||||
strbuf.WriteString(strings.Repeat(" ", w))
|
||||
} else {
|
||||
w = runewidth.StringWidth(str)
|
||||
w = util.StringWidth(str)
|
||||
strbuf.WriteString(str)
|
||||
}
|
||||
l += w
|
||||
@@ -2247,6 +2278,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
|
||||
}
|
||||
command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
|
||||
cmd := util.ExecCommand(command, false)
|
||||
cmd.Env = t.environ()
|
||||
t.executing.Set(true)
|
||||
if !background {
|
||||
cmd.Stdin = tui.TtyIn()
|
||||
@@ -2493,17 +2525,17 @@ func (t *Terminal) Loop() {
|
||||
_, query := t.Input()
|
||||
command := t.replacePlaceholder(commandTemplate, false, string(query), items)
|
||||
cmd := util.ExecCommand(command, true)
|
||||
env := t.environ()
|
||||
if pwindow != nil {
|
||||
height := pwindow.Height()
|
||||
env := os.Environ()
|
||||
lines := fmt.Sprintf("LINES=%d", height)
|
||||
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
|
||||
env = append(env, lines)
|
||||
env = append(env, "FZF_PREVIEW_"+lines)
|
||||
env = append(env, columns)
|
||||
env = append(env, "FZF_PREVIEW_"+columns)
|
||||
cmd.Env = env
|
||||
}
|
||||
cmd.Env = env
|
||||
|
||||
out, _ := cmd.StdoutPipe()
|
||||
cmd.Stderr = cmd.Stdout
|
||||
@@ -3289,7 +3321,7 @@ func (t *Terminal) Loop() {
|
||||
break
|
||||
}
|
||||
|
||||
// Prevew scrollbar dragging
|
||||
// Preview scrollbar dragging
|
||||
headerLines := t.previewOpts.headerLines
|
||||
pbarDragging = me.Down && (pbarDragging || clicked && t.hasPreviewWindow() && my >= t.pwindow.Top()+headerLines && my < t.pwindow.Top()+t.pwindow.Height() && mx == t.pwindow.Left()+t.pwindow.Width())
|
||||
if pbarDragging {
|
||||
|
||||
@@ -32,20 +32,26 @@ var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]
|
||||
var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
|
||||
|
||||
func (r *LightRenderer) stderr(str string) {
|
||||
r.stderrInternal(str, true)
|
||||
r.stderrInternal(str, true, "")
|
||||
}
|
||||
|
||||
// FIXME: Need better handling of non-displayable characters
|
||||
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) {
|
||||
const CR string = "\x1b[2m␍"
|
||||
const LF string = "\x1b[2m␊"
|
||||
|
||||
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
|
||||
bytes := []byte(str)
|
||||
runes := []rune{}
|
||||
for len(bytes) > 0 {
|
||||
r, sz := utf8.DecodeRune(bytes)
|
||||
nlcr := r == '\n' || r == '\r'
|
||||
if r >= 32 || r == '\x1b' || nlcr {
|
||||
if r == utf8.RuneError || nlcr && !allowNLCR {
|
||||
runes = append(runes, ' ')
|
||||
} else {
|
||||
if nlcr && !allowNLCR {
|
||||
if r == '\r' {
|
||||
runes = append(runes, []rune(CR+resetCode)...)
|
||||
} else {
|
||||
runes = append(runes, []rune(LF+resetCode)...)
|
||||
}
|
||||
} else if r != utf8.RuneError {
|
||||
runes = append(runes, r)
|
||||
}
|
||||
}
|
||||
@@ -54,8 +60,10 @@ func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) {
|
||||
r.queued.WriteString(string(runes))
|
||||
}
|
||||
|
||||
func (r *LightRenderer) csi(code string) {
|
||||
r.stderr("\x1b[" + code)
|
||||
func (r *LightRenderer) csi(code string) string {
|
||||
fullcode := "\x1b[" + code
|
||||
r.stderr(fullcode)
|
||||
return fullcode
|
||||
}
|
||||
|
||||
func (r *LightRenderer) flush() {
|
||||
@@ -825,12 +833,12 @@ func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
|
||||
w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.horizontal, (w.width-bcw)/hw)+repeat(' ', rem)+string(w.border.bottomRight))
|
||||
}
|
||||
|
||||
func (w *LightWindow) csi(code string) {
|
||||
w.renderer.csi(code)
|
||||
func (w *LightWindow) csi(code string) string {
|
||||
return w.renderer.csi(code)
|
||||
}
|
||||
|
||||
func (w *LightWindow) stderrInternal(str string, allowNLCR bool) {
|
||||
w.renderer.stderrInternal(str, allowNLCR)
|
||||
func (w *LightWindow) stderrInternal(str string, allowNLCR bool, resetCode string) {
|
||||
w.renderer.stderrInternal(str, allowNLCR, resetCode)
|
||||
}
|
||||
|
||||
func (w *LightWindow) Top() int {
|
||||
@@ -936,10 +944,10 @@ func colorCodes(fg Color, bg Color) []string {
|
||||
return codes
|
||||
}
|
||||
|
||||
func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) bool {
|
||||
func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) (bool, string) {
|
||||
codes := append(attrCodes(attr), colorCodes(fg, bg)...)
|
||||
w.csi(";" + strings.Join(codes, ";") + "m")
|
||||
return len(codes) > 0
|
||||
code := w.csi(";" + strings.Join(codes, ";") + "m")
|
||||
return len(codes) > 0, code
|
||||
}
|
||||
|
||||
func (w *LightWindow) Print(text string) {
|
||||
@@ -951,16 +959,17 @@ func cleanse(str string) string {
|
||||
}
|
||||
|
||||
func (w *LightWindow) CPrint(pair ColorPair, text string) {
|
||||
w.csiColor(pair.Fg(), pair.Bg(), pair.Attr())
|
||||
w.stderrInternal(cleanse(text), false)
|
||||
_, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Attr())
|
||||
w.stderrInternal(cleanse(text), false, code)
|
||||
w.csi("m")
|
||||
}
|
||||
|
||||
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
|
||||
if w.csiColor(fg, bg, attr) {
|
||||
hasColors, code := w.csiColor(fg, bg, attr)
|
||||
if hasColors {
|
||||
defer w.csi("m")
|
||||
}
|
||||
w.stderrInternal(cleanse(text), false)
|
||||
w.stderrInternal(cleanse(text), false, code)
|
||||
}
|
||||
|
||||
type wrappedLine struct {
|
||||
@@ -980,6 +989,8 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
|
||||
if len(rs) == 1 && rs[0] == '\t' {
|
||||
w = tabstop - (prefixLength+width)%tabstop
|
||||
str = repeat(' ', w)
|
||||
} else if rs[0] == '\r' {
|
||||
w++
|
||||
} else {
|
||||
w = runewidth.StringWidth(str)
|
||||
}
|
||||
@@ -998,12 +1009,12 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
|
||||
return lines
|
||||
}
|
||||
|
||||
func (w *LightWindow) fill(str string, onMove func()) FillReturn {
|
||||
func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
||||
allLines := strings.Split(str, "\n")
|
||||
for i, line := range allLines {
|
||||
lines := wrapLine(line, w.posx, w.width, w.tabstop)
|
||||
for j, wl := range lines {
|
||||
w.stderrInternal(wl.text, false)
|
||||
w.stderrInternal(wl.text, false, resetCode)
|
||||
w.posx += wl.displayWidth
|
||||
|
||||
// Wrap line
|
||||
@@ -1013,7 +1024,7 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
|
||||
}
|
||||
w.MoveAndClear(w.posy, w.posx)
|
||||
w.Move(w.posy+1, 0)
|
||||
onMove()
|
||||
w.renderer.stderr(resetCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1022,22 +1033,26 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
|
||||
return FillSuspend
|
||||
}
|
||||
w.Move(w.posy+1, 0)
|
||||
onMove()
|
||||
w.renderer.stderr(resetCode)
|
||||
return FillNextLine
|
||||
}
|
||||
return FillContinue
|
||||
}
|
||||
|
||||
func (w *LightWindow) setBg() {
|
||||
func (w *LightWindow) setBg() string {
|
||||
if w.bg != colDefault {
|
||||
w.csiColor(colDefault, w.bg, AttrRegular)
|
||||
_, code := w.csiColor(colDefault, w.bg, AttrRegular)
|
||||
return code
|
||||
}
|
||||
// Should clear dim attribute after ␍ in the preview window
|
||||
// e.g. printf "foo\rbar" | fzf --ansi --preview 'printf "foo\rbar"'
|
||||
return "\x1b[m"
|
||||
}
|
||||
|
||||
func (w *LightWindow) Fill(text string) FillReturn {
|
||||
w.Move(w.posy, w.posx)
|
||||
w.setBg()
|
||||
return w.fill(text, w.setBg)
|
||||
code := w.setBg()
|
||||
return w.fill(text, code)
|
||||
}
|
||||
|
||||
func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn {
|
||||
@@ -1048,11 +1063,11 @@ func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillRetu
|
||||
if bg == colDefault {
|
||||
bg = w.bg
|
||||
}
|
||||
if w.csiColor(fg, bg, attr) {
|
||||
if hasColors, resetCode := w.csiColor(fg, bg, attr); hasColors {
|
||||
defer w.csi("m")
|
||||
return w.fill(text, func() { w.csiColor(fg, bg, attr) })
|
||||
return w.fill(text, resetCode)
|
||||
}
|
||||
return w.fill(text, w.setBg)
|
||||
return w.fill(text, w.setBg())
|
||||
}
|
||||
|
||||
func (w *LightWindow) FinishFill() {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/gdamore/tcell/v2/encoding"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/rivo/uniseg"
|
||||
@@ -572,26 +573,27 @@ func (w *TcellWindow) printString(text string, pair ColorPair) {
|
||||
|
||||
gr := uniseg.NewGraphemes(text)
|
||||
for gr.Next() {
|
||||
st := style
|
||||
rs := gr.Runes()
|
||||
|
||||
if len(rs) == 1 {
|
||||
r := rs[0]
|
||||
if r < rune(' ') { // ignore control characters
|
||||
continue
|
||||
if r == '\r' {
|
||||
st = style.Dim(true)
|
||||
rs[0] = '␍'
|
||||
} else if r == '\n' {
|
||||
w.lastY++
|
||||
lx = 0
|
||||
continue
|
||||
} else if r == '\u000D' { // skip carriage return
|
||||
st = style.Dim(true)
|
||||
rs[0] = '␊'
|
||||
} else if r < rune(' ') { // ignore control characters
|
||||
continue
|
||||
}
|
||||
}
|
||||
var xPos = w.left + w.lastX + lx
|
||||
var yPos = w.top + w.lastY
|
||||
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
|
||||
_screen.SetContent(xPos, yPos, rs[0], rs[1:], style)
|
||||
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
|
||||
}
|
||||
lx += runewidth.StringWidth(string(rs))
|
||||
lx += util.StringWidth(string(rs))
|
||||
}
|
||||
w.lastX += lx
|
||||
}
|
||||
@@ -620,13 +622,22 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
||||
Italic(a&Attr(tcell.AttrItalic) != 0)
|
||||
|
||||
gr := uniseg.NewGraphemes(text)
|
||||
Loop:
|
||||
for gr.Next() {
|
||||
st := style
|
||||
rs := gr.Runes()
|
||||
if len(rs) == 1 && rs[0] == '\n' {
|
||||
w.lastY++
|
||||
w.lastX = 0
|
||||
lx = 0
|
||||
continue
|
||||
if len(rs) == 1 {
|
||||
r := rs[0]
|
||||
switch r {
|
||||
case '\r':
|
||||
st = style.Dim(true)
|
||||
rs[0] = '␍'
|
||||
case '\n':
|
||||
w.lastY++
|
||||
w.lastX = 0
|
||||
lx = 0
|
||||
continue Loop
|
||||
}
|
||||
}
|
||||
|
||||
// word wrap:
|
||||
@@ -643,8 +654,8 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
||||
return FillSuspend
|
||||
}
|
||||
|
||||
_screen.SetContent(xPos, yPos, rs[0], rs[1:], style)
|
||||
lx += runewidth.StringWidth(string(rs))
|
||||
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
|
||||
lx += util.StringWidth(string(rs))
|
||||
}
|
||||
w.lastX += lx
|
||||
if w.lastX == w.width {
|
||||
|
||||
@@ -93,6 +93,7 @@ const (
|
||||
Start
|
||||
Load
|
||||
Focus
|
||||
One
|
||||
|
||||
AltBS
|
||||
|
||||
@@ -525,28 +526,28 @@ func EmptyTheme() *ColorTheme {
|
||||
func NoColorTheme() *ColorTheme {
|
||||
return &ColorTheme{
|
||||
Colored: false,
|
||||
Input: ColorAttr{colDefault, AttrRegular},
|
||||
Fg: ColorAttr{colDefault, AttrRegular},
|
||||
Bg: ColorAttr{colDefault, AttrRegular},
|
||||
DarkBg: ColorAttr{colDefault, AttrRegular},
|
||||
Prompt: ColorAttr{colDefault, AttrRegular},
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
DarkBg: ColorAttr{colDefault, AttrUndefined},
|
||||
Prompt: ColorAttr{colDefault, AttrUndefined},
|
||||
Match: ColorAttr{colDefault, Underline},
|
||||
Current: ColorAttr{colDefault, Reverse},
|
||||
CurrentMatch: ColorAttr{colDefault, Reverse | Underline},
|
||||
Spinner: ColorAttr{colDefault, AttrRegular},
|
||||
Info: ColorAttr{colDefault, AttrRegular},
|
||||
Cursor: ColorAttr{colDefault, AttrRegular},
|
||||
Selected: ColorAttr{colDefault, AttrRegular},
|
||||
Header: ColorAttr{colDefault, AttrRegular},
|
||||
Border: ColorAttr{colDefault, AttrRegular},
|
||||
BorderLabel: ColorAttr{colDefault, AttrRegular},
|
||||
Disabled: ColorAttr{colDefault, AttrRegular},
|
||||
PreviewFg: ColorAttr{colDefault, AttrRegular},
|
||||
PreviewBg: ColorAttr{colDefault, AttrRegular},
|
||||
Gutter: ColorAttr{colDefault, AttrRegular},
|
||||
PreviewLabel: ColorAttr{colDefault, AttrRegular},
|
||||
Separator: ColorAttr{colDefault, AttrRegular},
|
||||
Scrollbar: ColorAttr{colDefault, AttrRegular},
|
||||
Spinner: ColorAttr{colDefault, AttrUndefined},
|
||||
Info: ColorAttr{colDefault, AttrUndefined},
|
||||
Cursor: ColorAttr{colDefault, AttrUndefined},
|
||||
Selected: ColorAttr{colDefault, AttrUndefined},
|
||||
Header: ColorAttr{colDefault, AttrUndefined},
|
||||
Border: ColorAttr{colDefault, AttrUndefined},
|
||||
BorderLabel: ColorAttr{colDefault, AttrUndefined},
|
||||
Disabled: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colDefault, AttrUndefined},
|
||||
Gutter: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colDefault, AttrUndefined},
|
||||
Separator: ColorAttr{colDefault, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colDefault, AttrUndefined},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,11 @@ import (
|
||||
"github.com/rivo/uniseg"
|
||||
)
|
||||
|
||||
// StringWidth returns string width where each CR/LF character takes 1 column
|
||||
func StringWidth(s string) int {
|
||||
return runewidth.StringWidth(s) + strings.Count(s, "\n") + strings.Count(s, "\r")
|
||||
}
|
||||
|
||||
// RunesWidth returns runes width
|
||||
func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) {
|
||||
width := 0
|
||||
@@ -22,8 +27,7 @@ func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int
|
||||
if len(rs) == 1 && rs[0] == '\t' {
|
||||
w = tabstop - (prefixWidth+width)%tabstop
|
||||
} else {
|
||||
s := string(rs)
|
||||
w = runewidth.StringWidth(s) + strings.Count(s, "\n")
|
||||
w = StringWidth(string(rs))
|
||||
}
|
||||
width += w
|
||||
if width > limit {
|
||||
@@ -41,7 +45,7 @@ func Truncate(input string, limit int) ([]rune, int) {
|
||||
gr := uniseg.NewGraphemes(input)
|
||||
for gr.Next() {
|
||||
rs := gr.Runes()
|
||||
w := runewidth.StringWidth(string(rs))
|
||||
w := StringWidth(string(rs))
|
||||
if width+w > limit {
|
||||
return runes, width
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestMin32(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContrain(t *testing.T) {
|
||||
func TestConstrain(t *testing.T) {
|
||||
if Constrain(-3, -1, 3) != -1 {
|
||||
t.Error("Expected", -1)
|
||||
}
|
||||
@@ -83,7 +83,7 @@ func TestContrain(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContrain32(t *testing.T) {
|
||||
func TestConstrain32(t *testing.T) {
|
||||
if Constrain32(-3, -1, 3) != -1 {
|
||||
t.Error("Expected", -1)
|
||||
}
|
||||
|
||||
@@ -1930,7 +1930,7 @@ class TestGoFZF < TestBase
|
||||
|
||||
def test_keep_right
|
||||
tmux.send_keys "seq 10000 | #{FZF} --read0 --keep-right", :Enter
|
||||
tmux.until { |lines| assert lines.any_include?('9999 10000') }
|
||||
tmux.until { |lines| assert lines.any_include?('9999␊10000') }
|
||||
end
|
||||
|
||||
def test_backward_eof
|
||||
@@ -2629,11 +2629,17 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_listen
|
||||
tmux.send_keys 'seq 10 | fzf --listen 6266', :Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.item_count }
|
||||
Net::HTTP.post(URI('http://localhost:6266'), 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ')
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
|
||||
{ '--listen 6266' => -> { URI('http://localhost:6266') },
|
||||
"--listen --sync --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'" =>
|
||||
-> { URI("http://localhost:#{File.read('/tmp/fzf-port').chomp}") } }.each do |opts, fn|
|
||||
tmux.send_keys "seq 10 | fzf #{opts}", :Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.item_count }
|
||||
Net::HTTP.post(fn.call, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ')
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
|
||||
teardown
|
||||
setup
|
||||
end
|
||||
end
|
||||
|
||||
def test_toggle_alternative_preview_window
|
||||
@@ -2656,6 +2662,67 @@ class TestGoFZF < TestBase
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal 99, lines.item_count }
|
||||
end
|
||||
|
||||
def test_no_extra_newline_issue_3209
|
||||
tmux.send_keys(%(seq 100 | #{FZF} --height 10 --preview-window up,wrap --preview 'printf "─%.0s" $(seq 1 "$((FZF_PREVIEW_COLUMNS - 5))"); printf $"\\e[7m%s\\e[0m" title; echo; echo something'), :Enter)
|
||||
expected = <<~OUTPUT
|
||||
╭──────────
|
||||
│ ─────────
|
||||
│ something
|
||||
│
|
||||
╰──────────
|
||||
3
|
||||
2
|
||||
> 1
|
||||
100/100 ─
|
||||
>
|
||||
OUTPUT
|
||||
tmux.until { assert_block(expected, _1) }
|
||||
end
|
||||
|
||||
def test_track
|
||||
tmux.send_keys "seq 1000 | #{FZF} --query 555 --track", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert_includes lines, '> 555'
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
index = tmux.until do |lines|
|
||||
assert_equal 28, lines.match_count
|
||||
assert_includes lines, '> 555'
|
||||
end.index('> 555')
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 271, lines.match_count
|
||||
assert_equal '> 555', lines[index]
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 1000, lines.match_count
|
||||
assert_equal '> 555', lines[index]
|
||||
end
|
||||
end
|
||||
|
||||
def test_one
|
||||
tmux.send_keys "seq 10 | #{FZF} --bind 'one:preview:echo {} is the only match'", :Enter
|
||||
tmux.send_keys '1'
|
||||
tmux.until do |lines|
|
||||
assert_equal 2, lines.match_count
|
||||
refute(lines.any? { _1.include?('only match') })
|
||||
end
|
||||
tmux.send_keys '0'
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert(lines.any? { _1.include?('only match') })
|
||||
end
|
||||
end
|
||||
|
||||
def test_height_range_with_exit_0
|
||||
tmux.send_keys "seq 10 | #{FZF} --height ~10% --exit-0", :Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.item_count }
|
||||
tmux.send_keys :c
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
end
|
||||
end
|
||||
|
||||
module TestShell
|
||||
@@ -2784,9 +2851,9 @@ module TestShell
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_equal '>', lines[-1] }
|
||||
tmux.send_keys 'foo bar'
|
||||
tmux.until { |lines| assert lines[-3]&.end_with?('bar"') }
|
||||
tmux.until { |lines| assert lines[-3]&.match?(/bar"␊?/) }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert lines[-1]&.end_with?('bar"') }
|
||||
tmux.until { |lines| assert lines[-1]&.match?(/bar"␊?/) }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
|
||||
end
|
||||
|
||||
6
typos.toml
Normal file
6
typos.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
# See https://github.com/crate-ci/typos/blob/master/docs/reference.md to configure typos
|
||||
[default.extend-words]
|
||||
ba = "ba"
|
||||
fo = "fo"
|
||||
enew = "enew"
|
||||
tabe = "tabe"
|
||||
Reference in New Issue
Block a user