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() }