Repository: junegunn/fzf
Pull request #1878 (2020-02-18 03:17:14)

Add explanation for the `g:fzf_color` vim setting
Getting my vim colors to work with  fzf was quite a challenge. After some experimentation and "code diving" I think I've made sense of it. Adding some notes on this to the README-VIM file that may help others facing similar issues.

junegunn: Thanks.

> For the **fg+**, **bg+**, **hl+**, **prompt**, **pointer**, and **marker**
> elements there is an extra condition: the color only matches if the highlight
> group also has `term=bold` set.  This last bit can cause a lot of confusion.

I'm not sure about this. There is no such restriction, and if it works that way, it's probably a bug.

Patch:
diff --git a/.travis.yml b/.travis.yml
index 650de9d7..32b3b691 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,27 +1,22 @@
 language: go
-dist: xenial
+env: GO111MODULE=on
+os:
+  - linux
+  - osx
+dist: bionic # For fish >= 2.3.0 string builtin
 addons:
   apt:
-    sources:
-      - sourceline: "ppa:pi-rho/dev"
-      - sourceline: "ppa:fish-shell/release-2"
     packages:
-    - tmux
-    - zsh
-    - fish
-
-env:
-  - GO111MODULE=on
-
-jobs:
-  include:
-    - stage: unittest
-      go: "1.13.x"
-      script: make && make test
-
-    - stage: cli
-      go: "1.13.x"
-      rvm: "2.5"
-      script: |
-        make install && ./install --all && tmux new "ruby test/test_go.rb > out && touch ok" && cat out && [ -e ok ]
-
+      - fish
+      - zsh
+  homebrew:
+    packages:
+      - fish
+      - tmux
+    update: true
+script:
+  - make test
+  # LC_ALL=C to avoid escape codes in
+  # printf %q $'\355\205\214\354\212\244\355\212\270' on macOS. Bash on
+  # macOS is built without HANDLE_MULTIBYTE?
+  - make install && ./install --all && LC_ALL=C tmux new-session -d && ruby test/test_go.rb --verbose
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2c315509..1a16fd53 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,20 @@
 CHANGELOG
 =========
 
+0.21.0 (WIP)
+------------
+- Style changes
+    - Unicode spinner
+    - `--border` will now print border with rounded corners around the
+      finder instead of printing horizontal lines above and below it.
+      The previous style is available via `--border=horizontal`
+- Added `--pointer` and `--marker` options
+- `--height` option is now available on Windows binary (@kelleyma49)
+- More keys and actions for `--bind`
+- Bug fixes and improvements
+- Vim plugin: Floating windows support
+- bash: Various improvements in key bindings (CTRL-T, CTRL-R, ALT-C)
+
 0.20.0
 ------
 - Customizable preview window color (`preview-fg` and `preview-bg` for `--color`)
diff --git a/README-VIM.md b/README-VIM.md
index 6bf84fa7..a795d8e2 100644
--- a/README-VIM.md
+++ b/README-VIM.md
@@ -159,6 +159,53 @@ let g:fzf_colors =
 let g:fzf_history_dir = '~/.local/share/fzf-history'
 ```
 
+##### Explanation of `g:fzf_colors`
+
+`g:fzf_colors` is a dictionary mapping fzf elements to a color specification
+list:
+
+    element: [ component, group1 [, group2, ...] ]
+
+- `element` is an fzf element to apply a color to:
+
+  | Element               | Description                                           |
+  | ---                   | ---                                                   |
+  | `fg`  / `bg`  / `hl`  | Item (foreground / background / highlight)            |
+  | `fg+` / `bg+` / `hl+` | Current item (foreground / background / highlight)    |
+  | `hl`  / `hl+`         | Highlighted substrings (normal / current)             |
+  | `gutter`              | Background of the gutter on the left                  |
+  | `pointer`             | Pointer to the current line (`>`)                     |
+  | `marker`              | Multi-select marker (`>`)                             |
+  | `border`              | Border around the window (`--border` and `--preview`) |
+  | `header`              | Header (`--header` or `--header-lines`)               |
+  | `info`                | Info line (match counters)                            |
+  | `spinner`             | Streaming input indicator                             |
+  | `prompt`              | Prompt before query (`> `)                            |
+
+- `component` specifies the component (`fg` / `bg`) from which to extract the
+  color when considering each of the following highlight groups
+
+- `group1 [, group2, ...]` is a list of highlight groups that are searched (in
+  order) for a matching color definition
+
+For example, consider the following specification:
+
+```vim
+  'prompt':  ['fg', 'Conditional', 'Comment'],
+```
+
+This means we color the **prompt**
+- using the `fg` attribute of the `Conditional` if it exists,
+- otherwise use the `fg` attribute of the `Comment` highlight group if it exists,
+- otherwise fall back to the default color settings for the **prompt**.
+
+You can examine the color option generated according the setting by printing
+the result of `fzf#wrap()` function like so:
+
+```vim
+:echo fzf#wrap()
+```
+
 `fzf#run`
 ---------
 
diff --git a/README.md b/README.md
index e138ebb6..669881b2 100644
--- a/README.md
+++ b/README.md
@@ -160,6 +160,7 @@ method used.
 
 - git: `cd ~/.fzf && git pull && ./install`
 - brew: `brew update; brew reinstall fzf`
+- macports: `sudo port upgrade fzf`
 - chocolatey: `choco upgrade fzf`
 - vim-plug: `:PlugUpdate fzf`
 
