summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2025-01-16 01:38:45 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2025-01-16 01:38:45 +0900
commit3e7f032ec200f00f92fa53f9aecde4b989c7dd66 (patch)
tree490418973327dc24fcfd036e14f856a11af183d7
parentb42f5bfb19b080eb12b886ec0787de47a069cc33 (diff)
downloadfzf-3e7f032ec200f00f92fa53f9aecde4b989c7dd66.tar.gz
Allow displaying --nth parts in a different text style
Close #4183
-rw-r--r--CHANGELOG.md15
-rw-r--r--man/man1/fzf.11
-rw-r--r--src/options.go6
-rw-r--r--src/result.go62
-rw-r--r--src/result_test.go42
-rw-r--r--src/terminal.go24
-rw-r--r--src/tui/dummy.go4
-rw-r--r--src/tui/tui.go10
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},
}
}