Commit initial
This commit is contained in:
		
						commit
						8694cb0d18
					
				
					 9 changed files with 1027 additions and 0 deletions
				
			
		
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
/test
 | 
			
		||||
							
								
								
									
										11
									
								
								LICENSE
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								LICENSE
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										78
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,78 @@
 | 
			
		|||
# gfishline
 | 
			
		||||
 | 
			
		||||
Gfishline est un prompt Linux inspiré de fish.
 | 
			
		||||
 | 
			
		||||
Il supporte la coloration syntaxique et l’autocomplétion.
 | 
			
		||||
 | 
			
		||||
## Utilisation typique
 | 
			
		||||
 | 
			
		||||
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 d’utiliser 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 d’un caractère vers la gauche                    |
 | 
			
		||||
| Ctrl+F, Right                          | Déplacement d’un caractère vers la droite                    |
 | 
			
		||||
| Alt+B, Ctrl+Left                       | Déplacement d’un mot vers la gauche                          |
 | 
			
		||||
| Alt+F, Ctrl+Right                      | Déplacement d’un 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 l’application (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 l’application (avec erreur)                          |
 | 
			
		||||
| Ctrl+P, Up (si ligne vide au départ)   | Remonte à l’historique précédent                             |
 | 
			
		||||
| Ctrl+N, Down (si ligne vide au départ) | Descend dans l’historique                                    |
 | 
			
		||||
| Ctrl+R, Up (si ligne non vide)         | Recherche dans l’historique (par préfixe de ligne) à partir de la fin |
 | 
			
		||||
| Ctrl+S, Down (si ligne non vide)       | Recherche dans l’historique (par préfixe de ligne) à partir du début |
 | 
			
		||||
| Ctrl+Y                                 | Copie le dernier élément supprimé                            |
 | 
			
		||||
| Alt+Y                                  | Remonte dans l’historique 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
									
								
							
							
						
						
									
										210
									
								
								completer.go
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										462
									
								
								fishline.go
									
										
									
									
									
										Normal 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 n’affiche 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
									
								
							
							
						
						
									
										97
									
								
								formater.go
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										5
									
								
								go.mod
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										2
									
								
								go.sum
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										161
									
								
								splitter.go
									
										
									
									
									
										Normal 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
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		
		Reference in a new issue