mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-16 07:13:48 -05:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb76893e18 | ||
|
|
88d812fe82 | ||
|
|
77f9f4664a | ||
|
|
5c2f85c39e | ||
|
|
ac4d22cd12 | ||
|
|
cf95e44cb4 | ||
|
|
65dd2bb429 | ||
|
|
6be855be6a | ||
|
|
b6e3f4423b | ||
|
|
0c61d81713 | ||
|
|
7c6f5dba63 | ||
|
|
44cfc7e62a | ||
|
|
96670d5f16 | ||
|
|
36b971ee4e | ||
|
|
f1a9629652 | ||
|
|
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 | ||
|
|
352ea07226 | ||
|
|
27018787af | ||
|
|
4e305eca26 | ||
|
|
9e9c0ceaf4 | ||
|
|
b3bf18b1c0 | ||
|
|
b1619f675f | ||
|
|
96c3de12eb | ||
|
|
719dbb8bae | ||
|
|
f38a7f7f8f | ||
|
|
6ea38b4438 | ||
|
|
f7447aece1 | ||
|
|
aa2b9ec476 | ||
|
|
3ee00f8bc2 | ||
|
|
fccab60a5c | ||
|
|
0f4af38457 | ||
|
|
aef39f1160 |
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
|
||||
15
.github/workflows/winget.yml
vendored
Normal file
15
.github/workflows/winget.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: Publish to Winget
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: windows-latest # Action can only run on Windows
|
||||
steps:
|
||||
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||
with:
|
||||
identifier: junegunn.fzf
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
installers-regex: '-windows_(armv7|arm64|amd64)\.zip$'
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
@@ -74,6 +74,7 @@ builds:
|
||||
- arm64
|
||||
- loong64
|
||||
- ppc64le
|
||||
- s390x
|
||||
goarm:
|
||||
- 5
|
||||
- 6
|
||||
|
||||
@@ -1 +1 @@
|
||||
golang 1.19
|
||||
golang 1.20.2
|
||||
|
||||
144
ADVANCED.md
144
ADVANCED.md
@@ -1,30 +1,33 @@
|
||||
Advanced fzf examples
|
||||
======================
|
||||
|
||||
*(Last update: 2022/08/25)*
|
||||
* *Last update: 2023/02/15*
|
||||
* *Requires fzf 0.38.0 or above*
|
||||
|
||||
---
|
||||
|
||||
<!-- vim-markdown-toc GFM -->
|
||||
|
||||
* [Introduction](#introduction)
|
||||
* [Screen Layout](#screen-layout)
|
||||
* [`--height`](#--height)
|
||||
* [`fzf-tmux`](#fzf-tmux)
|
||||
* [Popup window support](#popup-window-support)
|
||||
* [`--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)
|
||||
* [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 interactive Ripgrep launcher](#using-fzf-as-interactive-ripgrep-launcher)
|
||||
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
|
||||
* [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode)
|
||||
* [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter)
|
||||
* [Using fzf as interactive Ripgrep launcher](#using-fzf-as-interactive-ripgrep-launcher)
|
||||
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
|
||||
* [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-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)
|
||||
* [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)
|
||||
* [Generating fzf color theme from Vim color schemes](#generating-fzf-color-theme-from-vim-color-schemes)
|
||||
|
||||
<!-- vim-markdown-toc -->
|
||||
|
||||
@@ -236,15 +239,13 @@ file called `rfv`.
|
||||
# 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]}"
|
||||
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' \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
And run it with an initial query string.
|
||||
@@ -307,8 +308,12 @@ I know it's a lot to digest, let's try to break down the code.
|
||||
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]}`).
|
||||
- Instead of using shell script to process the final output of fzf, we use
|
||||
`become(...)` action which was added in [fzf 0.38.0][0.38.0] to turn fzf
|
||||
into a new process that opens the file with `vim` (`vim {1}`) and move the
|
||||
cursor to the line (`+{2}`).
|
||||
|
||||
[0.38.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0380
|
||||
|
||||
### Using fzf as interactive Ripgrep launcher
|
||||
|
||||
@@ -331,16 +336,14 @@ projects, and it will free up memory as you narrow down the results.
|
||||
# 3. Open the file in Vim
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
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]}"
|
||||
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' \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||

|
||||
@@ -358,8 +361,6 @@ IFS=: read -ra selected < <(
|
||||
|
||||
### 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.
|
||||
@@ -375,19 +376,17 @@ fzf-only search mode by *"unbinding"* `reload` action from `change` event.
|
||||
# 3. Open the file in Vim
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
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]}"
|
||||
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' \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
* Phase 1. Filtering with Ripgrep
|
||||
@@ -408,10 +407,8 @@ IFS=: read -ra selected < <(
|
||||
|
||||
### Switching between Ripgrep mode and fzf mode
|
||||
|
||||
*(Requires fzf 0.30.0 or above)*
|
||||
|
||||
fzf 0.30.0 added `rebind` action so we can "rebind" the bindings that were
|
||||
previously "unbound" via `unbind`.
|
||||
[fzf 0.30.0][0.30.0] added `rebind` action so we can "rebind" the bindings
|
||||
that were previously "unbound" via `unbind`.
|
||||
|
||||
This is an improved version of the previous example that allows us to switch
|
||||
between Ripgrep launcher mode and fzf-only filtering mode via CTRL-R and
|
||||
@@ -421,25 +418,34 @@ CTRL-F.
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Switch between Ripgrep launcher mode (CTRL-R) and fzf filtering mode (CTRL-F)
|
||||
rm -f /tmp/rg-fzf-{r,f}
|
||||
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 "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+clear-query+rebind(ctrl-r)" \
|
||||
--bind "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || true)+rebind(change,ctrl-f)" \
|
||||
--prompt '1. Ripgrep> ' \
|
||||
--delimiter : \
|
||||
--header '╱ CTRL-R (Ripgrep mode) ╱ CTRL-F (fzf mode) ╱' \
|
||||
--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]}"
|
||||
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 "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+rebind(ctrl-r)+transform-query(echo {q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f)" \
|
||||
--bind "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || true)+rebind(change,ctrl-f)+transform-query(echo {q} > /tmp/rg-fzf-f; cat /tmp/rg-fzf-r)" \
|
||||
--bind "start:unbind(ctrl-r)" \
|
||||
--prompt '1. ripgrep> ' \
|
||||
--delimiter : \
|
||||
--header '╱ CTRL-R (ripgrep mode) ╱ CTRL-F (fzf mode) ╱' \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
- To restore the query string when switching between modes, we store the
|
||||
current query in `/tmp/rg-fzf-{r,f}` files and restore the query using
|
||||
`transform-query` action which was added in [fzf 0.36.0][0.36.0].
|
||||
- Also note that we unbind `ctrl-r` binding on `start` event which is
|
||||
triggered once when fzf starts.
|
||||
|
||||
[0.30.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0300
|
||||
[0.36.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0360
|
||||
|
||||
Log tailing
|
||||
-----------
|
||||
|
||||
|
||||
92
CHANGELOG.md
92
CHANGELOG.md
@@ -1,6 +1,96 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.40.0
|
||||
------
|
||||
- Added `zero` event that is triggered when there's no match
|
||||
```sh
|
||||
# Reload the candidate list when there's no match
|
||||
echo $RANDOM | fzf --bind 'zero:reload(echo $RANDOM)+clear-query' --height 3
|
||||
```
|
||||
- New actions
|
||||
- Added `track` action which makes fzf track the current item when the
|
||||
search result is updated. If the user manually moves the cursor, or the
|
||||
item is not in the updated search result, tracking is automatically
|
||||
disabled. Tracking is useful when you want to see the surrounding items
|
||||
by deleting the query string.
|
||||
```sh
|
||||
# Narrow down the list with a query, point to a command,
|
||||
# and hit CTRL-T to see its surrounding commands.
|
||||
export FZF_CTRL_R_OPTS="
|
||||
--preview 'echo {}' --preview-window up:3:hidden:wrap
|
||||
--bind 'ctrl-/:toggle-preview'
|
||||
--bind 'ctrl-t:track+clear-query'
|
||||
--bind 'ctrl-y:execute-silent(echo -n {2..} | pbcopy)+abort'
|
||||
--color header:italic
|
||||
--header 'Press CTRL-Y to copy command into clipboard'"
|
||||
```
|
||||
- Added `change-header(...)`
|
||||
- Added `transform-header(...)`
|
||||
- Added `toggle-track` action
|
||||
- Fixed `--track` behavior when used with `--tac`
|
||||
- However, using `--track` with `--tac` is not recommended. The resulting
|
||||
behavior can be very confusing.
|
||||
- Bug fixes and improvements
|
||||
|
||||
0.39.0
|
||||
------
|
||||
- Added `one` event that is triggered when there's only one match
|
||||
```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
|
||||
- `become(...)` - Replace the current fzf process with the specified
|
||||
command using `execve(2)` system call.
|
||||
See https://github.com/junegunn/fzf#turning-into-a-different-process for
|
||||
more information.
|
||||
```sh
|
||||
# Open selected files in Vim
|
||||
fzf --multi --bind 'enter:become(vim {+})'
|
||||
|
||||
# Open the file in Vim and go to the line
|
||||
git grep --line-number . |
|
||||
fzf --delimiter : --nth 3.. --bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
- This action is not supported on Windows
|
||||
- `show-preview`
|
||||
- `hide-preview`
|
||||
- Bug fixes
|
||||
- `--preview-window 0,hidden` should not execute the preview command until
|
||||
`toggle-preview` action is triggered
|
||||
|
||||
0.37.0
|
||||
------
|
||||
- Added a way to customize the separator of inline info
|
||||
@@ -118,7 +208,7 @@ CHANGELOG
|
||||
- Added color name `preview-label` for `--preview-label` (defaults to `label`
|
||||
for `--border-label`)
|
||||
- Better support for (Windows) terminals where each box-drawing character
|
||||
takes 2 columns. Set `RUNEWIDTH_EASTASIAN` environment variable to `1`.
|
||||
takes 2 columns. Set `RUNEWIDTH_EASTASIAN` environment variable to `0` or `1`.
|
||||
- On Vim, the variable will be automatically set if `&ambiwidth` is `double`
|
||||
- Behavior changes
|
||||
- fzf will always execute the preview command if the command template
|
||||
|
||||
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
|
||||
|
||||
79
README.md
79
README.md
@@ -54,14 +54,15 @@ Table of Contents
|
||||
* [Advanced topics](#advanced-topics)
|
||||
* [Performance](#performance)
|
||||
* [Executing external programs](#executing-external-programs)
|
||||
* [Turning into a different process](#turning-into-a-different-process)
|
||||
* [Reloading the candidate list](#reloading-the-candidate-list)
|
||||
* [1. Update the list of processes by pressing CTRL-R](#1-update-the-list-of-processes-by-pressing-ctrl-r)
|
||||
* [2. Switch between sources by pressing CTRL-D or CTRL-F](#2-switch-between-sources-by-pressing-ctrl-d-or-ctrl-f)
|
||||
* [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)
|
||||
|
||||
@@ -123,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` |
|
||||
|
||||
@@ -136,15 +138,17 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
|
||||
### Windows
|
||||
|
||||
Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also
|
||||
available via [Chocolatey][choco] and [Scoop][scoop]:
|
||||
available via [Chocolatey][choco], [Scoop][scoop], and [Winget][winget]:
|
||||
|
||||
| Package manager | Command |
|
||||
| --- | --- |
|
||||
| Chocolatey | `choco install fzf` |
|
||||
| Scoop | `scoop install fzf` |
|
||||
| Package manager | Command |
|
||||
| --- | --- |
|
||||
| Chocolatey | `choco install fzf` |
|
||||
| Scoop | `scoop install fzf` |
|
||||
| Winget | `winget install fzf` |
|
||||
|
||||
[choco]: https://chocolatey.org/packages/fzf
|
||||
[scoop]: https://github.com/ScoopInstaller/Main/blob/master/bucket/fzf.json
|
||||
[winget]: https://github.com/microsoft/winget-pkgs/tree/master/manifests/j/junegunn/fzf
|
||||
|
||||
Known issues and limitations on Windows can be found on [the wiki
|
||||
page][windows-wiki].
|
||||
@@ -202,6 +206,22 @@ files excluding hidden ones. (You can override the default command with
|
||||
vim $(fzf)
|
||||
```
|
||||
|
||||
> *:bulb: A more robust solution would be to use `xargs` but we've presented
|
||||
> the above as it's easier to grasp*
|
||||
> ```sh
|
||||
> fzf --print0 | xargs -0 -o vim
|
||||
> ```
|
||||
|
||||
>
|
||||
> *:bulb: fzf also has the ability to turn itself into a different process.*
|
||||
>
|
||||
> ```sh
|
||||
> fzf --bind 'enter:become(vim {})'
|
||||
> ```
|
||||
>
|
||||
> *See [Turning into a different process](#turning-into-a-different-process)
|
||||
> for more information.*
|
||||
|
||||
### Using the finder
|
||||
|
||||
- `CTRL-K` / `CTRL-J` (or `CTRL-P` / `CTRL-N`) to move cursor up and down
|
||||
@@ -560,6 +580,47 @@ fzf --bind 'f1:execute(less -f {}),ctrl-y:execute-silent(echo {} | pbcopy)+abort
|
||||
|
||||
See *KEY BINDINGS* section of the man page for details.
|
||||
|
||||
### Turning into a different process
|
||||
|
||||
`become(...)` is similar to `execute(...)`/`execute-silent(...)` described
|
||||
above, but instead of executing the command and coming back to fzf on
|
||||
complete, it turns fzf into a new process for the command.
|
||||
|
||||
```sh
|
||||
fzf --bind 'enter:become(vim {})'
|
||||
```
|
||||
|
||||
Compared to the seemingly equivalent command substitution `vim "$(fzf)"`, this
|
||||
approach has several advantages:
|
||||
|
||||
* Vim will not open an empty file when you terminate fzf with
|
||||
<kbd>CTRL-C</kbd>
|
||||
* Vim will not open an empty file when you press <kbd>ENTER</kbd> on an empty
|
||||
result
|
||||
* Can handle multiple selections even when they have whitespaces
|
||||
```sh
|
||||
fzf --multi --bind 'enter:become(vim {+})'
|
||||
```
|
||||
|
||||
To be fair, running `fzf --print0 | xargs -0 -o vim` instead of `vim "$(fzf)"`
|
||||
resolves all of the issues mentioned. Nonetheless, `become(...)` still offers
|
||||
additional benefits in different scenarios.
|
||||
|
||||
* You can set up multiple bindings to handle the result in different ways
|
||||
without any wrapping script
|
||||
```sh
|
||||
fzf --bind 'enter:become(vim {}),ctrl-e:become(emacs {})'
|
||||
```
|
||||
* Previously, you would have to use `--expect=ctrl-e` and check the first
|
||||
line of the output of fzf
|
||||
* You can easily build the subsequent command using the field index
|
||||
expressions of fzf
|
||||
```sh
|
||||
# Open the file in Vim and go to the line
|
||||
git grep --line-number . |
|
||||
fzf --delimiter : --nth 3.. --bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
### Reloading the candidate list
|
||||
|
||||
By binding `reload` action to a key or an event, you can make fzf dynamically
|
||||
@@ -663,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
|
||||
@@ -692,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
|
||||
|
||||
10
bin/fzf-tmux
10
bin/fzf-tmux
@@ -179,11 +179,15 @@ trap 'cleanup' EXIT
|
||||
|
||||
envs="export TERM=$TERM "
|
||||
if [[ "$opt" =~ "-E" ]]; then
|
||||
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
|
||||
tmux_version=$(tmux -V)
|
||||
if [[ ! $tmux_version =~ 3\.2 ]]; then
|
||||
tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
|
||||
if [[ $(awk '{print ($1 > 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version > 3.2") = 1 ]]; then
|
||||
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
|
||||
opt="-B $opt"
|
||||
elif [[ $tmux_version = 3.2 ]]; then
|
||||
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.7.0
|
||||
golang.org/x/term v0.7.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.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.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.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
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.37.0
|
||||
version=0.40.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.37.0"
|
||||
$version="0.40.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.37"
|
||||
var version string = "0.40"
|
||||
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 "Jan 2023" "fzf 0.37.0" "fzf-tmux - open fzf in tmux split pane"
|
||||
.TH fzf-tmux 1 "May 2023" "fzf 0.40.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 "Jan 2023" "fzf 0.37.0" "fzf - a command-line fuzzy finder"
|
||||
.TH fzf 1 "May 2023" "fzf 0.40.0" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
@@ -92,6 +92,19 @@ 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. It is
|
||||
not recommended to use this option with \fB--tac\fR as the resulting behavior
|
||||
can be confusing. Also, consider using \fBtrack\fR action instead of this
|
||||
option.
|
||||
|
||||
.RS
|
||||
e.g.
|
||||
\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
|
||||
|
||||
@@ -231,8 +244,9 @@ Draw border around the finder
|
||||
.br
|
||||
|
||||
If you use a terminal emulator where each box-drawing character takes
|
||||
2 columns, try setting \fBRUNEWIDTH_EASTASIAN\fR to \fB1\fR. If the border is
|
||||
still not properly rendered, set \fB--no-unicode\fR.
|
||||
2 columns, try setting \fBRUNEWIDTH_EASTASIAN\fR environment variable to
|
||||
\fB0\fR or \fB1\fR. If the border is still not properly rendered, set
|
||||
\fB--no-unicode\fR.
|
||||
|
||||
.TP
|
||||
.BI "--border-label" [=LABEL]
|
||||
@@ -738,9 +752,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 +765,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 +997,28 @@ 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
|
||||
\fIzero\fR
|
||||
.RS
|
||||
Triggered when there's no match. \fBzero:abort\fR binding is comparable to
|
||||
\fB--exit-0\fR option, but the difference is that \fB--exit-0\fR is only
|
||||
effective before the interactive finder starts but \fBzero\fR event is
|
||||
triggered by the interactive finder.
|
||||
|
||||
e.g.
|
||||
\fB# Reload the candidate list when there's no match
|
||||
echo $RANDOM | fzf --bind 'zero:reload(echo $RANDOM)+clear-query' --height 3\fR
|
||||
.RE
|
||||
|
||||
\fIbackward-eof\fR
|
||||
.RS
|
||||
@@ -999,9 +1041,11 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty)
|
||||
\fBbackward-kill-word\fR \fIalt-bs\fR
|
||||
\fBbackward-word\fR \fIalt-b shift-left\fR
|
||||
\fBbecome(...)\fR (replace fzf process with the specified command; see below for the details)
|
||||
\fBbeginning-of-line\fR \fIctrl-a home\fR
|
||||
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
|
||||
\fBchange-border-label(...)\fR (change \fB--border-label\fR to the given string)
|
||||
\fBchange-header(...)\fR (change header to the given string; doesn't affect \fB--header-lines\fR)
|
||||
\fBchange-preview(...)\fR (change \fB--preview\fR option)
|
||||
\fBchange-preview-label(...)\fR (change \fB--preview-label\fR to the given string)
|
||||
\fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|')
|
||||
@@ -1036,6 +1080,7 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBpage-up\fR \fIpgup\fR
|
||||
\fBhalf-page-down\fR
|
||||
\fBhalf-page-up\fR
|
||||
\fBhide-preview\fR
|
||||
\fBpos(...)\fR (move cursor to the numeric position; negative number to count from the end)
|
||||
\fBprev-history\fR (\fIctrl-p\fR on \fB--history\fR)
|
||||
\fBprev-selected\fR (move to the previous selected item)
|
||||
@@ -1058,6 +1103,7 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBreplace-query\fR (replace query string with the current selection)
|
||||
\fBselect\fR
|
||||
\fBselect-all\fR (select all matches)
|
||||
\fBshow-preview\fR
|
||||
\fBtoggle\fR (\fIright-click\fR)
|
||||
\fBtoggle-all\fR (toggle all matches)
|
||||
\fBtoggle+down\fR \fIctrl-i (tab)\fR
|
||||
@@ -1067,8 +1113,11 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBtoggle-preview-wrap\fR
|
||||
\fBtoggle-search\fR (toggle search functionality)
|
||||
\fBtoggle-sort\fR
|
||||
\fBtoggle-track\fR
|
||||
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
|
||||
\fBtrack\fR (track the current item; automatically disabled if focus changes)
|
||||
\fBtransform-border-label(...)\fR (transform border label using an external command)
|
||||
\fBtransform-header(...)\fR (transform header using an external command)
|
||||
\fBtransform-preview-label(...)\fR (transform preview label using an external command)
|
||||
\fBtransform-prompt(...)\fR (transform prompt string using an external command)
|
||||
\fBtransform-query(...)\fR (transform query string using an external command)
|
||||
@@ -1141,6 +1190,14 @@ 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.
|
||||
|
||||
\fBbecome(...)\fR action is similar to \fBexecute(...)\fR, but it replaces the
|
||||
current fzf process with the specified command using \fBexecve(2)\fR system
|
||||
call.
|
||||
|
||||
\fBfzf --bind "enter:become(vim {})"\fR
|
||||
|
||||
\fBbecome(...)\fR is not supported on Windows.
|
||||
|
||||
.SS RELOAD INPUT
|
||||
|
||||
\fBreload(...)\fR action is used to dynamically update the input list
|
||||
|
||||
@@ -164,7 +164,7 @@ function s:get_version(bin)
|
||||
if has_key(s:versions, a:bin)
|
||||
return s:versions[a:bin]
|
||||
end
|
||||
let command = (&shell =~ 'powershell' ? '&' : '') . s:fzf_call('shellescape', a:bin) . ' --version --no-height'
|
||||
let command = (&shell =~ 'powershell\|pwsh' ? '&' : '') . s:fzf_call('shellescape', a:bin) . ' --version --no-height'
|
||||
let output = systemlist(command)
|
||||
if v:shell_error || empty(output)
|
||||
return ''
|
||||
@@ -512,9 +512,7 @@ try
|
||||
let optstr .= ' --height='.height
|
||||
endif
|
||||
" Respect --border option given in 'options'
|
||||
if stridx(optstr, '--border') < 0 && stridx(optstr, '--no-border') < 0
|
||||
let optstr .= s:border_opt(get(dict, 'window', 0))
|
||||
endif
|
||||
let optstr = join([s:border_opt(get(dict, 'window', 0)), optstr])
|
||||
let prev_default_command = $FZF_DEFAULT_COMMAND
|
||||
if len(source_command)
|
||||
let $FZF_DEFAULT_COMMAND = source_command
|
||||
@@ -741,7 +739,7 @@ function! s:calc_size(max, val, dict)
|
||||
return size
|
||||
endif
|
||||
let margin = match(opts, '--inline-info\|--info[^-]\{-}inline') > match(opts, '--no-inline-info\|--info[^-]\{-}\(default\|hidden\)') ? 1 : 2
|
||||
let margin += stridx(opts, '--border') > stridx(opts, '--no-border') ? 2 : 0
|
||||
let margin += match(opts, '--border\([^-]\|$\)') > match(opts, '--no-border\([^-]\|$\)') ? 2 : 0
|
||||
if stridx(opts, '--header') > stridx(opts, '--no-header')
|
||||
let margin += len(split(opts, "\n"))
|
||||
endif
|
||||
|
||||
@@ -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
|
||||
@@ -373,7 +374,7 @@ _fzf_setup_completion() {
|
||||
}
|
||||
|
||||
# Environment variables / Aliases / Hosts / Process
|
||||
_fzf_setup_completion 'var' export unset
|
||||
_fzf_setup_completion 'var' export unset printenv
|
||||
_fzf_setup_completion 'alias' unalias
|
||||
_fzf_setup_completion 'host' ssh telnet
|
||||
_fzf_setup_completion 'proc' kill
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
12
src/core.go
12
src/core.go
@@ -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}
|
||||
}
|
||||
@@ -298,10 +299,12 @@ func Run(opts *Options, version string, revision string) {
|
||||
|
||||
case EvtSearchNew:
|
||||
var command *string
|
||||
var changed bool
|
||||
switch val := value.(type) {
|
||||
case searchRequest:
|
||||
sort = val.sort
|
||||
command = val.command
|
||||
changed = val.changed
|
||||
if command != nil {
|
||||
useSnapshot = val.sync
|
||||
}
|
||||
@@ -313,10 +316,17 @@ func Run(opts *Options, version string, revision string) {
|
||||
} else {
|
||||
restart(*command)
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
break
|
||||
}
|
||||
if !useSnapshot {
|
||||
snapshot, _ = chunkList.Snapshot()
|
||||
newSnapshot, _ := chunkList.Snapshot()
|
||||
// We want to avoid showing empty list when reload is triggered
|
||||
// and the query string is changed at the same time i.e. command != nil && changed
|
||||
if command == nil || len(newSnapshot) > 0 {
|
||||
snapshot = newSnapshot
|
||||
}
|
||||
}
|
||||
reset := !useSnapshot && clearCache()
|
||||
matcher.Reset(snapshot, input(reset), true, !reading, sort, reset)
|
||||
|
||||
@@ -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,32 @@ func (mg *Merger) Length() int {
|
||||
return mg.count
|
||||
}
|
||||
|
||||
func (mg *Merger) First() Result {
|
||||
if mg.tac && !mg.sorted {
|
||||
return mg.Get(mg.count - 1)
|
||||
}
|
||||
return mg.Get(0)
|
||||
}
|
||||
|
||||
// FindIndex returns the index of the item with the given item index
|
||||
func (mg *Merger) FindIndex(itemIndex int32) int {
|
||||
index := -1
|
||||
if mg.pass {
|
||||
index = int(itemIndex)
|
||||
if mg.tac {
|
||||
index = mg.count - index - 1
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < mg.count; i++ {
|
||||
if mg.Get(i).item.Index() == itemIndex {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
// Get returns the pointer to the Result object indexed by the given integer
|
||||
func (mg *Merger) Get(idx int) Result {
|
||||
if mg.chunks != nil {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/junegunn/fzf/src/algo"
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/mattn/go-shellwords"
|
||||
@@ -32,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
|
||||
@@ -115,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
|
||||
@@ -163,6 +165,14 @@ func defaultMargin() [4]sizeSpec {
|
||||
return [4]sizeSpec{}
|
||||
}
|
||||
|
||||
type trackOption int
|
||||
|
||||
const (
|
||||
trackDisabled trackOption = iota
|
||||
trackEnabled
|
||||
trackCurrent
|
||||
)
|
||||
|
||||
type windowPosition int
|
||||
|
||||
const (
|
||||
@@ -265,6 +275,7 @@ type Options struct {
|
||||
WithNth []Range
|
||||
Delimiter Delimiter
|
||||
Sort int
|
||||
Track trackOption
|
||||
Tac bool
|
||||
Criteria []criterion
|
||||
Multi int
|
||||
@@ -315,7 +326,7 @@ type Options struct {
|
||||
PreviewLabel labelOpts
|
||||
Unicode bool
|
||||
Tabstop int
|
||||
ListenPort int
|
||||
ListenPort *int
|
||||
ClearOnExit bool
|
||||
Version bool
|
||||
}
|
||||
@@ -337,6 +348,7 @@ func defaultOptions() *Options {
|
||||
WithNth: make([]Range, 0),
|
||||
Delimiter: Delimiter{},
|
||||
Sort: 1000,
|
||||
Track: trackDisabled,
|
||||
Tac: false,
|
||||
Criteria: []criterion{byScore, byLength},
|
||||
Multi: 0,
|
||||
@@ -618,6 +630,10 @@ 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 "zero":
|
||||
add(tui.Zero)
|
||||
case "alt-enter", "alt-return":
|
||||
chords[tui.CtrlAltKey('m')] = key
|
||||
case "alt-space":
|
||||
@@ -921,7 +937,7 @@ const (
|
||||
|
||||
func init() {
|
||||
executeRegexp = regexp.MustCompile(
|
||||
`(?si)[:+](execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`)
|
||||
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`)
|
||||
splitRegexp = regexp.MustCompile("[,:]+")
|
||||
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
|
||||
}
|
||||
@@ -1077,6 +1093,10 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
appendAction(actToggleAll)
|
||||
case "toggle-search":
|
||||
appendAction(actToggleSearch)
|
||||
case "toggle-track":
|
||||
appendAction(actToggleTrack)
|
||||
case "track":
|
||||
appendAction(actTrack)
|
||||
case "select":
|
||||
appendAction(actSelect)
|
||||
case "select-all":
|
||||
@@ -1111,6 +1131,10 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
appendAction(actPrevSelected)
|
||||
case "next-selected":
|
||||
appendAction(actNextSelected)
|
||||
case "show-preview":
|
||||
appendAction(actShowPreview)
|
||||
case "hide-preview":
|
||||
appendAction(actHidePreview)
|
||||
case "toggle-preview":
|
||||
appendAction(actTogglePreview)
|
||||
case "toggle-preview-wrap":
|
||||
@@ -1167,6 +1191,10 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
actions = append(actions, &action{t: t, a: actionArg})
|
||||
}
|
||||
switch t {
|
||||
case actBecome:
|
||||
if util.IsWindows() {
|
||||
exit("become action is not supported on Windows")
|
||||
}
|
||||
case actUnbind, actRebind:
|
||||
parseKeyChordsImpl(actionArg, spec[0:offset]+" target required", exit)
|
||||
case actChangePreviewWindow:
|
||||
@@ -1219,6 +1247,8 @@ func isExecuteAction(str string) actionType {
|
||||
|
||||
prefix := actionNameRegexp.FindString(str)
|
||||
switch prefix {
|
||||
case "become":
|
||||
return actBecome
|
||||
case "reload":
|
||||
return actReload
|
||||
case "reload-sync":
|
||||
@@ -1231,6 +1261,8 @@ func isExecuteAction(str string) actionType {
|
||||
return actPreview
|
||||
case "change-border-label":
|
||||
return actChangeBorderLabel
|
||||
case "change-header":
|
||||
return actChangeHeader
|
||||
case "change-preview-label":
|
||||
return actChangePreviewLabel
|
||||
case "change-preview-window":
|
||||
@@ -1255,6 +1287,8 @@ func isExecuteAction(str string) actionType {
|
||||
return actTransformBorderLabel
|
||||
case "transform-preview-label":
|
||||
return actTransformPreviewLabel
|
||||
case "transform-header":
|
||||
return actTransformHeader
|
||||
case "transform-prompt":
|
||||
return actTransformPrompt
|
||||
case "transform-query":
|
||||
@@ -1551,6 +1585,10 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
opts.Sort = optionalNumeric(allArgs, &i, 1)
|
||||
case "+s", "--no-sort":
|
||||
opts.Sort = 0
|
||||
case "--track":
|
||||
opts.Track = trackEnabled
|
||||
case "--no-track":
|
||||
opts.Track = trackDisabled
|
||||
case "--tac":
|
||||
opts.Tac = true
|
||||
case "--no-tac":
|
||||
@@ -1745,9 +1783,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":
|
||||
@@ -1838,7 +1877,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 {
|
||||
@@ -1868,7 +1908,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")
|
||||
}
|
||||
|
||||
@@ -1990,9 +2030,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
|
||||
|
||||
293
src/terminal.go
293
src/terminal.go
@@ -3,9 +3,11 @@ package fzf
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"regexp"
|
||||
"sort"
|
||||
@@ -104,7 +106,6 @@ type previewer struct {
|
||||
version int64
|
||||
lines []string
|
||||
offset int
|
||||
enabled bool
|
||||
scrollable bool
|
||||
final bool
|
||||
following resumableState
|
||||
@@ -183,6 +184,7 @@ type Terminal struct {
|
||||
multi int
|
||||
sort bool
|
||||
toggleSort bool
|
||||
track trackOption
|
||||
delimiter Delimiter
|
||||
expect map[tui.Event]string
|
||||
keymap map[tui.Event][]*action
|
||||
@@ -201,9 +203,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
|
||||
@@ -310,6 +311,7 @@ const (
|
||||
actBackwardWord
|
||||
actCancel
|
||||
actChangeBorderLabel
|
||||
actChangeHeader
|
||||
actChangePreviewLabel
|
||||
actChangePrompt
|
||||
actChangeQuery
|
||||
@@ -337,6 +339,8 @@ const (
|
||||
actToggleUp
|
||||
actToggleIn
|
||||
actToggleOut
|
||||
actToggleTrack
|
||||
actTrack
|
||||
actDown
|
||||
actUp
|
||||
actPageUp
|
||||
@@ -350,9 +354,12 @@ const (
|
||||
actRefreshPreview
|
||||
actReplaceQuery
|
||||
actToggleSort
|
||||
actShowPreview
|
||||
actHidePreview
|
||||
actTogglePreview
|
||||
actTogglePreviewWrap
|
||||
actTransformBorderLabel
|
||||
actTransformHeader
|
||||
actTransformPreviewLabel
|
||||
actTransformPrompt
|
||||
actTransformQuery
|
||||
@@ -386,6 +393,7 @@ const (
|
||||
actDeselect
|
||||
actUnbind
|
||||
actRebind
|
||||
actBecome
|
||||
)
|
||||
|
||||
type placeholderFlags struct {
|
||||
@@ -400,6 +408,7 @@ type searchRequest struct {
|
||||
sort bool
|
||||
sync bool
|
||||
command *string
|
||||
changed bool
|
||||
}
|
||||
|
||||
type previewRequest struct {
|
||||
@@ -535,13 +544,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 {
|
||||
@@ -601,6 +606,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,
|
||||
@@ -620,11 +626,10 @@ 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,
|
||||
header: header,
|
||||
header: []string{},
|
||||
header0: header,
|
||||
ellipsis: opts.Ellipsis,
|
||||
ansi: opts.Ansi,
|
||||
@@ -643,7 +648,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
reqBox: util.NewEventBox(),
|
||||
initialPreviewOpts: opts.Preview,
|
||||
previewOpts: opts.Preview,
|
||||
previewer: previewer{0, []string{}, 0, len(opts.Preview.command) > 0, false, true, disabledState, "", []bool{}},
|
||||
previewer: previewer{0, []string{}, 0, false, true, disabledState, "", []bool{}},
|
||||
previewed: previewed{0, 0, 0, false},
|
||||
previewBox: previewBox,
|
||||
eventBox: eventBox,
|
||||
@@ -691,13 +696,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:
|
||||
@@ -737,7 +754,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
|
||||
}
|
||||
@@ -871,10 +888,21 @@ func reverseStringArray(input []string) []string {
|
||||
return reversed
|
||||
}
|
||||
|
||||
func (t *Terminal) changeHeader(header string) bool {
|
||||
lines := strings.Split(strings.TrimSuffix(header, "\n"), "\n")
|
||||
switch t.layout {
|
||||
case layoutDefault, layoutReverseList:
|
||||
lines = reverseStringArray(lines)
|
||||
}
|
||||
needFullRedraw := len(t.header0) != len(lines)
|
||||
t.header0 = lines
|
||||
return needFullRedraw
|
||||
}
|
||||
|
||||
// UpdateHeader updates the header
|
||||
func (t *Terminal) UpdateHeader(header []string) {
|
||||
t.mutex.Lock()
|
||||
t.header = append(append([]string{}, t.header0...), header...)
|
||||
t.header = header
|
||||
t.mutex.Unlock()
|
||||
t.reqBox.Set(reqHeader, nil)
|
||||
}
|
||||
@@ -895,16 +923,54 @@ 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 != trackDisabled {
|
||||
if t.merger.Length() > 0 {
|
||||
prevIndex = t.merger.Get(t.cy).item.Index()
|
||||
} else if merger.Length() > 0 {
|
||||
prevIndex = merger.First().item.Index()
|
||||
}
|
||||
}
|
||||
t.progress = 100
|
||||
t.merger = merger
|
||||
if reset {
|
||||
t.selected = make(map[int32]selectedItem)
|
||||
t.version++
|
||||
}
|
||||
if t.hasLoadActions && t.triggerLoad {
|
||||
if t.triggerLoad {
|
||||
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.track == trackCurrent {
|
||||
t.track = trackDisabled
|
||||
t.cy = pos
|
||||
t.offset = 0
|
||||
} else if t.cy > count {
|
||||
// Try to keep the vertical position when the list shrinks
|
||||
t.cy = count - util.Min(count, t.maxItems()) + pos
|
||||
}
|
||||
}
|
||||
if !t.reading {
|
||||
switch t.merger.Length() {
|
||||
case 0:
|
||||
zero := tui.Zero.AsEvent()
|
||||
if _, prs := t.keymap[zero]; prs {
|
||||
t.eventChan <- zero
|
||||
}
|
||||
case 1:
|
||||
one := tui.One.AsEvent()
|
||||
if _, prs := t.keymap[one]; prs {
|
||||
t.eventChan <- one
|
||||
}
|
||||
}
|
||||
}
|
||||
t.mutex.Unlock()
|
||||
t.reqBox.Set(reqInfo, nil)
|
||||
t.reqBox.Set(reqList, nil)
|
||||
@@ -1032,7 +1098,7 @@ func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) {
|
||||
if t.noInfoLine() {
|
||||
minAreaHeight -= 1
|
||||
}
|
||||
if t.mayNeedPreviewWindow() {
|
||||
if t.needPreviewWindow() {
|
||||
minPreviewHeight := 1 + borderLines(t.previewOpts.border)
|
||||
minPreviewWidth := 5
|
||||
switch t.previewOpts.position {
|
||||
@@ -1115,7 +1181,7 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
|
||||
|
||||
// Set up preview window
|
||||
noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
|
||||
if t.mayNeedPreviewWindow() {
|
||||
if forcePreview || t.needPreviewWindow() {
|
||||
var resizePreviewWindows func(previewOpts *previewOpts)
|
||||
resizePreviewWindows = func(previewOpts *previewOpts) {
|
||||
t.activePreviewOpts = previewOpts
|
||||
@@ -1244,6 +1310,8 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
|
||||
}
|
||||
}
|
||||
resizePreviewWindows(&t.previewOpts)
|
||||
} else {
|
||||
t.activePreviewOpts = &t.previewOpts
|
||||
}
|
||||
|
||||
// Without preview window
|
||||
@@ -1305,7 +1373,7 @@ func (t *Terminal) move(y int, x int, clear bool) {
|
||||
case layoutDefault:
|
||||
y = h - y - 1
|
||||
case layoutReverseList:
|
||||
n := 2 + len(t.header)
|
||||
n := 2 + len(t.header0) + len(t.header)
|
||||
if t.noInfoLine() {
|
||||
n--
|
||||
}
|
||||
@@ -1333,8 +1401,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)
|
||||
@@ -1399,7 +1466,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)
|
||||
@@ -1426,6 +1493,9 @@ func (t *Terminal) printInfo() {
|
||||
output += " -S"
|
||||
}
|
||||
}
|
||||
if t.track != trackDisabled {
|
||||
output += " +T"
|
||||
}
|
||||
if t.multi > 0 {
|
||||
if t.multi == maxMulti {
|
||||
output += fmt.Sprintf(" (%d)", len(t.selected))
|
||||
@@ -1451,7 +1521,7 @@ func (t *Terminal) printInfo() {
|
||||
}
|
||||
|
||||
func (t *Terminal) printHeader() {
|
||||
if len(t.header) == 0 {
|
||||
if len(t.header0)+len(t.header) == 0 {
|
||||
return
|
||||
}
|
||||
max := t.window.Height()
|
||||
@@ -1462,7 +1532,7 @@ func (t *Terminal) printHeader() {
|
||||
}
|
||||
}
|
||||
var state *ansiState
|
||||
for idx, lineStr := range t.header {
|
||||
for idx, lineStr := range append(append([]string{}, t.header0...), t.header...) {
|
||||
line := idx
|
||||
if !t.headerFirst {
|
||||
line++
|
||||
@@ -1496,7 +1566,7 @@ func (t *Terminal) printList() {
|
||||
if t.layout == layoutDefault {
|
||||
i = maxy - 1 - j
|
||||
}
|
||||
line := i + 2 + len(t.header)
|
||||
line := i + 2 + len(t.header0) + len(t.header)
|
||||
if t.noInfoLine() {
|
||||
line--
|
||||
}
|
||||
@@ -1802,7 +1872,7 @@ func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unc
|
||||
if ansi != nil {
|
||||
ansi.lbg = -1
|
||||
}
|
||||
line = strings.TrimSuffix(line, "\n")
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
if lineNo >= height || t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
|
||||
t.previewed.filled = true
|
||||
t.previewer.scrollable = true
|
||||
@@ -1824,12 +1894,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)
|
||||
@@ -1932,7 +2004,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
|
||||
@@ -2232,16 +2304,17 @@ func (t *Terminal) redraw() {
|
||||
t.printAll()
|
||||
}
|
||||
|
||||
func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, captureFirstLine bool) string {
|
||||
func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, capture bool, firstLineOnly bool) string {
|
||||
line := ""
|
||||
valid, list := t.buildPlusList(template, forcePlus, false)
|
||||
// captureFirstLine is used for transform-{prompt,query} and we don't want to
|
||||
valid, list := t.buildPlusList(template, forcePlus)
|
||||
// 'capture' is used for transform-* and we don't want to
|
||||
// return an empty string in those cases
|
||||
if !valid && !captureFirstLine {
|
||||
if !valid && !capture {
|
||||
return line
|
||||
}
|
||||
command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
|
||||
cmd := util.ExecCommand(command, false)
|
||||
cmd.Env = t.environ()
|
||||
t.executing.Set(true)
|
||||
if !background {
|
||||
cmd.Stdin = tui.TtyIn()
|
||||
@@ -2253,12 +2326,17 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
|
||||
t.redraw()
|
||||
t.refresh()
|
||||
} else {
|
||||
if captureFirstLine {
|
||||
if capture {
|
||||
out, _ := cmd.StdoutPipe()
|
||||
reader := bufio.NewReader(out)
|
||||
cmd.Start()
|
||||
line, _ = reader.ReadString('\n')
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
if firstLineOnly {
|
||||
line, _ = reader.ReadString('\n')
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
} else {
|
||||
bytes, _ := io.ReadAll(reader)
|
||||
line = string(bytes)
|
||||
}
|
||||
cmd.Wait()
|
||||
} else {
|
||||
cmd.Run()
|
||||
@@ -2273,13 +2351,13 @@ func (t *Terminal) hasPreviewer() bool {
|
||||
return t.previewBox != nil
|
||||
}
|
||||
|
||||
func (t *Terminal) mayNeedPreviewWindow() bool {
|
||||
return t.hasPreviewer() && t.previewer.enabled && t.previewOpts.Visible()
|
||||
func (t *Terminal) needPreviewWindow() bool {
|
||||
return t.hasPreviewer() && len(t.previewOpts.command) > 0 && t.previewOpts.Visible()
|
||||
}
|
||||
|
||||
// Check if previewer is currently in action (invisible previewer with size 0 or visible previewer)
|
||||
func (t *Terminal) isPreviewEnabled() bool {
|
||||
return t.hasPreviewer() && t.previewer.enabled && (!t.previewOpts.Visible() || t.pwindow != nil)
|
||||
func (t *Terminal) canPreview() bool {
|
||||
return t.hasPreviewer() && (!t.previewOpts.Visible() && !t.previewOpts.hidden || t.hasPreviewWindow())
|
||||
}
|
||||
|
||||
func (t *Terminal) hasPreviewWindow() bool {
|
||||
@@ -2294,10 +2372,10 @@ func (t *Terminal) currentItem() *Item {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Terminal) buildPlusList(template string, forcePlus bool, forceEvaluation bool) (bool, []*Item) {
|
||||
func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item) {
|
||||
current := t.currentItem()
|
||||
slot, plus, query := hasPreviewFlags(template)
|
||||
if !forceEvaluation && !(!slot || query || (forcePlus || plus) && len(t.selected) > 0) {
|
||||
if !(!slot || query || (forcePlus || plus) && len(t.selected) > 0) {
|
||||
return current != nil, []*Item{current, current}
|
||||
}
|
||||
|
||||
@@ -2387,7 +2465,7 @@ func (t *Terminal) Loop() {
|
||||
pad := fitpad.pad
|
||||
t.tui.Resize(func(termHeight int) int {
|
||||
contentHeight := fit + t.extraLines()
|
||||
if t.mayNeedPreviewWindow() {
|
||||
if t.needPreviewWindow() {
|
||||
if t.previewOpts.aboveOrBelow() {
|
||||
if t.previewOpts.size.percent {
|
||||
newContentHeight := int(float64(contentHeight) * 100. / (100. - t.previewOpts.size.size))
|
||||
@@ -2488,17 +2566,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
|
||||
@@ -2621,8 +2699,8 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
|
||||
refreshPreview := func(command string) {
|
||||
if len(command) > 0 && t.isPreviewEnabled() {
|
||||
_, list := t.buildPlusList(command, false, false)
|
||||
if len(command) > 0 && t.canPreview() {
|
||||
_, list := t.buildPlusList(command, false)
|
||||
t.cancelPreview()
|
||||
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.evaluateScrollOffset(), list})
|
||||
}
|
||||
@@ -2669,6 +2747,10 @@ func (t *Terminal) Loop() {
|
||||
currentIndex = currentItem.Index()
|
||||
}
|
||||
focusChanged := focusedIndex != currentIndex
|
||||
if focusChanged && t.track == trackCurrent {
|
||||
t.track = trackDisabled
|
||||
t.printInfo()
|
||||
}
|
||||
if onFocus != nil && focusChanged {
|
||||
t.serverChan <- onFocus
|
||||
}
|
||||
@@ -2696,7 +2778,7 @@ func (t *Terminal) Loop() {
|
||||
case reqFullRedraw:
|
||||
wasHidden := t.pwindow == nil
|
||||
t.redraw()
|
||||
if wasHidden && t.pwindow != nil {
|
||||
if wasHidden && t.hasPreviewWindow() {
|
||||
refreshPreview(t.previewOpts.command)
|
||||
}
|
||||
case reqClose:
|
||||
@@ -2781,7 +2863,7 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
select {
|
||||
case event = <-t.eventChan:
|
||||
needBarrier = event != tui.Load.AsEvent()
|
||||
needBarrier = !event.Is(tui.Load, tui.One, tui.Zero)
|
||||
case actions = <-t.serverChan:
|
||||
event = tui.Invalid.AsEvent()
|
||||
needBarrier = false
|
||||
@@ -2857,23 +2939,46 @@ func (t *Terminal) Loop() {
|
||||
doAction = func(a *action) bool {
|
||||
switch a.t {
|
||||
case actIgnore:
|
||||
case actBecome:
|
||||
valid, list := t.buildPlusList(a.a, false)
|
||||
if valid {
|
||||
command := t.replacePlaceholder(a.a, false, string(t.input), list)
|
||||
shell := os.Getenv("SHELL")
|
||||
if len(shell) == 0 {
|
||||
shell = "sh"
|
||||
}
|
||||
shellPath, err := exec.LookPath(shell)
|
||||
if err == nil {
|
||||
t.tui.Close()
|
||||
if t.history != nil {
|
||||
t.history.append(string(t.input))
|
||||
}
|
||||
util.SetStdin(tui.TtyIn())
|
||||
syscall.Exec(shellPath, []string{shell, "-c", command}, os.Environ())
|
||||
}
|
||||
}
|
||||
case actExecute, actExecuteSilent:
|
||||
t.executeCommand(a.a, false, a.t == actExecuteSilent, false)
|
||||
t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false)
|
||||
case actExecuteMulti:
|
||||
t.executeCommand(a.a, true, false, false)
|
||||
t.executeCommand(a.a, true, false, false, false)
|
||||
case actInvalid:
|
||||
t.mutex.Unlock()
|
||||
return false
|
||||
case actTogglePreview:
|
||||
if t.hasPreviewer() {
|
||||
if t.activePreviewOpts != nil {
|
||||
t.activePreviewOpts.Toggle()
|
||||
} else if !t.previewOpts.Visible() {
|
||||
t.previewer.enabled = !t.previewer.enabled
|
||||
}
|
||||
case actTogglePreview, actShowPreview, actHidePreview:
|
||||
var act bool
|
||||
switch a.t {
|
||||
case actShowPreview:
|
||||
act = !t.hasPreviewWindow() && len(t.previewOpts.command) > 0
|
||||
case actHidePreview:
|
||||
act = t.hasPreviewWindow()
|
||||
case actTogglePreview:
|
||||
act = t.hasPreviewWindow() || len(t.previewOpts.command) > 0
|
||||
}
|
||||
if act {
|
||||
t.activePreviewOpts.Toggle()
|
||||
updatePreviewWindow(false)
|
||||
if t.isPreviewEnabled() {
|
||||
valid, list := t.buildPlusList(t.previewOpts.command, false, false)
|
||||
if t.canPreview() {
|
||||
valid, list := t.buildPlusList(t.previewOpts.command, false)
|
||||
if valid {
|
||||
t.cancelPreview()
|
||||
t.previewBox.Set(reqPreviewEnqueue,
|
||||
@@ -2889,11 +2994,11 @@ func (t *Terminal) Loop() {
|
||||
req(reqPreviewRefresh)
|
||||
}
|
||||
case actTransformPrompt:
|
||||
prompt := t.executeCommand(a.a, false, true, true)
|
||||
prompt := t.executeCommand(a.a, false, true, true, true)
|
||||
t.prompt, t.promptLen = t.parsePrompt(prompt)
|
||||
req(reqPrompt)
|
||||
case actTransformQuery:
|
||||
query := t.executeCommand(a.a, false, true, true)
|
||||
query := t.executeCommand(a.a, false, true, true, true)
|
||||
t.input = []rune(query)
|
||||
t.cx = len(t.input)
|
||||
case actToggleSort:
|
||||
@@ -2942,6 +3047,19 @@ func (t *Terminal) Loop() {
|
||||
case actChangeQuery:
|
||||
t.input = []rune(a.a)
|
||||
t.cx = len(t.input)
|
||||
case actTransformHeader:
|
||||
header := t.executeCommand(a.a, false, true, true, false)
|
||||
if t.changeHeader(header) {
|
||||
req(reqFullRedraw)
|
||||
} else {
|
||||
req(reqHeader)
|
||||
}
|
||||
case actChangeHeader:
|
||||
if t.changeHeader(a.a) {
|
||||
req(reqFullRedraw)
|
||||
} else {
|
||||
req(reqHeader)
|
||||
}
|
||||
case actChangeBorderLabel:
|
||||
if t.border != nil {
|
||||
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(a.a, &tui.ColBorderLabel, false)
|
||||
@@ -2954,13 +3072,13 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
case actTransformBorderLabel:
|
||||
if t.border != nil {
|
||||
label := t.executeCommand(a.a, false, true, true)
|
||||
label := t.executeCommand(a.a, false, true, true, true)
|
||||
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false)
|
||||
req(reqRedrawBorderLabel)
|
||||
}
|
||||
case actTransformPreviewLabel:
|
||||
if t.pborder != nil {
|
||||
label := t.executeCommand(a.a, false, true, true)
|
||||
label := t.executeCommand(a.a, false, true, true, true)
|
||||
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false)
|
||||
req(reqRedrawPreviewLabel)
|
||||
}
|
||||
@@ -2968,7 +3086,6 @@ func (t *Terminal) Loop() {
|
||||
t.prompt, t.promptLen = t.parsePrompt(a.a)
|
||||
req(reqPrompt)
|
||||
case actPreview:
|
||||
t.previewer.enabled = true
|
||||
updatePreviewWindow(true)
|
||||
refreshPreview(a.a)
|
||||
case actRefreshPreview:
|
||||
@@ -3211,6 +3328,19 @@ func (t *Terminal) Loop() {
|
||||
t.paused = !t.paused
|
||||
changed = !t.paused
|
||||
req(reqPrompt)
|
||||
case actToggleTrack:
|
||||
switch t.track {
|
||||
case trackEnabled:
|
||||
t.track = trackDisabled
|
||||
case trackDisabled:
|
||||
t.track = trackEnabled
|
||||
}
|
||||
req(reqInfo)
|
||||
case actTrack:
|
||||
if t.track == trackDisabled {
|
||||
t.track = trackCurrent
|
||||
}
|
||||
req(reqInfo)
|
||||
case actEnableSearch:
|
||||
t.paused = false
|
||||
changed = true
|
||||
@@ -3262,7 +3392,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 {
|
||||
@@ -3288,7 +3418,7 @@ func (t *Terminal) Loop() {
|
||||
// Translate coordinates
|
||||
mx -= t.window.Left()
|
||||
my -= t.window.Top()
|
||||
min := 2 + len(t.header)
|
||||
min := 2 + len(t.header0) + len(t.header)
|
||||
if t.noInfoLine() {
|
||||
min--
|
||||
}
|
||||
@@ -3353,7 +3483,7 @@ func (t *Terminal) Loop() {
|
||||
case actReload, actReloadSync:
|
||||
t.failed = nil
|
||||
|
||||
valid, list := t.buildPlusList(a.a, false, false)
|
||||
valid, list := t.buildPlusList(a.a, false)
|
||||
if !valid {
|
||||
// We run the command even when there's no match
|
||||
// 1. If the template doesn't have any slots
|
||||
@@ -3381,9 +3511,8 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
case actChangePreview:
|
||||
if t.previewOpts.command != a.a {
|
||||
t.previewer.enabled = len(a.a) > 0
|
||||
updatePreviewWindow(false)
|
||||
t.previewOpts.command = a.a
|
||||
updatePreviewWindow(false)
|
||||
refreshPreview(t.previewOpts.command)
|
||||
}
|
||||
case actChangePreviewWindow:
|
||||
@@ -3403,7 +3532,7 @@ func (t *Terminal) Loop() {
|
||||
if !currentPreviewOpts.sameLayout(t.previewOpts) {
|
||||
wasHidden := t.pwindow == nil
|
||||
updatePreviewWindow(false)
|
||||
if wasHidden && t.pwindow != nil {
|
||||
if wasHidden && t.hasPreviewWindow() {
|
||||
refreshPreview(t.previewOpts.command)
|
||||
} else {
|
||||
req(reqPreviewRefresh)
|
||||
@@ -3480,12 +3609,10 @@ func (t *Terminal) Loop() {
|
||||
req(reqList)
|
||||
}
|
||||
|
||||
if queryChanged {
|
||||
if t.isPreviewEnabled() {
|
||||
_, _, q := hasPreviewFlags(t.previewOpts.command)
|
||||
if q {
|
||||
t.version++
|
||||
}
|
||||
if queryChanged && t.canPreview() && len(t.previewOpts.command) > 0 {
|
||||
_, _, q := hasPreviewFlags(t.previewOpts.command)
|
||||
if q {
|
||||
t.version++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3496,7 +3623,7 @@ func (t *Terminal) Loop() {
|
||||
t.mutex.Unlock() // Must be unlocked before touching reqBox
|
||||
|
||||
if changed || newCommand != nil {
|
||||
t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, sync: reloadSync, command: newCommand})
|
||||
t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, sync: reloadSync, command: newCommand, changed: changed})
|
||||
}
|
||||
for _, event := range events {
|
||||
t.reqBox.Set(event, nil)
|
||||
@@ -3560,7 +3687,7 @@ func (t *Terminal) vset(o int) bool {
|
||||
}
|
||||
|
||||
func (t *Terminal) maxItems() int {
|
||||
max := t.window.Height() - 2 - len(t.header)
|
||||
max := t.window.Height() - 2 - len(t.header0) - len(t.header)
|
||||
if t.noInfoLine() {
|
||||
max++
|
||||
}
|
||||
|
||||
@@ -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,8 @@ const (
|
||||
Start
|
||||
Load
|
||||
Focus
|
||||
One
|
||||
Zero
|
||||
|
||||
AltBS
|
||||
|
||||
@@ -282,6 +284,15 @@ type Event struct {
|
||||
MouseEvent *MouseEvent
|
||||
}
|
||||
|
||||
func (e Event) Is(types ...EventType) bool {
|
||||
for _, t := range types {
|
||||
if e.Type == t {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type MouseEvent struct {
|
||||
Y int
|
||||
X int
|
||||
@@ -525,28 +536,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)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// ExecCommand executes the given command with $SHELL
|
||||
@@ -45,3 +47,7 @@ func SetNonblock(file *os.File, nonblock bool) {
|
||||
func Read(fd int, b []byte) (int, error) {
|
||||
return syscall.Read(int(fd), b)
|
||||
}
|
||||
|
||||
func SetStdin(file *os.File) {
|
||||
unix.Dup2(int(file.Fd()), 0)
|
||||
}
|
||||
|
||||
@@ -81,3 +81,7 @@ func SetNonblock(file *os.File, nonblock bool) {
|
||||
func Read(fd int, b []byte) (int, error) {
|
||||
return syscall.Read(syscall.Handle(fd), b)
|
||||
}
|
||||
|
||||
func SetStdin(file *os.File) {
|
||||
// No-op
|
||||
}
|
||||
|
||||
420
test/test_go.rb
420
test/test_go.rb
@@ -180,7 +180,7 @@ class TestBase < Minitest::Test
|
||||
end
|
||||
|
||||
def writelines(path, lines)
|
||||
File.unlink(path) while File.exist?(path)
|
||||
FileUtils.rm_f(path) while File.exist?(path)
|
||||
File.open(path, 'w') { |f| f.puts lines }
|
||||
end
|
||||
|
||||
@@ -188,7 +188,7 @@ class TestBase < Minitest::Test
|
||||
wait { assert_path_exists tempname }
|
||||
File.read(tempname)
|
||||
ensure
|
||||
File.unlink(tempname) while File.exist?(tempname)
|
||||
FileUtils.rm_f(tempname) while File.exist?(tempname)
|
||||
@temp_suffix += 1
|
||||
tmux.prepare
|
||||
end
|
||||
@@ -905,11 +905,7 @@ class TestGoFZF < TestBase
|
||||
history_file = '/tmp/fzf-test-history'
|
||||
|
||||
# History with limited number of entries
|
||||
begin
|
||||
File.unlink(history_file)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(history_file)
|
||||
opts = "--history=#{history_file} --history-size=4"
|
||||
input = %w[00 11 22 33 44]
|
||||
input.each do |keys|
|
||||
@@ -955,7 +951,7 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| assert_equal '> 33', lines[-1] }
|
||||
tmux.send_keys :Enter
|
||||
ensure
|
||||
File.unlink(history_file)
|
||||
FileUtils.rm_f(history_file)
|
||||
end
|
||||
|
||||
def test_execute
|
||||
@@ -984,11 +980,7 @@ class TestGoFZF < TestBase
|
||||
], File.readlines(output, chomp: true)
|
||||
end
|
||||
ensure
|
||||
begin
|
||||
File.unlink(output)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(output)
|
||||
end
|
||||
|
||||
def test_execute_multi
|
||||
@@ -1013,20 +1005,12 @@ class TestGoFZF < TestBase
|
||||
], File.readlines(output, chomp: true)
|
||||
end
|
||||
ensure
|
||||
begin
|
||||
File.unlink(output)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(output)
|
||||
end
|
||||
|
||||
def test_execute_plus_flag
|
||||
output = tempname + '.tmp'
|
||||
begin
|
||||
File.unlink(output)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(output)
|
||||
writelines(tempname, ['foo bar', '123 456'])
|
||||
|
||||
tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute-silent(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter
|
||||
@@ -1059,21 +1043,13 @@ class TestGoFZF < TestBase
|
||||
], File.readlines(output, chomp: true)
|
||||
end
|
||||
rescue StandardError
|
||||
begin
|
||||
File.unlink(output)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(output)
|
||||
end
|
||||
|
||||
def test_execute_shell
|
||||
# Custom script to use as $SHELL
|
||||
output = tempname + '.out'
|
||||
begin
|
||||
File.unlink(output)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(output)
|
||||
writelines(tempname,
|
||||
['#!/usr/bin/env bash', "echo $1 / $2 > #{output}"])
|
||||
system("chmod +x #{tempname}")
|
||||
@@ -1087,11 +1063,7 @@ class TestGoFZF < TestBase
|
||||
assert_equal ["-c / 'foo'bar"], File.readlines(output, chomp: true)
|
||||
end
|
||||
ensure
|
||||
begin
|
||||
File.unlink(output)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(output)
|
||||
end
|
||||
|
||||
def test_cycle
|
||||
@@ -1485,6 +1457,83 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| assert_includes lines[1], ' {5-1 3 4} ' }
|
||||
end
|
||||
|
||||
def test_toggle_preview_without_default_preview_command
|
||||
tmux.send_keys %(seq 100 | #{FZF} --bind 'space:preview(echo [{}]),enter:toggle-preview' --preview-window up,border-double), :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.match_count
|
||||
refute_includes lines[1], '║ [1]'
|
||||
end
|
||||
|
||||
# toggle-preview should do nothing
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| refute_includes lines[1], '║ [1]' }
|
||||
tmux.send_keys :Up
|
||||
tmux.until do |lines|
|
||||
refute_includes lines[1], '║ [1]'
|
||||
refute_includes lines[1], '║ [2]'
|
||||
end
|
||||
|
||||
tmux.send_keys :Up
|
||||
tmux.until do |lines|
|
||||
assert_includes lines, '> 3'
|
||||
refute_includes lines[1], '║ [3]'
|
||||
end
|
||||
|
||||
# One-off preview action
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_includes lines[1], '║ [3]' }
|
||||
|
||||
# toggle-preview to hide it
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| refute_includes lines[1], '║ [3]' }
|
||||
|
||||
# toggle-preview again does nothing
|
||||
tmux.send_keys :Enter, :Up
|
||||
tmux.until do |lines|
|
||||
assert_includes lines, '> 4'
|
||||
refute_includes lines[1], '║ [4]'
|
||||
end
|
||||
end
|
||||
|
||||
def test_show_and_hide_preview
|
||||
tmux.send_keys %(seq 100 | #{FZF} --preview-window hidden,border-bold --preview 'echo [{}]' --bind 'a:show-preview,b:hide-preview'), :Enter
|
||||
|
||||
# Hidden by default
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.match_count
|
||||
refute_includes lines[1], '┃ [1]'
|
||||
end
|
||||
|
||||
# Show
|
||||
tmux.send_keys :a
|
||||
tmux.until { |lines| assert_includes lines[1], '┃ [1]' }
|
||||
|
||||
# Already shown
|
||||
tmux.send_keys :a
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines[1], '┃ [2]' }
|
||||
|
||||
# Hide
|
||||
tmux.send_keys :b
|
||||
tmux.send_keys :Up
|
||||
tmux.until do |lines|
|
||||
assert_includes lines, '> 3'
|
||||
refute_includes lines[1], '┃ [3]'
|
||||
end
|
||||
|
||||
# Already hidden
|
||||
tmux.send_keys :b
|
||||
tmux.send_keys :Up
|
||||
tmux.until do |lines|
|
||||
assert_includes lines, '> 4'
|
||||
refute_includes lines[1], '┃ [4]'
|
||||
end
|
||||
|
||||
# Show it again
|
||||
tmux.send_keys :a
|
||||
tmux.until { |lines| assert_includes lines[1], '┃ [4]' }
|
||||
end
|
||||
|
||||
def test_preview_hidden
|
||||
tmux.send_keys %(seq 1000 | #{FZF} --preview 'echo {{}-{}-$FZF_PREVIEW_LINES-$FZF_PREVIEW_COLUMNS}' --preview-window down:1:hidden --bind ?:toggle-preview), :Enter
|
||||
tmux.until { |lines| assert_equal '>', lines[-1] }
|
||||
@@ -1497,11 +1546,7 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_preview_size_0
|
||||
begin
|
||||
File.unlink(tempname)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(tempname)
|
||||
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0 --bind space:toggle-preview), :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.item_count
|
||||
@@ -1526,6 +1571,32 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
end
|
||||
|
||||
def test_preview_size_0_hidden
|
||||
FileUtils.rm_f(tempname)
|
||||
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0,hidden --bind space:toggle-preview), :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.send_keys :Down, :Down
|
||||
tmux.until { |lines| assert_includes lines, '> 3' }
|
||||
wait { refute_path_exists tempname }
|
||||
tmux.send_keys :Space
|
||||
wait do
|
||||
assert_path_exists tempname
|
||||
assert_equal %w[3], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
tmux.send_keys :Down
|
||||
wait do
|
||||
assert_equal %w[3 4], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
tmux.send_keys :Space, :Down
|
||||
tmux.until { |lines| assert_includes lines, '> 5' }
|
||||
tmux.send_keys :Down
|
||||
tmux.until { |lines| assert_includes lines, '> 6' }
|
||||
tmux.send_keys :Space
|
||||
wait do
|
||||
assert_equal %w[3 4 6], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
end
|
||||
|
||||
def test_preview_flags
|
||||
tmux.send_keys %(seq 10 | sed 's/^/:: /; s/$/ /' |
|
||||
#{FZF} --multi --preview 'echo {{2}/{s2}/{+2}/{+s2}/{q}/{n}/{+n}}'), :Enter
|
||||
@@ -1794,6 +1865,67 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| assert_equal '>', lines.last }
|
||||
end
|
||||
|
||||
def test_change_and_transform_header
|
||||
[
|
||||
'space:change-header:$(seq 4)',
|
||||
'space:transform-header:seq 4'
|
||||
].each_with_index do |binding, i|
|
||||
tmux.send_keys %(seq 3 | #{FZF} --header-lines 2 --header bar --bind "#{binding}"), :Enter
|
||||
expected = <<~OUTPUT
|
||||
> 3
|
||||
2
|
||||
1
|
||||
bar
|
||||
1/1
|
||||
>
|
||||
OUTPUT
|
||||
tmux.until { assert_block(expected, _1) }
|
||||
tmux.send_keys :Space
|
||||
expected = <<~OUTPUT
|
||||
> 3
|
||||
2
|
||||
1
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
1/1
|
||||
>
|
||||
OUTPUT
|
||||
tmux.until { assert_block(expected, _1) }
|
||||
next unless i.zero?
|
||||
|
||||
teardown
|
||||
setup
|
||||
end
|
||||
end
|
||||
|
||||
def test_change_header
|
||||
tmux.send_keys %(seq 3 | #{FZF} --header-lines 2 --header bar --bind "space:change-header:$(seq 4)"), :Enter
|
||||
expected = <<~OUTPUT
|
||||
> 3
|
||||
2
|
||||
1
|
||||
bar
|
||||
1/1
|
||||
>
|
||||
OUTPUT
|
||||
tmux.until { assert_block(expected, _1) }
|
||||
tmux.send_keys :Space
|
||||
expected = <<~OUTPUT
|
||||
> 3
|
||||
2
|
||||
1
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
1/1
|
||||
>
|
||||
OUTPUT
|
||||
tmux.until { assert_block(expected, _1) }
|
||||
end
|
||||
|
||||
def test_change_query
|
||||
tmux.send_keys %(: | #{FZF} --query foo --bind space:change-query:foobar), :Enter
|
||||
tmux.until { |lines| assert_equal 0, lines.item_count }
|
||||
@@ -1859,7 +1991,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
|
||||
@@ -2087,11 +2219,7 @@ class TestGoFZF < TestBase
|
||||
wait { refute system("pgrep -f #{script}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{script}")
|
||||
begin
|
||||
File.unlink(script)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(script)
|
||||
end
|
||||
|
||||
def test_kill_default_command_on_accept
|
||||
@@ -2109,11 +2237,7 @@ class TestGoFZF < TestBase
|
||||
wait { refute system("pgrep -f #{script}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{script}")
|
||||
begin
|
||||
File.unlink(script)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(script)
|
||||
end
|
||||
|
||||
def test_kill_reload_command_on_abort
|
||||
@@ -2134,11 +2258,7 @@ class TestGoFZF < TestBase
|
||||
wait { refute system("pgrep -f #{script}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{script}")
|
||||
begin
|
||||
File.unlink(script)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(script)
|
||||
end
|
||||
|
||||
def test_kill_reload_command_on_accept
|
||||
@@ -2158,11 +2278,7 @@ class TestGoFZF < TestBase
|
||||
wait { refute system("pgrep -f #{script}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{script}")
|
||||
begin
|
||||
File.unlink(script)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(script)
|
||||
end
|
||||
|
||||
def test_preview_header
|
||||
@@ -2574,11 +2690,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
|
||||
@@ -2588,6 +2710,162 @@ class TestGoFZF < TestBase
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_includes lines, '/1/1/' }
|
||||
end
|
||||
|
||||
def test_become
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind 'enter:become:seq {} | #{FZF}'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.send_keys 999
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
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 --bind t:toggle-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
|
||||
tmux.send_keys '555'
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert_includes lines, '> 555'
|
||||
assert_includes lines[-2], '+T'
|
||||
end
|
||||
tmux.send_keys 't'
|
||||
tmux.until do |lines|
|
||||
refute_includes lines[-2], '+T'
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 28, lines.match_count
|
||||
assert_includes lines, '> 55'
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 271, lines.match_count
|
||||
assert_includes lines, '> 5'
|
||||
end
|
||||
tmux.send_keys 't'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[-2], '+T'
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 1000, lines.match_count
|
||||
assert_includes lines, '> 5'
|
||||
end
|
||||
end
|
||||
|
||||
def test_track_action
|
||||
tmux.send_keys "seq 1000 | #{FZF} --query 555 --bind t:track", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert_includes lines, '> 555'
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 28, lines.match_count
|
||||
assert_includes lines, '> 55'
|
||||
end
|
||||
tmux.send_keys :t
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[-2], '+T'
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 271, lines.match_count
|
||||
assert_includes lines, '> 55'
|
||||
end
|
||||
|
||||
# Automatically disabled when the tracking item is no longer visible
|
||||
tmux.send_keys '4'
|
||||
tmux.until do |lines|
|
||||
assert_equal 28, lines.match_count
|
||||
refute_includes lines[-2], '+T'
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 271, lines.match_count
|
||||
assert_includes lines, '> 5'
|
||||
end
|
||||
tmux.send_keys :t
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[-2], '+T'
|
||||
end
|
||||
tmux.send_keys :Up
|
||||
tmux.until do |lines|
|
||||
refute_includes lines[-2], '+T'
|
||||
end
|
||||
end
|
||||
|
||||
def test_one_and_zero
|
||||
tmux.send_keys "seq 10 | #{FZF} --bind 'zero:preview(echo no match),one:preview(echo {} is the only match)'", :Enter
|
||||
tmux.send_keys '1'
|
||||
tmux.until do |lines|
|
||||
assert_equal 2, lines.match_count
|
||||
refute(lines.any? { _1.include?('only match') })
|
||||
refute(lines.any? { _1.include?('no match') })
|
||||
end
|
||||
tmux.send_keys '0'
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert(lines.any? { _1.include?('only match') })
|
||||
end
|
||||
tmux.send_keys '0'
|
||||
tmux.until do |lines|
|
||||
assert_equal 0, lines.match_count
|
||||
assert(lines.any? { _1.include?('no match') })
|
||||
end
|
||||
end
|
||||
|
||||
def test_height_range_with_exit_0
|
||||
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
|
||||
|
||||
def test_reload_and_change
|
||||
tmux.send_keys "(echo foo; echo bar) | #{FZF} --bind 'load:reload-sync(sleep 60)+change-query(bar)'", :Enter
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
end
|
||||
end
|
||||
|
||||
module TestShell
|
||||
@@ -2716,9 +2994,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