Repository: junegunn/fzf
Pull request #2251 (2020-11-14 09:16:01)

[zsh] Reload shared history before searching
When history sharing across shells is enabled (`setopt SHARE_HISTORY`), history written by shell A won't be available in shell B until re-rendering the prompt in B (e.g. by pressing Enter at the prompt).

With this change, C-r will always reload the history file if history sharing is enabled, making the latest history searchable immediately.


Patch:
diff --git a/.travis.yml b/.travis.yml
index e3683bff..40898c8f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,6 +6,7 @@ os:
   - linux
   - osx
 dist: bionic
+osx_image: xcode12.2
 addons:
   apt:
     packages:
@@ -17,7 +18,6 @@ addons:
     packages:
       - fish
       - tmux
-    update: true
 install: gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1
 script:
   - make test
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a305ecc7..9d2bf95b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,23 @@
 CHANGELOG
 =========
 
+0.24.4
+------
+- Added `--preview-window` option `follow`
+  ```sh
+  # Preview window will automatically scroll to the bottom
+  fzf --preview-window follow --preview 'for i in $(seq 100000); do
+    echo "$i"
+    sleep 0.01
+    (( i % 300 == 0 )) && printf "\033[2J"
+  done'
+  ```
+- Added `change-prompt` action
+  ```sh
+  fzf --prompt 'foo> ' --bind $'a:change-prompt:\x1b[31mbar> '
+  ```
+- Bug fixes and improvements
+
 0.24.3
 ------
 - Added `--padding` option
diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1
index 25465424..7f0cda19 100644
--- a/man/man1/fzf-tmux.1
+++ b/man/man1/fzf-tmux.1
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
 ..
-.TH fzf-tmux 1 "Nov 2020" "fzf 0.24.3" "fzf-tmux - open fzf in tmux split pane"
+.TH fzf-tmux 1 "Dec 2020" "fzf 0.24.3" "fzf-tmux - open fzf in tmux split pane"
 
 .SH NAME
 fzf-tmux - open fzf in tmux split pane
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 99147403..eeb83945 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 "Nov 2020" "fzf 0.24.3" "fzf - a command-line fuzzy finder"
+.TH fzf 1 "Dec 2020" "fzf 0.24.4" "fzf - a command-line fuzzy finder"
 
 .SH NAME
 fzf - a command-line fuzzy finder
@@ -439,7 +439,7 @@ e.g.
       done'\fR
 .RE
 .TP
-.BI "--preview-window=" "[POSITION][:SIZE[%]][:rounded|sharp|noborder][:[no]wrap][:[no]cycle][:[no]hidden][:+SCROLL[-OFFSET]][:default]"
+.BI "--preview-window=" "[POSITION][:SIZE[%]][:rounded|sharp|noborder][:[no]wrap][:[no]follow][:[no]cycle][:[no]hidden][:+SCROLL[-OFFSET]][:default]"
 
 .RS
 .B POSITION: (default: right)
@@ -448,27 +448,43 @@ e.g.
     \fBleft
     \fBright
 
-\fRDetermines the layout of the preview window. If the argument contains
-\fB:hidden\fR, the preview window will be hidden by default until
-\fBtoggle-preview\fR action is triggered. Long lines are truncated by default.
-Line wrap can be enabled with \fB:wrap\fR flag. Cyclic scrolling is enabled
-with \fB:cycle\fR flag.
+\fRDetermines the layout of the preview window.
 
-If size is given as 0, preview window will not be visible, but fzf will still
+* If the argument contains \fB:hidden\fR, the preview window will be hidden by
+default until \fBtoggle-preview\fR action is triggered.
+
+* If size is given as 0, preview window will not be visible, but fzf will still
 execute the command in the background.
 
-To change the style of the border of the preview window, specify one of
+* Long lines are truncated by default. Line wrap can be enabled with
+\fB:wrap\fR flag.
+
+* Preview window will automatically scroll to the bottom when \fB:follow\fR
+flag is set, similarly to how \fBtail -f\fR works.
+
+.RS
+e.g.
+      \fBfzf --preview-window follow --preview 'for i in $(seq 100000); do
+        echo "$i"
+        sleep 0.01
+        (( i % 300 == 0 )) && printf "\\033[2J"
+      done'\fR
+.RE
+
+* Cyclic scrolling is enabled with \fB:cycle\fR flag.
+
+* To change the style of the border of the preview window, specify one of
 \fBrounded\fR (border with rounded edges, default), \fBsharp\fR (border with
 sharp edges), or \fBnoborder\fR (no border).
 
-\fB+SCROLL[-OFFSET]\fR determines the initial scroll offset of the preview
+* \fB+SCROLL[-OFFSET]\fR determines the initial scroll offset of the preview
 window. \fBSCROLL\fR can be either a numeric integer or a single-field index
 expression that refers to a numeric integer. The optional \fB-OFFSET\fR part is
 for adjusting the base offset so that you can see the text above it. It should
 be given as a numeric integer (\fB-INTEGER\fR), or as a denominator form
 (\fB-/INTEGER\fR) for specifying a fraction of the preview window height.
 
