package prompt import ( "io/ioutil" "log" "os" "os/signal" "syscall" "time" ) const ( logfile = "/tmp/go-prompt-debug.log" envEnableLog = "GO_PROMPT_ENABLE_LOG" ) type Executor func(string) type Completer func(string) []Suggest type Prompt struct { in ConsoleParser buf *Buffer renderer *Render executor Executor history *History completion *CompletionManager keyBindings []KeyBind keyBindMode KeyBindMode } type Exec struct { input string } 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 readBuffer(bufCh, stopReadBufCh) exitCh := make(chan int) winSizeCh := make(chan *WinSize) go handleSignals(p.in, exitCh, winSizeCh) 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{}{} // 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.Text()) p.renderer.Render(p.buf, p.completion) // Set raw mode p.in.Setup() go readBuffer(bufCh, stopReadBufCh) } else { p.completion.Update(p.buf.Text()) 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() 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: 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 { p.buf = kb.Fn(p.buf) } } if p.keyBindMode == EmacsKeyBind { for i := range emacsKeyBindings { kb := emacsKeyBindings[i] if kb.Key == key { p.buf = kb.Fn(p.buf) } } } // Custom key bindings for i := range p.keyBindings { kb := p.keyBindings[i] if kb.Key == key { p.buf = kb.Fn(p.buf) } } return } 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 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) } } } 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() } func readBuffer(bufCh chan []byte, stopCh chan struct{}) { buf := make([]byte, 1024) log.Printf("[INFO] readBuffer start") for { time.Sleep(10 * time.Millisecond) select { case <-stopCh: log.Print("[INFO] stop readBuffer") return default: if n, err := syscall.Read(syscall.Stdin, buf); err == nil { bufCh <- buf[:n] } } } } func handleSignals(in ConsoleParser, exitCh chan int, winSizeCh chan *WinSize) { sigCh := make(chan os.Signal, 1) signal.Notify( sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGWINCH, ) for { s := <-sigCh switch s { case syscall.SIGINT: // kill -SIGINT XXXX or Ctrl+c log.Println("[SIGNAL] Catch SIGINT") exitCh <- 0 case syscall.SIGTERM: // kill -SIGTERM XXXX log.Println("[SIGNAL] Catch SIGTERM") exitCh <- 1 case syscall.SIGQUIT: // kill -SIGQUIT XXXX log.Println("[SIGNAL] Catch SIGQUIT") exitCh <- 0 case syscall.SIGWINCH: log.Println("[SIGNAL] Catch SIGWINCH") winSizeCh <- in.GetWinSize() default: time.Sleep(10 * time.Millisecond) } } }