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 }