~mna/zzterm

6b2ae1ec1f935ba681e198cad05e728c57f3e6a9 — Martin Angers 4 years ago 5be589c v0.3.0
support decoding multiple keys from one buffer, and load more if needed
2 files changed, 200 insertions(+), 34 deletions(-)

M input.go
M input_test.go
M input.go => input.go +91 -34
@@ 8,10 8,35 @@ import (
	"unicode/utf8"
)

// TimeoutError is the type of the error returned when ReadKey fails
// to return a key due to the read timeout expiring. If the underlying
// Read call returned an error, this error wraps it and it can be
// unwrapped with errors.Unwrap.
type TimeoutError struct {
	err error
}

// Error returns the error message for the TimeoutError.
func (e TimeoutError) Error() string {
	return "zzterm: timeout"
}

// Unwrap returns a non-nil error if TimeoutError wraps an underlying
// error returned by Read.
func (e TimeoutError) Unwrap() error {
	return e.err
}

// Timeout returns true.
func (e TimeoutError) Timeout() bool {
	return true
}

// Input reads input keys from a reader and returns the key pressed.
type Input struct {
	buf   []byte
	lastn int
	sz    int // size of the last key
	len   int // len of bytes loaded in the buffer
	lastm MouseEvent

	// immutable after NewInput


@@ 150,10 175,10 @@ func NewInput(opts ...Option) *Input {
// Bytes returns the uninterpreted bytes from the last key read. The bytes
// are valid only until the next call to ReadKey and should not be modified.
func (i *Input) Bytes() []byte {
	if i.lastn <= 0 {
	if i.sz <= 0 {
		return nil
	}
	return i.buf[:i.lastn:i.lastn]
	return i.buf[:i.sz:i.sz]
}

// Mouse returns the mouse event corresponding to the last key of type KeyMouse.


@@ 165,66 190,98 @@ func (i *Input) Mouse() MouseEvent {

const sgrMouseEventPrefix = "\x1b[<"

// ReadKey reads a key from r.
// ReadKey reads a key from r which should be the reader of a terminal set in raw
// mode. It is recommended to set a read timeout on the raw terminal so that a
// Read does not block indefinitely. In that case, if a call to ReadKey times out
// witout data for a key, it returns the zero-value of Key and a TimeoutError.
func (i *Input) ReadKey(r io.Reader) (Key, error) {
	if i.sz > 0 {
		// move buffer start to index 0 so that the maximum buffer
		// size is available for more reads if required and reads start
		// at 0.
		copy(i.buf, i.buf[i.sz:i.len])
		i.len -= i.sz
		i.sz = 0
	}

	// TODO: first, check if i.lastn > 0 and i.rd < i.lastn. If so, there
	// are more keys to decode from the buffer, try to get a rune from it.
	// If there is no valid rune from the buffer, then do a Read, using
	// the buffer after i.lastn. If the read times out, return invalid
	// rune error, not timeout, and consume that invalid rune byte(s).

	i.lastn = 0 // TODO: and i.rd = 0
	n, err := r.Read(i.buf)
	if err != nil || n == 0 {
		// TODO: if n == 0 and (err == nil || err == io.EOF || err.Timeout() == true)
		// return ErrTimeout (wrapping the original error and implementing Timeout() bool).
		return 0, err
	var rn rune = -1
	if i.len > 0 {
		// try to read a rune from the already loaded bytes
		c, sz := utf8.DecodeRune(i.buf[:i.len])
		if c == utf8.RuneError && sz < 2 {
			rn = -1
		} else {
			// valid rune
			rn = c
			i.sz = sz
		}
	}
	i.lastn = n
	buf := i.buf[:n]

	c, sz := utf8.DecodeRune(buf)
	if c == utf8.RuneError && sz < 2 {
		// TODO: i.rd++, always consume at least one byte
		return 0, errors.New("invalid rune")
	// if no valid rune, read more bytes
	if rn < 0 {
		n, err := r.Read(i.buf[i.len:])
		if err != nil || n == 0 {
			if i.len > 0 {
				// we have a partial (invalid) rune, skip over a byte, do
				// not return timeout error in this case (we have a byte)
				i.sz = 1
				return 0, errors.New("invalid rune")
			}
			// otherwise we have no byte at all, return TimeoutError if
			// n == 0 and (err == nil || err == io.EOF || err.Timeout() == true)
			if n == 0 {
				to, ok := err.(interface{ Timeout() bool })
				if err == nil || err == io.EOF || (ok && to.Timeout()) {
					return 0, TimeoutError{err: err}
				}
			}
			return 0, err
		}

		i.len += n
		c, sz := utf8.DecodeRune(i.buf[:i.len])
		if c == utf8.RuneError && sz < 2 {
			i.sz = 1 // always consume at least one byte
			return 0, errors.New("invalid rune")
		}
		rn = c
		i.sz = sz
	}
	// TODO: i.rd = sz

	// if c is a control character (if n == 1 so that if an escape
	// if rn is a control character (if i.len == 1 so that if an escape
	// sequence is read, it does not return immediately with just ESC)
	if n == 1 && (KeyType(c) <= KeyUS || KeyType(c) == KeyDEL) {
		return keyFromTypeMod(KeyType(c), ModNone), nil
	if i.len == 1 && (KeyType(rn) <= KeyUS || KeyType(rn) == KeyDEL) {
		return keyFromTypeMod(KeyType(rn), ModNone), nil
	}

	// translate escape sequences
	if KeyType(c) == KeyESC {
		if i.mouse && bytes.HasPrefix(buf, []byte(sgrMouseEventPrefix)) {
	if KeyType(rn) == KeyESC {
		if i.mouse && bytes.HasPrefix(i.buf[:i.len], []byte(sgrMouseEventPrefix)) {
			if k := i.decodeMouseEvent(); k.Type() == KeyMouse {
				// TODO: i.rd = i.lastn, reset with a fresh read on next key
				i.sz = i.len
				return k, nil
			}
		}
		// NOTE: important to use the string conversion exactly like that,
		// inside the brackets of the map key - the Go compiler optimizes
		// this to avoid any allocation.
		if key, ok := i.esc[string(buf)]; ok {
			// TODO: i.rd = i.lastn, reset with a fresh read on next key
		if key, ok := i.esc[string(i.buf[:i.len])]; ok {
			i.sz = i.len
			return key, nil
		}
		// if this is an unknown escape sequence, return KeyESCSeq and the
		// caller may get the uninterpreted sequence from i.Bytes.
		// TODO: i.rd = i.lastn, reset with a fresh read on next key
		i.sz = i.len
		return keyFromTypeMod(KeyESCSeq, ModNone), nil
	}
	return Key(c), nil
	return Key(rn), nil
}

// returns either a KeyMouse key, or a KeyESCSeq if it can't properly decode
// the mouse event.
func (i *Input) decodeMouseEvent() Key {
	// the prefix has already been validated, strip it from the working buffer
	buf := i.buf[len(sgrMouseEventPrefix):i.lastn]
	buf := i.buf[len(sgrMouseEventPrefix):i.len]
	if len(buf) < 6 {
		// 2 semicolons, trailing m/M, at least one byte in each section
		return keyFromTypeMod(KeyESCSeq, ModNone)

M input_test.go => input_test.go +109 -0
@@ 3,12 3,100 @@ package zzterm
import (
	"bytes"
	"encoding/json"
	"errors"
	"io"
	"io/ioutil"
	"log"
	"os"
	"strings"
	"testing"
)

func TestInput_ReadKey_Multiple(t *testing.T) {
	invalidRuneKey := Key('\x01')

	cases := []struct {
		in    string
		keys  []Key
		bytes []string
	}{
		{"", nil, nil},
		{"a", []Key{Key('a')}, []string{"a"}},
		{"ab", []Key{Key('a'), Key('b')}, []string{"a", "b"}},
		{"\xff", []Key{invalidRuneKey}, []string{"\xff"}},
		{"\xffa", []Key{invalidRuneKey, Key('a')}, []string{"\xff", "a"}},
		{"😿\x1b[abc", []Key{Key('😿'), keyFromTypeMod(KeyESCSeq, ModNone)}, []string{"😿", "\x1b[abc"}},
	}

	input := NewInput(WithMouse(), WithFocus())
	for _, c := range cases {
		t.Run(c.in, func(t *testing.T) {
			r := strings.NewReader(c.in)
			for i, wantk := range c.keys {
				wantb := c.bytes[i]
				got, err := input.ReadKey(r)
				if wantk == invalidRuneKey {
					if err.Error() != "invalid rune" {
						t.Fatalf("[%d]: want invalid rune, got %v", i, err)
					}
					wantk = Key(0)
				} else if err != nil {
					t.Fatalf("[%d]: want %s, got error %v", i, wantk, err)
				}

				if got.Type() != wantk.Type() {
					t.Fatalf("[%d]: want key type %s, got %s", i, wantk.Type(), got.Type())
				}
				if gotb := string(input.Bytes()); gotb != wantb {
					t.Fatalf("[%d]: want bytes %q, got %q", i, wantb, gotb)
				}
			}

			// after the loop, must return a "timeout" error (via EOF)
			got, err := input.ReadKey(r)
			if !errors.As(err, &TimeoutError{}) {
				t.Fatalf("after loop: want TimeoutError, got %v (key %v)", err, got)
			}
			if err := errors.Unwrap(err); err != io.EOF {
				t.Fatalf("after loop: want TimeoutError to wrap io.EOF, got %v", err)
			}
			if !os.IsTimeout(err) {
				t.Fatal("after loop: want TimeoutError to be identified as such with os.IsTimeout")
			}
		})
	}
}

func TestInput_ReadKey_BustBuffer(t *testing.T) {
	// this '⬼ ' character is 3 bytes in utf-8, so it ends up crossing
	// over the buffer size (which is an even number). This tests that
	// ReadKey properly tries a Read to get more bytes when it only
	// has an invalid rune to work with.
	want, wantn := "\xE2\xAC\xBC", 100
	r := strings.NewReader(strings.Repeat(want, wantn))
	input := NewInput()
	var count int
	for {
		key, err := input.ReadKey(r)
		if errors.Is(err, io.EOF) {
			break
		}
		if err != nil {
			t.Fatal(err)
		}
		count++
		if got := string(input.Bytes()); got != want {
			t.Fatalf("[%d]: got bytes %q", count, got)
		}
		if key.Type() != KeyRune || key.Rune() != '⬼' {
			t.Fatalf("[%d]: unexpected key %v", count, key)
		}
	}
	if count != 100 {
		t.Fatalf("want %d keys, got %d", wantn, count)
	}
}

type testcase struct {
	in  string
	r   rune


@@ 301,3 389,24 @@ func BenchmarkInput_ReadKey_Mouse(b *testing.B) {
		r.Reset(data)
	}
}

func BenchmarkInput_ReadKey_Multiple(b *testing.B) {
	input := NewInput(WithMouse())
	data := "a⬼\x1b[<6;123;542M"
	r := strings.NewReader(data)
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		var count int
		for j := 0; j < 3; j++ {
			if _, err := input.ReadKey(r); err != nil {
				b.Fatal(err)
			}
			count++
		}
		if count != 3 {
			b.Fatalf("want 3 keys, got %d", count)
		}
		r.Reset(data)
	}
}