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 c7df565dd4
commit 59566675d5
2 changed files with 163 additions and 38 deletions
+121 -17
View File
@@ -75,6 +75,17 @@ type Terminal struct {
// a read. It aliases into inBuf. // a read. It aliases into inBuf.
remainder []byte remainder []byte
inBuf [256]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 // 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 { func NewTerminal(c io.ReadWriter, prompt string) *Terminal {
return &Terminal{ return &Terminal{
Escape: &vt100EscapeCodes, Escape: &vt100EscapeCodes,
c: c, c: c,
prompt: prompt, prompt: prompt,
termWidth: 80, termWidth: 80,
termHeight: 24, termHeight: 24,
echo: true, echo: true,
historyIndex: -1,
} }
} }
@@ -104,6 +116,8 @@ const (
keyRight keyRight
keyAltLeft keyAltLeft
keyAltRight keyAltRight
keyHome
keyEnd
) )
// 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
@@ -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' { if 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':
@@ -238,6 +261,19 @@ func (t *Terminal) clearLineToRight() {
const maxLineLength = 4096 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 // 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 int) (line string, ok bool) { 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.pos++
t.moveCursorToPos(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: case keyEnter:
t.moveCursorToPos(len(t.line)) t.moveCursorToPos(len(t.line))
t.queue([]byte("\r\n")) t.queue([]byte("\r\n"))
@@ -320,16 +392,7 @@ func (t *Terminal) handleKey(key int) (line string, ok bool) {
t.lock.Lock() t.lock.Lock()
if newLine != nil { if newLine != nil {
if t.echo { t.setLine(newLine, newPos)
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
return return
} }
} }
@@ -483,6 +546,8 @@ func (t *Terminal) readLine() (line string, err error) {
t.c.Write(t.outBuf) t.c.Write(t.outBuf)
t.outBuf = t.outBuf[:0] t.outBuf = t.outBuf[:0]
if lineOk { if lineOk {
t.historyIndex = -1
t.history.Add(line)
return return
} }
@@ -501,7 +566,6 @@ func (t *Terminal) readLine() (line string, err error) {
t.remainder = t.inBuf[:n+len(t.remainder)] t.remainder = t.inBuf[:n+len(t.remainder)]
} }
panic("unreachable")
} }
// SetPrompt sets the prompt to be used when reading subsequent lines. // 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 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
}
+42 -21
View File
@@ -52,39 +52,54 @@ func TestClose(t *testing.T) {
} }
var keyPressTests = []struct { var keyPressTests = []struct {
in string in string
line string line string
err error err error
throwAwayLines int
}{ }{
{ {
"", err: io.EOF,
"",
io.EOF,
}, },
{ {
"\r", in: "\r",
"", line: "",
nil,
}, },
{ {
"foo\r", in: "foo\r",
"foo", line: "foo",
nil,
}, },
{ {
"a\x1b[Cb\r", // right in: "a\x1b[Cb\r", // right
"ab", line: "ab",
nil,
}, },
{ {
"a\x1b[Db\r", // left in: "a\x1b[Db\r", // left
"ba", line: "ba",
nil,
}, },
{ {
"a\177b\r", // backspace in: "a\177b\r", // backspace
"b", line: "b",
nil, },
{
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, bytesPerRead: j,
} }
ss := NewTerminal(c, "> ") 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() line, err := ss.ReadLine()
if line != test.line { if line != test.line {
t.Errorf("Line resulting from test %d (%d bytes per read) was '%s', expected '%s'", i, j, line, test.line) t.Errorf("Line resulting from test %d (%d bytes per read) was '%s', expected '%s'", i, j, line, test.line)