Commit initial
This commit is contained in:
commit
8694cb0d18
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…
Reference in New Issue
Block a user