Repository: junegunn/fzf (go)

Issue:
Alternate list color
It would be nice to have the ability to alternate the list-bg/list-fg color.

Rofi has this feature:

![Image](https://github.com/user-attachments/assets/19609a20-e819-4502-8b2c-90042beac6aa)

For multiline items or items that wrap, this would make it much easier to see where each item begins and ends.

Pull request:
Add 'alt-bg' color for striped lines
## Test cases

### 'jump' should show alternating background colors even when 'alt-bg' is not defined as before.

  ```sh
  go run main.go --bind load:jump
  ```

Two differences:
  * The alternating lines will not be in bold (was a bug)
  * The marker column will not be rendered with alternating background color

### Use alternating background color when 'alt-bg' is set

  ```sh
  go run main.go --color bg:238,alt-bg:237
  go run main.go --color bg:238,alt-bg:237 --highlight-line
  ```

### 'selected-bg' should take precedence

  ```sh
  go run main.go --color bg:238,alt-bg:237,selected-bg:232 \
                 --highlight-line --multi --bind 'load:select+up+select+up'
  ```

---

Close #4354


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b426e74..464faa9f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,23 @@
 CHANGELOG
 =========
 
+0.62.0
+------
+- Added `alt-bg` color to create striped lines to visually separate rows
+  ```sh
+  fzf --color bg:237,alt-bg:238,current-bg:236 --highlight-line
+
+  declare -f | perl -0777 -pe 's/^}\n/}\0/gm' |
+    bat --plain --language bash --color always |
+    fzf --read0 --ansi --reverse --multi \
+        --color bg:237,alt-bg:238,current-bg:236 --highlight-line
+  ```
+- [fish] Improvements in CTRL-R binding (@bitraid)
+    - You can trigger CTRL-R in the middle of a command to insert the selected item
+    - You can delete history items with SHIFT-DEL
+- Bug fixes and improvements
+    - Fixed unnecessary 100ms delay after `reload`
+
 0.61.3
 ------
 - Reverted #4351 as it caused `tmux run-shell 'fzf --tmux'` to fail (#4559 #4560)
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index e84e1eef..2159cf36 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -270,6 +270,7 @@ color mappings.
     \fBcurrent\-bg (bg+)      \fRBackground (current line)
       \fBgutter              \fRGutter on the left
     \fBcurrent\-hl (hl+)      \fRHighlighted substrings (current line)
+    \fBalt\-bg                \fRAlternate background color to create striped lines
     \fBquery (input\-fg)      \fRQuery string
       \fBdisabled            \fRQuery string when search is disabled (\fB\-\-disabled\fR)
     \fBinfo                  \fRInfo line (match counters)
diff --git a/src/options.go b/src/options.go
index e94e82f9..1e4c8904 100644
--- a/src/options.go
+++ b/src/options.go
@@ -1295,6 +1295,8 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro
 				mergeAttr(&theme.Current)
 			case "current-bg", "bg+":
 				mergeAttr(&theme.DarkBg)
+			case "alt-bg":
+				mergeAttr(&theme.AltBg)
 			case "selected-fg":
 				mergeAttr(&theme.SelectedFg)
 			case "selected-bg":
diff --git a/src/result.go b/src/result.go
index 28d42e7d..3e4a81c0 100644
--- a/src/result.go
+++ b/src/result.go
@@ -119,7 +119,7 @@ func minRank() Result {
 	return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
 }
 
-func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, attrNth tui.Attr, current bool) []colorOffset {
+func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, attrNth tui.Attr) []colorOffset {
 	itemColors := result.item.Colors()
 
 	// No ANSI codes
@@ -182,18 +182,10 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
 		fg := ansi.color.fg
 		bg := ansi.color.bg
 		if fg == -1 {
-			if current {
-				fg = theme.Current.Color
-			} else {
-				fg = theme.Fg.Color
-			}
+			fg = colBase.Fg()
 		}
 		if bg == -1 {
-			if current {
-				bg = theme.DarkBg.Color
-			} else {
-				bg = theme.Bg.Color
-			}
+			bg = colBase.Bg()
 		}
 		return tui.NewColorPair(fg, bg, ansi.color.attr).MergeAttr(base)
 	}
