287 lines
6.3 KiB
Go
287 lines
6.3 KiB
Go
package gfishline
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"gitea.zaclys.com/bvaudour/gob/format"
|
|
"gitea.zaclys.com/bvaudour/gob/shell/console"
|
|
"gitea.zaclys.com/bvaudour/gob/shell/console/atom"
|
|
)
|
|
|
|
func coordToIndex(x, y, c, n int) int {
|
|
if x < 0 || x >= c || y < 0 {
|
|
return -1
|
|
}
|
|
|
|
l, r := n/c, n%c
|
|
switch {
|
|
case (x < r && y >= l+1) || (x >= r && y >= l):
|
|
return -1
|
|
case r == 0:
|
|
return x*l + y
|
|
case x < r:
|
|
return x*(l+1) + y
|
|
default:
|
|
return r*(l+1) + (x-r)*l + y
|
|
}
|
|
}
|
|
|
|
func indexToCoord(i, c, n int) (x, y int) {
|
|
if i < 0 || i >= n {
|
|
return -1, -1
|
|
}
|
|
|
|
l, r := n/c, n%c
|
|
switch {
|
|
case r == 0:
|
|
x, y = i/l, i%l
|
|
case i < r*(l+1):
|
|
x, y = i/(l+1), i%(l+1)
|
|
default:
|
|
j := i - r*(l+1)
|
|
x, y = r+j/l, j%l
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
type CompleteFunc func(words []string, wordIndex int, isIndexSpace bool) (choices []string)
|
|
|
|
func NoCompletion(words []string, wordIndex int, isIndexSpace bool) (choices []string) {
|
|
return
|
|
}
|
|
|
|
func DefaultCompletion(commands, options []string) CompleteFunc {
|
|
return func(words []string, wordIndex int, isIndexSpace bool) (choices []string) {
|
|
if isIndexSpace {
|
|
if wordIndex <= 0 {
|
|
return commands
|
|
}
|
|
return options
|
|
}
|
|
|
|
word, sl := words[wordIndex], options
|
|
if wordIndex == 0 {
|
|
sl = commands
|
|
}
|
|
|
|
for _, choice := range sl {
|
|
if strings.HasPrefix(choice, word) {
|
|
choices = append(choices, choice)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
type completer struct {
|
|
*splitter
|
|
wordIndex int
|
|
wordPos int
|
|
isInit bool
|
|
needAdd bool
|
|
choices console.Cycler
|
|
}
|
|
|
|
func newCompleter(buffer string, cursor int, complete CompleteFunc) *completer {
|
|
c := completer{
|
|
splitter: newSplitter(buffer, cursor),
|
|
isInit: true,
|
|
}
|
|
|
|
wordIndex, wordCursor, isIndexSpace := c.compute()
|
|
if isIndexSpace && wordCursor == 0 && wordIndex > 0 {
|
|
isIndexSpace = false
|
|
c.cursor--
|
|
}
|
|
|
|
choices := complete(c.words, wordIndex, isIndexSpace)
|
|
slices.Sort(choices)
|
|
if !isIndexSpace {
|
|
current := c.words[wordIndex]
|
|
if slices.Contains(choices, current) {
|
|
choices = append([]string{current}, slices.DeleteFunc(choices, func(e string) bool { return e == current })...)
|
|
}
|
|
}
|
|
choices = slices.Compact(choices)
|
|
|
|
c.choices = atom.NewCycler(true, choices...)
|
|
c.needAdd, c.wordIndex, c.wordPos = isIndexSpace, wordIndex, wordCursor
|
|
|
|
return &c
|
|
}
|
|
|
|
func (c *completer) update(next string) {
|
|
c.setWord(c.wordIndex, next)
|
|
|
|
if c.isInit {
|
|
c.cursor += 2
|
|
if _, ok := c.space[c.wordIndex+1]; !ok {
|
|
c.space[c.wordIndex+1] = " "
|
|
}
|
|
c.isInit = false
|
|
}
|
|
}
|
|
|
|
func (c *completer) add(next string) {
|
|
c.addWord(c.wordIndex, next, c.wordPos)
|
|
c.isInit, c.needAdd = false, false
|
|
}
|
|
|
|
func (c *completer) set() (ok bool) {
|
|
var next string
|
|
if next, ok = console.FocusedElement(c.choices).Get(); !ok {
|
|
return
|
|
}
|
|
|
|
if c.needAdd {
|
|
c.add(next)
|
|
} else {
|
|
c.update(next)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (c *completer) prev() bool { return c.choices.Prev() && c.set() }
|
|
|
|
func (c *completer) next() bool { return c.choices.Next() && c.set() }
|
|
|
|
func (c *completer) move(index int) bool { return c.choices.SetCursor(index) && c.set() }
|
|
|
|
func (c *completer) gridInfo(terminalWidth, terminalHeight int) (nbChoices, colWidth, nbCols, nbLines int, choices []string) {
|
|
nbChoices = c.choices.Len()
|
|
if nbChoices == 0 {
|
|
return
|
|
}
|
|
|
|
var wordWidth int
|
|
choices = make([]string, nbChoices)
|
|
for i := 0; i < nbChoices; i++ {
|
|
word, _ := c.choices.Index(i).Get()
|
|
choices[i], wordWidth = word, max(atom.VisibleWidth(word)+1, wordWidth)
|
|
}
|
|
|
|
nbCols = min(max(terminalWidth/wordWidth, 1), 5)
|
|
nbLines = nbChoices / nbCols
|
|
if nbChoices%nbCols > 0 {
|
|
nbLines++
|
|
}
|
|
|
|
colWidth = terminalWidth / nbCols
|
|
if colWidth > wordWidth {
|
|
colWidth = (colWidth + wordWidth) >> 1
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (c *completer) infoMove(terminalWidth, terminalHeight int) (nbCols, nbLines, nbChoices, x, y, r int) {
|
|
nbChoices, _, nbCols, nbLines, _ = c.gridInfo(terminalWidth, terminalHeight)
|
|
|
|
if nbChoices > 0 {
|
|
i := c.choices.Cursor()
|
|
if i < 0 || i >= nbChoices {
|
|
x, y = -1, -1
|
|
} else {
|
|
x, y = indexToCoord(i, nbCols, nbChoices)
|
|
r = nbChoices % nbCols
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (c *completer) left(terminalWidth, terminalHeight int) bool {
|
|
nbCols, nbLines, nbChoices, x, y, r := c.infoMove(terminalWidth, terminalHeight)
|
|
if nbChoices == 0 || x < 0 {
|
|
return false
|
|
}
|
|
|
|
switch {
|
|
case x > 0:
|
|
x--
|
|
case y > 0:
|
|
x, y = nbCols-1, y-1
|
|
case r > 0:
|
|
x, y = r-1, nbLines-1
|
|
default:
|
|
x, y = nbCols-1, nbLines-1
|
|
}
|
|
|
|
i := coordToIndex(x, y, nbCols, nbChoices)
|
|
|
|
return i >= 0 && c.move(i)
|
|
}
|
|
|
|
func (c *completer) right(terminalWidth, terminalHeight int) bool {
|
|
nbCols, nbLines, nbChoices, x, y, r := c.infoMove(terminalWidth, terminalHeight)
|
|
if nbChoices == 0 || x < 0 {
|
|
return false
|
|
}
|
|
fmt.Println("debug0:", x, y)
|
|
|
|
if x < r-1 || (x < nbCols-1 && (r == 0 || y < nbLines-1)) {
|
|
x++
|
|
} else if y < nbLines-1 {
|
|
x, y = 0, y+1
|
|
} else {
|
|
x, y = 0, 0
|
|
}
|
|
fmt.Println("debug1:", x, y)
|
|
|
|
i := coordToIndex(x, y, nbCols, nbChoices)
|
|
|
|
return i >= 0 && c.move(i)
|
|
}
|
|
|
|
func (c *completer) formatChoices(currentWord string, terminalWidth, terminalHeight int) string {
|
|
nbChoices, colWidth, nbCols, nbLines, choices := c.gridInfo(terminalWidth, terminalHeight)
|
|
if nbChoices == 0 {
|
|
return ""
|
|
}
|
|
|
|
cursor := c.choices.Cursor()
|
|
lineMin, lineMax := 0, nbLines-1
|
|
var isPartial bool
|
|
if isPartial = terminalHeight < nbLines; isPartial {
|
|
_, cursorLine := indexToCoord(cursor, nbCols, nbChoices)
|
|
lineMin = max(0, cursorLine-terminalHeight+1)
|
|
lineMax = lineMin + terminalHeight - 1
|
|
}
|
|
|
|
var buf strings.Builder
|
|
for y := lineMin; y <= lineMax; y++ {
|
|
if y > 0 {
|
|
buf.WriteRune('\n')
|
|
}
|
|
for x := 0; x < nbCols; x++ {
|
|
i := coordToIndex(x, y, nbCols, nbChoices)
|
|
if i >= 0 {
|
|
word := choices[i]
|
|
realSize, displaySize := len([]rune(word)), atom.VisibleWidth(word)
|
|
size := colWidth + realSize - displaySize
|
|
word = strings.TrimPrefix(format.Left(word, size), currentWord)
|
|
if i == cursor {
|
|
buf.WriteString(format.Apply(currentWord, "bg_l_white", "black", "bold", "underline"))
|
|
buf.WriteString(format.Apply(word, "bg_l_white", "black"))
|
|
} else {
|
|
buf.WriteString(format.Apply(currentWord, "bold", "underline"))
|
|
buf.WriteString(word)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if isPartial {
|
|
position := fmt.Sprintf("lignes %d à %d (%d)", lineMin+1, lineMax+1, nbLines)
|
|
buf.WriteRune('\n')
|
|
buf.WriteString(format.Apply(position, "bg_l_cyan", "black"))
|
|
}
|
|
|
|
return buf.String()
|
|
}
|