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

Compare commits

...

82 Commits

Author SHA1 Message Date
Junegunn Choi
c4a9ccd6af 0.53.0 2024-06-06 22:03:26 +09:00
Junegunn Choi
cbf91f2ed3 ADVANCED.md: /dev/tty redirection no longer required 2024-06-06 21:58:02 +09:00
Junegunn Choi
b1460d4787 hasPreviewFlags should ignore escaped placeholder
This reload command wouldn't run before the fix:

  : | fzf --bind 'start:reload:echo \{}'
2024-06-06 17:40:15 +09:00
Junegunn Choi
7dc9e14874 Update docs 2024-06-06 17:40:15 +09:00
Junegunn Choi
1616ed543d Fix index out of bounds error caused by outdated offset 2024-06-06 00:23:58 +09:00
Junegunn Choi
dc73fba188 [man] Clarification on --scheme options 2024-06-05 14:29:50 +09:00
Junegunn Choi
ef148dfd37 Handle int32 overflow
yes | fzf --tail=10 --preview 'echo "{n}"'
2024-06-05 14:29:50 +09:00
Junegunn Choi
93bbb3032d Add --tail=NUM to limit the number of items to keep in memory 2024-06-04 17:50:46 +09:00
Junegunn Choi
4c83d8596d Add new options to bash completion 2024-06-03 09:45:20 +09:00
Junegunn Choi
d453e6d7db Update ADVANCED.md: Use --tmux instead of fzf-tmux 2024-06-03 09:41:40 +09:00
Junegunn Choi
c29533994f Fix invalid default of selected-hl (--color)
It should default to 'hl' instead of 'current-hl'
2024-06-02 18:09:41 +09:00
Junegunn Choi
1afe13b5b5 Merge remote-tracking branch 'origin/master' into devel 2024-06-02 17:59:04 +09:00
Junegunn Choi
36600eaaa9 Update CHANGELOG: clarification 2024-06-02 17:58:44 +09:00
junegunn
3ee1fc2034 Deploying to master from @ junegunn/fzf@124cd70710 🚀 2024-06-02 00:01:52 +00:00
Junegunn Choi
e2f93e5a2d --tmux vs. --height: Last one wins 2024-06-01 22:11:15 +09:00
Junegunn Choi
cfdf2f1153 Update README 2024-06-01 16:20:03 +09:00
Junegunn Choi
e042143e3f Immediately close standard output of the child process
Fix #3828
2024-06-01 15:22:05 +09:00
Junegunn Choi
7c613d0d9b Do not disable --height on mintty (because it works) 2024-06-01 14:45:54 +09:00
Junegunn Choi
b00d46bc14 Fix --height on Windows 2024-06-01 14:36:41 +09:00
Junegunn Choi
555b0d235b Ignore --height option if it's not supported on the platform
This is to make shell integration work out of the box on Git bash.

  eval "$(fzf --bash)"
  vim <CTRL-T>
    # would print '--height option is currently not supported on this platform'
2024-06-01 14:35:45 +09:00
Junegunn Choi
564daf9a7d Set standard input of 'man' process to os.Stdin 2024-06-01 13:23:46 +09:00
Junegunn Choi
41bcbe342f Revert "An '--expect' key should execute actions bound to the key"
To be backward compatible.

