gfishline/fishline.go

501 lines
11 KiB
Go
Raw Permalink Normal View History

2024-03-02 14:05:33 +00:00
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
}
2024-03-02 14:05:33 +00:00
func (fl *Fishline) fixState() bool {
ok1 := fl.st.FixState()
ok2 := fl.removeCompletion()
2024-03-02 14:05:33 +00:00
return ok1 || ok2
2024-03-02 14:05:33 +00:00
}
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() {
2024-03-02 14:05:33 +00:00
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() {
2024-03-02 14:05:33 +00:00
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)
}
2024-03-02 14:05:33 +00:00
}
case atom.Left: // Move left
if fl.isCompletionMode() && !isPassword {
changeProposal, noBeep = -1, fl.completionLeft()
} else {
noBeep, fix = fl.st.Left(), true
}
2024-03-02 14:05:33 +00:00
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)
2024-03-02 14:05:33 +00:00
if choices != "" {
output.WriteString("\n")
output.WriteString(choices)
2024-03-02 14:05:33 +00:00
}
}
}
//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() }