Commit initial

This commit is contained in:
Benjamin VAUDOUR 2024-03-02 15:05:33 +01:00
commit 8694cb0d18
9 changed files with 1027 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/test

11
LICENSE Normal file
View File

@ -0,0 +1,11 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

78
README.md Normal file
View File

@ -0,0 +1,78 @@
# gfishline
Gfishline est un prompt Linux inspiré de fish.
Il supporte la coloration syntaxique et lautocomplétion.
## Utilisationtypique
Gfishline peut être utilisé de la façon suivante:
```go
package main
import (
"fmt"
"os"
"gitea.zaclys.net/bvaudour/gfishline"
)
func main() {
prompt := "\033[1;31m> \033[m"
fl := gfishline.New()
// Les deux lignes ci-dessous permettent dutiliser la complétion et le formatage par défaut.
//
// Pour une complétion plus élaborée, utilisez fl.SetCompleter(cb) où cb est une fonction de la forme
// `func (word []string, wordIndex []string, isSpaceIndex bool) (choices []string)`.
//
// Pour un formatage plus élaboré, utilisez fl.SetFormater(cb) où cb est une fonction de la forme
// `func (in [string) (out [string])`.
fl.SetCommands("help", "exit", "print")
fl.SetOptions("--help", "-h")
defer fl.Close()
for {
input := fl.Prompt(prompt) // fl.PromptPassword(prompt) pour masquer ce qui est saisi
if result, ok := input.Ok(); ok {
//@TODO Do some stuff with input
if result == "exit" {
return
}
} else if err, ok := input.Err(); ok {
fmt.Printf("\033[1,31mErreur système : %s\033[m\n", err)
os.Exit(1)
}
}
}
```
## Raccourcis clavier <u>disponibles</u>
| Raccourci clavier | Action |
| -------------------------------------- | ------------------------------------------------------------ |
| Ctrl+A, Home | Retour au début de la saisie |
| Ctrl+E, End | Retour à la fin de la saisie |
| Ctrl+B, Left | Déplacement dun caractère vers la gauche |
| Ctrl+F, Right | Déplacement dun caractère vers la droite |
| Alt+B, Ctrl+Left | Déplacement dun mot vers la gauche |
| Alt+F, Ctrl+Right | Déplacement dun mot vers la droite |
| Ctrl+U | Suppression de tous les caractères du début de la ligne au curseur (curseur non compris) |
| Ctrl+K | Suppression de tous les carctères du curseur (compris) à la fin de la ligne |
| Ctrl+D (si saisie commencée), Del | Suppression du caractère sous le curseur |
| Ctrl+D (si ligne vide) | Termine lapplication (EOF) |
| Ctrl+H, Backspace | Suppression du caractère avant le curseur |
| Alt+D, Alt+Del | Suppression du prochain mot |
| Alt+Backspace | Suppression du précédent mot |
| Ctrl+C | Termine lapplication (avec erreur) |
| Ctrl+P, Up (si ligne vide au départ) | Remonte à lhistorique précédent |
| Ctrl+N, Down (si ligne vide au départ) | Descend dans lhistorique |
| Ctrl+R, Up (si ligne non vide) | Recherche dans lhistorique (par préfixe de ligne) à partir de la fin |
| Ctrl+S, Down (si ligne non vide) | Recherche dans lhistorique (par préfixe de ligne) à partir du début |
| Ctrl+Y | Copie le dernier élément supprimé |
| Alt+Y | Remonte dans lhistorique des éléments supprimés et les copie (implique Ctrl+Y précédemment lancé) |
| Ctrl+G, Cancel | Annule les dernières actions temporaires (historique, recherche, copie, historique de copie) |
| Tab | Complétion suivante |
| Shift+Tab | Complétion précédente |

210
completer.go Normal file
View File

@ -0,0 +1,210 @@
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 {
l, r := n/c, n%c
switch {
case r == 0:
return x*l + y
case x < r:
return x*(l+1) + y
case y >= l:
return -1
default:
return r*(l+1) + (x-r)*l + y
}
}
func indexToCoord(i, c, n int) (x, y int) {
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
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) formatChoices(currentWord string, terminalWidth, terminalHeight int) string {
var buf strings.Builder
l, cursor := c.choices.Len(), c.choices.Cursor()
if l == 0 {
return ""
}
var maxLen int
choices := make([]string, l)
for i := 0; i < l; i++ {
word, _ := c.choices.Index(i).Get()
maxLen, choices[i] = max(atom.VisibleWidth(word)+1, maxLen), word
}
maxCol := min(max(terminalWidth/maxLen, 1), 5)
lines := l / maxCol
if l%maxCol > 0 {
lines++
}
lineMin, lineMax := 0, lines-1
var isPartial bool
if isPartial = terminalHeight < lines; isPartial {
_, cursorLine := indexToCoord(cursor, maxCol, l)
if cursorLine < terminalHeight {
lineMax = min(terminalHeight, lines) - 1
} else {
lineMin, lineMax = cursorLine+1-min(terminalHeight, lines), cursorLine
}
}
colWith := terminalWidth / maxCol
if colWith > maxLen {
colWith = (colWith + maxLen) >> 1
}
for y := lineMin; y <= lineMax; y++ {
if y > 0 {
buf.WriteRune('\n')
}
for x := 0; x < maxCol; x++ {
i := coordToIndex(x, y, maxCol, l)
if i >= 0 {
word := choices[i]
realSize, displaySize := len([]rune(word)), atom.VisibleWidth(word)
size := maxLen + 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, lines)
buf.WriteRune('\n')
buf.WriteString(format.Apply(position, "bg_l_cyan", "black"))
}
return buf.String()
}

462
fishline.go Normal file
View File

@ -0,0 +1,462 @@
package gfishline
import (
"os"
"strings"
"gitea.zaclys.com/bvaudour/gob/format"
"gitea.zaclys.com/bvaudour/gob/option"
"gitea.zaclys.com/bvaudour/gob/shell/console/atom"
)
type Fishline struct {
st *atom.State
complete option.Option[CompleteFunc]
format option.Option[FormatFunc]
commands []string
options []string
proposal string
currentWord string
completion option.Option[*completer]
}
func New() *Fishline {
return &Fishline{
st: atom.NewState(),
}
}
func (fl *Fishline) SetCompleter(cb CompleteFunc) {
if cb == nil {
fl.complete = option.None[CompleteFunc]()
} else {
fl.complete = option.Some(cb)
}
}
func (fl *Fishline) SetFormater(cb FormatFunc) {
if cb == nil {
fl.format = option.None[FormatFunc]()
} else {
fl.format = option.Some(cb)
}
}
func (fl *Fishline) SetCommands(commands ...string) {
fl.commands = commands
}
func (fl *Fishline) SetOptions(options ...string) {
fl.options = options
}
func (fl *Fishline) isCompletionMode() bool {
return fl.completion.IsDefined()
}
func (fl *Fishline) removeCompletion() (ok bool) {
if ok = fl.completion.IsDefined(); ok {
fl.completion = option.None[*completer]()
fl.currentWord = ""
}
return
}
func (fl *Fishline) initCompletion() (completion *completer) {
var ok bool
if completion, ok = fl.completion.Get(); !ok {
buffer, cursor := fl.st.Buffer()
complete, exists := fl.complete.Get()
if !exists {
complete = DefaultCompletion(fl.commands, fl.options)
}
completion = newCompleter(buffer, cursor, complete)
fl.completion = option.Some(completion)
if !completion.needAdd && completion.wordIndex >= 0 && completion.wordIndex < len(completion.words) {
fl.currentWord = completion.words[completion.wordIndex]
}
}
return completion
}
func (fl *Fishline) setCompletion(completion *completer) {
fl.st.UnfocusYank()
fl.st.UnfocusHistory()
fl.st.SaveBuffer()
fl.st.RemoveHistorySearch()
buffer, cursor := completion.buffer()
fl.st.SetBuffer(buffer)
fl.st.SetCursor(cursor)
}
func (fl *Fishline) completionPrev() (ok bool) {
completion := fl.initCompletion()
if ok = completion.prev(); ok {
fl.setCompletion(completion)
}
return
}
func (fl *Fishline) completionNext() (ok bool) {
completion := fl.initCompletion()
if ok = completion.next(); ok {
fl.setCompletion(completion)
}
return
}
func (fl *Fishline) fixState() bool {
ok1 := fl.st.RemoveSavedBuffer()
ok2 := fl.st.RemoveHistorySearch()
ok3 := fl.st.UnfocusHistory()
ok4 := fl.st.UnfocusYank()
ok5 := fl.removeCompletion()
return ok1 || ok2 || ok3 || ok4 || ok5
}
func (fl *Fishline) cancel() (ok bool) {
if ok = fl.st.Cancel(); ok {
fl.removeCompletion()
}
return
}
func (fl *Fishline) yankPrev() (ok bool) {
if ok = fl.st.YankPrev(); ok {
fl.removeCompletion()
}
return
}
func (fl *Fishline) yank() (ok bool) {
if ok = fl.st.Yank(); ok {
fl.removeCompletion()
}
return
}
func (fl *Fishline) historyPrev() (ok bool) {
if ok = fl.st.HistoryPrev(); ok {
fl.removeCompletion()
}
return
}
func (fl *Fishline) historyNext() (ok bool) {
if ok = fl.st.HistoryNext(); ok {
fl.removeCompletion()
}
return
}
func (fl *Fishline) searchHistoryPrev() (ok bool) {
if ok = fl.st.SearchMotivePrev(); ok {
fl.removeCompletion()
}
return
}
func (fl *Fishline) searchHistoryNext() (ok bool) {
if ok = fl.st.SearchMotiveNext(); ok {
fl.removeCompletion()
}
return
}
func (fl *Fishline) execChar(r atom.Char, isPassword bool) (fix, stop bool) {
if stop = r == '\n' || r == '\r'; stop {
sp := newSplitter(fl.st.Buffer())
if stop = sp.isComplete(); stop {
fix, fl.proposal = true, ""
return
}
}
if r != atom.Tab && (r == '\n' || atom.IsPrintable(r)) {
fl.st.Insert(r)
fix = true
if !isPassword {
fl.proposal = fl.st.GetProposal()
}
return
}
var noBeep bool
var changeProposal int
switch r {
case atom.Bs, atom.C_H: // Back
noBeep, fix, changeProposal = fl.st.Back(), true, 1
case atom.C_W: // Back word
noBeep, fix, changeProposal = fl.st.BackWord(), true, 1
case atom.C_D: // Del or reset line
l := fl.st.BufferLen()
if l == 0 {
fl.st.Return()
os.Exit(0)
} else {
noBeep, fix, changeProposal = fl.st.Del(), true, 1
fl.st.Restart()
}
case atom.C_U: // Remove BOL
noBeep, fix, changeProposal = fl.st.RemoveBegin(), true, 1
case atom.C_K: // Remove EOL
noBeep, fix, changeProposal = fl.st.RemoveEnd(), true, 1
case atom.C_A: // Move to BOL
noBeep, fix = fl.st.Begin(), true
fl.proposal = fl.st.GetProposal()
case atom.C_E: // Move to EOL
noBeep, fix = fl.st.End(), true
if !noBeep && fl.proposal != "" {
noBeep, changeProposal = true, -1
fl.st.SetBuffer(fl.proposal)
}
case atom.C_B: // Move left
noBeep, fix = fl.st.Left(), true
case atom.C_F: // Move right
noBeep, fix = fl.st.Right(), true
if !noBeep && fl.proposal != "" {
noBeep, changeProposal = true, -1
fl.st.SetBuffer(fl.proposal)
}
case atom.C_T: // Transpose
noBeep, fix, changeProposal = fl.st.Transpose(), true, 1
case atom.C_P: // Previous history
if !isPassword {
noBeep, changeProposal = fl.historyPrev(), -1
}
case atom.C_N: // Next history
if !isPassword {
noBeep, changeProposal = fl.historyNext(), -1
}
case atom.C_S: // Search history
if !isPassword {
noBeep, changeProposal = fl.searchHistoryNext(), -1
}
case atom.C_R: // Reverse search history
if !isPassword {
noBeep, changeProposal = fl.searchHistoryPrev(), -1
}
case atom.C_G, atom.Esc: // Cancel history
noBeep, fix, changeProposal = fl.cancel(), true, 1
case atom.C_Y: // Yank
noBeep, changeProposal = fl.yank(), 1
case atom.C_C: // Emergency stop
fl.st.Return()
os.Exit(1)
case atom.Tab:
if !isPassword {
noBeep, changeProposal = fl.completionNext(), 1
}
}
if !noBeep {
fl.st.Beep()
} else {
if changeProposal > 0 {
fl.proposal = fl.st.GetProposal()
} else if changeProposal < 0 {
fl.proposal = ""
}
}
return
}
func (fl *Fishline) execSequence(s atom.Sequence, isPassword bool) (fix, stop bool) {
var noBeep bool
var changeProposal int
switch s {
case atom.Up: // Previous history
if !isPassword {
l := fl.st.BufferLen()
changeProposal = -1
if l == 0 || fl.st.IsHistoryMode() {
noBeep = fl.historyPrev()
} else {
noBeep = fl.searchHistoryPrev()
}
}
case atom.Down: // Next history
if !isPassword {
l := fl.st.BufferLen()
changeProposal = -1
if l == 0 || fl.st.IsHistoryMode() {
noBeep = fl.historyNext()
} else {
noBeep = fl.searchHistoryNext()
}
}
case atom.Right: // Move right
noBeep, fix = fl.st.Right(), true
if !noBeep && fl.proposal != "" {
noBeep, changeProposal = true, -1
fl.st.SetBuffer(fl.proposal)
}
case atom.Left: // Move left
noBeep, fix = fl.st.Left(), true
case atom.Ins: // Toggle insert
fl.st.ToggleReplace()
noBeep = true
case atom.Del: // Delete under cursor
noBeep, fix, changeProposal = fl.st.Del(), true, 1
case atom.Home: // Move BOL
noBeep, fix = fl.st.Begin(), true
case atom.End: // Move EOL
noBeep, fix = fl.st.End(), true
if !noBeep && fl.proposal != "" {
noBeep, changeProposal = true, -1
fl.st.SetBuffer(fl.proposal)
}
case atom.A_Bs: // Delete begin of word
noBeep, fix, changeProposal = fl.st.BackWord(), true, 1
case atom.A_D, atom.A_Del: // Delete end of word
noBeep, fix, changeProposal = fl.st.DelWord(), true, 1
case atom.C_Right, atom.A_F: // Next word
noBeep, fix = fl.st.NextWord(), true
case atom.C_Left, atom.A_B: // Begin of word
noBeep, fix = fl.st.PrevWord(), true
case atom.A_Y: // Previous yank
noBeep, changeProposal = fl.yankPrev(), 1
case atom.S_Tab:
noBeep, changeProposal = fl.completionPrev(), 1
}
if !noBeep {
fl.st.Beep()
} else {
if changeProposal > 0 {
fl.proposal = fl.st.GetProposal()
} else if changeProposal < 0 {
fl.proposal = ""
}
}
return
}
func (fl *Fishline) prompt(p string, isPassword bool) (result option.Result[string]) {
if err, ok := fl.st.SetPrompt(p).Get(); ok {
return option.Err[string](err)
}
fl.st.Start()
defer fl.st.Stop()
var refresh func() option.Option[error]
if isPassword {
refresh = func() option.Option[error] { return fl.st.Print("", 0) }
} else {
refresh = func() option.Option[error] {
buffer, cursor := fl.st.Buffer()
var output strings.Builder
if fl.st.IsHistorySearchMode() {
savedBuffer, _ := fl.st.SavedBuffer().Get()
if i := strings.Index(buffer, savedBuffer); i >= 0 {
output.WriteString(buffer[:i])
output.WriteString(format.Apply(savedBuffer, "bg_l_black"))
output.WriteString(buffer[i+len(savedBuffer):])
} else {
output.WriteString(buffer)
}
} else {
cb, ok := fl.format.Get()
if !ok {
cb = DefaultFormat(fl.commands, fl.options)
}
f := newFormater(buffer, cursor, cb)
output.WriteString(f.join())
var suffix string
if fl.proposal != buffer && strings.HasPrefix(fl.proposal, buffer) {
suffix = strings.TrimPrefix(fl.proposal, buffer)
output.WriteString(format.Apply(suffix, "l_black"))
}
if c, ok := fl.completion.Get(); ok {
w, h := fl.st.Width(), fl.st.Height()-1
wb := atom.VisibleWidth(fl.st.GetPrompt() + buffer + suffix)
hb := wb / w
if wb%w > 0 {
hb++
}
choices := c.formatChoices(fl.currentWord, w, h-hb)
if choices != "" {
output.WriteString("\n")
output.WriteString(c.formatChoices(fl.currentWord, w, h-hb))
}
}
}
//return option.None[error]()
return fl.st.Print(output.String(), cursor)
}
}
refresh()
var fix, stop, isErr bool
for {
n := fl.st.Next()
if err, ok := n.Err(); ok {
result, isErr = option.Err[string](err), true
break
}
key, _ := n.Ok()
if s, ok := key.Sequence(); ok {
fix, stop = fl.execSequence(s, isPassword)
} else if c, ok := key.Char(); ok {
fix, stop = fl.execChar(c, isPassword)
}
if fix {
fl.fixState()
}
err := refresh()
if e, ok := err.Get(); ok {
result, isErr = option.Err[string](e), true
break
}
if stop {
break
}
}
if !isErr {
r, _ := fl.st.Buffer()
result = option.Ok(r)
if !isPassword && len(r) > 0 {
fl.st.AppendHistory(r)
}
}
fl.st.Clear()
fl.fixState()
fl.st.Return()
return
}
// Prompt retourne la chaîne saisie.
func (fl *Fishline) Prompt(p string) option.Result[string] { return fl.prompt(p, false) }
// PromptPassword agit comme Prompt mais naffiche pas à lécran ce qui est saisi.
func (fl *Fishline) PromptPassword(p string) option.Result[string] { return fl.prompt(p, true) }
// Close ferme proprement le Readline.
func (fl *Fishline) Close() option.Option[error] { return fl.st.Close() }

97
formater.go Normal file
View File

@ -0,0 +1,97 @@
package gfishline
import (
"slices"
"strconv"
"strings"
"gitea.zaclys.com/bvaudour/gob/format"
"gitea.zaclys.com/bvaudour/gob/shell/scanner"
)
var (
specialChars = []string{"=", "*", "+", "-", ">", "<", "|", ">>", "<<"}
)
func isString(arg string) bool {
if len(arg) > 0 && (arg[0] == '\'' || arg[0] == '"') {
sc := scanner.NewScanner(strings.NewReader(arg))
return sc.Scan()
}
return false
}
func isIncompleteString(arg string) bool {
if len(arg) > 0 && (arg[0] == '\'' || arg[0] == '"') {
sc := scanner.NewScanner(strings.NewReader(arg))
return !sc.Scan()
}
return false
}
func isNumber(arg string) bool {
_, err := strconv.ParseInt(arg, 10, 64)
if err != nil {
_, err = strconv.ParseFloat(arg, 64)
}
return err == nil
}
type FormatFunc func(in []string) (out []string)
func NoFormat(in []string) (out []string) {
return in
}
func DefaultFormat(commands, options []string) FormatFunc {
return func(in []string) (out []string) {
out = make([]string, len(in))
for i, word := range in {
var formatOptions []string
switch {
case i == 0 && len(commands) > 0:
if slices.Contains(commands, word) {
formatOptions = append(formatOptions, "bold")
} else if isString(word) {
formatOptions = append(formatOptions, "yellow")
} else {
formatOptions = append(formatOptions, "l_red", "normal")
}
case i > 0 && len(options) > 0 && slices.Contains(options, word):
formatOptions = append(formatOptions, "l_green", "normal")
case isString(word):
formatOptions = append(formatOptions, "yellow")
case isIncompleteString(word):
formatOptions = append(formatOptions, "l_red", "normal")
case isNumber(word):
formatOptions = append(formatOptions, "l_yellow", "normal")
case slices.Contains(specialChars, word):
formatOptions = append(formatOptions, "l_blue", "normal")
}
if len(formatOptions) > 0 {
out[i] = format.Apply(word, formatOptions...)
} else {
out[i] = word
}
}
return
}
}
type formater struct {
*splitter
}
func newFormater(buffer string, cursor int, cb FormatFunc) *formater {
f := formater{
splitter: newSplitter(buffer, cursor),
}
f.words = cb(f.words)
return &f
}

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module gitea.zaclys.com/bvaudour/fishline
go 1.22.0
require gitea.zaclys.com/bvaudour/gob v0.0.0-20240302132617-3307dec97ce9 // indirect

2
go.sum Normal file
View File

@ -0,0 +1,2 @@
gitea.zaclys.com/bvaudour/gob v0.0.0-20240302132617-3307dec97ce9 h1:rWwHju2jTBu/V7ALO9tYiNJOqqanEDEpfAtNUDGrImQ=
gitea.zaclys.com/bvaudour/gob v0.0.0-20240302132617-3307dec97ce9/go.mod h1:Gw6x0KNKoXv6AMtRkaI+iWM2enVzwHikUSskuEzWQz4=

161
splitter.go Normal file
View File

@ -0,0 +1,161 @@
package gfishline
import (
"strings"
"gitea.zaclys.com/bvaudour/gob/shell/scanner"
)
type splitter struct {
space map[int]string
words []string
cursor int
}
func newSplitter(buffer string, cursor int) *splitter {
sp := splitter{
space: make(map[int]string),
cursor: cursor,
}
sc := scanner.NewRawScanner(strings.NewReader(buffer))
for sc.Scan() {
sp.words = append(sp.words, sc.Text())
}
for i, word := range sp.words {
if strings.HasPrefix(buffer, word) {
buffer = strings.TrimPrefix(buffer, word)
} else {
tmp := strings.TrimLeft(buffer, " ")
sp.space[i] = strings.Repeat(" ", len(buffer)-len(tmp))
buffer = strings.TrimPrefix(tmp, word)
}
}
if len(buffer) > 0 {
l := len(sp.words)
tmp := strings.TrimPrefix(buffer, " ")
if len(tmp) < len(buffer) {
sp.space[l] = strings.Repeat(" ", len(buffer)-len(tmp))
buffer = tmp
l++
}
if len(buffer) > 0 {
var lastWord, lastSpace []rune
for _, r := range buffer {
if r == ' ' {
lastSpace = append(lastSpace, r)
} else {
if len(lastWord) == 0 {
lastWord, lastSpace = append(lastWord, lastSpace...), lastSpace[:0]
}
lastWord = append(lastWord, r)
}
}
sp.words = append(sp.words, string(lastWord))
if len(lastSpace) > 0 {
sp.space[l] = string(lastSpace)
}
}
}
return &sp
}
func (sp *splitter) isComplete() (ok bool) {
l := len(sp.words)
if ok = l == 0; !ok {
sc := scanner.NewScanner(strings.NewReader(sp.words[l-1]))
ok = sc.Scan()
}
return
}
func (sp *splitter) join() string {
var buf strings.Builder
for i, word := range sp.words {
if space, ok := sp.space[i]; ok {
buf.WriteString(space)
}
buf.WriteString(word)
}
if space, ok := sp.space[len(sp.words)]; ok {
buf.WriteString(space)
}
return buf.String()
}
func (sp *splitter) buffer() (buffer string, cursor int) { return sp.join(), sp.cursor }
func (sp *splitter) setWord(index int, word string) {
delta := len([]rune(word)) - len([]rune(sp.words[index]))
sp.words[index] = word
sp.cursor += delta
}
func (sp *splitter) addWord(index int, word string, wordPos int) {
delta, l := len([]rune(word))+2, len(sp.words)
words := make([]string, l+1)
copy(words[:index], sp.words[:index])
copy(words[index+1:], sp.words[index:])
words[index] = word
space := make(map[int]string)
for i, s := range sp.space {
switch {
case i < index:
space[i] = s
case i > index:
space[i+1] = s
default:
space[i] = s[:wordPos]
space[i+1] = " " + s[wordPos:]
}
}
if _, ok := sp.space[index+1]; !ok {
sp.space[index+1] = " "
}
sp.words, sp.space = words, space
sp.cursor += delta
}
func (sp *splitter) compute() (wordIndex, wordCursor int, isIndexSpace bool) {
if isIndexSpace = len(sp.words) == 0 && len(sp.space) == 0; isIndexSpace {
return
}
i := 0
for wi, word := range sp.words {
if space, ok := sp.space[i]; ok {
l := len(space)
i += l
if isIndexSpace = i > sp.cursor; isIndexSpace {
wordIndex, wordCursor = wi, sp.cursor-(i-l)
return
}
}
l := len([]rune(word))
i += l
if i > sp.cursor {
wordIndex, wordCursor, isIndexSpace = wi, sp.cursor-(i-l), false
return
}
}
l := len(sp.words)
wordCursor = sp.cursor - i
if _, isIndexSpace = sp.space[l]; isIndexSpace {
wordIndex = l
} else {
wordIndex = l - 1
}
return
}