diff --git a/src/terminal.go b/src/terminal.go
index a847c3b0..a79d166f 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -1258,7 +1258,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, nil, t.theme, *color, *color, t.nthAttr, false)
+			offsets = result.colorOffsets(nil, nil, t.theme, *color, *color, t.nthAttr)
 		}
 		for limit > 0 {
 			if length > limit {
@@ -2791,17 +2791,28 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
 	_, selected := t.selected[item.Index()]
 	label := ""
 	extraWidth := 0
+	alt := false
+	altBg := t.theme.AltBg
+	selectedBg := selected && t.theme.SelectedBg != t.theme.ListBg
 	if t.jumping != jumpDisabled {
 		if index < len(t.jumpLabels) {
 			// Striped
-			current = index%2 == 0
+			if !altBg.IsColorDefined() {
+				altBg = t.theme.DarkBg
+				alt = index%2 == 0
+			} else {
+				alt = index%2 == 1
+			}
 			label = t.jumpLabels[index:index+1] + strings.Repeat(" ", util.Max(0, t.pointerLen-1))
 			if t.pointerLen == 0 {
 				extraWidth = 1
 			}
 		}
-	} else if current {
-		label = t.pointer
+	} else {
+		if current {
+			label = t.pointer
+		}
+		alt = !selectedBg && altBg.IsColorDefined() && index%2 == 1
 	}
 
 	// Avoid unnecessary redraw
@@ -2828,10 +2839,12 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
 	maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
 	postTask := func(lineNum int, width int, wrapped bool, forceRedraw bool) {
 		width += extraWidth
-		if (current || selected) && t.highlightLine {
+		if (current || selected || alt) && t.highlightLine {
 			color := tui.ColSelected
 			if current {
 				color = tui.ColCurrent
+			} else if alt {
+				color = color.WithBg(altBg)
 			}
 			fillSpaces := maxWidth - width
 			if wrapped {
@@ -2929,6 +2942,10 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
 			base = tui.ColNormal
 			match = tui.ColMatch
 		}
+		if alt {
+			base = base.WithBg(altBg)
+			match = match.WithBg(altBg)
+		}
 		finalLineNum = t.printHighlighted(result, base, match, false, true, line, maxLine, forceRedraw, preTask, postTask)
 	}
 	for i := 0; i < t.gap && finalLineNum < maxLine; i++ {
@@ -3027,7 +3044,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
 			sort.Sort(ByOrder(nthOffsets))
 		}
 	}
-	allOffsets := result.colorOffsets(charOffsets, nthOffsets, t.theme, colBase, colMatch, t.nthAttr, current)
+	allOffsets := result.colorOffsets(charOffsets, nthOffsets, t.theme, colBase, colMatch, t.nthAttr)
 
 	maxLines := 1
 	if t.canSpanMultiLines() {
diff --git a/src/tui/tui.go b/src/tui/tui.go
index e181deaa..4a8ffbed 100644
--- a/src/tui/tui.go
+++ b/src/tui/tui.go
@@ -308,6 +308,12 @@ func (p ColorPair) WithAttr(attr Attr) ColorPair {
 	return dup
 }
 
+func (p ColorPair) WithBg(bg ColorAttr) ColorPair {
+	dup := p
+	bgPair := ColorPair{colUndefined, bg.Color, bg.Attr}
+	return dup.Merge(bgPair)
+}
+
 func (p ColorPair) MergeAttr(other ColorPair) ColorPair {
 	return p.WithAttr(other.attr)
 }
@@ -328,6 +334,7 @@ type ColorTheme struct {
 	Bg               ColorAttr
 	ListFg           ColorAttr
 	ListBg           ColorAttr
+	AltBg            ColorAttr
 	Nth              ColorAttr
 	SelectedFg       ColorAttr
 	SelectedBg       ColorAttr
@@ -735,6 +742,7 @@ func EmptyTheme() *ColorTheme {
 		Bg:               ColorAttr{colUndefined, AttrUndefined},
 		ListFg:           ColorAttr{colUndefined, AttrUndefined},
 		ListBg:           ColorAttr{colUndefined, AttrUndefined},
+		AltBg:            ColorAttr{colUndefined, AttrUndefined},
 		SelectedFg:       ColorAttr{colUndefined, AttrUndefined},
 		SelectedBg:       ColorAttr{colUndefined, AttrUndefined},
 		SelectedMatch:    ColorAttr{colUndefined, AttrUndefined},
@@ -780,6 +788,7 @@ func NoColorTheme() *ColorTheme {
 		Bg:               ColorAttr{colDefault, AttrUndefined},
 		ListFg:           ColorAttr{colDefault, AttrUndefined},
 		ListBg:           ColorAttr{colDefault, AttrUndefined},
+		AltBg:            ColorAttr{colUndefined, AttrUndefined},
 		SelectedFg:       ColorAttr{colDefault, AttrUndefined},
 		SelectedBg:       ColorAttr{colDefault, AttrUndefined},
 		SelectedMatch:    ColorAttr{colDefault, AttrUndefined},
@@ -825,6 +834,7 @@ func init() {
 		Bg:               ColorAttr{colDefault, AttrUndefined},
 		ListFg:           ColorAttr{colUndefined, AttrUndefined},
 		ListBg:           ColorAttr{colUndefined, AttrUndefined},
+		AltBg:            ColorAttr{colUndefined, AttrUndefined},
 		SelectedFg:       ColorAttr{colUndefined, AttrUndefined},
 		SelectedBg:       ColorAttr{colUndefined, AttrUndefined},
 		SelectedMatch:    ColorAttr{colUndefined, AttrUndefined},
@@ -864,6 +874,7 @@ func init() {
 		Bg:               ColorAttr{colDefault, AttrUndefined},
 		ListFg:           ColorAttr{colUndefined, AttrUndefined},
 		ListBg:           ColorAttr{colUndefined, AttrUndefined},
+		AltBg:            ColorAttr{colUndefined, AttrUndefined},
 		SelectedFg:       ColorAttr{colUndefined, AttrUndefined},
 		SelectedBg:       ColorAttr{colUndefined, AttrUndefined},
 		SelectedMatch:    ColorAttr{colUndefined, AttrUndefined},
@@ -903,6 +914,7 @@ func init() {
 		Bg:               ColorAttr{colDefault, AttrUndefined},
 		ListFg:           ColorAttr{colUndefined, AttrUndefined},
 		ListBg:           ColorAttr{colUndefined, AttrUndefined},
+		AltBg:            ColorAttr{colUndefined, AttrUndefined},
 		SelectedFg:       ColorAttr{colUndefined, AttrUndefined},
 		SelectedBg:       ColorAttr{colUndefined, AttrUndefined},
 		SelectedMatch:    ColorAttr{colUndefined, AttrUndefined},


Test patch:
diff --git a/src/result_test.go b/src/result_test.go
index 520ee4b1..79cc1ee2 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, nil, tui.Dark256, colBase, colMatch, tui.AttrUndefined, true)
+	colors := item.colorOffsets(offsets, nil, tui.Dark256, colBase, colMatch, tui.AttrUndefined)
 	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 {
@@ -158,7 +158,7 @@ func TestColorOffset(t *testing.T) {
 
 	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)
+		colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, attr)
 
 		// [{[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}}