Merge pull request #3231 from fjl/liner-update

console, vendor: update liner and enable multiline mode
This commit is contained in:
Péter Szilágyi 2016-11-07 12:29:50 +02:00 committed by GitHub
commit e0e18f3841
9 changed files with 132 additions and 79 deletions

View File

@ -95,7 +95,7 @@ func newTerminalPrompter() *terminalPrompter {
} }
p.SetCtrlCAborts(true) p.SetCtrlCAborts(true)
p.SetTabCompletionStyle(liner.TabPrints) p.SetTabCompletionStyle(liner.TabPrints)
p.SetMultiLineMode(true)
return p return p
} }

View File

@ -18,7 +18,7 @@ github.com/mattn/go-runewidth v0.0.1-10-g737072b
github.com/mitchellh/go-wordwrap ad45545 github.com/mitchellh/go-wordwrap ad45545
github.com/nsf/termbox-go b6acae5 github.com/nsf/termbox-go b6acae5
github.com/pborman/uuid v1.0-17-g3d4f2ba github.com/pborman/uuid v1.0-17-g3d4f2ba
github.com/peterh/liner 8975875 github.com/peterh/liner 3c5f577
github.com/rcrowley/go-metrics ab2277b github.com/rcrowley/go-metrics ab2277b
github.com/rjeczalik/notify 7e20c15 github.com/rjeczalik/notify 7e20c15
github.com/robertkrimen/otto bf1c379 github.com/robertkrimen/otto bf1c379

View File

