m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-15 14:53:47 -05:00

Compare commits

...

60 Commits

Author SHA1 Message Date
Junegunn Choi
098ef4d7cf 0.41.0 2023-05-26 00:25:09 +09:00
Junegunn Choi
e3f91bfe1b Use Golang 1.20.4 2023-05-26 00:08:20 +09:00
Junegunn Choi
7374fe73a3 Avoid setting $FZF_DEFAULT_COMMAND
So that it's not propagated to the child processes and affect the
behavior of fzf started by them.

fzf 0.41.0 or above is required as it fixed the issue where
'become' process is not given a proper tty device.

Close #3299
2023-05-26 00:08:20 +09:00
dependabot[bot]
d2bde205f0 Bump golang.org/x/term from 0.7.0 to 0.8.0 (#3285)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.7.0 to 0.8.0.
- [Commits](https://github.com/golang/term/compare/v0.7.0...v0.8.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-05-23 23:55:57 +09:00
dependabot[bot]
5620f70f9a Bump crate-ci/typos from 1.13.16 to 1.14.10 (#3306)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.13.16 to 1.14.10.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.13.16...v1.14.10)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  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-05-23 23:55:34 +09:00
Syphdias
37f258b1bf Add key combinations for ctrl-delete and shift-delete (#3284)
Currently there is not option to bind ctrl-delete and shift-delete. As
suggested by issue #3240, shift-delete could be used to bind "delete
entry from history" as it is a common way to do so in other
applications, e.g. browsers.

This, however, does only implement to use the key combination itself and
does not assign a default action to any of them. This does enable to
call one's all predefined actions. With the exec action this can
expanded like the issue #3240 suggested.
If desirable, the key combinations could later get a default behavior.

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2023-05-21 18:40:05 +09:00
Junegunn Choi
e2dd2a133e Skip post hooks on 'make build'
https://github.com/goreleaser/goreleaser/issues/1469
2023-05-21 18:00:17 +09:00
Junegunn Choi
7514644e07 Update Dockerfile: --platform=linux/amd64 2023-05-21 17:59:18 +09:00
Junegunn Choi
16b0aeda7d Make sure 'become' process is given a proper tty device 2023-05-21 14:01:27 +09:00
Junegunn Choi
86e4f4a841 Update tcell renderer to support block border 2023-05-20 18:24:23 +09:00
Junegunn Choi
607eacf8c7 Allow unbind(focus)
Fix #3279
2023-05-17 10:58:03 +09:00
Junegunn Choi
7a049644a8 Fix panic when trying to render preview window of a negative height
Fix #3292
2023-05-17 00:06:35 +09:00
Junegunn Choi
17a13f00f8 Allow customizing scrollbar of the preview window via --scrollbar=xy 2023-05-16 23:59:08 +09:00
Junegunn Choi
43436e48e0 Add new border style: 'block' 2023-05-16 23:45:31 +09:00
Junegunn Choi
5a39102405 Allow customizing the color of preview scrollbar via 'preview-scrollbar' 2023-05-16 23:24:05 +09:00
Junegunn Choi
94999101e3 Fix the behavior of change-preview-window action (#3280)
* change-preview-window restores the initial preview window options,
  and overrides the properties that are specified
* However, 'hidden' property is treated differently. It is set to
  'false' if the specified properties of the action is non-empty.
* cf. toggle-preview takes the "current" preview window options and
  toggles the 'hidden' property.
2023-05-05 15:33:03 +09:00
Junegunn Choi
e619b7c4f4 Fix the background color of the scrollbar inside the preview window 2023-05-01 16:18:26 +09:00
Junegunn Choi
b7c2e8cb67 Fix caching when reload and query change triggered by the same binding 2023-05-01 13:53:34 +09:00
Junegunn Choi
fb76893e18 0.40.0 2023-05-01 01:59:21 +09:00
Junegunn Choi
88d812fe82 Do not display trailing carriage returns in the preview window
Close #3269
2023-04-30 18:14:40 +09:00
Junegunn Choi
77f9f4664a Fix search not triggered when query change and reload happen at the same time
Fix #3268
2023-04-30 18:14:40 +09:00
dependabot[bot]
5c2f85c39e Bump golang.org/x/term from 0.6.0 to 0.7.0 (#3249)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/golang/term/releases)
- [Commits](https://github.com/golang/term/compare/v0.6.0...v0.7.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-04-28 23:24:56 +09:00
dependabot[bot]
ac4d22cd12 Bump golang.org/x/sys from 0.6.0 to 0.7.0 (#3248)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/golang/sys/releases)
- [Commits](https://github.com/golang/sys/compare/v0.6.0...v0.7.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-04-28 23:20:28 +09:00
Junegunn Choi
cf95e44cb4 Add 'zero' event
Close #3263
2023-04-26 15:13:08 +09:00
Junegunn Choi
65dd2bb429 Add 'track' action 2023-04-22 23:42:09 +09:00
Junegunn Choi
6be855be6a Add change-header and transform-header
Close #3237
2023-04-22 22:01:37 +09:00
Junegunn Choi
b6e3f4423b [man] Suggest setting RUNEWIDTH_EASTASIAN to 0 or 1
Close #2389
2023-04-22 16:35:46 +09:00
Junegunn Choi
0c61d81713 Add toggle-track action 2023-04-22 15:48:51 +09:00
Junegunn Choi
7c6f5dba63 Fixed --track when used with --tac
Fix #3234
2023-04-22 15:09:43 +09:00
psarlov
44cfc7e62a [vim] Add check for powershell 7 users (#3257)
Co-authored-by: Pavel Sarlov <psarlov@asteasolutions.com>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2023-04-21 20:23:03 +09:00
Junegunn Choi
96670d5f16 Disallow using --track with --tac
Close #3234
2023-04-12 13:23:10 +09:00
Junegunn Choi
36b971ee4e [fzf-tmux] Try awk before bc 2023-04-11 16:29:29 +09:00
Junegunn Choi
f1a9629652 [fzf-tmux] Use awk if bc is not found
Fix #3235
2023-04-06 13:43:18 +09:00
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
35 changed files with 1209 additions and 439 deletions

View File

@@ -20,7 +20,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v4
with: with:
go-version: 1.19 go-version: 1.19

View File

@@ -20,7 +20,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v4
with: with:
go-version: 1.18 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.14.10

View File

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

View File

@@ -1 +1 @@
golang 1.19 golang 1.20.4

View File

@@ -1,8 +1,8 @@
Advanced fzf examples Advanced fzf examples
====================== ======================
* *Last update: 2023/02/15* * *Last update: 2023/05/26*
* *Requires fzf 0.38.0 or above* * *Requires fzf 0.41.0 or above*
--- ---
@@ -336,9 +336,8 @@ projects, and it will free up memory as you narrow down the results.
# 3. Open the file in Vim # 3. Open the file in Vim
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}" INITIAL_QUERY="${*:-}"
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \ : | fzf --ansi --disabled --query "$INITIAL_QUERY" \
fzf --ansi \ --bind "start:reload:$RG_PREFIX {q}" \
--disabled --query "$INITIAL_QUERY" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \ --bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--delimiter : \ --delimiter : \
--preview 'bat --color=always {1} --highlight-line {2}' \ --preview 'bat --color=always {1} --highlight-line {2}' \
@@ -348,11 +347,11 @@ fzf --ansi \
![image](https://user-images.githubusercontent.com/700826/113684212-f9ff0a00-96ff-11eb-8737-7bb571d320cc.png) ![image](https://user-images.githubusercontent.com/700826/113684212-f9ff0a00-96ff-11eb-8737-7bb571d320cc.png)
- Instead of starting fzf in `rg ... | fzf` form, we start fzf without an - Instead of starting fzf in the usual `rg ... | fzf` form, we start fzf with
explicit input, but with a custom `FZF_DEFAULT_COMMAND` variable. This way an empty input (`: | fzf`), then we make it start the initial Ripgrep
fzf can kill the initial Ripgrep process it starts with the initial query. process immediately via `start:reload` binding. This way, fzf owns the
Otherwise, the initial Ripgrep process will keep consuming system resources initial Ripgrep process so it can kill it on the next `reload`. Otherwise,
even after `reload` is triggered. the process will keep running in the background.
- Filtering is no longer a responsibility of fzf; hence `--disabled` - Filtering is no longer a responsibility of fzf; hence `--disabled`
- `{q}` in the reload command evaluates to the query string on fzf prompt. - `{q}` in the reload command evaluates to the query string on fzf prompt.
- `sleep 0.1` in the reload command is for "debouncing". This small delay will - `sleep 0.1` in the reload command is for "debouncing". This small delay will
@@ -376,12 +375,11 @@ fzf-only search mode by *"unbinding"* `reload` action from `change` event.
# 3. Open the file in Vim # 3. Open the file in Vim
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}" INITIAL_QUERY="${*:-}"
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \ : | fzf --ansi --disabled --query "$INITIAL_QUERY" \
fzf --ansi \ --bind "start:reload:$RG_PREFIX {q}" \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--disabled --query "$INITIAL_QUERY" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \ --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" \ --bind "alt-enter:unbind(change,alt-enter)+change-prompt(2. fzf> )+enable-search+clear-query" \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--prompt '1. ripgrep> ' \ --prompt '1. ripgrep> ' \
--delimiter : \ --delimiter : \
--preview 'bat --color=always {1} --highlight-line {2}' \ --preview 'bat --color=always {1} --highlight-line {2}' \
@@ -421,14 +419,12 @@ CTRL-F.
rm -f /tmp/rg-fzf-{r,f} rm -f /tmp/rg-fzf-{r,f}
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}" INITIAL_QUERY="${*:-}"
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \ : | fzf --ansi --disabled --query "$INITIAL_QUERY" \
fzf --ansi \ --bind "start:reload($RG_PREFIX {q})+unbind(ctrl-r)" \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--disabled --query "$INITIAL_QUERY" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \ --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-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 "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || true)+rebind(change,ctrl-f)+transform-query(echo {q} > /tmp/rg-fzf-f; cat /tmp/rg-fzf-r)" \
--bind "start:unbind(ctrl-r)" \ --color "hl:-1:underline,hl+:-1:underline:reverse" \
--prompt '1. ripgrep> ' \ --prompt '1. ripgrep> ' \
--delimiter : \ --delimiter : \
--header ' CTRL-R (ripgrep mode) CTRL-F (fzf mode) ' \ --header ' CTRL-R (ripgrep mode) CTRL-F (fzf mode) ' \
@@ -471,16 +467,17 @@ Kubernetes pods.
```bash ```bash
pods() { pods() {
FZF_DEFAULT_COMMAND="kubectl get pods --all-namespaces" \ : | command='kubectl get pods --all-namespaces' fzf \
fzf --info=inline --layout=reverse --header-lines=1 \ --info=inline --layout=reverse --header-lines=1 \
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \ --prompt "$(kubectl config current-context | sed 's/-context$//')> " \
--header $' Enter (kubectl exec) CTRL-O (open log in editor) CTRL-R (reload) \n\n' \ --header $' Enter (kubectl exec) CTRL-O (open log in editor) CTRL-R (reload) \n\n' \
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \ --bind 'start:reload:$command' \
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \ --bind 'ctrl-r:reload:$command' \
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \ --bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
--bind 'ctrl-r:reload:$FZF_DEFAULT_COMMAND' \ --bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \
--preview-window up:follow \ --bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@" --preview-window up:follow \
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
} }
``` ```

View File

@@ -1,6 +1,89 @@
CHANGELOG CHANGELOG
========= =========
0.41.0
------
- Added color name `preview-border` and `preview-scrollbar`
- Added new border style `block` which uses [block elements](https://en.wikipedia.org/wiki/Block_Elements)
- `--scrollbar` can take two characters, one for the main window, the other
for the preview window
- Putting it altogether:
```sh
fzf-tmux -p 80% --padding 1,2 --preview 'bat --style=plain --color=always {}' \
--color 'bg:237,bg+:235,gutter:237,border:238,scrollbar:236' \
--color 'preview-bg:235,preview-border:236,preview-scrollbar:234' \
--preview-window 'border-block' --border block --scrollbar '▌▐'
```
- Bug fixes and improvements
0.40.0
------
- Added `zero` event that is triggered when there's no match
```sh
# Reload the candidate list when there's no match
echo $RANDOM | fzf --bind 'zero:reload(echo $RANDOM)+clear-query' --height 3
```
- New actions
- Added `track` action which makes fzf track the current item when the
search result is updated. If the user manually moves the cursor, or the
item is not in the updated search result, tracking is automatically
disabled. Tracking is useful when you want to see the surrounding items
by deleting the query string.
```sh
# Narrow down the list with a query, point to a command,
# and hit CTRL-T to see its surrounding commands.
export FZF_CTRL_R_OPTS="
--preview 'echo {}' --preview-window up:3:hidden:wrap
--bind 'ctrl-/:toggle-preview'
--bind 'ctrl-t:track+clear-query'
--bind 'ctrl-y:execute-silent(echo -n {2..} | pbcopy)+abort'
--color header:italic
--header 'Press CTRL-Y to copy command into clipboard'"
```
- Added `change-header(...)`
- Added `transform-header(...)`
- Added `toggle-track` action
- Fixed `--track` behavior when used with `--tac`
- However, using `--track` with `--tac` is not recommended. The resulting
behavior can be very confusing.
- Bug fixes and improvements
0.39.0
------
- Added `one` event that is triggered when there's only one match
```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 0.38.0
------ ------
- New actions - New actions
@@ -140,7 +223,7 @@ CHANGELOG
- Added color name `preview-label` for `--preview-label` (defaults to `label` - Added color name `preview-label` for `--preview-label` (defaults to `label`
for `--border-label`) for `--border-label`)
- Better support for (Windows) terminals where each box-drawing character - Better support for (Windows) terminals where each box-drawing character
takes 2 columns. Set `RUNEWIDTH_EASTASIAN` environment variable to `1`. takes 2 columns. Set `RUNEWIDTH_EASTASIAN` environment variable to `0` or `1`.
- On Vim, the variable will be automatically set if `&ambiwidth` is `double` - On Vim, the variable will be automatically set if `&ambiwidth` is `double`
- Behavior changes - Behavior changes
- fzf will always execute the preview command if the command template - fzf will always execute the preview command if the command template

View File

@@ -1,4 +1,4 @@
FROM archlinux FROM --platform=linux/amd64 archlinux
RUN pacman -Sy && pacman --noconfirm -S awk git tmux zsh fish ruby procps go make gcc RUN pacman -Sy && pacman --noconfirm -S awk git tmux zsh fish ruby procps go make gcc
RUN gem install --no-document -v 5.14.2 minitest RUN gem install --no-document -v 5.14.2 minitest
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc

View File

@@ -20,7 +20,7 @@ VERSION_REGEX := $(subst .,\.,$(VERSION_TRIM))
ifdef FZF_REVISION ifdef FZF_REVISION
REVISION := $(FZF_REVISION) REVISION := $(FZF_REVISION)
else 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 endif
ifeq ($(REVISION),) ifeq ($(REVISION),)
$(error Not on git repository; cannot determine $$FZF_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 BINARY32 := fzf-$(GOOS)_386
BINARY64 := fzf-$(GOOS)_amd64 BINARY64 := fzf-$(GOOS)_amd64
BINARYS390 := fzf-$(GOOS)_s390x
BINARYARM5 := fzf-$(GOOS)_arm5 BINARYARM5 := fzf-$(GOOS)_arm5
BINARYARM6 := fzf-$(GOOS)_arm6 BINARYARM6 := fzf-$(GOOS)_arm6
BINARYARM7 := fzf-$(GOOS)_arm7 BINARYARM7 := fzf-$(GOOS)_arm7
@@ -43,6 +44,8 @@ ifeq ($(UNAME_M),x86_64)
BINARY := $(BINARY64) BINARY := $(BINARY64)
else ifeq ($(UNAME_M),amd64) else ifeq ($(UNAME_M),amd64)
BINARY := $(BINARY64) BINARY := $(BINARY64)
else ifeq ($(UNAME_M),s390x)
BINARY := $(BINARYS390)
else ifeq ($(UNAME_M),i686) else ifeq ($(UNAME_M),i686)
BINARY := $(BINARY32) BINARY := $(BINARY32)
else ifeq ($(UNAME_M),i386) else ifeq ($(UNAME_M),i386)
@@ -85,7 +88,7 @@ bench:
install: bin/fzf install: bin/fzf
build: build:
goreleaser --rm-dist --snapshot goreleaser build --rm-dist --snapshot --skip-post-hooks
release: release:
ifndef GITHUB_TOKEN ifndef GITHUB_TOKEN
@@ -132,6 +135,8 @@ target/$(BINARY32): $(SOURCES)
target/$(BINARY64): $(SOURCES) target/$(BINARY64): $(SOURCES)
GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@ GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@
target/$(BINARYS390): $(SOURCES)
GOARCH=s390x $(GO) build $(BUILD_FLAGS) -o $@
# https://github.com/golang/go/wiki/GoArm # https://github.com/golang/go/wiki/GoArm
target/$(BINARYARM5): $(SOURCES) target/$(BINARYARM5): $(SOURCES)
GOARCH=arm GOARM=5 $(GO) build $(BUILD_FLAGS) -o $@ 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 " If installed using Homebrew on Apple Silicon
set rtp+=/opt/homebrew/opt/fzf set rtp+=/opt/homebrew/opt/fzf
" If installed using git " If you have cloned fzf on ~/.fzf directory
set rtp+=~/.fzf set rtp+=~/.fzf
``` ```
@@ -26,7 +26,7 @@ written as:
" If installed using Homebrew " If installed using Homebrew
Plug '/usr/local/opt/fzf' Plug '/usr/local/opt/fzf'
" If installed using git " If you have cloned fzf on ~/.fzf directory
Plug '~/.fzf' Plug '~/.fzf'
``` ```
@@ -118,7 +118,7 @@ let g:fzf_action = {
" An action can be a reference to a function that processes selected lines " An action can be a reference to a function that processes selected lines
function! s:build_quickfix_list(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 copen
cc cc
endfunction endfunction

View File

@@ -61,8 +61,8 @@ Table of Contents
* [3. Interactive ripgrep integration](#3-interactive-ripgrep-integration) * [3. Interactive ripgrep integration](#3-interactive-ripgrep-integration)
* [Preview window](#preview-window) * [Preview window](#preview-window)
* [Tips](#tips) * [Tips](#tips)
* [Respecting `.gitignore`](#respecting-gitignore) * [Respecting `.gitignore`](#respecting-gitignore)
* [Fish shell](#fish-shell) * [Fish shell](#fish-shell)
* [Related projects](#related-projects) * [Related projects](#related-projects)
* [License](#license) * [License](#license)
@@ -124,6 +124,7 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
| pkg | FreeBSD | `pkg install fzf` | | pkg | FreeBSD | `pkg install fzf` |
| pkgin | NetBSD | `pkgin install fzf` | | pkgin | NetBSD | `pkgin install fzf` |
| pkg_add | OpenBSD | `pkg_add fzf` | | pkg_add | OpenBSD | `pkg_add fzf` |
| Portage | Gentoo | `emerge --ask app-shells/fzf` |
| XBPS | Void Linux | `sudo xbps-install -S fzf` | | XBPS | Void Linux | `sudo xbps-install -S fzf` |
| Zypper | openSUSE | `sudo zypper install fzf` | | Zypper | openSUSE | `sudo zypper install fzf` |
@@ -629,8 +630,8 @@ more details.
#### 1. Update the list of processes by pressing CTRL-R #### 1. Update the list of processes by pressing CTRL-R
```sh ```sh
FZF_DEFAULT_COMMAND='ps -ef' \ ps -ef |
fzf --bind 'ctrl-r:reload(eval "$FZF_DEFAULT_COMMAND")' \ fzf --bind 'ctrl-r:reload(ps -ef)' \
--header 'Press CTRL-R to reload' --header-lines=1 \ --header 'Press CTRL-R to reload' --header-lines=1 \
--height=50% --layout=reverse --height=50% --layout=reverse
``` ```
@@ -652,12 +653,12 @@ expression `{q}`. Also, note that we used `--disabled` option so that fzf
doesn't perform any secondary filtering. doesn't perform any secondary filtering.
```sh ```sh
INITIAL_QUERY="" : | rg_prefix='rg --column --line-number --no-heading --color=always --smart-case' \
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " fzf --bind 'start:reload:$rg_prefix ""' \
FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \ --bind 'change:reload:$rg_prefix {q} || true' \
fzf --bind "change:reload:$RG_PREFIX {q} || true" \ --bind 'enter:become(vim {1} +{2})' \
--ansi --disabled --query "$INITIAL_QUERY" \ --ansi --disabled \
--height=50% --layout=reverse --height=50% --layout=reverse
``` ```
If ripgrep doesn't find any matches, it will exit with a non-zero exit status, If ripgrep doesn't find any matches, it will exit with a non-zero exit status,
@@ -665,7 +666,7 @@ and fzf will warn you about it. To suppress the warning message, we added
`|| true` to the command, so that it always exits with 0. `|| true` to the command, so that it always exits with 0.
See ["Using fzf as interactive Ripgrep launcher"](https://github.com/junegunn/fzf/blob/master/ADVANCED.md#using-fzf-as-interactive-ripgrep-launcher) See ["Using fzf as interactive Ripgrep launcher"](https://github.com/junegunn/fzf/blob/master/ADVANCED.md#using-fzf-as-interactive-ripgrep-launcher)
for a fuller example with preview window options. for more sophisticated examples.
### Preview window ### Preview window
@@ -723,7 +724,7 @@ history | fzf
Tips Tips
---- ----
#### Respecting `.gitignore` ### Respecting `.gitignore`
You can use [fd](https://github.com/sharkdp/fd), You can use [fd](https://github.com/sharkdp/fd),
[ripgrep](https://github.com/BurntSushi/ripgrep), or [the silver [ripgrep](https://github.com/BurntSushi/ripgrep), or [the silver
@@ -752,7 +753,7 @@ hidden files, use the following command:
export FZF_DEFAULT_COMMAND='fd --type f --strip-cwd-prefix --hidden --follow --exclude .git' 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 `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 token on the command-line as the root directory for the recursive search. For

View File

@@ -179,12 +179,15 @@ trap 'cleanup' EXIT
envs="export TERM=$TERM " envs="export TERM=$TERM "
if [[ "$opt" =~ "-E" ]]; then if [[ "$opt" =~ "-E" ]]; then
tmux_version=$(tmux -V) tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
if [[ $tmux_version =~ ^tmux\ 3\.2[a-z]?$ ]]; then if [[ $(awk '{print ($1 > 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version > 3.2") = 1 ]]; then
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
else
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS" FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
opt="-B $opt" 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
fi fi
[[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" [[ -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* FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
============================================================================== ==============================================================================
@@ -32,7 +32,7 @@ depending on the package manager.
" If installed using Homebrew " If installed using Homebrew
set rtp+=/usr/local/opt/fzf set rtp+=/usr/local/opt/fzf
" If installed using git " If you have cloned fzf on ~/.fzf directory
set rtp+=~/.fzf set rtp+=~/.fzf
< <
If you use {vim-plug}{1}, the same can be written as: 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 " If installed using Homebrew
Plug '/usr/local/opt/fzf' Plug '/usr/local/opt/fzf'
" If installed using git " If you have cloned fzf on ~/.fzf directory
Plug '~/.fzf' Plug '~/.fzf'
< <
But if you want the latest Vim plugin file from GitHub rather than the one 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 " An action can be a reference to a function that processes selected lines
function! s:build_quickfix_list(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 copen
cc cc
endfunction endfunction

6
go.mod
View File

@@ -5,10 +5,10 @@ require (
github.com/mattn/go-isatty v0.0.17 github.com/mattn/go-isatty v0.0.17
github.com/mattn/go-runewidth v0.0.14 github.com/mattn/go-runewidth v0.0.14
github.com/mattn/go-shellwords v1.0.12 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 github.com/saracen/walker v0.1.3
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab golang.org/x/sys v0.8.0
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/term v0.8.0
) )
require ( 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 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= 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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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 h1:YtcKKmpRPy6XJTHJ75J2QYXXZYWnZNQxPCVqZSHVV/g=
github.com/saracen/walker v0.1.3/go.mod h1:FU+7qU8DeQQgSZDmmThMJi93kPkLFgy0oVAcLxurjIk= 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= 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-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-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-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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=

View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.38.0 version=0.41.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
@@ -178,6 +178,7 @@ case "$archi" in
Linux\ loongarch64) download fzf-$version-linux_loong64.tar.gz ;; Linux\ loongarch64) download fzf-$version-linux_loong64.tar.gz ;;
Linux\ ppc64le) download fzf-$version-linux_ppc64le.tar.gz ;; Linux\ ppc64le) download fzf-$version-linux_ppc64le.tar.gz ;;
Linux\ *64) download fzf-$version-linux_amd64.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 ;; FreeBSD\ *64) download fzf-$version-freebsd_amd64.tar.gz ;;
OpenBSD\ *64) download fzf-$version-openbsd_amd64.tar.gz ;; OpenBSD\ *64) download fzf-$version-openbsd_amd64.tar.gz ;;
CYGWIN*\ *64) download fzf-$version-windows_amd64.zip ;; CYGWIN*\ *64) download fzf-$version-windows_amd64.zip ;;

View File

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

View File

@@ -5,7 +5,7 @@ import (
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version string = "0.38" var version string = "0.41"
var revision string = "devel" var revision string = "devel"
func main() { 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf-tmux 1 "Feb 2023" "fzf 0.38.0" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "May 2023" "fzf 0.41.0" "fzf-tmux - open fzf in tmux split pane"
.SH NAME .SH NAME
fzf-tmux - open fzf in tmux split pane fzf-tmux - open fzf in tmux split pane

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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Feb 2023" "fzf 0.38.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "May 2023" "fzf 0.41.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -92,6 +92,19 @@ interface rather than a "fuzzy finder". You can later enable the search using
.B "+s, --no-sort" .B "+s, --no-sort"
Do not sort the result Do not sort the result
.TP .TP
.B "--track"
Make fzf track the current selection when the result list is updated.
This can be useful when browsing logs using fzf with sorting disabled. It is
not recommended to use this option with \fB--tac\fR as the resulting behavior
can be confusing. Also, consider using \fBtrack\fR action instead of this
option.
.RS
e.g.
\fBgit log --oneline --graph --color=always | nl |
fzf --ansi --track --no-sort --layout=reverse-list\fR
.RE
.TP
.B "--tac" .B "--tac"
Reverse the order of the input Reverse the order of the input
@@ -231,8 +244,9 @@ Draw border around the finder
.br .br
If you use a terminal emulator where each box-drawing character takes If you use a terminal emulator where each box-drawing character takes
2 columns, try setting \fBRUNEWIDTH_EASTASIAN\fR to \fB1\fR. If the border is 2 columns, try setting \fBRUNEWIDTH_EASTASIAN\fR environment variable to
still not properly rendered, set \fB--no-unicode\fR. \fB0\fR or \fB1\fR. If the border is still not properly rendered, set
\fB--no-unicode\fR.
.TP .TP
.BI "--border-label" [=LABEL] .BI "--border-label" [=LABEL]
@@ -363,9 +377,10 @@ Do not display horizontal separator on the info line. A synonym for
\fB--separator=''\fB \fB--separator=''\fB
.TP .TP
.BI "--scrollbar=" "CHAR" .BI "--scrollbar=" "CHAR1[CHAR2]"
Use the given character to render scrollbar. (default: '│' or ':' depending on Use the given character to render scrollbar. (default: '│' or ':' depending on
\fB--no-unicode\fR). \fB--no-unicode\fR). The optional \fBCHAR2\fR is used to render scrollbar of
the preview window.
.TP .TP
.B "--no-scrollbar" .B "--no-scrollbar"
@@ -419,28 +434,30 @@ color mappings.
\fBbw \fRNo colors (equivalent to \fB--no-color\fR) \fBbw \fRNo colors (equivalent to \fB--no-color\fR)
.B COLOR NAMES: .B COLOR NAMES:
\fBfg \fRText \fBfg \fRText
\fBpreview-fg \fRPreview window text \fBpreview-fg \fRPreview window text
\fBbg \fRBackground \fBbg \fRBackground
\fBpreview-bg \fRPreview window background \fBpreview-bg \fRPreview window background
\fBhl \fRHighlighted substrings \fBhl \fRHighlighted substrings
\fBfg+ \fRText (current line) \fBfg+ \fRText (current line)
\fBbg+ \fRBackground (current line) \fBbg+ \fRBackground (current line)
\fBgutter \fRGutter on the left \fBgutter \fRGutter on the left
\fBhl+ \fRHighlighted substrings (current line) \fBhl+ \fRHighlighted substrings (current line)
\fBquery \fRQuery string \fBquery \fRQuery string
\fBdisabled \fRQuery string when search is disabled (\fB--disabled\fR) \fBdisabled \fRQuery string when search is disabled (\fB--disabled\fR)
\fBinfo \fRInfo line (match counters) \fBinfo \fRInfo line (match counters)
\fBborder \fRBorder around the window (\fB--border\fR and \fB--preview\fR) \fBborder \fRBorder around the window (\fB--border\fR and \fB--preview\fR)
\fBseparator \fRHorizontal separator on info line \fBscrollbar \fRScrollbar
\fBscrollbar \fRScrollbar \fBpreview-border \fRBorder around the preview window (\fB--preview\fR)
\fBlabel \fRBorder label (\fB--border-label\fR and \fB--preview-label\fR) \fBpreview-scrollbar \fRScrollbar
\fBpreview-label \fRBorder label of the preview window (\fB--preview-label\fR) \fBseparator \fRHorizontal separator on info line
\fBprompt \fRPrompt \fBlabel \fRBorder label (\fB--border-label\fR and \fB--preview-label\fR)
\fBpointer \fRPointer to the current line \fBpreview-label \fRBorder label of the preview window (\fB--preview-label\fR)
\fBmarker \fRMulti-select marker \fBprompt \fRPrompt
\fBspinner \fRStreaming input indicator \fBpointer \fRPointer to the current line
\fBheader \fRHeader \fBmarker \fRMulti-select marker
\fBspinner \fRStreaming input indicator
\fBheader \fRHeader
.B ANSI COLORS: .B ANSI COLORS:
\fB-1 \fRDefault terminal foreground/background color \fB-1 \fRDefault terminal foreground/background color
@@ -738,9 +755,12 @@ ncurses finder only after the input stream is complete.
e.g. \fBfzf --multi | fzf --sync\fR e.g. \fBfzf --multi | fzf --sync\fR
.RE .RE
.TP .TP
.B "--listen=HTTP_PORT" .B "--listen[=HTTP_PORT]"
Start HTTP server on the given port. It allows external processes to send 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. e.g.
\fB# Start HTTP server on port 6266 \fB# Start HTTP server on port 6266
@@ -748,6 +768,9 @@ e.g.
# Send action to the server # Send action to the server
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )' 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 \fR
.TP .TP
.B "--version" .B "--version"
@@ -850,6 +873,8 @@ e.g.
.br .br
\fIctrl-space\fR \fIctrl-space\fR
.br .br
\fIctrl-delete\fR
.br
\fIctrl-\\\fR \fIctrl-\\\fR
.br .br
\fIctrl-]\fR \fIctrl-]\fR
@@ -918,6 +943,8 @@ e.g.
.br .br
\fIshift-right\fR \fIshift-right\fR
.br .br
\fIshift-delete\fR
.br
\fIalt-shift-up\fR \fIalt-shift-up\fR
.br .br
\fIalt-shift-down\fR \fIalt-shift-down\fR
@@ -977,6 +1004,28 @@ e.g.
# Beware not to introduce an infinite loop # Beware not to introduce an infinite loop
seq 10 | fzf --bind 'focus:up' --cycle\fR seq 10 | fzf --bind 'focus:up' --cycle\fR
.RE .RE
\fIone\fR
.RS
Triggered when there's only one match. \fBone:accept\fR binding is comparable
to \fB--select-1\fR option, but the difference is that \fB--select-1\fR is only
effective before the interactive finder starts but \fBone\fR event is triggered
by the interactive finder.
e.g.
\fB# Automatically select the only match
seq 10 | fzf --bind one:accept\fR
.RE
\fIzero\fR
.RS
Triggered when there's no match. \fBzero:abort\fR binding is comparable to
\fB--exit-0\fR option, but the difference is that \fB--exit-0\fR is only
effective before the interactive finder starts but \fBzero\fR event is
triggered by the interactive finder.
e.g.
\fB# Reload the candidate list when there's no match
echo $RANDOM | fzf --bind 'zero:reload(echo $RANDOM)+clear-query' --height 3\fR
.RE
\fIbackward-eof\fR \fIbackward-eof\fR
.RS .RS
@@ -1003,6 +1052,7 @@ A key or an event can be bound to one or more of the following actions.
\fBbeginning-of-line\fR \fIctrl-a home\fR \fBbeginning-of-line\fR \fIctrl-a home\fR
\fBcancel\fR (clear query string if not empty, abort fzf otherwise) \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-border-label(...)\fR (change \fB--border-label\fR to the given string)
\fBchange-header(...)\fR (change header to the given string; doesn't affect \fB--header-lines\fR)
\fBchange-preview(...)\fR (change \fB--preview\fR option) \fBchange-preview(...)\fR (change \fB--preview\fR option)
\fBchange-preview-label(...)\fR (change \fB--preview-label\fR to the given string) \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-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|')
@@ -1070,8 +1120,11 @@ A key or an event can be bound to one or more of the following actions.
\fBtoggle-preview-wrap\fR \fBtoggle-preview-wrap\fR
\fBtoggle-search\fR (toggle search functionality) \fBtoggle-search\fR (toggle search functionality)
\fBtoggle-sort\fR \fBtoggle-sort\fR
\fBtoggle-track\fR
\fBtoggle+up\fR \fIbtab (shift-tab)\fR \fBtoggle+up\fR \fIbtab (shift-tab)\fR
\fBtrack\fR (track the current item; automatically disabled if focus changes)
\fBtransform-border-label(...)\fR (transform border label using an external command) \fBtransform-border-label(...)\fR (transform border label using an external command)
\fBtransform-header(...)\fR (transform header using an external command)
\fBtransform-preview-label(...)\fR (transform preview label using an external command) \fBtransform-preview-label(...)\fR (transform preview label using an external command)
\fBtransform-prompt(...)\fR (transform prompt string using an external command) \fBtransform-prompt(...)\fR (transform prompt string using an external command)
\fBtransform-query(...)\fR (transform query string using an external command) \fBtransform-query(...)\fR (transform query string using an external command)
@@ -1145,7 +1198,7 @@ set, otherwise with \fBsh -c\fR, so in this case make sure that the command is
POSIX-compliant. POSIX-compliant.
\fBbecome(...)\fR action is similar to \fBexecute(...)\fR, but it replaces the \fBbecome(...)\fR action is similar to \fBexecute(...)\fR, but it replaces the
current fzf process with the specifed command using \fBexecve(2)\fR system current fzf process with the specified command using \fBexecve(2)\fR system
call. call.
\fBfzf --bind "enter:become(vim {})"\fR \fBfzf --bind "enter:become(vim {})"\fR

View File

@@ -164,7 +164,7 @@ function s:get_version(bin)
if has_key(s:versions, a:bin) if has_key(s:versions, a:bin)
return s:versions[a:bin] return s:versions[a:bin]
end end
let command = (&shell =~ 'powershell' ? '&' : '') . s:fzf_call('shellescape', a:bin) . ' --version --no-height' let command = (&shell =~ 'powershell\|pwsh' ? '&' : '') . s:fzf_call('shellescape', a:bin) . ' --version --no-height'
let output = systemlist(command) let output = systemlist(command)
if v:shell_error || empty(output) if v:shell_error || empty(output)
return '' return ''

View File

@@ -270,8 +270,9 @@ _fzf_complete_kill() {
} }
_fzf_proc_completion() { _fzf_proc_completion() {
_fzf_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <( _fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
command ps -ef | sed 1d command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
command ps -eo user,pid,ppid,time,args # For BusyBox
) )
} }
@@ -309,7 +310,7 @@ complete -o default -F _fzf_opts_completion fzf-tmux
d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}" d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}"
a_cmds=" a_cmds="
awk cat diff diff3 awk bat cat diff diff3
emacs emacsclient ex file ftp g++ gcc gvim head hg hx java emacs emacsclient ex file ftp g++ gcc gvim head hg hx java
javac ld less more mvim nvim patch perl python ruby javac ld less more mvim nvim patch perl python ruby
sed sftp sort source tail tee uniq vi view vim wc xdg-open sed sftp sort source tail tee uniq vi view vim wc xdg-open

View File

@@ -251,8 +251,9 @@ _fzf_complete_unalias() {
} }
_fzf_complete_kill() { _fzf_complete_kill() {
_fzf_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <( _fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
command ps -ef | sed 1d 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) { determine := func(final bool) {
if heightUnknown { if heightUnknown {
if total >= maxFit || final { if total >= maxFit || final {
deferred = false
heightUnknown = false heightUnknown = false
terminal.startChan <- fitpad{util.Min(total, maxFit), padHeight} terminal.startChan <- fitpad{util.Min(total, maxFit), padHeight}
} }
@@ -298,10 +299,12 @@ func Run(opts *Options, version string, revision string) {
case EvtSearchNew: case EvtSearchNew:
var command *string var command *string
var changed bool
switch val := value.(type) { switch val := value.(type) {
case searchRequest: case searchRequest:
sort = val.sort sort = val.sort
command = val.command command = val.command
changed = val.changed
if command != nil { if command != nil {
useSnapshot = val.sync useSnapshot = val.sync
} }
@@ -313,12 +316,20 @@ func Run(opts *Options, version string, revision string) {
} else { } else {
restart(*command) restart(*command)
} }
}
if !changed {
break break
} }
reset := false
if !useSnapshot { if !useSnapshot {
snapshot, _ = chunkList.Snapshot() newSnapshot, _ := chunkList.Snapshot()
// We want to avoid showing empty list when reload is triggered
// and the query string is changed at the same time i.e. command != nil && changed
if command == nil || len(newSnapshot) > 0 {
snapshot = newSnapshot
reset = clearCache()
}
} }
reset := !useSnapshot && clearCache()
matcher.Reset(snapshot, input(reset), true, !reading, sort, reset) matcher.Reset(snapshot, input(reset), true, !reading, sort, reset)
delay = false delay = false

View File

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

View File

@@ -33,6 +33,7 @@ const usage = `usage: fzf [options]
field index expressions field index expressions
-d, --delimiter=STR Field delimiter regex (default: AWK-style) -d, --delimiter=STR Field delimiter regex (default: AWK-style)
+s, --no-sort Do not sort the result +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 --tac Reverse the order of the input
--disabled Do not perform search --disabled Do not perform search
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
@@ -62,7 +63,7 @@ const usage = `usage: fzf [options]
(default: 10) (default: 10)
--layout=LAYOUT Choose layout: [default|reverse|reverse-list] --layout=LAYOUT Choose layout: [default|reverse|reverse-list]
--border[=STYLE] Draw border around the finder --border[=STYLE] Draw border around the finder
[rounded|sharp|horizontal|vertical| [rounded|sharp|bold|block|double|horizontal|vertical|
top|bottom|left|right|none] (default: rounded) top|bottom|left|right|none] (default: rounded)
--border-label=LABEL Label to print on the border --border-label=LABEL Label to print on the border
--border-label-pos=COL Position of the border label --border-label-pos=COL Position of the border label
@@ -74,7 +75,7 @@ const usage = `usage: fzf [options]
--info=STYLE Finder info style [default|hidden|inline|inline:SEPARATOR] --info=STYLE Finder info style [default|hidden|inline|inline:SEPARATOR]
--separator=STR String to form horizontal separator on info line --separator=STR String to form horizontal separator on info line
--no-separator Hide info line separator --no-separator Hide info line separator
--scrollbar[=CHAR] Scrollbar character --scrollbar[=C1[C2]] Scrollbar character(s) (each for main and preview window)
--no-scrollbar Hide scrollbar --no-scrollbar Hide scrollbar
--prompt=STR Input prompt (default: '> ') --prompt=STR Input prompt (default: '> ')
--pointer=STR Pointer to the current line (default: '>') --pointer=STR Pointer to the current line (default: '>')
@@ -116,7 +117,7 @@ const usage = `usage: fzf [options]
--read0 Read input delimited by ASCII NUL characters --read0 Read input delimited by ASCII NUL characters
--print0 Print output delimited by ASCII NUL characters --print0 Print output delimited by ASCII NUL characters
--sync Synchronous search for multi-staged filtering --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 --version Display version information and exit
Environment variables Environment variables
@@ -164,6 +165,14 @@ func defaultMargin() [4]sizeSpec {
return [4]sizeSpec{} return [4]sizeSpec{}
} }
type trackOption int
const (
trackDisabled trackOption = iota
trackEnabled
trackCurrent
)
type windowPosition int type windowPosition int
const ( const (
@@ -266,6 +275,7 @@ type Options struct {
WithNth []Range WithNth []Range
Delimiter Delimiter Delimiter Delimiter
Sort int Sort int
Track trackOption
Tac bool Tac bool
Criteria []criterion Criteria []criterion
Multi int Multi int
@@ -316,7 +326,7 @@ type Options struct {
PreviewLabel labelOpts PreviewLabel labelOpts
Unicode bool Unicode bool
Tabstop int Tabstop int
ListenPort int ListenPort *int
ClearOnExit bool ClearOnExit bool
Version bool Version bool
} }
@@ -338,6 +348,7 @@ func defaultOptions() *Options {
WithNth: make([]Range, 0), WithNth: make([]Range, 0),
Delimiter: Delimiter{}, Delimiter: Delimiter{},
Sort: 1000, Sort: 1000,
Track: trackDisabled,
Tac: false, Tac: false,
Criteria: []criterion{byScore, byLength}, Criteria: []criterion{byScore, byLength},
Multi: 0, Multi: 0,
@@ -533,6 +544,8 @@ func parseBorder(str string, optional bool) tui.BorderShape {
return tui.BorderSharp return tui.BorderSharp
case "bold": case "bold":
return tui.BorderBold return tui.BorderBold
case "block":
return tui.BorderBlock
case "double": case "double":
return tui.BorderDouble return tui.BorderDouble
case "horizontal": case "horizontal":
@@ -553,7 +566,7 @@ func parseBorder(str string, optional bool) tui.BorderShape {
if optional && str == "" { if optional && str == "" {
return tui.DefaultBorderShape return tui.DefaultBorderShape
} }
errorExit("invalid border style (expected: rounded|sharp|bold|double|horizontal|vertical|top|bottom|left|right|none)") errorExit("invalid border style (expected: rounded|sharp|bold|block|double|horizontal|vertical|top|bottom|left|right|none)")
} }
return tui.BorderNone return tui.BorderNone
} }
@@ -601,6 +614,8 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
add(tui.BSpace) add(tui.BSpace)
case "ctrl-space": case "ctrl-space":
add(tui.CtrlSpace) add(tui.CtrlSpace)
case "ctrl-delete":
add(tui.CtrlDelete)
case "ctrl-^", "ctrl-6": case "ctrl-^", "ctrl-6":
add(tui.CtrlCaret) add(tui.CtrlCaret)
case "ctrl-/", "ctrl-_": case "ctrl-/", "ctrl-_":
@@ -619,6 +634,10 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
add(tui.Load) add(tui.Load)
case "focus": case "focus":
add(tui.Focus) add(tui.Focus)
case "one":
add(tui.One)
case "zero":
add(tui.Zero)
case "alt-enter", "alt-return": case "alt-enter", "alt-return":
chords[tui.CtrlAltKey('m')] = key chords[tui.CtrlAltKey('m')] = key
case "alt-space": case "alt-space":
@@ -667,6 +686,8 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
add(tui.SLeft) add(tui.SLeft)
case "shift-right": case "shift-right":
add(tui.SRight) add(tui.SRight)
case "shift-delete":
add(tui.SDelete)
case "left-click": case "left-click":
add(tui.LeftClick) add(tui.LeftClick)
case "right-click": case "right-click":
@@ -873,10 +894,14 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
mergeAttr(&theme.CurrentMatch) mergeAttr(&theme.CurrentMatch)
case "border": case "border":
mergeAttr(&theme.Border) mergeAttr(&theme.Border)
case "preview-border":
mergeAttr(&theme.PreviewBorder)
case "separator": case "separator":
mergeAttr(&theme.Separator) mergeAttr(&theme.Separator)
case "scrollbar": case "scrollbar":
mergeAttr(&theme.Scrollbar) mergeAttr(&theme.Scrollbar)
case "preview-scrollbar":
mergeAttr(&theme.PreviewScrollbar)
case "label": case "label":
mergeAttr(&theme.BorderLabel) mergeAttr(&theme.BorderLabel)
case "preview-label": case "preview-label":
@@ -922,7 +947,7 @@ const (
func init() { func init() {
executeRegexp = regexp.MustCompile( executeRegexp = regexp.MustCompile(
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`) `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`)
splitRegexp = regexp.MustCompile("[,:]+") splitRegexp = regexp.MustCompile("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
} }
@@ -1078,6 +1103,10 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actToggleAll) appendAction(actToggleAll)
case "toggle-search": case "toggle-search":
appendAction(actToggleSearch) appendAction(actToggleSearch)
case "toggle-track":
appendAction(actToggleTrack)
case "track":
appendAction(actTrack)
case "select": case "select":
appendAction(actSelect) appendAction(actSelect)
case "select-all": case "select-all":
@@ -1242,6 +1271,8 @@ func isExecuteAction(str string) actionType {
return actPreview return actPreview
case "change-border-label": case "change-border-label":
return actChangeBorderLabel return actChangeBorderLabel
case "change-header":
return actChangeHeader
case "change-preview-label": case "change-preview-label":
return actChangePreviewLabel return actChangePreviewLabel
case "change-preview-window": case "change-preview-window":
@@ -1266,6 +1297,8 @@ func isExecuteAction(str string) actionType {
return actTransformBorderLabel return actTransformBorderLabel
case "transform-preview-label": case "transform-preview-label":
return actTransformPreviewLabel return actTransformPreviewLabel
case "transform-header":
return actTransformHeader
case "transform-prompt": case "transform-prompt":
return actTransformPrompt return actTransformPrompt
case "transform-query": case "transform-query":
@@ -1403,6 +1436,8 @@ func parsePreviewWindowImpl(opts *previewOpts, input string, exit func(string))
opts.border = tui.BorderSharp opts.border = tui.BorderSharp
case "border-bold": case "border-bold":
opts.border = tui.BorderBold opts.border = tui.BorderBold
case "border-block":
opts.border = tui.BorderBlock
case "border-double": case "border-double":
opts.border = tui.BorderDouble opts.border = tui.BorderDouble
case "noborder", "border-none": case "noborder", "border-none":
@@ -1562,6 +1597,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Sort = optionalNumeric(allArgs, &i, 1) opts.Sort = optionalNumeric(allArgs, &i, 1)
case "+s", "--no-sort": case "+s", "--no-sort":
opts.Sort = 0 opts.Sort = 0
case "--track":
opts.Track = trackEnabled
case "--no-track":
opts.Track = trackDisabled
case "--tac": case "--tac":
opts.Tac = true opts.Tac = true
case "--no-tac": case "--no-tac":
@@ -1756,9 +1795,10 @@ func parseOptions(opts *Options, allArgs []string) {
case "--tabstop": case "--tabstop":
opts.Tabstop = nextInt(allArgs, &i, "tab stop required") opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
case "--listen": case "--listen":
opts.ListenPort = nextInt(allArgs, &i, "listen port required") port := optionalNumeric(allArgs, &i, 0)
opts.ListenPort = &port
case "--no-listen": case "--no-listen":
opts.ListenPort = 0 opts.ListenPort = nil
case "--clear": case "--clear":
opts.ClearOnExit = true opts.ClearOnExit = true
case "--no-clear": case "--no-clear":
@@ -1849,7 +1889,8 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--tabstop="); match { } else if match, value := optString(arg, "--tabstop="); match {
opts.Tabstop = atoi(value) opts.Tabstop = atoi(value)
} else if match, value := optString(arg, "--listen="); match { } 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 { } else if match, value := optString(arg, "--hscroll-off="); match {
opts.HscrollOff = atoi(value) opts.HscrollOff = atoi(value)
} else if match, value := optString(arg, "--scroll-off="); match { } else if match, value := optString(arg, "--scroll-off="); match {
@@ -1879,7 +1920,7 @@ func parseOptions(opts *Options, allArgs []string) {
errorExit("tab stop must be a positive integer") 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") errorExit("invalid listen port")
} }
@@ -1923,8 +1964,16 @@ func postProcessOptions(opts *Options) {
errorExit("--height option is currently not supported on this platform") errorExit("--height option is currently not supported on this platform")
} }
if opts.Scrollbar != nil && runewidth.StringWidth(*opts.Scrollbar) > 1 { if opts.Scrollbar != nil {
errorExit("scrollbar display width should be 1") runes := []rune(*opts.Scrollbar)
if len(runes) > 2 {
errorExit("--scrollbar should be given one or two characters")
}
for _, r := range runes {
if runewidth.RuneWidth(r) != 1 {
errorExit("scrollbar display width should be 1")
}
}
} }
// Default actions for CTRL-N / CTRL-P when --history is set // Default actions for CTRL-N / CTRL-P when --history is set
@@ -1940,25 +1989,25 @@ func postProcessOptions(opts *Options) {
// Extend the default key map // Extend the default key map
keymap := defaultKeymap() keymap := defaultKeymap()
for key, actions := range opts.Keymap { for key, actions := range opts.Keymap {
var lastChangePreviewWindow *action reordered := []*action{}
for _, act := range actions { for _, act := range actions {
switch act.t { switch act.t {
case actToggleSort: case actToggleSort:
// To display "+S"/"-S" on info line // To display "+S"/"-S" on info line
opts.ToggleSort = true opts.ToggleSort = true
case actChangePreviewWindow: case actTogglePreview, actShowPreview, actHidePreview, actChangePreviewWindow:
lastChangePreviewWindow = act reordered = append(reordered, act)
} }
} }
// Re-organize actions so that we only keep the last change-preview-window // Re-organize actions so that we put actions that change the preview window first in the list.
// and it comes first in the list.
// * change-preview-window(up,+10)+preview(sleep 3; cat {})+change-preview-window(up,+20) // * change-preview-window(up,+10)+preview(sleep 3; cat {})+change-preview-window(up,+20)
// -> change-preview-window(up,+20)+preview(sleep 3; cat {}) // -> change-preview-window(up,+10)+change-preview-window(up,+20)+preview(sleep 3; cat {})
if lastChangePreviewWindow != nil { if len(reordered) > 0 {
reordered := []*action{lastChangePreviewWindow}
for _, act := range actions { for _, act := range actions {
if act.t != actChangePreviewWindow { switch act.t {
case actTogglePreview, actShowPreview, actHidePreview, actChangePreviewWindow:
default:
reordered = append(reordered, act) reordered = append(reordered, act)
} }
} }
@@ -2001,9 +2050,7 @@ func postProcessOptions(opts *Options) {
theme := opts.Theme theme := opts.Theme
boldify := func(c tui.ColorAttr) tui.ColorAttr { boldify := func(c tui.ColorAttr) tui.ColorAttr {
dup := c dup := c
if !theme.Colored { if (c.Attr & tui.AttrRegular) == 0 {
dup.Attr |= tui.Bold
} else if (c.Attr & tui.AttrRegular) == 0 {
dup.Attr |= tui.Bold dup.Attr |= tui.Bold
} }
return dup return dup

View File

@@ -19,14 +19,26 @@ const (
maxContentLength = 1024 * 1024 maxContentLength = 1024 * 1024
) )
func startHttpServer(port int, channel chan []*action) error { func startHttpServer(port int, channel chan []*action) (error, int) {
if port == 0 { if port < 0 {
return nil return nil, port
} }
listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port)) listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
if err != nil { 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() { go func() {
@@ -45,7 +57,7 @@ func startHttpServer(port int, channel chan []*action) error {
listener.Close() listener.Close()
}() }()
return nil return nil, port
} }
// Here we are writing a simplistic HTTP server without using net/http // Here we are writing a simplistic HTTP server without using net/http

View File

@@ -3,6 +3,7 @@ package fzf
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"math" "math"
"os" "os"
@@ -183,6 +184,7 @@ type Terminal struct {
multi int multi int
sort bool sort bool
toggleSort bool toggleSort bool
track trackOption
delimiter Delimiter delimiter Delimiter
expect map[tui.Event]string expect map[tui.Event]string
keymap map[tui.Event][]*action keymap map[tui.Event][]*action
@@ -197,13 +199,13 @@ type Terminal struct {
header0 []string header0 []string
ellipsis string ellipsis string
scrollbar string scrollbar string
previewScrollbar string
ansi bool ansi bool
tabstop int tabstop int
margin [4]sizeSpec margin [4]sizeSpec
padding [4]sizeSpec padding [4]sizeSpec
strong tui.Attr
unicode bool unicode bool
listenPort int listenPort *int
borderShape tui.BorderShape borderShape tui.BorderShape
cleanExit bool cleanExit bool
paused bool paused bool
@@ -310,6 +312,7 @@ const (
actBackwardWord actBackwardWord
actCancel actCancel
actChangeBorderLabel actChangeBorderLabel
actChangeHeader
actChangePreviewLabel actChangePreviewLabel
actChangePrompt actChangePrompt
actChangeQuery actChangeQuery
@@ -337,6 +340,8 @@ const (
actToggleUp actToggleUp
actToggleIn actToggleIn
actToggleOut actToggleOut
actToggleTrack
actTrack
actDown actDown
actUp actUp
actPageUp actPageUp
@@ -355,6 +360,7 @@ const (
actTogglePreview actTogglePreview
actTogglePreviewWrap actTogglePreviewWrap
actTransformBorderLabel actTransformBorderLabel
actTransformHeader
actTransformPreviewLabel actTransformPreviewLabel
actTransformPrompt actTransformPrompt
actTransformQuery actTransformQuery
@@ -403,6 +409,7 @@ type searchRequest struct {
sort bool sort bool
sync bool sync bool
command *string command *string
changed bool
} }
type previewRequest struct { type previewRequest struct {
@@ -538,13 +545,9 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
} }
var previewBox *util.EventBox var previewBox *util.EventBox
// We need to start previewer if HTTP server is enabled even when --preview option is not specified // 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() previewBox = util.NewEventBox()
} }
strongAttr := tui.Bold
if !opts.Bold {
strongAttr = tui.AttrRegular
}
var renderer tui.Renderer var renderer tui.Renderer
fullscreen := !opts.Height.auto && (opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100) fullscreen := !opts.Height.auto && (opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100)
if fullscreen { if fullscreen {
@@ -604,6 +607,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
multi: opts.Multi, multi: opts.Multi,
sort: opts.Sort > 0, sort: opts.Sort > 0,
toggleSort: opts.ToggleSort, toggleSort: opts.ToggleSort,
track: opts.Track,
delimiter: opts.Delimiter, delimiter: opts.Delimiter,
expect: opts.Expect, expect: opts.Expect,
keymap: opts.Keymap, keymap: opts.Keymap,
@@ -623,11 +627,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
previewLabelOpts: opts.PreviewLabel, previewLabelOpts: opts.PreviewLabel,
cleanExit: opts.ClearOnExit, cleanExit: opts.ClearOnExit,
paused: opts.Phony, paused: opts.Phony,
strong: strongAttr,
cycle: opts.Cycle, cycle: opts.Cycle,
headerFirst: opts.HeaderFirst, headerFirst: opts.HeaderFirst,
headerLines: opts.HeaderLines, headerLines: opts.HeaderLines,
header: header, header: []string{},
header0: header, header0: header,
ellipsis: opts.Ellipsis, ellipsis: opts.Ellipsis,
ansi: opts.Ansi, ansi: opts.Ansi,
@@ -688,22 +691,42 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
} else { } else {
t.scrollbar = "|" t.scrollbar = "|"
} }
t.previewScrollbar = t.scrollbar
} else { } else {
t.scrollbar = *opts.Scrollbar runes := []rune(*opts.Scrollbar)
if len(runes) > 0 {
t.scrollbar = string(runes[0])
t.previewScrollbar = t.scrollbar
if len(runes) > 1 {
t.previewScrollbar = string(runes[1])
}
}
} }
_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()] _, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
if err := startHttpServer(t.listenPort, t.serverChan); err != nil { if t.listenPort != nil {
errorExit(err.Error()) err, port := startHttpServer(*t.listenPort, t.serverChan)
if err != nil {
errorExit(err.Error())
}
t.listenPort = &port
} }
return &t 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 { func borderLines(shape tui.BorderShape) int {
switch shape { switch shape {
case tui.BorderHorizontal, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderDouble: case tui.BorderHorizontal, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderDouble:
return 2 return 2
case tui.BorderTop, tui.BorderBottom: case tui.BorderTop, tui.BorderBottom:
return 1 return 1
@@ -740,7 +763,7 @@ func (t *Terminal) ansiLabelPrinter(str string, color *tui.ColorPair, fill bool)
// Simpler printer for strings without ANSI colors or tab characters // Simpler printer for strings without ANSI colors or tab characters
if colors == nil && strings.IndexRune(str, '\t') < 0 { if colors == nil && strings.IndexRune(str, '\t') < 0 {
length := runewidth.StringWidth(str) length := util.StringWidth(str)
if length == 0 { if length == 0 {
return nil, 0 return nil, 0
} }
@@ -874,10 +897,21 @@ func reverseStringArray(input []string) []string {
return reversed return reversed
} }
func (t *Terminal) changeHeader(header string) bool {
lines := strings.Split(strings.TrimSuffix(header, "\n"), "\n")
switch t.layout {
case layoutDefault, layoutReverseList:
lines = reverseStringArray(lines)
}
needFullRedraw := len(t.header0) != len(lines)
t.header0 = lines
return needFullRedraw
}
// UpdateHeader updates the header // UpdateHeader updates the header
func (t *Terminal) UpdateHeader(header []string) { func (t *Terminal) UpdateHeader(header []string) {
t.mutex.Lock() t.mutex.Lock()
t.header = append(append([]string{}, t.header0...), header...) t.header = header
t.mutex.Unlock() t.mutex.Unlock()
t.reqBox.Set(reqHeader, nil) t.reqBox.Set(reqHeader, nil)
} }
@@ -898,16 +932,54 @@ func (t *Terminal) UpdateProgress(progress float32) {
// UpdateList updates Merger to display the list // UpdateList updates Merger to display the list
func (t *Terminal) UpdateList(merger *Merger, reset bool) { func (t *Terminal) UpdateList(merger *Merger, reset bool) {
t.mutex.Lock() t.mutex.Lock()
var prevIndex int32 = -1
if !reset && t.track != trackDisabled {
if t.merger.Length() > 0 {
prevIndex = t.merger.Get(t.cy).item.Index()
} else if merger.Length() > 0 {
prevIndex = merger.First().item.Index()
}
}
t.progress = 100 t.progress = 100
t.merger = merger t.merger = merger
if reset { if reset {
t.selected = make(map[int32]selectedItem) t.selected = make(map[int32]selectedItem)
t.version++ t.version++
} }
if t.hasLoadActions && t.triggerLoad { if t.triggerLoad {
t.triggerLoad = false t.triggerLoad = false
t.eventChan <- tui.Load.AsEvent() t.eventChan <- tui.Load.AsEvent()
} }
if prevIndex >= 0 {
pos := t.cy - t.offset
count := t.merger.Length()
i := t.merger.FindIndex(prevIndex)
if i >= 0 {
t.cy = i
t.offset = t.cy - pos
} else if t.track == trackCurrent {
t.track = trackDisabled
t.cy = pos
t.offset = 0
} else if t.cy > count {
// Try to keep the vertical position when the list shrinks
t.cy = count - util.Min(count, t.maxItems()) + pos
}
}
if !t.reading {
switch t.merger.Length() {
case 0:
zero := tui.Zero.AsEvent()
if _, prs := t.keymap[zero]; prs {
t.eventChan <- zero
}
case 1:
one := tui.One.AsEvent()
if _, prs := t.keymap[one]; prs {
t.eventChan <- one
}
}
}
t.mutex.Unlock() t.mutex.Unlock()
t.reqBox.Set(reqInfo, nil) t.reqBox.Set(reqInfo, nil)
t.reqBox.Set(reqList, nil) t.reqBox.Set(reqList, nil)
@@ -1010,7 +1082,7 @@ func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) {
if idx == 3 { if idx == 3 {
extraMargin[idx] += 1 + bw extraMargin[idx] += 1 + bw
} }
case tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderDouble: case tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderDouble:
extraMargin[idx] += 1 + bw*(idx%2) extraMargin[idx] += 1 + bw*(idx%2)
} }
marginInt[idx] = sizeSpecToInt(idx, sizeSpec) + extraMargin[idx] marginInt[idx] = sizeSpecToInt(idx, sizeSpec) + extraMargin[idx]
@@ -1103,7 +1175,7 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
t.border = t.tui.NewWindow( t.border = t.tui.NewWindow(
marginInt[0], marginInt[3], width+(1+bw), height, marginInt[0], marginInt[3], width+(1+bw), height,
false, tui.MakeBorderStyle(tui.BorderRight, t.unicode)) false, tui.MakeBorderStyle(tui.BorderRight, t.unicode))
case tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderDouble: case tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderDouble:
t.border = t.tui.NewWindow( t.border = t.tui.NewWindow(
marginInt[0]-1, marginInt[3]-(1+bw), width+(1+bw)*2, height+2, marginInt[0]-1, marginInt[3]-(1+bw), width+(1+bw)*2, height+2,
false, tui.MakeBorderStyle(t.borderShape, t.unicode)) false, tui.MakeBorderStyle(t.borderShape, t.unicode))
@@ -1137,7 +1209,7 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
} }
t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder) t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder)
switch previewOpts.border { switch previewOpts.border {
case tui.BorderSharp, tui.BorderRounded, tui.BorderBold, tui.BorderDouble: case tui.BorderSharp, tui.BorderRounded, tui.BorderBold, tui.BorderBlock, tui.BorderDouble:
pwidth -= (1 + bw) * 2 pwidth -= (1 + bw) * 2
pheight -= 2 pheight -= 2
x += 1 + bw x += 1 + bw
@@ -1163,6 +1235,8 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
// Need a column to show scrollbar // Need a column to show scrollbar
pwidth -= 1 pwidth -= 1
} }
pwidth = util.Max(0, pwidth)
pheight = util.Max(0, pheight)
t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, true, noBorder) t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, true, noBorder)
} }
verticalPad := 2 verticalPad := 2
@@ -1279,7 +1353,7 @@ func (t *Terminal) printLabel(window tui.Window, render labelPrinter, opts label
} }
switch borderShape { switch borderShape {
case tui.BorderHorizontal, tui.BorderTop, tui.BorderBottom, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderDouble: case tui.BorderHorizontal, tui.BorderTop, tui.BorderBottom, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderBlock, tui.BorderDouble:
if redrawBorder { if redrawBorder {
window.DrawHBorder() window.DrawHBorder()
} }
@@ -1310,7 +1384,7 @@ func (t *Terminal) move(y int, x int, clear bool) {
case layoutDefault: case layoutDefault:
y = h - y - 1 y = h - y - 1
case layoutReverseList: case layoutReverseList:
n := 2 + len(t.header) n := 2 + len(t.header0) + len(t.header)
if t.noInfoLine() { if t.noInfoLine() {
n-- n--
} }
@@ -1338,8 +1412,7 @@ func (t *Terminal) updatePromptOffset() ([]rune, []rune) {
_, overflow := t.trimLeft(t.input[:t.cx], maxWidth) _, overflow := t.trimLeft(t.input[:t.cx], maxWidth)
minOffset := int(overflow) 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) t.xoffset = util.Constrain(t.xoffset, minOffset, maxOffset)
before, _ := t.trimLeft(t.input[t.xoffset:t.cx], maxWidth) before, _ := t.trimLeft(t.input[t.xoffset:t.cx], maxWidth)
beforeLen := t.displayWidth(before) beforeLen := t.displayWidth(before)
@@ -1404,7 +1477,7 @@ func (t *Terminal) printInfo() {
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1 pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
str := t.infoSep str := t.infoSep
maxWidth := t.window.Width() - pos maxWidth := t.window.Width() - pos
width := runewidth.StringWidth(str) width := util.StringWidth(str)
if width > maxWidth { if width > maxWidth {
trimmed, _ := t.trimRight([]rune(str), maxWidth) trimmed, _ := t.trimRight([]rune(str), maxWidth)
str = string(trimmed) str = string(trimmed)
@@ -1431,6 +1504,9 @@ func (t *Terminal) printInfo() {
output += " -S" output += " -S"
} }
} }
if t.track != trackDisabled {
output += " +T"
}
if t.multi > 0 { if t.multi > 0 {
if t.multi == maxMulti { if t.multi == maxMulti {
output += fmt.Sprintf(" (%d)", len(t.selected)) output += fmt.Sprintf(" (%d)", len(t.selected))
@@ -1456,7 +1532,7 @@ func (t *Terminal) printInfo() {
} }
func (t *Terminal) printHeader() { func (t *Terminal) printHeader() {
if len(t.header) == 0 { if len(t.header0)+len(t.header) == 0 {
return return
} }
max := t.window.Height() max := t.window.Height()
@@ -1467,7 +1543,7 @@ func (t *Terminal) printHeader() {
} }
} }
var state *ansiState var state *ansiState
for idx, lineStr := range t.header { for idx, lineStr := range append(append([]string{}, t.header0...), t.header...) {
line := idx line := idx
if !t.headerFirst { if !t.headerFirst {
line++ line++
@@ -1501,7 +1577,7 @@ func (t *Terminal) printList() {
if t.layout == layoutDefault { if t.layout == layoutDefault {
i = maxy - 1 - j i = maxy - 1 - j
} }
line := i + 2 + len(t.header) line := i + 2 + len(t.header0) + len(t.header)
if t.noInfoLine() { if t.noInfoLine() {
line-- line--
} }
@@ -1807,7 +1883,7 @@ func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unc
if ansi != nil { if ansi != nil {
ansi.lbg = -1 ansi.lbg = -1
} }
line = strings.TrimSuffix(line, "\n") line = strings.TrimRight(line, "\r\n")
if lineNo >= height || t.pwindow.Y() == height-1 && t.pwindow.X() > 0 { if lineNo >= height || t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
t.previewed.filled = true t.previewed.filled = true
t.previewer.scrollable = true t.previewer.scrollable = true
@@ -1829,12 +1905,14 @@ func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unc
trimmed, isTrimmed = t.trimRight(trimmed, maxWidth-t.pwindow.X()) trimmed, isTrimmed = t.trimRight(trimmed, maxWidth-t.pwindow.X())
} }
str, width := t.processTabs(trimmed, prefixWidth) str, width := t.processTabs(trimmed, prefixWidth)
prefixWidth += width if width > prefixWidth {
if t.theme.Colored && ansi != nil && ansi.colored() { prefixWidth = width
lbg = ansi.lbg if t.theme.Colored && ansi != nil && ansi.colored() {
fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str) lbg = ansi.lbg
} else { fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str)
fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str) } else {
fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str)
}
} }
return !isTrimmed && return !isTrimmed &&
(fillRet == tui.FillContinue || t.previewOpts.wrap && fillRet == tui.FillNextLine) (fillRet == tui.FillContinue || t.previewOpts.wrap && fillRet == tui.FillNextLine)
@@ -1863,7 +1941,9 @@ func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unc
func (t *Terminal) renderPreviewScrollbar(yoff int, barLength int, barStart int) { func (t *Terminal) renderPreviewScrollbar(yoff int, barLength int, barStart int) {
height := t.pwindow.Height() height := t.pwindow.Height()
w := t.pborder.Width() w := t.pborder.Width()
redraw := false
if len(t.previewer.bar) != height { if len(t.previewer.bar) != height {
redraw = true
t.previewer.bar = make([]bool, height) t.previewer.bar = make([]bool, height)
} }
xshift := -1 - t.borderWidth xshift := -1 - t.borderWidth
@@ -1880,22 +1960,22 @@ func (t *Terminal) renderPreviewScrollbar(yoff int, barLength int, barStart int)
// Avoid unnecessary redraws // Avoid unnecessary redraws
bar := i >= yoff+barStart && i < yoff+barStart+barLength bar := i >= yoff+barStart && i < yoff+barStart+barLength
if bar == t.previewer.bar[i] && !t.tui.NeedScrollbarRedraw() { if !redraw && bar == t.previewer.bar[i] && !t.tui.NeedScrollbarRedraw() {
continue continue
} }
t.previewer.bar[i] = bar t.previewer.bar[i] = bar
t.pborder.Move(y, x) t.pborder.Move(y, x)
if i >= yoff+barStart && i < yoff+barStart+barLength { if i >= yoff+barStart && i < yoff+barStart+barLength {
t.pborder.CPrint(tui.ColScrollbar, t.scrollbar) t.pborder.CPrint(tui.ColPreviewScrollbar, t.previewScrollbar)
} else { } else {
t.pborder.Print(" ") t.pborder.CPrint(tui.ColPreviewScrollbar, " ")
} }
} }
} }
func (t *Terminal) printPreview() { func (t *Terminal) printPreview() {
if !t.hasPreviewWindow() { if !t.hasPreviewWindow() || t.pwindow.Height() == 0 {
return return
} }
numLines := len(t.previewer.lines) numLines := len(t.previewer.lines)
@@ -1937,7 +2017,7 @@ func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) {
w = t.tabstop - l%t.tabstop w = t.tabstop - l%t.tabstop
strbuf.WriteString(strings.Repeat(" ", w)) strbuf.WriteString(strings.Repeat(" ", w))
} else { } else {
w = runewidth.StringWidth(str) w = util.StringWidth(str)
strbuf.WriteString(str) strbuf.WriteString(str)
} }
l += w l += w
@@ -2237,16 +2317,17 @@ func (t *Terminal) redraw() {
t.printAll() t.printAll()
} }
func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, captureFirstLine bool) string { func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, capture bool, firstLineOnly bool) string {
line := "" line := ""
valid, list := t.buildPlusList(template, forcePlus) valid, list := t.buildPlusList(template, forcePlus)
// captureFirstLine is used for transform-{prompt,query} and we don't want to // 'capture' is used for transform-* and we don't want to
// return an empty string in those cases // return an empty string in those cases
if !valid && !captureFirstLine { if !valid && !capture {
return line return line
} }
command := t.replacePlaceholder(template, forcePlus, string(t.input), list) command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
cmd := util.ExecCommand(command, false) cmd := util.ExecCommand(command, false)
cmd.Env = t.environ()
t.executing.Set(true) t.executing.Set(true)
if !background { if !background {
cmd.Stdin = tui.TtyIn() cmd.Stdin = tui.TtyIn()
@@ -2258,12 +2339,17 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
t.redraw() t.redraw()
t.refresh() t.refresh()
} else { } else {
if captureFirstLine { if capture {
out, _ := cmd.StdoutPipe() out, _ := cmd.StdoutPipe()
reader := bufio.NewReader(out) reader := bufio.NewReader(out)
cmd.Start() cmd.Start()
line, _ = reader.ReadString('\n') if firstLineOnly {
line = strings.TrimRight(line, "\r\n") line, _ = reader.ReadString('\n')
line = strings.TrimRight(line, "\r\n")
} else {
bytes, _ := io.ReadAll(reader)
line = string(bytes)
}
cmd.Wait() cmd.Wait()
} else { } else {
cmd.Run() cmd.Run()
@@ -2493,17 +2579,17 @@ func (t *Terminal) Loop() {
_, query := t.Input() _, query := t.Input()
command := t.replacePlaceholder(commandTemplate, false, string(query), items) command := t.replacePlaceholder(commandTemplate, false, string(query), items)
cmd := util.ExecCommand(command, true) cmd := util.ExecCommand(command, true)
env := t.environ()
if pwindow != nil { if pwindow != nil {
height := pwindow.Height() height := pwindow.Height()
env := os.Environ()
lines := fmt.Sprintf("LINES=%d", height) lines := fmt.Sprintf("LINES=%d", height)
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width()) columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
env = append(env, lines) env = append(env, lines)
env = append(env, "FZF_PREVIEW_"+lines) env = append(env, "FZF_PREVIEW_"+lines)
env = append(env, columns) env = append(env, columns)
env = append(env, "FZF_PREVIEW_"+columns) env = append(env, "FZF_PREVIEW_"+columns)
cmd.Env = env
} }
cmd.Env = env
out, _ := cmd.StdoutPipe() out, _ := cmd.StdoutPipe()
cmd.Stderr = cmd.Stdout cmd.Stderr = cmd.Stdout
@@ -2633,11 +2719,6 @@ func (t *Terminal) Loop() {
} }
} }
var onFocus []*action
if actions, prs := t.keymap[tui.Focus.AsEvent()]; prs {
onFocus = actions
}
go func() { go func() {
var focusedIndex int32 = minItem.Index() var focusedIndex int32 = minItem.Index()
var version int64 = -1 var version int64 = -1
@@ -2674,7 +2755,11 @@ func (t *Terminal) Loop() {
currentIndex = currentItem.Index() currentIndex = currentItem.Index()
} }
focusChanged := focusedIndex != currentIndex focusChanged := focusedIndex != currentIndex
if onFocus != nil && focusChanged { if focusChanged && t.track == trackCurrent {
t.track = trackDisabled
t.printInfo()
}
if onFocus, prs := t.keymap[tui.Focus.AsEvent()]; prs && focusChanged {
t.serverChan <- onFocus t.serverChan <- onFocus
} }
if focusChanged || version != t.version { if focusChanged || version != t.version {
@@ -2786,7 +2871,7 @@ func (t *Terminal) Loop() {
} }
select { select {
case event = <-t.eventChan: case event = <-t.eventChan:
needBarrier = event != tui.Load.AsEvent() needBarrier = !event.Is(tui.Load, tui.One, tui.Zero)
case actions = <-t.serverChan: case actions = <-t.serverChan:
event = tui.Invalid.AsEvent() event = tui.Invalid.AsEvent()
needBarrier = false needBarrier = false
@@ -2876,14 +2961,22 @@ func (t *Terminal) Loop() {
if t.history != nil { if t.history != nil {
t.history.append(string(t.input)) t.history.append(string(t.input))
} }
/*
FIXME: It is not at all clear why this is required.
The following command will report 'not a tty', unless we open
/dev/tty *twice* after closing the standard input for 'reload'
in Reader.terminate().
: | fzf --bind 'start:reload:ls' --bind 'enter:become:tty'
*/
tui.TtyIn()
util.SetStdin(tui.TtyIn()) util.SetStdin(tui.TtyIn())
syscall.Exec(shellPath, []string{shell, "-c", command}, os.Environ()) syscall.Exec(shellPath, []string{shell, "-c", command}, os.Environ())
} }
} }
case actExecute, actExecuteSilent: case actExecute, actExecuteSilent:
t.executeCommand(a.a, false, a.t == actExecuteSilent, false) t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false)
case actExecuteMulti: case actExecuteMulti:
t.executeCommand(a.a, true, false, false) t.executeCommand(a.a, true, false, false, false)
case actInvalid: case actInvalid:
t.mutex.Unlock() t.mutex.Unlock()
return false return false
@@ -2917,11 +3010,11 @@ func (t *Terminal) Loop() {
req(reqPreviewRefresh) req(reqPreviewRefresh)
} }
case actTransformPrompt: case actTransformPrompt:
prompt := t.executeCommand(a.a, false, true, true) prompt := t.executeCommand(a.a, false, true, true, true)
t.prompt, t.promptLen = t.parsePrompt(prompt) t.prompt, t.promptLen = t.parsePrompt(prompt)
req(reqPrompt) req(reqPrompt)
case actTransformQuery: case actTransformQuery:
query := t.executeCommand(a.a, false, true, true) query := t.executeCommand(a.a, false, true, true, true)
t.input = []rune(query) t.input = []rune(query)
t.cx = len(t.input) t.cx = len(t.input)
case actToggleSort: case actToggleSort:
@@ -2970,6 +3063,19 @@ func (t *Terminal) Loop() {
case actChangeQuery: case actChangeQuery:
t.input = []rune(a.a) t.input = []rune(a.a)
t.cx = len(t.input) t.cx = len(t.input)
case actTransformHeader:
header := t.executeCommand(a.a, false, true, true, false)
if t.changeHeader(header) {
req(reqFullRedraw)
} else {
req(reqHeader)
}
case actChangeHeader:
if t.changeHeader(a.a) {
req(reqFullRedraw)
} else {
req(reqHeader)
}
case actChangeBorderLabel: case actChangeBorderLabel:
if t.border != nil { if t.border != nil {
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(a.a, &tui.ColBorderLabel, false) t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(a.a, &tui.ColBorderLabel, false)
@@ -2982,13 +3088,13 @@ func (t *Terminal) Loop() {
} }
case actTransformBorderLabel: case actTransformBorderLabel:
if t.border != nil { if t.border != nil {
label := t.executeCommand(a.a, false, true, true) label := t.executeCommand(a.a, false, true, true, true)
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false) t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false)
req(reqRedrawBorderLabel) req(reqRedrawBorderLabel)
} }
case actTransformPreviewLabel: case actTransformPreviewLabel:
if t.pborder != nil { if t.pborder != nil {
label := t.executeCommand(a.a, false, true, true) label := t.executeCommand(a.a, false, true, true, true)
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false) t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false)
req(reqRedrawPreviewLabel) req(reqRedrawPreviewLabel)
} }
@@ -3238,6 +3344,19 @@ func (t *Terminal) Loop() {
t.paused = !t.paused t.paused = !t.paused
changed = !t.paused changed = !t.paused
req(reqPrompt) req(reqPrompt)
case actToggleTrack:
switch t.track {
case trackEnabled:
t.track = trackDisabled
case trackDisabled:
t.track = trackEnabled
}
req(reqInfo)
case actTrack:
if t.track == trackDisabled {
t.track = trackCurrent
}
req(reqInfo)
case actEnableSearch: case actEnableSearch:
t.paused = false t.paused = false
changed = true changed = true
@@ -3289,7 +3408,7 @@ func (t *Terminal) Loop() {
break break
} }
// Prevew scrollbar dragging // Preview scrollbar dragging
headerLines := t.previewOpts.headerLines 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()) 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 { if pbarDragging {
@@ -3315,7 +3434,7 @@ func (t *Terminal) Loop() {
// Translate coordinates // Translate coordinates
mx -= t.window.Left() mx -= t.window.Left()
my -= t.window.Top() my -= t.window.Top()
min := 2 + len(t.header) min := 2 + len(t.header0) + len(t.header)
if t.noInfoLine() { if t.noInfoLine() {
min-- min--
} }
@@ -3420,6 +3539,9 @@ func (t *Terminal) Loop() {
// Split window options // Split window options
tokens := strings.Split(a.a, "|") tokens := strings.Split(a.a, "|")
if len(tokens[0]) > 0 && t.initialPreviewOpts.hidden {
t.previewOpts.hidden = false
}
parsePreviewWindow(&t.previewOpts, tokens[0]) parsePreviewWindow(&t.previewOpts, tokens[0])
if len(tokens) > 1 { if len(tokens) > 1 {
a.a = strings.Join(append(tokens[1:], tokens[0]), "|") a.a = strings.Join(append(tokens[1:], tokens[0]), "|")
@@ -3520,7 +3642,7 @@ func (t *Terminal) Loop() {
t.mutex.Unlock() // Must be unlocked before touching reqBox t.mutex.Unlock() // Must be unlocked before touching reqBox
if changed || newCommand != nil { if changed || newCommand != nil {
t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, sync: reloadSync, command: newCommand}) t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, sync: reloadSync, command: newCommand, changed: changed})
} }
for _, event := range events { for _, event := range events {
t.reqBox.Set(event, nil) t.reqBox.Set(event, nil)
@@ -3584,7 +3706,7 @@ func (t *Terminal) vset(o int) bool {
} }
func (t *Terminal) maxItems() int { func (t *Terminal) maxItems() int {
max := t.window.Height() - 2 - len(t.header) max := t.window.Height() - 2 - len(t.header0) - len(t.header)
if t.noInfoLine() { if t.noInfoLine() {
max++ max++
} }

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") var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
func (r *LightRenderer) stderr(str string) { func (r *LightRenderer) stderr(str string) {
r.stderrInternal(str, true) r.stderrInternal(str, true, "")
} }
// FIXME: Need better handling of non-displayable characters const CR string = "\x1b[2m␍"
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) { const LF string = "\x1b[2m␊"
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
bytes := []byte(str) bytes := []byte(str)
runes := []rune{} runes := []rune{}
for len(bytes) > 0 { for len(bytes) > 0 {
r, sz := utf8.DecodeRune(bytes) r, sz := utf8.DecodeRune(bytes)
nlcr := r == '\n' || r == '\r' nlcr := r == '\n' || r == '\r'
if r >= 32 || r == '\x1b' || nlcr { if r >= 32 || r == '\x1b' || nlcr {
if r == utf8.RuneError || nlcr && !allowNLCR { if nlcr && !allowNLCR {
runes = append(runes, ' ') if r == '\r' {
} else { runes = append(runes, []rune(CR+resetCode)...)
} else {
runes = append(runes, []rune(LF+resetCode)...)
}
} else if r != utf8.RuneError {
runes = append(runes, r) runes = append(runes, r)
} }
} }
@@ -54,8 +60,10 @@ func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) {
r.queued.WriteString(string(runes)) r.queued.WriteString(string(runes))
} }
func (r *LightRenderer) csi(code string) { func (r *LightRenderer) csi(code string) string {
r.stderr("\x1b[" + code) fullcode := "\x1b[" + code
r.stderr(fullcode)
return fullcode
} }
func (r *LightRenderer) flush() { func (r *LightRenderer) flush() {
@@ -422,7 +430,19 @@ func (r *LightRenderer) escSequence(sz *int) Event {
} }
return Event{Invalid, 0, nil} // INS return Event{Invalid, 0, nil} // INS
case '3': case '3':
return Event{Del, 0, nil} if r.buffer[3] == '~' {
return Event{Del, 0, nil}
}
if len(r.buffer) == 6 && r.buffer[5] == '~' {
*sz = 6
switch r.buffer[4] {
case '5':
return Event{CtrlDelete, 0, nil}
case '2':
return Event{SDelete, 0, nil}
}
}
return Event{Invalid, 0, nil}
case '4': case '4':
return Event{End, 0, nil} return Event{End, 0, nil}
case '5': case '5':
@@ -737,7 +757,7 @@ func (w *LightWindow) DrawHBorder() {
func (w *LightWindow) drawBorder(onlyHorizontal bool) { func (w *LightWindow) drawBorder(onlyHorizontal bool) {
switch w.border.shape { switch w.border.shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble: case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderDouble:
w.drawBorderAround(onlyHorizontal) w.drawBorderAround(onlyHorizontal)
case BorderHorizontal: case BorderHorizontal:
w.drawBorderHorizontal(true, true) w.drawBorderHorizontal(true, true)
@@ -768,14 +788,14 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
if w.preview { if w.preview {
color = ColPreviewBorder color = ColPreviewBorder
} }
hw := runewidth.RuneWidth(w.border.horizontal) hw := runewidth.RuneWidth(w.border.top)
if top { if top {
w.Move(0, 0) w.Move(0, 0)
w.CPrint(color, repeat(w.border.horizontal, w.width/hw)) w.CPrint(color, repeat(w.border.top, w.width/hw))
} }
if bottom { if bottom {
w.Move(w.height-1, 0) w.Move(w.height-1, 0)
w.CPrint(color, repeat(w.border.horizontal, w.width/hw)) w.CPrint(color, repeat(w.border.bottom, w.width/hw))
} }
} }
@@ -791,11 +811,11 @@ func (w *LightWindow) drawBorderVertical(left, right bool) {
for y := 0; y < w.height; y++ { for y := 0; y < w.height; y++ {
w.Move(y, 0) w.Move(y, 0)
if left { if left {
w.CPrint(color, string(w.border.vertical)) w.CPrint(color, string(w.border.left))
} }
w.CPrint(color, repeat(' ', width)) w.CPrint(color, repeat(' ', width))
if right { if right {
w.CPrint(color, string(w.border.vertical)) w.CPrint(color, string(w.border.right))
} }
} }
} }
@@ -806,31 +826,31 @@ func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
if w.preview { if w.preview {
color = ColPreviewBorder color = ColPreviewBorder
} }
hw := runewidth.RuneWidth(w.border.horizontal) hw := runewidth.RuneWidth(w.border.top)
tcw := runewidth.RuneWidth(w.border.topLeft) + runewidth.RuneWidth(w.border.topRight) tcw := runewidth.RuneWidth(w.border.topLeft) + runewidth.RuneWidth(w.border.topRight)
bcw := runewidth.RuneWidth(w.border.bottomLeft) + runewidth.RuneWidth(w.border.bottomRight) bcw := runewidth.RuneWidth(w.border.bottomLeft) + runewidth.RuneWidth(w.border.bottomRight)
rem := (w.width - tcw) % hw rem := (w.width - tcw) % hw
w.CPrint(color, string(w.border.topLeft)+repeat(w.border.horizontal, (w.width-tcw)/hw)+repeat(' ', rem)+string(w.border.topRight)) w.CPrint(color, string(w.border.topLeft)+repeat(w.border.top, (w.width-tcw)/hw)+repeat(' ', rem)+string(w.border.topRight))
if !onlyHorizontal { if !onlyHorizontal {
vw := runewidth.RuneWidth(w.border.vertical) vw := runewidth.RuneWidth(w.border.left)
for y := 1; y < w.height-1; y++ { for y := 1; y < w.height-1; y++ {
w.Move(y, 0) w.Move(y, 0)
w.CPrint(color, string(w.border.vertical)) w.CPrint(color, string(w.border.left))
w.CPrint(color, repeat(' ', w.width-vw*2)) w.CPrint(color, repeat(' ', w.width-vw*2))
w.CPrint(color, string(w.border.vertical)) w.CPrint(color, string(w.border.right))
} }
} }
w.Move(w.height-1, 0) w.Move(w.height-1, 0)
rem = (w.width - bcw) % hw rem = (w.width - bcw) % hw
w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.horizontal, (w.width-bcw)/hw)+repeat(' ', rem)+string(w.border.bottomRight)) w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.bottom, (w.width-bcw)/hw)+repeat(' ', rem)+string(w.border.bottomRight))
} }
func (w *LightWindow) csi(code string) { func (w *LightWindow) csi(code string) string {
w.renderer.csi(code) return w.renderer.csi(code)
} }
func (w *LightWindow) stderrInternal(str string, allowNLCR bool) { func (w *LightWindow) stderrInternal(str string, allowNLCR bool, resetCode string) {
w.renderer.stderrInternal(str, allowNLCR) w.renderer.stderrInternal(str, allowNLCR, resetCode)
} }
func (w *LightWindow) Top() int { func (w *LightWindow) Top() int {
@@ -936,10 +956,10 @@ func colorCodes(fg Color, bg Color) []string {
return codes 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)...) codes := append(attrCodes(attr), colorCodes(fg, bg)...)
w.csi(";" + strings.Join(codes, ";") + "m") code := w.csi(";" + strings.Join(codes, ";") + "m")
return len(codes) > 0 return len(codes) > 0, code
} }
func (w *LightWindow) Print(text string) { func (w *LightWindow) Print(text string) {
@@ -951,16 +971,17 @@ func cleanse(str string) string {
} }
func (w *LightWindow) CPrint(pair ColorPair, text string) { func (w *LightWindow) CPrint(pair ColorPair, text string) {
w.csiColor(pair.Fg(), pair.Bg(), pair.Attr()) _, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Attr())
w.stderrInternal(cleanse(text), false) w.stderrInternal(cleanse(text), false, code)
w.csi("m") w.csi("m")
} }
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) { 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") defer w.csi("m")
} }
w.stderrInternal(cleanse(text), false) w.stderrInternal(cleanse(text), false, code)
} }
type wrappedLine struct { type wrappedLine struct {
@@ -980,6 +1001,8 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
if len(rs) == 1 && rs[0] == '\t' { if len(rs) == 1 && rs[0] == '\t' {
w = tabstop - (prefixLength+width)%tabstop w = tabstop - (prefixLength+width)%tabstop
str = repeat(' ', w) str = repeat(' ', w)
} else if rs[0] == '\r' {
w++
} else { } else {
w = runewidth.StringWidth(str) w = runewidth.StringWidth(str)
} }
@@ -998,12 +1021,12 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
return lines 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") allLines := strings.Split(str, "\n")
for i, line := range allLines { for i, line := range allLines {
lines := wrapLine(line, w.posx, w.width, w.tabstop) lines := wrapLine(line, w.posx, w.width, w.tabstop)
for j, wl := range lines { for j, wl := range lines {
w.stderrInternal(wl.text, false) w.stderrInternal(wl.text, false, resetCode)
w.posx += wl.displayWidth w.posx += wl.displayWidth
// Wrap line // Wrap line
@@ -1013,7 +1036,7 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
} }
w.MoveAndClear(w.posy, w.posx) w.MoveAndClear(w.posy, w.posx)
w.Move(w.posy+1, 0) w.Move(w.posy+1, 0)
onMove() w.renderer.stderr(resetCode)
} }
} }
} }
@@ -1022,22 +1045,26 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
return FillSuspend return FillSuspend
} }
w.Move(w.posy+1, 0) w.Move(w.posy+1, 0)
onMove() w.renderer.stderr(resetCode)
return FillNextLine return FillNextLine
} }
return FillContinue return FillContinue
} }
func (w *LightWindow) setBg() { func (w *LightWindow) setBg() string {
if w.bg != colDefault { 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 { func (w *LightWindow) Fill(text string) FillReturn {
w.Move(w.posy, w.posx) w.Move(w.posy, w.posx)
w.setBg() code := w.setBg()
return w.fill(text, w.setBg) return w.fill(text, code)
} }
func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn { func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn {
@@ -1048,11 +1075,11 @@ func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillRetu
if bg == colDefault { if bg == colDefault {
bg = w.bg bg = w.bg
} }
if w.csiColor(fg, bg, attr) { if hasColors, resetCode := w.csiColor(fg, bg, attr); hasColors {
defer w.csi("m") 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() { func (w *LightWindow) FinishFill() {

View File

@@ -8,6 +8,7 @@ import (
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/encoding" "github.com/gdamore/tcell/v2/encoding"
"github.com/junegunn/fzf/src/util"
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"github.com/rivo/uniseg" "github.com/rivo/uniseg"
@@ -412,6 +413,12 @@ func (r *FullscreenRenderer) GetChar() Event {
case tcell.KeyHome: case tcell.KeyHome:
return Event{Home, 0, nil} return Event{Home, 0, nil}
case tcell.KeyDelete: case tcell.KeyDelete:
if ctrl {
return Event{CtrlDelete, 0, nil}
}
if shift {
return Event{SDelete, 0, nil}
}
return Event{Del, 0, nil} return Event{Del, 0, nil}
case tcell.KeyEnd: case tcell.KeyEnd:
return Event{End, 0, nil} return Event{End, 0, nil}
@@ -572,26 +579,27 @@ func (w *TcellWindow) printString(text string, pair ColorPair) {
gr := uniseg.NewGraphemes(text) gr := uniseg.NewGraphemes(text)
for gr.Next() { for gr.Next() {
st := style
rs := gr.Runes() rs := gr.Runes()
if len(rs) == 1 { if len(rs) == 1 {
r := rs[0] r := rs[0]
if r < rune(' ') { // ignore control characters if r == '\r' {
continue st = style.Dim(true)
rs[0] = '␍'
} else if r == '\n' { } else if r == '\n' {
w.lastY++ st = style.Dim(true)
lx = 0 rs[0] = '␊'
continue } else if r < rune(' ') { // ignore control characters
} else if r == '\u000D' { // skip carriage return
continue continue
} }
} }
var xPos = w.left + w.lastX + lx var xPos = w.left + w.lastX + lx
var yPos = w.top + w.lastY var yPos = w.top + w.lastY
if xPos < (w.left+w.width) && yPos < (w.top+w.height) { if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
_screen.SetContent(xPos, yPos, 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 w.lastX += lx
} }
@@ -620,13 +628,22 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
Italic(a&Attr(tcell.AttrItalic) != 0) Italic(a&Attr(tcell.AttrItalic) != 0)
gr := uniseg.NewGraphemes(text) gr := uniseg.NewGraphemes(text)
Loop:
for gr.Next() { for gr.Next() {
st := style
rs := gr.Runes() rs := gr.Runes()
if len(rs) == 1 && rs[0] == '\n' { if len(rs) == 1 {
w.lastY++ r := rs[0]
w.lastX = 0 switch r {
lx = 0 case '\r':
continue st = style.Dim(true)
rs[0] = '␍'
case '\n':
w.lastY++
w.lastX = 0
lx = 0
continue Loop
}
} }
// word wrap: // word wrap:
@@ -643,8 +660,8 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
return FillSuspend return FillSuspend
} }
_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 w.lastX += lx
if w.lastX == w.width { if w.lastX == w.width {
@@ -696,9 +713,9 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
style = w.normal.style() style = w.normal.style()
} }
hw := runewidth.RuneWidth(w.borderStyle.horizontal) hw := runewidth.RuneWidth(w.borderStyle.top)
switch shape { switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderHorizontal, BorderTop: case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderDouble, BorderHorizontal, BorderTop:
max := right - 2*hw max := right - 2*hw
if shape == BorderHorizontal || shape == BorderTop { if shape == BorderHorizontal || shape == BorderTop {
max = right - hw max = right - hw
@@ -709,36 +726,36 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
// ================== // ==================
// ( HH ) => TR is ignored // ( HH ) => TR is ignored
for x := left; x <= max; x += hw { for x := left; x <= max; x += hw {
_screen.SetContent(x, top, w.borderStyle.horizontal, nil, style) _screen.SetContent(x, top, w.borderStyle.top, nil, style)
} }
} }
switch shape { switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderHorizontal, BorderBottom: case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderDouble, BorderHorizontal, BorderBottom:
max := right - 2*hw max := right - 2*hw
if shape == BorderHorizontal || shape == BorderBottom { if shape == BorderHorizontal || shape == BorderBottom {
max = right - hw max = right - hw
} }
for x := left; x <= max; x += hw { for x := left; x <= max; x += hw {
_screen.SetContent(x, bot-1, w.borderStyle.horizontal, nil, style) _screen.SetContent(x, bot-1, w.borderStyle.bottom, nil, style)
} }
} }
if !onlyHorizontal { if !onlyHorizontal {
switch shape { switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderLeft: case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderDouble, BorderVertical, BorderLeft:
for y := top; y < bot; y++ { for y := top; y < bot; y++ {
_screen.SetContent(left, y, w.borderStyle.vertical, nil, style) _screen.SetContent(left, y, w.borderStyle.left, nil, style)
} }
} }
switch shape { switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderRight: case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderDouble, BorderVertical, BorderRight:
vw := runewidth.RuneWidth(w.borderStyle.vertical) vw := runewidth.RuneWidth(w.borderStyle.right)
for y := top; y < bot; y++ { for y := top; y < bot; y++ {
_screen.SetContent(right-vw, y, w.borderStyle.vertical, nil, style) _screen.SetContent(right-vw, y, w.borderStyle.right, nil, style)
} }
} }
} }
switch shape { switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble: case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderDouble:
_screen.SetContent(left, top, w.borderStyle.topLeft, nil, style) _screen.SetContent(left, top, w.borderStyle.topLeft, nil, style)
_screen.SetContent(right-runewidth.RuneWidth(w.borderStyle.topRight), top, w.borderStyle.topRight, nil, style) _screen.SetContent(right-runewidth.RuneWidth(w.borderStyle.topRight), top, w.borderStyle.topRight, nil, style)
_screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style) _screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style)

View File

@@ -41,6 +41,7 @@ const (
CtrlZ CtrlZ
ESC ESC
CtrlSpace CtrlSpace
CtrlDelete
// https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal // https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal
CtrlBackSlash CtrlBackSlash
@@ -74,6 +75,7 @@ const (
SDown SDown
SLeft SLeft
SRight SRight
SDelete
F1 F1
F2 F2
@@ -93,6 +95,8 @@ const (
Start Start
Load Load
Focus Focus
One
Zero
AltBS AltBS
@@ -251,29 +255,31 @@ func (p ColorPair) MergeNonDefault(other ColorPair) ColorPair {
} }
type ColorTheme struct { type ColorTheme struct {
Colored bool Colored bool
Input ColorAttr Input ColorAttr
Disabled ColorAttr Disabled ColorAttr
Fg ColorAttr Fg ColorAttr
Bg ColorAttr Bg ColorAttr
PreviewFg ColorAttr PreviewFg ColorAttr
PreviewBg ColorAttr PreviewBg ColorAttr
DarkBg ColorAttr DarkBg ColorAttr
Gutter ColorAttr Gutter ColorAttr
Prompt ColorAttr Prompt ColorAttr
Match ColorAttr Match ColorAttr
Current ColorAttr Current ColorAttr
CurrentMatch ColorAttr CurrentMatch ColorAttr
Spinner ColorAttr Spinner ColorAttr
Info ColorAttr Info ColorAttr
Cursor ColorAttr Cursor ColorAttr
Selected ColorAttr Selected ColorAttr
Header ColorAttr Header ColorAttr
Separator ColorAttr Separator ColorAttr
Scrollbar ColorAttr Scrollbar ColorAttr
Border ColorAttr Border ColorAttr
BorderLabel ColorAttr PreviewBorder ColorAttr
PreviewLabel ColorAttr PreviewScrollbar ColorAttr
BorderLabel ColorAttr
PreviewLabel ColorAttr
} }
type Event struct { type Event struct {
@@ -282,6 +288,15 @@ type Event struct {
MouseEvent *MouseEvent MouseEvent *MouseEvent
} }
func (e Event) Is(types ...EventType) bool {
for _, t := range types {
if e.Type == t {
return true
}
}
return false
}
type MouseEvent struct { type MouseEvent struct {
Y int Y int
X int X int
@@ -299,6 +314,7 @@ const (
BorderRounded BorderRounded
BorderSharp BorderSharp
BorderBold BorderBold
BorderBlock
BorderDouble BorderDouble
BorderHorizontal BorderHorizontal
BorderVertical BorderVertical
@@ -326,8 +342,10 @@ func (s BorderShape) HasTop() bool {
type BorderStyle struct { type BorderStyle struct {
shape BorderShape shape BorderShape
horizontal rune top rune
vertical rune bottom rune
left rune
right rune
topLeft rune topLeft rune
topRight rune topRight rune
bottomLeft rune bottomLeft rune
@@ -340,8 +358,10 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
if !unicode { if !unicode {
return BorderStyle{ return BorderStyle{
shape: shape, shape: shape,
horizontal: '-', top: '-',
vertical: '|', bottom: '-',
left: '|',
right: '|',
topLeft: '+', topLeft: '+',
topRight: '+', topRight: '+',
bottomLeft: '+', bottomLeft: '+',
@@ -352,8 +372,10 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
case BorderSharp: case BorderSharp:
return BorderStyle{ return BorderStyle{
shape: shape, shape: shape,
horizontal: '─', top: '─',
vertical: '', bottom: '',
left: '│',
right: '│',
topLeft: '┌', topLeft: '┌',
topRight: '┐', topRight: '┐',
bottomLeft: '└', bottomLeft: '└',
@@ -362,18 +384,37 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
case BorderBold: case BorderBold:
return BorderStyle{ return BorderStyle{
shape: shape, shape: shape,
horizontal: '━', top: '━',
vertical: '', bottom: '',
left: '┃',
right: '┃',
topLeft: '┏', topLeft: '┏',
topRight: '┓', topRight: '┓',
bottomLeft: '┗', bottomLeft: '┗',
bottomRight: '┛', bottomRight: '┛',
} }
case BorderBlock:
// ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜
// ▌ ▐
// ▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟
return BorderStyle{
shape: shape,
top: '▀',
bottom: '▄',
left: '▌',
right: '▐',
topLeft: '▛',
topRight: '▜',
bottomLeft: '▙',
bottomRight: '▟',
}
case BorderDouble: case BorderDouble:
return BorderStyle{ return BorderStyle{
shape: shape, shape: shape,
horizontal: '═', top: '═',
vertical: '', bottom: '',
left: '║',
right: '║',
topLeft: '╔', topLeft: '╔',
topRight: '╗', topRight: '╗',
bottomLeft: '╚', bottomLeft: '╚',
@@ -382,8 +423,10 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
} }
return BorderStyle{ return BorderStyle{
shape: shape, shape: shape,
horizontal: '─', top: '─',
vertical: '', bottom: '',
left: '│',
right: '│',
topLeft: '╭', topLeft: '╭',
topRight: '╮', topRight: '╮',
bottomLeft: '╰', bottomLeft: '╰',
@@ -394,8 +437,10 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
func MakeTransparentBorder() BorderStyle { func MakeTransparentBorder() BorderStyle {
return BorderStyle{ return BorderStyle{
shape: BorderRounded, shape: BorderRounded,
horizontal: ' ', top: ' ',
vertical: ' ', bottom: ' ',
left: ' ',
right: ' ',
topLeft: ' ', topLeft: ' ',
topRight: ' ', topRight: ' ',
bottomLeft: ' ', bottomLeft: ' ',
@@ -492,61 +537,66 @@ var (
ColPreviewBorder ColorPair ColPreviewBorder ColorPair
ColBorderLabel ColorPair ColBorderLabel ColorPair
ColPreviewLabel ColorPair ColPreviewLabel ColorPair
ColPreviewScrollbar ColorPair
) )
func EmptyTheme() *ColorTheme { func EmptyTheme() *ColorTheme {
return &ColorTheme{ return &ColorTheme{
Colored: true, Colored: true,
Input: ColorAttr{colUndefined, AttrUndefined}, Input: ColorAttr{colUndefined, AttrUndefined},
Fg: ColorAttr{colUndefined, AttrUndefined}, Fg: ColorAttr{colUndefined, AttrUndefined},
Bg: ColorAttr{colUndefined, AttrUndefined}, Bg: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{colUndefined, AttrUndefined}, DarkBg: ColorAttr{colUndefined, AttrUndefined},
Prompt: ColorAttr{colUndefined, AttrUndefined}, Prompt: ColorAttr{colUndefined, AttrUndefined},
Match: ColorAttr{colUndefined, AttrUndefined}, Match: ColorAttr{colUndefined, AttrUndefined},
Current: ColorAttr{colUndefined, AttrUndefined}, Current: ColorAttr{colUndefined, AttrUndefined},
CurrentMatch: ColorAttr{colUndefined, AttrUndefined}, CurrentMatch: ColorAttr{colUndefined, AttrUndefined},
Spinner: ColorAttr{colUndefined, AttrUndefined}, Spinner: ColorAttr{colUndefined, AttrUndefined},
Info: ColorAttr{colUndefined, AttrUndefined}, Info: ColorAttr{colUndefined, AttrUndefined},
Cursor: ColorAttr{colUndefined, AttrUndefined}, Cursor: ColorAttr{colUndefined, AttrUndefined},
Selected: ColorAttr{colUndefined, AttrUndefined}, Selected: ColorAttr{colUndefined, AttrUndefined},
Header: ColorAttr{colUndefined, AttrUndefined}, Header: ColorAttr{colUndefined, AttrUndefined},
Border: ColorAttr{colUndefined, AttrUndefined}, Border: ColorAttr{colUndefined, AttrUndefined},
BorderLabel: ColorAttr{colUndefined, AttrUndefined}, BorderLabel: ColorAttr{colUndefined, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined}, Disabled: ColorAttr{colUndefined, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined}, PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined}, PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined}, Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined}, PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined}, PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined}, PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
} }
} }
func NoColorTheme() *ColorTheme { func NoColorTheme() *ColorTheme {
return &ColorTheme{ return &ColorTheme{
Colored: false, Colored: false,
Input: ColorAttr{colDefault, AttrRegular}, Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrRegular}, Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrRegular}, Bg: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{colDefault, AttrRegular}, DarkBg: ColorAttr{colDefault, AttrUndefined},
Prompt: ColorAttr{colDefault, AttrRegular}, Prompt: ColorAttr{colDefault, AttrUndefined},
Match: ColorAttr{colDefault, Underline}, Match: ColorAttr{colDefault, Underline},
Current: ColorAttr{colDefault, Reverse}, Current: ColorAttr{colDefault, Reverse},
CurrentMatch: ColorAttr{colDefault, Reverse | Underline}, CurrentMatch: ColorAttr{colDefault, Reverse | Underline},
Spinner: ColorAttr{colDefault, AttrRegular}, Spinner: ColorAttr{colDefault, AttrUndefined},
Info: ColorAttr{colDefault, AttrRegular}, Info: ColorAttr{colDefault, AttrUndefined},
Cursor: ColorAttr{colDefault, AttrRegular}, Cursor: ColorAttr{colDefault, AttrUndefined},
Selected: ColorAttr{colDefault, AttrRegular}, Selected: ColorAttr{colDefault, AttrUndefined},
Header: ColorAttr{colDefault, AttrRegular}, Header: ColorAttr{colDefault, AttrUndefined},
Border: ColorAttr{colDefault, AttrRegular}, Border: ColorAttr{colDefault, AttrUndefined},
BorderLabel: ColorAttr{colDefault, AttrRegular}, BorderLabel: ColorAttr{colDefault, AttrUndefined},
Disabled: ColorAttr{colDefault, AttrRegular}, Disabled: ColorAttr{colDefault, AttrUndefined},
PreviewFg: ColorAttr{colDefault, AttrRegular}, PreviewFg: ColorAttr{colDefault, AttrUndefined},
PreviewBg: ColorAttr{colDefault, AttrRegular}, PreviewBg: ColorAttr{colDefault, AttrUndefined},
Gutter: ColorAttr{colDefault, AttrRegular}, Gutter: ColorAttr{colDefault, AttrUndefined},
PreviewLabel: ColorAttr{colDefault, AttrRegular}, PreviewBorder: ColorAttr{colDefault, AttrUndefined},
Separator: ColorAttr{colDefault, AttrRegular}, PreviewScrollbar: ColorAttr{colDefault, AttrUndefined},
Scrollbar: ColorAttr{colDefault, AttrRegular}, PreviewLabel: ColorAttr{colDefault, AttrUndefined},
Separator: ColorAttr{colDefault, AttrUndefined},
Scrollbar: ColorAttr{colDefault, AttrUndefined},
} }
} }
@@ -557,79 +607,85 @@ func errorExit(message string) {
func init() { func init() {
Default16 = &ColorTheme{ Default16 = &ColorTheme{
Colored: true, Colored: true,
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{colBlack, AttrUndefined}, DarkBg: ColorAttr{colBlack, AttrUndefined},
Prompt: ColorAttr{colBlue, AttrUndefined}, Prompt: ColorAttr{colBlue, AttrUndefined},
Match: ColorAttr{colGreen, AttrUndefined}, Match: ColorAttr{colGreen, AttrUndefined},
Current: ColorAttr{colYellow, AttrUndefined}, Current: ColorAttr{colYellow, AttrUndefined},
CurrentMatch: ColorAttr{colGreen, AttrUndefined}, CurrentMatch: ColorAttr{colGreen, AttrUndefined},
Spinner: ColorAttr{colGreen, AttrUndefined}, Spinner: ColorAttr{colGreen, AttrUndefined},
Info: ColorAttr{colWhite, AttrUndefined}, Info: ColorAttr{colWhite, AttrUndefined},
Cursor: ColorAttr{colRed, AttrUndefined}, Cursor: ColorAttr{colRed, AttrUndefined},
Selected: ColorAttr{colMagenta, AttrUndefined}, Selected: ColorAttr{colMagenta, AttrUndefined},
Header: ColorAttr{colCyan, AttrUndefined}, Header: ColorAttr{colCyan, AttrUndefined},
Border: ColorAttr{colBlack, AttrUndefined}, Border: ColorAttr{colBlack, AttrUndefined},
BorderLabel: ColorAttr{colWhite, AttrUndefined}, BorderLabel: ColorAttr{colWhite, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined}, Disabled: ColorAttr{colUndefined, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined}, PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined}, PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined}, Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined}, PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined}, PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined}, PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
} }
Dark256 = &ColorTheme{ Dark256 = &ColorTheme{
Colored: true, Colored: true,
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{236, AttrUndefined}, DarkBg: ColorAttr{236, AttrUndefined},
Prompt: ColorAttr{110, AttrUndefined}, Prompt: ColorAttr{110, AttrUndefined},
Match: ColorAttr{108, AttrUndefined}, Match: ColorAttr{108, AttrUndefined},
Current: ColorAttr{254, AttrUndefined}, Current: ColorAttr{254, AttrUndefined},
CurrentMatch: ColorAttr{151, AttrUndefined}, CurrentMatch: ColorAttr{151, AttrUndefined},
Spinner: ColorAttr{148, AttrUndefined}, Spinner: ColorAttr{148, AttrUndefined},
Info: ColorAttr{144, AttrUndefined}, Info: ColorAttr{144, AttrUndefined},
Cursor: ColorAttr{161, AttrUndefined}, Cursor: ColorAttr{161, AttrUndefined},
Selected: ColorAttr{168, AttrUndefined}, Selected: ColorAttr{168, AttrUndefined},
Header: ColorAttr{109, AttrUndefined}, Header: ColorAttr{109, AttrUndefined},
Border: ColorAttr{59, AttrUndefined}, Border: ColorAttr{59, AttrUndefined},
BorderLabel: ColorAttr{145, AttrUndefined}, BorderLabel: ColorAttr{145, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined}, Disabled: ColorAttr{colUndefined, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined}, PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined}, PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined}, Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined}, PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined}, PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined}, PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
} }
Light256 = &ColorTheme{ Light256 = &ColorTheme{
Colored: true, Colored: true,
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{251, AttrUndefined}, DarkBg: ColorAttr{251, AttrUndefined},
Prompt: ColorAttr{25, AttrUndefined}, Prompt: ColorAttr{25, AttrUndefined},
Match: ColorAttr{66, AttrUndefined}, Match: ColorAttr{66, AttrUndefined},
Current: ColorAttr{237, AttrUndefined}, Current: ColorAttr{237, AttrUndefined},
CurrentMatch: ColorAttr{23, AttrUndefined}, CurrentMatch: ColorAttr{23, AttrUndefined},
Spinner: ColorAttr{65, AttrUndefined}, Spinner: ColorAttr{65, AttrUndefined},
Info: ColorAttr{101, AttrUndefined}, Info: ColorAttr{101, AttrUndefined},
Cursor: ColorAttr{161, AttrUndefined}, Cursor: ColorAttr{161, AttrUndefined},
Selected: ColorAttr{168, AttrUndefined}, Selected: ColorAttr{168, AttrUndefined},
Header: ColorAttr{31, AttrUndefined}, Header: ColorAttr{31, AttrUndefined},
Border: ColorAttr{145, AttrUndefined}, Border: ColorAttr{145, AttrUndefined},
BorderLabel: ColorAttr{59, AttrUndefined}, BorderLabel: ColorAttr{59, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined}, Disabled: ColorAttr{colUndefined, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined}, PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined}, PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined}, Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined}, PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined}, PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined}, PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
} }
} }
@@ -670,8 +726,10 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
theme.PreviewFg = o(theme.Fg, theme.PreviewFg) theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
theme.PreviewBg = o(theme.Bg, theme.PreviewBg) theme.PreviewBg = o(theme.Bg, theme.PreviewBg)
theme.PreviewLabel = o(theme.BorderLabel, theme.PreviewLabel) theme.PreviewLabel = o(theme.BorderLabel, theme.PreviewLabel)
theme.PreviewBorder = o(theme.Border, theme.PreviewBorder)
theme.Separator = o(theme.Border, theme.Separator) theme.Separator = o(theme.Border, theme.Separator)
theme.Scrollbar = o(theme.Border, theme.Scrollbar) theme.Scrollbar = o(theme.Border, theme.Scrollbar)
theme.PreviewScrollbar = o(theme.PreviewBorder, theme.PreviewScrollbar)
initPalette(theme) initPalette(theme)
} }
@@ -709,5 +767,6 @@ func initPalette(theme *ColorTheme) {
ColBorderLabel = pair(theme.BorderLabel, theme.Bg) ColBorderLabel = pair(theme.BorderLabel, theme.Bg)
ColPreviewLabel = pair(theme.PreviewLabel, theme.PreviewBg) ColPreviewLabel = pair(theme.PreviewLabel, theme.PreviewBg)
ColPreview = pair(theme.PreviewFg, theme.PreviewBg) ColPreview = pair(theme.PreviewFg, theme.PreviewBg)
ColPreviewBorder = pair(theme.Border, theme.PreviewBg) ColPreviewBorder = pair(theme.PreviewBorder, theme.PreviewBg)
ColPreviewScrollbar = pair(theme.PreviewScrollbar, theme.PreviewBg)
} }

View File

@@ -11,6 +11,11 @@ import (
"github.com/rivo/uniseg" "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 // RunesWidth returns runes width
func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) { func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) {
width := 0 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' { if len(rs) == 1 && rs[0] == '\t' {
w = tabstop - (prefixWidth+width)%tabstop w = tabstop - (prefixWidth+width)%tabstop
} else { } else {
s := string(rs) w = StringWidth(string(rs))
w = runewidth.StringWidth(s) + strings.Count(s, "\n")
} }
width += w width += w
if width > limit { if width > limit {
@@ -41,7 +45,7 @@ func Truncate(input string, limit int) ([]rune, int) {
gr := uniseg.NewGraphemes(input) gr := uniseg.NewGraphemes(input)
for gr.Next() { for gr.Next() {
rs := gr.Runes() rs := gr.Runes()
w := runewidth.StringWidth(string(rs)) w := StringWidth(string(rs))
if width+w > limit { if width+w > limit {
return runes, width 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 { if Constrain(-3, -1, 3) != -1 {
t.Error("Expected", -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 { if Constrain32(-3, -1, 3) != -1 {
t.Error("Expected", -1) t.Error("Expected", -1)
} }

View File

@@ -1865,6 +1865,67 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal '>', lines.last } tmux.until { |lines| assert_equal '>', lines.last }
end end
def test_change_and_transform_header
[
'space:change-header:$(seq 4)',
'space:transform-header:seq 4'
].each_with_index do |binding, i|
tmux.send_keys %(seq 3 | #{FZF} --header-lines 2 --header bar --bind "#{binding}"), :Enter
expected = <<~OUTPUT
> 3
2
1
bar
1/1
>
OUTPUT
tmux.until { assert_block(expected, _1) }
tmux.send_keys :Space
expected = <<~OUTPUT
> 3
2
1
1
2
3
4
1/1
>
OUTPUT
tmux.until { assert_block(expected, _1) }
next unless i.zero?
teardown
setup
end
end
def test_change_header
tmux.send_keys %(seq 3 | #{FZF} --header-lines 2 --header bar --bind "space:change-header:$(seq 4)"), :Enter
expected = <<~OUTPUT
> 3
2
1
bar
1/1
>
OUTPUT
tmux.until { assert_block(expected, _1) }
tmux.send_keys :Space
expected = <<~OUTPUT
> 3
2
1
1
2
3
4
1/1
>
OUTPUT
tmux.until { assert_block(expected, _1) }
end
def test_change_query def test_change_query
tmux.send_keys %(: | #{FZF} --query foo --bind space:change-query:foobar), :Enter tmux.send_keys %(: | #{FZF} --query foo --bind space:change-query:foobar), :Enter
tmux.until { |lines| assert_equal 0, lines.item_count } tmux.until { |lines| assert_equal 0, lines.item_count }
@@ -1930,7 +1991,7 @@ class TestGoFZF < TestBase
def test_keep_right def test_keep_right
tmux.send_keys "seq 10000 | #{FZF} --read0 --keep-right", :Enter 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 end
def test_backward_eof def test_backward_eof
@@ -2432,6 +2493,39 @@ class TestGoFZF < TestBase
end end
end end
def test_change_preview_window_rotate_hidden
tmux.send_keys "seq 100 | #{FZF} --preview-window hidden --preview 'echo =={}==' --bind '" \
"a:change-preview-window(nohidden||down,1|)'", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.until { |lines| refute_includes lines[1], '==1==' }
tmux.send_keys 'a'
tmux.until { |lines| assert_includes lines[1], '==1==' }
tmux.send_keys 'a'
tmux.until { |lines| refute_includes lines[1], '==1==' }
tmux.send_keys 'a'
tmux.until { |lines| assert_includes lines[-2], '==1==' }
tmux.send_keys 'a'
tmux.until { |lines| refute_includes lines[-2], '==1==' }
tmux.send_keys 'a'
tmux.until { |lines| assert_includes lines[1], '==1==' }
end
def test_change_preview_window_rotate_hidden_down
tmux.send_keys "seq 100 | #{FZF} --bind '?:change-preview-window:up||down|' --preview 'echo =={}==' --preview-window hidden,down,1", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.until { |lines| refute_includes lines[1], '==1==' }
tmux.send_keys '?'
tmux.until { |lines| assert_includes lines[1], '==1==' }
tmux.send_keys '?'
tmux.until { |lines| refute_includes lines[1], '==1==' }
tmux.send_keys '?'
tmux.until { |lines| assert_includes lines[-2], '==1==' }
tmux.send_keys '?'
tmux.until { |lines| refute_includes lines[-2], '==1==' }
tmux.send_keys '?'
tmux.until { |lines| assert_includes lines[1], '==1==' }
end
def test_ellipsis def test_ellipsis
tmux.send_keys 'seq 1000 | tr "\n" , | fzf --ellipsis=SNIPSNIP -e -q500', :Enter tmux.send_keys 'seq 1000 | tr "\n" , | fzf --ellipsis=SNIPSNIP -e -q500', :Enter
tmux.until { |lines| assert_equal 1, lines.match_count } tmux.until { |lines| assert_equal 1, lines.match_count }
@@ -2534,12 +2628,16 @@ class TestGoFZF < TestBase
end end
def test_focus_event def test_focus_event
tmux.send_keys 'seq 100 | fzf --bind "focus:transform-prompt(echo [[{}]])"', :Enter tmux.send_keys 'seq 100 | fzf --bind "focus:transform-prompt(echo [[{}]]),?:unbind(focus)"', :Enter
tmux.until { |lines| assert_includes(lines[-1], '[[1]]') } tmux.until { |lines| assert_includes(lines[-1], '[[1]]') }
tmux.send_keys :Up tmux.send_keys :Up
tmux.until { |lines| assert_includes(lines[-1], '[[2]]') } tmux.until { |lines| assert_includes(lines[-1], '[[2]]') }
tmux.send_keys :X tmux.send_keys :X
tmux.until { |lines| assert_includes(lines[-1], '[[]]') } tmux.until { |lines| assert_includes(lines[-1], '[[]]') }
tmux.send_keys '?'
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.until { |lines| refute_includes(lines[-1], '[[1]]') }
end end
def test_labels_center def test_labels_center
@@ -2629,11 +2727,17 @@ class TestGoFZF < TestBase
end end
def test_listen def test_listen
tmux.send_keys 'seq 10 | fzf --listen 6266', :Enter { '--listen 6266' => -> { URI('http://localhost:6266') },
tmux.until { |lines| assert_equal 10, lines.item_count } "--listen --sync --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'" =>
Net::HTTP.post(URI('http://localhost:6266'), 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ') -> { URI("http://localhost:#{File.read('/tmp/fzf-port').chomp}") } }.each do |opts, fn|
tmux.until { |lines| assert_equal 100, lines.item_count } tmux.send_keys "seq 10 | fzf #{opts}", :Enter
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] } 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 end
def test_toggle_alternative_preview_window def test_toggle_alternative_preview_window
@@ -2656,6 +2760,181 @@ class TestGoFZF < TestBase
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| assert_equal 99, lines.item_count } tmux.until { |lines| assert_equal 99, lines.item_count }
end end
def test_no_extra_newline_issue_3209
tmux.send_keys(%(seq 100 | #{FZF} --height 10 --preview-window up,wrap --preview 'printf "─%.0s" $(seq 1 "$((FZF_PREVIEW_COLUMNS - 5))"); printf $"\\e[7m%s\\e[0m" title; echo; echo something'), :Enter)
expected = <<~OUTPUT
something
3
2
> 1
100/100
>
OUTPUT
tmux.until { assert_block(expected, _1) }
end
def test_track
tmux.send_keys "seq 1000 | #{FZF} --query 555 --track --bind t:toggle-track", :Enter
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> 555'
end
tmux.send_keys :BSpace
index = tmux.until do |lines|
assert_equal 28, lines.match_count
assert_includes lines, '> 555'
end.index('> 555')
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 271, lines.match_count
assert_equal '> 555', lines[index]
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 1000, lines.match_count
assert_equal '> 555', lines[index]
end
tmux.send_keys '555'
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> 555'
assert_includes lines[-2], '+T'
end
tmux.send_keys 't'
tmux.until do |lines|
refute_includes lines[-2], '+T'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 28, lines.match_count
assert_includes lines, '> 55'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 271, lines.match_count
assert_includes lines, '> 5'
end
tmux.send_keys 't'
tmux.until do |lines|
assert_includes lines[-2], '+T'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 1000, lines.match_count
assert_includes lines, '> 5'
end
end
def test_track_action
tmux.send_keys "seq 1000 | #{FZF} --query 555 --bind t:track", :Enter
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> 555'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 28, lines.match_count
assert_includes lines, '> 55'
end
tmux.send_keys :t
tmux.until do |lines|
assert_includes lines[-2], '+T'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 271, lines.match_count
assert_includes lines, '> 55'
end
# Automatically disabled when the tracking item is no longer visible
tmux.send_keys '4'
tmux.until do |lines|
assert_equal 28, lines.match_count
refute_includes lines[-2], '+T'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 271, lines.match_count
assert_includes lines, '> 5'
end
tmux.send_keys :t
tmux.until do |lines|
assert_includes lines[-2], '+T'
end
tmux.send_keys :Up
tmux.until do |lines|
refute_includes lines[-2], '+T'
end
end
def test_one_and_zero
tmux.send_keys "seq 10 | #{FZF} --bind 'zero:preview(echo no match),one:preview(echo {} is the only match)'", :Enter
tmux.send_keys '1'
tmux.until do |lines|
assert_equal 2, lines.match_count
refute(lines.any? { _1.include?('only match') })
refute(lines.any? { _1.include?('no match') })
end
tmux.send_keys '0'
tmux.until do |lines|
assert_equal 1, lines.match_count
assert(lines.any? { _1.include?('only match') })
end
tmux.send_keys '0'
tmux.until do |lines|
assert_equal 0, lines.match_count
assert(lines.any? { _1.include?('no match') })
end
end
def test_height_range_with_exit_0
tmux.send_keys "seq 10 | #{FZF} --height ~10% --exit-0", :Enter
tmux.until { |lines| assert_equal 10, lines.item_count }
tmux.send_keys :c
tmux.until { |lines| assert_equal 0, lines.match_count }
end
def test_reload_and_change
tmux.send_keys "(echo foo; echo bar) | #{FZF} --bind 'load:reload-sync(sleep 60)+change-query(bar)'", :Enter
tmux.until { |lines| assert_equal 1, lines.match_count }
end
def test_reload_and_change_cache
tmux.send_keys "echo bar | #{FZF} --bind 'zero:change-header(foo)+reload(echo foo)+clear-query'", :Enter
expected = <<~OUTPUT
> bar
1/1
>
OUTPUT
tmux.until { assert_block(expected, _1) }
tmux.send_keys :z
expected = <<~OUTPUT
> foo
foo
1/1
>
OUTPUT
tmux.until { assert_block(expected, _1) }
end
def test_delete_with_modifiers
tmux.send_keys "seq 100 | #{FZF} --bind 'ctrl-delete:up+up,shift-delete:down,focus:transform-prompt:echo [{}]'", :Enter
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.send_keys 'C-Delete'
tmux.until { |lines| assert_equal '[3]', lines[-1] }
tmux.send_keys 'S-Delete'
tmux.until { |lines| assert_equal '[2]', lines[-1] }
end
def test_become_tty
tmux.send_keys "sleep 0.5 | #{FZF} --bind 'start:reload:ls' --bind 'load:become:tty'", :Enter
tmux.until { |lines| assert_includes lines, '/dev/tty' }
end
end end
module TestShell module TestShell
@@ -2784,9 +3063,9 @@ module TestShell
tmux.send_keys 'C-r' tmux.send_keys 'C-r'
tmux.until { |lines| assert_equal '>', lines[-1] } tmux.until { |lines| assert_equal '>', lines[-1] }
tmux.send_keys 'foo bar' 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.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.send_keys :Enter
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] } tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
end 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"