Compare commits

...

88 Commits

Author SHA1 Message Date
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
32 changed files with 2207 additions and 802 deletions

View File

@@ -27,13 +27,13 @@ jobs:
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.0.0
ruby-version: 3.1.0
- name: Install packages
run: sudo apt-get install --yes zsh fish tmux
- 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
run: rubocop --require rubocop-minitest --require rubocop-performance

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ Advanced fzf examples
* [Toggling between data sources](#toggling-between-data-sources)
* [Ripgrep integration](#ripgrep-integration)
* [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter)
* [Using fzf as 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 between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode)
* [Log tailing](#log-tailing)
@@ -310,7 +310,7 @@ I know it's a lot to digest, let's try to break down the code.
- Once we selected a line, we open the file with `vim` (`vim
"${selected[0]}"`) and move the cursor to the line (`+${selected[1]}`).
### Using fzf as interative Ripgrep launcher
### Using fzf as interactive Ripgrep launcher
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

View File

@@ -1,6 +1,156 @@
CHANGELOG
=========
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 `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

View File

@@ -1,6 +1,6 @@
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
of this software and associated documentation files (the "Software"), to deal

View File

@@ -12,6 +12,9 @@ differ depending on the package manager.
" If installed using Homebrew
set rtp+=/usr/local/opt/fzf
" If installed using Homebrew on Apple Silicon
set rtp+=/opt/homebrew/opt/fzf
" If installed using git
set rtp+=~/.fzf
```
@@ -309,7 +312,7 @@ following options are allowed:
- `yoffset` [float default 0.5 range [0 ~ 1]]
- `xoffset` [float default 0.5 range [0 ~ 1]]
- `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]`
`fzf#wrap`
@@ -483,4 +486,4 @@ autocmd FileType fzf set laststatus=0 noshowmode noruler
The MIT License (MIT)
Copyright (c) 2013-2021 Junegunn Choi
Copyright (c) 2013-2023 Junegunn Choi

114
README.md
View File

@@ -25,11 +25,11 @@ Table of Contents
<!-- vim-markdown-toc GFM -->
* [Installation](#installation)
* [Using Homebrew](#using-homebrew)
* [Using git](#using-git)
* [Using Linux package managers](#using-linux-package-managers)
* [Windows](#windows)
* [As Vim plugin](#as-vim-plugin)
* [Using Homebrew](#using-homebrew)
* [Using git](#using-git)
* [Using Linux package managers](#using-linux-package-managers)
* [Windows](#windows)
* [As Vim plugin](#as-vim-plugin)
* [Upgrading fzf](#upgrading-fzf)
* [Building fzf](#building-fzf)
* [Usage](#usage)
@@ -52,16 +52,16 @@ Table of Contents
* [Custom fuzzy completion](#custom-fuzzy-completion)
* [Vim plugin](#vim-plugin)
* [Advanced topics](#advanced-topics)
* [Performance](#performance)
* [Executing external programs](#executing-external-programs)
* [Reloading the candidate list](#reloading-the-candidate-list)
* [1. Update the list of processes by pressing CTRL-R](#1-update-the-list-of-processes-by-pressing-ctrl-r)
* [2. Switch between sources by pressing CTRL-D or CTRL-F](#2-switch-between-sources-by-pressing-ctrl-d-or-ctrl-f)
* [3. Interactive ripgrep integration](#3-interactive-ripgrep-integration)
* [Preview window](#preview-window)
* [Performance](#performance)
* [Executing external programs](#executing-external-programs)
* [Reloading the candidate list](#reloading-the-candidate-list)
* [1. Update the list of processes by pressing CTRL-R](#1-update-the-list-of-processes-by-pressing-ctrl-r)
* [2. Switch between sources by pressing CTRL-D or CTRL-F](#2-switch-between-sources-by-pressing-ctrl-d-or-ctrl-f)
* [3. Interactive ripgrep integration](#3-interactive-ripgrep-integration)
* [Preview window](#preview-window)
* [Tips](#tips)
* [Respecting `.gitignore`](#respecting-gitignore)
* [Fish shell](#fish-shell)
* [Respecting `.gitignore`](#respecting-gitignore)
* [Fish shell](#fish-shell)
* [Related projects](#related-projects)
* [License](#license)
@@ -115,7 +115,7 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
| Package Manager | Linux Distribution | Command |
| --- | --- | --- |
| 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` |
| DNF | Fedora | `sudo dnf install fzf` |
| Nix | NixOS, etc. | `nix-env -iA nixpkgs.fzf` |
@@ -129,7 +129,7 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
> :warning: **Key bindings (CTRL-T / CTRL-R / ALT-C) and fuzzy auto-completion
> 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)
@@ -202,7 +202,7 @@ files excluding hidden ones. (You can override the default command with
vim $(fzf)
```
#### Using the finder
### Using the finder
- `CTRL-K` / `CTRL-J` (or `CTRL-P` / `CTRL-N`) to move cursor up and down
- `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit
@@ -211,7 +211,7 @@ vim $(fzf)
- Mouse: scroll, click, double-click; shift-click and shift-scroll on
multi-select mode
#### Layout
### Layout
fzf by default starts in fullscreen mode, but you can make it start below the
cursor with `--height` option.
@@ -234,7 +234,7 @@ default. For example,
export FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border'
```
#### Search syntax
### Search syntax
Unless otherwise specified, fzf starts in "extended-search mode" where you can
type in multiple search terms delimited by spaces. e.g. `^music .mp3$ sbtrkt
@@ -262,7 +262,7 @@ or `py`.
^core go$ | rb$ | py$
```
#### Environment variables
### Environment variables
- `FZF_DEFAULT_COMMAND`
- Default command to use when input is tty
@@ -278,11 +278,11 @@ or `py`.
- Default options
- e.g. `export FZF_DEFAULT_OPTS="--layout=reverse --inline-info"`
#### Options
### Options
See the man page (`man fzf`) for the full list of options.
#### Demo
### Demo
If you learn by watching videos, check out this screencast by [@samoshkin](https://github.com/samoshkin) to explore `fzf` features.
<a title="fzf - command-line fuzzy finder" href="https://www.youtube.com/watch?v=qgG5Jhi_Els">
@@ -333,17 +333,37 @@ fish.
- `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_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
- If you want to see the commands in chronological order, press `CTRL-R`
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
- 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
a tmux popup window by setting `FZF_TMUX_OPTS` (e.g. `-d 40%`).
a tmux popup window by setting `FZF_TMUX_OPTS` (e.g. `export FZF_TMUX_OPTS='-p80%,60%'`).
See `fzf-tmux --help` for available options.
More tips can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/Configuring-shell-key-bindings).
@@ -351,7 +371,7 @@ More tips can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/C
Fuzzy completion for bash and zsh
---------------------------------
#### Files and directories
### Files and directories
Fuzzy completion for files and directories can be triggered if the word before
the cursor ends with the trigger sequence, which is by default `**`.
@@ -380,7 +400,7 @@ cd **<TAB>
cd ~/github/fzf**<TAB>
```
#### Process IDs
### Process IDs
Fuzzy completion for PIDs is provided for kill command.
@@ -389,7 +409,7 @@ Fuzzy completion for PIDs is provided for kill command.
kill -9 **<TAB>
```
#### Host names
### Host names
For ssh and telnet commands, fuzzy completion for hostnames is provided. The
names are extracted from /etc/hosts and ~/.ssh/config.
@@ -399,7 +419,7 @@ ssh **<TAB>
telnet **<TAB>
```
#### Environment variables / Aliases
### Environment variables / Aliases
```sh
unset **<TAB>
@@ -407,7 +427,7 @@ export **<TAB>
unalias **<TAB>
```
#### Settings
### Settings
```sh
# Use ~~ as the trigger sequence instead of the default **
@@ -429,7 +449,7 @@ _fzf_compgen_dir() {
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.
# - You should make sure to pass the rest of the arguments to fzf.
_fzf_comprun() {
@@ -437,15 +457,15 @@ _fzf_comprun() {
shift
case "$command" in
cd) fzf "$@" --preview 'tree -C {} | head -200' ;;
export|unset) fzf "$@" --preview "eval 'echo \$'{}" ;;
ssh) fzf "$@" --preview 'dig {}' ;;
*) fzf "$@" ;;
cd) fzf --preview 'tree -C {} | head -200' "$@" ;;
export|unset) fzf --preview "eval 'echo \$'{}" "$@" ;;
ssh) fzf --preview 'dig {}' "$@" ;;
*) fzf --preview 'bat -n --color=always {}' "$@" ;;
esac
}
```
#### Supported commands
### Supported commands
On bash, fuzzy completion is enabled only for a predefined set of commands
(`complete | grep _fzf` to see the list). But you can enable it for other
@@ -457,7 +477,7 @@ _fzf_setup_completion path ag git kubectl
_fzf_setup_completion dir tree
```
#### Custom fuzzy completion
### Custom fuzzy completion
_**(Custom completion API is experimental and subject to change)**_
@@ -517,9 +537,8 @@ Advanced topics
### Performance
fzf is fast and is [getting even faster][perf]. Performance should not be
a problem in most use cases. However, you might want to be aware of the
options that affect performance.
fzf is fast. Performance should not be a problem in most use cases. However,
you might want to be aware of the options that can affect performance.
- `--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
@@ -527,12 +546,6 @@ options that affect performance.
- `--nth` makes fzf slower because it has to tokenize each line.
- `--with-nth` makes fzf slower as fzf has to tokenize and reassemble each
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
@@ -591,7 +604,7 @@ 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
`|| 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.
### Preview window
@@ -612,7 +625,7 @@ syntax-highlights the content of a file, such as
[Highlight](http://www.andre-simon.de/doku/highlight/en/highlight.php):
```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
@@ -622,6 +635,7 @@ You can customize the size, position, and border of the preview window using
```bash
fzf --height 40% --layout reverse --info inline --border \
--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'
```
@@ -708,4 +722,4 @@ https://github.com/junegunn/fzf/wiki/Related-projects
The MIT License (MIT)
Copyright (c) 2013-2021 Junegunn Choi
Copyright (c) 2013-2023 Junegunn Choi

View File

@@ -325,7 +325,7 @@ following options are allowed:
- `yoffset` [float default 0.5 range [0 ~ 1]]
- `xoffset` [float default 0.5 range [0 ~ 1]]
- `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]`
@@ -506,7 +506,7 @@ LICENSE *fzf-license*
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:

8
go.mod
View File

@@ -1,8 +1,8 @@
module github.com/junegunn/fzf
require (
github.com/gdamore/tcell/v2 v2.5.3
github.com/mattn/go-isatty v0.0.16
github.com/gdamore/tcell/v2 v2.5.4
github.com/mattn/go-isatty v0.0.17
github.com/mattn/go-runewidth v0.0.14
github.com/mattn/go-shellwords v1.0.12
github.com/rivo/uniseg v0.4.2
@@ -14,8 +14,8 @@ require (
require (
github.com/gdamore/encoding v1.0.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/text v0.3.7 // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
golang.org/x/text v0.5.0 // indirect
)
go 1.17

33
go.sum
View File

@@ -1,12 +1,11 @@
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/tcell/v2 v2.5.3 h1:b9XQrT6QGbgI7JvZOJXFNczOQeIYbo8BfeSMzt2sAV0=
github.com/gdamore/tcell/v2 v2.5.3/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo=
github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k=
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/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
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-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
@@ -16,17 +15,33 @@ github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/saracen/walker v0.1.3 h1:YtcKKmpRPy6XJTHJ75J2QYXXZYWnZNQxPCVqZSHVV/g=
github.com/saracen/walker v0.1.3/go.mod h1:FU+7qU8DeQQgSZDmmThMJi93kPkLFgy0oVAcLxurjIk=
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-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-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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/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.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-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
version=0.35.1
version=0.37.0
auto_completion=
key_bindings=
update_config=2
@@ -176,6 +176,7 @@ case "$archi" in
Linux\ armv8*) 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\ ppc64le) download fzf-$version-linux_ppc64le.tar.gz ;;
Linux\ *64) download fzf-$version-linux_amd64.tar.gz ;;
FreeBSD\ *64) download fzf-$version-freebsd_amd64.tar.gz ;;
OpenBSD\ *64) download fzf-$version-openbsd_amd64.tar.gz ;;

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
.ig
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
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
THE SOFTWARE.
..
.TH fzf-tmux 1 "Nov 2022" "fzf 0.35.1" "fzf-tmux - open fzf in tmux split pane"
.TH fzf-tmux 1 "Jan 2023" "fzf 0.37.0" "fzf-tmux - open fzf in tmux split pane"
.SH NAME
fzf-tmux - open fzf in tmux split pane

View File

@@ -1,7 +1,7 @@
.ig
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
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
THE SOFTWARE.
..
.TH fzf 1 "Nov 2022" "fzf 0.35.1" "fzf - a command-line fuzzy finder"
.TH fzf 1 "Jan 2023" "fzf 0.37.0" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -230,6 +230,10 @@ Draw border around the finder
.BR none
.br
If you use a terminal emulator where each box-drawing character takes
2 columns, try setting \fBRUNEWIDTH_EASTASIAN\fR to \fB1\fR. If the border is
still not properly rendered, set \fB--no-unicode\fR.
.TP
.BI "--border-label" [=LABEL]
Label to print on the horizontal border line. Should be used with one of the
@@ -333,11 +337,13 @@ e.g.
Determines the display style of finder info (match counters).
.br
.BR default " Display on the next line to the prompt"
.BR default " Display on the next line to the prompt"
.br
.BR inline " Display on the same line"
.BR inline " Display on the same line with the default separator ' < '"
.br
.BR hidden " Do not display finder info"
.BR inline:SEPARATOR " Display on the same line with a non-default separator"
.br
.BR hidden " Do not display finder info"
.br
.TP
@@ -356,6 +362,15 @@ ANSI color codes are supported.
Do not display horizontal separator on the info line. A synonym for
\fB--separator=''\fB
.TP
.BI "--scrollbar=" "CHAR"
Use the given character to render scrollbar. (default: '│' or ':' depending on
\fB--no-unicode\fR).
.TP
.B "--no-scrollbar"
Do not display scrollbar. A synonym for \fB--scrollbar=''\fB
.TP
.BI "--prompt=" "STR"
Input prompt (default: '> ')
@@ -404,26 +419,28 @@ color mappings.
\fBbw \fRNo colors (equivalent to \fB--no-color\fR)
.B COLOR NAMES:
\fBfg \fRText
\fBbg \fRBackground
\fBpreview-fg \fRPreview window text
\fBpreview-bg \fRPreview window background
\fBhl \fRHighlighted substrings
\fBfg+ \fRText (current line)
\fBbg+ \fRBackground (current line)
\fBgutter \fRGutter on the left (defaults to \fBbg+\fR)
\fBhl+ \fRHighlighted substrings (current line)
\fBquery \fRQuery string
\fBdisabled \fRQuery string when search is disabled
\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)
\fBlabel \fRBorder label (\fB--border-label\fR and \fB--preview-label\fR)
\fBprompt \fRPrompt
\fBpointer \fRPointer to the current line
\fBmarker \fRMulti-select marker
\fBspinner \fRStreaming input indicator
\fBheader \fRHeader
\fBfg \fRText
\fBpreview-fg \fRPreview window text
\fBbg \fRBackground
\fBpreview-bg \fRPreview window background
\fBhl \fRHighlighted substrings
\fBfg+ \fRText (current line)
\fBbg+ \fRBackground (current line)
\fBgutter \fRGutter on the left
\fBhl+ \fRHighlighted substrings (current line)
\fBquery \fRQuery string
\fBdisabled \fRQuery string when search is disabled (\fB--disabled\fR)
\fBinfo \fRInfo line (match counters)
\fBborder \fRBorder around the window (\fB--border\fR and \fB--preview\fR)
\fBseparator \fRHorizontal separator on info line
\fBscrollbar \fRScrollbar
\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
\fBpointer \fRPointer to the current line
\fBmarker \fRMulti-select marker
\fBspinner \fRStreaming input indicator
\fBheader \fRHeader
.B ANSI COLORS:
\fB-1 \fRDefault terminal foreground/background color
@@ -481,7 +498,7 @@ Use black background
.BI "--history=" "HISTORY_FILE"
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
\fBnext-history\fR and \fBprevious-history\fR.
\fBnext-history\fR and \fBprev-history\fR.
.TP
.BI "--history-size=" "N"
Maximum number of entries in the history file (default: 1000). The file is
@@ -535,7 +552,8 @@ e.g.
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
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
completes. ANSI escape sequence for clearing the display (\fBCSI 2 J\fR) is
@@ -556,9 +574,9 @@ 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.
.br
.B * border-rounded (default)
.B * border-rounded (default on non-Windows platforms)
.br
.B * border-sharp
.B * border-sharp (default on Windows)
.br
.B * border-bold
.br
@@ -720,6 +738,18 @@ ncurses finder only after the input stream is complete.
e.g. \fBfzf --multi | fzf --sync\fR
.RE
.TP
.B "--listen=HTTP_PORT"
Start HTTP server on the given port. It allows external processes to send
actions to perform via POST method.
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> )'
\fR
.TP
.B "--version"
Display version information and exit
@@ -914,6 +944,15 @@ e.g.
\fB# Move cursor to the last item and select all items
seq 1000 | fzf --multi --sync --bind start:last+select-all\fR
.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
.RS
Triggered whenever the query string is changed
@@ -922,6 +961,22 @@ e.g.
\fB# Move cursor to the first entry whenever the query is changed
fzf --bind change:first\fR
.RE
\fIfocus\fR
.RS
Triggered when the focus changes due to a vertical cursor movement or a search
result update.
e.g.
\fBfzf --bind 'focus:transform-preview-label:echo [ {} ]' --preview 'cat {}'
# Any action bound to the event runs synchronously and thus can make the interface sluggish
# e.g. lolcat isn't one of the fastest programs, and every cursor movement in
# fzf will be noticeably affected by its execution time
fzf --bind 'focus:transform-preview-label:echo [ {} ] | lolcat -f' --preview 'cat {}'
# Beware not to introduce an infinite loop
seq 10 | fzf --bind 'focus:up' --cycle\fR
.RE
\fIbackward-eof\fR
.RS
@@ -935,81 +990,93 @@ e.g.
.SS AVAILABLE ACTIONS:
A key or an event can be bound to one or more of the following actions.
\fBACTION: DEFAULT BINDINGS (NOTES):
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIenter double-click\fR
\fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
\fBbackward-char\fR \fIctrl-b left\fR
\fBbackward-delete-char\fR \fIctrl-h bspace\fR
\fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty)
\fBbackward-kill-word\fR \fIalt-bs\fR
\fBbackward-word\fR \fIalt-b shift-left\fR
\fBbeginning-of-line\fR \fIctrl-a home\fR
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
\fBchange-preview(...)\fR (change \fB--preview\fR option)
\fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|')
\fBchange-prompt(...)\fR (change prompt to the given string)
\fBclear-screen\fR \fIctrl-l\fR
\fBclear-selection\fR (clear multi-selection)
\fBclose\fR (close preview window if open, abort fzf otherwise)
\fBclear-query\fR (clear query string)
\fBdelete-char\fR \fIdel\fR
\fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty)
\fBACTION: DEFAULT BINDINGS (NOTES):
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIenter double-click\fR
\fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
\fBbackward-char\fR \fIctrl-b left\fR
\fBbackward-delete-char\fR \fIctrl-h bspace\fR
\fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty)
\fBbackward-kill-word\fR \fIalt-bs\fR
\fBbackward-word\fR \fIalt-b shift-left\fR
\fBbeginning-of-line\fR \fIctrl-a home\fR
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
\fBchange-border-label(...)\fR (change \fB--border-label\fR to the given string)
\fBchange-preview(...)\fR (change \fB--preview\fR option)
\fBchange-preview-label(...)\fR (change \fB--preview-label\fR to the given string)
\fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|')
\fBchange-prompt(...)\fR (change prompt to the given string)
\fBchange-query(...)\fR (change query string to the given string)
\fBclear-screen\fR \fIctrl-l\fR
\fBclear-selection\fR (clear multi-selection)
\fBclose\fR (close preview window if open, abort fzf otherwise)
\fBclear-query\fR (clear query string)
\fBdelete-char\fR \fIdel\fR
\fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty)
\fBdeselect\fR
\fBdeselect-all\fR (deselect all matches)
\fBdisable-search\fR (disable search functionality)
\fBdown\fR \fIctrl-j ctrl-n down\fR
\fBenable-search\fR (enable search functionality)
\fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details)
\fBexecute-silent(...)\fR (see below for the details)
\fBfirst\fR (move to the first match)
\fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR
\fBdeselect-all\fR (deselect all matches)
\fBdisable-search\fR (disable search functionality)
\fBdown\fR \fIctrl-j ctrl-n down\fR
\fBenable-search\fR (enable search functionality)
\fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details)
\fBexecute-silent(...)\fR (see below for the details)
\fBfirst\fR (move to the first match; same as \fBpos(1)\fR)
\fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR
\fBignore\fR
\fBjump\fR (EasyMotion-like 2-keystroke movement)
\fBjump-accept\fR (jump and accept)
\fBjump\fR (EasyMotion-like 2-keystroke movement)
\fBjump-accept\fR (jump and accept)
\fBkill-line\fR
\fBkill-word\fR \fIalt-d\fR
\fBlast\fR (move to the last match)
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
\fBpage-down\fR \fIpgdn\fR
\fBpage-up\fR \fIpgup\fR
\fBkill-word\fR \fIalt-d\fR
\fBlast\fR (move to the last match; same as \fBpos(-1)\fR)
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
\fBnext-selected\fR (move to the next selected item)
\fBpage-down\fR \fIpgdn\fR
\fBpage-up\fR \fIpgup\fR
\fBhalf-page-down\fR
\fBhalf-page-up\fR
\fBpreview(...)\fR (see below for the details)
\fBpreview-down\fR \fIshift-down\fR
\fBpreview-up\fR \fIshift-up\fR
\fBpos(...)\fR (move cursor to the numeric position; negative number to count from the end)
\fBprev-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBprev-selected\fR (move to the previous selected item)
\fBpreview(...)\fR (see below for the details)
\fBpreview-down\fR \fIshift-down\fR
\fBpreview-up\fR \fIshift-up\fR
\fBpreview-page-down\fR
\fBpreview-page-up\fR
\fBpreview-half-page-down\fR
\fBpreview-half-page-up\fR
\fBpreview-bottom\fR
\fBpreview-top\fR
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBprint-query\fR (print query and exit)
\fBput\fR (put the character to the prompt)
\fBprint-query\fR (print query and exit)
\fBput\fR (put the character to the prompt)
\fBput(...)\fR (put the given string to the prompt)
\fBrefresh-preview\fR
\fBrebind(...)\fR (rebind bindings after \fBunbind\fR)
\fBreload(...)\fR (see below for the details)
\fBreplace-query\fR (replace query string with the current selection)
\fBrebind(...)\fR (rebind bindings after \fBunbind\fR)
\fBreload(...)\fR (see below for the details)
\fBreload-sync(...)\fR (see below for the details)
\fBreplace-query\fR (replace query string with the current selection)
\fBselect\fR
\fBselect-all\fR (select all matches)
\fBtoggle\fR (\fIright-click\fR)
\fBtoggle-all\fR (toggle all matches)
\fBtoggle+down\fR \fIctrl-i (tab)\fR
\fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
\fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
\fBselect-all\fR (select all matches)
\fBtoggle\fR (\fIright-click\fR)
\fBtoggle-all\fR (toggle all matches)
\fBtoggle+down\fR \fIctrl-i (tab)\fR
\fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
\fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
\fBtoggle-preview\fR
\fBtoggle-preview-wrap\fR
\fBtoggle-search\fR (toggle search functionality)
\fBtoggle-search\fR (toggle search functionality)
\fBtoggle-sort\fR
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
\fBunbind(...)\fR (unbind bindings)
\fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR
\fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
\fBtransform-border-label(...)\fR (transform border label using an external command)
\fBtransform-preview-label(...)\fR (transform preview label using an external command)
\fBtransform-prompt(...)\fR (transform prompt string using an external command)
\fBtransform-query(...)\fR (transform query string using an external command)
\fBunbind(...)\fR (unbind bindings)
\fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR
\fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR
.SS ACTION COMPOSITION
@@ -1032,6 +1099,8 @@ that case, you can use any of the following alternative notations to avoid
parse errors.
\fBaction-name[...]\fR
\fBaction-name{...}\fR
\fBaction-name<...>\fR
\fBaction-name~...~\fR
\fBaction-name!...!\fR
\fBaction-name@...@\fR
@@ -1092,6 +1161,16 @@ e.g.
fzf --bind "change:reload:$RG_PREFIX {q} || true" \\
--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
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
"
@@ -511,7 +511,10 @@ try
let height = s:calc_size(&lines, dict.down, dict)
let optstr .= ' --height='.height
endif
let optstr .= s:border_opt(get(dict, 'window', 0))
" Respect --border option given in 'options'
if stridx(optstr, '--border') < 0 && stridx(optstr, '--no-border') < 0
let optstr .= s:border_opt(get(dict, 'window', 0))
endif
let prev_default_command = $FZF_DEFAULT_COMMAND
if len(source_command)
let $FZF_DEFAULT_COMMAND = source_command
@@ -755,9 +758,9 @@ function! s:border_opt(window)
endif
" Border style
let style = tolower(get(a:window, 'border', 'rounded'))
if !has_key(a:window, 'border') && !get(a:window, 'rounded', 1)
let style = 'sharp'
let style = tolower(get(a:window, 'border', ''))
if !has_key(a:window, 'border') && has_key(a:window, 'rounded')
let style = a:window.rounded ? 'rounded' : 'sharp'
endif
if style == 'none' || style == 'no'
return ''
@@ -765,7 +768,7 @@ function! s:border_opt(window)
" For --border styles, we need fzf 0.24.0 or above
call fzf#exec('0.24.0')
let opt = ' --border=' . style
let opt = ' --border ' . style
if has_key(a:window, 'highlight')
let color = s:get_color('fg', a:window.highlight)
if len(color)
@@ -827,6 +830,17 @@ if exists(':tnoremap')
tnoremap <silent> <Plug>(fzf-normal) <C-\><C-n>
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
let winrest = winrestcmd()
let pbuf = bufnr('')
@@ -896,6 +910,7 @@ function! s:execute_term(dict, command, temps) abort
endif
let command .= s:term_marker
if has('nvim')
call s:handle_ambidouble(fzf)
call termopen(command, fzf)
else
let term_opts = {'exit_cb': function(fzf.on_exit)}
@@ -907,6 +922,7 @@ function! s:execute_term(dict, command, temps) abort
else
let term_opts.curwin = 1
endif
call s:handle_ambidouble(term_opts)
let fzf.buf = term_start([&shell, &shellcmdflag, command], term_opts)
if is_popup && exists('#TerminalWinOpen')
doautocmd <nomodeline> TerminalWinOpen

View File

@@ -310,7 +310,7 @@ complete -o default -F _fzf_opts_completion fzf-tmux
d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}"
a_cmds="
awk cat diff diff3
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
sed sftp sort source tail tee uniq vi view vim wc xdg-open
basename bunzip2 bzip2 chmod chown curl cp dirname du

View File

@@ -1,6 +1,6 @@
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
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) {
pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0)
pos := posArray(withPos, len(pattern))
prevClass := charWhite
prevClass := initialCharClass
if sidx > 0 {
prevClass = charClassOf(text.Get(sidx - 1))
}

View File

@@ -213,15 +213,6 @@ func Run(opts *Options, version string, revision string) {
clearSelection := util.Once(false)
ticks := 0
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)
total := 0
query := []rune{}
@@ -236,6 +227,25 @@ func Run(opts *Options, version string, revision string) {
terminal.startChan <- fitpad{-1, -1}
}
}
useSnapshot := false
var snapshot []*Chunk
var prevSnapshot []*Chunk
var count int
restart := func(command string) {
reading = true
clearCache = util.Once(true)
clearSelection = util.Once(true)
// We should not update snapshot if reload is triggered again while
// the previous reload is in progress
if useSnapshot && prevSnapshot != nil {
snapshot, count = chunkList.Snapshot()
}
chunkList.Clear()
itemIndex = 0
header = make([]string, 0, opts.HeaderLines)
go reader.restart(command)
}
for {
delay := true
ticks++
@@ -267,7 +277,13 @@ func Run(opts *Options, version string, revision string) {
} else {
reading = reading && evt == EvtReadNew
}
snapshot, count := chunkList.Snapshot()
if useSnapshot && evt == EvtReadFin {
useSnapshot = false
prevSnapshot = nil
}
if !useSnapshot {
snapshot, count = chunkList.Snapshot()
}
total = count
terminal.UpdateCount(total, !reading, value.(*string))
if opts.Sync {
@@ -277,7 +293,7 @@ func Run(opts *Options, version string, revision string) {
if heightUnknown && !deferred {
determine(!reading)
}
reset := clearCache()
reset := !useSnapshot && clearCache()
matcher.Reset(snapshot, input(reset), false, !reading, sort, reset)
case EvtSearchNew:
@@ -286,6 +302,9 @@ func Run(opts *Options, version string, revision string) {
case searchRequest:
sort = val.sort
command = val.command
if command != nil {
useSnapshot = val.sync
}
}
if command != nil {
if reading {
@@ -296,8 +315,10 @@ func Run(opts *Options, version string, revision string) {
}
break
}
snapshot, _ := chunkList.Snapshot()
reset := clearCache()
if !useSnapshot {
snapshot, _ = chunkList.Snapshot()
}
reset := !useSnapshot && clearCache()
matcher.Reset(snapshot, input(reset), true, !reading, sort, reset)
delay = false

View File

@@ -70,9 +70,11 @@ const usage = `usage: fzf [options]
(default: 0 or center)
--margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L)
--padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
--info=STYLE Finder info style [default|inline|hidden]
--info=STYLE Finder info style [default|hidden|inline|inline:SEPARATOR]
--separator=STR String to form horizontal separator on info line
--no-separator Hide info line separator
--scrollbar[=CHAR] Scrollbar character
--no-scrollbar Hide scrollbar
--prompt=STR Input prompt (default: '> ')
--pointer=STR Pointer to the current line (default: '>')
--marker=STR Multi-select marker (default: '>')
@@ -113,6 +115,7 @@ const usage = `usage: fzf [options]
--read0 Read input delimited by ASCII NUL characters
--print0 Print output delimited by ASCII NUL characters
--sync Synchronous search for multi-staged filtering
--listen=HTTP_PORT Start HTTP server to receive actions (POST /)
--version Display version information and exit
Environment variables
@@ -122,6 +125,8 @@ const usage = `usage: fzf [options]
`
const defaultInfoSep = " < "
// Case denotes case-sensitivity of search
type Case int
@@ -204,6 +209,14 @@ type previewOpts struct {
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) {
opts.column = 0
opts.bottom = false
@@ -235,6 +248,10 @@ func (a previewOpts) sameContentLayout(b previewOpts) bool {
return a.wrap == b.wrap && a.headerLines == b.headerLines
}
func firstLine(s string) string {
return strings.SplitN(s, "\n", 2)[0]
}
// Options stores the values of command-line options
type Options struct {
Fuzzy bool
@@ -266,6 +283,7 @@ type Options struct {
ScrollOff int
FileWord bool
InfoStyle infoStyle
InfoSep string
Separator *string
JumpLabels string
Prompt string
@@ -289,6 +307,7 @@ type Options struct {
HeaderLines int
HeaderFirst bool
Ellipsis string
Scrollbar *string
Margin [4]sizeSpec
Padding [4]sizeSpec
BorderShape tui.BorderShape
@@ -296,12 +315,13 @@ type Options struct {
PreviewLabel labelOpts
Unicode bool
Tabstop int
ListenPort int
ClearOnExit bool
Version bool
}
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 {
@@ -357,6 +377,7 @@ func defaultOptions() *Options {
HeaderLines: 0,
HeaderFirst: false,
Ellipsis: "..",
Scrollbar: nil,
Margin: defaultMargin(),
Padding: defaultMargin(),
Unicode: true,
@@ -529,7 +550,7 @@ func parseBorder(str string, optional bool) tui.BorderShape {
return tui.BorderNone
default:
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)")
}
@@ -537,8 +558,13 @@ func parseBorder(str string, optional bool) tui.BorderShape {
}
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 {
errorExit(message)
exit(message)
return nil
}
str = regexp.MustCompile("(?i)(alt-),").ReplaceAllString(str, "$1"+string([]rune{escapedComma}))
@@ -588,6 +614,10 @@ func parseKeyChords(str string, message string) map[tui.Event]string {
add(tui.BackwardEOF)
case "start":
add(tui.Start)
case "load":
add(tui.Load)
case "focus":
add(tui.Focus)
case "alt-enter", "alt-return":
chords[tui.CtrlAltKey('m')] = key
case "alt-space":
@@ -670,7 +700,8 @@ func parseKeyChords(str string, message string) map[tui.Event]string {
} else if len(runes) == 1 {
chords[tui.Key(runes[0])] = key
} else {
errorExit("unsupported key: " + key)
exit("unsupported key: " + key)
return nil
}
}
}
@@ -843,8 +874,12 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
mergeAttr(&theme.Border)
case "separator":
mergeAttr(&theme.Separator)
case "scrollbar":
mergeAttr(&theme.Scrollbar)
case "label":
mergeAttr(&theme.BorderLabel)
case "preview-label":
mergeAttr(&theme.PreviewLabel)
case "prompt":
mergeAttr(&theme.Prompt)
case "spinner":
@@ -866,8 +901,9 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
}
var (
executeRegexp *regexp.Regexp
splitRegexp *regexp.Regexp
executeRegexp *regexp.Regexp
splitRegexp *regexp.Regexp
actionNameRegexp *regexp.Regexp
)
func firstKey(keymap map[tui.Event]string) tui.Event {
@@ -884,48 +920,271 @@ const (
)
func init() {
// Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
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)[:+](execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`)
splitRegexp = regexp.MustCompile("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
}
func parseKeymap(keymap map[tui.Event][]*action, str string) {
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
symbol := ":"
if strings.HasPrefix(src, "+") {
symbol = "+"
func maskActionContents(action string) string {
masked := ""
Loop:
for len(action) > 0 {
loc := executeRegexp.FindStringIndex(action)
if loc == nil {
masked += action
break
}
prefix := symbol + "execute"
if strings.HasPrefix(src[1:], "reload") {
prefix = symbol + "reload"
} else if strings.HasPrefix(src[1:], "change-preview-window") {
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"
}
masked += action[:loc[1]]
action = action[loc[1]:]
if len(action) == 0 {
break
}
return prefix + "(" + strings.Repeat(" ", len(src)-len(prefix)-2) + ")"
})
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]:]
}
masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1)
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -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
for i, maskedString := range maskedStrings {
originalStrings[i] = original[idx : idx+len(maskedString)]
idx += len(maskedString) + 1
}
actions := make([]*action, 0, len(maskedStrings))
appendAction := func(types ...actionType) {
actions = append(actions, toActions(types...)...)
}
prevSpec := ""
for specIndex, spec := range originalStrings {
spec = prevSpec + spec
specLower := strings.ToLower(spec)
switch specLower {
case "ignore":
appendAction(actIgnore)
case "beginning-of-line":
appendAction(actBeginningOfLine)
case "abort":
appendAction(actAbort)
case "accept":
appendAction(actAccept)
case "accept-non-empty":
appendAction(actAcceptNonEmpty)
case "print-query":
appendAction(actPrintQuery)
case "refresh-preview":
appendAction(actRefreshPreview)
case "replace-query":
appendAction(actReplaceQuery)
case "backward-char":
appendAction(actBackwardChar)
case "backward-delete-char":
appendAction(actBackwardDeleteChar)
case "backward-delete-char/eof":
appendAction(actBackwardDeleteCharEOF)
case "backward-word":
appendAction(actBackwardWord)
case "clear-screen":
appendAction(actClearScreen)
case "delete-char":
appendAction(actDeleteChar)
case "delete-char/eof":
appendAction(actDeleteCharEOF)
case "deselect":
appendAction(actDeselect)
case "end-of-line":
appendAction(actEndOfLine)
case "cancel":
appendAction(actCancel)
case "clear-query":
appendAction(actClearQuery)
case "clear-selection":
appendAction(actClearSelection)
case "forward-char":
appendAction(actForwardChar)
case "forward-word":
appendAction(actForwardWord)
case "jump":
appendAction(actJump)
case "jump-accept":
appendAction(actJumpAccept)
case "kill-line":
appendAction(actKillLine)
case "kill-word":
appendAction(actKillWord)
case "unix-line-discard", "line-discard":
appendAction(actUnixLineDiscard)
case "unix-word-rubout", "word-rubout":
appendAction(actUnixWordRubout)
case "yank":
appendAction(actYank)
case "backward-kill-word":
appendAction(actBackwardKillWord)
case "toggle-down":
appendAction(actToggle, actDown)
case "toggle-up":
appendAction(actToggle, actUp)
case "toggle-in":
appendAction(actToggleIn)
case "toggle-out":
appendAction(actToggleOut)
case "toggle-all":
appendAction(actToggleAll)
case "toggle-search":
appendAction(actToggleSearch)
case "select":
appendAction(actSelect)
case "select-all":
appendAction(actSelectAll)
case "deselect-all":
appendAction(actDeselectAll)
case "close":
appendAction(actClose)
case "toggle":
appendAction(actToggle)
case "down":
appendAction(actDown)
case "up":
appendAction(actUp)
case "first", "top":
appendAction(actFirst)
case "last":
appendAction(actLast)
case "page-up":
appendAction(actPageUp)
case "page-down":
appendAction(actPageDown)
case "half-page-up":
appendAction(actHalfPageUp)
case "half-page-down":
appendAction(actHalfPageDown)
case "prev-history", "previous-history":
appendAction(actPrevHistory)
case "next-history":
appendAction(actNextHistory)
case "prev-selected":
appendAction(actPrevSelected)
case "next-selected":
appendAction(actNextSelected)
case "toggle-preview":
appendAction(actTogglePreview)
case "toggle-preview-wrap":
appendAction(actTogglePreviewWrap)
case "toggle-sort":
appendAction(actToggleSort)
case "preview-top":
appendAction(actPreviewTop)
case "preview-bottom":
appendAction(actPreviewBottom)
case "preview-up":
appendAction(actPreviewUp)
case "preview-down":
appendAction(actPreviewDown)
case "preview-page-up":
appendAction(actPreviewPageUp)
case "preview-page-down":
appendAction(actPreviewPageDown)
case "preview-half-page-up":
appendAction(actPreviewHalfPageUp)
case "preview-half-page-down":
appendAction(actPreviewHalfPageDown)
case "enable-search":
appendAction(actEnableSearch)
case "disable-search":
appendAction(actDisableSearch)
case "put":
if putAllowed {
appendAction(actRune)
} else {
exit("unable to put non-printable character")
}
default:
t := isExecuteAction(specLower)
if t == actIgnore {
if specIndex == 0 && specLower == "" {
actions = append(prevActions, actions...)
} else {
exit("unknown action: " + spec)
}
} else {
offset := len(actionNameRegexp.FindString(spec))
var actionArg string
if spec[offset] == ':' {
if specIndex == len(originalStrings)-1 {
actionArg = spec[offset+1:]
actions = append(actions, &action{t: t, a: actionArg})
} else {
prevSpec = spec + "+"
continue
}
} else {
actionArg = spec[offset+1 : len(spec)-1]
actions = append(actions, &action{t: t, a: actionArg})
}
switch t {
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 = ""
}
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)]
@@ -933,7 +1192,7 @@ func parseKeymap(keymap map[tui.Event][]*action, str string) {
pair := strings.SplitN(pairStr, ":", 2)
if len(pair) < 2 {
errorExit("bind action not specified: " + origPairStr)
exit("bind action not specified: " + origPairStr)
}
var key tui.Event
if len(pair[0]) == 1 && pair[0][0] == escapedColon {
@@ -943,243 +1202,63 @@ func parseKeymap(keymap map[tui.Event][]*action, str string) {
} else if len(pair[0]) == 1 && pair[0][0] == escapedPlus {
key = tui.Key('+')
} else {
keys := parseKeyChords(pair[0], "key name required")
keys := parseKeyChordsImpl(pair[0], "key name required", exit)
key = firstKey(keys)
}
idx2 := len(pair[0]) + 1
specs := strings.Split(pair[1], "+")
actions := make([]*action, 0, len(specs))
appendAction := func(types ...actionType) {
actions = append(actions, toActions(types...)...)
}
prevSpec := ""
for specIndex, maskedSpec := range specs {
spec := origPairStr[idx2 : idx2+len(maskedSpec)]
idx2 += len(maskedSpec) + 1
spec = prevSpec + spec
specLower := strings.ToLower(spec)
switch specLower {
case "ignore":
appendAction(actIgnore)
case "beginning-of-line":
appendAction(actBeginningOfLine)
case "abort":
appendAction(actAbort)
case "accept":
appendAction(actAccept)
case "accept-non-empty":
appendAction(actAcceptNonEmpty)
case "print-query":
appendAction(actPrintQuery)
case "refresh-preview":
appendAction(actRefreshPreview)
case "replace-query":
appendAction(actReplaceQuery)
case "backward-char":
appendAction(actBackwardChar)
case "backward-delete-char":
appendAction(actBackwardDeleteChar)
case "backward-delete-char/eof":
appendAction(actBackwardDeleteCharEOF)
case "backward-word":
appendAction(actBackwardWord)
case "clear-screen":
appendAction(actClearScreen)
case "delete-char":
appendAction(actDeleteChar)
case "delete-char/eof":
appendAction(actDeleteCharEOF)
case "deselect":
appendAction(actDeselect)
case "end-of-line":
appendAction(actEndOfLine)
case "cancel":
appendAction(actCancel)
case "clear-query":
appendAction(actClearQuery)
case "clear-selection":
appendAction(actClearSelection)
case "forward-char":
appendAction(actForwardChar)
case "forward-word":
appendAction(actForwardWord)
case "jump":
appendAction(actJump)
case "jump-accept":
appendAction(actJumpAccept)
case "kill-line":
appendAction(actKillLine)
case "kill-word":
appendAction(actKillWord)
case "unix-line-discard", "line-discard":
appendAction(actUnixLineDiscard)
case "unix-word-rubout", "word-rubout":
appendAction(actUnixWordRubout)
case "yank":
appendAction(actYank)
case "backward-kill-word":
appendAction(actBackwardKillWord)
case "toggle-down":
appendAction(actToggle, actDown)
case "toggle-up":
appendAction(actToggle, actUp)
case "toggle-in":
appendAction(actToggleIn)
case "toggle-out":
appendAction(actToggleOut)
case "toggle-all":
appendAction(actToggleAll)
case "toggle-search":
appendAction(actToggleSearch)
case "select":
appendAction(actSelect)
case "select-all":
appendAction(actSelectAll)
case "deselect-all":
appendAction(actDeselectAll)
case "close":
appendAction(actClose)
case "toggle":
appendAction(actToggle)
case "down":
appendAction(actDown)
case "up":
appendAction(actUp)
case "first", "top":
appendAction(actFirst)
case "last":
appendAction(actLast)
case "page-up":
appendAction(actPageUp)
case "page-down":
appendAction(actPageDown)
case "half-page-up":
appendAction(actHalfPageUp)
case "half-page-down":
appendAction(actHalfPageDown)
case "previous-history":
appendAction(actPreviousHistory)
case "next-history":
appendAction(actNextHistory)
case "toggle-preview":
appendAction(actTogglePreview)
case "toggle-preview-wrap":
appendAction(actTogglePreviewWrap)
case "toggle-sort":
appendAction(actToggleSort)
case "preview-top":
appendAction(actPreviewTop)
case "preview-bottom":
appendAction(actPreviewBottom)
case "preview-up":
appendAction(actPreviewUp)
case "preview-down":
appendAction(actPreviewDown)
case "preview-page-up":
appendAction(actPreviewPageUp)
case "preview-page-down":
appendAction(actPreviewPageDown)
case "preview-half-page-up":
appendAction(actPreviewHalfPageUp)
case "preview-half-page-down":
appendAction(actPreviewHalfPageDown)
case "enable-search":
appendAction(actEnableSearch)
case "disable-search":
appendAction(actDisableSearch)
case "put":
if key.Type == tui.Rune && unicode.IsGraphic(key.Char) {
appendAction(actRune)
} else {
errorExit("unable to put non-printable character: " + pair[0])
}
default:
t := isExecuteAction(specLower)
if t == actIgnore {
if specIndex == 0 && specLower == "" {
actions = append(keymap[key], actions...)
} else {
errorExit("unknown action: " + spec)
}
} else {
var offset int
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
if spec[offset] == ':' {
if specIndex == len(specs)-1 {
actionArg = spec[offset+1:]
actions = append(actions, &action{t: t, a: actionArg})
} else {
prevSpec = spec + "+"
continue
}
} else {
actionArg = spec[offset+1 : len(spec)-1]
actions = append(actions, &action{t: t, a: actionArg})
}
if t == actUnbind || t == actRebind {
parseKeyChords(actionArg, spec[0:offset]+" target required")
}
}
}
prevSpec = ""
}
keymap[key] = actions
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 {
matches := executeRegexp.FindAllStringSubmatch(":"+str, -1)
if matches == nil || len(matches) != 1 {
masked := maskActionContents(":" + str)[1:]
if masked == str {
// Not masked
return actIgnore
}
prefix := matches[0][1]
if len(prefix) == 0 {
prefix = matches[0][2]
}
prefix := actionNameRegexp.FindString(str)
switch prefix {
case "reload":
return actReload
case "reload-sync":
return actReloadSync
case "unbind":
return actUnbind
case "rebind":
return actRebind
case "preview":
return actPreview
case "change-border-label":
return actChangeBorderLabel
case "change-preview-label":
return actChangePreviewLabel
case "change-preview-window":
return actChangePreviewWindow
case "change-preview":
return actChangePreview
case "change-prompt":
return actChangePrompt
case "change-query":
return actChangeQuery
case "pos":
return actPosition
case "execute":
return actExecute
case "execute-silent":
return actExecuteSilent
case "execute-multi":
return actExecuteMulti
case "put":
return actPut
case "transform-border-label":
return actTransformBorderLabel
case "transform-preview-label":
return actTransformPreviewLabel
case "transform-prompt":
return actTransformPrompt
case "transform-query":
return actTransformQuery
}
return actIgnore
}
@@ -1247,21 +1326,29 @@ func parseLayout(str string) layoutType {
return layoutDefault
}
func parseInfoStyle(str string) infoStyle {
func parseInfoStyle(str string) (infoStyle, string) {
switch str {
case "default":
return infoDefault
return infoDefault, ""
case "inline":
return infoInline
return infoInline, defaultInfoSep
case "hidden":
return infoHidden
return infoHidden, ""
default:
errorExit("invalid info style (expected: default|inline|hidden)")
prefix := "inline:"
if strings.HasPrefix(str, prefix) {
return infoInline, strings.ReplaceAll(str[len(prefix):], "\n", " ")
}
errorExit("invalid info style (expected: default|hidden|inline|inline:SEPARATOR)")
}
return infoDefault
return infoDefault, ""
}
func parsePreviewWindow(opts *previewOpts, input string) {
parsePreviewWindowImpl(opts, input, errorExit)
}
func parsePreviewWindowImpl(opts *previewOpts, input string, exit func(string)) {
tokenRegex := regexp.MustCompile(`[:,]*(<([1-9][0-9]*)\(([^)<]+)\)|[^,:]+)`)
sizeRegex := regexp.MustCompile("^[0-9]+%?$")
offsetRegex := regexp.MustCompile(`^(\+{-?[0-9]+})?([+-][0-9]+)*(-?/[1-9][0-9]*)?$`)
@@ -1333,7 +1420,8 @@ func parsePreviewWindow(opts *previewOpts, input string) {
} else if offsetRegex.MatchString(token) {
opts.scroll = token
} else {
errorExit("invalid preview window option: " + token)
exit("invalid preview window option: " + token)
return
}
}
}
@@ -1342,7 +1430,7 @@ func parsePreviewWindow(opts *previewOpts, input string) {
opts.alternative = &alternativeOpts
opts.alternative.hidden = false
opts.alternative.alternative = nil
parsePreviewWindow(opts.alternative, alternative)
parsePreviewWindowImpl(opts.alternative, alternative, exit)
}
}
@@ -1443,7 +1531,7 @@ func parseOptions(opts *Options, allArgs []string) {
case "--tiebreak":
opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
case "--bind":
parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required"))
parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required"), errorExit)
case "--color":
_, spec := optionalNextString(allArgs, &i)
if len(spec) == 0 {
@@ -1521,12 +1609,13 @@ func parseOptions(opts *Options, allArgs []string) {
case "--no-filepath-word":
opts.FileWord = false
case "--info":
opts.InfoStyle = parseInfoStyle(
opts.InfoStyle, opts.InfoSep = parseInfoStyle(
nextString(allArgs, &i, "info style required"))
case "--no-info":
opts.InfoStyle = infoHidden
case "--inline-info":
opts.InfoStyle = infoInline
opts.InfoSep = defaultInfoSep
case "--no-inline-info":
opts.InfoStyle = infoDefault
case "--separator":
@@ -1535,6 +1624,16 @@ func parseOptions(opts *Options, allArgs []string) {
case "--no-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":
opts.JumpLabels = nextString(allArgs, &i, "label characters required")
validateJumpLabels = true
@@ -1563,10 +1662,10 @@ func parseOptions(opts *Options, allArgs []string) {
case "--prompt":
opts.Prompt = nextString(allArgs, &i, "prompt string required")
case "--pointer":
opts.Pointer = nextString(allArgs, &i, "pointer sign string required")
opts.Pointer = firstLine(nextString(allArgs, &i, "pointer sign string required"))
validatePointer = true
case "--marker":
opts.Marker = nextString(allArgs, &i, "selected sign string required")
opts.Marker = firstLine(nextString(allArgs, &i, "selected sign string required"))
validateMarker = true
case "--sync":
opts.Sync = true
@@ -1645,6 +1744,10 @@ func parseOptions(opts *Options, allArgs []string) {
nextString(allArgs, &i, "padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
case "--tabstop":
opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
case "--listen":
opts.ListenPort = nextInt(allArgs, &i, "listen port required")
case "--no-listen":
opts.ListenPort = 0
case "--clear":
opts.ClearOnExit = true
case "--no-clear":
@@ -1677,10 +1780,10 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--prompt="); match {
opts.Prompt = value
} else if match, value := optString(arg, "--pointer="); match {
opts.Pointer = value
opts.Pointer = firstLine(value)
validatePointer = true
} else if match, value := optString(arg, "--marker="); match {
opts.Marker = value
opts.Marker = firstLine(value)
validateMarker = true
} else if match, value := optString(arg, "-n", "--nth="); match {
opts.Nth = splitNth(value)
@@ -1697,9 +1800,11 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--layout="); match {
opts.Layout = parseLayout(value)
} else if match, value := optString(arg, "--info="); match {
opts.InfoStyle = parseInfoStyle(value)
opts.InfoStyle, opts.InfoSep = parseInfoStyle(value)
} else if match, value := optString(arg, "--separator="); match {
opts.Separator = &value
} else if match, value := optString(arg, "--scrollbar="); match {
opts.Scrollbar = &value
} else if match, value := optString(arg, "--toggle-sort="); match {
parseToggleSort(opts.Keymap, value)
} else if match, value := optString(arg, "--expect="); match {
@@ -1711,7 +1816,7 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--color="); match {
opts.Theme = parseTheme(opts.Theme, value)
} 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 {
setHistory(value)
} else if match, value := optString(arg, "--history-size="); match {
@@ -1732,6 +1837,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Padding = parseMargin("padding", value)
} else if match, value := optString(arg, "--tabstop="); match {
opts.Tabstop = atoi(value)
} else if match, value := optString(arg, "--listen="); match {
opts.ListenPort = atoi(value)
} else if match, value := optString(arg, "--hscroll-off="); match {
opts.HscrollOff = atoi(value)
} else if match, value := optString(arg, "--scroll-off="); match {
@@ -1761,6 +1868,10 @@ func parseOptions(opts *Options, allArgs []string) {
errorExit("tab stop must be a positive integer")
}
if opts.ListenPort < 0 || opts.ListenPort > 65535 {
errorExit("invalid listen port")
}
if len(opts.JumpLabels) == 0 {
errorExit("empty jump labels")
}
@@ -1800,10 +1911,15 @@ func postProcessOptions(opts *Options) {
if !opts.Version && !tui.IsLightRendererSupported() && opts.Height.size > 0 {
errorExit("--height option is currently not supported on this platform")
}
if opts.Scrollbar != nil && runewidth.StringWidth(*opts.Scrollbar) > 1 {
errorExit("scrollbar display width should be 1")
}
// Default actions for CTRL-N / CTRL-P when --history is set
if opts.History != nil {
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 {
opts.Keymap[tui.CtrlN.AsEvent()] = toActions(actNextHistory)
@@ -1811,7 +1927,6 @@ func postProcessOptions(opts *Options) {
}
// Extend the default key map
previewEnabled := len(opts.Preview.command) > 0 || hasPreviewAction(opts)
keymap := defaultKeymap()
for key, actions := range opts.Keymap {
var lastChangePreviewWindow *action
@@ -1822,15 +1937,6 @@ func postProcessOptions(opts *Options) {
opts.ToggleSort = true
case actChangePreviewWindow:
lastChangePreviewWindow = 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)
}
}
}
@@ -1851,6 +1957,11 @@ func postProcessOptions(opts *Options) {
}
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 {
for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2]} {
if s.percent {

View File

@@ -262,13 +262,17 @@ func TestBind(t *testing.T) {
}
}
check(tui.CtrlA.AsEvent(), "", actBeginningOfLine)
errorString := ""
errorFn := func(e string) {
errorString = e
}
parseKeymap(keymap,
"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
"f1:execute(ls {+})+abort+execute(echo {+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
"f1:execute(ls {+})+abort+execute(echo \n{+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
"alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
"x:Execute(foo+bar),X:execute/bar+baz/"+
",f1:+first,f1:+top"+
",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up")
",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up", errorFn)
check(tui.CtrlA.AsEvent(), "", actKillLine)
check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown)
check(tui.Key('c'), "", actPageUp)
@@ -286,12 +290,15 @@ func TestBind(t *testing.T) {
check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute)
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)
}
parseKeymap(keymap, "f1:abort")
parseKeymap(keymap, "f1:abort", errorFn)
check(tui.F1.AsEvent(), "", actAbort)
if len(errorString) > 0 {
t.Errorf("error parsing keymap: %s", errorString)
}
}
func TestColorSpec(t *testing.T) {
@@ -354,10 +361,10 @@ func TestDefaultCtrlNP(t *testing.T) {
f.Close()
hist := "--history=" + f.Name()
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.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.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)
}
}

126
src/server.go Normal file
View File

@@ -0,0 +1,126 @@
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 {
if port == 0 {
return nil
}
listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
if err != nil {
return fmt.Errorf("port not available: %d", 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
}
// 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
}
var DefaultBorderShape BorderShape = BorderRounded
func (a Attr) Merge(b Attr) Attr {
return a | b
}
@@ -32,6 +34,7 @@ func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
func (r *FullscreenRenderer) Pause(bool) {}
func (r *FullscreenRenderer) Resume(bool, bool) {}
func (r *FullscreenRenderer) Clear() {}
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
func (r *FullscreenRenderer) Refresh() {}
func (r *FullscreenRenderer) Close() {}

View File

@@ -72,7 +72,7 @@ type LightRenderer struct {
forceBlack bool
clearOnExit bool
prevDownTime time.Time
clickY []int
clicks [][2]int
ttyin *os.File
buffer []byte
origState *term.State
@@ -174,10 +174,7 @@ func (r *LightRenderer) Init() {
}
}
if r.mouse {
r.csi("?1000h")
r.csi("?1006h")
}
r.enableMouse()
r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
r.csi("G")
r.csi("K")
@@ -569,25 +566,31 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
// ctrl := t & 0b1000
mod := t&0b1100 > 0
drag := t&0b100000 > 0
if scroll != 0 {
return Event{Mouse, 0, &MouseEvent{y, x, scroll, false, false, false, mod}}
}
double := false
if down {
if down && !drag {
now := time.Now()
if !left { // Right double click is not allowed
r.clickY = []int{}
r.clicks = [][2]int{}
} else if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clickY = append(r.clickY, y)
r.clicks = append(r.clicks, [2]int{x, y})
} else {
r.clickY = []int{y}
r.clicks = [][2]int{{x, y}}
}
r.prevDownTime = now
} 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 {
double = true
if double {
r.clicks = [][2]int{}
}
}
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
@@ -602,6 +605,7 @@ func (r *LightRenderer) rmcup() {
}
func (r *LightRenderer) Pause(clear bool) {
r.disableMouse()
r.restoreTerminal()
if clear {
if r.fullscreen {
@@ -614,6 +618,22 @@ func (r *LightRenderer) Pause(clear bool) {
}
}
func (r *LightRenderer) enableMouse() {
if r.mouse {
r.csi("?1000h")
r.csi("?1002h")
r.csi("?1006h")
}
}
func (r *LightRenderer) disableMouse() {
if r.mouse {
r.csi("?1000l")
r.csi("?1002l")
r.csi("?1006l")
}
}
func (r *LightRenderer) Resume(clear bool, sigcont bool) {
r.setupTerminal()
if clear {
@@ -622,13 +642,13 @@ func (r *LightRenderer) Resume(clear bool, sigcont bool) {
} else {
r.rmcup()
}
r.enableMouse()
r.flush()
} else if sigcont && !r.fullscreen && r.mouse {
// NOTE: SIGCONT (Coming back from CTRL-Z):
// It's highly likely that the offset we obtained at the beginning is
// no longer correct, so we simply disable mouse input.
r.csi("?1000l")
r.csi("?1006l")
r.disableMouse()
r.mouse = false
}
}
@@ -643,6 +663,10 @@ func (r *LightRenderer) Clear() {
r.flush()
}
func (r *LightRenderer) NeedScrollbarRedraw() bool {
return false
}
func (r *LightRenderer) RefreshWindows(windows []Window) {
r.flush()
}
@@ -666,10 +690,7 @@ func (r *LightRenderer) Close() {
} else if !r.fullscreen {
r.csi("u")
}
if r.mouse {
r.csi("?1000l")
r.csi("?1006l")
}
r.disableMouse()
r.flush()
r.closePlatform()
r.restoreTerminal()
@@ -706,25 +727,38 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, prev
w.fg = r.theme.Fg.Color
w.bg = r.theme.Bg.Color
}
w.drawBorder()
w.drawBorder(false)
return w
}
func (w *LightWindow) drawBorder() {
func (w *LightWindow) DrawHBorder() {
w.drawBorder(true)
}
func (w *LightWindow) drawBorder(onlyHorizontal bool) {
switch w.border.shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble:
w.drawBorderAround()
w.drawBorderAround(onlyHorizontal)
case BorderHorizontal:
w.drawBorderHorizontal(true, true)
case BorderVertical:
if onlyHorizontal {
return
}
w.drawBorderVertical(true, true)
case BorderTop:
w.drawBorderHorizontal(true, false)
case BorderBottom:
w.drawBorderHorizontal(false, true)
case BorderLeft:
if onlyHorizontal {
return
}
w.drawBorderVertical(true, false)
case BorderRight:
if onlyHorizontal {
return
}
w.drawBorderVertical(false, true)
}
}
@@ -734,13 +768,14 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
if w.preview {
color = ColPreviewBorder
}
hw := runewidth.RuneWidth(w.border.horizontal)
if top {
w.Move(0, 0)
w.CPrint(color, repeat(w.border.horizontal, w.width))
w.CPrint(color, repeat(w.border.horizontal, w.width/hw))
}
if bottom {
w.Move(w.height-1, 0)
w.CPrint(color, repeat(w.border.horizontal, w.width))
w.CPrint(color, repeat(w.border.horizontal, w.width/hw))
}
}
@@ -765,21 +800,29 @@ func (w *LightWindow) drawBorderVertical(left, right bool) {
}
}
func (w *LightWindow) drawBorderAround() {
func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
w.Move(0, 0)
color := ColBorder
if w.preview {
color = ColPreviewBorder
}
w.CPrint(color, string(w.border.topLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.topRight))
for y := 1; y < w.height-1; y++ {
w.Move(y, 0)
w.CPrint(color, string(w.border.vertical))
w.CPrint(color, repeat(' ', w.width-2))
w.CPrint(color, string(w.border.vertical))
hw := runewidth.RuneWidth(w.border.horizontal)
tcw := runewidth.RuneWidth(w.border.topLeft) + runewidth.RuneWidth(w.border.topRight)
bcw := runewidth.RuneWidth(w.border.bottomLeft) + runewidth.RuneWidth(w.border.bottomRight)
rem := (w.width - tcw) % hw
w.CPrint(color, string(w.border.topLeft)+repeat(w.border.horizontal, (w.width-tcw)/hw)+repeat(' ', rem)+string(w.border.topRight))
if !onlyHorizontal {
vw := runewidth.RuneWidth(w.border.vertical)
for y := 1; y < w.height-1; y++ {
w.Move(y, 0)
w.CPrint(color, string(w.border.vertical))
w.CPrint(color, repeat(' ', w.width-vw*2))
w.CPrint(color, string(w.border.vertical))
}
}
w.Move(w.height-1, 0)
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.horizontal, (w.width-bcw)/hw)+repeat(' ', rem)+string(w.border.bottomRight))
}
func (w *LightWindow) csi(code string) {
@@ -1020,7 +1063,7 @@ func (w *LightWindow) FinishFill() {
}
func (w *LightWindow) Erase() {
w.drawBorder()
w.drawBorder(false)
// We don't erase the window here to avoid flickering during scroll
w.Move(0, 0)
}

View File

@@ -6,8 +6,6 @@ import (
"os"
"time"
"runtime"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/encoding"
@@ -19,6 +17,8 @@ func HasFullscreenRenderer() bool {
return true
}
var DefaultBorderShape BorderShape = BorderSharp
func asTcellColor(color Color) tcell.Color {
if color == colDefault {
return tcell.ColorDefault
@@ -189,6 +189,10 @@ func (r *FullscreenRenderer) Clear() {
_screen.Clear()
}
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool {
return true
}
func (r *FullscreenRenderer) Refresh() {
// noop
}
@@ -218,54 +222,38 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}}
case button&tcell.WheelUp != 0:
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}}
case button&tcell.Button1 != 0 && !drag:
// all potential double click events put their 'line' coordinate in the clickY array
// double click event has two conditions, temporal and spatial, the first is checked here
now := time.Now()
if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clickY = append(r.clickY, y)
} else {
r.clickY = []int{y}
}
r.prevDownTime = now
case button&tcell.Button1 != 0:
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
now := time.Now()
if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clicks = append(r.clicks, [2]int{x, y})
} else {
r.clicks = [][2]int{{x, y}}
}
r.prevDownTime = now
// detect double clicks (also check for spatial condition)
n := len(r.clickY)
double := n > 1 && r.clickY[n-2] == r.clickY[n-1]
if double {
// make sure two consecutive double clicks require four clicks
r.clickY = []int{}
// detect double clicks (also check for spatial condition)
n := len(r.clicks)
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 {
// make sure two consecutive double clicks require four clicks
r.clicks = [][2]int{}
}
}
// fire single or double click event
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}}
case runtime.GOOS != "windows":
default:
// double and single taps on Windows don't quite work due to
// the console acting on the events and not allowing us
// to consume them.
left := button&tcell.Button1 != 0
down := left || button&tcell.Button3 != 0
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}}
}
@@ -524,7 +512,7 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
height: height,
normal: normal,
borderStyle: borderStyle}
w.drawBorder()
w.drawBorder(false)
return w
}
@@ -541,7 +529,7 @@ func fill(x, y, w, h int, n ColorPair, r rune) {
}
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 {
@@ -682,7 +670,11 @@ func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
return w.fillString(str, NewColorPair(fg, bg, a))
}
func (w *TcellWindow) drawBorder() {
func (w *TcellWindow) DrawHBorder() {
w.drawBorder(true)
}
func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
shape := w.borderStyle.shape
if shape == BorderNone {
return
@@ -704,35 +696,52 @@ func (w *TcellWindow) drawBorder() {
style = w.normal.style()
}
hw := runewidth.RuneWidth(w.borderStyle.horizontal)
switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderHorizontal, BorderTop:
for x := left; x < right; x++ {
max := right - 2*hw
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.horizontal, nil, style)
}
}
switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderHorizontal, BorderBottom:
for x := left; x < right; x++ {
max := right - 2*hw
if shape == BorderHorizontal || shape == BorderBottom {
max = right - hw
}
for x := left; x <= max; x += hw {
_screen.SetContent(x, bot-1, w.borderStyle.horizontal, nil, style)
}
}
switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderLeft:
for y := top; y < bot; y++ {
_screen.SetContent(left, y, w.borderStyle.vertical, nil, style)
if !onlyHorizontal {
switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderLeft:
for y := top; y < bot; y++ {
_screen.SetContent(left, y, w.borderStyle.vertical, nil, style)
}
}
}
switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderRight:
for y := top; y < bot; y++ {
_screen.SetContent(right-1, y, w.borderStyle.vertical, nil, style)
switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderRight:
vw := runewidth.RuneWidth(w.borderStyle.vertical)
for y := top; y < bot; y++ {
_screen.SetContent(right-vw, y, w.borderStyle.vertical, nil, style)
}
}
}
switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderDouble:
_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(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

@@ -91,6 +91,8 @@ const (
Change
BackwardEOF
Start
Load
Focus
AltBS
@@ -268,8 +270,10 @@ type ColorTheme struct {
Selected ColorAttr
Header ColorAttr
Separator ColorAttr
Scrollbar ColorAttr
Border ColorAttr
BorderLabel ColorAttr
PreviewLabel ColorAttr
}
type Event struct {
@@ -304,6 +308,22 @@ const (
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 {
shape BorderShape
horizontal rune
@@ -391,6 +411,7 @@ type Renderer interface {
RefreshWindows(windows []Window)
Refresh()
Close()
NeedScrollbarRedraw() bool
GetChar() Event
@@ -406,6 +427,7 @@ type Window interface {
Width() int
Height() int
DrawHBorder()
Refresh()
FinishFill()
Close()
@@ -428,7 +450,7 @@ type FullscreenRenderer struct {
mouse bool
forceBlack bool
prevDownTime time.Time
clickY []int
clicks [][2]int
}
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
@@ -437,7 +459,7 @@ func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Rende
mouse: mouse,
forceBlack: forceBlack,
prevDownTime: time.Unix(0, 0),
clickY: []int{}}
clicks: [][2]int{}}
return r
}
@@ -464,23 +486,21 @@ var (
ColInfo ColorPair
ColHeader ColorPair
ColSeparator ColorPair
ColScrollbar ColorPair
ColBorder ColorPair
ColPreview ColorPair
ColPreviewBorder ColorPair
ColBorderLabel ColorPair
ColPreviewLabel ColorPair
)
func EmptyTheme() *ColorTheme {
return &ColorTheme{
Colored: true,
Input: ColorAttr{colUndefined, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
Fg: ColorAttr{colUndefined, AttrUndefined},
Bg: ColorAttr{colUndefined, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined},
Prompt: ColorAttr{colUndefined, AttrUndefined},
Match: ColorAttr{colUndefined, AttrUndefined},
Current: ColorAttr{colUndefined, AttrUndefined},
@@ -490,9 +510,15 @@ func EmptyTheme() *ColorTheme {
Cursor: ColorAttr{colUndefined, AttrUndefined},
Selected: ColorAttr{colUndefined, AttrUndefined},
Header: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Border: ColorAttr{colUndefined, AttrUndefined},
BorderLabel: ColorAttr{colUndefined, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
}
}
@@ -500,13 +526,9 @@ func NoColorTheme() *ColorTheme {
return &ColorTheme{
Colored: false,
Input: ColorAttr{colDefault, AttrRegular},
Disabled: ColorAttr{colDefault, AttrRegular},
Fg: ColorAttr{colDefault, AttrRegular},
Bg: ColorAttr{colDefault, AttrRegular},
PreviewFg: ColorAttr{colDefault, AttrRegular},
PreviewBg: ColorAttr{colDefault, AttrRegular},
DarkBg: ColorAttr{colDefault, AttrRegular},
Gutter: ColorAttr{colDefault, AttrRegular},
Prompt: ColorAttr{colDefault, AttrRegular},
Match: ColorAttr{colDefault, Underline},
Current: ColorAttr{colDefault, Reverse},
@@ -516,9 +538,15 @@ func NoColorTheme() *ColorTheme {
Cursor: ColorAttr{colDefault, AttrRegular},
Selected: ColorAttr{colDefault, AttrRegular},
Header: ColorAttr{colDefault, AttrRegular},
Separator: ColorAttr{colDefault, AttrRegular},
Border: ColorAttr{colDefault, AttrRegular},
BorderLabel: ColorAttr{colDefault, AttrRegular},
Disabled: ColorAttr{colDefault, AttrRegular},
PreviewFg: ColorAttr{colDefault, AttrRegular},
PreviewBg: ColorAttr{colDefault, AttrRegular},
Gutter: ColorAttr{colDefault, AttrRegular},
PreviewLabel: ColorAttr{colDefault, AttrRegular},
Separator: ColorAttr{colDefault, AttrRegular},
Scrollbar: ColorAttr{colDefault, AttrRegular},
}
}
@@ -531,13 +559,9 @@ func init() {
Default16 = &ColorTheme{
Colored: true,
Input: ColorAttr{colDefault, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{colBlack, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined},
Prompt: ColorAttr{colBlue, AttrUndefined},
Match: ColorAttr{colGreen, AttrUndefined},
Current: ColorAttr{colYellow, AttrUndefined},
@@ -547,20 +571,22 @@ func init() {
Cursor: ColorAttr{colRed, AttrUndefined},
Selected: ColorAttr{colMagenta, AttrUndefined},
Header: ColorAttr{colCyan, AttrUndefined},
Separator: ColorAttr{colBlack, AttrUndefined},
Border: ColorAttr{colBlack, AttrUndefined},
BorderLabel: ColorAttr{colWhite, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
}
Dark256 = &ColorTheme{
Colored: true,
Input: ColorAttr{colDefault, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{236, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined},
Prompt: ColorAttr{110, AttrUndefined},
Match: ColorAttr{108, AttrUndefined},
Current: ColorAttr{254, AttrUndefined},
@@ -570,20 +596,22 @@ func init() {
Cursor: ColorAttr{161, AttrUndefined},
Selected: ColorAttr{168, AttrUndefined},
Header: ColorAttr{109, AttrUndefined},
Separator: ColorAttr{59, AttrUndefined},
Border: ColorAttr{59, AttrUndefined},
BorderLabel: ColorAttr{145, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
}
Light256 = &ColorTheme{
Colored: true,
Input: ColorAttr{colDefault, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{251, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined},
Prompt: ColorAttr{25, AttrUndefined},
Match: ColorAttr{66, AttrUndefined},
Current: ColorAttr{237, AttrUndefined},
@@ -593,9 +621,15 @@ func init() {
Cursor: ColorAttr{161, AttrUndefined},
Selected: ColorAttr{168, AttrUndefined},
Header: ColorAttr{31, AttrUndefined},
Separator: ColorAttr{145, AttrUndefined},
Border: ColorAttr{145, AttrUndefined},
BorderLabel: ColorAttr{59, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
}
}
@@ -615,13 +649,9 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
return c
}
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.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.Gutter = o(theme.DarkBg, o(baseTheme.Gutter, theme.Gutter))
theme.Prompt = o(baseTheme.Prompt, theme.Prompt)
theme.Match = o(baseTheme.Match, theme.Match)
theme.Current = o(baseTheme.Current, theme.Current)
@@ -631,10 +661,18 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
theme.Cursor = o(baseTheme.Cursor, theme.Cursor)
theme.Selected = o(baseTheme.Selected, theme.Selected)
theme.Header = o(baseTheme.Header, theme.Header)
theme.Separator = o(baseTheme.Separator, theme.Separator)
theme.Border = o(baseTheme.Border, theme.Border)
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.Separator = o(theme.Border, theme.Separator)
theme.Scrollbar = o(theme.Border, theme.Scrollbar)
initPalette(theme)
}
@@ -666,8 +704,10 @@ func initPalette(theme *ColorTheme) {
ColInfo = pair(theme.Info, theme.Bg)
ColHeader = pair(theme.Header, theme.Bg)
ColSeparator = pair(theme.Separator, theme.Bg)
ColScrollbar = pair(theme.Scrollbar, theme.Bg)
ColBorder = pair(theme.Border, theme.Bg)
ColBorderLabel = pair(theme.BorderLabel, theme.Bg)
ColPreviewLabel = pair(theme.PreviewLabel, theme.PreviewBg)
ColPreview = pair(theme.PreviewFg, theme.PreviewBg)
ColPreviewBorder = pair(theme.Border, theme.PreviewBg)
}

View File

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

View File

@@ -1,10 +1,72 @@
package util
import "testing"
import (
"math"
"strings"
"testing"
"time"
)
func TestMax(t *testing.T) {
if Max(10, 1) != 10 {
t.Error("Expected", 10)
}
if Max(-2, 5) != 5 {
t.Error("Invalid result")
t.Error("Expected", 5)
}
}
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)
}
}
@@ -21,6 +83,55 @@ func TestContrain(t *testing.T) {
}
}
func TestContrain32(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) {
o := Once(false)
if o() {
@@ -64,3 +175,12 @@ func TestTruncate(t *testing.T) {
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

@@ -7,6 +7,7 @@ require 'English'
require 'shellwords'
require 'erb'
require 'tempfile'
require 'net/http'
TEMPLATE = DATA.read
UNSETS = %w[
@@ -22,7 +23,7 @@ DEFAULT_TIMEOUT = 10
FILE = File.expand_path(__FILE__)
BASE = File.expand_path('..', __dir__)
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
since = Time.now
@@ -64,7 +65,7 @@ class Shell
end
def fish
UNSETS.map { |v| v + '= ' }.join + 'fish'
UNSETS.map { |v| v + '= ' }.join + ' FZF_DEFAULT_OPTS=--no-scrollbar fish'
end
end
end
@@ -921,7 +922,7 @@ class TestGoFZF < TestBase
end
wait do
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
# Update history entries (not changed on disk)
@@ -1501,7 +1502,7 @@ class TestGoFZF < TestBase
rescue StandardError
nil
end
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0), :Enter
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0 --bind space:toggle-preview), :Enter
tmux.until do |lines|
assert_equal 100, lines.item_count
assert_equal ' 100/100', lines[1]
@@ -1511,17 +1512,17 @@ class TestGoFZF < TestBase
assert_path_exists tempname
assert_equal %w[1], File.readlines(tempname, chomp: true)
end
tmux.send_keys :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.send_keys :Space, :Down, :Down
tmux.until { |lines| assert_equal '> 3', lines[4] }
wait do
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
@@ -1554,13 +1555,13 @@ class TestGoFZF < TestBase
end
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| refute_includes lines[1], ' foo ' }
tmux.until { |lines| assert_includes lines[1], ' foo foo' }
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.until { |lines| refute_includes lines[1], ' foo ' }
tmux.until { |lines| assert_includes lines[1], ' foo foo' }
end
def test_preview_q_no_match_with_initial_query
@@ -1586,6 +1587,11 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal '> 1', lines[-2] }
end
def test_info_inline_separator
tmux.send_keys 'seq 10 | fzf --info=inline:___ --no-separator', :Enter
tmux.until { |lines| assert_equal '> ___10/10', lines[-1] }
end
def test_change_first_last
tmux.send_keys %(seq 1000 | #{FZF} --bind change:first,alt-Z:last), :Enter
tmux.until { |lines| assert_equal 1000, lines.match_count }
@@ -1604,6 +1610,30 @@ class TestGoFZF < TestBase
tmux.send_keys :Enter
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
tmux.send_keys %(seq 1000 | #{fzf('--print-query --bind enter:accept-non-empty')}), :Enter
tmux.until { |lines| assert_equal 1000, lines.match_count }
@@ -1764,6 +1794,32 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal '>', lines.last }
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
tmux.send_keys %(seq 100 | #{FZF} --multi --bind space:clear-selection), :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
@@ -1892,8 +1948,69 @@ class TestGoFZF < TestBase
end
def test_preview_window_follow
tmux.send_keys "#{FZF} --preview 'seq 1000 | nl' --preview-window down:noborder:follow", :Enter
tmux.until { |lines| assert_equal '1000 1000', lines[-1].strip }
file = Tempfile.new('fzf-follow')
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
def test_toggle_preview_wrap
@@ -2119,6 +2236,15 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_includes lines[1], '4' }
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
tmux.send_keys "seq 1000 | #{FZF} --scroll-off=3 --bind l:last", :Enter
tmux.until { |lines| assert_equal 1000, lines.item_count }
@@ -2320,13 +2446,13 @@ class TestGoFZF < TestBase
tmux.send_keys "seq 3 | fzf --height ~100% --border=vertical --preview 'seq {}' --preview-window left,5,border-right --padding 1 --exit-0 --header $'hello\\nworld' --header-lines=2", :Enter
expected = <<~OUTPUT
1 > 3
2 2
3 1
hello
world
1/1
>
1 > 3
2 2
3 1
hello
world
1/1
>
OUTPUT
tmux.until { assert_block(expected, _1) }
@@ -2345,19 +2471,38 @@ class TestGoFZF < TestBase
end
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|
assert_match(/>100.*welcome/, lines[0])
assert_includes(lines[-2], '100/100 (100)')
end
end
def test_focus_event
tmux.send_keys 'seq 100 | fzf --bind "focus:transform-prompt(echo [[{}]])"', :Enter
tmux.until { |lines| assert_includes(lines[-1], '[[1]]') }
tmux.send_keys :Up
tmux.until { |lines| assert_includes(lines[-1], '[[2]]') }
tmux.send_keys :X
tmux.until { |lines| assert_includes(lines[-1], '[[]]') }
end
def test_labels_center
tmux.send_keys ': | fzf --border --border-label foobar --preview : --preview-label barfoo', :Enter
tmux.send_keys 'echo x | fzf --border --border-label foobar --preview : --preview-label barfoo --bind "space:change-border-label(foobarfoo)+change-preview-label(barfoobar),enter:transform-border-label(echo foo{}foo)+transform-preview-label(echo bar{}bar)"', :Enter
tmux.until do
assert_includes(_1[0], '─foobar─')
assert_includes(_1[1], '─barfoo─')
end
tmux.send_keys :space
tmux.until do
assert_includes(_1[0], '─foobarfoo─')
assert_includes(_1[1], '─barfoobar─')
end
tmux.send_keys :Enter
tmux.until do
assert_includes(_1[0], '─fooxfoo─')
assert_includes(_1[1], '─barxbar─')
end
end
def test_labels_left
@@ -2408,6 +2553,41 @@ class TestGoFZF < TestBase
tmux.send_keys 'seq 100 | fzf -q55 --no-separator', :Enter
tmux.until { assert(_1[-2] == ' 1/100') }
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
tmux.send_keys 'seq 10 | fzf --listen 6266', :Enter
tmux.until { |lines| assert_equal 10, lines.item_count }
Net::HTTP.post(URI('http://localhost:6266'), 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ')
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
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
end
module TestShell
@@ -2478,7 +2658,7 @@ module TestShell
tmux.prepare
tmux.send_keys :Escape, :c
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.prepare
tmux.send_keys :pwd, :Enter
@@ -2531,7 +2711,7 @@ module TestShell
def test_ctrl_r_multiline
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.send_keys 'C-r'
tmux.until { |lines| assert_equal '>', lines[-1] }
@@ -2540,7 +2720,7 @@ module TestShell
tmux.send_keys :Enter
tmux.until { |lines| assert lines[-1]&.end_with?('bar"') }
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
def test_ctrl_r_abort
@@ -2731,7 +2911,7 @@ module CompletionTest
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_match(/cat .*fzf-unicode.*1.* .*fzf-unicode.*2/, lines[-1]) }
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
def test_custom_completion_api
@@ -2821,7 +3001,7 @@ class TestFish < TestBase
end
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.until { |lines| assert_empty lines }
end
@@ -2840,6 +3020,8 @@ unset <%= UNSETS.join(' ') %>
unset $(env | sed -n /^_fzf_orig/s/=.*//p)
unset $(declare -F | sed -n "/_fzf/s/.*-f //p")
export FZF_DEFAULT_OPTS=--no-scrollbar
# Setup fzf
# ---------
if [[ ! "$PATH" == *<%= BASE %>/bin* ]]; then