Repository: junegunn/fzf
Pull request #1514 (2019-03-05 08:52:14)

[WIP] 0.18.0


Patch:
diff --git a/README.md b/README.md
index 44f4f05f..6a595aa9 100644
--- a/README.md
+++ b/README.md
@@ -88,6 +88,10 @@ brew install fzf
 $(brew --prefix)/opt/fzf/install
 ```
 
+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
@@ -494,15 +498,17 @@ fzf --preview 'head -100 {}'
 Preview window supports ANSI colors, so you can use programs that
 syntax-highlights the content of a file.
 
+- Bat: https://github.com/sharkdp/bat
 - Highlight: http://www.andre-simon.de/doku/highlight/en/highlight.php
 - CodeRay: http://coderay.rubychan.de/
 - Rouge: https://github.com/jneen/rouge
 
 ```bash
-# Try highlight, coderay, rougify in turn, then fall back to cat
+# Try bat, highlight, coderay, rougify in turn, then fall back to cat
 fzf --preview '[[ $(file --mime {}) =~ binary ]] &&
                  echo {} is a binary file ||
-                 (highlight -O ansi -l {} ||
+                 (bat --style=numbers --color=always {} ||
+                  highlight -O ansi -l {} ||
                   coderay {} ||
                   rougify {} ||
                   cat {}) 2> /dev/null | head -500'
diff --git a/install b/install
index 9139c726..4cc0214b 100755
--- a/install
+++ b/install
@@ -91,17 +91,20 @@ ask() {
 check_binary() {
   echo -n "  - Checking fzf executable ... "
   local output
-  output=$("$fzf_base"/bin/fzf --version 2>&1 | awk '{print $1}')
+  output=$("$fzf_base"/bin/fzf --version 2>&1)
   if [ $? -ne 0 ]; then
     echo "Error: $output"
     binary_error="Invalid binary"
-  elif [ "$version" != "$output" ]; then
-    echo "$output != $version"
-    binary_error="Invalid version"
   else
-    echo "$output"
-    binary_error=""
-    return 0
+    output=${output/ */}
+    if [ "$version" != "$output" ]; then
+      echo "$output != $version"
+      binary_error="Invalid version"
+    else
+      echo "$output"
+      binary_error=""
+      return 0
+    fi
   fi
   rm -f "$fzf_base"/bin/fzf
   return 1
@@ -110,7 +113,7 @@ check_binary() {
 link_fzf_in_path() {
   if which_fzf="$(command -v fzf)"; then
     echo "  - Found in \$PATH"
-    echo "  - Creating symlink: $which_fzf -> bin/fzf"
+    echo "  - Creating symlink: bin/fzf -> $which_fzf"
     (cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf)
     check_binary && return
   fi
@@ -269,7 +272,7 @@ for shell in $shells; do
 # Setup fzf
 # ---------
 if [[ ! "\$PATH" == *$fzf_base_esc/bin* ]]; then
-  export PATH="\$PATH:$fzf_base/bin"
+  export PATH="\${PATH:+\${PATH}:}$fzf_base/bin"
 fi
 
 # Auto-completion
@@ -279,7 +282,6 @@ $fzf_completion
 # Key bindings
 # ------------
 $fzf_key_bindings
-
 EOF
   echo "OK"
 done
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 68a387c2..9ff44c4f 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -174,6 +174,11 @@ A synonym for \fB--layout=reverse\fB
 .TP
 .B "--border"
 Draw border above and below the finder
+
+.TP
+.B "--no-unicode"
+Use ASCII characters instead of Unicode box drawing characters to draw border
+
 .TP
 .BI "--margin=" MARGIN
 Comma-separated expression for margins around the finder.
@@ -250,6 +255,7 @@ e.g. \fBfzf --color=bg+:24\fR
     \fBhl      \fRHighlighted substrings
     \fBfg+     \fRText (current line)
     \fBbg+     \fRBackground (current line)
+    \fBgutter  \fRGutter on the left (default to \fBbg+\fR)
     \fBhl+     \fRHighlighted substrings (current line)
     \fBinfo    \fRInfo
     \fBborder  \fRBorder of the preview window and horizontal separators (\fB--border\fR)
@@ -288,8 +294,11 @@ EXPRESSION\fR for the details).
 e.g. \fBfzf --preview='head -$LINES {}'\fR
      \fBls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR
 
-fzf overrides \fB$LINES\fR and \fB$COLUMNS\fR so that they represent the exact
-size of the preview window.
+fzf exports \fB$FZF_PREVIEW_LINES\fR and \fB$FZF_PREVIEW_COLUMNS\fR so that
+they represent the exact size of the preview window. (It also overrides
+\fB$LINES\fR and \fB$COLUMNS\fR with the same values but they can be reset
+by the default shell, so prefer to refer to the ones with \fBFZF_PREVIEW_\fR
+prefix.)
 
 A placeholder expression starting with \fB+\fR flag will be replaced to the
 space-separated list of the selected lines (or the current line if no selection
@@ -301,7 +310,9 @@ e.g. \fBfzf --multi --preview='head -10 {+}'\fR
 When using a field index expression, leading and trailing whitespace is stripped
 from the replacement string. To preserve the whitespace, use the \fBs\fR flag.
 
-Also, \fB{q}\fR is replaced to the current query string.
+Also, \fB{q}\fR is replaced to the current query string, and \fB{n}\fR is
+replaced to zero-based ordinal index of the line. Use \fB{+n}\fR if you want
+all index numbers when multiple lines are selected.
 
 Note that you can escape a placeholder pattern by prepending a backslash.
 
diff --git a/plugin/fzf.vim b/plugin/fzf.vim
index 5eca120e..8f2bc26a 100644
--- a/plugin/fzf.vim
+++ b/plugin/fzf.vim
@@ -457,7 +457,8 @@ function! s:pushd(dict)
     let cwd = s:fzf_getcwd()
     let w:fzf_pushd = {
     \   'command': haslocaldir() ? 'lcd' : (exists(':tcd') && haslocaldir(-1) ? 'tcd' : 'cd'),
-    \   'origin': cwd
+    \   'origin': cwd,
+    \   'bufname': bufname('')
     \ }
     execute 'lcd' s:escape(a:dict.dir)
     let cwd = s:fzf_getcwd()
@@ -493,7 +494,7 @@ function! s:dopopd()
   " matches 'dir' entry. However, it is possible that the sink function did
   " change the directory to 'dir'. In that case, the user will have an
   " unexpected result.
-  if s:fzf_getcwd() ==# w:fzf_pushd.dir
+  if s:fzf_getcwd() ==# w:fzf_pushd.dir && (!&autochdir || w:fzf_pushd.bufname ==# bufname(''))
     execute w:fzf_pushd.command s:escape(w:fzf_pushd.origin)
   endif
   unlet w:fzf_pushd
@@ -606,8 +607,9 @@ function! s:calc_size(max, val, dict)
     let srcsz = len(a:dict.source)
   endif
 
-  let opts = s:evaluate_opts(get(a:dict, 'options', '')).$FZF_DEFAULT_OPTS
+  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 += stridx(opts, '--border') > stridx(opts, '--no-border') ? 2 : 0
   let margin += stridx(opts, '--header') > stridx(opts, '--no-header')
   return srcsz >= 0 ? min([srcsz + margin, size]) : size
 endfunction
diff --git a/shell/completion.bash b/shell/completion.bash
index 93369954..15d2f952 100644
--- a/shell/completion.bash
+++ b/shell/completion.bash
@@ -9,6 +9,8 @@
 # - $FZF_COMPLETION_TRIGGER (default: '**')
 # - $FZF_COMPLETION_OPTS    (default: empty)
 
+if [[ $- =~ i ]]; then
+
 # To use custom commands instead of find, override _fzf_compgen_{path,dir}
 if ! declare -f _fzf_compgen_path > /dev/null; then
   _fzf_compgen_path() {
@@ -241,7 +243,7 @@ _fzf_complete_telnet() {
 
 _fzf_complete_ssh() {
   _fzf_complete '+m' "$@" < <(
-    cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host ' | command grep -v '[*?]' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}') \
+    cat <(cat ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host ' | command grep -v '[*?]' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}') \
         <(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
         <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
         awk '{if (length($2) > 0) {print $2}}' | sort -u
@@ -330,3 +332,5 @@ complete -F _fzf_complete_export -o default -o bashdefault export
 complete -F _fzf_complete_unalias -o default -o bashdefault unalias
 
 unset cmd d_cmds a_cmds x_cmds
+
+fi
diff --git a/shell/completion.zsh b/shell/completion.zsh
index f6052592..ed1f2796 100644
--- a/shell/completion.zsh
+++ b/shell/completion.zsh
@@ -9,6 +9,8 @@
 # - $FZF_COMPLETION_TRIGGER (default: '**')
 # - $FZF_COMPLETION_OPTS    (default: empty)
 
+if [[ $- =~ i ]]; then
+
 # To use custom commands instead of find, override _fzf_compgen_{path,dir}
 if ! declare -f _fzf_compgen_path > /dev/null; then
   _fzf_compgen_path() {
@@ -112,7 +114,8 @@ _fzf_complete_telnet() {
 
 _fzf_complete_ssh() {
   _fzf_complete '+m' "$@" < <(
-    command cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host ' | command grep -v '[*?]' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}') \
+    setopt localoptions nonomatch
+    command cat <(cat ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host ' | command grep -v '[*?]' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}') \
         <(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
         <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
         awk '{if (length($2) > 0) {print $2}}' | sort -u
@@ -192,3 +195,5 @@ fzf-completion() {
 
 zle     -N   fzf-completion
 bindkey '^I' fzf-completion
+
+fi
diff --git a/src/ansi.go b/src/ansi.go
index d7c81d30..c31a4f4f 100644
--- a/src/ansi.go
+++ b/src/ansi.go
@@ -32,6 +32,55 @@ func (s *ansiState) equals(t *ansiState) bool {
 	return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr
 }
 
+func (s *ansiState) ToString() string {
+	if !s.colored() {
+		return ""
+	}
+
+	ret := ""
+	if s.attr&tui.Bold > 0 {
+		ret += "1;"
+	}
+	if s.attr&tui.Dim > 0 {
+		ret += "2;"
+	}
+	if s.attr&tui.Italic > 0 {
+		ret += "3;"
+	}
+	if s.attr&tui.Underline > 0 {
+		ret += "4;"
+	}
+	if s.attr&tui.Blink > 0 {
+		ret += "5;"
+	}
+	if s.attr&tui.Reverse > 0 {
+		ret += "7;"
+	}
+	ret += toAnsiString(s.fg, 30) + toAnsiString(s.bg, 40)
+
+	return "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
+}
+
+func toAnsiString(color tui.Color, offset int) string {
+	col := int(color)
+	ret := ""
+	if col == -1 {
+		ret += strconv.Itoa(offset + 9)
+	} else if col < 8 {
+		ret += strconv.Itoa(offset + col)
+	} else if col < 16 {
+		ret += strconv.Itoa(offset - 30 + 90 + col - 8)
+	} else if col < 256 {
+		ret += strconv.Itoa(offset+8) + ";5;" + strconv.Itoa(col)
+	} else if col >= (1 << 24) {
+		r := strconv.Itoa((col >> 16) & 0xff)
+		g := strconv.Itoa((col >> 8) & 0xff)
+		b := strconv.Itoa(col & 0xff)
+		ret += strconv.Itoa(offset+8) + ";2;" + r + ";" + g + ";" + b
+	}
+	return ret + ";"
+}
+
 var ansiRegex *regexp.Regexp
 
 func init() {
diff --git a/src/core.go b/src/core.go
index 1653e6fd..2db5b3ae 100644
--- a/src/core.go
+++ b/src/core.go
@@ -63,12 +63,14 @@ func Run(opts *Options, revision string) {
 	ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) {
 		return util.ToChars(data), nil
 	}
+
+	var lineAnsiState, prevLineAnsiState *ansiState
 	if opts.Ansi {
 		if opts.Theme != nil {
-			var state *ansiState
 			ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
-				trimmed, offsets, newState := extractColor(string(data), state, nil)
-				state = newState
+				prevLineAnsiState = lineAnsiState
+				trimmed, offsets, newState := extractColor(string(data), lineAnsiState, nil)
+				lineAnsiState = newState
 				return util.ToChars([]byte(trimmed)), offsets
 			}
 		} else {
@@ -100,6 +102,22 @@ func Run(opts *Options, revision string) {
 	} else {
 		chunkList = NewChunkList(func(item *Item, data []byte) bool {
 			tokens := Tokenize(string(data), opts.Delimiter)
+			if opts.Ansi && opts.Theme != nil && len(tokens) > 1 {
+				var ansiState *ansiState
+				if prevLineAnsiState != nil {
+					ansiStateDup := *prevLineAnsiState
+					ansiState = &ansiStateDup
+				}
+				for _, token := range tokens {
+					prevAnsiState := ansiState
+					_, _, ansiState = extractColor(token.text.ToString(), ansiState, nil)
+					if prevAnsiState != nil {
+						token.text.Prepend("\x1b[m" + prevAnsiState.ToString())
+					} else {
+						token.text.Prepend("\x1b[m")
+					}
+				}
+			}
 			trans := Transform(tokens, opts.WithNth)
 			transformed := joinTokens(trans)
 			if len(header) < opts.HeaderLines {
diff --git a/src/options.go b/src/options.go
index fc32344c..59cdccc0 100644
--- a/src/options.go
+++ b/src/options.go
@@ -195,6 +195,7 @@ type Options struct {
 	HeaderLines int
 	Margin      [4]sizeSpec
 	Bordered    bool
+	Unicode     bool
 	Tabstop     int
 	ClearOnExit bool
 	Version     bool
@@ -244,6 +245,7 @@ func defaultOptions() *Options {
 		Header:      make([]string, 0),
 		HeaderLines: 0,
 		Margin:      defaultMargin(),
+		Unicode:     true,
 		Tabstop:     8,
 		ClearOnExit: true,
 		Version:     false}
@@ -576,6 +578,8 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
 				theme.Current = ansi
 			case "bg+":
 				theme.DarkBg = ansi
+			case "gutter":
+				theme.Gutter = ansi
 			case "hl":
 				theme.Match = ansi
 			case "hl+":
@@ -1150,6 +1154,10 @@ func parseOptions(opts *Options, allArgs []string) {
 			opts.Bordered = false
 		case "--border":
 			opts.Bordered = true
+		case "--no-unicode":
+			opts.Unicode = false
+		case "--unicode":
+			opts.Unicode = true
 		case "--margin":
 			opts.Margin = parseMargin(
 				nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
diff --git a/src/terminal.go b/src/terminal.go
index b2b38254..06623b28 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -24,7 +24,7 @@ import (
 var placeholder *regexp.Regexp
 
 func init() {
-	placeholder = regexp.MustCompile("\\\\?(?:{[+s]*[0-9,-.]*}|{q})")
+	placeholder = regexp.MustCompile("\\\\?(?:{[+s]*[0-9,-.]*}|{q}|{\\+?n})")
 }
 
 type jumpMode int
@@ -40,6 +40,7 @@ type previewer struct {
 	lines   int
 	offset  int
 	enabled bool
+	more    bool
 }
 
 type itemLine struct {
@@ -88,6 +89,7 @@ type Terminal struct {
 	tabstop    int
 	margin     [4]sizeSpec
 	strong     tui.Attr
+	unicode    bool
 	bordered   bool
 	cleanExit  bool
 	border     tui.Window
@@ -227,6 +229,7 @@ const (
 type placeholderFlags struct {
 	plus          bool
 	preserveSpace bool
+	number        bool
 	query         bool
 }
 
@@ -390,6 +393,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
 		printQuery: opts.PrintQuery,
 		history:    opts.History,
 		margin:     opts.Margin,
+		unicode:    opts.Unicode,
 		bordered:   opts.Bordered,
 		cleanExit:  opts.ClearOnExit,
 		strong:     strongAttr,
@@ -407,7 +411,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
 		selected:   make(map[int32]selectedItem),
 		reqBox:     util.NewEventBox(),
 		preview:    opts.Preview,
-		previewer:  previewer{"", 0, 0, previewBox != nil && !opts.Preview.hidden},
+		previewer:  previewer{"", 0, 0, previewBox != nil && !opts.Preview.hidden, false},
 		previewBox: previewBox,
 		eventBox:   eventBox,
 		mutex:      sync.Mutex{},
@@ -599,11 +603,12 @@ func (t *Terminal) resizeWindows() {
 			marginInt[0]-1,
 			marginInt[3],
 			width,
-			height+2, tui.BorderHorizontal)
+			height+2, tui.MakeBorderStyle(tui.BorderHorizontal, t.unicode))
 	}
+	noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
 	if previewVisible {
 		createPreviewWindow := func(y int, x int, w int, h int) {
-			t.pborder = t.tui.NewWindow(y, x, w, h, tui.BorderAround)
+			t.pborder = t.tui.NewWindow(y, x, w, h, tui.MakeBorderStyle(tui.BorderAround, t.unicode))
 			pwidth := w - 4
 			// ncurses auto-wraps the line when the cursor reaches the right-end of
 			// the window. To prevent unintended line-wraps, we use the width one
@@ -611,29 +616,28 @@ func (t *Terminal) resizeWindows() {
 			if !t.preview.wrap && t.tui.DoesAutoWrap() {
 				pwidth += 1
 			}
-			t.pwindow = t.tui.NewWindow(y+1, x+2, pwidth, h-2, tui.BorderNone)
-			os.Setenv("FZF_PREVIEW_HEIGHT", strconv.Itoa(h-2))
+			t.pwindow = t.tui.NewWindow(y+1, x+2, pwidth, h-2, noBorder)
 		}
 		switch t.preview.position {
 		case posUp:
 			pheight := calculateSize(height, t.preview.size, minHeight, 3)
 			t.window = t.tui.NewWindow(
-				marginInt[0]+pheight, marginInt[3], width, height-pheight, tui.BorderNone)
+				marginInt[0]+pheight, marginInt[3], width, height-pheight, noBorder)
 			createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
 		case posDown:
 			pheight := calculateSize(height, t.preview.size, minHeight, 3)
 			t.window = t.tui.NewWindow(
-				marginInt[0], marginInt[3], width, height-pheight, tui.BorderNone)
+				marginInt[0], marginInt[3], width, height-pheight, noBorder)
 			createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
 		case posLeft:
 			pwidth := calculateSize(width, t.preview.size, minWidth, 5)
 			t.window = t.tui.NewWindow(
-				marginInt[0], marginInt[3]+pwidth, width-pwidth, height, tui.BorderNone)
+				marginInt[0], marginInt[3]+pwidth, width-pwidth, height, noBorder)
 			createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
 		case posRight:
 			pwidth := calculateSize(width, t.preview.size, minWidth, 5)
 			t.window = t.tui.NewWindow(
-				marginInt[0], marginInt[3], width-pwidth, height, tui.BorderNone)
+				marginInt[0], marginInt[3], width-pwidth, height, noBorder)
 			createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height)
 		}
 	} else {
@@ -641,7 +645,7 @@ func (t *Terminal) resizeWindows() {
 			marginInt[0],
 			marginInt[3],
 			width,
-			height, tui.BorderNone)
+			height, noBorder)
 	}
 	for i := 0; i < t.window.Height(); i++ {
 		t.window.MoveAndClear(i, 0)
@@ -834,15 +838,16 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool) {
 	}
 
 	t.move(line, 0, false)
-	t.window.CPrint(tui.ColCursor, t.strong, label)
 	if current {
+		t.window.CPrint(tui.ColCurrentCursor, t.strong, label)
 		if selected {
-			t.window.CPrint(tui.ColSelected, t.strong, ">")
+			t.window.CPrint(tui.ColCurrentSelected, t.strong, ">")
 		} else {
-			t.window.CPrint(tui.ColCurrent, t.strong, " ")
+			t.window.CPrint(tui.ColCurrentSelected, t.strong, " ")
 		}
 		newLine.width = t.printHighlighted(result, t.strong, tui.ColCurrent, tui.ColCurrentMatch, true, true)
 	} else {
+		t.window.CPrint(tui.ColCursor, t.strong, label)
 		if selected {
 			t.window.CPrint(tui.ColSelected, t.strong, ">")
 		} else {
@@ -1023,18 +1028,17 @@ func (t *Terminal) printPreview() {
 	reader := bufio.NewReader(strings.NewReader(t.previewer.text))
 	lineNo := -t.previewer.offset
 	height := t.pwindow.Height()
+	t.previewer.more = t.previewer.offset > 0
 	var ansi *ansiState
-	for {
+	for ; ; lineNo++ {
 		line, err := reader.ReadString('\n')
 		eof := err == io.EOF
 		if !eof {
 			line = line[:len(line)-1]
 		}
-		lineNo++
-		if lineNo > height ||
-			t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
+		if lineNo >= height || t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
 			break
-		} else if lineNo > 0 {
+		} else if lineNo >= 0 {
 			var fillRet tui.FillReturn
 			prefixWidth := 0
 			_, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool {
@@ -1051,10 +1055,10 @@ func (t *Terminal) printPreview() {
 				}
 				return fillRet == tui.FillContinue
 			})
-			switch fillRet {
-			case tui.FillNextLine:
+			t.previewer.more = t.previewer.more || t.pwindow.Y() == height-1 && t.pwindow.X() > 0
+			if fillRet == tui.FillNextLine {
 				continue
-			case tui.FillSuspend:
+			} else if fillRet == tui.FillSuspend {
 				break
 			}
 			t.pwindow.Fill("\n")
@@ -1065,6 +1069,7 @@ func (t *Terminal) printPreview() {
 	}
 	t.pwindow.FinishFill()
 	if t.previewer.lines > height {
+		t.previewer.more = true
 		offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines)
 		pos := t.pwindow.Width() - len(offset)
 		if t.tui.DoesAutoWrap() {
@@ -1100,6 +1105,7 @@ func (t *Terminal) printAll() {
 }
 
 func (t *Terminal) refresh() {
+	t.placeCursor()
 	if !t.suppress {
 		windows := make([]tui.Window, 0, 4)
 		if t.bordered {
@@ -1198,6 +1204,9 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
 		case 's':
 			flags.preserveSpace = true
 			skipChars++
+		case 'n':
+			flags.number = true
+			skipChars++
 		case 'q':
 			flags.query = true
 		default:
@@ -1253,7 +1262,16 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, fo
 
 		if match == "{}" {
 			for idx, item := range items {
-				replacements[idx] = quoteEntry(item.AsString(stripAnsi))
+				if flags.number {
+					n := int(item.text.Index)
+					if n < 0 {
+						replacements[idx] = ""
+					} else {
+						replacements[idx] = strconv.Itoa(n)
+					}
+				} else {
+					replacements[idx] = quoteEntry(item.AsString(stripAnsi))
+				}
 			}
 			return strings.Join(replacements, " ")
 		}
@@ -1351,7 +1369,7 @@ func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item
 	//   2. or it contains {+} and we have more than one item already selected.
 	// To do so, we pass an empty Item instead of nil to trigger an update.
 	if current == nil {
-		current = &Item{}
+		current = &minItem
 	}
 
 	var sels []*Item
@@ -1435,7 +1453,6 @@ func (t *Terminal) Loop() {
 		t.printPrompt()
 		t.printInfo()
 		t.printHeader()
-		t.placeCursor()
 		t.refresh()
 		t.mutex.Unlock()
 		go func() {
@@ -1479,8 +1496,12 @@ func (t *Terminal) Loop() {
 					cmd := util.ExecCommand(command, true)
 					if t.pwindow != nil {
 						env := os.Environ()
-						env = append(env, fmt.Sprintf("LINES=%d", t.pwindow.Height()))
-						env = append(env, fmt.Sprintf("COLUMNS=%d", t.pwindow.Width()))
+						lines := fmt.Sprintf("LINES=%d", t.pwindow.Height())
+						columns := fmt.Sprintf("COLUMNS=%d", t.pwindow.Width())
+						env = append(env, lines)
+						env = append(env, "FZF_PREVIEW_"+lines)
+						env = append(env, columns)
+						env = append(env, "FZF_PREVIEW_"+columns)
 						cmd.Env = env
 					}
 					var out bytes.Buffer
@@ -1602,7 +1623,6 @@ func (t *Terminal) Loop() {
 						exit(func() int { return exitInterrupt })
 					}
 				}
-				t.placeCursor()
 				t.refresh()
 				t.mutex.Unlock()
 			})
@@ -1615,7 +1635,8 @@ func (t *Terminal) Loop() {
 
 		t.mutex.Lock()
 		previousInput := t.input
-		events := []util.EventType{reqPrompt}
+		previousCx := t.cx
+		events := []util.EventType{}
 		req := func(evts ...util.EventType) {
 			for _, event := range evts {
 				events = append(events, event)
@@ -1631,9 +1652,15 @@ func (t *Terminal) Loop() {
 			}
 		}
 		scrollPreview := func(amount int) {
-			t.previewer.offset = util.Constrain(
+			if !t.previewer.more {
+				return
+			}
+			newOffset := util.Constrain(
 				t.previewer.offset+amount, 0, t.previewer.lines-1)
-			req(reqPreviewRefresh)
+			if t.previewer.offset != newOffset {
+				t.previewer.offset = newOffset
+				req(reqPreviewRefresh)
+			}
 		}
 		for key, ret := range t.expect {
 			if keyMatch(key, event) {
@@ -1675,7 +1702,7 @@ func (t *Terminal) Loop() {
 							t.previewBox.Set(reqPreviewEnqueue, list)
 						}
 					}
-					req(reqList, reqInfo, reqHeader)
+					req(reqPrompt, reqList, reqInfo, reqHeader)
 				}
 			case actTogglePreviewWrap:
 				if t.hasPreviewWindow() {
@@ -1976,7 +2003,6 @@ func (t *Terminal) Loop() {
 			t.jumping = jumpDisabled
 			req(reqList)
 		}
-		t.mutex.Unlock() // Must be unlocked before touching reqBox
 
 		if changed {
 			if t.isPreviewEnabled() {
@@ -1985,6 +2011,15 @@ func (t *Terminal) Loop() {
 					t.version++
 				}
 			}
+		}
+
+		if changed || t.cx != previousCx {
+			req(reqPrompt)
+		}
+
+		t.mutex.Unlock() // Must be unlocked before touching reqBox
+
+		if changed {
 			t.eventBox.Set(EvtSearchNew, t.sort)
 		}
 		for _, event := range events {
diff --git a/src/tui/light.go b/src/tui/light.go
index edeb621c..2c2dc004 100644
--- a/src/tui/light.go
+++ b/src/tui/light.go
@@ -163,9 +163,9 @@ func (r *LightRenderer) findOffset() (row int, col int) {
 	return -1, -1
 }
 
-func repeat(s string, times int) string {
+func repeat(r rune, times int) string {
 	if times > 0 {
-		return strings.Repeat(s, times)
+		return strings.Repeat(string(r), times)
 	}
 	return ""
 }
@@ -676,7 +676,7 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, bord
 }
 
 func (w *LightWindow) drawBorder() {
-	switch w.border {
+	switch w.border.shape {
 	case BorderAround:
 		w.drawBorderAround()
 	case BorderHorizontal:
@@ -686,22 +686,24 @@ func (w *LightWindow) drawBorder() {
 
 func (w *LightWindow) drawBorderHorizontal() {
 	w.Move(0, 0)
-	w.CPrint(ColBorder, AttrRegular, repeat("─", w.width))
+	w.CPrint(ColBorder, AttrRegular, repeat(w.border.horizontal, w.width))
 	w.Move(w.height-1, 0)
-	w.CPrint(ColBorder, AttrRegular, repeat("─", w.width))
+	w.CPrint(ColBorder, AttrRegular, repeat(w.border.horizontal, w.width))
 }
 
 func (w *LightWindow) drawBorderAround() {
 	w.Move(0, 0)
-	w.CPrint(ColBorder, AttrRegular, "┌"+repeat("─", w.width-2)+"┐")
+	w.CPrint(ColBorder, 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(ColBorder, AttrRegular, "│")
-		w.cprint2(colDefault, w.bg, AttrRegular, repeat(" ", w.width-2))
-		w.CPrint(ColBorder, AttrRegular, "│")
+		w.CPrint(ColBorder, AttrRegular, string(w.border.vertical))
+		w.cprint2(colDefault, w.bg, AttrRegular, repeat(' ', w.width-2))
+		w.CPrint(ColBorder, AttrRegular, string(w.border.vertical))
 	}
 	w.Move(w.height-1, 0)
-	w.CPrint(ColBorder, AttrRegular, "└"+repeat("─", w.width-2)+"┘")
+	w.CPrint(ColBorder, AttrRegular,
+		string(w.border.bottomLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.bottomRight))
 }
 
 func (w *LightWindow) csi(code string) {
@@ -762,7 +764,7 @@ func (w *LightWindow) MoveAndClear(y int, x int) {
 	w.Move(y, x)
 	// We should not delete preview window on the right
 	// csi("K")
-	w.Print(repeat(" ", w.width-x))
+	w.Print(repeat(' ', w.width-x))
 	w.Move(y, x)
 }
 
@@ -858,7 +860,7 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
 		width += w
 		str := string(r)
 		if r == '\t' {
-			str = repeat(" ", w)
+			str = repeat(' ', w)
 		}
 		if prefixLength+width <= max {
 			line += str
diff --git a/src/tui/tcell.go b/src/tui/tcell.go
index d337385c..098e8a18 100644
--- a/src/tui/tcell.go
+++ b/src/tui/tcell.go
@@ -61,12 +61,8 @@ func (w *TcellWindow) Refresh() {
 	}
 	w.lastX = 0
 	w.lastY = 0
-	switch w.borderStyle {
-	case BorderAround:
-		w.drawBorder(true)
-	case BorderHorizontal:
-		w.drawBorder(false)
-	}
+
+	w.drawBorder()
 }
 
 func (w *TcellWindow) FinishFill() {
@@ -570,7 +566,11 @@ func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
 	return w.fillString(str, NewColorPair(fg, bg), a)
 }
 
-func (w *TcellWindow) drawBorder(around bool) {
+func (w *TcellWindow) drawBorder() {
+	if w.borderStyle.shape == BorderNone {
+		return
+	}
+
 	left := w.left
 	right := left + w.width
 	top := w.top
@@ -584,19 +584,19 @@ func (w *TcellWindow) drawBorder(around bool) {
 	}
 
 	for x := left; x < right; x++ {
-		_screen.SetContent(x, top, tcell.RuneHLine, nil, style)
-		_screen.SetContent(x, bot-1, tcell.RuneHLine, nil, style)
+		_screen.SetContent(x, top, w.borderStyle.horizontal, nil, style)
+		_screen.SetContent(x, bot-1, w.borderStyle.horizontal, nil, style)
 	}
 
-	if around {
+	if w.borderStyle.shape == BorderAround {
 		for y := top; y < bot; y++ {
-			_screen.SetContent(left, y, tcell.RuneVLine, nil, style)
-			_screen.SetContent(right-1, y, tcell.RuneVLine, nil, style)
+			_screen.SetContent(left, y, w.borderStyle.vertical, nil, style)
+			_screen.SetContent(right-1, y, w.borderStyle.vertical, nil, style)
 		}
 
-		_screen.SetContent(left, top, tcell.RuneULCorner, nil, style)
-		_screen.SetContent(right-1, top, tcell.RuneURCorner, nil, style)
-		_screen.SetContent(left, bot-1, tcell.RuneLLCorner, nil, style)
-		_screen.SetContent(right-1, bot-1, tcell.RuneLRCorner, nil, style)
+		_screen.SetContent(left, top, w.borderStyle.topLeft, nil, style)
+		_screen.SetContent(right-1, top, w.borderStyle.topRight, nil, style)
+		_screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style)
+		_screen.SetContent(right-1, bot-1, w.borderStyle.bottomRight, nil, style)
 	}
 }
diff --git a/src/tui/tui.go b/src/tui/tui.go
index f1fac5e3..be5bd26a 100644
--- a/src/tui/tui.go
+++ b/src/tui/tui.go
@@ -172,6 +172,7 @@ type ColorTheme struct {
 	Fg           Color
 	Bg           Color
 	DarkBg       Color
+	Gutter       Color
 	Prompt       Color
 	Match        Color
 	Current      Color
@@ -200,14 +201,47 @@ type MouseEvent struct {
 	Mod    bool
 }
 
-type BorderStyle int
+type BorderShape int
 
 const (
-	BorderNone BorderStyle = iota
+	BorderNone BorderShape = iota
 	BorderAround
 	BorderHorizontal
 )
 
+type BorderStyle struct {
+	shape       BorderShape
+	horizontal  rune
+	vertical    rune
+	topLeft     rune
+	topRight    rune
+	bottomLeft  rune
+	bottomRight rune
+}
+
+func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
+	if unicode {
+		return BorderStyle{
+			shape:       shape,
+			horizontal:  '─',
+			vertical:    '│',
+			topLeft:     '┌',
+			topRight:    '┐',
+			bottomLeft:  '└',
+			bottomRight: '┘',
+		}
+	}
+	return BorderStyle{
+		shape:       shape,
+		horizontal:  '-',
+		vertical:    '|',
+		topLeft:     '+',
+		topRight:    '+',
+		bottomLeft:  '+',
+		bottomRight: '+',
+	}
+}
+
 type Renderer interface {
 	Init()
 	Pause(clear bool)
@@ -272,17 +306,19 @@ var (
 	Dark256   *ColorTheme
 	Light256  *ColorTheme
 
-	ColNormal       ColorPair
-	ColPrompt       ColorPair
-	ColMatch        ColorPair
-	ColCurrent      ColorPair
-	ColCurrentMatch ColorPair
-	ColSpinner      ColorPair
-	ColInfo         ColorPair
-	ColCursor       ColorPair
-	ColSelected     ColorPair
-	ColHeader       ColorPair
-	ColBorder       ColorPair
+	ColPrompt          ColorPair
+	ColNormal          ColorPair
+	ColMatch           ColorPair
+	ColCursor          ColorPair
+	ColSelected        ColorPair
+	ColCurrent         ColorPair
+	ColCurrentMatch    ColorPair
+	ColCurrentCursor   ColorPair
+	ColCurrentSelected ColorPair
+	ColSpinner         ColorPair
+	ColInfo            ColorPair
+	ColHeader          ColorPair
+	ColBorder          ColorPair
 )
 
 func EmptyTheme() *ColorTheme {
@@ -290,6 +326,7 @@ func EmptyTheme() *ColorTheme {
 		Fg:           colUndefined,
 		Bg:           colUndefined,
 		DarkBg:       colUndefined,
+		Gutter:       colUndefined,
 		Prompt:       colUndefined,
 		Match:        colUndefined,
 		Current:      colUndefined,
@@ -312,6 +349,7 @@ func init() {
 		Fg:           colDefault,
 		Bg:           colDefault,
 		DarkBg:       colBlack,
+		Gutter:       colBlack,
 		Prompt:       colBlue,
 		Match:        colGreen,
 		Current:      colYellow,
@@ -326,6 +364,7 @@ func init() {
 		Fg:           colDefault,
 		Bg:           colDefault,
 		DarkBg:       236,
+		Gutter:       colUndefined,
 		Prompt:       110,
 		Match:        108,
 		Current:      254,
@@ -340,6 +379,7 @@ func init() {
 		Fg:           colDefault,
 		Bg:           colDefault,
 		DarkBg:       251,
+		Gutter:       colUndefined,
 		Prompt:       25,
 		Match:        66,
 		Current:      237,
@@ -371,6 +411,7 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
 	theme.Fg = o(baseTheme.Fg, theme.Fg)
 	theme.Bg = o(baseTheme.Bg, theme.Bg)
 	theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg)
+	theme.Gutter = o(theme.DarkBg, o(baseTheme.Gutter, theme.Gutter))
 	theme.Prompt = o(baseTheme.Prompt, theme.Prompt)
 	theme.Match = o(baseTheme.Match, theme.Match)
 	theme.Current = o(baseTheme.Current, theme.Current)
@@ -392,27 +433,31 @@ func initPalette(theme *ColorTheme) {
 		return ColorPair{fg, bg, idx}
 	}
 	if theme != nil {
-		ColNormal = pair(theme.Fg, theme.Bg)
 		ColPrompt = pair(theme.Prompt, theme.Bg)
+		ColNormal = pair(theme.Fg, theme.Bg)
 		ColMatch = pair(theme.Match, theme.Bg)
+		ColCursor = pair(theme.Cursor, theme.Gutter)
+		ColSelected = pair(theme.Selected, theme.Gutter)
 		ColCurrent = pair(theme.Current, theme.DarkBg)
 		ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg)
+		ColCurrentCursor = pair(theme.Cursor, theme.DarkBg)
+		ColCurrentSelected = pair(theme.Selected, theme.DarkBg)
 		ColSpinner = pair(theme.Spinner, theme.Bg)
 		ColInfo = pair(theme.Info, theme.Bg)
-		ColCursor = pair(theme.Cursor, theme.DarkBg)
-		ColSelected = pair(theme.Selected, theme.DarkBg)
 		ColHeader = pair(theme.Header, theme.Bg)
 		ColBorder = pair(theme.Border, theme.Bg)
 	} else {
-		ColNormal = pair(colDefault, colDefault)
 		ColPrompt = pair(colDefault, colDefault)
+		ColNormal = pair(colDefault, colDefault)
 		ColMatch = pair(colDefault, colDefault)
+		ColCursor = pair(colDefault, colDefault)
+		ColSelected = pair(colDefault, colDefault)
 		ColCurrent = pair(colDefault, colDefault)
 		ColCurrentMatch = pair(colDefault, colDefault)
+		ColCurrentCursor = pair(colDefault, colDefault)
+		ColCurrentSelected = pair(colDefault, colDefault)
 		ColSpinner = pair(colDefault, colDefault)
 		ColInfo = pair(colDefault, colDefault)
-		ColCursor = pair(colDefault, colDefault)
-		ColSelected = pair(colDefault, colDefault)
 		ColHeader = pair(colDefault, colDefault)
 		ColBorder = pair(colDefault, colDefault)
 	}
diff --git a/src/util/chars.go b/src/util/chars.go
index ec6fca0e..04a66d7d 100644
--- a/src/util/chars.go
+++ b/src/util/chars.go
@@ -171,3 +171,12 @@ func (chars *Chars) CopyRunes(dest []rune) {
 	}
 	return
 }
+
+func (chars *Chars) Prepend(prefix string) {
+	if runes := chars.optionalRunes(); runes != nil {
+		runes = append([]rune(prefix), runes...)
+		chars.slice = *(*[]byte)(unsafe.Pointer(&runes))
+	} else {
+		chars.slice = append([]byte(prefix), chars.slice...)
+	}
+}


Test patch:
diff --git a/src/ansi_test.go b/src/ansi_test.go
index d94ae931..e10893c9 100644
--- a/src/ansi_test.go
+++ b/src/ansi_test.go
@@ -2,6 +2,7 @@ package fzf
 
 import (
 	"fmt"
+	"strings"
 	"testing"
 
 	"github.com/junegunn/fzf/src/tui"
@@ -156,3 +157,31 @@ func TestExtractColor(t *testing.T) {
 		assert((*offsets)[1], 6, 11, 200, 100, false)
 	})
 }
+
+func TestAnsiCodeStringConversion(t *testing.T) {
+	assert := func(code string, prevState *ansiState, expected string) {
+		state := interpretCode(code, prevState)
+		if expected != state.ToString() {
+			t.Errorf("expected: %s, actual: %s",
+				strings.Replace(expected, "\x1b[", "\\x1b[", -1),
+				strings.Replace(state.ToString(), "\x1b[", "\\x1b[", -1))
+		}
+	}
+	assert("\x1b[m", nil, "")
+	assert("\x1b[m", &ansiState{attr: tui.Blink}, "")
+
+	assert("\x1b[31m", nil, "\x1b[31;49m")
+	assert("\x1b[41m", nil, "\x1b[39;41m")
+
+	assert("\x1b[92m", nil, "\x1b[92;49m")
+	assert("\x1b[102m", nil, "\x1b[39;102m")
+
+	assert("\x1b[31m", &ansiState{fg: 4, bg: 4}, "\x1b[31;44m")
+	assert("\x1b[1;2;31m", &ansiState{fg: 2, bg: -1, attr: tui.Reverse}, "\x1b[1;2;7;31;49m")
+	assert("\x1b[38;5;100;48;5;200m", nil, "\x1b[38;5;100;48;5;200m")
+	assert("\x1b[48;5;100;38;5;200m", nil, "\x1b[38;5;200;48;5;100m")
+	assert("\x1b[48;5;100;38;2;10;20;30;1m", nil, "\x1b[1;38;2;10;20;30;48;5;100m")
+	assert("\x1b[48;5;100;38;2;10;20;30;7m",
+		&ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1},
+		"\x1b[2;3;7;38;2;10;20;30;48;5;100m")
+}
diff --git a/test/test_go.rb b/test/test_go.rb
index c34a9259..4d3fa647 100755
--- a/test/test_go.rb
+++ b/test/test_go.rb
@@ -1371,7 +1371,7 @@ class TestGoFZF < TestBase
   end
 
   def test_preview_hidden
-    tmux.send_keys %(seq 1000 | #{FZF} --preview 'echo {{}-{}-\\$LINES-\\$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]+}/ }
@@ -1400,21 +1400,21 @@ class TestGoFZF < TestBase
 
   def test_preview_flags
     tmux.send_keys %(seq 10 | sed 's/^/:: /; s/$/  /' |
-        #{FZF} --multi --preview 'echo {{2}/{s2}/{+2}/{+s2}/{q}}'), :Enter
-    tmux.until { |lines| lines[1].include?('{1/1  /1/1  /}') }
+        #{FZF} --multi --preview 'echo {{2}/{s2}/{+2}/{+s2}/{q}/{n}/{+n}}'), :Enter
+    tmux.until { |lines| lines[1].include?('{1/1  /1/1  //0/0}') }
     tmux.send_keys '123'
-    tmux.until { |lines| lines[1].include?('{////123}') }
+    tmux.until { |lines| lines[1].include?('{////123//}') }
     tmux.send_keys 'C-u', '1'
     tmux.until { |lines| lines.match_count == 2 }
-    tmux.until { |lines| lines[1].include?('{1/1  /1/1  /1}') }
+    tmux.until { |lines| lines[1].include?('{1/1  /1/1  /1/0/0}') }
     tmux.send_keys :BTab
-    tmux.until { |lines| lines[1].include?('{10/10  /1/1  /1}') }
+    tmux.until { |lines| lines[1].include?('{10/10  /1/1  /1/9/0}') }
     tmux.send_keys :BTab
-    tmux.until { |lines| lines[1].include?('{10/10  /1 10/1   10  /1}') }
+    tmux.until { |lines| lines[1].include?('{10/10  /1 10/1   10  /1/9/0 9}') }
     tmux.send_keys '2'
-    tmux.until { |lines| lines[1].include?('{//1 10/1   10  /12}') }
+    tmux.until { |lines| lines[1].include?('{//1 10/1   10  /12//0 9}') }
     tmux.send_keys '3'
-    tmux.until { |lines| lines[1].include?('{//1 10/1   10  /123}') }
+    tmux.until { |lines| lines[1].include?('{//1 10/1   10  /123//0 9}') }
   end
 
   def test_preview_q_no_match