Close #3829
2024-06-01 13:21:59 +09:00
LangLangBart
dbe8dc344e [fish] Use builtins for cd and history (#3830)
Close #3826
2024-06-01 11:28:02 +09:00
Junegunn Choi
e33fb59da1 Update CHANGELOG 2024-05-31 16:57:35 +09:00
Junegunn Choi
7aa88aa115 Fix error message on invalid --tmux option
fzf --tmux foobar
  # not a valid integer: foobar
  # ->
  # invalid tmux option: foobar (expected: [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]])
2024-05-31 16:57:35 +09:00
LangLangBart
2b6d600879 [zsh] Enhance CTRL-R to display multi-line entires (#3823)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-05-31 16:57:35 +09:00
Junegunn Choi
05c765d442 [fish] Add --nth 2..,.. to allow anchored search against command 2024-05-31 16:57:35 +09:00
Junegunn Choi
49b496269c Fix index out of bounds error on scroll-down action 2024-05-31 16:57:35 +09:00
Junegunn Choi
7405925952 [bash] Indent multi-line history entries 2024-05-31 16:57:35 +09:00
Junegunn Choi
3afd543a7e [fish] Use perl instead of sed to strip leading tabs
https://github.com/junegunn/fzf/pull/3807#discussion_r1619520105
2024-05-30 10:23:20 +09:00
Junegunn Choi
b4f2cde5ac [fish] Better multi-line support for CTRL-R
Prepend each entry with an index number so that multi-line entries can
be clearly distinguished.
2024-05-29 20:16:49 +09:00
Junegunn Choi
ed53ef7cee [shell] Add --highlight-line to CTRL-R bindings 2024-05-29 20:13:41 +09:00
Junegunn Choi
12630b124d Make --tmux argument optional 2024-05-29 02:16:18 +09:00
Junegunn Choi
1d59ac09d2 Pass-through error message from 'tmux display-popup'
fzf --tmux 9999
    # height too large
2024-05-29 02:07:56 +09:00
Junegunn Choi
a8f3a0dd59 Merge branch 'master' into devel 2024-05-28 23:19:26 +09:00
Konstantin-Glukhov
124cd70710 [vim] Do not prepend CWD to path starting with a backslash on Windows (#3820)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-05-28 23:15:14 +09:00
Junegunn Choi
782de139c8 [vim] Native --tmux fix for Neovim 2024-05-28 19:27:31 +09:00
Junegunn Choi
32eb32ee5e Add multi-line example to CHANGELOG 2024-05-27 01:48:46 +09:00
Junegunn Choi
2f51eb2b41 Different marker for the first and last line of multi-line entries
Can be configured via `--marker-multi-line`
2024-05-27 01:35:05 +09:00
Junegunn Choi
0ccbd79e10 Fix --help output: marker default
Co-authored-by: LangLangBart <92653266+LangLangBart@users.noreply.github.com>
2024-05-26 09:24:30 +09:00
junegunn
99bd6de541 Deploying to master from @ junegunn/fzf@daa602422d 🚀 2024-05-26 00:01:51 +00:00
Junegunn Choi
1fef36e4bc Do not allow tabs in pointer and marker 2024-05-25 16:31:34 +09:00
Junegunn Choi
89375005b5 Fix option validation order 2024-05-25 16:23:13 +09:00
Junegunn Choi
88e78c9193 Update integration test to use named pipes 2024-05-25 12:03:20 +09:00
Junegunn Choi
29a19ad080 Update CHANGELOG 2024-05-25 09:40:17 +09:00
Junegunn Choi
2a039ab746 Describe exit code 126 2024-05-24 19:32:44 +09:00
Junegunn Choi
7e9a0fcdbd Change default --scroll-off to 3 2024-05-24 19:25:50 +09:00
Junegunn Choi
7a97532547 Fix --scroll-off for multi-line mode 2024-05-24 19:23:36 +09:00
Junegunn Choi
996abb2831 Fix incorrect colors for selected-{fg,bg,hl}
When a non-default base color scheme is specified, fzf would choose incorrect
colors for selected-*.

  fzf --color 'light,fg:238,bg:255,bg+:253' -m
2024-05-24 00:46:01 +09:00
Junegunn Choi
da500a358f Use bold bar as the default marker 2024-05-24 00:31:20 +09:00
Junegunn Choi
c36b846acc [vim] Open cmd.exe window only on mintty < 3.4.5 without winpty 2024-05-23 21:27:29 +09:00
Junegunn Choi
d9b5c9b2be Address review comments by @Konfekt
d4216b0dcc
2024-05-23 21:14:08 +09:00
Junegunn Choi
3dee8778d0 execute: Open separate handles to /dev/tty (in, out, err)
# This will no longer cause 'Vim: Warning: Output is not to a terminal'
  fzf --bind 'enter:execute:vim {}' > /tmp/foo
2024-05-23 21:11:12 +09:00
Junegunn Choi
d4216b0dcc Use MSYS=enable_pcon instead of winpty on mintty 3.4.5 or later 2024-05-23 18:42:54 +09:00
Enno
bfe2bf4dce [vim] Git Bash Mintty: only use cmd.exe if winpty missing (#3811)
* Git Bash Mintty: only use cmd.exe if winpty missing

Addresses https://github.com/junegunn/fzf/issues/3809

* preferably use term in Git Bash for popup window

See https://github.com/junegunn/fzf/pull/3811#issuecomment-2124241321
2024-05-23 09:07:54 +09:00
Junegunn Choi
561f9291fd [vim] Replace backslashes with forward slashes on win32unix 2024-05-23 09:03:43 +09:00
Junegunn Choi
b5b0d6b3ea Do not run as winpty proxy if winpty is not available 2024-05-23 08:47:38 +09:00
Junegunn Choi
a90426b7ca Add print(...) action 2024-05-22 22:18:24 +09:00
Junegunn Choi
303c3bae7f proxy: Pass SIGINT to the child fzf 2024-05-22 22:14:00 +09:00
Junegunn Choi
6b4358f641 An '--expect' key should execute actions bound to the key
Fix #3810
2024-05-22 20:39:09 +09:00
Junegunn Choi
552158f3ad Ignore SIGINT when running as proxy 2024-05-22 20:01:37 +09:00
Junegunn Choi
7205203dc8 Update CHANGELOG 2024-05-21 02:07:49 +09:00
Junegunn Choi
0cadf70072 Update the summary 2024-05-21 01:57:22 +09:00
Junegunn Choi
076b3d0a9a Embed man page in the binary and show it on 'fzf --man' 2024-05-21 01:06:10 +09:00
Junegunn Choi
7b0c9e04d3 Change default marker 2024-05-20 18:51:52 +09:00
Junegunn Choi
573df524fe Use winpty to launch fzf in Git bash (mintty)
Close #3806

Known limitation:
* --height cannot be used
2024-05-20 18:24:14 +09:00
Junegunn Choi
aee417c46a Respect $NO_COLOR environment variable
Close #1762
2024-05-20 10:50:00 +09:00
Junegunn Choi
04db44067d Implement multi-line display of multi-line items 2024-05-20 09:25:30 +09:00
Junegunn Choi
5b204c54f9 Change default pointer and marker character
* Pointer: '▌'
* Marker: '▏'

They will still be set to '>' if `--no-unicode` is given.

Reasons:
* They look okay
* They work better with multi-line items (WIP)
2024-05-19 15:51:32 +09:00
junegunn
daa602422d Deploying to master from @ junegunn/fzf@01e7668915 🚀 2024-05-19 00:01:47 +00:00
Junegunn Choi
04dfb14e32 Do not 'become' inside a tmux popup
fzf --tmux center --bind 'enter:become:vim {}'
2024-05-18 17:08:36 +09:00
Junegunn Choi
c24256cba3 Update README
* Tidy up
* Mention `--tmux`
2024-05-18 17:08:36 +09:00
Junegunn Choi
685fb71d89 [vim] Use native --tmux option instead of fzf-tmux when possible 2024-05-18 17:08:36 +09:00
Junegunn Choi
83b6033906 Add --tmux option to replace fzf-tmux script 2024-05-18 17:08:36 +09:00
Zhizhen He
01e7668915 chore: use strings.ReplaceAll (#3801) 2024-05-18 17:06:33 +09:00
Enno
0994d9c881 Make :FZF work in Vim from Git Bash (#3798)
* make :FZF work in Vim from Git Bash

Despite its title 'Calling fzf#run with a list as source fail (n)vim is used from git bash' the issue in 

https://github.com/junegunn/fzf/issues/3777

of running `:FZF` in Vim in Git Bash was apparently only fixed for Neovim in Git Bash on Windows 11, but not for Vim from Git Bash.

In view of this, replacing /C by ///C might be considered a universal fix.

This PR just proposes the patch in https://github.com/junegunn/fzf/issues/1983 that still seems open.

In view of the fourth item in the most recent 2.45.0 https://github.com/git-for-windows/build-extra/blob/main/ReleaseNotes.md#known-issues little seems to have changed regarding path conversion of arguments containing forward slashes

* prefer doubling slashed instead of generic env. var

If MSYS_NO_PATHCONV=1 is used, then all arguments are preserved, in particular possibly paths passed in s:command.
Therefore, only avoid converting `/C` from `cmd` to a path.
2024-05-15 17:26:49 +09:00
LangLangBart
030428ba43 docs: update zsh integration instructions (#3794) 2024-05-15 01:59:43 +09:00
Junegunn Choi
8a110e02b9 Fix tcell test case 2024-05-15 00:45:23 +09:00
Junegunn Choi
86d92c17c4 Refactor tui.TtyIn() 2024-05-15 00:28:56 +09:00
Junegunn Choi
c4cc7891b4 Revert "Close handles to /dev/tty", instead reuse handles 2024-05-15 00:15:29 +09:00
Junegunn Choi
218843b9f1 Close handles to /dev/tty 2024-05-14 21:49:47 +09:00
Junegunn Choi
d274d093af Render UI directly to /dev/tty
See https://github.com/junegunn/fzf/discussions/3792

This allows us to separately capture the standard error from fzf and its
child processes, and there's less chance of user errors of redirecting
the error stream and hiding fzf.
2024-05-14 16:32:26 +09:00
53 changed files with 2502 additions and 951 deletions

View File

@@ -1,18 +1,17 @@
Advanced fzf examples Advanced fzf examples
====================== ======================
* *Last update: 2024/01/20* * *Last update: 2024/06/06*
* *Requires fzf 0.46.0 or above* * *Requires fzf 0.53.0 or later*
--- ---
<!-- vim-markdown-toc GFM --> <!-- vim-markdown-toc GFM -->
* [Introduction](#introduction) * [Introduction](#introduction)
* [Screen Layout](#screen-layout) * [Display modes](#display-modes)
* [`--height`](#--height) * [`--height`](#--height)
* [`fzf-tmux`](#fzf-tmux) * [`--tmux`](#--tmux)
* [Popup window support](#popup-window-support)
* [Dynamic reloading of the list](#dynamic-reloading-of-the-list) * [Dynamic reloading of the list](#dynamic-reloading-of-the-list)
* [Updating the list of processes by pressing CTRL-R](#updating-the-list-of-processes-by-pressing-ctrl-r) * [Updating the list of processes by pressing CTRL-R](#updating-the-list-of-processes-by-pressing-ctrl-r)
* [Toggling between data sources](#toggling-between-data-sources) * [Toggling between data sources](#toggling-between-data-sources)
@@ -63,7 +62,7 @@ learn its wide variety of features.
This document will guide you through some examples that will familiarize you This document will guide you through some examples that will familiarize you
with the advanced features of fzf. with the advanced features of fzf.
Screen Layout Display modes
------------- -------------
### `--height` ### `--height`
@@ -104,56 +103,55 @@ Define `$FZF_DEFAULT_OPTS` like so:
export FZF_DEFAULT_OPTS="--height=40% --layout=reverse --info=inline --border --margin=1 --padding=1" export FZF_DEFAULT_OPTS="--height=40% --layout=reverse --info=inline --border --margin=1 --padding=1"
``` ```
### `fzf-tmux` ### `--tmux`
Before fzf had `--height` option, we would open fzf in a tmux split pane not (Requires tmux 3.3 or later)
to take up the whole screen. This is done using `fzf-tmux` script.
If you're using tmux, you can open fzf in a tmux popup using `--tmux` option.
```sh ```sh
# Open fzf on a tmux split pane below the current pane. # Open fzf in a tmux popup at the center of the screen with 70% width and height
# Takes the same set of options. fzf --tmux 70%
fzf-tmux --layout=reverse
``` ```
![image](https://user-images.githubusercontent.com/700826/113379973-f1cc6500-93b5-11eb-8860-c9bc4498aadf.png) ![image](https://github.com/junegunn/fzf/assets/700826/9c365405-c700-49b2-8985-60d822ed4cff)
The limitation of `fzf-tmux` is that it only works when you're on tmux unlike `--tmux` option is silently ignored if you're not on tmux. So if you're trying
`--height` option. But the advantage of it is that it's more flexible. to avoid opening fzf in fullscreen, try specifying both `--height` and `--tmux`.
(See `man fzf-tmux` for available options.)
```sh ```sh
# On the right (50%) # --tmux is specified later so it takes precedence over --height when on tmux.
fzf-tmux -r # If you're not on tmux, --tmux is ignored and --height is used instead.
fzf --height 70% --tmux 70%
# On the left (30%)
fzf-tmux -l30%
# Above the cursor
fzf-tmux -u30%
``` ```
![image](https://user-images.githubusercontent.com/700826/113379983-fa24a000-93b5-11eb-93eb-8a3d39b2f163.png) You can also specify the position, width, and height of the popup window in
the following format:
![image](https://user-images.githubusercontent.com/700826/113380001-0577cb80-93b6-11eb-95d0-2ba453866882.png) * `[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]`
![image](https://user-images.githubusercontent.com/700826/113380040-1d4f4f80-93b6-11eb-9bef-737fb120aafe.png)
#### Popup window support
But here's the really cool part; tmux 3.2 added support for popup windows. So
you can open fzf in a popup window, which is quite useful if you frequently
use split panes.
```sh ```sh
# Open tmux in a tmux popup window (default size: 50% of the screen) # 100% width and 60% height
fzf-tmux -p fzf --tmux 100%,60% --border horizontal
# 80% width, 60% height
fzf-tmux -p 80%,60%
``` ```
![image](https://user-images.githubusercontent.com/700826/113380106-4a9bfd80-93b6-11eb-8cee-aeb1c4ce1a1f.png) ![image](https://github.com/junegunn/fzf/assets/700826/f80d3514-d69f-42f2-a8de-a392a562bfcf)
```sh
# On the right (50% width)
fzf --tmux right
```
![image](https://github.com/junegunn/fzf/assets/700826/4033ade4-7efa-421b-a3fb-a430d197098a)
```sh
# On the left (40% width and 70% height)
fzf --tmux left,40%,70%
```
![image](https://github.com/junegunn/fzf/assets/700826/efe43881-2bf0-49ea-ab2e-1377f778cd52)
> [!TIP]
> You might also want to check out my tmux plugins which support this popup > You might also want to check out my tmux plugins which support this popup
> window layout. > window layout.
> >
@@ -536,8 +534,8 @@ pods() {
--bind 'start:reload:$command' \ --bind 'start:reload:$command' \
--bind 'ctrl-r:reload:$command' \ --bind 'ctrl-r:reload:$command' \
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \ --bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \ --bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash' \
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \ --bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2})' \
--preview-window up:follow \ --preview-window up:follow \
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@" --preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
} }

View File

@@ -1,6 +1,76 @@
CHANGELOG CHANGELOG
========= =========
0.53.0
------
- Multi-line display
- See [Processing multi-line items](https://junegunn.github.io/fzf/tips/processing-multi-line-items/)
- fzf can now display multi-line items
```sh
# All bash functions, highlighted
declare -f | perl -0777 -pe 's/^}\n/}\0/gm' |
bat --plain --language bash --color always |
fzf --read0 --ansi --reverse --multi --highlight-line
# Ripgrep multi-line output
rg --pretty bash | perl -0777 -pe 's/\n\n/\n\0/gm' |
fzf --read0 --ansi --multi --highlight-line --reverse --tmux 70%
```
- To disable multi-line display, use `--no-multi-line`
- CTRL-R bindings of bash, zsh, and fish have been updated to leverage multi-line display
- The default `--pointer` and `--marker` have been changed from `>` to Unicode bar characters as they look better with multi-line items
- Added `--marker-multi-line` to customize the select marker for multi-line entries with the default set to `╻┃╹`
```
╻First line
┃...
╹Last line
```
- Native tmux integration
- Added `--tmux` option to replace fzf-tmux script and simplify distribution
```sh
# --tmux [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
# Center, 100% width and 70% height
fzf --tmux 100%,70% --border horizontal --padding 1,2
# Left, 30% width
fzf --tmux left,30%
# Bottom, 50% height
fzf --tmux bottom,50%
```
- To keep the implementation simple, it only uses popups. You need tmux 3.3 or later.
- To use `--tmux` in Vim plugin:
```vim
let g:fzf_layout = { 'tmux': '100%,70%' }
```
- Added support for endless input streams
- See [Browsing log stream with fzf](https://junegunn.github.io/fzf/tips/browsing-log-streams/)
- Added `--tail=NUM` option to limit the number of items to keep in memory. This is useful when you want to browse an endless stream of data (e.g. log stream) with fzf while limiting memory usage.
```sh
# Interactive filtering of a log stream
tail -f *.log | fzf --tail 100000 --tac --no-sort --exact
```
- Better Windows Support
- fzf now works on Git bash (mintty) out of the box via winpty integration
- Many fixes and improvements for Windows
- man page is now embedded in the binary; `fzf --man` to see it
- Changed the default `--scroll-off` to 3, as we think it's a better default
- Process started by `execute` action now directly writes to and reads from `/dev/tty`. Manual `/dev/tty` redirection for interactive programs is no longer required.
```sh
# Vim will work fine without /dev/tty redirection
ls | fzf --bind 'space:execute:vim {}' > selected
```
- Added `print(...)` action to queue an arbitrary string to be printed on exit. This was mainly added to work around the limitation of `--expect` where it's not compatible with `--bind` on the same key and it would ignore other actions bound to it.
```sh
# This doesn't work as expected because --expect is not compatible with --bind
fzf --multi --expect ctrl-y --bind 'ctrl-y:select-all'
# This is something you can do instead
fzf --multi --bind 'enter:print()+accept,ctrl-y:select-all+print(ctrl-y)+accept'
```
- We also considered making them compatible, but realized that some users may have been relying on the current behavior.
- [`NO_COLOR`](https://no-color.org/) environment variable is now respected. If the variable is set, fzf defaults to `--no-color` unless otherwise specified.
0.52.1 0.52.1
------ ------
- Fixed a critical bug in the Windows version - Fixed a critical bug in the Windows version

View File

@@ -4,7 +4,7 @@ GOOS ?= $(shell $(GO) env GOOS)
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST))) MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
ROOT_DIR := $(shell dirname $(MAKEFILE)) ROOT_DIR := $(shell dirname $(MAKEFILE))
SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh) $(MAKEFILE) SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh man/man1/*.1) $(MAKEFILE)
ifdef FZF_VERSION ifdef FZF_VERSION
VERSION := $(FZF_VERSION) VERSION := $(FZF_VERSION)

View File

@@ -294,7 +294,7 @@ The following table summarizes the available options.
| `options` | string/list | Options to fzf | | `options` | string/list | Options to fzf |
| `dir` | string | Working directory | | `dir` | string | Working directory |
| `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) | | `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) |
| `tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%`) | | `tmux` | string | (Layout) `--tmux` options (e.g. `90%,70%`) |
| `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new`) | | `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new`) |
| `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}`) | | `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}`) |
@@ -457,12 +457,13 @@ let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
``` ```
Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2 Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2
or above) by putting fzf-tmux options in `tmux` key. or above) by putting `--tmux` option value in `tmux` key.
```vim ```vim
" See `man fzf-tmux` for available options " See `--tmux` option in `man fzf` for available options
" [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
if exists('$TMUX') if exists('$TMUX')
let g:fzf_layout = { 'tmux': '-p90%,60%' } let g:fzf_layout = { 'tmux': '90%,70%' }
else else
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
endif endif

209
README.md

File diff suppressed because one or more lines are too long

View File

@@ -132,8 +132,10 @@ if [[ -z "$TMUX" ]]; then
exit $? exit $?
fi fi
# --height option is not allowed. CTRL-Z is also disabled. # * --height option is not allowed
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore") # * CTRL-Z is also disabled
# * fzf-tmux script is not compatible with --tmux option in fzf 0.53.0 or later
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore" "--no-tmux")
# Handle zoomed tmux pane without popup options by moving it to a temp window # Handle zoomed tmux pane without popup options by moving it to a temp window
if [[ ! "$opt" =~ "-E" ]] && tmux list-panes -F '#F' | grep -q Z; then if [[ ! "$opt" =~ "-E" ]] && tmux list-panes -F '#F' | grep -q Z; then

View File

@@ -311,7 +311,7 @@ The following table summarizes the available options.
`options` | string/list | Options to fzf `options` | string/list | Options to fzf
`dir` | string | Working directory `dir` | string | Working directory
`up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` ) `up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` )
`tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%` ) `tmux` | string | (Layout) `--tmux` options (e.g. `90%,70%` )
`window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` ) `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` )
`window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}` ) `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}` )
---------------------------+---------------+---------------------------------------------------------------------- ---------------------------+---------------+----------------------------------------------------------------------
@@ -469,11 +469,12 @@ in Neovim.
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
< <
Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2 Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2
or above) by putting fzf-tmux options in `tmux` key. or above) by putting `--tmux` options in `tmux` key.
> >
" See `man fzf-tmux` for available options " See `--tmux` option in `man fzf` for available options
" [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
if exists('$TMUX') if exists('$TMUX')
let g:fzf_layout = { 'tmux': '-p90%,60%' } let g:fzf_layout = { 'tmux': '90%,70%' }
else else
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
endif endif

View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.52.1 version=0.53.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
@@ -265,7 +265,11 @@ fi
EOF EOF
if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then
echo "eval \"\$(fzf --$shell)\"" >> "$src" if [[ "$shell" = zsh ]]; then
echo "source <(fzf --$shell)" >> "$src"
else
echo "eval \"\$(fzf --$shell)\"" >> "$src"
fi
else else
cat >> "$src" << EOF cat >> "$src" << EOF
# Auto-completion # Auto-completion

View File

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

23
main.go
View File

@@ -4,13 +4,14 @@ import (
_ "embed" _ "embed"
"fmt" "fmt"
"os" "os"
"os/exec"
"strings" "strings"
fzf "github.com/junegunn/fzf/src" fzf "github.com/junegunn/fzf/src"
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version = "0.52" var version = "0.53"
var revision = "devel" var revision = "devel"
//go:embed shell/key-bindings.bash //go:embed shell/key-bindings.bash
@@ -28,6 +29,9 @@ var zshCompletion []byte
//go:embed shell/key-bindings.fish //go:embed shell/key-bindings.fish
var fishKeyBindings []byte var fishKeyBindings []byte
//go:embed man/man1/fzf.1
var manPage []byte
func printScript(label string, content []byte) { func printScript(label string, content []byte) {
fmt.Println("### " + label + " ###") fmt.Println("### " + label + " ###")
fmt.Println(strings.TrimSpace(string(content))) fmt.Println(strings.TrimSpace(string(content)))
@@ -35,7 +39,7 @@ func printScript(label string, content []byte) {
} }
func exit(code int, err error) { func exit(code int, err error) {
if err != nil { if code == fzf.ExitError {
fmt.Fprintln(os.Stderr, err.Error()) fmt.Fprintln(os.Stderr, err.Error())
} }
os.Exit(code) os.Exit(code)
@@ -76,6 +80,21 @@ func main() {
} }
return return
} }
if options.Man {
file := fzf.WriteTemporaryFile([]string{string(manPage)}, "\n")
if len(file) == 0 {
fmt.Print(string(manPage))
return
}
defer os.Remove(file)
cmd := exec.Command("man", file)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
fmt.Print(string(manPage))
}
return
}
code, err := fzf.Run(options) code, err := fzf.Run(options)
exit(code, err) exit(code, err)

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf-tmux 1 "May 2024" "fzf 0.52.1" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "Jun 2024" "fzf 0.53.0" "fzf-tmux - open fzf in tmux split pane"
.SH NAME .SH NAME
fzf-tmux - open fzf in tmux split pane fzf-tmux - open fzf in tmux split pane

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "May 2024" "fzf 0.52.1" "fzf - a command-line fuzzy finder" .TH fzf 1 "Jun 2024" "fzf 0.53.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -30,7 +30,10 @@ fzf - a command-line fuzzy finder
fzf [options] fzf [options]
.SH DESCRIPTION .SH DESCRIPTION
fzf is a general-purpose command-line fuzzy finder. fzf is an interactive filter program for any kind of list.
It implements a "fuzzy" matching algorithm, so you can quickly type in patterns
with omitted characters and still get the results you want.
.SH OPTIONS .SH OPTIONS
.SS Note .SS Note
@@ -58,14 +61,32 @@ Do not normalize latin script letters for matching.
.BI "--scheme=" SCHEME .BI "--scheme=" SCHEME
Choose scoring scheme tailored for different types of input. Choose scoring scheme tailored for different types of input.
.br .RS
.BR default " Generic scoring scheme designed to work well with any type of input" .B default
.br .RS
.BR path " Scoring scheme well suited for file paths Generic scoring scheme designed to work well with any type of input.
.br .RE
.BR history " Scoring scheme well suited for command history or any input where chronological ordering is important .RE
Sets \fB--tiebreak=index\fR as well.
.br .RS
.B path
.RS
Additional bonus point is only given to the characters after path separator.
You might want to choose this scheme over \fBdefault\fR if you have many files
with spaces in their paths.
.RE
.RE
.RS
.B history
.RS
Scoring scheme well suited for command history or any input where chronological
ordering is important. No additional bonus points are given so that we give
more weight to the chronological ordering. This also sets
\fB--tiebreak=index\fR.
.RE
.RE
.TP .TP
.BI "--algo=" TYPE .BI "--algo=" TYPE
Fuzzy matching algorithm (default: v2) Fuzzy matching algorithm (default: v2)
@@ -85,7 +106,8 @@ See \fBFIELD INDEX EXPRESSION\fR for the details.
Transform the presentation of each line using field index expressions Transform the presentation of each line using field index expressions
.TP .TP
.BI "-d, --delimiter=" "STR" .BI "-d, --delimiter=" "STR"
Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style) Field delimiter regex for \fB--nth\fR, \fB--with-nth\fR, and field index
expressions (default: AWK-style)
.TP .TP
.BI "--disabled" .BI "--disabled"
Do not perform search. With this option, fzf becomes a simple selector Do not perform search. With this option, fzf becomes a simple selector
@@ -96,6 +118,16 @@ interface rather than a "fuzzy finder". You can later enable the search using
.B "+s, --no-sort" .B "+s, --no-sort"
Do not sort the result Do not sort the result
.TP .TP
.B "--tail=NUM"
Maximum number of items to keep in memory. This is useful when you want to
browse an endless stream of data (e.g. log stream) with fzf while limiting
memory usage.
.RS
e.g.
\fB# Interactive filtering of a log stream
tail -f *.log | fzf --tail 100000 --tac --no-sort --exact\fR
.RE
.TP
.B "--track" .B "--track"
Make fzf track the current selection when the result list is updated. Make fzf track the current selection when the result list is updated.
This can be useful when browsing logs using fzf with sorting disabled. It is This can be useful when browsing logs using fzf with sorting disabled. It is
@@ -162,13 +194,16 @@ the details.
.B "--cycle" .B "--cycle"
Enable cyclic scroll Enable cyclic scroll
.TP .TP
.B "--no-multi-line"
Disable multi-line display of items when using \fB--read0\fR
.TP
.B "--keep-right" .B "--keep-right"
Keep the right end of the line visible when it's too long. Effective only when Keep the right end of the line visible when it's too long. Effective only when
the query string is empty. the query string is empty.
.TP .TP
.BI "--scroll-off=" "LINES" .BI "--scroll-off=" "LINES"
Number of screen lines to keep above or below when scrolling to the top or to Number of screen lines to keep above or below when scrolling to the top or to
the bottom (default: 0). the bottom (default: 3).
.TP .TP
.B "--no-hscroll" .B "--no-hscroll"
Disable horizontal scroll Disable horizontal scroll
@@ -204,17 +239,41 @@ height minus the given value.
fzf --height=-1 fzf --height=-1
When prefixed with \fB~\fR, fzf will automatically determine the height in the When prefixed with \fB~\fR, fzf will automatically determine the height in the
range according to the input size. Note that adaptive height is not compatible range according to the input size.
with top/bottom margin and padding given in percent size. It is also not
compatible with a negative height value.
# Will not take up 100% of the screen # Will not take up 100% of the screen
seq 5 | fzf --height=~100% seq 5 | fzf --height=~100%
Adaptive height has the following limitations:
.br
* Cannot be used with top/bottom margin and padding given in percent size
.br
* Negative value is not allowed
.br
* It will not find the right size when there are multi-line items
.TP .TP
.BI "--min-height=" "HEIGHT" .BI "--min-height=" "HEIGHT"
Minimum height when \fB--height\fR is given in percent (default: 10). Minimum height when \fB--height\fR is given in percent (default: 10).
Ignored when \fB--height\fR is not specified. Ignored when \fB--height\fR is not specified.
.TP
.BI "--tmux" "[=[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]]"
Start fzf in a tmux popup (default \fBcenter,50%\fR). Requires tmux 3.3 or
later. This option is ignored if you are not running fzf inside tmux.
e.g.
\fB# Popup in the center with 70% width and height
fzf --tmux 70%
# Popup on the left with 40% width and 100% height
fzf --tmux right,40%
# Popup on the bottom with 100% width and 30% height
fzf --tmux bottom,30%
# Popup on the top with 80% width and 40% height
fzf --tmux top,80%,40%\fR
.TP .TP
.BI "--layout=" "LAYOUT" .BI "--layout=" "LAYOUT"
Choose the layout (default: default) Choose the layout (default: default)
@@ -420,10 +479,14 @@ Do not display scrollbar. A synonym for \fB--scrollbar=''\fB
Input prompt (default: '> ') Input prompt (default: '> ')
.TP .TP
.BI "--pointer=" "STR" .BI "--pointer=" "STR"
Pointer to the current line (default: '>') Pointer to the current line (default: '▌' or '>' depending on \fB--no-unicode\fR)
.TP .TP
.BI "--marker=" "STR" .BI "--marker=" "STR"
Multi-select marker (default: '>') Multi-select marker (default: '┃' or '>' depending on \fB--no-unicode\fR)
.TP
.BI "--marker-multi-line=" "STR"
Multi-select marker for multi-line entries. 3 elements for top, middle, and bottom.
(default: '╻┃╹' or '.|'' depending on \fB--no-unicode\fR)
.TP .TP
.BI "--header=" "STR" .BI "--header=" "STR"
The given string will be printed as the sticky header. The lines are displayed The given string will be printed as the sticky header. The lines are displayed
@@ -455,7 +518,7 @@ color mappings.
.RS .RS
.B BASE SCHEME: .B BASE SCHEME:
(default: dark on 256-color terminal, otherwise 16) (default: \fBdark\fR on 256-color terminal, otherwise \fB16\fR; If \fBNO_COLOR\fR is set, \fBbw\fR)
\fBdark \fRColor scheme for dark 256-color terminal \fBdark \fRColor scheme for dark 256-color terminal
\fBlight \fRColor scheme for light 256-color terminal \fBlight \fRColor scheme for light 256-color terminal
@@ -794,6 +857,14 @@ list.
e.g. e.g.
\fBfzf --expect=ctrl-v,ctrl-t,alt-s --expect=f1,f2,~,@\fR \fBfzf --expect=ctrl-v,ctrl-t,alt-s --expect=f1,f2,~,@\fR
.RE .RE
This option is not compatible with \fB--bind\fR on the same key and will take
precedence over it. To combine the two, use \fBprint\fR action.
.RS
e.g.
\fBfzf --multi --bind 'enter:print()+accept,ctrl-y:select-all+print(ctrl-y)+accept'\fR
.RE
.TP .TP
.B "--read0" .B "--read0"
Read input delimited by ASCII NUL characters instead of newline characters Read input delimited by ASCII NUL characters instead of newline characters
@@ -871,9 +942,16 @@ e.g.
# Choose port automatically and export it as $FZF_PORT to the child process # Choose port automatically and export it as $FZF_PORT to the child process
fzf --listen --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port' fzf --listen --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'
\fR \fR
.SS Help
.TP .TP
.B "--version" .B "--version"
Display version information and exit Display version information and exit
.TP
.B "--help"
Show help message
.TP
.B "--man"
Show man page
.SS Directory traversal .SS Directory traversal
.TP .TP
@@ -911,7 +989,7 @@ e.g. \fBeval "$(fzf --bash)"\fR
.B "--zsh" .B "--zsh"
Print script to set up Zsh shell integration Print script to set up Zsh shell integration
e.g. \fBeval "$(fzf --zsh)"\fR e.g. \fBsource <(fzf --zsh)\fR
.TP .TP
.B "--fish" .B "--fish"
@@ -948,6 +1026,8 @@ you need to protect against DNS rebinding and privilege escalation attacks.
.br .br
.BR 2 " Error" .BR 2 " Error"
.br .br
.BR 126 " Permission denied error from \fBbecome\fR action"
.br
.BR 127 " Invalid shell command for \fBbecome\fR action" .BR 127 " Invalid shell command for \fBbecome\fR action"
.br .br
.BR 130 " Interrupted with \fBCTRL-C\fR or \fBESC\fR" .BR 130 " Interrupted with \fBCTRL-C\fR or \fBESC\fR"
@@ -1362,7 +1442,7 @@ A key or an event can be bound to one or more of the following actions.
\fBpreview-half-page-up\fR \fBpreview-half-page-up\fR
\fBpreview-bottom\fR \fBpreview-bottom\fR
\fBpreview-top\fR \fBpreview-top\fR
\fBprint-query\fR (print query and exit) \fBprint(...)\fR (add string to the output queue and print on exit)
\fBput\fR (put the character to the prompt) \fBput\fR (put the character to the prompt)
\fBput(...)\fR (put the given string to the prompt) \fBput(...)\fR (put the given string to the prompt)
\fBrefresh-preview\fR \fBrefresh-preview\fR

View File

@@ -327,7 +327,10 @@ function! s:common_sink(action, lines) abort
" the execution (e.g. `set autochdir` or `autocmd BufEnter * lcd ...`) " the execution (e.g. `set autochdir` or `autocmd BufEnter * lcd ...`)
let cwd = exists('w:fzf_pushd') ? w:fzf_pushd.dir : expand('%:p:h') let cwd = exists('w:fzf_pushd') ? w:fzf_pushd.dir : expand('%:p:h')
for item in a:lines for item in a:lines
if item[0] != '~' && item !~ (s:is_win ? '^[A-Z]:\' : '^/') if has('win32unix') && item !~ '/'
let item = substitute(item, '\', '/', 'g')
end
if item[0] != '~' && item !~ (s:is_win ? '^\([A-Z]:\)\?\' : '^/')
let sep = s:is_win ? '\' : '/' let sep = s:is_win ? '\' : '/'
let item = join([cwd, item], cwd[len(cwd)-1] == sep ? '' : sep) let item = join([cwd, item], cwd[len(cwd)-1] == sep ? '' : sep)
endif endif
@@ -487,6 +490,8 @@ function! s:extract_option(opts, name)
return opt return opt
endfunction endfunction
let s:need_cmd_window = has('win32unix') && $TERM_PROGRAM ==# 'mintty' && s:compare_versions($TERM_PROGRAM_VERSION, '3.4.5') < 0 && !executable('winpty')
function! fzf#run(...) abort function! fzf#run(...) abort
try try
let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh() let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
@@ -529,18 +534,19 @@ try
\ executable('tput') && filereadable('/dev/tty') \ executable('tput') && filereadable('/dev/tty')
let has_vim8_term = has('terminal') && has('patch-8.0.995') let has_vim8_term = has('terminal') && has('patch-8.0.995')
let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win
let use_term = has_nvim_term || let use_term = has_nvim_term || has_vim8_term
\ has_vim8_term && !has('win32unix') && (has('gui_running') || s:is_win || s:present(dict, 'down', 'up', 'left', 'right', 'window')) \ && !s:need_cmd_window
\ && (has('gui_running') || s:is_win || s:present(dict, 'down', 'up', 'left', 'right', 'window'))
let use_tmux = (has_key(dict, 'tmux') || (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:splittable(dict)) && s:tmux_enabled() let use_tmux = (has_key(dict, 'tmux') || (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:splittable(dict)) && s:tmux_enabled()
if prefer_tmux && use_tmux if prefer_tmux && use_tmux
let use_height = 0 let use_height = 0
let use_term = 0 let use_term = 0
endif endif
if use_term if use_term
let optstr .= ' --no-height' let optstr .= ' --no-height --no-tmux'
elseif use_height elseif use_height
let height = s:calc_size(&lines, dict.down, dict) let height = s:calc_size(&lines, dict.down, dict)
let optstr .= ' --height='.height let optstr .= ' --no-tmux --height='.height
endif endif
" Respect --border option given in $FZF_DEFAULT_OPTS and 'options' " Respect --border option given in $FZF_DEFAULT_OPTS and 'options'
let optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr]) let optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr])
@@ -573,19 +579,21 @@ function! s:fzf_tmux(dict)
if empty(size) if empty(size)
for o in ['up', 'down', 'left', 'right'] for o in ['up', 'down', 'left', 'right']
if s:present(a:dict, o) if s:present(a:dict, o)
let spec = a:dict[o] let size = o . ',' . a:dict[o]
if (o == 'up' || o == 'down') && spec[0] == '~'
let size = '-'.o[0].s:calc_size(&lines, spec, a:dict)
else
" Legacy boolean option
let size = '-'.o[0].(spec == 1 ? '' : substitute(spec, '^\~', '', ''))
endif
break break
endif endif
endfor endfor
endif endif
return printf('LINES=%d COLUMNS=%d %s %s %s --',
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-')) " Legacy fzf-tmux options
if size =~ '-'
return printf('LINES=%d COLUMNS=%d %s %s %s --',
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
end
" Using native --tmux option
let in = (has_key(a:dict, 'source') ? '' : ' --force-tty-in')
return printf('%s --tmux %s%s', fzf#shellescape(fzf#exec()), size, in)
endfunction endfunction
function! s:splittable(dict) function! s:splittable(dict)
@@ -708,10 +716,10 @@ function! s:execute(dict, command, use_height, temps) abort
call jobstart(cmd, fzf) call jobstart(cmd, fzf)
return [] return []
endif endif
elseif has('win32unix') && $TERM !=# 'cygwin' elseif s:need_cmd_window
let shellscript = s:fzf_tempname() let shellscript = s:fzf_tempname()
call s:writefile([command], shellscript) call s:writefile([command], shellscript)
let command = 'cmd.exe /C '.fzf#shellescape('set "TERM=" & start /WAIT sh -c '.shellscript) let command = 'start //WAIT sh -c '.shellscript
let a:temps.shellscript = shellscript let a:temps.shellscript = shellscript
endif endif
if a:use_height if a:use_height

View File

@@ -101,75 +101,83 @@ _fzf_opts_completion() {
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}" prev="${COMP_WORDS[COMP_CWORD-1]}"
opts=" opts="
-h --help
-e --exact
+x --no-extended
-q --query
-f --filter
--literal
--scheme
--expect
--disabled
--tiebreak
--bind
--color
-d --delimiter
-n --nth
--with-nth
+s --no-sort
--track
--tac
-i --ignore-case
+i --no-ignore-case
-m --multi
--ansi
--no-mouse
+c --no-color +c --no-color
--no-bold +i --no-ignore-case
--layout +s --no-sort
--reverse +x --no-extended
--cycle --ansi
--keep-right --bash
--no-hscroll --bind
--hscroll-off
--scroll-off
--filepath-word
--info
--separator
--no-separator
--no-scrollbar
--jump-labels
-1 --select-1
-0 --exit-0
--read0
--print0
--print-query
--prompt
--pointer
--marker
--sync
--history
--history-size
--header
--header-lines
--header-first
--ellipsis
--preview
--preview-window
--height
--min-height
--border --border
--border-label --border-label
--border-label-pos --border-label-pos
--color
--cycle
--disabled
--ellipsis
--expect
--filepath-word
--fish
--header
--header-first
--header-lines
--height
--highlight-line
--history
--history-size
--hscroll-off
--info
--jump-labels
--keep-right
--layout
--listen
--listen-unsafe
--literal
--man
--margin
--marker
--min-height
--no-bold
--no-clear
--no-hscroll
--no-mouse
--no-scrollbar
--no-separator
--no-unicode
--padding
--pointer
--preview
--preview-label --preview-label
--preview-label-pos --preview-label-pos
--no-unicode --preview-window
--margin --print-query
--padding --print0
--prompt
--read0
--reverse
--scheme
--scroll-off
--separator
--sync
--tabstop --tabstop
--listen --tac
--no-clear --tiebreak
--tmux
--track
--version --version
--with-nth
--with-shell
--zsh
-0 --exit-0
-1 --select-1
-d --delimiter
-e --exact
-f --filter
-h --help
-i --ignore-case
-m --multi
-n --nth
-q --query
--" --"
case "${prev}" in case "${prev}" in

View File

@@ -57,15 +57,15 @@ __fzf_cd__() {
if command -v perl > /dev/null; then if command -v perl > /dev/null; then
__fzf_history__() { __fzf_history__() {
local output script local output script
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++' script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; s/\n/\n\t/gm; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
output=$( output=$(
set +o pipefail set +o pipefail
builtin fc -lnr -2147483648 | builtin fc -lnr -2147483648 |
last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" | last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" |
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0") \ FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE" FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
) || return ) || return
READLINE_LINE=${output#*$'\t'} READLINE_LINE=$(command perl -pe 's/^\d*\t//' <<< "$output")
if [[ -z "$READLINE_POINT" ]]; then if [[ -z "$READLINE_POINT" ]]; then
echo "$READLINE_LINE" echo "$READLINE_LINE"
else else
@@ -91,7 +91,7 @@ else # awk - fallback for POSIX systems
set +o pipefail set +o pipefail
builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )* builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )*
command $__fzf_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )* command $__fzf_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0") \ FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE" FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
) || return ) || return
READLINE_LINE=${output#*$'\t'} READLINE_LINE=${output#*$'\t'}

View File

@@ -59,9 +59,6 @@ function fzf_key_bindings
function fzf-history-widget -d "Show command history" function fzf-history-widget -d "Show command history"
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "--scheme=history --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m")
set -lx FZF_DEFAULT_OPTS_FILE ''
set -l FISH_MAJOR (echo $version | cut -f1 -d.) set -l FISH_MAJOR (echo $version | cut -f1 -d.)
set -l FISH_MINOR (echo $version | cut -f2 -d.) set -l FISH_MINOR (echo $version | cut -f2 -d.)
@@ -69,10 +66,19 @@ function fzf_key_bindings
# history's -z flag was added in fish 2.4.0, so don't use it for versions # history's -z flag was added in fish 2.4.0, so don't use it for versions
# before 2.4.0. # before 2.4.0.
if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ]; if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ];
history -z | eval (__fzfcmd) --read0 --print0 -q '(commandline)' | read -lz result if type -P perl > /dev/null 2>&1
and commandline -- $result set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --highlight-line $FZF_CTRL_R_OPTS +m")
set -lx FZF_DEFAULT_OPTS_FILE ''
builtin history -z --reverse | command perl -0 -pe 's/^/$.\t/g; s/\n/\n\t/gm' | eval (__fzfcmd) --tac --read0 --print0 -q '(commandline)' | command perl -pe 's/^\d*\t//' | read -lz result
and commandline -- $result
else
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "--scheme=history --bind=ctrl-r:toggle-sort --highlight-line $FZF_CTRL_R_OPTS +m")
set -lx FZF_DEFAULT_OPTS_FILE ''
builtin history -z | eval (__fzfcmd) --read0 --print0 -q '(commandline)' | read -lz result
and commandline -- $result
end
else else
history | eval (__fzfcmd) -q '(commandline)' | read -l result builtin history | eval (__fzfcmd) -q '(commandline)' | read -l result
and commandline -- $result and commandline -- $result
end end
end end
@@ -93,7 +99,7 @@ function fzf_key_bindings
eval (__fzfcmd)' +m --query "'$fzf_query'"' | read -l result eval (__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
if [ -n "$result" ] if [ -n "$result" ]
cd -- $result builtin cd -- $result
# Remove last token from commandline. # Remove last token from commandline.
commandline -t "" commandline -t ""

View File

@@ -108,14 +108,22 @@ fi
fzf-history-widget() { fzf-history-widget() {
local selected num local selected num
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
selected="$(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' | # Ensure the associative history array, which maps event numbers to the full
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m") \ # history lines, is loaded, and that Perl is installed for multi-line output.
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))" if zmodload -F zsh/parameter p:history 2>/dev/null && (( ${#commands[perl]} )); then
selected="$(printf '%1$s\t%2$s\000' "${(vk)history[@]}" |
perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\s+(.*)/, $1)}++) { s/\n/\n\t/gm; print; }' |
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
else
selected="$(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
fi
local ret=$? local ret=$?
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
num=$(awk '{print $1}' <<< "$selected") if num=$(awk '{print $1; exit}' <<< "$selected" | grep -o '^[1-9][0-9]*'); then
if [[ "$num" =~ '^[1-9][0-9]*\*?$' ]]; then zle vi-fetch-history -n $num
zle vi-fetch-history -n ${num%\*}
else # selected is a custom query, not from history else # selected is a custom query, not from history
LBUFFER="$selected" LBUFFER="$selected"
fi fi

View File

@@ -98,32 +98,33 @@ func _() {
_ = x[actPreviewHalfPageDown-87] _ = x[actPreviewHalfPageDown-87]
_ = x[actPrevHistory-88] _ = x[actPrevHistory-88]
_ = x[actPrevSelected-89] _ = x[actPrevSelected-89]
_ = x[actPut-90] _ = x[actPrint-90]
_ = x[actNextHistory-91] _ = x[actPut-91]
_ = x[actNextSelected-92] _ = x[actNextHistory-92]
_ = x[actExecute-93] _ = x[actNextSelected-93]
_ = x[actExecuteSilent-94] _ = x[actExecute-94]
_ = x[actExecuteMulti-95] _ = x[actExecuteSilent-95]
_ = x[actSigStop-96] _ = x[actExecuteMulti-96]
_ = x[actFirst-97] _ = x[actSigStop-97]
_ = x[actLast-98] _ = x[actFirst-98]
_ = x[actReload-99] _ = x[actLast-99]
_ = x[actReloadSync-100] _ = x[actReload-100]
_ = x[actDisableSearch-101] _ = x[actReloadSync-101]
_ = x[actEnableSearch-102] _ = x[actDisableSearch-102]
_ = x[actSelect-103] _ = x[actEnableSearch-103]
_ = x[actDeselect-104] _ = x[actSelect-104]
_ = x[actUnbind-105] _ = x[actDeselect-105]
_ = x[actRebind-106] _ = x[actUnbind-106]
_ = x[actBecome-107] _ = x[actRebind-107]
_ = x[actResponse-108] _ = x[actBecome-108]
_ = x[actShowHeader-109] _ = x[actResponse-109]
_ = x[actHideHeader-110] _ = x[actShowHeader-110]
_ = x[actHideHeader-111]
} }
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader" const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader"
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 256, 277, 292, 306, 320, 333, 350, 358, 371, 387, 399, 407, 421, 435, 446, 457, 475, 492, 499, 518, 530, 544, 553, 568, 580, 593, 604, 615, 627, 641, 662, 677, 692, 709, 716, 721, 730, 741, 752, 765, 780, 791, 804, 811, 824, 837, 854, 869, 882, 896, 910, 926, 946, 958, 981, 999, 1023, 1041, 1058, 1068, 1084, 1106, 1119, 1135, 1147, 1161, 1177, 1195, 1215, 1237, 1251, 1266, 1272, 1286, 1301, 1311, 1327, 1342, 1352, 1360, 1367, 1376, 1389, 1405, 1420, 1429, 1440, 1449, 1458, 1467, 1478, 1491, 1504} var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 256, 277, 292, 306, 320, 333, 350, 358, 371, 387, 399, 407, 421, 435, 446, 457, 475, 492, 499, 518, 530, 544, 553, 568, 580, 593, 604, 615, 627, 641, 662, 677, 692, 709, 716, 721, 730, 741, 752, 765, 780, 791, 804, 811, 824, 837, 854, 869, 882, 896, 910, 926, 946, 958, 981, 999, 1023, 1041, 1058, 1068, 1084, 1106, 1119, 1135, 1147, 1161, 1177, 1195, 1215, 1237, 1251, 1266, 1274, 1280, 1294, 1309, 1319, 1335, 1350, 1360, 1368, 1375, 1384, 1397, 1413, 1428, 1437, 1448, 1457, 1466, 1475, 1486, 1499, 1512}
func (i actionType) String() string { func (i actionType) String() string {
if i < 0 || i >= actionType(len(_actionType_index)-1) { if i < 0 || i >= actionType(len(_actionType_index)-1) {

View File

@@ -342,8 +342,8 @@ func TestAnsiCodeStringConversion(t *testing.T) {
state := interpretCode(code, prevState) state := interpretCode(code, prevState)
if expected != state.ToString() { if expected != state.ToString() {
t.Errorf("expected: %s, actual: %s", t.Errorf("expected: %s, actual: %s",
strings.Replace(expected, "\x1b[", "\\x1b[", -1), strings.ReplaceAll(expected, "\x1b[", "\\x1b["),
strings.Replace(state.ToString(), "\x1b[", "\\x1b[", -1)) strings.ReplaceAll(state.ToString(), "\x1b[", "\\x1b["))
} }
} }
assert("\x1b[m", nil, "") assert("\x1b[m", nil, "")

View File

@@ -48,7 +48,12 @@ func CountItems(cs []*Chunk) int {
if len(cs) == 0 { if len(cs) == 0 {
return 0 return 0
} }
return chunkSize*(len(cs)-1) + cs[len(cs)-1].count if len(cs) == 1 {
return cs[0].count
}
// First chunk might not be full due to --tail=N
return cs[0].count + chunkSize*(len(cs)-2) + cs[len(cs)-1].count
} }
// Push adds the item to the list // Push adds the item to the list
@@ -72,18 +77,53 @@ func (cl *ChunkList) Clear() {
} }
// Snapshot returns immutable snapshot of the ChunkList // Snapshot returns immutable snapshot of the ChunkList
func (cl *ChunkList) Snapshot() ([]*Chunk, int) { func (cl *ChunkList) Snapshot(tail int) ([]*Chunk, int, bool) {
cl.mutex.Lock() cl.mutex.Lock()
changed := false
if tail > 0 && CountItems(cl.chunks) > tail {
changed = true
// Find the number of chunks to keep
numChunks := 0
for left, i := tail, len(cl.chunks)-1; left > 0 && i >= 0; i-- {
numChunks++
left -= cl.chunks[i].count
}
// Copy the chunks to keep
ret := make([]*Chunk, numChunks)
copy(ret, cl.chunks[len(cl.chunks)-numChunks:])
for left, i := tail, len(ret)-1; i >= 0; i-- {
chunk := ret[i]
if chunk.count > left {
newChunk := *chunk
newChunk.count = left
oldCount := chunk.count
for i := 0; i < left; i++ {
newChunk.items[i] = chunk.items[oldCount-left+i]
}
ret[i] = &newChunk
break
}
left -= chunk.count
}
cl.chunks = ret
}
ret := make([]*Chunk, len(cl.chunks)) ret := make([]*Chunk, len(cl.chunks))
copy(ret, cl.chunks) copy(ret, cl.chunks)
// Duplicate the last chunk // Duplicate the first and the last chunk
if cnt := len(ret); cnt > 0 { if cnt := len(ret); cnt > 0 {
if tail > 0 && cnt > 1 {
newChunk := *ret[0]
ret[0] = &newChunk
}
newChunk := *ret[cnt-1] newChunk := *ret[cnt-1]
ret[cnt-1] = &newChunk ret[cnt-1] = &newChunk
} }
cl.mutex.Unlock() cl.mutex.Unlock()
return ret, CountItems(ret) return ret, CountItems(ret), changed
} }

View File

@@ -17,7 +17,7 @@ func TestChunkList(t *testing.T) {
}) })
// Snapshot // Snapshot
snapshot, count := cl.Snapshot() snapshot, count, _ := cl.Snapshot(0)
if len(snapshot) > 0 || count > 0 { if len(snapshot) > 0 || count > 0 {
t.Error("Snapshot should be empty now") t.Error("Snapshot should be empty now")
} }
@@ -32,7 +32,7 @@ func TestChunkList(t *testing.T) {
} }
// But the new snapshot should contain the added items // But the new snapshot should contain the added items
snapshot, count = cl.Snapshot() snapshot, count, _ = cl.Snapshot(0)
if len(snapshot) != 1 && count != 2 { if len(snapshot) != 1 && count != 2 {
t.Error("Snapshot should not be empty now") t.Error("Snapshot should not be empty now")
} }
@@ -61,7 +61,7 @@ func TestChunkList(t *testing.T) {
} }
// New snapshot // New snapshot
snapshot, count = cl.Snapshot() snapshot, count, _ = cl.Snapshot(0)
if len(snapshot) != 3 || !snapshot[0].IsFull() || if len(snapshot) != 3 || !snapshot[0].IsFull() ||
!snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 { !snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 {
t.Error("Expected two full chunks and one more chunk") t.Error("Expected two full chunks and one more chunk")
@@ -78,3 +78,39 @@ func TestChunkList(t *testing.T) {
t.Error("Unexpected number of items:", lastChunkCount) t.Error("Unexpected number of items:", lastChunkCount)
} }
} }
func TestChunkListTail(t *testing.T) {
cl := NewChunkList(func(item *Item, s []byte) bool {
item.text = util.ToChars(s)
return true
})
total := chunkSize*2 + chunkSize/2
for i := 0; i < total; i++ {
cl.Push([]byte(fmt.Sprintf("item %d", i)))
}
snapshot, count, changed := cl.Snapshot(0)
assertCount := func(expected int, shouldChange bool) {
if count != expected || CountItems(snapshot) != expected {
t.Errorf("Unexpected count: %d (expected: %d)", count, expected)
}
if changed != shouldChange {
t.Error("Unexpected change status")
}
}
assertCount(total, false)
tail := chunkSize + chunkSize/2
snapshot, count, changed = cl.Snapshot(tail)
assertCount(tail, true)
snapshot, count, changed = cl.Snapshot(tail)
assertCount(tail, false)
snapshot, count, changed = cl.Snapshot(0)
assertCount(tail, false)
tail = chunkSize / 2
snapshot, count, changed = cl.Snapshot(tail)
assertCount(tail, true)
}

View File

@@ -67,9 +67,9 @@ const (
) )
const ( const (
ExitCancel = -1
ExitOk = 0 ExitOk = 0
ExitNoMatch = 1 ExitNoMatch = 1
ExitError = 2 ExitError = 2
ExitBecome = 126
ExitInterrupt = 130 ExitInterrupt = 130
) )

View File

@@ -2,6 +2,7 @@
package fzf package fzf
import ( import (
"os"
"sync" "sync"
"time" "time"
@@ -17,8 +18,38 @@ Matcher -> EvtSearchFin -> Terminal (update list)
Matcher -> EvtHeader -> Terminal (update header) Matcher -> EvtHeader -> Terminal (update header)
*/ */
type revision struct {
major int
minor int
}
func (r *revision) bumpMajor() {
r.major++
r.minor = 0
}
func (r *revision) bumpMinor() {
r.minor++
}
func (r revision) equals(other revision) bool {
return r.major == other.major && r.minor == other.minor
}
func (r revision) compatible(other revision) bool {
return r.major == other.major
}
// Run starts fzf // Run starts fzf
func Run(opts *Options) (int, error) { func Run(opts *Options) (int, error) {
if opts.Tmux != nil && len(os.Getenv("TMUX")) > 0 && opts.Tmux.index >= opts.Height.index {
return runTmux(os.Args, opts)
}
if needWinpty(opts) {
return runWinpty(os.Args, opts)
}
if err := postProcessOptions(opts); err != nil { if err := postProcessOptions(opts); err != nil {
return ExitError, err return ExitError, err
} }
@@ -146,8 +177,8 @@ func Run(opts *Options) (int, error) {
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos, opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
opts.Filter == nil, opts.Nth, opts.Delimiter, runes) opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
} }
inputRevision := 0 inputRevision := revision{}
snapshotRevision := 0 snapshotRevision := revision{}
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision) matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
// Filtering mode // Filtering mode
@@ -181,7 +212,8 @@ func Run(opts *Options) (int, error) {
eventBox.Unwatch(EvtReadNew) eventBox.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin) eventBox.WaitFor(EvtReadFin)
snapshot, _ := chunkList.Snapshot() // NOTE: Streaming filter is inherently not compatible with --tail
snapshot, _, _ := chunkList.Snapshot(opts.Tail)
merger, _ := matcher.scan(MatchRequest{ merger, _ := matcher.scan(MatchRequest{
chunks: snapshot, chunks: snapshot,
pattern: pattern}) pattern: pattern})
@@ -252,7 +284,7 @@ func Run(opts *Options) (int, error) {
reading = true reading = true
chunkList.Clear() chunkList.Clear()
itemIndex = 0 itemIndex = 0
inputRevision++ inputRevision.bumpMajor()
header = make([]string, 0, opts.HeaderLines) header = make([]string, 0, opts.HeaderLines)
go reader.restart(command, environ) go reader.restart(command, environ)
} }
@@ -297,10 +329,14 @@ func Run(opts *Options) (int, error) {
useSnapshot = false useSnapshot = false
} }
if !useSnapshot { if !useSnapshot {
if snapshotRevision != inputRevision { if !snapshotRevision.compatible(inputRevision) {
query = []rune{} query = []rune{}
} }
snapshot, count = chunkList.Snapshot() var changed bool
snapshot, count, changed = chunkList.Snapshot(opts.Tail)
if changed {
inputRevision.bumpMinor()
}
snapshotRevision = inputRevision snapshotRevision = inputRevision
} }
total = count total = count
@@ -340,7 +376,10 @@ func Run(opts *Options) (int, error) {
break break
} }
if !useSnapshot { if !useSnapshot {
newSnapshot, newCount := chunkList.Snapshot() newSnapshot, newCount, changed := chunkList.Snapshot(opts.Tail)
if changed {
inputRevision.bumpMinor()
}
// We want to avoid showing empty list when reload is triggered // We want to avoid showing empty list when reload is triggered
// and the query string is changed at the same time i.e. command != nil && changed // and the query string is changed at the same time i.e. command != nil && changed
if command == nil || newCount > 0 { if command == nil || newCount > 0 {

View File

@@ -6,8 +6,8 @@ import (
"unsafe" "unsafe"
) )
func writeTemporaryFile(data []string, printSep string) string { func WriteTemporaryFile(data []string, printSep string) string {
f, err := os.CreateTemp("", "fzf-preview-*") f, err := os.CreateTemp("", "fzf-temp-*")
if err != nil { if err != nil {
// Unable to create temporary file // Unable to create temporary file
// FIXME: Should we terminate the program? // FIXME: Should we terminate the program?

View File

@@ -1,6 +1,8 @@
package fzf package fzf
import ( import (
"math"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
@@ -17,7 +19,7 @@ func (item *Item) Index() int32 {
return item.text.Index return item.text.Index
} }
var minItem = Item{text: util.Chars{Index: -1}} var minItem = Item{text: util.Chars{Index: math.MinInt32}}
func (item *Item) TrimLength() uint16 { func (item *Item) TrimLength() uint16 {
return item.text.TrimLength() return item.text.TrimLength()

View File

@@ -16,7 +16,7 @@ type MatchRequest struct {
pattern *Pattern pattern *Pattern
final bool final bool
sort bool sort bool
revision int revision revision
} }
// Matcher is responsible for performing search // Matcher is responsible for performing search
@@ -30,7 +30,7 @@ type Matcher struct {
partitions int partitions int
slab []*util.Slab slab []*util.Slab
mergerCache map[string]*Merger mergerCache map[string]*Merger
revision int revision revision
} }
const ( const (
@@ -40,7 +40,7 @@ const (
// NewMatcher returns a new Matcher // NewMatcher returns a new Matcher
func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern, func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
sort bool, tac bool, eventBox *util.EventBox, revision int) *Matcher { sort bool, tac bool, eventBox *util.EventBox, revision revision) *Matcher {
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions) partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
return &Matcher{ return &Matcher{
cache: cache, cache: cache,
@@ -82,11 +82,13 @@ func (m *Matcher) Loop() {
break break
} }
cacheCleared := false
if request.sort != m.sort || request.revision != m.revision { if request.sort != m.sort || request.revision != m.revision {
m.sort = request.sort m.sort = request.sort
m.revision = request.revision m.revision = request.revision
m.mergerCache = make(map[string]*Merger) m.mergerCache = make(map[string]*Merger)
m.cache.Clear() m.cache.Clear()
cacheCleared = true
} }
// Restart search // Restart search
@@ -95,20 +97,20 @@ func (m *Matcher) Loop() {
cancelled := false cancelled := false
count := CountItems(request.chunks) count := CountItems(request.chunks)
foundCache := false if !cacheCleared {
if count == prevCount { if count == prevCount {
// Look up mergerCache // Look up mergerCache
if cached, found := m.mergerCache[patternString]; found { if cached, found := m.mergerCache[patternString]; found {
foundCache = true merger = cached
merger = cached }
} else {
// Invalidate mergerCache
prevCount = count
m.mergerCache = make(map[string]*Merger)
} }
} else {
// Invalidate mergerCache
prevCount = count
m.mergerCache = make(map[string]*Merger)
} }
if !foundCache { if merger == nil {
merger, cancelled = m.scan(request) merger, cancelled = m.scan(request)
} }
@@ -160,6 +162,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
return PassMerger(&request.chunks, m.tac, request.revision), false return PassMerger(&request.chunks, m.tac, request.revision), false
} }
minIndex := request.chunks[0].items[0].Index()
cancelled := util.NewAtomicBool(false) cancelled := util.NewAtomicBool(false)
slices := m.sliceChunks(request.chunks) slices := m.sliceChunks(request.chunks)
@@ -231,11 +234,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
partialResult := <-resultChan partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches partialResults[partialResult.index] = partialResult.matches
} }
return NewMerger(pattern, partialResults, m.sort, m.tac, request.revision), false return NewMerger(pattern, partialResults, m.sort, m.tac, request.revision, minIndex), false
} }
// Reset is called to interrupt/signal the ongoing search // Reset is called to interrupt/signal the ongoing search
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, revision int) { func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, revision revision) {
pattern := m.patternBuilder(patternRunes) pattern := m.patternBuilder(patternRunes)
var event util.EventType var event util.EventType

View File

@@ -3,8 +3,8 @@ package fzf
import "fmt" import "fmt"
// EmptyMerger is a Merger with no data // EmptyMerger is a Merger with no data
func EmptyMerger(revision int) *Merger { func EmptyMerger(revision revision) *Merger {
return NewMerger(nil, [][]Result{}, false, false, revision) return NewMerger(nil, [][]Result{}, false, false, revision, 0)
} }
// Merger holds a set of locally sorted lists of items and provides the view of // Merger holds a set of locally sorted lists of items and provides the view of
@@ -20,19 +20,25 @@ type Merger struct {
final bool final bool
count int count int
pass bool pass bool
revision int revision revision
minIndex int32
} }
// PassMerger returns a new Merger that simply returns the items in the // PassMerger returns a new Merger that simply returns the items in the
// original order // original order
func PassMerger(chunks *[]*Chunk, tac bool, revision int) *Merger { func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
var minIndex int32
if len(*chunks) > 0 {
minIndex = (*chunks)[0].items[0].Index()
}
mg := Merger{ mg := Merger{
pattern: nil, pattern: nil,
chunks: chunks, chunks: chunks,
tac: tac, tac: tac,
count: 0, count: 0,
pass: true, pass: true,
revision: revision} revision: revision,
minIndex: minIndex}
for _, chunk := range *mg.chunks { for _, chunk := range *mg.chunks {
mg.count += chunk.count mg.count += chunk.count
@@ -41,7 +47,7 @@ func PassMerger(chunks *[]*Chunk, tac bool, revision int) *Merger {
} }
// NewMerger returns a new Merger // NewMerger returns a new Merger
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision int) *Merger { func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision revision, minIndex int32) *Merger {
mg := Merger{ mg := Merger{
pattern: pattern, pattern: pattern,
lists: lists, lists: lists,
@@ -52,7 +58,8 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revisi
tac: tac, tac: tac,
final: false, final: false,
count: 0, count: 0,
revision: revision} revision: revision,
minIndex: minIndex}
for _, list := range mg.lists { for _, list := range mg.lists {
mg.count += len(list) mg.count += len(list)
@@ -61,7 +68,7 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revisi
} }
// Revision returns revision number // Revision returns revision number
func (mg *Merger) Revision() int { func (mg *Merger) Revision() revision {
return mg.revision return mg.revision
} }
@@ -81,7 +88,7 @@ func (mg *Merger) First() Result {
func (mg *Merger) FindIndex(itemIndex int32) int { func (mg *Merger) FindIndex(itemIndex int32) int {
index := -1 index := -1
if mg.pass { if mg.pass {
index = int(itemIndex) index = int(itemIndex - mg.minIndex)
if mg.tac { if mg.tac {
index = mg.count - index - 1 index = mg.count - index - 1
} }
@@ -102,6 +109,13 @@ func (mg *Merger) Get(idx int) Result {
if mg.tac { if mg.tac {
idx = mg.count - idx - 1 idx = mg.count - idx - 1
} }
firstChunk := (*mg.chunks)[0]
if firstChunk.count < chunkSize && idx >= firstChunk.count {
idx -= firstChunk.count
chunk := (*mg.chunks)[idx/chunkSize+1]
return Result{item: &chunk.items[idx%chunkSize]}
}
chunk := (*mg.chunks)[idx/chunkSize] chunk := (*mg.chunks)[idx/chunkSize]
return Result{item: &chunk.items[idx%chunkSize]} return Result{item: &chunk.items[idx%chunkSize]}
} }

View File

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

View File

@@ -16,132 +16,150 @@ import (
"github.com/rivo/uniseg" "github.com/rivo/uniseg"
) )
const Usage = `usage: fzf [options] const Usage = `fzf is an interactive filter program for any kind of list.
It implements a "fuzzy" matching algorithm, so you can quickly type in patterns
with omitted characters and still get the results you want.
Project URL: https://github.com/junegunn/fzf
Author: Junegunn Choi <junegunn.c@gmail.com>
Usage: fzf [options]
Search Search
-x, --extended Extended-search mode -x, --extended Extended-search mode
(enabled by default; +x or --no-extended to disable) (enabled by default; +x or --no-extended to disable)
-e, --exact Enable Exact-match -e, --exact Enable Exact-match
-i, --ignore-case Case-insensitive match (default: smart-case match) -i, --ignore-case Case-insensitive match (default: smart-case match)
+i, --no-ignore-case Case-sensitive match +i, --no-ignore-case Case-sensitive match
--scheme=SCHEME Scoring scheme [default|path|history] --scheme=SCHEME Scoring scheme [default|path|history]
--literal Do not normalize latin script letters before matching --literal Do not normalize latin script letters before matching
-n, --nth=N[,..] Comma-separated list of field index expressions -n, --nth=N[,..] Comma-separated list of field index expressions
for limiting search scope. Each can be a non-zero for limiting search scope. Each can be a non-zero
integer or a range expression ([BEGIN]..[END]). integer or a range expression ([BEGIN]..[END]).
--with-nth=N[,..] Transform the presentation of each line using --with-nth=N[,..] Transform the presentation of each line using
field index expressions field index expressions
-d, --delimiter=STR Field delimiter regex (default: AWK-style) -d, --delimiter=STR Field delimiter regex (default: AWK-style)
+s, --no-sort Do not sort the result +s, --no-sort Do not sort the result
--track Track the current selection when the result is updated --tail=NUM Maximum number of items to keep in memory
--tac Reverse the order of the input --track Track the current selection when the result is updated
--disabled Do not perform search --tac Reverse the order of the input
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply --disabled Do not perform search
when the scores are tied [length|chunk|begin|end|index] --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
(default: length) when the scores are tied [length|chunk|begin|end|index]
(default: length)
Interface Interface
-m, --multi[=MAX] Enable multi-select with tab/shift-tab -m, --multi[=MAX] Enable multi-select with tab/shift-tab
--no-mouse Disable mouse --no-mouse Disable mouse
--bind=KEYBINDS Custom key bindings. Refer to the man page. --bind=KEYBINDS Custom key bindings. Refer to the man page.
--cycle Enable cyclic scroll --cycle Enable cyclic scroll
--keep-right Keep the right end of the line visible on overflow --no-multi-line Disable multi-line display of items when using --read0
--scroll-off=LINES Number of screen lines to keep above or below when --keep-right Keep the right end of the line visible on overflow
scrolling to the top or to the bottom (default: 0) --scroll-off=LINES Number of screen lines to keep above or below when
--no-hscroll Disable horizontal scroll scrolling to the top or to the bottom (default: 0)
--hscroll-off=COLS Number of screen columns to keep to the right of the --no-hscroll Disable horizontal scroll
highlighted substring (default: 10) --hscroll-off=COLS Number of screen columns to keep to the right of the
--filepath-word Make word-wise movements respect path separators highlighted substring (default: 10)
--jump-labels=CHARS Label characters for jump mode --filepath-word Make word-wise movements respect path separators
--jump-labels=CHARS Label characters for jump mode
Layout Layout
--height=[~]HEIGHT[%] Display fzf window below the cursor with the given --height=[~]HEIGHT[%] Display fzf window below the cursor with the given
height instead of using fullscreen. height instead of using fullscreen.
A negative value is calculated as the terminal height A negative value is calculated as the terminal height
minus the given value. minus the given value.
If prefixed with '~', fzf will determine the height If prefixed with '~', fzf will determine the height
according to the input size. according to the input size.
--min-height=HEIGHT Minimum height when --height is given in percent --min-height=HEIGHT Minimum height when --height is given in percent
(default: 10) (default: 10)
--layout=LAYOUT Choose layout: [default|reverse|reverse-list] --tmux[=OPTS] Start fzf in a tmux popup (requires tmux 3.3+)
--border[=STYLE] Draw border around the finder [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical| (default: center,50%)
top|bottom|left|right|none] (default: rounded) --layout=LAYOUT Choose layout: [default|reverse|reverse-list]
--border-label=LABEL Label to print on the border --border[=STYLE] Draw border around the finder
--border-label-pos=COL Position of the border label [rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
[POSITIVE_INTEGER: columns from left| top|bottom|left|right|none] (default: rounded)
NEGATIVE_INTEGER: columns from right][:bottom] --border-label=LABEL Label to print on the border
(default: 0 or center) --border-label-pos=COL Position of the border label
--margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L) [POSITIVE_INTEGER: columns from left|
--padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L) NEGATIVE_INTEGER: columns from right][:bottom]
--info=STYLE Finder info style (default: 0 or center)
[default|right|hidden|inline[-right][:PREFIX]] --margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L)
--separator=STR String to form horizontal separator on info line --padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
--no-separator Hide info line separator --info=STYLE Finder info style
--scrollbar[=C1[C2]] Scrollbar character(s) (each for main and preview window) [default|right|hidden|inline[-right][:PREFIX]]
--no-scrollbar Hide scrollbar --separator=STR String to form horizontal separator on info line
--prompt=STR Input prompt (default: '> ') --no-separator Hide info line separator
--pointer=STR Pointer to the current line (default: '>') --scrollbar[=C1[C2]] Scrollbar character(s) (each for main and preview window)
--marker=STR Multi-select marker (default: '>') --no-scrollbar Hide scrollbar
--header=STR String to print as header --prompt=STR Input prompt (default: '> ')
--header-lines=N The first N lines of the input are treated as header --pointer=STR Pointer to the current line (default: '▌' or '>')
--header-first Print header before the prompt line --marker=STR Multi-select marker (default: '┃' or '>')
--ellipsis=STR Ellipsis to show when line is truncated (default: '..') --marker-multi-line=STR Multi-select marker for multi-line entries;
3 elements for top, middle, and bottom (default: '╻┃╹')
--header=STR String to print as header
--header-lines=N The first N lines of the input are treated as header
--header-first Print header before the prompt line
--ellipsis=STR Ellipsis to show when line is truncated (default: '..')
Display Display
--ansi Enable processing of ANSI color codes --ansi Enable processing of ANSI color codes
--tabstop=SPACES Number of spaces for a tab character (default: 8) --tabstop=SPACES Number of spaces for a tab character (default: 8)
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
--highlight-line Highlight the whole current line --highlight-line Highlight the whole current line
--no-bold Do not use bold text --no-bold Do not use bold text
History History
--history=FILE History file --history=FILE History file
--history-size=N Maximum number of history entries (default: 1000) --history-size=N Maximum number of history entries (default: 1000)
Preview Preview
--preview=COMMAND Command to preview highlighted line ({}) --preview=COMMAND Command to preview highlighted line ({})
--preview-window=OPT Preview window layout (default: right:50%) --preview-window=OPT Preview window layout (default: right:50%)
[up|down|left|right][,SIZE[%]] [up|down|left|right][,SIZE[%]]
[,[no]wrap][,[no]cycle][,[no]follow][,[no]hidden] [,[no]wrap][,[no]cycle][,[no]follow][,[no]hidden]
[,border-BORDER_OPT] [,border-BORDER_OPT]
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES] [,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
[,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)] [,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
--preview-label=LABEL --preview-label=LABEL
--preview-label-pos=N Same as --border-label and --border-label-pos, --preview-label-pos=N Same as --border-label and --border-label-pos,
but for preview window but for preview window
Scripting Scripting
-q, --query=STR Start the finder with the given query -q, --query=STR Start the finder with the given query
-1, --select-1 Automatically select the only match -1, --select-1 Automatically select the only match
-0, --exit-0 Exit immediately when there's no match -0, --exit-0 Exit immediately when there's no match
-f, --filter=STR Filter mode. Do not start interactive finder. -f, --filter=STR Filter mode. Do not start interactive finder.
--print-query Print query as the first line --print-query Print query as the first line
--expect=KEYS Comma-separated list of keys to complete fzf --expect=KEYS Comma-separated list of keys to complete fzf
--read0 Read input delimited by ASCII NUL characters --read0 Read input delimited by ASCII NUL characters
--print0 Print output delimited by ASCII NUL characters --print0 Print output delimited by ASCII NUL characters
--sync Synchronous search for multi-staged filtering --sync Synchronous search for multi-staged filtering
--with-shell=STR Shell command and flags to start child processes with --with-shell=STR Shell command and flags to start child processes with
--listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /) --listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /)
(To allow remote process execution, use --listen-unsafe) (To allow remote process execution, use --listen-unsafe)
--version Display version information and exit
Directory traversal (Only used when $FZF_DEFAULT_COMMAND is not set) Directory traversal (Only used when $FZF_DEFAULT_COMMAND is not set)
--walker=OPTS [file][,dir][,follow][,hidden] (default: file,follow,hidden) --walker=OPTS [file][,dir][,follow][,hidden] (default: file,follow,hidden)
--walker-root=DIR Root directory from which to start walker (default: .) --walker-root=DIR Root directory from which to start walker (default: .)
--walker-skip=DIRS Comma-separated list of directory names to skip --walker-skip=DIRS Comma-separated list of directory names to skip
(default: .git,node_modules) (default: .git,node_modules)
Shell integration Shell integration
--bash Print script to set up Bash shell integration --bash Print script to set up Bash shell integration
--zsh Print script to set up Zsh shell integration --zsh Print script to set up Zsh shell integration
--fish Print script to set up Fish shell integration --fish Print script to set up Fish shell integration
Help
--version Display version information and exit
--help Show this message
--man Show man page
Environment variables Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty FZF_DEFAULT_COMMAND Default command to use when input is tty
FZF_DEFAULT_OPTS Default options (e.g. '--layout=reverse --info=inline') FZF_DEFAULT_OPTS Default options (e.g. '--layout=reverse --info=inline')
FZF_DEFAULT_OPTS_FILE Location of the file to read default options from FZF_DEFAULT_OPTS_FILE Location of the file to read default options from
FZF_API_KEY X-API-Key header for HTTP server (--listen) FZF_API_KEY X-API-Key header for HTTP server (--listen)
` `
@@ -173,6 +191,7 @@ type heightSpec struct {
percent bool percent bool
auto bool auto bool
inverse bool inverse bool
index int
} }
type sizeSpec struct { type sizeSpec struct {
@@ -180,6 +199,13 @@ type sizeSpec struct {
percent bool percent bool
} }
func (s sizeSpec) String() string {
if s.percent {
return fmt.Sprintf("%d%%", int(s.size))
}
return fmt.Sprintf("%d", int(s.size))
}
func defaultMargin() [4]sizeSpec { func defaultMargin() [4]sizeSpec {
return [4]sizeSpec{} return [4]sizeSpec{}
} }
@@ -199,8 +225,16 @@ const (
posDown posDown
posLeft posLeft
posRight posRight
posCenter
) )
type tmuxOptions struct {
width sizeSpec
height sizeSpec
position windowPosition
index int
}
type layoutType int type layoutType int
const ( const (
@@ -248,6 +282,77 @@ func (o *previewOpts) Toggle() {
o.hidden = !o.hidden o.hidden = !o.hidden
} }
func defaultTmuxOptions(index int) *tmuxOptions {
return &tmuxOptions{
position: posCenter,
width: sizeSpec{50, true},
height: sizeSpec{50, true},
index: index}
}
func parseTmuxOptions(arg string, index int) (*tmuxOptions, error) {
var err error
opts := defaultTmuxOptions(index)
tokens := splitRegexp.Split(arg, -1)
errorToReturn := errors.New("invalid tmux option: " + arg + " (expected: [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]])")
if len(tokens) == 0 || len(tokens) > 3 {
return nil, errorToReturn
}
// Defaults to 'center'
switch tokens[0] {
case "top", "up":
opts.position = posUp
opts.width = sizeSpec{100, true}
case "bottom", "down":
opts.position = posDown
opts.width = sizeSpec{100, true}
case "left":
opts.position = posLeft
opts.height = sizeSpec{100, true}
case "right":
opts.position = posRight
opts.height = sizeSpec{100, true}
case "center":
default:
tokens = append([]string{"center"}, tokens...)
}
// One size given
var size1 sizeSpec
if len(tokens) > 1 {
if size1, err = parseSize(tokens[1], 100, "size"); err != nil {
return nil, errorToReturn
}
}
// Two sizes given
var size2 sizeSpec
if len(tokens) == 3 {
if size2, err = parseSize(tokens[2], 100, "size"); err != nil {
return nil, errorToReturn
}
opts.width = size1
opts.height = size2
} else if len(tokens) == 2 {
switch tokens[0] {
case "top", "up":
opts.height = size1
case "bottom", "down":
opts.height = size1
case "left":
opts.width = size1
case "right":
opts.width = size1
case "center":
opts.width = size1
opts.height = size1
}
}
return opts, nil
}
func parseLabelPosition(opts *labelOpts, arg string) error { func parseLabelPosition(opts *labelOpts, arg string) error {
opts.column = 0 opts.column = 0
opts.bottom = false opts.bottom = false
@@ -296,9 +401,14 @@ type walkerOpts struct {
type Options struct { type Options struct {
Input chan string Input chan string
Output chan string Output chan string
NoWinpty bool
Tmux *tmuxOptions
ForceTtyIn bool
ProxyScript string
Bash bool Bash bool
Zsh bool Zsh bool
Fish bool Fish bool
Man bool
Fuzzy bool Fuzzy bool
FuzzyAlgo algo.Algo FuzzyAlgo algo.Algo
Scheme string Scheme string
@@ -312,6 +422,7 @@ type Options struct {
Sort int Sort int
Track trackOption Track trackOption
Tac bool Tac bool
Tail int
Criteria []criterion Criteria []criterion
Multi int Multi int
Ansi bool Ansi bool
@@ -323,6 +434,7 @@ type Options struct {
MinHeight int MinHeight int
Layout layoutType Layout layoutType
Cycle bool Cycle bool
MultiLine bool
CursorLine bool CursorLine bool
KeepRight bool KeepRight bool
Hscroll bool Hscroll bool
@@ -334,8 +446,9 @@ type Options struct {
Separator *string Separator *string
JumpLabels string JumpLabels string
Prompt string Prompt string
Pointer string Pointer *string
Marker string Marker *string
MarkerMulti [3]string
Query string Query string
Select1 bool Select1 bool
Exit0 bool Exit0 bool
@@ -393,10 +506,18 @@ func defaultPreviewOpts(command string) previewOpts {
} }
func defaultOptions() *Options { func defaultOptions() *Options {
var theme *tui.ColorTheme
if os.Getenv("NO_COLOR") != "" {
theme = tui.NoColorTheme()
} else {
theme = tui.EmptyTheme()
}
return &Options{ return &Options{
Bash: false, Bash: false,
Zsh: false, Zsh: false,
Fish: false, Fish: false,
Man: false,
Fuzzy: true, Fuzzy: true,
FuzzyAlgo: algo.FuzzyMatchV2, FuzzyAlgo: algo.FuzzyMatchV2,
Scheme: "default", Scheme: "default",
@@ -414,23 +535,24 @@ func defaultOptions() *Options {
Multi: 0, Multi: 0,
Ansi: false, Ansi: false,
Mouse: true, Mouse: true,
Theme: tui.EmptyTheme(), Theme: theme,
Black: false, Black: false,
Bold: true, Bold: true,
MinHeight: 10, MinHeight: 10,
Layout: layoutDefault, Layout: layoutDefault,
Cycle: false, Cycle: false,
MultiLine: true,
KeepRight: false, KeepRight: false,
Hscroll: true, Hscroll: true,
HscrollOff: 10, HscrollOff: 10,
ScrollOff: 0, ScrollOff: 3,
FileWord: false, FileWord: false,
InfoStyle: infoDefault, InfoStyle: infoDefault,
Separator: nil, Separator: nil,
JumpLabels: defaultJumpLabels, JumpLabels: defaultJumpLabels,
Prompt: "> ", Prompt: "> ",
Pointer: ">", Pointer: nil,
Marker: ">", Marker: nil,
Query: "", Query: "",
Select1: false, Select1: false,
Exit0: false, Exit0: false,
@@ -554,7 +676,7 @@ func splitNth(str string) ([]Range, error) {
func delimiterRegexp(str string) Delimiter { func delimiterRegexp(str string) Delimiter {
// Special handling of \t // Special handling of \t
str = strings.Replace(str, "\\t", "\t", -1) str = strings.ReplaceAll(str, "\\t", "\t")
// 1. Pattern does not contain any special character // 1. Pattern does not contain any special character
if regexp.QuoteMeta(str) == str { if regexp.QuoteMeta(str) == str {
@@ -1077,7 +1199,7 @@ const (
func init() { func init() {
executeRegexp = regexp.MustCompile( executeRegexp = regexp.MustCompile(
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|transform|change-(?:preview-window|preview|multi)|(?:re|un)bind|pos|put)`) `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|transform|change-(?:preview-window|preview|multi)|(?:re|un)bind|pos|put|print)`)
splitRegexp = regexp.MustCompile("[,:]+") splitRegexp = regexp.MustCompile("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
} }
@@ -1132,9 +1254,9 @@ Loop:
masked += strings.Repeat(" ", loc[1]) masked += strings.Repeat(" ", loc[1])
action = action[loc[1]:] action = action[loc[1]:]
} }
masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1) masked = strings.ReplaceAll(masked, "::", string([]rune{escapedColon, ':'}))
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1) masked = strings.ReplaceAll(masked, ",:", string([]rune{escapedComma, ':'}))
masked = strings.Replace(masked, "+:", string([]rune{escapedPlus, ':'}), -1) masked = strings.ReplaceAll(masked, "+:", string([]rune{escapedPlus, ':'}))
return masked return masked
} }
@@ -1449,6 +1571,8 @@ func isExecuteAction(str string) actionType {
return actExecuteSilent return actExecuteSilent
case "execute-multi": case "execute-multi":
return actExecuteMulti return actExecuteMulti
case "print":
return actPrint
case "put": case "put":
return actPut return actPut
case "transform": case "transform":
@@ -1516,8 +1640,8 @@ func parseSize(str string, maxPercent float64, label string) (sizeSpec, error) {
return sizeSpec{val, percent}, nil return sizeSpec{val, percent}, nil
} }
func parseHeight(str string) (heightSpec, error) { func parseHeight(str string, index int) (heightSpec, error) {
heightSpec := heightSpec{} heightSpec := heightSpec{index: index}
if strings.HasPrefix(str, "~") { if strings.HasPrefix(str, "~") {
heightSpec.auto = true heightSpec.auto = true
str = str[1:] str = str[1:]
@@ -1734,7 +1858,38 @@ func parseMargin(opt string, margin string) ([4]sizeSpec, error) {
return [4]sizeSpec{}, errors.New("invalid " + opt + ": " + margin) return [4]sizeSpec{}, errors.New("invalid " + opt + ": " + margin)
} }
func parseOptions(opts *Options, allArgs []string) error { func parseMarkerMultiLine(str string) ([3]string, error) {
gr := uniseg.NewGraphemes(str)
parts := []string{}
totalWidth := 0
for gr.Next() {
s := string(gr.Runes())
totalWidth += uniseg.StringWidth(s)
parts = append(parts, s)
}
result := [3]string{}
if totalWidth != 3 && totalWidth != 6 {
return result, fmt.Errorf("invalid total marker width: %d (expected: 3 or 6)", totalWidth)
}
expected := totalWidth / 3
idx := 0
for _, part := range parts {
expected -= uniseg.StringWidth(part)
result[idx] += part
if expected <= 0 {
idx++
expected = totalWidth / 3
}
if idx == 3 {
break
}
}
return result, nil
}
func parseOptions(index *int, opts *Options, allArgs []string) error {
var err error var err error
var historyMax int var historyMax int
if opts.History == nil { if opts.History == nil {
@@ -1768,10 +1923,16 @@ func parseOptions(opts *Options, allArgs []string) error {
opts.Fish = false opts.Fish = false
opts.Help = false opts.Help = false
opts.Version = false opts.Version = false
opts.Man = false
} }
startIndex := *index
for i := 0; i < len(allArgs); i++ { for i := 0; i < len(allArgs); i++ {
arg := allArgs[i] arg := allArgs[i]
index := i + startIndex
switch arg { switch arg {
case "--man":
clearExitingOpts()
opts.Man = true
case "--bash": case "--bash":
clearExitingOpts() clearExitingOpts()
opts.Bash = true opts.Bash = true
@@ -1787,6 +1948,29 @@ func parseOptions(opts *Options, allArgs []string) error {
case "--version": case "--version":
clearExitingOpts() clearExitingOpts()
opts.Version = true opts.Version = true
case "--no-winpty":
opts.NoWinpty = true
case "--tmux":
given, str := optionalNextString(allArgs, &i)
if given {
if opts.Tmux, err = parseTmuxOptions(str, index); err != nil {
return err
}
} else {
opts.Tmux = defaultTmuxOptions(index)
}
case "--no-tmux":
opts.Tmux = nil
case "--force-tty-in":
// NOTE: We need this because `system('fzf --tmux < /dev/tty')` doesn't
// work on Neovim. Same as '-' option of fzf-tmux.
opts.ForceTtyIn = true
case "--no-force-tty-in":
opts.ForceTtyIn = false
case "--proxy-script":
if opts.ProxyScript, err = nextString(allArgs, &i, ""); err != nil {
return err
}
case "-x", "--extended": case "-x", "--extended":
opts.Extended = true opts.Extended = true
case "-e", "--exact": case "-e", "--exact":
@@ -1914,6 +2098,15 @@ func parseOptions(opts *Options, allArgs []string) error {
opts.Tac = true opts.Tac = true
case "--no-tac": case "--no-tac":
opts.Tac = false opts.Tac = false
case "--tail":
if opts.Tail, err = nextInt(allArgs, &i, "number of items to keep required"); err != nil {
return err
}
if opts.Tail <= 0 {
return errors.New("number of items to keep must be a positive integer")
}
case "--no-tail":
opts.Tail = 0
case "-i", "--ignore-case": case "-i", "--ignore-case":
opts.Case = CaseIgnore opts.Case = CaseIgnore
case "+i", "--no-ignore-case": case "+i", "--no-ignore-case":
@@ -1962,6 +2155,10 @@ func parseOptions(opts *Options, allArgs []string) error {
opts.CursorLine = false opts.CursorLine = false
case "--no-cycle": case "--no-cycle":
opts.Cycle = false opts.Cycle = false
case "--multi-line":
opts.MultiLine = true
case "--no-multi-line":
opts.MultiLine = false
case "--keep-right": case "--keep-right":
opts.KeepRight = true opts.KeepRight = true
case "--no-keep-right": case "--no-keep-right":
@@ -2049,17 +2246,27 @@ func parseOptions(opts *Options, allArgs []string) error {
return err return err
} }
case "--pointer": case "--pointer":
str, err := nextString(allArgs, &i, "pointer sign string required") str, err := nextString(allArgs, &i, "pointer sign required")
if err != nil { if err != nil {
return err return err
} }
opts.Pointer = firstLine(str) str = firstLine(str)
opts.Pointer = &str
case "--marker": case "--marker":
str, err := nextString(allArgs, &i, "selected sign string required") str, err := nextString(allArgs, &i, "marker sign required")
if err != nil { if err != nil {
return err return err
} }
opts.Marker = firstLine(str) str = firstLine(str)
opts.Marker = &str
case "--marker-multi-line":
str, err := nextString(allArgs, &i, "marker sign for multi-line entries required")
if err != nil {
return err
}
if opts.MarkerMulti, err = parseMarkerMultiLine(firstLine(str)); err != nil {
return err
}
case "--sync": case "--sync":
opts.Sync = true opts.Sync = true
case "--no-sync", "--async": case "--no-sync", "--async":
@@ -2123,7 +2330,7 @@ func parseOptions(opts *Options, allArgs []string) error {
if err != nil { if err != nil {
return err return err
} }
if opts.Height, err = parseHeight(str); err != nil { if opts.Height, err = parseHeight(str, index); err != nil {
return err return err
} }
case "--min-height": case "--min-height":
@@ -2264,6 +2471,10 @@ func parseOptions(opts *Options, allArgs []string) error {
if opts.FuzzyAlgo, err = parseAlgo(value); err != nil { if opts.FuzzyAlgo, err = parseAlgo(value); err != nil {
return err return err
} }
} else if match, value := optString(arg, "--tmux="); match {
if opts.Tmux, err = parseTmuxOptions(value, index); err != nil {
return err
}
} else if match, value := optString(arg, "--scheme="); match { } else if match, value := optString(arg, "--scheme="); match {
opts.Scheme = strings.ToLower(value) opts.Scheme = strings.ToLower(value)
} else if match, value := optString(arg, "-q", "--query="); match { } else if match, value := optString(arg, "-q", "--query="); match {
@@ -2291,9 +2502,15 @@ func parseOptions(opts *Options, allArgs []string) error {
} else if match, value := optString(arg, "--prompt="); match { } else if match, value := optString(arg, "--prompt="); match {
opts.Prompt = value opts.Prompt = value
} else if match, value := optString(arg, "--pointer="); match { } else if match, value := optString(arg, "--pointer="); match {
opts.Pointer = firstLine(value) str := firstLine(value)
opts.Pointer = &str
} else if match, value := optString(arg, "--marker="); match { } else if match, value := optString(arg, "--marker="); match {
opts.Marker = firstLine(value) str := firstLine(value)
opts.Marker = &str
} else if match, value := optString(arg, "--marker-multi-line="); match {
if opts.MarkerMulti, err = parseMarkerMultiLine(firstLine(value)); err != nil {
return err
}
} else if match, value := optString(arg, "-n", "--nth="); match { } else if match, value := optString(arg, "-n", "--nth="); match {
if opts.Nth, err = splitNth(value); err != nil { if opts.Nth, err = splitNth(value); err != nil {
return err return err
@@ -2309,7 +2526,7 @@ func parseOptions(opts *Options, allArgs []string) error {
return err return err
} }
} else if match, value := optString(arg, "--height="); match { } else if match, value := optString(arg, "--height="); match {
if opts.Height, err = parseHeight(value); err != nil { if opts.Height, err = parseHeight(value, index); err != nil {
return err return err
} }
} else if match, value := optString(arg, "--min-height="); match { } else if match, value := optString(arg, "--min-height="); match {
@@ -2425,11 +2642,19 @@ func parseOptions(opts *Options, allArgs []string) error {
} else if match, value := optString(arg, "--jump-labels="); match { } else if match, value := optString(arg, "--jump-labels="); match {
opts.JumpLabels = value opts.JumpLabels = value
validateJumpLabels = true validateJumpLabels = true
} else if match, value := optString(arg, "--tail="); match {
if opts.Tail, err = atoi(value); err != nil {
return err
}
if opts.Tail <= 0 {
return errors.New("number of items to keep must be a positive integer")
}
} else { } else {
return errors.New("unknown option: " + arg) return errors.New("unknown option: " + arg)
} }
} }
} }
*index += len(allArgs)
if opts.HeaderLines < 0 { if opts.HeaderLines < 0 {
return errors.New("header lines must be a non-negative integer") return errors.New("header lines must be a non-negative integer")
@@ -2471,23 +2696,17 @@ func validateSign(sign string, signOptName string) error {
return nil return nil
} }
// This function can have side-effects and alter some global states. func validateOptions(opts *Options) error {
// So we run it on fzf.Run and not on ParseOptions. if opts.Pointer != nil {
func postProcessOptions(opts *Options) error { if err := validateSign(*opts.Pointer, "pointer"); err != nil {
if opts.Ambidouble { return err
uniseg.EastAsianAmbiguousWidth = 2 }
} }
if err := validateSign(opts.Pointer, "pointer"); err != nil { if opts.Marker != nil {
return err if err := validateSign(*opts.Marker, "marker"); err != nil {
} return err
}
if err := validateSign(opts.Marker, "marker"); err != nil {
return err
}
if !tui.IsLightRendererSupported() && opts.Height.size > 0 {
return errors.New("--height option is currently not supported on this platform")
} }
if opts.Scrollbar != nil { if opts.Scrollbar != nil {
@@ -2502,6 +2721,72 @@ func postProcessOptions(opts *Options) error {
} }
} }
if opts.Height.auto {
for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2]} {
if s.percent {
return errors.New("adaptive height is not compatible with top/bottom percent margin")
}
}
for _, s := range []sizeSpec{opts.Padding[0], opts.Padding[2]} {
if s.percent {
return errors.New("adaptive height is not compatible with top/bottom percent padding")
}
}
}
return nil
}
// This function can have side-effects and alter some global states.
// So we run it on fzf.Run and not on ParseOptions.
func postProcessOptions(opts *Options) error {
if opts.Ambidouble {
uniseg.EastAsianAmbiguousWidth = 2
}
if opts.BorderShape == tui.BorderUndefined {
opts.BorderShape = tui.BorderNone
}
if opts.Pointer == nil {
defaultPointer := "▌"
if !opts.Unicode {
defaultPointer = ">"
}
opts.Pointer = &defaultPointer
}
markerLen := 1
if opts.Marker == nil {
// "▎" looks better, but not all terminals render it correctly
defaultMarker := "┃"
if !opts.Unicode {
defaultMarker = ">"
}
opts.Marker = &defaultMarker
} else {
markerLen = uniseg.StringWidth(*opts.Marker)
}
markerMultiLen := 1
if len(opts.MarkerMulti[0]) == 0 {
if opts.Unicode {
opts.MarkerMulti = [3]string{"╻", "┃", "╹"}
} else {
opts.MarkerMulti = [3]string{".", "|", "'"}
}
} else {
markerMultiLen = uniseg.StringWidth(opts.MarkerMulti[0])
}
if markerMultiLen > markerLen {
padded := *opts.Marker + " "
opts.Marker = &padded
} else if markerMultiLen < markerLen {
for idx := range opts.MarkerMulti {
opts.MarkerMulti[idx] += " "
}
}
// Default actions for CTRL-N / CTRL-P when --history is set // Default actions for CTRL-N / CTRL-P when --history is set
if opts.History != nil { if opts.History != nil {
if _, prs := opts.Keymap[tui.CtrlP.AsEvent()]; !prs { if _, prs := opts.Keymap[tui.CtrlP.AsEvent()]; !prs {
@@ -2548,19 +2833,6 @@ func postProcessOptions(opts *Options) error {
opts.Keymap[tui.DoubleClick.AsEvent()] = opts.Keymap[tui.CtrlM.AsEvent()] 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 {
return errors.New("adaptive height is not compatible with top/bottom percent margin")
}
}
for _, s := range []sizeSpec{opts.Padding[0], opts.Padding[2]} {
if s.percent {
return errors.New("adaptive height is not compatible with top/bottom percent padding")
}
}
}
// If we're not using extended search mode, --nth option becomes irrelevant // If we're not using extended search mode, --nth option becomes irrelevant
// if it contains the whole range // if it contains the whole range
if !opts.Extended || len(opts.Nth) == 1 { if !opts.Extended || len(opts.Nth) == 1 {
@@ -2589,12 +2861,22 @@ func postProcessOptions(opts *Options) error {
theme.Spinner = boldify(theme.Spinner) theme.Spinner = boldify(theme.Spinner)
} }
// If --height option is not supported on the platform, just ignore it
if !tui.IsLightRendererSupported() && opts.Height.size > 0 {
opts.Height = heightSpec{}
}
if err := opts.initProfiling(); err != nil {
return errors.New("failed to start pprof profiles: " + err.Error())
}
return processScheme(opts) return processScheme(opts)
} }
// ParseOptions parses command-line options // ParseOptions parses command-line options
func ParseOptions(useDefaults bool, args []string) (*Options, error) { func ParseOptions(useDefaults bool, args []string) (*Options, error) {
opts := defaultOptions() opts := defaultOptions()
index := 0
if useDefaults { if useDefaults {
// 1. Options from $FZF_DEFAULT_OPTS_FILE // 1. Options from $FZF_DEFAULT_OPTS_FILE
@@ -2609,7 +2891,7 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
return nil, errors.New(path + ": " + parseErr.Error()) return nil, errors.New(path + ": " + parseErr.Error())
} }
if len(words) > 0 { if len(words) > 0 {
if err := parseOptions(opts, words); err != nil { if err := parseOptions(&index, opts, words); err != nil {
return nil, errors.New(path + ": " + err.Error()) return nil, errors.New(path + ": " + err.Error())
} }
} }
@@ -2621,19 +2903,20 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
return nil, errors.New("$FZF_DEFAULT_OPTS: " + parseErr.Error()) return nil, errors.New("$FZF_DEFAULT_OPTS: " + parseErr.Error())
} }
if len(words) > 0 { if len(words) > 0 {
if err := parseOptions(opts, words); err != nil { if err := parseOptions(&index, opts, words); err != nil {
return nil, errors.New("$FZF_DEFAULT_OPTS: " + err.Error()) return nil, errors.New("$FZF_DEFAULT_OPTS: " + err.Error())
} }
} }
} }
// 3. Options from command-line arguments // 3. Options from command-line arguments
if err := parseOptions(opts, args); err != nil { if err := parseOptions(&index, opts, args); err != nil {
return nil, err return nil, err
} }
if err := opts.initProfiling(); err != nil { // 4. Final validation of merged options
return nil, errors.New("failed to start pprof profiles: " + err.Error()) if err := validateOptions(opts); err != nil {
return nil, err
} }
return opts, nil return opts, nil

View File

@@ -106,10 +106,11 @@ func TestSplitNth(t *testing.T) {
} }
func TestIrrelevantNth(t *testing.T) { func TestIrrelevantNth(t *testing.T) {
index := 0
{ {
opts := defaultOptions() opts := defaultOptions()
words := []string{"--nth", "..", "-x"} words := []string{"--nth", "..", "-x"}
parseOptions(opts, words) parseOptions(&index, opts, words)
postProcessOptions(opts) postProcessOptions(opts)
if len(opts.Nth) != 0 { if len(opts.Nth) != 0 {
t.Errorf("nth should be empty: %v", opts.Nth) t.Errorf("nth should be empty: %v", opts.Nth)
@@ -118,7 +119,7 @@ func TestIrrelevantNth(t *testing.T) {
for _, words := range [][]string{{"--nth", "..,3", "+x"}, {"--nth", "3,1..", "+x"}, {"--nth", "..-1,1", "+x"}} { for _, words := range [][]string{{"--nth", "..,3", "+x"}, {"--nth", "3,1..", "+x"}, {"--nth", "..-1,1", "+x"}} {
{ {
opts := defaultOptions() opts := defaultOptions()
parseOptions(opts, words) parseOptions(&index, opts, words)
postProcessOptions(opts) postProcessOptions(opts)
if len(opts.Nth) != 0 { if len(opts.Nth) != 0 {
t.Errorf("nth should be empty: %v", opts.Nth) t.Errorf("nth should be empty: %v", opts.Nth)
@@ -127,7 +128,7 @@ func TestIrrelevantNth(t *testing.T) {
{ {
opts := defaultOptions() opts := defaultOptions()
words = append(words, "-x") words = append(words, "-x")
parseOptions(opts, words) parseOptions(&index, opts, words)
postProcessOptions(opts) postProcessOptions(opts)
if len(opts.Nth) != 2 { if len(opts.Nth) != 2 {
t.Errorf("nth should not be empty: %v", opts.Nth) t.Errorf("nth should not be empty: %v", opts.Nth)
@@ -335,10 +336,11 @@ func TestColorSpec(t *testing.T) {
} }
func TestDefaultCtrlNP(t *testing.T) { func TestDefaultCtrlNP(t *testing.T) {
index := 0
check := func(words []string, et tui.EventType, expected actionType) { check := func(words []string, et tui.EventType, expected actionType) {
e := et.AsEvent() e := et.AsEvent()
opts := defaultOptions() opts := defaultOptions()
parseOptions(opts, words) parseOptions(&index, opts, words)
postProcessOptions(opts) postProcessOptions(opts)
if opts.Keymap[e][0].t != expected { if opts.Keymap[e][0].t != expected {
t.Error() t.Error()
@@ -364,8 +366,9 @@ func TestDefaultCtrlNP(t *testing.T) {
} }
func optsFor(words ...string) *Options { func optsFor(words ...string) *Options {
index := 0
opts := defaultOptions() opts := defaultOptions()
parseOptions(opts, words) parseOptions(&index, opts, words)
postProcessOptions(opts) postProcessOptions(opts)
return opts return opts
} }

View File

@@ -155,14 +155,14 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
} }
func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet { func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet {
str = strings.Replace(str, "\\ ", "\t", -1) str = strings.ReplaceAll(str, "\\ ", "\t")
tokens := _splitRegex.Split(str, -1) tokens := _splitRegex.Split(str, -1)
sets := []termSet{} sets := []termSet{}
set := termSet{} set := termSet{}
switchSet := false switchSet := false
afterBar := false afterBar := false
for _, token := range tokens { for _, token := range tokens {
typ, inv, text := termFuzzy, false, strings.Replace(token, "\t", " ", -1) typ, inv, text := termFuzzy, false, strings.ReplaceAll(token, "\t", " ")
lowerText := strings.ToLower(text) lowerText := strings.ToLower(text)
caseSensitive := caseMode == CaseRespect || caseSensitive := caseMode == CaseRespect ||
caseMode == CaseSmart && text != lowerText caseMode == CaseSmart && text != lowerText

146
src/proxy.go Normal file
View File

@@ -0,0 +1,146 @@
package fzf
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"time"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
)
const becomeSuffix = ".become"
func escapeSingleQuote(str string) string {
return "'" + strings.ReplaceAll(str, "'", "'\\''") + "'"
}
func fifo(name string) (string, error) {
ns := time.Now().UnixNano()
output := filepath.Join(os.TempDir(), fmt.Sprintf("fzf-%s-%d", name, ns))
output, err := mkfifo(output, 0600)
if err != nil {
return output, err
}
return output, nil
}
func runProxy(commandPrefix string, cmdBuilder func(temp string) *exec.Cmd, opts *Options, withExports bool) (int, error) {
output, err := fifo("proxy-output")
if err != nil {
return ExitError, err
}
defer os.Remove(output)
// Take the output
go func() {
withOutputPipe(output, func(outputFile io.ReadCloser) {
if opts.Output == nil {
io.Copy(os.Stdout, outputFile)
} else {
reader := bufio.NewReader(outputFile)
sep := opts.PrintSep[0]
for {
item, err := reader.ReadString(sep)
if err != nil {
break
}
opts.Output <- item
}
}
})
}()
var command string
commandPrefix += ` --no-force-tty-in --proxy-script "$0"`
if opts.Input == nil && (opts.ForceTtyIn || util.IsTty(os.Stdin)) {
command = fmt.Sprintf(`%s > %q`, commandPrefix, output)
} else {
input, err := fifo("proxy-input")
if err != nil {
return ExitError, err
}
defer os.Remove(input)
go func() {
withInputPipe(input, func(inputFile io.WriteCloser) {
if opts.Input == nil {
io.Copy(inputFile, os.Stdin)
} else {
for item := range opts.Input {
fmt.Fprint(inputFile, item+opts.PrintSep)
}
}
})
}()
if withExports {
command = fmt.Sprintf(`%s < %q > %q`, commandPrefix, input, output)
} else {
// For mintty: cannot directly read named pipe from Go code
command = fmt.Sprintf(`command cat %q | %s > %q`, input, commandPrefix, output)
}
}
// To ensure that the options are processed by a POSIX-compliant shell,
// we need to write the command to a temporary file and execute it with sh.
var exports []string
if withExports {
exports = os.Environ()
for idx, pairStr := range exports {
pair := strings.SplitN(pairStr, "=", 2)
exports[idx] = fmt.Sprintf("export %s=%s", pair[0], escapeSingleQuote(pair[1]))
}
}
temp := WriteTemporaryFile(append(exports, command), "\n")
defer os.Remove(temp)
cmd := cmdBuilder(temp)
cmd.Stderr = os.Stderr
intChan := make(chan os.Signal, 1)
defer close(intChan)
go func() {
if sig, valid := <-intChan; valid {
cmd.Process.Signal(sig)
}
}()
signal.Notify(intChan, os.Interrupt)
if err := cmd.Run(); err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
code := exitError.ExitCode()
if code == ExitBecome {
becomeFile := temp + becomeSuffix
data, err := os.ReadFile(becomeFile)
os.Remove(becomeFile)
if err != nil {
return ExitError, err
}
elems := strings.Split(string(data), "\x00")
if len(elems) < 1 {
return ExitError, errors.New("invalid become command")
}
command := elems[0]
env := []string{}
if len(elems) > 1 {
env = elems[1:]
}
executor := util.NewExecutor(opts.WithShell)
ttyin, err := tui.TtyIn()
if err != nil {
return ExitError, err
}
executor.Become(ttyin, env, command)
}
return code, err
}
}
return ExitOk, nil
}

38
src/proxy_unix.go Normal file
View File

@@ -0,0 +1,38 @@
//go:build !windows
package fzf
import (
"io"
"os"
"golang.org/x/sys/unix"
)
func sh() (string, error) {
return "sh", nil
}
func mkfifo(path string, mode uint32) (string, error) {
return path, unix.Mkfifo(path, mode)
}
func withOutputPipe(output string, task func(io.ReadCloser)) error {
outputFile, err := os.OpenFile(output, os.O_RDONLY, 0)
if err != nil {
return err
}
task(outputFile)
outputFile.Close()
return nil
}
func withInputPipe(input string, task func(io.WriteCloser)) error {
inputFile, err := os.OpenFile(input, os.O_WRONLY, 0)
if err != nil {
return err
}
task(inputFile)
inputFile.Close()
return nil
}

81
src/proxy_windows.go Normal file
View File

@@ -0,0 +1,81 @@
//go:build windows
package fzf
import (
"fmt"
"io"
"os/exec"
"strconv"
"strings"
"sync/atomic"
)
var shPath atomic.Value
func sh() (string, error) {
if cached := shPath.Load(); cached != nil {
return cached.(string), nil
}
cmd := exec.Command("cygpath", "-w", "/usr/bin/sh")
bytes, err := cmd.Output()
if err != nil {
return "", err
}
sh := strings.TrimSpace(string(bytes))
shPath.Store(sh)
return sh, nil
}
func mkfifo(path string, mode uint32) (string, error) {
m := strconv.FormatUint(uint64(mode), 8)
sh, err := sh()
if err != nil {
return path, err
}
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command mkfifo -m %s %q`, m, path))
if err := cmd.Run(); err != nil {
return path, err
}
return path + ".lnk", nil
}
func withOutputPipe(output string, task func(io.ReadCloser)) error {
sh, err := sh()
if err != nil {
return err
}
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command cat %q`, output))
outputFile, err := cmd.StdoutPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
task(outputFile)
cmd.Wait()
return nil
}
func withInputPipe(input string, task func(io.WriteCloser)) error {
sh, err := sh()
if err != nil {
return err
}
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command cat - > %q`, input))
inputFile, err := cmd.StdinPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
task(inputFile)
inputFile.Close()
cmd.Wait()
return nil
}

View File

@@ -25,6 +25,7 @@ type Reader struct {
finChan chan bool finChan chan bool
mutex sync.Mutex mutex sync.Mutex
exec *exec.Cmd exec *exec.Cmd
execOut io.ReadCloser
command *string command *string
killed bool killed bool
wait bool wait bool
@@ -32,7 +33,7 @@ type Reader struct {
// NewReader returns new Reader object // NewReader returns new Reader object
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, executor *util.Executor, delimNil bool, wait bool) *Reader { func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, executor *util.Executor, delimNil bool, wait bool) *Reader {
return &Reader{pusher, executor, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait} return &Reader{pusher, executor, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, nil, false, wait}
} }
func (r *Reader) startEventPoller() { func (r *Reader) startEventPoller() {
@@ -79,6 +80,7 @@ func (r *Reader) terminate() {
r.mutex.Lock() r.mutex.Lock()
r.killed = true r.killed = true
if r.exec != nil && r.exec.Process != nil { if r.exec != nil && r.exec.Process != nil {
r.execOut.Close()
util.KillCommand(r.exec) util.KillCommand(r.exec)
} else { } else {
os.Stdin.Close() os.Stdin.Close()
@@ -113,7 +115,7 @@ func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts,
var success bool var success bool
if inputChan != nil { if inputChan != nil {
success = r.readChannel(inputChan) success = r.readChannel(inputChan)
} else if util.IsTty() { } else if util.IsTty(os.Stdin) {
cmd := os.Getenv("FZF_DEFAULT_COMMAND") cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 { if len(cmd) == 0 {
success = r.readFiles(root, opts, ignores) success = r.readFiles(root, opts, ignores)
@@ -263,16 +265,23 @@ func (r *Reader) readFromCommand(command string, environ []string) bool {
if environ != nil { if environ != nil {
r.exec.Env = environ r.exec.Env = environ
} }
out, err := r.exec.StdoutPipe()
var err error
r.execOut, err = r.exec.StdoutPipe()
if err != nil { if err != nil {
r.exec = nil
r.mutex.Unlock() r.mutex.Unlock()
return false return false
} }
err = r.exec.Start() err = r.exec.Start()
r.mutex.Unlock()
if err != nil { if err != nil {
r.exec = nil
r.mutex.Unlock()
return false return false
} }
r.feed(out)
r.mutex.Unlock()
r.feed(r.execOut)
return r.exec.Wait() == nil return r.exec.Wait() == nil
} }

View File

@@ -15,6 +15,7 @@ type Offset [2]int32
type colorOffset struct { type colorOffset struct {
offset [2]int32 offset [2]int32
color tui.ColorPair color tui.ColorPair
match bool
} }
type Result struct { type Result struct {
@@ -109,7 +110,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
if len(itemColors) == 0 { if len(itemColors) == 0 {
var offsets []colorOffset var offsets []colorOffset
for _, off := range matchOffsets { for _, off := range matchOffsets {
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch}) offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch, match: true})
} }
return offsets return offsets
} }
@@ -193,12 +194,13 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
} }
} }
colors = append(colors, colorOffset{ colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, color: color}) offset: [2]int32{int32(start), int32(idx)}, color: color, match: true})
} else { } else {
ansi := itemColors[curr-1] ansi := itemColors[curr-1]
colors = append(colors, colorOffset{ colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, offset: [2]int32{int32(start), int32(idx)},
color: ansiToColorPair(ansi, colBase)}) color: ansiToColorPair(ansi, colBase),
match: false})
} }
} }
} }

File diff suppressed because it is too large Load Diff

57
src/tmux.go Normal file
View File

@@ -0,0 +1,57 @@
package fzf
import (
"os"
"os/exec"
"github.com/junegunn/fzf/src/tui"
)
func runTmux(args []string, opts *Options) (int, error) {
// Prepare arguments
fzf := args[0]
args = append([]string{"--bind=ctrl-z:ignore"}, args[1:]...)
if opts.BorderShape == tui.BorderUndefined {
args = append(args, "--border")
}
argStr := escapeSingleQuote(fzf)
for _, arg := range args {
argStr += " " + escapeSingleQuote(arg)
}
argStr += ` --no-tmux --no-height`
// Get current directory
dir, err := os.Getwd()
if err != nil {
dir = "."
}
// Set tmux options for popup placement
// C Both The centre of the terminal
// R -x The right side of the terminal
// P Both The bottom left of the pane
// M Both The mouse position
// W Both The window position on the status line
// S -y The line above or below the status line
tmuxArgs := []string{"display-popup", "-E", "-B", "-d", dir}
switch opts.Tmux.position {
case posUp:
tmuxArgs = append(tmuxArgs, "-xC", "-y0")
case posDown:
tmuxArgs = append(tmuxArgs, "-xC", "-yS")
case posLeft:
tmuxArgs = append(tmuxArgs, "-x0", "-yC")
case posRight:
tmuxArgs = append(tmuxArgs, "-xR", "-yC")
case posCenter:
tmuxArgs = append(tmuxArgs, "-xC", "-yC")
}
tmuxArgs = append(tmuxArgs, "-w"+opts.Tmux.width.String())
tmuxArgs = append(tmuxArgs, "-h"+opts.Tmux.height.String())
return runProxy(argStr, func(temp string) *exec.Cmd {
sh, _ := sh()
tmuxArgs = append(tmuxArgs, sh, temp)
return exec.Command("tmux", tmuxArgs...)
}, opts, true)
}

View File

@@ -107,11 +107,12 @@ func _() {
_ = x[Result-96] _ = x[Result-96]
_ = x[Jump-97] _ = x[Jump-97]
_ = x[JumpCancel-98] _ = x[JumpCancel-98]
_ = x[ClickHeader-99]
} }
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancel" const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeader"
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 452, 463, 472, 482, 492, 503, 511, 521, 530, 541, 556, 573, 579, 585, 596, 601, 605, 610, 613, 617, 623, 627, 637} var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 452, 463, 472, 482, 492, 503, 511, 521, 530, 541, 556, 573, 579, 585, 596, 601, 605, 610, 613, 617, 623, 627, 637, 648}
func (i EventType) String() string { func (i EventType) String() string {
if i < 0 || i >= EventType(len(_EventType_index)-1) { if i < 0 || i >= EventType(len(_EventType_index)-1) {

View File

@@ -73,7 +73,7 @@ func (r *LightRenderer) csi(code string) string {
func (r *LightRenderer) flush() { func (r *LightRenderer) flush() {
if r.queued.Len() > 0 { if r.queued.Len() > 0 {
fmt.Fprint(os.Stderr, "\x1b[?7l\x1b[?25l"+r.queued.String()+"\x1b[?25h\x1b[?7h") fmt.Fprint(r.ttyout, "\x1b[?7l\x1b[?25l"+r.queued.String()+"\x1b[?25h\x1b[?7h")
r.queued.Reset() r.queued.Reset()
} }
} }
@@ -88,6 +88,7 @@ type LightRenderer struct {
prevDownTime time.Time prevDownTime time.Time
clicks [][2]int clicks [][2]int
ttyin *os.File ttyin *os.File
ttyout *os.File
buffer []byte buffer []byte
origState *term.State origState *term.State
width int width int
@@ -126,10 +127,10 @@ type LightWindow struct {
bg Color bg Color
} }
func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) { func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
in, err := openTtyIn() out, err := openTtyOut()
if err != nil { if err != nil {
return nil, err out = os.Stderr
} }
r := LightRenderer{ r := LightRenderer{
closed: util.NewAtomicBool(false), closed: util.NewAtomicBool(false),
@@ -137,7 +138,8 @@ func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop in
forceBlack: forceBlack, forceBlack: forceBlack,
mouse: mouse, mouse: mouse,
clearOnExit: clearOnExit, clearOnExit: clearOnExit,
ttyin: in, ttyin: ttyin,
ttyout: out,
yoffset: 0, yoffset: 0,
tabstop: tabstop, tabstop: tabstop,
fullscreen: fullscreen, fullscreen: fullscreen,
@@ -1019,7 +1021,7 @@ func (w *LightWindow) Print(text string) {
} }
func cleanse(str string) string { func cleanse(str string) string {
return strings.Replace(str, "\x1b", "", -1) return strings.ReplaceAll(str, "\x1b", "")
} }
func (w *LightWindow) CPrint(pair ColorPair, text string) { func (w *LightWindow) CPrint(pair ColorPair, text string) {

View File

@@ -45,15 +45,15 @@ func (r *LightRenderer) initPlatform() error {
} }
func (r *LightRenderer) closePlatform() { func (r *LightRenderer) closePlatform() {
// NOOP r.ttyout.Close()
} }
func openTtyIn() (*os.File, error) { func openTty(mode int) (*os.File, error) {
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) in, err := os.OpenFile(consoleDevice, mode, 0)
if err != nil { if err != nil {
tty := ttyname() tty := ttyname()
if len(tty) > 0 { if len(tty) > 0 {
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil { if in, err := os.OpenFile(tty, mode, 0); err == nil {
return in, nil return in, nil
} }
} }
@@ -62,6 +62,14 @@ func openTtyIn() (*os.File, error) {
return in, nil return in, nil
} }
func openTtyIn() (*os.File, error) {
return openTty(syscall.O_RDONLY)
}
func openTtyOut() (*os.File, error) {
return openTty(syscall.O_WRONLY)
}
func (r *LightRenderer) setupTerminal() { func (r *LightRenderer) setupTerminal() {
term.MakeRaw(r.fd()) term.MakeRaw(r.fd())
} }

View File

@@ -96,6 +96,10 @@ func openTtyIn() (*os.File, error) {
return nil, nil return nil, nil
} }
func openTtyOut() (*os.File, error) {
return os.Stderr, nil
}
func (r *LightRenderer) setupTerminal() error { func (r *LightRenderer) setupTerminal() error {
if err := windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput); err != nil { if err := windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput); err != nil {
return err return err

View File

@@ -3,6 +3,7 @@
package tui package tui
import ( import (
"os"
"testing" "testing"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
@@ -20,7 +21,7 @@ func assert(t *testing.T, context string, got interface{}, want interface{}) boo
// Test the handling of the tcell keyboard events. // Test the handling of the tcell keyboard events.
func TestGetCharEventKey(t *testing.T) { func TestGetCharEventKey(t *testing.T) {
if util.ToTty() { if util.IsTty(os.Stdout) {
// This test is skipped when output goes to terminal, because it causes // This test is skipped when output goes to terminal, because it causes
// some glitches: // some glitches:
// - output lines may not start at the beginning of a row which makes // - output lines may not start at the beginning of a row which makes

View File

@@ -4,12 +4,19 @@ package tui
import ( import (
"os" "os"
"sync/atomic"
"syscall" "syscall"
) )
var devPrefixes = [...]string{"/dev/pts/", "/dev/"} var devPrefixes = [...]string{"/dev/pts/", "/dev/"}
var tty atomic.Value
func ttyname() string { func ttyname() string {
if cached := tty.Load(); cached != nil {
return cached.(string)
}
var stderr syscall.Stat_t var stderr syscall.Stat_t
if syscall.Fstat(2, &stderr) != nil { if syscall.Fstat(2, &stderr) != nil {
return "" return ""
@@ -27,24 +34,21 @@ func ttyname() string {
continue continue
} }
if stat, ok := info.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev { if stat, ok := info.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev {
return prefix + file.Name() value := prefix + file.Name()
tty.Store(value)
return value
} }
} }
} }
return "" return ""
} }
// TtyIn returns terminal device to be used as STDIN, falls back to os.Stdin // TtyIn returns terminal device to read user input
func TtyIn() *os.File { func TtyIn() (*os.File, error) {
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) return openTtyIn()
if err != nil { }
tty := ttyname()
if len(tty) > 0 { // TtyIn returns terminal device to write to
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil { func TtyOut() (*os.File, error) {
return in return openTtyOut()
}
}
return os.Stdin
}
return in
} }

View File

@@ -2,13 +2,20 @@
package tui package tui
import "os" import (
"os"
)
func ttyname() string { func ttyname() string {
return "" return ""
} }
// TtyIn on Windows returns os.Stdin // TtyIn on Windows returns os.Stdin
func TtyIn() *os.File { func TtyIn() (*os.File, error) {
return os.Stdin return os.Stdin, nil
}
// TtyIn on Windows returns nil
func TtyOut() (*os.File, error) {
return nil, nil
} }

View File

@@ -356,7 +356,8 @@ type MouseEvent struct {
type BorderShape int type BorderShape int
const ( const (
BorderNone BorderShape = iota BorderUndefined BorderShape = iota
BorderNone
BorderRounded BorderRounded
BorderSharp BorderSharp
BorderBold BorderBold
@@ -701,9 +702,9 @@ func init() {
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined}, SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined}, SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined}, SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{colBlack, AttrUndefined}, DarkBg: ColorAttr{colBlack, AttrUndefined},
Prompt: ColorAttr{colBlue, AttrUndefined}, Prompt: ColorAttr{colBlue, AttrUndefined},
Match: ColorAttr{colGreen, AttrUndefined}, Match: ColorAttr{colGreen, AttrUndefined},
@@ -731,9 +732,9 @@ func init() {
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined}, SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined}, SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined}, SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{236, AttrUndefined}, DarkBg: ColorAttr{236, AttrUndefined},
Prompt: ColorAttr{110, AttrUndefined}, Prompt: ColorAttr{110, AttrUndefined},
Match: ColorAttr{108, AttrUndefined}, Match: ColorAttr{108, AttrUndefined},
@@ -761,9 +762,9 @@ func init() {
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined}, SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined}, SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined}, SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{251, AttrUndefined}, DarkBg: ColorAttr{251, AttrUndefined},
Prompt: ColorAttr{25, AttrUndefined}, Prompt: ColorAttr{25, AttrUndefined},
Match: ColorAttr{66, AttrUndefined}, Match: ColorAttr{66, AttrUndefined},
@@ -822,7 +823,7 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
// These colors are not defined in the base themes // These colors are not defined in the base themes
theme.SelectedFg = o(theme.Fg, theme.SelectedFg) theme.SelectedFg = o(theme.Fg, theme.SelectedFg)
theme.SelectedBg = o(theme.Bg, theme.SelectedBg) theme.SelectedBg = o(theme.Bg, theme.SelectedBg)
theme.SelectedMatch = o(theme.CurrentMatch, theme.SelectedMatch) theme.SelectedMatch = o(theme.Match, theme.SelectedMatch)
theme.Disabled = o(theme.Input, theme.Disabled) theme.Disabled = o(theme.Input, theme.Disabled)
theme.Gutter = o(theme.DarkBg, theme.Gutter) theme.Gutter = o(theme.DarkBg, theme.Gutter)
theme.PreviewFg = o(theme.Fg, theme.PreviewFg) theme.PreviewFg = o(theme.Fg, theme.PreviewFg)

View File

@@ -1,6 +1,7 @@
package util package util
import ( import (
"bytes"
"fmt" "fmt"
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
@@ -74,6 +75,35 @@ func (chars *Chars) Bytes() []byte {
return chars.slice return chars.slice
} }
func (chars *Chars) NumLines(atMost int) (int, bool) {
lines := 1
if runes := chars.optionalRunes(); runes != nil {
for _, r := range runes {
if r == '\n' {
lines++
}
if lines > atMost {
return atMost, true
}
}
return lines, false
}
for idx := 0; idx < len(chars.slice); idx++ {
found := bytes.IndexByte(chars.slice[idx:], '\n')
if found < 0 {
break
}
idx += found
lines++
if lines > atMost {
return atMost, true
}
}
return lines, false
}
func (chars *Chars) optionalRunes() []rune { func (chars *Chars) optionalRunes() []rune {
if chars.inBytes { if chars.inBytes {
return nil return nil

View File

@@ -3,6 +3,7 @@ package util
import ( import (
"math" "math"
"os" "os"
"strconv"
"strings" "strings"
"time" "time"
@@ -137,14 +138,10 @@ func DurWithin(
return val return val
} }
// IsTty returns true if stdin is a terminal // IsTty returns true if the file is a terminal
func IsTty() bool { func IsTty(file *os.File) bool {
return isatty.IsTerminal(os.Stdin.Fd()) fd := file.Fd()
} return isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd)
// ToTty returns true if stdout is a terminal
func ToTty() bool {
return isatty.IsTerminal(os.Stdout.Fd())
} }
// Once returns a function that returns the specified boolean value only once // Once returns a function that returns the specified boolean value only once
@@ -188,3 +185,34 @@ func ToKebabCase(s string) string {
} }
return strings.ToLower(name) return strings.ToLower(name)
} }
// CompareVersions compares two version strings
func CompareVersions(v1, v2 string) int {
parts1 := strings.Split(v1, ".")
parts2 := strings.Split(v2, ".")
atoi := func(s string) int {
n, e := strconv.Atoi(s)
if e != nil {
return 0
}
return n
}
for i := 0; i < Max(len(parts1), len(parts2)); i++ {
var p1, p2 int
if i < len(parts1) {
p1 = atoi(parts1[i])
}
if i < len(parts2) {
p2 = atoi(parts2[i])
}
if p1 > p2 {
return 1
} else if p1 < p2 {
return -1
}
}
return 0
}

View File

@@ -203,3 +203,34 @@ func TestStringWidth(t *testing.T) {
t.Errorf("Expected: %d, Actual: %d", 1, w) t.Errorf("Expected: %d, Actual: %d", 1, w)
} }
} }
func TestCompareVersions(t *testing.T) {
assert := func(a, b string, expected int) {
if result := CompareVersions(a, b); result != expected {
t.Errorf("Expected: %d, Actual: %d", expected, result)
}
}
assert("2", "1", 1)
assert("2", "2", 0)
assert("2", "10", -1)
assert("2.1", "2.2", -1)
assert("2.1", "2.1.1", -1)
assert("1.2.3", "1.2.2", 1)
assert("1.2.3", "1.2.3", 0)
assert("1.2.3", "1.2.3.0", 0)
assert("1.2.3", "1.2.4", -1)
// Different number of parts
assert("1.0.0", "1", 0)
assert("1.0.0", "1.0", 0)
assert("1.0.0", "1.0.0", 0)
assert("1.0", "1.0.0", 0)
assert("1", "1.0.0", 0)
assert("1.0.0", "1.0.0.1", -1)
assert("1.0.0.1.0", "1.0.0.1", 0)
assert("", "3.4.5", -1)
}

View File

@@ -157,10 +157,10 @@ func (x *Executor) QuoteEntry(entry string) string {
*/ */
return escapeArg(entry) return escapeArg(entry)
case shellTypePowerShell: case shellTypePowerShell:
escaped := strings.Replace(entry, `"`, `\"`, -1) escaped := strings.ReplaceAll(entry, `"`, `\"`)
return "'" + strings.Replace(escaped, "'", "''", -1) + "'" return "'" + strings.ReplaceAll(escaped, "'", "''") + "'"
default: default:
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'" return "'" + strings.ReplaceAll(entry, "'", "'\\''") + "'"
} }
} }

13
src/winpty.go Normal file
View File

@@ -0,0 +1,13 @@
//go:build !windows
package fzf
import "errors"
func needWinpty(_ *Options) bool {
return false
}
func runWinpty(_ []string, _ *Options) (int, error) {
return ExitError, errors.New("Not supported")
}

75
src/winpty_windows.go Normal file
View File

@@ -0,0 +1,75 @@
//go:build windows
package fzf
import (
"fmt"
"os"
"os/exec"
"strings"
"github.com/junegunn/fzf/src/util"
)
func isMintty345() bool {
return util.CompareVersions(os.Getenv("TERM_PROGRAM_VERSION"), "3.4.5") >= 0
}
func needWinpty(opts *Options) bool {
if os.Getenv("TERM_PROGRAM") != "mintty" {
return false
}
if isMintty345() {
/*
See: https://github.com/junegunn/fzf/issues/3809
"MSYS=enable_pcon" allows fzf to run properly on mintty 3.4.5 or later.
*/
if strings.Contains(os.Getenv("MSYS"), "enable_pcon") {
return false
}
// Setting the environment variable here unfortunately doesn't help,
// so we need to start a child process with "MSYS=enable_pcon"
// os.Setenv("MSYS", "enable_pcon")
return true
}
if opts.NoWinpty {
return false
}
if _, err := exec.LookPath("winpty"); err != nil {
return false
}
return true
}
func runWinpty(args []string, opts *Options) (int, error) {
sh, err := sh()
if err != nil {
return ExitError, err
}
argStr := escapeSingleQuote(args[0])
for _, arg := range args[1:] {
argStr += " " + escapeSingleQuote(arg)
}
argStr += ` --no-winpty`
if isMintty345() {
return runProxy(argStr, func(temp string) *exec.Cmd {
cmd := exec.Command(sh, temp)
cmd.Env = append(os.Environ(), "MSYS=enable_pcon")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd
}, opts, false)
}
return runProxy(argStr, func(temp string) *exec.Cmd {
cmd := exec.Command(sh, "-c", fmt.Sprintf(`winpty < /dev/tty > /dev/tty -- sh %q`, temp))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd
}, opts, false)
}

View File

@@ -24,7 +24,7 @@ DEFAULT_TIMEOUT = 10
FILE = File.expand_path(__FILE__) FILE = File.expand_path(__FILE__)
BASE = File.expand_path('..', __dir__) BASE = File.expand_path('..', __dir__)
Dir.chdir(BASE) Dir.chdir(BASE)
FZF = "FZF_DEFAULT_OPTS=--no-scrollbar FZF_DEFAULT_COMMAND= #{BASE}/bin/fzf" FZF = "FZF_DEFAULT_OPTS=\"--no-scrollbar --pointer \\> --marker \\>\" FZF_DEFAULT_COMMAND= #{BASE}/bin/fzf"
def wait def wait
since = Time.now since = Time.now
@@ -66,7 +66,7 @@ class Shell
end end
def fish def fish
"unset #{UNSETS.join(' ')}; FZF_DEFAULT_OPTS=--no-scrollbar fish_history= fish" "unset #{UNSETS.join(' ')}; FZF_DEFAULT_OPTS=\"--no-scrollbar --pointer '>' --marker '>'\" fish_history= fish"
end end
end end
end end
@@ -169,29 +169,34 @@ class Tmux
end end
class TestBase < Minitest::Test class TestBase < Minitest::Test
TEMPNAME = '/tmp/output' TEMPNAME = Dir::Tmpname.create(%w[fzf]) {}
FIFONAME = Dir::Tmpname.create(%w[fzf-fifo]) {}
attr_reader :tmux attr_reader :tmux
def writelines(lines)
File.write(TEMPNAME, lines.join("\n"))
end
def tempname def tempname
@temp_suffix ||= 0 TEMPNAME
[TEMPNAME,
caller_locations.map(&:label).find { |l| l.start_with?('test_') },
@temp_suffix].join('-')
end end
def writelines(path, lines) def fzf_output
FileUtils.rm_f(path) while File.exist?(path) @thread.join.value.chomp.tap { @thread = nil }
File.open(path, 'w') { |f| f.puts lines }
end end
def readonce def fzf_output_lines
wait { assert_path_exists tempname } fzf_output.lines(chomp: true)
File.read(tempname) end
ensure
FileUtils.rm_f(tempname) while File.exist?(tempname) def setup
@temp_suffix += 1 FileUtils.rm_f([TEMPNAME, FIFONAME])
tmux.prepare File.mkfifo(FIFONAME)
end
def teardown
FileUtils.rm_f([TEMPNAME, FIFONAME])
end end
alias assert_equal_org assert_equal alias assert_equal_org assert_equal
@@ -201,8 +206,12 @@ class TestBase < Minitest::Test
assert_equal_org(expected, actual) assert_equal_org(expected, actual)
end end
# Run fzf with its output piped to a fifo
def fzf(*opts) def fzf(*opts)
fzf!(*opts) + " > #{tempname}.tmp; mv #{tempname}.tmp #{tempname}" raise 'fzf_output not taken' if @thread
@thread = Thread.new { File.read(FIFONAME) }
fzf!(*opts) + " > #{FIFONAME}"
end end
def fzf!(*opts) def fzf!(*opts)
@@ -226,6 +235,7 @@ class TestGoFZF < TestBase
end end
def teardown def teardown
super
@tmux.kill @tmux.kill
end end
@@ -251,7 +261,7 @@ class TestGoFZF < TestBase
end end
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal '3910', readonce.chomp assert_equal '3910', fzf_output
end end
def test_fzf_default_command def test_fzf_default_command
@@ -259,7 +269,7 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal '> hello', lines[-3] } tmux.until { |lines| assert_equal '> hello', lines[-3] }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal 'hello', readonce.chomp assert_equal 'hello', fzf_output
end end
def test_fzf_default_command_failure def test_fzf_default_command_failure
@@ -355,7 +365,7 @@ class TestGoFZF < TestBase
:PgUp, 'C-J', :Down, :Tab, :Tab # 8, 7 :PgUp, 'C-J', :Down, :Tab, :Tab # 8, 7
tmux.until { |lines| assert_equal ' 10/10 (6)', lines[-2] } tmux.until { |lines| assert_equal ' 10/10 (6)', lines[-2] }
tmux.send_keys 'C-M' tmux.send_keys 'C-M'
assert_equal %w[3 2 5 6 8 7], readonce.lines(chomp: true) assert_equal %w[3 2 5 6 8 7], fzf_output_lines
end end
def test_multi_max def test_multi_max
@@ -462,12 +472,12 @@ class TestGoFZF < TestBase
tmux.send_keys :BTab, :BTab tmux.send_keys :BTab, :BTab
tmux.until { |lines| assert_equal ' 2/2 (2)', lines[-2] } tmux.until { |lines| assert_equal ' 2/2 (2)', lines[-2] }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal [' 1st 2nd 3rd/', ' first second third/'], readonce.lines(chomp: true) assert_equal [' 1st 2nd 3rd/', ' first second third/'], fzf_output_lines
else else
tmux.send_keys '^', '3' tmux.send_keys '^', '3'
tmux.until { |lines| assert_equal ' 1/2', lines[-2] } tmux.until { |lines| assert_equal ' 1/2', lines[-2] }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal [' 1st 2nd 3rd/'], readonce.lines(chomp: true) assert_equal [' 1st 2nd 3rd/'], fzf_output_lines
end end
end end
end end
@@ -479,18 +489,18 @@ class TestGoFZF < TestBase
tmux.send_keys(*Array.new(110) { rev ? :Down : :Up }) tmux.send_keys(*Array.new(110) { rev ? :Down : :Up })
tmux.until { |lines| assert_includes lines, '> 100' } tmux.until { |lines| assert_includes lines, '> 100' }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal '100', readonce.chomp assert_equal '100', fzf_output
end end
end end
def test_select_1 def test_select_1
tmux.send_keys "seq 1 100 | #{fzf(:with_nth, '..,..', :print_query, :q, 5555, :'1')}", :Enter tmux.send_keys "seq 1 100 | #{fzf(:with_nth, '..,..', :print_query, :q, 5555, :'1')}", :Enter
assert_equal %w[5555 55], readonce.lines(chomp: true) assert_equal %w[5555 55], fzf_output_lines
end end
def test_exit_0 def test_exit_0
tmux.send_keys "seq 1 100 | #{fzf(:with_nth, '..,..', :print_query, :q, 555_555, :'0')}", :Enter tmux.send_keys "seq 1 100 | #{fzf(:with_nth, '..,..', :print_query, :q, 555_555, :'0')}", :Enter
assert_equal %w[555555], readonce.lines(chomp: true) assert_equal %w[555555], fzf_output_lines
end end
def test_select_1_exit_0_fail def test_select_1_exit_0_fail
@@ -500,7 +510,7 @@ class TestGoFZF < TestBase
tmux.send_keys :BTab, :BTab, :BTab tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| assert_equal ' 19/100 (3)', lines[-2] } tmux.until { |lines| assert_equal ' 19/100 (3)', lines[-2] }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal %w[5 5 50 51], readonce.lines(chomp: true) assert_equal %w[5 5 50 51], fzf_output_lines
end end
end end
@@ -508,11 +518,11 @@ class TestGoFZF < TestBase
tmux.paste "(echo abc; echo $'\\352\\260\\200\\353\\202\\230\\353\\213\\244') | #{fzf(:query, "$'\\352\\260\\200\\353\\213\\244'")}" tmux.paste "(echo abc; echo $'\\352\\260\\200\\353\\202\\230\\353\\213\\244') | #{fzf(:query, "$'\\352\\260\\200\\353\\213\\244'")}"
tmux.until { |lines| assert_equal ' 1/2', lines[-2] } tmux.until { |lines| assert_equal ' 1/2', lines[-2] }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal %w[가나다], readonce.lines(chomp: true) assert_equal %w[가나다], fzf_output_lines
end end
def test_sync def test_sync
tmux.send_keys "seq 1 100 | #{fzf!(:multi)} | awk '{print $1 $1}' | #{fzf(:sync)}", :Enter tmux.send_keys "seq 1 100 | #{FZF} --multi | awk '{print $1 $1}' | #{fzf(:sync)}", :Enter
tmux.until { |lines| assert_equal '>', lines[-1] } tmux.until { |lines| assert_equal '>', lines[-1] }
tmux.send_keys 9 tmux.send_keys 9
tmux.until { |lines| assert_equal ' 19/100 (0)', lines[-2] } tmux.until { |lines| assert_equal ' 19/100 (0)', lines[-2] }
@@ -521,7 +531,7 @@ class TestGoFZF < TestBase
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| assert_equal '>', lines[-1] } tmux.until { |lines| assert_equal '>', lines[-1] }
tmux.send_keys 'C-K', :Enter tmux.send_keys 'C-K', :Enter
assert_equal %w[9090], readonce.lines(chomp: true) assert_equal %w[9090], fzf_output_lines
end end
def test_tac def test_tac
@@ -530,7 +540,7 @@ class TestGoFZF < TestBase
tmux.send_keys :BTab, :BTab, :BTab tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| assert_equal ' 1000/1000 (3)', lines[-2] } tmux.until { |lines| assert_equal ' 1000/1000 (3)', lines[-2] }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal %w[1000 999 998], readonce.lines(chomp: true) assert_equal %w[1000 999 998], fzf_output_lines
end end
def test_tac_sort def test_tac_sort
@@ -541,7 +551,7 @@ class TestGoFZF < TestBase
tmux.send_keys :BTab, :BTab, :BTab tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| assert_equal ' 28/1000 (3)', lines[-2] } tmux.until { |lines| assert_equal ' 28/1000 (3)', lines[-2] }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal %w[99 999 998], readonce.lines(chomp: true) assert_equal %w[99 999 998], fzf_output_lines
end end
def test_tac_nosort def test_tac_nosort
@@ -552,7 +562,7 @@ class TestGoFZF < TestBase
tmux.send_keys :BTab, :BTab, :BTab tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| assert_equal ' 10/1000 (3)', lines[-2] } tmux.until { |lines| assert_equal ' 10/1000 (3)', lines[-2] }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal %w[1000 900 800], readonce.lines(chomp: true) assert_equal %w[1000 900 800], fzf_output_lines
end end
def test_expect def test_expect
@@ -563,7 +573,7 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal ' 1/100', lines[-2] } tmux.until { |lines| assert_equal ' 1/100', lines[-2] }
tmux.send_keys(*feed) tmux.send_keys(*feed)
tmux.prepare tmux.prepare
assert_equal [expected, '55'], readonce.lines(chomp: true) assert_equal [expected, '55'], fzf_output_lines
end end
test.call('ctrl-t', 'C-T') test.call('ctrl-t', 'C-T')
test.call('ctrl-t', 'Enter', '') test.call('ctrl-t', 'Enter', '')
@@ -580,13 +590,20 @@ class TestGoFZF < TestBase
test.call('@', '@') test.call('@', '@')
end end
def test_expect_with_bound_actions
tmux.send_keys "seq 1 100 | #{fzf('--query 1 --print-query --expect z --bind z:up+up')}", :Enter
tmux.until { |lines| assert_equal 20, lines.match_count }
tmux.send_keys('z')
assert_equal %w[1 z 1], fzf_output_lines
end
def test_expect_print_query def test_expect_print_query
tmux.send_keys "seq 1 100 | #{fzf('--expect=alt-z', :print_query)}", :Enter tmux.send_keys "seq 1 100 | #{fzf('--expect=alt-z', :print_query)}", :Enter
tmux.until { |lines| assert_equal ' 100/100', lines[-2] } tmux.until { |lines| assert_equal ' 100/100', lines[-2] }
tmux.send_keys '55' tmux.send_keys '55'
tmux.until { |lines| assert_equal ' 1/100', lines[-2] } tmux.until { |lines| assert_equal ' 1/100', lines[-2] }
tmux.send_keys :Escape, :z tmux.send_keys :Escape, :z
assert_equal %w[55 alt-z 55], readonce.lines(chomp: true) assert_equal %w[55 alt-z 55], fzf_output_lines
end end
def test_expect_printable_character_print_query def test_expect_printable_character_print_query
@@ -595,12 +612,12 @@ class TestGoFZF < TestBase
tmux.send_keys '55' tmux.send_keys '55'
tmux.until { |lines| assert_equal ' 1/100', lines[-2] } tmux.until { |lines| assert_equal ' 1/100', lines[-2] }
tmux.send_keys 'z' tmux.send_keys 'z'
assert_equal %w[55 z 55], readonce.lines(chomp: true) assert_equal %w[55 z 55], fzf_output_lines
end end
def test_expect_print_query_select_1 def test_expect_print_query_select_1
tmux.send_keys "seq 1 100 | #{fzf('-q55 -1 --expect=alt-z --print-query')}", :Enter tmux.send_keys "seq 1 100 | #{fzf('-q55 -1 --expect=alt-z --print-query')}", :Enter
assert_equal ['55', '', '55'], readonce.lines(chomp: true) assert_equal ['55', '', '55'], fzf_output_lines
end end
def test_toggle_sort def test_toggle_sort
@@ -614,12 +631,12 @@ class TestGoFZF < TestBase
tmux.send_keys :Tab tmux.send_keys :Tab
tmux.until { |lines| assert_equal ' 4/111 +S (2)', lines[-2] } tmux.until { |lines| assert_equal ' 4/111 +S (2)', lines[-2] }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal %w[111 11], readonce.lines(chomp: true) assert_equal %w[111 11], fzf_output_lines
end end
end end
def test_unicode_case def test_unicode_case
writelines(tempname, %w[строКА1 СТРОКА2 строка3 Строка4]) writelines(%w[строКА1 СТРОКА2 строка3 Строка4])
assert_equal %w[СТРОКА2 Строка4], `#{FZF} -fС < #{tempname}`.lines(chomp: true) assert_equal %w[СТРОКА2 Строка4], `#{FZF} -fС < #{tempname}`.lines(chomp: true)
assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `#{FZF} -fс < #{tempname}`.lines(chomp: true) assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `#{FZF} -fс < #{tempname}`.lines(chomp: true)
end end
@@ -631,7 +648,7 @@ class TestGoFZF < TestBase
----foobar-- ----foobar--
-------foobar- -------foobar-
] ]
writelines(tempname, input) writelines(input)
assert_equal input, `#{FZF} -ffoobar --tiebreak=index < #{tempname}`.lines(chomp: true) assert_equal input, `#{FZF} -ffoobar --tiebreak=index < #{tempname}`.lines(chomp: true)
@@ -664,7 +681,7 @@ class TestGoFZF < TestBase
end end
def test_tiebreak_index_begin def test_tiebreak_index_begin
writelines(tempname, [ writelines([
'xoxxxxxoxx', 'xoxxxxxoxx',
'xoxxxxxox', 'xoxxxxxox',
'xxoxxxoxx', 'xxoxxxoxx',
@@ -713,10 +730,8 @@ class TestGoFZF < TestBase
end end
def test_tiebreak_begin_algo_v2 def test_tiebreak_begin_algo_v2
writelines(tempname, [ writelines(['baz foo bar',
'baz foo bar', 'foo bar baz'])
'foo bar baz'
])
assert_equal [ assert_equal [
'foo bar baz', 'foo bar baz',
'baz foo bar' 'baz foo bar'
@@ -724,14 +739,12 @@ class TestGoFZF < TestBase
end end
def test_tiebreak_end def test_tiebreak_end
writelines(tempname, [ writelines(['xoxxxxxxxx',
'xoxxxxxxxx', 'xxoxxxxxxx',
'xxoxxxxxxx', 'xxxoxxxxxx',
'xxxoxxxxxx', 'xxxxoxxxx',
'xxxxoxxxx', 'xxxxxoxxx',
'xxxxxoxxx', ' xxxxoxxx'])
' xxxxoxxx'
])
assert_equal [ assert_equal [
' xxxxoxxx', ' xxxxoxxx',
@@ -760,7 +773,7 @@ class TestGoFZF < TestBase
'xoxxxxxxxx' 'xoxxxxxxxx'
], `#{FZF} -fo --tiebreak=end,length,begin < #{tempname}`.lines(chomp: true) ], `#{FZF} -fo --tiebreak=end,length,begin < #{tempname}`.lines(chomp: true)
writelines(tempname, ['/bar/baz', '/foo/bar/baz']) writelines(['/bar/baz', '/foo/bar/baz'])
assert_equal [ assert_equal [
'/foo/bar/baz', '/foo/bar/baz',
'/bar/baz' '/bar/baz'
@@ -774,7 +787,7 @@ class TestGoFZF < TestBase
12345:he 12345:he
1234567:h 1234567:h
] ]
writelines(tempname, input) writelines(input)
output = %w[ output = %w[
1:hell 1:hell
@@ -789,11 +802,9 @@ class TestGoFZF < TestBase
end end
def test_tiebreak_chunk def test_tiebreak_chunk
writelines(tempname, [ writelines(['1 foobarbaz ba',
'1 foobarbaz ba', '2 foobar baz',
'2 foobar baz', '3 foo barbaz'])
'3 foo barbaz'
])
assert_equal [ assert_equal [
'3 foo barbaz', '3 foo barbaz',
@@ -823,7 +834,7 @@ class TestGoFZF < TestBase
end end
def test_invalid_cache_query_type def test_invalid_cache_query_type
command = %[(echo 'foo$bar'; echo 'barfoo'; echo 'foo^bar'; echo "foo'1-2"; seq 100) | #{fzf}] command = %[(echo 'foo$bar'; echo 'barfoo'; echo 'foo^bar'; echo "foo'1-2"; seq 100) | #{FZF}]
# Suffix match # Suffix match
tmux.send_keys command, :Enter tmux.send_keys command, :Enter
@@ -862,14 +873,14 @@ class TestGoFZF < TestBase
tmux.send_keys "seq 1 1000 | #{fzf('-m --bind=ctrl-j:accept,u:up,T:toggle-up,t:toggle')}", :Enter tmux.send_keys "seq 1 1000 | #{fzf('-m --bind=ctrl-j:accept,u:up,T:toggle-up,t:toggle')}", :Enter
tmux.until { |lines| assert_equal ' 1000/1000 (0)', lines[-2] } tmux.until { |lines| assert_equal ' 1000/1000 (0)', lines[-2] }
tmux.send_keys 'uuu', 'TTT', 'tt', 'uu', 'ttt', 'C-j' tmux.send_keys 'uuu', 'TTT', 'tt', 'uu', 'ttt', 'C-j'
assert_equal %w[4 5 6 9], readonce.lines(chomp: true) assert_equal %w[4 5 6 9], fzf_output_lines
end end
def test_bind_print_query def test_bind_print_query
tmux.send_keys "seq 1 1000 | #{fzf('-m --bind=ctrl-j:print-query')}", :Enter tmux.send_keys "seq 1 1000 | #{fzf('-m --bind=ctrl-j:print-query')}", :Enter
tmux.until { |lines| assert_equal ' 1000/1000 (0)', lines[-2] } tmux.until { |lines| assert_equal ' 1000/1000 (0)', lines[-2] }
tmux.send_keys 'print-my-query', 'C-j' tmux.send_keys 'print-my-query', 'C-j'
assert_equal %w[print-my-query], readonce.lines(chomp: true) assert_equal %w[print-my-query], fzf_output_lines
end end
def test_bind_replace_query def test_bind_replace_query
@@ -924,7 +935,7 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal ' 10/100 (12)', lines[-2] } tmux.until { |lines| assert_equal ' 10/100 (12)', lines[-2] }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal %w[1 2 10 20 30 40 50 60 70 80 90 100], assert_equal %w[1 2 10 20 30 40 50 60 70 80 90 100],
readonce.lines(chomp: true) fzf_output_lines
end end
def test_history def test_history
@@ -936,7 +947,7 @@ class TestGoFZF < TestBase
input = %w[00 11 22 33 44] input = %w[00 11 22 33 44]
input.each do |keys| input.each do |keys|
tmux.prepare tmux.prepare
tmux.send_keys "seq 100 | #{fzf(opts)}", :Enter tmux.send_keys "seq 100 | #{FZF} #{opts}", :Enter
tmux.until { |lines| assert_equal ' 100/100', lines[-2] } tmux.until { |lines| assert_equal ' 100/100', lines[-2] }
tmux.send_keys keys tmux.send_keys keys
tmux.until { |lines| assert_equal ' 1/100', lines[-2] } tmux.until { |lines| assert_equal ' 1/100', lines[-2] }
@@ -948,7 +959,7 @@ class TestGoFZF < TestBase
end end
# Update history entries (not changed on disk) # Update history entries (not changed on disk)
tmux.send_keys "seq 100 | #{fzf(opts)}", :Enter tmux.send_keys "seq 100 | #{FZF} #{opts}", :Enter
tmux.until { |lines| assert_equal ' 100/100', lines[-2] } tmux.until { |lines| assert_equal ' 100/100', lines[-2] }
tmux.send_keys 'C-p' tmux.send_keys 'C-p'
tmux.until { |lines| assert_equal '> 44', lines[-1] } tmux.until { |lines| assert_equal '> 44', lines[-1] }
@@ -971,7 +982,7 @@ class TestGoFZF < TestBase
end end
# Respect --bind option # Respect --bind option
tmux.send_keys "seq 100 | #{fzf(opts + ' --bind ctrl-p:next-history,ctrl-n:previous-history')}", :Enter tmux.send_keys "seq 100 | #{FZF} #{opts} --bind ctrl-p:next-history,ctrl-n:previous-history", :Enter
tmux.until { |lines| assert_equal ' 100/100', lines[-2] } tmux.until { |lines| assert_equal ' 100/100', lines[-2] }
tmux.send_keys 'C-n', 'C-n', 'C-n', 'C-n', 'C-p' tmux.send_keys 'C-n', 'C-n', 'C-n', 'C-n', 'C-p'
tmux.until { |lines| assert_equal '> 33', lines[-1] } tmux.until { |lines| assert_equal '> 33', lines[-1] }
@@ -983,8 +994,8 @@ class TestGoFZF < TestBase
def test_execute def test_execute
output = '/tmp/fzf-test-execute' output = '/tmp/fzf-test-execute'
opts = %[--bind "alt-a:execute(echo /{}/ >> #{output})+change-header(alt-a),alt-b:execute[echo /{}{}/ >> #{output}]+change-header(alt-b),C:execute(echo /{}{}{}/ >> #{output})+change-header(C)"] opts = %[--bind "alt-a:execute(echo /{}/ >> #{output})+change-header(alt-a),alt-b:execute[echo /{}{}/ >> #{output}]+change-header(alt-b),C:execute(echo /{}{}{}/ >> #{output})+change-header(C)"]
writelines(tempname, %w[foo'bar foo"bar foo$bar]) writelines(%w[foo'bar foo"bar foo$bar])
tmux.send_keys "cat #{tempname} | #{fzf(opts)}", :Enter tmux.send_keys "cat #{tempname} | #{FZF} #{opts}", :Enter
tmux.until { |lines| assert_equal 3, lines.item_count } tmux.until { |lines| assert_equal 3, lines.item_count }
ready = ->(s) { tmux.until { |lines| assert_includes lines[-3], s } } ready = ->(s) { tmux.until { |lines| assert_includes lines[-3], s } }
@@ -1026,8 +1037,8 @@ class TestGoFZF < TestBase
def test_execute_multi def test_execute_multi
output = '/tmp/fzf-test-execute-multi' output = '/tmp/fzf-test-execute-multi'
opts = %[--multi --bind "alt-a:execute-multi(echo {}/{+} >> #{output})+change-header(alt-a),alt-b:change-header(alt-b)"] opts = %[--multi --bind "alt-a:execute-multi(echo {}/{+} >> #{output})+change-header(alt-a),alt-b:change-header(alt-b)"]
writelines(tempname, %w[foo'bar foo"bar foo$bar foobar]) writelines(%w[foo'bar foo"bar foo$bar foobar])
tmux.send_keys "cat #{tempname} | #{fzf(opts)}", :Enter tmux.send_keys "cat #{tempname} | #{FZF} #{opts}", :Enter
ready = ->(s) { tmux.until { |lines| assert_includes lines[-3], s } } ready = ->(s) { tmux.until { |lines| assert_includes lines[-3], s } }
tmux.until { |lines| assert_equal ' 4/4 (0)', lines[-2] } tmux.until { |lines| assert_equal ' 4/4 (0)', lines[-2] }
@@ -1062,7 +1073,7 @@ class TestGoFZF < TestBase
def test_execute_plus_flag def test_execute_plus_flag
output = tempname + '.tmp' output = tempname + '.tmp'
FileUtils.rm_f(output) FileUtils.rm_f(output)
writelines(tempname, ['foo bar', '123 456']) writelines(['foo bar', '123 456'])
tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute-silent(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute-silent(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter
@@ -1101,8 +1112,7 @@ class TestGoFZF < TestBase
# Custom script to use as $SHELL # Custom script to use as $SHELL
output = tempname + '.out' output = tempname + '.out'
FileUtils.rm_f(output) FileUtils.rm_f(output)
writelines(tempname, writelines(['#!/usr/bin/env bash', "echo $1 / $2 > #{output}"])
['#!/usr/bin/env bash', "echo $1 / $2 > #{output}"])
system("chmod +x #{tempname}") system("chmod +x #{tempname}")
tmux.send_keys "echo foo | SHELL=#{tempname} fzf --bind 'enter:execute:{}bar'", :Enter tmux.send_keys "echo foo | SHELL=#{tempname} fzf --bind 'enter:execute:{}bar'", :Enter
@@ -1118,7 +1128,7 @@ class TestGoFZF < TestBase
end end
def test_cycle def test_cycle
tmux.send_keys "seq 8 | #{fzf(:cycle)}", :Enter tmux.send_keys "seq 8 | #{FZF} --cycle", :Enter
tmux.until { |lines| assert_equal ' 8/8', lines[-2] } tmux.until { |lines| assert_equal ' 8/8', lines[-2] }
tmux.send_keys :Down tmux.send_keys :Down
tmux.until { |lines| assert_equal '> 8', lines[-10] } tmux.until { |lines| assert_equal '> 8', lines[-10] }
@@ -1148,7 +1158,7 @@ class TestGoFZF < TestBase
tmux.send_keys :Down tmux.send_keys :Down
end end
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal '50', readonce.chomp assert_equal '50', fzf_output
end end
def test_header_lines_reverse def test_header_lines_reverse
@@ -1163,7 +1173,7 @@ class TestGoFZF < TestBase
tmux.send_keys :Up tmux.send_keys :Up
end end
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal '50', readonce.chomp assert_equal '50', fzf_output
end end
def test_header_lines_reverse_list def test_header_lines_reverse_list
@@ -1178,7 +1188,7 @@ class TestGoFZF < TestBase
tmux.send_keys :Up tmux.send_keys :Up
end end
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal '50', readonce.chomp assert_equal '50', fzf_output
end end
def test_header_lines_overflow def test_header_lines_overflow
@@ -1188,7 +1198,7 @@ class TestGoFZF < TestBase
assert_equal ' 1', lines[-3] assert_equal ' 1', lines[-3]
end end
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal '', readonce.chomp assert_equal '', fzf_output
end end
def test_header_lines_with_nth def test_header_lines_with_nth
@@ -1200,11 +1210,11 @@ class TestGoFZF < TestBase
assert_equal '> 66666', lines[-8] assert_equal '> 66666', lines[-8]
end end
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal '6', readonce.chomp assert_equal '6', fzf_output
end end
def test_header def test_header
tmux.send_keys "seq 100 | #{fzf("--header \"$(head -5 #{FILE})\"")}", :Enter tmux.send_keys %[seq 100 | #{FZF} --header "$(head -5 #{FILE})"], :Enter
header = File.readlines(FILE, chomp: true).take(5) header = File.readlines(FILE, chomp: true).take(5)
tmux.until do |lines| tmux.until do |lines|
assert_equal ' 100/100', lines[-2] assert_equal ' 100/100', lines[-2]
@@ -1214,7 +1224,7 @@ class TestGoFZF < TestBase
end end
def test_header_reverse def test_header_reverse
tmux.send_keys "seq 100 | #{fzf("--header \"$(head -5 #{FILE})\" --reverse")}", :Enter tmux.send_keys %[seq 100 | #{FZF} --header "$(head -5 #{FILE})" --reverse], :Enter
header = File.readlines(FILE, chomp: true).take(5) header = File.readlines(FILE, chomp: true).take(5)
tmux.until do |lines| tmux.until do |lines|
assert_equal ' 100/100', lines[1] assert_equal ' 100/100', lines[1]
@@ -1224,7 +1234,7 @@ class TestGoFZF < TestBase
end end
def test_header_reverse_list def test_header_reverse_list
tmux.send_keys "seq 100 | #{fzf("--header \"$(head -5 #{FILE})\" --layout=reverse-list")}", :Enter tmux.send_keys %[seq 100 | #{FZF} --header "$(head -5 #{FILE})" --layout=reverse-list], :Enter
header = File.readlines(FILE, chomp: true).take(5) header = File.readlines(FILE, chomp: true).take(5)
tmux.until do |lines| tmux.until do |lines|
assert_equal ' 100/100', lines[-2] assert_equal ' 100/100', lines[-2]
@@ -1234,7 +1244,7 @@ class TestGoFZF < TestBase
end end
def test_header_and_header_lines def test_header_and_header_lines
tmux.send_keys "seq 100 | #{fzf("--header-lines 10 --header \"$(head -5 #{FILE})\"")}", :Enter tmux.send_keys %[seq 100 | #{FZF} --header-lines 10 --header "$(head -5 #{FILE})"], :Enter
header = File.readlines(FILE, chomp: true).take(5) header = File.readlines(FILE, chomp: true).take(5)
tmux.until do |lines| tmux.until do |lines|
assert_equal ' 90/90', lines[-2] assert_equal ' 90/90', lines[-2]
@@ -1244,7 +1254,7 @@ class TestGoFZF < TestBase
end end
def test_header_and_header_lines_reverse def test_header_and_header_lines_reverse
tmux.send_keys "seq 100 | #{fzf("--reverse --header-lines 10 --header \"$(head -5 #{FILE})\"")}", :Enter tmux.send_keys %[seq 100 | #{FZF} --reverse --header-lines 10 --header "$(head -5 #{FILE})"], :Enter
header = File.readlines(FILE, chomp: true).take(5) header = File.readlines(FILE, chomp: true).take(5)
tmux.until do |lines| tmux.until do |lines|
assert_equal ' 90/90', lines[1] assert_equal ' 90/90', lines[1]
@@ -1254,7 +1264,7 @@ class TestGoFZF < TestBase
end end
def test_header_and_header_lines_reverse_list def test_header_and_header_lines_reverse_list
tmux.send_keys "seq 100 | #{fzf("--layout=reverse-list --header-lines 10 --header \"$(head -5 #{FILE})\"")}", :Enter tmux.send_keys %[seq 100 | #{FZF} --layout=reverse-list --header-lines 10 --header "$(head -5 #{FILE})"], :Enter
header = File.readlines(FILE, chomp: true).take(5) header = File.readlines(FILE, chomp: true).take(5)
tmux.until do |lines| tmux.until do |lines|
assert_equal ' 90/90', lines[-2] assert_equal ' 90/90', lines[-2]
@@ -1297,7 +1307,7 @@ class TestGoFZF < TestBase
end end
def test_cancel def test_cancel
tmux.send_keys "seq 10 | #{fzf('--bind 2:cancel')}", :Enter tmux.send_keys "seq 10 | #{FZF} --bind 2:cancel", :Enter
tmux.until { |lines| assert_equal ' 10/10', lines[-2] } tmux.until { |lines| assert_equal ' 10/10', lines[-2] }
tmux.send_keys '123' tmux.send_keys '123'
tmux.until do |lines| tmux.until do |lines|
@@ -1313,7 +1323,7 @@ class TestGoFZF < TestBase
end end
def test_margin def test_margin
tmux.send_keys "yes | head -1000 | #{fzf('--margin 5,3')}", :Enter tmux.send_keys "yes | head -1000 | #{FZF} --margin 5,3", :Enter
tmux.until do |lines| tmux.until do |lines|
assert_equal '', lines[4] assert_equal '', lines[4]
assert_equal ' y', lines[5] assert_equal ' y', lines[5]
@@ -1322,13 +1332,13 @@ class TestGoFZF < TestBase
end end
def test_margin_reverse def test_margin_reverse
tmux.send_keys "seq 1000 | #{fzf('--margin 7,5 --reverse')}", :Enter tmux.send_keys "seq 1000 | #{FZF} --margin 7,5 --reverse", :Enter
tmux.until { |lines| assert_equal ' 1000/1000', lines[1 + 7] } tmux.until { |lines| assert_equal ' 1000/1000', lines[1 + 7] }
tmux.send_keys :Enter tmux.send_keys :Enter
end end
def test_margin_reverse_list def test_margin_reverse_list
tmux.send_keys "yes | head -1000 | #{fzf('--margin 5,3 --layout=reverse-list')}", :Enter tmux.send_keys "yes | head -1000 | #{FZF} --margin 5,3 --layout=reverse-list", :Enter
tmux.until do |lines| tmux.until do |lines|
assert_equal '', lines[4] assert_equal '', lines[4]
assert_equal ' > y', lines[5] assert_equal ' > y', lines[5]
@@ -1337,7 +1347,7 @@ class TestGoFZF < TestBase
end end
def test_tabstop def test_tabstop
writelines(tempname, %W[f\too\tba\tr\tbaz\tbarfooq\tux]) writelines(%W[f\too\tba\tr\tbaz\tbarfooq\tux])
{ {
1 => '> f oo ba r baz barfooq ux', 1 => '> f oo ba r baz barfooq ux',
2 => '> f oo ba r baz barfooq ux', 2 => '> f oo ba r baz barfooq ux',
@@ -1359,14 +1369,14 @@ class TestGoFZF < TestBase
end end
def test_with_nth_basic def test_with_nth_basic
writelines(tempname, ['hello world ', 'byebye']) writelines(['hello world ', 'byebye'])
assert_equal \ assert_equal \
'hello world ', 'hello world ',
`#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp `#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp
end end
def test_with_nth_ansi def test_with_nth_ansi
writelines(tempname, ["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye']) writelines(["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye'])
assert_equal \ assert_equal \
'hello world ', 'hello world ',
`#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi < #{tempname}`.chomp `#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi < #{tempname}`.chomp
@@ -1374,7 +1384,7 @@ class TestGoFZF < TestBase
def test_with_nth_no_ansi def test_with_nth_no_ansi
src = "\x1b[33mhello \x1b[34;1mworld\x1b[m " src = "\x1b[33mhello \x1b[34;1mworld\x1b[m "
writelines(tempname, [src, 'byebye']) writelines([src, 'byebye'])
assert_equal \ assert_equal \
src, src,
`#{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi < #{tempname}`.chomp `#{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi < #{tempname}`.chomp
@@ -1429,7 +1439,7 @@ class TestGoFZF < TestBase
end end
def test_hscroll_off def test_hscroll_off
writelines(tempname, ['=' * 10_000 + '0123456789']) writelines(['=' * 10_000 + '0123456789'])
[0, 3, 6].each do |off| [0, 3, 6].each do |off|
tmux.prepare tmux.prepare
tmux.send_keys "#{FZF} --hscroll-off=#{off} -q 0 < #{tempname}", :Enter tmux.send_keys "#{FZF} --hscroll-off=#{off} -q 0 < #{tempname}", :Enter
@@ -1475,7 +1485,7 @@ class TestGoFZF < TestBase
tmux.send_keys :Tab tmux.send_keys :Tab
tmux.until { |lines| assert_equal '>>1', lines[-3] } tmux.until { |lines| assert_equal '>>1', lines[-3] }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal %w[5 2 1], readonce.lines(chomp: true) assert_equal %w[5 2 1], fzf_output_lines
end end
def test_jump_accept def test_jump_accept
@@ -1484,11 +1494,11 @@ class TestGoFZF < TestBase
tmux.send_keys 'C-j' tmux.send_keys 'C-j'
tmux.until { |lines| assert_equal '5 5', lines[-7] } tmux.until { |lines| assert_equal '5 5', lines[-7] }
tmux.send_keys '3' tmux.send_keys '3'
assert_equal '3', readonce.chomp assert_equal '3', fzf_output
end end
def test_jump_events def test_jump_events
tmux.send_keys "seq 1000 | #{fzf("--multi --jump-labels 12345 --bind 'ctrl-j:jump,jump:preview(echo jumped to {}),jump-cancel:preview(echo jump cancelled at {})'")}", :Enter tmux.send_keys "seq 1000 | #{FZF} --multi --jump-labels 12345 --bind 'ctrl-j:jump,jump:preview(echo jumped to {}),jump-cancel:preview(echo jump cancelled at {})'", :Enter
tmux.until { |lines| assert_equal ' 1000/1000 (0)', lines[-2] } tmux.until { |lines| assert_equal ' 1000/1000 (0)', lines[-2] }
tmux.send_keys 'C-j' tmux.send_keys 'C-j'
tmux.until { |lines| assert_includes lines[-7], '5 5' } tmux.until { |lines| assert_includes lines[-7], '5 5' }
@@ -1507,7 +1517,7 @@ class TestGoFZF < TestBase
end end
def test_pointer_with_jump def test_pointer_with_jump
tmux.send_keys "seq 10 | #{fzf("--multi --jump-labels 12345 --bind 'ctrl-j:jump' --pointer '>>'")}", :Enter tmux.send_keys "seq 10 | #{FZF} --multi --jump-labels 12345 --bind 'ctrl-j:jump' --pointer '>>'", :Enter
tmux.until { |lines| assert_equal ' 10/10 (0)', lines[-2] } tmux.until { |lines| assert_equal ' 10/10 (0)', lines[-2] }
tmux.send_keys 'C-j' tmux.send_keys 'C-j'
# Correctly padded jump label should appear # Correctly padded jump label should appear
@@ -1519,7 +1529,7 @@ class TestGoFZF < TestBase
end end
def test_marker def test_marker
tmux.send_keys "seq 10 | #{fzf("--multi --marker '>>'")}", :Enter tmux.send_keys "seq 10 | #{FZF} --multi --marker '>>'", :Enter
tmux.until { |lines| assert_equal ' 10/10 (0)', lines[-2] } tmux.until { |lines| assert_equal ' 10/10 (0)', lines[-2] }
tmux.send_keys :BTab tmux.send_keys :BTab
# Assert that specified marker is displayed # Assert that specified marker is displayed
@@ -1643,7 +1653,6 @@ class TestGoFZF < TestBase
end end
def test_preview_size_0 def test_preview_size_0
FileUtils.rm_f(tempname)
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0 --bind space:toggle-preview), :Enter tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0 --bind space:toggle-preview), :Enter
tmux.until do |lines| tmux.until do |lines|
assert_equal 100, lines.item_count assert_equal 100, lines.item_count
@@ -1669,7 +1678,6 @@ class TestGoFZF < TestBase
end end
def test_preview_size_0_hidden def test_preview_size_0_hidden
FileUtils.rm_f(tempname)
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0,hidden --bind space:toggle-preview), :Enter tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0,hidden --bind space:toggle-preview), :Enter
tmux.until { |lines| assert_equal 100, lines.item_count } tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.send_keys :Down, :Down tmux.send_keys :Down, :Down
@@ -1739,14 +1747,11 @@ class TestGoFZF < TestBase
end end
def test_no_clear def test_no_clear
tmux.send_keys "seq 10 | fzf --no-clear --inline-info --height 5 > #{tempname}", :Enter tmux.send_keys "seq 10 | #{fzf('--no-clear --inline-info --height 5')}", :Enter
prompt = '> < 10/10' prompt = '> < 10/10'
tmux.until { |lines| assert_equal prompt, lines[-1] } tmux.until { |lines| assert_equal prompt, lines[-1] }
tmux.send_keys :Enter tmux.send_keys :Enter
wait do assert_equal %w[1], fzf_output_lines
assert_path_exists tempname
assert_equal %w[1], File.readlines(tempname, chomp: true)
end
tmux.until { |lines| assert_equal prompt, lines[-1] } tmux.until { |lines| assert_equal prompt, lines[-1] }
end end
@@ -1815,7 +1820,7 @@ class TestGoFZF < TestBase
tmux.send_keys '999' tmux.send_keys '999'
tmux.until { |lines| assert_equal ' 1/1000', lines[-2] } tmux.until { |lines| assert_equal ' 1/1000', lines[-2] }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal %w[999 999], readonce.lines(chomp: true) assert_equal %w[999 999], fzf_output_lines
end end
def test_accept_non_empty_with_multi_selection def test_accept_non_empty_with_multi_selection
@@ -1827,7 +1832,7 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal ' 0/1000 (1)', lines[-2] } tmux.until { |lines| assert_equal ' 0/1000 (1)', lines[-2] }
# fzf will exit in this case even though there's no match for the current query # fzf will exit in this case even though there's no match for the current query
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal %w[foo 1], readonce.lines(chomp: true) assert_equal %w[foo 1], fzf_output_lines
end end
def test_accept_non_empty_with_empty_list def test_accept_non_empty_with_empty_list
@@ -1835,7 +1840,7 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal ' 0/0', lines[-2] } tmux.until { |lines| assert_equal ' 0/0', lines[-2] }
tmux.send_keys :Enter tmux.send_keys :Enter
# fzf will exit anyway since input list is empty # fzf will exit anyway since input list is empty
assert_equal %w[foo], readonce.lines(chomp: true) assert_equal %w[foo], fzf_output_lines
end end
def test_accept_or_print_query_without_match def test_accept_or_print_query_without_match
@@ -1844,7 +1849,7 @@ class TestGoFZF < TestBase
tmux.send_keys 99_999 tmux.send_keys 99_999
tmux.until { |lines| assert_equal 0, lines.match_count } tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal %w[99999], readonce.lines(chomp: true) assert_equal %w[99999], fzf_output_lines
end end
def test_accept_or_print_query_with_match def test_accept_or_print_query_with_match
@@ -1853,7 +1858,7 @@ class TestGoFZF < TestBase
tmux.send_keys '^99$' tmux.send_keys '^99$'
tmux.until { |lines| assert_equal 1, lines.match_count } tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal %w[99], readonce.lines(chomp: true) assert_equal %w[99], fzf_output_lines
end end
def test_accept_or_print_query_with_multi_selection def test_accept_or_print_query_with_multi_selection
@@ -1864,7 +1869,7 @@ class TestGoFZF < TestBase
tmux.send_keys 99_999 tmux.send_keys 99_999
tmux.until { |lines| assert_equal 0, lines.match_count } tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal %w[1 2 3], readonce.lines(chomp: true) assert_equal %w[1 2 3], fzf_output_lines
end end
def test_preview_update_on_select def test_preview_update_on_select
@@ -1886,7 +1891,7 @@ class TestGoFZF < TestBase
'foo bar', 'foo bar',
'bar foo' 'bar foo'
] ]
writelines(tempname, input) writelines(input)
assert_equal input.length, `#{FZF} -f'foo bar' < #{tempname}`.lines.length assert_equal input.length, `#{FZF} -f'foo bar' < #{tempname}`.lines.length
assert_equal input.length - 1, `#{FZF} -f'^foo bar$' < #{tempname}`.lines.length assert_equal input.length - 1, `#{FZF} -f'^foo bar$' < #{tempname}`.lines.length
@@ -1912,7 +1917,7 @@ class TestGoFZF < TestBase
end end
def test_preview_correct_tab_width_after_ansi_reset_code def test_preview_correct_tab_width_after_ansi_reset_code
writelines(tempname, ["\x1b[31m+\x1b[m\t\x1b[32mgreen"]) writelines(["\x1b[31m+\x1b[m\t\x1b[32mgreen"])
tmux.send_keys "#{FZF} --preview 'cat #{tempname}'", :Enter tmux.send_keys "#{FZF} --preview 'cat #{tempname}'", :Enter
tmux.until { |lines| assert_includes lines[1], ' + green ' } tmux.until { |lines| assert_includes lines[1], ' + green ' }
end end
@@ -2102,7 +2107,7 @@ class TestGoFZF < TestBase
end end
def test_backward_delete_char_eof def test_backward_delete_char_eof
tmux.send_keys "seq 1000 | #{fzf("--bind 'bs:backward-delete-char/eof'")}", :Enter tmux.send_keys "seq 1000 | #{FZF} --bind 'bs:backward-delete-char/eof'", :Enter
tmux.until { |lines| assert_equal ' 1000/1000', lines[-2] } tmux.until { |lines| assert_equal ' 1000/1000', lines[-2] }
tmux.send_keys '11' tmux.send_keys '11'
tmux.until { |lines| assert_equal '> 11', lines[-1] } tmux.until { |lines| assert_equal '> 11', lines[-1] }
@@ -2116,7 +2121,7 @@ class TestGoFZF < TestBase
def test_strip_xterm_osc_sequence def test_strip_xterm_osc_sequence
%W[\x07 \x1b\\].each do |esc| %W[\x07 \x1b\\].each do |esc|
writelines(tempname, [%(printf $1"\e]4;3;rgb:aa/bb/cc#{esc} "$2)]) writelines([%(printf $1"\e]4;3;rgb:aa/bb/cc#{esc} "$2)])
File.chmod(0o755, tempname) File.chmod(0o755, tempname)
tmux.prepare tmux.prepare
tmux.send_keys \ tmux.send_keys \
@@ -2128,7 +2133,7 @@ class TestGoFZF < TestBase
end end
def test_keep_right def test_keep_right
tmux.send_keys "seq 10000 | #{FZF} --read0 --keep-right", :Enter tmux.send_keys "seq 10000 | #{FZF} --read0 --keep-right --no-multi-line", :Enter
tmux.until { |lines| assert lines.any_include?('9999␊10000') } tmux.until { |lines| assert lines.any_include?('9999␊10000') }
end end
@@ -2343,81 +2348,69 @@ class TestGoFZF < TestBase
end end
def test_kill_default_command_on_abort def test_kill_default_command_on_abort
script = tempname + '.sh' writelines(['#!/usr/bin/env bash',
writelines(script,
['#!/usr/bin/env bash',
"echo 'Started'", "echo 'Started'",
'while :; do sleep 1; done']) 'while :; do sleep 1; done'])
system("chmod +x #{script}") system("chmod +x #{tempname}")
tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{script}"), :Enter tmux.send_keys FZF.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{tempname}"), :Enter
tmux.until { |lines| assert_equal 1, lines.item_count } tmux.until { |lines| assert_equal 1, lines.item_count }
tmux.send_keys 'C-c' tmux.send_keys 'C-c'
tmux.send_keys 'C-l', 'closed' tmux.send_keys 'C-l', 'closed'
tmux.until { |lines| assert_includes lines[0], 'closed' } tmux.until { |lines| assert_includes lines[0], 'closed' }
wait { refute system("pgrep -f #{script}") } wait { refute system("pgrep -f #{tempname}") }
ensure ensure
system("pkill -9 -f #{script}") system("pkill -9 -f #{tempname}")
FileUtils.rm_f(script)
end end
def test_kill_default_command_on_accept def test_kill_default_command_on_accept
script = tempname + '.sh' writelines(['#!/usr/bin/env bash',
writelines(script,
['#!/usr/bin/env bash',
"echo 'Started'", "echo 'Started'",
'while :; do sleep 1; done']) 'while :; do sleep 1; done'])
system("chmod +x #{script}") system("chmod +x #{tempname}")
tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{script}"), :Enter tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{tempname}"), :Enter
tmux.until { |lines| assert_equal 1, lines.item_count } tmux.until { |lines| assert_equal 1, lines.item_count }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal 'Started', readonce.chomp assert_equal 'Started', fzf_output
wait { refute system("pgrep -f #{script}") } wait { refute system("pgrep -f #{tempname}") }
ensure ensure
system("pkill -9 -f #{script}") system("pkill -9 -f #{tempname}")
FileUtils.rm_f(script)
end end
def test_kill_reload_command_on_abort def test_kill_reload_command_on_abort
script = tempname + '.sh' writelines(['#!/usr/bin/env bash',
writelines(script,
['#!/usr/bin/env bash',
"echo 'Started'", "echo 'Started'",
'while :; do sleep 1; done']) 'while :; do sleep 1; done'])
system("chmod +x #{script}") system("chmod +x #{tempname}")
tmux.send_keys "seq 1 3 | #{fzf("--bind 'ctrl-r:reload(#{script})'")}", :Enter tmux.send_keys "seq 1 3 | #{FZF} --bind 'ctrl-r:reload(#{tempname})'", :Enter
tmux.until { |lines| assert_equal 3, lines.item_count } tmux.until { |lines| assert_equal 3, lines.item_count }
tmux.send_keys 'C-r' tmux.send_keys 'C-r'
tmux.until { |lines| assert_equal 1, lines.item_count } tmux.until { |lines| assert_equal 1, lines.item_count }
tmux.send_keys 'C-c' tmux.send_keys 'C-c'
tmux.send_keys 'C-l', 'closed' tmux.send_keys 'C-l', 'closed'
tmux.until { |lines| assert_includes lines[0], 'closed' } tmux.until { |lines| assert_includes lines[0], 'closed' }
wait { refute system("pgrep -f #{script}") } wait { refute system("pgrep -f #{tempname}") }
ensure ensure
system("pkill -9 -f #{script}") system("pkill -9 -f #{tempname}")
FileUtils.rm_f(script)
end end
def test_kill_reload_command_on_accept def test_kill_reload_command_on_accept
script = tempname + '.sh' writelines(['#!/usr/bin/env bash',
writelines(script,
['#!/usr/bin/env bash',
"echo 'Started'", "echo 'Started'",
'while :; do sleep 1; done']) 'while :; do sleep 1; done'])
system("chmod +x #{script}") system("chmod +x #{tempname}")
tmux.send_keys "seq 1 3 | #{fzf("--bind 'ctrl-r:reload(#{script})'")}", :Enter tmux.send_keys "seq 1 3 | #{fzf("--bind 'ctrl-r:reload(#{tempname})'")}", :Enter
tmux.until { |lines| assert_equal 3, lines.item_count } tmux.until { |lines| assert_equal 3, lines.item_count }
tmux.send_keys 'C-r' tmux.send_keys 'C-r'
tmux.until { |lines| assert_equal 1, lines.item_count } tmux.until { |lines| assert_equal 1, lines.item_count }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal 'Started', readonce.chomp assert_equal 'Started', fzf_output
wait { refute system("pgrep -f #{script}") } wait { refute system("pgrep -f #{tempname}") }
ensure ensure
system("pkill -9 -f #{script}") system("pkill -9 -f #{tempname}")
FileUtils.rm_f(script)
end end
def test_preview_header def test_preview_header
@@ -2759,8 +2752,9 @@ class TestGoFZF < TestBase
def assert_block(expected, lines) def assert_block(expected, lines)
cols = expected.lines.map(&:chomp).map(&:length).max cols = expected.lines.map(&:chomp).map(&:length).max
actual = lines.reverse.take(expected.lines.length).reverse.map { _1[0, cols].rstrip + "\n" }.join top = lines.take(expected.lines.length).map { _1[0, cols].rstrip + "\n" }.join
assert_equal_org expected, actual bottom = lines.reverse.take(expected.lines.length).reverse.map { _1[0, cols].rstrip + "\n" }.join
assert_includes [top, bottom], expected
end end
def test_height_range_fit def test_height_range_fit
@@ -3275,6 +3269,55 @@ class TestGoFZF < TestBase
tmux.send_keys '99' tmux.send_keys '99'
tmux.until { |lines| assert(lines.any? { |line| line.include?('0 / 0') }) } tmux.until { |lines| assert(lines.any? { |line| line.include?('0 / 0') }) }
end end
def test_fzf_multi_line
tmux.send_keys %[(echo -en '0\\0'; echo -en '1\\n2\\0'; seq 1000) | fzf --read0 --multi --bind load:select-all --border rounded], :Enter
block = <<~BLOCK
998
999
1000
1
2
>>0
3/3 (3)
>
BLOCK
tmux.until { assert_block(block, _1) }
tmux.send_keys :Up, :Up
block = <<~BLOCK
>1
>2
>3
BLOCK
tmux.until { assert_block(block, _1) }
block = <<~BLOCK
>
>
BLOCK
tmux.until { assert_block(block, _1) }
end
def test_fzf_multi_line_reverse
tmux.send_keys %[(echo -en '0\\0'; echo -en '1\\n2\\0'; seq 1000) | fzf --read0 --multi --bind load:select-all --border rounded --reverse], :Enter
block = <<~BLOCK
>
3/3 (3)
>>0
1
2
1
2
3
BLOCK
tmux.until { assert_block(block, _1) }
end
end end
module TestShell module TestShell
@@ -3313,7 +3356,7 @@ module TestShell
end end
def test_ctrl_t_unicode def test_ctrl_t_unicode
writelines(tempname, ['fzf-unicode 테스트1', 'fzf-unicode 테스트2']) writelines(['fzf-unicode 테스트1', 'fzf-unicode 테스트2'])
set_var('FZF_CTRL_T_COMMAND', "cat #{tempname}") set_var('FZF_CTRL_T_COMMAND', "cat #{tempname}")
tmux.prepare tmux.prepare
@@ -3397,12 +3440,16 @@ module TestShell
end end
def test_ctrl_r_multiline def test_ctrl_r_multiline
# NOTE: Current bash implementation shows an extra new line if there's
# only entry in the history
tmux.send_keys ':', :Enter
tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] } tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
tmux.prepare tmux.prepare
tmux.send_keys 'C-r' tmux.send_keys 'C-r'
tmux.until { |lines| assert_equal '>', lines[-1] } tmux.until { |lines| assert_equal '>', lines[-1] }
tmux.send_keys 'foo bar' tmux.send_keys 'foo bar'
tmux.until { |lines| assert lines[-4]&.match?(/"foo/) } unless shell == :zsh
tmux.until { |lines| assert lines[-3]&.match?(/bar"␊?/) } tmux.until { |lines| assert lines[-3]&.match?(/bar"␊?/) }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| assert lines[-1]&.match?(/bar"␊?/) } tmux.until { |lines| assert lines[-1]&.match?(/bar"␊?/) }
@@ -3742,7 +3789,7 @@ unset <%= UNSETS.join(' ') %>
unset $(env | sed -n /^_fzf_orig/s/=.*//p) unset $(env | sed -n /^_fzf_orig/s/=.*//p)
unset $(declare -F | sed -n "/_fzf/s/.*-f //p") unset $(declare -F | sed -n "/_fzf/s/.*-f //p")
export FZF_DEFAULT_OPTS=--no-scrollbar export FZF_DEFAULT_OPTS="--no-scrollbar --pointer '>' --marker '>'"
# Setup fzf # Setup fzf
# --------- # ---------