@@ -403,6 +404,21 @@ _fzf_compgen_path() {
 _fzf_compgen_dir() {
   fd --type d --hidden --follow --exclude ".git" . "$1"
 }
+
+# (EXPERIMENTAL) Advanced customization of fzf options via _fzf_comprun function
+# - The first argument to the function is the name of the command.
+# - You should make sure to pass the rest of the arguments to fzf.
+_fzf_comprun() {
+  local command=$1
+  shift
+
+  case "$command" in
+    cd)           fzf "$@" --preview 'tree -C {} | head -200' ;;
+    export|unset) fzf "$@" --preview "eval 'echo \$'{}" "$@" ;;
+    ssh)          fzf "$@" --preview 'dig {}' ;;
+    *)            fzf "$@" ;;
+  esac
+}
 ```
 
 #### Supported commands
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index b7060af9..5c39f367 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 "Dec 2019" "fzf 0.20.0" "fzf - a command-line fuzzy finder"
+.TH fzf 1 "Mar 2020" "fzf 0.21.0" "fzf - a command-line fuzzy finder"
 
 .SH NAME
 fzf - a command-line fuzzy finder
@@ -178,8 +178,16 @@ Choose the layout (default: default)
 A synonym for \fB--layout=reverse\fB
 
 .TP
-.B "--border"
-Draw border above and below the finder
+.BI "--border" [=STYLE]
+Draw border around the finder
+
+.br
+.BR rounded "     Border with rounded corners (default)"
+.br
+.BR sharp "       Border with sharp corners"
+.br
+.BR horizontal "  Horizontal lines above and below the finder"
+.br
 
 .TP
 .B "--no-unicode"
@@ -280,8 +288,8 @@ format.
     \fBbg+        \fRBackground (current line)
     \fBgutter     \fRGutter on the left (defaults to \fBbg+\fR)
     \fBhl+        \fRHighlighted substrings (current line)
-    \fBinfo       \fRInfo
-    \fBborder     \fRBorder of the preview window and horizontal separators (\fB--border\fR)
+    \fBinfo       \fRInfo line (match counters)
+    \fBborder     \fRBorder around the window (\fB--border\fR and \fB--preview\fR)
     \fBprompt     \fRPrompt
     \fBpointer    \fRPointer to the current line
     \fBmarker     \fRMulti-select marker
@@ -600,6 +608,8 @@ e.g.
 .br
 \fIend\fR
 .br
+\fIinsert\fR
+.br
 \fIpgup\fR        (\fIpage-up\fR)
 .br
 \fIpgdn\fR        (\fIpage-down\fR)
@@ -631,62 +641,63 @@ or any single character
 .SS AVAILABLE ACTIONS:
 A key or an event can be bound to one or more of the following actions.
 
-  \fBACTION:               DEFAULT BINDINGS (NOTES):
-    \fBabort\fR                 \fIctrl-c  ctrl-g  ctrl-q  esc\fR
-    \fBaccept\fR                \fIenter   double-click\fR
-    \fBaccept-non-empty\fR      (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
-    \fBbackward-char\fR         \fIctrl-b  left\fR
-    \fBbackward-delete-char\fR  \fIctrl-h  bspace\fR
-    \fBbackward-kill-word\fR    \fIalt-bs\fR
-    \fBbackward-word\fR         \fIalt-b   shift-left\fR
-    \fBbeginning-of-line\fR     \fIctrl-a  home\fR
-    \fBcancel\fR                (clear query string if not empty, abort fzf otherwise)
-    \fBclear-screen\fR          \fIctrl-l\fR
-    \fBclear-selection\fR       (clear multi-selection)
-    \fBclear-query\fR           (clear query string)
-    \fBdelete-char\fR           \fIdel\fR
-    \fBdelete-char/eof\fR       \fIctrl-d\fR
-    \fBdeselect-all\fR          (deselect all matches)
-    \fBdown\fR                  \fIctrl-j  ctrl-n  down\fR
-    \fBend-of-line\fR           \fIctrl-e  end\fR
-    \fBexecute(...)\fR          (see below for the details)
-    \fBexecute-silent(...)\fR   (see below for the details)
-    \fRexecute-multi(...)\fR    (deprecated in favor of \fB{+}\fR expression)
-    \fBforward-char\fR          \fIctrl-f  right\fR
-    \fBforward-word\fR          \fIalt-f   shift-right\fR
+  \fBACTION:                   DEFAULT BINDINGS (NOTES):
+    \fBabort\fR                     \fIctrl-c  ctrl-g  ctrl-q  esc\fR
+    \fBaccept\fR                    \fIenter   double-click\fR
+    \fBaccept-non-empty\fR          (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
+    \fBbackward-char\fR             \fIctrl-b  left\fR
+    \fBbackward-delete-char\fR      \fIctrl-h  bspace\fR
+    \fBbackward-delete-char/eof\fR  (same as \fBbackward-delete-char\fR except aborts fzf if query is empty)
+    \fBbackward-kill-word\fR        \fIalt-bs\fR
+    \fBbackward-word\fR             \fIalt-b   shift-left\fR
+    \fBbeginning-of-line\fR         \fIctrl-a  home\fR
+    \fBcancel\fR                    (clear query string if not empty, abort fzf otherwise)
+    \fBclear-screen\fR              \fIctrl-l\fR
+    \fBclear-selection\fR           (clear multi-selection)
+    \fBclear-query\fR               (clear query string)
+    \fBdelete-char\fR               \fIdel\fR
+    \fBdelete-char/eof\fR           \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty)
+    \fBdeselect-all\fR              (deselect all matches)
+    \fBdown\fR                      \fIctrl-j  ctrl-n  down\fR
+    \fBend-of-line\fR               \fIctrl-e  end\fR
+    \fBexecute(...)\fR              (see below for the details)
+    \fBexecute-silent(...)\fR       (see below for the details)
+    \fRexecute-multi(...)\fR        (deprecated in favor of \fB{+}\fR expression)
+    \fBforward-char\fR              \fIctrl-f  right\fR
+    \fBforward-word\fR              \fIalt-f   shift-right\fR
     \fBignore\fR
-    \fBjump\fR                  (EasyMotion-like 2-keystroke movement)
-    \fBjump-accept\fR           (jump and accept)
+    \fBjump\fR                      (EasyMotion-like 2-keystroke movement)
+    \fBjump-accept\fR               (jump and accept)
     \fBkill-line\fR
-    \fBkill-word\fR             \fIalt-d\fR
-    \fBnext-history\fR          (\fIctrl-n\fR on \fB--history\fR)
-    \fBpage-down\fR             \fIpgdn\fR
-    \fBpage-up\fR               \fIpgup\fR
+    \fBkill-word\fR                 \fIalt-d\fR
+    \fBnext-history\fR              (\fIctrl-n\fR on \fB--history\fR)
+    \fBpage-down\fR                 \fIpgdn\fR
+    \fBpage-up\fR                   \fIpgup\fR
     \fBhalf-page-down\fR
     \fBhalf-page-up\fR
-    \fBpreview-down\fR          \fIshift-down\fR
-    \fBpreview-up\fR            \fIshift-up\fR
+    \fBpreview-down\fR              \fIshift-down\fR
+    \fBpreview-up\fR                \fIshift-up\fR
     \fBpreview-page-down\fR
     \fBpreview-page-up\fR
-    \fBprevious-history\fR      (\fIctrl-p\fR on \fB--history\fR)
-    \fBprint-query\fR           (print query and exit)
-    \fBreload(...)\fR           (see below for the details)
-    \fBreplace-query\fR         (replace query string with the current selection)
-    \fBselect-all\fR            (select all matches)
-    \fBtoggle\fR                (\fIright-click\fR)
-    \fBtoggle-all\fR            (toggle all matches)
-    \fBtoggle+down\fR           \fIctrl-i  (tab)\fR
-    \fBtoggle-in\fR             (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
-    \fBtoggle-out\fR            (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
+    \fBprevious-history\fR          (\fIctrl-p\fR on \fB--history\fR)
+    \fBprint-query\fR               (print query and exit)
+    \fBreload(...)\fR               (see below for the details)
+    \fBreplace-query\fR             (replace query string with the current selection)
+    \fBselect-all\fR                (select all matches)
+    \fBtoggle\fR                    (\fIright-click\fR)
+    \fBtoggle-all\fR                (toggle all matches)
+    \fBtoggle+down\fR               \fIctrl-i  (tab)\fR
+    \fBtoggle-in\fR                 (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
+    \fBtoggle-out\fR                (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
     \fBtoggle-preview\fR
     \fBtoggle-preview-wrap\fR
     \fBtoggle-sort\fR
-    \fBtoggle+up\fR             \fIbtab    (shift-tab)\fR
-    \fBtop\fR                   (move to the top result)
-    \fBunix-line-discard\fR     \fIctrl-u\fR
-    \fBunix-word-rubout\fR      \fIctrl-w\fR
-    \fBup\fR                    \fIctrl-k  ctrl-p  up\fR
-    \fByank\fR                  \fIctrl-y\fR
+    \fBtoggle+up\fR                 \fIbtab    (shift-tab)\fR
+    \fBtop\fR                       (move to the top result)
+    \fBunix-line-discard\fR         \fIctrl-u\fR
+    \fBunix-word-rubout\fR          \fIctrl-w\fR
+    \fBup\fR                        \fIctrl-k  ctrl-p  up\fR
+    \fByank\fR                      \fIctrl-y\fR
 
 .SS ACTION COMPOSITION
 
diff --git a/plugin/fzf.vim b/plugin/fzf.vim
index 17d1dce1..6fa3d14d 100644
--- a/plugin/fzf.vim
+++ b/plugin/fzf.vim
@@ -642,9 +642,11 @@ function! s:calc_size(max, val, dict)
   endif
 
   let opts = $FZF_DEFAULT_OPTS.' '.s:evaluate_opts(get(a:dict, 'options', ''))
-  let margin = stridx(opts, '--inline-info') > stridx(opts, '--no-inline-info') ? 1 : 2
+  let margin = match(opts, '--inline-info\|--info[^-]\{-}inline') > match(opts, '--no-inline-info\|--info[^-]\{-}\(default\|hidden\)') ? 1 : 2
   let margin += stridx(opts, '--border') > stridx(opts, '--no-border') ? 2 : 0
-  let margin += stridx(opts, '--header') > stridx(opts, '--no-header')
+  if stridx(opts, '--header') > stridx(opts, '--no-header')
+    let margin += len(split(opts, "\n"))
+  endif
   return srcsz >= 0 ? min([srcsz + margin, size]) : size
 endfunction
 
@@ -662,6 +664,9 @@ function! s:split(dict)
   try
     if s:present(a:dict, 'window')
       if type(a:dict.window) == type({})
+        if !has('nvim') && !has('patch-8.2.191')
+          throw 'Vim 8.2.191 or later is required for pop-up window'
+        end
         call s:popup(a:dict.window)
       else
         execute 'keepalt' a:dict.window
diff --git a/shell/completion.bash b/shell/completion.bash
index 9d52a6ba..bd94c51a 100644
--- a/shell/completion.bash
+++ b/shell/completion.bash
@@ -34,9 +34,16 @@ fi
 # To redraw line after fzf closes (printf '\e[5n')
 bind '"\e[0n": redraw-current-line'
 
-__fzfcmd_complete() {
-  [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] &&
-    echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
+__fzf_comprun() {
+  if [ "$(type -t _fzf_comprun 2>&1)" = function ]; then
+    _fzf_comprun "$@"
+  elif [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ]; then
+    shift
+    fzf-tmux -d "${FZF_TMUX_HEIGHT:-40%}" "$@"
+  else
+    shift
+    fzf "$@"
+  fi
 }
 
 __fzf_orig_completion_filter() {
@@ -142,8 +149,7 @@ _fzf_handle_dynamic_completion() {
 }
 
 __fzf_generic_path_completion() {
-  local cur base dir leftover matches trigger cmd fzf
-  fzf="$(__fzfcmd_complete)"
+  local cur base dir leftover matches trigger cmd
   cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
   COMPREPLY=()
   trigger=${FZF_COMPLETION_TRIGGER-'**'}
@@ -159,7 +165,7 @@ __fzf_generic_path_completion() {
         leftover=${leftover/#\/}
         [ -z "$dir" ] && dir='.'
         [ "$dir" != "/" ] && dir="${dir/%\//}"
-        matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $2 -q "$leftover" | while read -r item; do
+        matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $2" __fzf_comprun "$4" -q "$leftover" | while read -r item; do
           printf "%q$3 " "$item"
         done)
         matches=${matches% }
@@ -184,10 +190,9 @@ __fzf_generic_path_completion() {
 }
 
 _fzf_complete() {
-  local cur selected trigger cmd fzf post
+  local cur selected trigger cmd post
   post="$(caller 0 | awk '{print $2}')_post"
   type -t "$post" > /dev/null 2>&1 || post=cat
-  fzf="$(__fzfcmd_complete)"
 
   cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
   trigger=${FZF_COMPLETION_TRIGGER-'**'}
@@ -195,7 +200,7 @@ _fzf_complete() {
   if [[ "$cur" == *"$trigger" ]]; then
     cur=${cur:0:${#cur}-${#trigger}}
 
-    selected=$(cat | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $1 -q "$cur" | $post | tr '\n' ' ')
+    selected=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $1" __fzf_comprun "$2" -q "$cur" | $post | tr '\n' ' ')
     selected=${selected% } # Strip trailing space not to repeat "-o nospace"
     if [ -n "$selected" ]; then
       COMPREPLY=("$selected")
@@ -226,9 +231,9 @@ _fzf_dir_completion() {
 _fzf_complete_kill() {
   [ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1
 
-  local selected fzf
-  fzf="$(__fzfcmd_complete)"
-  selected=$(command ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" $fzf -m | awk '{print $2}' | tr '\n' ' ')
+  local selected
+  selected=$(command ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS --preview 'echo {}' --preview-window down:3:wrap" __fzf_comprun "kill" -m | awk '{print $2}' | tr '\n' ' ')
+  selected=${selected% }
   printf '\e[5n'
 
   if [ -n "$selected" ]; then
@@ -321,8 +326,8 @@ _fzf_setup_completion() {
     return 1
   fi
   shift
+  eval "$(complete -p "$@" 2> /dev/null | grep -v "$fn" | __fzf_orig_completion_filter)"
   for cmd in "$@"; do
-    eval "$(complete -p "$cmd" 2> /dev/null | grep -v "$fn" | __fzf_orig_completion_filter)"
     case "$kind" in
       dir)   __fzf_defc "$cmd" "$fn" "-o nospace -o dirnames" ;;
       var)   __fzf_defc "$cmd" "$fn" "-o default -o nospace -v" ;;
diff --git a/shell/completion.zsh b/shell/completion.zsh
index 6adb874a..95f6d685 100644
--- a/shell/completion.zsh
+++ b/shell/completion.zsh
@@ -31,20 +31,40 @@ fi
 
 ###########################################################
 
-__fzfcmd_complete() {
-  [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] &&
-    echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
+__fzf_comprun() {
+  if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then
+    _fzf_comprun "$@"
+  elif [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ]; then
+    shift
+    fzf-tmux -d "${FZF_TMUX_HEIGHT:-40%}" "$@"
+  else
+    shift
+    fzf "$@"
+  fi
+}
+
+# Extract the name of the command. e.g. foo=1 bar baz**<tab>
+__fzf_extract_command() {
+  local token tokens
+  tokens=(${(z)1})
+  for token in $tokens; do
+    if [[ "$token" =~ [[:alnum:]] && ! "$token" =~ "=" ]]; then
+      echo "$token"
+      return
+    fi
+  done
+  echo "${tokens[1]}"
 }
 
 __fzf_generic_path_completion() {
-  local base lbuf compgen fzf_opts suffix tail fzf dir leftover matches
+  local base lbuf cmd compgen fzf_opts suffix tail dir leftover matches
   base=$1
   lbuf=$2
+  cmd=$(__fzf_extract_command "$lbuf")
   compgen=$3
   fzf_opts=$4
   suffix=$5
   tail=$6
-  fzf="$(__fzfcmd_complete)"
 
   setopt localoptions nonomatch
   eval "base=$base"
@@ -55,7 +75,7 @@ __fzf_generic_path_completion() {
       leftover=${leftover/#\/}
       [ -z "$dir" ] && dir='.'
       [ "$dir" != "/" ] && dir="${dir/%\//}"
-      matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${(Q)${(Z+n+)fzf}} ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" | while read item; do
+      matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" | while read item; do
         echo -n "${(q)item}$suffix "
       done)
       matches=${matches% }
@@ -87,17 +107,16 @@ _fzf_feed_fifo() (
 )
 
 _fzf_complete() {
-  local fifo fzf_opts lbuf fzf matches post
+  local fifo fzf_opts lbuf cmd matches post
   fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$"
   fzf_opts=$1
   lbuf=$2
+  cmd=$(__fzf_extract_command "$lbuf")
   post="${funcstack[2]}_post"
   type $post > /dev/null 2>&1 || post=cat
 
-  fzf="$(__fzfcmd_complete)"
-
   _fzf_feed_fifo "$fifo"
-  matches=$(cat "$fifo" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${(Q)${(Z+n+)fzf}} ${(Q)${(Z+n+)fzf_opts}} -q "${(Q)prefix}" | $post | tr '\n' ' ')
+  matches=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
   if [ -n "$matches" ]; then
     LBUFFER="$lbuf$matches"
   fi
@@ -141,7 +160,7 @@ _fzf_complete_unalias() {
 }
 
 fzf-completion() {
-  local tokens cmd prefix trigger tail fzf matches lbuf d_cmds
+  local tokens cmd prefix trigger tail matches lbuf d_cmds
   setopt localoptions noshwordsplit noksh_arrays noposixbuiltins
 
   # http://zsh.sourceforge.net/FAQ/zshfaq03.html
@@ -152,7 +171,7 @@ fzf-completion() {
     return
   fi
 
-  cmd=${tokens[1]}
+  cmd=$(__fzf_extract_command "$LBUFFER")
 
   # Explicitly allow for empty trigger.
   trigger=${FZF_COMPLETION_TRIGGER-'**'}
@@ -167,8 +186,7 @@ fzf-completion() {
   tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))}
   # Kill completion (do not require trigger sequence)
   if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then
-    fzf="$(__fzfcmd_complete)"
-    matches=$(command ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" ${(Q)${(Z+n+)fzf}} -m | awk '{print $2}' | tr '\n' ' ')
+    matches=$(command ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS --preview 'echo {}' --preview-window down:3:wrap" __fzf_comprun "$cmd" -m | awk '{print $2}' | tr '\n' ' ')
     if [ -n "$matches" ]; then
       LBUFFER="$LBUFFER$matches"
     fi
diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash
index d411992d..f05c3b50 100644
--- a/shell/key-bindings.bash
+++ b/shell/key-bindings.bash
@@ -51,69 +51,57 @@ __fzf_cd__() {
   dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m) && printf 'cd %q' "$dir"
 }
 
-__fzf_history__() (
-  local line
-  shopt -u nocaseglob nocasematch
-  line=$(
-    HISTTIMEFORMAT= builtin history |
-    FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS --tac --sync -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m" $(__fzfcmd) |
-    command grep '^ *[0-9]') &&
-    if [[ $- =~ H ]]; then
-      sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line"
-    else
-      sed 's/^ *\([0-9]*\)\** *//' <<< "$line"
-    fi
-)
+__fzf_history__() {
+  local output
+  output=$(
+    builtin fc -lnr -2147483648 |
+      last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -p -l0 -e 'BEGIN { getc; $/ = "\n\t"; $HISTCMD = $ENV{last_hist} + 1 } s/^[ *]//; $_ = $HISTCMD - $. . "\t$_"' |
+      FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m --read0" $(__fzfcmd) --query "$READLINE_LINE"
+  ) || return
+  READLINE_LINE=${output#*$'\t'}
+  if [ -z "$READLINE_POINT" ]; then
+    echo "$READLINE_LINE"
+  else
+    READLINE_POINT=0x7fffffff
+  fi
+}
 
 # Required to refresh the prompt after fzf
 bind -m emacs-standard '"\er": redraw-current-line'
-bind -m emacs-standard '"\e^": history-expand-line'
 
-# CTRL-T - Paste the selected file path into the command line
-if [ $BASH_VERSINFO -gt 3 ]; then
-  bind -m emacs-standard -x '"\C-t": "fzf-file-widget"'
-elif __fzf_use_tmux__; then
-  bind -m emacs-standard '"\C-t": " \C-u \C-a\C-k`__fzf_select_tmux__`\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"'
-else
-  bind -m emacs-standard '"\C-t": " \C-u \C-a\C-k`__fzf_select__`\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"'
-fi
-
-# CTRL-R - Paste the selected command from history into the command line
-bind -m emacs-standard '"\C-r": " \C-e\C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\er\e^"'
+bind -m vi-command '"\C-z": emacs-editing-mode'
+bind -m vi-insert '"\C-z": emacs-editing-mode'
+bind -m emacs-standard '"\C-z": vi-editing-mode'
 
-# ALT-C - cd into the selected directory
-bind -m emacs-standard '"\ec": " \C-e\C-u`__fzf_cd__`\e\C-e\er\C-m"'
-
-# We'd usually use "\e" to enter vi-movement-mode so we can do our magic,
-# but this incurs a very noticeable delay of a half second or so,
-# because many other commands start with "\e".
-# Instead, we bind an unused key, "\C-x\C-a",
-# to also enter vi-movement-mode,
-# and then use that thereafter.
-# (We imagine that "\C-x\C-a" is relatively unlikely to be in use.)
-bind -m vi-insert '"\C-x\C-a": vi-movement-mode'
-
-bind -m vi-insert '"\C-x\C-e": shell-expand-line'
-bind -m vi-insert '"\C-x\C-r": redraw-current-line'
-bind -m vi-insert '"\C-x^": history-expand-line'
+if [ "${BASH_VERSINFO[0]}" -lt 4 ]; then
+  # CTRL-T - Paste the selected file path into the command line
+  if __fzf_use_tmux__; then
+    bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select_tmux__`\e\C-e\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"'
+  else
+    bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"'
+  fi
+  bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
+  bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
 
