ssh/terminal: support home, end, up and down keys.

R=golang-dev, dave
CC=golang-dev
https://golang.org/cl/9777043
This commit is contained in:
Adam Langley
2013-06-07 10:21:53 -04:00
parent 78f827c126
commit 7d4f6f0986
2 changed files with 163 additions and 38 deletions

View File

@@ -75,6 +75,17 @@ type Terminal struct {
// a read. It aliases into inBuf.
remainder []byte
inBuf [256]byte
// history contains previously entered commands so that they can be
// accessed with the up and down keys.
history stRingBuffer
// historyIndex stores the currently accessed history entry, where zero
// means the immediately previous entry.
historyIndex int
// When navigating up and down the history it's possible to return to
// the incomplete, initial line. That value is stored in
// historyPending.
historyPending string
}
// NewTerminal runs a VT100 terminal on the given ReadWriter. If the ReadWriter is
@@ -83,12 +94,13 @@ type Terminal struct {
// "> ").
func NewTerminal(c io.ReadWriter, prompt string) *Terminal {
return &Terminal{
Escape: &vt100EscapeCodes,
c: c,
prompt: prompt,
termWidth: 80,
termHeight: 24,
echo: true,
Escape: &vt100EscapeCodes,
c: c,
prompt: prompt,
termWidth: 80,
termHeight: 24,
echo: true,
historyIndex: -1,
}
}
@@ -104,6 +116,8 @@ const (
keyRight
keyAltLeft
keyAltRight
keyHome
keyEnd
)
// bytesToKey tries to parse a key sequence from b. If successful, it returns
@@ -130,6 +144,15 @@ func bytesToKey(b []byte) (int, []byte) {
}
}
if len(b) >= 3 && b[0] == keyEscape && b[1] == 'O' {
switch b[2] {
case 'H':
return keyHome, b[3:]
case 'F':
return keyEnd, b[3:]
}
}
if len(b) >= 6 && b[0] == keyEscape && b[1] == '[' && b[2] == '1' && b[3] == ';' && b[4] == '3' {
switch b[5] {
case 'C':
@@ -238,6 +261,19 @@ func (t *Terminal) clearLineToRight() {
const maxLineLength = 4096
func (t *Terminal) setLine(newLine []byte, newPos int) {
if t.echo {
t.moveCursorToPos(0)
t.writeLine(newLine)
for i := len(newLine); i < len(t.line); i++ {
t.writeLine(space)
}
t.moveCursorToPos(newPos)
}
t.line = newLine
t.pos = newPos
}
// handleKey processes the given key and, optionally, returns a line of text
// that the user has entered.
func (t *Terminal) handleKey(key int) (line string, ok bool) {
@@ -303,6 +339,42 @@ func (t *Terminal) handleKey(key int) (line string, ok bool) {
}
t.pos++
t.moveCursorToPos(t.pos)
case keyHome:
if t.pos == 0 {
return
}
t.pos = 0
t.moveCursorToPos(t.pos)
case keyEnd:
if t.pos == len(t.line) {
return
}
t.pos = len(t.line)
t.moveCursorToPos(t.pos)
case keyUp:
entry, ok := t.history.NthPreviousEntry(t.historyIndex + 1)
if !ok {
return "", false
}
if t.historyIndex == -1 {
t.historyPending = string(t.line)
}
t.historyIndex++
t.setLine([]byte(entry), len(entry))
case keyDown:
switch t.historyIndex {
case -1:
return
case 0:
t.setLine([]byte(t.historyPending), len(t.historyPending))
t.historyIndex--
default:
entry, ok := t.history.NthPreviousEntry(t.historyIndex - 1)
if ok {
t.historyIndex--
t.setLine([]byte(entry), len(entry))
}
}
case keyEnter:
t.moveCursorToPos(len(t.line))
t.queue([]byte("\r\n"))
@@ -320,16 +392,7 @@ func (t *Terminal) handleKey(key int) (line string, ok bool) {
t.lock.Lock()
if newLine != nil {
if t.echo {
t.moveCursorToPos(0)
t.writeLine(newLine)
for i := len(newLine); i < len(t.line); i++ {
t.writeLine(space)
}
t.moveCursorToPos(newPos)
}
t.line = newLine
t.pos = newPos
t.setLine(newLine, newPos)
return
}
}
@@ -483,6 +546,8 @@ func (t *Terminal) readLine() (line string, err error) {
t.c.Write(t.outBuf)
t.outBuf = t.outBuf[:0]
if lineOk {
t.historyIndex = -1
t.history.Add(line)
return
}
@@ -501,7 +566,6 @@ func (t *Terminal) readLine() (line string, err error) {
t.remainder = t.inBuf[:n+len(t.remainder)]
}
panic("unreachable")
}
// SetPrompt sets the prompt to be used when reading subsequent lines.
@@ -518,3 +582,43 @@ func (t *Terminal) SetSize(width, height int) {
t.termWidth, t.termHeight = width, height
}
// stRingBuffer is a ring buffer of strings.
type stRingBuffer struct {
// entries contains max elements.
entries []string
max int
// head contains the index of the element most recently added to the ring.
head int
// size contains the number of elements in the ring.
size int
}
func (s *stRingBuffer) Add(a string) {
if s.entries == nil {
const defaultNumEntries = 100
s.entries = make([]string, defaultNumEntries)
s.max = defaultNumEntries
}
s.head = (s.head + 1) % s.max
s.entries[s.head] = a
if s.size < s.max {
s.size++
}
}
// NthPreviousEntry returns the value passed to the nth previous call to Add.
// If n is zero then the immediately prior value is returned, if one, then the
// next most recent, and so on. If such an element doesn't exist then ok is
// false.
func (s *stRingBuffer) NthPreviousEntry(n int) (value string, ok bool) {
if n >= s.size {
return "", false
}
index := s.head - n
if index < 0 {
index += s.max
}
return s.entries[index], true
}

View File

@@ -52,39 +52,54 @@ func TestClose(t *testing.T) {
}
var keyPressTests = []struct {
in string
line string
err error
in string
line string
err error
throwAwayLines int
}{
{
"",
"",
io.EOF,
err: io.EOF,
},
{
"\r",
"",
nil,
in: "\r",
line: "",
},
{
"foo\r",
"foo",
nil,
in: "foo\r",
line: "foo",
},
{
"a\x1b[Cb\r", // right
"ab",
nil,
in: "a\x1b[Cb\r", // right
line: "ab",
},
{
"a\x1b[Db\r", // left
"ba",
nil,
in: "a\x1b[Db\r", // left
line: "ba",
},
{
"a\177b\r", // backspace
"b",
nil,
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,
},
}
@@ -96,6 +111,12 @@ func TestKeyPresses(t *testing.T) {
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)