Skip to content

Commit

Permalink
fix(key): invert the control loop
Browse files Browse the repository at this point in the history
Instead of reading messages in an array and then sending them into a
channel, this version of key.go writes to the channel directly.
  • Loading branch information
knz committed Oct 21, 2022
1 parent ce2a006 commit 45575f6
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 42 deletions.
40 changes: 21 additions & 19 deletions key.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package tea

import (
"context"
"fmt"
"io"
"regexp"
"unicode/utf8"

"github.com/mattn/go-localereader"
)

// KeyMsg contains information about a keypress. KeyMsgs are always sent to
Expand Down Expand Up @@ -539,27 +538,30 @@ func (u unknownCSISequenceMsg) String() string {

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.
func readInputs(input io.Reader) ([]Msg, error) {
func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error {
var buf [256]byte

input = localereader.NewReader(input)

// Read and block
numBytes, err := input.Read(buf[:])
if err != nil {
return nil, err
}
b := buf[:numBytes]

var msgs []Msg
for i, w := 0, 0; i < len(b); i += w {
var msg Msg
w, msg = detectOneMsg(b[i:])
msgs = append(msgs, msg)
for {
// Read and block.
numBytes, err := input.Read(buf[:])
if err != nil {
return err
}
b := buf[:numBytes]

var i, w int
for i, w = 0, 0; i < len(b); i += w {
var msg Msg
w, msg = detectOneMsg(b[i:])
select {
case msgs <- msg:
case <-ctx.Done():
return ctx.Err()
}
}
}
return msgs, nil
}

var unknownCSIRe = regexp.MustCompile(`^\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]`)
Expand Down
55 changes: 49 additions & 6 deletions key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ package tea

import (
"bytes"
"context"
"errors"
"flag"
"fmt"
"io"
"math/rand"
"reflect"
"runtime"
"sort"
"strings"
"sync"
"testing"
"time"
)
Expand Down Expand Up @@ -442,12 +446,7 @@ func TestReadInput(t *testing.T) {

for i, td := range testData {
t.Run(fmt.Sprintf("%d: %s", i, td.keyname), func(t *testing.T) {
msgs, err := readInputs(bytes.NewReader(td.in))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

// Compute the title for the event sequence.
msgs := testReadInputs(t, bytes.NewReader(td.in))
var buf strings.Builder
for i, msg := range msgs {
if i > 0 {
Expand All @@ -459,6 +458,7 @@ func TestReadInput(t *testing.T) {
fmt.Fprintf(&buf, "%#v:%T", msg, msg)
}
}

title := buf.String()
if title != td.keyname {
t.Errorf("expected message titles:\n %s\ngot:\n %s", td.keyname, title)
Expand All @@ -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
// of interleaved control sequences and control characters.
type randTest struct {
Expand Down
22 changes: 5 additions & 17 deletions tty.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io"
"time"

localereader "github.com/mattn/go-localereader"
"github.com/muesli/cancelreader"
)

Expand Down Expand Up @@ -68,23 +69,10 @@ func (p *Program) initCancelReader() error {
func (p *Program) readLoop() {
defer close(p.readLoopDone)

for {
if p.ctx.Err() != nil {
return
}

msgs, err := readInputs(p.cancelReader)
if err != nil {
if !errors.Is(err, io.EOF) && !errors.Is(err, cancelreader.ErrCanceled) {
p.errs <- err
}

return
}

for _, msg := range msgs {
p.msgs <- msg
}
input := localereader.NewReader(p.cancelReader)
err := readInputs(p.ctx, p.msgs, input)
if !errors.Is(err, io.EOF) && !errors.Is(err, cancelreader.ErrCanceled) {
p.errs <- err
}
}

Expand Down

0 comments on commit 45575f6

Please sign in to comment.