2017-07-15 08:37:54 +00:00
|
|
|
package prompt
|
|
|
|
|
|
|
|
import (
|
2017-08-03 07:57:29 +00:00
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
2017-07-15 08:37:54 +00:00
|
|
|
"os"
|
|
|
|
"os/signal"
|
2017-07-15 13:23:47 +00:00
|
|
|
"syscall"
|
2017-07-15 09:03:18 +00:00
|
|
|
"time"
|
2017-07-15 08:37:54 +00:00
|
|
|
)
|
|
|
|
|
2017-08-03 07:57:29 +00:00
|
|
|
const (
|
|
|
|
logfile = "/tmp/go-prompt-debug.log"
|
|
|
|
envEnableLog = "GO_PROMPT_ENABLE_LOG"
|
|
|
|
)
|
|
|
|
|
2017-08-07 09:48:33 +00:00
|
|
|
type Executor func(string)
|
2017-08-04 11:30:50 +00:00
|
|
|
type Completer func(string) []Suggest
|
2017-07-15 08:37:54 +00:00
|
|
|
|
|
|
|
type Prompt struct {
|
2017-08-06 06:21:14 +00:00
|
|
|
in ConsoleParser
|
|
|
|
buf *Buffer
|
|
|
|
renderer *Render
|
|
|
|
executor Executor
|
|
|
|
history *History
|
|
|
|
completion *CompletionManager
|
2017-07-15 08:37:54 +00:00
|
|
|
}
|
|
|
|
|
2017-08-04 07:22:55 +00:00
|
|
|
type Exec struct {
|
|
|
|
input string
|
|
|
|
}
|
|
|
|
|
2017-07-15 08:37:54 +00:00
|
|
|
func (p *Prompt) Run() {
|
2017-08-10 16:18:12 +00:00
|
|
|
// 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.")
|
|
|
|
}
|
|
|
|
|
2017-07-15 08:37:54 +00:00
|
|
|
p.setUp()
|
|
|
|
defer p.tearDown()
|
2017-08-03 07:57:29 +00:00
|
|
|
|
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-09 16:03:43 +00:00
|
|
|
go 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)
|
2017-07-15 09:03:18 +00:00
|
|
|
go handleSignals(p.in, exitCh, winSizeCh)
|
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)
|
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{}{}
|
|
|
|
|
|
|
|
// Unset raw mode
|
2017-08-09 05:04:10 +00:00
|
|
|
// Reset to Blocking mode because returned EAGAIN when still set non-blocking mode.
|
2017-08-09 07:20:31 +00:00
|
|
|
p.in.TearDown()
|
2017-08-07 09:48:33 +00:00
|
|
|
p.executor(e.input)
|
2017-07-23 15:35:13 +00:00
|
|
|
|
2017-08-09 12:33:47 +00:00
|
|
|
p.completion.Update(p.buf.Text())
|
|
|
|
p.renderer.Render(p.buf, p.completion)
|
2017-08-07 09:48:33 +00:00
|
|
|
|
|
|
|
// Set raw mode
|
2017-08-09 12:28:34 +00:00
|
|
|
p.in.Setup()
|
2017-08-09 16:03:43 +00:00
|
|
|
go readBuffer(bufCh, stopReadBufCh)
|
2017-07-15 09:03:18 +00:00
|
|
|
} else {
|
2017-08-09 12:33:47 +00:00
|
|
|
p.completion.Update(p.buf.Text())
|
|
|
|
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) {
|
2017-08-03 06:53:38 +00:00
|
|
|
key := p.in.GetKey(b)
|
2017-07-23 15:56:41 +00:00
|
|
|
|
2017-08-03 06:53:38 +00:00
|
|
|
switch key {
|
2017-08-03 07:57:29 +00:00
|
|
|
case ControlJ, Enter:
|
2017-08-09 12:33:47 +00:00
|
|
|
if s, ok := p.completion.GetSelectedSuggestion(); ok {
|
2017-07-23 15:35:13 +00:00
|
|
|
w := p.buf.Document().GetWordBeforeCursor()
|
|
|
|
if w != "" {
|
|
|
|
p.buf.DeleteBeforeCursor(len([]rune(w)))
|
|
|
|
}
|
2017-08-09 12:33:47 +00:00
|
|
|
p.buf.InsertText(s.Text, false, true)
|
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()}
|
|
|
|
log.Printf("[History] %s", p.buf.Text())
|
2017-07-23 15:35:13 +00:00
|
|
|
p.buf = NewBuffer()
|
2017-08-04 11:19:12 +00:00
|
|
|
p.completion.Reset()
|
2017-08-04 10:16:39 +00:00
|
|
|
if exec.input != "" {
|
|
|
|
p.history.Add(exec.input)
|
|
|
|
}
|
2017-08-09 16:26:32 +00:00
|
|
|
case ControlA:
|
|
|
|
x := []rune(p.buf.Document().TextBeforeCursor())
|
|
|
|
p.buf.CursorLeft(len(x))
|
|
|
|
case ControlE:
|
|
|
|
x := []rune(p.buf.Document().TextAfterCursor())
|
|
|
|
p.buf.CursorRight(len(x))
|
|
|
|
case ControlK:
|
|
|
|
x := []rune(p.buf.Document().TextAfterCursor())
|
|
|
|
p.buf.Delete(len(x))
|
|
|
|
case ControlU:
|
|
|
|
x := []rune(p.buf.Document().TextBeforeCursor())
|
|
|
|
p.buf.DeleteBeforeCursor(len(x))
|
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.completion.Reset()
|
|
|
|
p.history.Clear()
|
2017-07-23 15:56:41 +00:00
|
|
|
case ControlD:
|
2017-08-09 16:26:32 +00:00
|
|
|
if p.buf.Text() == "" {
|
|
|
|
shouldExit = true
|
|
|
|
} else {
|
|
|
|
p.buf.Delete(1)
|
|
|
|
}
|
2017-07-23 15:56:41 +00:00
|
|
|
case Up:
|
2017-08-04 11:19:12 +00:00
|
|
|
if !p.completion.Completing() {
|
2017-08-04 10:16:39 +00:00
|
|
|
if newBuf, changed := p.history.Older(p.buf); changed {
|
|
|
|
p.buf = newBuf
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2017-07-23 15:56:41 +00:00
|
|
|
fallthrough
|
|
|
|
case BackTab:
|
2017-08-04 11:19:12 +00:00
|
|
|
p.completion.Previous()
|
2017-07-23 15:56:41 +00:00
|
|
|
case Down:
|
2017-08-04 11:19:12 +00:00
|
|
|
if !p.completion.Completing() {
|
2017-08-04 10:16:39 +00:00
|
|
|
if newBuf, changed := p.history.Newer(p.buf); changed {
|
|
|
|
p.buf = newBuf
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2017-07-23 15:56:41 +00:00
|
|
|
fallthrough
|
2017-08-04 02:22:26 +00:00
|
|
|
case Tab, ControlI:
|
2017-08-04 11:19:12 +00:00
|
|
|
p.completion.Next()
|
2017-08-03 06:53:38 +00:00
|
|
|
case Left:
|
|
|
|
p.buf.CursorLeft(1)
|
|
|
|
case Right:
|
|
|
|
p.buf.CursorRight(1)
|
|
|
|
case Backspace:
|
2017-08-09 12:33:47 +00:00
|
|
|
if s, ok := p.completion.GetSelectedSuggestion(); ok {
|
2017-08-04 07:22:55 +00:00
|
|
|
w := p.buf.Document().GetWordBeforeCursor()
|
|
|
|
if w != "" {
|
|
|
|
p.buf.DeleteBeforeCursor(len([]rune(w)))
|
|
|
|
}
|
2017-08-09 12:33:47 +00:00
|
|
|
p.buf.InsertText(s.Text, false, true)
|
2017-08-04 11:19:12 +00:00
|
|
|
p.completion.Reset()
|
2017-08-04 07:22:55 +00:00
|
|
|
}
|
2017-08-03 06:53:38 +00:00
|
|
|
p.buf.DeleteBeforeCursor(1)
|
|
|
|
case NotDefined:
|
2017-08-09 12:33:47 +00:00
|
|
|
if s, ok := p.completion.GetSelectedSuggestion(); ok {
|
2017-08-03 06:53:38 +00:00
|
|
|
w := p.buf.Document().GetWordBeforeCursor()
|
|
|
|
if w != "" {
|
|
|
|
p.buf.DeleteBeforeCursor(len([]rune(w)))
|
|
|
|
}
|
2017-08-09 12:33:47 +00:00
|
|
|
p.buf.InsertText(s.Text, false, true)
|
2017-08-03 06:53:38 +00:00
|
|
|
}
|
2017-08-04 11:19:12 +00:00
|
|
|
p.completion.Reset()
|
2017-08-03 06:53:38 +00:00
|
|
|
p.buf.InsertText(string(b), false, true)
|
2017-07-23 15:56:41 +00:00
|
|
|
default:
|
2017-08-04 11:19:12 +00:00
|
|
|
p.completion.Reset()
|
2017-07-23 15:35:13 +00:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-08-09 16:03:43 +00:00
|
|
|
func (p *Prompt) Input() string {
|
2017-08-10 16:18:12 +00:00
|
|
|
// 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.")
|
|
|
|
}
|
|
|
|
|
2017-08-09 16:03:43 +00:00
|
|
|
p.setUp()
|
|
|
|
defer p.tearDown()
|
|
|
|
|
|
|
|
p.renderer.Render(p.buf, p.completion)
|
|
|
|
bufCh := make(chan []byte, 128)
|
|
|
|
stopReadBufCh := make(chan struct{})
|
|
|
|
go 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.Text())
|
|
|
|
p.renderer.Render(p.buf, p.completion)
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-15 08:37:54 +00:00
|
|
|
func (p *Prompt) setUp() {
|
|
|
|
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() {
|
|
|
|
p.in.TearDown()
|
2017-07-15 11:22:56 +00:00
|
|
|
p.renderer.TearDown()
|
2017-07-15 08:37:54 +00:00
|
|
|
}
|
|
|
|
|
2017-08-09 16:03:43 +00:00
|
|
|
func readBuffer(bufCh chan []byte, stopCh chan struct{}) {
|
2017-07-15 08:37:54 +00:00
|
|
|
buf := make([]byte, 1024)
|
|
|
|
|
2017-08-09 12:28:34 +00:00
|
|
|
log.Printf("[INFO] readBuffer start")
|
2017-07-15 08:37:54 +00:00
|
|
|
for {
|
2017-08-07 09:48:33 +00:00
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
select {
|
|
|
|
case <-stopCh:
|
2017-08-09 16:03:43 +00:00
|
|
|
log.Print("[INFO] stop readBuffer")
|
2017-08-07 09:48:33 +00:00
|
|
|
return
|
|
|
|
default:
|
|
|
|
if n, err := syscall.Read(syscall.Stdin, buf); err == nil {
|
|
|
|
bufCh <- buf[:n]
|
|
|
|
}
|
2017-07-15 08:37:54 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-04 02:22:26 +00:00
|
|
|
func handleSignals(in ConsoleParser, exitCh chan int, winSizeCh chan *WinSize) {
|
2017-07-15 08:37:54 +00:00
|
|
|
sigCh := make(chan os.Signal, 1)
|
|
|
|
signal.Notify(
|
|
|
|
sigCh,
|
|
|
|
syscall.SIGINT,
|
|
|
|
syscall.SIGTERM,
|
|
|
|
syscall.SIGQUIT,
|
|
|
|
syscall.SIGWINCH,
|
|
|
|
)
|
|
|
|
|
|
|
|
for {
|
|
|
|
s := <-sigCh
|
|
|
|
switch s {
|
2017-08-06 06:21:14 +00:00
|
|
|
case syscall.SIGINT: // kill -SIGINT XXXX or Ctrl+c
|
2017-08-04 07:22:55 +00:00
|
|
|
log.Println("[SIGNAL] Catch SIGINT")
|
2017-08-04 02:22:26 +00:00
|
|
|
exitCh <- 0
|
2017-07-15 08:37:54 +00:00
|
|
|
|
2017-08-06 06:21:14 +00:00
|
|
|
case syscall.SIGTERM: // kill -SIGTERM XXXX
|
2017-08-04 07:22:55 +00:00
|
|
|
log.Println("[SIGNAL] Catch SIGTERM")
|
2017-08-04 02:22:26 +00:00
|
|
|
exitCh <- 1
|
2017-07-15 08:37:54 +00:00
|
|
|
|
2017-08-06 06:21:14 +00:00
|
|
|
case syscall.SIGQUIT: // kill -SIGQUIT XXXX
|
2017-08-04 07:22:55 +00:00
|
|
|
log.Println("[SIGNAL] Catch SIGQUIT")
|
2017-08-04 02:22:26 +00:00
|
|
|
exitCh <- 0
|
2017-07-15 08:37:54 +00:00
|
|
|
|
|
|
|
case syscall.SIGWINCH:
|
2017-08-04 07:22:55 +00:00
|
|
|
log.Println("[SIGNAL] Catch SIGWINCH")
|
2017-07-15 08:37:54 +00:00
|
|
|
winSizeCh <- in.GetWinSize()
|
|
|
|
default:
|
2017-08-06 06:21:14 +00:00
|
|
|
time.Sleep(10 * time.Millisecond)
|
2017-07-15 08:37:54 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|