From 3e7f032ec200f00f92fa53f9aecde4b989c7dd66 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 16 Jan 2025 01:38:45 +0900 Subject: [PATCH] Allow displaying --nth parts in a different text style Close #4183 --- CHANGELOG.md | 15 +++++++++++ man/man1/fzf.1 | 1 + src/options.go | 6 +++++ src/result.go | 62 ++++++++++++++++++++++++++++++++-------------- src/result_test.go | 42 +++++++++++++++++++------------ src/terminal.go | 24 ++++++++++++++++-- src/tui/dummy.go | 4 +++ src/tui/tui.go | 10 ++++++++ 8 files changed, 128 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75e873b2..5c772aa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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. - This means you can just write `--delimiter '|'` instead of escaping it as `--delimiter '\|'` - Bug fixes diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index d848d0e9..b237c110 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -242,6 +242,7 @@ color mappings. \fBmarker \fRMulti\-select marker \fBspinner \fRStreaming input indicator \fBheader (header\-fg) \fRHeader + \fBnth \fRParts of the line specified by \fB\-\-nth\fR (only supports attributes) .B ANSI COLORS: \fB\-1 \fRDefault terminal foreground/background color diff --git a/src/options.go b/src/options.go index 87ec9e6f..3dc5d2a6 100644 --- a/src/options.go +++ b/src/options.go @@ -1207,6 +1207,8 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro mergeAttr(&theme.SelectedFg) case "selected-bg": mergeAttr(&theme.SelectedBg) + case "nth": + mergeAttr(&theme.Nth) case "gutter": mergeAttr(&theme.Gutter) 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 } diff --git a/src/result.go b/src/result.go index f10db19b..ada31d59 100644 --- a/src/result.go +++ b/src/result.go @@ -104,11 +104,11 @@ func minRank() Result { 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() // No ANSI codes - if len(itemColors) == 0 { + if len(itemColors) == 0 && len(nthOffsets) == 0 { var offsets []colorOffset for _, off := range matchOffsets { 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 var maxCol int32 - for _, off := range matchOffsets { + for _, off := range append(matchOffsets, nthOffsets...) { if off[1] > maxCol { 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 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 i := off[0]; i < off[1]; i++ { - // Negative of 1-based index of itemColors - // - The extra -1 means highlighted - if cols[i] >= 0 { - cols[i] = cols[i]*-1 - 1 - } + cols[i].match = true + } + } + + 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 ansiToColorPair := func(ansi ansiOffset, base tui.ColorPair) tui.ColorPair { fg := ansi.color.fg @@ -175,12 +188,12 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, } var colors []colorOffset add := func(idx int) { - if curr != 0 && idx > start { - if curr < 0 { + if (curr.color || curr.nth || curr.match) && idx > start { + if curr.match { color := colMatch var url *url - if curr < -1 && theme.Colored { - ansi := itemColors[-curr-2] + if curr.color && theme.Colored { + ansi := itemColors[curr.index] url = ansi.color.url origColor := ansiToColorPair(ansi, colMatch) // 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) } } + if curr.nth { + color = color.WithAttr(attrNth) + } colors = append(colors, colorOffset{ offset: [2]int32{int32(start), int32(idx)}, color: color, match: true, url: url}) - } else { - ansi := itemColors[curr-1] + } else if curr.color { + ansi := itemColors[curr.index] + color := ansiToColorPair(ansi, colBase) + if curr.nth { + color = color.WithAttr(attrNth) + } colors = append(colors, colorOffset{ offset: [2]int32{int32(start), int32(idx)}, - color: ansiToColorPair(ansi, colBase), + color: color, match: false, url: ansi.color.url}) + } else { + colors = append(colors, colorOffset{ + offset: [2]int32{int32(start), int32(idx)}, + color: colBase.WithAttr(attrNth), + match: false, + url: nil}) } } } diff --git a/src/result_test.go b/src/result_test.go index c11e1ab5..520ee4b1 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, 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) { o := colors[idx] 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) 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}} - // {[22 25] {2 6 1}} {[25 27] {2 6 9}} {[27 30] {-1 -1 8}} - // {[30 32] {3 7 8}} {[32 33] {-1 -1 8}} {[33 35] {4 8 9}} - // {[35 40] {4 8 1}}] - assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined)) - assert(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline)) - assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined)) - assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold)) - assert(4, 25, 27, tui.NewColorPair(2, 6, tui.Bold|tui.Underline)) - 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, 40, tui.NewColorPair(4, 8, tui.Bold)) + 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) + + // [{[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}} + // {[30 32] {3 7 8}} {[32 33] {-1 -1 8}} {[33 35] {4 8 9}} + // {[35 37] {4 8 1}} {[37 39] {4 8 x|1}} {[39 40] {4 8 x|1}}] + assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined)) + assert(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline)) + assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined)) + assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold)) + assert(4, 25, 27, tui.NewColorPair(2, 6, tui.Bold|tui.Underline)) + 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)) + } } diff --git a/src/terminal.go b/src/terminal.go index 2b86e010..a49fb5e8 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -301,7 +301,9 @@ type Terminal struct { scrollbar string previewScrollbar string ansi bool + nthAttr tui.Attr nth []Range + nthCurrent []Range tabstop int margin [4]sizeSpec padding [4]sizeSpec @@ -885,7 +887,9 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor header: []string{}, header0: opts.Header, ansi: opts.Ansi, + nthAttr: opts.Theme.Nth.Attr, nth: opts.Nth, + nthCurrent: opts.Nth, tabstop: opts.Tabstop, hasStartActions: 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) { if offsets == nil { // 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 { if length > limit { @@ -2717,7 +2721,22 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat } 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 if t.canSpanMultiLines() { @@ -4667,6 +4686,7 @@ func (t *Terminal) Loop() error { // The default newNth = &t.nth } + t.nthCurrent = *newNth // Cycle if len(tokens) > 1 { a.a = strings.Join(append(tokens[1:], tokens[0]), "|") diff --git a/src/tui/dummy.go b/src/tui/dummy.go index a49677c6..aaa9a7ea 100644 --- a/src/tui/dummy.go +++ b/src/tui/dummy.go @@ -11,6 +11,10 @@ func HasFullscreenRenderer() bool { var DefaultBorderShape = BorderRounded func (a Attr) Merge(b Attr) Attr { + if b == AttrRegular { + return b + } + return a | b } diff --git a/src/tui/tui.go b/src/tui/tui.go index 0ab4874b..921c7a04 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -205,6 +205,10 @@ type ColorAttr struct { Attr Attr } +func (a ColorAttr) IsColorDefined() bool { + return a.Color != colUndefined +} + func NewColorAttr() ColorAttr { return ColorAttr{Color: colUndefined, Attr: AttrUndefined} } @@ -305,6 +309,7 @@ type ColorTheme struct { Bg ColorAttr ListFg ColorAttr ListBg ColorAttr + Nth ColorAttr SelectedFg ColorAttr SelectedBg ColorAttr SelectedMatch ColorAttr @@ -703,6 +708,7 @@ func EmptyTheme() *ColorTheme { HeaderBg: ColorAttr{colUndefined, AttrUndefined}, HeaderBorder: ColorAttr{colUndefined, AttrUndefined}, HeaderLabel: ColorAttr{colUndefined, AttrUndefined}, + Nth: ColorAttr{colUndefined, AttrUndefined}, } } @@ -746,6 +752,7 @@ func NoColorTheme() *ColorTheme { HeaderBg: ColorAttr{colDefault, AttrUndefined}, HeaderBorder: ColorAttr{colDefault, AttrUndefined}, HeaderLabel: ColorAttr{colDefault, AttrUndefined}, + Nth: ColorAttr{colUndefined, AttrUndefined}, } } @@ -786,6 +793,7 @@ func init() { InputBg: ColorAttr{colUndefined, AttrUndefined}, InputBorder: ColorAttr{colUndefined, AttrUndefined}, InputLabel: ColorAttr{colUndefined, AttrUndefined}, + Nth: ColorAttr{colUndefined, AttrUndefined}, } Dark256 = &ColorTheme{ Colored: true, @@ -823,6 +831,7 @@ func init() { InputBg: ColorAttr{colUndefined, AttrUndefined}, InputBorder: ColorAttr{colUndefined, AttrUndefined}, InputLabel: ColorAttr{colUndefined, AttrUndefined}, + Nth: ColorAttr{colUndefined, AttrUndefined}, } Light256 = &ColorTheme{ Colored: true, @@ -863,6 +872,7 @@ func init() { HeaderBg: ColorAttr{colUndefined, AttrUndefined}, HeaderBorder: ColorAttr{colUndefined, AttrUndefined}, HeaderLabel: ColorAttr{colUndefined, AttrUndefined}, + Nth: ColorAttr{colUndefined, AttrUndefined}, } }