Support full-line background color in the list section

Close #4432
This commit is contained in:
Junegunn Choi
2025-06-24 22:50:02 +09:00
parent 8d81730ec2
commit 4811e52af3
7 changed files with 106 additions and 22 deletions

View File

@@ -156,13 +156,13 @@ func isCtrlSeqStart(c uint8) bool {
// nextAnsiEscapeSequence returns the ANSI escape sequence and is equivalent to
// calling FindStringIndex() on the below regex (which was originally used):
//
// "(?:\x1b[\\[()][0-9;:?]*[a-zA-Z@]|\x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)"
// "(?:\x1b[\\[()][0-9;:?]*[a-zA-Z@]|\x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08|\n)"
func nextAnsiEscapeSequence(s string) (int, int) {
// fast check for ANSI escape sequences
i := 0
for ; i < len(s); i++ {
switch s[i] {
case '\x0e', '\x0f', '\x1b', '\x08':
case '\x0e', '\x0f', '\x1b', '\x08', '\n':
// We ignore the fact that '\x08' cannot be the first char
// in the string and be an escape sequence for the sake of
// speed and simplicity.
@@ -174,6 +174,9 @@ func nextAnsiEscapeSequence(s string) (int, int) {
Loop:
for ; i < len(s); i++ {
switch s[i] {
case '\n':
// match: `\n`
return i, i + 1
case '\x08':
// backtrack to match: `.\x08`
if i > 0 && s[i-1] != '\n' {
@@ -265,13 +268,27 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
output.WriteString(prev)
}
newState := interpretCode(str[start:idx], state)
if !newState.equals(state) {
code := str[start:idx]
newState := interpretCode(code, state)
if code == "\n" || !newState.equals(state) {
if state != nil {
// Update last offset
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
}
if code == "\n" {
output.WriteRune('\n')
// Full-background marker
if newState.lbg >= 0 {
marker := newState
marker.attr |= tui.FullBg
offsets = append(offsets, ansiOffset{
[2]int32{int32(runeCount), int32(runeCount)},
marker,
})
}
}
if newState.colored() {
// Append new offset
if pstate == nil {
@@ -349,6 +366,13 @@ func parseAnsiCode(s string) (int, string) {
}
func interpretCode(ansiCode string, prevState *ansiState) ansiState {
if ansiCode == "\n" {
if prevState != nil {
return *prevState
}
return ansiState{-1, -1, 0, -1, nil}
}
var state ansiState
if prevState == nil {
state = ansiState{-1, -1, 0, -1, nil}
@@ -435,6 +459,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
state.fg = -1
state.bg = -1
state.attr = 0
state.lbg = -1
state256 = 0
default:
if num >= 30 && num <= 37 {
@@ -477,6 +502,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
state.fg = -1
state.bg = -1
state.attr = 0
state.lbg = -1
}
if state256 > 0 {

View File

@@ -22,7 +22,7 @@ import (
// (archived from http://ascii-table.com/ansi-escape-sequences-vt-100.php)
// - http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
// - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
var ansiRegexReference = regexp.MustCompile("(?:\x1b[\\[()][0-9;:]*[a-zA-Z@]|\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)")
var ansiRegexReference = regexp.MustCompile("(?:\x1b[\\[()][0-9;:]*[a-zA-Z@]|\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08|\n)")
func testParserReference(t testing.TB, str string) {
t.Helper()

View File

@@ -6,6 +6,7 @@ import (
"sync"
"time"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
)
@@ -78,6 +79,16 @@ func Run(opts *Options) (int, error) {
prevLineAnsiState = lineAnsiState
trimmed, offsets, newState := extractColor(byteString(data), lineAnsiState, nil)
lineAnsiState = newState
// Full line background is found. Add a special marker.
if !opts.ReadZero && offsets != nil && newState != nil && len(*offsets) > 0 && newState.lbg >= 0 {
marker := (*offsets)[len(*offsets)-1]
marker.offset[0] = marker.offset[1]
marker.color.bg = newState.lbg
marker.color.attr = marker.color.attr | tui.FullBg
newOffsets := append(*offsets, marker)
offsets = &newOffsets
}
return util.ToChars(stringBytes(trimmed)), offsets
}
}

View File

@@ -19,6 +19,10 @@ type colorOffset struct {
url *url
}
func (co colorOffset) IsFullBgMarker(at int32) bool {
return at == co.offset[0] && at == co.offset[1] && co.color.Attr()&tui.FullBg > 0
}
type Result struct {
item *Item
points [4]uint16
@@ -149,12 +153,20 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
color bool
match bool
nth bool
fbg tui.Color
}
cols := make([]cellInfo, maxCol)
cols := make([]cellInfo, maxCol+1)
for idx := range cols {
cols[idx].fbg = -1
}
for colorIndex, ansi := range itemColors {
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
cols[i] = cellInfo{colorIndex, true, false, false}
if ansi.offset[0] == ansi.offset[1] && ansi.color.attr&tui.FullBg > 0 {
cols[ansi.offset[0]].fbg = ansi.color.lbg
} else {
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
cols[i] = cellInfo{colorIndex, true, false, false, cols[i].fbg}
}
}
}
@@ -176,7 +188,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
// ------------ ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
var curr cellInfo = cellInfo{0, false, false, false}
curr := cellInfo{0, false, false, false, -1}
start := 0
ansiToColorPair := func(ansi ansiOffset, base tui.ColorPair) tui.ColorPair {
if !theme.Colored {
@@ -194,6 +206,13 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
}
var colors []colorOffset
add := func(idx int) {
if curr.fbg >= 0 {
colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(start)},
color: tui.NewColorPair(-1, curr.fbg, tui.FullBg),
match: false,
url: nil})
}
if (curr.color || curr.nth || curr.match) && idx > start {
if curr.match {
var color tui.ColorPair

View File

@@ -3155,11 +3155,13 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
}
maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + t.barCol())
postTask := func(lineNum int, width int, wrapped bool, forceRedraw bool) {
postTask := func(lineNum int, width int, wrapped bool, forceRedraw bool, lbg tui.ColorPair) {
width += extraWidth
if (current || selected || alt) && t.highlightLine {
if (current || selected || alt) && t.highlightLine || lbg.IsFullBgMarker() {
color := tui.ColSelected
if current {
if lbg.IsFullBgMarker() {
color = lbg
} else if current {
color = tui.ColCurrent
} else if alt {
color = color.WithBg(altBg)
@@ -3311,7 +3313,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, lineNum int, maxLineNum int, forceRedraw bool, preTask func(markerClass) int, postTask func(int, int, bool, bool)) int {
func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMatch tui.ColorPair, current bool, match bool, lineNum int, maxLineNum int, forceRedraw bool, preTask func(markerClass) int, postTask func(int, int, bool, bool, tui.ColorPair)) int {
var displayWidth int
item := result.item
matchOffsets := []Offset{}
@@ -3396,9 +3398,19 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
line := lines[lineOffset]
finalLineNum = lineNum
offsets := []colorOffset{}
for _, offset := range allOffsets {
if offset.offset[0] >= int32(from+len(line)) {
allOffsets = allOffsets[len(offsets):]
lbg := tui.NoColorPair()
var lineLen int
for idx, offset := range allOffsets {
lineLen = len(line)
if lineLen > 0 && line[lineLen-1] == '\n' {
lineLen--
}
lineEnd := int32(from + lineLen)
if offset.offset[0] >= lineEnd {
if offset.IsFullBgMarker(lineEnd) {
lbg = offset.color
}
allOffsets = allOffsets[idx:]
break
}
@@ -3406,23 +3418,30 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
continue
}
if offset.offset[1] < int32(from+len(line)) {
if offset.offset[1] < lineEnd {
offset.offset[0] -= int32(from)
offset.offset[1] -= int32(from)
offsets = append(offsets, offset)
} else {
if idx < len(allOffsets)-1 {
next := allOffsets[idx+1]
if next.IsFullBgMarker(lineEnd) {
lbg = next.color
idx++
}
}
dupe := offset
dupe.offset[0] = int32(from + len(line))
dupe.offset[0] = lineEnd
offset.offset[0] -= int32(from)
offset.offset[1] = int32(from + len(line))
offset.offset[1] = lineEnd
offsets = append(offsets, offset)
allOffsets = append([]colorOffset{dupe}, allOffsets[len(offsets):]...)
allOffsets = append([]colorOffset{dupe}, allOffsets[idx+1:]...)
break
}
}
from += len(line)
from += lineLen
if lineOffset < skipLines {
continue
}
@@ -3553,7 +3572,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
t.printColoredString(t.window, line, offsets, colBase)
}
if postTask != nil {
postTask(actualLineNum, displayWidth, wasWrapped, forceRedraw)
postTask(actualLineNum, displayWidth, wasWrapped, forceRedraw, lbg)
} else {
t.markOtherLine(actualLineNum)
}

View File

@@ -24,6 +24,7 @@ const (
AttrRegular = Attr(1 << 8)
AttrClear = Attr(1 << 9)
BoldForce = Attr(1 << 10)
FullBg = Attr(1 << 11)
Bold = Attr(1)
Dim = Attr(1 << 1)

View File

@@ -273,6 +273,10 @@ func NewColorPair(fg Color, bg Color, attr Attr) ColorPair {
return ColorPair{fg, bg, attr}
}
func NoColorPair() ColorPair {
return ColorPair{-1, -1, 0}
}
func (p ColorPair) Fg() Color {
return p.fg
}
@@ -285,6 +289,10 @@ func (p ColorPair) Attr() Attr {
return p.attr
}
func (p ColorPair) IsFullBgMarker() bool {
return p.attr&FullBg > 0
}
func (p ColorPair) HasBg() bool {
return p.attr&Reverse == 0 && p.bg != colDefault ||
p.attr&Reverse > 0 && p.fg != colDefault