Allow displaying --nth parts in a different text style

Close #4183
This commit is contained in:
Junegunn Choi 2025-01-16 01:38:45 +09:00
parent b42f5bfb19
commit 3e7f032ec2
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
8 changed files with 128 additions and 36 deletions

View File

@ -82,6 +82,21 @@ 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 # 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 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'
ps -ef | fzf --reverse --header-lines 1 --header-border bottom --input-border \
--color nth:regular,fg:dim,current-fg:dim \
--nth 8.. --bind 'ctrl-n:change-nth(..|1|2|3|4|5|6|7|)'
```
- 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. - 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 '\|'` - This means you can just write `--delimiter '|'` instead of escaping it as `--delimiter '\|'`
- Bug fixes - Bug fixes

View File

@ -242,6 +242,7 @@ color mappings.
\fBmarker \fRMulti\-select marker \fBmarker \fRMulti\-select marker
\fBspinner \fRStreaming input indicator \fBspinner \fRStreaming input indicator
\fBheader (header\-fg) \fRHeader \fBheader (header\-fg) \fRHeader
\fBnth \fRParts of the line specified by \fB\-\-nth\fR (only supports attributes)
.B ANSI COLORS: .B ANSI COLORS:
\fB\-1 \fRDefault terminal foreground/background color \fB\-1 \fRDefault terminal foreground/background color

View File

@ -1207,6 +1207,8 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro
mergeAttr(&theme.SelectedFg) mergeAttr(&theme.SelectedFg)
case "selected-bg": case "selected-bg":
mergeAttr(&theme.SelectedBg) mergeAttr(&theme.SelectedBg)
case "nth":
mergeAttr(&theme.Nth)
case "gutter": case "gutter":
mergeAttr(&theme.Gutter) mergeAttr(&theme.Gutter)
case "hl": case "hl":
@ -2966,6 +2968,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 return nil
} }

View File

@ -104,11 +104,11 @@ func minRank() Result {
return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}} 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() itemColors := result.item.Colors()
// No ANSI codes // No ANSI codes
if len(itemColors) == 0 { if len(itemColors) == 0 && len(nthOffsets) == 0 {
var offsets []colorOffset var offsets []colorOffset
for _, off := range matchOffsets { for _, off := range matchOffsets {
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch, match: true}) 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 // Find max column
var maxCol int32 var maxCol int32
for _, off := range matchOffsets { for _, off := range append(matchOffsets, nthOffsets...) {
if off[1] > maxCol { if off[1] > maxCol {
maxCol = off[1] 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 colorIndex, ansi := range itemColors {
for i := ansi.offset[0]; i < ansi.offset[1]; i++ { 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 _, off := range matchOffsets {
for i := off[0]; i < off[1]; i++ { for i := off[0]; i < off[1]; i++ {
// Negative of 1-based index of itemColors cols[i].match = true
// - The extra -1 means highlighted }
if cols[i] >= 0 { }
cols[i] = cols[i]*-1 - 1
} 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 start := 0
ansiToColorPair := func(ansi ansiOffset, base tui.ColorPair) tui.ColorPair { ansiToColorPair := func(ansi ansiOffset, base tui.ColorPair) tui.ColorPair {
fg := ansi.color.fg fg := ansi.color.fg
@ -175,12 +188,12 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
} }
var colors []colorOffset var colors []colorOffset
add := func(idx int) { add := func(idx int) {
if curr != 0 && idx > start { if (curr.color || curr.nth || curr.match) && idx > start {
if curr < 0 { if curr.match {
color := colMatch color := colMatch
var url *url var url *url
if curr < -1 && theme.Colored { if curr.color && theme.Colored {
ansi := itemColors[-curr-2] ansi := itemColors[curr.index]
url = ansi.color.url url = ansi.color.url
origColor := ansiToColorPair(ansi, colMatch) origColor := ansiToColorPair(ansi, colMatch)
// hl or hl+ only sets the foreground color, so colMatch is the // 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) color = origColor.MergeNonDefault(color)
} }
} }
if curr.nth {
color = color.WithAttr(attrNth)
}
colors = append(colors, colorOffset{ colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, color: color, match: true, url: url}) offset: [2]int32{int32(start), int32(idx)}, color: color, match: true, url: url})
} else { } else if curr.color {
ansi := itemColors[curr-1] ansi := itemColors[curr.index]
color := ansiToColorPair(ansi, colBase)
if curr.nth {
color = color.WithAttr(attrNth)
}
colors = append(colors, colorOffset{ colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, offset: [2]int32{int32(start), int32(idx)},
color: ansiToColorPair(ansi, colBase), color: color,
match: false, match: false,
url: ansi.color.url}) url: ansi.color.url})
} else {
colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)},
color: colBase.WithAttr(attrNth),
match: false,
url: nil})
} }
} }
} }

