go-prompt/prompt.go
2017-08-04 16:23:18 +09:00

219 lines
4.6 KiB
Go

package prompt
import (
"context"
"io/ioutil"
"log"
"os"
"os/signal"
"syscall"
"time"
)
const (
logfile = "/tmp/go-prompt-debug.log"
envEnableLog = "GO_PROMPT_ENABLE_LOG"
)
type Executor func(context.Context, string) string
type Completer func(string) []Completion
type Completion struct {
Text string
Description string
}
type Prompt struct {
in ConsoleParser
buf *Buffer
renderer *Render
executor Executor
completer Completer
maxCompletions uint16
selected int // -1 means nothing one is selected.
history []string
}
func (p *Prompt) Run() {
p.setUp()
defer p.tearDown()
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("Logging is enabled.")
}
p.renderer.Render(p.buf, p.completer(p.buf.Text()), p.maxCompletions, p.selected)
bufCh := make(chan []byte, 128)
go readBuffer(bufCh)
exitCh := make(chan struct{})
winSizeCh := make(chan *WinSize)
go handleSignals(p.in, exitCh, winSizeCh)
for {
select {
case b := <-bufCh:
if shouldExecute, shouldExit, input := p.feed(b); shouldExit {
return
} else if shouldExecute {
ctx, _ := context.WithCancel(context.Background())
p.renderer.RenderResult(p.executor(ctx, input))
completions := p.completer(p.buf.Text())
p.updateSelectedCompletion(completions)
p.renderer.Render(p.buf, completions, p.maxCompletions, p.selected)
} else {
completions := p.completer(p.buf.Text())
p.updateSelectedCompletion(completions)
p.renderer.Render(p.buf, completions, p.maxCompletions, p.selected)
}
case w := <-winSizeCh:
p.renderer.UpdateWinSize(w)
completions := p.completer(p.buf.Text())
p.renderer.Render(p.buf, completions, p.maxCompletions, p.selected)
case <-exitCh:
return
default:
time.Sleep(10 * time.Millisecond)
}
}
}
func (p *Prompt) feed(b []byte) (shouldExecute, shouldExit bool, input string) {
shouldExecute = false
key := p.in.GetKey(b)
switch key {
case ControlJ, Enter:
if p.selected != -1 {
c := p.completer(p.buf.Text())[p.selected]
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)
shouldExecute = true
input = p.buf.Text()
p.history = append(p.history, input)
log.Printf("History: %s", input)
p.buf = NewBuffer()
p.selected = -1
case ControlC:
p.renderer.BreakLine(p.buf)
p.buf = NewBuffer()
p.selected = -1
case ControlD:
shouldExit = true
case Up:
fallthrough
case BackTab:
p.selected -= 1
case ControlI:
fallthrough
case Down:
fallthrough
case Tab:
p.selected += 1
case Left:
p.buf.CursorLeft(1)
case Right:
p.buf.CursorRight(1)
case Backspace:
p.buf.DeleteBeforeCursor(1)
case NotDefined:
if p.selected != -1 {
c := p.completer(p.buf.Text())[p.selected]
w := p.buf.Document().GetWordBeforeCursor()
if w != "" {
p.buf.DeleteBeforeCursor(len([]rune(w)))
}
p.buf.InsertText(c.Text, false, true)
}
p.selected = -1
p.buf.InsertText(string(b), false, true)
default:
p.selected = -1
}
return
}
func (p *Prompt) updateSelectedCompletion(completions []Completion) {
max := int(p.maxCompletions)
if len(completions) < max {
max = len(completions)
}
if p.selected >= max {
p.selected = -1
} else if p.selected < -1 {
p.selected = max - 1
}
}
func (p *Prompt) setUp() {
p.in.Setup()
p.renderer.Setup()
p.renderer.UpdateWinSize(p.in.GetWinSize())
p.selected = -1 // -1 means nothing one is selected.
}
func (p *Prompt) tearDown() {
p.in.TearDown()
p.renderer.TearDown()
}
func readBuffer(bufCh chan []byte) {
buf := make([]byte, 1024)
for {
if n, err := syscall.Read(syscall.Stdin, buf); err == nil {
bufCh <- buf[:n]
}
}
}
func handleSignals(in ConsoleParser, exitCh chan struct{}, winSizeCh chan *WinSize) {
sigCh := make(chan os.Signal, 1)
signal.Notify(
sigCh,
syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT,
syscall.SIGWINCH,
)
for {
s := <-sigCh
switch s {
// kill -SIGHUP XXXX
case syscall.SIGHUP:
exitCh <- struct{}{}
// kill -SIGINT XXXX or Ctrl+c
case syscall.SIGINT:
exitCh <- struct{}{}
// kill -SIGTERM XXXX
case syscall.SIGTERM:
exitCh <- struct{}{}
// kill -SIGQUIT XXXX
case syscall.SIGQUIT:
exitCh <- struct{}{}
case syscall.SIGWINCH:
winSizeCh <- in.GetWinSize()
default:
}
}
}