fix(key): invert the control loop

Instead of reading messages in an array and then sending them into a
channel, this version of key.go writes to the channel directly.
This commit is contained in:
Raphael 'kena' Poss 2022-10-21 18:18:56 +02:00 committed by Christian Muehlhaeuser
parent ed4f2ec1ca
commit b1e7f42ab0
3 changed files with 76 additions and 43 deletions

26
key.go
View File

@ -1,12 +1,11 @@
package tea package tea
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"regexp" "regexp"
"unicode/utf8" "unicode/utf8"
"github.com/mattn/go-localereader"
) )
// KeyMsg contains information about a keypress. KeyMsgs are always sent to // KeyMsg contains information about a keypress. KeyMsgs are always sent to
@ -539,27 +538,30 @@ func (u unknownCSISequenceMsg) String() string {
var spaceRunes = []rune{' '} var spaceRunes = []rune{' '}
// readInputs reads keypress and mouse inputs from a TTY and returns messages // readInputs reads keypress and mouse inputs from a TTY and produces messages
// containing information about the key or mouse events accordingly. // containing information about the key or mouse events accordingly.
func readInputs(input io.Reader) ([]Msg, error) { func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error {
var buf [256]byte var buf [256]byte
input = localereader.NewReader(input) for {
// Read and block.
// Read and block
numBytes, err := input.Read(buf[:]) numBytes, err := input.Read(buf[:])
if err != nil { if err != nil {
return nil, err return err
} }
b := buf[:numBytes] b := buf[:numBytes]
var msgs []Msg var i, w int
for i, w := 0, 0; i < len(b); i += w { for i, w = 0, 0; i < len(b); i += w {
var msg Msg var msg Msg
w, msg = detectOneMsg(b[i:]) w, msg = detectOneMsg(b[i:])
msgs = append(msgs, msg) select {
case msgs <- msg:
case <-ctx.Done():
return ctx.Err()
}
}
} }
return msgs, nil
} }
var unknownCSIRe = regexp.MustCompile(`^\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]`) var unknownCSIRe = regexp.MustCompile(`^\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]`)

View File

@ -2,13 +2,17 @@ package tea
import ( import (
"bytes" "bytes"
"context"
"errors"
"flag" "flag"
"fmt" "fmt"
"io"
"math/rand" "math/rand"
"reflect" "reflect"
"runtime" "runtime"
"sort" "sort"
"strings" "strings"
"sync"
"testing" "testing"
"time" "time"
) )
@ -442,12 +446,7 @@ func TestReadInput(t *testing.T) {
for i, td := range testData { for i, td := range testData {
t.Run(fmt.Sprintf("%d: %s", i, td.keyname), func(t *testing.T) { t.Run(fmt.Sprintf("%d: %s", i, td.keyname), func(t *testing.T) {
msgs, err := readInputs(bytes.NewReader(td.in)) msgs := testReadInputs(t, bytes.NewReader(td.in))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Compute the title for the event sequence.
var buf strings.Builder var buf strings.Builder
for i, msg := range msgs { for i, msg := range msgs {
if i > 0 { if i > 0 {
@ -459,6 +458,7 @@ func TestReadInput(t *testing.T) {
fmt.Fprintf(&buf, "%#v:%T", msg, msg) fmt.Fprintf(&buf, "%#v:%T", msg, msg)
} }
} }
title := buf.String() title := buf.String()
if title != td.keyname { if title != td.keyname {
t.Errorf("expected message titles:\n %s\ngot:\n %s", td.keyname, title) t.Errorf("expected message titles:\n %s\ngot:\n %s", td.keyname, title)
@ -475,6 +475,49 @@ func TestReadInput(t *testing.T) {
} }
} }
func testReadInputs(t *testing.T, input io.Reader) []Msg {
// We'll check that the input reader finishes at the end
// without error.
var wg sync.WaitGroup
var inputErr error
ctx, cancel := context.WithCancel(context.Background())
defer func() {
cancel()
wg.Wait()
if inputErr != nil && !errors.Is(inputErr, io.EOF) {
t.Fatalf("unexpected input error: %v", inputErr)
}
}()
// The messages we're consuming.
msgsC := make(chan Msg)
// Start the reader in the background.
wg.Add(1)
go func() {
defer wg.Done()
inputErr = readInputs(ctx, msgsC, input)
msgsC <- nil
}()
var msgs []Msg
loop:
for {
select {
case msg := <-msgsC:
if msg == nil {
// end of input marker for the test.
break loop
}
msgs = append(msgs, msg)
case <-time.After(2 * time.Second):
t.Errorf("timeout waiting for input event")
break loop
}
}
return msgs
}
// randTest defines the test input and expected output for a sequence // randTest defines the test input and expected output for a sequence
// of interleaved control sequences and control characters. // of interleaved control sequences and control characters.
type randTest struct { type randTest struct {

18
tty.go
View File

@ -7,6 +7,7 @@ import (
"time" "time"
isatty "github.com/mattn/go-isatty" isatty "github.com/mattn/go-isatty"
localereader "github.com/mattn/go-localereader"
"github.com/muesli/cancelreader" "github.com/muesli/cancelreader"
"golang.org/x/term" "golang.org/x/term"
) )
@ -71,27 +72,14 @@ func (p *Program) initCancelReader() error {
func (p *Program) readLoop() { func (p *Program) readLoop() {
defer close(p.readLoopDone) defer close(p.readLoopDone)
for { input := localereader.NewReader(p.cancelReader)
if p.ctx.Err() != nil { err := readInputs(p.ctx, p.msgs, input)
return
}
msgs, err := readInputs(p.cancelReader)
if err != nil {
if !errors.Is(err, io.EOF) && !errors.Is(err, cancelreader.ErrCanceled) { if !errors.Is(err, io.EOF) && !errors.Is(err, cancelreader.ErrCanceled) {
select { select {
case <-p.ctx.Done(): case <-p.ctx.Done():
case p.errs <- err: case p.errs <- err:
} }
} }
return
}
for _, msg := range msgs {
p.msgs <- msg
}
}
} }
// waitForReadLoop waits for the cancelReader to finish its read loop. // waitForReadLoop waits for the cancelReader to finish its read loop.