View File

@ -131,7 +131,7 @@ func TestColorOffset(t *testing.T) {
colBase := tui.NewColorPair(89, 189, tui.AttrUndefined) colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
colMatch := tui.NewColorPair(99, 199, 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) { assert := func(idx int, b int32, e int32, c tui.ColorPair) {
o := colors[idx] o := colors[idx]
if o.offset[0] != b || o.offset[1] != e || o.color != c { 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) colRegular := tui.NewColorPair(-1, -1, tui.AttrUndefined)
colUnderline := tui.NewColorPair(-1, -1, tui.Underline) 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}} nthOffsets := []Offset{{37, 39}, {42, 45}}
// {[22 25] {2 6 1}} {[25 27] {2 6 9}} {[27 30] {-1 -1 8}} for _, attr := range []tui.Attr{tui.AttrRegular, tui.StrikeThrough} {
// {[30 32] {3 7 8}} {[32 33] {-1 -1 8}} {[33 35] {4 8 9}} colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, attr, true)
// {[35 40] {4 8 1}}]
assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined)) // [{[0 5] {1 5 0}} {[5 15] {1 5 8}} {[15 20] {1 5 0}}
assert(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline)) // {[22 25] {2 6 1}} {[25 27] {2 6 9}} {[27 30] {-1 -1 8}}
assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined)) // {[30 32] {3 7 8}} {[32 33] {-1 -1 8}} {[33 35] {4 8 9}}
assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold)) // {[35 37] {4 8 1}} {[37 39] {4 8 x|1}} {[39 40] {4 8 x|1}}]
assert(4, 25, 27, tui.NewColorPair(2, 6, tui.Bold|tui.Underline)) assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined))
assert(5, 27, 30, colUnderline) assert(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline))
assert(6, 30, 32, tui.NewColorPair(3, 7, tui.Underline)) assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined))
assert(7, 32, 33, colUnderline) assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold))
assert(8, 33, 35, tui.NewColorPair(4, 8, tui.Bold|tui.Underline)) assert(4, 25, 27, tui.NewColorPair(2, 6, tui.Bold|tui.Underline))
assert(9, 35, 40, tui.NewColorPair(4, 8, tui.Bold)) 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))
}
} }

View File

