Skip to content

Commit a5d413f

Browse files
shazowbradfitz
authored andcommitted
ssh/terminal: Use move-N sequences for >1 cursor moves
Before, we emitted N single-move sequences on a cursor move. For example, "move 4 left" would emit "^[[D^[[D^[[D^[[D". With this change, it would emit "^[[4D". Using variable move sequences when possible reduces the amount of rendering output that the terminal implementation produces. This can have some low-level performance benefits, but also helps consumers reason through the produced output. Includes a test with a couple of cases. Note: The old implementation used ^[[D instead of ^[D which is also valid. This is true in several unrelated places, so this implementation continues to use ^[[D for consistency. Change-Id: If38eaaed8fb4075499fdda54c06681dc34c3ad70 GitHub-Last-Rev: 92ef253 GitHub-Pull-Request: #82 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/169077 Reviewed-by: Adam Langley <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent b7391e9 commit a5d413f

File tree

2 files changed

+82
-28
lines changed

2 files changed

+82
-28
lines changed

Diff for: ssh/terminal/terminal.go

+39-28
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package terminal
77
import (
88
"bytes"
99
"io"
10+
"strconv"
1011
"sync"
1112
"unicode/utf8"
1213
)
@@ -271,34 +272,44 @@ func (t *Terminal) moveCursorToPos(pos int) {
271272
}
272273

273274
func (t *Terminal) move(up, down, left, right int) {
274-
movement := make([]rune, 3*(up+down+left+right))
275-
m := movement
276-
for i := 0; i < up; i++ {
277-
m[0] = keyEscape
278-
m[1] = '['
279-
m[2] = 'A'
280-
m = m[3:]
281-
}
282-
for i := 0; i < down; i++ {
283-
m[0] = keyEscape
284-
m[1] = '['
285-
m[2] = 'B'
286-
m = m[3:]
287-
}
288-
for i := 0; i < left; i++ {
289-
m[0] = keyEscape
290-
m[1] = '['
291-
m[2] = 'D'
292-
m = m[3:]
293-
}
294-
for i := 0; i < right; i++ {
295-
m[0] = keyEscape
296-
m[1] = '['
297-
m[2] = 'C'
298-
m = m[3:]
299-
}
300-
301-
t.queue(movement)
275+
m := []rune{}
276+
277+
// 1 unit up can be expressed as ^[[A or ^[A
278+
// 5 units up can be expressed as ^[[5A
279+
280+
if up == 1 {
281+
m = append(m, keyEscape, '[', 'A')
282+
} else if up > 1 {
283+
m = append(m, keyEscape, '[')
284+
m = append(m, []rune(strconv.Itoa(up))...)
285+
m = append(m, 'A')
286+
}
287+
288+
if down == 1 {
289+
m = append(m, keyEscape, '[', 'B')
290+
} else if down > 1 {
291+
m = append(m, keyEscape, '[')
292+
m = append(m, []rune(strconv.Itoa(down))...)
293+
m = append(m, 'B')
294+
}
295+
296+
if right == 1 {
297+
m = append(m, keyEscape, '[', 'C')
298+
} else if right > 1 {
299+
m = append(m, keyEscape, '[')
300+
m = append(m, []rune(strconv.Itoa(right))...)
301+
m = append(m, 'C')
302+
}
303+
304+
if left == 1 {
305+
m = append(m, keyEscape, '[', 'D')
306+
} else if left > 1 {
307+
m = append(m, keyEscape, '[')
308+
m = append(m, []rune(strconv.Itoa(left))...)
309+
m = append(m, 'D')
310+
}
311+
312+
t.queue(m)
302313
}
303314

304315
func (t *Terminal) clearLineToRight() {

Diff for: ssh/terminal/terminal_test.go

+43
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,49 @@ func TestKeyPresses(t *testing.T) {
237237
}
238238
}
239239

240+
var renderTests = []struct {
241+
in string
242+
received string
243+
err error
244+
}{
245+
{
246+
// Cursor move after keyHome (left 4) then enter (right 4, newline)
247+
in: "abcd\x1b[H\r",
248+
received: "> abcd\x1b[4D\x1b[4C\r\n",
249+
},
250+
{
251+
// Write, home, prepend, enter. Prepends rewrites the line.
252+
in: "cdef\x1b[Hab\r",
253+
received: "> cdef" + // Initial input
254+
"\x1b[4Da" + // Move cursor back, insert first char
255+
"cdef" + // Copy over original string
256+
"\x1b[4Dbcdef" + // Repeat for second char with copy
257+
"\x1b[4D" + // Put cursor back in position to insert again
258+
"\x1b[4C\r\n", // Put cursor at the end of the line and newline.
259+
},
260+
}
261+
262+
func TestRender(t *testing.T) {
263+
for i, test := range renderTests {
264+
for j := 1; j < len(test.in); j++ {
265+
c := &MockTerminal{
266+
toSend: []byte(test.in),
267+
bytesPerRead: j,
268+
}
269+
ss := NewTerminal(c, "> ")
270+
_, err := ss.ReadLine()
271+
if err != test.err {
272+
t.Errorf("Error resulting from test %d (%d bytes per read) was '%v', expected '%v'", i, j, err, test.err)
273+
break
274+
}
275+
if test.received != string(c.received) {
276+
t.Errorf("Results rendered from test %d (%d bytes per read) was '%s', expected '%s'", i, j, c.received, test.received)
277+
break
278+
}
279+
}
280+
}
281+
}
282+
240283
func TestPasswordNotSaved(t *testing.T) {
241284
c := &MockTerminal{
242285
toSend: []byte("password\r\x1b[A\r"),

0 commit comments

Comments
 (0)