-# CTRL-T - Paste the selected file path into the command line
-# - FIXME: Selected items are attached to the end regardless of cursor position
-if [ $BASH_VERSINFO -gt 3 ]; then
-  bind -m vi-insert -x '"\C-t": "fzf-file-widget"'
-elif __fzf_use_tmux__; then
-  bind -m vi-insert '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select_tmux__`\C-x\C-e\C-x\C-a0P$xa"'
+  # CTRL-R - Paste the selected command from history into the command line
+  bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u"$(__fzf_history__)"\e\C-e\er"'
+  bind -m vi-command '"\C-r": "\C-z\C-r\C-z"'
+  bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
 else
-  bind -m vi-insert '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select__`\C-x\C-e\C-x\C-a0Px$a \C-x\C-r\C-x\C-axa "'
+  # CTRL-T - Paste the selected file path into the command line
+  bind -m emacs-standard -x '"\C-t": fzf-file-widget'
+  bind -m vi-command -x '"\C-t": fzf-file-widget'
+  bind -m vi-insert -x '"\C-t": fzf-file-widget'
+
+  # CTRL-R - Paste the selected command from history into the command line
+  bind -m emacs-standard -x '"\C-r": __fzf_history__'
+  bind -m vi-command -x '"\C-r": __fzf_history__'
+  bind -m vi-insert -x '"\C-r": __fzf_history__'
 fi
-bind -m vi-command '"\C-t": "i\C-t"'
-
-# CTRL-R - Paste the selected command from history into the command line
-bind -m vi-insert '"\C-r": "\C-x\C-addi`__fzf_history__`\C-x\C-e\C-x\C-r\C-x^\C-x\C-a$a"'
-bind -m vi-command '"\C-r": "i\C-r"'
 
 # ALT-C - cd into the selected directory
-bind -m vi-insert '"\ec": "\C-x\C-addi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"'
-bind -m vi-command '"\ec": "ddi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"'
+bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"'
+bind -m vi-command '"\ec": "\C-z\ec\C-z"'
+bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
 
 fi
diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish
index 035ba55e..c4bad838 100644
--- a/shell/key-bindings.fish
+++ b/shell/key-bindings.fish
@@ -10,13 +10,13 @@ function fzf_key_bindings
 
     # "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not
     # $dir itself, even if hidden.
-    set -q FZF_CTRL_T_COMMAND; or set -l FZF_CTRL_T_COMMAND "
+    test -n "$FZF_CTRL_T_COMMAND"; or set -l FZF_CTRL_T_COMMAND "
     command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
     -o -type f -print \
     -o -type d -print \
     -o -type l -print 2> /dev/null | sed 's@^\./@@'"
 
-    set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
+    test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
     begin
       set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS"
       eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
@@ -36,7 +36,7 @@ function fzf_key_bindings
   end
 
   function fzf-history-widget -d "Show command history"
-    set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
+    test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
     begin
       set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m"
 
@@ -62,10 +62,10 @@ function fzf_key_bindings
     set -l dir $commandline[1]
     set -l fzf_query $commandline[2]
 
-    set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND "
+    test -n "$FZF_ALT_C_COMMAND"; or set -l FZF_ALT_C_COMMAND "
     command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
     -o -type d -print 2> /dev/null | sed 's@^\./@@'"
-    set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
+    test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
     begin
       set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS"
       eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
@@ -82,8 +82,8 @@ function fzf_key_bindings
   end
 
   function __fzfcmd
-    set -q FZF_TMUX; or set FZF_TMUX 0
-    set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
+    test -n "$FZF_TMUX"; or set FZF_TMUX 0
+    test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
     if [ $FZF_TMUX -eq 1 ]
       echo "fzf-tmux -d$FZF_TMUX_HEIGHT"
     else
diff --git a/src/algo/algo.go b/src/algo/algo.go
index a627514c..b3f29e66 100644
--- a/src/algo/algo.go
+++ b/src/algo/algo.go
@@ -773,12 +773,17 @@ func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text *util.Ch
 		return Result{0, 0, 0}, nil
 	}
 
-	if text.Length() < len(pattern) {
+	trimmedLen := 0
+	if !unicode.IsSpace(pattern[0]) {
+		trimmedLen = text.LeadingWhitespaces()
+	}
+
+	if text.Length()-trimmedLen < len(pattern) {
 		return Result{-1, -1, 0}, nil
 	}
 
 	for index, r := range pattern {
-		char := text.Get(index)
+		char := text.Get(trimmedLen + index)
 		if !caseSensitive {
 			char = unicode.ToLower(char)
 		}
@@ -790,14 +795,17 @@ func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text *util.Ch
 		}
 	}
 	lenPattern := len(pattern)
-	score, _ := calculateScore(caseSensitive, normalize, text, pattern, 0, lenPattern, false)
-	return Result{0, lenPattern, score}, nil
+	score, _ := calculateScore(caseSensitive, normalize, text, pattern, trimmedLen, trimmedLen+lenPattern, false)
+	return Result{trimmedLen, trimmedLen + lenPattern, score}, nil
 }
 
 // SuffixMatch performs suffix-match
 func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
 	lenRunes := text.Length()
-	trimmedLen := lenRunes - text.TrailingWhitespaces()
+	trimmedLen := lenRunes
+	if len(pattern) == 0 || !unicode.IsSpace(pattern[len(pattern)-1]) {
+		trimmedLen -= text.TrailingWhitespaces()
+	}
 	if len(pattern) == 0 {
 		return Result{trimmedLen, trimmedLen, 0}, nil
 	}
