go-prompt/prompt.go

297 lines
6.9 KiB
Go

package prompt
import (
"bytes"
"os"
"time"
"git.tcp.direct/Mirrors/go-prompt/internal/debug"
)
// Executor is called when user input something text.
type Executor func(string)
// ExitChecker is called after user input to check if prompt must stop and exit go-prompt Run loop.
// User input means: selecting/typing an entry, then, if said entry content matches the ExitChecker function criteria:
// - immediate exit (if breakline is false) without executor called
// - exit after typing <return> (meaning breakline is true), and the executor is called first, before exit.
// Exit means exit go-prompt (not the overall Go program)
type ExitChecker func(in string, breakline bool) bool
// Completer should return the suggest item from Document.
type Completer func(Document) []Suggest
// Prompt is core struct of go-prompt.
type Prompt struct {
in ConsoleParser
buf *Buffer
renderer *Render
executor Executor
history *History
completion *CompletionManager
keyBindings []KeyBind
ASCIICodeBindings []ASCIICodeBind
keyBindMode KeyBindMode
completionOnDown bool
exitChecker ExitChecker
skipTearDown bool
}
// Exec is the struct contains user input context.
type Exec struct {
input string
}
// Run starts prompt.
func (p *Prompt) Run() {
p.skipTearDown = false
defer debug.Teardown()
debug.Log("start prompt")
p.setUp()
defer p.tearDown()
if p.completion.showAtStart {
p.completion.Update(*p.buf.Document())
}
p.renderer.Render(p.buf, p.completion)
bufCh := make(chan []byte, 128)
stopReadBufCh := make(chan struct{})
go p.readBuffer(bufCh, stopReadBufCh)
exitCh := make(chan int)
winSizeCh := make(chan *WinSize)
stopHandleSignalCh := make(chan struct{})
go p.handleSignals(exitCh, winSizeCh, stopHandleSignalCh)
for {
select {
case b := <-bufCh:
if shouldExit, e := p.feed(b); shouldExit {
p.renderer.BreakLine(p.buf)
stopReadBufCh <- struct{}{}
stopHandleSignalCh <- struct{}{}
return
} else if e != nil {
// Stop goroutine to run readBuffer function
stopReadBufCh <- struct{}{}
stopHandleSignalCh <- struct{}{}
// Unset raw mode
// Reset to Blocking mode because returned EAGAIN when still set non-blocking mode.
debug.AssertNoError(p.in.TearDown())
p.executor(e.input)
p.completion.Update(*p.buf.Document())
p.renderer.Render(p.buf, p.completion)
if p.exitChecker != nil && p.exitChecker(e.input, true) {
p.skipTearDown = true
return
}
// Set raw mode
debug.AssertNoError(p.in.Setup())
go p.readBuffer(bufCh, stopReadBufCh)
go p.handleSignals(exitCh, winSizeCh, stopHandleSignalCh)
} else {
p.completion.Update(*p.buf.Document())
p.renderer.Render(p.buf, p.completion)
}
case w := <-winSizeCh:
p.renderer.UpdateWinSize(w)
p.renderer.Render(p.buf, p.completion)
case code := <-exitCh:
p.renderer.BreakLine(p.buf)
p.tearDown()
os.Exit(code)
default:
time.Sleep(10 * time.Millisecond)
}
}
}
func (p *Prompt) feed(b []byte) (shouldExit bool, exec *Exec) {
key := GetKey(b)
p.buf.lastKeyStroke = key
// completion
completing := p.completion.Completing()
p.handleCompletionKeyBinding(key, completing)
switch key {
case Enter, ControlJ, ControlM:
p.renderer.BreakLine(p.buf)
exec = &Exec{input: p.buf.Text()}
p.buf = NewBuffer()
if exec.input != "" {
p.history.Add(exec.input)
}
case ControlC:
p.renderer.BreakLine(p.buf)
p.buf = NewBuffer()
p.history.Clear()
case Up, ControlP:
if !completing { // Don't use p.completion.Completing() because it takes double operation when switch to selected=-1.
if newBuf, changed := p.history.Older(p.buf); changed {
p.buf = newBuf
}
}
case Down, ControlN:
if !completing { // Don't use p.completion.Completing() because it takes double operation when switch to selected=-1.
if newBuf, changed := p.history.Newer(p.buf); changed {
p.buf = newBuf
}
return
}
case ControlD:
if p.buf.Text() == "" {
shouldExit = true
return
}
case NotDefined:
if p.handleASCIICodeBinding(b) {
return
}
p.buf.InsertText(string(b), false, true)
}
shouldExit = p.handleKeyBinding(key)
return
}
func (p *Prompt) handleCompletionKeyBinding(key Key, completing bool) {
switch key {
case Down:
if completing || p.completionOnDown {
p.completion.Next()
}
case Tab, ControlI:
p.completion.Next()
case Up:
if completing {
p.completion.Previous()
}
case BackTab:
p.completion.Previous()
default:
if s, ok := p.completion.GetSelectedSuggestion(); ok {
w := p.buf.Document().GetWordBeforeCursorUntilSeparator(p.completion.wordSeparator)
if w != "" {
p.buf.DeleteBeforeCursor(len([]rune(w)))
}
p.buf.InsertText(s.Text, false, true)
}
p.completion.Reset()
}
}
func (p *Prompt) handleKeyBinding(key Key) bool {
shouldExit := false
for i := range commonKeyBindings {
kb := commonKeyBindings[i]
if kb.Key == key {
kb.Fn(p.buf)
}
}
if p.keyBindMode == EmacsKeyBind {
for i := range emacsKeyBindings {
kb := emacsKeyBindings[i]
if kb.Key == key {
kb.Fn(p.buf)
}
}
}
// Custom key bindings
for i := range p.keyBindings {
kb := p.keyBindings[i]
if kb.Key == key {
kb.Fn(p.buf)
}
}
if p.exitChecker != nil && p.exitChecker(p.buf.Text(), false) {
shouldExit = true
}
return shouldExit
}
func (p *Prompt) handleASCIICodeBinding(b []byte) bool {
checked := false
for _, kb := range p.ASCIICodeBindings {
if bytes.Equal(kb.ASCIICode, b) {
kb.Fn(p.buf)
checked = true
}
}
return checked
}
// Input just returns user input text.
func (p *Prompt) Input() string {
defer debug.Teardown()
debug.Log("start prompt")
p.setUp()
defer p.tearDown()
if p.completion.showAtStart {
p.completion.Update(*p.buf.Document())
}
p.renderer.Render(p.buf, p.completion)
bufCh := make(chan []byte, 128)
stopReadBufCh := make(chan struct{})
go p.readBuffer(bufCh, stopReadBufCh)
for {
select {
case b := <-bufCh:
if shouldExit, e := p.feed(b); shouldExit {
p.renderer.BreakLine(p.buf)
stopReadBufCh <- struct{}{}
return ""
} else if e != nil {
// Stop goroutine to run readBuffer function
stopReadBufCh <- struct{}{}
return e.input
} else {
p.completion.Update(*p.buf.Document())
p.renderer.Render(p.buf, p.completion)
}
default:
time.Sleep(10 * time.Millisecond)
}
}
}
func (p *Prompt) readBuffer(bufCh chan []byte, stopCh chan struct{}) {
debug.Log("start reading buffer")
for {
select {
case <-stopCh:
debug.Log("stop reading buffer")
return
default:
if b, err := p.in.Read(); err == nil && !(len(b) == 1 && b[0] == 0) {
bufCh <- b
}
}
time.Sleep(10 * time.Millisecond)
}
}
func (p *Prompt) setUp() {
debug.AssertNoError(p.in.Setup())
p.renderer.Setup()
p.renderer.UpdateWinSize(p.in.GetWinSize())
}
func (p *Prompt) tearDown() {
if !p.skipTearDown {
debug.AssertNoError(p.in.TearDown())
}
p.renderer.TearDown()
}