gfishline/fishline.go

501 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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