go-prompt/prompt.go

297 lines
6.9 KiB
Go
Raw Normal View History

2017-07-15 08:37:54 +00:00
package prompt
import (
2018-04-11 12:55:49 +00:00
"bytes"
2017-07-15 08:37:54 +00:00
"os"
2017-07-15 09:03:18 +00:00
"time"
2017-07-15 08:37:54 +00:00
2021-07-12 01:51:19 +00:00
"git.tcp.direct/tcp.direct/go-prompt/internal/debug"
2017-08-03 07:57:29 +00:00
)
2017-08-21 15:47:15 +00:00
// Executor is called when user input something text.
2017-08-07 09:48:33 +00:00
type Executor func(string)
2017-08-21 15:47:15 +00:00
// 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
2017-08-21 15:47:15 +00:00
// Completer should return the suggest item from Document.
type Completer func(Document) []Suggest
2017-07-15 08:37:54 +00:00
2017-08-21 15:47:15 +00:00
// Prompt is core struct of go-prompt.
2017-07-15 08:37:54 +00:00
type Prompt struct {
in ConsoleParser
buf *Buffer
renderer *Render
executor Executor
history *History
completion *CompletionManager
keyBindings []KeyBind
ASCIICodeBindings []ASCIICodeBind
keyBindMode KeyBindMode
2019-08-26 16:25:26 +00:00
completionOnDown bool
exitChecker ExitChecker
skipTearDown bool
2017-07-15 08:37:54 +00:00
}
2017-08-21 15:47:15 +00:00
// Exec is the struct contains user input context.
2017-08-04 07:22:55 +00:00
type Exec struct {
input string
}
2017-08-21 15:47:15 +00:00
// Run starts prompt.
2017-07-15 08:37:54 +00:00
func (p *Prompt) Run() {
p.skipTearDown = false
2018-12-09 17:33:29 +00:00
defer debug.Teardown()
debug.Log("start prompt")
2017-07-15 08:37:54 +00:00
p.setUp()
defer p.tearDown()
2017-08-03 07:57:29 +00:00
2018-10-18 10:41:40 +00:00
if p.completion.showAtStart {
p.completion.Update(*p.buf.Document())
}
2017-08-09 12:33:47 +00:00
p.renderer.Render(p.buf, p.completion)
2017-07-15 08:37:54 +00:00
bufCh := make(chan []byte, 128)
2017-08-07 09:48:33 +00:00
stopReadBufCh := make(chan struct{})
2017-08-16 03:22:38 +00:00
go p.readBuffer(bufCh, stopReadBufCh)
2017-07-15 08:37:54 +00:00
2017-08-04 02:22:26 +00:00
exitCh := make(chan int)
2017-07-23 15:35:13 +00:00
winSizeCh := make(chan *WinSize)
stopHandleSignalCh := make(chan struct{})
2017-08-16 03:22:38 +00:00
go p.handleSignals(exitCh, winSizeCh, stopHandleSignalCh)
2017-07-15 08:37:54 +00:00
for {
2017-07-15 09:03:18 +00:00
select {
2017-07-15 13:23:47 +00:00
case b := <-bufCh:
2017-08-07 09:48:33 +00:00
if shouldExit, e := p.feed(b); shouldExit {
2017-08-09 13:47:08 +00:00
p.renderer.BreakLine(p.buf)
2018-05-13 10:34:13 +00:00
stopReadBufCh <- struct{}{}
stopHandleSignalCh <- struct{}{}
2017-07-15 09:03:18 +00:00
return
2017-08-07 09:48:33 +00:00
} else if e != nil {
// Stop goroutine to run readBuffer function
stopReadBufCh <- struct{}{}
stopHandleSignalCh <- struct{}{}
2017-08-07 09:48:33 +00:00
// Unset raw mode
2017-08-09 05:04:10 +00:00
// Reset to Blocking mode because returned EAGAIN when still set non-blocking mode.
2018-12-14 13:28:41 +00:00
debug.AssertNoError(p.in.TearDown())
2017-08-07 09:48:33 +00:00
p.executor(e.input)
2017-07-23 15:35:13 +00:00
p.completion.Update(*p.buf.Document())
2017-08-09 12:33:47 +00:00
p.renderer.Render(p.buf, p.completion)
2017-08-07 09:48:33 +00:00
if p.exitChecker != nil && p.exitChecker(e.input, true) {
p.skipTearDown = true
return
}
2017-08-07 09:48:33 +00:00
// Set raw mode
2018-12-14 13:28:41 +00:00
debug.AssertNoError(p.in.Setup())
2017-08-16 03:22:38 +00:00
go p.readBuffer(bufCh, stopReadBufCh)
go p.handleSignals(exitCh, winSizeCh, stopHandleSignalCh)
2017-07-15 09:03:18 +00:00
} else {
p.completion.Update(*p.buf.Document())
2017-08-09 12:33:47 +00:00
p.renderer.Render(p.buf, p.completion)
2017-07-15 09:03:18 +00:00
}
2017-07-15 13:23:47 +00:00
case w := <-winSizeCh:
2017-07-15 09:03:18 +00:00
p.renderer.UpdateWinSize(w)
2017-08-09 12:33:47 +00:00
p.renderer.Render(p.buf, p.completion)
2017-08-04 02:22:26 +00:00
case code := <-exitCh:
2017-08-09 13:47:08 +00:00
p.renderer.BreakLine(p.buf)
2017-08-04 02:22:26 +00:00
p.tearDown()
os.Exit(code)
2017-07-15 09:03:18 +00:00
default:
time.Sleep(10 * time.Millisecond)
2017-07-15 08:37:54 +00:00
}
}
}
2017-08-04 07:22:55 +00:00
func (p *Prompt) feed(b []byte) (shouldExit bool, exec *Exec) {
2018-12-17 17:59:59 +00:00
key := GetKey(b)
2019-12-22 20:18:48 +00:00
p.buf.lastKeyStroke = key
2017-08-13 04:18:53 +00:00
// completion
completing := p.completion.Completing()
2018-02-19 14:34:45 +00:00
p.handleCompletionKeyBinding(key, completing)
2017-08-13 04:18:53 +00:00
switch key {
case Enter, ControlJ, ControlM:
2017-07-23 15:35:13 +00:00
p.renderer.BreakLine(p.buf)
2017-08-04 07:22:55 +00:00
exec = &Exec{input: p.buf.Text()}
2017-07-23 15:35:13 +00:00
p.buf = NewBuffer()
2017-08-04 10:16:39 +00:00
if exec.input != "" {
p.history.Add(exec.input)
}
2017-07-23 15:56:41 +00:00
case ControlC:
2017-07-23 15:35:13 +00:00
p.renderer.BreakLine(p.buf)
p.buf = NewBuffer()
2017-08-04 11:19:12 +00:00
p.history.Clear()
2017-08-13 04:09:45 +00:00
case Up, ControlP:
2017-08-13 04:18:53 +00:00
if !completing { // Don't use p.completion.Completing() because it takes double operation when switch to selected=-1.
2017-08-04 10:16:39 +00:00
if newBuf, changed := p.history.Older(p.buf); changed {
p.buf = newBuf
}
}
2017-08-13 04:09:45 +00:00
case Down, ControlN:
2017-08-13 04:18:53 +00:00
if !completing { // Don't use p.completion.Completing() because it takes double operation when switch to selected=-1.
2017-08-04 10:16:39 +00:00
if newBuf, changed := p.history.Newer(p.buf); changed {
p.buf = newBuf
}
return
}
2017-08-13 04:09:45 +00:00
case ControlD:
if p.buf.Text() == "" {
shouldExit = true
return
}
case NotDefined:
if p.handleASCIICodeBinding(b) {
return
}
2017-08-13 04:09:45 +00:00
p.buf.InsertText(string(b), false, true)
}
shouldExit = p.handleKeyBinding(key)
2018-02-19 14:32:20 +00:00
return
}
2018-02-19 14:34:45 +00:00
func (p *Prompt) handleCompletionKeyBinding(key Key, completing bool) {
switch key {
case Down:
if completing || p.completionOnDown {
2018-02-19 14:34:45 +00:00
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)
2018-02-19 14:34:45 +00:00
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
2017-08-13 04:09:45 +00:00
for i := range commonKeyBindings {
kb := commonKeyBindings[i]
if kb.Key == key {
2017-08-14 16:49:53 +00:00
kb.Fn(p.buf)
2017-08-13 04:09:45 +00:00
}
}
2017-08-13 04:33:51 +00:00
if p.keyBindMode == EmacsKeyBind {
for i := range emacsKeyBindings {
kb := emacsKeyBindings[i]
if kb.Key == key {
2017-08-14 16:49:53 +00:00
kb.Fn(p.buf)
2017-08-13 04:33:51 +00:00
}
2017-08-13 04:09:45 +00:00
}
}
2017-08-13 04:33:51 +00:00
// Custom key bindings
2017-08-13 04:09:45 +00:00
for i := range p.keyBindings {
kb := p.keyBindings[i]
if kb.Key == key {
2017-08-14 16:49:53 +00:00
kb.Fn(p.buf)
2017-08-13 04:09:45 +00:00
}
2017-07-23 15:35:13 +00:00
}
if p.exitChecker != nil && p.exitChecker(p.buf.Text(), false) {
shouldExit = true
}
return shouldExit
2017-07-23 15:35:13 +00:00
}
func (p *Prompt) handleASCIICodeBinding(b []byte) bool {
checked := false
for _, kb := range p.ASCIICodeBindings {
2020-02-24 13:06:18 +00:00
if bytes.Equal(kb.ASCIICode, b) {
kb.Fn(p.buf)
checked = true
}
}
return checked
}
2017-08-21 15:47:15 +00:00
// Input just returns user input text.
2017-08-09 16:03:43 +00:00
func (p *Prompt) Input() string {
2018-12-09 17:33:29 +00:00
defer debug.Teardown()
debug.Log("start prompt")
2017-08-09 16:03:43 +00:00
p.setUp()
defer p.tearDown()
2018-10-18 10:41:40 +00:00
if p.completion.showAtStart {
p.completion.Update(*p.buf.Document())
}
2017-08-09 16:03:43 +00:00
p.renderer.Render(p.buf, p.completion)
bufCh := make(chan []byte, 128)
stopReadBufCh := make(chan struct{})
2017-08-16 03:22:38 +00:00
go p.readBuffer(bufCh, stopReadBufCh)
2017-08-09 16:03:43 +00:00
for {
select {
case b := <-bufCh:
if shouldExit, e := p.feed(b); shouldExit {
p.renderer.BreakLine(p.buf)
2018-05-13 10:34:13 +00:00
stopReadBufCh <- struct{}{}
2017-08-09 16:03:43 +00:00
return ""
} else if e != nil {
// Stop goroutine to run readBuffer function
stopReadBufCh <- struct{}{}
return e.input
} else {
p.completion.Update(*p.buf.Document())
2017-08-09 16:03:43 +00:00
p.renderer.Render(p.buf, p.completion)
}
default:
time.Sleep(10 * time.Millisecond)
}
}
}
2018-02-12 10:12:31 +00:00
func (p *Prompt) readBuffer(bufCh chan []byte, stopCh chan struct{}) {
2018-12-09 17:33:29 +00:00
debug.Log("start reading buffer")
2018-02-12 10:12:31 +00:00
for {
select {
case <-stopCh:
2018-12-09 17:33:29 +00:00
debug.Log("stop reading buffer")
2018-02-12 10:12:31 +00:00
return
default:
if b, err := p.in.Read(); err == nil && !(len(b) == 1 && b[0] == 0) {
2018-02-12 10:12:31 +00:00
bufCh <- b
}
}
time.Sleep(10 * time.Millisecond)
2018-02-12 10:12:31 +00:00
}
}
2017-07-15 08:37:54 +00:00
func (p *Prompt) setUp() {
2018-12-14 13:28:41 +00:00
debug.AssertNoError(p.in.Setup())
2017-07-15 11:22:56 +00:00
p.renderer.Setup()
2017-07-15 16:04:18 +00:00
p.renderer.UpdateWinSize(p.in.GetWinSize())
2017-07-15 08:37:54 +00:00
}
func (p *Prompt) tearDown() {
if !p.skipTearDown {
debug.AssertNoError(p.in.TearDown())
}
2017-07-15 11:22:56 +00:00
p.renderer.TearDown()
2017-07-15 08:37:54 +00:00
}