501 lines
11 KiB
Go
501 lines
11 KiB
Go
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) getCompletionInfos(buffer ...string) (w, h int) {
|
||
var str string
|
||
if len(buffer) > 0 {
|
||
str = buffer[0]
|
||
} else {
|
||
str, _ = fl.st.Buffer()
|
||
}
|
||
|
||
w, h = fl.st.Width(), fl.st.Height()-1
|
||
hb, _, _ := atom.CursorOffset(fl.st.GetPrompt()+str, 0, 0)
|
||
|
||
return w, h - hb
|
||
}
|
||
|
||
func (fl *Fishline) completionLeft() (ok bool) {
|
||
completion := fl.initCompletion()
|
||
|
||
if ok = completion.left(fl.getCompletionInfos()); ok {
|
||
fl.setCompletion(completion)
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
func (fl *Fishline) completionRight() (ok bool) {
|
||
completion := fl.initCompletion()
|
||
|
||
if ok = completion.right(fl.getCompletionInfos()); ok {
|
||
fl.setCompletion(completion)
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
func (fl *Fishline) fixState() bool {
|
||
ok1 := fl.st.FixState()
|
||
ok2 := fl.removeCompletion()
|
||
|
||
return ok1 || ok2
|
||
}
|
||
|
||
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 fl.isCompletionMode() {
|
||
noBeep = fl.completionPrev()
|
||
} else 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 fl.isCompletionMode() {
|
||
noBeep = fl.completionNext()
|
||
} else if l == 0 || fl.st.IsHistoryMode() {
|
||
noBeep = fl.historyNext()
|
||
} else {
|
||
noBeep = fl.searchHistoryNext()
|
||
}
|
||
}
|
||
case atom.Right: // Move right
|
||
if fl.isCompletionMode() && !isPassword {
|
||
changeProposal, noBeep = -1, fl.completionRight()
|
||
} else {
|
||
noBeep, fix = fl.st.Right(), true
|
||
if !noBeep && fl.proposal != "" {
|
||
noBeep, changeProposal = true, -1
|
||
fl.st.SetBuffer(fl.proposal)
|
||
}
|
||
}
|
||
case atom.Left: // Move left
|
||
if fl.isCompletionMode() && !isPassword {
|
||
changeProposal, noBeep = -1, fl.completionLeft()
|
||
} else {
|
||
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.getCompletionInfos(buffer + suffix)
|
||
choices := c.formatChoices(fl.currentWord, w, h)
|
||
if choices != "" {
|
||
output.WriteString("\n")
|
||
output.WriteString(choices)
|
||
}
|
||
}
|
||
}
|
||
|
||
//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() }
|