m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-16 23:33:39 -05:00

Compare commits

...

59 Commits

Author SHA1 Message Date
Junegunn Choi
20230402d0 0.39.0 2023-04-02 23:33:37 +09:00
Junegunn Choi
5c2c3a6c88 Use Go 1.20.2 2023-04-02 23:27:22 +09:00
tyama711
fb019d43bf Fix a bug of height range with -1 or -0 (#3226)
Fixed a bug that when both heightUnknown and deferred are true, deferred is not properly reset and the program terminates abnormally.

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2023-04-02 23:26:13 +09:00
Junegunn Choi
025aa33773 [fzf-tmux] Disallow popup mode on tmux 3.1 or below
Close #3198
2023-04-02 00:16:00 +09:00
Junegunn Choi
302e21fd58 [shell] Update kill completion
* Explicitly specify the list of fields for consistent experience
* Add fallback command for BusyBox (Close #3219)
* Apply `--header-lines=1` to show the column header
2023-04-01 19:52:34 +09:00
Junegunn Choi
211512ae64 Fix Rubocop error 2023-04-01 17:29:13 +09:00
Junegunn Choi
8ec917b1c3 Add 'one' event
Close #2629
Close #2494
Close #459
2023-04-01 17:25:47 +09:00
Junegunn Choi
1c7534f009 Add --track option to track the current selection
Close #3186
Related #1890
2023-04-01 12:59:44 +09:00
Sten Arthur Laane
ae745d9397 Add bat to bash autocomplete commands (#3223)
Bat is a common alternative to cat, it's even referenced multiple times
in fzf docs. This makes `bat **` work by default.
2023-03-27 12:21:37 +09:00
Junegunn Choi
60f37aae2f Respect 'regular' attribute in 'bw' base theme
Don't make the text bold if an element is explicitly specified as
'regular'.

Fix #3222
2023-03-26 23:39:05 +09:00
Junegunn Choi
d7daf5f724 Render CR and LF as ␍ and ␊
Close #2529
2023-03-25 10:41:19 +09:00
Vitaly Zdanevich
e5103d9429 README.md: package managers: add Portage/Gentoo (#3205) 2023-03-22 09:57:50 +09:00
dependabot[bot]
8fecb29848 Bump github.com/rivo/uniseg from 0.4.2 to 0.4.4 (#3192)
Bumps [github.com/rivo/uniseg](https://github.com/rivo/uniseg) from 0.4.2 to 0.4.4.
- [Release notes](https://github.com/rivo/uniseg/releases)
- [Commits](https://github.com/rivo/uniseg/compare/v0.4.2...v0.4.4)

---
updated-dependencies:
- dependency-name: github.com/rivo/uniseg
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-21 16:53:20 +09:00
dependabot[bot]
290ea6179d Bump golang.org/x/term from 0.0.0-20210927222741-03fcf44c2211 to 0.6.0 (#3203)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.0.0-20210927222741-03fcf44c2211 to 0.6.0.
- [Release notes](https://github.com/golang/term/releases)
- [Commits](https://github.com/golang/term/commits/v0.6.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-21 16:19:55 +09:00
dependabot[bot]
9695a40fc9 Bump golang.org/x/sys from 0.0.0-20220811171246-fbc7d0a398ab to 0.6.0 (#3202)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.0.0-20220811171246-fbc7d0a398ab to 0.6.0.
- [Release notes](https://github.com/golang/sys/releases)
- [Commits](https://github.com/golang/sys/commits/v0.6.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-21 16:14:14 +09:00
dependabot[bot]
1913b95227 Bump actions/setup-go from 3 to 4 (#3216)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-21 16:13:02 +09:00
Junegunn Choi
a874aea692 [vim] More explanation on 'set rtp+=~/.fzf' instruction
Close #3171
2023-03-20 22:33:14 +09:00
Michael Vorburger ⛑️
69c52099e7 docs: Fix intention of README (#3214)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2023-03-20 13:00:33 +09:00
Junegunn Choi
cfc0747d5d Follow Rubocop suggestion 2023-03-19 15:53:31 +09:00
Junegunn Choi
fcd7e8768d Omit port number in --listen for automatic port assignment
Close #3200
2023-03-19 15:48:39 +09:00
Junegunn Choi
3c34dd8275 Fix extra new line in the preview window
When a colored text ends at the right end of the window

Fix #3209
2023-03-17 13:22:20 +09:00
Junegunn Choi
1116e481be [vim] Update setqflist example
Without 'lnum', cfdo doesn't work

Close https://github.com/junegunn/fzf.vim/issues/1435
2023-03-10 22:22:22 +09:00
dependabot[bot]
63cf9d04de Bump crate-ci/typos from 1.13.10 to 1.13.16 (#3194)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2023-03-07 01:21:23 +09:00
Zhizhen He
3364d4d147 Add spell check workflow (#3183) 2023-02-23 00:36:04 +09:00
Junegunn Choi
57ad21e4bd Build and release s390x binaries 2023-02-22 14:31:41 +09:00
Julian Ruess
414f87981f Add support for s390x architecture
Signed-off-by: Julian Ruess <julianr@linux.ibm.com>
2023-02-22 14:31:41 +09:00
Junegunn Choi
b1459c79cf Make sure that the query before the cursor is not hidden
Close #3176
2023-02-19 16:38:26 +09:00
Junegunn Choi
352ea07226 0.38.0 2023-02-15 23:24:42 +09:00
Junegunn Choi
27018787af Describe become(...) action and use it to simplify examples 2023-02-15 23:24:42 +09:00
Junegunn Choi
4e305eca26 become: Set stdin to /dev/tty 2023-02-15 23:24:42 +09:00
sitiom
9e9c0ceaf4 Add Winget Releaser workflow (#3164) 2023-02-15 16:47:12 +09:00
Junegunn Choi
b3bf18b1c0 [fzf-tmux] Fix version check
The output of `tmux -V` starts with "tmux ".
2023-02-13 15:25:39 +09:00
Junegunn Choi
b1619f675f [fzf-tmux] Do not set --margin 0,1 on tmux 3.3 or above
Close #3162
2023-02-13 14:49:02 +09:00
Junegunn Choi
96c3de12eb Run 'become' only when the command template is properly evaluated 2023-02-12 22:06:21 +09:00
Junegunn Choi
719dbb8bae Update ADVANCED.md: transform-query to restore the query string
Close #2961
2023-02-12 17:23:17 +09:00
Junegunn Choi
f38a7f7f8f [bash] Enable environment variable completion for printenv
Close #3145
2023-02-12 16:58:36 +09:00
Junegunn Choi
6ea38b4438 Add become(...) action that replaces current fzf process
Close #3159
2023-02-11 20:26:31 +09:00
Junegunn Choi
f7447aece1 Code cleanup 2023-02-01 18:16:58 +09:00
Junegunn Choi
aa2b9ec476 Add 'show-preview' and 'hide-preview'
For cases where 'toggle-preview' is not enough
2023-01-31 17:34:11 +09:00
Junegunn Choi
3ee00f8bc2 toggle-preview should not show empty preview window 2023-01-30 22:13:29 +09:00
Junegunn Choi
fccab60a5c --preview-window 0,hidden should not execute the preview command
Until `toggle-preview` action is triggered

Fix #3149
2023-01-30 21:39:18 +09:00
Junegunn Choi
0f4af38457 [vim] Simplify --border injection
Prepend the border options so that the user can override them in
'options' entry of the spec.
2023-01-27 14:00:22 +09:00
Junegunn Choi
aef39f1160 [vim] Fix missing --border when --border-label is present 2023-01-27 11:00:59 +09:00
Junegunn Choi
2023012408 0.37.0 2023-01-24 22:11:14 +09:00
Junegunn Choi
95a7661bb1 Sanitize input strings that should be a single line 2023-01-24 19:35:29 +09:00
Junegunn Choi
618d317803 Support custom separator of inline info
Close #2030
Close #3084
2023-01-24 17:55:06 +09:00
Junegunn Choi
ae897c8cdb No need to touch mouse flag if it's already false 2023-01-24 12:45:07 +09:00
Junegunn Choi
d0a0f3c052 Temporarily disable mouse mode when switching to an external command 2023-01-24 12:41:40 +09:00
Junegunn Choi
91b9591b10 Reenable mouse mode when coming back from an external program
Close #3141
2023-01-24 12:00:41 +09:00
Junegunn Choi
aa7361337d Make test case pass on 32-bit platforms
Close #3127
2023-01-23 18:30:36 +09:00
Junegunn Choi
284d77fe2e Add 'focus' event
Can we find a better name? I have considered the followings.

* 'point', because "the pointer" points to the current item.
* 'shift', 'switch', 'move', etc. These are not technically correct
  because the current item can change without cursor movement (--tac,
  reload, search update)
* 'change' is already taken. 'change-current' feels a bit wordy and
  sounds wrong, 'current-changed' is wordy and doesn't go well with the
  other event names
* 'target', not straightforward

Close #3053
2023-01-23 16:38:24 +09:00
Junegunn Choi
826178f1e2 Do not restore terminal state while running an external command 2023-01-23 02:24:13 +09:00
Junegunn Choi
acccf8a9b8 Fix TOC 2023-01-23 02:24:13 +09:00
Francesco Bigagnoli
57c066f0be Fix bat url in README (#3129) 2023-01-23 02:21:16 +09:00
Nachum Barcohen
e44f64ae92 Add Helix editor to bash autocompletion (#3137) 2023-01-23 02:21:04 +09:00
Junegunn Choi
d51980a3f5 Add 'transform-border-label' and 'transform-preview-label' 2023-01-22 02:18:19 +09:00
jpcrs
c3d73e7ecb Add change-border-label and change-preview-label actions, update man 2023-01-22 02:18:19 +09:00
Junegunn Choi
b077f6821d Action argument in enclosed form should allow new lines
Close #3138
2023-01-21 22:20:26 +09:00
Junegunn Choi
a79de11af7 README: Add FZF_TMUX_OPTS example for tmux popup 2023-01-19 13:25:08 +09:00
38 changed files with 1183 additions and 515 deletions

View File

@@ -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

View File

@@ -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
View 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
View 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 }}

View File

@@ -74,6 +74,7 @@ builds:
- arm64
- loong64
- ppc64le
- s390x
goarm:
- 5
- 6

View File

@@ -1 +1 @@
golang 1.19
golang 1.20.2

View File

@@ -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})'
```
![image](https://user-images.githubusercontent.com/700826/113684212-f9ff0a00-96ff-11eb-8737-7bb571d320cc.png)
@@ -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
-----------

View File

@@ -1,6 +1,91 @@
CHANGELOG
=========
0.39.0
------
- Added `one` event that is triggered when there's only one match
```sh
# Automatically select the only match
seq 10 | fzf --bind one:accept
```
- Added `--track` option that makes fzf track the current selection when the
result list is updated. This can be useful when browsing logs using fzf with
sorting disabled.
```sh
git log --oneline --graph --color=always | nl |
fzf --ansi --track --no-sort --layout=reverse-list
```
- If you use `--listen` option without a port number fzf will automatically
allocate an available port and export it as `$FZF_PORT` environment
variable.
```sh
# Automatic port assignment
fzf --listen --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'
# Say hello
curl "localhost:$(cat /tmp/fzf-port)" -d 'preview:echo Hello, fzf is listening on $FZF_PORT.'
```
- A carriage return and a line feed character will be rendered as dim ␍ and
␊ respectively.
```sh
printf "foo\rbar\nbaz" | fzf --read0 --preview 'echo {}'
```
- fzf will stop rendering a non-displayable characters as a space. This will
likely cause less glitches in the preview window.
```sh
fzf --preview 'head -1000 /dev/random'
```
- Bug fixes and improvements
0.38.0
------
- New actions
- `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
```sh
fzf --info 'inline: ' --prompt ' ' --color prompt:bright-yellow
```
- New event
- `focus` - Triggered when the focus changes due to a vertical cursor
movement or a search result update
```sh
fzf --bind 'focus:transform-preview-label:echo [ {} ]' --preview 'cat {}'
# Any action bound to the event runs synchronously and thus can make the interface sluggish
# e.g. lolcat isn't one of the fastest programs, and every cursor movement in
# fzf will be noticeably affected by its execution time
fzf --bind 'focus:transform-preview-label:echo [ {} ] | lolcat -f' --preview 'cat {}'
# Beware not to introduce an infinite loop
seq 10 | fzf --bind 'focus:up' --cycle
```
- New actions
- `change-border-label`
- `change-preview-label`
- `transform-border-label`
- `transform-preview-label`
- Bug fixes and improvements
0.36.0
------
- Added `--listen=HTTP_PORT` option to start HTTP server. It allows external

View File

@@ -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 $@

View File

@@ -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

135
README.md
View File

@@ -33,35 +33,36 @@ Table of Contents
* [Upgrading fzf](#upgrading-fzf)
* [Building fzf](#building-fzf)
* [Usage](#usage)
* [Using the finder](#using-the-finder)
* [Layout](#layout)
* [Search syntax](#search-syntax)
* [Environment variables](#environment-variables)
* [Options](#options)
* [Demo](#demo)
* [Using the finder](#using-the-finder)
* [Layout](#layout)
* [Search syntax](#search-syntax)
* [Environment variables](#environment-variables)
* [Options](#options)
* [Demo](#demo)
* [Examples](#examples)
* [`fzf-tmux` script](#fzf-tmux-script)
* [Key bindings for command-line](#key-bindings-for-command-line)
* [Fuzzy completion for bash and zsh](#fuzzy-completion-for-bash-and-zsh)
* [Files and directories](#files-and-directories)
* [Process IDs](#process-ids)
* [Host names](#host-names)
* [Environment variables / Aliases](#environment-variables--aliases)
* [Settings](#settings)
* [Supported commands](#supported-commands)
* [Custom fuzzy completion](#custom-fuzzy-completion)
* [Files and directories](#files-and-directories)
* [Process IDs](#process-ids)
* [Host names](#host-names)
* [Environment variables / Aliases](#environment-variables--aliases)
* [Settings](#settings)
* [Supported commands](#supported-commands)
* [Custom fuzzy completion](#custom-fuzzy-completion)
* [Vim plugin](#vim-plugin)
* [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,7 +206,23 @@ files excluding hidden ones. (You can override the default command with
vim $(fzf)
```
#### Using the finder
> *: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
- `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit
@@ -211,7 +231,7 @@ vim $(fzf)
- Mouse: scroll, click, double-click; shift-click and shift-scroll on
multi-select mode
#### Layout
### Layout
fzf by default starts in fullscreen mode, but you can make it start below the
cursor with `--height` option.
@@ -234,7 +254,7 @@ default. For example,
export FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border'
```
#### Search syntax
### Search syntax
Unless otherwise specified, fzf starts in "extended-search mode" where you can
type in multiple search terms delimited by spaces. e.g. `^music .mp3$ sbtrkt
@@ -262,7 +282,7 @@ or `py`.
^core go$ | rb$ | py$
```
#### Environment variables
### Environment variables
- `FZF_DEFAULT_COMMAND`
- Default command to use when input is tty
@@ -278,11 +298,11 @@ or `py`.
- Default options
- e.g. `export FZF_DEFAULT_OPTS="--layout=reverse --inline-info"`
#### Options
### Options
See the man page (`man fzf`) for the full list of options.
#### Demo
### Demo
If you learn by watching videos, check out this screencast by [@samoshkin](https://github.com/samoshkin) to explore `fzf` features.
<a title="fzf - command-line fuzzy finder" href="https://www.youtube.com/watch?v=qgG5Jhi_Els">
@@ -335,7 +355,7 @@ fish.
- Set `FZF_CTRL_T_COMMAND` to override the default command
- Set `FZF_CTRL_T_OPTS` to pass additional options to fzf
```sh
# Preview file content using bat (https://github.com/sharkdp/fd)
# Preview file content using bat (https://github.com/sharkdp/bat)
export FZF_CTRL_T_OPTS="
--preview 'bat -n --color=always {}'
--bind 'ctrl-/:change-preview-window(down|hidden|)'"
@@ -363,7 +383,7 @@ fish.
```
If you're on a tmux session, you can start fzf in a tmux split-pane or in
a tmux popup window by setting `FZF_TMUX_OPTS` (e.g. `-d 40%`).
a tmux popup window by setting `FZF_TMUX_OPTS` (e.g. `export FZF_TMUX_OPTS='-p80%,60%'`).
See `fzf-tmux --help` for available options.
More tips can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/Configuring-shell-key-bindings).
@@ -371,7 +391,7 @@ More tips can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/C
Fuzzy completion for bash and zsh
---------------------------------
#### Files and directories
### Files and directories
Fuzzy completion for files and directories can be triggered if the word before
the cursor ends with the trigger sequence, which is by default `**`.
@@ -400,7 +420,7 @@ cd **<TAB>
cd ~/github/fzf**<TAB>
```
#### Process IDs
### Process IDs
Fuzzy completion for PIDs is provided for kill command.
@@ -409,7 +429,7 @@ Fuzzy completion for PIDs is provided for kill command.
kill -9 **<TAB>
```
#### Host names
### Host names
For ssh and telnet commands, fuzzy completion for hostnames is provided. The
names are extracted from /etc/hosts and ~/.ssh/config.
@@ -419,7 +439,7 @@ ssh **<TAB>
telnet **<TAB>
```
#### Environment variables / Aliases
### Environment variables / Aliases
```sh
unset **<TAB>
@@ -427,7 +447,7 @@ export **<TAB>
unalias **<TAB>
```
#### Settings
### Settings
```sh
# Use ~~ as the trigger sequence instead of the default **
@@ -465,7 +485,7 @@ _fzf_comprun() {
}
```
#### Supported commands
### Supported commands
On bash, fuzzy completion is enabled only for a predefined set of commands
(`complete | grep _fzf` to see the list). But you can enable it for other
@@ -477,7 +497,7 @@ _fzf_setup_completion path ag git kubectl
_fzf_setup_completion dir tree
```
#### Custom fuzzy completion
### Custom fuzzy completion
_**(Custom completion API is experimental and subject to change)**_
@@ -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

View File

@@ -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 [[ $(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")"

View File

@@ -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
View File

@@ -5,10 +5,10 @@ require (
github.com/mattn/go-isatty v0.0.17
github.com/mattn/go-runewidth v0.0.14
github.com/mattn/go-shellwords v1.0.12
github.com/rivo/uniseg v0.4.2
github.com/rivo/uniseg v0.4.4
github.com/saracen/walker v0.1.3
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
golang.org/x/sys v0.6.0
golang.org/x/term v0.6.0
)
require (

10
go.sum
View File

@@ -11,8 +11,8 @@ github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/saracen/walker v0.1.3 h1:YtcKKmpRPy6XJTHJ75J2QYXXZYWnZNQxPCVqZSHVV/g=
github.com/saracen/walker v0.1.3/go.mod h1:FU+7qU8DeQQgSZDmmThMJi93kPkLFgy0oVAcLxurjIk=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@@ -31,11 +31,13 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=

View File

@@ -2,7 +2,7 @@
set -u
version=0.36.0
version=0.39.0
auto_completion=
key_bindings=
update_config=2
@@ -178,6 +178,7 @@ case "$archi" in
Linux\ loongarch64) download fzf-$version-linux_loong64.tar.gz ;;
Linux\ ppc64le) download fzf-$version-linux_ppc64le.tar.gz ;;
Linux\ *64) download fzf-$version-linux_amd64.tar.gz ;;
Linux\ s390x) download fzf-$version-linux_s390x.tar.gz ;;
FreeBSD\ *64) download fzf-$version-freebsd_amd64.tar.gz ;;
OpenBSD\ *64) download fzf-$version-openbsd_amd64.tar.gz ;;
CYGWIN*\ *64) download fzf-$version-windows_amd64.zip ;;

View File

@@ -1,4 +1,4 @@
$version="0.36.0"
$version="0.39.0"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition

View File

@@ -5,7 +5,7 @@ import (
"github.com/junegunn/fzf/src/protector"
)
var version string = "0.36"
var version string = "0.39"
var revision string = "devel"
func main() {

View File

@@ -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.36.0" "fzf-tmux - open fzf in tmux split pane"
.TH fzf-tmux 1 "Apr 2023" "fzf 0.39.0" "fzf-tmux - open fzf in tmux split pane"
.SH NAME
fzf-tmux - open fzf in tmux split pane

View File

@@ -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.36.0" "fzf - a command-line fuzzy finder"
.TH fzf 1 "Apr 2023" "fzf 0.39.0" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -92,6 +92,16 @@ interface rather than a "fuzzy finder". You can later enable the search using
.B "+s, --no-sort"
Do not sort the result
.TP
.B "--track"
Make fzf track the current selection when the result list is updated.
This can be useful when browsing logs using fzf with sorting disabled.
.RS
e.g.
\fBgit log --oneline --graph --color=always | nl |
fzf --ansi --track --no-sort --layout=reverse-list\fR
.RE
.TP
.B "--tac"
Reverse the order of the input
@@ -337,11 +347,13 @@ e.g.
Determines the display style of finder info (match counters).
.br
.BR default " Display on the next line to the prompt"
.BR default " Display on the next line to the prompt"
.br
.BR inline " Display on the same line"
.BR inline " Display on the same line with the default separator ' < '"
.br
.BR hidden " Do not display finder info"
.BR inline:SEPARATOR " Display on the same line with a non-default separator"
.br
.BR hidden " Do not display finder info"
.br
.TP
@@ -736,9 +748,12 @@ ncurses finder only after the input stream is complete.
e.g. \fBfzf --multi | fzf --sync\fR
.RE
.TP
.B "--listen=HTTP_PORT"
.B "--listen[=HTTP_PORT]"
Start HTTP server on the given port. It allows external processes to send
actions to perform via POST method.
actions to perform via POST method. If the port number is omitted or given as
0, fzf will choose the port automatically and export it as \fBFZF_PORT\fR
environment variable to the child processes started via \fBexecute\fR and
\fBexecute-silent\fR actions.
e.g.
\fB# Start HTTP server on port 6266
@@ -746,6 +761,9 @@ e.g.
# Send action to the server
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
# Choose port automatically and export it as $FZF_PORT to the child process
fzf --listen --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'
\fR
.TP
.B "--version"
@@ -959,6 +977,33 @@ e.g.
\fB# Move cursor to the first entry whenever the query is changed
fzf --bind change:first\fR
.RE
\fIfocus\fR
.RS
Triggered when the focus changes due to a vertical cursor movement or a search
result update.
e.g.
\fBfzf --bind 'focus:transform-preview-label:echo [ {} ]' --preview 'cat {}'
# Any action bound to the event runs synchronously and thus can make the interface sluggish
# e.g. lolcat isn't one of the fastest programs, and every cursor movement in
# fzf will be noticeably affected by its execution time
fzf --bind 'focus:transform-preview-label:echo [ {} ] | lolcat -f' --preview 'cat {}'
# Beware not to introduce an infinite loop
seq 10 | fzf --bind 'focus:up' --cycle\fR
.RE
\fIone\fR
.RS
Triggered when there's only one match. \fBone:accept\fR binding is comparable
to \fB--select-1\fR option, but the difference is that \fB--select-1\fR is only
effective before the interactive finder starts but \fBone\fR event is triggered
by the interactive finder.
e.g.
\fB# Automatically select the only match
seq 10 | fzf --bind one:accept\fR
.RE
\fIbackward-eof\fR
.RS
@@ -972,89 +1017,96 @@ e.g.
.SS AVAILABLE ACTIONS:
A key or an event can be bound to one or more of the following actions.
\fBACTION: DEFAULT BINDINGS (NOTES):
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIenter double-click\fR
\fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
\fBbackward-char\fR \fIctrl-b left\fR
\fBbackward-delete-char\fR \fIctrl-h bspace\fR
\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
\fBbeginning-of-line\fR \fIctrl-a home\fR
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
\fBchange-preview(...)\fR (change \fB--preview\fR option)
\fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|')
\fBchange-prompt(...)\fR (change prompt to the given string)
\fBchange-query(...)\fR (change query string to the given string)
\fBclear-screen\fR \fIctrl-l\fR
\fBclear-selection\fR (clear multi-selection)
\fBclose\fR (close preview window if open, abort fzf otherwise)
\fBclear-query\fR (clear query string)
\fBdelete-char\fR \fIdel\fR
\fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty)
\fBACTION: DEFAULT BINDINGS (NOTES):
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIenter double-click\fR
\fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
\fBbackward-char\fR \fIctrl-b left\fR
\fBbackward-delete-char\fR \fIctrl-h bspace\fR
\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-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 '|')
\fBchange-prompt(...)\fR (change prompt to the given string)
\fBchange-query(...)\fR (change query string to the given string)
\fBclear-screen\fR \fIctrl-l\fR
\fBclear-selection\fR (clear multi-selection)
\fBclose\fR (close preview window if open, abort fzf otherwise)
\fBclear-query\fR (clear query string)
\fBdelete-char\fR \fIdel\fR
\fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty)
\fBdeselect\fR
\fBdeselect-all\fR (deselect all matches)
\fBdisable-search\fR (disable search functionality)
\fBdown\fR \fIctrl-j ctrl-n down\fR
\fBenable-search\fR (enable search functionality)
\fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details)
\fBexecute-silent(...)\fR (see below for the details)
\fBfirst\fR (move to the first match; same as \fBpos(1)\fR)
\fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR
\fBdeselect-all\fR (deselect all matches)
\fBdisable-search\fR (disable search functionality)
\fBdown\fR \fIctrl-j ctrl-n down\fR
\fBenable-search\fR (enable search functionality)
\fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details)
\fBexecute-silent(...)\fR (see below for the details)
\fBfirst\fR (move to the first match; same as \fBpos(1)\fR)
\fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR
\fBignore\fR
\fBjump\fR (EasyMotion-like 2-keystroke movement)
\fBjump-accept\fR (jump and accept)
\fBjump\fR (EasyMotion-like 2-keystroke movement)
\fBjump-accept\fR (jump and accept)
\fBkill-line\fR
\fBkill-word\fR \fIalt-d\fR
\fBlast\fR (move to the last match; same as \fBpos(-1)\fR)
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
\fBnext-selected\fR (move to the next selected item)
\fBpage-down\fR \fIpgdn\fR
\fBpage-up\fR \fIpgup\fR
\fBkill-word\fR \fIalt-d\fR
\fBlast\fR (move to the last match; same as \fBpos(-1)\fR)
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
\fBnext-selected\fR (move to the next selected item)
\fBpage-down\fR \fIpgdn\fR
\fBpage-up\fR \fIpgup\fR
\fBhalf-page-down\fR
\fBhalf-page-up\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)
\fBpreview(...)\fR (see below for the details)
\fBpreview-down\fR \fIshift-down\fR
\fBpreview-up\fR \fIshift-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)
\fBpreview(...)\fR (see below for the details)
\fBpreview-down\fR \fIshift-down\fR
\fBpreview-up\fR \fIshift-up\fR
\fBpreview-page-down\fR
\fBpreview-page-up\fR
\fBpreview-half-page-down\fR
\fBpreview-half-page-up\fR
\fBpreview-bottom\fR
\fBpreview-top\fR
\fBprint-query\fR (print query and exit)
\fBput\fR (put the character to the prompt)
\fBput(...)\fR (put the given string to the prompt)
\fBprint-query\fR (print query and exit)
\fBput\fR (put the character to the prompt)
\fBput(...)\fR (put the given string to the prompt)
\fBrefresh-preview\fR
\fBrebind(...)\fR (rebind bindings after \fBunbind\fR)
\fBreload(...)\fR (see below for the details)
\fBreload-sync(...)\fR (see below for the details)
\fBreplace-query\fR (replace query string with the current selection)
\fBrebind(...)\fR (rebind bindings after \fBunbind\fR)
\fBreload(...)\fR (see below for the details)
\fBreload-sync(...)\fR (see below for the details)
\fBreplace-query\fR (replace query string with the current selection)
\fBselect\fR
\fBselect-all\fR (select all matches)
\fBtoggle\fR (\fIright-click\fR)
\fBtoggle-all\fR (toggle all matches)
\fBtoggle+down\fR \fIctrl-i (tab)\fR
\fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
\fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\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
\fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
\fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
\fBtoggle-preview\fR
\fBtoggle-preview-wrap\fR
\fBtoggle-search\fR (toggle search functionality)
\fBtoggle-search\fR (toggle search functionality)
\fBtoggle-sort\fR
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
\fBtransform-prompt(...)\fR (transform prompt string using an external command)
\fBtransform-query(...)\fR (transform query string using an external command)
\fBunbind(...)\fR (unbind bindings)
\fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR
\fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
\fBtransform-border-label(...)\fR (transform border label 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)
\fBunbind(...)\fR (unbind bindings)
\fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR
\fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR
.SS ACTION COMPOSITION
@@ -1119,6 +1171,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

View File

@@ -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

View File

@@ -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,8 +310,8 @@ complete -o default -F _fzf_opts_completion fzf-tmux
d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}"
a_cmds="
awk cat diff diff3
emacs emacsclient ex file ftp g++ gcc gvim head hg java
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
basename bunzip2 bzip2 chmod chown curl cp dirname du
@@ -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

View File

@@ -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
)
}

View File

@@ -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}
}

View File

@@ -17,6 +17,7 @@ type Merger struct {
tac bool
final bool
count int
pass bool
}
// PassMerger returns a new Merger that simply returns the items in the
@@ -26,7 +27,8 @@ func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
pattern: nil,
chunks: chunks,
tac: tac,
count: 0}
count: 0,
pass: true}
for _, chunk := range *mg.chunks {
mg.count += chunk.count
@@ -58,6 +60,19 @@ func (mg *Merger) Length() int {
return mg.count
}
// FindIndex returns the index of the item with the given item index
func (mg *Merger) FindIndex(itemIndex int32) int {
if mg.pass {
return int(itemIndex)
}
for i := 0; i < mg.count; i++ {
if mg.Get(i).item.Index() == itemIndex {
return i
}
}
return -1
}
// Get returns the pointer to the Result object indexed by the given integer
func (mg *Merger) Get(idx int) Result {
if mg.chunks != nil {

View File

@@ -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
@@ -70,7 +72,7 @@ const usage = `usage: fzf [options]
(default: 0 or center)
--margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L)
--padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
--info=STYLE Finder info style [default|inline|hidden]
--info=STYLE Finder info style [default|hidden|inline|inline:SEPARATOR]
--separator=STR String to form horizontal separator on info line
--no-separator Hide info line separator
--scrollbar[=CHAR] Scrollbar character
@@ -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
@@ -125,6 +127,8 @@ const usage = `usage: fzf [options]
`
const defaultInfoSep = " < "
// Case denotes case-sensitivity of search
type Case int
@@ -246,6 +250,10 @@ func (a previewOpts) sameContentLayout(b previewOpts) bool {
return a.wrap == b.wrap && a.headerLines == b.headerLines
}
func firstLine(s string) string {
return strings.SplitN(s, "\n", 2)[0]
}
// Options stores the values of command-line options
type Options struct {
Fuzzy bool
@@ -259,6 +267,7 @@ type Options struct {
WithNth []Range
Delimiter Delimiter
Sort int
Track bool
Tac bool
Criteria []criterion
Multi int
@@ -277,6 +286,7 @@ type Options struct {
ScrollOff int
FileWord bool
InfoStyle infoStyle
InfoSep string
Separator *string
JumpLabels string
Prompt string
@@ -308,7 +318,7 @@ type Options struct {
PreviewLabel labelOpts
Unicode bool
Tabstop int
ListenPort int
ListenPort *int
ClearOnExit bool
Version bool
}
@@ -330,6 +340,7 @@ func defaultOptions() *Options {
WithNth: make([]Range, 0),
Delimiter: Delimiter{},
Sort: 1000,
Track: false,
Tac: false,
Criteria: []criterion{byScore, byLength},
Multi: 0,
@@ -609,6 +620,10 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
add(tui.Start)
case "load":
add(tui.Load)
case "focus":
add(tui.Focus)
case "one":
add(tui.One)
case "alt-enter", "alt-return":
chords[tui.CtrlAltKey('m')] = key
case "alt-space":
@@ -912,7 +927,7 @@ const (
func init() {
executeRegexp = regexp.MustCompile(
`(?si)[:+](execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`)
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`)
splitRegexp = regexp.MustCompile("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
}
@@ -954,7 +969,7 @@ Loop:
ce = regexp.QuoteMeta(ce)
// @$ or @+
loc = regexp.MustCompile(fmt.Sprintf(`^%s.*?(%s[+,]|%s$)`, cs, ce, ce)).FindStringIndex(action)
loc = regexp.MustCompile(fmt.Sprintf(`(?s)^%s.*?(%s[+,]|%s$)`, cs, ce, ce)).FindStringIndex(action)
if loc == nil {
masked += action
break
@@ -1102,6 +1117,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":
@@ -1158,6 +1177,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:
@@ -1210,6 +1233,8 @@ func isExecuteAction(str string) actionType {
prefix := actionNameRegexp.FindString(str)
switch prefix {
case "become":
return actBecome
case "reload":
return actReload
case "reload-sync":
@@ -1220,6 +1245,10 @@ func isExecuteAction(str string) actionType {
return actRebind
case "preview":
return actPreview
case "change-border-label":
return actChangeBorderLabel
case "change-preview-label":
return actChangePreviewLabel
case "change-preview-window":
return actChangePreviewWindow
case "change-preview":
@@ -1238,6 +1267,10 @@ func isExecuteAction(str string) actionType {
return actExecuteMulti
case "put":
return actPut
case "transform-border-label":
return actTransformBorderLabel
case "transform-preview-label":
return actTransformPreviewLabel
case "transform-prompt":
return actTransformPrompt
case "transform-query":
@@ -1309,18 +1342,22 @@ func parseLayout(str string) layoutType {
return layoutDefault
}
func parseInfoStyle(str string) infoStyle {
func parseInfoStyle(str string) (infoStyle, string) {
switch str {
case "default":
return infoDefault
return infoDefault, ""
case "inline":
return infoInline
return infoInline, defaultInfoSep
case "hidden":
return infoHidden
return infoHidden, ""
default:
errorExit("invalid info style (expected: default|inline|hidden)")
prefix := "inline:"
if strings.HasPrefix(str, prefix) {
return infoInline, strings.ReplaceAll(str[len(prefix):], "\n", " ")
}
errorExit("invalid info style (expected: default|hidden|inline|inline:SEPARATOR)")
}
return infoDefault
return infoDefault, ""
}
func parsePreviewWindow(opts *previewOpts, input string) {
@@ -1530,6 +1567,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Sort = optionalNumeric(allArgs, &i, 1)
case "+s", "--no-sort":
opts.Sort = 0
case "--track":
opts.Track = true
case "--no-track":
opts.Track = false
case "--tac":
opts.Tac = true
case "--no-tac":
@@ -1588,12 +1629,13 @@ func parseOptions(opts *Options, allArgs []string) {
case "--no-filepath-word":
opts.FileWord = false
case "--info":
opts.InfoStyle = parseInfoStyle(
opts.InfoStyle, opts.InfoSep = parseInfoStyle(
nextString(allArgs, &i, "info style required"))
case "--no-info":
opts.InfoStyle = infoHidden
case "--inline-info":
opts.InfoStyle = infoInline
opts.InfoSep = defaultInfoSep
case "--no-inline-info":
opts.InfoStyle = infoDefault
case "--separator":
@@ -1640,10 +1682,10 @@ func parseOptions(opts *Options, allArgs []string) {
case "--prompt":
opts.Prompt = nextString(allArgs, &i, "prompt string required")
case "--pointer":
opts.Pointer = nextString(allArgs, &i, "pointer sign string required")
opts.Pointer = firstLine(nextString(allArgs, &i, "pointer sign string required"))
validatePointer = true
case "--marker":
opts.Marker = nextString(allArgs, &i, "selected sign string required")
opts.Marker = firstLine(nextString(allArgs, &i, "selected sign string required"))
validateMarker = true
case "--sync":
opts.Sync = true
@@ -1723,9 +1765,10 @@ func parseOptions(opts *Options, allArgs []string) {
case "--tabstop":
opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
case "--listen":
opts.ListenPort = nextInt(allArgs, &i, "listen port required")
port := optionalNumeric(allArgs, &i, 0)
opts.ListenPort = &port
case "--no-listen":
opts.ListenPort = 0
opts.ListenPort = nil
case "--clear":
opts.ClearOnExit = true
case "--no-clear":
@@ -1758,10 +1801,10 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--prompt="); match {
opts.Prompt = value
} else if match, value := optString(arg, "--pointer="); match {
opts.Pointer = value
opts.Pointer = firstLine(value)
validatePointer = true
} else if match, value := optString(arg, "--marker="); match {
opts.Marker = value
opts.Marker = firstLine(value)
validateMarker = true
} else if match, value := optString(arg, "-n", "--nth="); match {
opts.Nth = splitNth(value)
@@ -1778,7 +1821,7 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--layout="); match {
opts.Layout = parseLayout(value)
} else if match, value := optString(arg, "--info="); match {
opts.InfoStyle = parseInfoStyle(value)
opts.InfoStyle, opts.InfoSep = parseInfoStyle(value)
} else if match, value := optString(arg, "--separator="); match {
opts.Separator = &value
} else if match, value := optString(arg, "--scrollbar="); match {
@@ -1816,7 +1859,8 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--tabstop="); match {
opts.Tabstop = atoi(value)
} else if match, value := optString(arg, "--listen="); match {
opts.ListenPort = atoi(value)
port := atoi(value)
opts.ListenPort = &port
} else if match, value := optString(arg, "--hscroll-off="); match {
opts.HscrollOff = atoi(value)
} else if match, value := optString(arg, "--scroll-off="); match {
@@ -1846,7 +1890,7 @@ func parseOptions(opts *Options, allArgs []string) {
errorExit("tab stop must be a positive integer")
}
if opts.ListenPort < 0 || opts.ListenPort > 65535 {
if opts.ListenPort != nil && (*opts.ListenPort < 0 || *opts.ListenPort > 65535) {
errorExit("invalid listen port")
}
@@ -1968,9 +2012,7 @@ func postProcessOptions(opts *Options) {
theme := opts.Theme
boldify := func(c tui.ColorAttr) tui.ColorAttr {
dup := c
if !theme.Colored {
dup.Attr |= tui.Bold
} else if (c.Attr & tui.AttrRegular) == 0 {
if (c.Attr & tui.AttrRegular) == 0 {
dup.Attr |= tui.Bold
}
return dup

View File

@@ -268,7 +268,7 @@ func TestBind(t *testing.T) {
}
parseKeymap(keymap,
"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
"f1:execute(ls {+})+abort+execute(echo {+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
"f1:execute(ls {+})+abort+execute(echo \n{+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
"alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
"x:Execute(foo+bar),X:execute/bar+baz/"+
",f1:+first,f1:+top"+

View File

@@ -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

View File

@@ -6,6 +6,7 @@ import (
"io/ioutil"
"math"
"os"
"os/exec"
"os/signal"
"regexp"
"sort"
@@ -104,7 +105,6 @@ type previewer struct {
version int64
lines []string
offset int
enabled bool
scrollable bool
final bool
following resumableState
@@ -147,6 +147,7 @@ type labelPrinter func(tui.Window, int)
type Terminal struct {
initDelay time.Duration
infoStyle infoStyle
infoSep string
separator labelPrinter
separatorLen int
spinner []string
@@ -182,6 +183,7 @@ type Terminal struct {
multi int
sort bool
toggleSort bool
track bool
delimiter Delimiter
expect map[tui.Event]string
keymap map[tui.Event][]*action
@@ -200,9 +202,8 @@ type Terminal struct {
tabstop int
margin [4]sizeSpec
padding [4]sizeSpec
strong tui.Attr
unicode bool
listenPort int
listenPort *int
borderShape tui.BorderShape
cleanExit bool
paused bool
@@ -276,6 +277,8 @@ const (
reqRefresh
reqReinit
reqFullRedraw
reqRedrawBorderLabel
reqRedrawPreviewLabel
reqClose
reqPrintQuery
reqPreviewEnqueue
@@ -306,6 +309,8 @@ const (
actBackwardDeleteCharEOF
actBackwardWord
actCancel
actChangeBorderLabel
actChangePreviewLabel
actChangePrompt
actChangeQuery
actClearScreen
@@ -345,8 +350,12 @@ const (
actRefreshPreview
actReplaceQuery
actToggleSort
actShowPreview
actHidePreview
actTogglePreview
actTogglePreviewWrap
actTransformBorderLabel
actTransformPreviewLabel
actTransformPrompt
actTransformQuery
actPreview
@@ -379,6 +388,7 @@ const (
actDeselect
actUnbind
actRebind
actBecome
)
type placeholderFlags struct {
@@ -528,13 +538,9 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
}
var previewBox *util.EventBox
// We need to start previewer if HTTP server is enabled even when --preview option is not specified
if len(opts.Preview.command) > 0 || hasPreviewAction(opts) || opts.ListenPort > 0 {
if len(opts.Preview.command) > 0 || hasPreviewAction(opts) || opts.ListenPort != nil {
previewBox = util.NewEventBox()
}
strongAttr := tui.Bold
if !opts.Bold {
strongAttr = tui.AttrRegular
}
var renderer tui.Renderer
fullscreen := !opts.Height.auto && (opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100)
if fullscreen {
@@ -573,6 +579,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
t := Terminal{
initDelay: delay,
infoStyle: opts.InfoStyle,
infoSep: opts.InfoSep,
separator: nil,
spinner: makeSpinner(opts.Unicode),
queryLen: [2]int{0, 0},
@@ -593,6 +600,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
multi: opts.Multi,
sort: opts.Sort > 0,
toggleSort: opts.ToggleSort,
track: opts.Track,
delimiter: opts.Delimiter,
expect: opts.Expect,
keymap: opts.Keymap,
@@ -612,7 +620,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
previewLabelOpts: opts.PreviewLabel,
cleanExit: opts.ClearOnExit,
paused: opts.Phony,
strong: strongAttr,
cycle: opts.Cycle,
headerFirst: opts.HeaderFirst,
headerLines: opts.HeaderLines,
@@ -635,7 +642,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,
@@ -683,13 +690,25 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
if err := startHttpServer(t.listenPort, t.serverChan); err != nil {
errorExit(err.Error())
if t.listenPort != nil {
err, port := startHttpServer(*t.listenPort, t.serverChan)
if err != nil {
errorExit(err.Error())
}
t.listenPort = &port
}
return &t
}
func (t *Terminal) environ() []string {
env := os.Environ()
if t.listenPort != nil {
env = append(env, fmt.Sprintf("FZF_PORT=%d", *t.listenPort))
}
return env
}
func borderLines(shape tui.BorderShape) int {
switch shape {
case tui.BorderHorizontal, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderDouble:
@@ -723,12 +742,13 @@ func (t *Terminal) ansiLabelPrinter(str string, color *tui.ColorPair, fill bool)
}
// Extract ANSI color codes
str = firstLine(str)
text, colors, _ := extractColor(str, nil, nil)
runes := []rune(text)
// 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
}
@@ -777,6 +797,7 @@ func (t *Terminal) ansiLabelPrinter(str string, color *tui.ColorPair, fill bool)
func (t *Terminal) parsePrompt(prompt string) (func(), int) {
var state *ansiState
prompt = firstLine(prompt)
trimmed, colors, _ := extractColor(prompt, state, nil)
item := &Item{text: util.ToChars([]byte(trimmed)), colors: colors}
@@ -885,6 +906,10 @@ func (t *Terminal) UpdateProgress(progress float32) {
// UpdateList updates Merger to display the list
func (t *Terminal) UpdateList(merger *Merger, reset bool) {
t.mutex.Lock()
var prevIndex int32 = -1
if !reset && t.track && t.merger.Length() > 0 {
prevIndex = t.merger.Get(t.cy).item.Index()
}
t.progress = 100
t.merger = merger
if reset {
@@ -895,6 +920,24 @@ func (t *Terminal) UpdateList(merger *Merger, reset bool) {
t.triggerLoad = false
t.eventChan <- tui.Load.AsEvent()
}
if prevIndex >= 0 {
pos := t.cy - t.offset
count := t.merger.Length()
i := t.merger.FindIndex(prevIndex)
if i >= 0 {
t.cy = i
t.offset = t.cy - pos
} else if t.cy > count {
// Try to keep the vertical position when the list shrinks
t.cy = count - util.Min(count, t.maxItems()) + pos
}
}
if !t.reading && t.merger.Length() == 1 {
one := tui.One.AsEvent()
if _, prs := t.keymap[one]; prs {
t.eventChan <- one
}
}
t.mutex.Unlock()
t.reqBox.Set(reqInfo, nil)
t.reqBox.Set(reqList, nil)
@@ -1022,7 +1065,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 {
@@ -1105,7 +1148,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
@@ -1234,6 +1277,8 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
}
}
resizePreviewWindows(&t.previewOpts)
} else {
t.activePreviewOpts = &t.previewOpts
}
// Without preview window
@@ -1250,37 +1295,44 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
}
// Print border label
printLabel := func(window tui.Window, render labelPrinter, opts labelOpts, length int, borderShape tui.BorderShape) {
if window == nil || render == nil {
return
}
switch borderShape {
case tui.BorderHorizontal, tui.BorderTop, tui.BorderBottom, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderDouble:
var col int
if opts.column == 0 {
col = util.Max(0, (window.Width()-length)/2)
} else if opts.column < 0 {
col = util.Max(0, window.Width()+opts.column+1-length)
} else {
col = util.Min(opts.column-1, window.Width()-length)
}
row := 0
if borderShape == tui.BorderBottom || opts.bottom {
row = window.Height() - 1
}
window.Move(row, col)
render(window, window.Width())
}
}
printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape)
printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.previewOpts.border)
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, false)
t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.previewOpts.border, false)
for i := 0; i < t.window.Height(); i++ {
t.window.MoveAndClear(i, 0)
}
}
func (t *Terminal) printLabel(window tui.Window, render labelPrinter, opts labelOpts, length int, borderShape tui.BorderShape, redrawBorder bool) {
if window == nil {
return
}
switch borderShape {
case tui.BorderHorizontal, tui.BorderTop, tui.BorderBottom, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderDouble:
if redrawBorder {
window.DrawHBorder()
}
if render == nil {
return
}
var col int
if opts.column == 0 {
col = util.Max(0, (window.Width()-length)/2)
} else if opts.column < 0 {
col = util.Max(0, window.Width()+opts.column+1-length)
} else {
col = util.Min(opts.column-1, window.Width()-length)
}
row := 0
if borderShape == tui.BorderBottom || opts.bottom {
row = window.Height() - 1
}
window.Move(row, col)
render(window, window.Width())
}
}
func (t *Terminal) move(y int, x int, clear bool) {
h := t.window.Height()
@@ -1316,8 +1368,7 @@ func (t *Terminal) updatePromptOffset() ([]rune, []rune) {
_, overflow := t.trimLeft(t.input[:t.cx], maxWidth)
minOffset := int(overflow)
maxOffset := util.Min(util.Min(len(t.input), minOffset+maxWidth), t.cx)
maxOffset := minOffset + (maxWidth-util.Max(0, maxWidth-t.cx))/2
t.xoffset = util.Constrain(t.xoffset, minOffset, maxOffset)
before, _ := t.trimLeft(t.input[t.xoffset:t.cx], maxWidth)
beforeLen := t.displayWidth(before)
@@ -1380,16 +1431,21 @@ func (t *Terminal) printInfo() {
pos = 2
case infoInline:
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
if pos+len(" < ") > t.window.Width() {
return
str := t.infoSep
maxWidth := t.window.Width() - pos
width := util.StringWidth(str)
if width > maxWidth {
trimmed, _ := t.trimRight([]rune(str), maxWidth)
str = string(trimmed)
width = maxWidth
}
t.move(line, pos, t.separatorLen == 0)
if t.reading {
t.window.CPrint(tui.ColSpinner, " < ")
t.window.CPrint(tui.ColSpinner, str)
} else {
t.window.CPrint(tui.ColPrompt, " < ")
t.window.CPrint(tui.ColPrompt, str)
}
pos += len(" < ")
pos += width
case infoHidden:
return
}
@@ -1802,12 +1858,14 @@ func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unc
trimmed, isTrimmed = t.trimRight(trimmed, maxWidth-t.pwindow.X())
}
str, width := t.processTabs(trimmed, prefixWidth)
prefixWidth += width
if t.theme.Colored && ansi != nil && ansi.colored() {
lbg = ansi.lbg
fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str)
} else {
fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str)
if width > prefixWidth {
prefixWidth = width
if t.theme.Colored && ansi != nil && ansi.colored() {
lbg = ansi.lbg
fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str)
} else {
fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str)
}
}
return !isTrimmed &&
(fillRet == tui.FillContinue || t.previewOpts.wrap && fillRet == tui.FillNextLine)
@@ -1910,7 +1968,7 @@ func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) {
w = t.tabstop - l%t.tabstop
strbuf.WriteString(strings.Repeat(" ", w))
} else {
w = runewidth.StringWidth(str)
w = util.StringWidth(str)
strbuf.WriteString(str)
}
l += w
@@ -2212,7 +2270,7 @@ func (t *Terminal) redraw() {
func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, captureFirstLine bool) string {
line := ""
valid, list := t.buildPlusList(template, forcePlus, false)
valid, list := t.buildPlusList(template, forcePlus)
// captureFirstLine is used for transform-{prompt,query} and we don't want to
// return an empty string in those cases
if !valid && !captureFirstLine {
@@ -2220,6 +2278,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
}
command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
cmd := util.ExecCommand(command, false)
cmd.Env = t.environ()
t.executing.Set(true)
if !background {
cmd.Stdin = tui.TtyIn()
@@ -2231,7 +2290,6 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
t.redraw()
t.refresh()
} else {
t.tui.Pause(false)
if captureFirstLine {
out, _ := cmd.StdoutPipe()
reader := bufio.NewReader(out)
@@ -2242,7 +2300,6 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
} else {
cmd.Run()
}
t.tui.Resume(false, false)
}
t.executing.Set(false)
cleanTemporaryFiles()
@@ -2253,13 +2310,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 {
@@ -2274,10 +2331,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}
}
@@ -2367,7 +2424,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))
@@ -2468,17 +2525,17 @@ func (t *Terminal) Loop() {
_, query := t.Input()
command := t.replacePlaceholder(commandTemplate, false, string(query), items)
cmd := util.ExecCommand(command, true)
env := t.environ()
if pwindow != nil {
height := pwindow.Height()
env := os.Environ()
lines := fmt.Sprintf("LINES=%d", height)
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
env = append(env, lines)
env = append(env, "FZF_PREVIEW_"+lines)
env = append(env, columns)
env = append(env, "FZF_PREVIEW_"+columns)
cmd.Env = env
}
cmd.Env = env
out, _ := cmd.StdoutPipe()
cmd.Stderr = cmd.Stdout
@@ -2601,13 +2658,18 @@ 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})
}
}
var onFocus []*action
if actions, prs := t.keymap[tui.Focus.AsEvent()]; prs {
onFocus = actions
}
go func() {
var focusedIndex int32 = minItem.Index()
var version int64 = -1
@@ -2643,7 +2705,11 @@ func (t *Terminal) Loop() {
if currentItem != nil {
currentIndex = currentItem.Index()
}
if focusedIndex != currentIndex || version != t.version {
focusChanged := focusedIndex != currentIndex
if onFocus != nil && focusChanged {
t.serverChan <- onFocus
}
if focusChanged || version != t.version {
version = t.version
focusedIndex = currentIndex
refreshPreview(t.previewOpts.command)
@@ -2657,13 +2723,17 @@ func (t *Terminal) Loop() {
t.printHeader()
case reqRefresh:
t.suppress = false
case reqRedrawBorderLabel:
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, true)
case reqRedrawPreviewLabel:
t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.previewOpts.border, true)
case reqReinit:
t.tui.Resume(t.fullscreen, t.sigstop)
t.redraw()
case reqFullRedraw:
wasHidden := t.pwindow == nil
t.redraw()
if wasHidden && t.pwindow != nil {
if wasHidden && t.hasPreviewWindow() {
refreshPreview(t.previewOpts.command)
}
case reqClose:
@@ -2824,6 +2894,24 @@ 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)
case actExecuteMulti:
@@ -2831,16 +2919,21 @@ func (t *Terminal) Loop() {
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,
@@ -2909,11 +3002,32 @@ func (t *Terminal) Loop() {
case actChangeQuery:
t.input = []rune(a.a)
t.cx = len(t.input)
case actChangeBorderLabel:
if t.border != nil {
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(a.a, &tui.ColBorderLabel, false)
req(reqRedrawBorderLabel)
}
case actChangePreviewLabel:
if t.pborder != nil {
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(a.a, &tui.ColPreviewLabel, false)
req(reqRedrawPreviewLabel)
}
case actTransformBorderLabel:
if t.border != nil {
label := t.executeCommand(a.a, false, 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)
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false)
req(reqRedrawPreviewLabel)
}
case actChangePrompt:
t.prompt, t.promptLen = t.parsePrompt(a.a)
req(reqPrompt)
case actPreview:
t.previewer.enabled = true
updatePreviewWindow(true)
refreshPreview(a.a)
case actRefreshPreview:
@@ -3207,7 +3321,7 @@ func (t *Terminal) Loop() {
break
}
// Prevew scrollbar dragging
// Preview scrollbar dragging
headerLines := t.previewOpts.headerLines
pbarDragging = me.Down && (pbarDragging || clicked && t.hasPreviewWindow() && my >= t.pwindow.Top()+headerLines && my < t.pwindow.Top()+t.pwindow.Height() && mx == t.pwindow.Left()+t.pwindow.Width())
if pbarDragging {
@@ -3298,7 +3412,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
@@ -3326,9 +3440,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:
@@ -3348,7 +3461,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)
@@ -3425,12 +3538,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++
}
}

View File

@@ -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() {
@@ -174,11 +182,7 @@ func (r *LightRenderer) Init() {
}
}
if r.mouse {
r.csi("?1000h")
r.csi("?1002h")
r.csi("?1006h")
}
r.enableMouse()
r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
r.csi("G")
r.csi("K")
@@ -609,6 +613,7 @@ func (r *LightRenderer) rmcup() {
}
func (r *LightRenderer) Pause(clear bool) {
r.disableMouse()
r.restoreTerminal()
if clear {
if r.fullscreen {
@@ -621,6 +626,22 @@ func (r *LightRenderer) Pause(clear bool) {
}
}
func (r *LightRenderer) enableMouse() {
if r.mouse {
r.csi("?1000h")
r.csi("?1002h")
r.csi("?1006h")
}
}
func (r *LightRenderer) disableMouse() {
if r.mouse {
r.csi("?1000l")
r.csi("?1002l")
r.csi("?1006l")
}
}
func (r *LightRenderer) Resume(clear bool, sigcont bool) {
r.setupTerminal()
if clear {
@@ -629,14 +650,13 @@ func (r *LightRenderer) Resume(clear bool, sigcont bool) {
} else {
r.rmcup()
}
r.enableMouse()
r.flush()
} else if sigcont && !r.fullscreen && r.mouse {
// NOTE: SIGCONT (Coming back from CTRL-Z):
// It's highly likely that the offset we obtained at the beginning is
// no longer correct, so we simply disable mouse input.
r.csi("?1000l")
r.csi("?1002l")
r.csi("?1006l")
r.disableMouse()
r.mouse = false
}
}
@@ -678,11 +698,7 @@ func (r *LightRenderer) Close() {
} else if !r.fullscreen {
r.csi("u")
}
if r.mouse {
r.csi("?1000l")
r.csi("?1002l")
r.csi("?1006l")
}
r.disableMouse()
r.flush()
r.closePlatform()
r.restoreTerminal()
@@ -719,25 +735,38 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, prev
w.fg = r.theme.Fg.Color
w.bg = r.theme.Bg.Color
}
w.drawBorder()
w.drawBorder(false)
return w
}
func (w *LightWindow) drawBorder() {
func (w *LightWindow) DrawHBorder() {
w.drawBorder(true)
}
func (w *LightWindow) drawBorder(onlyHorizontal bool) {
switch w.border.shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble:
w.drawBorderAround()
w.drawBorderAround(onlyHorizontal)
case BorderHorizontal:
w.drawBorderHorizontal(true, true)
case BorderVertical:
if onlyHorizontal {
return
}
w.drawBorderVertical(true, true)
case BorderTop:
w.drawBorderHorizontal(true, false)
case BorderBottom:
w.drawBorderHorizontal(false, true)
case BorderLeft:
if onlyHorizontal {
return
}
w.drawBorderVertical(true, false)
case BorderRight:
if onlyHorizontal {
return
}
w.drawBorderVertical(false, true)
}
}
@@ -779,35 +808,37 @@ func (w *LightWindow) drawBorderVertical(left, right bool) {
}
}
func (w *LightWindow) drawBorderAround() {
func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
w.Move(0, 0)
color := ColBorder
if w.preview {
color = ColPreviewBorder
}
hw := runewidth.RuneWidth(w.border.horizontal)
vw := runewidth.RuneWidth(w.border.vertical)
tcw := runewidth.RuneWidth(w.border.topLeft) + runewidth.RuneWidth(w.border.topRight)
bcw := runewidth.RuneWidth(w.border.bottomLeft) + runewidth.RuneWidth(w.border.bottomRight)
rem := (w.width - tcw) % hw
w.CPrint(color, string(w.border.topLeft)+repeat(w.border.horizontal, (w.width-tcw)/hw)+repeat(' ', rem)+string(w.border.topRight))
for y := 1; y < w.height-1; y++ {
w.Move(y, 0)
w.CPrint(color, string(w.border.vertical))
w.CPrint(color, repeat(' ', w.width-vw*2))
w.CPrint(color, string(w.border.vertical))
if !onlyHorizontal {
vw := runewidth.RuneWidth(w.border.vertical)
for y := 1; y < w.height-1; y++ {
w.Move(y, 0)
w.CPrint(color, string(w.border.vertical))
w.CPrint(color, repeat(' ', w.width-vw*2))
w.CPrint(color, string(w.border.vertical))
}
}
w.Move(w.height-1, 0)
rem = (w.width - bcw) % hw
w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.horizontal, (w.width-bcw)/hw)+repeat(' ', rem)+string(w.border.bottomRight))
}
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 {
@@ -913,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) {
@@ -928,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 {
@@ -957,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)
}
@@ -975,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
@@ -990,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)
}
}
}
@@ -999,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 {
@@ -1025,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() {
@@ -1040,7 +1078,7 @@ func (w *LightWindow) FinishFill() {
}
func (w *LightWindow) Erase() {
w.drawBorder()
w.drawBorder(false)
// We don't erase the window here to avoid flickering during scroll
w.Move(0, 0)
}

View File

@@ -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"
@@ -512,7 +513,7 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
height: height,
normal: normal,
borderStyle: borderStyle}
w.drawBorder()
w.drawBorder(false)
return w
}
@@ -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 {
@@ -670,7 +681,11 @@ func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
return w.fillString(str, NewColorPair(fg, bg, a))
}
func (w *TcellWindow) drawBorder() {
func (w *TcellWindow) DrawHBorder() {
w.drawBorder(true)
}
func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
shape := w.borderStyle.shape
if shape == BorderNone {
return
@@ -718,17 +733,19 @@ func (w *TcellWindow) drawBorder() {
_screen.SetContent(x, bot-1, w.borderStyle.horizontal, nil, style)
}
}
switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderLeft:
for y := top; y < bot; y++ {
_screen.SetContent(left, y, w.borderStyle.vertical, nil, style)
if !onlyHorizontal {
switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderLeft:
for y := top; y < bot; y++ {
_screen.SetContent(left, y, w.borderStyle.vertical, nil, style)
}
}
}
switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderRight:
vw := runewidth.RuneWidth(w.borderStyle.vertical)
for y := top; y < bot; y++ {
_screen.SetContent(right-vw, y, w.borderStyle.vertical, nil, style)
switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderRight:
vw := runewidth.RuneWidth(w.borderStyle.vertical)
for y := top; y < bot; y++ {
_screen.SetContent(right-vw, y, w.borderStyle.vertical, nil, style)
}
}
}
switch shape {

View File

@@ -92,6 +92,8 @@ const (
BackwardEOF
Start
Load
Focus
One
AltBS
@@ -426,6 +428,7 @@ type Window interface {
Width() int
Height() int
DrawHBorder()
Refresh()
FinishFill()
Close()
@@ -523,28 +526,28 @@ func EmptyTheme() *ColorTheme {
func NoColorTheme() *ColorTheme {
return &ColorTheme{
Colored: false,
Input: ColorAttr{colDefault, AttrRegular},
Fg: ColorAttr{colDefault, AttrRegular},
Bg: ColorAttr{colDefault, AttrRegular},
DarkBg: ColorAttr{colDefault, AttrRegular},
Prompt: ColorAttr{colDefault, AttrRegular},
Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{colDefault, AttrUndefined},
Prompt: ColorAttr{colDefault, AttrUndefined},
Match: ColorAttr{colDefault, Underline},
Current: ColorAttr{colDefault, Reverse},
CurrentMatch: ColorAttr{colDefault, Reverse | Underline},
Spinner: ColorAttr{colDefault, AttrRegular},
Info: ColorAttr{colDefault, AttrRegular},
Cursor: ColorAttr{colDefault, AttrRegular},
Selected: ColorAttr{colDefault, AttrRegular},
Header: ColorAttr{colDefault, AttrRegular},
Border: ColorAttr{colDefault, AttrRegular},
BorderLabel: ColorAttr{colDefault, AttrRegular},
Disabled: ColorAttr{colDefault, AttrRegular},
PreviewFg: ColorAttr{colDefault, AttrRegular},
PreviewBg: ColorAttr{colDefault, AttrRegular},
Gutter: ColorAttr{colDefault, AttrRegular},
PreviewLabel: ColorAttr{colDefault, AttrRegular},
Separator: ColorAttr{colDefault, AttrRegular},
Scrollbar: ColorAttr{colDefault, AttrRegular},
Spinner: ColorAttr{colDefault, AttrUndefined},
Info: ColorAttr{colDefault, AttrUndefined},
Cursor: ColorAttr{colDefault, AttrUndefined},
Selected: ColorAttr{colDefault, AttrUndefined},
Header: ColorAttr{colDefault, AttrUndefined},
Border: ColorAttr{colDefault, AttrUndefined},
BorderLabel: ColorAttr{colDefault, AttrUndefined},
Disabled: ColorAttr{colDefault, AttrUndefined},
PreviewFg: ColorAttr{colDefault, AttrUndefined},
PreviewBg: ColorAttr{colDefault, AttrUndefined},
Gutter: ColorAttr{colDefault, AttrUndefined},
PreviewLabel: ColorAttr{colDefault, AttrUndefined},
Separator: ColorAttr{colDefault, AttrUndefined},
Scrollbar: ColorAttr{colDefault, AttrUndefined},
}
}

View File

@@ -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
}

View File

@@ -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)
}
@@ -115,7 +115,7 @@ func TestAsUint16(t *testing.T) {
if AsUint16(math.MinInt16) != 0 {
t.Error("Expected", 0)
}
if AsUint16(math.MaxUint32) != math.MaxUint16 {
if AsUint16(math.MaxUint16+1) != math.MaxUint16 {
t.Error("Expected", math.MaxUint16)
}
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
@@ -1587,6 +1658,11 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal '> 1', lines[-2] }
end
def test_info_inline_separator
tmux.send_keys 'seq 10 | fzf --info=inline:___ --no-separator', :Enter
tmux.until { |lines| assert_equal '> ___10/10', lines[-1] }
end
def test_change_first_last
tmux.send_keys %(seq 1000 | #{FZF} --bind change:first,alt-Z:last), :Enter
tmux.until { |lines| assert_equal 1000, lines.match_count }
@@ -1854,7 +1930,7 @@ class TestGoFZF < TestBase
def test_keep_right
tmux.send_keys "seq 10000 | #{FZF} --read0 --keep-right", :Enter
tmux.until { |lines| assert lines.any_include?('9999 10000') }
tmux.until { |lines| assert lines.any_include?('999910000') }
end
def test_backward_eof
@@ -2082,11 +2158,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
@@ -2104,11 +2176,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
@@ -2129,11 +2197,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
@@ -2153,11 +2217,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
@@ -2473,12 +2533,31 @@ class TestGoFZF < TestBase
end
end
def test_focus_event
tmux.send_keys 'seq 100 | fzf --bind "focus:transform-prompt(echo [[{}]])"', :Enter
tmux.until { |lines| assert_includes(lines[-1], '[[1]]') }
tmux.send_keys :Up
tmux.until { |lines| assert_includes(lines[-1], '[[2]]') }
tmux.send_keys :X
tmux.until { |lines| assert_includes(lines[-1], '[[]]') }
end
def test_labels_center
tmux.send_keys ': | fzf --border --border-label foobar --preview : --preview-label barfoo', :Enter
tmux.send_keys 'echo x | fzf --border --border-label foobar --preview : --preview-label barfoo --bind "space:change-border-label(foobarfoo)+change-preview-label(barfoobar),enter:transform-border-label(echo foo{}foo)+transform-preview-label(echo bar{}bar)"', :Enter
tmux.until do
assert_includes(_1[0], '─foobar─')
assert_includes(_1[1], '─barfoo─')
end
tmux.send_keys :space
tmux.until do
assert_includes(_1[0], '─foobarfoo─')
assert_includes(_1[1], '─barfoobar─')
end
tmux.send_keys :Enter
tmux.until do
assert_includes(_1[0], '─fooxfoo─')
assert_includes(_1[1], '─barxbar─')
end
end
def test_labels_left
@@ -2550,11 +2629,17 @@ class TestGoFZF < TestBase
end
def test_listen
tmux.send_keys 'seq 10 | fzf --listen 6266', :Enter
tmux.until { |lines| assert_equal 10, lines.item_count }
Net::HTTP.post(URI('http://localhost:6266'), 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ')
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
{ '--listen 6266' => -> { URI('http://localhost:6266') },
"--listen --sync --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'" =>
-> { URI("http://localhost:#{File.read('/tmp/fzf-port').chomp}") } }.each do |opts, fn|
tmux.send_keys "seq 10 | fzf #{opts}", :Enter
tmux.until { |lines| assert_equal 10, lines.item_count }
Net::HTTP.post(fn.call, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ')
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
teardown
setup
end
end
def test_toggle_alternative_preview_window
@@ -2564,6 +2649,80 @@ 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", :Enter
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> 555'
end
tmux.send_keys :BSpace
index = tmux.until do |lines|
assert_equal 28, lines.match_count
assert_includes lines, '> 555'
end.index('> 555')
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 271, lines.match_count
assert_equal '> 555', lines[index]
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 1000, lines.match_count
assert_equal '> 555', lines[index]
end
end
def test_one
tmux.send_keys "seq 10 | #{FZF} --bind 'one:preview:echo {} is the only match'", :Enter
tmux.send_keys '1'
tmux.until do |lines|
assert_equal 2, lines.match_count
refute(lines.any? { _1.include?('only match') })
end
tmux.send_keys '0'
tmux.until do |lines|
assert_equal 1, lines.match_count
assert(lines.any? { _1.include?('only match') })
end
end
def test_height_range_with_exit_0
tmux.send_keys "seq 10 | #{FZF} --height ~10% --exit-0", :Enter
tmux.until { |lines| assert_equal 10, lines.item_count }
tmux.send_keys :c
tmux.until { |lines| assert_equal 0, lines.match_count }
end
end
module TestShell
@@ -2692,9 +2851,9 @@ module TestShell
tmux.send_keys 'C-r'
tmux.until { |lines| assert_equal '>', lines[-1] }
tmux.send_keys 'foo bar'
tmux.until { |lines| assert lines[-3]&.end_with?('bar"') }
tmux.until { |lines| assert lines[-3]&.match?(/bar"␊?/) }
tmux.send_keys :Enter
tmux.until { |lines| assert lines[-1]&.end_with?('bar"') }
tmux.until { |lines| assert lines[-1]&.match?(/bar"␊?/) }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
end

6
typos.toml Normal file
View 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"