зеркало из https://github.com/golang/term.git
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:
Родитель
21bda07dd6
Коммит
d037c2cd54
145
terminal.go
145
terminal.go
|
@ -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) {
|
||||||
|
|
Загрузка…
Ссылка в новой задаче