-\fBdefault\fR resets all options previously set to the default.
+* \fBdefault\fR resets all options previously set to the default.
 
 .RS
 e.g.
@@ -712,6 +728,14 @@ e.g.
 .br
 \fIshift-right\fR
 .br
+\fIalt-shift-up\fR
+.br
+\fIalt-shift-down\fR
+.br
+\fIalt-shift-left\fR
+.br
+\fIalt-shift-right\fR
+.br
 \fIleft-click\fR
 .br
 \fIright-click\fR
@@ -753,6 +777,7 @@ A key or an event can be bound to one or more of the following actions.
     \fBbackward-word\fR             \fIalt-b   shift-left\fR
     \fBbeginning-of-line\fR         \fIctrl-a  home\fR
     \fBcancel\fR                    (clear query string if not empty, abort fzf otherwise)
+    \fBchange-prompt(...)\fR        (change prompt to the given string)
     \fBclear-screen\fR              \fIctrl-l\fR
     \fBclear-selection\fR           (clear multi-selection)
     \fBclear-query\fR               (clear query string)
@@ -808,42 +833,51 @@ A key or an event can be bound to one or more of the following actions.
 Multiple actions can be chained using \fB+\fR separator.
 
 e.g.
-     \fBfzf --bind 'ctrl-a:select-all+accept'\fR
+     \fBfzf --multi --bind 'ctrl-a:select-all+accept'\fR
+     \fBfzf --multi --bind 'ctrl-a:select-all' --bind 'ctrl-a:+accept'\fR
 
-.SS COMMAND EXECUTION
+.SS ACTION ARGUMENT
 
-With \fBexecute(...)\fR action, you can execute arbitrary commands without
-leaving fzf. For example, you can turn fzf into a simple file browser by
-binding \fBenter\fR key to \fBless\fR command like follows.
-
-    \fBfzf --bind "enter:execute(less {})"\fR
+An action denoted with \fB(...)\fR suffix takes an argument.
 
-You can use the same placeholder expressions as in \fB--preview\fR.
+e.g.
+     \fBfzf --bind 'ctrl-a:change-prompt(NewPrompt> )'\fR
+     \fBfzf --bind 'ctrl-v:preview(cat {})' --preview-window hidden\fR
 
-If the command contains parentheses, fzf may fail to parse the expression. In
+If the argument contains parentheses, fzf may fail to parse the expression. In
 that case, you can use any of the following alternative notations to avoid
 parse errors.
 
-    \fBexecute[...]\fR
-    \fBexecute~...~\fR
-    \fBexecute!...!\fR
-    \fBexecute@...@\fR
-    \fBexecute#...#\fR
-    \fBexecute$...$\fR
-    \fBexecute%...%\fR
-    \fBexecute^...^\fR
-    \fBexecute&...&\fR
-    \fBexecute*...*\fR
-    \fBexecute;...;\fR
-    \fBexecute/.../\fR
-    \fBexecute|...|\fR
-    \fBexecute:...\fR
+    \fBaction-name[...]\fR
+    \fBaction-name~...~\fR
+    \fBaction-name!...!\fR
+    \fBaction-name@...@\fR
+    \fBaction-name#...#\fR
+    \fBaction-name$...$\fR
+    \fBaction-name%...%\fR
+    \fBaction-name^...^\fR
+    \fBaction-name&...&\fR
+    \fBaction-name*...*\fR
+    \fBaction-name;...;\fR
+    \fBaction-name/.../\fR
+    \fBaction-name|...|\fR
+    \fBaction-name:...\fR
 .RS
 The last one is the special form that frees you from parse errors as it does
 not expect the closing character. The catch is that it should be the last one
 in the comma-separated list of key-action pairs.
 .RE
 
+.SS COMMAND EXECUTION
+
+With \fBexecute(...)\fR action, you can execute arbitrary commands without
+leaving fzf. For example, you can turn fzf into a simple file browser by
+binding \fBenter\fR key to \fBless\fR command like follows.
+
+    \fBfzf --bind "enter:execute(less {})"\fR
+
+You can use the same placeholder expressions as in \fB--preview\fR.
+
 fzf switches to the alternate screen when executing a command. However, if the
 command is expected to complete quickly, and you are not interested in its
 output, you might want to use \fBexecute-silent\fR instead, which silently
diff --git a/plugin/fzf.vim b/plugin/fzf.vim
index 5fa555a3..47051157 100644
--- a/plugin/fzf.vim
+++ b/plugin/fzf.vim
@@ -645,7 +645,8 @@ function! s:execute(dict, command, use_height, temps) abort
   endif
   let exit_status = v:shell_error
   redraw!
