go.crypto/ssh/terminal: support bracketed paste mode.

Some terminals support a mode where pasted text is bracketed by escape sequences. This is very useful for terminal applications that otherwise have no good way to tell pastes and typed text apart.

This change allows applications to enable this mode and, if the terminal supports it, will suppress autocompletes during pastes and indicate to the caller that a line came entirely from pasted text.

LGTM=bradfitz
R=bradfitz
CC=golang-codereviews
https://golang.org/cl/171330043
This commit is contained in:
Adam Langley 2014-11-16 14:01:45 -08:00
Родитель 21bda07dd6
Коммит d037c2cd54
2 изменённых файлов: 128 добавлений и 35 удалений

Просмотреть файл

@ -5,6 +5,7 @@
package terminal package terminal
import ( import (
"bytes"
"io" "io"
"sync" "sync"
"unicode/utf8" "unicode/utf8"
@ -61,6 +62,9 @@ type Terminal struct {
pos int pos int
// echo is true if local echo is enabled // echo is true if local echo is enabled
echo bool echo bool
// pasteActive is true iff there is a bracketed paste operation in
// progress.
pasteActive bool
// cursorX contains the current X value of the cursor where the left // cursorX contains the current X value of the cursor where the left
// edge is 0. cursorY contains the row number where the first row of // edge is 0. cursorY contains the row number where the first row of
@ -124,28 +128,35 @@ const (
keyDeleteWord keyDeleteWord
keyDeleteLine keyDeleteLine
keyClearScreen keyClearScreen
keyPasteStart
keyPasteEnd
) )
var pasteStart = []byte{keyEscape, '[', '2', '0', '0', '~'}
var pasteEnd = []byte{keyEscape, '[', '2', '0', '1', '~'}
// bytesToKey tries to parse a key sequence from b. If successful, it returns // bytesToKey tries to parse a key sequence from b. If successful, it returns
// the key and the remainder of the input. Otherwise it returns utf8.RuneError. // the key and the remainder of the input. Otherwise it returns utf8.RuneError.
func bytesToKey(b []byte) (rune, []byte) { func bytesToKey(b []byte, pasteActive bool) (rune, []byte) {
if len(b) == 0 { if len(b) == 0 {
return utf8.RuneError, nil return utf8.RuneError, nil
} }
switch b[0] { if !pasteActive {
case 1: // ^A switch b[0] {
return keyHome, b[1:] case 1: // ^A
case 5: // ^E return keyHome, b[1:]
return keyEnd, b[1:] case 5: // ^E
case 8: // ^H return keyEnd, b[1:]
return keyBackspace, b[1:] case 8: // ^H
case 11: // ^K return keyBackspace, b[1:]
return keyDeleteLine, b[1:] case 11: // ^K
case 12: // ^L return keyDeleteLine, b[1:]
return keyClearScreen, b[1:] case 12: // ^L
case 23: // ^W return keyClearScreen, b[1:]
return keyDeleteWord, b[1:] case 23: // ^W
return keyDeleteWord, b[1:]
}
} }
if b[0] != keyEscape { if b[0] != keyEscape {
@ -156,7 +167,7 @@ func bytesToKey(b []byte) (rune, []byte) {
return r, b[l:] return r, b[l:]
} }
if len(b) >= 3 && b[0] == keyEscape && b[1] == '[' { if !pasteActive && len(b) >= 3 && b[0] == keyEscape && b[1] == '[' {
switch b[2] { switch b[2] {
case 'A': case 'A':
return keyUp, b[3:] return keyUp, b[3:]
@ -173,7 +184,7 @@ func bytesToKey(b []byte) (rune, []byte) {
} }
} }
if len(b) >= 6 && b[0] == keyEscape && b[1] == '[' && b[2] == '1' && b[3] == ';' && b[4] == '3' { if !pasteActive && len(b) >= 6 && b[0] == keyEscape && b[1] == '[' && b[2] == '1' && b[3] == ';' && b[4] == '3' {
switch b[5] { switch b[5] {
case 'C': case 'C':
return keyAltRight, b[6:] return keyAltRight, b[6:]
@ -182,12 +193,20 @@ func bytesToKey(b []byte) (rune, []byte) {
} }
} }
if !pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteStart) {
return keyPasteStart, b[6:]
}
if pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteEnd) {
return keyPasteEnd, b[6:]
}
// If we get here then we have a key that we don't recognise, or a // If we get here then we have a key that we don't recognise, or a
// partial sequence. It's not clear how one should find the end of a // partial sequence. It's not clear how one should find the end of a
// sequence without knowing them all, but it seems that [a-zA-Z] only // sequence without knowing them all, but it seems that [a-zA-Z~] only
// appears at the end of a sequence. // appears at the end of a sequence.
for i, c := range b[0:] { for i, c := range b[0:] {
if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' { if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '~' {
return keyUnknown, b[i+1:] return keyUnknown, b[i+1:]
} }
} }
@ -409,6 +428,11 @@ func visualLength(runes []rune) int {
// handleKey processes the given key and, optionally, returns a line of text // handleKey processes the given key and, optionally, returns a line of text
// that the user has entered. // that the user has entered.
func (t *Terminal) handleKey(key rune) (line string, ok bool) { func (t *Terminal) handleKey(key rune) (line string, ok bool) {
if t.pasteActive && key != keyEnter {
t.addKeyToLine(key)
return
}
switch key { switch key {
case keyBackspace: case keyBackspace:
if t.pos == 0 { if t.pos == 0 {
@ -533,23 +557,29 @@ func (t *Terminal) handleKey(key rune) (line string, ok bool) {
if len(t.line) == maxLineLength { if len(t.line) == maxLineLength {
return return
} }
if len(t.line) == cap(t.line) { t.addKeyToLine(key)
newLine := make([]rune, len(t.line), 2*(1+len(t.line)))
copy(newLine, t.line)
t.line = newLine
}
t.line = t.line[:len(t.line)+1]
copy(t.line[t.pos+1:], t.line[t.pos:])
t.line[t.pos] = key
if t.echo {
t.writeLine(t.line[t.pos:])
}
t.pos++
t.moveCursorToPos(t.pos)
} }
return return
} }
// addKeyToLine inserts the given key at the current position in the current
// line.
func (t *Terminal) addKeyToLine(key rune) {
if len(t.line) == cap(t.line) {
newLine := make([]rune, len(t.line), 2*(1+len(t.line)))
copy(newLine, t.line)
t.line = newLine
}
t.line = t.line[:len(t.line)+1]
copy(t.line[t.pos+1:], t.line[t.pos:])
t.line[t.pos] = key
if t.echo {
t.writeLine(t.line[t.pos:])
}
t.pos++
t.moveCursorToPos(t.pos)
}
func (t *Terminal) writeLine(line []rune) { func (t *Terminal) writeLine(line []rune) {
for len(line) != 0 { for len(line) != 0 {
remainingOnLine := t.termWidth - t.cursorX remainingOnLine := t.termWidth - t.cursorX
@ -643,19 +673,36 @@ func (t *Terminal) readLine() (line string, err error) {
t.outBuf = t.outBuf[:0] t.outBuf = t.outBuf[:0]
} }
lineIsPasted := t.pasteActive
for { for {
rest := t.remainder rest := t.remainder
lineOk := false lineOk := false
for !lineOk { for !lineOk {
var key rune var key rune
key, rest = bytesToKey(rest) key, rest = bytesToKey(rest, t.pasteActive)
if key == utf8.RuneError { if key == utf8.RuneError {
break break
} }
if key == keyCtrlD { if !t.pasteActive {
if len(t.line) == 0 { if key == keyCtrlD {
return "", io.EOF if len(t.line) == 0 {
return "", io.EOF
}
} }
if key == keyPasteStart {
t.pasteActive = true
if len(t.line) == 0 {
lineIsPasted = true
}
continue
}
} else if key == keyPasteEnd {
t.pasteActive = false
continue
}
if !t.pasteActive {
lineIsPasted = false
} }
line, lineOk = t.handleKey(key) line, lineOk = t.handleKey(key)
} }
@ -672,6 +719,9 @@ func (t *Terminal) readLine() (line string, err error) {
t.historyIndex = -1 t.historyIndex = -1
t.history.Add(line) t.history.Add(line)
} }
if lineIsPasted {
err = ErrPasteIndicator
}
return return
} }
@ -772,6 +822,31 @@ func (t *Terminal) SetSize(width, height int) error {
return err return err
} }
type pasteIndicatorError struct{}
func (pasteIndicatorError) Error() string {
return "terminal: ErrPasteIndicator not correctly handled"
}
// ErrPasteIndicator may be returned from ReadLine as the error, in addition
// to valid line data. It indicates that bracketed paste mode is enabled and
// that the returned line consists only of pasted data. Programs may wish to
// interpret pasted data more literally than typed data.
var ErrPasteIndicator = pasteIndicatorError{}
// SetBracketedPasteMode requests that the terminal bracket paste operations
// with markers. Not all terminals support this but, if it is supported, then
// enabling this mode will stop any autocomplete callback from running due to
// pastes. Additionally, any lines that are completely pasted will be returned
// from ReadLine with the error set to ErrPasteIndicator.
func (t *Terminal) SetBracketedPasteMode(on bool) {
if on {
io.WriteString(t.c, "\x1b[?2004h")
} else {
io.WriteString(t.c, "\x1b[?2004l")
}
}
// stRingBuffer is a ring buffer of strings. // stRingBuffer is a ring buffer of strings.
type stRingBuffer struct { type stRingBuffer struct {
// entries contains max elements. // entries contains max elements.

Просмотреть файл

@ -179,6 +179,24 @@ var keyPressTests = []struct {
in: "abcd\x1b[D\x1b[D\025\r", in: "abcd\x1b[D\x1b[D\025\r",
line: "cd", line: "cd",
}, },
{
// Bracketed paste mode: control sequences should be returned
// verbatim in paste mode.
in: "abc\x1b[200~de\177f\x1b[201~\177\r",
line: "abcde\177",
},
{
// Enter in bracketed paste mode should still work.
in: "abc\x1b[200~d\refg\x1b[201~h\r",
line: "efgh",
throwAwayLines: 1,
},
{
// Lines consisting entirely of pasted data should be indicated as such.
in: "\x1b[200~a\r",
line: "a",
err: ErrPasteIndicator,
},
} }
func TestKeyPresses(t *testing.T) { func TestKeyPresses(t *testing.T) {