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