Skip to content

Commit

Permalink
support ANSI Escape Sequence Select Graphic Rendition (#163)
Browse files Browse the repository at this point in the history
* update go.mod

* support Select Graphic Rendition

* add fuzzing for preview window

* add tests related to SGR

* use Go 1.19
  • Loading branch information
ktr0731 authored Oct 23, 2022
1 parent 3ecdbfe commit e22fee8
Show file tree
Hide file tree
Showing 18 changed files with 289 additions and 98 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
go: ['1.17']
go: ['1.19']
steps:
- name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v3
Expand Down
15 changes: 8 additions & 7 deletions example/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,21 @@ go 1.17
replace github.com/ktr0731/go-fuzzyfinder => ../

require (
github.com/ktr0731/go-fuzzyfinder v0.5.1
github.com/mattn/go-isatty v0.0.14
github.com/ktr0731/go-fuzzyfinder v0.6.0
github.com/mattn/go-isatty v0.0.16
github.com/spf13/pflag v1.0.5
)

require (
github.com/gdamore/encoding v1.0.0 // indirect
github.com/gdamore/tcell/v2 v2.5.3 // indirect
github.com/ktr0731/go-ansisgr v0.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/nsf/termbox-go v0.0.0-20201124104050-ed494de23a00 // indirect
github.com/nsf/termbox-go v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 // indirect
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect
golang.org/x/text v0.3.7 // indirect
github.com/rivo/uniseg v0.4.2 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/term v0.1.0 // indirect
golang.org/x/text v0.4.0 // indirect
)
43 changes: 34 additions & 9 deletions example/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,54 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/ktr0731/go-ansisgr v0.1.0 h1:fbuupput8739hQbEmZn1cEKjqQFwtCCZNznnF6ANo5w=
github.com/ktr0731/go-ansisgr v0.1.0/go.mod h1:G9lxwgBwH0iey0Dw5YQd7n6PmQTwTuTM/X5Sgm/UrzE=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/nsf/termbox-go v0.0.0-20201124104050-ed494de23a00 h1:Rl8NelBe+n7SuLbJyw13ho7CGWUt2BjGGKIoreCWQ/c=
github.com/nsf/termbox-go v0.0.0-20201124104050-ed494de23a00/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY=
github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 h1:saXMvIOKvRFwbOMicHXr0B1uwoxq9dGmLe5ExMES6c4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
32 changes: 32 additions & 0 deletions fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package fuzzyfinder_test

import (
"errors"
"testing"

"github.com/gdamore/tcell/v2"
fuzzyfinder "github.com/ktr0731/go-fuzzyfinder"
)

func FuzzPreviewWindow(f *testing.F) {
slice := []string{"foo"}

f.Add("Lorem ipsum dolor sit amet, consectetur adipiscing elit")
f.Add("Sed eget dui libero.\nVivamus tempus, magna nec mollis convallis, ipsum justo tincidunt ligula, ut varius est mi id nisl.\nMorbi commodo turpis risus, nec vehicula leo auctor sit amet.\nUt imperdiet suscipit massa ac vehicula.\nInterdum et malesuada fames ac ante ipsum primis in faucibus.\nPraesent ligula orci, facilisis pulvinar varius eget, iaculis in erat.\nProin pellentesque arcu sed nisl consectetur tristique.\nQuisque tempus blandit dignissim.\nPhasellus dignissim sollicitudin mauris, sed gravida arcu luctus tincidunt.\nNunc rhoncus sed eros vel molestie.\nAenean sodales tortor eu libero rutrum, et lobortis orci scelerisque.\nPraesent sollicitudin, nunc ut consequat commodo, risus velit consectetur nibh, quis pretium nunc elit et erat.")
f.Add("foo\x1b[31;1;44;0;90;105;38;5;12;48;5;226;38;2;10;20;30;48;2;200;100;50mbar")

f.Fuzz(func(t *testing.T, s string) {
finder, term := fuzzyfinder.NewWithMockedTerminal()
events := []tcell.Event{key(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone})}
term.SetEventsV2(events...)

_, err := finder.Find(
slice,
func(int) string { return slice[0] },
fuzzyfinder.WithPreviewWindow(func(i, width, height int) string { return s }),
)
if !errors.Is(err, fuzzyfinder.ErrAbort) {
t.Fatalf("Find must return ErrAbort, but got '%s'", err)
}
})
}
1 change: 1 addition & 0 deletions fuzzing_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build fuzz
// +build fuzz

package fuzzyfinder_test
Expand Down
136 changes: 63 additions & 73 deletions fuzzyfinder.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"flag"
"fmt"
"reflect"
"regexp"
"sort"
"strings"
"sync"
Expand All @@ -17,16 +16,16 @@ import (
"unicode/utf8"

"github.com/gdamore/tcell/v2"
"github.com/ktr0731/go-ansisgr"
"github.com/ktr0731/go-fuzzyfinder/matching"
runewidth "github.com/mattn/go-runewidth"
"github.com/pkg/errors"
)

var (
// ErrAbort is returned from Find* functions if there are no selections.
ErrAbort = errors.New("abort")
errEntered = errors.New("entered")
colorsRegex = regexp.MustCompile(`\[[0-9;]*m`)
ErrAbort = errors.New("abort")
errEntered = errors.New("entered")
)