@ -32,6 +32,7 @@ type commonState struct {
cursorRows int cursorRows int
maxRows int maxRows int
shouldRestart ShouldRestart shouldRestart ShouldRestart
needRefresh bool
} }
// TabStyle is used to select how tab completions are displayed. // TabStyle is used to select how tab completions are displayed.
@ -58,7 +59,12 @@ var ErrPromptAborted = errors.New("prompt aborted")
// platform is normally supported, but stdout has been redirected // platform is normally supported, but stdout has been redirected
var ErrNotTerminalOutput = errors.New("standard output is not a terminal") var ErrNotTerminalOutput = errors.New("standard output is not a terminal")
// Max elements to save on the killring // ErrInvalidPrompt is returned from Prompt or PasswordPrompt if the
// prompt contains any unprintable runes (including substrings that could
// be colour codes on some platforms).
var ErrInvalidPrompt = errors.New("invalid prompt")
// KillRingMax is the max number of elements to save on the killring.
const KillRingMax = 60 const KillRingMax = 60
// HistoryLimit is the maximum number of entries saved in the scrollback history. // HistoryLimit is the maximum number of entries saved in the scrollback history.
@ -133,6 +139,13 @@ func (s *State) AppendHistory(item string) {
} }
} }
// ClearHistory clears the scroollback history.
func (s *State) ClearHistory() {
s.historyMutex.Lock()
defer s.historyMutex.Unlock()
s.history = nil
}
// Returns the history lines starting with prefix // Returns the history lines starting with prefix
func (s *State) getHistoryByPrefix(prefix string) (ph []string) { func (s *State) getHistoryByPrefix(prefix string) (ph []string) {
for _, h := range s.history { for _, h := range s.history {

View File

@ -31,11 +31,6 @@ type State struct {
// NewLiner initializes a new *State, and sets the terminal into raw mode. To // NewLiner initializes a new *State, and sets the terminal into raw mode. To
// restore the terminal to its previous state, call State.Close(). // restore the terminal to its previous state, call State.Close().
//
// Note if you are still using Go 1.0: NewLiner handles SIGWINCH, so it will
// leak a channel every time you call it. Therefore, it is recommened that you
// upgrade to a newer release of Go, or ensure that NewLiner is only called
// once.
func NewLiner() *State { func NewLiner() *State {
var s State var s State
s.r = bufio.NewReader(os.Stdin) s.r = bufio.NewReader(os.Stdin)
@ -87,8 +82,12 @@ func (s *State) startPrompt() {
s.restartPrompt() s.restartPrompt()
} }
func (s *State) inputWaiting() bool {
return len(s.next) > 0
}
func (s *State) restartPrompt() { func (s *State) restartPrompt() {
next := make(chan nexter) next := make(chan nexter, 200)
go func() { go func() {
for { for {
var n nexter var n nexter
@ -126,8 +125,6 @@ func (s *State) nextPending(timeout <-chan time.Time) (rune, error) {
s.pending = s.pending[1:] s.pending = s.pending[1:]
return rv, errTimedOut return rv, errTimedOut
} }
// not reached
return 0, nil
} }
func (s *State) readNext() (interface{}, error) { func (s *State) readNext() (interface{}, error) {
@ -349,7 +346,7 @@ func (s *State) readNext() (interface{}, error) {
// Close returns the terminal to its previous mode // Close returns the terminal to its previous mode
func (s *State) Close() error { func (s *State) Close() error {
stopSignal(s.winch) signal.Stop(s.winch)
if !s.inputRedirected { if !s.inputRedirected {
s.origMode.ApplyMode() s.origMode.ApplyMode()
} }

View File

@ -10,13 +10,14 @@ import (
var ( var (
kernel32 = syscall.NewLazyDLL("kernel32.dll") kernel32 = syscall.NewLazyDLL("kernel32.dll")
procGetStdHandle = kernel32.NewProc("GetStdHandle") procGetStdHandle = kernel32.NewProc("GetStdHandle")
procReadConsoleInput = kernel32.NewProc("ReadConsoleInputW") procReadConsoleInput = kernel32.NewProc("ReadConsoleInputW")
procGetConsoleMode = kernel32.NewProc("GetConsoleMode") procGetNumberOfConsoleInputEvents = kernel32.NewProc("GetNumberOfConsoleInputEvents")
procSetConsoleMode = kernel32.NewProc("SetConsoleMode") procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") procSetConsoleMode = kernel32.NewProc("SetConsoleMode")
procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition")
procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW") procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW")
) )
// These names are from the Win32 api, so they use underscores (contrary to // These names are from the Win32 api, so they use underscores (contrary to
@ -147,6 +148,21 @@ const (
modKeys = shiftPressed | leftAltPressed | rightAltPressed | leftCtrlPressed | rightCtrlPressed modKeys = shiftPressed | leftAltPressed | rightAltPressed | leftCtrlPressed | rightCtrlPressed
) )
// inputWaiting only returns true if the next call to readNext will return immediately.
func (s *State) inputWaiting() bool {
var num uint32
ok, _, _ := procGetNumberOfConsoleInputEvents.Call(uintptr(s.handle), uintptr(unsafe.Pointer(&num)))
if ok == 0 {
// call failed, so we cannot guarantee a non-blocking readNext
return false
}
// during a "paste" input events are always an odd number, and
// the last one results in a blocking readNext, so return false
// when num is 1 or 0.
return num > 1
}
func (s *State) readNext() (interface{}, error) { func (s *State) readNext() (interface{}, error) {
if s.repeat > 0 { if s.repeat > 0 {
s.repeat-- s.repeat--
@ -263,7 +279,6 @@ func (s *State) readNext() (interface{}, error) {
} }
return s.key, nil return s.key, nil
} }
return unknown, nil
} }
// Close returns the terminal to its previous mode // Close returns the terminal to its previous mode

View File

@ -90,11 +90,11 @@ const (
) )
func (s *State) refresh(prompt []rune, buf []rune, pos int) error { func (s *State) refresh(prompt []rune, buf []rune, pos int) error {
s.needRefresh = false
if s.multiLineMode { if s.multiLineMode {
return s.refreshMultiLine(prompt, buf, pos) return s.refreshMultiLine(prompt, buf, pos)
} else {
return s.refreshSingleLine(prompt, buf, pos)
} }
return s.refreshSingleLine(prompt, buf, pos)
} }
func (s *State) refreshSingleLine(prompt []rune, buf []rune, pos int) error { func (s *State) refreshSingleLine(prompt []rune, buf []rune, pos int) error {
@ -387,8 +387,6 @@ func (s *State) tabComplete(p []rune, line []rune, pos int) ([]rune, int, interf
} }
return []rune(head + pick + tail), hl + utf8.RuneCountInString(pick), next, nil return []rune(head + pick + tail), hl + utf8.RuneCountInString(pick), next, nil
} }
// Not reached
return line, pos, rune(esc), nil
} }
// reverse intelligent search, implements a bash-like history search. // reverse intelligent search, implements a bash-like history search.
@ -556,8 +554,6 @@ func (s *State) yank(p []rune, text []rune, pos int) ([]rune, int, interface{},
} }
} }
} }
return line, pos, esc, nil
} }
// Prompt displays p and returns a line of user input, not including a trailing // Prompt displays p and returns a line of user input, not including a trailing
@ -573,6 +569,11 @@ func (s *State) Prompt(prompt string) (string, error) {
// including a trailing newline character. An io.EOF error is returned if the user // including a trailing newline character. An io.EOF error is returned if the user
// signals end-of-file by pressing Ctrl-D. // signals end-of-file by pressing Ctrl-D.
func (s *State) PromptWithSuggestion(prompt string, text string, pos int) (string, error) { func (s *State) PromptWithSuggestion(prompt string, text string, pos int) (string, error) {
for _, r := range prompt {
if unicode.Is(unicode.C, r) {
return "", ErrInvalidPrompt
}
}
if s.inputRedirected || !s.terminalSupported { if s.inputRedirected || !s.terminalSupported {
return s.promptUnsupported(prompt) return s.promptUnsupported(prompt)
} }
@ -587,8 +588,9 @@ func (s *State) PromptWithSuggestion(prompt string, text string, pos int) (strin
p := []rune(prompt) p := []rune(prompt)
var line = []rune(text) var line = []rune(text)
historyEnd := "" historyEnd := ""
prefixHistory := s.getHistoryByPrefix(string(line)) var historyPrefix []string
historyPos := len(prefixHistory) historyPos := 0
historyStale := true
historyAction := false // used to mark history related actions historyAction := false // used to mark history related actions
killAction := 0 // used to mark kill related actions killAction := 0 // used to mark kill related actions
@ -628,21 +630,21 @@ mainLoop:
break mainLoop break mainLoop
case ctrlA: // Start of line case ctrlA: // Start of line
pos = 0 pos = 0
s.refresh(p, line, pos) s.needRefresh = true
case ctrlE: // End of line case ctrlE: // End of line
pos = len(line) pos = len(line)
s.refresh(p, line, pos) s.needRefresh = true
case ctrlB: // left case ctrlB: // left
if pos > 0 { if pos > 0 {
pos -= len(getSuffixGlyphs(line[:pos], 1)) pos -= len(getSuffixGlyphs(line[:pos], 1))
s.refresh(p, line, pos) s.needRefresh = true
} else { } else {
fmt.Print(beep) fmt.Print(beep)
} }
case ctrlF: // right case ctrlF: // right
if pos < len(line) { if pos < len(line) {
pos += len(getPrefixGlyphs(line[pos:], 1)) pos += len(getPrefixGlyphs(line[pos:], 1))
s.refresh(p, line, pos) s.needRefresh = true
} else { } else {
fmt.Print(beep) fmt.Print(beep)
} }
@ -661,7 +663,7 @@ mainLoop:
} else { } else {
n := len(getPrefixGlyphs(line[pos:], 1)) n := len(getPrefixGlyphs(line[pos:], 1))
line = append(line[:pos], line[pos+n:]...) line = append(line[:pos], line[pos+n:]...)
s.refresh(p, line, pos) s.needRefresh = true
} }
case ctrlK: // delete remainder of line case ctrlK: // delete remainder of line
if pos >= len(line) { if pos >= len(line) {
@ -675,32 +677,42 @@ mainLoop:
killAction = 2 // Mark that there was a kill action killAction = 2 // Mark that there was a kill action
line = line[:pos] line = line[:pos]
s.refresh(p, line, pos) s.needRefresh = true
} }
case ctrlP: // up case ctrlP: // up
historyAction = true historyAction = true
if historyStale {
historyPrefix = s.getHistoryByPrefix(string(line))
historyPos = len(historyPrefix)
historyStale = false
}
if historyPos > 0 { if historyPos > 0 {
if historyPos == len(prefixHistory) { if historyPos == len(historyPrefix) {
historyEnd = string(line) historyEnd = string(line)
} }
historyPos-- historyPos--
line = []rune(prefixHistory[historyPos]) line = []rune(historyPrefix[historyPos])
pos = len(line) pos = len(line)
s.refresh(p, line, pos) s.needRefresh = true
} else { } else {
fmt.Print(beep) fmt.Print(beep)
} }
case ctrlN: // down case ctrlN: // down
historyAction = true historyAction = true
if historyPos < len(prefixHistory) { if historyStale {
historyPrefix = s.getHistoryByPrefix(string(line))
historyPos = len(historyPrefix)
historyStale = false
}
if historyPos < len(historyPrefix) {
historyPos++ historyPos++
if historyPos == len(prefixHistory) { if historyPos == len(historyPrefix) {
line = []rune(historyEnd) line = []rune(historyEnd)
} else { } else {
line = []rune(prefixHistory[historyPos]) line = []rune(historyPrefix[historyPos])
} }
pos = len(line) pos = len(line)
s.refresh(p, line, pos) s.needRefresh = true
} else { } else {
fmt.Print(beep) fmt.Print(beep)
} }
@ -718,11 +730,11 @@ mainLoop:
copy(line[pos-len(prev):], next) copy(line[pos-len(prev):], next)
copy(line[pos-len(prev)+len(next):], scratch) copy(line[pos-len(prev)+len(next):], scratch)
pos += len(next) pos += len(next)
s.refresh(p, line, pos) s.needRefresh = true
} }
case ctrlL: // clear screen case ctrlL: // clear screen
s.eraseScreen() s.eraseScreen()
s.refresh(p, line, pos) s.needRefresh = true
case ctrlC: // reset case ctrlC: // reset
fmt.Println("^C") fmt.Println("^C")
if s.multiLineMode { if s.multiLineMode {
@ -742,7 +754,7 @@ mainLoop:
n := len(getSuffixGlyphs(line[:pos], 1)) n := len(getSuffixGlyphs(line[:pos], 1))
line = append(line[:pos-n], line[pos:]...) line = append(line[:pos-n], line[pos:]...)
pos -= n pos -= n
s.refresh(p, line, pos) s.needRefresh = true
} }
case ctrlU: // Erase line before cursor case ctrlU: // Erase line before cursor
if killAction > 0 { if killAction > 0 {
@ -754,7 +766,7 @@ mainLoop:
killAction = 2 // Mark that there was some killing killAction = 2 // Mark that there was some killing
line = line[pos:] line = line[pos:]
pos = 0 pos = 0
s.refresh(p, line, pos) s.needRefresh = true
case ctrlW: // Erase word case ctrlW: // Erase word
if pos == 0 { if pos == 0 {
fmt.Print(beep) fmt.Print(beep)
@ -791,13 +803,13 @@ mainLoop:
} }
killAction = 2 // Mark that there was some killing killAction = 2 // Mark that there was some killing
s.refresh(p, line, pos) s.needRefresh = true
case ctrlY: // Paste from Yank buffer case ctrlY: // Paste from Yank buffer
line, pos, next, err = s.yank(p, line, pos) line, pos, next, err = s.yank(p, line, pos)
goto haveNext goto haveNext
case ctrlR: // Reverse Search case ctrlR: // Reverse Search
line, pos, next, err = s.reverseISearch(line, pos) line, pos, next, err = s.reverseISearch(line, pos)
s.refresh(p, line, pos) s.needRefresh = true
goto haveNext goto haveNext
case tab: // Tab completion case tab: // Tab completion
line, pos, next, err = s.tabComplete(p, line, pos) line, pos, next, err = s.tabComplete(p, line, pos)
@ -812,14 +824,16 @@ mainLoop:
case 0, 28, 29, 30, 31: case 0, 28, 29, 30, 31:
fmt.Print(beep) fmt.Print(beep)
default: default:
if pos == len(line) && !s.multiLineMode && countGlyphs(p)+countGlyphs(line) < s.columns-1 { if pos == len(line) && !s.multiLineMode &&
len(p)+len(line) < s.columns*4 && // Avoid countGlyphs on large lines
countGlyphs(p)+countGlyphs(line) < s.columns-1 {
line = append(line, v) line = append(line, v)
fmt.Printf("%c", v) fmt.Printf("%c", v)
pos++ pos++
} else { } else {
line = append(line[:pos], append([]rune{v}, line[pos:]...)...) line = append(line[:pos], append([]rune{v}, line[pos:]...)...)
pos++ pos++
s.refresh(p, line, pos) s.needRefresh = true
} }
} }
case action: case action:
@ -887,24 +901,34 @@ mainLoop:
} }
case up: case up:
historyAction = true historyAction = true
if historyStale {
historyPrefix = s.getHistoryByPrefix(string(line))
historyPos = len(historyPrefix)
historyStale = false
}
if historyPos > 0 { if historyPos > 0 {
if historyPos == len(prefixHistory) { if historyPos == len(historyPrefix) {
historyEnd = string(line) historyEnd = string(line)
} }
historyPos-- historyPos--
line = []rune(prefixHistory[historyPos]) line = []rune(historyPrefix[historyPos])
pos = len(line) pos = len(line)
} else { } else {
fmt.Print(beep) fmt.Print(beep)
} }
case down: case down:
historyAction = true historyAction = true
if historyPos < len(prefixHistory) { if historyStale {
historyPrefix = s.getHistoryByPrefix(string(line))
historyPos = len(historyPrefix)
historyStale = false
}
if historyPos < len(historyPrefix) {
historyPos++ historyPos++
if historyPos == len(prefixHistory) { if historyPos == len(historyPrefix) {
line = []rune(historyEnd) line = []rune(historyEnd)
} else { } else {
line = []rune(prefixHistory[historyPos]) line = []rune(historyPrefix[historyPos])
} }
pos = len(line) pos = len(line)
} else { } else {
@ -928,11 +952,13 @@ mainLoop:
s.cursorRows = 1 s.cursorRows = 1
} }
} }
s.needRefresh = true
}
if s.needRefresh && !s.inputWaiting() {
s.refresh(p, line, pos) s.refresh(p, line, pos)
} }
if !historyAction { if !historyAction {
prefixHistory = s.getHistoryByPrefix(string(line)) historyStale = true
historyPos = len(prefixHistory)
} }
if killAction > 0 { if killAction > 0 {
killAction-- killAction--
@ -944,6 +970,11 @@ mainLoop:
// PasswordPrompt displays p, and then waits for user input. The input typed by // PasswordPrompt displays p, and then waits for user input. The input typed by
// the user is not displayed in the terminal. // the user is not displayed in the terminal.
func (s *State) PasswordPrompt(prompt string) (string, error) { func (s *State) PasswordPrompt(prompt string) (string, error) {
for _, r := range prompt {
if unicode.Is(unicode.C, r) {
return "", ErrInvalidPrompt
}
}
if !s.terminalSupported { if !s.terminalSupported {
return "", errors.New("liner: function not supported in this terminal") return "", errors.New("liner: function not supported in this terminal")
} }

View File

@ -1,12 +0,0 @@
// +build go1.1,!windows
package liner
import (
"os"
"os/signal"
)
func stopSignal(c chan<- os.Signal) {
signal.Stop(c)
}

View File

@ -1,11 +0,0 @@
// +build !go1.1,!windows
package liner
import (
"os"
)
func stopSignal(c chan<- os.Signal) {
// signal.Stop does not exist before Go 1.1
}

View File

@ -25,6 +25,12 @@ var doubleWidth = []*unicode.RangeTable{
func countGlyphs(s []rune) int { func countGlyphs(s []rune) int {
n := 0 n := 0
for _, r := range s { for _, r := range s {
// speed up the common case
if r < 127 {
n++
continue
}
switch { switch {
case unicode.IsOneOf(zeroWidth, r): case unicode.IsOneOf(zeroWidth, r):
case unicode.IsOneOf(doubleWidth, r): case unicode.IsOneOf(doubleWidth, r):
@ -39,6 +45,10 @@ func countGlyphs(s []rune) int {
func countMultiLineGlyphs(s []rune, columns int, start int) int { func countMultiLineGlyphs(s []rune, columns int, start int) int {
n := start n := start
for _, r := range s { for _, r := range s {
if r < 127 {
n++
continue
}
switch { switch {
case unicode.IsOneOf(zeroWidth, r): case unicode.IsOneOf(zeroWidth, r):
case unicode.IsOneOf(doubleWidth, r): case unicode.IsOneOf(doubleWidth, r):
@ -58,6 +68,11 @@ func countMultiLineGlyphs(s []rune, columns int, start int) int {
func getPrefixGlyphs(s []rune, num int) []rune { func getPrefixGlyphs(s []rune, num int) []rune {
p := 0 p := 0
for n := 0; n < num && p < len(s); p++ { for n := 0; n < num && p < len(s); p++ {
// speed up the common case
if s[p] < 127 {
n++
continue
}
if !unicode.IsOneOf(zeroWidth, s[p]) { if !unicode.IsOneOf(zeroWidth, s[p]) {
n++ n++
} }
@ -71,6 +86,11 @@ func getPrefixGlyphs(s []rune, num int) []rune {
func getSuffixGlyphs(s []rune, num int) []rune { func getSuffixGlyphs(s []rune, num int) []rune {
p := len(s) p := len(s)
for n := 0; n < num && p > 0; p-- { for n := 0; n < num && p > 0; p-- {
// speed up the common case
if s[p-1] < 127 {
n++
continue
}
if !unicode.IsOneOf(zeroWidth, s[p-1]) { if !unicode.IsOneOf(zeroWidth, s[p-1]) {
n++ n++
} }