Skip to content

Commit

Permalink
support home and end keys (#58)
Browse files Browse the repository at this point in the history
* support home and end keys

Fixes #53

* don't allocate to read ANSI sequences

* simplify home/end/delete switch
  • Loading branch information
slingamn authored May 5, 2024
1 parent c3eccd1 commit e90ce13
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 13 deletions.
41 changes: 41 additions & 0 deletions readline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,44 @@ func TestRace(t *testing.T) {

rl.Readline()
}

func TestParseCPRResponse(t *testing.T) {
badResponses := []string{
"",
";",
"\x00",
"\x00;",
";\x00",
"x",
"1;a",
"a;1",
"a;1;",
"1;1;",
"1;1;1",
}
for _, response := range badResponses {
if _, err := parseCPRResponse([]byte(response)); err == nil {
t.Fatalf("expected parsing of `%s` to fail, but did not", response)
}
}

goodResponses := []struct {
input string
output cursorPosition
}{
{"1;2", cursorPosition{1, 2}},
{"0;2", cursorPosition{0, 2}},
{"0;0", cursorPosition{0, 0}},
{"48378;9999999", cursorPosition{48378, 9999999}},
}

for _, response := range goodResponses {
got, err := parseCPRResponse([]byte(response.input))
if err != nil {
t.Fatalf("could not parse `%s`: %v", response.input, err)
}
if got != response.output {
t.Fatalf("expected %s to parse to %#v, got %#v", response.input, response.output, got)
}
}
}
33 changes: 20 additions & 13 deletions terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package readline

import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
Expand Down Expand Up @@ -276,6 +276,7 @@ func (t *terminal) ioloop() {
defer t.Close()

buf := bufio.NewReader(t.GetConfig().Stdin)
var ansiBuf bytes.Buffer

for {
select {
Expand All @@ -293,7 +294,7 @@ func (t *terminal) ioloop() {
if r == '\x1b' {
// we're starting an ANSI escape sequence:
// keep reading until we reach the end of the sequence
result, err = t.consumeANSIEscape(buf)
result, err = t.consumeANSIEscape(buf, &ansiBuf)
if err != nil {
return
}
Expand All @@ -309,7 +310,8 @@ func (t *terminal) ioloop() {
}
}

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

// data consists of ; and 0-9 , anything else terminates the sequence
var dataBuf strings.Builder
var type_ rune
for {
r, _, err := buf.ReadRune()
if err != nil {
return result, err
}
if r == ';' || ('0' <= r && r <= '9') {
dataBuf.WriteRune(r)
ansiBuf.WriteRune(r)
} else {
type_ = r
break
}
}
data := dataBuf.String()

var r rune
switch type_ {
case 'R':
if initial == '[' {
// DSR CPR response; if we can't parse it, just ignore it
// (do not return an error here because that would stop ioloop())
if cpos, err := parseCPRResponse(data); err == nil {
if cpos, err := parseCPRResponse(ansiBuf.Bytes()); err == nil {
return readResult{r: 0, ok: false, pos: &cpos}, nil
}
}
Expand All @@ -357,8 +357,15 @@ func (t *terminal) consumeANSIEscape(buf *bufio.Reader) (result readResult, err
case 'F':
r = CharLineEnd
case '~':
if initial == '[' && data == "3" {
r = MetaDeleteKey // this is the key typically labeled "Delete"
if initial == '[' {
switch string(ansiBuf.Bytes()) {
case "3":
r = MetaDeleteKey // this is the key typically labeled "Delete"
case "7":
r = CharLineStart // "Home" key
case "8":
r = CharLineEnd // "End" key
}
}
case 'Z':
if initial == '[' {
Expand All @@ -372,10 +379,10 @@ func (t *terminal) consumeANSIEscape(buf *bufio.Reader) (result readResult, err
return // default: no interpretable rune value
}

func parseCPRResponse(payload string) (cursorPosition, error) {
if parts := strings.Split(payload, ";"); len(parts) == 2 {
if row, err := strconv.Atoi(parts[0]); err == nil {
if col, err := strconv.Atoi(parts[1]); err == nil {
func parseCPRResponse(payload []byte) (cursorPosition, error) {
if semicolonIdx := bytes.IndexByte(payload, ';'); semicolonIdx != -1 {
if row, err := strconv.Atoi(string(payload[:semicolonIdx])); err == nil {
if col, err := strconv.Atoi(string(payload[semicolonIdx+1:])); err == nil {
return cursorPosition{row: row, col: col}, nil
}
}
Expand Down

0 comments on commit e90ce13

Please sign in to comment.