@ -301,7 +301,9 @@ type Terminal struct {
scrollbar string scrollbar string
previewScrollbar string previewScrollbar string
ansi bool ansi bool
nthAttr tui.Attr
nth []Range nth []Range
nthCurrent []Range
tabstop int tabstop int
margin [4]sizeSpec margin [4]sizeSpec
padding [4]sizeSpec padding [4]sizeSpec
@ -885,7 +887,9 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
header: []string{}, header: []string{},
header0: opts.Header, header0: opts.Header,
ansi: opts.Ansi, ansi: opts.Ansi,
nthAttr: opts.Theme.Nth.Attr,
nth: opts.Nth, nth: opts.Nth,
nthCurrent: opts.Nth,
tabstop: opts.Tabstop, tabstop: opts.Tabstop,
hasStartActions: false, hasStartActions: false,
hasResultActions: false, hasResultActions: false,
@ -1171,7 +1175,7 @@ func (t *Terminal) ansiLabelPrinter(str string, color *tui.ColorPair, fill bool)
printFn := func(window tui.Window, limit int) { printFn := func(window tui.Window, limit int) {
if offsets == nil { if offsets == nil {
// tui.Col* are not initialized until renderer.Init() // 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 { for limit > 0 {
if length > limit { if length > limit {
@ -2717,7 +2721,22 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
} }
sort.Sort(ByOrder(charOffsets)) sort.Sort(ByOrder(charOffsets))
} }
allOffsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current) var nthOffsets []Offset
if len(t.nth) > 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 maxLines := 1
if t.canSpanMultiLines() { if t.canSpanMultiLines() {
@ -4667,6 +4686,7 @@ func (t *Terminal) Loop() error {
// The default // The default
newNth = &t.nth newNth = &t.nth
} }
t.nthCurrent = *newNth
// Cycle // Cycle
if len(tokens) > 1 { if len(tokens) > 1 {
a.a = strings.Join(append(tokens[1:], tokens[0]), "|") a.a = strings.Join(append(tokens[1:], tokens[0]), "|")

View File

@ -11,6 +11,10 @@ func HasFullscreenRenderer() bool {
var DefaultBorderShape = BorderRounded var DefaultBorderShape = BorderRounded
func (a Attr) Merge(b Attr) Attr { func (a Attr) Merge(b Attr) Attr {
if b == AttrRegular {
return b
}
return a | b return a | b
} }

View File

@ -205,6 +205,10 @@ type ColorAttr struct {
Attr Attr Attr Attr
} }
func (a ColorAttr) IsColorDefined() bool {
return a.Color != colUndefined
}
func NewColorAttr() ColorAttr { func NewColorAttr() ColorAttr {
return ColorAttr{Color: colUndefined, Attr: AttrUndefined} return ColorAttr{Color: colUndefined, Attr: AttrUndefined}
} }
@ -305,6 +309,7 @@ type ColorTheme struct {
Bg ColorAttr Bg ColorAttr
ListFg ColorAttr ListFg ColorAttr
ListBg ColorAttr ListBg ColorAttr
Nth ColorAttr
SelectedFg ColorAttr SelectedFg ColorAttr
SelectedBg ColorAttr SelectedBg ColorAttr
SelectedMatch ColorAttr SelectedMatch ColorAttr
@ -703,6 +708,7 @@ func EmptyTheme() *ColorTheme {
HeaderBg: ColorAttr{colUndefined, AttrUndefined}, HeaderBg: ColorAttr{colUndefined, AttrUndefined},
HeaderBorder: ColorAttr{colUndefined, AttrUndefined}, HeaderBorder: ColorAttr{colUndefined, AttrUndefined},
HeaderLabel: ColorAttr{colUndefined, AttrUndefined}, HeaderLabel: ColorAttr{colUndefined, AttrUndefined},
Nth: ColorAttr{colUndefined, AttrUndefined},
} }
} }
@ -746,6 +752,7 @@ func NoColorTheme() *ColorTheme {
HeaderBg: ColorAttr{colDefault, AttrUndefined}, HeaderBg: ColorAttr{colDefault, AttrUndefined},
HeaderBorder: ColorAttr{colDefault, AttrUndefined}, HeaderBorder: ColorAttr{colDefault, AttrUndefined},
HeaderLabel: ColorAttr{colDefault, AttrUndefined}, HeaderLabel: ColorAttr{colDefault, AttrUndefined},
Nth: ColorAttr{colUndefined, AttrUndefined},
} }
} }
@ -786,6 +793,7 @@ func init() {
InputBg: ColorAttr{colUndefined, AttrUndefined}, InputBg: ColorAttr{colUndefined, AttrUndefined},
InputBorder: ColorAttr{colUndefined, AttrUndefined}, InputBorder: ColorAttr{colUndefined, AttrUndefined},
InputLabel: ColorAttr{colUndefined, AttrUndefined}, InputLabel: ColorAttr{colUndefined, AttrUndefined},
Nth: ColorAttr{colUndefined, AttrUndefined},
} }
Dark256 = &ColorTheme{ Dark256 = &ColorTheme{
Colored: true, Colored: true,
@ -823,6 +831,7 @@ func init() {
InputBg: ColorAttr{colUndefined, AttrUndefined}, InputBg: ColorAttr{colUndefined, AttrUndefined},
InputBorder: ColorAttr{colUndefined, AttrUndefined}, InputBorder: ColorAttr{colUndefined, AttrUndefined},
InputLabel: ColorAttr{colUndefined, AttrUndefined}, InputLabel: ColorAttr{colUndefined, AttrUndefined},
Nth: ColorAttr{colUndefined, AttrUndefined},
} }
Light256 = &ColorTheme{ Light256 = &ColorTheme{
Colored: true, Colored: true,
@ -863,6 +872,7 @@ func init() {
HeaderBg: ColorAttr{colUndefined, AttrUndefined}, HeaderBg: ColorAttr{colUndefined, AttrUndefined},
HeaderBorder: ColorAttr{colUndefined, AttrUndefined}, HeaderBorder: ColorAttr{colUndefined, AttrUndefined},
HeaderLabel: ColorAttr{colUndefined, AttrUndefined}, HeaderLabel: ColorAttr{colUndefined, AttrUndefined},
Nth: ColorAttr{colUndefined, AttrUndefined},
} }
} }