Repository: charmbracelet/bubbletea (go)

Issue:
bug: the input reader gobbles up characters after escape sequences
When a composite input is inserted in the terminal, the terminal can deliver many bytes at once to the process, including escape sequences interleaved with regular keypresses. (This also happens after a process is resumed from sleep and there were input queued while the process was sleeping.)

Currently the input reader only recognizes input boundaries when the read call returns (i.e. no more input to be read).
It is also unable to recognize when an escape sequence is followed by regular bytes (it fails to detect the sequence in the special control character map).

Instead, the input reader should only match a *prefix* to the current input bytes. Then after the prefix is recognized, it should only discard the prefix, then continue to match on the remainder bytes.

This is partly addressed by #397 but it should really be fixed independently of the bracketed paste feature, as it would benefit any composite input.

Pull request description:
fix(key): support very long buffered input
Fixes #502.

First two commits from #568 and #569. 

Split away from #511 as discussed in https://github.com/charmbracelet/bubbletea/pull/511#issuecomment-1287061948

cc @muesli 

Patch:
diff --git a/key.go b/key.go
index b712228..fe438a6 100644
--- a/key.go
+++ b/key.go
@@ -543,6 +543,8 @@ var spaceRunes = []rune{' '}
 func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error {
 	var buf [256]byte
 
+	var leftOverFromPrevIteration []byte
+loop:
 	for {
 		// Read and block.
 		numBytes, err := input.Read(buf[:])
@@ -550,11 +552,31 @@ func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error {
 			return fmt.Errorf("error reading input: %w", err)
 		}
 		b := buf[:numBytes]
+		if leftOverFromPrevIteration != nil {
+			b = append(leftOverFromPrevIteration, b...)
+		}
+
+		// If we had a short read (numBytes < len(buf)), we're sure that
+		// the end of this read is an event boundary, so there is no doubt
+		// if we are encountering the end of the buffer while parsing a message.
+		// However, if we've succeeded in filling up the buffer, there may
+		// be more data in the OS buffer ready to be read in, to complete
+		// the last message in the input. In that case, we will retry with
+		// the left over data in the next iteration.
+		canHaveMoreData := numBytes == len(buf)
 
 		var i, w int
 		for i, w = 0, 0; i < len(b); i += w {
 			var msg Msg
-			w, msg = detectOneMsg(b[i:])
+			w, msg = detectOneMsg(b[i:], canHaveMoreData)
+			if w == 0 {
+				// Expecting more bytes beyond the current buffer. Try waiting
+				// for more input.
+				leftOverFromPrevIteration = make([]byte, 0, len(b[i:])+len(buf))
+				leftOverFromPrevIteration = append(leftOverFromPrevIteration, b[i:]...)
+				continue loop
+			}
+
 			select {
 			case msgs <- msg:
 			case <-ctx.Done():
@@ -565,12 +587,13 @@ func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error {
 				return err
 			}
 		}
+		leftOverFromPrevIteration = nil
 	}
 }
 
 var unknownCSIRe = regexp.MustCompile(`^\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]`)
 
-func detectOneMsg(b []byte) (w int, msg Msg) {
+func detectOneMsg(b []byte, canHaveMoreData bool) (w int, msg Msg) {
 	// Detect mouse events.
 	const mouseEventLen = 6
 	if len(b) >= mouseEventLen && b[0] == '\x1b' && b[1] == '[' && b[2] == 'M' {
@@ -618,6 +641,15 @@ func detectOneMsg(b []byte) (w int, msg Msg) {
 			break
 		}
 	}
+	if i >= len(b) && canHaveMoreData {
+		// We have encountered the end of the input buffer. Alas, we can't
+		// be sure whether the data in the remainder of the buffer is
+		// complete (maybe there was a short read). Instead of sending anything
+		// dumb to the message channel, do a short read. The outer loop will
+		// handle this case by extending the buffer as necessary.
+		return 0, nil
+	}
+
 	// If we found at least one rune, we report the bunch of them as
 	// a single KeyRunes or KeySpace event.
 	if len(runes) > 0 {


Test patch:
diff --git a/key_test.go b/key_test.go
index d466b05..ae8643b 100644
--- a/key_test.go
+++ b/key_test.go
@@ -200,7 +200,7 @@ func TestDetectOneMsg(t *testing.T) {
 
 	for _, tc := range td {
 		t.Run(fmt.Sprintf("%q", string(tc.seq)), func(t *testing.T) {
-			width, msg := detectOneMsg(tc.seq)
+			width, msg := detectOneMsg(tc.seq, false /* canHaveMoreData */)
 			if width != len(tc.seq) {
 				t.Errorf("parser did not consume the entire input: got %d, expected %d", width, len(tc.seq))
 			}
@@ -211,6 +211,25 @@ func TestDetectOneMsg(t *testing.T) {
 	}
 }
 
+func TestReadLongInput(t *testing.T) {
+	input := strings.Repeat("a", 1000)
+	msgs := testReadInputs(t, bytes.NewReader([]byte(input)))
+	if len(msgs) != 1 {
+		t.Errorf("expected 1 messages, got %d", len(msgs))
+	}
+	km := msgs[0]
+	k := Key(km.(KeyMsg))
+	if k.Type != KeyRunes {
+		t.Errorf("expected key runes, got %d", k.Type)
+	}
+	if len(k.Runes) != 1000 || !reflect.DeepEqual(k.Runes, []rune(input)) {
+		t.Errorf("unexpected runes: %+v", k)
+	}
+	if k.Alt {
+		t.Errorf("unexpected alt")
+	}
+}
+
 func TestReadInput(t *testing.T) {
 	type test struct {
 		keyname string