@@ -828,14 +836,30 @@ func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text *util.Ch
 // EqualMatch performs equal-match
 func EqualMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
 	lenPattern := len(pattern)
-	if text.Length() != lenPattern {
+	if lenPattern == 0 {
+		return Result{-1, -1, 0}, nil
+	}
+
+	// Strip leading whitespaces
+	trimmedLen := 0
+	if !unicode.IsSpace(pattern[0]) {
+		trimmedLen = text.LeadingWhitespaces()
+	}
+
+	// Strip trailing whitespaces
+	trimmedEndLen := 0
+	if !unicode.IsSpace(pattern[lenPattern-1]) {
+		trimmedEndLen = text.TrailingWhitespaces()
+	}
+
+	if text.Length()-trimmedLen-trimmedEndLen != lenPattern {
 		return Result{-1, -1, 0}, nil
 	}
 	match := true
 	if normalize {
 		runes := text.ToRunes()
 		for idx, pchar := range pattern {
-			char := runes[idx]
+			char := runes[trimmedLen+idx]
 			if !caseSensitive {
 				char = unicode.To(unicode.LowerCase, char)
 			}
@@ -845,14 +869,15 @@ func EqualMatch(caseSensitive bool, normalize bool, forward bool, text *util.Cha
 			}
 		}
 	} else {
-		runesStr := text.ToString()
+		runes := text.ToRunes()
+		runesStr := string(runes[trimmedLen : len(runes)-trimmedEndLen])
 		if !caseSensitive {
 			runesStr = strings.ToLower(runesStr)
 		}
 		match = runesStr == string(pattern)
 	}
 	if match {
-		return Result{0, lenPattern, (scoreMatch+bonusBoundary)*lenPattern +
+		return Result{trimmedLen, trimmedLen + lenPattern, (scoreMatch+bonusBoundary)*lenPattern +
 			(bonusFirstCharMultiplier-1)*bonusBoundary}, nil
 	}
 	return Result{-1, -1, 0}, nil
diff --git a/src/ansi.go b/src/ansi.go
index b5b3a203..b15bd91c 100644
--- a/src/ansi.go
+++ b/src/ansi.go
@@ -90,10 +90,11 @@ func init() {
 		- http://ascii-table.com/ansi-escape-sequences.php
 		- http://ascii-table.com/ansi-escape-sequences-vt-100.php
 		- http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
+		- https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
 	*/
 	// The following regular expression will include not all but most of the
 	// frequently used ANSI sequences
-	ansiRegex = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b.|[\x0e\x0f]|.\x08)")
+	ansiRegex = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)")
 }
 
 func findAnsiStart(str string) int {
diff --git a/src/options.go b/src/options.go
index 3bbeb1c5..0fd97050 100644
--- a/src/options.go
+++ b/src/options.go
@@ -11,7 +11,6 @@ import (
 
 	"github.com/junegunn/fzf/src/algo"
 	"github.com/junegunn/fzf/src/tui"
-	"github.com/junegunn/fzf/src/util"
 
 	"github.com/mattn/go-runewidth"
 	"github.com/mattn/go-shellwords"
@@ -57,7 +56,8 @@ const usage = `usage: fzf [options]
     --min-height=HEIGHT   Minimum height when --height is given in percent
                           (default: 10)
     --layout=LAYOUT       Choose layout: [default|reverse|reverse-list]
-    --border              Draw border above and below the finder
+    --border[=STYLE]      Draw border around the finder
+                          [rounded|sharp|horizontal] (default: rounded)
     --margin=MARGIN       Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
     --info=STYLE          Finder info style [default|inline|hidden]
     --prompt=STR          Input prompt (default: '> ')
@@ -212,7 +212,7 @@ type Options struct {
 	Header      []string
 	HeaderLines int
 	Margin      [4]sizeSpec
-	Bordered    bool
+	BorderShape tui.BorderShape
 	Unicode     bool
 	Tabstop     int
 	ClearOnExit bool
@@ -301,12 +301,12 @@ func nextString(args []string, i *int, message string) string {
 	return args[*i]
 }
 
-func optionalNextString(args []string, i *int) string {
-	if len(args) > *i+1 && !strings.HasPrefix(args[*i+1], "-") {
+func optionalNextString(args []string, i *int) (bool, string) {
+	if len(args) > *i+1 && !strings.HasPrefix(args[*i+1], "-") && !strings.HasPrefix(args[*i+1], "+") {
 		*i++
-		return args[*i]
+		return true, args[*i]
 	}
-	return ""
+	return false, ""
 }
 
 func atoi(str string) int {
@@ -400,6 +400,23 @@ func parseAlgo(str string) algo.Algo {
 	return algo.FuzzyMatchV2
 }
 
+func parseBorder(str string, optional bool) tui.BorderShape {
+	switch str {
+	case "rounded":
+		return tui.BorderRounded
+	case "sharp":
+		return tui.BorderSharp
+	case "horizontal":
+		return tui.BorderHorizontal
+	default:
+		if optional && str == "" {
+			return tui.BorderRounded
+		}
+		errorExit("invalid border style (expected: rounded|sharp|horizontal)")
+	}
+	return tui.BorderNone
+}
+
 func parseKeyChords(str string, message string) map[int]string {
 	if len(str) == 0 {
 		errorExit(message)
@@ -472,6 +489,8 @@ func parseKeyChords(str string, message string) map[int]string {
 			chord = tui.Home
 		case "end":
 			chord = tui.End
+		case "insert":
+			chord = tui.Insert
 		case "pgup", "page-up":
 			chord = tui.PgUp
 		case "pgdn", "page-down":
@@ -738,6 +757,8 @@ func parseKeymap(keymap map[int][]action, str string) {
 				appendAction(actBackwardChar)
 			case "backward-delete-char":
 				appendAction(actBackwardDeleteChar)
+			case "backward-delete-char/eof":
+				appendAction(actBackwardDeleteCharEOF)
 			case "backward-word":
 				appendAction(actBackwardWord)
 			case "clear-screen":
@@ -1094,7 +1115,7 @@ func parseOptions(opts *Options, allArgs []string) {
 		case "--bind":
 			parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required"))
 		case "--color":
-			spec := optionalNextString(allArgs, &i)
+			_, spec := optionalNextString(allArgs, &i)
 			if len(spec) == 0 {
 				opts.Theme = tui.EmptyTheme()
 			} else {
@@ -1242,9 +1263,10 @@ func parseOptions(opts *Options, allArgs []string) {
 		case "--no-margin":
 			opts.Margin = defaultMargin()
 		case "--no-border":
-			opts.Bordered = false
+			opts.BorderShape = tui.BorderNone
 		case "--border":
-			opts.Bordered = true
+			hasArg, arg := optionalNextString(allArgs, &i)
+			opts.BorderShape = parseBorder(arg, !hasArg)
 		case "--no-unicode":
 			opts.Unicode = false
 		case "--unicode":
@@ -1269,6 +1291,8 @@ func parseOptions(opts *Options, allArgs []string) {
 				opts.Filter = &value
 			} else if match, value := optString(arg, "-d", "--delimiter="); match {
 				opts.Delimiter = delimiterRegexp(value)
+			} else if match, value := optString(arg, "--border="); match {
+				opts.BorderShape = parseBorder(value, false)
 			} else if match, value := optString(arg, "--prompt="); match {
 				opts.Prompt = value
 			} else if match, value := optString(arg, "--pointer="); match {
@@ -1387,8 +1411,8 @@ func validateSign(sign string, signOptName string) error {
 }
 
 func postProcessOptions(opts *Options) {
-	if util.IsWindows() && opts.Height.size > 0 {
-		errorExit("--height option is currently not supported on Windows")
+	if !tui.IsLightRendererSupported() && opts.Height.size > 0 {
+		errorExit("--height option is currently not supported on this platform")
 	}
 	// Default actions for CTRL-N / CTRL-P when --history is set
 	if opts.History != nil {
diff --git a/src/terminal.go b/src/terminal.go
index bc975546..a6a5c5ba 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -100,7 +100,7 @@ type Terminal struct {
 	margin       [4]sizeSpec
 	strong       tui.Attr
 	unicode      bool
-	bordered     bool
+	borderShape  tui.BorderShape
 	cleanExit    bool
 	border       tui.Window
 	window       tui.Window
@@ -187,6 +187,7 @@ const (
 	actAcceptNonEmpty
 	actBackwardChar
 	actBackwardDeleteChar
+	actBackwardDeleteCharEOF
 	actBackwardWord
 	actCancel
 	actClearScreen
@@ -369,9 +370,9 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
 				effectiveMinHeight *= 2
 			}
 			if opts.InfoStyle != infoDefault {
-				effectiveMinHeight -= 1
+				effectiveMinHeight--
 			}
-			if opts.Bordered {
+			if opts.BorderShape != tui.BorderNone {
 				effectiveMinHeight += 2
 			}
 			return util.Min(termHeight, util.Max(maxHeight, effectiveMinHeight))
@@ -390,62 +391,62 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
 		spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
 	}
 	t := Terminal{
-		initDelay:  delay,
-		infoStyle:  opts.InfoStyle,
-		spinner:    spinner,
-		queryLen:   [2]int{0, 0},
-		layout:     opts.Layout,
-		fullscreen: fullscreen,
-		hscroll:    opts.Hscroll,
-		hscrollOff: opts.HscrollOff,
-		wordRubout: wordRubout,
-		wordNext:   wordNext,
-		cx:         len(input),
-		cy:         0,
-		offset:     0,
-		xoffset:    0,
-		yanked:     []rune{},
-		input:      input,
-		multi:      opts.Multi,
-		sort:       opts.Sort > 0,
-		toggleSort: opts.ToggleSort,
-		delimiter:  opts.Delimiter,
-		expect:     opts.Expect,
-		keymap:     opts.Keymap,
-		pressed:    "",
-		printQuery: opts.PrintQuery,
-		history:    opts.History,
-		margin:     opts.Margin,
-		unicode:    opts.Unicode,
-		bordered:   opts.Bordered,
-		cleanExit:  opts.ClearOnExit,
-		strong:     strongAttr,
-		cycle:      opts.Cycle,
-		header:     header,
-		header0:    header,
-		ansi:       opts.Ansi,
-		tabstop:    opts.Tabstop,
-		reading:    true,
-		failed:     nil,
-		jumping:    jumpDisabled,
-		jumpLabels: opts.JumpLabels,
-		printer:    opts.Printer,
-		printsep:   opts.PrintSep,
-		merger:     EmptyMerger,
-		selected:   make(map[int32]selectedItem),
-		reqBox:     util.NewEventBox(),
-		preview:    opts.Preview,
-		previewer:  previewer{"", 0, 0, previewBox != nil && !opts.Preview.hidden, false},
-		previewBox: previewBox,
-		eventBox:   eventBox,
-		mutex:      sync.Mutex{},
-		suppress:   true,
-		slab:       util.MakeSlab(slab16Size, slab32Size),
-		theme:      opts.Theme,
-		startChan:  make(chan bool, 1),
-		killChan:   make(chan int),
-		tui:        renderer,
-		initFunc:   func() { renderer.Init() }}
+		initDelay:   delay,
+		infoStyle:   opts.InfoStyle,
+		spinner:     spinner,
+		queryLen:    [2]int{0, 0},
+		layout:      opts.Layout,
+		fullscreen:  fullscreen,
+		hscroll:     opts.Hscroll,
+		hscrollOff:  opts.HscrollOff,
+		wordRubout:  wordRubout,
+		wordNext:    wordNext,
+		cx:          len(input),
+		cy:          0,
+		offset:      0,
+		xoffset:     0,
+		yanked:      []rune{},
+		input:       input,
+		multi:       opts.Multi,
+		sort:        opts.Sort > 0,
+		toggleSort:  opts.ToggleSort,
+		delimiter:   opts.Delimiter,
+		expect:      opts.Expect,
+		keymap:      opts.Keymap,
+		pressed:     "",
+		printQuery:  opts.PrintQuery,
+		history:     opts.History,
+		margin:      opts.Margin,
+		unicode:     opts.Unicode,
+		borderShape: opts.BorderShape,
+		cleanExit:   opts.ClearOnExit,
+		strong:      strongAttr,
+		cycle:       opts.Cycle,
+		header:      header,
+		header0:     header,
+		ansi:        opts.Ansi,
+		tabstop:     opts.Tabstop,
+		reading:     true,
+		failed:      nil,
+		jumping:     jumpDisabled,
+		jumpLabels:  opts.JumpLabels,
+		printer:     opts.Printer,
+		printsep:    opts.PrintSep,
+		merger:      EmptyMerger,
+		selected:    make(map[int32]selectedItem),
+		reqBox:      util.NewEventBox(),
+		preview:     opts.Preview,
+		previewer:   previewer{"", 0, 0, previewBox != nil && !opts.Preview.hidden, false},
+		previewBox:  previewBox,
+		eventBox:    eventBox,
+		mutex:       sync.Mutex{},
+		suppress:    true,
+		slab:        util.MakeSlab(slab16Size, slab32Size),
+		theme:       opts.Theme,
+		startChan:   make(chan bool, 1),
+		killChan:    make(chan int),
+		tui:         renderer,
+		initFunc:    func() { renderer.Init() }}
 	t.prompt, t.promptLen = t.processTabs([]rune(opts.Prompt), 0)
 	t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0)
 	t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0)
@@ -594,8 +595,11 @@ func (t *Terminal) resizeWindows() {
 		} else {
 			marginInt[idx] = int(sizeSpec.size)
 		}
-		if t.bordered && idx%2 == 0 {
-			marginInt[idx] += 1
+		switch t.borderShape {
+		case tui.BorderHorizontal:
+			marginInt[idx] += 1 - idx%2
+		case tui.BorderRounded, tui.BorderSharp:
+			marginInt[idx] += 1 + idx%2
 		}
 	}
 	adjust := func(idx1 int, idx2 int, max int, min int) {
@@ -635,18 +639,26 @@ func (t *Terminal) resizeWindows() {
 
 	width := screenWidth - marginInt[1] - marginInt[3]
 	height := screenHeight - marginInt[0] - marginInt[2]
-	if t.bordered {
+	switch t.borderShape {
+	case tui.BorderHorizontal:
 		t.border = t.tui.NewWindow(
 			marginInt[0]-1,
 			marginInt[3],
 			width,
 			height+2,
 			false, tui.MakeBorderStyle(tui.BorderHorizontal, t.unicode))
+	case tui.BorderRounded, tui.BorderSharp:
+		t.border = t.tui.NewWindow(
+			marginInt[0]-1,
+			marginInt[3]-2,
+			width+4,
+			height+2,
+			false, tui.MakeBorderStyle(t.borderShape, t.unicode))
 	}
 	noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
 	if previewVisible {
 		createPreviewWindow := func(y int, x int, w int, h int) {
-			previewBorder := tui.MakeBorderStyle(tui.BorderAround, t.unicode)
+			previewBorder := tui.MakeBorderStyle(tui.BorderRounded, t.unicode)
 			if !t.preview.border {
 				previewBorder = tui.MakeTransparentBorder()
 			}
@@ -1145,7 +1157,7 @@ func (t *Terminal) refresh() {
 	t.placeCursor()
 	if !t.suppress {
 		windows := make([]tui.Window, 0, 4)
-		if t.bordered {
+		if t.borderShape != tui.BorderNone {
 			windows = append(windows, t.border)
 		}
 		if t.hasPreviewWindow() {
@@ -1846,6 +1858,13 @@ func (t *Terminal) Loop() {
 					t.input = []rune{}
 					t.cx = 0
 				}
+			case actBackwardDeleteCharEOF:
+				if len(t.input) == 0 {
+					req(reqQuit)
+				} else if t.cx > 0 {
+					t.input = append(t.input[:t.cx-1], t.input[t.cx:]...)
+					t.cx--
+				}
 			case actForwardChar:
 				if t.cx < len(t.input) {
 					t.cx++
diff --git a/src/tui/light.go b/src/tui/light.go
index c32a1baa..3ff84870 100644
--- a/src/tui/light.go
+++ b/src/tui/light.go
@@ -7,7 +7,6 @@ import (
 	"regexp"
 	"strconv"
 	"strings"
-	"syscall"
 	"time"
 	"unicode/utf8"
 
@@ -23,27 +22,13 @@ const (
 	defaultEscDelay = 100
 	escPollInterval = 5
 	offsetPollTries = 10
+	maxInputBuffer  = 10 * 1024
 )
 
 const consoleDevice string = "/dev/tty"
 
 var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
 
-func openTtyIn() *os.File {
-	in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
-	if err != nil {
-		tty := ttyname()
-		if len(tty) > 0 {
-			if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
-				return in
-			}
-		}
-		fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice)
-		os.Exit(2)
-	}
-	return in
-}
-
 func (r *LightRenderer) stderr(str string) {
 	r.stderrInternal(str, true)
 }
@@ -100,11 +85,19 @@ type LightRenderer struct {
 	y             int
 	x             int
 	maxHeightFunc func(int) int
+
+	// Windows only
+	ttyinChannel    chan byte
+	inHandle        uintptr
+	outHandle       uintptr
+	origStateInput  uint32
+	origStateOutput uint32
 }
 
 type LightWindow struct {
 	renderer *LightRenderer
 	colored  bool
+	preview  bool
 	border   BorderStyle
 	top      int
 	left     int
@@ -132,10 +125,6 @@ func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop in
 	return &r
 }
 
-func (r *LightRenderer) fd() int {
-	return int(r.ttyin.Fd())
-}
-
 func (r *LightRenderer) defaultTheme() *ColorTheme {
 	if strings.Contains(os.Getenv("TERM"), "256") {
 		return Dark256
@@ -147,22 +136,6 @@ func (r *LightRenderer) defaultTheme() *ColorTheme {
 	return Default16
 }
 
-func (r *LightRenderer) findOffset() (row int, col int) {
-	r.csi("6n")
-	r.flush()
-	bytes := []byte{}
-	for tries := 0; tries < offsetPollTries; tries++ {
-		bytes = r.getBytesInternal(bytes, tries > 0)
-		offsets := offsetRegexp.FindSubmatch(bytes)
-		if len(offsets) > 3 {
-			// add anything we skipped over to the input buffer
-			r.buffer = append(r.buffer, offsets[1]...)
-			return atoi(string(offsets[2]), 0) - 1, atoi(string(offsets[3]), 0) - 1
-		}
-	}
-	return -1, -1
-}
-
 func repeat(r rune, times int) string {
 	if times > 0 {
 		return strings.Repeat(string(r), times)
@@ -181,13 +154,9 @@ func atoi(s string, defaultValue int) int {
 func (r *LightRenderer) Init() {
 	r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay)
 
-	fd := r.fd()
-	origState, err := terminal.GetState(fd)
-	if err != nil {
+	if err := r.initPlatform(); err != nil {
 		errorExit(err.Error())
 	}
-	r.origState = origState
-	terminal.MakeRaw(fd)
 	r.updateTerminalSize()
 	initTheme(r.theme, r.defaultTheme(), r.forceBlack)
 
@@ -260,28 +229,6 @@ func getEnv(name string, defaultValue int) int {
 	return atoi(env, defaultValue)
 }
 
-func (r *LightRenderer) updateTerminalSize() {
-	width, height, err := terminal.GetSize(r.fd())
-	if err == nil {
-		r.width = width
-		r.height = r.maxHeightFunc(height)
-	} else {
-		r.width = getEnv("COLUMNS", defaultWidth)
-		r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight))
-	}
-}
-
-func (r *LightRenderer) getch(nonblock bool) (int, bool) {
-	b := make([]byte, 1)
-	fd := r.fd()
-	util.SetNonblock(r.ttyin, nonblock)
-	_, err := util.Read(fd, b)
-	if err != nil {
-		return 0, false
-	}
-	return int(b[0]), true
-}
-
 func (r *LightRenderer) getBytes() []byte {
 	return r.getBytesInternal(r.buffer, false)
 }
@@ -316,6 +263,13 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
 		}
 		buffer = append(buffer, byte(c))
 		pc = c
+
+		// This should never happen under normal conditions,
+		// so terminate fzf immediately.
+		if len(buffer) > maxInputBuffer {
+			r.Close()
+			panic(fmt.Sprintf("Input buffer overflow (%d): %v", len(buffer), buffer))
+		}
 	}
 
 	return buffer
@@ -453,7 +407,10 @@ func (r *LightRenderer) escSequence(sz *int) Event {
 			*sz = 4
 			switch r.buffer[2] {
 			case 50:
-				if len(r.buffer) == 5 && r.buffer[4] == 126 {
+				if r.buffer[3] == 126 {
+					return Event{Insert, 0, nil}
+				}
+				if len(r.buffer) > 4 && r.buffer[4] == 126 {
 					*sz = 5
 					switch r.buffer[3] {
 					case 48:
@@ -467,7 +424,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
 					}
 				}
 				// Bracketed paste mode: \e[200~ ... \e[201~
-				if r.buffer[3] == '0' && (r.buffer[4] == '0' || r.buffer[4] == '1') && r.buffer[5] == '~' {
+				if len(r.buffer) > 5 && r.buffer[3] == '0' && (r.buffer[4] == '0' || r.buffer[4] == '1') && r.buffer[5] == '~' {
 					// Immediately discard the sequence from the buffer and reread input
 					r.buffer = r.buffer[6:]
 					*sz = 0
@@ -592,7 +549,7 @@ func (r *LightRenderer) rmcup() {
 }
 
 func (r *LightRenderer) Pause(clear bool) {
-	terminal.Restore(r.fd(), r.origState)
+	r.restoreTerminal()
 	if clear {
 		if r.fullscreen {
 			r.rmcup()
@@ -605,7 +562,7 @@ func (r *LightRenderer) Pause(clear bool) {
 }
 
 func (r *LightRenderer) Resume(clear bool) {
-	terminal.MakeRaw(r.fd())
+	r.setupTerminal()
 	if clear {
 		if r.fullscreen {
 			r.smcup()
@@ -659,7 +616,8 @@ func (r *LightRenderer) Close() {
 		r.csi("?1000l")
 	}
 	r.flush()
-	terminal.Restore(r.fd(), r.origState)
+	r.closePlatform()
+	r.restoreTerminal()
 }
 
 func (r *LightRenderer) MaxX() int {
@@ -678,6 +636,7 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, prev
 	w := &LightWindow{
 		renderer: r,
 		colored:  r.theme != nil,
+		preview:  preview,
 		border:   borderStyle,
 		top:      top,
 		left:     left,
@@ -701,7 +660,7 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, prev
 
 func (w *LightWindow) drawBorder() {
 	switch w.border.shape {
-	case BorderAround:
+	case BorderRounded, BorderSharp:
 		w.drawBorderAround()
 	case BorderHorizontal:
 		w.drawBorderHorizontal()
@@ -717,16 +676,20 @@ func (w *LightWindow) drawBorderHorizontal() {
 
 func (w *LightWindow) drawBorderAround() {
 	w.Move(0, 0)
-	w.CPrint(ColPreviewBorder, AttrRegular,
+	color := ColBorder
+	if w.preview {
+		color = ColPreviewBorder
+	}
+	w.CPrint(color, AttrRegular,
 		string(w.border.topLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.topRight))
 	for y := 1; y < w.height-1; y++ {
 		w.Move(y, 0)
-		w.CPrint(ColPreviewBorder, AttrRegular, string(w.border.vertical))
-		w.CPrint(ColPreviewBorder, AttrRegular, repeat(' ', w.width-2))
-		w.CPrint(ColPreviewBorder, AttrRegular, string(w.border.vertical))
+		w.CPrint(color, AttrRegular, string(w.border.vertical))
+		w.CPrint(color, AttrRegular, repeat(' ', w.width-2))
+		w.CPrint(color, AttrRegular, string(w.border.vertical))
 	}
 	w.Move(w.height-1, 0)
-	w.CPrint(ColPreviewBorder, AttrRegular,
+	w.CPrint(color, AttrRegular,
 		string(w.border.bottomLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.bottomRight))
 }
 
diff --git a/src/tui/light_unix.go b/src/tui/light_unix.go
new file mode 100644
index 00000000..e4ce6313
--- /dev/null
+++ b/src/tui/light_unix.go
@@ -0,0 +1,97 @@
+// +build !windows
+
+package tui
+
+import (
+	"fmt"
+	"os"
+	"syscall"
+
+	"github.com/junegunn/fzf/src/util"
+	"golang.org/x/crypto/ssh/terminal"
+)
+
+func IsLightRendererSupported() bool {
+	return true
+}
+
+func (r *LightRenderer) fd() int {
+	return int(r.ttyin.Fd())
+}
+
+func (r *LightRenderer) initPlatform() error {
+	fd := r.fd()
+	origState, err := terminal.GetState(fd)
+	if err != nil {
+		return err
+	}
+	r.origState = origState
+	terminal.MakeRaw(fd)
+	return nil
+}
+
+func (r *LightRenderer) closePlatform() {
+	// NOOP
+}
+
+func openTtyIn() *os.File {
+	in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
+	if err != nil {
+		tty := ttyname()
+		if len(tty) > 0 {
+			if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
+				return in
+			}
+		}
+		fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice)
+		os.Exit(2)
+	}
+	return in
+}
+
+func (r *LightRenderer) setupTerminal() {
+	terminal.MakeRaw(r.fd())
+}
+
+func (r *LightRenderer) restoreTerminal() {
+	terminal.Restore(r.fd(), r.origState)
+}
+
+func (r *LightRenderer) updateTerminalSize() {
+	width, height, err := terminal.GetSize(r.fd())
+
+	if err == nil {
+		r.width = width
+		r.height = r.maxHeightFunc(height)
+	} else {
+		r.width = getEnv("COLUMNS", defaultWidth)
+		r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight))
+	}
+}
+
+func (r *LightRenderer) findOffset() (row int, col int) {
+	r.csi("6n")
+	r.flush()
+	bytes := []byte{}
+	for tries := 0; tries < offsetPollTries; tries++ {
+		bytes = r.getBytesInternal(bytes, tries > 0)
+		offsets := offsetRegexp.FindSubmatch(bytes)
+		if len(offsets) > 3 {
+			// Add anything we skipped over to the input buffer
+			r.buffer = append(r.buffer, offsets[1]...)
+			return atoi(string(offsets[2]), 0) - 1, atoi(string(offsets[3]), 0) - 1
+		}
+	}
+	return -1, -1
+}
+
+func (r *LightRenderer) getch(nonblock bool) (int, bool) {
+	b := make([]byte, 1)
+	fd := r.fd()
+	util.SetNonblock(r.ttyin, nonblock)
+	_, err := util.Read(fd, b)
+	if err != nil {
+		return 0, false
+	}
+	return int(b[0]), true
+}
diff --git a/src/tui/light_windows.go b/src/tui/light_windows.go
new file mode 100644
index 00000000..6f876af2
--- /dev/null
+++ b/src/tui/light_windows.go
@@ -0,0 +1,132 @@
+//+build windows
+
+package tui
+
+import (
+	"os"
+	"syscall"
+
+	"github.com/junegunn/fzf/src/util"
+	"golang.org/x/sys/windows"
+)
+
+var (
+	consoleFlagsInput  = uint32(windows.ENABLE_VIRTUAL_TERMINAL_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_EXTENDED_FLAGS)
+	consoleFlagsOutput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING | windows.ENABLE_PROCESSED_OUTPUT | windows.DISABLE_NEWLINE_AUTO_RETURN)
+)
+
+// IsLightRendererSupported checks to see if the Light renderer is supported
+func IsLightRendererSupported() bool {
+	var oldState uint32
+	// enable vt100 emulation (https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences)
+	if windows.GetConsoleMode(windows.Stderr, &oldState) != nil {
+		return false
+	}
+	// attempt to set mode to determine if we support VT 100 codes. This will work on newer Windows 10
+	// version:
+	canSetVt100 := windows.SetConsoleMode(windows.Stderr, oldState|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) == nil
+	var checkState uint32
+	if windows.GetConsoleMode(windows.Stderr, &checkState) != nil ||
+		(checkState&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING {
+		return false
+	}
+	windows.SetConsoleMode(windows.Stderr, oldState)
+	return canSetVt100
+}
+
+func (r *LightRenderer) initPlatform() error {
+	//outHandle := windows.Stdout
+	outHandle, _ := syscall.Open("CONOUT$", syscall.O_RDWR, 0)
+	// enable vt100 emulation (https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences)
+	if err := windows.GetConsoleMode(windows.Handle(outHandle), &r.origStateOutput); err != nil {
+		return err
+	}
+	r.outHandle = uintptr(outHandle)
+	inHandle, _ := syscall.Open("CONIN$", syscall.O_RDWR, 0)
+	if err := windows.GetConsoleMode(windows.Handle(inHandle), &r.origStateInput); err != nil {
+		return err
+	}
+	r.inHandle = uintptr(inHandle)
+
+	r.setupTerminal()
+
+	// channel for non-blocking reads. Buffer to make sure
+	// we get the ESC sets:
+	r.ttyinChannel = make(chan byte, 12)
+
+	// the following allows for non-blocking IO.
+	// syscall.SetNonblock() is a NOOP under Windows.
+	go func() {
+		fd := int(r.inHandle)
+		b := make([]byte, 1)
+		for {
+			// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
+			_ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
+
+			_, err := util.Read(fd, b)
+			if err == nil {
+				r.ttyinChannel <- b[0]
+			}
+		}
+	}()
+
+	return nil
+}
+
+func (r *LightRenderer) closePlatform() {
+	windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput)
+	windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput)
+}
+
+func openTtyIn() *os.File {
+	// not used
+	return nil
+}
+
+func (r *LightRenderer) setupTerminal() error {
+	if err := windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput); err != nil {
+		return err
+	}
+	return windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
+}
+
+func (r *LightRenderer) restoreTerminal() error {
+	if err := windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput); err != nil {
+		return err
+	}
+	return windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput)
+}
+
+func (r *LightRenderer) updateTerminalSize() {
+	var bufferInfo windows.ConsoleScreenBufferInfo
+	if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil {
+		r.width = getEnv("COLUMNS", defaultWidth)
+		r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight))
+
+	} else {
+		r.width = int(bufferInfo.Window.Right - bufferInfo.Window.Left)
+		r.height = r.maxHeightFunc(int(bufferInfo.Window.Bottom - bufferInfo.Window.Top))
+	}
+}
+
+func (r *LightRenderer) findOffset() (row int, col int) {
+	var bufferInfo windows.ConsoleScreenBufferInfo
+	if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil {
+		return -1, -1
+	}
+	return int(bufferInfo.CursorPosition.X), int(bufferInfo.CursorPosition.Y)
+}
+
+func (r *LightRenderer) getch(nonblock bool) (int, bool) {
+	if nonblock {
+		select {
+		case bc := <-r.ttyinChannel:
+			return int(bc), true
+		default:
+			return 0, false
+		}
+	} else {
+		bc := <-r.ttyinChannel
+		return int(bc), true
+	}
+}
diff --git a/src/tui/tcell.go b/src/tui/tcell.go
index 339f7dff..db891655 100644
--- a/src/tui/tcell.go
+++ b/src/tui/tcell.go
@@ -28,6 +28,7 @@ type Attr tcell.Style
 
 type TcellWindow struct {
 	color       bool
+	preview     bool
 	top         int
 	left        int
 	width       int
@@ -318,6 +319,8 @@ func (r *FullscreenRenderer) GetChar() Event {
 			}
 			return Event{Right, 0, nil}
 
+		case tcell.KeyInsert:
+			return Event{Insert, 0, nil}
 		case tcell.KeyHome:
 			return Event{Home, 0, nil}
 		case tcell.KeyDelete:
@@ -416,6 +419,7 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
 	}
 	return &TcellWindow{
 		color:       r.theme != nil,
+		preview:     preview,
 		top:         top,
 		left:        left,
 		width:       width,
@@ -589,7 +593,7 @@ func (w *TcellWindow) drawBorder() {
 
 	var style tcell.Style
 	if w.color {
-		if w.borderStyle.shape == BorderAround {
+		if w.preview {
 			style = ColPreviewBorder.style()
 		} else {
 			style = ColBorder.style()
@@ -603,7 +607,7 @@ func (w *TcellWindow) drawBorder() {
 		_screen.SetContent(x, bot-1, w.borderStyle.horizontal, nil, style)
 	}
 
-	if w.borderStyle.shape == BorderAround {
+	if w.borderStyle.shape != BorderHorizontal {
 		for y := top; y < bot; y++ {
 			_screen.SetContent(left, y, w.borderStyle.vertical, nil, style)
 			_screen.SetContent(right-1, y, w.borderStyle.vertical, nil, style)
diff --git a/src/tui/tui.go b/src/tui/tui.go
index d4b8562c..4968b366 100644
--- a/src/tui/tui.go
+++ b/src/tui/tui.go
@@ -66,6 +66,7 @@ const (
 	Right
 	Home
 	End
+	Insert
 
 	SUp
 	SDown
@@ -209,7 +210,8 @@ type BorderShape int
 
 const (
 	BorderNone BorderShape = iota
-	BorderAround
+	BorderRounded
+	BorderSharp
 	BorderHorizontal
 )
 
@@ -227,14 +229,25 @@ type BorderCharacter int
 
 func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
 	if unicode {
+		if shape == BorderRounded {
+			return BorderStyle{
+				shape:       shape,
+				horizontal:  '─',
+				vertical:    '│',
+				topLeft:     '╭',
+				topRight:    '╮',
+				bottomLeft:  '╰',
+				bottomRight: '╯',
+			}
+		}
 		return BorderStyle{
 			shape:       shape,
 			horizontal:  '─',
 			vertical:    '│',
-			topLeft:     '╭',
-			topRight:    '╮',
-			bottomLeft:  '╰',
-			bottomRight: '╯',
+			topLeft:     '┌',
+			topRight:    '┐',
+			bottomLeft:  '└',
+			bottomRight: '┘',
 		}
 	}
 	return BorderStyle{
@@ -250,7 +263,7 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
 
 func MakeTransparentBorder() BorderStyle {
 	return BorderStyle{
-		shape:       BorderAround,
+		shape:       BorderRounded,
 		horizontal:  ' ',
 		vertical:    ' ',
 		topLeft:     ' ',
diff --git a/src/util/chars.go b/src/util/chars.go
index a57ba4bb..41de9243 100644
--- a/src/util/chars.go
+++ b/src/util/chars.go
@@ -130,6 +130,18 @@ func (chars *Chars) TrimLength() uint16 {
 	return chars.trimLength
 }
 
+func (chars *Chars) LeadingWhitespaces() int {
+	whitespaces := 0
+	for i := 0; i < chars.Length(); i++ {
+		char := chars.Get(i)
+		if !unicode.IsSpace(char) {
+			break
+		}
+		whitespaces++
+	}
+	return whitespaces
+}
+
 func (chars *Chars) TrailingWhitespaces() int {
 	whitespaces := 0
 	for i := chars.Length() - 1; i >= 0; i-- {


Test patch:
diff --git a/src/algo/algo_test.go b/src/algo/algo_test.go
index 610c30e5..d8f40cdf 100644
--- a/src/algo/algo_test.go
+++ b/src/algo/algo_test.go
@@ -136,6 +136,10 @@ func TestPrefixMatch(t *testing.T) {
 		assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3, score)
 		assertMatch(t, PrefixMatch, false, dir, "foOBarBaZ", "foo", 0, 3, score)
 		assertMatch(t, PrefixMatch, false, dir, "f-oBarbaz", "f-o", 0, 3, score)
+
+		assertMatch(t, PrefixMatch, false, dir, " fooBar", "foo", 1, 4, score)
+		assertMatch(t, PrefixMatch, false, dir, " fooBar", " fo", 0, 3, score)
+		assertMatch(t, PrefixMatch, false, dir, "     fo", "foo", -1, -1, 0)
 	}
 }
 
@@ -148,6 +152,13 @@ func TestSuffixMatch(t *testing.T) {
 			scoreMatch*3+bonusConsecutive*2)
 		assertMatch(t, SuffixMatch, false, dir, "fooBarBaZ", "baz", 6, 9,
 			(scoreMatch+bonusCamel123)*3+bonusCamel123*(bonusFirstCharMultiplier-1))
+
+		// Strip trailing white space from the string
+		assertMatch(t, SuffixMatch, false, dir, "fooBarbaz ", "baz", 6, 9,
+			scoreMatch*3+bonusConsecutive*2)
+		// Only when the pattern doesn't end with a space
+		assertMatch(t, SuffixMatch, false, dir, "fooBarbaz ", "baz ", 6, 10,
+			scoreMatch*4+bonusConsecutive*2+bonusNonWord)
 	}
 }
 
diff --git a/src/pattern_test.go b/src/pattern_test.go
index bfadf5d8..5a622952 100644
--- a/src/pattern_test.go
+++ b/src/pattern_test.go
@@ -98,6 +98,9 @@ func TestEqual(t *testing.T) {
 	}
 	match("ABC", -1, -1)
 	match("AbC", 0, 3)
+	match("AbC  ", 0, 3)
+	match(" AbC ", 1, 4)
+	match("  AbC", 2, 5)
 }
 
 func TestCaseSensitivity(t *testing.T) {
diff --git a/test/test_go.rb b/test/test_go.rb
index 22862d7d..fa4a6ef9 100755
--- a/test/test_go.rb
+++ b/test/test_go.rb
@@ -9,13 +9,23 @@ require 'minitest/autorun'
 require 'fileutils'
 require 'English'
 require 'shellwords'
-
+require 'erb'
+require 'tempfile'
+
+TEMPLATE = DATA.read
+UNSETS = %w[
+  FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS
+  FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS
+  FZF_ALT_C_COMMAND
+  FZF_ALT_C_OPTS FZF_CTRL_R_OPTS
+  fish_history
+].freeze
 DEFAULT_TIMEOUT = 20
 
 FILE = File.expand_path(__FILE__)
-base = File.expand_path('../../', __FILE__)
-Dir.chdir base
-FZF = "FZF_DEFAULT_OPTS= FZF_DEFAULT_COMMAND= #{base}/bin/fzf"
+BASE = File.expand_path('..', __dir__)
+Dir.chdir(BASE)
+FZF = "FZF_DEFAULT_OPTS= FZF_DEFAULT_COMMAND= #{BASE}/bin/fzf"
 
 class NilClass
   def include?(_str)
@@ -42,89 +52,71 @@ end
 
 class Shell
   class << self
-    def unsets
-      'unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS FZF_ALT_C_COMMAND FZF_ALT_C_OPTS FZF_CTRL_R_OPTS;'
-    end
-
     def bash
-      'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.bash'
+      @bash ||=
+        begin
+          bashrc = '/tmp/fzf.bash'
+          File.open(bashrc, 'w') do |f|
+            f.puts(ERB.new(TEMPLATE).result(binding))
+          end
+
+          "bash --rcfile #{bashrc}"
+        end
     end
 
     def zsh
-      FileUtils.mkdir_p '/tmp/fzf-zsh'
-      FileUtils.cp File.expand_path('~/.fzf.zsh'), '/tmp/fzf-zsh/.zshrc'
-      'PS1= PROMPT_COMMAND= HISTSIZE=100 ZDOTDIR=/tmp/fzf-zsh zsh'
+      @zsh ||=
+        begin
+          zdotdir = '/tmp/fzf-zsh'
+          FileUtils.rm_rf(zdotdir)
+          FileUtils.mkdir_p(zdotdir)
+          File.open("#{zdotdir}/.zshrc", 'w') do |f|
+            f.puts(ERB.new(TEMPLATE).result(binding))
+          end
+          "ZDOTDIR=#{zdotdir} zsh"
+        end
     end
 
     def fish
-      'fish'
+      UNSETS.map { |v| v + '= ' }.join + 'fish'
     end
   end
 end
 
 class Tmux
-  TEMPNAME = '/tmp/fzf-test.txt'
-
   attr_reader :win
 
   def initialize(shell = :bash)
-    @win =
-      case shell
-      when :bash
-        go("new-window -d -P -F '#I' '#{Shell.unsets + Shell.bash}'").first
-      when :zsh
-        go("new-window -d -P -F '#I' '#{Shell.unsets + Shell.zsh}'").first
-      when :fish
-        go("new-window -d -P -F '#I' '#{Shell.unsets + Shell.fish}'").first
-      else
-        raise "Unknown shell: #{shell}"
-      end
-    go("set-window-option -t #{@win} pane-base-index 0")
-    @lines = `tput lines`.chomp.to_i
-
+    @win = go(%W[new-window -d -P -F #I #{Shell.send(shell)}]).first
+    go(%W[set-window-option -t #{@win} pane-base-index 0])
     return unless shell == :fish
+
     send_keys('function fish_prompt; end; clear', :Enter)
     self.until(&:empty?)
   end
 
   def kill
-    go("kill-window -t #{win} 2> /dev/null")
+    go(%W[kill-window -t #{win}])
   end
 
   def send_keys(*args)
     target =
       if args.last.is_a?(Hash)
         hash = args.pop
-        go("select-window -t #{win}")
+        go(%W[select-window -t #{win}])
         "#{win}.#{hash[:pane]}"
       else
         win
       end
-    enum = (args + [nil]).each_cons(2)
-    loop do
-      pair = enum.next
-      if pair.first == :Escape
-        arg = pair.compact.map { |key| %("#{key}") }.join(' ')
-        go(%(send-keys -t #{target} #{arg}))
-        enum.next if pair.last
-      else
-        go(%(send-keys -t #{target} "#{pair.first}"))
-      end
-      break unless pair.last
-    end
+    go(%W[send-keys -t #{target}] + args.map(&:to_s))
   end
 
   def paste(str)
-    `tmux setb '#{str.gsub("'", "'\\''")}' \\; pasteb -t #{win} \\; send-keys -t #{win} Enter`
+    system('tmux', 'setb', str, ';', 'pasteb', '-t', win, ';', 'send-keys', '-t', win, 'Enter')
   end
 
   def capture(pane = 0)
-    File.unlink TEMPNAME while File.exist? TEMPNAME
-    wait do
-      go("capture-pane -t #{win}.#{pane} \\; save-buffer #{TEMPNAME} 2> /dev/null")
-      $CHILD_STATUS.exitstatus.zero?
-    end
-    File.read(TEMPNAME).split($INPUT_RECORD_SEPARATOR)[0, @lines].reverse.drop_while(&:empty?).reverse
+    go(%W[capture-pane -p -t #{win}.#{pane}]).reverse.drop_while(&:empty?).reverse
   end
 
   def until(refresh = false, pane = 0)
@@ -175,7 +167,7 @@ class Tmux
     tries = 0
     begin
       self.until do |lines|
-        send_keys 'C-u', 'hello'
+        send_keys ' ', 'C-u', 'hello', :Left, :Right
         lines[-1].end_with?('hello')
       end
     rescue StandardError
@@ -186,8 +178,8 @@ class Tmux
 
   private
 
-  def go(*args)
-    `tmux #{args.join ' '}`.split($INPUT_RECORD_SEPARATOR)
+  def go(args)
+    IO.popen(['tmux'] + args) { |io| io.readlines(chomp: true) }
   end
 end
 
@@ -491,14 +483,14 @@ class TestGoFZF < TestBase
   end
 
   def test_query_unicode
-    tmux.paste "(echo abc; echo 가나다) | #{fzf :query, '가다'}"
+    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| lines[-2].include? '1/2' }
     tmux.send_keys :Enter
     assert_equal ['가나다'], readonce.split($INPUT_RECORD_SEPARATOR)
   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| lines[-1] == '>' }
     tmux.send_keys 9
     tmux.until { |lines| lines[-2] == '  19/100' }
@@ -779,7 +771,7 @@ class TestGoFZF < TestBase
   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
@@ -938,7 +930,7 @@ class TestGoFZF < TestBase
 
   def test_execute
     output = '/tmp/fzf-test-execute'
-    opts = %[--bind \\"alt-a:execute(echo /{}/ >> #{output}),alt-b:execute[echo /{}{}/ >> #{output}],C:execute:echo /{}{}{}/ >> #{output}\\"]
+    opts = %[--bind "alt-a:execute(echo /{}/ >> #{output}),alt-b:execute[echo /{}{}/ >> #{output}],C:execute:echo /{}{}{}/ >> #{output}"]
     wait = ->(exp) { tmux.until { |lines| lines[-2].include? exp } }
     writelines tempname, %w[foo'bar foo"bar foo$bar]
     tmux.send_keys "cat #{tempname} | #{fzf opts}; sync", :Enter
@@ -977,7 +969,7 @@ class TestGoFZF < TestBase
 
   def test_execute_multi
     output = '/tmp/fzf-test-execute-multi'
-    opts = %[--multi --bind \\"alt-a:execute-multi(echo {}/{+} >> #{output}; sync)\\"]
+    opts = %[--multi --bind "alt-a:execute-multi(echo {}/{+} >> #{output}; sync)"]
     writelines tempname, %w[foo'bar foo"bar foo$bar foobar]
     tmux.send_keys "cat #{tempname} | #{fzf opts}", :Enter
     tmux.until { |lines| lines[-2].include? '4/4' }
@@ -1163,7 +1155,7 @@ class TestGoFZF < TestBase
   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).take(5).map(&:strip)
     tmux.until do |lines|
       lines[-2].include?('100/100') &&
@@ -1173,7 +1165,7 @@ class TestGoFZF < TestBase
   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).take(5).map(&:strip)
     tmux.until do |lines|
       lines[1].include?('100/100') &&
@@ -1183,7 +1175,7 @@ class TestGoFZF < TestBase
   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).take(5).map(&:strip)
     tmux.until do |lines|
       lines[-2].include?('100/100') &&
@@ -1193,7 +1185,7 @@ class TestGoFZF < TestBase
   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).take(5).map(&:strip)
     tmux.until do |lines|
       lines[-2].include?('90/90') &&
