Skip to content

Commit e90ce13

Browse files
authored
support home and end keys (#58)
* support home and end keys Fixes #53 * don't allocate to read ANSI sequences * simplify home/end/delete switch
1 parent c3eccd1 commit e90ce13

File tree

2 files changed

+61
-13
lines changed

2 files changed

+61
-13
lines changed

readline_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,44 @@ func TestRace(t *testing.T) {
2525

2626
rl.Readline()
2727
}
28+
29+
func TestParseCPRResponse(t *testing.T) {
30+
badResponses := []string{
31+
"",
32+
";",
33+
"\x00",
34+
"\x00;",
35+
";\x00",
36+
"x",
37+
"1;a",
38+
"a;1",
39+
"a;1;",
40+
"1;1;",
41+
"1;1;1",
42+
}
43+
for _, response := range badResponses {
44+
if _, err := parseCPRResponse([]byte(response)); err == nil {
45+
t.Fatalf("expected parsing of `%s` to fail, but did not", response)
46+
}
47+
}
48+
49+
goodResponses := []struct {
50+
input string
51+
output cursorPosition
52+
}{
53+
{"1;2", cursorPosition{1, 2}},
54+
{"0;2", cursorPosition{0, 2}},
55+
{"0;0", cursorPosition{0, 0}},
56+
{"48378;9999999", cursorPosition{48378, 9999999}},
57+
}
58+
59+
for _, response := range goodResponses {
60+
got, err := parseCPRResponse([]byte(response.input))
61+
if err != nil {
62+
t.Fatalf("could not parse `%s`: %v", response.input, err)
63+
}
64+
if got != response.output {
65+
t.Fatalf("expected %s to parse to %#v, got %#v", response.input, response.output, got)
66+
}
67+
}
68+
}

terminal.go

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ package readline
22

33
import (
44
"bufio"
5+
"bytes"
56
"errors"
67
"fmt"
78
"io"
89
"strconv"
9-
"strings"
1010
"sync"
1111
"sync/atomic"
1212
"time"
@@ -276,6 +276,7 @@ func (t *terminal) ioloop() {
276276
defer t.Close()
277277

278278
buf := bufio.NewReader(t.GetConfig().Stdin)
279+
var ansiBuf bytes.Buffer
279280

280281
for {
281282
select {
@@ -293,7 +294,7 @@ func (t *terminal) ioloop() {
293294
if r == '\x1b' {
294295
// we're starting an ANSI escape sequence:
295296
// keep reading until we reach the end of the sequence
296-
result, err = t.consumeANSIEscape(buf)
297+
result, err = t.consumeANSIEscape(buf, &ansiBuf)
297298
if err != nil {
298299
return
299300
}
@@ -309,7 +310,8 @@ func (t *terminal) ioloop() {
309310
}
310311
}
311312

312-
func (t *terminal) consumeANSIEscape(buf *bufio.Reader) (result readResult, err error) {
313+
func (t *terminal) consumeANSIEscape(buf *bufio.Reader, ansiBuf *bytes.Buffer) (result readResult, err error) {
314+
ansiBuf.Reset()
313315
// initial character is either [ or O; if we got something else,
314316
// treat the sequence as terminated and don't interpret it
315317
initial, _, err := buf.ReadRune()
@@ -318,29 +320,27 @@ func (t *terminal) consumeANSIEscape(buf *bufio.Reader) (result readResult, err
318320
}
319321

320322
// data consists of ; and 0-9 , anything else terminates the sequence
321-
var dataBuf strings.Builder
322323
var type_ rune
323324
for {
324325
r, _, err := buf.ReadRune()
325326
if err != nil {
326327
return result, err
327328
}
328329
if r == ';' || ('0' <= r && r <= '9') {
329-
dataBuf.WriteRune(r)
330+
ansiBuf.WriteRune(r)
330331
} else {
331332
type_ = r
332333
break
333334
}
334335
}
335-
data := dataBuf.String()
336336

337337
var r rune
338338
switch type_ {
339339
case 'R':
340340
if initial == '[' {
341341
// DSR CPR response; if we can't parse it, just ignore it
342342
// (do not return an error here because that would stop ioloop())
343-
if cpos, err := parseCPRResponse(data); err == nil {
343+
if cpos, err := parseCPRResponse(ansiBuf.Bytes()); err == nil {
344344
return readResult{r: 0, ok: false, pos: &cpos}, nil
345345
}
346346
}
@@ -357,8 +357,15 @@ func (t *terminal) consumeANSIEscape(buf *bufio.Reader) (result readResult, err
357357
case 'F':
358358
r = CharLineEnd
359359
case '~':
360-
if initial == '[' && data == "3" {
361-
r = MetaDeleteKey // this is the key typically labeled "Delete"
360+
if initial == '[' {
361+
switch string(ansiBuf.Bytes()) {
362+
case "3":
363+
r = MetaDeleteKey // this is the key typically labeled "Delete"
364+
case "7":
365+
r = CharLineStart // "Home" key
366+
case "8":
367+
r = CharLineEnd // "End" key
368+
}
362369
}
363370
case 'Z':
364371
if initial == '[' {
@@ -372,10 +379,10 @@ func (t *terminal) consumeANSIEscape(buf *bufio.Reader) (result readResult, err
372379
return // default: no interpretable rune value
373380
}
374381

375-
func parseCPRResponse(payload string) (cursorPosition, error) {
376-
if parts := strings.Split(payload, ";"); len(parts) == 2 {
377-
if row, err := strconv.Atoi(parts[0]); err == nil {
378-
if col, err := strconv.Atoi(parts[1]); err == nil {
382+
func parseCPRResponse(payload []byte) (cursorPosition, error) {
383+
if semicolonIdx := bytes.IndexByte(payload, ';'); semicolonIdx != -1 {
384+
if row, err := strconv.Atoi(string(payload[:semicolonIdx])); err == nil {
385+
if col, err := strconv.Atoi(string(payload[semicolonIdx+1:])); err == nil {
379386
return cursorPosition{row: row, col: col}, nil
380387
}
381388
}

0 commit comments

Comments
 (0)