~mna/zzterm

f420c6641afb5c30bdaaea99650688b6b59b26f1 — Martin Angers 4 years ago 6b2ae1e v0.4.0
do not allocate on timeout, return a constant error value
2 files changed, 40 insertions(+), 28 deletions(-)

M input.go
M input_test.go
M input.go => input.go +12 -20
@@ 8,30 8,22 @@ 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"
}
type timeoutError string

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

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

// ErrTimeout is the error returned when ReadKey fails to return a key due to
// the read timeout expiring.
const ErrTimeout = timeoutError("zzterm: timetout")

// Input reads input keys from a reader and returns the key pressed.
type Input struct {
	buf   []byte


@@ 193,7 185,7 @@ const sgrMouseEventPrefix = "\x1b[<"
// 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.
// witout data for a key, it returns the zero-value of Key and ErrTimeout.
func (i *Input) ReadKey(r io.Reader) (Key, error) {
	if i.sz > 0 {
		// move buffer start to index 0 so that the maximum buffer


@@ 227,12 219,12 @@ func (i *Input) ReadKey(r io.Reader) (Key, error) {
				i.sz = 1
				return 0, errors.New("invalid rune")
			}
			// otherwise we have no byte at all, return TimeoutError if
			// otherwise we have no byte at all, return ErrTimeout 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, ErrTimeout
				}
			}
			return 0, err

M input_test.go => input_test.go +28 -8
@@ 4,7 4,6 @@ import (
	"bytes"
	"encoding/json"
	"errors"
	"io"
	"io/ioutil"
	"log"
	"os"


@@ 54,14 53,11 @@ func TestInput_ReadKey_Multiple(t *testing.T) {

			// 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 !errors.Is(err, ErrTimeout) {
				t.Fatalf("after loop: want ErrTimeout, got %v (key %v)", err, got)
			}
			if !os.IsTimeout(err) {
				t.Fatal("after loop: want TimeoutError to be identified as such with os.IsTimeout")
				t.Fatal("after loop: want ErrTimeout to be identified as such with os.IsTimeout")
			}
		})
	}


@@ 78,7 74,7 @@ func TestInput_ReadKey_BustBuffer(t *testing.T) {
	var count int
	for {
		key, err := input.ReadKey(r)
		if errors.Is(err, io.EOF) {
		if errors.Is(err, ErrTimeout) {
			break
		}
		if err != nil {


@@ 410,3 406,27 @@ func BenchmarkInput_ReadKey_Multiple(b *testing.B) {
		r.Reset(data)
	}
}

func BenchmarkInput_ReadKey_Timeout(b *testing.B) {
	input := NewInput()
	data := "⬼"
	r := strings.NewReader(data)
	b.ResetTimer()

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