Repository: junegunn/fzf (language: go)

Issue:
Breaking Change in Keybinding Behavior due to Commit 6b4358f
### Checklist

- [X] I have read through the manual page (`man fzf`)
- [X] I have searched through the existing issues
- [X] For bug reports, I have checked if the bug is reproducible in the latest version of fzf

### Output of `fzf --version`

0.52.1 (7aa88aa1)

### OS

- [ ] Linux
- [X] macOS
- [ ] Windows
- [ ] Etc.

### Shell

- [ ] bash
- [X] zsh
- [ ] fish

### Problem / Steps to reproduce

### Issue

When executing the command below and pressing <kbd>⌃ Control</kbd> + <kbd>Y</kbd>, I observe a different result since commit 6b4358f (Ref: #3810).


```sh
# Current situation with 'devel' branch
seq 10 | fzf --bind 'ctrl-y:toggle+down' --expect 'ctrl-y' --bind 'load:pos:4'
# ctrl-y
# 3
```

```sh
# Old behavior prior to 6b4358f
seq 10 | fzf --bind 'ctrl-y:toggle+down' --expect 'ctrl-y' --bind 'load:pos:4'
# ctrl-y
# 4
```

This seems like a **breaking change**. If a user has set some custom configuration in their `FZF_DEFAULT_OPTS` and runs a script where a user uses this keybind with `--expect`, it will cause unintended issues.

---

#### Background Info

In my situation, I had a list of PRs and set `ctrl-y` in my `FZF_DEFAULT_OPTS` to perform a toggle-down operation. In the script, the `ctrl-y` key was configured with `--expect` to checkout the selected PR. This has led to the wrong PR being checked out every time.

Unsetting `FZF_DEFAULT_OPTS` in the script serves as a _solution_, however, it results in the loss of numerous custom configurations set by the user. What is the best course of action here ?

Reloading on windows seems broken
### Checklist

- [X] I have read through the manual page (`man fzf`)
- [X] I have searched through the existing issues
- [X] For bug reports, I have checked if the bug is reproducible in the latest version of fzf

### Output of `fzf --version`

0.52.1 (6432f00)

### OS

- [ ] Linux
- [ ] macOS
- [X] Windows
- [ ] Etc.

### Shell

- [ ] bash
- [ ] zsh
- [ ] fish

### Problem / Steps to reproduce

When running the following command on a large codebase on windows:
```
fzf --ansi --disabled --bind="change:reload:rg --line-number --column --color=always {q} 2>&1"
```
Changes in input do not trigger a reload. For example entring `main` the search will stay stuck on `m` this only happens on windows though testing the same command on Linux yields the expected results.

I tried with setting `$env:SHELL=powershell` and forcing `$env:SHELL=cmd` and I still have the issue. Also tried using `ctrl-r` event rather than `change` and I have the same issue. I'm wondering if killing the process on windows is somehow broken or not fulling implemented (saw a FIXME in the code regarding process groups but seems to be related to previews)

Rg version:
ripgrep 14.1.0 (rev e50df40a19)

Also tried with older versions of fzf going back to 0.41 without luck.



Discussion / hints:

Fix patch:
diff --git a/ADVANCED.md b/ADVANCED.md
index 2288f3debe5..187b6f77c02 100644
--- a/ADVANCED.md
+++ b/ADVANCED.md
@@ -1,18 +1,17 @@
 Advanced fzf examples
 ======================
 
-* *Last update: 2024/01/20*
-* *Requires fzf 0.46.0 or above*
+* *Last update: 2024/06/06*
+* *Requires fzf 0.53.0 or later*
 
 ---
 
 <!-- vim-markdown-toc GFM -->
 
 * [Introduction](#introduction)
-* [Screen Layout](#screen-layout)
+* [Display modes](#display-modes)
     * [`--height`](#--height)
-    * [`fzf-tmux`](#fzf-tmux)
-        * [Popup window support](#popup-window-support)
+    * [`--tmux`](#--tmux)
 * [Dynamic reloading of the list](#dynamic-reloading-of-the-list)
     * [Updating the list of processes by pressing CTRL-R](#updating-the-list-of-processes-by-pressing-ctrl-r)
     * [Toggling between data sources](#toggling-between-data-sources)
@@ -63,7 +62,7 @@ learn its wide variety of features.
 This document will guide you through some examples that will familiarize you
 with the advanced features of fzf.
 
-Screen Layout
+Display modes
 -------------
 
 ### `--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"
 ```
 
-### `fzf-tmux`
+### `--tmux`
 
-Before fzf had `--height` option, we would open fzf in a tmux split pane not
-to take up the whole screen. This is done using `fzf-tmux` script.
+(Requires tmux 3.3 or later)
+
+If you're using tmux, you can open fzf in a tmux popup using `--tmux` option.
 
 ```sh
-# Open fzf on a tmux split pane below the current pane.
-# Takes the same set of options.
-fzf-tmux --layout=reverse
+# Open fzf in a tmux popup at the center of the screen with 70% width and height
+fzf --tmux 70%
 ```
 
-![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
-`--height` option. But the advantage of it is that it's more flexible.
-(See `man fzf-tmux` for available options.)
+`--tmux` option is silently ignored if you're not on tmux. So if you're trying
+to avoid opening fzf in fullscreen, try specifying both `--height` and `--tmux`.
 
 ```sh
-# On the right (50%)
-fzf-tmux -r
-
-# On the left (30%)
-fzf-tmux -l30%
-
-# Above the cursor
-fzf-tmux -u30%
+# --tmux is specified later so it takes precedence over --height when on tmux.
+# If you're not on tmux, --tmux is ignored and --height is used instead.
+fzf  --height 70% --tmux 70%
 ```
 
-![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
+```sh
+# 100% width and 60% height
+fzf --tmux 100%,60% --border horizontal
+```
 
-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.
+![image](https://github.com/junegunn/fzf/assets/700826/f80d3514-d69f-42f2-a8de-a392a562bfcf)
 
 ```sh
-# Open tmux in a tmux popup window (default size: 50% of the screen)
-fzf-tmux -p
+# On the right (50% width)
+fzf --tmux right
+```
 
-# 80% width, 60% height
-fzf-tmux -p 80%,60%
+![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://user-images.githubusercontent.com/700826/113380106-4a9bfd80-93b6-11eb-8cee-aeb1c4ce1a1f.png)
+![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
 > window layout.
 >
@@ -536,8 +534,8 @@ pods() {
     --bind 'start:reload:$command' \
     --bind 'ctrl-r:reload:$command' \
     --bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
-    --bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \
-    --bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \
+    --bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash' \
+    --bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2})' \
     --preview-window up:follow \
     --preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
 }
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8a094afe7a7..60c1d877f39 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,76 @@
 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
 ------
 - Fixed a critical bug in the Windows version
diff --git a/Makefile b/Makefile
index f4abe6e6da5..f14c72c3a28 100644
--- a/Makefile
+++ b/Makefile
@@ -4,7 +4,7 @@ GOOS           ?= $(shell $(GO) env GOOS)
 
 MAKEFILE       := $(realpath $(lastword $(MAKEFILE_LIST)))
 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
 VERSION        := $(FZF_VERSION)
diff --git a/README-VIM.md b/README-VIM.md
index 6751d25a21c..e727dd1e5e1 100644
--- a/README-VIM.md
+++ b/README-VIM.md
@@ -294,7 +294,7 @@ The following table summarizes the available options.
 | `options`                  | string/list   | Options to fzf                                                        |
 | `dir`                      | string        | Working directory                                                     |
 | `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)  | 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
-or above) by putting fzf-tmux options in `tmux` key.
+or above) by putting `--tmux` option value in `tmux` key.
 
 ```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')
-  let g:fzf_layout = { 'tmux': '-p90%,60%' }
+  let g:fzf_layout = { 'tmux': '90%,70%' }
 else
   let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
 endif
diff --git a/README.md b/README.md
index fe532935988..de41fe6711b 100644
--- a/README.md
+++ b/README.md
@@ -23,19 +23,19 @@ fzf is a general-purpose command-line fuzzy finder.
 
 <img src="https://raw.githubusercontent.com/junegunn/i/master/fzf-preview.png" width=640>
 
-It's an interactive Unix filter for command-line that can be used with any
-list; files, command history, processes, hostnames, bookmarks, git commits,
-etc.
+It's an interactive filter program for any kind of list; files, command
+history, processes, hostnames, bookmarks, git commits, etc. It implements
+a "fuzzy" matching algorithm, so you can quickly type in patterns with omitted
+characters and still get the results you want.
 
 Pros
 ----
 
 - Portable, no dependencies
 - Blazingly fast
-- The most comprehensive feature set
-- Flexible layout
+- Extremely versatile
 - Batteries included
-    - Vim/Neovim plugin, key bindings, and fuzzy auto-completion
+    - bash/zsh/fish integration, tmux integration, Vim/Neovim plugin
 
 Sponsors ❤️
 -----------
@@ -53,22 +53,24 @@ Table of Contents
 
 * [Installation](#installation)
     * [Using Homebrew](#using-homebrew)
+    * [Linux packages](#linux-packages)
+    * [Windows packages](#windows-packages)
     * [Using git](#using-git)
-    * [Using Linux package managers](#using-linux-package-managers)
-    * [Windows](#windows)
+    * [Binary releases](#binary-releases)
     * [Setting up shell integration](#setting-up-shell-integration)
-    * [As Vim plugin](#as-vim-plugin)
+    * [Vim/Neovim plugin](#vimneovim-plugin)
 * [Upgrading fzf](#upgrading-fzf)
 * [Building fzf](#building-fzf)
 * [Usage](#usage)
     * [Using the finder](#using-the-finder)
-    * [Layout](#layout)
+    * [Display modes](#display-modes)
+        * [`--height` mode](#--height-mode)
+        * [`--tmux` mode](#--tmux-mode)
     * [Search syntax](#search-syntax)
     * [Environment variables](#environment-variables)
     * [Options](#options)
     * [Demo](#demo)
 * [Examples](#examples)
-* [`fzf-tmux` script](#fzf-tmux-script)
 * [Key bindings for command-line](#key-bindings-for-command-line)
 * [Fuzzy completion for bash and zsh](#fuzzy-completion-for-bash-and-zsh)
     * [Files and directories](#files-and-directories)
@@ -101,28 +103,12 @@ Table of Contents
 Installation
 ------------
 
-fzf project consists of the following components:
-
-- `fzf` executable
-- `fzf-tmux` script for launching fzf in a tmux pane
-- Shell integration
-    - Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish)
-    - Fuzzy completion (bash, zsh)
-- Vim/Neovim plugin
-
-You can [download fzf executable][bin] alone if you don't need the extra
-stuff.
-
-[bin]: https://github.com/junegunn/fzf/releases
-
 ### Using Homebrew
 
-You can use [Homebrew](https://brew.sh/) (on macOS or Linux)
-to install fzf.
+You can use [Homebrew](https://brew.sh/) (on macOS or Linux) to install fzf.
 
 ```sh
 brew install fzf
-  # To build fzf from the latest source: brew install --HEAD fzf
 ```
 
 > [!IMPORTANT]
@@ -133,20 +119,7 @@ fzf is also available [via MacPorts][portfile]: `sudo port install fzf`
 
 [portfile]: https://github.com/macports/macports-ports/blob/master/sysutils/fzf/Portfile
 
-### Using git
-
-Alternatively, you can "git clone" this repository to any directory and run
-[install](https://github.com/junegunn/fzf/blob/master/install) script.
-
-```sh
-git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
-~/.fzf/install
-```
-
-The install script will add lines to your shell configuration file to modify
-`$PATH` and set up shell integration.
-
-### Using Linux package managers
+### Linux packages
 
 | Package Manager | Linux Distribution      | Command                            |
 | --------------- | ----------------------- | ---------------------------------- |
@@ -170,10 +143,10 @@ The install script will add lines to your shell configuration file to modify
 
 [![Packaging status](https://repology.org/badge/vertical-allrepos/fzf.svg)](https://repology.org/project/fzf/versions)
 
-### Windows
+### Windows packages
 
-Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also
-available via [Chocolatey][choco], [Scoop][scoop], [Winget][winget], and [MSYS2][msys2]:
+On Windows, fzf is available via [Chocolatey][choco], [Scoop][scoop],
+[Winget][winget], and [MSYS2][msys2]:
 
 | Package manager | Command                               |
 | --------------- | ------------------------------------- |
@@ -187,10 +160,24 @@ available via [Chocolatey][choco], [Scoop][scoop], [Winget][winget], and [MSYS2]
 [winget]: https://github.com/microsoft/winget-pkgs/tree/master/manifests/j/junegunn/fzf
 [msys2]: https://packages.msys2.org/base/mingw-w64-fzf
 
-Known issues and limitations on Windows can be found on [the wiki
-page][windows-wiki].
+### Using git
 
-[windows-wiki]: https://github.com/junegunn/fzf/wiki/Windows
+Alternatively, you can "git clone" this repository to any directory and run
+[install](https://github.com/junegunn/fzf/blob/master/install) script.
+
+```sh
+git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
+~/.fzf/install
+```
+
+The install script will add lines to your shell configuration file to modify
+`$PATH` and set up shell integration.
+
+### Binary releases
+
+You can download the official fzf binaries from the releases page.
+
+* https://github.com/junegunn/fzf/releases
 
 ### Setting up shell integration
 
@@ -231,20 +218,26 @@ Add the following line to your shell configuration file.
 >
 > Setting the variables after sourcing the script will have no effect.
 
-### As Vim plugin
+### Vim/Neovim plugin
 
-If you use
-[vim-plug](https://github.com/junegunn/vim-plug), add this line to your Vim
-configuration file:
+If you use [vim-plug](https://github.com/junegunn/vim-plug), add this to
+your Vim configuration file:
 
 ```vim
 Plug 'junegunn/fzf', { 'do': { -> fzf#install() } }
+Plug 'junegunn/fzf.vim'
 ```
 
-`fzf#install()` makes sure that you have the latest binary, but it's optional,
-so you can omit it if you use a plugin manager that doesn't support hooks.
+* `junegunn/fzf` provides the basic library functions
+    * `fzf#install()` makes sure that you have the latest binary
+* `junegunn/fzf.vim` is [a separate project](https://github.com/junegunn/fzf.vim)
+  that provides a variety of useful commands
 
-For more installation options, see [README-VIM.md](README-VIM.md).
+To learn more about the Vim integration, see [README-VIM.md](README-VIM.md).
+
+> [!TIP]
+> If you use Neovim and prefer Lua-based plugins, check out
+> [fzf-lua](https://github.com/ibhagwan/fzf-lua).
 
 Upgrading fzf
 -------------
@@ -312,29 +305,71 @@ vim $(fzf)
 - Mouse: scroll, click, double-click; shift-click and shift-scroll on
   multi-select mode
 
-### Layout
+### Display modes
+
+fzf by default runs in fullscreen mode, but there are other display modes.
+
+#### `--height` mode
 
-fzf by default starts in fullscreen mode, but you can make it start below the
-cursor with `--height` option.
+With `--height HEIGHT[%]`, fzf will start below the cursor with the given height.
 
 ```sh
-vim $(fzf --height 40%)
+fzf --height 40%
+```
+
+`reverse` layout and `--border` goes well with this option.
+
+```sh
+fzf --height 40% --layout reverse --border
 ```
 
-Also, check out `--reverse` and `--layout` options if you prefer
-"top-down" layout instead of the default "bottom-up" layout.
+By prepending `~` to the height, you're setting the maximum height.
 
 ```sh
-vim $(fzf --height 40% --reverse)
+# Will take as few lines as possible to display the list
+seq 3 | fzf --height ~100%
+seq 3000 | fzf --height ~100%
 ```
 
-You can add these options to `$FZF_DEFAULT_OPTS` so that they're applied by
-default. For example,
+Height value can be a negative number.
 
 ```sh
-export FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border'
+# Screen height - 3
+fzf --height -3
 ```
 
+#### `--tmux` mode
+
+With `--tmux` option, fzf will start in a tmux popup.
+
+```sh
+# --tmux [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
+
+fzf --tmux center         # Center, 50% width and height
+fzf --tmux 80%            # Center, 80% width and height
+fzf --tmux 100%,50%       # Center, 100% width and 50% height
+fzf --tmux left,40%       # Left, 40% width
+fzf --tmux left,40%,90%   # Left, 40% width, 90% height
+fzf --tmux top,40%        # Top, 40% height
+fzf --tmux bottom,80%,40% # Bottom, 80% height, 40% height
+```
+
+`--tmux` is silently ignored when you're not on tmux.
+
+> [!NOTE]
+> If you're stuck with an old version of tmux that doesn't support popup,
+> or if you want to open fzf in a regular tmux pane, check out
+> [fzf-tmux](bin/fzf-tmux) script.
+
+> [!TIP]
+> You can add these options to `$FZF_DEFAULT_OPTS` so that they're applied by
+> default. For example,
+>
+> ```sh
+> # Open in tmux popup if on tmux, otherwise use --height mode
+> export FZF_DEFAULT_OPTS='--tmux bottom,40% --height 40% --layout reverse --border'
+> ```
+
 ### Search syntax
 
 Unless otherwise specified, fzf starts in "extended-search mode" where you can
@@ -406,39 +441,11 @@ Examples
       and are not thoroughly tested*
 * [Advanced fzf examples](https://github.com/junegunn/fzf/blob/master/ADVANCED.md)
 
-`fzf-tmux` script
------------------
-
-[fzf-tmux](bin/fzf-tmux) is a bash script that opens fzf in a tmux pane.
-
-```sh
-# usage: fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS]
-
-# See available options
-fzf-tmux --help
-
-# select git branches in horizontal split below (15 lines)
-git branch | fzf-tmux -d 15
-
-# select multiple words in vertical split on the left (20% of screen width)
-cat /usr/share/dict/words | fzf-tmux -l 20% --multi --reverse
-```
-
-It will still work even when you're not on tmux, silently ignoring `-[pudlr]`
-options, so you can invariably use `fzf-tmux` in your scripts.
-
-Alternatively, you can use `--height HEIGHT[%]` option not to start fzf in
-fullscreen mode.
-
-```sh
-fzf --height 40%
-```
-
 Key bindings for command-line
 -----------------------------
 
-The install script will setup the following key bindings for bash, zsh, and
-fish.
+By [setting up shell integration](#setting-up-shell-integration), you can use
+the following key bindings in bash, zsh, and fish.
 
 - `CTRL-T` - Paste the selected files and directories onto the command-line
     - The list is generated using `--walker file,dir,follow,hidden` option
@@ -482,9 +489,9 @@ fish.
     - Can be disabled by setting `FZF_ALT_C_COMMAND` to an empty string when
       sourcing the script
 
-If you're on a tmux session, you can start fzf in a tmux split-pane or in
-a tmux popup window by setting `FZF_TMUX_OPTS` (e.g. `export FZF_TMUX_OPTS='-p80%,60%'`).
-See `fzf-tmux --help` for available options.
+Display modes for these bindings can be separately configured via
+`FZF_{CTRL_T,CTRL_R,ALT_C}_OPTS` or globally via `FZF_DEFAULT_OPTS`.
+(e.g. `FZF_CTRL_R_OPTS='--tmux bottom,60% --height 60% --border top'`)
 
 More tips can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/Configuring-shell-key-bindings).
 
diff --git a/bin/fzf-tmux b/bin/fzf-tmux
index e66dcda5bab..713697370da 100755
--- a/bin/fzf-tmux
+++ b/bin/fzf-tmux
@@ -132,8 +132,10 @@ if [[ -z "$TMUX" ]]; then
   exit $?
 fi
 
-# --height option is not allowed. CTRL-Z is also disabled.
-args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore")
+# * --height option is not allowed
+# * 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
 if [[ ! "$opt" =~ "-E" ]] && tmux list-panes -F '#F' | grep -q Z; then
diff --git a/doc/fzf.txt b/doc/fzf.txt
index cf8c6af3627..f946184f3c3 100644
--- a/doc/fzf.txt
+++ b/doc/fzf.txt
@@ -311,7 +311,7 @@ The following table summarizes the available options.
   `options`                   | string/list   | Options to fzf
   `dir`                       | string        | Working directory
   `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)  | 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 } }
 <
 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')
-      let g:fzf_layout = { 'tmux': '-p90%,60%' }
+      let g:fzf_layout = { 'tmux': '90%,70%' }
     else
       let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
     endif
diff --git a/main.go b/main.go
index 7c76bef01e3..84b8be13cf6 100644
--- a/main.go
+++ b/main.go
@@ -4,6 +4,7 @@ import (
 	_ "embed"
 	"fmt"
 	"os"
+	"os/exec"
 	"strings"
 
 	fzf "github.com/junegunn/fzf/src"
@@ -28,6 +29,9 @@ var zshCompletion []byte
 //go:embed shell/key-bindings.fish
 var fishKeyBindings []byte
 
+//go:embed man/man1/fzf.1
+var manPage []byte
+
 func printScript(label string, content []byte) {
 	fmt.Println("### " + label + " ###")
 	fmt.Println(strings.TrimSpace(string(content)))
@@ -35,7 +39,7 @@ func printScript(label string, content []byte) {
 }
 
 func exit(code int, err error) {
-	if err != nil {
+	if code == fzf.ExitError {
 		fmt.Fprintln(os.Stderr, err.Error())
 	}
 	os.Exit(code)
@@ -76,6 +80,21 @@ func main() {
 		}
 		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)
 	exit(code, err)
diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1
index 11161e483a7..3ab36d44830 100644
--- a/man/man1/fzf-tmux.1
+++ b/man/man1/fzf-tmux.1
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
 ..
-.TH fzf-tmux 1 "May 2024" "fzf 0.52.1" "fzf-tmux - open fzf in tmux split pane"
+.TH fzf-tmux 1 "May 2024" "fzf 0.53.0" "fzf-tmux - open fzf in tmux split pane"
 
 .SH NAME
 fzf-tmux - open fzf in tmux split pane
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 0f99739d125..f48aff4f436 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
 ..
-.TH fzf 1 "May 2024" "fzf 0.52.1" "fzf - a command-line fuzzy finder"
+.TH fzf 1 "May 2024" "fzf 0.53.0" "fzf - a command-line fuzzy finder"
 
 .SH NAME
 fzf - a command-line fuzzy finder
@@ -30,7 +30,10 @@ fzf - a command-line fuzzy finder
 fzf [options]
 
 .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
 .SS Note
@@ -58,14 +61,32 @@ Do not normalize latin script letters for matching.
 .BI "--scheme=" SCHEME
 Choose scoring scheme tailored for different types of input.
 
-.br
-.BR default "  Generic scoring scheme designed to work well with any type of input"
-.br
-.BR path "     Scoring scheme well suited for file paths
-.br
-.BR history "  Scoring scheme well suited for command history or any input where chronological ordering is important
-         Sets \fB--tiebreak=index\fR as well.
-.br
+.RS
+.B default
+.RS
+Generic scoring scheme designed to work well with any type of input.
+.RE
+.RE
+
+.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
 .BI "--algo=" TYPE
 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
 .TP
 .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
 .BI "--disabled"
 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"
 Do not sort the result
 .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"
 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
@@ -162,13 +194,16 @@ the details.
 .B "--cycle"
 Enable cyclic scroll
 .TP
+.B "--no-multi-line"
+Disable multi-line display of items when using \fB--read0\fR
+.TP
 .B "--keep-right"
 Keep the right end of the line visible when it's too long. Effective only when
 the query string is empty.
 .TP
 .BI "--scroll-off=" "LINES"
 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
 .B "--no-hscroll"
 Disable horizontal scroll
@@ -204,17 +239,41 @@ height minus the given value.
   fzf --height=-1
 
 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
-with top/bottom margin and padding given in percent size. It is also not
-compatible with a negative height value.
+range according to the input size.
 
   # Will not take up 100% of the screen
   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
 .BI "--min-height=" "HEIGHT"
 Minimum height when \fB--height\fR is given in percent (default: 10).
 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
 .BI "--layout=" "LAYOUT"
 Choose the layout (default: default)
@@ -420,10 +479,14 @@ Do not display scrollbar. A synonym for \fB--scrollbar=''\fB
 Input prompt (default: '> ')
 .TP
 .BI "--pointer=" "STR"
-Pointer to the current line (default: '>')
+Pointer to the current line (default: '▌' or '>' depending on \fB--no-unicode\fR)
 .TP
 .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
 .BI "--header=" "STR"
 The given string will be printed as the sticky header. The lines are displayed
@@ -455,7 +518,7 @@ color mappings.
 
 .RS
 .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
     \fBlight   \fRColor scheme for light 256-color terminal
@@ -794,6 +857,14 @@ list.
 e.g.
      \fBfzf --expect=ctrl-v,ctrl-t,alt-s --expect=f1,f2,~,@\fR
 .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
 .B "--read0"
 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
      fzf --listen --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'
      \fR
+.SS Help
 .TP
 .B "--version"
 Display version information and exit
+.TP
+.B "--help"
+Show help message
+.TP
+.B "--man"
+Show man page
 
 .SS Directory traversal
 .TP
@@ -948,6 +1026,8 @@ you need to protect against DNS rebinding and privilege escalation attacks.
 .br
 .BR 2 "      Error"
 .br
+.BR 126 "    Permission denied error from \fBbecome\fR action"
+.br
 .BR 127 "    Invalid shell command for \fBbecome\fR action"
 .br
 .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-bottom\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 given string to the prompt)
     \fBrefresh-preview\fR
diff --git a/plugin/fzf.vim b/plugin/fzf.vim
index 3a8765ba0d4..0a8d570f96a 100644
--- a/plugin/fzf.vim
+++ b/plugin/fzf.vim
@@ -327,6 +327,9 @@ function! s:common_sink(action, lines) abort
     " the execution (e.g. `set autochdir` or `autocmd BufEnter * lcd ...`)
     let cwd = exists('w:fzf_pushd') ? w:fzf_pushd.dir : expand('%:p:h')
     for item in a:lines
+      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 item = join([cwd, item], cwd[len(cwd)-1] == sep ? '' : sep)
@@ -487,6 +490,8 @@ function! s:extract_option(opts, name)
   return opt
 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
 try
   let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
@@ -529,18 +534,19 @@ try
         \ executable('tput') && filereadable('/dev/tty')
   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 use_term = has_nvim_term ||
-    \ has_vim8_term && !has('win32unix') && (has('gui_running') || s:is_win || s:present(dict, 'down', 'up', 'left', 'right', 'window'))
+  let use_term = has_nvim_term || has_vim8_term
+    \ && !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()
   if prefer_tmux && use_tmux
     let use_height = 0
     let use_term = 0
   endif
   if use_term
-    let optstr .= ' --no-height'
+    let optstr .= ' --no-height --no-tmux'
   elseif use_height
     let height = s:calc_size(&lines, dict.down, dict)
-    let optstr .= ' --height='.height
+    let optstr .= ' --no-tmux --height='.height
   endif
   " 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])
@@ -573,19 +579,21 @@ function! s:fzf_tmux(dict)
   if empty(size)
     for o in ['up', 'down', 'left', 'right']
       if s:present(a:dict, o)
-        let spec = 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
+        let size = o . ',' . a:dict[o]
         break
       endif
     endfor
   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
 
 function! s:splittable(dict)
@@ -708,10 +716,10 @@ function! s:execute(dict, command, use_height, temps) abort
       call jobstart(cmd, fzf)
       return []
     endif
-  elseif has('win32unix') && $TERM !=# 'cygwin'
+  elseif s:need_cmd_window
     let shellscript = s:fzf_tempname()
     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
   endif
   if a:use_height
diff --git a/shell/completion.bash b/shell/completion.bash
index 9cd5b217a7e..c63ef33b1db 100644
--- a/shell/completion.bash
+++ b/shell/completion.bash
@@ -101,75 +101,83 @@ _fzf_opts_completion() {
   cur="${COMP_WORDS[COMP_CWORD]}"
   prev="${COMP_WORDS[COMP_CWORD-1]}"
   opts="
-    -h --help
-    -e --exact
+    +c --no-color
+    +i --no-ignore-case
+    +s --no-sort
     +x --no-extended
-    -q --query
-    -f --filter
-    --literal
-    --scheme
-    --expect
-    --disabled
-    --tiebreak
+    --ansi
+    --bash
     --bind
+    --border
+    --border-label
+    --border-label-pos
     --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
-    --no-bold
-    --layout
-    --reverse
     --cycle
-    --keep-right
-    --no-hscroll
-    --hscroll-off
-    --scroll-off
+    --disabled
+    --ellipsis
+    --expect
     --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
+    --fish
     --header
-    --header-lines
     --header-first
-    --ellipsis
-    --preview
-    --preview-window
+    --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
-    --border
-    --border-label
-    --border-label-pos
-    --preview-label
-    --preview-label-pos
+    --no-bold
+    --no-clear
+    --no-hscroll
+    --no-mouse
+    --no-scrollbar
+    --no-separator
     --no-unicode
-    --margin
     --padding
+    --pointer
+    --preview
+    --preview-label
+    --preview-label-pos
+    --preview-window
+    --print-query
+    --print0
+    --prompt
+    --read0
+    --reverse
+    --scheme
+    --scroll-off
+    --separator
+    --sync
     --tabstop
-    --listen
-    --no-clear
+    --tac
+    --tiebreak
+    --tmux
+    --track
     --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
diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash
index a789b19de9e..5218febe83a 100644
--- a/shell/key-bindings.bash
+++ b/shell/key-bindings.bash
@@ -57,15 +57,15 @@ __fzf_cd__() {
 if command -v perl > /dev/null; then
   __fzf_history__() {
     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=$(
       set +o pipefail
       builtin fc -lnr -2147483648 |
         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"
     ) || return
-    READLINE_LINE=${output#*$'\t'}
+    READLINE_LINE=$(command perl -pe 's/^\d*\t//' <<< "$output")
     if [[ -z "$READLINE_POINT" ]]; then
       echo "$READLINE_LINE"
     else
@@ -91,7 +91,7 @@ else # awk - fallback for POSIX systems
       set +o pipefail
       builtin fc -lnr -2147483648 2> /dev/null |   # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )*
         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"
     ) || return
     READLINE_LINE=${output#*$'\t'}
diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish
index 4c780ce842e..d1db09bc185 100644
--- a/shell/key-bindings.fish
+++ b/shell/key-bindings.fish
@@ -59,9 +59,6 @@ function fzf_key_bindings
   function fzf-history-widget -d "Show command history"
     test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
     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_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
       # before 2.4.0.
       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
-        and commandline -- $result
+        if type -P perl > /dev/null 2>&1
+          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
-        history | eval (__fzfcmd) -q '(commandline)' | read -l result
+        builtin history | eval (__fzfcmd) -q '(commandline)' | read -l result
         and commandline -- $result
       end
     end
@@ -93,7 +99,7 @@ function fzf_key_bindings
       eval (__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
 
       if [ -n "$result" ]
-        cd -- $result
+        builtin cd -- $result
 
         # Remove last token from commandline.
         commandline -t ""
diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh
index 56e3ae5c0f9..07f02bfdd32 100644
--- a/shell/key-bindings.zsh
+++ b/shell/key-bindings.zsh
@@ -108,14 +108,22 @@ fi
 fzf-history-widget() {
   local selected num
   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 }' |
-    FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m") \
-    FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
+  # Ensure the associative history array, which maps event numbers to the full
+  # history lines, is loaded, and that Perl is installed for multi-line output.
+  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=$?
   if [ -n "$selected" ]; then
-    num=$(awk '{print $1}' <<< "$selected")
-    if [[ "$num" =~ '^[1-9][0-9]*\*?$' ]]; then
-      zle vi-fetch-history -n ${num%\*}
+    if num=$(awk '{print $1; exit}' <<< "$selected" | grep -o '^[1-9][0-9]*'); then
+      zle vi-fetch-history -n $num
     else # selected is a custom query, not from history
       LBUFFER="$selected"
     fi
diff --git a/src/actiontype_string.go b/src/actiontype_string.go
index 6b07134bfcf..2a597ea5495 100644
--- a/src/actiontype_string.go
+++ b/src/actiontype_string.go
@@ -98,32 +98,33 @@ func _() {
 	_ = x[actPreviewHalfPageDown-87]
 	_ = x[actPrevHistory-88]
 	_ = x[actPrevSelected-89]
-	_ = x[actPut-90]
-	_ = x[actNextHistory-91]
-	_ = x[actNextSelected-92]
-	_ = x[actExecute-93]
-	_ = x[actExecuteSilent-94]
-	_ = x[actExecuteMulti-95]
-	_ = x[actSigStop-96]
-	_ = x[actFirst-97]
-	_ = x[actLast-98]
-	_ = x[actReload-99]
-	_ = x[actReloadSync-100]
-	_ = x[actDisableSearch-101]
-	_ = x[actEnableSearch-102]
-	_ = x[actSelect-103]
-	_ = x[actDeselect-104]
-	_ = x[actUnbind-105]
-	_ = x[actRebind-106]
-	_ = x[actBecome-107]
-	_ = x[actResponse-108]
-	_ = x[actShowHeader-109]
-	_ = x[actHideHeader-110]
+	_ = x[actPrint-90]
+	_ = x[actPut-91]
+	_ = x[actNextHistory-92]
+	_ = x[actNextSelected-93]
+	_ = x[actExecute-94]
+	_ = x[actExecuteSilent-95]
+	_ = x[actExecuteMulti-96]
+	_ = x[actSigStop-97]
+	_ = x[actFirst-98]
+	_ = x[actLast-99]
+	_ = x[actReload-100]
+	_ = x[actReloadSync-101]
+	_ = x[actDisableSearch-102]
+	_ = x[actEnableSearch-103]
+	_ = x[actSelect-104]
+	_ = x[actDeselect-105]
+	_ = x[actUnbind-106]
+	_ = x[actRebind-107]
+	_ = x[actBecome-108]
+	_ = x[actResponse-109]
+	_ = 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 {
 	if i < 0 || i >= actionType(len(_actionType_index)-1) {
diff --git a/src/chunklist.go b/src/chunklist.go
index cd635c25809..91177b17355 100644
--- a/src/chunklist.go
+++ b/src/chunklist.go
@@ -48,7 +48,12 @@ func CountItems(cs []*Chunk) int {
 	if len(cs) == 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
@@ -72,18 +77,53 @@ func (cl *ChunkList) Clear() {
 }
 
 // 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()
 
+	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))
 	copy(ret, cl.chunks)
 
-	// Duplicate the last chunk
+	// Duplicate the first and the last chunk
 	if cnt := len(ret); cnt > 0 {
+		if tail > 0 && cnt > 1 {
+			newChunk := *ret[0]
+			ret[0] = &newChunk
+		}
 		newChunk := *ret[cnt-1]
 		ret[cnt-1] = &newChunk
 	}
 
 	cl.mutex.Unlock()
-	return ret, CountItems(ret)
+	return ret, CountItems(ret), changed
 }
diff --git a/src/constants.go b/src/constants.go
index dd2e870e2e3..980e158318e 100644
--- a/src/constants.go
+++ b/src/constants.go
@@ -67,9 +67,9 @@ const (
 )
 
 const (
-	ExitCancel    = -1
 	ExitOk        = 0
 	ExitNoMatch   = 1
 	ExitError     = 2
+	ExitBecome    = 126
 	ExitInterrupt = 130
 )
diff --git a/src/core.go b/src/core.go
index 2a07b82ad22..f2563a0813a 100644
--- a/src/core.go
+++ b/src/core.go
@@ -2,6 +2,7 @@
 package fzf
 
 import (
+	"os"
 	"sync"
 	"time"
 
@@ -17,8 +18,38 @@ Matcher  -> EvtSearchFin      -> Terminal (update list)
 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
 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 {
 		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.Filter == nil, opts.Nth, opts.Delimiter, runes)
 	}
-	inputRevision := 0
-	snapshotRevision := 0
+	inputRevision := revision{}
+	snapshotRevision := revision{}
 	matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
 
 	// Filtering mode
@@ -181,7 +212,8 @@ func Run(opts *Options) (int, error) {
 			eventBox.Unwatch(EvtReadNew)
 			eventBox.WaitFor(EvtReadFin)
 
-			snapshot, _ := chunkList.Snapshot()
+			// NOTE: Streaming filter is inherently not compatible with --tail
+			snapshot, _, _ := chunkList.Snapshot(opts.Tail)
 			merger, _ := matcher.scan(MatchRequest{
 				chunks:  snapshot,
 				pattern: pattern})
@@ -252,7 +284,7 @@ func Run(opts *Options) (int, error) {
 		reading = true
 		chunkList.Clear()
 		itemIndex = 0
-		inputRevision++
+		inputRevision.bumpMajor()
 		header = make([]string, 0, opts.HeaderLines)
 		go reader.restart(command, environ)
 	}
@@ -297,10 +329,14 @@ func Run(opts *Options) (int, error) {
 						useSnapshot = false
 					}
 					if !useSnapshot {
-						if snapshotRevision != inputRevision {
+						if !snapshotRevision.compatible(inputRevision) {
 							query = []rune{}
 						}
-						snapshot, count = chunkList.Snapshot()
+						var changed bool
+						snapshot, count, changed = chunkList.Snapshot(opts.Tail)
+						if changed {
+							inputRevision.bumpMinor()
+						}
 						snapshotRevision = inputRevision
 					}
 					total = count
@@ -340,7 +376,10 @@ func Run(opts *Options) (int, error) {
 						break
 					}
 					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
 						// and the query string is changed at the same time i.e. command != nil && changed
 						if command == nil || newCount > 0 {
diff --git a/src/functions.go b/src/functions.go
index f16371a277c..59ca0bad713 100644
--- a/src/functions.go
+++ b/src/functions.go
@@ -6,8 +6,8 @@ import (
 	"unsafe"
 )
 
-func writeTemporaryFile(data []string, printSep string) string {
-	f, err := os.CreateTemp("", "fzf-preview-*")
+func WriteTemporaryFile(data []string, printSep string) string {
+	f, err := os.CreateTemp("", "fzf-temp-*")
 	if err != nil {
 		// Unable to create temporary file
 		// FIXME: Should we terminate the program?
diff --git a/src/item.go b/src/item.go
index cb778cb9cfd..c07d7fffc81 100644
--- a/src/item.go
+++ b/src/item.go
@@ -1,6 +1,8 @@
 package fzf
 
 import (
+	"math"
+
 	"github.com/junegunn/fzf/src/util"
 )
 
@@ -17,7 +19,7 @@ func (item *Item) Index() int32 {
 	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 {
 	return item.text.TrimLength()
diff --git a/src/matcher.go b/src/matcher.go
index 26426e4f126..869663fae9b 100644
--- a/src/matcher.go
+++ b/src/matcher.go
@@ -16,7 +16,7 @@ type MatchRequest struct {
 	pattern  *Pattern
 	final    bool
 	sort     bool
-	revision int
+	revision revision
 }
 
 // Matcher is responsible for performing search
@@ -30,7 +30,7 @@ type Matcher struct {
 	partitions     int
 	slab           []*util.Slab
 	mergerCache    map[string]*Merger
-	revision       int
+	revision       revision
 }
 
 const (
@@ -40,7 +40,7 @@ const (
 
 // NewMatcher returns a new Matcher
 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)
 	return &Matcher{
 		cache:          cache,
@@ -82,11 +82,13 @@ func (m *Matcher) Loop() {
 			break
 		}
 
+		cacheCleared := false
 		if request.sort != m.sort || request.revision != m.revision {
 			m.sort = request.sort
 			m.revision = request.revision
 			m.mergerCache = make(map[string]*Merger)
 			m.cache.Clear()
+			cacheCleared = true
 		}
 
 		// Restart search
@@ -95,20 +97,20 @@ func (m *Matcher) Loop() {
 		cancelled := false
 		count := CountItems(request.chunks)
 
-		foundCache := false
-		if count == prevCount {
-			// Look up mergerCache
-			if cached, found := m.mergerCache[patternString]; found {
-				foundCache = true
-				merger = cached
+		if !cacheCleared {
+			if count == prevCount {
+				// Look up mergerCache
+				if cached, found := m.mergerCache[patternString]; found {
+					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)
 		}
 
@@ -160,6 +162,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
 		return PassMerger(&request.chunks, m.tac, request.revision), false
 	}
 
+	minIndex := request.chunks[0].items[0].Index()
 	cancelled := util.NewAtomicBool(false)
 
 	slices := m.sliceChunks(request.chunks)
@@ -231,11 +234,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
 		partialResult := <-resultChan
 		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
-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)
 
 	var event util.EventType
diff --git a/src/merger.go b/src/merger.go
index 6a235043038..fee8a59ab8e 100644
--- a/src/merger.go
+++ b/src/merger.go
@@ -3,8 +3,8 @@ package fzf
 import "fmt"
 
 // EmptyMerger is a Merger with no data
-func EmptyMerger(revision int) *Merger {
-	return NewMerger(nil, [][]Result{}, false, false, revision)
+func EmptyMerger(revision revision) *Merger {
+	return NewMerger(nil, [][]Result{}, false, false, revision, 0)
 }
 
 // Merger holds a set of locally sorted lists of items and provides the view of
@@ -20,19 +20,25 @@ type Merger struct {
 	final    bool
 	count    int
 	pass     bool
-	revision int
+	revision revision
+	minIndex int32
 }
 
 // PassMerger returns a new Merger that simply returns the items in the
 // 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{
 		pattern:  nil,
 		chunks:   chunks,
 		tac:      tac,
 		count:    0,
 		pass:     true,
-		revision: revision}
+		revision: revision,
+		minIndex: minIndex}
 
 	for _, chunk := range *mg.chunks {
 		mg.count += chunk.count
@@ -41,7 +47,7 @@ func PassMerger(chunks *[]*Chunk, tac bool, revision int) *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{
 		pattern:  pattern,
 		lists:    lists,
@@ -52,7 +58,8 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revisi
 		tac:      tac,
 		final:    false,
 		count:    0,
-		revision: revision}
+		revision: revision,
+		minIndex: minIndex}
 
 	for _, list := range mg.lists {
 		mg.count += len(list)
@@ -61,7 +68,7 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revisi
 }
 
 // Revision returns revision number
-func (mg *Merger) Revision() int {
+func (mg *Merger) Revision() revision {
 	return mg.revision
 }
 
@@ -81,7 +88,7 @@ func (mg *Merger) First() Result {
 func (mg *Merger) FindIndex(itemIndex int32) int {
 	index := -1
 	if mg.pass {
-		index = int(itemIndex)
+		index = int(itemIndex - mg.minIndex)
 		if mg.tac {
 			index = mg.count - index - 1
 		}
@@ -102,6 +109,13 @@ func (mg *Merger) Get(idx int) Result {
 		if mg.tac {
 			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]
 		return Result{item: &chunk.items[idx%chunkSize]}
 	}
diff --git a/src/options.go b/src/options.go
index 0265693a3cf..35784e5cbf6 100644
--- a/src/options.go
+++ b/src/options.go
@@ -16,132 +16,150 @@ import (
 	"github.com/rivo/uniseg"
 )
 
-const Usage = `usage: fzf [options]
+const Usage = `fzf is an interactive filter program for any kind of list.
 
-  Search
-    -x, --extended         Extended-search mode
-                           (enabled by default; +x or --no-extended to disable)
-    -e, --exact            Enable Exact-match
-    -i, --ignore-case      Case-insensitive match (default: smart-case match)
-    +i, --no-ignore-case   Case-sensitive match
-    --scheme=SCHEME        Scoring scheme [default|path|history]
-    --literal              Do not normalize latin script letters before matching
-    -n, --nth=N[,..]       Comma-separated list of field index expressions
-                           for limiting search scope. Each can be a non-zero
-                           integer or a range expression ([BEGIN]..[END]).
-    --with-nth=N[,..]      Transform the presentation of each line using
-                           field index expressions
-    -d, --delimiter=STR    Field delimiter regex (default: AWK-style)
-    +s, --no-sort          Do not sort the result
-    --track                Track the current selection when the result is updated
-    --tac                  Reverse the order of the input
-    --disabled             Do not perform search
-    --tiebreak=CRI[,..]    Comma-separated list of sort criteria to apply
-                           when the scores are tied [length|chunk|begin|end|index]
-                           (default: length)
+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
+    -x, --extended          Extended-search mode
+                            (enabled by default; +x or --no-extended to disable)
+    -e, --exact             Enable Exact-match
+    -i, --ignore-case       Case-insensitive match (default: smart-case match)
+    +i, --no-ignore-case    Case-sensitive match
+    --scheme=SCHEME         Scoring scheme [default|path|history]
+    --literal               Do not normalize latin script letters before matching
+    -n, --nth=N[,..]        Comma-separated list of field index expressions
+                            for limiting search scope. Each can be a non-zero
+                            integer or a range expression ([BEGIN]..[END]).
+    --with-nth=N[,..]       Transform the presentation of each line using
+                            field index expressions
+    -d, --delimiter=STR     Field delimiter regex (default: AWK-style)
+    +s, --no-sort           Do not sort the result
+    --tail=NUM              Maximum number of items to keep in memory
+    --track                 Track the current selection when the result is updated
+    --tac                   Reverse the order of the input
+    --disabled              Do not perform search
+    --tiebreak=CRI[,..]     Comma-separated list of sort criteria to apply
+                            when the scores are tied [length|chunk|begin|end|index]
+                            (default: length)
   Interface
-    -m, --multi[=MAX]      Enable multi-select with tab/shift-tab
-    --no-mouse             Disable mouse
-    --bind=KEYBINDS        Custom key bindings. Refer to the man page.
-    --cycle                Enable cyclic scroll
-    --keep-right           Keep the right end of the line visible on overflow
-    --scroll-off=LINES     Number of screen lines to keep above or below when
-                           scrolling to the top or to the bottom (default: 0)
-    --no-hscroll           Disable horizontal scroll
-    --hscroll-off=COLS     Number of screen columns to keep to the right of the
-                           highlighted substring (default: 10)
-    --filepath-word        Make word-wise movements respect path separators
-    --jump-labels=CHARS    Label characters for jump mode
+    -m, --multi[=MAX]       Enable multi-select with tab/shift-tab
+    --no-mouse              Disable mouse
+    --bind=KEYBINDS         Custom key bindings. Refer to the man page.
+    --cycle                 Enable cyclic scroll
+    --no-multi-line         Disable multi-line display of items when using --read0
+    --keep-right            Keep the right end of the line visible on overflow
+    --scroll-off=LINES      Number of screen lines to keep above or below when
+                            scrolling to the top or to the bottom (default: 0)
+    --no-hscroll            Disable horizontal scroll
+    --hscroll-off=COLS      Number of screen columns to keep to the right of the
+                            highlighted substring (default: 10)
+    --filepath-word         Make word-wise movements respect path separators
+    --jump-labels=CHARS     Label characters for jump mode
 
   Layout
-    --height=[~]HEIGHT[%]  Display fzf window below the cursor with the given
-                           height instead of using fullscreen.
-                           A negative value is calculated as the terminal height
-                           minus the given value.
-                           If prefixed with '~', fzf will determine the height
-                           according to the input size.
-    --min-height=HEIGHT    Minimum height when --height is given in percent
-                           (default: 10)
-    --layout=LAYOUT        Choose layout: [default|reverse|reverse-list]
-    --border[=STYLE]       Draw border around the finder
-                           [rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
-                            top|bottom|left|right|none] (default: rounded)
-    --border-label=LABEL   Label to print on the border
-    --border-label-pos=COL Position of the border label
-                           [POSITIVE_INTEGER: columns from left|
-                            NEGATIVE_INTEGER: columns from right][:bottom]
-                           (default: 0 or center)
-    --margin=MARGIN        Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L)
-    --padding=PADDING      Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
-    --info=STYLE           Finder info style
-                           [default|right|hidden|inline[-right][:PREFIX]]
-    --separator=STR        String to form horizontal separator on info line
-    --no-separator         Hide info line separator
-    --scrollbar[=C1[C2]]   Scrollbar character(s) (each for main and preview window)
-    --no-scrollbar         Hide scrollbar
-    --prompt=STR           Input prompt (default: '> ')
-    --pointer=STR          Pointer to the current line (default: '>')
-    --marker=STR           Multi-select marker (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: '..')
+    --height=[~]HEIGHT[%]   Display fzf window below the cursor with the given
+                            height instead of using fullscreen.
+                            A negative value is calculated as the terminal height
+                            minus the given value.
+                            If prefixed with '~', fzf will determine the height
+                            according to the input size.
+    --min-height=HEIGHT     Minimum height when --height is given in percent
+                            (default: 10)
+    --tmux[=OPTS]           Start fzf in a tmux popup (requires tmux 3.3+)
+                            [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
+                            (default: center,50%)
+    --layout=LAYOUT         Choose layout: [default|reverse|reverse-list]
+    --border[=STYLE]        Draw border around the finder
+                            [rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
+                             top|bottom|left|right|none] (default: rounded)
+    --border-label=LABEL    Label to print on the border
+    --border-label-pos=COL  Position of the border label
+                            [POSITIVE_INTEGER: columns from left|
+                             NEGATIVE_INTEGER: columns from right][:bottom]
+                            (default: 0 or center)
+    --margin=MARGIN         Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L)
+    --padding=PADDING       Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
+    --info=STYLE            Finder info style
+                            [default|right|hidden|inline[-right][:PREFIX]]
+    --separator=STR         String to form horizontal separator on info line
+    --no-separator          Hide info line separator
+    --scrollbar[=C1[C2]]    Scrollbar character(s) (each for main and preview window)
+    --no-scrollbar          Hide scrollbar
+    --prompt=STR            Input prompt (default: '> ')
+    --pointer=STR           Pointer to the current line (default: '▌' or '>')
+    --marker=STR            Multi-select marker (default: '┃' or '>')
+    --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
-    --ansi                 Enable processing of ANSI color codes
-    --tabstop=SPACES       Number of spaces for a tab character (default: 8)
-    --color=COLSPEC        Base scheme (dark|light|16|bw) and/or custom colors
-    --highlight-line       Highlight the whole current line
-    --no-bold              Do not use bold text
+    --ansi                  Enable processing of ANSI color codes
+    --tabstop=SPACES        Number of spaces for a tab character (default: 8)
+    --color=COLSPEC         Base scheme (dark|light|16|bw) and/or custom colors
+    --highlight-line        Highlight the whole current line
+    --no-bold               Do not use bold text
 
   History
-    --history=FILE         History file
-    --history-size=N       Maximum number of history entries (default: 1000)
+    --history=FILE          History file
+    --history-size=N        Maximum number of history entries (default: 1000)
 
   Preview
-    --preview=COMMAND      Command to preview highlighted line ({})
-    --preview-window=OPT   Preview window layout (default: right:50%)
-                           [up|down|left|right][,SIZE[%]]
-                           [,[no]wrap][,[no]cycle][,[no]follow][,[no]hidden]
-                           [,border-BORDER_OPT]
-                           [,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
-                           [,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
+    --preview=COMMAND       Command to preview highlighted line ({})
+    --preview-window=OPT    Preview window layout (default: right:50%)
+                            [up|down|left|right][,SIZE[%]]
+                            [,[no]wrap][,[no]cycle][,[no]follow][,[no]hidden]
+                            [,border-BORDER_OPT]
+                            [,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
+                            [,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
     --preview-label=LABEL
-    --preview-label-pos=N  Same as --border-label and --border-label-pos,
-                           but for preview window
+    --preview-label-pos=N   Same as --border-label and --border-label-pos,
+                            but for preview window
 
   Scripting
-    -q, --query=STR        Start the finder with the given query
-    -1, --select-1         Automatically select the only match
-    -0, --exit-0           Exit immediately when there's no match
-    -f, --filter=STR       Filter mode. Do not start interactive finder.
-    --print-query          Print query as the first line
-    --expect=KEYS          Comma-separated list of keys to complete fzf
-    --read0                Read input delimited by ASCII NUL characters
-    --print0               Print output delimited by ASCII NUL characters
-    --sync                 Synchronous search for multi-staged filtering
-    --with-shell=STR       Shell command and flags to start child processes with
-    --listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /)
-                           (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)
-    --walker=OPTS          [file][,dir][,follow][,hidden] (default: file,follow,hidden)
-    --walker-root=DIR      Root directory from which to start walker (default: .)
-    --walker-skip=DIRS     Comma-separated list of directory names to skip
-                           (default: .git,node_modules)
+    -q, --query=STR         Start the finder with the given query
+    -1, --select-1          Automatically select the only match
+    -0, --exit-0            Exit immediately when there's no match
+    -f, --filter=STR        Filter mode. Do not start interactive finder.
+    --print-query           Print query as the first line
+    --expect=KEYS           Comma-separated list of keys to complete fzf
+    --read0                 Read input delimited by ASCII NUL characters
+    --print0                Print output delimited by ASCII NUL characters
+    --sync                  Synchronous search for multi-staged filtering
+    --with-shell=STR        Shell command and flags to start child processes with
+    --listen[=[ADDR:]PORT]  Start HTTP server to receive actions (POST /)
+                            (To allow remote process execution, use --listen-unsafe)
+
+  Directory traversal       (Only used when $FZF_DEFAULT_COMMAND is not set)
+    --walker=OPTS           [file][,dir][,follow][,hidden] (default: file,follow,hidden)
+    --walker-root=DIR       Root directory from which to start walker (default: .)
+    --walker-skip=DIRS      Comma-separated list of directory names to skip
+                            (default: .git,node_modules)
 
   Shell integration
-    --bash                 Print script to set up Bash shell integration
-    --zsh                  Print script to set up Zsh shell integration
-    --fish                 Print script to set up Fish shell integration
+    --bash                  Print script to set up Bash shell integration
+    --zsh                   Print script to set up Zsh 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
-    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_FILE  Location of the file to read default options from
-    FZF_API_KEY            X-API-Key header for HTTP server (--listen)
+    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_FILE   Location of the file to read default options from
+    FZF_API_KEY             X-API-Key header for HTTP server (--listen)
 
 `
 
@@ -173,6 +191,7 @@ type heightSpec struct {
 	percent bool
 	auto    bool
 	inverse bool
+	index   int
 }
 
 type sizeSpec struct {
@@ -180,6 +199,13 @@ type sizeSpec struct {
 	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 {
 	return [4]sizeSpec{}
 }
@@ -199,8 +225,16 @@ const (
 	posDown
 	posLeft
 	posRight
+	posCenter
 )
 
+type tmuxOptions struct {
+	width    sizeSpec
+	height   sizeSpec
+	position windowPosition
+	index    int
+}
+
 type layoutType int
 
 const (
@@ -248,6 +282,77 @@ func (o *previewOpts) Toggle() {
 	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 {
 	opts.column = 0
 	opts.bottom = false
@@ -296,9 +401,14 @@ type walkerOpts struct {
 type Options struct {
 	Input        chan string
 	Output       chan string
+	NoWinpty     bool
+	Tmux         *tmuxOptions
+	ForceTtyIn   bool
+	ProxyScript  string
 	Bash         bool
 	Zsh          bool
 	Fish         bool
+	Man          bool
 	Fuzzy        bool
 	FuzzyAlgo    algo.Algo
 	Scheme       string
@@ -312,6 +422,7 @@ type Options struct {
 	Sort         int
 	Track        trackOption
 	Tac          bool
+	Tail         int
 	Criteria     []criterion
 	Multi        int
 	Ansi         bool
@@ -323,6 +434,7 @@ type Options struct {
 	MinHeight    int
 	Layout       layoutType
 	Cycle        bool
+	MultiLine    bool
 	CursorLine   bool
 	KeepRight    bool
 	Hscroll      bool
@@ -334,8 +446,9 @@ type Options struct {
 	Separator    *string
 	JumpLabels   string
 	Prompt       string
-	Pointer      string
-	Marker       string
+	Pointer      *string
+	Marker       *string
+	MarkerMulti  [3]string
 	Query        string
 	Select1      bool
 	Exit0        bool
@@ -393,10 +506,18 @@ func defaultPreviewOpts(command string) previewOpts {
 }
 
 func defaultOptions() *Options {
+	var theme *tui.ColorTheme
+	if os.Getenv("NO_COLOR") != "" {
+		theme = tui.NoColorTheme()
+	} else {
+		theme = tui.EmptyTheme()
+	}
+
 	return &Options{
 		Bash:         false,
 		Zsh:          false,
 		Fish:         false,
+		Man:          false,
 		Fuzzy:        true,
 		FuzzyAlgo:    algo.FuzzyMatchV2,
 		Scheme:       "default",
@@ -414,23 +535,24 @@ func defaultOptions() *Options {
 		Multi:        0,
 		Ansi:         false,
 		Mouse:        true,
-		Theme:        tui.EmptyTheme(),
+		Theme:        theme,
 		Black:        false,
 		Bold:         true,
 		MinHeight:    10,
 		Layout:       layoutDefault,
 		Cycle:        false,
+		MultiLine:    true,
 		KeepRight:    false,
 		Hscroll:      true,
 		HscrollOff:   10,
-		ScrollOff:    0,
+		ScrollOff:    3,
 		FileWord:     false,
 		InfoStyle:    infoDefault,
 		Separator:    nil,
 		JumpLabels:   defaultJumpLabels,
 		Prompt:       "> ",
-		Pointer:      ">",
-		Marker:       ">",
+		Pointer:      nil,
+		Marker:       nil,
 		Query:        "",
 		Select1:      false,
 		Exit0:        false,
@@ -1077,7 +1199,7 @@ const (
 
 func init() {
 	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("[,:]+")
 	actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
 }
@@ -1449,6 +1571,8 @@ func isExecuteAction(str string) actionType {
 		return actExecuteSilent
 	case "execute-multi":
 		return actExecuteMulti
+	case "print":
+		return actPrint
 	case "put":
 		return actPut
 	case "transform":
@@ -1516,8 +1640,8 @@ func parseSize(str string, maxPercent float64, label string) (sizeSpec, error) {
 	return sizeSpec{val, percent}, nil
 }
 
-func parseHeight(str string) (heightSpec, error) {
-	heightSpec := heightSpec{}
+func parseHeight(str string, index int) (heightSpec, error) {
+	heightSpec := heightSpec{index: index}
 	if strings.HasPrefix(str, "~") {
 		heightSpec.auto = true
 		str = str[1:]
@@ -1734,7 +1858,38 @@ func parseMargin(opt string, margin string) ([4]sizeSpec, error) {
 	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 historyMax int
 	if opts.History == nil {
@@ -1768,10 +1923,16 @@ func parseOptions(opts *Options, allArgs []string) error {
 		opts.Fish = false
 		opts.Help = false
 		opts.Version = false
+		opts.Man = false
 	}
+	startIndex := *index
 	for i := 0; i < len(allArgs); i++ {
 		arg := allArgs[i]
+		index := i + startIndex
 		switch arg {
+		case "--man":
+			clearExitingOpts()
+			opts.Man = true
 		case "--bash":
 			clearExitingOpts()
 			opts.Bash = true
@@ -1787,6 +1948,29 @@ func parseOptions(opts *Options, allArgs []string) error {
 		case "--version":
 			clearExitingOpts()
 			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":
 			opts.Extended = true
 		case "-e", "--exact":
@@ -1914,6 +2098,15 @@ func parseOptions(opts *Options, allArgs []string) error {
 			opts.Tac = true
 		case "--no-tac":
 			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":
 			opts.Case = CaseIgnore
 		case "+i", "--no-ignore-case":
@@ -1962,6 +2155,10 @@ func parseOptions(opts *Options, allArgs []string) error {
 			opts.CursorLine = false
 		case "--no-cycle":
 			opts.Cycle = false
+		case "--multi-line":
+			opts.MultiLine = true
+		case "--no-multi-line":
+			opts.MultiLine = false
 		case "--keep-right":
 			opts.KeepRight = true
 		case "--no-keep-right":
@@ -2049,17 +2246,27 @@ func parseOptions(opts *Options, allArgs []string) error {
 				return err
 			}
 		case "--pointer":
-			str, err := nextString(allArgs, &i, "pointer sign string required")
+			str, err := nextString(allArgs, &i, "pointer sign required")
 			if err != nil {
 				return err
 			}
-			opts.Pointer = firstLine(str)
+			str = firstLine(str)
+			opts.Pointer = &str
 		case "--marker":
-			str, err := nextString(allArgs, &i, "selected sign string required")
+			str, err := nextString(allArgs, &i, "marker sign required")
 			if err != nil {
 				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":
 			opts.Sync = true
 		case "--no-sync", "--async":
@@ -2123,7 +2330,7 @@ func parseOptions(opts *Options, allArgs []string) error {
 			if err != nil {
 				return err
 			}
-			if opts.Height, err = parseHeight(str); err != nil {
+			if opts.Height, err = parseHeight(str, index); err != nil {
 				return err
 			}
 		case "--min-height":
@@ -2264,6 +2471,10 @@ func parseOptions(opts *Options, allArgs []string) error {
 				if opts.FuzzyAlgo, err = parseAlgo(value); err != nil {
 					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 {
 				opts.Scheme = strings.ToLower(value)
 			} 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 {
 				opts.Prompt = value
 			} 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 {
-				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 {
 				if opts.Nth, err = splitNth(value); err != nil {
 					return err
@@ -2309,7 +2526,7 @@ func parseOptions(opts *Options, allArgs []string) error {
 					return err
 				}
 			} 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
 				}
 			} 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 {
 				opts.JumpLabels = value
 				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 {
 				return errors.New("unknown option: " + arg)
 			}
 		}
 	}
+	*index += len(allArgs)
 
 	if opts.HeaderLines < 0 {
 		return errors.New("header lines must be a non-negative integer")
@@ -2471,6 +2696,47 @@ func validateSign(sign string, signOptName string) error {
 	return nil
 }
 
+func validateOptions(opts *Options) error {
+	if opts.Pointer != nil {
+		if err := validateSign(*opts.Pointer, "pointer"); err != nil {
+			return err
+		}
+	}
+
+	if opts.Marker != nil {
+		if err := validateSign(*opts.Marker, "marker"); err != nil {
+			return err
+		}
+	}
+
+	if opts.Scrollbar != nil {
+		runes := []rune(*opts.Scrollbar)
+		if len(runes) > 2 {
+			return errors.New("--scrollbar should be given one or two characters")
+		}
+		for _, r := range runes {
+			if uniseg.StringWidth(string(r)) != 1 {
+				return errors.New("scrollbar display width should be 1")
+			}
+		}
+	}
+
+	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 {
@@ -2478,27 +2744,46 @@ func postProcessOptions(opts *Options) error {
 		uniseg.EastAsianAmbiguousWidth = 2
 	}
 
-	if err := validateSign(opts.Pointer, "pointer"); err != nil {
-		return err
+	if opts.BorderShape == tui.BorderUndefined {
+		opts.BorderShape = tui.BorderNone
 	}
 
-	if err := validateSign(opts.Marker, "marker"); err != nil {
-		return err
+	if opts.Pointer == nil {
+		defaultPointer := "▌"
+		if !opts.Unicode {
+			defaultPointer = ">"
+		}
+		opts.Pointer = &defaultPointer
 	}
 
-	if !tui.IsLightRendererSupported() && opts.Height.size > 0 {
-		return errors.New("--height option is currently not supported on this platform")
+	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)
 	}
 
-	if opts.Scrollbar != nil {
-		runes := []rune(*opts.Scrollbar)
-		if len(runes) > 2 {
-			return errors.New("--scrollbar should be given one or two characters")
+	markerMultiLen := 1
+	if len(opts.MarkerMulti[0]) == 0 {
+		if opts.Unicode {
+			opts.MarkerMulti = [3]string{"╻", "┃", "╹"}
+		} else {
+			opts.MarkerMulti = [3]string{".", "|", "'"}
 		}
-		for _, r := range runes {
-			if uniseg.StringWidth(string(r)) != 1 {
-				return errors.New("scrollbar display width should be 1")
-			}
+	} 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] += " "
 		}
 	}
 
@@ -2548,19 +2833,6 @@ func postProcessOptions(opts *Options) error {
 		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 it contains the whole range
 	if !opts.Extended || len(opts.Nth) == 1 {
@@ -2589,12 +2861,22 @@ func postProcessOptions(opts *Options) error {
 		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)
 }
 
 // ParseOptions parses command-line options
 func ParseOptions(useDefaults bool, args []string) (*Options, error) {
 	opts := defaultOptions()
+	index := 0
 
 	if useDefaults {
 		// 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())
 			}
 			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())
 				}
 			}
@@ -2621,19 +2903,20 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
 			return nil, errors.New("$FZF_DEFAULT_OPTS: " + parseErr.Error())
 		}
 		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())
 			}
 		}
 	}
 
 	// 3. Options from command-line arguments
-	if err := parseOptions(opts, args); err != nil {
+	if err := parseOptions(&index, opts, args); err != nil {
 		return nil, err
 	}
 
-	if err := opts.initProfiling(); err != nil {
-		return nil, errors.New("failed to start pprof profiles: " + err.Error())
+	// 4. Final validation of merged options
+	if err := validateOptions(opts); err != nil {
+		return nil, err
 	}
 
 	return opts, nil
diff --git a/src/proxy.go b/src/proxy.go
new file mode 100644
index 00000000000..d53805b1ac9
--- /dev/null
+++ b/src/proxy.go
@@ -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
+}
diff --git a/src/proxy_unix.go b/src/proxy_unix.go
new file mode 100644
index 00000000000..189d0e5694b
--- /dev/null
+++ b/src/proxy_unix.go
@@ -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
+}
diff --git a/src/proxy_windows.go b/src/proxy_windows.go
new file mode 100644
index 00000000000..a957da8a15e
--- /dev/null
+++ b/src/proxy_windows.go
@@ -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
+}
diff --git a/src/reader.go b/src/reader.go
index 99a609e2bec..528f370b173 100644
--- a/src/reader.go
+++ b/src/reader.go
@@ -25,6 +25,7 @@ type Reader struct {
 	finChan  chan bool
 	mutex    sync.Mutex
 	exec     *exec.Cmd
+	execOut  io.ReadCloser
 	command  *string
 	killed   bool
 	wait     bool
@@ -32,7 +33,7 @@ type Reader struct {
 
 // NewReader returns new Reader object
 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() {
@@ -79,6 +80,7 @@ func (r *Reader) terminate() {
 	r.mutex.Lock()
 	r.killed = true
 	if r.exec != nil && r.exec.Process != nil {
+		r.execOut.Close()
 		util.KillCommand(r.exec)
 	} else {
 		os.Stdin.Close()
@@ -113,7 +115,7 @@ func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts,
 	var success bool
 	if inputChan != nil {
 		success = r.readChannel(inputChan)
-	} else if util.IsTty() {
+	} else if util.IsTty(os.Stdin) {
 		cmd := os.Getenv("FZF_DEFAULT_COMMAND")
 		if len(cmd) == 0 {
 			success = r.readFiles(root, opts, ignores)
@@ -263,16 +265,23 @@ func (r *Reader) readFromCommand(command string, environ []string) bool {
 	if environ != nil {
 		r.exec.Env = environ
 	}
-	out, err := r.exec.StdoutPipe()
+
+	var err error
+	r.execOut, err = r.exec.StdoutPipe()
 	if err != nil {
+		r.exec = nil
 		r.mutex.Unlock()
 		return false
 	}
+
 	err = r.exec.Start()
-	r.mutex.Unlock()
 	if err != nil {
+		r.exec = nil
+		r.mutex.Unlock()
 		return false
 	}
-	r.feed(out)
+
+	r.mutex.Unlock()
+	r.feed(r.execOut)
 	return r.exec.Wait() == nil
 }
diff --git a/src/result.go b/src/result.go
index b0428294ffc..b9e737dec42 100644
--- a/src/result.go
+++ b/src/result.go
@@ -15,6 +15,7 @@ type Offset [2]int32
 type colorOffset struct {
 	offset [2]int32
 	color  tui.ColorPair
+	match  bool
 }
 
 type Result struct {
@@ -109,7 +110,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
 	if len(itemColors) == 0 {
 		var offsets []colorOffset
 		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
 	}
@@ -193,12 +194,13 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
 					}
 				}
 				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 {
 				ansi := itemColors[curr-1]
 				colors = append(colors, colorOffset{
 					offset: [2]int32{int32(start), int32(idx)},
-					color:  ansiToColorPair(ansi, colBase)})
+					color:  ansiToColorPair(ansi, colBase),
+					match:  false})
 			}
 		}
 	}
diff --git a/src/terminal.go b/src/terminal.go
index 87178a77b5a..386e08504e3 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -51,6 +51,7 @@ var whiteSuffix *regexp.Regexp
 var offsetComponentRegex *regexp.Regexp
 var offsetTrimCharsRegex *regexp.Regexp
 var passThroughRegex *regexp.Regexp
+var ttyin *os.File
 
 const clearCode string = "\x1b[2J"
 
@@ -148,14 +149,26 @@ type eachLine struct {
 }
 
 type itemLine struct {
-	offset   int
-	current  bool
-	selected bool
-	label    string
-	queryLen int
-	width    int
-	bar      bool
-	result   Result
+	firstLine int
+	cy        int
+	current   bool
+	selected  bool
+	label     string
+	queryLen  int
+	width     int
+	hasBar    bool
+	result    Result
+	empty     bool
+	other     bool
+	minIndex  int32
+}
+
+func (t *Terminal) markEmptyLine(line int) {
+	t.prevLines[line] = itemLine{firstLine: line, empty: true}
+}
+
+func (t *Terminal) markOtherLine(line int) {
+	t.prevLines[line] = itemLine{firstLine: line, other: true}
 }
 
 type fitpad struct {
@@ -163,10 +176,17 @@ type fitpad struct {
 	pad int
 }
 
-var emptyLine = itemLine{}
-
 type labelPrinter func(tui.Window, int)
 
+type markerClass int
+
+const (
+	markerSingle markerClass = iota
+	markerTop
+	markerMiddle
+	markerBottom
+)
+
 type StatusItem struct {
 	Index int    `json:"index"`
 	Text  string `json:"text"`
@@ -208,6 +228,7 @@ type Terminal struct {
 	marker             string
 	markerLen          int
 	markerEmpty        string
+	markerMultiLine    [3]string
 	queryLen           [2]int
 	layout             layoutType
 	fullscreen         bool
@@ -224,6 +245,7 @@ type Terminal struct {
 	yanked             []rune
 	input              []rune
 	multi              int
+	multiLine          bool
 	sort               bool
 	toggleSort         bool
 	track              trackOption
@@ -232,6 +254,7 @@ type Terminal struct {
 	keymap             map[tui.Event][]*action
 	keymapOrg          map[tui.Event][]*action
 	pressed            string
+	printQueue         []string
 	printQuery         bool
 	history            *History
 	cycle              bool
@@ -279,7 +302,7 @@ type Terminal struct {
 	merger             *Merger
 	selected           map[int32]selectedItem
 	version            int64
-	revision           int
+	revision           revision
 	reqBox             *util.EventBox
 	initialPreviewOpts previewOpts
 	previewOpts        previewOpts
@@ -312,6 +335,7 @@ type Terminal struct {
 	forcePreview       bool
 	clickHeaderLine    int
 	clickHeaderColumn  int
+	proxyScript        string
 }
 
 type selectedItem struct {
@@ -351,6 +375,7 @@ const (
 	reqPreviewDisplay
 	reqPreviewRefresh
 	reqPreviewDelayed
+	reqBecome
 	reqQuit
 	reqFatal
 )
@@ -427,7 +452,7 @@ const (
 	actOffsetDown
 	actJump
 	actJumpAccept // XXX Deprecated in favor of jump:accept binding
-	actPrintQuery
+	actPrintQuery // XXX Deprecated (not very useful, just use --print-query)
 	actRefreshPreview
 	actReplaceQuery
 	actToggleSort
@@ -454,6 +479,7 @@ const (
 	actPreviewHalfPageDown
 	actPrevHistory
 	actPrevSelected
+	actPrint
 	actPut
 	actNextHistory
 	actNextSelected
@@ -677,11 +703,19 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
 	var renderer tui.Renderer
 	fullscreen := !opts.Height.auto && (opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100)
 	var err error
+	// Reuse ttyin if available to avoid having multiple file descriptors open
+	// when you run fzf multiple times in your Go program. Closing it is known to
+	// cause problems with 'become' action and invalid terminal state after exit.
+	if ttyin == nil {
+		if ttyin, err = tui.TtyIn(); err != nil {
+			return nil, err
+		}
+	}
 	if fullscreen {
 		if tui.HasFullscreenRenderer() {
 			renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse)
 		} else {
-			renderer, err = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit,
+			renderer, err = tui.NewLightRenderer(ttyin, opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit,
 				true, func(h int) int { return h })
 		}
 	} else {
@@ -697,7 +731,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
 			effectiveMinHeight += borderLines(opts.BorderShape)
 			return util.Min(termHeight, util.Max(evaluateHeight(opts, termHeight), effectiveMinHeight))
 		}
-		renderer, err = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, false, maxHeightFunc)
+		renderer, err = tui.NewLightRenderer(ttyin, opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, false, maxHeightFunc)
 	}
 	if err != nil {
 		return nil, err
@@ -728,6 +762,11 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
 		hscroll:            opts.Hscroll,
 		hscrollOff:         opts.HscrollOff,
 		scrollOff:          opts.ScrollOff,
+		pointer:            *opts.Pointer,
+		pointerLen:         uniseg.StringWidth(*opts.Pointer),
+		marker:             *opts.Marker,
+		markerLen:          uniseg.StringWidth(*opts.Marker),
+		markerMultiLine:    opts.MarkerMulti,
 		wordRubout:         wordRubout,
 		wordNext:           wordNext,
 		cx:                 len(input),
@@ -737,6 +776,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
 		yanked:             []rune{},
 		input:              input,
 		multi:              opts.Multi,
+		multiLine:          opts.ReadZero && opts.MultiLine,
 		sort:               opts.Sort > 0,
 		toggleSort:         opts.ToggleSort,
 		track:              opts.Track,
@@ -782,7 +822,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
 		jumpLabels:         opts.JumpLabels,
 		printer:            opts.Printer,
 		printsep:           opts.PrintSep,
-		merger:             EmptyMerger(0),
+		proxyScript:        opts.ProxyScript,
+		merger:             EmptyMerger(revision{}),
 		selected:           make(map[int32]selectedItem),
 		reqBox:             util.NewEventBox(),
 		initialPreviewOpts: opts.Preview,
@@ -802,14 +843,12 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
 		serverOutputChan:   make(chan string),
 		eventChan:          make(chan tui.Event, 6), // (load + result + zero|one) | (focus) | (resize) | (GetChar)
 		tui:                renderer,
-		ttyin:              tui.TtyIn(),
+		ttyin:              ttyin,
 		initFunc:           func() error { return renderer.Init() },
 		executing:          util.NewAtomicBool(false),
 		lastAction:         actStart,
 		lastFocus:          minItem.Index()}
 	t.prompt, t.promptLen = t.parsePrompt(opts.Prompt)
-	t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0)
-	t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0)
 	// Pre-calculated empty pointer and marker signs
 	t.pointerEmpty = strings.Repeat(" ", t.pointerLen)
 	t.markerEmpty = strings.Repeat(" ", t.markerLen)
@@ -1006,8 +1045,9 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) {
 		}
 	}
 	output := func() {
+		line := t.promptLine()
 		t.printHighlighted(
-			Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false)
+			Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false, line, line, true, nil, nil)
 	}
 	_, promptLen := t.processTabs([]rune(trimmed), 0)
 
@@ -1028,22 +1068,46 @@ func (t *Terminal) noSeparatorLine() bool {
 	return noSeparatorLine(t.infoStyle, t.separatorLen > 0)
 }
 
-func getScrollbar(total int, height int, offset int) (int, int) {
-	if total == 0 || total <= height {
+func getScrollbar(perLine int, total int, height int, offset int) (int, int) {
+	if total == 0 || total*perLine <= height {
 		return 0, 0
 	}
-	barLength := util.Max(1, height*height/total)
+	barLength := util.Max(1, height*height/(total*perLine))
 	var barStart int
 	if total == height {
 		barStart = 0
 	} else {
-		barStart = (height - barLength) * offset / (total - height)
+		barStart = util.Min(height-barLength, (height*perLine-barLength)*offset/(total*perLine-height))
 	}
 	return barLength, barStart
 }
 
+// Estimate the average number of lines per item. Instead of going through all
+// items, we only check a few items around the current cursor position.
+func (t *Terminal) avgNumLines() int {
+	if !t.multiLine {
+		return 1
+	}
+
+	maxItems := t.maxItems()
+	numLines := 0
+	count := 0
+	total := t.merger.Length()
+	offset := util.Max(0, util.Min(t.offset, total-maxItems-1))
+	for idx := 0; idx < maxItems && idx+offset < total; idx++ {
+		item := t.merger.Get(idx + offset)
+		lines, _ := item.item.text.NumLines(maxItems)
+		numLines += lines
+		count++
+	}
+	if count == 0 {
+		return 1
+	}
+	return numLines / count
+}
+
 func (t *Terminal) getScrollbar() (int, int) {
-	return getScrollbar(t.merger.Length(), t.maxItems(), t.offset)
+	return getScrollbar(t.avgNumLines(), t.merger.Length(), t.maxItems(), t.offset)
 }
 
 // Input returns current query string
@@ -1101,8 +1165,8 @@ func (t *Terminal) UpdateProgress(progress float32) {
 func (t *Terminal) UpdateList(merger *Merger, triggerResultEvent bool) {
 	t.mutex.Lock()
 	prevIndex := minItem.Index()
-	reset := t.revision != merger.Revision()
-	if !reset && t.track != trackDisabled {
+	newRevision := merger.Revision()
+	if t.revision.compatible(newRevision) && t.track != trackDisabled {
 		if t.merger.Length() > 0 {
 			prevIndex = t.currentIndex()
 		} else if merger.Length() > 0 {
@@ -1111,9 +1175,29 @@ func (t *Terminal) UpdateList(merger *Merger, triggerResultEvent bool) {
 	}
 	t.progress = 100
 	t.merger = merger
-	if reset {
-		t.selected = make(map[int32]selectedItem)
-		t.revision = merger.Revision()
+	if !t.revision.equals(newRevision) {
+		if !t.revision.compatible(newRevision) {
+			// Reloaded: clear selection
+			t.selected = make(map[int32]selectedItem)
+		} else {
+			// Trimmed by --tail: filter selection by index
+			filtered := make(map[int32]selectedItem)
+			minIndex := merger.minIndex
+			maxIndex := minIndex + int32(merger.Length())
+			for k, v := range t.selected {
+				var included bool
+				if maxIndex > minIndex {
+					included = k >= minIndex && k < maxIndex
+				} else { // int32 overflow [==>   <==]
+					included = k >= minIndex || k < maxIndex
+				}
+				if included {
+					filtered[k] = v
+				}
+			}
+			t.selected = filtered
+		}
+		t.revision = newRevision
 		t.version++
 	}
 	if t.triggerLoad {
@@ -1165,6 +1249,9 @@ func (t *Terminal) output() bool {
 	if len(t.expect) > 0 {
 		t.printer(t.pressed)
 	}
+	for _, s := range t.printQueue {
+		t.printer(s)
+	}
 	found := len(t.selected) > 0
 	if !found {
 		current := t.currentItem()
@@ -1619,7 +1706,6 @@ func (t *Terminal) placeCursor() {
 }
 
 func (t *Terminal) printPrompt() {
-	t.move(t.promptLine(), 0, true)
 	t.prompt()
 
 	before, after := t.updatePromptOffset()
@@ -1642,6 +1728,10 @@ func (t *Terminal) trimMessage(message string, maxWidth int) string {
 func (t *Terminal) printInfo() {
 	pos := 0
 	line := t.promptLine()
+	move := func(y int, x int, clear bool) {
+		t.move(y, x, clear)
+		t.markOtherLine(y)
+	}
 	printSpinner := func() {
 		if t.reading {
 			duration := int64(spinnerDuration)
@@ -1660,7 +1750,7 @@ func (t *Terminal) printInfo() {
 			str = string(trimmed)
 			width = maxWidth
 		}
-		t.move(line, pos, t.separatorLen == 0)
+		move(line, pos, t.separatorLen == 0)
 		if t.reading {
 			t.window.CPrint(tui.ColSpinner, str)
 		} else {
@@ -1669,7 +1759,6 @@ func (t *Terminal) printInfo() {
 		pos += width
 	}
 	printSeparator := func(fillLength int, pad bool) {
-		// --------_
 		if t.separatorLen > 0 {
 			t.separator(t.window, fillLength)
 			t.window.Print(" ")
@@ -1679,12 +1768,12 @@ func (t *Terminal) printInfo() {
 	}
 	switch t.infoStyle {
 	case infoDefault:
-		t.move(line+1, 0, t.separatorLen == 0)
+		move(line+1, 0, t.separatorLen == 0)
 		printSpinner()
 		t.window.Print(" ") // Margin
 		pos = 2
 	case infoRight:
-		t.move(line+1, 0, false)
+		move(line+1, 0, false)
 	case infoInlineRight:
 		pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
 	case infoInline:
@@ -1692,7 +1781,7 @@ func (t *Terminal) printInfo() {
 		printInfoPrefix()
 	case infoHidden:
 		if t.separatorLen > 0 {
-			t.move(line+1, 0, false)
+			move(line+1, 0, false)
 			printSeparator(t.window.Width()-1, false)
 		}
 		return
@@ -1752,7 +1841,7 @@ func (t *Terminal) printInfo() {
 
 	if t.infoStyle == infoInlineRight {
 		if len(t.infoPrefix) == 0 {
-			t.move(line, pos, false)
+			move(line, pos, false)
 			newPos := util.Max(pos, t.window.Width()-util.StringWidth(output)-3)
 			t.window.Print(strings.Repeat(" ", newPos-pos))
 			pos = newPos
@@ -1776,7 +1865,7 @@ func (t *Terminal) printInfo() {
 
 	if t.infoStyle == infoInlineRight {
 		if t.separatorLen > 0 {
-			t.move(line+1, 0, false)
+			move(line+1, 0, false)
 			printSeparator(t.window.Width()-1, false)
 		}
 		return
@@ -1826,10 +1915,9 @@ func (t *Terminal) printHeader() {
 			text:   util.ToChars([]byte(trimmed)),
 			colors: colors}
 
-		t.move(line, 0, true)
-		t.window.Print("  ")
 		t.printHighlighted(Result{item: item},
-			tui.ColHeader, tui.ColHeader, false, false)
+			tui.ColHeader, tui.ColHeader, false, false, line, line, true,
+			func(markerClass) { t.window.Print("  ") }, nil)
 	}
 }
 
@@ -1837,53 +1925,66 @@ func (t *Terminal) printList() {
 	t.constrain()
 	barLength, barStart := t.getScrollbar()
 
-	maxy := t.maxItems()
+	maxy := t.maxItems() - 1
 	count := t.merger.Length() - t.offset
-	for j := 0; j < maxy; j++ {
-		i := j
-		if t.layout == layoutDefault {
-			i = maxy - 1 - j
-		}
-		line := i + 2 + t.visibleHeaderLines()
-		if t.noSeparatorLine() {
-			line--
-		}
-		if i < count {
-			t.printItem(t.merger.Get(i+t.offset), line, i, i == t.cy-t.offset, i >= barStart && i < barStart+barLength)
-		} else if t.prevLines[i] != emptyLine || t.prevLines[i].offset != line {
-			t.prevLines[i] = emptyLine
+
+	// Start line
+	startLine := 2 + t.visibleHeaderLines()
+	if t.noSeparatorLine() {
+		startLine--
+	}
+	maxy += startLine
+
+	barRange := [2]int{startLine + barStart, startLine + barStart + barLength}
+	for line, itemCount := startLine, 0; line <= maxy; line, itemCount = line+1, itemCount+1 {
+		if itemCount < count {
+			item := t.merger.Get(itemCount + t.offset)
+			line = t.printItem(item, line, maxy, itemCount, itemCount == t.cy-t.offset, barRange)
+		} else if !t.prevLines[line].empty {
 			t.move(line, 0, true)
+			t.markEmptyLine(line)
+			// If the screen is not filled with the list in non-multi-line mode,
+			// scrollbar is not visible at all. But in multi-line mode, we may need
+			// to redraw the scrollbar character at the end.
+			if t.multiLine {
+				t.prevLines[line].hasBar = t.printBar(line, true, barRange)
+			}
+		}
+	}
+}
+
+func (t *Terminal) printBar(lineNum int, forceRedraw bool, barRange [2]int) bool {
+	hasBar := lineNum >= barRange[0] && lineNum < barRange[1]
+	if len(t.scrollbar) > 0 && (hasBar != t.prevLines[lineNum].hasBar || forceRedraw) {
+		t.move(lineNum, t.window.Width()-1, true)
+		if hasBar {
+			t.window.CPrint(tui.ColScrollbar, t.scrollbar)
 		}
 	}
+	return hasBar
 }
 
-func (t *Terminal) printItem(result Result, line int, i int, current bool, bar bool) {
+func (t *Terminal) printItem(result Result, line int, maxLine int, index int, current bool, barRange [2]int) int {
 	item := result.item
 	_, selected := t.selected[item.Index()]
 	label := ""
 	if t.jumping != jumpDisabled {
-		if i < len(t.jumpLabels) {
+		if index < len(t.jumpLabels) {
 			// Striped
-			current = i%2 == 0
-			label = t.jumpLabels[i:i+1] + strings.Repeat(" ", t.pointerLen-1)
+			current = index%2 == 0
+			label = t.jumpLabels[index:index+1] + strings.Repeat(" ", t.pointerLen-1)
 		}
 	} else if current {
 		label = t.pointer
 	}
 
 	// Avoid unnecessary redraw
-	newLine := itemLine{offset: line, current: current, selected: selected, label: label,
-		result: result, queryLen: len(t.input), width: 0, bar: bar}
-	prevLine := t.prevLines[i]
-	forceRedraw := prevLine.offset != newLine.offset
-	printBar := func() {
-		if len(t.scrollbar) > 0 && (bar != prevLine.bar || forceRedraw) {
-			t.prevLines[i].bar = bar
-			t.move(line, t.window.Width()-1, true)
-			if bar {
-				t.window.CPrint(tui.ColScrollbar, t.scrollbar)
-			}
-		}
+	newLine := itemLine{firstLine: line, cy: index + t.offset, current: current, selected: selected, label: label,
+		result: result, queryLen: len(t.input), width: 0, hasBar: line >= barRange[0] && line < barRange[1], minIndex: t.merger.minIndex}
+	prevLine := t.prevLines[line]
+	forceRedraw := prevLine.other || prevLine.firstLine != newLine.firstLine || prevLine.minIndex != t.merger.minIndex
+	printBar := func(lineNum int, forceRedraw bool) bool {
+		return t.printBar(lineNum, forceRedraw, barRange)
 	}
 
 	if !forceRedraw &&
@@ -1892,33 +1993,77 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool, bar b
 		prevLine.label == newLine.label &&
 		prevLine.queryLen == newLine.queryLen &&
 		prevLine.result == newLine.result {
-		printBar()
-		return
+		t.prevLines[line].hasBar = printBar(line, false)
+		if !t.multiLine {
+			return line
+		}
+		lines, _ := item.text.NumLines(maxLine - line + 1)
+		return line + lines - 1
 	}
 
-	t.move(line, 0, forceRedraw)
-	if current {
-		if len(label) == 0 {
-			t.window.CPrint(tui.ColCurrentCursorEmpty, t.pointerEmpty)
+	maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
+	postTask := func(lineNum int, width int) {
+		if (current || selected) && t.highlightLine {
+			color := tui.ColSelected
+			if current {
+				color = tui.ColCurrent
+			}
+			fillSpaces := maxWidth - width
+			if fillSpaces > 0 {
+				t.window.CPrint(color, strings.Repeat(" ", fillSpaces))
+			}
+			newLine.width = maxWidth
 		} else {
-			t.window.CPrint(tui.ColCurrentCursor, label)
+			fillSpaces := t.prevLines[lineNum].width - width
+			if fillSpaces > 0 {
+				t.window.Print(strings.Repeat(" ", fillSpaces))
+			}
+			newLine.width = width
 		}
-		if selected {
-			t.window.CPrint(tui.ColCurrentMarker, t.marker)
-		} else {
-			t.window.CPrint(tui.ColCurrentSelectedEmpty, t.markerEmpty)
+		// When width is 0, line is completely cleared. We need to redraw scrollbar
+		newLine.hasBar = printBar(lineNum, forceRedraw || width == 0)
+		t.prevLines[lineNum] = newLine
+	}
+
+	var finalLineNum int
+	markerFor := func(markerClass markerClass) string {
+		marker := t.marker
+		switch markerClass {
+		case markerTop:
+			marker = t.markerMultiLine[0]
+		case markerMiddle:
+			marker = t.markerMultiLine[1]
+		case markerBottom:
+			marker = t.markerMultiLine[2]
 		}
-		newLine.width = t.printHighlighted(result, tui.ColCurrent, tui.ColCurrentMatch, true, true)
-	} else {
-		if len(label) == 0 {
-			t.window.CPrint(tui.ColCursorEmpty, t.pointerEmpty)
-		} else {
-			t.window.CPrint(tui.ColCursor, label)
+		return marker
+	}
+	if current {
+		preTask := func(marker markerClass) {
+			if len(label) == 0 {
+				t.window.CPrint(tui.ColCurrentCursorEmpty, t.pointerEmpty)
+			} else {
+				t.window.CPrint(tui.ColCurrentCursor, label)
+			}
+			if selected {
+				t.window.CPrint(tui.ColCurrentMarker, markerFor(marker))
+			} else {
+				t.window.CPrint(tui.ColCurrentSelectedEmpty, t.markerEmpty)
+			}
 		}
-		if selected {
-			t.window.CPrint(tui.ColMarker, t.marker)
-		} else {
-			t.window.Print(t.markerEmpty)
+		finalLineNum = t.printHighlighted(result, tui.ColCurrent, tui.ColCurrentMatch, true, true, line, maxLine, forceRedraw, preTask, postTask)
+	} else {
+		preTask := func(marker markerClass) {
+			if len(label) == 0 {
+				t.window.CPrint(tui.ColCursorEmpty, t.pointerEmpty)
+			} else {
+				t.window.CPrint(tui.ColCursor, label)
+			}
+			if selected {
+				t.window.CPrint(tui.ColMarker, markerFor(marker))
+			} else {
+				t.window.Print(t.markerEmpty)
+			}
 		}
 		var base, match tui.ColorPair
 		if selected {
@@ -1928,27 +2073,9 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool, bar b
 			base = tui.ColNormal
 			match = tui.ColMatch
 		}
-		newLine.width = t.printHighlighted(result, base, match, false, true)
-	}
-	if (current || selected) && t.highlightLine {
-		color := tui.ColSelected
-		if current {
-			color = tui.ColCurrent
-		}
-		maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
-		fillSpaces := maxWidth - newLine.width
-		newLine.width = maxWidth
-		if fillSpaces > 0 {
-			t.window.CPrint(color, strings.Repeat(" ", fillSpaces))
-		}
-	} else {
-		fillSpaces := prevLine.width - newLine.width
-		if fillSpaces > 0 {
-			t.window.Print(strings.Repeat(" ", fillSpaces))
-		}
+		finalLineNum = t.printHighlighted(result, base, match, false, true, line, maxLine, forceRedraw, preTask, postTask)
 	}
-	printBar()
-	t.prevLines[i] = newLine
+	return finalLineNum
 }
 
 func (t *Terminal) trimRight(runes []rune, width int) ([]rune, bool) {
@@ -1989,12 +2116,9 @@ func (t *Terminal) overflow(runes []rune, max int) bool {
 	return t.displayWidthWithLimit(runes, 0, max) > max
 }
 
-func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMatch tui.ColorPair, current bool, match bool) int {
+func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMatch tui.ColorPair, current bool, match bool, lineNum int, maxLineNum int, forceRedraw bool, preTask func(markerClass), postTask func(int, int)) int {
+	var displayWidth int
 	item := result.item
-
-	// Overflow
-	text := make([]rune, item.text.Length())
-	copy(text, item.text.ToRunes())
 	matchOffsets := []Offset{}
 	var pos *[]int
 	if match && t.merger.pattern != nil {
@@ -2009,69 +2133,169 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
 		}
 		sort.Sort(ByOrder(charOffsets))
 	}
-	var maxe int
-	for _, offset := range charOffsets {
-		maxe = util.Max(maxe, int(offset[1]))
+	allOffsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current)
+
+	from := 0
+	text := make([]rune, item.text.Length())
+	copy(text, item.text.ToRunes())
+
+	finalLineNum := lineNum
+	numItemLines := 1
+	cutoff := 0
+	overflow := false
+	topCutoff := false
+	if t.multiLine {
+		maxLines := maxLineNum - lineNum + 1
+		numItemLines, overflow = item.text.NumLines(maxLines)
+		// Cut off the upper lines in the 'default' layout
+		if t.layout == layoutDefault && !current && maxLines == numItemLines && overflow {
+			actualLines, _ := item.text.NumLines(math.MaxInt32)
+			cutoff = actualLines - maxLines
+			topCutoff = true
+		}
 	}
+	for lineOffset := 0; from <= len(text) && (lineNum <= maxLineNum || maxLineNum == 0); lineOffset++ {
+		finalLineNum = lineNum
 
-	offsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current)
-	maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
-	ellipsis, ellipsisWidth := util.Truncate(t.ellipsis, maxWidth/2)
-	maxe = util.Constrain(maxe+util.Min(maxWidth/2-ellipsisWidth, t.hscrollOff), 0, len(text))
-	displayWidth := t.displayWidthWithLimit(text, 0, maxWidth)
-	if displayWidth > maxWidth {
-		transformOffsets := func(diff int32, rightTrim bool) {
-			for idx, offset := range offsets {
-				b, e := offset.offset[0], offset.offset[1]
-				el := int32(len(ellipsis))
-				b += el - diff
-				e += el - diff
-				b = util.Max32(b, el)
-				if rightTrim {
-					e = util.Min32(e, int32(maxWidth-ellipsisWidth))
-				}
-				offsets[idx].offset[0] = b
-				offsets[idx].offset[1] = util.Max32(b, e)
+		line := text[from:]
+		if t.multiLine {
+			for idx, r := range text[from:] {
+				if r == '\n' {
+					line = line[:idx]
+					break
+				}
 			}
 		}
-		if t.hscroll {
-			if t.keepRight && pos == nil {
-				trimmed, diff := t.trimLeft(text, maxWidth-ellipsisWidth)
-				transformOffsets(diff, false)
-				text = append(ellipsis, trimmed...)
-			} else if !t.overflow(text[:maxe], maxWidth-ellipsisWidth) {
-				// Stri..
-				text, _ = t.trimRight(text, maxWidth-ellipsisWidth)
-				text = append(text, ellipsis...)
+
+		offsets := []colorOffset{}
+		for _, offset := range allOffsets {
+			if offset.offset[0] >= int32(from) && offset.offset[1] <= int32(from+len(line)) {
+				offset.offset[0] -= int32(from)
+				offset.offset[1] -= int32(from)
+				offsets = append(offsets, offset)
 			} else {
-				// Stri..
-				rightTrim := false
-				if t.overflow(text[maxe:], ellipsisWidth) {
-					text = append(text[:maxe], ellipsis...)
-					rightTrim = true
-				}
-				// ..ri..
-				var diff int32
-				text, diff = t.trimLeft(text, maxWidth-ellipsisWidth)
-
-				// Transform offsets
-				transformOffsets(diff, rightTrim)
-				text = append(ellipsis, text...)
+				allOffsets = allOffsets[len(offsets):]
+				break
 			}
-		} else {
-			text, _ = t.trimRight(text, maxWidth-ellipsisWidth)
-			text = append(text, ellipsis...)
+		}
+
+		from += len(line) + 1
+
+		if cutoff > 0 {
+			cutoff--
+			lineOffset--
+			continue
+		}
+
+		var maxe int
+		for _, offset := range offsets {
+			if offset.match {
+				maxe = util.Max(maxe, int(offset.offset[1]))
+			}
+		}
+
+		actualLineNum := lineNum
+		if t.layout == layoutDefault {
+			actualLineNum = (lineNum - lineOffset) + (numItemLines - lineOffset) - 1
+		}
+		t.move(actualLineNum, 0, forceRedraw)
+
+		if preTask != nil {
+			var marker markerClass
+			if numItemLines == 1 {
+				if !overflow {
+					marker = markerSingle
+				} else if topCutoff {
+					marker = markerBottom
+				} else {
+					marker = markerTop
+				}
+			} else {
+				if lineOffset == 0 { // First line
+					if topCutoff {
+						marker = markerMiddle
+					} else {
+						marker = markerTop
+					}
+				} else if lineOffset == numItemLines-1 { // Last line
+					if topCutoff || !overflow {
+						marker = markerBottom
+					} else {
+						marker = markerMiddle
+					}
+				} else {
+					marker = markerMiddle
+				}
+			}
+
+			preTask(marker)
+		}
+
+		maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
+		ellipsis, ellipsisWidth := util.Truncate(t.ellipsis, maxWidth/2)
+		maxe = util.Constrain(maxe+util.Min(maxWidth/2-ellipsisWidth, t.hscrollOff), 0, len(line))
+		displayWidth = t.displayWidthWithLimit(line, 0, maxWidth)
+		if displayWidth > maxWidth {
+			transformOffsets := func(diff int32, rightTrim bool) {
+				for idx, offset := range offsets {
+					b, e := offset.offset[0], offset.offset[1]
+					el := int32(len(ellipsis))
+					b += el - diff
+					e += el - diff
+					b = util.Max32(b, el)
+					if rightTrim {
+						e = util.Min32(e, int32(maxWidth-ellipsisWidth))
+					}
+					offsets[idx].offset[0] = b
+					offsets[idx].offset[1] = util.Max32(b, e)
+				}
+			}
+			if t.hscroll {
+				if t.keepRight && pos == nil {
+					trimmed, diff := t.trimLeft(line, maxWidth-ellipsisWidth)
+					transformOffsets(diff, false)
+					line = append(ellipsis, trimmed...)
+				} else if !t.overflow(line[:maxe], maxWidth-ellipsisWidth) {
+					// Stri..
+					line, _ = t.trimRight(line, maxWidth-ellipsisWidth)
+					line = append(line, ellipsis...)
+				} else {
+					// Stri..
+					rightTrim := false
+					if t.overflow(line[maxe:], ellipsisWidth) {
+						line = append(line[:maxe], ellipsis...)
+						rightTrim = true
+					}
+					// ..ri..
+					var diff int32
+					line, diff = t.trimLeft(line, maxWidth-ellipsisWidth)
+
+					// Transform offsets
+					transformOffsets(diff, rightTrim)
+					line = append(ellipsis, line...)
+				}
+			} else {
+				line, _ = t.trimRight(line, maxWidth-ellipsisWidth)
+				line = append(line, ellipsis...)
 
-			for idx, offset := range offsets {
-				offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-len(ellipsis)))
-				offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
+				for idx, offset := range offsets {
+					offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-len(ellipsis)))
+					offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
+				}
 			}
+			displayWidth = t.displayWidthWithLimit(line, 0, displayWidth)
+		}
+
+		t.printColoredString(t.window, line, offsets, colBase)
+		if postTask != nil {
+			postTask(actualLineNum, displayWidth)
+		} else {
+			t.markOtherLine(actualLineNum)
 		}
-		displayWidth = t.displayWidthWithLimit(text, 0, displayWidth)
+		lineNum += 1
 	}
 
-	t.printColoredString(t.window, text, offsets, colBase)
-	return displayWidth
+	return finalLineNum
 }
 
 func (t *Terminal) printColoredString(window tui.Window, text []rune, offsets []colorOffset, colBase tui.ColorPair) {
@@ -2169,7 +2393,7 @@ func (t *Terminal) renderPreviewArea(unchanged bool) {
 	}
 
 	effectiveHeight := height - headerLines
-	barLength, barStart := getScrollbar(len(body), effectiveHeight, util.Min(len(body)-effectiveHeight, t.previewer.offset-headerLines))
+	barLength, barStart := getScrollbar(1, len(body), effectiveHeight, util.Min(len(body)-effectiveHeight, t.previewer.offset-headerLines))
 	t.renderPreviewScrollbar(headerLines, barLength, barStart)
 }
 
@@ -2538,7 +2762,10 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
 
 func hasPreviewFlags(template string) (slot bool, plus bool, forceUpdate bool) {
 	for _, match := range placeholder.FindAllString(template, -1) {
-		_, _, flags := parsePlaceholder(match)
+		escaped, _, flags := parsePlaceholder(match)
+		if escaped {
+			continue
+		}
 		if flags.plus {
 			plus = true
 		}
@@ -2642,11 +2869,18 @@ func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
 			replace = func(item *Item) string {
 				switch {
 				case flags.number:
-					n := int(item.text.Index)
-					if n < 0 {
-						return ""
+					n := item.text.Index
+					if n == minItem.Index() {
+						// NOTE: Item index should normally be positive, but if there's no
+						// match, it will be set to math.MinInt32, and we don't want to
+						// show that value. However, int32 can overflow, especially when
+						// `--tail` is used with an endless input stream, and the index of
+						// an item actually can be math.MinInt32. In that case, you're
+						// getting an incorrect value, but we're going to ignore that for
+						// now.
+						return "''"
 					}
-					return strconv.Itoa(n)
+					return strconv.Itoa(int(n))
 				case flags.file:
 					return item.AsString(params.stripAnsi)
 				default:
@@ -2709,7 +2943,7 @@ func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
 		}
 
 		if flags.file {
-			file := writeTemporaryFile(replacements, params.printsep)
+			file := WriteTemporaryFile(replacements, params.printsep)
 			tempFiles = append(tempFiles, file)
 			return file
 		}
@@ -2738,9 +2972,30 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
 	cmd.Env = t.environ()
 	t.executing.Set(true)
 	if !background {
-		cmd.Stdin = t.ttyin
+		// Open a separate handle for tty input
+		if in, _ := tui.TtyIn(); in != nil {
+			cmd.Stdin = in
+			if in != os.Stdin {
+				defer in.Close()
+			}
+		}
+
 		cmd.Stdout = os.Stdout
+		if !util.IsTty(os.Stdout) {
+			if out, _ := tui.TtyOut(); out != nil {
+				cmd.Stdout = out
+				defer out.Close()
+			}
+		}
+
 		cmd.Stderr = os.Stderr
+		if !util.IsTty(os.Stderr) {
+			if out, _ := tui.TtyOut(); out != nil {
+				cmd.Stderr = out
+				defer out.Close()
+			}
+		}
+
 		t.tui.Pause(true)
 		cmd.Run()
 		t.tui.Resume(true, false)
@@ -3299,6 +3554,9 @@ func (t *Terminal) Loop() error {
 							return ExitOk
 						})
 						return
+					case reqBecome:
+						exit(func() int { return ExitBecome })
+						return
 					case reqQuit:
 						exit(func() int { return ExitInterrupt })
 						return
@@ -3381,6 +3639,14 @@ func (t *Terminal) Loop() error {
 		}
 
 		t.mutex.Lock()
+		for key, ret := range t.expect {
+			if keyMatch(key, event) {
+				t.pressed = ret
+				t.reqBox.Set(reqClose, nil)
+				t.mutex.Unlock()
+				return nil
+			}
+		}
 		previousInput := t.input
 		previousCx := t.cx
 		t.lastKey = event.KeyName()
@@ -3425,14 +3691,6 @@ func (t *Terminal) Loop() error {
 		scrollPreviewBy := func(amount int) {
 			scrollPreviewTo(t.previewer.offset + amount)
 		}
-		for key, ret := range t.expect {
-			if keyMatch(key, event) {
-				t.pressed = ret
-				t.reqBox.Set(reqClose, nil)
-				t.mutex.Unlock()
-				return nil
-			}
-		}
 
 		actionsFor := func(eventType tui.EventType) []*action {
 			return t.keymap[eventType.AsEvent()]
@@ -3473,7 +3731,14 @@ func (t *Terminal) Loop() error {
 					if t.history != nil {
 						t.history.append(string(t.input))
 					}
-					t.executor.Become(t.ttyin, t.environ(), command)
+
+					if len(t.proxyScript) > 0 {
+						data := strings.Join(append([]string{command}, t.environ()...), "\x00")
+						os.WriteFile(t.proxyScript+becomeSuffix, []byte(data), 0600)
+						req(reqBecome)
+					} else {
+						t.executor.Become(t.ttyin, t.environ(), command)
+					}
 				}
 			case actExecute, actExecuteSilent:
 				t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false)
@@ -3813,6 +4078,8 @@ func (t *Terminal) Loop() error {
 				suffix := copySlice(t.input[t.cx:])
 				t.input = append(append(t.input[:t.cx], str...), suffix...)
 				t.cx += len(str)
+			case actPrint:
+				t.printQueue = append(t.printQueue, a.a)
 			case actUnixLineDiscard:
 				beof = len(t.input) == 0
 				if t.cx > 0 {
@@ -4009,7 +4276,7 @@ func (t *Terminal) Loop() error {
 				if pbarDragging {
 					effectiveHeight := t.pwindow.Height() - headerLines
 					numLines := len(t.previewer.lines) - headerLines
-					barLength, _ := getScrollbar(numLines, effectiveHeight, util.Min(numLines-effectiveHeight, t.previewer.offset-headerLines))
+					barLength, _ := getScrollbar(1, numLines, effectiveHeight, util.Min(numLines-effectiveHeight, t.previewer.offset-headerLines))
 					if barLength > 0 {
 						y := my - t.pwindow.Top() - headerLines - barLength/2
 						y = util.Constrain(y, 0, effectiveHeight-barLength)
@@ -4055,7 +4322,8 @@ func (t *Terminal) Loop() error {
 							total := t.merger.Length()
 							prevOffset := t.offset
 							// barStart = (maxItems - barLength) * t.offset / (total - maxItems)
-							t.offset = int(math.Ceil(float64(newBarStart) * float64(total-maxItems) / float64(maxItems-barLength)))
+							perLine := t.avgNumLines()
+							t.offset = int(math.Ceil(float64(newBarStart) * float64(total*perLine-maxItems) / float64(maxItems*perLine-barLength)))
 							t.cy = t.offset + t.cy - prevOffset
 							req(reqList)
 						}
@@ -4063,11 +4331,18 @@ func (t *Terminal) Loop() error {
 					break
 				}
 
+				// There can be empty lines after the list in multi-line mode
+				prevLine := t.prevLines[my]
+				if prevLine.empty {
+					break
+				}
+
 				// Double-click on an item
+				cy := prevLine.cy
 				if me.Double && mx < t.window.Width()-1 {
 					// Double-click
 					if my >= min {
-						if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
+						if t.vset(cy) && t.cy < t.merger.Length() {
 							return doActions(actionsFor(tui.DoubleClick))
 						}
 					}
@@ -4080,7 +4355,7 @@ func (t *Terminal) Loop() error {
 						// Prompt
 						t.cx = mxCons + t.xoffset
 					} else if my >= min {
-						t.vset(t.offset + my - min)
+						t.vset(cy)
 						req(reqList)
 						evt := tui.RightClick
 						if me.Mod {
@@ -4299,26 +4574,91 @@ func (t *Terminal) Loop() error {
 func (t *Terminal) constrain() {
 	// count of items to display allowed by filtering
 	count := t.merger.Length()
-	// count of lines can be displayed
-	height := t.maxItems()
+	maxLines := t.maxItems()
+
+	// May need to try again after adjusting the offset
+	t.offset = util.Constrain(t.offset, 0, count)
+	for tries := 0; tries < maxLines; tries++ {
+		numItems := maxLines
+		// How many items can be fit on screen including the current item?
+		if t.multiLine && t.merger.Length() > 0 {
+			numItemsFound := 0
+			linesSum := 0
+
+			add := func(i int) bool {
+				lines, _ := t.merger.Get(i).item.text.NumLines(numItems - linesSum)
+				linesSum += lines
+				if linesSum >= numItems {
+					if numItemsFound == 0 {
+						numItemsFound = 1
+					}
+					return false
+				}
+				numItemsFound++
+				return true
+			}
 
-	t.cy = util.Constrain(t.cy, 0, util.Max(0, count-1))
+			for i := t.offset; i < t.merger.Length(); i++ {
+				if !add(i) {
+					break
+				}
+			}
 
-	minOffset := util.Max(t.cy-height+1, 0)
-	maxOffset := util.Max(util.Min(count-height, t.cy), 0)
-	t.offset = util.Constrain(t.offset, minOffset, maxOffset)
-	if t.scrollOff == 0 {
-		return
-	}
+			// We can possibly fit more items "before" the offset on screen
+			if linesSum < numItems {
+				for i := t.offset - 1; i >= 0; i-- {
+					if !add(i) {
+						break
+					}
+				}
+			}
 
-	scrollOff := util.Min(height/2, t.scrollOff)
-	for {
-		prevOffset := t.offset
-		if t.cy-t.offset < scrollOff {
-			t.offset = util.Max(minOffset, t.offset-1)
+			numItems = numItemsFound
 		}
-		if t.cy-t.offset >= height-scrollOff {
-			t.offset = util.Min(maxOffset, t.offset+1)
+
+		t.cy = util.Constrain(t.cy, 0, util.Max(0, count-1))
+		minOffset := util.Max(t.cy-numItems+1, 0)
+		maxOffset := util.Max(util.Min(count-numItems, t.cy), 0)
+		prevOffset := t.offset
+		t.offset = util.Constrain(t.offset, minOffset, maxOffset)
+		if t.scrollOff > 0 {
+			scrollOff := util.Min(maxLines/2, t.scrollOff)
+			newOffset := t.offset
+			// 2-phase adjustment to avoid infinite loop of alternating between moving up and down
+			for phase := 0; phase < 2; phase++ {
+				for {
+					prevOffset := newOffset
+					numItems := t.merger.Length()
+					itemLines := 1
+					if t.multiLine && t.cy < numItems {
+						itemLines, _ = t.merger.Get(t.cy).item.text.NumLines(maxLines)
+					}
+					linesBefore := t.cy - newOffset
+					if t.multiLine {
+						linesBefore = 0
+						for i := newOffset; i < t.cy && i < numItems; i++ {
+							lines, _ := t.merger.Get(i).item.text.NumLines(maxLines - linesBefore - itemLines)
+							linesBefore += lines
+						}
+					}
+					linesAfter := maxLines - (linesBefore + itemLines)
+
+					// Stuck in the middle, nothing to do
+					if linesBefore < scrollOff && linesAfter < scrollOff {
+						break
+					}
+
+					if phase == 0 && linesBefore < scrollOff {
+						newOffset = util.Max(minOffset, newOffset-1)
+					} else if phase == 1 && linesAfter < scrollOff {
+						newOffset = util.Min(maxOffset, newOffset+1)
+					}
+					if newOffset == prevOffset {
+						break
+					}
+				}
+				t.offset = newOffset
+			}
 		}
 		if t.offset == prevOffset {
 			break
diff --git a/src/tmux.go b/src/tmux.go
new file mode 100644
index 00000000000..3be956615cf
--- /dev/null
+++ b/src/tmux.go
@@ -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)
+}
diff --git a/src/tui/eventtype_string.go b/src/tui/eventtype_string.go
index d752163d05a..a62ba073f19 100644
--- a/src/tui/eventtype_string.go
+++ b/src/tui/eventtype_string.go
@@ -107,11 +107,12 @@ func _() {
 	_ = x[Result-96]
 	_ = x[Jump-97]
 	_ = 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 {
 	if i < 0 || i >= EventType(len(_EventType_index)-1) {
diff --git a/src/tui/light.go b/src/tui/light.go
index 1181d167433..f202899a142 100644
--- a/src/tui/light.go
+++ b/src/tui/light.go
@@ -127,11 +127,7 @@ type LightWindow struct {
 	bg       Color
 }
 
-func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
-	in, err := openTtyIn()
-	if err != nil {
-		return nil, err
-	}
+func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
 	out, err := openTtyOut()
 	if err != nil {
 		out = os.Stderr
@@ -142,7 +138,7 @@ func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop in
 		forceBlack:    forceBlack,
 		mouse:         mouse,
 		clearOnExit:   clearOnExit,
-		ttyin:         in,
+		ttyin:         ttyin,
 		ttyout:        out,
 		yoffset:       0,
 		tabstop:       tabstop,
diff --git a/src/tui/light_unix.go b/src/tui/light_unix.go
index 8d5a279b5e9..06099d2f794 100644
--- a/src/tui/light_unix.go
+++ b/src/tui/light_unix.go
@@ -7,7 +7,6 @@ import (
 	"os"
 	"os/exec"
 	"strings"
-	"sync"
 	"syscall"
 
 	"github.com/junegunn/fzf/src/util"
@@ -15,13 +14,6 @@ import (
 	"golang.org/x/term"
 )
 
-var (
-	tty    string
-	ttyin  *os.File
-	ttyout *os.File
-	mutex  sync.Mutex
-)
-
 func IsLightRendererSupported() bool {
 	return true
 }
@@ -53,15 +45,13 @@ func (r *LightRenderer) initPlatform() error {
 }
 
 func (r *LightRenderer) closePlatform() {
-	// NOOP
+	r.ttyout.Close()
 }
 
 func openTty(mode int) (*os.File, error) {
 	in, err := os.OpenFile(consoleDevice, mode, 0)
 	if err != nil {
-		if len(tty) == 0 {
-			tty = ttyname()
-		}
+		tty := ttyname()
 		if len(tty) > 0 {
 			if in, err := os.OpenFile(tty, mode, 0); err == nil {
 				return in, nil
@@ -73,31 +63,11 @@ func openTty(mode int) (*os.File, error) {
 }
 
 func openTtyIn() (*os.File, error) {
-	mutex.Lock()
-	defer mutex.Unlock()
-
-	if ttyin != nil {
-		return ttyin, nil
-	}
-	in, err := openTty(syscall.O_RDONLY)
-	if err == nil {
-		ttyin = in
-	}
-	return in, err
+	return openTty(syscall.O_RDONLY)
 }
 
 func openTtyOut() (*os.File, error) {
-	mutex.Lock()
-	defer mutex.Unlock()
-
-	if ttyout != nil {
-		return ttyout, nil
-	}
-	out, err := openTty(syscall.O_WRONLY)
-	if err == nil {
-		ttyout = out
-	}
-	return out, err
+	return openTty(syscall.O_WRONLY)
 }
 
 func (r *LightRenderer) setupTerminal() {
diff --git a/src/tui/light_windows.go b/src/tui/light_windows.go
index 8c459d660be..b7fc3402852 100644
--- a/src/tui/light_windows.go
+++ b/src/tui/light_windows.go
@@ -97,8 +97,7 @@ func openTtyIn() (*os.File, error) {
 }
 
 func openTtyOut() (*os.File, error) {
-	// not used
-	return nil, nil
+	return os.Stderr, nil
 }
 
 func (r *LightRenderer) setupTerminal() error {
diff --git a/src/tui/ttyname_unix.go b/src/tui/ttyname_unix.go
index 384115fba04..d0350a0bc4c 100644
--- a/src/tui/ttyname_unix.go
+++ b/src/tui/ttyname_unix.go
@@ -4,12 +4,19 @@ package tui
 
 import (
 	"os"
+	"sync/atomic"
 	"syscall"
 )
 
 var devPrefixes = [...]string{"/dev/pts/", "/dev/"}
 
+var tty atomic.Value
+
 func ttyname() string {
+	if cached := tty.Load(); cached != nil {
+		return cached.(string)
+	}
+
 	var stderr syscall.Stat_t
 	if syscall.Fstat(2, &stderr) != nil {
 		return ""
@@ -27,17 +34,21 @@ func ttyname() string {
 				continue
 			}
 			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 ""
 }
 
-// TtyIn returns terminal device to be used as STDIN, falls back to os.Stdin
-func TtyIn() *os.File {
-	if in, err := openTtyIn(); err == nil {
-		return in
-	}
-	return os.Stdin
+// TtyIn returns terminal device to read user input
+func TtyIn() (*os.File, error) {
+	return openTtyIn()
+}
+
+// TtyIn returns terminal device to write to
+func TtyOut() (*os.File, error) {
+	return openTtyOut()
 }
diff --git a/src/tui/ttyname_windows.go b/src/tui/ttyname_windows.go
index 39b84f70035..0313c608c69 100644
--- a/src/tui/ttyname_windows.go
+++ b/src/tui/ttyname_windows.go
@@ -2,13 +2,20 @@
 
 package tui
 
-import "os"
+import (
+	"os"
+)
 
 func ttyname() string {
 	return ""
 }
 
 // TtyIn on Windows returns os.Stdin
-func TtyIn() *os.File {
-	return os.Stdin
+func TtyIn() (*os.File, error) {
+	return os.Stdin, nil
+}
+
+// TtyIn on Windows returns nil
+func TtyOut() (*os.File, error) {
+	return nil, nil
 }
diff --git a/src/tui/tui.go b/src/tui/tui.go
index aed41a9d3b6..96a726516d8 100644
--- a/src/tui/tui.go
+++ b/src/tui/tui.go
@@ -356,7 +356,8 @@ type MouseEvent struct {
 type BorderShape int
 
 const (
-	BorderNone BorderShape = iota
+	BorderUndefined BorderShape = iota
+	BorderNone
 	BorderRounded
 	BorderSharp
 	BorderBold
@@ -701,9 +702,9 @@ func init() {
 		Input:            ColorAttr{colDefault, AttrUndefined},
 		Fg:               ColorAttr{colDefault, AttrUndefined},
 		Bg:               ColorAttr{colDefault, AttrUndefined},
-		SelectedFg:       ColorAttr{colDefault, AttrUndefined},
-		SelectedBg:       ColorAttr{colDefault, AttrUndefined},
-		SelectedMatch:    ColorAttr{colDefault, AttrUndefined},
+		SelectedFg:       ColorAttr{colUndefined, AttrUndefined},
+		SelectedBg:       ColorAttr{colUndefined, AttrUndefined},
+		SelectedMatch:    ColorAttr{colUndefined, AttrUndefined},
 		DarkBg:           ColorAttr{colBlack, AttrUndefined},
 		Prompt:           ColorAttr{colBlue, AttrUndefined},
 		Match:            ColorAttr{colGreen, AttrUndefined},
@@ -731,9 +732,9 @@ func init() {
 		Input:            ColorAttr{colDefault, AttrUndefined},
 		Fg:               ColorAttr{colDefault, AttrUndefined},
 		Bg:               ColorAttr{colDefault, AttrUndefined},
-		SelectedFg:       ColorAttr{colDefault, AttrUndefined},
-		SelectedBg:       ColorAttr{colDefault, AttrUndefined},
-		SelectedMatch:    ColorAttr{colDefault, AttrUndefined},
+		SelectedFg:       ColorAttr{colUndefined, AttrUndefined},
+		SelectedBg:       ColorAttr{colUndefined, AttrUndefined},
+		SelectedMatch:    ColorAttr{colUndefined, AttrUndefined},
 		DarkBg:           ColorAttr{236, AttrUndefined},
 		Prompt:           ColorAttr{110, AttrUndefined},
 		Match:            ColorAttr{108, AttrUndefined},
@@ -761,9 +762,9 @@ func init() {
 		Input:            ColorAttr{colDefault, AttrUndefined},
 		Fg:               ColorAttr{colDefault, AttrUndefined},
 		Bg:               ColorAttr{colDefault, AttrUndefined},
-		SelectedFg:       ColorAttr{colDefault, AttrUndefined},
-		SelectedBg:       ColorAttr{colDefault, AttrUndefined},
-		SelectedMatch:    ColorAttr{colDefault, AttrUndefined},
+		SelectedFg:       ColorAttr{colUndefined, AttrUndefined},
+		SelectedBg:       ColorAttr{colUndefined, AttrUndefined},
+		SelectedMatch:    ColorAttr{colUndefined, AttrUndefined},
 		DarkBg:           ColorAttr{251, AttrUndefined},
 		Prompt:           ColorAttr{25, 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
 	theme.SelectedFg = o(theme.Fg, theme.SelectedFg)
 	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.Gutter = o(theme.DarkBg, theme.Gutter)
 	theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
diff --git a/src/util/chars.go b/src/util/chars.go
index f84d365f7b6..82773f409f6 100644
--- a/src/util/chars.go
+++ b/src/util/chars.go
@@ -1,6 +1,7 @@
 package util
 
 import (
+	"bytes"
 	"fmt"
 	"unicode"
 	"unicode/utf8"
@@ -74,6 +75,35 @@ func (chars *Chars) Bytes() []byte {
 	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 {
 	if chars.inBytes {
 		return nil
diff --git a/src/util/util.go b/src/util/util.go
index f6e00e9c025..ec5a1ea0559 100644
--- a/src/util/util.go
+++ b/src/util/util.go
@@ -3,6 +3,7 @@ package util
 import (
 	"math"
 	"os"
+	"strconv"
 	"strings"
 	"time"
 
@@ -137,14 +138,10 @@ func DurWithin(
 	return val
 }
 
-// IsTty returns true if stdin is a terminal
-func IsTty() bool {
-	return isatty.IsTerminal(os.Stdin.Fd())
-}
-
-// ToTty returns true if stdout is a terminal
-func ToTty() bool {
-	return isatty.IsTerminal(os.Stdout.Fd())
+// IsTty returns true if the file is a terminal
+func IsTty(file *os.File) bool {
+	fd := file.Fd()
+	return isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd)
 }
 
 // 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)
 }
+
+// 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
+}
diff --git a/src/winpty.go b/src/winpty.go
new file mode 100644
index 00000000000..adee3b0b74f
--- /dev/null
+++ b/src/winpty.go
@@ -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")
+}
diff --git a/src/winpty_windows.go b/src/winpty_windows.go
new file mode 100644
index 00000000000..78020d73810
--- /dev/null
+++ b/src/winpty_windows.go
@@ -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)
+}


Test patch:
diff --git a/src/chunklist_test.go b/src/chunklist_test.go
index 6c1d09ebdeb..c07e4adf959 100644
--- a/src/chunklist_test.go
+++ b/src/chunklist_test.go
@@ -17,7 +17,7 @@ func TestChunkList(t *testing.T) {
 	})
 
 	// Snapshot
-	snapshot, count := cl.Snapshot()
+	snapshot, count, _ := cl.Snapshot(0)
 	if len(snapshot) > 0 || count > 0 {
 		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
-	snapshot, count = cl.Snapshot()
+	snapshot, count, _ = cl.Snapshot(0)
 	if len(snapshot) != 1 && count != 2 {
 		t.Error("Snapshot should not be empty now")
 	}
@@ -61,7 +61,7 @@ func TestChunkList(t *testing.T) {
 	}
 
 	// New snapshot
-	snapshot, count = cl.Snapshot()
+	snapshot, count, _ = cl.Snapshot(0)
 	if len(snapshot) != 3 || !snapshot[0].IsFull() ||
 		!snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 {
 		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)
 	}
 }
+
+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)
+}
diff --git a/src/merger_test.go b/src/merger_test.go
index 5fd1576fd70..a6b28f6eca1 100644
--- a/src/merger_test.go
+++ b/src/merger_test.go
@@ -23,10 +23,11 @@ func randResult() Result {
 }
 
 func TestEmptyMerger(t *testing.T) {
-	assert(t, EmptyMerger(0).Length() == 0, "Not empty")
-	assert(t, EmptyMerger(0).count == 0, "Invalid count")
-	assert(t, len(EmptyMerger(0).lists) == 0, "Invalid lists")
-	assert(t, len(EmptyMerger(0).merged) == 0, "Invalid merged list")
+	r := revision{}
+	assert(t, EmptyMerger(r).Length() == 0, "Not empty")
+	assert(t, EmptyMerger(r).count == 0, "Invalid count")
+	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) {
@@ -57,7 +58,7 @@ func TestMergerUnsorted(t *testing.T) {
 	cnt := len(items)
 
 	// 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")
 	for i := 0; i < cnt; i++ {
 		assert(t, items[i] == mg.Get(i), "Invalid Get")
@@ -69,7 +70,7 @@ func TestMergerSorted(t *testing.T) {
 	cnt := len(items)
 
 	// 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")
 	sort.Sort(ByRelevance(items))
 	for i := 0; i < cnt; i++ {
@@ -79,7 +80,7 @@ func TestMergerSorted(t *testing.T) {
 	}
 
 	// Inverse order
-	mg2 := NewMerger(nil, lists, true, false, 0)
+	mg2 := NewMerger(nil, lists, true, false, revision{}, 0)
 	for i := cnt - 1; i >= 0; i-- {
 		if items[i] != mg2.Get(i) {
 			t.Error("Not sorted", items[i], mg2.Get(i))
diff --git a/src/options_test.go b/src/options_test.go
index 270af5c8b57..4a859b0e7c1 100644
--- a/src/options_test.go
+++ b/src/options_test.go
@@ -106,10 +106,11 @@ func TestSplitNth(t *testing.T) {
 }
 
 func TestIrrelevantNth(t *testing.T) {
+	index := 0
 	{
 		opts := defaultOptions()
 		words := []string{"--nth", "..", "-x"}
-		parseOptions(opts, words)
+		parseOptions(&index, opts, words)
 		postProcessOptions(opts)
 		if len(opts.Nth) != 0 {
 			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"}} {
 		{
 			opts := defaultOptions()
-			parseOptions(opts, words)
+			parseOptions(&index, opts, words)
 			postProcessOptions(opts)
 			if len(opts.Nth) != 0 {
 				t.Errorf("nth should be empty: %v", opts.Nth)
@@ -127,7 +128,7 @@ func TestIrrelevantNth(t *testing.T) {
 		{
 			opts := defaultOptions()
 			words = append(words, "-x")
-			parseOptions(opts, words)
+			parseOptions(&index, opts, words)
 			postProcessOptions(opts)
 			if len(opts.Nth) != 2 {
 				t.Errorf("nth should not be empty: %v", opts.Nth)
@@ -335,10 +336,11 @@ func TestColorSpec(t *testing.T) {
 }
 
 func TestDefaultCtrlNP(t *testing.T) {
+	index := 0
 	check := func(words []string, et tui.EventType, expected actionType) {
 		e := et.AsEvent()
 		opts := defaultOptions()
-		parseOptions(opts, words)
+		parseOptions(&index, opts, words)
 		postProcessOptions(opts)
 		if opts.Keymap[e][0].t != expected {
 			t.Error()
@@ -364,8 +366,9 @@ func TestDefaultCtrlNP(t *testing.T) {
 }
 
 func optsFor(words ...string) *Options {
+	index := 0
 	opts := defaultOptions()
-	parseOptions(opts, words)
+	parseOptions(&index, opts, words)
 	postProcessOptions(opts)
 	return opts
 }
diff --git a/src/tui/tcell_test.go b/src/tui/tcell_test.go
index 54b9c9b3a2c..217ad048674 100644
--- a/src/tui/tcell_test.go
+++ b/src/tui/tcell_test.go
@@ -3,6 +3,7 @@
 package tui
 
 import (
+	"os"
 	"testing"
 
 	"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.
 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
 		// some glitches:
 		// - output lines may not start at the beginning of a row which makes
diff --git a/src/util/util_test.go b/src/util/util_test.go
index af0762b5ed4..013f3c239e3 100644
--- a/src/util/util_test.go
+++ b/src/util/util_test.go
@@ -203,3 +203,34 @@ func TestStringWidth(t *testing.T) {
 		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)
+}
diff --git a/test/test_go.rb b/test/test_go.rb
index 5563c080258..6675f77531f 100755
--- a/test/test_go.rb
+++ b/test/test_go.rb
@@ -24,7 +24,7 @@
 FILE = File.expand_path(__FILE__)
 BASE = File.expand_path('..', __dir__)
 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
   since = Time.now
@@ -66,7 +66,7 @@ def zsh
     end
 
     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
@@ -169,29 +169,34 @@ def go(args)
 end
 
 class TestBase < Minitest::Test
-  TEMPNAME = '/tmp/output'
+  TEMPNAME = Dir::Tmpname.create(%w[fzf]) {}
+  FIFONAME = Dir::Tmpname.create(%w[fzf-fifo]) {}
 
   attr_reader :tmux
 
+  def writelines(lines)
+    File.write(TEMPNAME, lines.join("\n"))
+  end
+
   def tempname
-    @temp_suffix ||= 0
-    [TEMPNAME,
-     caller_locations.map(&:label).find { |l| l.start_with?('test_') },
-     @temp_suffix].join('-')
+    TEMPNAME
   end
 
-  def writelines(path, lines)
-    FileUtils.rm_f(path) while File.exist?(path)
-    File.open(path, 'w') { |f| f.puts lines }
+  def fzf_output
+    @thread.join.value.chomp.tap { @thread = nil }
   end
 
-  def readonce
-    wait { assert_path_exists tempname }
-    File.read(tempname)
-  ensure
-    FileUtils.rm_f(tempname) while File.exist?(tempname)
-    @temp_suffix += 1
-    tmux.prepare
+  def fzf_output_lines
+    fzf_output.lines(chomp: true)
+  end
+
+  def setup
+    FileUtils.rm_f([TEMPNAME, FIFONAME])
+    File.mkfifo(FIFONAME)
+  end
+
+  def teardown
+    FileUtils.rm_f([TEMPNAME, FIFONAME])
   end
 
   alias assert_equal_org assert_equal
@@ -201,8 +206,12 @@ def assert_equal(expected, actual)
     assert_equal_org(expected, actual)
   end
 
+  # Run fzf with its output piped to a fifo
   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
 
   def fzf!(*opts)
@@ -226,6 +235,7 @@ def setup
   end
 
   def teardown
+    super
     @tmux.kill
   end
 
@@ -251,7 +261,7 @@ def test_vanilla
     end
 
     tmux.send_keys :Enter
-    assert_equal '3910', readonce.chomp
+    assert_equal '3910', fzf_output
   end
 
   def test_fzf_default_command
@@ -259,7 +269,7 @@ def test_fzf_default_command
     tmux.until { |lines| assert_equal '> hello', lines[-3] }
 
     tmux.send_keys :Enter
-    assert_equal 'hello', readonce.chomp
+    assert_equal 'hello', fzf_output
   end
 
   def test_fzf_default_command_failure
@@ -355,7 +365,7 @@ def test_multi_order
                    :PgUp, 'C-J', :Down, :Tab, :Tab # 8, 7
     tmux.until { |lines| assert_equal '  10/10 (6)', lines[-2] }
     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
 
   def test_multi_max
@@ -462,12 +472,12 @@ def test_with_nth
         tmux.send_keys :BTab, :BTab
         tmux.until { |lines| assert_equal '  2/2 (2)', lines[-2] }
         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
         tmux.send_keys '^', '3'
         tmux.until { |lines| assert_equal '  1/2', lines[-2] }
         tmux.send_keys :Enter
-        assert_equal ['  1st 2nd 3rd/'], readonce.lines(chomp: true)
+        assert_equal ['  1st 2nd 3rd/'], fzf_output_lines
       end
     end
   end
@@ -479,18 +489,18 @@ def test_scroll
       tmux.send_keys(*Array.new(110) { rev ? :Down : :Up })
       tmux.until { |lines| assert_includes lines, '> 100' }
       tmux.send_keys :Enter
-      assert_equal '100', readonce.chomp
+      assert_equal '100', fzf_output
     end
   end
 
   def test_select_1
     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
 
   def test_exit_0
     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
 
   def test_select_1_exit_0_fail
@@ -500,7 +510,7 @@ def test_select_1_exit_0_fail
       tmux.send_keys :BTab, :BTab, :BTab
       tmux.until { |lines| assert_equal '  19/100 (3)', lines[-2] }
       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
 
@@ -508,11 +518,11 @@ def test_query_unicode
     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.send_keys :Enter
-    assert_equal %w[가나다], readonce.lines(chomp: true)
+    assert_equal %w[가나다], fzf_output_lines
   end
 
   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.send_keys 9
     tmux.until { |lines| assert_equal '  19/100 (0)', lines[-2] }
@@ -521,7 +531,7 @@ def test_sync
     tmux.send_keys :Enter
     tmux.until { |lines| assert_equal '>', lines[-1] }
     tmux.send_keys 'C-K', :Enter
-    assert_equal %w[9090], readonce.lines(chomp: true)
+    assert_equal %w[9090], fzf_output_lines
   end
 
   def test_tac
@@ -530,7 +540,7 @@ def test_tac
     tmux.send_keys :BTab, :BTab, :BTab
     tmux.until { |lines| assert_equal '  1000/1000 (3)', lines[-2] }
     tmux.send_keys :Enter
-    assert_equal %w[1000 999 998], readonce.lines(chomp: true)
+    assert_equal %w[1000 999 998], fzf_output_lines
   end
 
   def test_tac_sort
@@ -541,7 +551,7 @@ def test_tac_sort
     tmux.send_keys :BTab, :BTab, :BTab
     tmux.until { |lines| assert_equal '  28/1000 (3)', lines[-2] }
     tmux.send_keys :Enter
-    assert_equal %w[99 999 998], readonce.lines(chomp: true)
+    assert_equal %w[99 999 998], fzf_output_lines
   end
 
   def test_tac_nosort
@@ -552,7 +562,7 @@ def test_tac_nosort
     tmux.send_keys :BTab, :BTab, :BTab
     tmux.until { |lines| assert_equal '  10/1000 (3)', lines[-2] }
     tmux.send_keys :Enter
-    assert_equal %w[1000 900 800], readonce.lines(chomp: true)
+    assert_equal %w[1000 900 800], fzf_output_lines
   end
 
   def test_expect
@@ -563,7 +573,7 @@ def test_expect
       tmux.until { |lines| assert_equal '  1/100', lines[-2] }
       tmux.send_keys(*feed)
       tmux.prepare
-      assert_equal [expected, '55'], readonce.lines(chomp: true)
+      assert_equal [expected, '55'], fzf_output_lines
     end
     test.call('ctrl-t', 'C-T')
     test.call('ctrl-t', 'Enter', '')
@@ -580,13 +590,20 @@ def test_expect
     test.call('@', '@')
   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
     tmux.send_keys "seq 1 100 | #{fzf('--expect=alt-z', :print_query)}", :Enter
     tmux.until { |lines| assert_equal '  100/100', lines[-2] }
     tmux.send_keys '55'
     tmux.until { |lines| assert_equal '  1/100', lines[-2] }
     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
 
   def test_expect_printable_character_print_query
@@ -595,12 +612,12 @@ def test_expect_printable_character_print_query
     tmux.send_keys '55'
     tmux.until { |lines| assert_equal '  1/100', lines[-2] }
     tmux.send_keys 'z'
-    assert_equal %w[55 z 55], readonce.lines(chomp: true)
+    assert_equal %w[55 z 55], fzf_output_lines
   end
 
   def test_expect_print_query_select_1
     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
 
   def test_toggle_sort
@@ -614,12 +631,12 @@ def test_toggle_sort
       tmux.send_keys :Tab
       tmux.until { |lines| assert_equal '  4/111 +S (2)', lines[-2] }
       tmux.send_keys :Enter
-      assert_equal %w[111 11], readonce.lines(chomp: true)
+      assert_equal %w[111 11], fzf_output_lines
     end
   end
 
   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[строКА1 СТРОКА2 строка3 Строка4], `#{FZF} -fс < #{tempname}`.lines(chomp: true)
   end
@@ -631,7 +648,7 @@ def test_tiebreak
       ----foobar--
       -------foobar-
     ]
-    writelines(tempname, input)
+    writelines(input)
 
     assert_equal input, `#{FZF} -ffoobar --tiebreak=index < #{tempname}`.lines(chomp: true)
 
@@ -664,7 +681,7 @@ def test_tiebreak
   end
 
   def test_tiebreak_index_begin
-    writelines(tempname, [
+    writelines([
                  'xoxxxxxoxx',
                  'xoxxxxxox',
                  'xxoxxxoxx',
@@ -713,10 +730,8 @@ def test_tiebreak_index_begin
   end
 
   def test_tiebreak_begin_algo_v2
-    writelines(tempname, [
-                 'baz foo bar',
-                 'foo bar baz'
-               ])
+    writelines(['baz foo bar',
+                'foo bar baz'])
     assert_equal [
       'foo bar baz',
       'baz foo bar'
@@ -724,14 +739,12 @@ def test_tiebreak_begin_algo_v2
   end
 
   def test_tiebreak_end
-    writelines(tempname, [
-                 'xoxxxxxxxx',
-                 'xxoxxxxxxx',
-                 'xxxoxxxxxx',
-                 'xxxxoxxxx',
-                 'xxxxxoxxx',
-                 '  xxxxoxxx'
-               ])
+    writelines(['xoxxxxxxxx',
+                'xxoxxxxxxx',
+                'xxxoxxxxxx',
+                'xxxxoxxxx',
+                'xxxxxoxxx',
+                '  xxxxoxxx'])
 
     assert_equal [
       '  xxxxoxxx',
@@ -760,7 +773,7 @@ def test_tiebreak_end
       'xoxxxxxxxx'
     ], `#{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 [
       '/foo/bar/baz',
       '/bar/baz'
@@ -774,7 +787,7 @@ def test_tiebreak_length_with_nth
       12345:he
       1234567:h
     ]
-    writelines(tempname, input)
+    writelines(input)
 
     output = %w[
       1:hell
@@ -789,11 +802,9 @@ def test_tiebreak_length_with_nth
   end
 
   def test_tiebreak_chunk
-    writelines(tempname, [
-                 '1 foobarbaz ba',
-                 '2 foobar baz',
-                 '3 foo barbaz'
-               ])
+    writelines(['1 foobarbaz ba',
+                '2 foobar baz',
+                '3 foo barbaz'])
 
     assert_equal [
       '3 foo barbaz',
@@ -823,7 +834,7 @@ def test_invalid_cache
   end
 
   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
     tmux.send_keys command, :Enter
@@ -862,14 +873,14 @@ def test_bind
     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.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
 
   def test_bind_print_query
     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.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
 
   def test_bind_replace_query
@@ -924,7 +935,7 @@ def test_select_all_deselect_all_toggle_all
     tmux.until { |lines| assert_equal '  10/100 (12)', lines[-2] }
     tmux.send_keys :Enter
     assert_equal %w[1 2 10 20 30 40 50 60 70 80 90 100],
-                 readonce.lines(chomp: true)
+                 fzf_output_lines
   end
 
   def test_history
@@ -936,7 +947,7 @@ def test_history
     input = %w[00 11 22 33 44]
     input.each do |keys|
       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.send_keys keys
       tmux.until { |lines| assert_equal '  1/100', lines[-2] }
@@ -948,7 +959,7 @@ def test_history
     end
 
     # 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.send_keys 'C-p'
     tmux.until { |lines| assert_equal '> 44', lines[-1] }
@@ -971,7 +982,7 @@ def test_history
     end
 
     # 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.send_keys 'C-n', 'C-n', 'C-n', 'C-n', 'C-p'
     tmux.until { |lines| assert_equal '> 33', lines[-1] }
@@ -983,8 +994,8 @@ def test_history
   def 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)"]
-    writelines(tempname, %w[foo'bar foo"bar foo$bar])
-    tmux.send_keys "cat #{tempname} | #{fzf(opts)}", :Enter
+    writelines(%w[foo'bar foo"bar foo$bar])
+    tmux.send_keys "cat #{tempname} | #{FZF} #{opts}", :Enter
     tmux.until { |lines| assert_equal 3, lines.item_count }
 
     ready = ->(s) { tmux.until { |lines| assert_includes lines[-3], s } }
@@ -1026,8 +1037,8 @@ def test_execute
   def 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)"]
-    writelines(tempname, %w[foo'bar foo"bar foo$bar foobar])
-    tmux.send_keys "cat #{tempname} | #{fzf(opts)}", :Enter
+    writelines(%w[foo'bar foo"bar foo$bar foobar])
+    tmux.send_keys "cat #{tempname} | #{FZF} #{opts}", :Enter
     ready = ->(s) { tmux.until { |lines| assert_includes lines[-3], s } }
 
     tmux.until { |lines| assert_equal '  4/4 (0)', lines[-2] }
@@ -1062,7 +1073,7 @@ def test_execute_multi
   def test_execute_plus_flag
     output = tempname + '.tmp'
     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
 
@@ -1101,8 +1112,7 @@ def test_execute_shell
     # Custom script to use as $SHELL
     output = tempname + '.out'
     FileUtils.rm_f(output)
-    writelines(tempname,
-               ['#!/usr/bin/env bash', "echo $1 / $2 > #{output}"])
+    writelines(['#!/usr/bin/env bash', "echo $1 / $2 > #{output}"])
     system("chmod +x #{tempname}")
 
     tmux.send_keys "echo foo | SHELL=#{tempname} fzf --bind 'enter:execute:{}bar'", :Enter
@@ -1118,7 +1128,7 @@ def test_execute_shell
   end
 
   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.send_keys :Down
     tmux.until { |lines| assert_equal '> 8', lines[-10] }
@@ -1148,7 +1158,7 @@ def test_header_lines
       tmux.send_keys :Down
     end
     tmux.send_keys :Enter
-    assert_equal '50', readonce.chomp
+    assert_equal '50', fzf_output
   end
 
   def test_header_lines_reverse
@@ -1163,7 +1173,7 @@ def test_header_lines_reverse
       tmux.send_keys :Up
     end
     tmux.send_keys :Enter
-    assert_equal '50', readonce.chomp
+    assert_equal '50', fzf_output
   end
 
   def test_header_lines_reverse_list
@@ -1178,7 +1188,7 @@ def test_header_lines_reverse_list
       tmux.send_keys :Up
     end
     tmux.send_keys :Enter
-    assert_equal '50', readonce.chomp
+    assert_equal '50', fzf_output
   end
 
   def test_header_lines_overflow
@@ -1188,7 +1198,7 @@ def test_header_lines_overflow
       assert_equal '  1', lines[-3]
     end
     tmux.send_keys :Enter
-    assert_equal '', readonce.chomp
+    assert_equal '', fzf_output
   end
 
   def test_header_lines_with_nth
@@ -1200,11 +1210,11 @@ def test_header_lines_with_nth
       assert_equal '> 66666', lines[-8]
     end
     tmux.send_keys :Enter
-    assert_equal '6', readonce.chomp
+    assert_equal '6', fzf_output
   end
 
   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)
     tmux.until do |lines|
       assert_equal '  100/100', lines[-2]
@@ -1214,7 +1224,7 @@ def test_header
   end
 
   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)
     tmux.until do |lines|
       assert_equal '  100/100', lines[1]
@@ -1224,7 +1234,7 @@ def test_header_reverse
   end
 
   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)
     tmux.until do |lines|
       assert_equal '  100/100', lines[-2]
@@ -1234,7 +1244,7 @@ def test_header_reverse_list
   end
 
   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)
     tmux.until do |lines|
       assert_equal '  90/90', lines[-2]
@@ -1244,7 +1254,7 @@ def test_header_and_header_lines
   end
 
   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)
     tmux.until do |lines|
       assert_equal '  90/90', lines[1]
@@ -1254,7 +1264,7 @@ def test_header_and_header_lines_reverse
   end
 
   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)
     tmux.until do |lines|
       assert_equal '  90/90', lines[-2]
@@ -1297,7 +1307,7 @@ def test_toggle_header
   end
 
   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.send_keys '123'
     tmux.until do |lines|
@@ -1313,7 +1323,7 @@ def test_cancel
   end
 
   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|
       assert_equal '', lines[4]
       assert_equal '     y', lines[5]
@@ -1322,13 +1332,13 @@ def test_margin
   end
 
   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.send_keys :Enter
   end
 
   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|
       assert_equal '', lines[4]
       assert_equal '   > y', lines[5]
@@ -1337,7 +1347,7 @@ def test_margin_reverse_list
   end
 
   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',
       2 => '> f oo  ba  r baz barfooq ux',
@@ -1359,14 +1369,14 @@ def test_tabstop
   end
 
   def test_with_nth_basic
-    writelines(tempname, ['hello world ', 'byebye'])
+    writelines(['hello world ', 'byebye'])
     assert_equal \
       'hello world ',
       `#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp
   end
 
   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 \
       'hello world ',
       `#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi < #{tempname}`.chomp
@@ -1374,7 +1384,7 @@ def test_with_nth_ansi
 
   def test_with_nth_no_ansi
     src = "\x1b[33mhello \x1b[34;1mworld\x1b[m "
-    writelines(tempname, [src, 'byebye'])
+    writelines([src, 'byebye'])
     assert_equal \
       src,
       `#{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi < #{tempname}`.chomp
@@ -1429,7 +1439,7 @@ def test_or_operator
   end
 
   def test_hscroll_off
-    writelines(tempname, ['=' * 10_000 + '0123456789'])
+    writelines(['=' * 10_000 + '0123456789'])
     [0, 3, 6].each do |off|
       tmux.prepare
       tmux.send_keys "#{FZF} --hscroll-off=#{off} -q 0 < #{tempname}", :Enter
@@ -1475,7 +1485,7 @@ def test_jump
     tmux.send_keys :Tab
     tmux.until { |lines| assert_equal '>>1', lines[-3] }
     tmux.send_keys :Enter
-    assert_equal %w[5 2 1], readonce.lines(chomp: true)
+    assert_equal %w[5 2 1], fzf_output_lines
   end
 
   def test_jump_accept
@@ -1484,11 +1494,11 @@ def test_jump_accept
     tmux.send_keys 'C-j'
     tmux.until { |lines| assert_equal '5 5', lines[-7] }
     tmux.send_keys '3'
-    assert_equal '3', readonce.chomp
+    assert_equal '3', fzf_output
   end
 
   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.send_keys 'C-j'
     tmux.until { |lines| assert_includes lines[-7], '5 5' }
@@ -1507,7 +1517,7 @@ def test_pointer
   end
 
   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.send_keys 'C-j'
     # Correctly padded jump label should appear
@@ -1519,7 +1529,7 @@ def test_pointer_with_jump
   end
 
   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.send_keys :BTab
     # Assert that specified marker is displayed
@@ -1643,7 +1653,6 @@ def test_preview_hidden
   end
 
   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.until do |lines|
       assert_equal 100, lines.item_count
@@ -1669,7 +1678,6 @@ def test_preview_size_0
   end
 
   def test_preview_size_0_hidden
-    FileUtils.rm_f(tempname)
     tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0,hidden --bind space:toggle-preview), :Enter
     tmux.until { |lines| assert_equal 100, lines.item_count }
     tmux.send_keys :Down, :Down
@@ -1739,14 +1747,11 @@ def test_preview_q_no_match_with_initial_query
   end
 
   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'
     tmux.until { |lines| assert_equal prompt, lines[-1] }
     tmux.send_keys :Enter
-    wait do
-      assert_path_exists tempname
-      assert_equal %w[1], File.readlines(tempname, chomp: true)
-    end
+    assert_equal %w[1], fzf_output_lines
     tmux.until { |lines| assert_equal prompt, lines[-1] }
   end
 
@@ -1815,7 +1820,7 @@ def test_accept_non_empty
     tmux.send_keys '999'
     tmux.until { |lines| assert_equal '  1/1000', lines[-2] }
     tmux.send_keys :Enter
-    assert_equal %w[999 999], readonce.lines(chomp: true)
+    assert_equal %w[999 999], fzf_output_lines
   end
 
   def test_accept_non_empty_with_multi_selection
@@ -1827,7 +1832,7 @@ def test_accept_non_empty_with_multi_selection
     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
     tmux.send_keys :Enter
-    assert_equal %w[foo 1], readonce.lines(chomp: true)
+    assert_equal %w[foo 1], fzf_output_lines
   end
 
   def test_accept_non_empty_with_empty_list
@@ -1835,7 +1840,7 @@ def test_accept_non_empty_with_empty_list
     tmux.until { |lines| assert_equal '  0/0', lines[-2] }
     tmux.send_keys :Enter
     # 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
 
   def test_accept_or_print_query_without_match
@@ -1844,7 +1849,7 @@ def test_accept_or_print_query_without_match
     tmux.send_keys 99_999
     tmux.until { |lines| assert_equal 0, lines.match_count }
     tmux.send_keys :Enter
-    assert_equal %w[99999], readonce.lines(chomp: true)
+    assert_equal %w[99999], fzf_output_lines
   end
 
   def test_accept_or_print_query_with_match
@@ -1853,7 +1858,7 @@ def test_accept_or_print_query_with_match
     tmux.send_keys '^99$'
     tmux.until { |lines| assert_equal 1, lines.match_count }
     tmux.send_keys :Enter
-    assert_equal %w[99], readonce.lines(chomp: true)
+    assert_equal %w[99], fzf_output_lines
   end
 
   def test_accept_or_print_query_with_multi_selection
@@ -1864,7 +1869,7 @@ def test_accept_or_print_query_with_multi_selection
     tmux.send_keys 99_999
     tmux.until { |lines| assert_equal 0, lines.match_count }
     tmux.send_keys :Enter
-    assert_equal %w[1 2 3], readonce.lines(chomp: true)
+    assert_equal %w[1 2 3], fzf_output_lines
   end
 
   def test_preview_update_on_select
@@ -1886,7 +1891,7 @@ def test_escaped_meta_characters
       'foo bar',
       'bar foo'
     ]
-    writelines(tempname, input)
+    writelines(input)
 
     assert_equal input.length, `#{FZF} -f'foo bar' < #{tempname}`.lines.length
     assert_equal input.length - 1, `#{FZF} -f'^foo bar$' < #{tempname}`.lines.length
@@ -1912,7 +1917,7 @@ def test_inverse_only_search_should_not_sort_the_result
   end
 
   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.until { |lines| assert_includes lines[1], ' +       green ' }
   end
@@ -2102,7 +2107,7 @@ def test_clear_selection
   end
 
   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.send_keys '11'
     tmux.until { |lines| assert_equal '> 11', lines[-1] }
@@ -2116,7 +2121,7 @@ def test_backward_delete_char_eof
 
   def test_strip_xterm_osc_sequence
     %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)
       tmux.prepare
       tmux.send_keys \
@@ -2128,7 +2133,7 @@ def test_strip_xterm_osc_sequence
   end
 
   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') }
   end
 
@@ -2343,81 +2348,69 @@ def test_interrupt_execute
   end
 
   def test_kill_default_command_on_abort
-    script = tempname + '.sh'
-    writelines(script,
-               ['#!/usr/bin/env bash',
+    writelines(['#!/usr/bin/env bash',
                 "echo 'Started'",
                 '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.send_keys 'C-c'
     tmux.send_keys 'C-l', 'closed'
     tmux.until { |lines| assert_includes lines[0], 'closed' }
-    wait { refute system("pgrep -f #{script}") }
+    wait { refute system("pgrep -f #{tempname}") }
   ensure
-    system("pkill -9 -f #{script}")
-    FileUtils.rm_f(script)
+    system("pkill -9 -f #{tempname}")
   end
 
   def test_kill_default_command_on_accept
-    script = tempname + '.sh'
-    writelines(script,
-               ['#!/usr/bin/env bash',
+    writelines(['#!/usr/bin/env bash',
                 "echo 'Started'",
                 '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.send_keys :Enter
-    assert_equal 'Started', readonce.chomp
-    wait { refute system("pgrep -f #{script}") }
+    assert_equal 'Started', fzf_output
+    wait { refute system("pgrep -f #{tempname}") }
   ensure
-    system("pkill -9 -f #{script}")
-    FileUtils.rm_f(script)
+    system("pkill -9 -f #{tempname}")
   end
 
   def test_kill_reload_command_on_abort
-    script = tempname + '.sh'
-    writelines(script,
-               ['#!/usr/bin/env bash',
+    writelines(['#!/usr/bin/env bash',
                 "echo 'Started'",
                 '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.send_keys 'C-r'
     tmux.until { |lines| assert_equal 1, lines.item_count }
     tmux.send_keys 'C-c'
     tmux.send_keys 'C-l', 'closed'
     tmux.until { |lines| assert_includes lines[0], 'closed' }
-    wait { refute system("pgrep -f #{script}") }
+    wait { refute system("pgrep -f #{tempname}") }
   ensure
-    system("pkill -9 -f #{script}")
-    FileUtils.rm_f(script)
+    system("pkill -9 -f #{tempname}")
   end
 
   def test_kill_reload_command_on_accept
-    script = tempname + '.sh'
-    writelines(script,
-               ['#!/usr/bin/env bash',
+    writelines(['#!/usr/bin/env bash',
                 "echo 'Started'",
                 '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.send_keys 'C-r'
     tmux.until { |lines| assert_equal 1, lines.item_count }
     tmux.send_keys :Enter
-    assert_equal 'Started', readonce.chomp
-    wait { refute system("pgrep -f #{script}") }
+    assert_equal 'Started', fzf_output
+    wait { refute system("pgrep -f #{tempname}") }
   ensure
-    system("pkill -9 -f #{script}")
-    FileUtils.rm_f(script)
+    system("pkill -9 -f #{tempname}")
   end
 
   def test_preview_header
@@ -2759,8 +2752,9 @@ def test_ellipsis
 
   def assert_block(expected, lines)
     cols = expected.lines.map(&:chomp).map(&:length).max
-    actual = lines.reverse.take(expected.lines.length).reverse.map { _1[0, cols].rstrip + "\n" }.join
-    assert_equal_org expected, actual
+    top = lines.take(expected.lines.length).map { _1[0, cols].rstrip + "\n" }.join
+    bottom = lines.reverse.take(expected.lines.length).reverse.map { _1[0, cols].rstrip + "\n" }.join
+    assert_includes [top, bottom], expected
   end
 
   def test_height_range_fit
@@ -3275,6 +3269,55 @@ def test_fzf_pos
     tmux.send_keys '99'
     tmux.until { |lines| assert(lines.any? { |line| line.include?('0 / 0') }) }
   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
 
 module TestShell
@@ -3313,7 +3356,7 @@ def test_ctrl_t
   end
 
   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}")
 
     tmux.prepare
@@ -3397,12 +3440,16 @@ def test_ctrl_r
   end
 
   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.until { |lines| assert_equal %w[foo bar], lines[-2..] }
     tmux.prepare
     tmux.send_keys 'C-r'
     tmux.until { |lines| assert_equal '>', lines[-1] }
     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.send_keys :Enter
     tmux.until { |lines| assert lines[-1]&.match?(/bar"␊?/) }
@@ -3742,7 +3789,7 @@ def set_var(name, val)
 unset $(env | sed -n /^_fzf_orig/s/=.*//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
 # ---------