Fix nth attr merge order to respect precedence hierarchy (#4697)

* Fix nth attr merge order to respect precedence hierarchy

nth attrs were merged ON TOP of current-fg/selected-fg attrs, so
nth:regular would clear attrs like underline from current-fg. Fix the
merge chain to apply nth BEFORE the line-type overlay:

  fg < nth < selected-fg < current-fg < hl < selected-hl < current-hl

Store raw current-fg and selected-fg attrs in ColorTheme before they
get merged with fg/ListFg, then pass them as nthOverlay through
printHighlighted to colorOffsets where the correct merge chain is
computed: fgAttr.Merge(nthAttr).Merge(nthOverlay).

Fix #4687

* Make current-fg inherit from list-fg instead of fg

current-fg was inheriting from fg, not list-fg, so setting list-fg
had no effect on the current line. e.g.

  fzf --color fg:dim,list-fg:underline,nth:regular -d / --nth -1

The non-nth part of the current line was dim (from fg) instead of
underline (from list-fg). Resolve ListFg/ListBg earlier in InitTheme
so Current can inherit from them.

* Make selected-fg inherit from list-fg via merge instead of override

selected-fg used o() which replaces the attr from list-fg entirely.
e.g. with fg:dim,selected-fg:italic, the dim was lost on selected
lines because o() replaced dim with italic instead of merging them.
Use ColorAttr.Merge() so attrs are combined additively, consistent
with how current-fg inherits from list-fg.

* Apply selected-fg attrs on current line when item is selected

When an item is both current and selected, the current-line rendering
ignored selected-fg attrs entirely. e.g.

  echo foo | fzf --color fg:dim,nth:regular,current-fg:underline,selected-fg:italic --bind result:select --multi

The italic from selected-fg was lost. Merge NthSelectedAttr into the
overlay and rebuild colBase attr with the correct precedence chain:
fg < selected-fg < current-fg.
This commit is contained in:
Junegunn Choi
2026-03-02 12:00:21 +09:00
committed by GitHub
parent 97ac7794cf
commit b4e585779a
4 changed files with 82 additions and 22 deletions

View File

@@ -1554,7 +1554,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, 0, false)
}
for limit > 0 {
if length > limit {
@@ -1617,7 +1617,7 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) {
return 1
}
t.printHighlighted(
Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false, false, line, line, true, preTask, nil)
Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false, false, line, line, true, preTask, nil, 0)
})
t.wrap = wrap
}
@@ -3185,7 +3185,7 @@ func (t *Terminal) printFooter() {
func(markerClass) int {
t.footerWindow.Print(indent)
return indentSize
}, nil)
}, nil, 0)
}
})
t.wrap = wrap
@@ -3269,7 +3269,7 @@ func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShap
func(markerClass) int {
t.window.Print(indent)
return indentSize
}, nil)
}, nil, 0)
}
t.wrap = wrap
}
@@ -3507,7 +3507,14 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
}
return indentSize
}
finalLineNum = t.printHighlighted(result, tui.ColCurrent, tui.ColCurrentMatch, true, true, !matched, line, maxLine, forceRedraw, preTask, postTask)
colCurrent := tui.ColCurrent
nthOverlay := t.theme.NthCurrentAttr
if selected {
nthOverlay = t.theme.NthSelectedAttr.Merge(t.theme.NthCurrentAttr)
baseAttr := tui.ColNormal.Attr().Merge(t.theme.NthSelectedAttr).Merge(t.theme.NthCurrentAttr)
colCurrent = colCurrent.WithNewAttr(baseAttr)
}
finalLineNum = t.printHighlighted(result, colCurrent, tui.ColCurrentMatch, true, true, !matched, line, maxLine, forceRedraw, preTask, postTask, nthOverlay)
} else {
preTask := func(marker markerClass) int {
w := t.window.Width() - t.pointerLen
@@ -3541,7 +3548,11 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
base = base.WithBg(altBg)
match = match.WithBg(altBg)
}
finalLineNum = t.printHighlighted(result, base, match, false, true, !matched, line, maxLine, forceRedraw, preTask, postTask)
var nthOverlay tui.Attr
if selected {
nthOverlay = t.theme.NthSelectedAttr
}
finalLineNum = t.printHighlighted(result, base, match, false, true, !matched, line, maxLine, forceRedraw, preTask, postTask, nthOverlay)
}
for i := 0; i < t.gap && finalLineNum < maxLine; i++ {
finalLineNum++
@@ -3642,7 +3653,7 @@ func (t *Terminal) overflow(runes []rune, max int) bool {
return t.displayWidthWithLimit(runes, 0, max) > max
}
func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMatch tui.ColorPair, current bool, match bool, hidden bool, lineNum int, maxLineNum int, forceRedraw bool, preTask func(markerClass) int, postTask func(int, int, bool, bool, tui.ColorPair)) int {
func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMatch tui.ColorPair, current bool, match bool, hidden bool, lineNum int, maxLineNum int, forceRedraw bool, preTask func(markerClass) int, postTask func(int, int, bool, bool, tui.ColorPair), nthOverlay tui.Attr) int {
var displayWidth int
item := result.item
matchOffsets := []Offset{}
@@ -3683,7 +3694,9 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
// 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 it to clear the style.
colBase = colBase.WithAttr(t.nthAttr)
fgAttr := tui.ColNormal.Attr()
nthAttrFinal := fgAttr.Merge(t.nthAttr).Merge(nthOverlay)
colBase = colBase.WithNewAttr(nthAttrFinal)
}
if !wholeCovered && t.nthAttr > 0 {
var tokens []Token
@@ -3702,7 +3715,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, hidden)
allOffsets := result.colorOffsets(charOffsets, nthOffsets, t.theme, colBase, colMatch, t.nthAttr, nthOverlay, hidden)
// Determine split offset for horizontal scrolling with freeze
splitOffset1 := -1