Quellcode durchsuchen

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
Adam Langley vor 11 Jahren
Ursprung
Commit
5ff91abc76
2 geänderte Dateien mit 128 neuen und 35 gelöschten Zeilen
  1. 110 35
      ssh/terminal/terminal.go
  2. 18 0
      ssh/terminal/terminal_test.go

+ 110 - 35
ssh/terminal/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] {
-	case 1: // ^A
-		return keyHome, b[1:]
-	case 5: // ^E
-		return keyEnd, b[1:]
-	case 8: // ^H
-		return keyBackspace, b[1:]
-	case 11: // ^K
-		return keyDeleteLine, b[1:]
-	case 12: // ^L
-		return keyClearScreen, b[1:]
-	case 23: // ^W
-		return keyDeleteWord, b[1:]
+	if !pasteActive {
+		switch b[0] {
+		case 1: // ^A
+			return keyHome, b[1:]
+		case 5: // ^E
+			return keyEnd, b[1:]
+		case 8: // ^H
+			return keyBackspace, b[1:]
+		case 11: // ^K
+			return keyDeleteLine, b[1:]
+		case 12: // ^L
+			return keyClearScreen, 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) {
-			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)
+		t.addKeyToLine(key)
 	}
 	}
 	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 len(t.line) == 0 {
-					return "", io.EOF
+			if !t.pasteActive {
+				if key == keyCtrlD {
+					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.

+ 18 - 0
ssh/terminal/terminal_test.go

@@ -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) {