266 lines
5.7 KiB
Go
266 lines
5.7 KiB
Go
package prompt
|
|
|
|
import (
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
logfile = "/tmp/go-prompt-debug.log"
|
|
envEnableLog = "GO_PROMPT_ENABLE_LOG"
|
|
)
|
|
|
|
// Executor is called when user input something text.
|
|
type Executor func(string)
|
|
|
|
// 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
|
|
keyBindMode KeyBindMode
|
|
}
|
|
|
|
// Exec is the struct contains user input context.
|
|
type Exec struct {
|
|
input string
|
|
}
|
|
|
|
// Run starts prompt.
|
|
func (p *Prompt) Run() {
|
|
// Logging
|
|
if os.Getenv(envEnableLog) != "true" {
|
|
log.SetOutput(ioutil.Discard)
|
|
} else if f, err := os.OpenFile(logfile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666); err != nil {
|
|
log.SetOutput(ioutil.Discard)
|
|
} else {
|
|
defer f.Close()
|
|
log.SetOutput(f)
|
|
log.Println("[INFO] Logging is enabled.")
|
|
}
|
|
|
|
p.setUp()
|
|
defer p.tearDown()
|
|
|
|
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)
|
|
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.
|
|
p.in.TearDown()
|
|
p.executor(e.input)
|
|
|
|
p.completion.Update(*p.buf.Document())
|
|
p.renderer.Render(p.buf, p.completion)
|
|
|
|
// Set raw mode
|
|
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 := p.in.GetKey(b)
|
|
|
|
// completion
|
|
completing := p.completion.Completing()
|
|
switch key {
|
|
case Down:
|
|
if completing {
|
|
p.completion.Next()
|
|
}
|
|
case Tab, ControlI:
|
|
p.completion.Next()
|
|
case Up:
|
|
if completing {
|
|
p.completion.Previous()
|
|
}
|
|
case BackTab:
|
|
p.completion.Previous()
|
|
case ControlSpace:
|
|
return
|
|
default:
|
|
if s, ok := p.completion.GetSelectedSuggestion(); ok {
|
|
w := p.buf.Document().GetWordBeforeCursor()
|
|
if w != "" {
|
|
p.buf.DeleteBeforeCursor(len([]rune(w)))
|
|
}
|
|
p.buf.InsertText(s.Text, false, true)
|
|
}
|
|
p.completion.Reset()
|
|
}
|
|
|
|
switch key {
|
|
case Enter, ControlJ, ControlM:
|
|
p.renderer.BreakLine(p.buf)
|
|
|
|
exec = &Exec{input: p.buf.Text()}
|
|
log.Printf("[History] %s", 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:
|
|
p.buf.InsertText(string(b), false, true)
|
|
}
|
|
|
|
// Key bindings
|
|
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)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Input just returns user input text.
|
|
func (p *Prompt) Input() string {
|
|
// Logging
|
|
if os.Getenv(envEnableLog) != "true" {
|
|
log.SetOutput(ioutil.Discard)
|
|
} else if f, err := os.OpenFile(logfile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666); err != nil {
|
|
log.SetOutput(ioutil.Discard)
|
|
} else {
|
|
defer f.Close()
|
|
log.SetOutput(f)
|
|
log.Println("[INFO] Logging is enabled.")
|
|
}
|
|
|
|
p.setUp()
|
|
defer p.tearDown()
|
|
|
|
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)
|
|
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{}) {
|
|
log.Printf("[INFO] readBuffer start")
|
|
for {
|
|
time.Sleep(10 * time.Millisecond)
|
|
select {
|
|
case <-stopCh:
|
|
log.Print("[INFO] stop readBuffer")
|
|
return
|
|
default:
|
|
if b, err := p.in.Read(); err == nil {
|
|
bufCh <- b
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Prompt) setUp() {
|
|
p.in.Setup()
|
|
p.renderer.Setup()
|
|
p.renderer.UpdateWinSize(p.in.GetWinSize())
|
|
}
|
|
|
|
func (p *Prompt) tearDown() {
|
|
p.in.TearDown()
|
|
p.renderer.TearDown()
|
|
}
|