m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-09 11:53:47 -05:00

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 // nextAnsiEscapeSequence returns the ANSI escape sequence and is equivalent to
// calling FindStringIndex() on the below regex (which was originally used): // 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) { func nextAnsiEscapeSequence(s string) (int, int) {
// fast check for ANSI escape sequences // fast check for ANSI escape sequences
i := 0 i := 0
for ; i < len(s); i++ { for ; i < len(s); i++ {
switch 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 // We ignore the fact that '\x08' cannot be the first char
// in the string and be an escape sequence for the sake of // in the string and be an escape sequence for the sake of
// speed and simplicity. // speed and simplicity.
@@ -174,6 +174,9 @@ func nextAnsiEscapeSequence(s string) (int, int) {
Loop: Loop:
for ; i < len(s); i++ { for ; i < len(s); i++ {
switch s[i] { switch s[i] {
case '\n':
// match: `\n`
return i, i + 1
case '\x08': case '\x08':
// backtrack to match: `.\x08` // backtrack to match: `.\x08`
if i > 0 && s[i-1] != '\n' { 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) output.WriteString(prev)
} }
newState := interpretCode(str[start:idx], state) code := str[start:idx]
if !newState.equals(state) { newState := interpretCode(code, state)
if code == "\n" || !newState.equals(state) {
if state != nil { if state != nil {
// Update last offset // Update last offset
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount) (&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() { if newState.colored() {
// Append new offset // Append new offset
if pstate == nil { if pstate == nil {
@@ -349,6 +366,13 @@ func parseAnsiCode(s string) (int, string) {
} }
func interpretCode(ansiCode string, prevState *ansiState) ansiState { 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 var state ansiState
if prevState == nil { if prevState == nil {
state = ansiState{-1, -1, 0, -1, nil} state = ansiState{-1, -1, 0, -1, nil}
@@ -435,6 +459,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
state.fg = -1 state.fg = -1
state.bg = -1 state.bg = -1
state.attr = 0 state.attr = 0
state.lbg = -1
state256 = 0 state256 = 0
default: default:
if num >= 30 && num <= 37 { if num >= 30 && num <= 37 {
@@ -477,6 +502,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
state.fg = -1 state.fg = -1
state.bg = -1 state.bg = -1
state.attr = 0 state.attr = 0
state.lbg = -1
} }
if state256 > 0 { if state256 > 0 {

View File

@@ -22,7 +22,7 @@ import (
// (archived from http://ascii-table.com/ansi-escape-sequences-vt-100.php) // (archived from http://ascii-table.com/ansi-escape-sequences-vt-100.php)
// - http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html // - http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
// - https://invisible-island.net/xterm/ctlseqs/ctlseqs.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) { func testParserReference(t testing.TB, str string) {
t.Helper() t.Helper()

View File

@@ -6,6 +6,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
@@ -78,6 +79,16 @@ func Run(opts *Options) (int, error) {
prevLineAnsiState = lineAnsiState prevLineAnsiState = lineAnsiState
trimmed, offsets, newState := extractColor(byteString(data), lineAnsiState, nil) trimmed, offsets, newState := extractColor(byteString(data), lineAnsiState, nil)
lineAnsiState = newState 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 return util.ToChars(stringBytes(trimmed)), offsets
} }
} }

View File

@@ -19,6 +19,10 @@ type colorOffset struct {
url *url 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 { type Result struct {
item *Item item *Item
points [4]uint16 points [4]uint16
@@ -149,12 +153,20 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
color bool color bool
match bool match bool
nth 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 colorIndex, ansi := range itemColors {
for i := ansi.offset[0]; i < ansi.offset[1]; i++ { if ansi.offset[0] == ansi.offset[1] && ansi.color.attr&tui.FullBg > 0 {
cols[i] = cellInfo{colorIndex, true, false, false} 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 start := 0
ansiToColorPair := func(ansi ansiOffset, base tui.ColorPair) tui.ColorPair { ansiToColorPair := func(ansi ansiOffset, base tui.ColorPair) tui.ColorPair {
if !theme.Colored { if !theme.Colored {
@@ -194,6 +206,13 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
} }
var colors []colorOffset var colors []colorOffset
add := func(idx int) { 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.color || curr.nth || curr.match) && idx > start {
if curr.match { if curr.match {
var color tui.ColorPair 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()) 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 width += extraWidth
if (current || selected || alt) && t.highlightLine { if (current || selected || alt) && t.highlightLine || lbg.IsFullBgMarker() {
color := tui.ColSelected color := tui.ColSelected
if current { if lbg.IsFullBgMarker() {
color = lbg
} else if current {
color = tui.ColCurrent color = tui.ColCurrent
} else if alt { } else if alt {
color = color.WithBg(altBg) color = color.WithBg(altBg)
@@ -3311,7 +3313,7 @@ func (t *Terminal) overflow(runes []rune, max int) bool {
return t.displayWidthWithLimit(runes, 0, max) > max 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 var displayWidth int
item := result.item item := result.item
matchOffsets := []Offset{} matchOffsets := []Offset{}
@@ -3396,9 +3398,19 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
line := lines[lineOffset] line := lines[lineOffset]
finalLineNum = lineNum finalLineNum = lineNum
offsets := []colorOffset{} offsets := []colorOffset{}
for _, offset := range allOffsets { lbg := tui.NoColorPair()
if offset.offset[0] >= int32(from+len(line)) { var lineLen int
allOffsets = allOffsets[len(offsets):] 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 break
} }
@@ -3406,23 +3418,30 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
continue continue
} }
if offset.offset[1] < int32(from+len(line)) { if offset.offset[1] < lineEnd {
offset.offset[0] -= int32(from) offset.offset[0] -= int32(from)
offset.offset[1] -= int32(from) offset.offset[1] -= int32(from)
offsets = append(offsets, offset) offsets = append(offsets, offset)
} else { } else {
if idx < len(allOffsets)-1 {
next := allOffsets[idx+1]
if next.IsFullBgMarker(lineEnd) {
lbg = next.color
idx++
}
}
dupe := offset dupe := offset
dupe.offset[0] = int32(from + len(line)) dupe.offset[0] = lineEnd
offset.offset[0] -= int32(from) offset.offset[0] -= int32(from)
offset.offset[1] = int32(from + len(line)) offset.offset[1] = lineEnd
offsets = append(offsets, offset) offsets = append(offsets, offset)
allOffsets = append([]colorOffset{dupe}, allOffsets[len(offsets):]...) allOffsets = append([]colorOffset{dupe}, allOffsets[idx+1:]...)
break break
} }
} }
from += len(line) from += lineLen
if lineOffset < skipLines { if lineOffset < skipLines {
continue continue
} }
@@ -3553,7 +3572,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
t.printColoredString(t.window, line, offsets, colBase) t.printColoredString(t.window, line, offsets, colBase)
} }
if postTask != nil { if postTask != nil {
postTask(actualLineNum, displayWidth, wasWrapped, forceRedraw) postTask(actualLineNum, displayWidth, wasWrapped, forceRedraw, lbg)
} else { } else {
t.markOtherLine(actualLineNum) t.markOtherLine(actualLineNum)
} }

View File

@@ -24,6 +24,7 @@ const (
AttrRegular = Attr(1 << 8) AttrRegular = Attr(1 << 8)
AttrClear = Attr(1 << 9) AttrClear = Attr(1 << 9)
BoldForce = Attr(1 << 10) BoldForce = Attr(1 << 10)
FullBg = Attr(1 << 11)
Bold = Attr(1) Bold = Attr(1)
Dim = Attr(1 << 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} return ColorPair{fg, bg, attr}
} }
func NoColorPair() ColorPair {
return ColorPair{-1, -1, 0}
}
func (p ColorPair) Fg() Color { func (p ColorPair) Fg() Color {
return p.fg return p.fg
} }
@@ -285,6 +289,10 @@ func (p ColorPair) Attr() Attr {
return p.attr return p.attr
} }
func (p ColorPair) IsFullBgMarker() bool {
return p.attr&FullBg > 0
}
func (p ColorPair) HasBg() bool { func (p ColorPair) HasBg() bool {
return p.attr&Reverse == 0 && p.bg != colDefault || return p.attr&Reverse == 0 && p.bg != colDefault ||
p.attr&Reverse > 0 && p.fg != colDefault p.attr&Reverse > 0 && p.fg != colDefault