Repository: junegunn/fzf
Pull request #4181 (2025-01-15 07:33:47)

chore: fix function name


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3e30749b..bd1dd62e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,7 +17,7 @@ Also, fzf now offers "style presets" for quick customization, which can be activ
 | `minimal` | <img src="https://raw.githubusercontent.com/junegunn/i/master/fzf-style-minimal.png"/> |
 
 - Style presets (#4160)
-    - `--style=full`
+    - `--style=full[:BORDER_STYLE]`
     - `--style=default`
     - `--style=minimal`
 - Border and label for the list section (#4148)
@@ -61,6 +61,16 @@ Also, fzf now offers "style presets" for quick customization, which can be activ
         - `transform-header-label`
 - Added `--preview-border[=STYLE]` as short for `--preview-window=border[-STYLE]`
 - Added new preview border style `line` which draws a single separator line between the preview window and the rest of the interface
+- fzf will now render a dashed line (`┈┈`) in each `--gap` for better visual separation.
+  ```sh
+  # All bash/zsh functions, highlighted
+  declare -f |
+    perl -0 -pe 's/^}\n/}\0/gm' |
+    bat --plain --language bash --color always |
+    fzf --read0 --ansi --layout reverse --multi --highlight-line \
+        --gap
+  ```
+    * You can customize the line using `--gap-line[=STR]`.
 - You can specify `border-native` to `--tmux` so that native tmux border is used instead of `--border`. This can be useful if you start a different program from inside the popup.
   ```sh
   fzf --tmux border-native --bind 'enter:execute:less {}'
@@ -72,6 +82,22 @@ Also, fzf now offers "style presets" for quick customization, which can be activ
   # Start with --nth 1, then 2, then 3, then back to the default, 1
   echo 'foo foobar foobarbaz' | fzf --bind 'space:change-nth(2|3|)' --nth 1 -q foo
   ```
+- `--nth` parts of each line can now be rendered in a different text style
+  ```sh
+  # nth in a different style
+  ls -al | fzf --nth -1 --color nth:italic
+  ls -al | fzf --nth -1 --color nth:reverse
+  ls -al | fzf --nth -1 --color nth:reverse:bold
+
+  # Dim the other parts
+  ls -al | fzf --nth -1 --color nth:regular,fg:dim,current-fg:dim
+
+  # With 'change-nth'. The current nth option is exported as $FZF_NTH.
+  ps -ef | fzf --reverse --header-lines 1 --header-border bottom --input-border \
+             --color nth:regular,fg:dim,current-fg:dim \
+             --bind 'ctrl-n:change-nth(8..|1|2|3|4|5|6|7|)' \
+             --bind 'result:transform-prompt:echo "${FZF_NTH}> "'
+  ```
 - A single-character delimiter is now treated as a plain string delimiter rather than a regular expression delimiter, even if it's a regular expression meta-character.
     - This means you can just write `--delimiter '|'` instead of escaping it as `--delimiter '\|'`
 - Bug fixes
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 85cc28f9..456006d6 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -189,7 +189,7 @@ e.g. \fB# Avoid rendering both fzf instances at the same time
 .SS GLOBAL STYLE
 .TP
 .BI "\-\-style=" "PRESET"
-Apply a style preset [default|minimal|full]
+Apply a style preset [default|minimal|full[:BORDER_STYLE]]
 .TP
 .BI "\-\-color=" "[BASE_SCHEME][,COLOR_NAME[:ANSI_COLOR][:ANSI_ATTRIBUTES]]..."
 Color configuration. The name of the base color scheme is followed by custom
@@ -242,6 +242,7 @@ color mappings.
     \fBmarker                \fRMulti\-select marker
     \fBspinner               \fRStreaming input indicator
     \fBheader (header\-fg)   \fRHeader
+    \fBnth                   \fRParts of the line specified by \fB\-\-nth\fR (only supports attributes)
 
 .B ANSI COLORS:
     \fB\-1         \fRDefault terminal foreground/background color
@@ -543,6 +544,10 @@ e.g.
 .BI "\-\-gap" "[=N]"
 Render empty lines between each item
 .TP
+.BI "\-\-gap\-line" "[=STR]"
+The given string will be repeated to draw a horizontal line on each gap
+(default: '┈' or '\-' depending on \fB\-\-no\-unicode\fR).
+.TP
 .B "\-\-keep\-right"
 Keep the right end of the line visible when it's too long. Effective only when
 the query string is empty.
diff --git a/src/options.go b/src/options.go
index 3932cb56..9042b6d7 100644
--- a/src/options.go
+++ b/src/options.go
@@ -56,7 +56,7 @@ Usage: fzf [options]
     --sync                   Synchronous search for multi-staged filtering
 
   GLOBAL STYLE
-    --style=PRESET           Apply a style preset [default|minimal|full]
+    --style=PRESET           Apply a style preset [default|minimal|full[:BORDER_STYLE]
     --color=COLSPEC          Base scheme (dark|light|16|bw) and/or custom colors
     --no-color               Disable colors
     --no-bold                Do not use bold text
@@ -97,6 +97,8 @@ Usage: fzf [options]
     --track                  Track the current selection when the result is updated
     --tac                    Reverse the order of the input
     --gap[=N]                Render empty lines between each item
+    --gap-line[=STR]         Draw horizontal line on each gap using the string
+                             (default: '┈' or '-')
     --keep-right             Keep the right end of the line visible on overflow
     --scroll-off=LINES       Number of screen lines to keep above or below when
                              scrolling to the top or to the bottom (default: 0)
@@ -127,7 +129,8 @@ Usage: fzf [options]
     --info=STYLE             Finder info style
                              [default|right|hidden|inline[-right][:PREFIX]]
     --info-command=COMMAND   Command to generate info line
-    --separator=STR          String to form horizontal separator on info line
+    --separator=STR          Draw horizontal separator on info line using the string
+                             (default: '─' or '-')
     --no-separator           Hide info line separator
     --filepath-word          Make word-wise movements respect path separators
     --input-border[=STYLE]   Draw border around the input section
@@ -211,6 +214,9 @@ Usage: fzf [options]
 
 `
 
+// Can be changed by --style
+var defaultBorderShape tui.BorderShape = tui.DefaultBorderShape
+
 const defaultInfoPrefix = " < "
 
 // Case denotes case-sensitivity of search
@@ -582,6 +588,7 @@ type Options struct {
 	HeaderLines       int
 	HeaderFirst       bool
 	Gap               int
+	GapLine           *string
 	Ellipsis          *string
 	Scrollbar         *string
 	Margin            [4]sizeSpec
@@ -624,7 +631,7 @@ func filterNonEmpty(input []string) []string {
 }
 
 func defaultPreviewOpts(command string) previewOpts {
-	return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, true, tui.DefaultBorderShape, 0, 0, nil}
+	return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, true, defaultBorderShape, 0, 0, nil}
 }
 
 func defaultOptions() *Options {
@@ -838,7 +845,7 @@ func parseBorder(str string, optional bool, allowLine bool) (tui.BorderShape, er
 		return tui.BorderNone, nil
 	}
 	if optional && str == "" {
-		return tui.DefaultBorderShape, nil
+		return defaultBorderShape, nil
 	}
 	return tui.BorderNone, errors.New("invalid border style (expected: rounded|sharp|bold|block|thinblock|double|horizontal|vertical|top|bottom|left|right|none)")
 }
@@ -1203,6 +1210,8 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro
 				mergeAttr(&theme.SelectedFg)
 			case "selected-bg":
 				mergeAttr(&theme.SelectedBg)
+			case "nth":
+				mergeAttr(&theme.Nth)
 			case "gutter":
 				mergeAttr(&theme.Gutter)
 			case "hl":
@@ -2567,6 +2576,15 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
 			}
 		case "--no-gap":
 			opts.Gap = 0
+		case "--gap-line":
+			if given, bar := optionalNextString(); given {
+				opts.GapLine = &bar
+			} else {
+				opts.GapLine = nil
+			}
+		case "--no-gap-line":
+			empty := ""
+			opts.GapLine = &empty
 		case "--ellipsis":
 			str, err := nextString("ellipsis string required")
 			if err != nil {
@@ -2714,7 +2732,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
 				return err
 			}
 		case "--style":
-			preset, err := nextString("preset name required: [default|minimal|full]")
+			preset, err := nextString("preset name required: [default|minimal|full[:BORDER_STYLE]]")
 			if err != nil {
 				return err
 			}
@@ -2867,12 +2885,15 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
 }
 
 func applyPreset(opts *Options, preset string) error {
+	// Reset to the platform default
+	defaultBorderShape = tui.DefaultBorderShape
+
 	switch strings.ToLower(preset) {
 	case "default":
 		opts.ListBorderShape = tui.BorderUndefined
 		opts.InputBorderShape = tui.BorderUndefined
 		opts.HeaderBorderShape = tui.BorderUndefined
-		opts.Preview.border = tui.DefaultBorderShape
+		opts.Preview.border = defaultBorderShape
 		opts.Preview.info = true
 		opts.InfoStyle = infoDefault
 		opts.Theme.Gutter = tui.NewColorAttr()
@@ -2891,19 +2912,29 @@ func applyPreset(opts *Options, preset string) error {
 		opts.Separator = &empty
 		opts.Scrollbar = &empty
 		opts.CursorLine = false
-	case "full":
-		opts.ListBorderShape = tui.DefaultBorderShape
-		opts.InputBorderShape = tui.DefaultBorderShape
-		opts.HeaderBorderShape = tui.DefaultBorderShape
-		opts.Preview.border = tui.DefaultBorderShape
+	default:
+		tokens := strings.SplitN(preset, ":", 2)
+		if tokens[0] != "full" {
+			return errors.New("unsupported style preset: " + preset)
+		}
+		if len(tokens) == 2 && len(tokens[1]) > 0 {
+			var err error
+			defaultBorderShape, err = parseBorder(tokens[1], false, false)
+			if err != nil {
+				return err
+			}
+		}
+
+		opts.ListBorderShape = defaultBorderShape
+		opts.InputBorderShape = defaultBorderShape
+		opts.HeaderBorderShape = defaultBorderShape
+		opts.Preview.border = defaultBorderShape
 		opts.Preview.info = true
 		opts.InfoStyle = infoInlineRight
 		opts.Theme.Gutter = tui.NewColorAttr()
 		opts.Separator = nil
 		opts.Scrollbar = nil
 		opts.CursorLine = true
-	default:
-		return errors.New("unsupported style preset: " + preset)
 	}
 	return nil
 }
@@ -2953,6 +2984,10 @@ func validateOptions(opts *Options) error {
 		}
 	}
 
+	if opts.Theme.Nth.IsColorDefined() {
+		return errors.New("only ANSI attributes are allowed for 'nth' (regular, bold, underline, reverse, dim, italic, strikethrough)")
+	}
+
 	return nil
 }
 
@@ -2987,6 +3022,14 @@ func postProcessOptions(opts *Options) error {
 		opts.Pointer = &defaultPointer
 	}
 
+	if opts.GapLine == nil {
+		defaultGapLine := "┈"
+		if !opts.Unicode {
+			defaultGapLine = "-"
+		}
+		opts.GapLine = &defaultGapLine
+	}
+
 	markerLen := 1
 	if opts.Marker == nil {
 		if opts.MarkerMulti != nil && opts.MarkerMulti[0] == "" {
diff --git a/src/result.go b/src/result.go
index f10db19b..ada31d59 100644
--- a/src/result.go
+++ b/src/result.go
@@ -104,11 +104,11 @@ func minRank() Result {
 	return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
 }
 
-func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, current bool) []colorOffset {
+func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, attrNth tui.Attr, current bool) []colorOffset {
 	itemColors := result.item.Colors()
 
 	// No ANSI codes
-	if len(itemColors) == 0 {
+	if len(itemColors) == 0 && len(nthOffsets) == 0 {
 		var offsets []colorOffset
 		for _, off := range matchOffsets {
 			offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch, match: true})
@@ -118,7 +118,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
 
 	// Find max column
 	var maxCol int32
-	for _, off := range matchOffsets {
+	for _, off := range append(matchOffsets, nthOffsets...) {
 		if off[1] > maxCol {
 			maxCol = off[1]
 		}
@@ -129,20 +129,33 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
 		}
 	}
 
-	cols := make([]int, maxCol)
+	type cellInfo struct {
+		index int
+		color bool
+		match bool
+		nth   bool
+	}
+
+	cols := make([]cellInfo, maxCol)
 	for colorIndex, ansi := range itemColors {
 		for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
-			cols[i] = colorIndex + 1 // 1-based index of itemColors
+			cols[i] = cellInfo{colorIndex, true, false, false}
 		}
 	}
 
 	for _, off := range matchOffsets {
 		for i := off[0]; i < off[1]; i++ {
-			// Negative of 1-based index of itemColors
-			// - The extra -1 means highlighted
-			if cols[i] >= 0 {
-				cols[i] = cols[i]*-1 - 1
-			}
+			cols[i].match = true
+		}
+	}
+
+	for _, off := range nthOffsets {
+		// Exclude the whole line
+		if int(off[1])-int(off[0]) == result.item.text.Length() {
+			continue
+		}
+		for i := off[0]; i < off[1]; i++ {
+			cols[i].nth = true
 		}
 	}
 
@@ -152,7 +165,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
 	// ------------  ----  --  ----
 	//   ++++++++      ++++++++++
 	// --++++++++--  --++++++++++---
-	curr := 0
+	var curr cellInfo = cellInfo{0, false, false, false}
 	start := 0
 	ansiToColorPair := func(ansi ansiOffset, base tui.ColorPair) tui.ColorPair {
 		fg := ansi.color.fg
@@ -175,12 +188,12 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
 	}
 	var colors []colorOffset
 	add := func(idx int) {
-		if curr != 0 && idx > start {
-			if curr < 0 {
+		if (curr.color || curr.nth || curr.match) && idx > start {
+			if curr.match {
 				color := colMatch
 				var url *url
-				if curr < -1 && theme.Colored {
-					ansi := itemColors[-curr-2]
+				if curr.color && theme.Colored {
+					ansi := itemColors[curr.index]
 					url = ansi.color.url
 					origColor := ansiToColorPair(ansi, colMatch)
 					// hl or hl+ only sets the foreground color, so colMatch is the
@@ -197,15 +210,28 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
 						color = origColor.MergeNonDefault(color)
 					}
 				}
+				if curr.nth {
+					color = color.WithAttr(attrNth)
+				}
 				colors = append(colors, colorOffset{
 					offset: [2]int32{int32(start), int32(idx)}, color: color, match: true, url: url})
-			} else {
-				ansi := itemColors[curr-1]
+			} else if curr.color {
+				ansi := itemColors[curr.index]
+				color := ansiToColorPair(ansi, colBase)
+				if curr.nth {
+					color = color.WithAttr(attrNth)
+				}
 				colors = append(colors, colorOffset{
 					offset: [2]int32{int32(start), int32(idx)},
-					color:  ansiToColorPair(ansi, colBase),
+					color:  color,
 					match:  false,
 					url:    ansi.color.url})
+			} else {
+				colors = append(colors, colorOffset{
+					offset: [2]int32{int32(start), int32(idx)},
+					color:  colBase.WithAttr(attrNth),
+					match:  false,
+					url:    nil})
 			}
 		}
 	}
diff --git a/src/terminal.go b/src/terminal.go
index d7ca89b6..2889bf01 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -267,6 +267,8 @@ type Terminal struct {
 	hscrollOff         int
 	scrollOff          int
 	gap                int
+	gapLine            labelPrinter
+	gapLineLen         int
 	wordRubout         string
 	wordNext           string
 	cx                 int
@@ -299,7 +301,9 @@ type Terminal struct {
 	scrollbar          string
 	previewScrollbar   string
 	ansi               bool
+	nthAttr            tui.Attr
 	nth                []Range
+	nthCurrent         []Range
 	tabstop            int
 	margin             [4]sizeSpec
 	padding            [4]sizeSpec
@@ -883,7 +887,9 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
 		header:             []string{},
 		header0:            opts.Header,
 		ansi:               opts.Ansi,
+		nthAttr:            opts.Theme.Nth.Attr,
 		nth:                opts.Nth,
+		nthCurrent:         opts.Nth,
 		tabstop:            opts.Tabstop,
 		hasStartActions:    false,
 		hasResultActions:   false,
@@ -949,6 +955,11 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
 		t.separator, t.separatorLen = t.ansiLabelPrinter(bar, &tui.ColSeparator, true)
 	}
 
+	// Gap line
+	if t.gap > 0 && len(*opts.GapLine) > 0 {
+		t.gapLine, t.gapLineLen = t.ansiLabelPrinter(*opts.GapLine, &tui.ColListBorder, true)
+	}
+
 	if opts.Ellipsis != nil {
 		t.ellipsis = *opts.Ellipsis
 	} else if t.unicode {
@@ -1035,6 +1046,9 @@ func (t *Terminal) environImpl(forPreview bool) []string {
 	env = append(env, "FZF_PREVIEW_LABEL="+t.previewLabelOpts.label)
 	env = append(env, "FZF_BORDER_LABEL="+t.borderLabelOpts.label)
 	env = append(env, "FZF_LIST_LABEL="+t.listLabelOpts.label)
+	if len(t.nthCurrent) > 0 {
+		env = append(env, "FZF_NTH="+RangesToString(t.nthCurrent))
+	}
 	env = append(env, fmt.Sprintf("FZF_TOTAL_COUNT=%d", t.count))
 	env = append(env, fmt.Sprintf("FZF_MATCH_COUNT=%d", t.merger.Length()))
 	env = append(env, fmt.Sprintf("FZF_SELECT_COUNT=%d", len(t.selected)))
@@ -1164,7 +1178,7 @@ func (t *Terminal) ansiLabelPrinter(str string, color *tui.ColorPair, fill bool)
 	printFn := func(window tui.Window, limit int) {
 		if offsets == nil {
 			// tui.Col* are not initialized until renderer.Init()
-			offsets = result.colorOffsets(nil, t.theme, *color, *color, false)
+			offsets = result.colorOffsets(nil, nil, t.theme, *color, *color, t.nthAttr, false)
 		}
 		for limit > 0 {
 			if length > limit {
@@ -2446,9 +2460,7 @@ func (t *Terminal) canSpanMultiLines() bool {
 	return t.multiLine || t.wrap || t.gap > 0
 }
 
-func (t *Terminal) renderEmptyLine(line int, barRange [2]int) {
-	t.move(line, 0, true)
-	t.markEmptyLine(line)
+func (t *Terminal) renderBar(line int, barRange [2]int) {
 	// If the screen is not filled with the list in non-multi-line mode,
 	// scrollbar is not visible at all. But in multi-line mode, we may need
 	// to redraw the scrollbar character at the end.
@@ -2457,6 +2469,29 @@ func (t *Terminal) renderEmptyLine(line int, barRange [2]int) {
 	}
 }
 
+func (t *Terminal) renderEmptyLine(line int, barRange [2]int) {
+	t.move(line, 0, true)
+	t.markEmptyLine(line)
+	t.renderBar(line, barRange)
+}
+
+func (t *Terminal) renderGapLine(line int, barRange [2]int, drawLine bool) {
+	t.move(line, 0, false)
+	t.window.CPrint(tui.ColCursorEmpty, t.pointerEmpty)
+	t.window.Print(t.markerEmpty)
+	x := t.pointerLen + t.markerLen
+
+	width := t.window.Width() - x - 1
+	if drawLine && t.gapLine != nil {
+		t.gapLine(t.window, width)
+	} else {
+		t.move(line, x, true)
+	}
+	t.markOtherLine(line)
+	t.renderBar(line, barRange)
+	t.prevLines[line].width = width
+}
+
 func (t *Terminal) printList() {
 	t.constrain()
 	barLength, barStart := t.getScrollbar()
@@ -2629,7 +2664,7 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
 	}
 	for i := 0; i < t.gap && finalLineNum < maxLine; i++ {
 		finalLineNum++
-		t.renderEmptyLine(finalLineNum, barRange)
+		t.renderGapLine(finalLineNum, barRange, i == t.gap-1)
 	}
 	return finalLineNum
 }
@@ -2689,7 +2724,41 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
 		}
 		sort.Sort(ByOrder(charOffsets))
 	}
-	allOffsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current)
+
+	wholeCovered := len(t.nthCurrent) == 0
+	for _, nth := range t.nthCurrent {
+		// Do we still want to apply a different style when the current nth
+		// covers the whole string? Probably not. And we can simplify the logic.
+		if nth.IsFull() {
+			wholeCovered = true
+			break
+		}
+	}
+	// But if 'nth' is set to 'regular', it's a sign that you're applying
+	// a different style to the rest of the string. e.g. 'nth:regular,fg:dim'
+	// In this case, we still need to apply and clear the style.
+	// We do the same when postTask is nil, which means we're printing header lines.
+	if t.nthAttr == tui.AttrRegular && wholeCovered || postTask == nil {
+		if t.nthAttr == tui.AttrRegular {
+			colBase = colBase.WithAttr(t.nthAttr)
+		}
+	}
+	var nthOffsets []Offset
+	if !wholeCovered && t.nthAttr > 0 && postTask != nil {
+		var tokens []Token
+		if item.transformed != nil {
+			tokens = item.transformed.tokens
+		} else {
+			tokens = Transform(Tokenize(item.text.ToString(), t.delimiter), t.nthCurrent)
+		}
+		for _, token := range tokens {
+			start := token.prefixLength
+			end := start + int32(token.text.Length())
+			nthOffsets = append(nthOffsets, Offset{int32(start), int32(end)})
+		}
+		sort.Sort(ByOrder(nthOffsets))
+	}
+	allOffsets := result.colorOffsets(charOffsets, nthOffsets, t.theme, colBase, colMatch, t.nthAttr, current)
 
 	maxLines := 1
 	if t.canSpanMultiLines() {
@@ -4639,6 +4708,7 @@ func (t *Terminal) Loop() error {
 					// The default
 					newNth = &t.nth
 				}
+				t.nthCurrent = *newNth
 				// Cycle
 				if len(tokens) > 1 {
 					a.a = strings.Join(append(tokens[1:], tokens[0]), "|")
diff --git a/src/tmux.go b/src/tmux.go
index e459000a..87ea38cc 100644
--- a/src/tmux.go
+++ b/src/tmux.go
@@ -12,7 +12,9 @@ func runTmux(args []string, opts *Options) (int, error) {
 	fzf, rest := args[0], args[1:]
 	args = []string{"--bind=ctrl-z:ignore"}
 	if !opts.Tmux.border && opts.BorderShape == tui.BorderUndefined {
-		args = append(args, "--border")
+		// We append --border option at the end, because `--style=full:STYLE`
+		// may have changed the default border style.
+		rest = append(rest, "--border")
 	}
 	if opts.Tmux.border && opts.Margin == defaultMargin() {
 		args = append(args, "--margin=0,1")
diff --git a/src/tokenizer.go b/src/tokenizer.go
index 6b2ef72b..e5a8e977 100644
--- a/src/tokenizer.go
+++ b/src/tokenizer.go
@@ -18,6 +18,36 @@ type Range struct {
 	end   int
 }
 
+func (r Range) IsFull() bool {
+	return r.begin == rangeEllipsis && r.end == rangeEllipsis
+}
+
+func RangesToString(ranges []Range) string {
+	strs := []string{}
+	for _, r := range ranges {
+		s := ""
+		if r.begin == rangeEllipsis && r.end == rangeEllipsis {
+			s = ".."
+		} else if r.begin == r.end {
+			s = strconv.Itoa(r.begin)
+		} else {
+			if r.begin != rangeEllipsis {
+				s += strconv.Itoa(r.begin)
+			}
+
+			if r.begin != -1 {
+				s += ".."
+				if r.end != rangeEllipsis {
+					s += strconv.Itoa(r.end)
+				}
+			}
+		}
+		strs = append(strs, s)
+	}
+
+	return strings.Join(strs, ",")
+}
+
 // Token contains the tokenized part of the strings and its prefix length
 type Token struct {
 	text         *util.Chars
@@ -41,7 +71,7 @@ func (d Delimiter) String() string {
 }
 
 func newRange(begin int, end int) Range {
-	if begin == 1 {
+	if begin == 1 && end != 1 {
 		begin = rangeEllipsis
 	}
 	if end == -1 {
@@ -73,7 +103,7 @@ func ParseRange(str *string) (Range, bool) {
 		}
 		begin, err1 := strconv.Atoi(ns[0])
 		end, err2 := strconv.Atoi(ns[1])
-		if err1 != nil || err2 != nil || begin == 0 || end == 0 {
+		if err1 != nil || err2 != nil || begin == 0 || end == 0 || begin < 0 && end > 0 {
 			return Range{}, false
 		}
 		return newRange(begin, end), true
diff --git a/src/tui/dummy.go b/src/tui/dummy.go
index a49677c6..aaa9a7ea 100644
--- a/src/tui/dummy.go
+++ b/src/tui/dummy.go
@@ -11,6 +11,10 @@ func HasFullscreenRenderer() bool {
 var DefaultBorderShape = BorderRounded
 
 func (a Attr) Merge(b Attr) Attr {
+	if b == AttrRegular {
+		return b
+	}
+
 	return a | b
 }
 
diff --git a/src/tui/ttyname_windows.go b/src/tui/ttyname_windows.go
index 0313c608..dfe89eb3 100644
--- a/src/tui/ttyname_windows.go
+++ b/src/tui/ttyname_windows.go
@@ -15,7 +15,7 @@ func TtyIn() (*os.File, error) {
 	return os.Stdin, nil
 }
 
-// TtyIn on Windows returns nil
+// TtyOut on Windows returns nil
 func TtyOut() (*os.File, error) {
 	return nil, nil
 }
diff --git a/src/tui/tui.go b/src/tui/tui.go
index 0ab4874b..921c7a04 100644
--- a/src/tui/tui.go
+++ b/src/tui/tui.go
@@ -205,6 +205,10 @@ type ColorAttr struct {
 	Attr  Attr
 }
 
+func (a ColorAttr) IsColorDefined() bool {
+	return a.Color != colUndefined
+}
+
 func NewColorAttr() ColorAttr {
 	return ColorAttr{Color: colUndefined, Attr: AttrUndefined}
 }
@@ -305,6 +309,7 @@ type ColorTheme struct {
 	Bg               ColorAttr
 	ListFg           ColorAttr
 	ListBg           ColorAttr
+	Nth              ColorAttr
 	SelectedFg       ColorAttr
 	SelectedBg       ColorAttr
 	SelectedMatch    ColorAttr
@@ -703,6 +708,7 @@ func EmptyTheme() *ColorTheme {
 		HeaderBg:         ColorAttr{colUndefined, AttrUndefined},
 		HeaderBorder:     ColorAttr{colUndefined, AttrUndefined},
 		HeaderLabel:      ColorAttr{colUndefined, AttrUndefined},
+		Nth:              ColorAttr{colUndefined, AttrUndefined},
 	}
 }
 
@@ -746,6 +752,7 @@ func NoColorTheme() *ColorTheme {
 		HeaderBg:         ColorAttr{colDefault, AttrUndefined},
 		HeaderBorder:     ColorAttr{colDefault, AttrUndefined},
 		HeaderLabel:      ColorAttr{colDefault, AttrUndefined},
+		Nth:              ColorAttr{colUndefined, AttrUndefined},
 	}
 }
 
@@ -786,6 +793,7 @@ func init() {
 		InputBg:          ColorAttr{colUndefined, AttrUndefined},
 		InputBorder:      ColorAttr{colUndefined, AttrUndefined},
 		InputLabel:       ColorAttr{colUndefined, AttrUndefined},
+		Nth:              ColorAttr{colUndefined, AttrUndefined},
 	}
 	Dark256 = &ColorTheme{
 		Colored:          true,
@@ -823,6 +831,7 @@ func init() {
 		InputBg:          ColorAttr{colUndefined, AttrUndefined},
 		InputBorder:      ColorAttr{colUndefined, AttrUndefined},
 		InputLabel:       ColorAttr{colUndefined, AttrUndefined},
+		Nth:              ColorAttr{colUndefined, AttrUndefined},
 	}
 	Light256 = &ColorTheme{
 		Colored:          true,
@@ -863,6 +872,7 @@ func init() {
 		HeaderBg:         ColorAttr{colUndefined, AttrUndefined},
 		HeaderBorder:     ColorAttr{colUndefined, AttrUndefined},
 		HeaderLabel:      ColorAttr{colUndefined, AttrUndefined},
+		Nth:              ColorAttr{colUndefined, AttrUndefined},
 	}
 }
 


Test patch:
diff --git a/src/result_test.go b/src/result_test.go
index c11e1ab5..520ee4b1 100644
--- a/src/result_test.go
+++ b/src/result_test.go
@@ -131,7 +131,7 @@ func TestColorOffset(t *testing.T) {
 
 	colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
 	colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
-	colors := item.colorOffsets(offsets, tui.Dark256, colBase, colMatch, true)
+	colors := item.colorOffsets(offsets, nil, tui.Dark256, colBase, colMatch, tui.AttrUndefined, true)
 	assert := func(idx int, b int32, e int32, c tui.ColorPair) {
 		o := colors[idx]
 		if o.offset[0] != b || o.offset[1] != e || o.color != c {
@@ -155,20 +155,30 @@ func TestColorOffset(t *testing.T) {
 
 	colRegular := tui.NewColorPair(-1, -1, tui.AttrUndefined)
 	colUnderline := tui.NewColorPair(-1, -1, tui.Underline)
-	colors = item.colorOffsets(offsets, tui.Dark256, colRegular, colUnderline, true)
 
-	// [{[0 5] {1 5 0}} {[5 15] {1 5 8}} {[15 20] {1 5 0}}
-	//  {[22 25] {2 6 1}} {[25 27] {2 6 9}} {[27 30] {-1 -1 8}}
-	//  {[30 32] {3 7 8}} {[32 33] {-1 -1 8}} {[33 35] {4 8 9}}
-	//  {[35 40] {4 8 1}}]
-	assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined))
-	assert(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline))
-	assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined))
-	assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold))
-	assert(4, 25, 27, tui.NewColorPair(2, 6, tui.Bold|tui.Underline))
-	assert(5, 27, 30, colUnderline)
-	assert(6, 30, 32, tui.NewColorPair(3, 7, tui.Underline))
-	assert(7, 32, 33, colUnderline)
-	assert(8, 33, 35, tui.NewColorPair(4, 8, tui.Bold|tui.Underline))
-	assert(9, 35, 40, tui.NewColorPair(4, 8, tui.Bold))
+	nthOffsets := []Offset{{37, 39}, {42, 45}}
+	for _, attr := range []tui.Attr{tui.AttrRegular, tui.StrikeThrough} {
+		colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, attr, true)
+
+		// [{[0 5] {1 5 0}} {[5 15] {1 5 8}} {[15 20] {1 5 0}}
+		//  {[22 25] {2 6 1}} {[25 27] {2 6 9}} {[27 30] {-1 -1 8}}
+		//  {[30 32] {3 7 8}} {[32 33] {-1 -1 8}} {[33 35] {4 8 9}}
+		//  {[35 37] {4 8 1}} {[37 39] {4 8 x|1}} {[39 40] {4 8 x|1}}]
+		assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined))
+		assert(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline))
+		assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined))
+		assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold))
+		assert(4, 25, 27, tui.NewColorPair(2, 6, tui.Bold|tui.Underline))
+		assert(5, 27, 30, colUnderline)
+		assert(6, 30, 32, tui.NewColorPair(3, 7, tui.Underline))
+		assert(7, 32, 33, colUnderline)
+		assert(8, 33, 35, tui.NewColorPair(4, 8, tui.Bold|tui.Underline))
+		assert(9, 35, 37, tui.NewColorPair(4, 8, tui.Bold))
+		expected := tui.Bold | attr
+		if attr == tui.AttrRegular {
+			expected = tui.AttrRegular
+		}
+		assert(10, 37, 39, tui.NewColorPair(4, 8, expected))
+		assert(11, 39, 40, tui.NewColorPair(4, 8, tui.Bold))
+	}
 }
diff --git a/src/tokenizer_test.go b/src/tokenizer_test.go
index 3119f797..39f32dc8 100644
--- a/src/tokenizer_test.go
+++ b/src/tokenizer_test.go
@@ -40,6 +40,18 @@ func TestParseRange(t *testing.T) {
 			t.Errorf("%v", r)
 		}
 	}
+	{
+		i := "1..3..5"
+		if r, ok := ParseRange(&i); ok {
+			t.Errorf("%v", r)
+		}
+	}
+	{
+		i := "-3..3"
+		if r, ok := ParseRange(&i); ok {
+			t.Errorf("%v", r)
+		}
+	}
 }
 
 func TestTokenize(t *testing.T) {
diff --git a/test/test_go.rb b/test/test_go.rb
index 6a7f2ce4..e782b979 100755
--- a/test/test_go.rb
+++ b/test/test_go.rb
@@ -3423,28 +3423,28 @@ class TestGoFZF < TestBase
       │ >
       │   100/100 ──────
       │ > 1
-      │
+      │   ┈┈┈┈┈┈┈┈┈┈┈┈┈┈
       │   2
-      │
+      │   ┈┈┈┈┈┈┈┈┈┈┈┈┈┈
       │   3
-      │
+      │   ┈┈┈┈┈┈┈┈┈┈┈┈┈┈
       │   4
     BLOCK
     tmux.until { assert_block(block, _1) }
   end
 
   def test_gap_2
-    tmux.send_keys %(seq 100 | #{FZF} --gap=2 --border --reverse), :Enter
+    tmux.send_keys %(seq 100 | #{FZF} --gap=2 --gap-line xyz --border --reverse), :Enter
     block = <<~BLOCK
       ╭─────────────────
       │ >
       │   100/100 ──────
       │ > 1
       │
-      │
+      │   xyzxyzxyzxyzxy
       │   2
       │
-      │
+      │   xyzxyzxyzxyzxy
       │   3
     BLOCK
     tmux.until { assert_block(block, _1) }
@@ -3719,6 +3719,24 @@ class TestGoFZF < TestBase
     tmux.until { assert_block(block, _1) }
   end
 
+  def test_style_full_adaptive_height_double
+    tmux.send_keys %(seq 1| #{FZF} --style=full:double --border --height=~100% --header-lines=1 --info=default), :Enter
+    block = <<~BLOCK
+      ╔══════════
+      ║ ╔════════
+      ║ ╚════════
+      ║ ╔════════
+      ║ ║   1
+      ║ ╚════════
+      ║ ╔════════
+      ║ ║   0/0
+      ║ ║ >
+      ║ ╚════════
+      ╚══════════
+    BLOCK
+    tmux.until { assert_block(block, _1) }
+  end
+
   def test_change_nth
     input = [
       *[''] * 1000,
@@ -3729,19 +3747,38 @@ class TestGoFZF < TestBase
       *[''] * 1000
     ]
     writelines(input)
-    tmux.send_keys %(#{FZF} -qfoo -n1 --bind 'space:change-nth:2|3|4|5|' < #{tempname}), :Enter
+    nths = '1,2..4,-1,-3..,..2'
+    tmux.send_keys %(#{FZF} -qfoo -n#{nths} --bind 'space:change-nth(2|3|4|5|),result:transform-prompt:echo "[$FZF_NTH] "' < #{tempname}), :Enter
 
-    tmux.until { |lines| assert_equal 4, lines.match_count }
+    tmux.until do |lines|
+      assert lines.any_include?("[#{nths}] foo")
+      assert_equal 4, lines.match_count
+    end
     tmux.send_keys :Space
-    tmux.until { |lines| assert_equal 3, lines.match_count }
+    tmux.until do |lines|
+      assert lines.any_include?('[2] foo')
+      assert_equal 3, lines.match_count
+    end
     tmux.send_keys :Space
-    tmux.until { |lines| assert_equal 2, lines.match_count }
+    tmux.until do |lines|
+      assert lines.any_include?('[3] foo')
+      assert_equal 2, lines.match_count
+    end
     tmux.send_keys :Space
-    tmux.until { |lines| assert_equal 1, lines.match_count }
+    tmux.until do |lines|
+      assert lines.any_include?('[4] foo')
+      assert_equal 1, lines.match_count
+    end
     tmux.send_keys :Space
-    tmux.until { |lines| assert_equal 0, lines.match_count }
+    tmux.until do |lines|
+      assert lines.any_include?('[5] foo')
+      assert_equal 0, lines.match_count
+    end
     tmux.send_keys :Space
-    tmux.until { |lines| assert_equal 4, lines.match_count }
+    tmux.until do |lines|
+      assert lines.any_include?("[#{nths}] foo")
+      assert_equal 4, lines.match_count
+    end
   end
 end