// Finds the minimum value among the arguments
Expand Down Expand Up @@ -151,8 +150,7 @@ func (f *finder) _draw() {
// prompt line
var promptLinePad int

//nolint:staticcheck
for _, r := range []rune(f.opt.promptString) {
for _, r := range f.opt.promptString {
style := tcell.StyleDefault.
Foreground(tcell.ColorBlue).
Background(tcell.ColorDefault)
Expand All @@ -179,7 +177,7 @@ func (f *finder) _draw() {
// Header line
if len(f.opt.header) > 0 {
w = 0
for _, r := range []rune(runewidth.Truncate(f.opt.header, maxWidth-2, "..")) {
for _, r := range runewidth.Truncate(f.opt.header, maxWidth-2, "..") {
style := tcell.StyleDefault.
Foreground(tcell.ColorGreen).
Background(tcell.ColorDefault)
Expand Down Expand Up @@ -278,53 +276,6 @@ func (f *finder) _draw() {
}
}

func parseColor(rs *[]rune) (tcell.Color, bool) {
// only parses for 16 colors
// convert to string for easier parsing
str := string(*rs)
ansi := colorsRegex.FindStringSubmatch(str)
var bold bool

if len(ansi) == 0 {
// no color is being passed, return defaults
return tcell.ColorDefault, false
}

// ANSI color value is being passed
// find if bold is specified
if len(ansi[0]) > 4 && ansi[0][3:5] == ";1" {
bold = true
}

// find the color value
color := ansi[0][1:3]

// strip color codes (also strips trailing '[0m')
stripped := colorsRegex.ReplaceAllString(str, "")
*rs = []rune(stripped)

switch color {
case "30":
return tcell.ColorBlack, bold
case "31":
return tcell.ColorRed, bold
case "32":
return tcell.ColorGreen, bold
case "33":
return tcell.ColorYellow, bold
case "34":
return tcell.ColorBlue, bold
case "35":
return tcell.ColorDarkMagenta, bold
case "36":
return tcell.ColorDarkCyan, bold
case "37":
return tcell.ColorWhite, bold
default:
return tcell.ColorDefault, bold
}
}

func (f *finder) _drawPreview() {
if f.opt.previewFunc == nil {
return
Expand All @@ -338,11 +289,7 @@ func (f *finder) _drawPreview() {
idx = f.state.matched[f.state.y].Idx
}

sp := strings.Split(f.opt.previewFunc(idx, width, height), "\n")
prevLines := make([][]rune, 0, len(sp))
for _, s := range sp {
prevLines = append(prevLines, []rune(s))
}
iter := ansisgr.NewIterator(f.opt.previewFunc(idx, width, height))

// top line
for i := width / 2; i < width; i++ {
Expand Down Expand Up @@ -384,6 +331,8 @@ func (f *finder) _drawPreview() {
const vline = '│'
var wvline = runewidth.RuneWidth(vline)
for h := 1; h < height-1; h++ {
// donePreviewLine indicates the preview string of the current line identified by h is already drawn.
var donePreviewLine bool
w := width / 2
for i := width / 2; i < width; i++ {
switch {
Expand All @@ -410,20 +359,24 @@ func (f *finder) _drawPreview() {
f.term.SetContent(w, h, ' ', nil, style)
w++
default: // Preview text
if h-1 >= len(prevLines) {
w++
if donePreviewLine {
continue
}
j := i - width/2 - 2 // Two spaces.
l := prevLines[h-1]
// parse colors here and strip color codes from runes
col, isBold := parseColor(&l)
if j >= len(l) {
w++

r, rstyle, ok := iter.Next()
if !ok || r == '\n' {
// Consumed all preview characters.
donePreviewLine = true
continue
}
rw := runewidth.RuneWidth(l[j])

rw := runewidth.RuneWidth(r)
if w+rw > width-1-2 {
donePreviewLine = true

// Discard the rest of the current line.
consumeIterator(iter, '\n')

style := tcell.StyleDefault.
Foreground(tcell.ColorDefault).
Background(tcell.ColorDefault)
Expand All @@ -435,11 +388,39 @@ func (f *finder) _drawPreview() {
continue
}

style := tcell.StyleDefault.
Foreground(col).
Background(tcell.ColorDefault).
Bold(isBold)
f.term.SetContent(w, h, l[j], nil, style)
style := tcell.StyleDefault
if color, ok := rstyle.Foreground(); ok {
switch color.Mode() {
case ansisgr.Mode16:
style = style.Foreground(tcell.PaletteColor(color.Value() - 30))
case ansisgr.Mode256:
style = style.Foreground(tcell.PaletteColor(color.Value()))
case ansisgr.ModeRGB:
r, g, b := color.RGB()
style = style.Foreground(tcell.NewRGBColor(int32(r), int32(g), int32(b)))
}
}
if color, valid := rstyle.Background(); valid {
switch color.Mode() {
case ansisgr.Mode16:
style = style.Background(tcell.PaletteColor(color.Value() - 40))
case ansisgr.Mode256:
style = style.Background(tcell.PaletteColor(color.Value()))
case ansisgr.ModeRGB:
r, g, b := color.RGB()
style = style.Background(tcell.NewRGBColor(int32(r), int32(g), int32(b)))
}
}

style = style.
Bold(rstyle.Bold()).
Dim(rstyle.Dim()).
Italic(rstyle.Italic()).
Underline(rstyle.Underline()).
Blink(rstyle.Blink()).
Reverse(rstyle.Reverse()).
StrikeThrough(rstyle.Strikethrough())
f.term.SetContent(w, h, r, nil, style)
w += rw
}
}
Expand Down Expand Up @@ -824,3 +805,12 @@ func (f *finder) FindMulti(slice interface{}, itemFunc func(i int) string, opts
func isInTesting() bool {
return flag.Lookup("test.v") != nil
}

func consumeIterator(iter *ansisgr.Iterator, r rune) {
for {
r, _, ok := iter.Next()
if !ok || r == '\n' {
return
}
}
}
Loading

0 comments on commit e22fee8

Please sign in to comment.