add my own session manager
This commit is contained in:
+906
@@ -0,0 +1,906 @@
|
||||
// Package fuzzyfinder provides terminal user interfaces for fuzzy-finding.
|
||||
//
|
||||
// Note that, all functions are not goroutine-safe.
|
||||
package fuzzyfinder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/ktr0731/go-ansisgr"
|
||||
"github.com/ktr0731/go-fuzzyfinder/matching"
|
||||
runewidth "github.com/mattn/go-runewidth"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrAbort is returned from Find* functions if there are no selections.
|
||||
ErrAbort = errors.New("abort")
|
||||
errEntered = errors.New("entered")
|
||||
)
|
||||
|
||||
// Finds the minimum value among the arguments
|
||||
func min(vars ...int) int {
|
||||
min := vars[0]
|
||||
|
||||
for _, i := range vars {
|
||||
if min > i {
|
||||
min = i
|
||||
}
|
||||
}
|
||||
|
||||
return min
|
||||
}
|
||||
|
||||
type state struct {
|
||||
items []string // All item names.
|
||||
allMatched []matching.Matched // All items.
|
||||
matched []matching.Matched // Matched items against the input.
|
||||
|
||||
// x is the current index of the prompt line.
|
||||
x int
|
||||
// cursorX is the position of prompt line.
|
||||
// Note that cursorX is the actual width of input runes.
|
||||
cursorX int
|
||||
|
||||
// The current index of filtered items (matched).
|
||||
// The initial value is 0.
|
||||
y int
|
||||
// cursorY is the position of item line.
|
||||
// Note that the max size of cursorY depends on max height.
|
||||
cursorY int
|
||||
|
||||
input []rune
|
||||
|
||||
// selections holds whether a key is selected or not. Each key is
|
||||
// an index of an item (Matched.Idx). Each value represents the position
|
||||
// which it is selected.
|
||||
selection map[int]int
|
||||
// selectionIdx holds the next index, which is used to a selection's value.
|
||||
selectionIdx int
|
||||
}
|
||||
|
||||
type finder struct {
|
||||
term terminal
|
||||
stateMu sync.RWMutex
|
||||
state state
|
||||
drawTimer *time.Timer
|
||||
eventCh chan struct{}
|
||||
opt *opt
|
||||
|
||||
termEventsChan <-chan tcell.Event
|
||||
}
|
||||
|
||||
func newFinder() *finder {
|
||||
return &finder{}
|
||||
}
|
||||
|
||||
func (f *finder) initFinder(items []string, matched []matching.Matched, opt opt) error {
|
||||
if f.term == nil {
|
||||
screen, err := tcell.NewScreen()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to new screen")
|
||||
}
|
||||
f.term = &termImpl{
|
||||
screen: screen,
|
||||
}
|
||||
if err := f.term.Init(); err != nil {
|
||||
return errors.Wrap(err, "failed to initialize screen")
|
||||
}
|
||||
|
||||
eventsChan := make(chan tcell.Event)
|
||||
go f.term.ChannelEvents(eventsChan, nil)
|
||||
f.termEventsChan = eventsChan
|
||||
}
|
||||
|
||||
f.opt = &opt
|
||||
f.state = state{}
|
||||
|
||||
var cursorPositioned bool
|
||||
if opt.multi {
|
||||
f.state.selection = map[int]int{}
|
||||
f.state.selectionIdx = 1
|
||||
|
||||
// Apply preselection
|
||||
for i := range items {
|
||||
if opt.preselected(i) {
|
||||
f.state.selection[i] = f.state.selectionIdx
|
||||
f.state.selectionIdx++
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// In non-multi mode, set the cursor position to the first preselected item
|
||||
for i := range items {
|
||||
if opt.preselected(i) {
|
||||
cursorPositioned = true
|
||||
// Find the matched item index
|
||||
for j, m := range matched {
|
||||
if m.Idx == i {
|
||||
f.state.y = j
|
||||
f.state.cursorY = min(j, len(matched)-1)
|
||||
break
|
||||
}
|
||||
}
|
||||
break // Only use the first preselected item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f.state.items = items
|
||||
f.state.matched = matched
|
||||
f.state.allMatched = matched
|
||||
|
||||
// If no preselected item is found and beginAtTop is true, set the cursor to the last item
|
||||
if !cursorPositioned && opt.beginAtTop {
|
||||
f.state.cursorY = len(f.state.matched) - 1
|
||||
f.state.y = len(f.state.matched) - 1
|
||||
}
|
||||
|
||||
if !isInTesting() {
|
||||
f.drawTimer = time.AfterFunc(0, func() {
|
||||
f.stateMu.Lock()
|
||||
f._draw()
|
||||
f._drawPreview()
|
||||
f.stateMu.Unlock()
|
||||
f.term.Show()
|
||||
})
|
||||
f.drawTimer.Stop()
|
||||
}
|
||||
f.eventCh = make(chan struct{}, 30) // A large value
|
||||
|
||||
if opt.query != "" {
|
||||
f.state.input = []rune(opt.query)
|
||||
f.state.cursorX = runewidth.StringWidth(opt.query)
|
||||
f.state.x = len(opt.query)
|
||||
f.filter()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *finder) updateItems(items []string, matched []matching.Matched) {
|
||||
f.stateMu.Lock()
|
||||
f.state.items = items
|
||||
f.state.matched = matched
|
||||
f.state.allMatched = matched
|
||||
|
||||
// Apply preselection to any new items
|
||||
if f.opt.multi {
|
||||
for i := 0; i < len(items); i++ {
|
||||
// Check if this item is not already in the selection and should be preselected
|
||||
if _, exists := f.state.selection[i]; !exists && f.opt.preselected(i) {
|
||||
f.state.selection[i] = f.state.selectionIdx
|
||||
f.state.selectionIdx++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f.stateMu.Unlock()
|
||||
f.eventCh <- struct{}{}
|
||||
}
|
||||
|
||||
// _draw is used from draw with a timer.
|
||||
func (f *finder) _draw() {
|
||||
width, height := f.term.Size()
|
||||
f.term.Clear()
|
||||
|
||||
maxWidth := width
|
||||
if f.opt.previewFunc != nil {
|
||||
maxWidth = width/2 - 1
|
||||
}
|
||||
|
||||
maxHeight := height
|
||||
|
||||
// prompt line
|
||||
var promptLinePad int
|
||||
|
||||
for _, r := range f.opt.promptString {
|
||||
style := tcell.StyleDefault.
|
||||
Foreground(tcell.ColorBlue).
|
||||
Background(tcell.ColorDefault)
|
||||
|
||||
f.term.SetContent(promptLinePad, maxHeight-1, r, nil, style)
|
||||
promptLinePad++
|
||||
}
|
||||
var r rune
|
||||
var w int
|
||||
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.SetContent(promptLinePad+w, maxHeight-1, r, nil, style)
|
||||
w += runewidth.RuneWidth(r)
|
||||
}
|
||||
f.term.ShowCursor(promptLinePad+f.state.cursorX, maxHeight-1)
|
||||
|
||||
maxHeight--
|
||||
|
||||
// Header line
|
||||
if len(f.opt.header) > 0 {
|
||||
w = 0
|
||||
for _, r := range runewidth.Truncate(f.opt.header, maxWidth-2, "..") {
|
||||
style := tcell.StyleDefault.
|
||||
Foreground(tcell.ColorGreen).
|
||||
Background(tcell.ColorDefault)
|
||||
f.term.SetContent(2+w, maxHeight-1, r, nil, style)
|
||||
w += runewidth.RuneWidth(r)
|
||||
}
|
||||
maxHeight--
|
||||
}
|
||||
|
||||
// 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.SetContent(2+i, maxHeight-1, r, nil, style)
|
||||
}
|
||||
maxHeight--
|
||||
|
||||
// Item lines
|
||||
itemAreaHeight := maxHeight - 1
|
||||
matched := f.state.matched
|
||||
offset := f.state.cursorY
|
||||
y := f.state.y
|
||||
// From the first (the most bottom) item in the item lines to the end.
|
||||
matched = matched[y-offset:]
|
||||
|
||||
for i, m := range matched {
|
||||
if i > itemAreaHeight {
|
||||
break
|
||||
}
|
||||
if i == f.state.cursorY {
|
||||
style := tcell.StyleDefault.
|
||||
Foreground(tcell.ColorRed).
|
||||
Background(tcell.ColorBlack)
|
||||
|
||||
f.term.SetContent(0, maxHeight-1-i, '>', nil, style)
|
||||
f.term.SetContent(1, maxHeight-1-i, ' ', nil, style)
|
||||
}
|
||||
|
||||
if f.opt.multi {
|
||||
if _, ok := f.state.selection[m.Idx]; ok {
|
||||
style := tcell.StyleDefault.
|
||||
Foreground(tcell.ColorRed).
|
||||
Background(tcell.ColorBlack)
|
||||
|
||||
f.term.SetContent(1, maxHeight-1-i, '>', nil, style)
|
||||
}
|
||||
}
|
||||
|
||||
var posIdx int
|
||||
w := 2
|
||||
for j, r := range []rune(f.state.items[m.Idx]) {
|
||||
style := tcell.StyleDefault.
|
||||
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) {
|
||||
if unicode.ToLower(f.state.input[posIdx]) == unicode.ToLower(r) {
|
||||
style = tcell.StyleDefault.
|
||||
Foreground(tcell.ColorGreen).
|
||||
Background(tcell.ColorDefault)
|
||||
hasHighlighted = true
|
||||
posIdx++
|
||||
}
|
||||
}
|
||||
}
|
||||
if i == f.state.cursorY {
|
||||
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)
|
||||
// Shorten item cells.
|
||||
if w+rw+2 > maxWidth {
|
||||
f.term.SetContent(w, maxHeight-1-i, '.', nil, style)
|
||||
f.term.SetContent(w+1, maxHeight-1-i, '.', nil, style)
|
||||
break
|
||||
} else {
|
||||
f.term.SetContent(w, maxHeight-1-i, r, nil, style)
|
||||
w += rw
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *finder) _drawPreview() {
|
||||
if f.opt.previewFunc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
width, height := f.term.Size()
|
||||
var idx int
|
||||
if len(f.state.matched) == 0 {
|
||||
idx = -1
|
||||
} else {
|
||||
idx = f.state.matched[f.state.y].Idx
|
||||
}
|
||||
|
||||
iter := ansisgr.NewIterator(f.opt.previewFunc(idx, width, height))
|
||||
|
||||
// top line
|
||||
for i := width / 2; i < width; i++ {
|
||||
var r rune
|
||||
switch {
|
||||
case i == width/2:
|
||||
r = '┌'
|
||||
case i == width-1:
|
||||
r = '┐'
|
||||
default:
|
||||
r = '─'
|
||||
}
|
||||
|
||||
style := tcell.StyleDefault.
|
||||
Foreground(tcell.ColorBlack).
|
||||
Background(tcell.ColorDefault)
|
||||
|
||||
f.term.SetContent(i, 0, r, nil, style)
|
||||
}
|
||||
// bottom line
|
||||
for i := width / 2; i < width; i++ {
|
||||
var r rune
|
||||
switch {
|
||||
case i == width/2:
|
||||
r = '└'
|
||||
case i == width-1:
|
||||
r = '┘'
|
||||
default:
|
||||
r = '─'
|
||||
}
|
||||
|
||||
style := tcell.StyleDefault.
|
||||
Foreground(tcell.ColorBlack).
|
||||
Background(tcell.ColorDefault)
|
||||
|
||||
f.term.SetContent(i, height-1, r, nil, style)
|
||||
}
|
||||
// Start with h=1 to exclude each corner rune.
|
||||
const vline = '│'
|
||||
var wvline = runewidth.RuneWidth(vline)
|
||||
for h := 1; h < height-1; h++ {
|
||||
// donePreviewLine indicates the preview string of the current line identified by h is already drawn.
|
||||
var donePreviewLine bool
|
||||
w := width / 2
|
||||
for i := width / 2; i < width; i++ {
|
||||
switch {
|
||||
// Left vertical line.
|
||||
case i == width/2:
|
||||
style := tcell.StyleDefault.
|
||||
Foreground(tcell.ColorBlack).
|
||||
Background(tcell.ColorDefault)
|
||||
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.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.
|
||||
Foreground(tcell.ColorDefault).
|
||||
Background(tcell.ColorDefault)
|
||||
|
||||
f.term.SetContent(w, h, ' ', nil, style)
|
||||
w++
|
||||
default: // Preview text
|
||||
if donePreviewLine {
|
||||
continue
|
||||
}
|
||||
|
||||
r, rstyle, ok := iter.Next()
|
||||
if !ok || r == '\n' {
|
||||
// Consumed all preview characters.
|
||||
donePreviewLine = true
|
||||
continue
|
||||
}
|
||||
|
||||
rw := runewidth.RuneWidth(r)
|
||||
if w+rw > width-1-2 {
|
||||
donePreviewLine = true
|
||||
|
||||
// Discard the rest of the current line.
|
||||
consumeIterator(iter, '\n')
|
||||
|
||||
style := tcell.StyleDefault.
|
||||
Foreground(tcell.ColorDefault).
|
||||
Background(tcell.ColorDefault)
|
||||
|
||||
f.term.SetContent(w, h, '.', nil, style)
|
||||
f.term.SetContent(w+1, h, '.', nil, style)
|
||||
|
||||
w += 2
|
||||
continue
|
||||
}
|
||||
|
||||
style := tcell.StyleDefault
|
||||
if color, ok := rstyle.Foreground(); ok {
|
||||
switch color.Mode() {
|
||||
case ansisgr.Mode16:
|
||||
style = style.Foreground(tcell.PaletteColor(color.Value() - 30))
|
||||
case ansisgr.Mode256:
|
||||
style = style.Foreground(tcell.PaletteColor(color.Value()))
|
||||
case ansisgr.ModeRGB:
|
||||
r, g, b := color.RGB()
|
||||
style = style.Foreground(tcell.NewRGBColor(int32(r), int32(g), int32(b)))
|
||||
}
|
||||
}
|
||||
if color, valid := rstyle.Background(); valid {
|
||||
switch color.Mode() {
|
||||
case ansisgr.Mode16:
|
||||
style = style.Background(tcell.PaletteColor(color.Value() - 40))
|
||||
case ansisgr.Mode256:
|
||||
style = style.Background(tcell.PaletteColor(color.Value()))
|
||||
case ansisgr.ModeRGB:
|
||||
r, g, b := color.RGB()
|
||||
style = style.Background(tcell.NewRGBColor(int32(r), int32(g), int32(b)))
|
||||
}
|
||||
}
|
||||
|
||||
style = style.
|
||||
Bold(rstyle.Bold()).
|
||||
Dim(rstyle.Dim()).
|
||||
Italic(rstyle.Italic()).
|
||||
Underline(rstyle.Underline()).
|
||||
Blink(rstyle.Blink()).
|
||||
Reverse(rstyle.Reverse()).
|
||||
StrikeThrough(rstyle.Strikethrough())
|
||||
f.term.SetContent(w, h, r, nil, style)
|
||||
w += rw
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *finder) draw(d time.Duration) {
|
||||
f.stateMu.RLock()
|
||||
defer f.stateMu.RUnlock()
|
||||
|
||||
if isInTesting() {
|
||||
// Don't use goroutine scheduling.
|
||||
f._draw()
|
||||
f._drawPreview()
|
||||
f.term.Show()
|
||||
} else {
|
||||
f.drawTimer.Reset(d)
|
||||
}
|
||||
}
|
||||
|
||||
// readKey reads a key input.
|
||||
// It returns ErrAbort if esc, CTRL-C or CTRL-D keys are inputted,
|
||||
// errEntered in case of enter key, and a context error when the passed
|
||||
// context is cancelled.
|
||||
func (f *finder) readKey(ctx context.Context) error {
|
||||
f.stateMu.RLock()
|
||||
prevInputLen := len(f.state.input)
|
||||
f.stateMu.RUnlock()
|
||||
defer func() {
|
||||
f.stateMu.RLock()
|
||||
currentInputLen := len(f.state.input)
|
||||
f.stateMu.RUnlock()
|
||||
if prevInputLen != currentInputLen {
|
||||
f.eventCh <- struct{}{}
|
||||
}
|
||||
}()
|
||||
|
||||
var e tcell.Event
|
||||
|
||||
select {
|
||||
case ee := <-f.termEventsChan:
|
||||
e = ee
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
f.stateMu.Lock()
|
||||
defer f.stateMu.Unlock()
|
||||
|
||||
_, screenHeight := f.term.Size()
|
||||
matchedLinesCount := len(f.state.matched)
|
||||
|
||||
// Max number of lines to scroll by using PgUp and PgDn
|
||||
var pageScrollBy = screenHeight - 3
|
||||
|
||||
switch e := e.(type) {
|
||||
case *tcell.EventKey:
|
||||
switch e.Key() {
|
||||
case tcell.KeyEsc, tcell.KeyCtrlC, tcell.KeyCtrlD:
|
||||
return ErrAbort
|
||||
case tcell.KeyBackspace, tcell.KeyBackspace2:
|
||||
if len(f.state.input) == 0 {
|
||||
return nil
|
||||
}
|
||||
if f.state.x == 0 {
|
||||
return nil
|
||||
}
|
||||
x := f.state.x
|
||||
f.state.cursorX -= runewidth.RuneWidth(f.state.input[x-1])
|
||||
f.state.x--
|
||||
f.state.input = append(f.state.input[:x-1], f.state.input[x:]...)
|
||||
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 tcell.KeyEnter:
|
||||
return errEntered
|
||||
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 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 tcell.KeyCtrlA, tcell.KeyHome:
|
||||
f.state.cursorX = 0
|
||||
f.state.x = 0
|
||||
case tcell.KeyCtrlE, tcell.KeyEnd:
|
||||
f.state.cursorX = runewidth.StringWidth(string(f.state.input))
|
||||
f.state.x = len(f.state.input)
|
||||
case tcell.KeyCtrlW:
|
||||
in := f.state.input[:f.state.x]
|
||||
inStr := string(in)
|
||||
pos := strings.LastIndex(strings.TrimRightFunc(inStr, unicode.IsSpace), " ")
|
||||
if pos == -1 {
|
||||
f.state.input = []rune{}
|
||||
f.state.cursorX = 0
|
||||
f.state.x = 0
|
||||
return nil
|
||||
}
|
||||
pos = utf8.RuneCountInString(inStr[:pos])
|
||||
newIn := f.state.input[:pos+1]
|
||||
f.state.input = newIn
|
||||
f.state.cursorX = runewidth.StringWidth(string(newIn))
|
||||
f.state.x = len(newIn)
|
||||
case tcell.KeyCtrlU:
|
||||
f.state.input = f.state.input[f.state.x:]
|
||||
f.state.cursorX = 0
|
||||
f.state.x = 0
|
||||
case tcell.KeyUp, tcell.KeyCtrlK, tcell.KeyCtrlP:
|
||||
if f.state.y+1 < matchedLinesCount {
|
||||
f.state.y++
|
||||
}
|
||||
if f.state.cursorY+1 < min(matchedLinesCount, screenHeight-2) {
|
||||
f.state.cursorY++
|
||||
}
|
||||
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 tcell.KeyPgUp:
|
||||
f.state.y += min(pageScrollBy, matchedLinesCount-1-f.state.y)
|
||||
maxCursorY := min(screenHeight-3, matchedLinesCount-1)
|
||||
f.state.cursorY += min(pageScrollBy, maxCursorY-f.state.cursorY)
|
||||
case tcell.KeyPgDn:
|
||||
f.state.y -= min(pageScrollBy, f.state.y)
|
||||
f.state.cursorY -= min(pageScrollBy, f.state.cursorY)
|
||||
case tcell.KeyTab:
|
||||
if !f.opt.multi {
|
||||
return nil
|
||||
}
|
||||
idx := f.state.matched[f.state.y].Idx
|
||||
if _, ok := f.state.selection[idx]; ok {
|
||||
delete(f.state.selection, idx)
|
||||
} else {
|
||||
f.state.selection[idx] = f.state.selectionIdx
|
||||
f.state.selectionIdx++
|
||||
}
|
||||
if f.state.y > 0 {
|
||||
f.state.y--
|
||||
}
|
||||
if f.state.cursorY > 0 {
|
||||
f.state.cursorY--
|
||||
}
|
||||
default:
|
||||
if e.Rune() != 0 {
|
||||
width, _ := f.term.Size()
|
||||
maxLineWidth := width - 2 - 1
|
||||
if len(f.state.input)+1 > maxLineWidth {
|
||||
// Discard inputted rune.
|
||||
return nil
|
||||
}
|
||||
|
||||
x := f.state.x
|
||||
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 *tcell.EventResize:
|
||||
f.term.Clear()
|
||||
|
||||
width, height := f.term.Size()
|
||||
itemAreaHeight := height - 2 - 1
|
||||
if itemAreaHeight >= 0 && f.state.cursorY > itemAreaHeight {
|
||||
f.state.cursorY = itemAreaHeight
|
||||
}
|
||||
|
||||
maxLineWidth := width - 2 - 1
|
||||
if maxLineWidth < 0 {
|
||||
f.state.input = nil
|
||||
f.state.cursorX = 0
|
||||
f.state.x = 0
|
||||
} else if len(f.state.input)+1 > maxLineWidth {
|
||||
// Discard inputted rune.
|
||||
f.state.input = f.state.input[:maxLineWidth]
|
||||
f.state.cursorX = runewidth.StringWidth(string(f.state.input))
|
||||
f.state.x = maxLineWidth
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *finder) filter() {
|
||||
f.stateMu.RLock()
|
||||
if len(f.state.input) == 0 {
|
||||
f.stateMu.RUnlock()
|
||||
f.stateMu.Lock()
|
||||
defer f.stateMu.Unlock()
|
||||
f.state.matched = f.state.allMatched
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: If input is not delete operation, it is able to
|
||||
// reduce total iteration.
|
||||
// FindAll may take a lot of time, so it is desired to use RLock to avoid goroutine blocking.
|
||||
matchedItems := matching.FindAll(string(f.state.input), f.state.items, matching.WithMode(matching.Mode(f.opt.mode)))
|
||||
f.stateMu.RUnlock()
|
||||
|
||||
f.stateMu.Lock()
|
||||
defer f.stateMu.Unlock()
|
||||
f.state.matched = matchedItems
|
||||
if len(f.state.matched) == 0 {
|
||||
f.state.cursorY = 0
|
||||
f.state.y = 0
|
||||
return
|
||||
}
|
||||
|
||||
// If we are in single-select mode, try to move cursor to the first preselected item
|
||||
// that's still in the matched results
|
||||
if !f.opt.multi {
|
||||
for i, m := range f.state.matched {
|
||||
if f.opt.preselected(m.Idx) {
|
||||
f.state.y = i
|
||||
f.state.cursorY = min(i, len(f.state.matched)-1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case f.state.cursorY >= len(f.state.matched):
|
||||
f.state.cursorY = len(f.state.matched) - 1
|
||||
f.state.y = len(f.state.matched) - 1
|
||||
case f.state.y >= len(f.state.matched):
|
||||
f.state.y = len(f.state.matched) - 1
|
||||
}
|
||||
}
|
||||
|
||||
func (f *finder) find(slice interface{}, itemFunc func(i int) string, opts []Option) ([]int, error) {
|
||||
if itemFunc == nil {
|
||||
return nil, errors.New("itemFunc must not be nil")
|
||||
}
|
||||
|
||||
opt := defaultOption
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(slice)
|
||||
if opt.hotReload && (rv.Kind() != reflect.Ptr || reflect.Indirect(rv).Kind() != reflect.Slice) {
|
||||
return nil, errors.Errorf("the first argument must be a pointer to a slice, but got %T", slice)
|
||||
} else if !opt.hotReload && rv.Kind() != reflect.Slice {
|
||||
return nil, errors.Errorf("the first argument must be a slice, but got %T", slice)
|
||||
}
|
||||
|
||||
makeItems := func(sliceLen int) ([]string, []matching.Matched) {
|
||||
items := make([]string, sliceLen)
|
||||
matched := make([]matching.Matched, sliceLen)
|
||||
for i := 0; i < sliceLen; i++ {
|
||||
items[i] = itemFunc(i)
|
||||
matched[i] = matching.Matched{Idx: i} //nolint:exhaustivestruct
|
||||
}
|
||||
return items, matched
|
||||
}
|
||||
|
||||
var (
|
||||
items []string
|
||||
matched []matching.Matched
|
||||
)
|
||||
|
||||
var parentContext context.Context
|
||||
if opt.context != nil {
|
||||
parentContext = opt.context
|
||||
} else {
|
||||
parentContext = context.Background()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(parentContext)
|
||||
defer cancel()
|
||||
|
||||
inited := make(chan struct{})
|
||||
if opt.hotReload && rv.Kind() == reflect.Ptr {
|
||||
opt.hotReloadLock.Lock()
|
||||
rvv := reflect.Indirect(rv)
|
||||
items, matched = makeItems(rvv.Len())
|
||||
opt.hotReloadLock.Unlock()
|
||||
|
||||
go func() {
|
||||
<-inited
|
||||
|
||||
var prev int
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(30 * time.Millisecond):
|
||||
opt.hotReloadLock.Lock()
|
||||
curr := rvv.Len()
|
||||
if prev != curr {
|
||||
items, matched = makeItems(curr)
|
||||
f.updateItems(items, matched)
|
||||
}
|
||||
opt.hotReloadLock.Unlock()
|
||||
prev = curr
|
||||
}
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
items, matched = makeItems(rv.Len())
|
||||
}
|
||||
|
||||
if err := f.initFinder(items, matched, opt); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to initialize the fuzzy finder")
|
||||
}
|
||||
|
||||
if !isInTesting() {
|
||||
defer f.term.Fini()
|
||||
}
|
||||
|
||||
close(inited)
|
||||
|
||||
if opt.selectOne && len(f.state.matched) == 1 {
|
||||
return []int{f.state.matched[0].Idx}, nil
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-f.eventCh:
|
||||
f.filter()
|
||||
f.draw(0)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
f.draw(10 * time.Millisecond)
|
||||
|
||||
err := f.readKey(ctx)
|
||||
// hack for earning time to filter exec
|
||||
if isInTesting() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
switch {
|
||||
case errors.Is(err, ErrAbort):
|
||||
return nil, ErrAbort
|
||||
case errors.Is(err, errEntered):
|
||||
f.stateMu.RLock()
|
||||
defer f.stateMu.RUnlock()
|
||||
|
||||
if len(f.state.matched) == 0 {
|
||||
return nil, ErrAbort
|
||||
}
|
||||
if f.opt.multi {
|
||||
if len(f.state.selection) == 0 {
|
||||
return []int{f.state.matched[f.state.y].Idx}, nil
|
||||
}
|
||||
poss, idxs := make([]int, 0, len(f.state.selection)), make([]int, 0, len(f.state.selection))
|
||||
for idx, pos := range f.state.selection {
|
||||
idxs = append(idxs, idx)
|
||||
poss = append(poss, pos)
|
||||
}
|
||||
sort.Slice(idxs, func(i, j int) bool {
|
||||
return poss[i] < poss[j]
|
||||
})
|
||||
return idxs, nil
|
||||
}
|
||||
return []int{f.state.matched[f.state.y].Idx}, nil
|
||||
case err != nil:
|
||||
return nil, errors.Wrap(err, "failed to read a key")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// when the cursor which points to the currently selected item is changed.
|
||||
// If itemFunc is nil, Find returns an error.
|
||||
//
|
||||
// itemFunc receives an argument i, which is the index of the item currently
|
||||
// selected.
|
||||
//
|
||||
// Find returns ErrAbort if a call to Find is finished with no selection.
|
||||
func Find(slice interface{}, itemFunc func(i int) string, opts ...Option) (int, error) {
|
||||
f := newFinder()
|
||||
return f.Find(slice, itemFunc, opts...)
|
||||
}
|
||||
|
||||
func (f *finder) Find(slice interface{}, itemFunc func(i int) string, opts ...Option) (int, error) {
|
||||
res, err := f.find(slice, itemFunc, opts)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res[0], err
|
||||
}
|
||||
|
||||
// FindMulti is nearly the same as Find. The only difference from Find is that
|
||||
// the user can select multiple items at once, by using the tab key.
|
||||
func FindMulti(slice interface{}, itemFunc func(i int) string, opts ...Option) ([]int, error) {
|
||||
f := newFinder()
|
||||
return f.FindMulti(slice, itemFunc, opts...)
|
||||
}
|
||||
|
||||
func (f *finder) FindMulti(slice interface{}, itemFunc func(i int) string, opts ...Option) ([]int, error) {
|
||||
opts = append(opts, withMulti())
|
||||
res, err := f.find(slice, itemFunc, opts)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func isInTesting() bool {
|
||||
return flag.Lookup("test.v") != nil
|
||||
}
|
||||
|
||||
func consumeIterator(iter *ansisgr.Iterator, r rune) {
|
||||
for {
|
||||
r, _, ok := iter.Next()
|
||||
if !ok || r == '\n' {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user