From 7d4f6f0986732548c5c620acb3fc56ba3d06ca13 Mon Sep 17 00:00:00 2001 From: Adam Langley Date: Fri, 7 Jun 2013 10:21:53 -0400 Subject: [PATCH] ssh/terminal: support home, end, up and down keys. R=golang-dev, dave CC=golang-dev https://golang.org/cl/9777043 --- terminal.go | 138 +++++++++++++++++++++++++++++++++++++++++------ terminal_test.go | 63 ++++++++++++++-------- 2 files changed, 163 insertions(+), 38 deletions(-) diff --git a/terminal.go b/terminal.go index c1ed0c0..d956b51 100644 --- a/terminal.go +++ b/terminal.go @@ -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 +} diff --git a/terminal_test.go b/terminal_test.go index a219721..ffcda79 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -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)