From efa7b4958391b1948697216c8a5451bd7d6a3c54 Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Sat, 28 Nov 2020 23:44:26 +0900 Subject: [PATCH 01/26] replace with tcell function --- fuzzyfinder.go | 172 ++++++++++++++++++++++++++++++++----------------- go.mod | 1 + go.sum | 25 ++++--- tcell.go | 47 ++------------ 4 files changed, 131 insertions(+), 114 deletions(-) diff --git a/fuzzyfinder.go b/fuzzyfinder.go index 4565486..88efe4e 100644 --- a/fuzzyfinder.go +++ b/fuzzyfinder.go @@ -15,9 +15,9 @@ import ( "unicode" "unicode/utf8" + "github.com/gdamore/tcell/v2" "github.com/ktr0731/go-fuzzyfinder/matching" runewidth "github.com/mattn/go-runewidth" - "github.com/nsf/termbox-go" "github.com/pkg/errors" ) @@ -70,10 +70,16 @@ type finder struct { func (f *finder) initFinder(items []string, matched []matching.Matched, opt opt) error { if f.term == nil { - f.term = &termImpl{} + screen, err := tcell.NewScreen() + if err != nil { + return errors.Wrap(err, "failed to new screen") + } + f.term = &termImpl{ + s: screen, + } } - if err := f.term.init(); err != nil { + if err := f.term.(*termImpl).s.Init(); err != nil { return errors.Wrap(err, "failed to initialize termbox") } @@ -91,7 +97,7 @@ func (f *finder) initFinder(items []string, matched []matching.Matched, opt opt) f.drawTimer = time.AfterFunc(0, func() { f._draw() f._drawPreview() - f.term.flush() + f.term.screen().Show() }) f.drawTimer.Stop() } @@ -110,8 +116,8 @@ func (f *finder) updateItems(items []string, matched []matching.Matched) { // _draw is used from draw with a timer. func (f *finder) _draw() { - width, height := f.term.size() - f.term.clear(termbox.ColorDefault, termbox.ColorDefault) + width, height := f.term.screen().Size() + f.term.screen().Clear() maxWidth := width if f.opt.previewFunc != nil { @@ -120,22 +126,34 @@ func (f *finder) _draw() { // prompt line var promptLinePad int + style := tcell.StyleDefault. + Foreground(tcell.ColorBlue). + Background(tcell.ColorDefault) + for _, r := range []rune(f.opt.promptString) { - f.term.setCell(promptLinePad, height-1, r, termbox.ColorBlue, termbox.ColorDefault) + f.term.screen().SetContent(promptLinePad, height-1, r, nil, style) promptLinePad++ } var r rune var w int + style = tcell.StyleDefault. + Foreground(tcell.ColorDefault). + Background(tcell.ColorDefault). + Bold(true) for _, r = range f.state.input { // Add a space between '>' and runes. - f.term.setCell(promptLinePad+w, height-1, r, termbox.ColorDefault|termbox.AttrBold, termbox.ColorDefault) + f.term.screen().SetContent(promptLinePad+w, height-1, r, nil, style) w += runewidth.RuneWidth(r) } - f.term.setCursor(promptLinePad+f.state.cursorX, height-1) + f.term.screen().ShowCursor(promptLinePad+f.state.cursorX, height-1) + + style = tcell.StyleDefault. + Foreground(tcell.ColorYellow). + Background(tcell.ColorDefault) // Number line for i, r := range fmt.Sprintf("%d/%d", len(f.state.matched), len(f.state.items)) { - f.term.setCell(2+i, height-2, r, termbox.ColorYellow, termbox.ColorDefault) + f.term.screen().SetContent(2+i, height-2, r, nil, style) } // Item lines @@ -151,45 +169,59 @@ func (f *finder) _draw() { break } if i == f.state.cursorY { - f.term.setCell(0, height-3-i, '>', termbox.ColorRed, termbox.ColorBlack) - f.term.setCell(1, height-3-i, ' ', termbox.ColorRed, termbox.ColorBlack) + style = tcell.StyleDefault. + Foreground(tcell.ColorRed). + Background(tcell.ColorBlack) + + f.term.screen().SetContent(0, height-3-i, '>', nil, style) + f.term.screen().SetContent(1, height-3-i, ' ', nil, style) } if f.opt.multi { if _, ok := f.state.selection[m.Idx]; ok { - f.term.setCell(1, height-3-i, '>', termbox.ColorRed, termbox.ColorBlack) + f.term.screen().SetContent(1, height-3-i, '>', nil, style) } } var posIdx int w := 2 for j, r := range []rune(f.state.items[m.Idx]) { - fg := termbox.ColorDefault - bg := termbox.ColorDefault + style = tcell.StyleDefault. + Foreground(tcell.ColorDefault). + Background(tcell.ColorDefault) // Highlight selected strings. if posIdx < len(f.state.input) { from, to := m.Pos[0], m.Pos[1] if !(from == -1 && to == -1) && (from <= j && j <= to) { if unicode.ToLower(f.state.input[posIdx]) == unicode.ToLower(r) { - fg |= termbox.ColorGreen + currentFg, _, _ := style.Decompose() + currentFgR, currentFgG, currentFgB := currentFg.RGB() + mixColor := tcell.ColorGreen + mixR, mixG, mixB := mixColor.RGB() + newFg := tcell.NewRGBColor((currentFgR+mixR)/2, (currentFgG+mixG)/2, (currentFgB+mixB)/2) + style = tcell.StyleDefault. + Foreground(newFg). + Background(tcell.ColorDefault) posIdx++ } } } if i == f.state.cursorY { - fg |= termbox.AttrBold | termbox.ColorYellow - bg = termbox.ColorBlack + style = tcell.StyleDefault. + Foreground(tcell.ColorYellow). + Bold(true). + Background(tcell.ColorBlack) } rw := runewidth.RuneWidth(r) // Shorten item cells. if w+rw+2 > maxWidth { - f.term.setCell(w, height-3-i, '.', fg, bg) - f.term.setCell(w+1, height-3-i, '.', fg, bg) + f.term.screen().SetContent(w, height-3-i, '.', nil, style) + f.term.screen().SetContent(w+1, height-3-i, '.', nil, style) w += 2 break } else { - f.term.setCell(w, height-3-i, r, fg, bg) + f.term.screen().SetContent(w, height-3-i, r, nil, style) w += rw } } @@ -201,7 +233,7 @@ func (f *finder) _drawPreview() { return } - width, height := f.term.size() + width, height := f.term.screen().Size() var idx int if len(f.state.matched) == 0 { idx = -1 @@ -215,6 +247,10 @@ func (f *finder) _drawPreview() { prevLines = append(prevLines, []rune(s)) } + style := tcell.StyleDefault. + Foreground(tcell.ColorBlack). + Background(tcell.ColorDefault) + // top line for i := width / 2; i < width; i++ { var r rune @@ -225,9 +261,13 @@ func (f *finder) _drawPreview() { } else { r = '─' } - f.term.setCell(i, 0, r, termbox.ColorBlack, termbox.ColorDefault) + f.term.screen().SetContent(i, 0, r, nil, style) } // bottom line + style = tcell.StyleDefault. + Foreground(tcell.ColorBlack). + Background(tcell.ColorDefault) + for i := width / 2; i < width; i++ { var r rune if i == width/2 { @@ -237,7 +277,7 @@ func (f *finder) _drawPreview() { } else { r = '─' } - f.term.setCell(i, height-1, r, termbox.ColorBlack, termbox.ColorDefault) + f.term.screen().SetContent(i, height-1, r, nil, style) } // Start with h=1 to exclude each corner rune. const vline = '│' @@ -248,15 +288,25 @@ func (f *finder) _drawPreview() { switch { // Left vertical line. case i == width/2: - f.term.setCell(i, h, vline, termbox.ColorBlack, termbox.ColorDefault) + style = tcell.StyleDefault. + Foreground(tcell.ColorBlack). + Background(tcell.ColorDefault) + f.term.screen().SetContent(i, h, vline, nil, style) w += wvline // Right vertical line. case i == width-1: - f.term.setCell(i, h, vline, termbox.ColorBlack, termbox.ColorDefault) + style = tcell.StyleDefault. + Foreground(tcell.ColorBlack). + Background(tcell.ColorDefault) + f.term.screen().SetContent(i, h, vline, nil, style) w += wvline // Spaces between left and right vertical lines. case w == width/2+wvline, w == width-1-wvline: - f.term.setCell(w, h, ' ', termbox.ColorDefault, termbox.ColorDefault) + style = tcell.StyleDefault. + Foreground(tcell.ColorDefault). + Background(tcell.ColorDefault) + + f.term.screen().SetContent(w, h, ' ', nil, style) w++ default: // Preview text if h-1 >= len(prevLines) { @@ -271,13 +321,18 @@ func (f *finder) _drawPreview() { } rw := runewidth.RuneWidth(l[j]) if w+rw > width-1-2 { - f.term.setCell(w, h, '.', termbox.ColorDefault, termbox.ColorDefault) - f.term.setCell(w+1, h, '.', termbox.ColorDefault, termbox.ColorDefault) + style = tcell.StyleDefault. + Foreground(tcell.ColorBlack). + Background(tcell.ColorDefault) + + f.term.screen().SetContent(w, h, '.', nil, style) + f.term.screen().SetContent(w+1, h, '.', nil, style) + w += 2 continue } - f.term.setCell(w, h, l[j], termbox.ColorDefault, termbox.ColorDefault) + f.term.screen().SetContent(w, h, l[j], nil, style) w += rw } } @@ -292,7 +347,7 @@ func (f *finder) draw(d time.Duration) { // Don't use goroutine scheduling. f._draw() f._drawPreview() - f.term.flush() + f.term.screen().Show() } else { f.drawTimer.Reset(d) } @@ -314,16 +369,16 @@ func (f *finder) readKey() error { } }() - e := f.term.pollEvent() + e := f.term.screen().PollEvent() f.stateMu.Lock() defer f.stateMu.Unlock() - switch e.Type { - case termbox.EventKey: - switch e.Key { - case termbox.KeyEsc, termbox.KeyCtrlC, termbox.KeyCtrlD: + switch e := e.(type) { + case *tcell.EventKey: + switch e.Key() { + case tcell.KeyEsc, tcell.KeyCtrlC, tcell.KeyCtrlD: return ErrAbort - case termbox.KeyBackspace, termbox.KeyBackspace2: + case tcell.KeyBackspace, tcell.KeyBackspace2: if len(f.state.input) == 0 { return nil } @@ -334,32 +389,32 @@ func (f *finder) readKey() error { f.state.cursorX -= runewidth.RuneWidth(f.state.input[len(f.state.input)-1]) f.state.x-- f.state.input = f.state.input[0 : len(f.state.input)-1] - case termbox.KeyDelete: + case tcell.KeyDelete: if f.state.x == len(f.state.input) { return nil } x := f.state.x f.state.input = append(f.state.input[:x], f.state.input[x+1:]...) - case termbox.KeyEnter: + case tcell.KeyEnter: return errEntered - case termbox.KeyArrowLeft, termbox.KeyCtrlB: + case tcell.KeyLeft, tcell.KeyCtrlB: if f.state.x > 0 { f.state.cursorX -= runewidth.RuneWidth(f.state.input[f.state.x-1]) f.state.x-- } - case termbox.KeyArrowRight, termbox.KeyCtrlF: + case tcell.KeyRight, tcell.KeyCtrlF: if f.state.x < len(f.state.input) { f.state.cursorX += runewidth.RuneWidth(f.state.input[f.state.x]) f.state.x++ } - case termbox.KeyCtrlA: + case tcell.KeyCtrlA: f.state.cursorX = 0 f.state.x = 0 - case termbox.KeyCtrlE: + case tcell.KeyCtrlE: f.state.cursorX = runewidth.StringWidth(string(f.state.input)) f.state.x = len(f.state.input) - case termbox.KeyCtrlW: + case tcell.KeyCtrlW: in := f.state.input[:f.state.x] inStr := string(in) pos := strings.LastIndex(strings.TrimRightFunc(inStr, unicode.IsSpace), " ") @@ -374,26 +429,26 @@ func (f *finder) readKey() error { f.state.input = newIn f.state.cursorX = runewidth.StringWidth(string(newIn)) f.state.x = len(newIn) - case termbox.KeyCtrlU: + case tcell.KeyCtrlU: f.state.input = f.state.input[f.state.x:] f.state.cursorX = 0 f.state.x = 0 - case termbox.KeyArrowUp, termbox.KeyCtrlK, termbox.KeyCtrlP: + case tcell.KeyUp, tcell.KeyCtrlK, tcell.KeyCtrlP: if f.state.y+1 < len(f.state.matched) { f.state.y++ } - _, height := f.term.size() + _, height := f.term.screen().Size() if f.state.cursorY+1 < height-2 && f.state.cursorY+1 < len(f.state.matched) { f.state.cursorY++ } - case termbox.KeyArrowDown, termbox.KeyCtrlJ, termbox.KeyCtrlN: + case tcell.KeyDown, tcell.KeyCtrlJ, tcell.KeyCtrlN: if f.state.y > 0 { f.state.y-- } if f.state.cursorY-1 >= 0 { f.state.cursorY-- } - case termbox.KeyTab: + case tcell.KeyTab: if !f.opt.multi { return nil } @@ -411,11 +466,8 @@ func (f *finder) readKey() error { f.state.cursorY-- } default: - if e.Key == termbox.KeySpace { - e.Ch = ' ' - } - if e.Ch != 0 { - width, _ := f.term.size() + if e.Rune() != 0 { + width, _ := f.term.screen().Size() maxLineWidth := width - 2 - 1 if len(f.state.input)+1 > maxLineWidth { // Discard inputted rune. @@ -423,17 +475,17 @@ func (f *finder) readKey() error { } x := f.state.x - f.state.input = append(f.state.input[:x], append([]rune{e.Ch}, f.state.input[x:]...)...) - f.state.cursorX += runewidth.RuneWidth(e.Ch) + f.state.input = append(f.state.input[:x], append([]rune{e.Rune()}, f.state.input[x:]...)...) + f.state.cursorX += runewidth.RuneWidth(e.Rune()) f.state.x++ } } - case termbox.EventResize: + case *tcell.EventResize: // To get the actual window size, clear all buffers. // See termbox.Clear's documentation for more details. - f.term.clear(termbox.ColorDefault, termbox.ColorDefault) + f.term.screen().Clear() - width, height := f.term.size() + width, height := f.term.screen().Size() itemAreaHeight := height - 2 - 1 if itemAreaHeight >= 0 && f.state.cursorY > itemAreaHeight { f.state.cursorY = itemAreaHeight @@ -553,7 +605,7 @@ func (f *finder) find(slice interface{}, itemFunc func(i int) string, opts []Opt if err := f.initFinder(items, matched, opt); err != nil { return nil, errors.Wrap(err, "failed to initialize the fuzzy finder") } - defer f.term.close() + defer f.term.screen().Fini() close(inited) diff --git a/go.mod b/go.mod index ed57611..e623957 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module github.com/ktr0731/go-fuzzyfinder require ( + github.com/gdamore/tcell/v2 v2.0.0 github.com/google/go-cmp v0.5.4 github.com/google/gofuzz v1.2.0 github.com/mattn/go-runewidth v0.0.9 diff --git a/go.sum b/go.sum index a28fa08..21982e1 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,23 @@ -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell/v2 v2.0.0 h1:GRWG8aLfWAlekj9Q6W29bVvkHENc6hp79XOqG4AWDOs= +github.com/gdamore/tcell/v2 v2.0.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1 h1:lh3PyZvY+B9nFliSGTn5uFuqQQJGuNrD0MLCokv09ag= github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10= +golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/tcell.go b/tcell.go index d2bd4fb..38b5d42 100644 --- a/tcell.go +++ b/tcell.go @@ -1,53 +1,18 @@ package fuzzyfinder import ( - "github.com/nsf/termbox-go" + "github.com/gdamore/tcell/v2" ) -// terminal is an abstraction for mocking termbox-go. type terminal interface { - init() error - size() (width int, height int) - clear(termbox.Attribute, termbox.Attribute) error - setCell(x, y int, ch rune, fg, bg termbox.Attribute) - setCursor(x, y int) - pollEvent() termbox.Event - flush() error - close() + screen() tcell.Screen } // termImpl is the implementation for termbox-go. -type termImpl struct{} - -func (t *termImpl) init() error { - return termbox.Init() -} - -func (t *termImpl) size() (width int, height int) { - return termbox.Size() -} - -func (t *termImpl) clear(fg termbox.Attribute, bg termbox.Attribute) error { - termbox.Clear(fg, bg) - return nil -} - -func (t *termImpl) setCell(x int, y int, ch rune, fg termbox.Attribute, bg termbox.Attribute) { - termbox.SetCell(x, y, ch, fg, bg) -} - -func (t *termImpl) setCursor(x int, y int) { - termbox.SetCursor(x, y) -} - -func (t *termImpl) pollEvent() termbox.Event { - return termbox.PollEvent() -} - -func (t *termImpl) flush() error { - return termbox.Flush() +type termImpl struct { + s tcell.Screen } -func (t *termImpl) close() { - termbox.Close() +func (t *termImpl) screen() tcell.Screen { + return t.s } From 6ae4881eff72d495f87688dad94bc06f333ed393 Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Sun, 29 Nov 2020 21:56:38 +0900 Subject: [PATCH 02/26] mix color logic was wrong --- fuzzyfinder.go | 77 +++++++++++++++++++++++--------------------------- tcell.go | 8 +++--- 2 files changed, 40 insertions(+), 45 deletions(-) diff --git a/fuzzyfinder.go b/fuzzyfinder.go index 88efe4e..2665536 100644 --- a/fuzzyfinder.go +++ b/fuzzyfinder.go @@ -7,6 +7,7 @@ import ( "context" "flag" "fmt" + "log" "reflect" "sort" "strings" @@ -75,12 +76,11 @@ func (f *finder) initFinder(items []string, matched []matching.Matched, opt opt) return errors.Wrap(err, "failed to new screen") } f.term = &termImpl{ - s: screen, + screen: screen, + } + if err := f.term.Screen().Init(); err != nil { + return errors.Wrap(err, "failed to initialize termbox") } - } - - if err := f.term.(*termImpl).s.Init(); err != nil { - return errors.Wrap(err, "failed to initialize termbox") } f.opt = &opt @@ -97,7 +97,7 @@ func (f *finder) initFinder(items []string, matched []matching.Matched, opt opt) f.drawTimer = time.AfterFunc(0, func() { f._draw() f._drawPreview() - f.term.screen().Show() + f.term.Screen().Show() }) f.drawTimer.Stop() } @@ -116,8 +116,8 @@ func (f *finder) updateItems(items []string, matched []matching.Matched) { // _draw is used from draw with a timer. func (f *finder) _draw() { - width, height := f.term.screen().Size() - f.term.screen().Clear() + width, height := f.term.Screen().Size() + f.term.Screen().Clear() maxWidth := width if f.opt.previewFunc != nil { @@ -131,7 +131,7 @@ func (f *finder) _draw() { Background(tcell.ColorDefault) for _, r := range []rune(f.opt.promptString) { - f.term.screen().SetContent(promptLinePad, height-1, r, nil, style) + f.term.Screen().SetContent(promptLinePad, height-1, r, nil, style) promptLinePad++ } var r rune @@ -142,10 +142,10 @@ func (f *finder) _draw() { Bold(true) for _, r = range f.state.input { // Add a space between '>' and runes. - f.term.screen().SetContent(promptLinePad+w, height-1, r, nil, style) + f.term.Screen().SetContent(promptLinePad+w, height-1, r, nil, style) w += runewidth.RuneWidth(r) } - f.term.screen().ShowCursor(promptLinePad+f.state.cursorX, height-1) + f.term.Screen().ShowCursor(promptLinePad+f.state.cursorX, height-1) style = tcell.StyleDefault. Foreground(tcell.ColorYellow). @@ -153,7 +153,7 @@ func (f *finder) _draw() { // Number line for i, r := range fmt.Sprintf("%d/%d", len(f.state.matched), len(f.state.items)) { - f.term.screen().SetContent(2+i, height-2, r, nil, style) + f.term.Screen().SetContent(2+i, height-2, r, nil, style) } // Item lines @@ -173,13 +173,13 @@ func (f *finder) _draw() { Foreground(tcell.ColorRed). Background(tcell.ColorBlack) - f.term.screen().SetContent(0, height-3-i, '>', nil, style) - f.term.screen().SetContent(1, height-3-i, ' ', nil, style) + f.term.Screen().SetContent(0, height-3-i, '>', nil, style) + f.term.Screen().SetContent(1, height-3-i, ' ', nil, style) } if f.opt.multi { if _, ok := f.state.selection[m.Idx]; ok { - f.term.screen().SetContent(1, height-3-i, '>', nil, style) + f.term.Screen().SetContent(1, height-3-i, '>', nil, style) } } @@ -194,13 +194,8 @@ func (f *finder) _draw() { from, to := m.Pos[0], m.Pos[1] if !(from == -1 && to == -1) && (from <= j && j <= to) { if unicode.ToLower(f.state.input[posIdx]) == unicode.ToLower(r) { - currentFg, _, _ := style.Decompose() - currentFgR, currentFgG, currentFgB := currentFg.RGB() - mixColor := tcell.ColorGreen - mixR, mixG, mixB := mixColor.RGB() - newFg := tcell.NewRGBColor((currentFgR+mixR)/2, (currentFgG+mixG)/2, (currentFgB+mixB)/2) style = tcell.StyleDefault. - Foreground(newFg). + Foreground(tcell.ColorGreen). Background(tcell.ColorDefault) posIdx++ } @@ -208,7 +203,7 @@ func (f *finder) _draw() { } if i == f.state.cursorY { style = tcell.StyleDefault. - Foreground(tcell.ColorYellow). + Foreground(tcell.ColorDarkCyan). Bold(true). Background(tcell.ColorBlack) } @@ -216,12 +211,12 @@ func (f *finder) _draw() { rw := runewidth.RuneWidth(r) // Shorten item cells. if w+rw+2 > maxWidth { - f.term.screen().SetContent(w, height-3-i, '.', nil, style) - f.term.screen().SetContent(w+1, height-3-i, '.', nil, style) + f.term.Screen().SetContent(w, height-3-i, '.', nil, style) + f.term.Screen().SetContent(w+1, height-3-i, '.', nil, style) w += 2 break } else { - f.term.screen().SetContent(w, height-3-i, r, nil, style) + f.term.Screen().SetContent(w, height-3-i, r, nil, style) w += rw } } @@ -233,7 +228,7 @@ func (f *finder) _drawPreview() { return } - width, height := f.term.screen().Size() + width, height := f.term.Screen().Size() var idx int if len(f.state.matched) == 0 { idx = -1 @@ -261,7 +256,7 @@ func (f *finder) _drawPreview() { } else { r = '─' } - f.term.screen().SetContent(i, 0, r, nil, style) + f.term.Screen().SetContent(i, 0, r, nil, style) } // bottom line style = tcell.StyleDefault. @@ -277,7 +272,7 @@ func (f *finder) _drawPreview() { } else { r = '─' } - f.term.screen().SetContent(i, height-1, r, nil, style) + f.term.Screen().SetContent(i, height-1, r, nil, style) } // Start with h=1 to exclude each corner rune. const vline = '│' @@ -291,14 +286,14 @@ func (f *finder) _drawPreview() { style = tcell.StyleDefault. Foreground(tcell.ColorBlack). Background(tcell.ColorDefault) - f.term.screen().SetContent(i, h, vline, nil, style) + f.term.Screen().SetContent(i, h, vline, nil, style) w += wvline // Right vertical line. case i == width-1: style = tcell.StyleDefault. Foreground(tcell.ColorBlack). Background(tcell.ColorDefault) - f.term.screen().SetContent(i, h, vline, nil, style) + f.term.Screen().SetContent(i, h, vline, nil, style) w += wvline // Spaces between left and right vertical lines. case w == width/2+wvline, w == width-1-wvline: @@ -306,7 +301,7 @@ func (f *finder) _drawPreview() { Foreground(tcell.ColorDefault). Background(tcell.ColorDefault) - f.term.screen().SetContent(w, h, ' ', nil, style) + f.term.Screen().SetContent(w, h, ' ', nil, style) w++ default: // Preview text if h-1 >= len(prevLines) { @@ -325,14 +320,14 @@ func (f *finder) _drawPreview() { Foreground(tcell.ColorBlack). Background(tcell.ColorDefault) - f.term.screen().SetContent(w, h, '.', nil, style) - f.term.screen().SetContent(w+1, h, '.', nil, style) + f.term.Screen().SetContent(w, h, '.', nil, style) + f.term.Screen().SetContent(w+1, h, '.', nil, style) w += 2 continue } - f.term.screen().SetContent(w, h, l[j], nil, style) + f.term.Screen().SetContent(w, h, l[j], nil, style) w += rw } } @@ -347,7 +342,7 @@ func (f *finder) draw(d time.Duration) { // Don't use goroutine scheduling. f._draw() f._drawPreview() - f.term.screen().Show() + f.term.Screen().Show() } else { f.drawTimer.Reset(d) } @@ -369,7 +364,7 @@ func (f *finder) readKey() error { } }() - e := f.term.screen().PollEvent() + e := f.term.Screen().PollEvent() f.stateMu.Lock() defer f.stateMu.Unlock() @@ -437,7 +432,7 @@ func (f *finder) readKey() error { if f.state.y+1 < len(f.state.matched) { f.state.y++ } - _, height := f.term.screen().Size() + _, height := f.term.Screen().Size() if f.state.cursorY+1 < height-2 && f.state.cursorY+1 < len(f.state.matched) { f.state.cursorY++ } @@ -467,7 +462,7 @@ func (f *finder) readKey() error { } default: if e.Rune() != 0 { - width, _ := f.term.screen().Size() + width, _ := f.term.Screen().Size() maxLineWidth := width - 2 - 1 if len(f.state.input)+1 > maxLineWidth { // Discard inputted rune. @@ -483,9 +478,9 @@ func (f *finder) readKey() error { case *tcell.EventResize: // To get the actual window size, clear all buffers. // See termbox.Clear's documentation for more details. - f.term.screen().Clear() + f.term.Screen().Clear() - width, height := f.term.screen().Size() + width, height := f.term.Screen().Size() itemAreaHeight := height - 2 - 1 if itemAreaHeight >= 0 && f.state.cursorY > itemAreaHeight { f.state.cursorY = itemAreaHeight @@ -605,7 +600,7 @@ func (f *finder) find(slice interface{}, itemFunc func(i int) string, opts []Opt if err := f.initFinder(items, matched, opt); err != nil { return nil, errors.Wrap(err, "failed to initialize the fuzzy finder") } - defer f.term.screen().Fini() + // defer f.term.Screen().Fini() close(inited) diff --git a/tcell.go b/tcell.go index 38b5d42..a630513 100644 --- a/tcell.go +++ b/tcell.go @@ -5,14 +5,14 @@ import ( ) type terminal interface { - screen() tcell.Screen + Screen() tcell.Screen } // termImpl is the implementation for termbox-go. type termImpl struct { - s tcell.Screen + screen tcell.Screen } -func (t *termImpl) screen() tcell.Screen { - return t.s +func (t *termImpl) Screen() tcell.Screen { + return t.screen } From c1de08d22caed5d4ffc26a242196a7051e232eca Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Mon, 30 Nov 2020 00:44:16 +0900 Subject: [PATCH 03/26] misuse style --- fuzzyfinder.go | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/fuzzyfinder.go b/fuzzyfinder.go index 2665536..6ea48a1 100644 --- a/fuzzyfinder.go +++ b/fuzzyfinder.go @@ -7,7 +7,6 @@ import ( "context" "flag" "fmt" - "log" "reflect" "sort" "strings" @@ -178,6 +177,10 @@ func (f *finder) _draw() { } if f.opt.multi { + style = tcell.StyleDefault. + Foreground(tcell.ColorRed). + Background(tcell.ColorBlack) + if _, ok := f.state.selection[m.Idx]; ok { f.term.Screen().SetContent(1, height-3-i, '>', nil, style) } @@ -190,6 +193,7 @@ func (f *finder) _draw() { Foreground(tcell.ColorDefault). Background(tcell.ColorDefault) // Highlight selected strings. + hasHighlighted := false if posIdx < len(f.state.input) { from, to := m.Pos[0], m.Pos[1] if !(from == -1 && to == -1) && (from <= j && j <= to) { @@ -197,15 +201,23 @@ func (f *finder) _draw() { style = tcell.StyleDefault. Foreground(tcell.ColorGreen). Background(tcell.ColorDefault) + hasHighlighted = true posIdx++ } } } if i == f.state.cursorY { - style = tcell.StyleDefault. - Foreground(tcell.ColorDarkCyan). - Bold(true). - Background(tcell.ColorBlack) + if hasHighlighted { + style = tcell.StyleDefault. + Foreground(tcell.ColorDarkCyan). + Bold(true). + Background(tcell.ColorBlack) + } else { + style = tcell.StyleDefault. + Foreground(tcell.ColorYellow). + Bold(true). + Background(tcell.ColorBlack) + } } rw := runewidth.RuneWidth(r) @@ -317,7 +329,7 @@ func (f *finder) _drawPreview() { rw := runewidth.RuneWidth(l[j]) if w+rw > width-1-2 { style = tcell.StyleDefault. - Foreground(tcell.ColorBlack). + Foreground(tcell.ColorDefault). Background(tcell.ColorDefault) f.term.Screen().SetContent(w, h, '.', nil, style) @@ -327,6 +339,9 @@ func (f *finder) _drawPreview() { continue } + style = tcell.StyleDefault. + Foreground(tcell.ColorDefault). + Background(tcell.ColorDefault) f.term.Screen().SetContent(w, h, l[j], nil, style) w += rw } From 87172bed3bed1b340045de7a69d3c5ba6fdbc15e Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Mon, 30 Nov 2020 22:38:09 +0900 Subject: [PATCH 04/26] migrate to tcell in test --- example_test.go | 30 ++--- fuzzing_test.go | 114 ++++++++--------- fuzzyfinder_test.go | 174 +++++++++++++++++--------- go.mod | 1 + go.sum | 29 +++++ helper_test.go | 7 +- mock.go | 292 +++++++++++++++++--------------------------- mock_test.go | 42 ++++--- 8 files changed, 361 insertions(+), 328 deletions(-) diff --git a/example_test.go b/example_test.go index e507bd7..fc8b5f9 100644 --- a/example_test.go +++ b/example_test.go @@ -4,8 +4,8 @@ import ( "fmt" "io/ioutil" + "github.com/gdamore/tcell/v2" fuzzyfinder "github.com/ktr0731/go-fuzzyfinder" - "github.com/nsf/termbox-go" ) func ExampleFind() { @@ -69,24 +69,20 @@ func ExampleFindMulti() { } func ExampleTerminalMock() { - keys := func(str string) []termbox.Event { - s := []rune(str) - e := make([]termbox.Event, 0, len(s)) - for _, r := range s { - e = append(e, termbox.Event{Type: termbox.EventKey, Ch: r}) - } - return e - } - // Initialize a mocked terminal. term := fuzzyfinder.UseMockedTerminal() + screen, ok := term.Screen().(tcell.SimulationScreen) + if !ok { + panic("screen type unmatch") + } // Set the window size and events. - term.SetSize(60, 10) - term.SetEvents(append( - keys("foo"), - termbox.Event{Type: termbox.EventKey, Key: termbox.KeyEsc})...) + screen.SetSize(60, 10) + keys := "foo" + for _, r := range keys { + screen.InjectKey(tcell.KeyRune, r, tcell.ModNone) + } + screen.InjectKey(tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone) - // Call fuzzyfinder.Find. slice := []string{"foo", "bar", "baz"} fuzzyfinder.Find(slice, func(i int) string { return slice[i] }) @@ -94,6 +90,6 @@ func ExampleTerminalMock() { // We can test it by the golden files testing pattern. // // See https://speakerdeck.com/mitchellh/advanced-testing-with-go?slide=19 - res := term.GetResult() - ioutil.WriteFile("ui.out", []byte(res), 0644) + result := term.GetResult() + ioutil.WriteFile("ui.out", []byte(result), 0644) } diff --git a/fuzzing_test.go b/fuzzing_test.go index 3f3711a..c64ef42 100644 --- a/fuzzing_test.go +++ b/fuzzing_test.go @@ -1,5 +1,3 @@ -// +build fuzz - package fuzzyfinder_test import ( @@ -11,72 +9,71 @@ import ( "sync" "testing" + "github.com/gdamore/tcell/v2" + fuzz "github.com/google/gofuzz" fuzzyfinder "github.com/ktr0731/go-fuzzyfinder" - "github.com/nsf/termbox-go" ) type fuzzKey struct { - key termbox.Key + key tcell.Key name string } var ( letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789一花二乃三玖四葉五月") - tbkeys = []termbox.Key{ - termbox.KeyCtrlA, - termbox.KeyCtrlB, - termbox.KeyCtrlE, - termbox.KeyCtrlF, - termbox.KeyBackspace, - termbox.KeyTab, - termbox.KeyCtrlJ, - termbox.KeyCtrlK, - termbox.KeyCtrlN, - termbox.KeyCtrlP, - termbox.KeyCtrlU, - termbox.KeyCtrlW, - termbox.KeySpace, - termbox.KeyBackspace2, - termbox.KeyArrowUp, - termbox.KeyArrowDown, - termbox.KeyArrowLeft, - termbox.KeyArrowRight, + tbkeys = []tcell.Key{ + tcell.KeyCtrlA, + tcell.KeyCtrlB, + tcell.KeyCtrlE, + tcell.KeyCtrlF, + tcell.KeyBackspace, + tcell.KeyTab, + tcell.KeyCtrlJ, + tcell.KeyCtrlK, + tcell.KeyCtrlN, + tcell.KeyCtrlP, + tcell.KeyCtrlU, + tcell.KeyCtrlW, + tcell.KeyBackspace2, + tcell.KeyUp, + tcell.KeyDown, + tcell.KeyLeft, + tcell.KeyRight, } - keyMap = map[termbox.Key]string{ - termbox.KeyCtrlA: "A", - termbox.KeyCtrlB: "B", - termbox.KeyCtrlE: "E", - termbox.KeyCtrlF: "F", - termbox.KeyBackspace: "backspace", - termbox.KeyTab: "tab", - termbox.KeyCtrlJ: "J", - termbox.KeyCtrlK: "K", - termbox.KeyCtrlN: "N", - termbox.KeyCtrlP: "P", - termbox.KeyCtrlU: "U", - termbox.KeyCtrlW: "W", - termbox.KeySpace: "space", - termbox.KeyBackspace2: "backspace2", - termbox.KeyArrowUp: "up", - termbox.KeyArrowDown: "down", - termbox.KeyArrowLeft: "left", - termbox.KeyArrowRight: "right", + keyMap = map[tcell.Key]string{ + tcell.KeyCtrlA: "A", + tcell.KeyCtrlB: "B", + tcell.KeyCtrlE: "E", + tcell.KeyCtrlF: "F", + tcell.KeyBackspace: "backspace", + tcell.KeyTab: "tab", + tcell.KeyCtrlJ: "J", + tcell.KeyCtrlK: "K", + tcell.KeyCtrlN: "N", + tcell.KeyCtrlP: "P", + tcell.KeyCtrlU: "U", + tcell.KeyCtrlW: "W", + tcell.KeyBackspace2: "backspace2", + tcell.KeyUp: "up", + tcell.KeyDown: "down", + tcell.KeyLeft: "left", + tcell.KeyRight: "right", } ) var ( out = flag.String("fuzzout", "fuzz.out", "fuzzing error cases") hotReload = flag.Bool("hotreload", false, "enable hot-reloading") - numCases = flag.Int("numCases", 30, "number of test cases") - numEvents = flag.Int("numEvents", 100, "number of events") + numCases = flag.Int("numCases", 10, "number of test cases") + numEvents = flag.Int("numEvents", 10, "number of events") ) // TestFuzz executes fuzzing tests. // // Example: // -// go test -tags fuzz -run TestFuzz -numCases 1000 -numEvents 100 +// go test -tags fuzz -run TestFuzz -numCases 10 -numEvents 10 // func TestFuzz(t *testing.T) { f, err := os.Create(*out) @@ -87,25 +84,32 @@ func TestFuzz(t *testing.T) { fuzz := fuzz.New() - for i := 0; i < rand.Intn(*numCases)+10; i++ { - n := rand.Intn(*numEvents) + 10 - events := make([]termbox.Event, n) + min := func(a, b int) int { + if a < b { + return a + } + return b + } + + // number of events in tcell.SimulationScreen is limited 10 + for i := 0; i < rand.Intn(min(*numCases, 10)); i++ { + n := rand.Intn(min(*numEvents, 10)) + events := make([]tcell.Event, n) for i := 0; i < n; i++ { if rand.Intn(10) > 3 { events[i] = ch(letters[rand.Intn(len(letters)-1)]) } else { - events[i] = key(tbkeys[rand.Intn(len(tbkeys)-1)]) + k := tbkeys[rand.Intn(len(tbkeys)-1)] + events[i] = key(input{k, rune(k), tcell.ModNone}) } } var name string for _, e := range events { - if e.Key == termbox.KeySpace { - name += " " - } else if e.Ch != 0 { - name += string(e.Ch) + if e.(*tcell.EventKey).Rune() != 0 { + name += string(e.(*tcell.EventKey).Rune()) } else { - name += "[" + keyMap[e.Key] + "]" + name += "[" + keyMap[e.(*tcell.EventKey).Key()] + "]" } } @@ -122,7 +126,7 @@ func TestFuzz(t *testing.T) { tracks := tracks f, term := fuzzyfinder.NewWithMockedTerminal() - events = append(events, key(termbox.KeyEsc)) + events = append(events, key(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone})) term.SetEvents(events...) var ( diff --git a/fuzzyfinder_test.go b/fuzzyfinder_test.go index 801fdc3..feedff9 100644 --- a/fuzzyfinder_test.go +++ b/fuzzyfinder_test.go @@ -11,9 +11,9 @@ import ( "testing" "time" + "github.com/gdamore/tcell/v2" "github.com/google/go-cmp/cmp" fuzzyfinder "github.com/ktr0731/go-fuzzyfinder" - "github.com/nsf/termbox-go" ) var ( @@ -113,35 +113,80 @@ func TestReal(t *testing.T) { func TestFind(t *testing.T) { cases := map[string]struct { - events []termbox.Event + events []tcell.Event }{ - "initial": {}, - "input lo": {runes("lo")}, - "input glow": {runes("glow")}, - "arrow up-down": {keys(termbox.KeyArrowUp, termbox.KeyArrowUp, termbox.KeyArrowDown)}, - "arrow left-right": {append(runes("ゆるふわ樹海ガール"), keys(termbox.KeyArrowLeft, termbox.KeyArrowLeft, termbox.KeyArrowLeft, termbox.KeyArrowRight)...)}, - "backspace": {append(runes("adrenaline!!! -TV Ver.-"), keys(termbox.KeyBackspace, termbox.KeyBackspace)...)}, - "backspace empty": {keys(termbox.KeyBackspace2, termbox.KeyBackspace2)}, - "backspace2": {append(runes("オレンジ"), keys(termbox.KeyBackspace2, termbox.KeyBackspace2)...)}, - "delete": {append(runes("オレンジ"), keys(termbox.KeyCtrlA, termbox.KeyDelete)...)}, - "delete empty": {keys(termbox.KeyCtrlA, termbox.KeyDelete)}, - "ctrl-e": {append(runes("恋をしたのは"), keys(termbox.KeyCtrlA, termbox.KeyCtrlE)...)}, - "ctrl-w": {append(runes("ハロ / ハワユ"), key(termbox.KeyCtrlW))}, - "ctrl-w emtpy": {keys(termbox.KeyCtrlW)}, - "ctrl-u": {append(runes("恋をしたのは"), keys(termbox.KeyArrowLeft, termbox.KeyCtrlU, termbox.KeyArrowRight)...)}, - "long item": {keys(termbox.KeyArrowUp, termbox.KeyArrowUp, termbox.KeyArrowUp)}, - "paging": {keys(termbox.KeyArrowUp, termbox.KeyArrowUp, termbox.KeyArrowUp, termbox.KeyArrowUp, termbox.KeyArrowUp, termbox.KeyArrowUp, termbox.KeyArrowUp, termbox.KeyArrowUp)}, - "tab doesn't work": {keys(termbox.KeyTab)}, - "backspace doesnt change x if cursorX is 0": {append(runes("a"), keys(termbox.KeyCtrlA, termbox.KeyBackspace, termbox.KeyCtrlF)...)}, + "initial": {}, + "input lo": {runes("lo")}, + "input glow": {runes("glow")}, + "arrow up-down": {keys([]input{ + {tcell.KeyUp, rune(tcell.KeyUp), tcell.ModNone}, + {tcell.KeyUp, rune(tcell.KeyUp), tcell.ModNone}, + {tcell.KeyDown, rune(tcell.KeyDown), tcell.ModNone}, + }...)}, + "arrow left-right": {append(runes("ゆるふわ樹海"), keys([]input{ + {tcell.KeyLeft, rune(tcell.KeyLeft), tcell.ModNone}, + {tcell.KeyLeft, rune(tcell.KeyLeft), tcell.ModNone}, + {tcell.KeyRight, rune(tcell.KeyRight), tcell.ModNone}, + }...)...)}, + "backspace": {append(runes("adr .-"), keys([]input{ + {tcell.KeyBackspace, rune(tcell.KeyBackspace), tcell.ModNone}, + {tcell.KeyBackspace, rune(tcell.KeyBackspace), tcell.ModNone}, + }...)...)}, + "backspace empty": {keys(input{tcell.KeyBackspace2, rune(tcell.KeyBackspace2), tcell.ModNone})}, + "backspace2": {append(runes("オレンジ"), keys([]input{ + {tcell.KeyBackspace2, rune(tcell.KeyBackspace2), tcell.ModNone}, + {tcell.KeyBackspace2, rune(tcell.KeyBackspace2), tcell.ModNone}, + }...)...)}, + "delete": {append(runes("オレンジ"), keys([]input{ + {tcell.KeyCtrlA, 'A', tcell.ModCtrl}, + {tcell.KeyDelete, rune(tcell.KeyDelete), tcell.ModNone}, + }...)...)}, + "delete empty": {keys([]input{ + {tcell.KeyCtrlA, 'A', tcell.ModCtrl}, + {tcell.KeyDelete, rune(tcell.KeyDelete), tcell.ModNone}, + }...)}, + "ctrl-e": {append(runes("恋をしたのは"), keys([]input{ + {tcell.KeyCtrlA, 'A', tcell.ModCtrl}, + {tcell.KeyCtrlE, 'E', tcell.ModCtrl}, + }...)...)}, + "ctrl-w": {append(runes("ハロ / ハワユ"), keys(input{tcell.KeyCtrlW, 'W', tcell.ModCtrl})...)}, + "ctrl-w empty": {keys(input{tcell.KeyCtrlW, 'W', tcell.ModCtrl})}, + "ctrl-u": {append(runes("恋をしたのは"), keys([]input{ + {tcell.KeyLeft, rune(tcell.KeyLeft), tcell.ModNone}, + {tcell.KeyCtrlU, 'U', tcell.ModCtrl}, + {tcell.KeyRight, rune(tcell.KeyRight), tcell.ModNone}, + }...)...)}, + "long item": {keys([]input{ + {tcell.KeyUp, rune(tcell.KeyUp), tcell.ModNone}, + {tcell.KeyUp, rune(tcell.KeyUp), tcell.ModNone}, + {tcell.KeyUp, rune(tcell.KeyUp), tcell.ModNone}, + }...)}, + "paging": {keys([]input{ + {tcell.KeyUp, rune(tcell.KeyUp), tcell.ModNone}, + {tcell.KeyUp, rune(tcell.KeyUp), tcell.ModNone}, + {tcell.KeyUp, rune(tcell.KeyUp), tcell.ModNone}, + {tcell.KeyUp, rune(tcell.KeyUp), tcell.ModNone}, + {tcell.KeyUp, rune(tcell.KeyUp), tcell.ModNone}, + {tcell.KeyUp, rune(tcell.KeyUp), tcell.ModNone}, + {tcell.KeyUp, rune(tcell.KeyUp), tcell.ModNone}, + {tcell.KeyUp, rune(tcell.KeyUp), tcell.ModNone}, + }...)}, + "tab doesn't work": {keys(input{tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone})}, + "backspace doesnt change x if cursorX is 0": {append(runes("a"), keys([]input{ + {tcell.KeyCtrlA, 'A', tcell.ModCtrl}, + {tcell.KeyBackspace, rune(tcell.KeyBackspace), tcell.ModNone}, + {tcell.KeyCtrlF, 'F', tcell.ModCtrl}, + }...)...)}, } + var seqMutex sync.Mutex for name, c := range cases { + seqMutex.Lock() t.Run(name, func(t *testing.T) { - c := c events := c.events f, term := fuzzyfinder.NewWithMockedTerminal() - events = append(events, key(termbox.KeyEsc)) + events = append(events, key(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone})) term.SetEvents(events...) assertWithGolden(t, func(t *testing.T) string { @@ -162,15 +207,18 @@ func TestFind(t *testing.T) { t.Fatalf("Find must return ErrAbort, but got '%s'", err) } - return term.GetResult() + res := term.GetResult() + term.Screen().Fini() + return res }) }) + seqMutex.Unlock() } } func TestFind_hotReload(t *testing.T) { f, term := fuzzyfinder.NewWithMockedTerminal() - events := append(runes("adrena"), keys(termbox.KeyEsc)...) + events := append(runes("adrena"), keys(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone})...) term.SetEvents(events...) var mu sync.Mutex @@ -199,16 +247,18 @@ func TestFind_hotReload(t *testing.T) { t.Fatalf("Find must return ErrAbort, but got '%s'", err) } - return term.GetResult() + res := term.GetResult() + term.Screen().Fini() + return res }) } func TestFind_enter(t *testing.T) { cases := map[string]struct { - events []termbox.Event + events []tcell.Event expected int }{ - "initial": {events: keys(termbox.KeyTab), expected: 0}, + "initial": {events: keys(input{tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}), expected: 0}, "mode smart to case-sensitive": {events: runes("CHI"), expected: 7}, } @@ -218,7 +268,7 @@ func TestFind_enter(t *testing.T) { events := c.events f, term := fuzzyfinder.NewWithMockedTerminal() - events = append(events, key(termbox.KeyEnter)) + events = append(events, key(input{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone})) term.SetEvents(events...) idx, err := f.Find( @@ -257,16 +307,29 @@ func TestFind_error(t *testing.T) { func TestFindMulti(t *testing.T) { cases := map[string]struct { - events []termbox.Event + events []tcell.Event expected []int abort bool }{ - "input glow": {events: runes("glow"), expected: []int{0}}, - "select two items": {events: keys(termbox.KeyTab, termbox.KeyArrowUp, termbox.KeyTab), expected: []int{0, 1}}, - "select two items with another order": {events: keys(termbox.KeyArrowUp, termbox.KeyTab, termbox.KeyTab), expected: []int{1, 0}}, - "toggle": {events: keys(termbox.KeyTab, termbox.KeyTab), expected: []int{0}}, - "empty result": {events: append(runes("ffffffffffffff")), abort: true}, - "resize window": {events: []termbox.Event{termbox.Event{Type: termbox.EventResize}}, expected: []int{0}}, + "input glow": {events: runes("glow"), expected: []int{0}}, + "select two items": {events: keys([]input{ + {tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, + {tcell.KeyUp, rune(tcell.KeyUp), tcell.ModNone}, + {tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, + }...), expected: []int{0, 1}}, + "select two items with another order": {events: keys([]input{ + {tcell.KeyUp, rune(tcell.KeyUp), tcell.ModNone}, + {tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, + {tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, + }...), expected: []int{1, 0}}, + "toggle": {events: keys([]input{ + {tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, + {tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, + }...), expected: []int{0}}, + "empty result": {events: append(runes("fffffff")), abort: true}, + "resize window": {events: []tcell.Event{ + tcell.NewEventResize(10, 10), + }, expected: []int{0}}, } for name, c := range cases { t.Run(name, func(t *testing.T) { @@ -274,7 +337,7 @@ func TestFindMulti(t *testing.T) { events := c.events f, term := fuzzyfinder.NewWithMockedTerminal() - events = append(events, key(termbox.KeyEnter)) + events = append(events, key(input{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone})) term.SetEvents(events...) idxs, err := f.FindMulti( @@ -312,7 +375,7 @@ func BenchmarkFind(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { f, term := fuzzyfinder.NewWithMockedTerminal() - term.SetEvents(append(runes("adrele!!"), key(termbox.KeyEsc))...) + term.SetEvents(append(runes("adrele!!"), key(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone}))...) f.Find( tracks, func(i int) string { @@ -333,7 +396,7 @@ func BenchmarkFind(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { f, term := fuzzyfinder.NewWithMockedTerminal() - term.SetEvents(append(runes("adrele!!"), key(termbox.KeyEsc))...) + term.SetEvents(append(runes("adrele!!"), key(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone}))...) f.Find( &tracks, func(i int) string { @@ -351,36 +414,33 @@ func BenchmarkFind(b *testing.B) { }) } -func runes(s string) []termbox.Event { +func runes(s string) []tcell.Event { r := []rune(s) - e := make([]termbox.Event, 0, len(r)) + e := make([]tcell.Event, 0, len(r)) for _, r := range r { e = append(e, ch(r)) } return e } -func ch(r rune) termbox.Event { - if r == ' ' { - return key(termbox.KeySpace) - } - return termbox.Event{ - Type: termbox.EventKey, - Ch: r, - } +func ch(r rune) tcell.Event { + return key(input{tcell.KeyRune, r, tcell.ModNone}) } -func key(key termbox.Key) termbox.Event { - return termbox.Event{ - Type: termbox.EventKey, - Key: key, - } +func key(input input) tcell.Event { + return tcell.NewEventKey(input.key, input.ch, input.mod) } -func keys(keys ...termbox.Key) []termbox.Event { - k := make([]termbox.Event, 0, len(keys)) - for _, _key := range keys { - k = append(k, key(_key)) +func keys(inputs ...input) []tcell.Event { + k := make([]tcell.Event, 0, len(inputs)) + for _, in := range inputs { + k = append(k, key(in)) } return k } + +type input struct { + key tcell.Key + ch rune + mod tcell.ModMask +} diff --git a/go.mod b/go.mod index e623957..51d5c6c 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/mattn/go-runewidth v0.0.9 github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1 github.com/pkg/errors v0.9.1 + golang.org/x/tools v0.0.0-20201125231158-b5590deeca9b // indirect ) go 1.13 diff --git a/go.sum b/go.sum index 21982e1..90c1777 100644 --- a/go.sum +++ b/go.sum @@ -15,9 +15,38 @@ github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1 h1:lh3PyZvY+B9nFliS github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +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.0.0-20201125231158-b5590deeca9b h1:Lq5JUTFhiybGVf28jB6QRpqd13/JPOaCnET17PVzYJE= +golang.org/x/tools v0.0.0-20201125231158-b5590deeca9b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/helper_test.go b/helper_test.go index 82563f4..a392ca7 100644 --- a/helper_test.go +++ b/helper_test.go @@ -1,6 +1,8 @@ package fuzzyfinder -import "time" +import ( + "github.com/gdamore/tcell/v2" +) func New() *finder { return &finder{} @@ -10,7 +12,6 @@ func NewWithMockedTerminal() (*finder, *TerminalMock) { f := New() m := f.UseMockedTerminal() w, h := 60, 10 // A normally value. - m.SetSize(w, h) - m.sleepDuration = 500 * time.Microsecond + m.Screen().(tcell.SimulationScreen).SetSize(w, h) return f, m } diff --git a/mock.go b/mock.go index 86de3c6..54f6f4f 100644 --- a/mock.go +++ b/mock.go @@ -4,189 +4,103 @@ import ( "bytes" "fmt" "sync" - "time" + "github.com/gdamore/tcell/v2" runewidth "github.com/mattn/go-runewidth" - "github.com/nsf/termbox-go" ) -type cell struct { - ch rune - bg, fg termbox.Attribute -} - // TerminalMock is a mocked terminal for testing. // Most users should use it by calling UseMockedTerminal. type TerminalMock struct { - sizeMu sync.RWMutex - width, height int - - eventsMu sync.Mutex - events []termbox.Event - - cellsMu sync.RWMutex - cells []*cell - + screen tcell.SimulationScreen resultMu sync.RWMutex result string - - sleepDuration time.Duration -} - -// SetSize changes the pseudo-size of the window. -// Note that SetSize resets added cells. -func (m *TerminalMock) SetSize(w, h int) { - m.sizeMu.Lock() - defer m.sizeMu.Unlock() - m.cellsMu.Lock() - defer m.cellsMu.Unlock() - m.width = w - m.height = h - m.cells = make([]*cell, w*h) -} - -// SetEvents sets all events, which are fetched by pollEvent. -// A user of this must set the EscKey event at the end. -func (m *TerminalMock) SetEvents(e ...termbox.Event) { - m.eventsMu.Lock() - defer m.eventsMu.Unlock() - m.events = e } -// GetResult returns a flushed string that is displayed to the actual terminal. -// It contains all escape sequences such that ANSI escape code. -func (m *TerminalMock) GetResult() string { - m.resultMu.RLock() - defer m.resultMu.RUnlock() - return m.result -} - -func (m *TerminalMock) init() error { - return nil -} - -func (m *TerminalMock) size() (width int, height int) { - m.sizeMu.RLock() - defer m.sizeMu.RUnlock() - return m.width, m.height -} - -func (m *TerminalMock) clear(fg termbox.Attribute, bg termbox.Attribute) error { - // TODO - return nil +// UseMockedTerminal switches the terminal, which is used from +// this package to a mocked one. +func UseMockedTerminal() *TerminalMock { + return defaultFinder.UseMockedTerminal() } -func (m *TerminalMock) setCell(x int, y int, ch rune, fg termbox.Attribute, bg termbox.Attribute) { - m.sizeMu.RLock() - defer m.sizeMu.RUnlock() - m.cellsMu.Lock() - defer m.cellsMu.Unlock() - - if x < 0 || x >= m.width { - return - } - if y < 0 || y >= m.height { - return - } - m.cells[y*m.width+x] = &cell{ch: ch, fg: fg, bg: bg} +func (t *TerminalMock) Screen() tcell.Screen { + return t.screen } -func (m *TerminalMock) setCursor(x int, y int) { - m.sizeMu.RLock() - defer m.sizeMu.RUnlock() - m.cellsMu.Lock() - defer m.cellsMu.Unlock() - if x < 0 || x >= m.width { - return - } - if y < 0 || y >= m.height { - return - } - i := y*m.width + x - if m.cells[i] == nil { - m.cells[y*m.width+x] = &cell{ch: '\u2588', fg: termbox.ColorWhite, bg: termbox.ColorDefault} - } else { - // Cursor on a rune. - m.cells[y*m.width+x].bg = termbox.ColorWhite - } -} +func (t *TerminalMock) GetResult() string { + var s string -func (m *TerminalMock) pollEvent() termbox.Event { - m.eventsMu.Lock() - defer m.eventsMu.Unlock() - if len(m.events) == 0 { - panic("pollEvent called with empty events. have you set expected events by SetEvents?") + // set cursor for snapshot test + setCursor := func() { + cursorX, cursorY, _ := t.screen.GetCursor() + mainc, _, _, _ := t.screen.GetContent(cursorX, cursorY) + if mainc == ' ' { + t.screen.SetContent(cursorX, cursorY, '\u2588', nil, tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorDefault)) + } else { + t.screen.SetContent(cursorX, cursorY, mainc, nil, tcell.StyleDefault.Background(tcell.ColorWhite)) + } + t.screen.Show() } - e := m.events[0] - m.events = m.events[1:] - // Wait a moment for goroutine scheduling. - time.Sleep(m.sleepDuration) - return e -} -// flush displays all items with formatted layout. -func (m *TerminalMock) flush() error { - m.cellsMu.RLock() - - var s string - for j := 0; j < m.height; j++ { - prevFg, prevBg := termbox.ColorDefault, termbox.ColorDefault - for i := 0; i < m.width; i++ { - c := m.cells[j*m.width+i] - if c == nil { - s += " " - prevFg, prevBg = termbox.ColorDefault, termbox.ColorDefault - continue - } else { - var fgReset bool - if c.fg != prevFg { + setCursor() + + cells, width, height := t.screen.GetContents() + + for h := 0; h < height; h++ { + prevFg, prevBg := tcell.ColorDefault, tcell.ColorDefault + for w := 0; w < width; w++ { + cell := cells[h*width+w] + fg, bg, attr := cell.Style.Decompose() + var fgReset bool + if fg != prevFg { + s += "\x1b\x5b\x6d" // Reset previous color. + s += parseAttr(&fg, nil, attr) + prevFg = fg + prevBg = tcell.ColorDefault + fgReset = true + } + if bg != prevBg { + if !fgReset { s += "\x1b\x5b\x6d" // Reset previous color. - s += parseAttr(c.fg, true) - prevFg = c.fg - prevBg = termbox.ColorDefault - fgReset = true - } - if c.bg != prevBg { - if !fgReset { - s += "\x1b\x5b\x6d" // Reset previous color. - prevFg = termbox.ColorDefault - } - s += parseAttr(c.bg, false) - prevBg = c.bg - } - s += string(c.ch) - rw := runewidth.RuneWidth(c.ch) - if rw != 0 { - i += rw - 1 + prevFg = tcell.ColorDefault } + s += parseAttr(nil, &bg, attr) + prevBg = bg + } + s += string(cell.Runes[:]) + rw := runewidth.RuneWidth(cell.Runes[0]) + if rw != 0 { + w += rw - 1 } } s += "\n" } s += "\x1b\x5b\x6d" // Reset previous color. - - m.cellsMu.RUnlock() - m.cellsMu.Lock() - m.cells = make([]*cell, m.width*m.height) - m.cellsMu.Unlock() - - m.resultMu.Lock() - defer m.resultMu.Unlock() - m.result = s - - return nil + return s } -func (m *TerminalMock) close() {} - -// UseMockedTerminal switches the terminal, which is used from -// this package to a mocked one. -func UseMockedTerminal() *TerminalMock { - return defaultFinder.UseMockedTerminal() +func (t *TerminalMock) SetEvents(events ...tcell.Event) { + for _, event := range events { + switch event.(type) { + case *tcell.EventKey: + ek := event.(*tcell.EventKey) + t.screen.InjectKey(ek.Key(), ek.Rune(), ek.Modifiers()) + case *tcell.EventResize: + er := event.(*tcell.EventResize) + w, h := er.Size() + t.screen.SetSize(w, h) + } + } } func (f *finder) UseMockedTerminal() *TerminalMock { - m := &TerminalMock{} + screen := tcell.NewSimulationScreen("UTF-8") + if err := screen.Init(); err != nil { + panic(err) + } + m := &TerminalMock{ + screen: screen, + } f.term = m return m } @@ -194,41 +108,59 @@ func (f *finder) UseMockedTerminal() *TerminalMock { // parseAttr parses an attribute of termbox // as an escape sequence. // parseAttr doesn't support output modes othar than color256 in termbox-go. -func parseAttr(attr termbox.Attribute, isFg bool) string { +func parseAttr(fg, bg *tcell.Color, attr tcell.AttrMask) string { + if attr == tcell.AttrInvalid { + panic(fmt.Sprintf("invalid color code: %d", attr)) + } + var buf bytes.Buffer + buf.WriteString("\x1b[") - if attr >= termbox.AttrReverse { - buf.WriteString("7;") - attr -= termbox.AttrReverse - } - if attr >= termbox.AttrUnderline { - buf.WriteString("4;") - attr -= termbox.AttrUnderline - } - if attr >= termbox.AttrBold { - buf.WriteString("1;") - attr -= termbox.AttrBold + parseAttrMask := func() { + if attr >= tcell.AttrUnderline { + buf.WriteString("4;") + attr -= tcell.AttrUnderline + } + if attr >= tcell.AttrReverse { + buf.WriteString("7;") + attr -= tcell.AttrReverse + } + if attr >= tcell.AttrBold { + buf.WriteString("1;") + attr -= tcell.AttrBold + } } - if attr > termbox.ColorWhite { - panic(fmt.Sprintf("invalid color code: %d", attr)) - } + if fg != nil || bg != nil { + isFg := fg != nil && bg == nil - if attr == termbox.ColorDefault { if isFg { - buf.WriteString("39") - } else { - buf.WriteString("49") - } - } else { - color := int(attr) - 1 - if isFg { - fmt.Fprintf(&buf, "38;5;%d", color) + parseAttrMask() + if *fg == tcell.ColorDefault { + buf.WriteString("39") + } else { + fmt.Fprintf(&buf, "38;5;%d", toAnsi3bit(*fg)) + } } else { - fmt.Fprintf(&buf, "48;5;%d", color) + if *bg == tcell.ColorDefault { + buf.WriteString("49") + } else { + fmt.Fprintf(&buf, "48;5;%d", toAnsi3bit(*bg)) + } } + buf.WriteString("m") } - buf.WriteString("m") - return buf.String() } + +func toAnsi3bit(color tcell.Color) int { + colors := []tcell.Color{ + tcell.ColorBlack, tcell.ColorRed, tcell.ColorGreen, tcell.ColorYellow, tcell.ColorBlue, tcell.ColorDarkMagenta, tcell.ColorDarkCyan, tcell.ColorWhite, + } + for i, c := range colors { + if c == color { + return i + } + } + return 0 +} diff --git a/mock_test.go b/mock_test.go index 2332bc1..4421db0 100644 --- a/mock_test.go +++ b/mock_test.go @@ -3,44 +3,49 @@ package fuzzyfinder import ( "testing" + "github.com/gdamore/tcell/v2" "github.com/google/go-cmp/cmp" - "github.com/nsf/termbox-go" ) func Test_parseAttr(t *testing.T) { cases := map[string]struct { - attr termbox.Attribute - isFg bool + attr tcell.AttrMask + fg tcell.Color + bg tcell.Color + isBg bool expected string willPanic bool }{ "ColorDefault": { - attr: termbox.ColorDefault, - isFg: true, + fg: tcell.ColorDefault, expected: "\x1b[39m", }, "ColorDefault bg": { - attr: termbox.ColorDefault, + bg: tcell.ColorDefault, + isBg: true, expected: "\x1b[49m", }, "ColorGreen": { - attr: termbox.ColorGreen, - expected: "\x1b[48;5;2m", + fg: tcell.ColorGreen, + expected: "\x1b[38;5;2m", }, "ColorGreen with bold": { - attr: termbox.ColorGreen | termbox.AttrBold, - expected: "\x1b[1;48;5;2m", + attr: tcell.AttrBold, + fg: tcell.ColorGreen, + expected: "\x1b[1;38;5;2m", }, "ColorGreen with bold and underline": { - attr: termbox.ColorGreen | termbox.AttrBold | termbox.AttrUnderline, - expected: "\x1b[4;1;48;5;2m", + attr: tcell.AttrBold | tcell.AttrUnderline, + fg: tcell.ColorGreen, + expected: "\x1b[4;1;38;5;2m", }, "ColorGreen with reverse": { - attr: termbox.ColorGreen | termbox.AttrReverse, - expected: "\x1b[7;48;5;2m", + attr: tcell.AttrReverse, + fg: tcell.ColorGreen, + expected: "\x1b[7;38;5;2m", }, "invalid color": { - attr: termbox.ColorWhite + 1, + attr: tcell.AttrInvalid, willPanic: true, }, } @@ -55,7 +60,12 @@ func Test_parseAttr(t *testing.T) { } }() } - actual := parseAttr(c.attr, c.isFg) + var actual string + if c.isBg { + actual = parseAttr(nil, &c.bg, c.attr) + } else { + actual = parseAttr(&c.fg, nil, c.attr) + } if diff := cmp.Diff(c.expected, actual); diff != "" { t.Errorf("diff found: \n%s\nexpected = %x, actual = %x", diff, c.expected, actual) } From 825a74ea2f8ac614520d2e137ab9a0df231b104c Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Mon, 30 Nov 2020 22:48:36 +0900 Subject: [PATCH 05/26] update golden files --- testdata/fixtures/testfind-arrow_left-right.golden | 4 ++-- testdata/fixtures/testfind-arrow_up-down.golden | 6 +++--- testdata/fixtures/testfind-backspace.golden | 4 ++-- testdata/fixtures/testfind-backspace2.golden | 4 ++-- ...estfind-backspace_doesnt_change_x_if_cursorx_is_0.golden | 6 +++--- testdata/fixtures/testfind-backspace_empty.golden | 6 +++--- testdata/fixtures/testfind-ctrl-e.golden | 4 ++-- testdata/fixtures/testfind-ctrl-u.golden | 4 ++-- testdata/fixtures/testfind-ctrl-w.golden | 4 ++-- ...ind-ctrl-w_emtpy.golden => testfind-ctrl-w_empty.golden} | 6 +++--- testdata/fixtures/testfind-delete.golden | 4 ++-- testdata/fixtures/testfind-delete_empty.golden | 6 +++--- testdata/fixtures/testfind-initial.golden | 6 +++--- testdata/fixtures/testfind-input_glow.golden | 6 +++--- testdata/fixtures/testfind-input_lo.golden | 6 +++--- testdata/fixtures/testfind-long_item.golden | 6 +++--- testdata/fixtures/testfind-paging.golden | 6 +++--- testdata/fixtures/testfind-tab_doesnt_work.golden | 6 +++--- testdata/fixtures/testfind_hotreload.golden | 6 +++--- 19 files changed, 50 insertions(+), 50 deletions(-) rename testdata/fixtures/{testfind-ctrl-w_emtpy.golden => testfind-ctrl-w_empty.golden} (67%) diff --git a/testdata/fixtures/testfind-arrow_left-right.golden b/testdata/fixtures/testfind-arrow_left-right.golden index 331a64f..e67a53d 100644 --- a/testdata/fixtures/testfind-arrow_left-right.golden +++ b/testdata/fixtures/testfind-arrow_left-right.golden @@ -6,6 +6,6 @@ │ │ │ │ │ │ - 0/9 │ │ -> ゆるふわ樹海ガール └────────────────────────────┘ + 0/9 │ │ +> ゆるふわ樹海 └────────────────────────────┘  \ No newline at end of file diff --git a/testdata/fixtures/testfind-arrow_up-down.golden b/testdata/fixtures/testfind-arrow_up-down.golden index 965ef8b..0c9b9a6 100644 --- a/testdata/fixtures/testfind-arrow_up-down.golden +++ b/testdata/fixtures/testfind-arrow_up-down.golden @@ -4,8 +4,8 @@ closing │ │ ソラニン │ │ adrenaline!!! │ │ -> ヒトリノ夜 │ │ +> ヒトリノ夜 │ │ あの日自分が出て行ってや.. │ │ - 9/9 │ │ -> █ └────────────────────────────┘ + 9/9 │ │ +> █ └────────────────────────────┘  \ No newline at end of file diff --git a/testdata/fixtures/testfind-backspace.golden b/testdata/fixtures/testfind-backspace.golden index 2f0f518..2fad9ba 100644 --- a/testdata/fixtures/testfind-backspace.golden +++ b/testdata/fixtures/testfind-backspace.golden @@ -6,6 +6,6 @@ │ │ │ │ │ │ - 0/9 │ │ -> adrenaline!!! -TV Ver█ └────────────────────────────┘ + 0/9 │ │ +> adr █ └────────────────────────────┘  \ No newline at end of file diff --git a/testdata/fixtures/testfind-backspace2.golden b/testdata/fixtures/testfind-backspace2.golden index 1adfcbc..8ed188b 100644 --- a/testdata/fixtures/testfind-backspace2.golden +++ b/testdata/fixtures/testfind-backspace2.golden @@ -6,6 +6,6 @@ │ │ │ │ │ │ - 0/9 │ │ -> オレ█ └────────────────────────────┘ + 0/9 │ │ +> オレ█ └────────────────────────────┘  \ No newline at end of file diff --git a/testdata/fixtures/testfind-backspace_doesnt_change_x_if_cursorx_is_0.golden b/testdata/fixtures/testfind-backspace_doesnt_change_x_if_cursorx_is_0.golden index ac08c85..a3a49af 100644 --- a/testdata/fixtures/testfind-backspace_doesnt_change_x_if_cursorx_is_0.golden +++ b/testdata/fixtures/testfind-backspace_doesnt_change_x_if_cursorx_is_0.golden @@ -5,7 +5,7 @@ │ │ │ │ Catch the Moment │ │ -> adrenaline!!! │ │ - 2/9 │ │ -> a█ └────────────────────────────┘ +> adrenaline!!! │ │ + 2/9 │ │ +> a█ └────────────────────────────┘  \ No newline at end of file diff --git a/testdata/fixtures/testfind-backspace_empty.golden b/testdata/fixtures/testfind-backspace_empty.golden index 3548920..f386d2d 100644 --- a/testdata/fixtures/testfind-backspace_empty.golden +++ b/testdata/fixtures/testfind-backspace_empty.golden @@ -5,7 +5,7 @@ ソラニン │ │ adrenaline!!! │ │ ヒトリノ夜 │ │ -> あの日自分が出て行ってや.. │ │ - 9/9 │ │ -> █ └────────────────────────────┘ +> あの日自分が出て行ってや.. │ │ + 9/9 │ │ +> █ └────────────────────────────┘  \ No newline at end of file diff --git a/testdata/fixtures/testfind-ctrl-e.golden b/testdata/fixtures/testfind-ctrl-e.golden index f07e550..32d933b 100644 --- a/testdata/fixtures/testfind-ctrl-e.golden +++ b/testdata/fixtures/testfind-ctrl-e.golden @@ -6,6 +6,6 @@ │ │ │ │ │ │ - 0/9 │ │ -> 恋をしたのは█ └────────────────────────────┘ + 0/9 │ │ +> 恋をしたのは█ └────────────────────────────┘  \ No newline at end of file diff --git a/testdata/fixtures/testfind-ctrl-u.golden b/testdata/fixtures/testfind-ctrl-u.golden index 4b90bab..4187243 100644 --- a/testdata/fixtures/testfind-ctrl-u.golden +++ b/testdata/fixtures/testfind-ctrl-u.golden @@ -6,6 +6,6 @@ │ │ │ │ │ │ - 0/9 │ │ -> は█ └────────────────────────────┘ + 0/9 │ │ +> は█ └────────────────────────────┘  \ No newline at end of file diff --git a/testdata/fixtures/testfind-ctrl-w.golden b/testdata/fixtures/testfind-ctrl-w.golden index bbd9d93..467692b 100644 --- a/testdata/fixtures/testfind-ctrl-w.golden +++ b/testdata/fixtures/testfind-ctrl-w.golden @@ -6,6 +6,6 @@ │ │ │ │ │ │ - 0/9 │ │ -> ハロ / █ └────────────────────────────┘ + 0/9 │ │ +> ハロ / █ └────────────────────────────┘  \ No newline at end of file diff --git a/testdata/fixtures/testfind-ctrl-w_emtpy.golden b/testdata/fixtures/testfind-ctrl-w_empty.golden similarity index 67% rename from testdata/fixtures/testfind-ctrl-w_emtpy.golden rename to testdata/fixtures/testfind-ctrl-w_empty.golden index 3548920..f386d2d 100644 --- a/testdata/fixtures/testfind-ctrl-w_emtpy.golden +++ b/testdata/fixtures/testfind-ctrl-w_empty.golden @@ -5,7 +5,7 @@ ソラニン │ │ adrenaline!!! │ │ ヒトリノ夜 │ │ -> あの日自分が出て行ってや.. │ │ - 9/9 │ │ -> █ └────────────────────────────┘ +> あの日自分が出て行ってや.. │ │ + 9/9 │ │ +> █ └────────────────────────────┘  \ No newline at end of file diff --git a/testdata/fixtures/testfind-delete.golden b/testdata/fixtures/testfind-delete.golden index cc46143..81c3d08 100644 --- a/testdata/fixtures/testfind-delete.golden +++ b/testdata/fixtures/testfind-delete.golden @@ -6,6 +6,6 @@ │ │ │ │ │ │ - 0/9 │ │ -> レンジ └────────────────────────────┘ + 0/9 │ │ +> レンジ └────────────────────────────┘  \ No newline at end of file diff --git a/testdata/fixtures/testfind-delete_empty.golden b/testdata/fixtures/testfind-delete_empty.golden index 3548920..f386d2d 100644 --- a/testdata/fixtures/testfind-delete_empty.golden +++ b/testdata/fixtures/testfind-delete_empty.golden @@ -5,7 +5,7 @@ ソラニン │ │ adrenaline!!! │ │ ヒトリノ夜 │ │ -> あの日自分が出て行ってや.. │ │ - 9/9 │ │ -> █ └────────────────────────────┘ +> あの日自分が出て行ってや.. │ │ + 9/9 │ │ +> █ └────────────────────────────┘  \ No newline at end of file diff --git a/testdata/fixtures/testfind-initial.golden b/testdata/fixtures/testfind-initial.golden index 3548920..f386d2d 100644 --- a/testdata/fixtures/testfind-initial.golden +++ b/testdata/fixtures/testfind-initial.golden @@ -5,7 +5,7 @@ ソラニン │ │ adrenaline!!! │ │ ヒトリノ夜 │ │ -> あの日自分が出て行ってや.. │ │ - 9/9 │ │ -> █ └────────────────────────────┘ +> あの日自分が出て行ってや.. │ │ + 9/9 │ │ +> █ └────────────────────────────┘  \ No newline at end of file diff --git a/testdata/fixtures/testfind-input_glow.golden b/testdata/fixtures/testfind-input_glow.golden index 643ac03..7f08c5b 100644 --- a/testdata/fixtures/testfind-input_glow.golden +++ b/testdata/fixtures/testfind-input_glow.golden @@ -5,7 +5,7 @@ │ │ │ │ │ │ -> glow │ │ - 1/9 │ │ -> glow█ └────────────────────────────┘ +> glow │ │ + 1/9 │ │ +> glow█ └────────────────────────────┘  \ No newline at end of file diff --git a/testdata/fixtures/testfind-input_lo.golden b/testdata/fixtures/testfind-input_lo.golden index 4f39df8..0a5c647 100644 --- a/testdata/fixtures/testfind-input_lo.golden +++ b/testdata/fixtures/testfind-input_lo.golden @@ -5,7 +5,7 @@ │ │ │ │ closing │ │ -> glow │ │ - 2/9 │ │ -> lo█ └────────────────────────────┘ +> glow │ │ + 2/9 │ │ +> lo█ └────────────────────────────┘  \ No newline at end of file diff --git a/testdata/fixtures/testfind-long_item.golden b/testdata/fixtures/testfind-long_item.golden index b84ccdc..b31ab20 100644 --- a/testdata/fixtures/testfind-long_item.golden +++ b/testdata/fixtures/testfind-long_item.golden @@ -2,10 +2,10 @@ メーベル │ Name: ソラニン │ glow │ Artist: ASIAN KUNG-FU GEN..│ closing │ │ -> ソラニン │ │ +> ソラニン │ │ adrenaline!!! │ │ ヒトリノ夜 │ │ あの日自分が出て行ってや.. │ │ - 9/9 │ │ -> █ └────────────────────────────┘ + 9/9 │ │ +> █ └────────────────────────────┘  \ No newline at end of file diff --git a/testdata/fixtures/testfind-paging.golden b/testdata/fixtures/testfind-paging.golden index 3807efd..a2ae44c 100644 --- a/testdata/fixtures/testfind-paging.golden +++ b/testdata/fixtures/testfind-paging.golden @@ -1,4 +1,4 @@ -> Catch the Moment ┌────────────────────────────┐ +> Catch the Moment ┌────────────────────────────┐ ICHIDAIJI │ Name: Catch the Moment │ メーベル │ Artist: LiSA │ glow │ │ @@ -6,6 +6,6 @@ ソラニン │ │ adrenaline!!! │ │ ヒトリノ夜 │ │ - 9/9 │ │ -> █ └────────────────────────────┘ + 9/9 │ │ +> █ └────────────────────────────┘  \ No newline at end of file diff --git a/testdata/fixtures/testfind-tab_doesnt_work.golden b/testdata/fixtures/testfind-tab_doesnt_work.golden index 3548920..f386d2d 100644 --- a/testdata/fixtures/testfind-tab_doesnt_work.golden +++ b/testdata/fixtures/testfind-tab_doesnt_work.golden @@ -5,7 +5,7 @@ ソラニン │ │ adrenaline!!! │ │ ヒトリノ夜 │ │ -> あの日自分が出て行ってや.. │ │ - 9/9 │ │ -> █ └────────────────────────────┘ +> あの日自分が出て行ってや.. │ │ + 9/9 │ │ +> █ └────────────────────────────┘  \ No newline at end of file diff --git a/testdata/fixtures/testfind_hotreload.golden b/testdata/fixtures/testfind_hotreload.golden index e727483..4a567d3 100644 --- a/testdata/fixtures/testfind_hotreload.golden +++ b/testdata/fixtures/testfind_hotreload.golden @@ -5,7 +5,7 @@ │ │ │ │ │ │ -> adrenaline!!! │ │ - 1/9 │ │ -> adrena█ └────────────────────────────┘ +> adrenaline!!! │ │ + 1/9 │ │ +> adrena█ └────────────────────────────┘  \ No newline at end of file From 7e24714ce87163f35d02355ce13e651794b71a7d Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Tue, 1 Dec 2020 03:52:34 +0900 Subject: [PATCH 06/26] call Fini if not in test --- fuzzyfinder.go | 71 +++++++++++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/fuzzyfinder.go b/fuzzyfinder.go index 6ea48a1..7484165 100644 --- a/fuzzyfinder.go +++ b/fuzzyfinder.go @@ -78,7 +78,7 @@ func (f *finder) initFinder(items []string, matched []matching.Matched, opt opt) screen: screen, } if err := f.term.Screen().Init(); err != nil { - return errors.Wrap(err, "failed to initialize termbox") + return errors.Wrap(err, "failed to initialize screen") } } @@ -125,33 +125,35 @@ func (f *finder) _draw() { // prompt line var promptLinePad int - style := tcell.StyleDefault. - Foreground(tcell.ColorBlue). - Background(tcell.ColorDefault) for _, r := range []rune(f.opt.promptString) { + style := tcell.StyleDefault. + Foreground(tcell.ColorBlue). + Background(tcell.ColorDefault) + f.term.Screen().SetContent(promptLinePad, height-1, r, nil, style) promptLinePad++ } var r rune var w int - style = tcell.StyleDefault. - Foreground(tcell.ColorDefault). - Background(tcell.ColorDefault). - Bold(true) for _, r = range f.state.input { + style := tcell.StyleDefault. + Foreground(tcell.ColorDefault). + Background(tcell.ColorDefault). + Bold(true) + // Add a space between '>' and runes. f.term.Screen().SetContent(promptLinePad+w, height-1, r, nil, style) w += runewidth.RuneWidth(r) } f.term.Screen().ShowCursor(promptLinePad+f.state.cursorX, height-1) - style = tcell.StyleDefault. - Foreground(tcell.ColorYellow). - Background(tcell.ColorDefault) - // Number line for i, r := range fmt.Sprintf("%d/%d", len(f.state.matched), len(f.state.items)) { + style := tcell.StyleDefault. + Foreground(tcell.ColorYellow). + Background(tcell.ColorDefault) + f.term.Screen().SetContent(2+i, height-2, r, nil, style) } @@ -168,7 +170,7 @@ func (f *finder) _draw() { break } if i == f.state.cursorY { - style = tcell.StyleDefault. + style := tcell.StyleDefault. Foreground(tcell.ColorRed). Background(tcell.ColorBlack) @@ -177,11 +179,11 @@ func (f *finder) _draw() { } if f.opt.multi { - style = tcell.StyleDefault. - Foreground(tcell.ColorRed). - Background(tcell.ColorBlack) - if _, ok := f.state.selection[m.Idx]; ok { + style := tcell.StyleDefault. + Foreground(tcell.ColorRed). + Background(tcell.ColorBlack) + f.term.Screen().SetContent(1, height-3-i, '>', nil, style) } } @@ -189,7 +191,7 @@ func (f *finder) _draw() { var posIdx int w := 2 for j, r := range []rune(f.state.items[m.Idx]) { - style = tcell.StyleDefault. + style := tcell.StyleDefault. Foreground(tcell.ColorDefault). Background(tcell.ColorDefault) // Highlight selected strings. @@ -254,10 +256,6 @@ func (f *finder) _drawPreview() { prevLines = append(prevLines, []rune(s)) } - style := tcell.StyleDefault. - Foreground(tcell.ColorBlack). - Background(tcell.ColorDefault) - // top line for i := width / 2; i < width; i++ { var r rune @@ -268,13 +266,14 @@ func (f *finder) _drawPreview() { } else { r = '─' } + + style := tcell.StyleDefault. + Foreground(tcell.ColorBlack). + Background(tcell.ColorDefault) + f.term.Screen().SetContent(i, 0, r, nil, style) } // bottom line - style = tcell.StyleDefault. - Foreground(tcell.ColorBlack). - Background(tcell.ColorDefault) - for i := width / 2; i < width; i++ { var r rune if i == width/2 { @@ -284,6 +283,11 @@ func (f *finder) _drawPreview() { } else { r = '─' } + + style := tcell.StyleDefault. + Foreground(tcell.ColorBlack). + Background(tcell.ColorDefault) + f.term.Screen().SetContent(i, height-1, r, nil, style) } // Start with h=1 to exclude each corner rune. @@ -295,21 +299,21 @@ func (f *finder) _drawPreview() { switch { // Left vertical line. case i == width/2: - style = tcell.StyleDefault. + style := tcell.StyleDefault. Foreground(tcell.ColorBlack). Background(tcell.ColorDefault) f.term.Screen().SetContent(i, h, vline, nil, style) w += wvline // Right vertical line. case i == width-1: - style = tcell.StyleDefault. + style := tcell.StyleDefault. Foreground(tcell.ColorBlack). Background(tcell.ColorDefault) f.term.Screen().SetContent(i, h, vline, nil, style) w += wvline // Spaces between left and right vertical lines. case w == width/2+wvline, w == width-1-wvline: - style = tcell.StyleDefault. + style := tcell.StyleDefault. Foreground(tcell.ColorDefault). Background(tcell.ColorDefault) @@ -328,7 +332,7 @@ func (f *finder) _drawPreview() { } rw := runewidth.RuneWidth(l[j]) if w+rw > width-1-2 { - style = tcell.StyleDefault. + style := tcell.StyleDefault. Foreground(tcell.ColorDefault). Background(tcell.ColorDefault) @@ -339,7 +343,7 @@ func (f *finder) _drawPreview() { continue } - style = tcell.StyleDefault. + style := tcell.StyleDefault. Foreground(tcell.ColorDefault). Background(tcell.ColorDefault) f.term.Screen().SetContent(w, h, l[j], nil, style) @@ -615,7 +619,6 @@ func (f *finder) find(slice interface{}, itemFunc func(i int) string, opts []Opt if err := f.initFinder(items, matched, opt); err != nil { return nil, errors.Wrap(err, "failed to initialize the fuzzy finder") } - // defer f.term.Screen().Fini() close(inited) @@ -637,6 +640,10 @@ func (f *finder) find(slice interface{}, itemFunc func(i int) string, opts []Opt err := f.readKey() switch { case err == ErrAbort: + if !isInTesting() { + f.term.Screen().Fini() + } + return nil, ErrAbort case err == errEntered: f.stateMu.RLock() From f6c262aa5d633fbb77553c15bec700d6096e9bb6 Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Tue, 1 Dec 2020 05:04:21 +0900 Subject: [PATCH 07/26] make more stable --- fuzzyfinder_test.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/fuzzyfinder_test.go b/fuzzyfinder_test.go index feedff9..d463eae 100644 --- a/fuzzyfinder_test.go +++ b/fuzzyfinder_test.go @@ -179,9 +179,7 @@ func TestFind(t *testing.T) { }...)...)}, } - var seqMutex sync.Mutex for name, c := range cases { - seqMutex.Lock() t.Run(name, func(t *testing.T) { events := c.events @@ -207,12 +205,12 @@ func TestFind(t *testing.T) { t.Fatalf("Find must return ErrAbort, but got '%s'", err) } + time.Sleep(4000 * time.Millisecond) res := term.GetResult() term.Screen().Fini() return res }) }) - seqMutex.Unlock() } } @@ -247,6 +245,7 @@ func TestFind_hotReload(t *testing.T) { t.Fatalf("Find must return ErrAbort, but got '%s'", err) } + time.Sleep(2000 * time.Millisecond) res := term.GetResult() term.Screen().Fini() return res @@ -258,8 +257,16 @@ func TestFind_enter(t *testing.T) { events []tcell.Event expected int }{ - "initial": {events: keys(input{tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}), expected: 0}, - "mode smart to case-sensitive": {events: runes("CHI"), expected: 7}, + "initial": {events: keys(input{tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}), expected: 0}, + "mode smart to case-sensitive": {events: append(runes("JI"), keys([]input{ + {tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, // tab earn time for filter + {tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, + {tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, + {tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, + {tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, + {tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, + {tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, + }...)...), expected: 7}, } for name, c := range cases { @@ -277,6 +284,7 @@ func TestFind_enter(t *testing.T) { return tracks[i].Name }, ) + if err != nil { t.Fatalf("Find must not return an error, but got '%s'", err) } From 703ab69dc030b0bf8e0f27c42b1c036a46854a06 Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Tue, 1 Dec 2020 15:13:23 +0900 Subject: [PATCH 08/26] go test parallel one --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 979318b..cb18da2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,7 +30,7 @@ jobs: run: go build - name: Test - run: go test -v -coverpkg ./... -covermode atomic -coverprofile coverage.txt -tags fuzz -numCases 3000 -numEvents 300 ./... + run: go -p 1 test -v -coverpkg ./... -covermode atomic -coverprofile coverage.txt -tags fuzz -numCases 10 -numEvents 10 ./... - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 From fd4b8784d2849f76861c8060e9017d93bb5177b3 Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Tue, 1 Dec 2020 15:18:23 +0900 Subject: [PATCH 09/26] call Fini when find done --- fuzzyfinder.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/fuzzyfinder.go b/fuzzyfinder.go index 7484165..39f7811 100644 --- a/fuzzyfinder.go +++ b/fuzzyfinder.go @@ -640,10 +640,6 @@ func (f *finder) find(slice interface{}, itemFunc func(i int) string, opts []Opt err := f.readKey() switch { case err == ErrAbort: - if !isInTesting() { - f.term.Screen().Fini() - } - return nil, ErrAbort case err == errEntered: f.stateMu.RLock() @@ -689,6 +685,10 @@ func Find(slice interface{}, itemFunc func(i int) string, opts ...Option) (int, func (f *finder) Find(slice interface{}, itemFunc func(i int) string, opts ...Option) (int, error) { res, err := f.find(slice, itemFunc, opts) + if !isInTesting() { + f.term.Screen().Fini() + } + if err != nil { return 0, err } @@ -703,7 +703,11 @@ func FindMulti(slice interface{}, itemFunc func(i int) string, opts ...Option) ( func (f *finder) FindMulti(slice interface{}, itemFunc func(i int) string, opts ...Option) ([]int, error) { opts = append(opts, withMulti()) - return f.find(slice, itemFunc, opts) + res, err := f.find(slice, itemFunc, opts) + if !isInTesting() { + f.term.Screen().Fini() + } + return res, err } func isInTesting() bool { From 7b394bec3401c0f82b0fd1b41454bce59337be3c Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Tue, 1 Dec 2020 18:25:55 +0900 Subject: [PATCH 10/26] apply workaround for tcell bug --- fuzzyfinder.go | 17 +++++++++++++++++ go.mod | 6 ++++-- go.sum | 26 ++------------------------ 3 files changed, 23 insertions(+), 26 deletions(-) diff --git a/fuzzyfinder.go b/fuzzyfinder.go index 39f7811..3729b3f 100644 --- a/fuzzyfinder.go +++ b/fuzzyfinder.go @@ -18,6 +18,7 @@ import ( "github.com/gdamore/tcell/v2" "github.com/ktr0731/go-fuzzyfinder/matching" runewidth "github.com/mattn/go-runewidth" + "github.com/micmonay/keybd_event" "github.com/pkg/errors" ) @@ -669,6 +670,20 @@ func (f *finder) find(slice interface{}, itemFunc func(i int) string, opts []Opt } } +// This method avoid tcell bug https://github.com/gdamore/tcell/issues/194 +// Aditional EOL event is sent to ensure, consequent events, are correctly handled +func sendExtraEventFix() { + kb, err := keybd_event.NewKeyBonding() + if err != nil { + panic(err) + } + kb.SetKeys(keybd_event.VK_ENTER) + err = kb.Launching() + if err != nil { + panic(err) + } +} + // Find displays a UI that provides fuzzy finding against the provided slice. // The argument slice must be of a slice type. If not, Find returns // an error. itemFunc is called by the length of slice. previewFunc is called @@ -687,6 +702,7 @@ func (f *finder) Find(slice interface{}, itemFunc func(i int) string, opts ...Op res, err := f.find(slice, itemFunc, opts) if !isInTesting() { f.term.Screen().Fini() + sendExtraEventFix() } if err != nil { @@ -706,6 +722,7 @@ func (f *finder) FindMulti(slice interface{}, itemFunc func(i int) string, opts res, err := f.find(slice, itemFunc, opts) if !isInTesting() { f.term.Screen().Fini() + sendExtraEventFix() } return res, err } diff --git a/go.mod b/go.mod index 51d5c6c..aef39c4 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,11 @@ require ( github.com/google/go-cmp v0.5.4 github.com/google/gofuzz v1.2.0 github.com/mattn/go-runewidth v0.0.9 - github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1 + github.com/micmonay/keybd_event v1.1.0 github.com/pkg/errors v0.9.1 - golang.org/x/tools v0.0.0-20201125231158-b5590deeca9b // indirect + golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f // indirect + golang.org/x/text v0.3.3 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect ) go 1.13 diff --git a/go.sum b/go.sum index 90c1777..52fb6df 100644 --- a/go.sum +++ b/go.sum @@ -11,27 +11,10 @@ github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1 h1:lh3PyZvY+B9nFliSGTn5uFuqQQJGuNrD0MLCokv09ag= -github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= +github.com/micmonay/keybd_event v1.1.0 h1:fQzkqiG/Siwji1Ju9NDkIb8FSFFlPU76YbJntrXdtQw= +github.com/micmonay/keybd_event v1.1.0/go.mod h1:QS2Kfz0PbPezFqMPEot+l/cK78/tHLZtZ7AbYUCRKsQ= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= @@ -41,11 +24,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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.0.0-20201125231158-b5590deeca9b h1:Lq5JUTFhiybGVf28jB6QRpqd13/JPOaCnET17PVzYJE= -golang.org/x/tools v0.0.0-20201125231158-b5590deeca9b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= From ae89753639f1ef25775ff09e6eb66a4e7f18ea1f Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Tue, 1 Dec 2020 18:30:11 +0900 Subject: [PATCH 11/26] mis flag position --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cb18da2..69d5824 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,7 +30,7 @@ jobs: run: go build - name: Test - run: go -p 1 test -v -coverpkg ./... -covermode atomic -coverprofile coverage.txt -tags fuzz -numCases 10 -numEvents 10 ./... + run: go test -p 1 -v -coverpkg ./... -covermode atomic -coverprofile coverage.txt -tags fuzz -numCases 10 -numEvents 10 ./... - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 From 7fa1592fd7a8b75e77d1291178af91872278c75b Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Tue, 1 Dec 2020 22:41:57 +0900 Subject: [PATCH 12/26] take lock --- .github/workflows/main.yml | 2 +- fuzzyfinder_test.go | 2 -- mock.go | 5 +++++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 69d5824..feb6705 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,7 +30,7 @@ jobs: run: go build - name: Test - run: go test -p 1 -v -coverpkg ./... -covermode atomic -coverprofile coverage.txt -tags fuzz -numCases 10 -numEvents 10 ./... + run: go test -v -coverpkg ./... -covermode atomic -coverprofile coverage.txt -tags fuzz -numCases 10 -numEvents 10 ./... - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 diff --git a/fuzzyfinder_test.go b/fuzzyfinder_test.go index d463eae..cbbdc71 100644 --- a/fuzzyfinder_test.go +++ b/fuzzyfinder_test.go @@ -205,7 +205,6 @@ func TestFind(t *testing.T) { t.Fatalf("Find must return ErrAbort, but got '%s'", err) } - time.Sleep(4000 * time.Millisecond) res := term.GetResult() term.Screen().Fini() return res @@ -245,7 +244,6 @@ func TestFind_hotReload(t *testing.T) { t.Fatalf("Find must return ErrAbort, but got '%s'", err) } - time.Sleep(2000 * time.Millisecond) res := term.GetResult() term.Screen().Fini() return res diff --git a/mock.go b/mock.go index 54f6f4f..23e6edc 100644 --- a/mock.go +++ b/mock.go @@ -44,6 +44,8 @@ func (t *TerminalMock) GetResult() string { setCursor() + t.resultMu.Lock() + cells, width, height := t.screen.GetContents() for h := 0; h < height; h++ { @@ -76,6 +78,9 @@ func (t *TerminalMock) GetResult() string { s += "\n" } s += "\x1b\x5b\x6d" // Reset previous color. + + t.resultMu.Unlock() + return s } From 0e1592c06551c55cca57fc4088c9cebf7a889110 Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Tue, 1 Dec 2020 22:44:02 +0900 Subject: [PATCH 13/26] sleep to filter --- fuzzyfinder.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fuzzyfinder.go b/fuzzyfinder.go index 3729b3f..bafb3a7 100644 --- a/fuzzyfinder.go +++ b/fuzzyfinder.go @@ -639,6 +639,10 @@ func (f *finder) find(slice interface{}, itemFunc func(i int) string, opts []Opt f.draw(10 * time.Millisecond) err := f.readKey() + // hack for earning time to filter exec + if isInTesting() { + time.Sleep(50 * time.Millisecond) + } switch { case err == ErrAbort: return nil, ErrAbort From db0b77008f3192894445513b4758a7dfc782a804 Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Tue, 1 Dec 2020 22:49:10 +0900 Subject: [PATCH 14/26] remove comment about termbox --- fuzzyfinder.go | 2 -- mock.go | 4 +--- tcell.go | 1 - 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/fuzzyfinder.go b/fuzzyfinder.go index bafb3a7..37c5001 100644 --- a/fuzzyfinder.go +++ b/fuzzyfinder.go @@ -496,8 +496,6 @@ func (f *finder) readKey() error { } } case *tcell.EventResize: - // To get the actual window size, clear all buffers. - // See termbox.Clear's documentation for more details. f.term.Screen().Clear() width, height := f.term.Screen().Size() diff --git a/mock.go b/mock.go index 23e6edc..fd9070f 100644 --- a/mock.go +++ b/mock.go @@ -110,12 +110,10 @@ func (f *finder) UseMockedTerminal() *TerminalMock { return m } -// parseAttr parses an attribute of termbox // as an escape sequence. -// parseAttr doesn't support output modes othar than color256 in termbox-go. func parseAttr(fg, bg *tcell.Color, attr tcell.AttrMask) string { if attr == tcell.AttrInvalid { - panic(fmt.Sprintf("invalid color code: %d", attr)) + panic("invalid attribute") } var buf bytes.Buffer diff --git a/tcell.go b/tcell.go index a630513..7a99a2a 100644 --- a/tcell.go +++ b/tcell.go @@ -8,7 +8,6 @@ type terminal interface { Screen() tcell.Screen } -// termImpl is the implementation for termbox-go. type termImpl struct { screen tcell.Screen } From f095d03b4652de4f1aec04942f2d3131dc7f04d9 Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Fri, 4 Dec 2020 02:56:07 +0900 Subject: [PATCH 15/26] embed screen --- example_test.go | 10 ++------ fuzzyfinder.go | 62 ++++++++++++++++++++++----------------------- fuzzyfinder_test.go | 4 +-- helper_test.go | 6 +---- mock.go | 26 +++++++++---------- tcell.go | 10 +++----- 6 files changed, 52 insertions(+), 66 deletions(-) diff --git a/example_test.go b/example_test.go index fc8b5f9..8ba0fc7 100644 --- a/example_test.go +++ b/example_test.go @@ -71,17 +71,11 @@ func ExampleFindMulti() { func ExampleTerminalMock() { // Initialize a mocked terminal. term := fuzzyfinder.UseMockedTerminal() - screen, ok := term.Screen().(tcell.SimulationScreen) - if !ok { - panic("screen type unmatch") - } - // Set the window size and events. - screen.SetSize(60, 10) keys := "foo" for _, r := range keys { - screen.InjectKey(tcell.KeyRune, r, tcell.ModNone) + term.InjectKey(tcell.KeyRune, r, tcell.ModNone) } - screen.InjectKey(tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone) + term.InjectKey(tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone) slice := []string{"foo", "bar", "baz"} fuzzyfinder.Find(slice, func(i int) string { return slice[i] }) diff --git a/fuzzyfinder.go b/fuzzyfinder.go index 37c5001..d2a040d 100644 --- a/fuzzyfinder.go +++ b/fuzzyfinder.go @@ -78,7 +78,7 @@ func (f *finder) initFinder(items []string, matched []matching.Matched, opt opt) f.term = &termImpl{ screen: screen, } - if err := f.term.Screen().Init(); err != nil { + if err := f.term.Init(); err != nil { return errors.Wrap(err, "failed to initialize screen") } } @@ -97,7 +97,7 @@ func (f *finder) initFinder(items []string, matched []matching.Matched, opt opt) f.drawTimer = time.AfterFunc(0, func() { f._draw() f._drawPreview() - f.term.Screen().Show() + f.term.Show() }) f.drawTimer.Stop() } @@ -116,8 +116,8 @@ func (f *finder) updateItems(items []string, matched []matching.Matched) { // _draw is used from draw with a timer. func (f *finder) _draw() { - width, height := f.term.Screen().Size() - f.term.Screen().Clear() + width, height := f.term.Size() + f.term.Clear() maxWidth := width if f.opt.previewFunc != nil { @@ -132,7 +132,7 @@ func (f *finder) _draw() { Foreground(tcell.ColorBlue). Background(tcell.ColorDefault) - f.term.Screen().SetContent(promptLinePad, height-1, r, nil, style) + f.term.SetContent(promptLinePad, height-1, r, nil, style) promptLinePad++ } var r rune @@ -144,10 +144,10 @@ func (f *finder) _draw() { Bold(true) // Add a space between '>' and runes. - f.term.Screen().SetContent(promptLinePad+w, height-1, r, nil, style) + f.term.SetContent(promptLinePad+w, height-1, r, nil, style) w += runewidth.RuneWidth(r) } - f.term.Screen().ShowCursor(promptLinePad+f.state.cursorX, height-1) + f.term.ShowCursor(promptLinePad+f.state.cursorX, height-1) // Number line for i, r := range fmt.Sprintf("%d/%d", len(f.state.matched), len(f.state.items)) { @@ -155,7 +155,7 @@ func (f *finder) _draw() { Foreground(tcell.ColorYellow). Background(tcell.ColorDefault) - f.term.Screen().SetContent(2+i, height-2, r, nil, style) + f.term.SetContent(2+i, height-2, r, nil, style) } // Item lines @@ -175,8 +175,8 @@ func (f *finder) _draw() { Foreground(tcell.ColorRed). Background(tcell.ColorBlack) - f.term.Screen().SetContent(0, height-3-i, '>', nil, style) - f.term.Screen().SetContent(1, height-3-i, ' ', nil, style) + f.term.SetContent(0, height-3-i, '>', nil, style) + f.term.SetContent(1, height-3-i, ' ', nil, style) } if f.opt.multi { @@ -185,7 +185,7 @@ func (f *finder) _draw() { Foreground(tcell.ColorRed). Background(tcell.ColorBlack) - f.term.Screen().SetContent(1, height-3-i, '>', nil, style) + f.term.SetContent(1, height-3-i, '>', nil, style) } } @@ -226,12 +226,12 @@ func (f *finder) _draw() { rw := runewidth.RuneWidth(r) // Shorten item cells. if w+rw+2 > maxWidth { - f.term.Screen().SetContent(w, height-3-i, '.', nil, style) - f.term.Screen().SetContent(w+1, height-3-i, '.', nil, style) + f.term.SetContent(w, height-3-i, '.', nil, style) + f.term.SetContent(w+1, height-3-i, '.', nil, style) w += 2 break } else { - f.term.Screen().SetContent(w, height-3-i, r, nil, style) + f.term.SetContent(w, height-3-i, r, nil, style) w += rw } } @@ -243,7 +243,7 @@ func (f *finder) _drawPreview() { return } - width, height := f.term.Screen().Size() + width, height := f.term.Size() var idx int if len(f.state.matched) == 0 { idx = -1 @@ -272,7 +272,7 @@ func (f *finder) _drawPreview() { Foreground(tcell.ColorBlack). Background(tcell.ColorDefault) - f.term.Screen().SetContent(i, 0, r, nil, style) + f.term.SetContent(i, 0, r, nil, style) } // bottom line for i := width / 2; i < width; i++ { @@ -289,7 +289,7 @@ func (f *finder) _drawPreview() { Foreground(tcell.ColorBlack). Background(tcell.ColorDefault) - f.term.Screen().SetContent(i, height-1, r, nil, style) + f.term.SetContent(i, height-1, r, nil, style) } // Start with h=1 to exclude each corner rune. const vline = '│' @@ -303,14 +303,14 @@ func (f *finder) _drawPreview() { style := tcell.StyleDefault. Foreground(tcell.ColorBlack). Background(tcell.ColorDefault) - f.term.Screen().SetContent(i, h, vline, nil, style) + f.term.SetContent(i, h, vline, nil, style) w += wvline // Right vertical line. case i == width-1: style := tcell.StyleDefault. Foreground(tcell.ColorBlack). Background(tcell.ColorDefault) - f.term.Screen().SetContent(i, h, vline, nil, style) + f.term.SetContent(i, h, vline, nil, style) w += wvline // Spaces between left and right vertical lines. case w == width/2+wvline, w == width-1-wvline: @@ -318,7 +318,7 @@ func (f *finder) _drawPreview() { Foreground(tcell.ColorDefault). Background(tcell.ColorDefault) - f.term.Screen().SetContent(w, h, ' ', nil, style) + f.term.SetContent(w, h, ' ', nil, style) w++ default: // Preview text if h-1 >= len(prevLines) { @@ -337,8 +337,8 @@ func (f *finder) _drawPreview() { Foreground(tcell.ColorDefault). Background(tcell.ColorDefault) - f.term.Screen().SetContent(w, h, '.', nil, style) - f.term.Screen().SetContent(w+1, h, '.', nil, style) + f.term.SetContent(w, h, '.', nil, style) + f.term.SetContent(w+1, h, '.', nil, style) w += 2 continue @@ -347,7 +347,7 @@ func (f *finder) _drawPreview() { style := tcell.StyleDefault. Foreground(tcell.ColorDefault). Background(tcell.ColorDefault) - f.term.Screen().SetContent(w, h, l[j], nil, style) + f.term.SetContent(w, h, l[j], nil, style) w += rw } } @@ -362,7 +362,7 @@ func (f *finder) draw(d time.Duration) { // Don't use goroutine scheduling. f._draw() f._drawPreview() - f.term.Screen().Show() + f.term.Show() } else { f.drawTimer.Reset(d) } @@ -384,7 +384,7 @@ func (f *finder) readKey() error { } }() - e := f.term.Screen().PollEvent() + e := f.term.PollEvent() f.stateMu.Lock() defer f.stateMu.Unlock() @@ -452,7 +452,7 @@ func (f *finder) readKey() error { if f.state.y+1 < len(f.state.matched) { f.state.y++ } - _, height := f.term.Screen().Size() + _, height := f.term.Size() if f.state.cursorY+1 < height-2 && f.state.cursorY+1 < len(f.state.matched) { f.state.cursorY++ } @@ -482,7 +482,7 @@ func (f *finder) readKey() error { } default: if e.Rune() != 0 { - width, _ := f.term.Screen().Size() + width, _ := f.term.Size() maxLineWidth := width - 2 - 1 if len(f.state.input)+1 > maxLineWidth { // Discard inputted rune. @@ -496,9 +496,9 @@ func (f *finder) readKey() error { } } case *tcell.EventResize: - f.term.Screen().Clear() + f.term.Clear() - width, height := f.term.Screen().Size() + width, height := f.term.Size() itemAreaHeight := height - 2 - 1 if itemAreaHeight >= 0 && f.state.cursorY > itemAreaHeight { f.state.cursorY = itemAreaHeight @@ -703,7 +703,7 @@ func Find(slice interface{}, itemFunc func(i int) string, opts ...Option) (int, func (f *finder) Find(slice interface{}, itemFunc func(i int) string, opts ...Option) (int, error) { res, err := f.find(slice, itemFunc, opts) if !isInTesting() { - f.term.Screen().Fini() + f.term.Fini() sendExtraEventFix() } @@ -723,7 +723,7 @@ func (f *finder) FindMulti(slice interface{}, itemFunc func(i int) string, opts opts = append(opts, withMulti()) res, err := f.find(slice, itemFunc, opts) if !isInTesting() { - f.term.Screen().Fini() + f.term.Fini() sendExtraEventFix() } return res, err diff --git a/fuzzyfinder_test.go b/fuzzyfinder_test.go index cbbdc71..d191022 100644 --- a/fuzzyfinder_test.go +++ b/fuzzyfinder_test.go @@ -206,7 +206,7 @@ func TestFind(t *testing.T) { } res := term.GetResult() - term.Screen().Fini() + term.Fini() return res }) }) @@ -245,7 +245,7 @@ func TestFind_hotReload(t *testing.T) { } res := term.GetResult() - term.Screen().Fini() + term.Fini() return res }) } diff --git a/helper_test.go b/helper_test.go index a392ca7..96414d9 100644 --- a/helper_test.go +++ b/helper_test.go @@ -1,9 +1,5 @@ package fuzzyfinder -import ( - "github.com/gdamore/tcell/v2" -) - func New() *finder { return &finder{} } @@ -12,6 +8,6 @@ func NewWithMockedTerminal() (*finder, *TerminalMock) { f := New() m := f.UseMockedTerminal() w, h := 60, 10 // A normally value. - m.Screen().(tcell.SimulationScreen).SetSize(w, h) + m.SetSize(w, h) return f, m } diff --git a/mock.go b/mock.go index fd9070f..3241d5e 100644 --- a/mock.go +++ b/mock.go @@ -9,10 +9,12 @@ import ( runewidth "github.com/mattn/go-runewidth" ) +type simScreen tcell.SimulationScreen + // TerminalMock is a mocked terminal for testing. // Most users should use it by calling UseMockedTerminal. type TerminalMock struct { - screen tcell.SimulationScreen + simScreen resultMu sync.RWMutex result string } @@ -23,30 +25,26 @@ func UseMockedTerminal() *TerminalMock { return defaultFinder.UseMockedTerminal() } -func (t *TerminalMock) Screen() tcell.Screen { - return t.screen -} - func (t *TerminalMock) GetResult() string { var s string // set cursor for snapshot test setCursor := func() { - cursorX, cursorY, _ := t.screen.GetCursor() - mainc, _, _, _ := t.screen.GetContent(cursorX, cursorY) + cursorX, cursorY, _ := t.GetCursor() + mainc, _, _, _ := t.GetContent(cursorX, cursorY) if mainc == ' ' { - t.screen.SetContent(cursorX, cursorY, '\u2588', nil, tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorDefault)) + t.SetContent(cursorX, cursorY, '\u2588', nil, tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorDefault)) } else { - t.screen.SetContent(cursorX, cursorY, mainc, nil, tcell.StyleDefault.Background(tcell.ColorWhite)) + t.SetContent(cursorX, cursorY, mainc, nil, tcell.StyleDefault.Background(tcell.ColorWhite)) } - t.screen.Show() + t.Show() } setCursor() t.resultMu.Lock() - cells, width, height := t.screen.GetContents() + cells, width, height := t.GetContents() for h := 0; h < height; h++ { prevFg, prevBg := tcell.ColorDefault, tcell.ColorDefault @@ -89,11 +87,11 @@ func (t *TerminalMock) SetEvents(events ...tcell.Event) { switch event.(type) { case *tcell.EventKey: ek := event.(*tcell.EventKey) - t.screen.InjectKey(ek.Key(), ek.Rune(), ek.Modifiers()) + t.InjectKey(ek.Key(), ek.Rune(), ek.Modifiers()) case *tcell.EventResize: er := event.(*tcell.EventResize) w, h := er.Size() - t.screen.SetSize(w, h) + t.SetSize(w, h) } } } @@ -104,7 +102,7 @@ func (f *finder) UseMockedTerminal() *TerminalMock { panic(err) } m := &TerminalMock{ - screen: screen, + simScreen: screen, } f.term = m return m diff --git a/tcell.go b/tcell.go index 7a99a2a..7c431c6 100644 --- a/tcell.go +++ b/tcell.go @@ -4,14 +4,12 @@ import ( "github.com/gdamore/tcell/v2" ) +type screen tcell.Screen + type terminal interface { - Screen() tcell.Screen + screen } type termImpl struct { - screen tcell.Screen -} - -func (t *termImpl) Screen() tcell.Screen { - return t.screen + screen } From a3025849386142aab6e568ea0ae69b986ee5c383 Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Fri, 4 Dec 2020 02:58:36 +0900 Subject: [PATCH 16/26] mis-remove build tag --- fuzzing_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fuzzing_test.go b/fuzzing_test.go index c64ef42..a7a6b25 100644 --- a/fuzzing_test.go +++ b/fuzzing_test.go @@ -1,3 +1,5 @@ +// +build fuzz + package fuzzyfinder_test import ( From 56c78306a4a0821d23183e0f9407daa4a0406da2 Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Fri, 4 Dec 2020 03:03:12 +0900 Subject: [PATCH 17/26] don't use panic, return error --- fuzzyfinder.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/fuzzyfinder.go b/fuzzyfinder.go index d2a040d..d1d03f7 100644 --- a/fuzzyfinder.go +++ b/fuzzyfinder.go @@ -674,16 +674,17 @@ func (f *finder) find(slice interface{}, itemFunc func(i int) string, opts []Opt // This method avoid tcell bug https://github.com/gdamore/tcell/issues/194 // Aditional EOL event is sent to ensure, consequent events, are correctly handled -func sendExtraEventFix() { +func sendExtraEventFix() error { kb, err := keybd_event.NewKeyBonding() if err != nil { - panic(err) + return err } kb.SetKeys(keybd_event.VK_ENTER) err = kb.Launching() if err != nil { - panic(err) + return err } + return nil } // Find displays a UI that provides fuzzy finding against the provided slice. @@ -704,7 +705,9 @@ func (f *finder) Find(slice interface{}, itemFunc func(i int) string, opts ...Op res, err := f.find(slice, itemFunc, opts) if !isInTesting() { f.term.Fini() - sendExtraEventFix() + if err := sendExtraEventFix(); err != nil { + return 0, err + } } if err != nil { @@ -724,7 +727,9 @@ func (f *finder) FindMulti(slice interface{}, itemFunc func(i int) string, opts res, err := f.find(slice, itemFunc, opts) if !isInTesting() { f.term.Fini() - sendExtraEventFix() + if err := sendExtraEventFix(); err != nil { + return nil, err + } } return res, err } From a432d84dffe095e8535a55a20357a78dbea636ef Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Fri, 4 Dec 2020 03:09:40 +0900 Subject: [PATCH 18/26] follow comment style --- mock.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mock.go b/mock.go index 3241d5e..0c89a77 100644 --- a/mock.go +++ b/mock.go @@ -25,6 +25,7 @@ func UseMockedTerminal() *TerminalMock { return defaultFinder.UseMockedTerminal() } +// GetResult returns the result showed in screen func (t *TerminalMock) GetResult() string { var s string @@ -108,7 +109,7 @@ func (f *finder) UseMockedTerminal() *TerminalMock { return m } -// as an escape sequence. +// parseAttr parses color and attribute for test func parseAttr(fg, bg *tcell.Color, attr tcell.AttrMask) string { if attr == tcell.AttrInvalid { panic("invalid attribute") From a19c30d141c151b70bc3921367768dc2177453aa Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Sun, 13 Dec 2020 20:21:45 +0900 Subject: [PATCH 19/26] numCases doesn't interfere tcell limit --- .github/workflows/main.yml | 2 +- fuzzing_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index feb6705..9dff24d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,7 +30,7 @@ jobs: run: go build - name: Test - run: go test -v -coverpkg ./... -covermode atomic -coverprofile coverage.txt -tags fuzz -numCases 10 -numEvents 10 ./... + run: go test -v -coverpkg ./... -covermode atomic -coverprofile coverage.txt -tags fuzz -numCases 3000 -numEvents 10 ./... - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 diff --git a/fuzzing_test.go b/fuzzing_test.go index a7a6b25..8646934 100644 --- a/fuzzing_test.go +++ b/fuzzing_test.go @@ -93,8 +93,8 @@ func TestFuzz(t *testing.T) { return b } - // number of events in tcell.SimulationScreen is limited 10 - for i := 0; i < rand.Intn(min(*numCases, 10)); i++ { + for i := 0; i < rand.Intn(*numCases)+10; i++ { + // number of events in tcell.SimulationScreen is limited 10 n := rand.Intn(min(*numEvents, 10)) events := make([]tcell.Event, n) for i := 0; i < n; i++ { From e37529d90681bd7b44e2fcd2b8ab4dea5a7ca782 Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Sun, 13 Dec 2020 20:25:01 +0900 Subject: [PATCH 20/26] call Fini in find --- fuzzyfinder.go | 6 ++++-- fuzzyfinder_test.go | 2 -- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fuzzyfinder.go b/fuzzyfinder.go index d1d03f7..3c1af3c 100644 --- a/fuzzyfinder.go +++ b/fuzzyfinder.go @@ -619,6 +619,10 @@ func (f *finder) find(slice interface{}, itemFunc func(i int) string, opts []Opt return nil, errors.Wrap(err, "failed to initialize the fuzzy finder") } + if !isInTesting() { + defer f.term.Fini() + } + close(inited) go func() { @@ -704,7 +708,6 @@ func Find(slice interface{}, itemFunc func(i int) string, opts ...Option) (int, func (f *finder) Find(slice interface{}, itemFunc func(i int) string, opts ...Option) (int, error) { res, err := f.find(slice, itemFunc, opts) if !isInTesting() { - f.term.Fini() if err := sendExtraEventFix(); err != nil { return 0, err } @@ -726,7 +729,6 @@ func (f *finder) FindMulti(slice interface{}, itemFunc func(i int) string, opts opts = append(opts, withMulti()) res, err := f.find(slice, itemFunc, opts) if !isInTesting() { - f.term.Fini() if err := sendExtraEventFix(); err != nil { return nil, err } diff --git a/fuzzyfinder_test.go b/fuzzyfinder_test.go index d191022..cc180aa 100644 --- a/fuzzyfinder_test.go +++ b/fuzzyfinder_test.go @@ -206,7 +206,6 @@ func TestFind(t *testing.T) { } res := term.GetResult() - term.Fini() return res }) }) @@ -245,7 +244,6 @@ func TestFind_hotReload(t *testing.T) { } res := term.GetResult() - term.Fini() return res }) } From f371c187688ccfb444f8947e46271f691d0c981f Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Sun, 13 Dec 2020 20:27:30 +0900 Subject: [PATCH 21/26] backward compatible for public interface --- fuzzing_test.go | 4 +- fuzzyfinder_test.go | 55 +++++--- go.mod | 1 + go.sum | 2 + helper_test.go | 8 ++ mock.go | 335 ++++++++++++++++++++++++++++++++++++-------- mock_test.go | 61 +++++++- 7 files changed, 384 insertions(+), 82 deletions(-) diff --git a/fuzzing_test.go b/fuzzing_test.go index 8646934..aa77bfd 100644 --- a/fuzzing_test.go +++ b/fuzzing_test.go @@ -129,8 +129,10 @@ func TestFuzz(t *testing.T) { f, term := fuzzyfinder.NewWithMockedTerminal() events = append(events, key(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone})) - term.SetEvents(events...) + if !term.IsTermboxVer() { + term.SetEventsV2(events...) + } var ( iface interface{} promptStr string diff --git a/fuzzyfinder_test.go b/fuzzyfinder_test.go index cc180aa..704ddac 100644 --- a/fuzzyfinder_test.go +++ b/fuzzyfinder_test.go @@ -183,9 +183,12 @@ func TestFind(t *testing.T) { t.Run(name, func(t *testing.T) { events := c.events - f, term := fuzzyfinder.NewWithMockedTerminal() + f, term := fuzzyfinder.NewWithMockedTerminalV2() events = append(events, key(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone})) - term.SetEvents(events...) + + if !term.IsTermboxVer() { + term.SetEventsV2(events...) + } assertWithGolden(t, func(t *testing.T) string { _, err := f.Find( @@ -213,9 +216,12 @@ func TestFind(t *testing.T) { } func TestFind_hotReload(t *testing.T) { - f, term := fuzzyfinder.NewWithMockedTerminal() + f, term := fuzzyfinder.NewWithMockedTerminalV2() events := append(runes("adrena"), keys(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone})...) - term.SetEvents(events...) + + if !term.IsTermboxVer() { + term.SetEventsV2(events...) + } var mu sync.Mutex assertWithGolden(t, func(t *testing.T) string { @@ -253,16 +259,8 @@ func TestFind_enter(t *testing.T) { events []tcell.Event expected int }{ - "initial": {events: keys(input{tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}), expected: 0}, - "mode smart to case-sensitive": {events: append(runes("JI"), keys([]input{ - {tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, // tab earn time for filter - {tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, - {tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, - {tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, - {tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, - {tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, - {tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, - }...)...), expected: 7}, + "initial": {events: keys(input{tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}), expected: 0}, + "mode smart to case-sensitive": {events: append(runes("JI")), expected: 7}, } for name, c := range cases { @@ -270,9 +268,12 @@ func TestFind_enter(t *testing.T) { c := c events := c.events - f, term := fuzzyfinder.NewWithMockedTerminal() + f, term := fuzzyfinder.NewWithMockedTerminalV2() events = append(events, key(input{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone})) - term.SetEvents(events...) + + if !term.IsTermboxVer() { + term.SetEventsV2(events...) + } idx, err := f.Find( tracks, @@ -340,10 +341,12 @@ func TestFindMulti(t *testing.T) { c := c events := c.events - f, term := fuzzyfinder.NewWithMockedTerminal() + f, term := fuzzyfinder.NewWithMockedTerminalV2() events = append(events, key(input{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone})) - term.SetEvents(events...) + if !term.IsTermboxVer() { + term.SetEventsV2(events...) + } idxs, err := f.FindMulti( tracks, func(i int) string { @@ -378,8 +381,12 @@ func BenchmarkFind(b *testing.B) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - f, term := fuzzyfinder.NewWithMockedTerminal() - term.SetEvents(append(runes("adrele!!"), key(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone}))...) + f, term := fuzzyfinder.NewWithMockedTerminalV2() + events := append(runes("adrele!!"), key(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone})) + if !term.IsTermboxVer() { + term.SetEventsV2(events...) + } + f.Find( tracks, func(i int) string { @@ -399,8 +406,12 @@ func BenchmarkFind(b *testing.B) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - f, term := fuzzyfinder.NewWithMockedTerminal() - term.SetEvents(append(runes("adrele!!"), key(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone}))...) + f, term := fuzzyfinder.NewWithMockedTerminalV2() + events := append(runes("adrele!!"), key(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone})) + + if !term.IsTermboxVer() { + term.SetEventsV2(events...) + } f.Find( &tracks, func(i int) string { diff --git a/go.mod b/go.mod index aef39c4..66bc283 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/google/gofuzz v1.2.0 github.com/mattn/go-runewidth v0.0.9 github.com/micmonay/keybd_event v1.1.0 + github.com/nsf/termbox-go v0.0.0-20201124104050-ed494de23a00 github.com/pkg/errors v0.9.1 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f // indirect golang.org/x/text v0.3.3 // indirect diff --git a/go.sum b/go.sum index 52fb6df..5ae5ef2 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/Qd github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/micmonay/keybd_event v1.1.0 h1:fQzkqiG/Siwji1Ju9NDkIb8FSFFlPU76YbJntrXdtQw= github.com/micmonay/keybd_event v1.1.0/go.mod h1:QS2Kfz0PbPezFqMPEot+l/cK78/tHLZtZ7AbYUCRKsQ= +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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10= diff --git a/helper_test.go b/helper_test.go index 96414d9..633b0a8 100644 --- a/helper_test.go +++ b/helper_test.go @@ -11,3 +11,11 @@ func NewWithMockedTerminal() (*finder, *TerminalMock) { m.SetSize(w, h) return f, m } + +func NewWithMockedTerminalV2() (*finder, *TerminalMock) { + f := New() + m := f.UseMockedTerminalV2() + w, h := 60, 10 // A normally value. + m.SetSize(w, h) + return f, m +} diff --git a/mock.go b/mock.go index 0c89a77..130c48c 100644 --- a/mock.go +++ b/mock.go @@ -4,100 +4,279 @@ import ( "bytes" "fmt" "sync" + "time" "github.com/gdamore/tcell/v2" runewidth "github.com/mattn/go-runewidth" + "github.com/nsf/termbox-go" ) +type cell struct { + ch rune + bg, fg termbox.Attribute +} + type simScreen tcell.SimulationScreen // TerminalMock is a mocked terminal for testing. // Most users should use it by calling UseMockedTerminal. type TerminalMock struct { simScreen + sizeMu sync.RWMutex + width, height int + + eventsMu sync.Mutex + events []termbox.Event + + cellsMu sync.RWMutex + cells []*cell + resultMu sync.RWMutex result string + + sleepDuration time.Duration } -// UseMockedTerminal switches the terminal, which is used from -// this package to a mocked one. -func UseMockedTerminal() *TerminalMock { - return defaultFinder.UseMockedTerminal() +func (m *TerminalMock) IsTermboxVer() bool { + return m.simScreen == nil } -// GetResult returns the result showed in screen -func (t *TerminalMock) GetResult() string { - var s string +// SetSize changes the pseudo-size of the window. +// Note that SetSize resets added cells. +func (m *TerminalMock) SetSize(w, h int) { + if m.IsTermboxVer() { + m.sizeMu.Lock() + defer m.sizeMu.Unlock() + m.cellsMu.Lock() + defer m.cellsMu.Unlock() + m.width = w + m.height = h + m.cells = make([]*cell, w*h) + } else { + m.simScreen.SetSize(w, h) + } +} - // set cursor for snapshot test - setCursor := func() { - cursorX, cursorY, _ := t.GetCursor() - mainc, _, _, _ := t.GetContent(cursorX, cursorY) - if mainc == ' ' { - t.SetContent(cursorX, cursorY, '\u2588', nil, tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorDefault)) - } else { - t.SetContent(cursorX, cursorY, mainc, nil, tcell.StyleDefault.Background(tcell.ColorWhite)) +// SetEvents sets all events, which are fetched by pollEvent. +// A user of this must set the EscKey event at the end. +func (m *TerminalMock) SetEvents(events ...termbox.Event) { + if m.IsTermboxVer() { + m.eventsMu.Lock() + defer m.eventsMu.Unlock() + m.events = events + } +} + +func (m *TerminalMock) SetEventsV2(events ...tcell.Event) { + for _, event := range events { + switch event.(type) { + case *tcell.EventKey: + ek := event.(*tcell.EventKey) + m.simScreen.InjectKey(ek.Key(), ek.Rune(), ek.Modifiers()) + case *tcell.EventResize: + er := event.(*tcell.EventResize) + w, h := er.Size() + m.simScreen.SetSize(w, h) } - t.Show() } +} + +// GetResult returns a flushed string that is displayed to the actual terminal. +// It contains all escape sequences such that ANSI escape code. +func (m *TerminalMock) GetResult() string { + if m.IsTermboxVer() { + m.resultMu.RLock() + defer m.resultMu.RUnlock() + return m.result + } else { + var s string - setCursor() + // set cursor for snapshot test + setCursor := func() { + cursorX, cursorY, _ := m.simScreen.GetCursor() + mainc, _, _, _ := m.simScreen.GetContent(cursorX, cursorY) + if mainc == ' ' { + m.simScreen.SetContent(cursorX, cursorY, '\u2588', nil, tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorDefault)) + } else { + m.simScreen.SetContent(cursorX, cursorY, mainc, nil, tcell.StyleDefault.Background(tcell.ColorWhite)) + } + m.simScreen.Show() + } - t.resultMu.Lock() + setCursor() - cells, width, height := t.GetContents() + m.resultMu.Lock() - for h := 0; h < height; h++ { - prevFg, prevBg := tcell.ColorDefault, tcell.ColorDefault - for w := 0; w < width; w++ { - cell := cells[h*width+w] - fg, bg, attr := cell.Style.Decompose() - var fgReset bool - if fg != prevFg { - s += "\x1b\x5b\x6d" // Reset previous color. - s += parseAttr(&fg, nil, attr) - prevFg = fg - prevBg = tcell.ColorDefault - fgReset = true - } - if bg != prevBg { - if !fgReset { + cells, width, height := m.simScreen.GetContents() + + for h := 0; h < height; h++ { + prevFg, prevBg := tcell.ColorDefault, tcell.ColorDefault + for w := 0; w < width; w++ { + cell := cells[h*width+w] + fg, bg, attr := cell.Style.Decompose() + var fgReset bool + if fg != prevFg { s += "\x1b\x5b\x6d" // Reset previous color. - prevFg = tcell.ColorDefault + s += parseAttrV2(&fg, nil, attr) + prevFg = fg + prevBg = tcell.ColorDefault + fgReset = true + } + if bg != prevBg { + if !fgReset { + s += "\x1b\x5b\x6d" // Reset previous color. + prevFg = tcell.ColorDefault + } + s += parseAttrV2(nil, &bg, attr) + prevBg = bg + } + s += string(cell.Runes[:]) + rw := runewidth.RuneWidth(cell.Runes[0]) + if rw != 0 { + w += rw - 1 } - s += parseAttr(nil, &bg, attr) - prevBg = bg } - s += string(cell.Runes[:]) - rw := runewidth.RuneWidth(cell.Runes[0]) - if rw != 0 { - w += rw - 1 + s += "\n" + } + s += "\x1b\x5b\x6d" // Reset previous color. + + m.resultMu.Unlock() + + return s + } +} + +func (m *TerminalMock) init() error { + return nil +} + +func (m *TerminalMock) size() (width int, height int) { + m.sizeMu.RLock() + defer m.sizeMu.RUnlock() + return m.width, m.height +} + +func (m *TerminalMock) clear(fg termbox.Attribute, bg termbox.Attribute) error { + // TODO + return nil +} + +func (m *TerminalMock) setCell(x int, y int, ch rune, fg termbox.Attribute, bg termbox.Attribute) { + m.sizeMu.RLock() + defer m.sizeMu.RUnlock() + m.cellsMu.Lock() + defer m.cellsMu.Unlock() + + if x < 0 || x >= m.width { + return + } + if y < 0 || y >= m.height { + return + } + m.cells[y*m.width+x] = &cell{ch: ch, fg: fg, bg: bg} +} + +func (m *TerminalMock) setCursor(x int, y int) { + m.sizeMu.RLock() + defer m.sizeMu.RUnlock() + m.cellsMu.Lock() + defer m.cellsMu.Unlock() + if x < 0 || x >= m.width { + return + } + if y < 0 || y >= m.height { + return + } + i := y*m.width + x + if m.cells[i] == nil { + m.cells[y*m.width+x] = &cell{ch: '\u2588', fg: termbox.ColorWhite, bg: termbox.ColorDefault} + } else { + // Cursor on a rune. + m.cells[y*m.width+x].bg = termbox.ColorWhite + } +} + +func (m *TerminalMock) pollEvent() termbox.Event { + m.eventsMu.Lock() + defer m.eventsMu.Unlock() + if len(m.events) == 0 { + panic("pollEvent called with empty events. have you set expected events by SetEvents?") + } + e := m.events[0] + m.events = m.events[1:] + // Wait a moment for goroutine scheduling. + time.Sleep(m.sleepDuration) + return e +} + +// flush displays all items with formatted layout. +func (m *TerminalMock) flush() error { + m.cellsMu.RLock() + + var s string + for j := 0; j < m.height; j++ { + prevFg, prevBg := termbox.ColorDefault, termbox.ColorDefault + for i := 0; i < m.width; i++ { + c := m.cells[j*m.width+i] + if c == nil { + s += " " + prevFg, prevBg = termbox.ColorDefault, termbox.ColorDefault + continue + } else { + var fgReset bool + if c.fg != prevFg { + s += "\x1b\x5b\x6d" // Reset previous color. + s += parseAttr(c.fg, true) + prevFg = c.fg + prevBg = termbox.ColorDefault + fgReset = true + } + if c.bg != prevBg { + if !fgReset { + s += "\x1b\x5b\x6d" // Reset previous color. + prevFg = termbox.ColorDefault + } + s += parseAttr(c.bg, false) + prevBg = c.bg + } + s += string(c.ch) + rw := runewidth.RuneWidth(c.ch) + if rw != 0 { + i += rw - 1 + } } } s += "\n" } s += "\x1b\x5b\x6d" // Reset previous color. - t.resultMu.Unlock() + m.cellsMu.RUnlock() + m.cellsMu.Lock() + m.cells = make([]*cell, m.width*m.height) + m.cellsMu.Unlock() - return s + m.resultMu.Lock() + defer m.resultMu.Unlock() + m.result = s + + return nil } -func (t *TerminalMock) SetEvents(events ...tcell.Event) { - for _, event := range events { - switch event.(type) { - case *tcell.EventKey: - ek := event.(*tcell.EventKey) - t.InjectKey(ek.Key(), ek.Rune(), ek.Modifiers()) - case *tcell.EventResize: - er := event.(*tcell.EventResize) - w, h := er.Size() - t.SetSize(w, h) - } - } +func (m *TerminalMock) close() {} + +// UseMockedTerminal switches the terminal, which is used from +// this package to a mocked one. +func UseMockedTerminal() *TerminalMock { + return defaultFinder.UseMockedTerminal() } func (f *finder) UseMockedTerminal() *TerminalMock { + m := &TerminalMock{} + f.term = m + return m +} + +func (f *finder) UseMockedTerminalV2() *TerminalMock { screen := tcell.NewSimulationScreen("UTF-8") if err := screen.Init(); err != nil { panic(err) @@ -109,8 +288,50 @@ func (f *finder) UseMockedTerminal() *TerminalMock { return m } -// parseAttr parses color and attribute for test -func parseAttr(fg, bg *tcell.Color, attr tcell.AttrMask) string { +// parseAttr parses an attribute of termbox +// as an escape sequence. +// parseAttr doesn't support output modes othar than color256 in termbox-go. +func parseAttr(attr termbox.Attribute, isFg bool) string { + var buf bytes.Buffer + buf.WriteString("\x1b[") + if attr >= termbox.AttrReverse { + buf.WriteString("7;") + attr -= termbox.AttrReverse + } + if attr >= termbox.AttrUnderline { + buf.WriteString("4;") + attr -= termbox.AttrUnderline + } + if attr >= termbox.AttrBold { + buf.WriteString("1;") + attr -= termbox.AttrBold + } + + if attr > termbox.ColorWhite { + panic(fmt.Sprintf("invalid color code: %d", attr)) + } + + if attr == termbox.ColorDefault { + if isFg { + buf.WriteString("39") + } else { + buf.WriteString("49") + } + } else { + color := int(attr) - 1 + if isFg { + fmt.Fprintf(&buf, "38;5;%d", color) + } else { + fmt.Fprintf(&buf, "48;5;%d", color) + } + } + buf.WriteString("m") + + return buf.String() +} + +// parseAttrV2 parses color and attribute for test +func parseAttrV2(fg, bg *tcell.Color, attr tcell.AttrMask) string { if attr == tcell.AttrInvalid { panic("invalid attribute") } diff --git a/mock_test.go b/mock_test.go index 4421db0..9978120 100644 --- a/mock_test.go +++ b/mock_test.go @@ -5,9 +5,66 @@ import ( "github.com/gdamore/tcell/v2" "github.com/google/go-cmp/cmp" + "github.com/nsf/termbox-go" ) func Test_parseAttr(t *testing.T) { + cases := map[string]struct { + attr termbox.Attribute + isFg bool + expected string + willPanic bool + }{ + "ColorDefault": { + attr: termbox.ColorDefault, + isFg: true, + expected: "\x1b[39m", + }, + "ColorDefault bg": { + attr: termbox.ColorDefault, + expected: "\x1b[49m", + }, + "ColorGreen": { + attr: termbox.ColorGreen, + expected: "\x1b[48;5;2m", + }, + "ColorGreen with bold": { + attr: termbox.ColorGreen | termbox.AttrBold, + expected: "\x1b[1;48;5;2m", + }, + "ColorGreen with bold and underline": { + attr: termbox.ColorGreen | termbox.AttrBold | termbox.AttrUnderline, + expected: "\x1b[4;1;48;5;2m", + }, + "ColorGreen with reverse": { + attr: termbox.ColorGreen | termbox.AttrReverse, + expected: "\x1b[7;48;5;2m", + }, + "invalid color": { + attr: termbox.ColorWhite + 1, + willPanic: true, + }, + } + + for name, c := range cases { + c := c + t.Run(name, func(t *testing.T) { + if c.willPanic { + defer func() { + if err := recover(); err == nil { + t.Errorf("must panic") + } + }() + } + actual := parseAttr(c.attr, c.isFg) + if diff := cmp.Diff(c.expected, actual); diff != "" { + t.Errorf("diff found: \n%s\nexpected = %x, actual = %x", diff, c.expected, actual) + } + }) + } +} + +func Test_parseAttrV2(t *testing.T) { cases := map[string]struct { attr tcell.AttrMask fg tcell.Color @@ -62,9 +119,9 @@ func Test_parseAttr(t *testing.T) { } var actual string if c.isBg { - actual = parseAttr(nil, &c.bg, c.attr) + actual = parseAttrV2(nil, &c.bg, c.attr) } else { - actual = parseAttr(&c.fg, nil, c.attr) + actual = parseAttrV2(&c.fg, nil, c.attr) } if diff := cmp.Diff(c.expected, actual); diff != "" { t.Errorf("diff found: \n%s\nexpected = %x, actual = %x", diff, c.expected, actual) From 0b0c9642b319a63cf40415f638414ea4f038bf6c Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Sun, 13 Dec 2020 20:33:11 +0900 Subject: [PATCH 22/26] use v2 --- fuzzing_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fuzzing_test.go b/fuzzing_test.go index aa77bfd..3869b2f 100644 --- a/fuzzing_test.go +++ b/fuzzing_test.go @@ -67,7 +67,7 @@ var ( var ( out = flag.String("fuzzout", "fuzz.out", "fuzzing error cases") hotReload = flag.Bool("hotreload", false, "enable hot-reloading") - numCases = flag.Int("numCases", 10, "number of test cases") + numCases = flag.Int("numCases", 30, "number of test cases") numEvents = flag.Int("numEvents", 10, "number of events") ) @@ -127,7 +127,7 @@ func TestFuzz(t *testing.T) { var mu sync.Mutex tracks := tracks - f, term := fuzzyfinder.NewWithMockedTerminal() + f, term := fuzzyfinder.NewWithMockedTerminalV2() events = append(events, key(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone})) if !term.IsTermboxVer() { From bedd40430930e26ad46d0cc773c4be096226bb44 Mon Sep 17 00:00:00 2001 From: momochi29 <31027514+tjmtmmnk@users.noreply.github.com> Date: Sun, 13 Dec 2020 20:36:43 +0900 Subject: [PATCH 23/26] Update fuzzyfinder.go Co-authored-by: ktr --- fuzzyfinder.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fuzzyfinder.go b/fuzzyfinder.go index 3c1af3c..11658e1 100644 --- a/fuzzyfinder.go +++ b/fuzzyfinder.go @@ -677,7 +677,7 @@ func (f *finder) find(slice interface{}, itemFunc func(i int) string, opts []Opt } // This method avoid tcell bug https://github.com/gdamore/tcell/issues/194 -// Aditional EOL event is sent to ensure, consequent events, are correctly handled +// Additional EOL event is sent to ensure, consequent events, are correctly handled. func sendExtraEventFix() error { kb, err := keybd_event.NewKeyBonding() if err != nil { From ae3ab9f2840acdd4ee3336a49a1b636800490147 Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Sun, 13 Dec 2020 21:34:00 +0900 Subject: [PATCH 24/26] clarify v2 --- example_test.go | 4 +- fuzzing_test.go | 5 +- fuzzyfinder_test.go | 31 +++------- helper_test.go | 2 +- mock.go | 138 ++++++++++++++++++++++---------------------- 5 files changed, 81 insertions(+), 99 deletions(-) diff --git a/example_test.go b/example_test.go index 8ba0fc7..6c63e98 100644 --- a/example_test.go +++ b/example_test.go @@ -70,7 +70,7 @@ func ExampleFindMulti() { func ExampleTerminalMock() { // Initialize a mocked terminal. - term := fuzzyfinder.UseMockedTerminal() + term := fuzzyfinder.UseMockedTerminalV2() keys := "foo" for _, r := range keys { term.InjectKey(tcell.KeyRune, r, tcell.ModNone) @@ -84,6 +84,6 @@ func ExampleTerminalMock() { // We can test it by the golden files testing pattern. // // See https://speakerdeck.com/mitchellh/advanced-testing-with-go?slide=19 - result := term.GetResult() + result := term.GetResultV2() ioutil.WriteFile("ui.out", []byte(result), 0644) } diff --git a/fuzzing_test.go b/fuzzing_test.go index 3869b2f..bffe9ff 100644 --- a/fuzzing_test.go +++ b/fuzzing_test.go @@ -130,9 +130,8 @@ func TestFuzz(t *testing.T) { f, term := fuzzyfinder.NewWithMockedTerminalV2() events = append(events, key(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone})) - if !term.IsTermboxVer() { - term.SetEventsV2(events...) - } + term.SetEventsV2(events...) + var ( iface interface{} promptStr string diff --git a/fuzzyfinder_test.go b/fuzzyfinder_test.go index 704ddac..b4caffd 100644 --- a/fuzzyfinder_test.go +++ b/fuzzyfinder_test.go @@ -185,10 +185,7 @@ func TestFind(t *testing.T) { f, term := fuzzyfinder.NewWithMockedTerminalV2() events = append(events, key(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone})) - - if !term.IsTermboxVer() { - term.SetEventsV2(events...) - } + term.SetEventsV2(events...) assertWithGolden(t, func(t *testing.T) string { _, err := f.Find( @@ -208,7 +205,7 @@ func TestFind(t *testing.T) { t.Fatalf("Find must return ErrAbort, but got '%s'", err) } - res := term.GetResult() + res := term.GetResultV2() return res }) }) @@ -218,10 +215,7 @@ func TestFind(t *testing.T) { func TestFind_hotReload(t *testing.T) { f, term := fuzzyfinder.NewWithMockedTerminalV2() events := append(runes("adrena"), keys(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone})...) - - if !term.IsTermboxVer() { - term.SetEventsV2(events...) - } + term.SetEventsV2(events...) var mu sync.Mutex assertWithGolden(t, func(t *testing.T) string { @@ -249,7 +243,7 @@ func TestFind_hotReload(t *testing.T) { t.Fatalf("Find must return ErrAbort, but got '%s'", err) } - res := term.GetResult() + res := term.GetResultV2() return res }) } @@ -270,10 +264,7 @@ func TestFind_enter(t *testing.T) { f, term := fuzzyfinder.NewWithMockedTerminalV2() events = append(events, key(input{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone})) - - if !term.IsTermboxVer() { - term.SetEventsV2(events...) - } + term.SetEventsV2(events...) idx, err := f.Find( tracks, @@ -343,10 +334,8 @@ func TestFindMulti(t *testing.T) { f, term := fuzzyfinder.NewWithMockedTerminalV2() events = append(events, key(input{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone})) + term.SetEventsV2(events...) - if !term.IsTermboxVer() { - term.SetEventsV2(events...) - } idxs, err := f.FindMulti( tracks, func(i int) string { @@ -383,9 +372,7 @@ func BenchmarkFind(b *testing.B) { for i := 0; i < b.N; i++ { f, term := fuzzyfinder.NewWithMockedTerminalV2() events := append(runes("adrele!!"), key(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone})) - if !term.IsTermboxVer() { - term.SetEventsV2(events...) - } + term.SetEventsV2(events...) f.Find( tracks, @@ -408,10 +395,8 @@ func BenchmarkFind(b *testing.B) { for i := 0; i < b.N; i++ { f, term := fuzzyfinder.NewWithMockedTerminalV2() events := append(runes("adrele!!"), key(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone})) + term.SetEventsV2(events...) - if !term.IsTermboxVer() { - term.SetEventsV2(events...) - } f.Find( &tracks, func(i int) string { diff --git a/helper_test.go b/helper_test.go index 633b0a8..3085a88 100644 --- a/helper_test.go +++ b/helper_test.go @@ -16,6 +16,6 @@ func NewWithMockedTerminalV2() (*finder, *TerminalMock) { f := New() m := f.UseMockedTerminalV2() w, h := 60, 10 // A normally value. - m.SetSize(w, h) + m.SetSizeV2(w, h) return f, m } diff --git a/mock.go b/mock.go index 130c48c..485fdb9 100644 --- a/mock.go +++ b/mock.go @@ -37,34 +37,28 @@ type TerminalMock struct { sleepDuration time.Duration } -func (m *TerminalMock) IsTermboxVer() bool { - return m.simScreen == nil -} - // SetSize changes the pseudo-size of the window. // Note that SetSize resets added cells. func (m *TerminalMock) SetSize(w, h int) { - if m.IsTermboxVer() { - m.sizeMu.Lock() - defer m.sizeMu.Unlock() - m.cellsMu.Lock() - defer m.cellsMu.Unlock() - m.width = w - m.height = h - m.cells = make([]*cell, w*h) - } else { - m.simScreen.SetSize(w, h) - } + m.sizeMu.Lock() + defer m.sizeMu.Unlock() + m.cellsMu.Lock() + defer m.cellsMu.Unlock() + m.width = w + m.height = h + m.cells = make([]*cell, w*h) +} + +func (m *TerminalMock) SetSizeV2(w, h int) { + m.simScreen.SetSize(w, h) } // SetEvents sets all events, which are fetched by pollEvent. // A user of this must set the EscKey event at the end. func (m *TerminalMock) SetEvents(events ...termbox.Event) { - if m.IsTermboxVer() { - m.eventsMu.Lock() - defer m.eventsMu.Unlock() - m.events = events - } + m.eventsMu.Lock() + defer m.eventsMu.Unlock() + m.events = events } func (m *TerminalMock) SetEventsV2(events ...tcell.Event) { @@ -84,66 +78,66 @@ func (m *TerminalMock) SetEventsV2(events ...tcell.Event) { // GetResult returns a flushed string that is displayed to the actual terminal. // It contains all escape sequences such that ANSI escape code. func (m *TerminalMock) GetResult() string { - if m.IsTermboxVer() { - m.resultMu.RLock() - defer m.resultMu.RUnlock() - return m.result - } else { - var s string - - // set cursor for snapshot test - setCursor := func() { - cursorX, cursorY, _ := m.simScreen.GetCursor() - mainc, _, _, _ := m.simScreen.GetContent(cursorX, cursorY) - if mainc == ' ' { - m.simScreen.SetContent(cursorX, cursorY, '\u2588', nil, tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorDefault)) - } else { - m.simScreen.SetContent(cursorX, cursorY, mainc, nil, tcell.StyleDefault.Background(tcell.ColorWhite)) - } - m.simScreen.Show() - } + m.resultMu.RLock() + defer m.resultMu.RUnlock() + return m.result +} + +func (m *TerminalMock) GetResultV2() string { + var s string - setCursor() + // set cursor for snapshot test + setCursor := func() { + cursorX, cursorY, _ := m.simScreen.GetCursor() + mainc, _, _, _ := m.simScreen.GetContent(cursorX, cursorY) + if mainc == ' ' { + m.simScreen.SetContent(cursorX, cursorY, '\u2588', nil, tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorDefault)) + } else { + m.simScreen.SetContent(cursorX, cursorY, mainc, nil, tcell.StyleDefault.Background(tcell.ColorWhite)) + } + m.simScreen.Show() + } - m.resultMu.Lock() + setCursor() - cells, width, height := m.simScreen.GetContents() + m.resultMu.Lock() - for h := 0; h < height; h++ { - prevFg, prevBg := tcell.ColorDefault, tcell.ColorDefault - for w := 0; w < width; w++ { - cell := cells[h*width+w] - fg, bg, attr := cell.Style.Decompose() - var fgReset bool - if fg != prevFg { + cells, width, height := m.simScreen.GetContents() + + for h := 0; h < height; h++ { + prevFg, prevBg := tcell.ColorDefault, tcell.ColorDefault + for w := 0; w < width; w++ { + cell := cells[h*width+w] + fg, bg, attr := cell.Style.Decompose() + var fgReset bool + if fg != prevFg { + s += "\x1b\x5b\x6d" // Reset previous color. + s += parseAttrV2(&fg, nil, attr) + prevFg = fg + prevBg = tcell.ColorDefault + fgReset = true + } + if bg != prevBg { + if !fgReset { s += "\x1b\x5b\x6d" // Reset previous color. - s += parseAttrV2(&fg, nil, attr) - prevFg = fg - prevBg = tcell.ColorDefault - fgReset = true - } - if bg != prevBg { - if !fgReset { - s += "\x1b\x5b\x6d" // Reset previous color. - prevFg = tcell.ColorDefault - } - s += parseAttrV2(nil, &bg, attr) - prevBg = bg - } - s += string(cell.Runes[:]) - rw := runewidth.RuneWidth(cell.Runes[0]) - if rw != 0 { - w += rw - 1 + prevFg = tcell.ColorDefault } + s += parseAttrV2(nil, &bg, attr) + prevBg = bg + } + s += string(cell.Runes[:]) + rw := runewidth.RuneWidth(cell.Runes[0]) + if rw != 0 { + w += rw - 1 } - s += "\n" } - s += "\x1b\x5b\x6d" // Reset previous color. + s += "\n" + } + s += "\x1b\x5b\x6d" // Reset previous color. - m.resultMu.Unlock() + m.resultMu.Unlock() - return s - } + return s } func (m *TerminalMock) init() error { @@ -270,6 +264,10 @@ func UseMockedTerminal() *TerminalMock { return defaultFinder.UseMockedTerminal() } +func UseMockedTerminalV2() *TerminalMock { + return defaultFinder.UseMockedTerminalV2() +} + func (f *finder) UseMockedTerminal() *TerminalMock { m := &TerminalMock{} f.term = m From eb34660fc0dd052d39d563bfe255ee058d17194d Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Thu, 24 Dec 2020 17:48:56 +0900 Subject: [PATCH 25/26] use v2 flag --- mock.go | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/mock.go b/mock.go index 485fdb9..b70f5dc 100644 --- a/mock.go +++ b/mock.go @@ -35,11 +35,16 @@ type TerminalMock struct { result string sleepDuration time.Duration + v2 bool } // SetSize changes the pseudo-size of the window. // Note that SetSize resets added cells. func (m *TerminalMock) SetSize(w, h int) { + if m.v2 { + m.simScreen.SetSize(w, h) + return + } m.sizeMu.Lock() defer m.sizeMu.Unlock() m.cellsMu.Lock() @@ -49,10 +54,7 @@ func (m *TerminalMock) SetSize(w, h int) { m.cells = make([]*cell, w*h) } -func (m *TerminalMock) SetSizeV2(w, h int) { - m.simScreen.SetSize(w, h) -} - +// Deprecated: Use SetEventsV2 // SetEvents sets all events, which are fetched by pollEvent. // A user of this must set the EscKey event at the end. func (m *TerminalMock) SetEvents(events ...termbox.Event) { @@ -61,6 +63,8 @@ func (m *TerminalMock) SetEvents(events ...termbox.Event) { m.events = events } +// SetEventsV2 sets all events, which are fetched by pollEvent. +// A user of this must set the EscKey event at the end. func (m *TerminalMock) SetEventsV2(events ...tcell.Event) { for _, event := range events { switch event.(type) { @@ -78,12 +82,12 @@ func (m *TerminalMock) SetEventsV2(events ...tcell.Event) { // GetResult returns a flushed string that is displayed to the actual terminal. // It contains all escape sequences such that ANSI escape code. func (m *TerminalMock) GetResult() string { - m.resultMu.RLock() - defer m.resultMu.RUnlock() - return m.result -} + if !m.v2 { + m.resultMu.RLock() + defer m.resultMu.RUnlock() + return m.result + } -func (m *TerminalMock) GetResultV2() string { var s string // set cursor for snapshot test @@ -264,6 +268,8 @@ func UseMockedTerminal() *TerminalMock { return defaultFinder.UseMockedTerminal() } +// UseMockedTerminalV2 switches the terminal, which is used from +// this package to a mocked one. func UseMockedTerminalV2() *TerminalMock { return defaultFinder.UseMockedTerminalV2() } @@ -281,6 +287,7 @@ func (f *finder) UseMockedTerminalV2() *TerminalMock { } m := &TerminalMock{ simScreen: screen, + v2: true, } f.term = m return m @@ -328,7 +335,7 @@ func parseAttr(attr termbox.Attribute, isFg bool) string { return buf.String() } -// parseAttrV2 parses color and attribute for test +// parseAttrV2 parses color and attribute for testing func parseAttrV2(fg, bg *tcell.Color, attr tcell.AttrMask) string { if attr == tcell.AttrInvalid { panic("invalid attribute") From 159aa65bf370605388cb3b0e5c6d7f66e6df9f88 Mon Sep 17 00:00:00 2001 From: tjmtmmnk Date: Thu, 24 Dec 2020 18:19:15 +0900 Subject: [PATCH 26/26] replace no longer needed functions --- example_test.go | 2 +- fuzzing_test.go | 2 +- fuzzyfinder_test.go | 16 ++++++++-------- helper_test.go | 10 +--------- 4 files changed, 11 insertions(+), 19 deletions(-) diff --git a/example_test.go b/example_test.go index 6c63e98..caf8dbb 100644 --- a/example_test.go +++ b/example_test.go @@ -84,6 +84,6 @@ func ExampleTerminalMock() { // We can test it by the golden files testing pattern. // // See https://speakerdeck.com/mitchellh/advanced-testing-with-go?slide=19 - result := term.GetResultV2() + result := term.GetResult() ioutil.WriteFile("ui.out", []byte(result), 0644) } diff --git a/fuzzing_test.go b/fuzzing_test.go index bffe9ff..cb1fdb1 100644 --- a/fuzzing_test.go +++ b/fuzzing_test.go @@ -127,7 +127,7 @@ func TestFuzz(t *testing.T) { var mu sync.Mutex tracks := tracks - f, term := fuzzyfinder.NewWithMockedTerminalV2() + f, term := fuzzyfinder.NewWithMockedTerminal() events = append(events, key(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone})) term.SetEventsV2(events...) diff --git a/fuzzyfinder_test.go b/fuzzyfinder_test.go index b4caffd..230a50d 100644 --- a/fuzzyfinder_test.go +++ b/fuzzyfinder_test.go @@ -183,7 +183,7 @@ func TestFind(t *testing.T) { t.Run(name, func(t *testing.T) { events := c.events - f, term := fuzzyfinder.NewWithMockedTerminalV2() + f, term := fuzzyfinder.NewWithMockedTerminal() events = append(events, key(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone})) term.SetEventsV2(events...) @@ -205,7 +205,7 @@ func TestFind(t *testing.T) { t.Fatalf("Find must return ErrAbort, but got '%s'", err) } - res := term.GetResultV2() + res := term.GetResult() return res }) }) @@ -213,7 +213,7 @@ func TestFind(t *testing.T) { } func TestFind_hotReload(t *testing.T) { - f, term := fuzzyfinder.NewWithMockedTerminalV2() + f, term := fuzzyfinder.NewWithMockedTerminal() events := append(runes("adrena"), keys(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone})...) term.SetEventsV2(events...) @@ -243,7 +243,7 @@ func TestFind_hotReload(t *testing.T) { t.Fatalf("Find must return ErrAbort, but got '%s'", err) } - res := term.GetResultV2() + res := term.GetResult() return res }) } @@ -262,7 +262,7 @@ func TestFind_enter(t *testing.T) { c := c events := c.events - f, term := fuzzyfinder.NewWithMockedTerminalV2() + f, term := fuzzyfinder.NewWithMockedTerminal() events = append(events, key(input{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone})) term.SetEventsV2(events...) @@ -332,7 +332,7 @@ func TestFindMulti(t *testing.T) { c := c events := c.events - f, term := fuzzyfinder.NewWithMockedTerminalV2() + f, term := fuzzyfinder.NewWithMockedTerminal() events = append(events, key(input{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone})) term.SetEventsV2(events...) @@ -370,7 +370,7 @@ func BenchmarkFind(b *testing.B) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - f, term := fuzzyfinder.NewWithMockedTerminalV2() + f, term := fuzzyfinder.NewWithMockedTerminal() events := append(runes("adrele!!"), key(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone})) term.SetEventsV2(events...) @@ -393,7 +393,7 @@ func BenchmarkFind(b *testing.B) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - f, term := fuzzyfinder.NewWithMockedTerminalV2() + f, term := fuzzyfinder.NewWithMockedTerminal() events := append(runes("adrele!!"), key(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone})) term.SetEventsV2(events...) diff --git a/helper_test.go b/helper_test.go index 3085a88..9f65cb5 100644 --- a/helper_test.go +++ b/helper_test.go @@ -5,17 +5,9 @@ func New() *finder { } func NewWithMockedTerminal() (*finder, *TerminalMock) { - f := New() - m := f.UseMockedTerminal() - w, h := 60, 10 // A normally value. - m.SetSize(w, h) - return f, m -} - -func NewWithMockedTerminalV2() (*finder, *TerminalMock) { f := New() m := f.UseMockedTerminalV2() w, h := 60, 10 // A normally value. - m.SetSizeV2(w, h) + m.SetSize(w, h) return f, m }