-  return s:exit_handler(exit_status, command) ? s:collect(a:temps) : []
+  let lines = s:collect(a:temps)
+  return s:exit_handler(exit_status, command) ? lines : []
 endfunction
 
 function! s:execute_tmux(dict, command, temps) abort
@@ -659,7 +660,8 @@ function! s:execute_tmux(dict, command, temps) abort
   call system(command)
   let exit_status = v:shell_error
   redraw!
-  return s:exit_handler(exit_status, command) ? s:collect(a:temps) : []
+  let lines = s:collect(a:temps)
+  return s:exit_handler(exit_status, command) ? lines : []
 endfunction
 
 function! s:calc_size(max, val, dict)
@@ -806,12 +808,12 @@ function! s:execute_term(dict, command, temps) abort
       execute self.winrest
     endif
 
+    let lines = s:collect(self.temps)
     if !s:exit_handler(a:code, self.command, 1)
       return
     endif
 
     call s:pushd(self.dict)
-    let lines = s:collect(self.temps)
     call s:callback(self.dict, lines)
     call self.switch_back(s:getpos() == self.ppos)
   endfunction
@@ -836,6 +838,9 @@ function! s:execute_term(dict, command, temps) abort
         let term_opts.curwin = 1
       endif
       let fzf.buf = term_start([&shell, &shellcmdflag, command], term_opts)
+      if exists('&termwinkey')
+        call setbufvar(fzf.buf, '&termwinkey', '<c-z>')
+      endif
       if is_popup && exists('#TerminalWinOpen')
         doautocmd <nomodeline> TerminalWinOpen
       endif
@@ -843,6 +848,7 @@ function! s:execute_term(dict, command, temps) abort
         call term_wait(fzf.buf, 20)
       endif
     endif
+    tnoremap <buffer> <c-z> <nop>
   finally
     call s:dopopd()
   endtry
diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh
index 63306142..4755c58d 100644
--- a/shell/key-bindings.zsh
+++ b/shell/key-bindings.zsh
@@ -45,6 +45,7 @@ __fsel() {
     -o -type d -print \
     -o -type l -print 2> /dev/null | cut -b3-"}"
   setopt localoptions pipefail no_aliases 2> /dev/null
+  local item
   eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read item; do
     echo -n "${(q)item} "
   done
@@ -106,6 +107,7 @@ bindkey '\ec' fzf-cd-widget
 fzf-history-widget() {
   local selected num
   setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
+  [[ -o sharehistory ]] && fc -RI
   selected=( $(fc -rl 1 | perl -ne 'print if !$seen{(/^\s*[0-9]+\**\s+(.*)/, $1)}++' |
     FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) )
   local ret=$?
diff --git a/src/ansi.go b/src/ansi.go
index b15bd91c..0d716c0d 100644
--- a/src/ansi.go
+++ b/src/ansi.go
@@ -19,17 +19,18 @@ type ansiState struct {
 	fg   tui.Color
 	bg   tui.Color
 	attr tui.Attr
+	lbg  tui.Color
 }
 
 func (s *ansiState) colored() bool {
-	return s.fg != -1 || s.bg != -1 || s.attr > 0
+	return s.fg != -1 || s.bg != -1 || s.attr > 0 || s.lbg >= 0
 }
 
 func (s *ansiState) equals(t *ansiState) bool {
 	if t == nil {
 		return !s.colored()
 	}
-	return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr
+	return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr && s.lbg == t.lbg
 }
 
 func (s *ansiState) ToString() string {
@@ -195,11 +196,14 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
 	// State
 	var state *ansiState
 	if prevState == nil {
-		state = &ansiState{-1, -1, 0}
+		state = &ansiState{-1, -1, 0, -1}
 	} else {
-		state = &ansiState{prevState.fg, prevState.bg, prevState.attr}
+		state = &ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg}
 	}
 	if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
+		if strings.HasSuffix(ansiCode, "0K") {
+			state.lbg = prevState.bg
+		}
 		return state
 	}
 
diff --git a/src/options.go b/src/options.go
index d7e1eaa6..0607e5dc 100644
--- a/src/options.go
+++ b/src/options.go
@@ -83,7 +83,7 @@ const usage = `usage: fzf [options]
     --preview=COMMAND     Command to preview highlighted line ({})
     --preview-window=OPT  Preview window layout (default: right:50%)
                           [up|down|left|right][:SIZE[%]]
-                          [:[no]wrap][:[no]cycle][:[no]hidden]
+                          [:[no]wrap][:[no]cycle][:[no]follow][:[no]hidden]
                           [:rounded|sharp|noborder]
                           [:+SCROLL[-OFFSET]]
                           [:default]
@@ -169,6 +169,7 @@ type previewOpts struct {
 	hidden   bool
 	wrap     bool
 	cycle    bool
+	follow   bool
 	border   tui.BorderShape
 }
 
@@ -231,7 +232,7 @@ type Options struct {
 }
 
 func defaultPreviewOpts(command string) previewOpts {
-	return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, tui.BorderRounded}
+	return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded}
 }
 
 func defaultOptions() *Options {
@@ -524,6 +525,14 @@ func parseKeyChords(str string, message string) map[int]string {
 			chord = tui.PgUp
 		case "pgdn", "page-down":
 			chord = tui.PgDn
+		case "alt-shift-up", "shift-alt-up":
+			chord = tui.AltSUp
+		case "alt-shift-down", "shift-alt-down":
+			chord = tui.AltSDown
+		case "alt-shift-left", "shift-alt-left":
+			chord = tui.AltSLeft
+		case "alt-shift-right", "shift-alt-right":
+			chord = tui.AltSRight
 		case "shift-up":
 			chord = tui.SUp
 		case "shift-down":
@@ -727,7 +736,7 @@ func init() {
 	// Backreferences are not supported.
 	// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
 	executeRegexp = regexp.MustCompile(
-		`(?si)[:+](execute(?:-multi|-silent)?|reload|preview):.+|[:+](execute(?:-multi|-silent)?|reload|preview)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
+		`(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
 }
 
 func parseKeymap(keymap map[int][]action, str string) {
@@ -741,6 +750,8 @@ func parseKeymap(keymap map[int][]action, str string) {
 			prefix = symbol + "reload"
 		} else if strings.HasPrefix(src[1:], "preview") {
 			prefix = symbol + "preview"
+		} else if strings.HasPrefix(src[1:], "change-prompt") {
+			prefix = symbol + "change-prompt"
 		} else if src[len(prefix)] == '-' {
 			c := src[len(prefix)+1]
 			if c == 's' || c == 'S' {
@@ -914,6 +925,8 @@ func parseKeymap(keymap map[int][]action, str string) {
 						offset = len("reload")
 					case actPreview:
 						offset = len("preview")
+					case actChangePrompt:
+						offset = len("change-prompt")
 					case actExecuteSilent:
 						offset = len("execute-silent")
 					case actExecuteMulti:
@@ -953,6 +966,8 @@ func isExecuteAction(str string) actionType {
 		return actReload
 	case "preview":
 		return actPreview
+	case "change-prompt":
+		return actChangePrompt
 	case "execute":
 		return actExecute
 	case "execute-silent":
@@ -1067,6 +1082,10 @@ func parsePreviewWindow(opts *previewOpts, input string) {
 			opts.border = tui.BorderSharp
 		case "noborder":
 			opts.border = tui.BorderNone
+		case "follow":
+			opts.follow = true
+		case "nofollow":
+			opts.follow = false
 		default:
 			if sizeRegex.MatchString(token) {
 				opts.size = parseSize(token, 99, "window size")
diff --git a/src/result.go b/src/result.go
index b3971f44..77b4e3b0 100644
--- a/src/result.go
+++ b/src/result.go
@@ -160,7 +160,19 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
 				color := colMatch
 				if curr < -1 && theme.Colored {
 					origColor := ansiToColorPair(itemColors[-curr-2], colMatch)
-					color = origColor.MergeNonDefault(color)
+					// hl or hl+ only sets the foreground color, so colMatch is the
+					// combination of either [hl and bg] or [hl+ and bg+].
+					//
+					// If the original text already has background color, and the
+					// forground color of colMatch is -1, we shouldn't only apply the
+					// background color of colMatch.
+					// e.g. echo -e "\x1b[32;7mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline
+					//      echo -e "\x1b[42mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline
+					if color.Fg().IsDefault() && origColor.HasBg() {
+						color = origColor
+					} else {
+						color = origColor.MergeNonDefault(color)
+					}
 				}
 				colors = append(colors, colorOffset{
 					offset: [2]int32{int32(start), int32(idx)}, color: color})
diff --git a/src/terminal.go b/src/terminal.go
index 0a174822..83b3a7b5 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -51,6 +51,7 @@ type previewer struct {
 	enabled    bool
 	scrollable bool
 	final      bool
+	following  bool
 	spinner    string
 }
 
@@ -140,7 +141,7 @@ type Terminal struct {
 	selected     map[int32]selectedItem
 	version      int64
 	reqBox       *util.EventBox
-	preview      previewOpts
+	previewOpts  previewOpts
 	previewer    previewer
 	previewed    previewed
 	previewBox   *util.EventBox
@@ -214,6 +215,7 @@ const (
 	actBackwardDeleteCharEOF
 	actBackwardWord
 	actCancel
+	actChangePrompt
 	actClearScreen
 	actClearQuery
 	actClearSelection
@@ -492,8 +494,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
 		merger:      EmptyMerger,
 		selected:    make(map[int32]selectedItem),
 		reqBox:      util.NewEventBox(),
-		preview:     opts.Preview,
-		previewer:   previewer{0, []string{}, 0, previewBox != nil && !opts.Preview.hidden, false, true, ""},
+		previewOpts: opts.Preview,
+		previewer:   previewer{0, []string{}, 0, previewBox != nil && !opts.Preview.hidden, false, true, false, ""},
 		previewed:   previewed{0, 0, 0, false},
 		previewBox:  previewBox,
 		eventBox:    eventBox,
@@ -525,7 +527,7 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) {
 	//             // unless the part has a non-default ANSI state
 	loc := whiteSuffix.FindStringIndex(trimmed)
 	if loc != nil {
-		blankState := ansiOffset{[2]int32{int32(loc[0]), int32(loc[1])}, ansiState{-1, -1, tui.AttrClear}}
+		blankState := ansiOffset{[2]int32{int32(loc[0]), int32(loc[1])}, ansiState{-1, -1, tui.AttrClear, -1}}
 		if item.colors != nil {
 			lastColor := (*item.colors)[len(*item.colors)-1]
 			if lastColor.offset[1] < int32(loc[1]) {
@@ -656,8 +658,6 @@ func (t *Terminal) displayWidth(runes []rune) int {
 const (
 	minWidth  = 4
 	minHeight = 4
-
-	maxDisplayWidthCalc = 1024
 )
 
 func calculateSize(base int, size sizeSpec, occupied int, minSize int, pad int) int {
@@ -733,11 +733,11 @@ func (t *Terminal) resizeWindows() {
 		}
 	}
 
-	previewVisible := t.isPreviewEnabled() && t.preview.size.size > 0
+	previewVisible := t.isPreviewEnabled() && t.previewOpts.size.size > 0
 	minAreaWidth := minWidth
 	minAreaHeight := minHeight
 	if previewVisible {
-		switch t.preview.position {
+		switch t.previewOpts.position {
 		case posUp, posDown:
 			minAreaHeight *= 2
 		case posLeft, posRight:
@@ -806,8 +806,8 @@ func (t *Terminal) resizeWindows() {
 		createPreviewWindow := func(y int, x int, w int, h int) {
 			pwidth := w
 			pheight := h
-			if t.preview.border != tui.BorderNone {
-				previewBorder := tui.MakeBorderStyle(t.preview.border, t.unicode)
+			if t.previewOpts.border != tui.BorderNone {
+				previewBorder := tui.MakeBorderStyle(t.previewOpts.border, t.unicode)
 				t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder)
 				pwidth -= 4
 				pheight -= 2
@@ -823,28 +823,28 @@ func (t *Terminal) resizeWindows() {
 		}
 		verticalPad := 2
 		minPreviewHeight := 3
-		if t.preview.border == tui.BorderNone {
+		if t.previewOpts.border == tui.BorderNone {
 			verticalPad = 0
 			minPreviewHeight = 1
 		}
-		switch t.preview.position {
+		switch t.previewOpts.position {
 		case posUp:
-			pheight := calculateSize(height, t.preview.size, minHeight, minPreviewHeight, verticalPad)
+			pheight := calculateSize(height, t.previewOpts.size, minHeight, minPreviewHeight, verticalPad)
 			t.window = t.tui.NewWindow(
 				marginInt[0]+pheight, marginInt[3], width, height-pheight, false, noBorder)
 			createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
 		case posDown:
-			pheight := calculateSize(height, t.preview.size, minHeight, minPreviewHeight, verticalPad)
+			pheight := calculateSize(height, t.previewOpts.size, minHeight, minPreviewHeight, verticalPad)
 			t.window = t.tui.NewWindow(
 				marginInt[0], marginInt[3], width, height-pheight, false, noBorder)
 			createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
 		case posLeft:
-			pwidth := calculateSize(width, t.preview.size, minWidth, 5, 4)
+			pwidth := calculateSize(width, t.previewOpts.size, minWidth, 5, 4)
 			t.window = t.tui.NewWindow(
 				marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false, noBorder)
 			createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
 		case posRight:
-			pwidth := calculateSize(width, t.preview.size, minWidth, 5, 4)
+			pwidth := calculateSize(width, t.previewOpts.size, minWidth, 5, 4)
 			t.window = t.tui.NewWindow(
 				marginInt[0], marginInt[3], width-pwidth, height, false, noBorder)
 			createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height)
@@ -1116,13 +1116,15 @@ func (t *Terminal) displayWidthWithLimit(runes []rune, prefixWidth int, limit in
 }
 
 func (t *Terminal) trimLeft(runes []rune, width int) ([]rune, int32) {
-	if len(runes) > maxDisplayWidthCalc && len(runes) > width {
-		trimmed := len(runes) - width
-		return runes[trimmed:], int32(trimmed)
+	var trimmed int32
+	// Assume that each rune takes at least one column on screen
+	if len(runes) > width {
+		diff := len(runes) - width
+		trimmed = int32(diff)
+		runes = runes[diff:]
 	}
 
 	currentWidth := t.displayWidth(runes)
-	var trimmed int32
 
 	for currentWidth > width && len(runes) > 0 {
 		runes = runes[1:]
@@ -1277,6 +1279,10 @@ func (t *Terminal) renderPreviewText(unchanged bool) {
 	}
 	var ansi *ansiState
 	for _, line := range t.previewer.lines {
+		var lbg tui.Color = -1
+		if ansi != nil {
+			ansi.lbg = -1
+		}
 		line = strings.TrimSuffix(line, "\n")
 		if lineNo >= height || t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
 			t.previewed.filled = true
@@ -1286,12 +1292,13 @@ func (t *Terminal) renderPreviewText(unchanged bool) {
 			prefixWidth := 0
 			_, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool {
 				trimmed := []rune(str)
-				if !t.preview.wrap {
+				if !t.previewOpts.wrap {
 					trimmed, _ = t.trimRight(trimmed, maxWidth-t.pwindow.X())
 				}
 				str, width := t.processTabs(trimmed, prefixWidth)
 				prefixWidth += width
 				if t.theme.Colored && ansi != nil && ansi.colored() {
+					lbg = ansi.lbg
 					fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str)
 				} else {
 					fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str)
@@ -1308,7 +1315,12 @@ func (t *Terminal) renderPreviewText(unchanged bool) {
 			if unchanged && lineNo == 0 {
 				break
 			}
-			t.pwindow.Fill("\n")
+			if lbg >= 0 {
+				t.pwindow.CFill(-1, lbg, tui.AttrRegular,
+					strings.Repeat(" ", t.pwindow.Width()-t.pwindow.X())+"\n")
+			} else {
+				t.pwindow.Fill("\n")
+			}
 		}
 		lineNo++
 	}
@@ -1548,7 +1560,7 @@ func atopi(s string) int {
 }
 
 func (t *Terminal) evaluateScrollOffset(list []*Item, height int) int {
-	offsetExpr := t.replacePlaceholder(t.preview.scroll, false, "", list)
+	offsetExpr := t.replacePlaceholder(t.previewOpts.scroll, false, "", list)
 	nums := strings.Split(offsetExpr, "-")
 	switch len(nums) {
 	case 0:
@@ -2030,7 +2042,7 @@ func (t *Terminal) Loop() {
 						if focusedIndex != currentIndex || version != t.version {
 							version = t.version
 							focusedIndex = currentIndex
-							refreshPreview(t.preview.command)
+							refreshPreview(t.previewOpts.command)
 						}
 					case reqJump:
 						if t.merger.Length() == 0 {
@@ -2055,10 +2067,15 @@ func (t *Terminal) Loop() {
 						})
 					case reqPreviewDisplay:
 						result := value.(previewResult)
-						t.previewer.version = result.version
+						if t.previewer.version != result.version {
+							t.previewer.version = result.version
+							t.previewer.following = t.previewOpts.follow
+						}
 						t.previewer.lines = result.lines
 						t.previewer.spinner = result.spinner
-						if result.offset >= 0 {
+						if t.previewer.following {
+							t.previewer.offset = len(t.previewer.lines) - t.pwindow.Height()
+						} else if result.offset >= 0 {
 							t.previewer.offset = util.Constrain(result.offset, 0, len(t.previewer.lines)-1)
 						}
 						t.printPreview()
@@ -2122,9 +2139,10 @@ func (t *Terminal) Loop() {
 			if !t.previewer.scrollable {
 				return
 			}
+			t.previewer.following = false
 			newOffset := t.previewer.offset + amount
 			numLines := len(t.previewer.lines)
-			if t.preview.cycle {
+			if t.previewOpts.cycle {
 				newOffset = (newOffset + numLines) % numLines
 			}
 			newOffset = util.Constrain(newOffset, 0, numLines-1)
@@ -2165,17 +2183,17 @@ func (t *Terminal) Loop() {
 				if t.hasPreviewer() {
 					togglePreview(!t.previewer.enabled)
 					if t.previewer.enabled {
-						valid, list := t.buildPlusList(t.preview.command, false)
+						valid, list := t.buildPlusList(t.previewOpts.command, false)
 						if valid {
 							t.cancelPreview()
 							t.previewBox.Set(reqPreviewEnqueue,
-								previewRequest{t.preview.command, t.pwindow, list})
+								previewRequest{t.previewOpts.command, t.pwindow, list})
 						}
 					}
 				}
 			case actTogglePreviewWrap:
 				if t.hasPreviewWindow() {
-					t.preview.wrap = !t.preview.wrap
+					t.previewOpts.wrap = !t.previewOpts.wrap
 					req(reqPreviewRefresh)
 				}
 			case actToggleSort:
@@ -2213,11 +2231,14 @@ func (t *Terminal) Loop() {
 				}
 			case actPrintQuery:
 				req(reqPrintQuery)
+			case actChangePrompt:
+				t.prompt, t.promptLen = t.parsePrompt(a.a)
+				req(reqPrompt)
 			case actPreview:
 				togglePreview(true)
 				refreshPreview(a.a)
 			case actRefreshPreview:
-				refreshPreview(t.preview.command)
+				refreshPreview(t.previewOpts.command)
 			case actReplaceQuery:
 				if t.cy >= 0 && t.cy < t.merger.Length() {
 					t.input = t.merger.Get(t.cy).item.text.ToRunes()
@@ -2540,7 +2561,7 @@ func (t *Terminal) Loop() {
 
 		if queryChanged {
 			if t.isPreviewEnabled() {
-				_, _, q := hasPreviewFlags(t.preview.command)
+				_, _, q := hasPreviewFlags(t.previewOpts.command)
 				if q {
 					t.version++
 				}
diff --git a/src/tui/dummy.go b/src/tui/dummy.go
index ca50e655..af7e759c 100644
--- a/src/tui/dummy.go
+++ b/src/tui/dummy.go
@@ -4,7 +4,7 @@
 
 package tui
 
-type Attr int
+type Attr int32
 
 func HasFullscreenRenderer() bool {
 	return false
diff --git a/src/tui/light.go b/src/tui/light.go
index ad536f6d..4f50d687 100644
--- a/src/tui/light.go
+++ b/src/tui/light.go
@@ -463,20 +463,54 @@ func (r *LightRenderer) escSequence(sz *int) Event {
 					}
 					return Event{Invalid, 0, nil}
 				case ';':
-					if len(r.buffer) != 6 {
+					if len(r.buffer) < 6 {
 						return Event{Invalid, 0, nil}
 					}
 					*sz = 6
 					switch r.buffer[4] {
-					case '2', '5':
-						switch r.buffer[5] {
+					case '1', '2', '3', '5':
+						alt := r.buffer[4] == '3'
+						altShift := r.buffer[4] == '1' && r.buffer[5] == '0'
+						char := r.buffer[5]
+						if altShift {
+							if len(r.buffer) < 7 {
+								return Event{Invalid, 0, nil}
+							}
+							*sz = 7
+							char = r.buffer[6]
+						}
+						switch char {
 						case 'A':
+							if alt {
+								return Event{AltUp, 0, nil}
+							}
+							if altShift {
+								return Event{AltSUp, 0, nil}
+							}
 							return Event{SUp, 0, nil}
 						case 'B':
+							if alt {
+								return Event{AltDown, 0, nil}
+							}
+							if altShift {
+								return Event{AltSDown, 0, nil}
+							}
 							return Event{SDown, 0, nil}
 						case 'C':
+							if alt {
+								return Event{AltRight, 0, nil}
+							}
+							if altShift {
+								return Event{AltSRight, 0, nil}
+							}
 							return Event{SRight, 0, nil}
 						case 'D':
+							if alt {
+								return Event{AltLeft, 0, nil}
+							}
+							if altShift {
+								return Event{AltSLeft, 0, nil}
+							}
 							return Event{SLeft, 0, nil}
 						}
 					} // r.buffer[4]
diff --git a/src/tui/tcell.go b/src/tui/tcell.go
index 4f80d069..16d3151a 100644
--- a/src/tui/tcell.go
+++ b/src/tui/tcell.go
@@ -222,7 +222,10 @@ func (r *FullscreenRenderer) GetChar() Event {
 
 		// process keyboard:
 	case *tcell.EventKey:
-		alt := (ev.Modifiers() & tcell.ModAlt) > 0
+		mods := ev.Modifiers()
+		alt := (mods & tcell.ModAlt) > 0
+		shift := (mods & tcell.ModShift) > 0
+		altShift := alt && shift
 		keyfn := func(r rune) int {
 			if alt {
 				return CtrlAltA - 'a' + int(r)
@@ -297,21 +300,45 @@ func (r *FullscreenRenderer) GetChar() Event {
 			return Event{BSpace, 0, nil}
 
 		case tcell.KeyUp:
+			if altShift {
+				return Event{AltSUp, 0, nil}
+			}
+			if shift {
+				return Event{SUp, 0, nil}
+			}
 			if alt {
 				return Event{AltUp, 0, nil}
 			}
 			return Event{Up, 0, nil}
 		case tcell.KeyDown:
+			if altShift {
+				return Event{AltSDown, 0, nil}
+			}
+			if shift {
+				return Event{SDown, 0, nil}
+			}
 			if alt {
 				return Event{AltDown, 0, nil}
 			}
 			return Event{Down, 0, nil}
 		case tcell.KeyLeft:
+			if altShift {
+				return Event{AltSLeft, 0, nil}
+			}
+			if shift {
+				return Event{SLeft, 0, nil}
+			}
 			if alt {
 				return Event{AltLeft, 0, nil}
 			}
 			return Event{Left, 0, nil}
 		case tcell.KeyRight:
+			if altShift {
+				return Event{AltSRight, 0, nil}
+			}
+			if shift {
+				return Event{SRight, 0, nil}
+			}
 			if alt {
 				return Event{AltRight, 0, nil}
 			}
diff --git a/src/tui/tui.go b/src/tui/tui.go
index 0feea038..edbc9a14 100644
--- a/src/tui/tui.go
+++ b/src/tui/tui.go
@@ -98,6 +98,11 @@ const (
 	AltLeft
 	AltRight
 
+	AltSUp
+	AltSDown
+	AltSLeft
+	AltSRight
+
 	Alt0
 )
 
@@ -119,6 +124,10 @@ const (
 
 type Color int32
 
+func (c Color) IsDefault() bool {
+	return c == colDefault
+}
+
 func (c Color) is24() bool {
 	return c > 0 && (c&(1<<24)) > 0
 }
@@ -185,6 +194,11 @@ func (p ColorPair) Attr() Attr {
 	return p.attr
 }
 
+func (p ColorPair) HasBg() bool {
+	return p.attr&Reverse == 0 && p.bg != colDefault ||
+		p.attr&Reverse > 0 && p.fg != colDefault
+}
+
 func (p ColorPair) merge(other ColorPair, except Color) ColorPair {
 	dup := p
 	dup.attr = dup.attr.Merge(other.attr)


Test patch:
diff --git a/src/ansi_test.go b/src/ansi_test.go
index e10893c9..497eca44 100644
--- a/src/ansi_test.go
+++ b/src/ansi_test.go
@@ -168,7 +168,7 @@ func TestAnsiCodeStringConversion(t *testing.T) {
 		}
 	}
 	assert("\x1b[m", nil, "")
-	assert("\x1b[m", &ansiState{attr: tui.Blink}, "")
+	assert("\x1b[m", &ansiState{attr: tui.Blink, lbg: -1}, "")
 
 	assert("\x1b[31m", nil, "\x1b[31;49m")
 	assert("\x1b[41m", nil, "\x1b[39;41m")
@@ -176,8 +176,8 @@ func TestAnsiCodeStringConversion(t *testing.T) {
 	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[31m", &ansiState{fg: 4, bg: 4, lbg: -1}, "\x1b[31;44m")
+	assert("\x1b[1;2;31m", &ansiState{fg: 2, bg: -1, attr: tui.Reverse, lbg: -1}, "\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")
diff --git a/src/result_test.go b/src/result_test.go
index 7b21ba54..1f3d338c 100644
--- a/src/result_test.go
+++ b/src/result_test.go
@@ -109,10 +109,10 @@ func TestColorOffset(t *testing.T) {
 	item := Result{
 		item: &Item{
 			colors: &[]ansiOffset{
-				{[2]int32{0, 20}, ansiState{1, 5, 0}},
-				{[2]int32{22, 27}, ansiState{2, 6, tui.Bold}},
-				{[2]int32{30, 32}, ansiState{3, 7, 0}},
-				{[2]int32{33, 40}, ansiState{4, 8, tui.Bold}}}}}
+				{[2]int32{0, 20}, ansiState{1, 5, 0, -1}},
+				{[2]int32{22, 27}, ansiState{2, 6, tui.Bold, -1}},
+				{[2]int32{30, 32}, ansiState{3, 7, 0, -1}},
+				{[2]int32{33, 40}, ansiState{4, 8, tui.Bold, -1}}}}}
 
 	colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
 	colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
diff --git a/test/test_go.rb b/test/test_go.rb
index 3ff118c1..416692e2 100755
--- a/test/test_go.rb
+++ b/test/test_go.rb
@@ -1823,6 +1823,20 @@ class TestGoFZF < TestBase
     tmux.until { |lines| lines.item_count == 100 }
     tmux.until { |lines| lines[1]&.include?('[200]') }
   end
+
+  def test_change_prompt
+    tmux.send_keys "#{FZF} --bind 'a:change-prompt(a> ),b:change-prompt:b> ' --query foo", :Enter
+    tmux.until { |lines| assert_equal '> foo', lines[-1] }
+    tmux.send_keys 'a'
+    tmux.until { |lines| assert_equal 'a> foo', lines[-1] }
+    tmux.send_keys 'b'
+    tmux.until { |lines| assert_equal 'b> foo', lines[-1] }
+  end
+
+  def test_preview_window_follow
+    tmux.send_keys "#{FZF} --preview 'seq 1000 | nl' --preview-window down:noborder:follow", :Enter
+    tmux.until { |lines| assert_equal '1000  1000', lines[-1].strip }
+  end
 end
 
 module TestShell