go-prompt/prompt.go

254 lines
5.6 KiB
Go
Raw Normal View History

2017-07-15 08:37:54 +00:00
package prompt
import (
2017-07-23 15:35:13 +00:00
"context"
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
completer Completer
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
ctx context.Context
}
func (e *Exec) Context() context.Context {
if e.ctx == nil {
e.ctx = context.Background()
}
return e.ctx
}
2017-07-15 08:37:54 +00:00
func (p *Prompt) Run() {
p.setUp()
defer p.tearDown()
2017-08-03 07:57:29 +00:00
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)
2017-08-04 07:22:55 +00:00
log.Println("[INFO] Logging is enabled.")
2017-08-03 07:57:29 +00:00
}
2017-08-04 11:19:12 +00:00
p.renderer.Render(p.buf, p.completer(p.buf.Text()), p.completion.Max, p.completion.selected)
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{})
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-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
p.in.TearDown()
p.executor(e.input)
2017-07-23 15:35:13 +00:00
completions := p.completer(p.buf.Text())
2017-08-04 11:19:12 +00:00
p.completion.update(completions)
p.renderer.Render(p.buf, completions, p.completion.Max, p.completion.selected)
2017-08-07 09:48:33 +00:00
// Set raw mode
p.in.Setup()
go readBuffer(bufCh, stopReadBufCh)
2017-07-15 09:03:18 +00:00
} else {
2017-07-23 15:35:13 +00:00
completions := p.completer(p.buf.Text())
2017-08-04 11:19:12 +00:00
p.completion.update(completions)
p.renderer.Render(p.buf, completions, p.completion.Max, p.completion.selected)
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)
completions := p.completer(p.buf.Text())
2017-08-04 11:19:12 +00:00
p.renderer.Render(p.buf, completions, p.completion.Max, p.completion.selected)
2017-08-04 02:22:26 +00:00
case code := <-exitCh:
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-04 11:19:12 +00:00
if p.completion.Completing() {
c := p.completer(p.buf.Text())[p.completion.selected]
2017-07-23 15:35:13 +00:00
w := p.buf.Document().GetWordBeforeCursor()
if w != "" {
p.buf.DeleteBeforeCursor(len([]rune(w)))
}
p.buf.InsertText(c.Text, false, true)
}
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-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-07-23 15:35:13 +00:00
shouldExit = true
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-04 11:19:12 +00:00
if p.completion.Completing() {
c := p.completer(p.buf.Text())[p.completion.selected]
2017-08-04 07:22:55 +00:00
w := p.buf.Document().GetWordBeforeCursor()
if w != "" {
p.buf.DeleteBeforeCursor(len([]rune(w)))
}
p.buf.InsertText(c.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-04 11:19:12 +00:00
if p.completion.Completing() {
c := p.completer(p.buf.Text())[p.completion.selected]
2017-08-03 06:53:38 +00:00
w := p.buf.Document().GetWordBeforeCursor()
if w != "" {
p.buf.DeleteBeforeCursor(len([]rune(w)))
}
p.buf.InsertText(c.Text, false, true)
}
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-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-07 09:48:33 +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 03:50:07 +00:00
// Set NonBlocking mode because if syscall.Read block this goroutine, it cannot receive data from stopCh.
2017-08-07 09:48:33 +00:00
_, _, e := syscall.Syscall(syscall.SYS_FCNTL, uintptr(syscall.Stdin),
uintptr(syscall.F_SETFL), uintptr(syscall.O_ASYNC|syscall.O_NONBLOCK))
if e != 0 {
log.Println("[ERROR] Cannot set non blocking mode.")
panic(e)
}
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:
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
}
}
}