Skip to content

Commit 02cc180

Browse files
committed
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.
1 parent b7391e9 commit 02cc180

File tree

2 files changed

+83
-28
lines changed

2 files changed

+83
-28
lines changed

ssh/terminal/terminal.go

Lines changed: 39 additions & 28 deletions
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() {

ssh/terminal/terminal_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ func TestClose(t *testing.T) {
5959
var keyPressTests = []struct {
6060
in string
6161
line string
62+
received string
6263
err error
6364
throwAwayLines int
6465
}{
@@ -237,6 +238,49 @@ func TestKeyPresses(t *testing.T) {
237238
}
238239
}
239240

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

0 commit comments

Comments
 (0)