@@ -1203,7 +1195,7 @@ class TestGoFZF < TestBase
   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).take(5).map(&:strip)
     tmux.until do |lines|
       lines[1].include?('90/90') &&
@@ -1213,7 +1205,7 @@ class TestGoFZF < TestBase
   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).take(5).map(&:strip)
     tmux.until do |lines|
       lines[-2].include?('90/90') &&
@@ -1324,7 +1316,7 @@ class TestGoFZF < TestBase
 
   def test_exitstatus_empty
     { '99' => '0', '999' => '1' }.each do |query, status|
-      tmux.send_keys "seq 100 | #{FZF} -q #{query}; echo --\\$?--", :Enter
+      tmux.send_keys "seq 100 | #{FZF} -q #{query}; echo --$?--", :Enter
       tmux.until { |lines| lines[-2] =~ %r{ [10]/100} }
       tmux.send_keys :Enter
       tmux.until { |lines| lines.last.include? "--#{status}--" }
@@ -1469,7 +1461,7 @@ class TestGoFZF < TestBase
   end
 
   def test_preview_hidden
-    tmux.send_keys %(seq 1000 | #{FZF} --preview 'echo {{}-{}-\\$FZF_PREVIEW_LINES-\\$FZF_PREVIEW_COLUMNS}' --preview-window down:1:hidden --bind ?:toggle-preview), :Enter
+    tmux.send_keys %(seq 1000 | #{FZF} --preview 'echo {{}-{}-$FZF_PREVIEW_LINES-$FZF_PREVIEW_COLUMNS}' --preview-window down:1:hidden --bind ?:toggle-preview), :Enter
     tmux.until { |lines| lines[-1] == '>' }
     tmux.send_keys '?'
     tmux.until { |lines| lines[-2] =~ / {1-1-1-[0-9]+}/ }
@@ -1710,11 +1702,38 @@ class TestGoFZF < TestBase
     tmux.until { |lines| lines.match_count.zero? }
     tmux.until { |lines| !lines[-2].include?('(1)') }
   end
+
+  def test_backward_delete_char_eof
+    tmux.send_keys "seq 1000 | #{fzf "--bind 'bs:backward-delete-char/eof'"}", :Enter
+    tmux.until { |lines| lines[-2] == '  1000/1000' }
+    tmux.send_keys '11'
+    tmux.until { |lines| lines[-1] == '> 11' }
+    tmux.send_keys :BSpace
+    tmux.until { |lines| lines[-1] == '> 1' }
+    tmux.send_keys :BSpace
+    tmux.until { |lines| lines[-1] == '>' }
+    tmux.send_keys :BSpace
+    tmux.prepare
+  end
+
+  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)]
+      File.chmod(0o755, tempname)
+      tmux.prepare
+      tmux.send_keys(
+        %(echo foo bar | #{FZF} --preview '#{tempname} {2} {1}'), :Enter
+      )
+      tmux.until { |lines| lines.any_include?('bar foo') }
+      tmux.send_keys :Enter
+    end
+  end
 end
 
 module TestShell
   def setup
-    super
+    @tmux = Tmux.new shell
+    tmux.prepare
   end
 
   def teardown
@@ -1774,7 +1793,7 @@ module TestShell
     tmux.until { |lines| lines.select_count == 2 }
 
     tmux.send_keys :Enter
-    tmux.until { |lines| lines.any_include?(/echo.*fzf-unicode.*1.*fzf-unicode.*2/) }
+    tmux.until { |lines| lines.join.match(/echo.*fzf-unicode.*1.*fzf-unicode.*2/) }
     tmux.send_keys :Enter
     tmux.until { |lines| lines.any_include?(/^fzf-unicode.*1.*fzf-unicode.*2/) }
   end
@@ -1831,6 +1850,37 @@ module TestShell
     tmux.until { |lines| lines[-1] == '3rd' }
   end
 
+  def test_ctrl_r_multiline
+    tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
+    tmux.until { |lines| lines[-2..-1] == ['foo', 'bar'] }
+    retries do
+      tmux.prepare
+      tmux.send_keys 'C-r'
+      tmux.until { |lines| lines[-1] == '>' }
+    end
+    tmux.send_keys 'foo bar'
+    tmux.until { |lines| lines[-3].end_with? 'bar"' }
+    tmux.send_keys :Enter
+    tmux.until { |lines| lines[-1].end_with? 'bar"' }
+    tmux.send_keys :Enter
+    tmux.until { |lines| lines[-2..-1] == ['foo', 'bar'] }
+  end
+
+  def test_ctrl_r_abort
+    skip "doesn't restore the original line when search is aborted pre Bash 4" if shell == :bash && /(?<= version )\d+/.match(`#{Shell.bash} --version`).to_s.to_i < 4
+    %w[foo ' "].each do |query|
+      retries do
+        tmux.prepare
+        tmux.send_keys(:Space, 'C-e', 'C-u', query)
+        tmux.until { |lines| lines[-1].start_with? query }
+        tmux.send_keys 'C-r'
+        tmux.until { |lines| lines[-1] == "> #{query}" }
+      end
+      tmux.send_keys 'C-g'
+      tmux.until { |lines| lines[-1].start_with? query }
+    end
+  end
+
   def retries(times = 3)
     (times - 1).times do
       begin
@@ -1960,7 +2010,7 @@ module CompletionTest
   end
 
   def test_custom_completion
-    tmux.send_keys '_fzf_compgen_path() { echo "\$1"; seq 10; }', :Enter
+    tmux.send_keys '_fzf_compgen_path() { echo "$1"; seq 10; }', :Enter
     tmux.prepare
     tmux.send_keys 'ls /tmp/**', :Tab
     tmux.until { |lines| lines.match_count == 11 }
@@ -1991,7 +2041,7 @@ module CompletionTest
 
   def test_file_completion_unicode
     FileUtils.mkdir_p '/tmp/fzf-test'
-    tmux.paste 'cd /tmp/fzf-test; echo -n test3 > "fzf-unicode 테스트1"; echo -n test4 > "fzf-unicode 테스트2"'
+    tmux.paste "cd /tmp/fzf-test; echo -n test3 > $'fzf-unicode \\355\\205\\214\\354\\212\\244\\355\\212\\2701'; echo -n test4 > $'fzf-unicode \\355\\205\\214\\354\\212\\244\\355\\212\\2702'"
     tmux.prepare
     tmux.send_keys 'cat fzf-unicode**', :Tab
     tmux.until { |lines| lines.match_count == 2 }
@@ -2020,17 +2070,16 @@ class TestBash < TestBase
   include TestShell
   include CompletionTest
 
+  def shell
+    :bash
+  end
+
   def new_shell
     tmux.prepare
     tmux.send_keys "FZF_TMUX=1 #{Shell.bash}", :Enter
     tmux.prepare
   end
 
-  def setup
-    super
-    @tmux = Tmux.new :bash
-  end
-
   def test_dynamic_completion_loader
     tmux.paste 'touch /tmp/foo; _fzf_completion_loader=1'
     tmux.paste '_completion_loader() { complete -o default fake; }'
@@ -2053,20 +2102,23 @@ class TestZsh < TestBase
   include TestShell
   include CompletionTest
 
+  def shell
+    :zsh
+  end
+
   def new_shell
     tmux.send_keys "FZF_TMUX=1 #{Shell.zsh}", :Enter
     tmux.prepare
   end
-
-  def setup
-    super
-    @tmux = Tmux.new :zsh
-  end
 end
 
 class TestFish < TestBase
   include TestShell
 
+  def shell
+    :fish
+  end
+
   def new_shell
     tmux.send_keys 'env FZF_TMUX=1 fish', :Enter
     tmux.send_keys 'function fish_prompt; end; clear', :Enter
@@ -2078,9 +2130,22 @@ class TestFish < TestBase
     tmux.send_keys "set -g #{name} '#{val}'", :Enter
     tmux.prepare
   end
-
-  def setup
-    super
-    @tmux = Tmux.new :fish
-  end
 end
+
+__END__
+# Setup fzf
+# ---------
+if [[ ! "$PATH" == *<%= BASE %>/bin* ]]; then
+  export PATH="${PATH:+${PATH}:}<%= BASE %>/bin"
+fi
+
+# Auto-completion
+# ---------------
+[[ $- == *i* ]] && source "<%= BASE %>/shell/completion.<%= __method__ %>" 2> /dev/null
+
+# Key bindings
+# ------------
+source "<%= BASE %>/shell/key-bindings.<%= __method__ %>"
+
+PS1= PROMPT_COMMAND= HISTFILE= HISTSIZE=100
+unset <%= UNSETS.join(' ') %>