Compare commits

...

182 Commits

Author SHA1 Message Date
Junegunn Choi
d471067e3f 0.42.0 2023-06-15 00:37:41 +09:00
Junegunn Choi
d0b7780239 Add --info=right
Related: #3322
2023-06-11 16:09:15 +09:00
Junegunn Choi
e627ca6bd7 Add --info=inline-right
Close #3322
2023-06-10 23:11:05 +09:00
Junegunn Choi
c97172bdd4 Fix background color of spinner on the preview window 2023-06-10 14:48:47 +09:00
Mike
ce8a745fb4 Add new border style: 'thinblock' (#3327)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2023-06-10 14:48:29 +09:00
Junegunn Choi
3e9efd1401 [vim] Only prepend --border option in $FZF_DEFAULT_OPTS
Fix #3318
2023-06-03 09:03:04 +09:00
Junegunn Choi
20340190b5 [fzf-tmux] Pass $BAT_THEME
This may anger some purists, but bat is widely used as the previewer so
I think it's worth it.
2023-05-31 20:16:30 +09:00
Junegunn Choi
265040a78c [vim] Respect --border optin in $FZF_DEFAULT_OPTS 2023-05-31 20:09:14 +09:00
Junegunn Choi
448d7e0c5a Update test case 2023-05-27 16:01:30 +09:00
Junegunn Choi
6eb1874c5a 0.41.1 2023-05-27 15:54:22 +09:00
Junegunn Choi
4c70745cc1 Fix bug where preview is not updated after reload when --disabled is set
Fix #3311
2023-05-27 15:51:04 +09:00
Junegunn Choi
7795748a3f Remove dead code 2023-05-27 15:10:47 +09:00
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
Junegunn Choi
352ea07226 0.38.0 2023-02-15 23:24:42 +09:00
Junegunn Choi
27018787af Describe become(...) action and use it to simplify examples 2023-02-15 23:24:42 +09:00
Junegunn Choi
4e305eca26 become: Set stdin to /dev/tty 2023-02-15 23:24:42 +09:00
sitiom
9e9c0ceaf4 Add Winget Releaser workflow (#3164) 2023-02-15 16:47:12 +09:00
Junegunn Choi
b3bf18b1c0 [fzf-tmux] Fix version check
The output of `tmux -V` starts with "tmux ".
2023-02-13 15:25:39 +09:00
Junegunn Choi
b1619f675f [fzf-tmux] Do not set --margin 0,1 on tmux 3.3 or above
Close #3162
2023-02-13 14:49:02 +09:00
Junegunn Choi
96c3de12eb Run 'become' only when the command template is properly evaluated 2023-02-12 22:06:21 +09:00
Junegunn Choi
719dbb8bae Update ADVANCED.md: transform-query to restore the query string
Close #2961
2023-02-12 17:23:17 +09:00
Junegunn Choi
f38a7f7f8f [bash] Enable environment variable completion for printenv
Close #3145
2023-02-12 16:58:36 +09:00
Junegunn Choi
6ea38b4438 Add become(...) action that replaces current fzf process
Close #3159
2023-02-11 20:26:31 +09:00
Junegunn Choi
f7447aece1 Code cleanup 2023-02-01 18:16:58 +09:00
Junegunn Choi
aa2b9ec476 Add 'show-preview' and 'hide-preview'
For cases where 'toggle-preview' is not enough
2023-01-31 17:34:11 +09:00
Junegunn Choi
3ee00f8bc2 toggle-preview should not show empty preview window 2023-01-30 22:13:29 +09:00
Junegunn Choi
fccab60a5c --preview-window 0,hidden should not execute the preview command
Until `toggle-preview` action is triggered

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

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

Close #3053
2023-01-23 16:38:24 +09:00
Junegunn Choi
826178f1e2 Do not restore terminal state while running an external command 2023-01-23 02:24:13 +09:00
Junegunn Choi
acccf8a9b8 Fix TOC 2023-01-23 02:24:13 +09:00
Francesco Bigagnoli
57c066f0be Fix bat url in README (#3129) 2023-01-23 02:21:16 +09:00
Nachum Barcohen
e44f64ae92 Add Helix editor to bash autocompletion (#3137) 2023-01-23 02:21:04 +09:00
Junegunn Choi
d51980a3f5 Add 'transform-border-label' and 'transform-preview-label' 2023-01-22 02:18:19 +09:00
jpcrs
c3d73e7ecb Add change-border-label and change-preview-label actions, update man 2023-01-22 02:18:19 +09:00
Junegunn Choi
b077f6821d Action argument in enclosed form should allow new lines
Close #3138
2023-01-21 22:20:26 +09:00
Junegunn Choi
a79de11af7 README: Add FZF_TMUX_OPTS example for tmux popup 2023-01-19 13:25:08 +09:00
Junegunn Choi
2023011763 0.36.0 2023-01-17 01:33:05 +09:00
Junegunn Choi
b46e40e86b [vim] Automatically set RUNEWIDTH_EASTASIAN=1 when &ambiwidth == double 2023-01-17 01:03:16 +09:00
Junegunn Choi
a6d6cdd165 [vim] Use system-default border style
* 'rounded' on non-Windows platforms
* 'sharp' on Windows
2023-01-16 22:44:22 +09:00
Junegunn Choi
dc8da605f9 Fix rendering of double-column borders on light renderer 2023-01-16 19:55:44 +09:00
Junegunn Choi
8b299a29c7 Fix rendering of double-column borders 2023-01-16 19:34:28 +09:00
Junegunn Choi
3109b865d2 Fix typo on man page 2023-01-16 01:32:48 +09:00
Junegunn Choi
0c5956c43c Better support for Windows terminals
* Default border style on Windows is changed to `sharp` because some
  Windows terminals are not capable of displaying `rounded` border
  characters correctly.
* If your terminal emulator renders each box-drawing character with
  2 columns, set `RUNEWIDTH_EASTASIAN` environment variable to `1`.
2023-01-16 01:26:39 +09:00
Junegunn Choi
1c83b39691 Update README examples 2023-01-13 21:15:03 +09:00
Junegunn Choi
77874b473c Update Rubocop dependencies 2023-01-12 23:37:23 +09:00
Junegunn Choi
b7cce7be15 Remove unused block argument 2023-01-12 23:24:39 +09:00
Junegunn Choi
3cd3362417 Fix test failure 2023-01-12 23:23:29 +09:00
Junegunn Choi
e97e925efb Resume preview following if the user scrolls the window to the bottom 2023-01-12 23:18:41 +09:00
Farooq Karimi Zadeh
0f032235cf Correct package manager commands for apt (#3117) 2023-01-10 10:46:13 +09:00
Junegunn Choi
e0f0984da7 Allow re-enabling preview follow on change-preview-window 2023-01-09 11:08:06 +09:00
Junegunn Choi
4d22b5aaef Disable preview follow after dragging the scrollbar
TBD: Should we re-enable follow once the offset reaches the bottom?
2023-01-09 11:07:31 +09:00
Junegunn Choi
80b8846318 Run preview command when preview window appears after resize (#3113)
# Start fzf in a small screen so that the preview window is hidden
  fzf --bind 'ctrl-p:toggle-preview' --preview 'stat {}' --preview-window='right,50%,<100(down,50%,hidden)'

  # Enlarge the screen until the preview window appears. It should not be empty.
2023-01-07 16:11:35 +09:00
Junegunn Choi
bf641faafa Prevent fzf crashing on malformed remote action 2023-01-07 15:38:02 +09:00
Junegunn Choi
23d8b78ce1 Allow toggling of alternative preview window layout that is hidden
Fix #3113
2023-01-07 15:12:31 +09:00
Junegunn Choi
3b2244077d Add scrollbar to the preview window 2023-01-06 15:36:12 +09:00
Junegunn Choi
ee5cdb9713 Reduce flickering of the scroll info panel on the preview window 2023-01-05 01:59:22 +09:00
Junegunn Choi
03d02d67f7 Fix cyclic scrolling with non-zero preview header lines
e.g. fzf --preview-window 'cycle,~2' --preview 'echo foo; echo bar; seq 100'
2023-01-04 22:00:00 +09:00
Junegunn Choi
5798145581 Fix preview border on tcell renderer 2023-01-04 10:09:39 +09:00
dependabot[bot]
51ef0b7f66 Bump github.com/gdamore/tcell/v2 from 2.5.3 to 2.5.4
Bumps [github.com/gdamore/tcell/v2](https://github.com/gdamore/tcell) from 2.5.3 to 2.5.4.
- [Release notes](https://github.com/gdamore/tcell/releases)
- [Changelog](https://github.com/gdamore/tcell/blob/main/CHANGESv2.md)
- [Commits](https://github.com/gdamore/tcell/compare/v2.5.3...v2.5.4)

---
updated-dependencies:
- dependency-name: github.com/gdamore/tcell/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-03 02:58:09 +09:00
dependabot[bot]
97b4542c73 Bump github.com/mattn/go-isatty from 0.0.16 to 0.0.17
Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.16 to 0.0.17.
- [Release notes](https://github.com/mattn/go-isatty/releases)
- [Commits](https://github.com/mattn/go-isatty/compare/v0.0.16...v0.0.17)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-isatty
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-03 02:38:57 +09:00
Junegunn Choi
c1cd0c09a2 Allow dragging of the preview window 2023-01-03 01:46:37 +09:00
Junegunn Choi
1fc1f47d80 Fix double-click on light renderer 2023-01-03 01:39:16 +09:00
Junegunn Choi
ec471a5bc2 Make sure two consecutive double clicks require four clicks 2023-01-03 01:36:51 +09:00
Junegunn Choi
a893fc0ca2 Clicks with different x coordinates shouldn't be seen as a double-click 2023-01-03 01:21:40 +09:00
Junegunn Choi
3761dc0433 Avoid superfluous update of scrollbar 2023-01-02 01:10:01 +09:00
Junegunn Choi
aa71a07fbe Fix scrollbar rendering (#3096) 2023-01-01 21:18:20 +09:00
Junegunn Choi
088293f5e7 Restore mouse drag mode (#3096) 2023-01-01 21:04:40 +09:00
Junegunn Choi
7c660aa86e Allow dragging of scrollbar 2023-01-01 19:01:56 +09:00
Junegunn Choi
435d8fa0a2 Colors for 'separator' and 'scrollbar' will default to that for 'border' 2023-01-01 19:01:56 +09:00
Junegunn Choi
5cd6f1d064 Add scrollbar
Close #3096
2023-01-01 14:48:14 +09:00
Junegunn Choi
ec20dfe312 Only allow local requests 2022-12-31 23:13:14 +09:00
Junegunn Choi
924ffb5a35 Fix cache being immediately cleared on reload-sync 2022-12-31 09:33:23 +09:00
Junegunn Choi
62c7f59b94 Add transform-prompt(...) action 2022-12-31 09:27:11 +09:00
Junegunn Choi
e97176b1d7 Update transform-query examples for zsh
Close #3107
2022-12-30 17:12:55 +09:00
Junegunn Choi
d649f5d826 Always execute preview command if {q} is in the template
Even when {q} is empty. Because, why not?

While this can be seen as a breaking change, there is an easy workaround
to keep the old behavior.

    # This will show // even when the query is empty
    : | fzf --preview 'echo /{q}/'

    # But if you don't want it,
    : | fzf --preview '[ -n {q} ] || exit; echo /{q}/'

Close #2759
2022-12-30 14:29:17 +09:00
Junegunn Choi
6c37177cf5 Add reload-sync action
Close #2816
2022-12-29 20:04:33 +09:00
Junegunn Choi
14775aa975 Add 'load' event that is triggered when the input stream is complete
and the first search (with or without query) is complete
2022-12-29 20:01:50 +09:00
Junegunn Choi
44b6336372 Make server channel buffered
Not to block an action that calls the API

  fzf --listen 6266 --bind 'space:execute-silent:curl localhost:6266 -d up'
2022-12-28 12:52:19 +09:00
Junegunn Choi
36d2bb332b Add transform-query(...) action
Test case authored by @SpicyLemon

Close #1930
Close #2465
Close #2559
Close #2509 (e.g. fzf --bind 'space:transform-query:printf %s%s {q} {}')
2022-12-28 00:05:31 +09:00
Junegunn Choi
4dbe45640a Remove $FZF_LISTEN_PORT
It is not worth the added complexity.
2022-12-27 22:12:06 +09:00
Junegunn Choi
4b3f0b9f08 Allow put action with an argument i.e. put(...) 2022-12-27 19:54:46 +09:00
Junegunn Choi
12af069dca Add pos(...) action to move the cursor to the numeric position
# Put the cursor on the 10th item
  seq 100 | fzf --sync --bind 'start:pos(10)'

  # Put the cursor on the 10th to last item
  seq 100 | fzf --sync --bind 'start:pos(-10)'

Close #3069
Close #395
2022-12-27 01:08:42 +09:00
Junegunn Choi
d42e708d31 Update README-VIM: Different homebrew prefix on Apple Silicon
Close #3095
2022-12-26 16:25:36 +09:00
Junegunn Choi
b7bb973118 Revert "Add GET endpoints for getting the state of the finder"
This reverts commit 750b2a6313.

This can cause a deadlock if the endpoints are accessed in the core event
loop via execute action.

  fzf --listen 6266 --bind 'space:execute:curl localhost:6266'

Technically, there's no reason to use the API because the information is
already available via `{}` and `{q}`, but I'd like to completely remove
the risk of misuse.
2022-12-25 20:00:00 +09:00
Junegunn Choi
750b2a6313 Add GET endpoints for getting the state of the finder
* GET / (or GET /current)
* GET /query
2022-12-25 16:27:02 +09:00
Philipp Wagner
de0da86bd7 Add ppc64le binaries (#3067)
Little-endian 64 bit PowerPC (ppc64le) is the "normal" PowerPC
architecture supported by standard Linux distributions (RedHat, SUSE,
Ubuntu, etc.).

Add support for this architecture in the install script, and add binary
builds for it as well.
2022-12-23 15:41:13 +09:00
Junegunn Choi
8e283f512a Fix bind spec parser 2022-12-23 15:37:39 +09:00
Junegunn Choi
73162a4bc3 Rewrite bind spec parser 2022-12-23 03:28:16 +09:00
Junegunn Choi
1a9761736e Add time and size limit to remote requests 2022-12-22 20:44:49 +09:00
Junegunn Choi
fd1f7665a7 Abort fzf if --listen port is unavailable 2022-12-21 13:02:25 +09:00
Junegunn Choi
6d14573fd0 Add test case for --listen 2022-12-21 01:35:08 +09:00
Junegunn Choi
cf69b836ac Only trim CR and NF from the submitted expression
So the trailing space in the following case is respected.

  curl -XPOST localhost:6266 -d "change-prompt:$(date)> "
2022-12-21 01:35:08 +09:00
Junegunn Choi
a7a771b92b Break out of jump mode when any action is submitted to the server 2022-12-21 01:35:08 +09:00
Junegunn Choi
def011c029 Fix parse error of actions with arguments 2022-12-21 01:35:08 +09:00
Junegunn Choi
4b055bf260 Rewrite HTTP server without net/http
This cuts down the binary size from 5.7MB to 3.3MB.
2022-12-21 01:35:08 +09:00
Junegunn Choi
1ba7484d60 Add --listen=HTTP_PORT option to receive actions
Supersedes #2019

See also:
* #1728
* https://github.com/junegunn/fzf.vim/pull/1044
2022-12-21 01:35:08 +09:00
Junegunn Choi
51c518da1e Add change-query(...) action 2022-12-18 00:26:31 +09:00
polluks2
a3b6b03dfb Fix typo (#3093)
Co-authored-by: polluks <polluks@sdf.lonestar.org>
2022-12-17 23:58:43 +09:00
Junegunn Choi
18e3b38c69 Add 'next-selected' and 'prev-selected' actions
Close #2749
2022-12-11 00:59:34 +09:00
Junegunn Choi
0ad30063ff Rename previous-history to prev-history
previous-history is still supported for backward compatibility
2022-12-10 22:42:56 +09:00
Junegunn Choi
7812c64a31 Fix uninitialized colors in base themes
Fix #3079
2022-12-10 16:55:19 +09:00
Junegunn Choi
3d2376ab52 Add color name 'preview-label' (#3053) 2022-12-09 12:05:27 +09:00
Junegunn Choi
6b207bbf2b Fix inconsistent bonus points in exact match
Exact match would assign a different bonus point to the first character
when non-default --scheme was used.

Fix #3073
2022-12-04 22:17:39 +09:00
Bjørn Forsman
3f079ba7c6 README.md: Clarify on FZF_*_OPTS (#3064)
At first I thought they were appended to FZF_*_COMMAND. Let's make it
clear that these are passed to `fzf` itself.
2022-12-02 14:57:32 +09:00
Junegunn Choi
8f4c89f50e Make 'double-click' behave the same as 'enter' by default
Close #3061
2022-11-29 20:27:29 +09:00
OHZEKI Naoki
6b7a543c82 Add more util tests (#3062)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2022-11-27 16:30:59 +09:00
Junegunn Choi
2ba68d24f2 Do not erase info separator before redrawing it 2022-11-25 23:51:49 +09:00
Bruno Heridet
46877e0a92 test(eventbox): remove obsolete EvtClose const (#3059) 2022-11-23 19:38:14 +09:00
Junegunn Choi
b55f555487 0.35.1 2022-11-18 20:42:56 +09:00
Junegunn Choi
a38b63be18 Fix mouse event above fzf finder
Fix #2949
2022-11-18 20:32:59 +09:00
Junegunn Choi
1bebd6f4f5 Fix panic on inverse match query with --tiebreak=chunk
Fix #3055
2022-11-18 20:16:43 +09:00
Bruno Heridet
3da63f394d doc(man): complete the definition of what --no-unicode impacts (#3054) 2022-11-18 10:23:21 +09:00
Zhizhen He
2a54e3d770 Fix typos in the source code (#3048) 2022-11-18 10:23:04 +09:00
dependabot[bot]
06b02ba46e Bump actions/dependency-review-action from 2 to 3 (#3046)
Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 2 to 3.
- [Release notes](https://github.com/actions/dependency-review-action/releases)
- [Commits](https://github.com/actions/dependency-review-action/compare/v2...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-18 10:22:08 +09:00
49 changed files with 3880 additions and 1302 deletions

View File

@@ -11,4 +11,4 @@ jobs:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@v2 uses: actions/dependency-review-action@v3

View File

@@ -20,20 +20,20 @@ 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
- name: Setup Ruby - name: Setup Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.0.0 ruby-version: 3.1.0
- name: Install packages - name: Install packages
run: sudo apt-get install --yes zsh fish tmux run: sudo apt-get install --yes zsh fish tmux
- name: Install Ruby gems - name: Install Ruby gems
run: sudo gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1 run: sudo gem install --no-document minitest:5.17.0 rubocop:1.43.0 rubocop-minitest:0.25.1 rubocop-performance:1.15.2
- name: Rubocop - name: Rubocop
run: rubocop --require rubocop-minitest --require rubocop-performance run: rubocop --require rubocop-minitest --require rubocop-performance

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

15
.github/workflows/winget.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: Publish to Winget
on:
release:
types: [released]
jobs:
publish:
runs-on: windows-latest # Action can only run on Windows
steps:
- uses: vedantmgoyal2009/winget-releaser@v2
with:
identifier: junegunn.fzf
version: ${{ github.event.release.tag_name }}
installers-regex: '-windows_(armv7|arm64|amd64)\.zip$'
token: ${{ secrets.WINGET_TOKEN }}

View File

@@ -73,6 +73,8 @@ builds:
- arm - arm
- arm64 - arm64
- loong64 - loong64
- ppc64le
- s390x
goarm: goarm:
- 5 - 5
- 6 - 6

View File

@@ -28,3 +28,5 @@ Style/WordArray:
MinSize: 1 MinSize: 1
Minitest/AssertEqual: Minitest/AssertEqual:
Enabled: false Enabled: false
Naming/VariableNumber:
Enabled: false

View File

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

View File

@@ -1,7 +1,10 @@
Advanced fzf examples Advanced fzf examples
====================== ======================
*(Last update: 2022/08/25)* * *Last update: 2023/05/26*
* *Requires fzf 0.41.0 or above*
---
<!-- vim-markdown-toc GFM --> <!-- vim-markdown-toc GFM -->
@@ -15,7 +18,7 @@ Advanced fzf examples
* [Toggling between data sources](#toggling-between-data-sources) * [Toggling between data sources](#toggling-between-data-sources)
* [Ripgrep integration](#ripgrep-integration) * [Ripgrep integration](#ripgrep-integration)
* [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter) * [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter)
* [Using fzf as interative Ripgrep launcher](#using-fzf-as-interative-ripgrep-launcher) * [Using fzf as interactive Ripgrep launcher](#using-fzf-as-interactive-ripgrep-launcher)
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode) * [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
* [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode) * [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode)
* [Log tailing](#log-tailing) * [Log tailing](#log-tailing)
@@ -236,15 +239,13 @@ file called `rfv`.
# 1. Search for text in files using Ripgrep # 1. Search for text in files using Ripgrep
# 2. Interactively narrow down the list using fzf # 2. Interactively narrow down the list using fzf
# 3. Open the file in Vim # 3. Open the file in Vim
IFS=: read -ra selected < <(
rg --color=always --line-number --no-heading --smart-case "${*:-}" | rg --color=always --line-number --no-heading --smart-case "${*:-}" |
fzf --ansi \ fzf --ansi \
--color "hl:-1:underline,hl+:-1:underline:reverse" \ --color "hl:-1:underline,hl+:-1:underline:reverse" \
--delimiter : \ --delimiter : \
--preview 'bat --color=always {1} --highlight-line {2}' \ --preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' --preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
) --bind 'enter:become(vim {1} +{2})'
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
``` ```
And run it with an initial query string. And run it with an initial query string.
@@ -307,10 +308,14 @@ I know it's a lot to digest, let's try to break down the code.
position in the window position in the window
- `~3` makes the top three lines fixed header so that they are always - `~3` makes the top three lines fixed header so that they are always
visible regardless of the scroll offset visible regardless of the scroll offset
- Once we selected a line, we open the file with `vim` (`vim - Instead of using shell script to process the final output of fzf, we use
"${selected[0]}"`) and move the cursor to the line (`+${selected[1]}`). `become(...)` action which was added in [fzf 0.38.0][0.38.0] to turn fzf
into a new process that opens the file with `vim` (`vim {1}`) and move the
cursor to the line (`+{2}`).
### Using fzf as interative Ripgrep launcher [0.38.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0380
### Using fzf as interactive Ripgrep launcher
We have learned that we can bind `reload` action to a key (e.g. We have learned that we can bind `reload` action to a key (e.g.
`--bind=ctrl-r:execute(ps -ef)`). In the next example, we are going to **bind `--bind=ctrl-r:execute(ps -ef)`). In the next example, we are going to **bind
@@ -331,25 +336,22 @@ 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="${*:-}"
IFS=: read -ra selected < <( : | fzf --ansi --disabled --query "$INITIAL_QUERY" \
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \ --bind "start:reload:$RG_PREFIX {q}" \
fzf --ansi \
--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}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' --preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
) --bind 'enter:become(vim {1} +{2})'
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
``` ```
![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
@@ -358,8 +360,6 @@ IFS=: read -ra selected < <(
### Switching to fzf-only search mode ### Switching to fzf-only search mode
*(Requires fzf 0.27.1 or above)*
In the previous example, we lost fuzzy matching capability as we completely In the previous example, we lost fuzzy matching capability as we completely
delegated search functionality to Ripgrep. But we can dynamically switch to delegated search functionality to Ripgrep. But we can dynamically switch to
fzf-only search mode by *"unbinding"* `reload` action from `change` event. fzf-only search mode by *"unbinding"* `reload` action from `change` event.
@@ -375,19 +375,16 @@ 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="${*:-}"
IFS=: read -ra selected < <( : | fzf --ansi --disabled --query "$INITIAL_QUERY" \
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \ --bind "start:reload:$RG_PREFIX {q}" \
fzf --ansi \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--disabled --query "$INITIAL_QUERY" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \ --bind "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}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' --preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
) --bind 'enter:become(vim {1} +{2})'
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
``` ```
* Phase 1. Filtering with Ripgrep * Phase 1. Filtering with Ripgrep
@@ -408,10 +405,8 @@ IFS=: read -ra selected < <(
### Switching between Ripgrep mode and fzf mode ### Switching between Ripgrep mode and fzf mode
*(Requires fzf 0.30.0 or above)* [fzf 0.30.0][0.30.0] added `rebind` action so we can "rebind" the bindings
that were previously "unbound" via `unbind`.
fzf 0.30.0 added `rebind` action so we can "rebind" the bindings that were
previously "unbound" via `unbind`.
This is an improved version of the previous example that allows us to switch This is an improved version of the previous example that allows us to switch
between Ripgrep launcher mode and fzf-only filtering mode via CTRL-R and between Ripgrep launcher mode and fzf-only filtering mode via CTRL-R and
@@ -421,25 +416,32 @@ CTRL-F.
#!/usr/bin/env bash #!/usr/bin/env bash
# Switch between Ripgrep launcher mode (CTRL-R) and fzf filtering mode (CTRL-F) # Switch between Ripgrep launcher mode (CTRL-R) and fzf filtering mode (CTRL-F)
rm -f /tmp/rg-fzf-{r,f}
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}" INITIAL_QUERY="${*:-}"
IFS=: read -ra selected < <( : | fzf --ansi --disabled --query "$INITIAL_QUERY" \
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \ --bind "start:reload($RG_PREFIX {q})+unbind(ctrl-r)" \
fzf --ansi \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--disabled --query "$INITIAL_QUERY" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \ --bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+clear-query+rebind(ctrl-r)" \ --bind "ctrl-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)" \ --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)" \
--prompt '1. Ripgrep> ' \ --color "hl:-1:underline,hl+:-1:underline:reverse" \
--prompt '1. ripgrep> ' \
--delimiter : \ --delimiter : \
--header ' CTRL-R (Ripgrep mode) CTRL-F (fzf mode) ' \ --header ' CTRL-R (ripgrep mode) CTRL-F (fzf mode) ' \
--preview 'bat --color=always {1} --highlight-line {2}' \ --preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' --preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
) --bind 'enter:become(vim {1} +{2})'
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
``` ```
- To restore the query string when switching between modes, we store the
current query in `/tmp/rg-fzf-{r,f}` files and restore the query using
`transform-query` action which was added in [fzf 0.36.0][0.36.0].
- Also note that we unbind `ctrl-r` binding on `start` event which is
triggered once when fzf starts.
[0.30.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0300
[0.36.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0360
Log tailing Log tailing
----------- -----------
@@ -465,14 +467,15 @@ 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 'start:reload:$command' \
--bind 'ctrl-r:reload:$command' \
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \ --bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \ --bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \ --bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \
--bind 'ctrl-r:reload:$FZF_DEFAULT_COMMAND' \
--preview-window up:follow \ --preview-window up:follow \
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@" --preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
} }

View File

@@ -1,6 +1,287 @@
CHANGELOG CHANGELOG
========= =========
0.42.0
------
- Added new info style: `--info=right`
- Added new info style: `--info=inline-right`
- Added new border style `thinblock` which uses symbols for legacy computing
[one eighth block elements](https://en.wikipedia.org/wiki/Symbols_for_Legacy_Computing)
- Similarly to `block`, this style is suitable when using a different
background color because the window is completely contained within the border.
```sh
BAT_THEME=GitHub fzf --info=right --border=thinblock --preview-window=border-thinblock \
--margin=3 --scrollbar=▏▕ --preview='bat --color=always --style=numbers {}' \
--color=light,query:238,fg:238,bg:251,bg+:249,gutter:251,border:248,preview-bg:253
```
- This style may not render correctly depending on the font and the
terminal emulator.
0.41.1
------
- Fixed a bug where preview window is not updated when `--disabled` is set and
a reload is triggered by `change:reload` binding
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
------
- New actions
- `become(...)` - Replace the current fzf process with the specified
command using `execve(2)` system call.
See https://github.com/junegunn/fzf#turning-into-a-different-process for
more information.
```sh
# Open selected files in Vim
fzf --multi --bind 'enter:become(vim {+})'
# Open the file in Vim and go to the line
git grep --line-number . |
fzf --delimiter : --nth 3.. --bind 'enter:become(vim {1} +{2})'
```
- This action is not supported on Windows
- `show-preview`
- `hide-preview`
- Bug fixes
- `--preview-window 0,hidden` should not execute the preview command until
`toggle-preview` action is triggered
0.37.0
------
- Added a way to customize the separator of inline info
```sh
fzf --info 'inline: ' --prompt ' ' --color prompt:bright-yellow
```
- New event
- `focus` - Triggered when the focus changes due to a vertical cursor
movement or a search result update
```sh
fzf --bind 'focus:transform-preview-label:echo [ {} ]' --preview 'cat {}'
# Any action bound to the event runs synchronously and thus can make the interface sluggish
# e.g. lolcat isn't one of the fastest programs, and every cursor movement in
# fzf will be noticeably affected by its execution time
fzf --bind 'focus:transform-preview-label:echo [ {} ] | lolcat -f' --preview 'cat {}'
# Beware not to introduce an infinite loop
seq 10 | fzf --bind 'focus:up' --cycle
```
- New actions
- `change-border-label`
- `change-preview-label`
- `transform-border-label`
- `transform-preview-label`
- Bug fixes and improvements
0.36.0
------
- Added `--listen=HTTP_PORT` option to start HTTP server. It allows external
processes to send actions to perform via POST method.
```sh
# Start HTTP server on port 6266
fzf --listen 6266
# Send actions to the server
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
```
- Added draggable scrollbar to the main search window and the preview window
```sh
# Hide scrollbar
fzf --no-scrollbar
# Customize scrollbar
fzf --scrollbar ┆ --color scrollbar:blue
```
- New event
- Added `load` event that is triggered when the input stream is complete
and the initial processing of the list is complete.
```sh
# Change the prompt to "loaded" when the input stream is complete
(seq 10; sleep 1; seq 11 20) | fzf --prompt 'Loading> ' --bind 'load:change-prompt:Loaded> '
# You can use it instead of 'start' event without `--sync` if asynchronous
# trigger is not an issue.
(seq 10; sleep 1; seq 11 20) | fzf --bind 'load:last'
```
- New actions
- Added `pos(...)` action to move the cursor to the numeric position
- `first` and `last` are equivalent to `pos(1)` and `pos(-1)` respectively
```sh
# Put the cursor on the 10th item
seq 100 | fzf --sync --bind 'start:pos(10)'
# Put the cursor on the 10th to last item
seq 100 | fzf --sync --bind 'start:pos(-10)'
```
- Added `reload-sync(...)` action which replaces the current list only after
the reload process is complete. This is useful when the command takes
a while to produce the initial output and you don't want fzf to run against
an empty list while the command is running.
```sh
# You can still filter and select entries from the initial list for 3 seconds
seq 100 | fzf --bind 'load:reload-sync(sleep 3; seq 1000)+unbind(load)'
```
- Added `next-selected` and `prev-selected` actions to move between selected
items
```sh
# `next-selected` will move the pointer to the next selected item below the current line
# `prev-selected` will move the pointer to the previous selected item above the current line
seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected
# Both actions respect --layout option
seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected --layout reverse
```
- Added `change-query(...)` action that simply changes the query string to the
given static string. This can be useful when used with `--listen`.
```sh
curl localhost:6266 -d "change-query:$(date)"
```
- Added `transform-prompt(...)` action for transforming the prompt string
using an external command
```sh
# Press space to change the prompt string using an external command
# (only the first line of the output is taken)
fzf --bind 'space:reload(ls),load:transform-prompt(printf "%s> " "$(date)")'
```
- Added `transform-query(...)` action for transforming the query string using
an external command
```sh
# Press space to convert the query to uppercase letters
fzf --bind 'space:transform-query(tr "[:lower:]" "[:upper:]" <<< {q})'
# Bind it to 'change' event for automatic conversion
fzf --bind 'change:transform-query(tr "[:lower:]" "[:upper:]" <<< {q})'
# Can only type numbers
fzf --bind 'change:transform-query(sed "s/[^0-9]//g" <<< {q})'
```
- `put` action can optionally take an argument string
```sh
# a will put 'alpha' on the prompt, ctrl-b will put 'bravo'
fzf --bind 'a:put+put(lpha),ctrl-b:put(bravo)'
```
- Added color name `preview-label` for `--preview-label` (defaults to `label`
for `--border-label`)
- Better support for (Windows) terminals where each box-drawing character
takes 2 columns. Set `RUNEWIDTH_EASTASIAN` environment variable to `0` or `1`.
- On Vim, the variable will be automatically set if `&ambiwidth` is `double`
- Behavior changes
- fzf will always execute the preview command if the command template
contains `{q}` even when it's empty. If you prefer the old behavior,
you'll have to check if `{q}` is empty in your command.
```sh
# This will show // even when the query is empty
: | fzf --preview 'echo /{q}/'
# But if you don't want it,
: | fzf --preview '[ -n {q} ] || exit; echo /{q}/'
```
- `double-click` will behave the same as `enter` unless otherwise specified,
so you don't have to repeat the same action twice in `--bind` in most cases.
```sh
# No need to bind 'double-click' to the same action
fzf --bind 'enter:execute:less {}' # --bind 'double-click:execute:less {}'
```
- If the color for `separator` is not specified, it will default to the
color for `border`. Same holds true for `scrollbar`. This is to reduce
the number of configuration items required to achieve a consistent color
scheme.
- If `follow` flag is specified in `--preview-window` option, fzf will
automatically scroll to the bottom of the streaming preview output. But
when the user manually scrolls the window, the following stops. With
this version, fzf will resume following if the user scrolls the window
to the bottom.
- Default border style on Windows is changed to `sharp` because some
Windows terminals are not capable of displaying `rounded` border
characters correctly.
- Minor bug fixes and improvements
0.35.1
------
- Fixed a bug where fzf with `--tiebreak=chunk` crashes on inverse match query
- Fixed a bug where clicking above fzf would paste escape sequences
0.35.0 0.35.0
------ ------
- Added `start` event that is triggered only once when fzf finder starts. - Added `start` event that is triggered only once when fzf finder starts.

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

@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2022 Junegunn Choi Copyright (c) 2013-2023 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

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

@@ -12,7 +12,10 @@ differ 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 installed using Homebrew on Apple Silicon
set rtp+=/opt/homebrew/opt/fzf
" If you have cloned fzf on ~/.fzf directory
set rtp+=~/.fzf set rtp+=~/.fzf
``` ```
@@ -23,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'
``` ```
@@ -115,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
@@ -309,7 +312,7 @@ following options are allowed:
- `yoffset` [float default 0.5 range [0 ~ 1]] - `yoffset` [float default 0.5 range [0 ~ 1]]
- `xoffset` [float default 0.5 range [0 ~ 1]] - `xoffset` [float default 0.5 range [0 ~ 1]]
- `relative` [boolean default v:false] - `relative` [boolean default v:false]
- `border` [string default `rounded`]: Border style - `border` [string default `rounded` (`sharp` on Windows)]: Border style
- `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]` - `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]`
`fzf#wrap` `fzf#wrap`
@@ -483,4 +486,4 @@ autocmd FileType fzf set laststatus=0 noshowmode noruler
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2021 Junegunn Choi Copyright (c) 2013-2023 Junegunn Choi

169
README.md
View File

@@ -54,6 +54,7 @@ Table of Contents
* [Advanced topics](#advanced-topics) * [Advanced topics](#advanced-topics)
* [Performance](#performance) * [Performance](#performance)
* [Executing external programs](#executing-external-programs) * [Executing external programs](#executing-external-programs)
* [Turning into a different process](#turning-into-a-different-process)
* [Reloading the candidate list](#reloading-the-candidate-list) * [Reloading the candidate list](#reloading-the-candidate-list)
* [1. Update the list of processes by pressing CTRL-R](#1-update-the-list-of-processes-by-pressing-ctrl-r) * [1. Update the list of processes by pressing CTRL-R](#1-update-the-list-of-processes-by-pressing-ctrl-r)
* [2. Switch between sources by pressing CTRL-D or CTRL-F](#2-switch-between-sources-by-pressing-ctrl-d-or-ctrl-f) * [2. Switch between sources by pressing CTRL-D or CTRL-F](#2-switch-between-sources-by-pressing-ctrl-d-or-ctrl-f)
@@ -115,7 +116,7 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
| Package Manager | Linux Distribution | Command | | Package Manager | Linux Distribution | Command |
| --- | --- | --- | | --- | --- | --- |
| APK | Alpine Linux | `sudo apk add fzf` | | APK | Alpine Linux | `sudo apk add fzf` |
| APT | Debian 9+/Ubuntu 19.10+ | `sudo apt-get install fzf` | | APT | Debian 9+/Ubuntu 19.10+ | `sudo apt install fzf` |
| Conda | | `conda install -c conda-forge fzf` | | Conda | | `conda install -c conda-forge fzf` |
| DNF | Fedora | `sudo dnf install fzf` | | DNF | Fedora | `sudo dnf install fzf` |
| Nix | NixOS, etc. | `nix-env -iA nixpkgs.fzf` | | Nix | NixOS, etc. | `nix-env -iA nixpkgs.fzf` |
@@ -123,28 +124,31 @@ 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` |
> :warning: **Key bindings (CTRL-T / CTRL-R / ALT-C) and fuzzy auto-completion > :warning: **Key bindings (CTRL-T / CTRL-R / ALT-C) and fuzzy auto-completion
> may not be enabled by default.** > may not be enabled by default.**
> >
> Refer to the package documentation for more information. (e.g. `apt-cache show fzf`) > Refer to the package documentation for more information. (e.g. `apt show fzf`)
[![Packaging status](https://repology.org/badge/vertical-allrepos/fzf.svg)](https://repology.org/project/fzf/versions) [![Packaging status](https://repology.org/badge/vertical-allrepos/fzf.svg)](https://repology.org/project/fzf/versions)
### Windows ### Windows
Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also
available via [Chocolatey][choco] and [Scoop][scoop]: available via [Chocolatey][choco], [Scoop][scoop], and [Winget][winget]:
| Package manager | Command | | Package manager | Command |
| --- | --- | | --- | --- |
| Chocolatey | `choco install fzf` | | Chocolatey | `choco install fzf` |
| Scoop | `scoop install fzf` | | Scoop | `scoop install fzf` |
| Winget | `winget install fzf` |
[choco]: https://chocolatey.org/packages/fzf [choco]: https://chocolatey.org/packages/fzf
[scoop]: https://github.com/ScoopInstaller/Main/blob/master/bucket/fzf.json [scoop]: https://github.com/ScoopInstaller/Main/blob/master/bucket/fzf.json
[winget]: https://github.com/microsoft/winget-pkgs/tree/master/manifests/j/junegunn/fzf
Known issues and limitations on Windows can be found on [the wiki Known issues and limitations on Windows can be found on [the wiki
page][windows-wiki]. page][windows-wiki].
@@ -202,7 +206,23 @@ files excluding hidden ones. (You can override the default command with
vim $(fzf) vim $(fzf)
``` ```
#### Using the finder > *:bulb: A more robust solution would be to use `xargs` but we've presented
> the above as it's easier to grasp*
> ```sh
> fzf --print0 | xargs -0 -o vim
> ```
>
> *:bulb: fzf also has the ability to turn itself into a different process.*
>
> ```sh
> fzf --bind 'enter:become(vim {})'
> ```
>
> *See [Turning into a different process](#turning-into-a-different-process)
> for more information.*
### Using the finder
- `CTRL-K` / `CTRL-J` (or `CTRL-P` / `CTRL-N`) to move cursor up and down - `CTRL-K` / `CTRL-J` (or `CTRL-P` / `CTRL-N`) to move cursor up and down
- `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit - `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit
@@ -211,7 +231,7 @@ vim $(fzf)
- Mouse: scroll, click, double-click; shift-click and shift-scroll on - Mouse: scroll, click, double-click; shift-click and shift-scroll on
multi-select mode multi-select mode
#### Layout ### Layout
fzf by default starts in fullscreen mode, but you can make it start below the fzf by default starts in fullscreen mode, but you can make it start below the
cursor with `--height` option. cursor with `--height` option.
@@ -234,7 +254,7 @@ default. For example,
export FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border' export FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border'
``` ```
#### Search syntax ### Search syntax
Unless otherwise specified, fzf starts in "extended-search mode" where you can Unless otherwise specified, fzf starts in "extended-search mode" where you can
type in multiple search terms delimited by spaces. e.g. `^music .mp3$ sbtrkt type in multiple search terms delimited by spaces. e.g. `^music .mp3$ sbtrkt
@@ -262,7 +282,7 @@ or `py`.
^core go$ | rb$ | py$ ^core go$ | rb$ | py$
``` ```
#### Environment variables ### Environment variables
- `FZF_DEFAULT_COMMAND` - `FZF_DEFAULT_COMMAND`
- Default command to use when input is tty - Default command to use when input is tty
@@ -278,11 +298,11 @@ or `py`.
- Default options - Default options
- e.g. `export FZF_DEFAULT_OPTS="--layout=reverse --inline-info"` - e.g. `export FZF_DEFAULT_OPTS="--layout=reverse --inline-info"`
#### Options ### Options
See the man page (`man fzf`) for the full list of options. See the man page (`man fzf`) for the full list of options.
#### Demo ### Demo
If you learn by watching videos, check out this screencast by [@samoshkin](https://github.com/samoshkin) to explore `fzf` features. If you learn by watching videos, check out this screencast by [@samoshkin](https://github.com/samoshkin) to explore `fzf` features.
<a title="fzf - command-line fuzzy finder" href="https://www.youtube.com/watch?v=qgG5Jhi_Els"> <a title="fzf - command-line fuzzy finder" href="https://www.youtube.com/watch?v=qgG5Jhi_Els">
@@ -333,17 +353,37 @@ fish.
- `CTRL-T` - Paste the selected files and directories onto the command-line - `CTRL-T` - Paste the selected files and directories onto the command-line
- Set `FZF_CTRL_T_COMMAND` to override the default command - Set `FZF_CTRL_T_COMMAND` to override the default command
- Set `FZF_CTRL_T_OPTS` to pass additional options - Set `FZF_CTRL_T_OPTS` to pass additional options to fzf
```sh
# Preview file content using bat (https://github.com/sharkdp/bat)
export FZF_CTRL_T_OPTS="
--preview 'bat -n --color=always {}'
--bind 'ctrl-/:change-preview-window(down|hidden|)'"
```
- `CTRL-R` - Paste the selected command from history onto the command-line - `CTRL-R` - Paste the selected command from history onto the command-line
- If you want to see the commands in chronological order, press `CTRL-R` - If you want to see the commands in chronological order, press `CTRL-R`
again which toggles sorting by relevance again which toggles sorting by relevance
- Set `FZF_CTRL_R_OPTS` to pass additional options - Set `FZF_CTRL_R_OPTS` to pass additional options to fzf
```sh
# CTRL-/ to toggle small preview window to see the full command
# CTRL-Y to copy the command into clipboard using pbcopy
export FZF_CTRL_R_OPTS="
--preview 'echo {}' --preview-window up:3:hidden:wrap
--bind 'ctrl-/:toggle-preview'
--bind 'ctrl-y:execute-silent(echo -n {2..} | pbcopy)+abort'
--color header:italic
--header 'Press CTRL-Y to copy command into clipboard'"
```
- `ALT-C` - cd into the selected directory - `ALT-C` - cd into the selected directory
- Set `FZF_ALT_C_COMMAND` to override the default command - Set `FZF_ALT_C_COMMAND` to override the default command
- Set `FZF_ALT_C_OPTS` to pass additional options - Set `FZF_ALT_C_OPTS` to pass additional options to fzf
```sh
# Print tree structure in the preview window
export FZF_ALT_C_OPTS="--preview 'tree -C {}'"
```
If you're on a tmux session, you can start fzf in a tmux split-pane or in If you're on a tmux session, you can start fzf in a tmux split-pane or in
a tmux popup window by setting `FZF_TMUX_OPTS` (e.g. `-d 40%`). a tmux popup window by setting `FZF_TMUX_OPTS` (e.g. `export FZF_TMUX_OPTS='-p80%,60%'`).
See `fzf-tmux --help` for available options. See `fzf-tmux --help` for available options.
More tips can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/Configuring-shell-key-bindings). More tips can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/Configuring-shell-key-bindings).
@@ -351,7 +391,7 @@ More tips can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/C
Fuzzy completion for bash and zsh Fuzzy completion for bash and zsh
--------------------------------- ---------------------------------
#### Files and directories ### Files and directories
Fuzzy completion for files and directories can be triggered if the word before Fuzzy completion for files and directories can be triggered if the word before
the cursor ends with the trigger sequence, which is by default `**`. the cursor ends with the trigger sequence, which is by default `**`.
@@ -380,7 +420,7 @@ cd **<TAB>
cd ~/github/fzf**<TAB> cd ~/github/fzf**<TAB>
``` ```
#### Process IDs ### Process IDs
Fuzzy completion for PIDs is provided for kill command. Fuzzy completion for PIDs is provided for kill command.
@@ -389,7 +429,7 @@ Fuzzy completion for PIDs is provided for kill command.
kill -9 **<TAB> kill -9 **<TAB>
``` ```
#### Host names ### Host names
For ssh and telnet commands, fuzzy completion for hostnames is provided. The For ssh and telnet commands, fuzzy completion for hostnames is provided. The
names are extracted from /etc/hosts and ~/.ssh/config. names are extracted from /etc/hosts and ~/.ssh/config.
@@ -399,7 +439,7 @@ ssh **<TAB>
telnet **<TAB> telnet **<TAB>
``` ```
#### Environment variables / Aliases ### Environment variables / Aliases
```sh ```sh
unset **<TAB> unset **<TAB>
@@ -407,7 +447,7 @@ export **<TAB>
unalias **<TAB> unalias **<TAB>
``` ```
#### Settings ### Settings
```sh ```sh
# Use ~~ as the trigger sequence instead of the default ** # Use ~~ as the trigger sequence instead of the default **
@@ -429,7 +469,7 @@ _fzf_compgen_dir() {
fd --type d --hidden --follow --exclude ".git" . "$1" fd --type d --hidden --follow --exclude ".git" . "$1"
} }
# (EXPERIMENTAL) Advanced customization of fzf options via _fzf_comprun function # Advanced customization of fzf options via _fzf_comprun function
# - The first argument to the function is the name of the command. # - The first argument to the function is the name of the command.
# - You should make sure to pass the rest of the arguments to fzf. # - You should make sure to pass the rest of the arguments to fzf.
_fzf_comprun() { _fzf_comprun() {
@@ -437,15 +477,15 @@ _fzf_comprun() {
shift shift
case "$command" in case "$command" in
cd) fzf "$@" --preview 'tree -C {} | head -200' ;; cd) fzf --preview 'tree -C {} | head -200' "$@" ;;
export|unset) fzf "$@" --preview "eval 'echo \$'{}" ;; export|unset) fzf --preview "eval 'echo \$'{}" "$@" ;;
ssh) fzf "$@" --preview 'dig {}' ;; ssh) fzf --preview 'dig {}' "$@" ;;
*) fzf "$@" ;; *) fzf --preview 'bat -n --color=always {}' "$@" ;;
esac esac
} }
``` ```
#### Supported commands ### Supported commands
On bash, fuzzy completion is enabled only for a predefined set of commands On bash, fuzzy completion is enabled only for a predefined set of commands
(`complete | grep _fzf` to see the list). But you can enable it for other (`complete | grep _fzf` to see the list). But you can enable it for other
@@ -457,7 +497,7 @@ _fzf_setup_completion path ag git kubectl
_fzf_setup_completion dir tree _fzf_setup_completion dir tree
``` ```
#### Custom fuzzy completion ### Custom fuzzy completion
_**(Custom completion API is experimental and subject to change)**_ _**(Custom completion API is experimental and subject to change)**_
@@ -517,9 +557,8 @@ Advanced topics
### Performance ### Performance
fzf is fast and is [getting even faster][perf]. Performance should not be fzf is fast. Performance should not be a problem in most use cases. However,
a problem in most use cases. However, you might want to be aware of the you might want to be aware of the options that can affect performance.
options that affect performance.
- `--ansi` tells fzf to extract and parse ANSI color codes in the input, and it - `--ansi` tells fzf to extract and parse ANSI color codes in the input, and it
makes the initial scanning slower. So it's not recommended that you add it makes the initial scanning slower. So it's not recommended that you add it
@@ -527,12 +566,6 @@ options that affect performance.
- `--nth` makes fzf slower because it has to tokenize each line. - `--nth` makes fzf slower because it has to tokenize each line.
- `--with-nth` makes fzf slower as fzf has to tokenize and reassemble each - `--with-nth` makes fzf slower as fzf has to tokenize and reassemble each
line. line.
- If you absolutely need better performance, you can consider using
`--algo=v1` (the default being `v2`) to make fzf use a faster greedy
algorithm. However, this algorithm is not guaranteed to find the optimal
ordering of the matches and is not recommended.
[perf]: https://junegunn.kr/images/fzf-0.17.0.png
### Executing external programs ### Executing external programs
@@ -547,6 +580,47 @@ fzf --bind 'f1:execute(less -f {}),ctrl-y:execute-silent(echo {} | pbcopy)+abort
See *KEY BINDINGS* section of the man page for details. See *KEY BINDINGS* section of the man page for details.
### Turning into a different process
`become(...)` is similar to `execute(...)`/`execute-silent(...)` described
above, but instead of executing the command and coming back to fzf on
complete, it turns fzf into a new process for the command.
```sh
fzf --bind 'enter:become(vim {})'
```
Compared to the seemingly equivalent command substitution `vim "$(fzf)"`, this
approach has several advantages:
* Vim will not open an empty file when you terminate fzf with
<kbd>CTRL-C</kbd>
* Vim will not open an empty file when you press <kbd>ENTER</kbd> on an empty
result
* Can handle multiple selections even when they have whitespaces
```sh
fzf --multi --bind 'enter:become(vim {+})'
```
To be fair, running `fzf --print0 | xargs -0 -o vim` instead of `vim "$(fzf)"`
resolves all of the issues mentioned. Nonetheless, `become(...)` still offers
additional benefits in different scenarios.
* You can set up multiple bindings to handle the result in different ways
without any wrapping script
```sh
fzf --bind 'enter:become(vim {}),ctrl-e:become(emacs {})'
```
* Previously, you would have to use `--expect=ctrl-e` and check the first
line of the output of fzf
* You can easily build the subsequent command using the field index
expressions of fzf
```sh
# Open the file in Vim and go to the line
git grep --line-number . |
fzf --delimiter : --nth 3.. --bind 'enter:become(vim {1} +{2})'
```
### Reloading the candidate list ### Reloading the candidate list
By binding `reload` action to a key or an event, you can make fzf dynamically By binding `reload` action to a key or an event, you can make fzf dynamically
@@ -556,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
``` ```
@@ -579,11 +653,11 @@ 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
``` ```
@@ -591,8 +665,8 @@ If ripgrep doesn't find any matches, it will exit with a non-zero exit status,
and fzf will warn you about it. To suppress the warning message, we added 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 interative Ripgrep launcher"](https://github.com/junegunn/fzf/blob/master/ADVANCED.md#using-fzf-as-interative-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
@@ -612,7 +686,7 @@ syntax-highlights the content of a file, such as
[Highlight](http://www.andre-simon.de/doku/highlight/en/highlight.php): [Highlight](http://www.andre-simon.de/doku/highlight/en/highlight.php):
```bash ```bash
fzf --preview 'bat --style=numbers --color=always --line-range :500 {}' fzf --preview 'bat --color=always {}' --preview-window '~3'
``` ```
You can customize the size, position, and border of the preview window using You can customize the size, position, and border of the preview window using
@@ -622,6 +696,7 @@ You can customize the size, position, and border of the preview window using
```bash ```bash
fzf --height 40% --layout reverse --info inline --border \ fzf --height 40% --layout reverse --info inline --border \
--preview 'file {}' --preview-window up,1,border-horizontal \ --preview 'file {}' --preview-window up,1,border-horizontal \
--bind 'ctrl-/:change-preview-window(50%|hidden|)' \
--color 'fg:#bbccdd,fg+:#ddeeff,bg:#334455,preview-bg:#223344,border:#778899' --color 'fg:#bbccdd,fg+:#ddeeff,bg:#334455,preview-bg:#223344,border:#778899'
``` ```
@@ -649,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
@@ -678,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
@@ -708,4 +783,4 @@ https://github.com/junegunn/fzf/wiki/Related-projects
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2021 Junegunn Choi Copyright (c) 2013-2023 Junegunn Choi

View File

@@ -179,15 +179,20 @@ trap 'cleanup' EXIT
envs="export TERM=$TERM " envs="export TERM=$TERM "
if [[ "$opt" =~ "-E" ]]; then if [[ "$opt" =~ "-E" ]]; then
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS" tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
tmux_verson=$(tmux -V) if [[ $(awk '{print ($1 > 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version > 3.2") = 1 ]]; then
if [[ ! $tmux_verson =~ 3\.2 ]]; then
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")"
[[ -n "$FZF_DEFAULT_COMMAND" ]] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" [[ -n "$FZF_DEFAULT_COMMAND" ]] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
[[ -n "$BAT_THEME" ]] && envs="$envs BAT_THEME=$(printf %q "$BAT_THEME")"
echo "$envs;" > "$argsf" echo "$envs;" > "$argsf"
# Build arguments to fzf # Build arguments to fzf

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
@@ -325,7 +325,7 @@ following options are allowed:
- `yoffset` [float default 0.5 range [0 ~ 1]] - `yoffset` [float default 0.5 range [0 ~ 1]]
- `xoffset` [float default 0.5 range [0 ~ 1]] - `xoffset` [float default 0.5 range [0 ~ 1]]
- `relative` [boolean default v:false] - `relative` [boolean default v:false]
- `border` [string default `rounded`]: Border style - `border` [string default `rounded` (`sharp` on Windows)]: Border style
- `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]` - `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]`
@@ -506,7 +506,7 @@ LICENSE *fzf-license*
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2021 Junegunn Choi Copyright (c) 2013-2023 Junegunn Choi
============================================================================== ==============================================================================
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap: vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:

14
go.mod
View File

@@ -1,21 +1,21 @@
module github.com/junegunn/fzf module github.com/junegunn/fzf
require ( require (
github.com/gdamore/tcell/v2 v2.5.3 github.com/gdamore/tcell/v2 v2.5.4
github.com/mattn/go-isatty v0.0.16 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 (
github.com/gdamore/encoding v1.0.0 // indirect github.com/gdamore/encoding v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.5.0 // indirect
) )
go 1.17 go 1.17

43
go.sum
View File

@@ -1,32 +1,49 @@
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.5.3 h1:b9XQrT6QGbgI7JvZOJXFNczOQeIYbo8BfeSMzt2sAV0= github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k=
github.com/gdamore/tcell/v2 v2.5.3/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo= github.com/gdamore/tcell/v2 v2.5.4/go.mod h1:dZgRy5v4iMobMEcWNYBtREnDZAT9DYmfqIkrgEMxLyw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
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=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-20220318055525-2edf467146b5/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-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= 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-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.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 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=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.35.0 version=0.42.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
@@ -176,7 +176,9 @@ case "$archi" in
Linux\ armv8*) download fzf-$version-linux_arm64.tar.gz ;; Linux\ armv8*) download fzf-$version-linux_arm64.tar.gz ;;
Linux\ aarch64*) download fzf-$version-linux_arm64.tar.gz ;; Linux\ aarch64*) download fzf-$version-linux_arm64.tar.gz ;;
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\ *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.35.0" $version="0.42.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.35" var version string = "0.42"
var revision string = "devel" var revision string = "devel"
func main() { func main() {

View File

@@ -1,7 +1,7 @@
.ig .ig
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2021 Junegunn Choi Copyright (c) 2013-2023 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -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 "Nov 2022" "fzf 0.35.0" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "Jun 2023" "fzf 0.42.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

@@ -1,7 +1,7 @@
.ig .ig
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2021 Junegunn Choi Copyright (c) 2013-2023 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -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 "Nov 2022" "fzf 0.35.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "Jun 2023" "fzf 0.42.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
@@ -215,6 +228,10 @@ Draw border around the finder
.br .br
.BR double " Border with double lines" .BR double " Border with double lines"
.br .br
.BR block " Border using block elements; suitable when using different background colors"
.br
.BR thinblock " Border using legacy computing symbols; may not be displayed on some terminals"
.br
.BR horizontal " Horizontal lines above and below the finder" .BR horizontal " Horizontal lines above and below the finder"
.br .br
.BR vertical " Vertical lines on each side of the finder" .BR vertical " Vertical lines on each side of the finder"
@@ -230,6 +247,11 @@ Draw border around the finder
.BR none .BR none
.br .br
If you use a terminal emulator where each box-drawing character takes
2 columns, try setting \fBRUNEWIDTH_EASTASIAN\fR environment variable to
\fB0\fR or \fB1\fR. If the border is still not properly rendered, set
\fB--no-unicode\fR.
.TP .TP
.BI "--border-label" [=LABEL] .BI "--border-label" [=LABEL]
Label to print on the horizontal border line. Should be used with one of the Label to print on the horizontal border line. Should be used with one of the
@@ -276,7 +298,8 @@ the label. Label is printed on the top border line by default, add
.TP .TP
.B "--no-unicode" .B "--no-unicode"
Use ASCII characters instead of Unicode box drawing characters to draw border Use ASCII characters instead of Unicode drawing characters to draw borders,
the spinner and the horizontal separator.
.TP .TP
.BI "--margin=" MARGIN .BI "--margin=" MARGIN
@@ -334,7 +357,11 @@ Determines the display style of finder info (match counters).
.br .br
.BR default " Display on the next line to the prompt" .BR default " Display on the next line to the prompt"
.br .br
.BR inline " Display on the same line" .BR inline " Display on the same line with the default separator ' < '"
.br
.BR inline:SEPARATOR " Display on the same line with a non-default separator"
.br
.BR inline-right " Display on the right end of the same line
.br .br
.BR hidden " Do not display finder info" .BR hidden " Do not display finder info"
.br .br
@@ -355,6 +382,16 @@ ANSI color codes are supported.
Do not display horizontal separator on the info line. A synonym for Do not display horizontal separator on the info line. A synonym for
\fB--separator=''\fB \fB--separator=''\fB
.TP
.BI "--scrollbar=" "CHAR1[CHAR2]"
Use the given character to render scrollbar. (default: '│' or ':' depending on
\fB--no-unicode\fR). The optional \fBCHAR2\fR is used to render scrollbar of
the preview window.
.TP
.B "--no-scrollbar"
Do not display scrollbar. A synonym for \fB--scrollbar=''\fB
.TP .TP
.BI "--prompt=" "STR" .BI "--prompt=" "STR"
Input prompt (default: '> ') Input prompt (default: '> ')
@@ -404,20 +441,24 @@ color mappings.
.B COLOR NAMES: .B COLOR NAMES:
\fBfg \fRText \fBfg \fRText
\fBbg \fRBackground
\fBpreview-fg \fRPreview window text \fBpreview-fg \fRPreview window text
\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 (defaults to \fBbg+\fR) \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 \fBdisabled \fRQuery string when search is disabled (\fB--disabled\fR)
\fBinfo \fRInfo line (match counters) \fBinfo \fRInfo line (match counters)
\fBseparator \fRHorizontal separator on info 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)
\fBscrollbar \fRScrollbar
\fBpreview-border \fRBorder around the preview window (\fB--preview\fR)
\fBpreview-scrollbar \fRScrollbar
\fBseparator \fRHorizontal separator on info line
\fBlabel \fRBorder label (\fB--border-label\fR and \fB--preview-label\fR) \fBlabel \fRBorder label (\fB--border-label\fR and \fB--preview-label\fR)
\fBpreview-label \fRBorder label of the preview window (\fB--preview-label\fR)
\fBprompt \fRPrompt \fBprompt \fRPrompt
\fBpointer \fRPointer to the current line \fBpointer \fRPointer to the current line
\fBmarker \fRMulti-select marker \fBmarker \fRMulti-select marker
@@ -480,7 +521,7 @@ Use black background
.BI "--history=" "HISTORY_FILE" .BI "--history=" "HISTORY_FILE"
Load search history from the specified file and update the file on completion. Load search history from the specified file and update the file on completion.
When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically remapped to When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically remapped to
\fBnext-history\fR and \fBprevious-history\fR. \fBnext-history\fR and \fBprev-history\fR.
.TP .TP
.BI "--history-size=" "N" .BI "--history-size=" "N"
Maximum number of entries in the history file (default: 1000). The file is Maximum number of entries in the history file (default: 1000). The file is
@@ -534,7 +575,8 @@ e.g.
Note that you can escape a placeholder pattern by prepending a backslash. Note that you can escape a placeholder pattern by prepending a backslash.
Preview window will be updated even when there is no match for the current Preview window will be updated even when there is no match for the current
query if any of the placeholder expressions evaluates to a non-empty string. query if any of the placeholder expressions evaluates to a non-empty string
or \fB{q}\fR is in the command template.
Since 0.24.0, fzf can render partial preview content before the preview command Since 0.24.0, fzf can render partial preview content before the preview command
completes. ANSI escape sequence for clearing the display (\fBCSI 2 J\fR) is completes. ANSI escape sequence for clearing the display (\fBCSI 2 J\fR) is
@@ -555,14 +597,18 @@ Label to print on the horizontal border line of the preview window.
Should be used with one of the following \fB--preview-window\fR options. Should be used with one of the following \fB--preview-window\fR options.
.br .br
.B * border-rounded (default) .B * border-rounded (default on non-Windows platforms)
.br .br
.B * border-sharp .B * border-sharp (default on Windows)
.br .br
.B * border-bold .B * border-bold
.br .br
.B * border-double .B * border-double
.br .br
.B * border-block
.br
.B * border-thinblock
.br
.B * border-horizontal .B * border-horizontal
.br .br
.B * border-top .B * border-top
@@ -719,6 +765,24 @@ 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]"
Start HTTP server on the given port. It allows external processes to send
actions to perform via POST method. If the port number is omitted or given as
0, fzf will choose the port automatically and export it as \fBFZF_PORT\fR
environment variable to the child processes started via \fBexecute\fR and
\fBexecute-silent\fR actions.
e.g.
\fB# Start HTTP server on port 6266
fzf --listen 6266
# Send action to the server
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
# Choose port automatically and export it as $FZF_PORT to the child process
fzf --listen --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'
\fR
.TP
.B "--version" .B "--version"
Display version information and exit Display version information and exit
@@ -819,6 +883,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
@@ -887,6 +953,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
@@ -913,6 +981,15 @@ e.g.
\fB# Move cursor to the last item and select all items \fB# Move cursor to the last item and select all items
seq 1000 | fzf --multi --sync --bind start:last+select-all\fR seq 1000 | fzf --multi --sync --bind start:last+select-all\fR
.RE .RE
\fIload\fR
.RS
Triggered when the input stream is complete and the initial processing of the
list is complete.
e.g.
\fB# Change the prompt to "loaded" when the input stream is complete
(seq 10; sleep 1; seq 11 20) | fzf --prompt 'Loading> ' --bind 'load:change-prompt:Loaded> '\fR
.RE
\fIchange\fR \fIchange\fR
.RS .RS
Triggered whenever the query string is changed Triggered whenever the query string is changed
@@ -921,6 +998,44 @@ e.g.
\fB# Move cursor to the first entry whenever the query is changed \fB# Move cursor to the first entry whenever the query is changed
fzf --bind change:first\fR fzf --bind change:first\fR
.RE .RE
\fIfocus\fR
.RS
Triggered when the focus changes due to a vertical cursor movement or a search
result update.
e.g.
\fBfzf --bind 'focus:transform-preview-label:echo [ {} ]' --preview 'cat {}'
# Any action bound to the event runs synchronously and thus can make the interface sluggish
# e.g. lolcat isn't one of the fastest programs, and every cursor movement in
# fzf will be noticeably affected by its execution time
fzf --bind 'focus:transform-preview-label:echo [ {} ] | lolcat -f' --preview 'cat {}'
# Beware not to introduce an infinite loop
seq 10 | fzf --bind 'focus:up' --cycle\fR
.RE
\fIone\fR
.RS
Triggered when there's only one match. \fBone:accept\fR binding is comparable
to \fB--select-1\fR option, but the difference is that \fB--select-1\fR is only
effective before the interactive finder starts but \fBone\fR event is triggered
by the interactive finder.
e.g.
\fB# Automatically select the only match
seq 10 | fzf --bind one:accept\fR
.RE
\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
@@ -943,11 +1058,16 @@ A key or an event can be bound to one or more of the following actions.
\fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty) \fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty)
\fBbackward-kill-word\fR \fIalt-bs\fR \fBbackward-kill-word\fR \fIalt-bs\fR
\fBbackward-word\fR \fIalt-b shift-left\fR \fBbackward-word\fR \fIalt-b shift-left\fR
\fBbecome(...)\fR (replace fzf process with the specified command; see below for the details)
\fBbeginning-of-line\fR \fIctrl-a home\fR \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-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-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 '|')
\fBchange-prompt(...)\fR (change prompt to the given string) \fBchange-prompt(...)\fR (change prompt to the given string)
\fBchange-query(...)\fR (change query string to the given string)
\fBclear-screen\fR \fIctrl-l\fR \fBclear-screen\fR \fIctrl-l\fR
\fBclear-selection\fR (clear multi-selection) \fBclear-selection\fR (clear multi-selection)
\fBclose\fR (close preview window if open, abort fzf otherwise) \fBclose\fR (close preview window if open, abort fzf otherwise)
@@ -962,7 +1082,7 @@ A key or an event can be bound to one or more of the following actions.
\fBend-of-line\fR \fIctrl-e end\fR \fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details) \fBexecute(...)\fR (see below for the details)
\fBexecute-silent(...)\fR (see below for the details) \fBexecute-silent(...)\fR (see below for the details)
\fBfirst\fR (move to the first match) \fBfirst\fR (move to the first match; same as \fBpos(1)\fR)
\fBforward-char\fR \fIctrl-f right\fR \fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR \fBforward-word\fR \fIalt-f shift-right\fR
\fBignore\fR \fBignore\fR
@@ -970,12 +1090,17 @@ A key or an event can be bound to one or more of the following actions.
\fBjump-accept\fR (jump and accept) \fBjump-accept\fR (jump and accept)
\fBkill-line\fR \fBkill-line\fR
\fBkill-word\fR \fIalt-d\fR \fBkill-word\fR \fIalt-d\fR
\fBlast\fR (move to the last match) \fBlast\fR (move to the last match; same as \fBpos(-1)\fR)
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR) \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
\fBnext-selected\fR (move to the next selected item)
\fBpage-down\fR \fIpgdn\fR \fBpage-down\fR \fIpgdn\fR
\fBpage-up\fR \fIpgup\fR \fBpage-up\fR \fIpgup\fR
\fBhalf-page-down\fR \fBhalf-page-down\fR
\fBhalf-page-up\fR \fBhalf-page-up\fR
\fBhide-preview\fR
\fBpos(...)\fR (move cursor to the numeric position; negative number to count from the end)
\fBprev-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBprev-selected\fR (move to the previous selected item)
\fBpreview(...)\fR (see below for the details) \fBpreview(...)\fR (see below for the details)
\fBpreview-down\fR \fIshift-down\fR \fBpreview-down\fR \fIshift-down\fR
\fBpreview-up\fR \fIshift-up\fR \fBpreview-up\fR \fIshift-up\fR
@@ -985,15 +1110,17 @@ A key or an event can be bound to one or more of the following actions.
\fBpreview-half-page-up\fR \fBpreview-half-page-up\fR
\fBpreview-bottom\fR \fBpreview-bottom\fR
\fBpreview-top\fR \fBpreview-top\fR
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBprint-query\fR (print query and exit) \fBprint-query\fR (print query and exit)
\fBput\fR (put the character to the prompt) \fBput\fR (put the character to the prompt)
\fBput(...)\fR (put the given string to the prompt)
\fBrefresh-preview\fR \fBrefresh-preview\fR
\fBrebind(...)\fR (rebind bindings after \fBunbind\fR) \fBrebind(...)\fR (rebind bindings after \fBunbind\fR)
\fBreload(...)\fR (see below for the details) \fBreload(...)\fR (see below for the details)
\fBreload-sync(...)\fR (see below for the details)
\fBreplace-query\fR (replace query string with the current selection) \fBreplace-query\fR (replace query string with the current selection)
\fBselect\fR \fBselect\fR
\fBselect-all\fR (select all matches) \fBselect-all\fR (select all matches)
\fBshow-preview\fR
\fBtoggle\fR (\fIright-click\fR) \fBtoggle\fR (\fIright-click\fR)
\fBtoggle-all\fR (toggle all matches) \fBtoggle-all\fR (toggle all matches)
\fBtoggle+down\fR \fIctrl-i (tab)\fR \fBtoggle+down\fR \fIctrl-i (tab)\fR
@@ -1003,7 +1130,14 @@ 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-header(...)\fR (transform header using an external command)
\fBtransform-preview-label(...)\fR (transform preview label using an external command)
\fBtransform-prompt(...)\fR (transform prompt string using an external command)
\fBtransform-query(...)\fR (transform query string using an external command)
\fBunbind(...)\fR (unbind bindings) \fBunbind(...)\fR (unbind bindings)
\fBunix-line-discard\fR \fIctrl-u\fR \fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR \fBunix-word-rubout\fR \fIctrl-w\fR
@@ -1031,6 +1165,8 @@ that case, you can use any of the following alternative notations to avoid
parse errors. parse errors.
\fBaction-name[...]\fR \fBaction-name[...]\fR
\fBaction-name{...}\fR
\fBaction-name<...>\fR
\fBaction-name~...~\fR \fBaction-name~...~\fR
\fBaction-name!...!\fR \fBaction-name!...!\fR
\fBaction-name@...@\fR \fBaction-name@...@\fR
@@ -1071,6 +1207,14 @@ On *nix systems, fzf runs the command with \fB$SHELL -c\fR if \fBSHELL\fR is
set, otherwise with \fBsh -c\fR, so in this case make sure that the command is 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
current fzf process with the specified command using \fBexecve(2)\fR system
call.
\fBfzf --bind "enter:become(vim {})"\fR
\fBbecome(...)\fR is not supported on Windows.
.SS RELOAD INPUT .SS RELOAD INPUT
\fBreload(...)\fR action is used to dynamically update the input list \fBreload(...)\fR action is used to dynamically update the input list
@@ -1091,6 +1235,16 @@ e.g.
fzf --bind "change:reload:$RG_PREFIX {q} || true" \\ fzf --bind "change:reload:$RG_PREFIX {q} || true" \\
--ansi --disabled --query "$INITIAL_QUERY"\fR --ansi --disabled --query "$INITIAL_QUERY"\fR
\fBreload-sync(...)\fR is a synchronous version of \fBreload\fR that replaces
the list only when the command is complete. This is useful when the command
takes a while to produce the initial output and you don't want fzf to run
against an empty list while the command is running.
e.g.
\fB# You can still filter and select entries from the initial list for 3 seconds
seq 100 | fzf --bind 'load:reload-sync(sleep 3; seq 1000)+unbind(load)'\fR
.SS PREVIEW BINDING .SS PREVIEW BINDING
With \fBpreview(...)\fR action, you can specify multiple different preview With \fBpreview(...)\fR action, you can specify multiple different preview

View File

@@ -1,4 +1,4 @@
" Copyright (c) 2017 Junegunn Choi " Copyright (c) 2013-2023 Junegunn Choi
" "
" MIT License " MIT License
" "
@@ -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 ''
@@ -456,6 +456,30 @@ function! s:writefile(...)
endif endif
endfunction endfunction
function! s:extract_option(opts, name)
let opt = ''
let expect = 0
" There are a few cases where this function doesn't work as expected.
" Let's just assume such cases are extremely unlikely in real world.
" e.g. --query --border
for word in split(a:opts)
if expect && word !~ '^"\=-'
let opt = opt . ' ' . word
let expect = 0
elseif word == '--no-'.a:name
let opt = ''
elseif word =~ '^--'.a:name.'='
let opt = word
elseif word =~ '^--'.a:name.'$'
let opt = word
let expect = 1
elseif expect
let expect = 0
endif
endfor
return opt
endfunction
function! fzf#run(...) abort function! fzf#run(...) abort
try try
let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh() let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
@@ -511,7 +535,8 @@ try
let height = s:calc_size(&lines, dict.down, dict) let height = s:calc_size(&lines, dict.down, dict)
let optstr .= ' --height='.height let optstr .= ' --height='.height
endif endif
let optstr .= s:border_opt(get(dict, 'window', 0)) " Respect --border option given in $FZF_DEFAULT_OPTS and 'options'
let optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr])
let prev_default_command = $FZF_DEFAULT_COMMAND let prev_default_command = $FZF_DEFAULT_COMMAND
if len(source_command) if len(source_command)
let $FZF_DEFAULT_COMMAND = source_command let $FZF_DEFAULT_COMMAND = source_command
@@ -738,7 +763,7 @@ function! s:calc_size(max, val, dict)
return size return size
endif endif
let margin = match(opts, '--inline-info\|--info[^-]\{-}inline') > match(opts, '--no-inline-info\|--info[^-]\{-}\(default\|hidden\)') ? 1 : 2 let margin = match(opts, '--inline-info\|--info[^-]\{-}inline') > match(opts, '--no-inline-info\|--info[^-]\{-}\(default\|hidden\)') ? 1 : 2
let margin += stridx(opts, '--border') > stridx(opts, '--no-border') ? 2 : 0 let margin += match(opts, '--border\([^-]\|$\)') > match(opts, '--no-border\([^-]\|$\)') ? 2 : 0
if stridx(opts, '--header') > stridx(opts, '--no-header') if stridx(opts, '--header') > stridx(opts, '--no-header')
let margin += len(split(opts, "\n")) let margin += len(split(opts, "\n"))
endif endif
@@ -755,9 +780,9 @@ function! s:border_opt(window)
endif endif
" Border style " Border style
let style = tolower(get(a:window, 'border', 'rounded')) let style = tolower(get(a:window, 'border', ''))
if !has_key(a:window, 'border') && !get(a:window, 'rounded', 1) if !has_key(a:window, 'border') && has_key(a:window, 'rounded')
let style = 'sharp' let style = a:window.rounded ? 'rounded' : 'sharp'
endif endif
if style == 'none' || style == 'no' if style == 'none' || style == 'no'
return '' return ''
@@ -765,7 +790,7 @@ function! s:border_opt(window)
" For --border styles, we need fzf 0.24.0 or above " For --border styles, we need fzf 0.24.0 or above
call fzf#exec('0.24.0') call fzf#exec('0.24.0')
let opt = ' --border=' . style let opt = ' --border ' . style
if has_key(a:window, 'highlight') if has_key(a:window, 'highlight')
let color = s:get_color('fg', a:window.highlight) let color = s:get_color('fg', a:window.highlight)
if len(color) if len(color)
@@ -827,6 +852,17 @@ if exists(':tnoremap')
tnoremap <silent> <Plug>(fzf-normal) <C-\><C-n> tnoremap <silent> <Plug>(fzf-normal) <C-\><C-n>
endif endif
let s:warned = 0
function! s:handle_ambidouble(dict)
if &ambiwidth == 'double'
let a:dict.env = { 'RUNEWIDTH_EASTASIAN': '1' }
elseif !s:warned && $RUNEWIDTH_EASTASIAN == '1' && &ambiwidth !=# 'double'
call s:warn("$RUNEWIDTH_EASTASIAN is '1' but &ambiwidth is not 'double'")
2sleep
let s:warned = 1
endif
endfunction
function! s:execute_term(dict, command, temps) abort function! s:execute_term(dict, command, temps) abort
let winrest = winrestcmd() let winrest = winrestcmd()
let pbuf = bufnr('') let pbuf = bufnr('')
@@ -896,6 +932,7 @@ function! s:execute_term(dict, command, temps) abort
endif endif
let command .= s:term_marker let command .= s:term_marker
if has('nvim') if has('nvim')
call s:handle_ambidouble(fzf)
call termopen(command, fzf) call termopen(command, fzf)
else else
let term_opts = {'exit_cb': function(fzf.on_exit)} let term_opts = {'exit_cb': function(fzf.on_exit)}
@@ -907,6 +944,7 @@ function! s:execute_term(dict, command, temps) abort
else else
let term_opts.curwin = 1 let term_opts.curwin = 1
endif endif
call s:handle_ambidouble(term_opts)
let fzf.buf = term_start([&shell, &shellcmdflag, command], term_opts) let fzf.buf = term_start([&shell, &shellcmdflag, command], term_opts)
if is_popup && exists('#TerminalWinOpen') if is_popup && exists('#TerminalWinOpen')
doautocmd <nomodeline> TerminalWinOpen doautocmd <nomodeline> TerminalWinOpen

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,8 +310,8 @@ 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 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
basename bunzip2 bzip2 chmod chown curl cp dirname du basename bunzip2 bzip2 chmod chown curl cp dirname du
@@ -373,7 +374,7 @@ _fzf_setup_completion() {
} }
# Environment variables / Aliases / Hosts / Process # Environment variables / Aliases / Hosts / Process
_fzf_setup_completion 'var' export unset _fzf_setup_completion 'var' export unset printenv
_fzf_setup_completion 'alias' unalias _fzf_setup_completion 'alias' unalias
_fzf_setup_completion 'host' ssh telnet _fzf_setup_completion 'host' ssh telnet
_fzf_setup_completion 'proc' kill _fzf_setup_completion 'proc' kill

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

@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2021 Junegunn Choi Copyright (c) 2013-2023 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -617,7 +617,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) { func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) {
pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0) pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0)
pos := posArray(withPos, len(pattern)) pos := posArray(withPos, len(pattern))
prevClass := charWhite prevClass := initialCharClass
if sidx > 0 { if sidx > 0 {
prevClass = charClassOf(text.Get(sidx - 1)) prevClass = charClassOf(text.Get(sidx - 1))
} }

View File

@@ -138,7 +138,9 @@ func Run(opts *Options, version string, revision string) {
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos, opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
opts.Filter == nil, opts.Nth, opts.Delimiter, runes) opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
} }
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) inputRevision := 0
snapshotRevision := 0
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox, inputRevision)
// Filtering mode // Filtering mode
if opts.Filter != nil { if opts.Filter != nil {
@@ -209,25 +211,15 @@ func Run(opts *Options, version string, revision string) {
// Event coordination // Event coordination
reading := true reading := true
clearCache := util.Once(false)
clearSelection := util.Once(false)
ticks := 0 ticks := 0
var nextCommand *string var nextCommand *string
restart := func(command string) {
reading = true
clearCache = util.Once(true)
clearSelection = util.Once(true)
chunkList.Clear()
itemIndex = 0
header = make([]string, 0, opts.HeaderLines)
go reader.restart(command)
}
eventBox.Watch(EvtReadNew) eventBox.Watch(EvtReadNew)
total := 0 total := 0
query := []rune{} query := []rune{}
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}
} }
@@ -236,10 +228,23 @@ func Run(opts *Options, version string, revision string) {
terminal.startChan <- fitpad{-1, -1} terminal.startChan <- fitpad{-1, -1}
} }
} }
useSnapshot := false
var snapshot []*Chunk
var count int
restart := func(command string) {
reading = true
chunkList.Clear()
itemIndex = 0
inputRevision++
header = make([]string, 0, opts.HeaderLines)
go reader.restart(command)
}
for { for {
delay := true delay := true
ticks++ ticks++
input := func(reloaded bool) []rune { input := func() []rune {
reloaded := snapshotRevision != inputRevision
paused, input := terminal.Input() paused, input := terminal.Input()
if reloaded && paused { if reloaded && paused {
query = []rune{} query = []rune{}
@@ -267,25 +272,35 @@ func Run(opts *Options, version string, revision string) {
} else { } else {
reading = reading && evt == EvtReadNew reading = reading && evt == EvtReadNew
} }
snapshot, count := chunkList.Snapshot() if useSnapshot && evt == EvtReadFin {
useSnapshot = false
}
if !useSnapshot {
snapshot, count = chunkList.Snapshot()
snapshotRevision = inputRevision
}
total = count total = count
terminal.UpdateCount(total, !reading, value.(*string)) terminal.UpdateCount(total, !reading, value.(*string))
if opts.Sync { if opts.Sync {
opts.Sync = false opts.Sync = false
terminal.UpdateList(PassMerger(&snapshot, opts.Tac), false) terminal.UpdateList(PassMerger(&snapshot, opts.Tac, snapshotRevision))
} }
if heightUnknown && !deferred { if heightUnknown && !deferred {
determine(!reading) determine(!reading)
} }
reset := clearCache() matcher.Reset(snapshot, input(), false, !reading, sort, snapshotRevision)
matcher.Reset(snapshot, input(reset), false, !reading, sort, reset)
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 {
useSnapshot = val.sync
}
} }
if command != nil { if command != nil {
if reading { if reading {
@@ -294,11 +309,20 @@ func Run(opts *Options, version string, revision string) {
} else { } else {
restart(*command) restart(*command)
} }
}
if !changed {
break break
} }
snapshot, _ := chunkList.Snapshot() if !useSnapshot {
reset := clearCache() newSnapshot, _ := chunkList.Snapshot()
matcher.Reset(snapshot, input(reset), true, !reading, sort, reset) // 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
snapshotRevision = inputRevision
}
}
matcher.Reset(snapshot, input(), true, !reading, sort, snapshotRevision)
delay = false delay = false
case EvtSearchProgress: case EvtSearchProgress:
@@ -338,7 +362,7 @@ func Run(opts *Options, version string, revision string) {
determine(val.final) determine(val.final)
} }
} }
terminal.UpdateList(val, clearSelection()) terminal.UpdateList(val)
} }
} }
} }

View File

@@ -16,7 +16,7 @@ type MatchRequest struct {
pattern *Pattern pattern *Pattern
final bool final bool
sort bool sort bool
clearCache bool revision int
} }
// Matcher is responsible for performing search // Matcher is responsible for performing search
@@ -29,6 +29,7 @@ type Matcher struct {
partitions int partitions int
slab []*util.Slab slab []*util.Slab
mergerCache map[string]*Merger mergerCache map[string]*Merger
revision int
} }
const ( const (
@@ -38,7 +39,7 @@ const (
// NewMatcher returns a new Matcher // NewMatcher returns a new Matcher
func NewMatcher(patternBuilder func([]rune) *Pattern, func NewMatcher(patternBuilder func([]rune) *Pattern,
sort bool, tac bool, eventBox *util.EventBox) *Matcher { sort bool, tac bool, eventBox *util.EventBox, revision int) *Matcher {
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions) partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
return &Matcher{ return &Matcher{
patternBuilder: patternBuilder, patternBuilder: patternBuilder,
@@ -48,7 +49,8 @@ func NewMatcher(patternBuilder func([]rune) *Pattern,
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
partitions: partitions, partitions: partitions,
slab: make([]*util.Slab, partitions), slab: make([]*util.Slab, partitions),
mergerCache: make(map[string]*Merger)} mergerCache: make(map[string]*Merger),
revision: revision}
} }
// Loop puts Matcher in action // Loop puts Matcher in action
@@ -70,8 +72,9 @@ func (m *Matcher) Loop() {
events.Clear() events.Clear()
}) })
if request.sort != m.sort || request.clearCache { if request.sort != m.sort || request.revision != m.revision {
m.sort = request.sort m.sort = request.sort
m.revision = request.revision
m.mergerCache = make(map[string]*Merger) m.mergerCache = make(map[string]*Merger)
clearChunkCache() clearChunkCache()
} }
@@ -140,11 +143,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
numChunks := len(request.chunks) numChunks := len(request.chunks)
if numChunks == 0 { if numChunks == 0 {
return EmptyMerger, false return EmptyMerger(request.revision), false
} }
pattern := request.pattern pattern := request.pattern
if pattern.IsEmpty() { if pattern.IsEmpty() {
return PassMerger(&request.chunks, m.tac), false return PassMerger(&request.chunks, m.tac, request.revision), false
} }
cancelled := util.NewAtomicBool(false) cancelled := util.NewAtomicBool(false)
@@ -218,11 +221,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
partialResult := <-resultChan partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches partialResults[partialResult.index] = partialResult.matches
} }
return NewMerger(pattern, partialResults, m.sort, m.tac), false return NewMerger(pattern, partialResults, m.sort, m.tac, request.revision), false
} }
// Reset is called to interrupt/signal the ongoing search // Reset is called to interrupt/signal the ongoing search
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, clearCache bool) { func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, revision int) {
pattern := m.patternBuilder(patternRunes) pattern := m.patternBuilder(patternRunes)
var event util.EventType var event util.EventType
@@ -231,5 +234,5 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
} else { } else {
event = reqRetry event = reqRetry
} }
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, clearCache}) m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, revision})
} }

View File

@@ -3,7 +3,9 @@ package fzf
import "fmt" import "fmt"
// EmptyMerger is a Merger with no data // EmptyMerger is a Merger with no data
var EmptyMerger = NewMerger(nil, [][]Result{}, false, false) func EmptyMerger(revision int) *Merger {
return NewMerger(nil, [][]Result{}, false, false, revision)
}
// Merger holds a set of locally sorted lists of items and provides the view of // Merger holds a set of locally sorted lists of items and provides the view of
// a single, globally-sorted list // a single, globally-sorted list
@@ -17,16 +19,20 @@ type Merger struct {
tac bool tac bool
final bool final bool
count int count int
pass bool
revision int
} }
// PassMerger returns a new Merger that simply returns the items in the // PassMerger returns a new Merger that simply returns the items in the
// original order // original order
func PassMerger(chunks *[]*Chunk, tac bool) *Merger { func PassMerger(chunks *[]*Chunk, tac bool, revision int) *Merger {
mg := Merger{ mg := Merger{
pattern: nil, pattern: nil,
chunks: chunks, chunks: chunks,
tac: tac, tac: tac,
count: 0} count: 0,
pass: true,
revision: revision}
for _, chunk := range *mg.chunks { for _, chunk := range *mg.chunks {
mg.count += chunk.count mg.count += chunk.count
@@ -35,7 +41,7 @@ func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
} }
// NewMerger returns a new Merger // NewMerger returns a new Merger
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merger { func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision int) *Merger {
mg := Merger{ mg := Merger{
pattern: pattern, pattern: pattern,
lists: lists, lists: lists,
@@ -45,7 +51,8 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merge
sorted: sorted, sorted: sorted,
tac: tac, tac: tac,
final: false, final: false,
count: 0} count: 0,
revision: revision}
for _, list := range mg.lists { for _, list := range mg.lists {
mg.count += len(list) mg.count += len(list)
@@ -53,11 +60,42 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merge
return &mg return &mg
} }
// Revision returns revision number
func (mg *Merger) Revision() int {
return mg.revision
}
// Length returns the number of items // Length returns the number of items
func (mg *Merger) Length() int { 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

@@ -23,10 +23,10 @@ func randResult() Result {
} }
func TestEmptyMerger(t *testing.T) { func TestEmptyMerger(t *testing.T) {
assert(t, EmptyMerger.Length() == 0, "Not empty") assert(t, EmptyMerger(0).Length() == 0, "Not empty")
assert(t, EmptyMerger.count == 0, "Invalid count") assert(t, EmptyMerger(0).count == 0, "Invalid count")
assert(t, len(EmptyMerger.lists) == 0, "Invalid lists") assert(t, len(EmptyMerger(0).lists) == 0, "Invalid lists")
assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list") assert(t, len(EmptyMerger(0).merged) == 0, "Invalid merged list")
} }
func buildLists(partiallySorted bool) ([][]Result, []Result) { func buildLists(partiallySorted bool) ([][]Result, []Result) {
@@ -57,7 +57,7 @@ func TestMergerUnsorted(t *testing.T) {
cnt := len(items) cnt := len(items)
// Not sorted: same order // Not sorted: same order
mg := NewMerger(nil, lists, false, false) mg := NewMerger(nil, lists, false, false, 0)
assert(t, cnt == mg.Length(), "Invalid Length") assert(t, cnt == mg.Length(), "Invalid Length")
for i := 0; i < cnt; i++ { for i := 0; i < cnt; i++ {
assert(t, items[i] == mg.Get(i), "Invalid Get") assert(t, items[i] == mg.Get(i), "Invalid Get")
@@ -69,7 +69,7 @@ func TestMergerSorted(t *testing.T) {
cnt := len(items) cnt := len(items)
// Sorted sorted order // Sorted sorted order
mg := NewMerger(nil, lists, true, false) mg := NewMerger(nil, lists, true, false, 0)
assert(t, cnt == mg.Length(), "Invalid Length") assert(t, cnt == mg.Length(), "Invalid Length")
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
for i := 0; i < cnt; i++ { for i := 0; i < cnt; i++ {
@@ -79,7 +79,7 @@ func TestMergerSorted(t *testing.T) {
} }
// Inverse order // Inverse order
mg2 := NewMerger(nil, lists, true, false) mg2 := NewMerger(nil, lists, true, false, 0)
for i := cnt - 1; i >= 0; i-- { for i := cnt - 1; i >= 0; i-- {
if items[i] != mg2.Get(i) { if items[i] != mg2.Get(i) {
t.Error("Not sorted", items[i], mg2.Get(i)) t.Error("Not sorted", items[i], mg2.Get(i))

View File

@@ -10,6 +10,7 @@ import (
"github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"github.com/mattn/go-shellwords" "github.com/mattn/go-shellwords"
@@ -32,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
@@ -61,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|thinblock|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
@@ -70,9 +72,12 @@ const usage = `usage: fzf [options]
(default: 0 or center) (default: 0 or center)
--margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L) --margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L)
--padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L) --padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
--info=STYLE Finder info style [default|inline|hidden] --info=STYLE Finder info style
[default|right|hidden|inline[:SEPARATOR]|inline-right]
--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[=C1[C2]] Scrollbar character(s) (each for main and preview window)
--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: '>')
--marker=STR Multi-select marker (default: '>') --marker=STR Multi-select marker (default: '>')
@@ -113,6 +118,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 /)
--version Display version information and exit --version Display version information and exit
Environment variables Environment variables
@@ -122,6 +128,8 @@ const usage = `usage: fzf [options]
` `
const defaultInfoSep = " < "
// Case denotes case-sensitivity of search // Case denotes case-sensitivity of search
type Case int type Case int
@@ -158,6 +166,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 (
@@ -179,10 +195,16 @@ type infoStyle int
const ( const (
infoDefault infoStyle = iota infoDefault infoStyle = iota
infoRight
infoInline infoInline
infoInlineRight
infoHidden infoHidden
) )
func (s infoStyle) noExtraLine() bool {
return s == infoInline || s == infoInlineRight || s == infoHidden
}
type labelOpts struct { type labelOpts struct {
label string label string
column int column int
@@ -204,6 +226,14 @@ type previewOpts struct {
alternative *previewOpts alternative *previewOpts
} }
func (o *previewOpts) Visible() bool {
return o.size.size > 0 || o.alternative != nil && o.alternative.size.size > 0
}
func (o *previewOpts) Toggle() {
o.hidden = !o.hidden
}
func parseLabelPosition(opts *labelOpts, arg string) { func parseLabelPosition(opts *labelOpts, arg string) {
opts.column = 0 opts.column = 0
opts.bottom = false opts.bottom = false
@@ -235,6 +265,10 @@ func (a previewOpts) sameContentLayout(b previewOpts) bool {
return a.wrap == b.wrap && a.headerLines == b.headerLines return a.wrap == b.wrap && a.headerLines == b.headerLines
} }
func firstLine(s string) string {
return strings.SplitN(s, "\n", 2)[0]
}
// Options stores the values of command-line options // Options stores the values of command-line options
type Options struct { type Options struct {
Fuzzy bool Fuzzy bool
@@ -248,6 +282,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
@@ -266,6 +301,7 @@ type Options struct {
ScrollOff int ScrollOff int
FileWord bool FileWord bool
InfoStyle infoStyle InfoStyle infoStyle
InfoSep string
Separator *string Separator *string
JumpLabels string JumpLabels string
Prompt string Prompt string
@@ -289,6 +325,7 @@ type Options struct {
HeaderLines int HeaderLines int
HeaderFirst bool HeaderFirst bool
Ellipsis string Ellipsis string
Scrollbar *string
Margin [4]sizeSpec Margin [4]sizeSpec
Padding [4]sizeSpec Padding [4]sizeSpec
BorderShape tui.BorderShape BorderShape tui.BorderShape
@@ -296,12 +333,13 @@ type Options struct {
PreviewLabel labelOpts PreviewLabel labelOpts
Unicode bool Unicode bool
Tabstop int Tabstop int
ListenPort *int
ClearOnExit bool ClearOnExit bool
Version bool Version bool
} }
func defaultPreviewOpts(command string) previewOpts { func defaultPreviewOpts(command string) previewOpts {
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded, 0, 0, nil} return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.DefaultBorderShape, 0, 0, nil}
} }
func defaultOptions() *Options { func defaultOptions() *Options {
@@ -317,6 +355,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,
@@ -357,6 +396,7 @@ func defaultOptions() *Options {
HeaderLines: 0, HeaderLines: 0,
HeaderFirst: false, HeaderFirst: false,
Ellipsis: "..", Ellipsis: "..",
Scrollbar: nil,
Margin: defaultMargin(), Margin: defaultMargin(),
Padding: defaultMargin(), Padding: defaultMargin(),
Unicode: true, Unicode: true,
@@ -511,6 +551,10 @@ 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 "thinblock":
return tui.BorderThinBlock
case "double": case "double":
return tui.BorderDouble return tui.BorderDouble
case "horizontal": case "horizontal":
@@ -529,16 +573,21 @@ func parseBorder(str string, optional bool) tui.BorderShape {
return tui.BorderNone return tui.BorderNone
default: default:
if optional && str == "" { if optional && str == "" {
return tui.BorderRounded 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|thinblock|double|horizontal|vertical|top|bottom|left|right|none)")
} }
return tui.BorderNone return tui.BorderNone
} }
func parseKeyChords(str string, message string) map[tui.Event]string { func parseKeyChords(str string, message string) map[tui.Event]string {
return parseKeyChordsImpl(str, message, errorExit)
}
func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.Event]string {
if len(str) == 0 { if len(str) == 0 {
errorExit(message) exit(message)
return nil
} }
str = regexp.MustCompile("(?i)(alt-),").ReplaceAllString(str, "$1"+string([]rune{escapedComma})) str = regexp.MustCompile("(?i)(alt-),").ReplaceAllString(str, "$1"+string([]rune{escapedComma}))
@@ -574,6 +623,8 @@ func parseKeyChords(str string, message string) map[tui.Event]string {
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-_":
@@ -588,6 +639,14 @@ func parseKeyChords(str string, message string) map[tui.Event]string {
add(tui.BackwardEOF) add(tui.BackwardEOF)
case "start": case "start":
add(tui.Start) add(tui.Start)
case "load":
add(tui.Load)
case "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":
@@ -636,6 +695,8 @@ func parseKeyChords(str string, message string) map[tui.Event]string {
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":
@@ -670,7 +731,8 @@ func parseKeyChords(str string, message string) map[tui.Event]string {
} else if len(runes) == 1 { } else if len(runes) == 1 {
chords[tui.Key(runes[0])] = key chords[tui.Key(runes[0])] = key
} else { } else {
errorExit("unsupported key: " + key) exit("unsupported key: " + key)
return nil
} }
} }
} }
@@ -841,10 +903,18 @@ 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":
mergeAttr(&theme.Scrollbar)
case "preview-scrollbar":
mergeAttr(&theme.PreviewScrollbar)
case "label": case "label":
mergeAttr(&theme.BorderLabel) mergeAttr(&theme.BorderLabel)
case "preview-label":
mergeAttr(&theme.PreviewLabel)
case "prompt": case "prompt":
mergeAttr(&theme.Prompt) mergeAttr(&theme.Prompt)
case "spinner": case "spinner":
@@ -868,6 +938,7 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
var ( var (
executeRegexp *regexp.Regexp executeRegexp *regexp.Regexp
splitRegexp *regexp.Regexp splitRegexp *regexp.Regexp
actionNameRegexp *regexp.Regexp
) )
func firstKey(keymap map[tui.Event]string) tui.Event { func firstKey(keymap map[tui.Event]string) tui.Event {
@@ -884,79 +955,88 @@ const (
) )
func init() { func init() {
// Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
executeRegexp = regexp.MustCompile( executeRegexp = regexp.MustCompile(
`(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|(?:re|un)bind):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|(?:re|un)bind)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`) `(?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-]+")
} }
func parseKeymap(keymap map[tui.Event][]*action, str string) { func maskActionContents(action string) string {
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { masked := ""
symbol := ":" Loop:
if strings.HasPrefix(src, "+") { for len(action) > 0 {
symbol = "+" loc := executeRegexp.FindStringIndex(action)
if loc == nil {
masked += action
break
} }
prefix := symbol + "execute" masked += action[:loc[1]]
if strings.HasPrefix(src[1:], "reload") { action = action[loc[1]:]
prefix = symbol + "reload" if len(action) == 0 {
} else if strings.HasPrefix(src[1:], "change-preview-window") { break
prefix = symbol + "change-preview-window"
} else if strings.HasPrefix(src[1:], "change-preview") {
prefix = symbol + "change-preview"
} else if strings.HasPrefix(src[1:], "preview") {
prefix = symbol + "preview"
} else if strings.HasPrefix(src[1:], "unbind") {
prefix = symbol + "unbind"
} else if strings.HasPrefix(src[1:], "rebind") {
prefix = symbol + "rebind"
} else if strings.HasPrefix(src[1:], "change-prompt") {
prefix = symbol + "change-prompt"
} else if src[len(prefix)] == '-' {
c := src[len(prefix)+1]
if c == 's' || c == 'S' {
prefix += "-silent"
} else {
prefix += "-multi"
} }
cs := string(action[0])
ce := ")"
switch action[0] {
case ':':
masked += strings.Repeat(" ", len(action))
break Loop
case '(':
ce = ")"
case '{':
ce = "}"
case '[':
ce = "]"
case '<':
ce = ">"
case '~', '!', '@', '#', '$', '%', '^', '&', '*', ';', '/', '|':
ce = string(cs)
default:
continue
}
cs = regexp.QuoteMeta(cs)
ce = regexp.QuoteMeta(ce)
// @$ or @+
loc = regexp.MustCompile(fmt.Sprintf(`(?s)^%s.*?(%s[+,]|%s$)`, cs, ce, ce)).FindStringIndex(action)
if loc == nil {
masked += action
break
}
// Keep + or , at the end
lastChar := action[loc[1]-1]
if lastChar == '+' || lastChar == ',' {
loc[1]--
}
masked += strings.Repeat(" ", loc[1])
action = action[loc[1]:]
} }
return prefix + "(" + strings.Repeat(" ", len(src)-len(prefix)-2) + ")"
})
masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1) masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1)
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1) masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1)
masked = strings.Replace(masked, "+:", string([]rune{escapedPlus, ':'}), -1) masked = strings.Replace(masked, "+:", string([]rune{escapedPlus, ':'}), -1)
return masked
}
func parseSingleActionList(str string, exit func(string)) []*action {
// We prepend a colon to satisfy executeRegexp and remove it later
masked := maskActionContents(":" + str)[1:]
return parseActionList(masked, str, []*action{}, false, exit)
}
func parseActionList(masked string, original string, prevActions []*action, putAllowed bool, exit func(string)) []*action {
maskedStrings := strings.Split(masked, "+")
originalStrings := make([]string, len(maskedStrings))
idx := 0 idx := 0
for _, pairStr := range strings.Split(masked, ",") { for i, maskedString := range maskedStrings {
origPairStr := str[idx : idx+len(pairStr)] originalStrings[i] = original[idx : idx+len(maskedString)]
idx += len(pairStr) + 1 idx += len(maskedString) + 1
pair := strings.SplitN(pairStr, ":", 2)
if len(pair) < 2 {
errorExit("bind action not specified: " + origPairStr)
} }
var key tui.Event actions := make([]*action, 0, len(maskedStrings))
if len(pair[0]) == 1 && pair[0][0] == escapedColon {
key = tui.Key(':')
} else if len(pair[0]) == 1 && pair[0][0] == escapedComma {
key = tui.Key(',')
} else if len(pair[0]) == 1 && pair[0][0] == escapedPlus {
key = tui.Key('+')
} else {
keys := parseKeyChords(pair[0], "key name required")
key = firstKey(keys)
}
idx2 := len(pair[0]) + 1
specs := strings.Split(pair[1], "+")
actions := make([]*action, 0, len(specs))
appendAction := func(types ...actionType) { appendAction := func(types ...actionType) {
actions = append(actions, toActions(types...)...) actions = append(actions, toActions(types...)...)
} }
prevSpec := "" prevSpec := ""
for specIndex, maskedSpec := range specs { for specIndex, spec := range originalStrings {
spec := origPairStr[idx2 : idx2+len(maskedSpec)]
idx2 += len(maskedSpec) + 1
spec = prevSpec + spec spec = prevSpec + spec
specLower := strings.ToLower(spec) specLower := strings.ToLower(spec)
switch specLower { switch specLower {
@@ -1032,6 +1112,10 @@ func parseKeymap(keymap map[tui.Event][]*action, str string) {
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":
@@ -1058,10 +1142,18 @@ func parseKeymap(keymap map[tui.Event][]*action, str string) {
appendAction(actHalfPageUp) appendAction(actHalfPageUp)
case "half-page-down": case "half-page-down":
appendAction(actHalfPageDown) appendAction(actHalfPageDown)
case "previous-history": case "prev-history", "previous-history":
appendAction(actPreviousHistory) appendAction(actPrevHistory)
case "next-history": case "next-history":
appendAction(actNextHistory) appendAction(actNextHistory)
case "prev-selected":
appendAction(actPrevSelected)
case "next-selected":
appendAction(actNextSelected)
case "show-preview":
appendAction(actShowPreview)
case "hide-preview":
appendAction(actHidePreview)
case "toggle-preview": case "toggle-preview":
appendAction(actTogglePreview) appendAction(actTogglePreview)
case "toggle-preview-wrap": case "toggle-preview-wrap":
@@ -1089,46 +1181,24 @@ func parseKeymap(keymap map[tui.Event][]*action, str string) {
case "disable-search": case "disable-search":
appendAction(actDisableSearch) appendAction(actDisableSearch)
case "put": case "put":
if key.Type == tui.Rune && unicode.IsGraphic(key.Char) { if putAllowed {
appendAction(actRune) appendAction(actRune)
} else { } else {
errorExit("unable to put non-printable character: " + pair[0]) exit("unable to put non-printable character")
} }
default: default:
t := isExecuteAction(specLower) t := isExecuteAction(specLower)
if t == actIgnore { if t == actIgnore {
if specIndex == 0 && specLower == "" { if specIndex == 0 && specLower == "" {
actions = append(keymap[key], actions...) actions = append(prevActions, actions...)
} else { } else {
errorExit("unknown action: " + spec) exit("unknown action: " + spec)
} }
} else { } else {
var offset int offset := len(actionNameRegexp.FindString(spec))
switch t {
case actReload:
offset = len("reload")
case actPreview:
offset = len("preview")
case actChangePreviewWindow:
offset = len("change-preview-window")
case actChangePreview:
offset = len("change-preview")
case actChangePrompt:
offset = len("change-prompt")
case actUnbind:
offset = len("unbind")
case actRebind:
offset = len("rebind")
case actExecuteSilent:
offset = len("execute-silent")
case actExecuteMulti:
offset = len("execute-multi")
default:
offset = len("execute")
}
var actionArg string var actionArg string
if spec[offset] == ':' { if spec[offset] == ':' {
if specIndex == len(specs)-1 { if specIndex == len(originalStrings)-1 {
actionArg = spec[offset+1:] actionArg = spec[offset+1:]
actions = append(actions, &action{t: t, a: actionArg}) actions = append(actions, &action{t: t, a: actionArg})
} else { } else {
@@ -1139,47 +1209,109 @@ func parseKeymap(keymap map[tui.Event][]*action, str string) {
actionArg = spec[offset+1 : len(spec)-1] actionArg = spec[offset+1 : len(spec)-1]
actions = append(actions, &action{t: t, a: actionArg}) actions = append(actions, &action{t: t, a: actionArg})
} }
if t == actUnbind || t == actRebind { switch t {
parseKeyChords(actionArg, spec[0:offset]+" target required") case actBecome:
if util.IsWindows() {
exit("become action is not supported on Windows")
}
case actUnbind, actRebind:
parseKeyChordsImpl(actionArg, spec[0:offset]+" target required", exit)
case actChangePreviewWindow:
opts := previewOpts{}
for _, arg := range strings.Split(actionArg, "|") {
// Make sure that each expression is valid
parsePreviewWindowImpl(&opts, arg, exit)
}
} }
} }
} }
prevSpec = "" prevSpec = ""
} }
keymap[key] = actions return actions
}
func parseKeymap(keymap map[tui.Event][]*action, str string, exit func(string)) {
masked := maskActionContents(str)
idx := 0
for _, pairStr := range strings.Split(masked, ",") {
origPairStr := str[idx : idx+len(pairStr)]
idx += len(pairStr) + 1
pair := strings.SplitN(pairStr, ":", 2)
if len(pair) < 2 {
exit("bind action not specified: " + origPairStr)
}
var key tui.Event
if len(pair[0]) == 1 && pair[0][0] == escapedColon {
key = tui.Key(':')
} else if len(pair[0]) == 1 && pair[0][0] == escapedComma {
key = tui.Key(',')
} else if len(pair[0]) == 1 && pair[0][0] == escapedPlus {
key = tui.Key('+')
} else {
keys := parseKeyChordsImpl(pair[0], "key name required", exit)
key = firstKey(keys)
}
putAllowed := key.Type == tui.Rune && unicode.IsGraphic(key.Char)
keymap[key] = parseActionList(pair[1], origPairStr[len(pair[0])+1:], keymap[key], putAllowed, exit)
} }
} }
func isExecuteAction(str string) actionType { func isExecuteAction(str string) actionType {
matches := executeRegexp.FindAllStringSubmatch(":"+str, -1) masked := maskActionContents(":" + str)[1:]
if matches == nil || len(matches) != 1 { if masked == str {
// Not masked
return actIgnore return actIgnore
} }
prefix := matches[0][1]
if len(prefix) == 0 { prefix := actionNameRegexp.FindString(str)
prefix = matches[0][2]
}
switch prefix { switch prefix {
case "become":
return actBecome
case "reload": case "reload":
return actReload return actReload
case "reload-sync":
return actReloadSync
case "unbind": case "unbind":
return actUnbind return actUnbind
case "rebind": case "rebind":
return actRebind return actRebind
case "preview": case "preview":
return actPreview return actPreview
case "change-border-label":
return actChangeBorderLabel
case "change-header":
return actChangeHeader
case "change-preview-label":
return actChangePreviewLabel
case "change-preview-window": case "change-preview-window":
return actChangePreviewWindow return actChangePreviewWindow
case "change-preview": case "change-preview":
return actChangePreview return actChangePreview
case "change-prompt": case "change-prompt":
return actChangePrompt return actChangePrompt
case "change-query":
return actChangeQuery
case "pos":
return actPosition
case "execute": case "execute":
return actExecute return actExecute
case "execute-silent": case "execute-silent":
return actExecuteSilent return actExecuteSilent
case "execute-multi": case "execute-multi":
return actExecuteMulti return actExecuteMulti
case "put":
return actPut
case "transform-border-label":
return actTransformBorderLabel
case "transform-preview-label":
return actTransformPreviewLabel
case "transform-header":
return actTransformHeader
case "transform-prompt":
return actTransformPrompt
case "transform-query":
return actTransformQuery
} }
return actIgnore return actIgnore
} }
@@ -1247,21 +1379,33 @@ func parseLayout(str string) layoutType {
return layoutDefault return layoutDefault
} }
func parseInfoStyle(str string) infoStyle { func parseInfoStyle(str string) (infoStyle, string) {
switch str { switch str {
case "default": case "default":
return infoDefault return infoDefault, ""
case "right":
return infoRight, ""
case "inline": case "inline":
return infoInline return infoInline, defaultInfoSep
case "inline-right":
return infoInlineRight, ""
case "hidden": case "hidden":
return infoHidden return infoHidden, ""
default: default:
errorExit("invalid info style (expected: default|inline|hidden)") prefix := "inline:"
if strings.HasPrefix(str, prefix) {
return infoInline, strings.ReplaceAll(str[len(prefix):], "\n", " ")
} }
return infoDefault errorExit("invalid info style (expected: default|right|hidden|inline[:SEPARATOR]|inline-right)")
}
return infoDefault, ""
} }
func parsePreviewWindow(opts *previewOpts, input string) { func parsePreviewWindow(opts *previewOpts, input string) {
parsePreviewWindowImpl(opts, input, errorExit)
}
func parsePreviewWindowImpl(opts *previewOpts, input string, exit func(string)) {
tokenRegex := regexp.MustCompile(`[:,]*(<([1-9][0-9]*)\(([^)<]+)\)|[^,:]+)`) tokenRegex := regexp.MustCompile(`[:,]*(<([1-9][0-9]*)\(([^)<]+)\)|[^,:]+)`)
sizeRegex := regexp.MustCompile("^[0-9]+%?$") sizeRegex := regexp.MustCompile("^[0-9]+%?$")
offsetRegex := regexp.MustCompile(`^(\+{-?[0-9]+})?([+-][0-9]+)*(-?/[1-9][0-9]*)?$`) offsetRegex := regexp.MustCompile(`^(\+{-?[0-9]+})?([+-][0-9]+)*(-?/[1-9][0-9]*)?$`)
@@ -1305,6 +1449,10 @@ func parsePreviewWindow(opts *previewOpts, input 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-thinblock":
opts.border = tui.BorderThinBlock
case "border-double": case "border-double":
opts.border = tui.BorderDouble opts.border = tui.BorderDouble
case "noborder", "border-none": case "noborder", "border-none":
@@ -1333,7 +1481,8 @@ func parsePreviewWindow(opts *previewOpts, input string) {
} else if offsetRegex.MatchString(token) { } else if offsetRegex.MatchString(token) {
opts.scroll = token opts.scroll = token
} else { } else {
errorExit("invalid preview window option: " + token) exit("invalid preview window option: " + token)
return
} }
} }
} }
@@ -1342,7 +1491,7 @@ func parsePreviewWindow(opts *previewOpts, input string) {
opts.alternative = &alternativeOpts opts.alternative = &alternativeOpts
opts.alternative.hidden = false opts.alternative.hidden = false
opts.alternative.alternative = nil opts.alternative.alternative = nil
parsePreviewWindow(opts.alternative, alternative) parsePreviewWindowImpl(opts.alternative, alternative, exit)
} }
} }
@@ -1443,7 +1592,7 @@ func parseOptions(opts *Options, allArgs []string) {
case "--tiebreak": case "--tiebreak":
opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
case "--bind": case "--bind":
parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required")) parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required"), errorExit)
case "--color": case "--color":
_, spec := optionalNextString(allArgs, &i) _, spec := optionalNextString(allArgs, &i)
if len(spec) == 0 { if len(spec) == 0 {
@@ -1463,6 +1612,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":
@@ -1521,12 +1674,13 @@ func parseOptions(opts *Options, allArgs []string) {
case "--no-filepath-word": case "--no-filepath-word":
opts.FileWord = false opts.FileWord = false
case "--info": case "--info":
opts.InfoStyle = parseInfoStyle( opts.InfoStyle, opts.InfoSep = parseInfoStyle(
nextString(allArgs, &i, "info style required")) nextString(allArgs, &i, "info style required"))
case "--no-info": case "--no-info":
opts.InfoStyle = infoHidden opts.InfoStyle = infoHidden
case "--inline-info": case "--inline-info":
opts.InfoStyle = infoInline opts.InfoStyle = infoInline
opts.InfoSep = defaultInfoSep
case "--no-inline-info": case "--no-inline-info":
opts.InfoStyle = infoDefault opts.InfoStyle = infoDefault
case "--separator": case "--separator":
@@ -1535,6 +1689,16 @@ func parseOptions(opts *Options, allArgs []string) {
case "--no-separator": case "--no-separator":
nosep := "" nosep := ""
opts.Separator = &nosep opts.Separator = &nosep
case "--scrollbar":
given, bar := optionalNextString(allArgs, &i)
if given {
opts.Scrollbar = &bar
} else {
opts.Scrollbar = nil
}
case "--no-scrollbar":
noBar := ""
opts.Scrollbar = &noBar
case "--jump-labels": case "--jump-labels":
opts.JumpLabels = nextString(allArgs, &i, "label characters required") opts.JumpLabels = nextString(allArgs, &i, "label characters required")
validateJumpLabels = true validateJumpLabels = true
@@ -1563,10 +1727,10 @@ func parseOptions(opts *Options, allArgs []string) {
case "--prompt": case "--prompt":
opts.Prompt = nextString(allArgs, &i, "prompt string required") opts.Prompt = nextString(allArgs, &i, "prompt string required")
case "--pointer": case "--pointer":
opts.Pointer = nextString(allArgs, &i, "pointer sign string required") opts.Pointer = firstLine(nextString(allArgs, &i, "pointer sign string required"))
validatePointer = true validatePointer = true
case "--marker": case "--marker":
opts.Marker = nextString(allArgs, &i, "selected sign string required") opts.Marker = firstLine(nextString(allArgs, &i, "selected sign string required"))
validateMarker = true validateMarker = true
case "--sync": case "--sync":
opts.Sync = true opts.Sync = true
@@ -1645,6 +1809,11 @@ func parseOptions(opts *Options, allArgs []string) {
nextString(allArgs, &i, "padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)")) nextString(allArgs, &i, "padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
case "--tabstop": case "--tabstop":
opts.Tabstop = nextInt(allArgs, &i, "tab stop required") opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
case "--listen":
port := optionalNumeric(allArgs, &i, 0)
opts.ListenPort = &port
case "--no-listen":
opts.ListenPort = nil
case "--clear": case "--clear":
opts.ClearOnExit = true opts.ClearOnExit = true
case "--no-clear": case "--no-clear":
@@ -1677,10 +1846,10 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--prompt="); match { } else if match, value := optString(arg, "--prompt="); match {
opts.Prompt = value opts.Prompt = value
} else if match, value := optString(arg, "--pointer="); match { } else if match, value := optString(arg, "--pointer="); match {
opts.Pointer = value opts.Pointer = firstLine(value)
validatePointer = true validatePointer = true
} else if match, value := optString(arg, "--marker="); match { } else if match, value := optString(arg, "--marker="); match {
opts.Marker = value opts.Marker = firstLine(value)
validateMarker = true validateMarker = true
} else if match, value := optString(arg, "-n", "--nth="); match { } else if match, value := optString(arg, "-n", "--nth="); match {
opts.Nth = splitNth(value) opts.Nth = splitNth(value)
@@ -1697,9 +1866,11 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--layout="); match { } else if match, value := optString(arg, "--layout="); match {
opts.Layout = parseLayout(value) opts.Layout = parseLayout(value)
} else if match, value := optString(arg, "--info="); match { } else if match, value := optString(arg, "--info="); match {
opts.InfoStyle = parseInfoStyle(value) opts.InfoStyle, opts.InfoSep = parseInfoStyle(value)
} else if match, value := optString(arg, "--separator="); match { } else if match, value := optString(arg, "--separator="); match {
opts.Separator = &value opts.Separator = &value
} else if match, value := optString(arg, "--scrollbar="); match {
opts.Scrollbar = &value
} else if match, value := optString(arg, "--toggle-sort="); match { } else if match, value := optString(arg, "--toggle-sort="); match {
parseToggleSort(opts.Keymap, value) parseToggleSort(opts.Keymap, value)
} else if match, value := optString(arg, "--expect="); match { } else if match, value := optString(arg, "--expect="); match {
@@ -1711,7 +1882,7 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--color="); match { } else if match, value := optString(arg, "--color="); match {
opts.Theme = parseTheme(opts.Theme, value) opts.Theme = parseTheme(opts.Theme, value)
} else if match, value := optString(arg, "--bind="); match { } else if match, value := optString(arg, "--bind="); match {
parseKeymap(opts.Keymap, value) parseKeymap(opts.Keymap, value, errorExit)
} else if match, value := optString(arg, "--history="); match { } else if match, value := optString(arg, "--history="); match {
setHistory(value) setHistory(value)
} else if match, value := optString(arg, "--history-size="); match { } else if match, value := optString(arg, "--history-size="); match {
@@ -1732,6 +1903,9 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Padding = parseMargin("padding", value) opts.Padding = parseMargin("padding", value)
} 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 {
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 {
@@ -1761,6 +1935,10 @@ 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 != nil && (*opts.ListenPort < 0 || *opts.ListenPort > 65535) {
errorExit("invalid listen port")
}
if len(opts.JumpLabels) == 0 { if len(opts.JumpLabels) == 0 {
errorExit("empty jump labels") errorExit("empty jump labels")
} }
@@ -1800,10 +1978,23 @@ func postProcessOptions(opts *Options) {
if !opts.Version && !tui.IsLightRendererSupported() && opts.Height.size > 0 { if !opts.Version && !tui.IsLightRendererSupported() && opts.Height.size > 0 {
errorExit("--height option is currently not supported on this platform") errorExit("--height option is currently not supported on this platform")
} }
if opts.Scrollbar != nil {
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
if opts.History != nil { if opts.History != nil {
if _, prs := opts.Keymap[tui.CtrlP.AsEvent()]; !prs { if _, prs := opts.Keymap[tui.CtrlP.AsEvent()]; !prs {
opts.Keymap[tui.CtrlP.AsEvent()] = toActions(actPreviousHistory) opts.Keymap[tui.CtrlP.AsEvent()] = toActions(actPrevHistory)
} }
if _, prs := opts.Keymap[tui.CtrlN.AsEvent()]; !prs { if _, prs := opts.Keymap[tui.CtrlN.AsEvent()]; !prs {
opts.Keymap[tui.CtrlN.AsEvent()] = toActions(actNextHistory) opts.Keymap[tui.CtrlN.AsEvent()] = toActions(actNextHistory)
@@ -1811,37 +2002,27 @@ func postProcessOptions(opts *Options) {
} }
// Extend the default key map // Extend the default key map
previewEnabled := len(opts.Preview.command) > 0 || hasPreviewAction(opts)
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)
if !previewEnabled {
// Doesn't matter
continue
}
opts := previewOpts{}
for _, arg := range strings.Split(act.a, "|") {
// Make sure that each expression is valid
parsePreviewWindow(&opts, arg)
}
} }
} }
// 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)
} }
} }
@@ -1851,6 +2032,11 @@ func postProcessOptions(opts *Options) {
} }
opts.Keymap = keymap opts.Keymap = keymap
// If 'double-click' is left unbound, bind it to the action bound to 'enter'
if _, prs := opts.Keymap[tui.DoubleClick.AsEvent()]; !prs {
opts.Keymap[tui.DoubleClick.AsEvent()] = opts.Keymap[tui.CtrlM.AsEvent()]
}
if opts.Height.auto { if opts.Height.auto {
for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2]} { for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2]} {
if s.percent { if s.percent {
@@ -1879,9 +2065,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

@@ -262,13 +262,17 @@ func TestBind(t *testing.T) {
} }
} }
check(tui.CtrlA.AsEvent(), "", actBeginningOfLine) check(tui.CtrlA.AsEvent(), "", actBeginningOfLine)
errorString := ""
errorFn := func(e string) {
errorString = e
}
parseKeymap(keymap, parseKeymap(keymap,
"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+ "ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
"f1:execute(ls {+})+abort+execute(echo {+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ "f1:execute(ls {+})+abort+execute(echo \n{+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
"alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+ "alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
"x:Execute(foo+bar),X:execute/bar+baz/"+ "x:Execute(foo+bar),X:execute/bar+baz/"+
",f1:+first,f1:+top"+ ",f1:+first,f1:+top"+
",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up") ",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up", errorFn)
check(tui.CtrlA.AsEvent(), "", actKillLine) check(tui.CtrlA.AsEvent(), "", actKillLine)
check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown) check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown)
check(tui.Key('c'), "", actPageUp) check(tui.Key('c'), "", actPageUp)
@@ -286,12 +290,15 @@ func TestBind(t *testing.T) {
check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute) check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute)
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char)) parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char), errorFn)
check(tui.Key([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute) check(tui.Key([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute)
} }
parseKeymap(keymap, "f1:abort") parseKeymap(keymap, "f1:abort", errorFn)
check(tui.F1.AsEvent(), "", actAbort) check(tui.F1.AsEvent(), "", actAbort)
if len(errorString) > 0 {
t.Errorf("error parsing keymap: %s", errorString)
}
} }
func TestColorSpec(t *testing.T) { func TestColorSpec(t *testing.T) {
@@ -354,10 +361,10 @@ func TestDefaultCtrlNP(t *testing.T) {
f.Close() f.Close()
hist := "--history=" + f.Name() hist := "--history=" + f.Name()
check([]string{hist}, tui.CtrlN, actNextHistory) check([]string{hist}, tui.CtrlN, actNextHistory)
check([]string{hist}, tui.CtrlP, actPreviousHistory) check([]string{hist}, tui.CtrlP, actPrevHistory)
check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlN, actAccept) check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlP, actPreviousHistory) check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlP, actPrevHistory)
check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlN, actNextHistory) check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlN, actNextHistory)
check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlP, actAccept) check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
@@ -466,3 +473,38 @@ func TestValidateSign(t *testing.T) {
} }
} }
} }
func TestParseSingleActionList(t *testing.T) {
actions := parseSingleActionList("Execute@foo+bar,baz@+up+up+reload:down+down", func(string) {})
if len(actions) != 4 {
t.Errorf("Invalid number of actions parsed:%d", len(actions))
}
if actions[0].t != actExecute || actions[0].a != "foo+bar,baz" {
t.Errorf("Invalid action parsed: %v", actions[0])
}
if actions[1].t != actUp || actions[2].t != actUp {
t.Errorf("Invalid action parsed: %v / %v", actions[1], actions[2])
}
if actions[3].t != actReload || actions[3].a != "down+down" {
t.Errorf("Invalid action parsed: %v", actions[3])
}
}
func TestParseSingleActionListError(t *testing.T) {
err := ""
parseSingleActionList("change-query(foobar)baz", func(e string) {
err = e
})
if len(err) == 0 {
t.Errorf("Failed to detect error")
}
}
func TestMaskActionContents(t *testing.T) {
original := ":execute((f)(o)(o)(b)(a)(r))+change-query@qu@ry@+up,x:reload:hello:world"
expected := ":execute +change-query +up,x:reload "
masked := maskActionContents(original)
if masked != expected {
t.Errorf("Not masked: %s", masked)
}
}

View File

@@ -50,20 +50,21 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
// Higher is better // Higher is better
val = math.MaxUint16 - util.AsUint16(score) val = math.MaxUint16 - util.AsUint16(score)
case byChunk: case byChunk:
if validOffsetFound {
b := minBegin b := minBegin
e := maxEnd e := maxEnd
l := item.text.Length()
for ; b >= 1; b-- { for ; b >= 1; b-- {
if unicode.IsSpace(item.text.Get(b - 1)) { if unicode.IsSpace(item.text.Get(b - 1)) {
break break
} }
} }
for ; e < l; e++ { for ; e < numChars; e++ {
if unicode.IsSpace(item.text.Get(e)) { if unicode.IsSpace(item.text.Get(e)) {
break break
} }
} }
val = util.AsUint16(e - b) val = util.AsUint16(e - b)
}
case byLength: case byLength:
val = item.TrimLength() val = item.TrimLength()
case byBegin, byEnd: case byBegin, byEnd:

138
src/server.go Normal file
View File

@@ -0,0 +1,138 @@
package fzf
import (
"bufio"
"bytes"
"errors"
"fmt"
"net"
"strconv"
"strings"
"time"
)
const (
crlf = "\r\n"
httpOk = "HTTP/1.1 200 OK" + crlf
httpBadRequest = "HTTP/1.1 400 Bad Request" + crlf
httpReadTimeout = 10 * time.Second
maxContentLength = 1024 * 1024
)
func startHttpServer(port int, channel chan []*action) (error, int) {
if port < 0 {
return nil, port
}
listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
if err != nil {
return fmt.Errorf("port not available: %d", port), 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() {
for {
conn, err := listener.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
break
} else {
continue
}
}
conn.Write([]byte(handleHttpRequest(conn, channel)))
conn.Close()
}
listener.Close()
}()
return nil, port
}
// Here we are writing a simplistic HTTP server without using net/http
// package to reduce the size of the binary.
//
// * No --listen: 2.8MB
// * --listen with net/http: 5.7MB
// * --listen w/o net/http: 3.3MB
func handleHttpRequest(conn net.Conn, channel chan []*action) string {
contentLength := 0
body := ""
bad := func(message string) string {
message += "\n"
return httpBadRequest + fmt.Sprintf("Content-Length: %d%s", len(message), crlf+crlf+message)
}
conn.SetReadDeadline(time.Now().Add(httpReadTimeout))
scanner := bufio.NewScanner(conn)
scanner.Split(func(data []byte, atEOF bool) (int, []byte, error) {
found := bytes.Index(data, []byte(crlf))
if found >= 0 {
token := data[:found+len(crlf)]
return len(token), token, nil
}
if atEOF || len(body)+len(data) >= contentLength {
return 0, data, bufio.ErrFinalToken
}
return 0, nil, nil
})
section := 0
for scanner.Scan() {
text := scanner.Text()
switch section {
case 0:
if !strings.HasPrefix(text, "POST / HTTP") {
return bad("invalid request method")
}
section++
case 1:
if text == crlf {
if contentLength == 0 {
return bad("content-length header missing")
}
section++
continue
}
pair := strings.SplitN(text, ":", 2)
if len(pair) == 2 && strings.ToLower(pair[0]) == "content-length" {
length, err := strconv.Atoi(strings.TrimSpace(pair[1]))
if err != nil || length <= 0 || length > maxContentLength {
return bad("invalid content length")
}
contentLength = length
}
case 2:
body += text
}
}
if len(body) < contentLength {
return bad("incomplete request")
}
body = body[:contentLength]
errorMessage := ""
actions := parseSingleActionList(strings.Trim(string(body), "\r\n"), func(message string) {
errorMessage = message
})
if len(errorMessage) > 0 {
return bad(errorMessage)
}
if len(actions) == 0 {
return bad("no action specified")
}
channel <- actions
return httpOk
}

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,8 @@ func HasFullscreenRenderer() bool {
return false return false
} }
var DefaultBorderShape BorderShape = BorderRounded
func (a Attr) Merge(b Attr) Attr { func (a Attr) Merge(b Attr) Attr {
return a | b return a | b
} }
@@ -32,6 +34,7 @@ func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
func (r *FullscreenRenderer) Pause(bool) {} func (r *FullscreenRenderer) Pause(bool) {}
func (r *FullscreenRenderer) Resume(bool, bool) {} func (r *FullscreenRenderer) Resume(bool, bool) {}
func (r *FullscreenRenderer) Clear() {} func (r *FullscreenRenderer) Clear() {}
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
func (r *FullscreenRenderer) Refresh() {} func (r *FullscreenRenderer) Refresh() {}
func (r *FullscreenRenderer) Close() {} func (r *FullscreenRenderer) Close() {}

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' {
runes = append(runes, []rune(CR+resetCode)...)
} else { } 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() {
@@ -72,7 +80,7 @@ type LightRenderer struct {
forceBlack bool forceBlack bool
clearOnExit bool clearOnExit bool
prevDownTime time.Time prevDownTime time.Time
clickY []int clicks [][2]int
ttyin *os.File ttyin *os.File
buffer []byte buffer []byte
origState *term.State origState *term.State
@@ -174,10 +182,7 @@ func (r *LightRenderer) Init() {
} }
} }
if r.mouse { r.enableMouse()
r.csi("?1000h")
r.csi("?1006h")
}
r.csi(fmt.Sprintf("%dA", r.MaxY()-1)) r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
r.csi("G") r.csi("G")
r.csi("K") r.csi("K")
@@ -425,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':
if r.buffer[3] == '~' {
return Event{Del, 0, nil} 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':
@@ -545,7 +562,7 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
t := atoi(elems[0], -1) t := atoi(elems[0], -1)
x := atoi(elems[1], -1) - 1 x := atoi(elems[1], -1) - 1
y := atoi(elems[2], -1) - 1 - r.yoffset y := atoi(elems[2], -1) - 1 - r.yoffset
if t < 0 || x < 0 || y < 0 { if t < 0 || x < 0 {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
*sz += end + 1 *sz += end + 1
@@ -569,25 +586,31 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
// ctrl := t & 0b1000 // ctrl := t & 0b1000
mod := t&0b1100 > 0 mod := t&0b1100 > 0
drag := t&0b100000 > 0
if scroll != 0 { if scroll != 0 {
return Event{Mouse, 0, &MouseEvent{y, x, scroll, false, false, false, mod}} return Event{Mouse, 0, &MouseEvent{y, x, scroll, false, false, false, mod}}
} }
double := false double := false
if down { if down && !drag {
now := time.Now() now := time.Now()
if !left { // Right double click is not allowed if !left { // Right double click is not allowed
r.clickY = []int{} r.clicks = [][2]int{}
} else if now.Sub(r.prevDownTime) < doubleClickDuration { } else if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clickY = append(r.clickY, y) r.clicks = append(r.clicks, [2]int{x, y})
} else { } else {
r.clickY = []int{y} r.clicks = [][2]int{{x, y}}
} }
r.prevDownTime = now r.prevDownTime = now
} else { } else {
if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] && n := len(r.clicks)
if len(r.clicks) > 1 && r.clicks[n-2][0] == r.clicks[n-1][0] && r.clicks[n-2][1] == r.clicks[n-1][1] &&
time.Since(r.prevDownTime) < doubleClickDuration { time.Since(r.prevDownTime) < doubleClickDuration {
double = true double = true
if double {
r.clicks = [][2]int{}
}
} }
} }
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}} return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
@@ -602,6 +625,7 @@ func (r *LightRenderer) rmcup() {
} }
func (r *LightRenderer) Pause(clear bool) { func (r *LightRenderer) Pause(clear bool) {
r.disableMouse()
r.restoreTerminal() r.restoreTerminal()
if clear { if clear {
if r.fullscreen { if r.fullscreen {
@@ -614,6 +638,22 @@ func (r *LightRenderer) Pause(clear bool) {
} }
} }
func (r *LightRenderer) enableMouse() {
if r.mouse {
r.csi("?1000h")
r.csi("?1002h")
r.csi("?1006h")
}
}
func (r *LightRenderer) disableMouse() {
if r.mouse {
r.csi("?1000l")
r.csi("?1002l")
r.csi("?1006l")
}
}
func (r *LightRenderer) Resume(clear bool, sigcont bool) { func (r *LightRenderer) Resume(clear bool, sigcont bool) {
r.setupTerminal() r.setupTerminal()
if clear { if clear {
@@ -622,13 +662,13 @@ func (r *LightRenderer) Resume(clear bool, sigcont bool) {
} else { } else {
r.rmcup() r.rmcup()
} }
r.enableMouse()
r.flush() r.flush()
} else if sigcont && !r.fullscreen && r.mouse { } else if sigcont && !r.fullscreen && r.mouse {
// NOTE: SIGCONT (Coming back from CTRL-Z): // NOTE: SIGCONT (Coming back from CTRL-Z):
// It's highly likely that the offset we obtained at the beginning is // It's highly likely that the offset we obtained at the beginning is
// no longer correct, so we simply disable mouse input. // no longer correct, so we simply disable mouse input.
r.csi("?1000l") r.disableMouse()
r.csi("?1006l")
r.mouse = false r.mouse = false
} }
} }
@@ -643,6 +683,10 @@ func (r *LightRenderer) Clear() {
r.flush() r.flush()
} }
func (r *LightRenderer) NeedScrollbarRedraw() bool {
return false
}
func (r *LightRenderer) RefreshWindows(windows []Window) { func (r *LightRenderer) RefreshWindows(windows []Window) {
r.flush() r.flush()
} }
@@ -666,10 +710,7 @@ func (r *LightRenderer) Close() {
} else if !r.fullscreen { } else if !r.fullscreen {
r.csi("u") r.csi("u")
} }
if r.mouse { r.disableMouse()
r.csi("?1000l")
r.csi("?1006l")
}
r.flush() r.flush()
r.closePlatform() r.closePlatform()
r.restoreTerminal() r.restoreTerminal()
@@ -706,25 +747,38 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, prev
w.fg = r.theme.Fg.Color w.fg = r.theme.Fg.Color
w.bg = r.theme.Bg.Color w.bg = r.theme.Bg.Color
} }
w.drawBorder() w.drawBorder(false)
return w return w
} }
func (w *LightWindow) drawBorder() { func (w *LightWindow) DrawHBorder() {
w.drawBorder(true)
}
func (w *LightWindow) drawBorder(onlyHorizontal bool) {
switch w.border.shape { switch w.border.shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble: case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble:
w.drawBorderAround() w.drawBorderAround(onlyHorizontal)
case BorderHorizontal: case BorderHorizontal:
w.drawBorderHorizontal(true, true) w.drawBorderHorizontal(true, true)
case BorderVertical: case BorderVertical:
if onlyHorizontal {
return
}
w.drawBorderVertical(true, true) w.drawBorderVertical(true, true)
case BorderTop: case BorderTop:
w.drawBorderHorizontal(true, false) w.drawBorderHorizontal(true, false)
case BorderBottom: case BorderBottom:
w.drawBorderHorizontal(false, true) w.drawBorderHorizontal(false, true)
case BorderLeft: case BorderLeft:
if onlyHorizontal {
return
}
w.drawBorderVertical(true, false) w.drawBorderVertical(true, false)
case BorderRight: case BorderRight:
if onlyHorizontal {
return
}
w.drawBorderVertical(false, true) w.drawBorderVertical(false, true)
} }
} }
@@ -734,13 +788,14 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
if w.preview { if w.preview {
color = ColPreviewBorder color = ColPreviewBorder
} }
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)) 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)) w.CPrint(color, repeat(w.border.bottom, w.width/hw))
} }
} }
@@ -756,38 +811,46 @@ 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))
} }
} }
} }
func (w *LightWindow) drawBorderAround() { func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
w.Move(0, 0) w.Move(0, 0)
color := ColBorder color := ColBorder
if w.preview { if w.preview {
color = ColPreviewBorder color = ColPreviewBorder
} }
w.CPrint(color, string(w.border.topLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.topRight)) hw := runewidth.RuneWidth(w.border.top)
tcw := runewidth.RuneWidth(w.border.topLeft) + runewidth.RuneWidth(w.border.topRight)
bcw := runewidth.RuneWidth(w.border.bottomLeft) + runewidth.RuneWidth(w.border.bottomRight)
rem := (w.width - tcw) % hw
w.CPrint(color, string(w.border.topLeft)+repeat(w.border.top, (w.width-tcw)/hw)+repeat(' ', rem)+string(w.border.topRight))
if !onlyHorizontal {
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-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)
w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.bottomRight)) rem = (w.width - bcw) % hw
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 {
@@ -893,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) {
@@ -908,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 {
@@ -937,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)
} }
@@ -955,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
@@ -970,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)
} }
} }
} }
@@ -979,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 {
@@ -1005,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() {
@@ -1020,7 +1090,7 @@ func (w *LightWindow) FinishFill() {
} }
func (w *LightWindow) Erase() { func (w *LightWindow) Erase() {
w.drawBorder() w.drawBorder(false)
// We don't erase the window here to avoid flickering during scroll // We don't erase the window here to avoid flickering during scroll
w.Move(0, 0) w.Move(0, 0)
} }

View File

@@ -6,10 +6,9 @@ import (
"os" "os"
"time" "time"
"runtime"
"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"
@@ -19,6 +18,8 @@ func HasFullscreenRenderer() bool {
return true return true
} }
var DefaultBorderShape BorderShape = BorderSharp
func asTcellColor(color Color) tcell.Color { func asTcellColor(color Color) tcell.Color {
if color == colDefault { if color == colDefault {
return tcell.ColorDefault return tcell.ColorDefault
@@ -189,6 +190,10 @@ func (r *FullscreenRenderer) Clear() {
_screen.Clear() _screen.Clear()
} }
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool {
return true
}
func (r *FullscreenRenderer) Refresh() { func (r *FullscreenRenderer) Refresh() {
// noop // noop
} }
@@ -218,54 +223,38 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}} return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}}
case button&tcell.WheelUp != 0: case button&tcell.WheelUp != 0:
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}} return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}}
case button&tcell.Button1 != 0 && !drag: case button&tcell.Button1 != 0:
// all potential double click events put their 'line' coordinate in the clickY array double := false
if !drag {
// all potential double click events put their coordinates in the clicks array
// double click event has two conditions, temporal and spatial, the first is checked here // double click event has two conditions, temporal and spatial, the first is checked here
now := time.Now() now := time.Now()
if now.Sub(r.prevDownTime) < doubleClickDuration { if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clickY = append(r.clickY, y) r.clicks = append(r.clicks, [2]int{x, y})
} else { } else {
r.clickY = []int{y} r.clicks = [][2]int{{x, y}}
} }
r.prevDownTime = now r.prevDownTime = now
// detect double clicks (also check for spatial condition) // detect double clicks (also check for spatial condition)
n := len(r.clickY) n := len(r.clicks)
double := n > 1 && r.clickY[n-2] == r.clickY[n-1] double = n > 1 && r.clicks[n-2][0] == r.clicks[n-1][0] && r.clicks[n-2][1] == r.clicks[n-1][1]
if double { if double {
// make sure two consecutive double clicks require four clicks // make sure two consecutive double clicks require four clicks
r.clickY = []int{} r.clicks = [][2]int{}
}
} }
// fire single or double click event // fire single or double click event
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, mod}} return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, mod}}
case button&tcell.Button2 != 0 && !drag: case button&tcell.Button2 != 0:
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, mod}} return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, mod}}
case runtime.GOOS != "windows": default:
// double and single taps on Windows don't quite work due to // double and single taps on Windows don't quite work due to
// the console acting on the events and not allowing us // the console acting on the events and not allowing us
// to consume them. // to consume them.
left := button&tcell.Button1 != 0 left := button&tcell.Button1 != 0
down := left || button&tcell.Button3 != 0 down := left || button&tcell.Button3 != 0
double := false double := false
if down {
now := time.Now()
if !left {
r.clickY = []int{}
} else if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clickY = append(r.clickY, x)
} else {
r.clickY = []int{x}
r.prevDownTime = now
}
} else {
if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
time.Now().Sub(r.prevDownTime) < doubleClickDuration {
double = true
}
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}} return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
} }
@@ -424,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}
@@ -524,7 +519,7 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
height: height, height: height,
normal: normal, normal: normal,
borderStyle: borderStyle} borderStyle: borderStyle}
w.drawBorder() w.drawBorder(false)
return w return w
} }
@@ -541,7 +536,7 @@ func fill(x, y, w, h int, n ColorPair, r rune) {
} }
func (w *TcellWindow) Erase() { func (w *TcellWindow) Erase() {
fill(w.left-1, w.top, w.width+1, w.height, w.normal, ' ') fill(w.left-1, w.top, w.width+1, w.height-1, w.normal, ' ')
} }
func (w *TcellWindow) Enclose(y int, x int) bool { func (w *TcellWindow) Enclose(y int, x int) bool {
@@ -584,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
} }
@@ -632,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 {
r := rs[0]
switch r {
case '\r':
st = style.Dim(true)
rs[0] = '␍'
case '\n':
w.lastY++ w.lastY++
w.lastX = 0 w.lastX = 0
lx = 0 lx = 0
continue continue Loop
}
} }
// word wrap: // word wrap:
@@ -655,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 {
@@ -682,7 +687,11 @@ func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
return w.fillString(str, NewColorPair(fg, bg, a)) return w.fillString(str, NewColorPair(fg, bg, a))
} }
func (w *TcellWindow) drawBorder() { func (w *TcellWindow) DrawHBorder() {
w.drawBorder(true)
}
func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
shape := w.borderStyle.shape shape := w.borderStyle.shape
if shape == BorderNone { if shape == BorderNone {
return return
@@ -704,35 +713,52 @@ func (w *TcellWindow) drawBorder() {
style = w.normal.style() style = w.normal.style()
} }
hw := runewidth.RuneWidth(w.borderStyle.top)
switch shape { switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderHorizontal, BorderTop: case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderTop:
for x := left; x < right; x++ { max := right - 2*hw
_screen.SetContent(x, top, w.borderStyle.horizontal, nil, style) if shape == BorderHorizontal || shape == BorderTop {
max = right - hw
}
// tcell has an issue displaying two overlapping wide runes
// e.g. SetContent( HH )
// SetContent( TR )
// ==================
// ( HH ) => TR is ignored
for x := left; x <= max; x += hw {
_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, BorderThinBlock, BorderDouble, BorderHorizontal, BorderBottom:
for x := left; x < right; x++ { max := right - 2*hw
_screen.SetContent(x, bot-1, w.borderStyle.horizontal, nil, style) if shape == BorderHorizontal || shape == BorderBottom {
max = right - hw
}
for x := left; x <= max; x += hw {
_screen.SetContent(x, bot-1, w.borderStyle.bottom, nil, style)
} }
} }
if !onlyHorizontal {
switch shape { switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderLeft: case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, 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, BorderThinBlock, BorderDouble, BorderVertical, BorderRight:
vw := runewidth.RuneWidth(w.borderStyle.right)
for y := top; y < bot; y++ { for y := top; y < bot; y++ {
_screen.SetContent(right-1, 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, BorderThinBlock, BorderDouble:
_screen.SetContent(left, top, w.borderStyle.topLeft, nil, style) _screen.SetContent(left, top, w.borderStyle.topLeft, nil, style)
_screen.SetContent(right-1, 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)
_screen.SetContent(right-1, bot-1, w.borderStyle.bottomRight, nil, style) _screen.SetContent(right-runewidth.RuneWidth(w.borderStyle.bottomRight), bot-1, w.borderStyle.bottomRight, 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
@@ -91,6 +93,10 @@ const (
Change Change
BackwardEOF BackwardEOF
Start Start
Load
Focus
One
Zero
AltBS AltBS
@@ -268,8 +274,12 @@ type ColorTheme struct {
Selected ColorAttr Selected ColorAttr
Header ColorAttr Header ColorAttr
Separator ColorAttr Separator ColorAttr
Scrollbar ColorAttr
Border ColorAttr Border ColorAttr
PreviewBorder ColorAttr
PreviewScrollbar ColorAttr
BorderLabel ColorAttr BorderLabel ColorAttr
PreviewLabel ColorAttr
} }
type Event struct { type Event struct {
@@ -278,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
@@ -295,6 +314,8 @@ const (
BorderRounded BorderRounded
BorderSharp BorderSharp
BorderBold BorderBold
BorderBlock
BorderThinBlock
BorderDouble BorderDouble
BorderHorizontal BorderHorizontal
BorderVertical BorderVertical
@@ -304,10 +325,28 @@ const (
BorderRight BorderRight
) )
func (s BorderShape) HasRight() bool {
switch s {
case BorderNone, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
return false
}
return true
}
func (s BorderShape) HasTop() bool {
switch s {
case BorderNone, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
return false
}
return true
}
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
@@ -320,8 +359,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: '+',
@@ -332,8 +373,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: '└',
@@ -342,18 +385,54 @@ 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 BorderThinBlock:
// 🭽▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔🭾
// ▏ ▕
// 🭼▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁🭿
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: '╚',
@@ -362,8 +441,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: '╰',
@@ -374,8 +455,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: ' ',
@@ -391,6 +474,7 @@ type Renderer interface {
RefreshWindows(windows []Window) RefreshWindows(windows []Window)
Refresh() Refresh()
Close() Close()
NeedScrollbarRedraw() bool
GetChar() Event GetChar() Event
@@ -406,6 +490,7 @@ type Window interface {
Width() int Width() int
Height() int Height() int
DrawHBorder()
Refresh() Refresh()
FinishFill() FinishFill()
Close() Close()
@@ -428,7 +513,7 @@ type FullscreenRenderer struct {
mouse bool mouse bool
forceBlack bool forceBlack bool
prevDownTime time.Time prevDownTime time.Time
clickY []int clicks [][2]int
} }
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer { func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
@@ -437,7 +522,7 @@ func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Rende
mouse: mouse, mouse: mouse,
forceBlack: forceBlack, forceBlack: forceBlack,
prevDownTime: time.Unix(0, 0), prevDownTime: time.Unix(0, 0),
clickY: []int{}} clicks: [][2]int{}}
return r return r
} }
@@ -464,23 +549,23 @@ var (
ColInfo ColorPair ColInfo ColorPair
ColHeader ColorPair ColHeader ColorPair
ColSeparator ColorPair ColSeparator ColorPair
ColScrollbar ColorPair
ColBorder ColorPair ColBorder ColorPair
ColPreview ColorPair ColPreview ColorPair
ColPreviewBorder ColorPair ColPreviewBorder ColorPair
ColBorderLabel ColorPair ColBorderLabel ColorPair
ColPreviewLabel ColorPair
ColPreviewScrollbar ColorPair
ColPreviewSpinner ColorPair
) )
func EmptyTheme() *ColorTheme { func EmptyTheme() *ColorTheme {
return &ColorTheme{ return &ColorTheme{
Colored: true, Colored: true,
Input: ColorAttr{colUndefined, AttrUndefined}, Input: ColorAttr{colUndefined, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
Fg: ColorAttr{colUndefined, AttrUndefined}, Fg: ColorAttr{colUndefined, AttrUndefined},
Bg: ColorAttr{colUndefined, AttrUndefined}, Bg: ColorAttr{colUndefined, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{colUndefined, AttrUndefined}, DarkBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: 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},
@@ -490,35 +575,47 @@ func EmptyTheme() *ColorTheme {
Cursor: ColorAttr{colUndefined, AttrUndefined}, Cursor: ColorAttr{colUndefined, AttrUndefined},
Selected: ColorAttr{colUndefined, AttrUndefined}, Selected: ColorAttr{colUndefined, AttrUndefined},
Header: ColorAttr{colUndefined, AttrUndefined}, Header: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Border: ColorAttr{colUndefined, AttrUndefined}, Border: ColorAttr{colUndefined, AttrUndefined},
BorderLabel: ColorAttr{colUndefined, AttrUndefined}, BorderLabel: ColorAttr{colUndefined, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
PreviewScrollbar: 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},
Disabled: ColorAttr{colDefault, AttrRegular}, Fg: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrRegular}, Bg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrRegular}, DarkBg: ColorAttr{colDefault, AttrUndefined},
PreviewFg: ColorAttr{colDefault, AttrRegular}, Prompt: ColorAttr{colDefault, AttrUndefined},
PreviewBg: ColorAttr{colDefault, AttrRegular},
DarkBg: ColorAttr{colDefault, AttrRegular},
Gutter: ColorAttr{colDefault, AttrRegular},
Prompt: ColorAttr{colDefault, AttrRegular},
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},
Separator: ColorAttr{colDefault, AttrRegular}, Border: ColorAttr{colDefault, AttrUndefined},
Border: ColorAttr{colDefault, AttrRegular}, BorderLabel: ColorAttr{colDefault, AttrUndefined},
BorderLabel: ColorAttr{colDefault, AttrRegular}, Disabled: ColorAttr{colDefault, AttrUndefined},
PreviewFg: ColorAttr{colDefault, AttrUndefined},
PreviewBg: ColorAttr{colDefault, AttrUndefined},
Gutter: ColorAttr{colDefault, AttrUndefined},
PreviewBorder: ColorAttr{colDefault, AttrUndefined},
PreviewScrollbar: ColorAttr{colDefault, AttrUndefined},
PreviewLabel: ColorAttr{colDefault, AttrUndefined},
Separator: ColorAttr{colDefault, AttrUndefined},
Scrollbar: ColorAttr{colDefault, AttrUndefined},
} }
} }
@@ -531,13 +628,9 @@ func init() {
Default16 = &ColorTheme{ Default16 = &ColorTheme{
Colored: true, Colored: true,
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{colBlack, AttrUndefined}, DarkBg: ColorAttr{colBlack, AttrUndefined},
Gutter: ColorAttr{colUndefined, 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},
@@ -547,20 +640,24 @@ func init() {
Cursor: ColorAttr{colRed, AttrUndefined}, Cursor: ColorAttr{colRed, AttrUndefined},
Selected: ColorAttr{colMagenta, AttrUndefined}, Selected: ColorAttr{colMagenta, AttrUndefined},
Header: ColorAttr{colCyan, AttrUndefined}, Header: ColorAttr{colCyan, AttrUndefined},
Separator: ColorAttr{colBlack, AttrUndefined},
Border: ColorAttr{colBlack, AttrUndefined}, Border: ColorAttr{colBlack, AttrUndefined},
BorderLabel: ColorAttr{colWhite, AttrUndefined}, BorderLabel: ColorAttr{colWhite, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
PreviewScrollbar: 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},
Disabled: ColorAttr{colUndefined, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{236, AttrUndefined}, DarkBg: ColorAttr{236, AttrUndefined},
Gutter: ColorAttr{colUndefined, 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},
@@ -570,20 +667,24 @@ func init() {
Cursor: ColorAttr{161, AttrUndefined}, Cursor: ColorAttr{161, AttrUndefined},
Selected: ColorAttr{168, AttrUndefined}, Selected: ColorAttr{168, AttrUndefined},
Header: ColorAttr{109, AttrUndefined}, Header: ColorAttr{109, AttrUndefined},
Separator: ColorAttr{59, AttrUndefined},
Border: ColorAttr{59, AttrUndefined}, Border: ColorAttr{59, AttrUndefined},
BorderLabel: ColorAttr{145, AttrUndefined}, BorderLabel: ColorAttr{145, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
PreviewScrollbar: 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},
Disabled: ColorAttr{colUndefined, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{251, AttrUndefined}, DarkBg: ColorAttr{251, AttrUndefined},
Gutter: ColorAttr{colUndefined, 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},
@@ -593,9 +694,17 @@ func init() {
Cursor: ColorAttr{161, AttrUndefined}, Cursor: ColorAttr{161, AttrUndefined},
Selected: ColorAttr{168, AttrUndefined}, Selected: ColorAttr{168, AttrUndefined},
Header: ColorAttr{31, AttrUndefined}, Header: ColorAttr{31, AttrUndefined},
Separator: ColorAttr{145, AttrUndefined},
Border: ColorAttr{145, AttrUndefined}, Border: ColorAttr{145, AttrUndefined},
BorderLabel: ColorAttr{59, AttrUndefined}, BorderLabel: ColorAttr{59, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
} }
} }
@@ -615,13 +724,9 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
return c return c
} }
theme.Input = o(baseTheme.Input, theme.Input) theme.Input = o(baseTheme.Input, theme.Input)
theme.Disabled = o(theme.Input, o(baseTheme.Disabled, theme.Disabled))
theme.Fg = o(baseTheme.Fg, theme.Fg) theme.Fg = o(baseTheme.Fg, theme.Fg)
theme.Bg = o(baseTheme.Bg, theme.Bg) theme.Bg = o(baseTheme.Bg, theme.Bg)
theme.PreviewFg = o(theme.Fg, o(baseTheme.PreviewFg, theme.PreviewFg))
theme.PreviewBg = o(theme.Bg, o(baseTheme.PreviewBg, theme.PreviewBg))
theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg) theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg)
theme.Gutter = o(theme.DarkBg, o(baseTheme.Gutter, theme.Gutter))
theme.Prompt = o(baseTheme.Prompt, theme.Prompt) theme.Prompt = o(baseTheme.Prompt, theme.Prompt)
theme.Match = o(baseTheme.Match, theme.Match) theme.Match = o(baseTheme.Match, theme.Match)
theme.Current = o(baseTheme.Current, theme.Current) theme.Current = o(baseTheme.Current, theme.Current)
@@ -631,10 +736,20 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
theme.Cursor = o(baseTheme.Cursor, theme.Cursor) theme.Cursor = o(baseTheme.Cursor, theme.Cursor)
theme.Selected = o(baseTheme.Selected, theme.Selected) theme.Selected = o(baseTheme.Selected, theme.Selected)
theme.Header = o(baseTheme.Header, theme.Header) theme.Header = o(baseTheme.Header, theme.Header)
theme.Separator = o(baseTheme.Separator, theme.Separator)
theme.Border = o(baseTheme.Border, theme.Border) theme.Border = o(baseTheme.Border, theme.Border)
theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel) theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel)
// These colors are not defined in the base themes
theme.Disabled = o(theme.Input, theme.Disabled)
theme.Gutter = o(theme.DarkBg, theme.Gutter)
theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
theme.PreviewBg = o(theme.Bg, theme.PreviewBg)
theme.PreviewLabel = o(theme.BorderLabel, theme.PreviewLabel)
theme.PreviewBorder = o(theme.Border, theme.PreviewBorder)
theme.Separator = o(theme.Border, theme.Separator)
theme.Scrollbar = o(theme.Border, theme.Scrollbar)
theme.PreviewScrollbar = o(theme.PreviewBorder, theme.PreviewScrollbar)
initPalette(theme) initPalette(theme)
} }
@@ -666,8 +781,12 @@ func initPalette(theme *ColorTheme) {
ColInfo = pair(theme.Info, theme.Bg) ColInfo = pair(theme.Info, theme.Bg)
ColHeader = pair(theme.Header, theme.Bg) ColHeader = pair(theme.Header, theme.Bg)
ColSeparator = pair(theme.Separator, theme.Bg) ColSeparator = pair(theme.Separator, theme.Bg)
ColScrollbar = pair(theme.Scrollbar, theme.Bg)
ColBorder = pair(theme.Border, theme.Bg) ColBorder = pair(theme.Border, theme.Bg)
ColBorderLabel = pair(theme.BorderLabel, theme.Bg) ColBorderLabel = pair(theme.BorderLabel, theme.Bg)
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)
ColPreviewSpinner = pair(theme.Spinner, theme.PreviewBg)
} }

View File

@@ -9,7 +9,6 @@ const (
EvtSearchNew EvtSearchNew
EvtSearchProgress EvtSearchProgress
EvtSearchFin EvtSearchFin
EvtClose
) )
func TestEventBox(t *testing.T) { func TestEventBox(t *testing.T) {

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

@@ -1,14 +1,76 @@
package util package util
import "testing" import (
"math"
"strings"
"testing"
"time"
)
func TestMax(t *testing.T) { func TestMax(t *testing.T) {
if Max(10, 1) != 10 {
t.Error("Expected", 10)
}
if Max(-2, 5) != 5 { if Max(-2, 5) != 5 {
t.Error("Invalid result") t.Error("Expected", 5)
} }
} }
func TestContrain(t *testing.T) { func TestMax16(t *testing.T) {
if Max16(10, 1) != 10 {
t.Error("Expected", 10)
}
if Max16(-2, 5) != 5 {
t.Error("Expected", 5)
}
if Max16(math.MaxInt16, 0) != math.MaxInt16 {
t.Error("Expected", math.MaxInt16)
}
if Max16(0, math.MinInt16) != 0 {
t.Error("Expected", 0)
}
}
func TestMax32(t *testing.T) {
if Max32(10, 1) != 10 {
t.Error("Expected", 10)
}
if Max32(-2, 5) != 5 {
t.Error("Expected", 5)
}
if Max32(math.MaxInt32, 0) != math.MaxInt32 {
t.Error("Expected", math.MaxInt32)
}
if Max32(0, math.MinInt32) != 0 {
t.Error("Expected", 0)
}
}
func TestMin(t *testing.T) {
if Min(10, 1) != 1 {
t.Error("Expected", 1)
}
if Min(-2, 5) != -2 {
t.Error("Expected", -2)
}
}
func TestMin32(t *testing.T) {
if Min32(10, 1) != 1 {
t.Error("Expected", 1)
}
if Min32(-2, 5) != -2 {
t.Error("Expected", -2)
}
if Min32(math.MaxInt32, 0) != 0 {
t.Error("Expected", 0)
}
if Min32(0, math.MinInt32) != math.MinInt32 {
t.Error("Expected", math.MinInt32)
}
}
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)
} }
@@ -21,6 +83,55 @@ func TestContrain(t *testing.T) {
} }
} }
func TestConstrain32(t *testing.T) {
if Constrain32(-3, -1, 3) != -1 {
t.Error("Expected", -1)
}
if Constrain32(2, -1, 3) != 2 {
t.Error("Expected", 2)
}
if Constrain32(5, -1, 3) != 3 {
t.Error("Expected", 3)
}
if Constrain32(0, math.MinInt32, math.MaxInt32) != 0 {
t.Error("Expected", 0)
}
}
func TestAsUint16(t *testing.T) {
if AsUint16(5) != 5 {
t.Error("Expected", 5)
}
if AsUint16(-10) != 0 {
t.Error("Expected", 0)
}
if AsUint16(math.MaxUint16) != math.MaxUint16 {
t.Error("Expected", math.MaxUint16)
}
if AsUint16(math.MinInt32) != 0 {
t.Error("Expected", 0)
}
if AsUint16(math.MinInt16) != 0 {
t.Error("Expected", 0)
}
if AsUint16(math.MaxUint16+1) != math.MaxUint16 {
t.Error("Expected", math.MaxUint16)
}
}
func TestDurWithIn(t *testing.T) {
if DurWithin(time.Duration(5), time.Duration(1), time.Duration(8)) != time.Duration(5) {
t.Error("Expected", time.Duration(0))
}
if DurWithin(time.Duration(0)*time.Second, time.Second, time.Duration(3)*time.Second) != time.Second {
t.Error("Expected", time.Second)
}
if DurWithin(time.Duration(10)*time.Second, time.Duration(0), time.Second) != time.Second {
t.Error("Expected", time.Second)
}
}
func TestOnce(t *testing.T) { func TestOnce(t *testing.T) {
o := Once(false) o := Once(false)
if o() { if o() {
@@ -64,3 +175,12 @@ func TestTruncate(t *testing.T) {
t.Errorf("Expected: 6, actual: %d", width) t.Errorf("Expected: 6, actual: %d", width)
} }
} }
func TestRepeatToFill(t *testing.T) {
if RepeatToFill("abcde", 10, 50) != strings.Repeat("abcde", 5) {
t.Error("Expected:", strings.Repeat("abcde", 5))
}
if RepeatToFill("abcde", 10, 42) != strings.Repeat("abcde", 4)+"abcde"[:2] {
t.Error("Expected:", strings.Repeat("abcde", 4)+"abcde"[:2])
}
}

View File

@@ -6,6 +6,8 @@ import (
"os" "os"
"os/exec" "os/exec"
"syscall" "syscall"
"golang.org/x/sys/unix"
) )
// ExecCommand executes the given command with $SHELL // ExecCommand executes the given command with $SHELL
@@ -45,3 +47,7 @@ func SetNonblock(file *os.File, nonblock bool) {
func Read(fd int, b []byte) (int, error) { func Read(fd int, b []byte) (int, error) {
return syscall.Read(int(fd), b) return syscall.Read(int(fd), b)
} }
func SetStdin(file *os.File) {
unix.Dup2(int(file.Fd()), 0)
}

View File

@@ -81,3 +81,7 @@ func SetNonblock(file *os.File, nonblock bool) {
func Read(fd int, b []byte) (int, error) { func Read(fd int, b []byte) (int, error) {
return syscall.Read(syscall.Handle(fd), b) return syscall.Read(syscall.Handle(fd), b)
} }
func SetStdin(file *os.File) {
// No-op
}

View File

@@ -7,6 +7,7 @@ require 'English'
require 'shellwords' require 'shellwords'
require 'erb' require 'erb'
require 'tempfile' require 'tempfile'
require 'net/http'
TEMPLATE = DATA.read TEMPLATE = DATA.read
UNSETS = %w[ UNSETS = %w[
@@ -22,7 +23,7 @@ DEFAULT_TIMEOUT = 10
FILE = File.expand_path(__FILE__) FILE = File.expand_path(__FILE__)
BASE = File.expand_path('..', __dir__) BASE = File.expand_path('..', __dir__)
Dir.chdir(BASE) Dir.chdir(BASE)
FZF = "FZF_DEFAULT_OPTS= FZF_DEFAULT_COMMAND= #{BASE}/bin/fzf" FZF = "FZF_DEFAULT_OPTS=--no-scrollbar FZF_DEFAULT_COMMAND= #{BASE}/bin/fzf"
def wait def wait
since = Time.now since = Time.now
@@ -64,7 +65,7 @@ class Shell
end end
def fish def fish
UNSETS.map { |v| v + '= ' }.join + 'fish' UNSETS.map { |v| v + '= ' }.join + ' FZF_DEFAULT_OPTS=--no-scrollbar fish'
end end
end end
end end
@@ -179,7 +180,7 @@ class TestBase < Minitest::Test
end end
def writelines(path, lines) def writelines(path, lines)
File.unlink(path) while File.exist?(path) FileUtils.rm_f(path) while File.exist?(path)
File.open(path, 'w') { |f| f.puts lines } File.open(path, 'w') { |f| f.puts lines }
end end
@@ -187,7 +188,7 @@ class TestBase < Minitest::Test
wait { assert_path_exists tempname } wait { assert_path_exists tempname }
File.read(tempname) File.read(tempname)
ensure ensure
File.unlink(tempname) while File.exist?(tempname) FileUtils.rm_f(tempname) while File.exist?(tempname)
@temp_suffix += 1 @temp_suffix += 1
tmux.prepare tmux.prepare
end end
@@ -779,6 +780,10 @@ class TestGoFZF < TestBase
'2 foobar baz', '2 foobar baz',
'3 foo barbaz' '3 foo barbaz'
], `#{FZF} -fba --tiebreak=chunk < #{tempname}`.lines(chomp: true) ], `#{FZF} -fba --tiebreak=chunk < #{tempname}`.lines(chomp: true)
assert_equal [
'3 foo barbaz'
], `#{FZF} -f'!foobar' --tiebreak=chunk < #{tempname}`.lines(chomp: true)
end end
def test_invalid_cache def test_invalid_cache
@@ -900,11 +905,7 @@ class TestGoFZF < TestBase
history_file = '/tmp/fzf-test-history' history_file = '/tmp/fzf-test-history'
# History with limited number of entries # History with limited number of entries
begin FileUtils.rm_f(history_file)
File.unlink(history_file)
rescue StandardError
nil
end
opts = "--history=#{history_file} --history-size=4" opts = "--history=#{history_file} --history-size=4"
input = %w[00 11 22 33 44] input = %w[00 11 22 33 44]
input.each do |keys| input.each do |keys|
@@ -917,7 +918,7 @@ class TestGoFZF < TestBase
end end
wait do wait do
assert_path_exists history_file assert_path_exists history_file
assert_equal input[1..-1], File.readlines(history_file, chomp: true) assert_equal input[1..], File.readlines(history_file, chomp: true)
end end
# Update history entries (not changed on disk) # Update history entries (not changed on disk)
@@ -950,7 +951,7 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal '> 33', lines[-1] } tmux.until { |lines| assert_equal '> 33', lines[-1] }
tmux.send_keys :Enter tmux.send_keys :Enter
ensure ensure
File.unlink(history_file) FileUtils.rm_f(history_file)
end end
def test_execute def test_execute
@@ -979,11 +980,7 @@ class TestGoFZF < TestBase
], File.readlines(output, chomp: true) ], File.readlines(output, chomp: true)
end end
ensure ensure
begin FileUtils.rm_f(output)
File.unlink(output)
rescue StandardError
nil
end
end end
def test_execute_multi def test_execute_multi
@@ -1008,20 +1005,12 @@ class TestGoFZF < TestBase
], File.readlines(output, chomp: true) ], File.readlines(output, chomp: true)
end end
ensure ensure
begin FileUtils.rm_f(output)
File.unlink(output)
rescue StandardError
nil
end
end end
def test_execute_plus_flag def test_execute_plus_flag
output = tempname + '.tmp' output = tempname + '.tmp'
begin FileUtils.rm_f(output)
File.unlink(output)
rescue StandardError
nil
end
writelines(tempname, ['foo bar', '123 456']) writelines(tempname, ['foo bar', '123 456'])
tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute-silent(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute-silent(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter
@@ -1054,21 +1043,13 @@ class TestGoFZF < TestBase
], File.readlines(output, chomp: true) ], File.readlines(output, chomp: true)
end end
rescue StandardError rescue StandardError
begin FileUtils.rm_f(output)
File.unlink(output)
rescue StandardError
nil
end
end end
def test_execute_shell def test_execute_shell
# Custom script to use as $SHELL # Custom script to use as $SHELL
output = tempname + '.out' output = tempname + '.out'
begin FileUtils.rm_f(output)
File.unlink(output)
rescue StandardError
nil
end
writelines(tempname, writelines(tempname,
['#!/usr/bin/env bash', "echo $1 / $2 > #{output}"]) ['#!/usr/bin/env bash', "echo $1 / $2 > #{output}"])
system("chmod +x #{tempname}") system("chmod +x #{tempname}")
@@ -1082,11 +1063,7 @@ class TestGoFZF < TestBase
assert_equal ["-c / 'foo'bar"], File.readlines(output, chomp: true) assert_equal ["-c / 'foo'bar"], File.readlines(output, chomp: true)
end end
ensure ensure
begin FileUtils.rm_f(output)
File.unlink(output)
rescue StandardError
nil
end
end end
def test_cycle def test_cycle
@@ -1480,6 +1457,83 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_includes lines[1], ' {5-1 3 4} ' } tmux.until { |lines| assert_includes lines[1], ' {5-1 3 4} ' }
end end
def test_toggle_preview_without_default_preview_command
tmux.send_keys %(seq 100 | #{FZF} --bind 'space:preview(echo [{}]),enter:toggle-preview' --preview-window up,border-double), :Enter
tmux.until do |lines|
assert_equal 100, lines.match_count
refute_includes lines[1], '║ [1]'
end
# toggle-preview should do nothing
tmux.send_keys :Enter
tmux.until { |lines| refute_includes lines[1], '║ [1]' }
tmux.send_keys :Up
tmux.until do |lines|
refute_includes lines[1], '║ [1]'
refute_includes lines[1], '║ [2]'
end
tmux.send_keys :Up
tmux.until do |lines|
assert_includes lines, '> 3'
refute_includes lines[1], '║ [3]'
end
# One-off preview action
tmux.send_keys :Space
tmux.until { |lines| assert_includes lines[1], '║ [3]' }
# toggle-preview to hide it
tmux.send_keys :Enter
tmux.until { |lines| refute_includes lines[1], '║ [3]' }
# toggle-preview again does nothing
tmux.send_keys :Enter, :Up
tmux.until do |lines|
assert_includes lines, '> 4'
refute_includes lines[1], '║ [4]'
end
end
def test_show_and_hide_preview
tmux.send_keys %(seq 100 | #{FZF} --preview-window hidden,border-bold --preview 'echo [{}]' --bind 'a:show-preview,b:hide-preview'), :Enter
# Hidden by default
tmux.until do |lines|
assert_equal 100, lines.match_count
refute_includes lines[1], '┃ [1]'
end
# Show
tmux.send_keys :a
tmux.until { |lines| assert_includes lines[1], '┃ [1]' }
# Already shown
tmux.send_keys :a
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines[1], '┃ [2]' }
# Hide
tmux.send_keys :b
tmux.send_keys :Up
tmux.until do |lines|
assert_includes lines, '> 3'
refute_includes lines[1], '┃ [3]'
end
# Already hidden
tmux.send_keys :b
tmux.send_keys :Up
tmux.until do |lines|
assert_includes lines, '> 4'
refute_includes lines[1], '┃ [4]'
end
# Show it again
tmux.send_keys :a
tmux.until { |lines| assert_includes lines[1], '┃ [4]' }
end
def test_preview_hidden def test_preview_hidden
tmux.send_keys %(seq 1000 | #{FZF} --preview 'echo {{}-{}-$FZF_PREVIEW_LINES-$FZF_PREVIEW_COLUMNS}' --preview-window down:1:hidden --bind ?:toggle-preview), :Enter tmux.send_keys %(seq 1000 | #{FZF} --preview 'echo {{}-{}-$FZF_PREVIEW_LINES-$FZF_PREVIEW_COLUMNS}' --preview-window down:1:hidden --bind ?:toggle-preview), :Enter
tmux.until { |lines| assert_equal '>', lines[-1] } tmux.until { |lines| assert_equal '>', lines[-1] }
@@ -1492,12 +1546,8 @@ class TestGoFZF < TestBase
end end
def test_preview_size_0 def test_preview_size_0
begin FileUtils.rm_f(tempname)
File.unlink(tempname) tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0 --bind space:toggle-preview), :Enter
rescue StandardError
nil
end
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0), :Enter
tmux.until do |lines| tmux.until do |lines|
assert_equal 100, lines.item_count assert_equal 100, lines.item_count
assert_equal ' 100/100', lines[1] assert_equal ' 100/100', lines[1]
@@ -1507,17 +1557,43 @@ class TestGoFZF < TestBase
assert_path_exists tempname assert_path_exists tempname
assert_equal %w[1], File.readlines(tempname, chomp: true) assert_equal %w[1], File.readlines(tempname, chomp: true)
end end
tmux.send_keys :Down tmux.send_keys :Space, :Down, :Down
tmux.until { |lines| assert_equal '> 2', lines[3] }
wait do
assert_path_exists tempname
assert_equal %w[1 2], File.readlines(tempname, chomp: true)
end
tmux.send_keys :Down
tmux.until { |lines| assert_equal '> 3', lines[4] } tmux.until { |lines| assert_equal '> 3', lines[4] }
wait do wait do
assert_path_exists tempname assert_path_exists tempname
assert_equal %w[1 2 3], File.readlines(tempname, chomp: true) assert_equal %w[1], File.readlines(tempname, chomp: true)
end
tmux.send_keys :Space, :Down
tmux.until { |lines| assert_equal '> 4', lines[5] }
wait do
assert_path_exists tempname
assert_equal %w[1 3 4], File.readlines(tempname, chomp: true)
end
end
def test_preview_size_0_hidden
FileUtils.rm_f(tempname)
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0,hidden --bind space:toggle-preview), :Enter
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.send_keys :Down, :Down
tmux.until { |lines| assert_includes lines, '> 3' }
wait { refute_path_exists tempname }
tmux.send_keys :Space
wait do
assert_path_exists tempname
assert_equal %w[3], File.readlines(tempname, chomp: true)
end
tmux.send_keys :Down
wait do
assert_equal %w[3 4], File.readlines(tempname, chomp: true)
end
tmux.send_keys :Space, :Down
tmux.until { |lines| assert_includes lines, '> 5' }
tmux.send_keys :Down
tmux.until { |lines| assert_includes lines, '> 6' }
tmux.send_keys :Space
wait do
assert_equal %w[3 4 6], File.readlines(tempname, chomp: true)
end end
end end
@@ -1550,13 +1626,13 @@ class TestGoFZF < TestBase
end end
def test_preview_q_no_match def test_preview_q_no_match
tmux.send_keys %(: | #{FZF} --preview 'echo foo {q}'), :Enter tmux.send_keys %(: | #{FZF} --preview 'echo foo {q} foo'), :Enter
tmux.until { |lines| assert_equal 0, lines.match_count } tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.until { |lines| refute_includes lines[1], ' foo ' } tmux.until { |lines| assert_includes lines[1], ' foo foo' }
tmux.send_keys 'bar' tmux.send_keys 'bar'
tmux.until { |lines| assert_includes lines[1], ' foo bar ' } tmux.until { |lines| assert_includes lines[1], ' foo bar foo' }
tmux.send_keys 'C-u' tmux.send_keys 'C-u'
tmux.until { |lines| refute_includes lines[1], ' foo ' } tmux.until { |lines| assert_includes lines[1], ' foo foo' }
end end
def test_preview_q_no_match_with_initial_query def test_preview_q_no_match_with_initial_query
@@ -1582,6 +1658,11 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal '> 1', lines[-2] } tmux.until { |lines| assert_equal '> 1', lines[-2] }
end end
def test_info_inline_separator
tmux.send_keys 'seq 10 | fzf --info=inline:___ --no-separator', :Enter
tmux.until { |lines| assert_equal '> ___10/10', lines[-1] }
end
def test_change_first_last def test_change_first_last
tmux.send_keys %(seq 1000 | #{FZF} --bind change:first,alt-Z:last), :Enter tmux.send_keys %(seq 1000 | #{FZF} --bind change:first,alt-Z:last), :Enter
tmux.until { |lines| assert_equal 1000, lines.match_count } tmux.until { |lines| assert_equal 1000, lines.match_count }
@@ -1600,6 +1681,30 @@ class TestGoFZF < TestBase
tmux.send_keys :Enter tmux.send_keys :Enter
end end
def test_pos
tmux.send_keys %(seq 1000 | #{FZF} --bind 'a:pos(3),b:pos(-3),c:pos(1),d:pos(-1),e:pos(0)' --preview 'echo {}/{}'), :Enter
tmux.until { |lines| assert_equal 1000, lines.match_count }
tmux.send_keys :a
tmux.until { |lines| assert_includes lines[1], ' 3/3' }
tmux.send_keys :b
tmux.until { |lines| assert_includes lines[1], ' 998/998' }
tmux.send_keys :c
tmux.until { |lines| assert_includes lines[1], ' 1/1' }
tmux.send_keys :d
tmux.until { |lines| assert_includes lines[1], ' 1000/1000' }
tmux.send_keys :e
tmux.until { |lines| assert_includes lines[1], ' 1/1' }
end
def test_put
tmux.send_keys %(seq 1000 | #{FZF} --bind 'a:put+put,b:put+put(ravo)' --preview 'echo {q}/{q}'), :Enter
tmux.until { |lines| assert_equal 1000, lines.match_count }
tmux.send_keys :a
tmux.until { |lines| assert_includes lines[1], ' aa/aa' }
tmux.send_keys :b
tmux.until { |lines| assert_includes lines[1], ' aabravo/aabravo' }
end
def test_accept_non_empty def test_accept_non_empty
tmux.send_keys %(seq 1000 | #{fzf('--print-query --bind enter:accept-non-empty')}), :Enter tmux.send_keys %(seq 1000 | #{fzf('--print-query --bind enter:accept-non-empty')}), :Enter
tmux.until { |lines| assert_equal 1000, lines.match_count } tmux.until { |lines| assert_equal 1000, lines.match_count }
@@ -1760,6 +1865,93 @@ 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
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 '> foo', lines.last }
tmux.send_keys :Space, 'baz'
tmux.until { |lines| assert_equal '> foobarbaz', lines.last }
end
def test_transform_query
tmux.send_keys %{#{FZF} --bind 'ctrl-r:transform-query(rev <<< {q}),ctrl-u:transform-query: tr "[:lower:]" "[:upper:]" <<< {q}' --query bar}, :Enter
tmux.until { |lines| assert_equal '> bar', lines[-1] }
tmux.send_keys 'C-r'
tmux.until { |lines| assert_equal '> rab', lines[-1] }
tmux.send_keys 'C-u'
tmux.until { |lines| assert_equal '> RAB', lines[-1] }
end
def test_transform_prompt
tmux.send_keys %{#{FZF} --bind 'ctrl-r:transform-query(rev <<< {q}),ctrl-u:transform-query: tr "[:lower:]" "[:upper:]" <<< {q}' --query bar}, :Enter
tmux.until { |lines| assert_equal '> bar', lines[-1] }
tmux.send_keys 'C-r'
tmux.until { |lines| assert_equal '> rab', lines[-1] }
tmux.send_keys 'C-u'
tmux.until { |lines| assert_equal '> RAB', lines[-1] }
end
def test_clear_selection def test_clear_selection
tmux.send_keys %(seq 100 | #{FZF} --multi --bind space:clear-selection), :Enter tmux.send_keys %(seq 100 | #{FZF} --multi --bind space:clear-selection), :Enter
tmux.until { |lines| assert_equal 100, lines.match_count } tmux.until { |lines| assert_equal 100, lines.match_count }
@@ -1799,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
@@ -1888,8 +2080,69 @@ class TestGoFZF < TestBase
end end
def test_preview_window_follow def test_preview_window_follow
tmux.send_keys "#{FZF} --preview 'seq 1000 | nl' --preview-window down:noborder:follow", :Enter file = Tempfile.new('fzf-follow')
tmux.until { |lines| assert_equal '1000 1000', lines[-1].strip } file.sync = true
tmux.send_keys %(seq 100 | #{FZF} --preview 'tail -f "#{file.path}"' --preview-window follow --bind 'up:preview-up,down:preview-down,space:change-preview-window:follow|nofollow' --preview-window '~3'), :Enter
tmux.until { |lines| lines.item_count == 100 }
# Write to the temporary file, and check if the preview window is showing
# the last line of the file
3.times { file.puts _1 } # header lines
1000.times { file.puts _1 }
tmux.until { |lines| assert_includes lines[1], '/1003' }
tmux.until { |lines| assert_includes lines[-2], '999' }
# Scroll the preview window and fzf should stop following the file content
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines[-2], '998' }
file.puts 'foo', 'bar'
tmux.until do |lines|
assert_includes lines[1], '/1005'
assert_includes lines[-2], '998'
end
# Scroll back to the bottom and fzf should start following the file again
%w[999 foo bar].each do |item|
wait do
tmux.send_keys :Down
tmux.until { |lines| assert_includes lines[-2], item }
end
end
file.puts 'baz'
tmux.until do |lines|
assert_includes lines[1], '/1006'
assert_includes lines[-2], 'baz'
end
# Scroll upwards to stop following
tmux.send_keys :Up
wait { assert_includes lines[-2], 'bar' }
file.puts 'aaa'
tmux.until do |lines|
assert_includes lines[1], '/1007'
assert_includes lines[-2], 'bar'
end
# Manually enable following
tmux.send_keys :Space
tmux.until { |lines| assert_includes lines[-2], 'aaa' }
file.puts 'bbb'
tmux.until do |lines|
assert_includes lines[1], '/1008'
assert_includes lines[-2], 'bbb'
end
# Disable following
tmux.send_keys :Space
file.puts 'ccc', 'ddd'
tmux.until do |lines|
assert_includes lines[1], '/1010'
assert_includes lines[-2], 'bbb'
end
rescue StandardError
file.close
file.unlink
end end
def test_toggle_preview_wrap def test_toggle_preview_wrap
@@ -1966,11 +2219,7 @@ class TestGoFZF < TestBase
wait { refute system("pgrep -f #{script}") } wait { refute system("pgrep -f #{script}") }
ensure ensure
system("pkill -9 -f #{script}") system("pkill -9 -f #{script}")
begin FileUtils.rm_f(script)
File.unlink(script)
rescue StandardError
nil
end
end end
def test_kill_default_command_on_accept def test_kill_default_command_on_accept
@@ -1988,11 +2237,7 @@ class TestGoFZF < TestBase
wait { refute system("pgrep -f #{script}") } wait { refute system("pgrep -f #{script}") }
ensure ensure
system("pkill -9 -f #{script}") system("pkill -9 -f #{script}")
begin FileUtils.rm_f(script)
File.unlink(script)
rescue StandardError
nil
end
end end
def test_kill_reload_command_on_abort def test_kill_reload_command_on_abort
@@ -2013,11 +2258,7 @@ class TestGoFZF < TestBase
wait { refute system("pgrep -f #{script}") } wait { refute system("pgrep -f #{script}") }
ensure ensure
system("pkill -9 -f #{script}") system("pkill -9 -f #{script}")
begin FileUtils.rm_f(script)
File.unlink(script)
rescue StandardError
nil
end
end end
def test_kill_reload_command_on_accept def test_kill_reload_command_on_accept
@@ -2037,11 +2278,7 @@ class TestGoFZF < TestBase
wait { refute system("pgrep -f #{script}") } wait { refute system("pgrep -f #{script}") }
ensure ensure
system("pkill -9 -f #{script}") system("pkill -9 -f #{script}")
begin FileUtils.rm_f(script)
File.unlink(script)
rescue StandardError
nil
end
end end
def test_preview_header def test_preview_header
@@ -2115,6 +2352,15 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_includes lines[1], '4' } tmux.until { |lines| assert_includes lines[1], '4' }
end end
def test_reload_sync
tmux.send_keys "seq 100 | #{FZF} --bind 'load:reload-sync(sleep 1; seq 1000)+unbind(load)'", :Enter
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.send_keys '00'
tmux.until { |lines| assert_equal 1, lines.match_count }
# After 1 second
tmux.until { |lines| assert_equal 10, lines.match_count }
end
def test_scroll_off def test_scroll_off
tmux.send_keys "seq 1000 | #{FZF} --scroll-off=3 --bind l:last", :Enter tmux.send_keys "seq 1000 | #{FZF} --scroll-off=3 --bind l:last", :Enter
tmux.until { |lines| assert_equal 1000, lines.item_count } tmux.until { |lines| assert_equal 1000, lines.item_count }
@@ -2247,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 }
@@ -2341,19 +2620,42 @@ class TestGoFZF < TestBase
end end
def test_start_event def test_start_event
tmux.send_keys 'seq 100 | fzf --multi --sync --preview-window border-none --bind "start:select-all+last+preview(echo welcome)"', :Enter tmux.send_keys 'seq 100 | fzf --multi --sync --preview-window hidden:border-none --bind "start:select-all+last+preview(echo welcome)"', :Enter
tmux.until do |lines| tmux.until do |lines|
assert_match(/>100.*welcome/, lines[0]) assert_match(/>100.*welcome/, lines[0])
assert_includes(lines[-2], '100/100 (100)') assert_includes(lines[-2], '100/100 (100)')
end end
end end
def test_focus_event
tmux.send_keys 'seq 100 | fzf --bind "focus:transform-prompt(echo [[{}]]),?:unbind(focus)"', :Enter
tmux.until { |lines| assert_includes(lines[-1], '[[1]]') }
tmux.send_keys :Up
tmux.until { |lines| assert_includes(lines[-1], '[[2]]') }
tmux.send_keys :X
tmux.until { |lines| assert_includes(lines[-1], '[[]]') }
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
def test_labels_center def test_labels_center
tmux.send_keys ': | fzf --border --border-label foobar --preview : --preview-label barfoo', :Enter tmux.send_keys 'echo x | fzf --border --border-label foobar --preview : --preview-label barfoo --bind "space:change-border-label(foobarfoo)+change-preview-label(barfoobar),enter:transform-border-label(echo foo{}foo)+transform-preview-label(echo bar{}bar)"', :Enter
tmux.until do tmux.until do
assert_includes(_1[0], '─foobar─') assert_includes(_1[0], '─foobar─')
assert_includes(_1[1], '─barfoo─') assert_includes(_1[1], '─barfoo─')
end end
tmux.send_keys :space
tmux.until do
assert_includes(_1[0], '─foobarfoo─')
assert_includes(_1[1], '─barfoobar─')
end
tmux.send_keys :Enter
tmux.until do
assert_includes(_1[0], '─fooxfoo─')
assert_includes(_1[1], '─barxbar─')
end
end end
def test_labels_left def test_labels_left
@@ -2404,6 +2706,253 @@ class TestGoFZF < TestBase
tmux.send_keys 'seq 100 | fzf -q55 --no-separator', :Enter tmux.send_keys 'seq 100 | fzf -q55 --no-separator', :Enter
tmux.until { assert(_1[-2] == ' 1/100') } tmux.until { assert(_1[-2] == ' 1/100') }
end end
def test_info_right
tmux.send_keys "#{FZF} --info=right --separator x --bind 'start:reload:seq 100; sleep 10'", :Enter
tmux.until { assert_match(%r{xxx [⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] 100/100}, _1[-2]) }
end
def test_info_inline_right
tmux.send_keys "#{FZF} --info=inline-right --bind 'start:reload:seq 100; sleep 10'", :Enter
tmux.until { assert_match(%r{[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] 100/100}, _1[-1]) }
end
def test_prev_next_selected
tmux.send_keys 'seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected', :Enter
tmux.until { |lines| assert_equal 10, lines.item_count }
tmux.send_keys :BTab, :BTab, :Up, :BTab
tmux.until { |lines| assert_equal 3, lines.select_count }
tmux.send_keys 'C-n'
tmux.until { |lines| assert_includes lines, '>>4' }
tmux.send_keys 'C-n'
tmux.until { |lines| assert_includes lines, '>>2' }
tmux.send_keys 'C-n'
tmux.until { |lines| assert_includes lines, '>>1' }
tmux.send_keys 'C-n'
tmux.until { |lines| assert_includes lines, '>>4' }
tmux.send_keys 'C-p'
tmux.until { |lines| assert_includes lines, '>>1' }
tmux.send_keys 'C-p'
tmux.until { |lines| assert_includes lines, '>>2' }
end
def test_listen
{ '--listen 6266' => -> { URI('http://localhost:6266') },
"--listen --sync --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'" =>
-> { URI("http://localhost:#{File.read('/tmp/fzf-port').chomp}") } }.each do |opts, fn|
tmux.send_keys "seq 10 | fzf #{opts}", :Enter
tmux.until { |lines| assert_equal 10, lines.item_count }
Net::HTTP.post(fn.call, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ')
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
teardown
setup
end
end
def test_toggle_alternative_preview_window
tmux.send_keys "seq 10 | #{FZF} --bind space:toggle-preview --preview-window '<100000(hidden,up,border-none)' --preview 'echo /{}/{}/'", :Enter
tmux.until { |lines| assert_equal 10, lines.item_count }
tmux.until { |lines| refute_includes lines, '/1/1/' }
tmux.send_keys :Space
tmux.until { |lines| assert_includes lines, '/1/1/' }
end
def test_become
tmux.send_keys "seq 100 | #{FZF} --bind 'enter:become:seq {} | #{FZF}'", :Enter
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.send_keys 999
tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal 99, lines.item_count }
end
def test_no_extra_newline_issue_3209
tmux.send_keys(%(seq 100 | #{FZF} --height 10 --preview-window up,wrap --preview 'printf "─%.0s" $(seq 1 "$((FZF_PREVIEW_COLUMNS - 5))"); printf $"\\e[7m%s\\e[0m" title; echo; echo something'), :Enter)
expected = <<~OUTPUT
something
3
2
> 1
100/100
>
OUTPUT
tmux.until { assert_block(expected, _1) }
end
def test_track
tmux.send_keys "seq 1000 | #{FZF} --query 555 --track --bind t:toggle-track", :Enter
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> 555'
end
tmux.send_keys :BSpace
index = tmux.until do |lines|
assert_equal 28, lines.match_count
assert_includes lines, '> 555'
end.index('> 555')
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 271, lines.match_count
assert_equal '> 555', lines[index]
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 1000, lines.match_count
assert_equal '> 555', lines[index]
end
tmux.send_keys '555'
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> 555'
assert_includes lines[-2], '+T'
end
tmux.send_keys 't'
tmux.until do |lines|
refute_includes lines[-2], '+T'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 28, lines.match_count
assert_includes lines, '> 55'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 271, lines.match_count
assert_includes lines, '> 5'
end
tmux.send_keys 't'
tmux.until do |lines|
assert_includes lines[-2], '+T'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 1000, lines.match_count
assert_includes lines, '> 5'
end
end
def test_track_action
tmux.send_keys "seq 1000 | #{FZF} --query 555 --bind t:track", :Enter
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> 555'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 28, lines.match_count
assert_includes lines, '> 55'
end
tmux.send_keys :t
tmux.until do |lines|
assert_includes lines[-2], '+T'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 271, lines.match_count
assert_includes lines, '> 55'
end
# Automatically disabled when the tracking item is no longer visible
tmux.send_keys '4'
tmux.until do |lines|
assert_equal 28, lines.match_count
refute_includes lines[-2], '+T'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 271, lines.match_count
assert_includes lines, '> 5'
end
tmux.send_keys :t
tmux.until do |lines|
assert_includes lines[-2], '+T'
end
tmux.send_keys :Up
tmux.until do |lines|
refute_includes lines[-2], '+T'
end
end
def test_one_and_zero
tmux.send_keys "seq 10 | #{FZF} --bind 'zero:preview(echo no match),one:preview(echo {} is the only match)'", :Enter
tmux.send_keys '1'
tmux.until do |lines|
assert_equal 2, lines.match_count
refute(lines.any? { _1.include?('only match') })
refute(lines.any? { _1.include?('no match') })
end
tmux.send_keys '0'
tmux.until do |lines|
assert_equal 1, lines.match_count
assert(lines.any? { _1.include?('only match') })
end
tmux.send_keys '0'
tmux.until do |lines|
assert_equal 0, lines.match_count
assert(lines.any? { _1.include?('no match') })
end
end
def test_height_range_with_exit_0
tmux.send_keys "seq 10 | #{FZF} --height ~10% --exit-0", :Enter
tmux.until { |lines| assert_equal 10, lines.item_count }
tmux.send_keys :c
tmux.until { |lines| assert_equal 0, lines.match_count }
end
def test_reload_and_change
tmux.send_keys "(echo foo; echo bar) | #{FZF} --bind 'load:reload-sync(sleep 60)+change-query(bar)'", :Enter
tmux.until { |lines| assert_equal 1, lines.match_count }
end
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
def test_disabled_preview_update
tmux.send_keys "echo bar | #{FZF} --disabled --bind 'change:reload:echo foo' --preview 'echo [{q}-{}]'", :Enter
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.until { |lines| assert(lines.any? { |line| line.include?('[-bar]') }) }
tmux.send_keys :x
tmux.until { |lines| assert(lines.any? { |line| line.include?('[x-foo]') }) }
end
end end
module TestShell module TestShell
@@ -2474,7 +3023,7 @@ module TestShell
tmux.prepare tmux.prepare
tmux.send_keys :Escape, :c tmux.send_keys :Escape, :c
lines = tmux.until { |lines| assert_operator lines.match_count, :>, 0 } lines = tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
expected = lines.reverse.find { |l| l.start_with?('> ') }[2..-1] expected = lines.reverse.find { |l| l.start_with?('> ') }[2..]
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.prepare tmux.prepare
tmux.send_keys :pwd, :Enter tmux.send_keys :pwd, :Enter
@@ -2527,16 +3076,16 @@ module TestShell
def test_ctrl_r_multiline def test_ctrl_r_multiline
tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..-1] } tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
tmux.prepare tmux.prepare
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..-1] } tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
end end
def test_ctrl_r_abort def test_ctrl_r_abort
@@ -2727,7 +3276,7 @@ module CompletionTest
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until(true) { |lines| assert_match(/cat .*fzf-unicode.*1.* .*fzf-unicode.*2/, lines[-1]) } tmux.until(true) { |lines| assert_match(/cat .*fzf-unicode.*1.* .*fzf-unicode.*2/, lines[-1]) }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| assert_equal %w[test3 test4], lines[-2..-1] } tmux.until { |lines| assert_equal %w[test3 test4], lines[-2..] }
end end
def test_custom_completion_api def test_custom_completion_api
@@ -2817,7 +3366,7 @@ class TestFish < TestBase
end end
def new_shell def new_shell
tmux.send_keys 'env FZF_TMUX=1 fish', :Enter tmux.send_keys 'env FZF_TMUX=1 FZF_DEFAULT_OPTS=--no-scrollbar fish', :Enter
tmux.send_keys 'function fish_prompt; end; clear', :Enter tmux.send_keys 'function fish_prompt; end; clear', :Enter
tmux.until { |lines| assert_empty lines } tmux.until { |lines| assert_empty lines }
end end
@@ -2836,6 +3385,8 @@ unset <%= UNSETS.join(' ') %>
unset $(env | sed -n /^_fzf_orig/s/=.*//p) unset $(env | sed -n /^_fzf_orig/s/=.*//p)
unset $(declare -F | sed -n "/_fzf/s/.*-f //p") unset $(declare -F | sed -n "/_fzf/s/.*-f //p")
export FZF_DEFAULT_OPTS=--no-scrollbar
# Setup fzf # Setup fzf
# --------- # ---------
if [[ ! "$PATH" == *<%= BASE %>/bin* ]]; then if [[ ! "$PATH" == *<%= BASE %>/bin* ]]; then

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"