| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296 |
- package prompt
- import (
- "bytes"
- "os"
- "time"
- "github.com/c-bata/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()
- }
|