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
244 lines
4.6 KiB
Go
244 lines
4.6 KiB
Go
// Copyright 2011 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package terminal
|
|
|
|
import (
|
|
"io"
|
|
"testing"
|
|
)
|
|
|
|
type MockTerminal struct {
|
|
toSend []byte
|
|
bytesPerRead int
|
|
received []byte
|
|
}
|
|
|
|
func (c *MockTerminal) Read(data []byte) (n int, err error) {
|
|
n = len(data)
|
|
if n == 0 {
|
|
return
|
|
}
|
|
if n > len(c.toSend) {
|
|
n = len(c.toSend)
|
|
}
|
|
if n == 0 {
|
|
return 0, io.EOF
|
|
}
|
|
if c.bytesPerRead > 0 && n > c.bytesPerRead {
|
|
n = c.bytesPerRead
|
|
}
|
|
copy(data, c.toSend[:n])
|
|
c.toSend = c.toSend[n:]
|
|
return
|
|
}
|
|
|
|
func (c *MockTerminal) Write(data []byte) (n int, err error) {
|
|
c.received = append(c.received, data...)
|
|
return len(data), nil
|
|
}
|
|
|
|
func TestClose(t *testing.T) {
|
|
c := &MockTerminal{}
|
|
ss := NewTerminal(c, "> ")
|
|
line, err := ss.ReadLine()
|
|
if line != "" {
|
|
t.Errorf("Expected empty line but got: %s", line)
|
|
}
|
|
if err != io.EOF {
|
|
t.Errorf("Error should have been EOF but got: %s", err)
|
|
}
|
|
}
|
|
|
|
var keyPressTests = []struct {
|
|
in string
|
|
line string
|
|
err error
|
|
throwAwayLines int
|
|
}{
|
|
{
|
|
err: io.EOF,
|
|
},
|
|
{
|
|
in: "\r",
|
|
line: "",
|
|
},
|
|
{
|
|
in: "foo\r",
|
|
line: "foo",
|
|
},
|
|
{
|
|
in: "a\x1b[Cb\r", // right
|
|
line: "ab",
|
|
},
|
|
{
|
|
in: "a\x1b[Db\r", // left
|
|
line: "ba",
|
|
},
|
|
{
|
|
in: "a\177b\r", // backspace
|
|
line: "b",
|
|
},
|
|
{
|
|
in: "\x1b[A\r", // up
|
|
},
|
|
{
|
|
in: "\x1b[B\r", // down
|
|
},
|
|
{
|
|
in: "line\x1b[A\x1b[B\r", // up then down
|
|
line: "line",
|
|
},
|
|
{
|
|
in: "line1\rline2\x1b[A\r", // recall previous line.
|
|
line: "line1",
|
|
throwAwayLines: 1,
|
|
},
|
|
{
|
|
// recall two previous lines and append.
|
|
in: "line1\rline2\rline3\x1b[A\x1b[Axxx\r",
|
|
line: "line1xxx",
|
|
throwAwayLines: 2,
|
|
},
|
|
{
|
|
// Ctrl-A to move to beginning of line followed by ^K to kill
|
|
// line.
|
|
in: "a b \001\013\r",
|
|
line: "",
|
|
},
|
|
{
|
|
// Ctrl-A to move to beginning of line, Ctrl-E to move to end,
|
|
// finally ^K to kill nothing.
|
|
in: "a b \001\005\013\r",
|
|
line: "a b ",
|
|
},
|
|
{
|
|
in: "\027\r",
|
|
line: "",
|
|
},
|
|
{
|
|
in: "a\027\r",
|
|
line: "",
|
|
},
|
|
{
|
|
in: "a \027\r",
|
|
line: "",
|
|
},
|
|
{
|
|
in: "a b\027\r",
|
|
line: "a ",
|
|
},
|
|
{
|
|
in: "a b \027\r",
|
|
line: "a ",
|
|
},
|
|
{
|
|
in: "one two thr\x1b[D\027\r",
|
|
line: "one two r",
|
|
},
|
|
{
|
|
in: "\013\r",
|
|
line: "",
|
|
},
|
|
{
|
|
in: "a\013\r",
|
|
line: "a",
|
|
},
|
|
{
|
|
in: "ab\x1b[D\013\r",
|
|
line: "a",
|
|
},
|
|
{
|
|
in: "Ξεσκεπάζω\r",
|
|
line: "Ξεσκεπάζω",
|
|
},
|
|
{
|
|
in: "£\r\x1b[A\177\r", // non-ASCII char, enter, up, backspace.
|
|
line: "",
|
|
throwAwayLines: 1,
|
|
},
|
|
{
|
|
in: "£\r££\x1b[A\x1b[B\177\r", // non-ASCII char, enter, 2x non-ASCII, up, down, backspace, enter.
|
|
line: "£",
|
|
throwAwayLines: 1,
|
|
},
|
|
{
|
|
// Ctrl-D at the end of the line should be ignored.
|
|
in: "a\004\r",
|
|
line: "a",
|
|
},
|
|
{
|
|
// a, b, left, Ctrl-D should erase the b.
|
|
in: "ab\x1b[D\004\r",
|
|
line: "a",
|
|
},
|
|
{
|
|
// a, b, c, d, left, left, ^U should erase to the beginning of
|
|
// the line.
|
|
in: "abcd\x1b[D\x1b[D\025\r",
|
|
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) {
|
|
for i, test := range keyPressTests {
|
|
for j := 1; j < len(test.in); j++ {
|
|
c := &MockTerminal{
|
|
toSend: []byte(test.in),
|
|
bytesPerRead: j,
|
|
}
|
|
ss := NewTerminal(c, "> ")
|
|
for k := 0; k < test.throwAwayLines; k++ {
|
|
_, err := ss.ReadLine()
|
|
if err != nil {
|
|
t.Errorf("Throwaway line %d from test %d resulted in error: %s", k, i, err)
|
|
}
|
|
}
|
|
line, err := ss.ReadLine()
|
|
if line != test.line {
|
|
t.Errorf("Line resulting from test %d (%d bytes per read) was '%s', expected '%s'", i, j, line, test.line)
|
|
break
|
|
}
|
|
if err != test.err {
|
|
t.Errorf("Error resulting from test %d (%d bytes per read) was '%v', expected '%v'", i, j, err, test.err)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPasswordNotSaved(t *testing.T) {
|
|
c := &MockTerminal{
|
|
toSend: []byte("password\r\x1b[A\r"),
|
|
bytesPerRead: 1,
|
|
}
|
|
ss := NewTerminal(c, "> ")
|
|
pw, _ := ss.ReadPassword("> ")
|
|
if pw != "password" {
|
|
t.Fatalf("failed to read password, got %s", pw)
|
|
}
|
|
line, _ := ss.ReadLine()
|
|
if len(line) > 0 {
|
|
t.Fatalf("password was saved in history")
|
|
}
|
|
}
|