Repository: charmbracelet/bubbletea (go)

Issue:
Unknown CSI inputs result in garbage key events
When a terminal CSI sequence is not recognized by bubbletea, it is not completely ignored; instead, bubbletea will match the initial escape byte and the character immediately afterwards as a single key with the alt modifier, then interpret the rest as regular key inputs. For example, ESC [ 1003 h  gets parsed as alt+1, then 0, then 0, then 3, then h.

Since the CSI sequences all have a regular structure, they could be parsed and ignored as a single unit.

(The definition is strict: a CSI sequence starts with `ESC [` and terminates with a byte value in the range 0x40 to 0x7E.)

Pull request description:
fix: properly skip over unrecognized CSI sequences.
Fixes #466.

Also includes a removal of now-unnecessary code.

cc @muesli 

Patch:
diff --git a/key.go b/key.go
index 8e88c68..8a27702 100644
--- a/key.go
+++ b/key.go
@@ -2,7 +2,6 @@ package tea
 
 import (
 	"errors"
-	"fmt"
 	"io"
 	"unicode/utf8"
 
@@ -540,18 +539,12 @@ var sequences = map[string]Key{
 	"\x1b\x1b[32~": {Type: KeyF18, Alt: true}, // urxvt
 	"\x1b\x1b[33~": {Type: KeyF19, Alt: true}, // urxvt
 	"\x1b\x1b[34~": {Type: KeyF20, Alt: true}, // urxvt
-}
-
-// Hex code mappings.
-var hexes = map[string]Key{
-	"1b0d": {Type: KeyEnter, Alt: true},
-	"1b7f": {Type: KeyBackspace, Alt: true},
 
-	// Powershell
-	"1b4f41": {Type: KeyUp, Alt: false},
-	"1b4f42": {Type: KeyDown, Alt: false},
-	"1b4f43": {Type: KeyRight, Alt: false},
-	"1b4f44": {Type: KeyLeft, Alt: false},
+	// Powershell sequences.
+	"\x1bOA": {Type: KeyUp, Alt: false},
+	"\x1bOB": {Type: KeyDown, Alt: false},
+	"\x1bOC": {Type: KeyRight, Alt: false},
+	"\x1bOD": {Type: KeyLeft, Alt: false},
 }
 
 // readInputs reads keypress and mouse inputs from a TTY and returns messages
@@ -617,10 +610,9 @@ func readInputs(input io.Reader) ([]Msg, error) {
 			continue
 		}
 
-		// Some of these need special handling.
-		hex := fmt.Sprintf("%x", runes)
-		if k, ok := hexes[hex]; ok {
-			msgs = append(msgs, KeyMsg(k))
+		// Is this an unrecognized CSI sequence? If so, ignore it.
+		if len(runes) > 2 && runes[0] == 0x1b && (runes[1] == '[' ||
+			(len(runes) > 3 && runes[1] == 0x1b && runes[2] == '[')) {
 			continue
 		}
 


Test patch:
diff --git a/key_test.go b/key_test.go
index 869b32f..c119cda 100644
--- a/key_test.go
+++ b/key_test.go
@@ -2,6 +2,7 @@ package tea
 
 import (
 	"bytes"
+	"fmt"
 	"testing"
 )
 
@@ -49,11 +50,12 @@ func TestKeyTypeString(t *testing.T) {
 
 func TestReadInput(t *testing.T) {
 	type test struct {
-		in  []byte
-		out []Msg
+		keyname string
+		in      []byte
+		out     []Msg
 	}
-	for out, td := range map[string]test{
-		"a": {
+	for i, td := range []test{
+		{"a",
 			[]byte{'a'},
 			[]Msg{
 				KeyMsg{
@@ -62,7 +64,7 @@ func TestReadInput(t *testing.T) {
 				},
 			},
 		},
-		" ": {
+		{" ",
 			[]byte{' '},
 			[]Msg{
 				KeyMsg{
@@ -71,7 +73,7 @@ func TestReadInput(t *testing.T) {
 				},
 			},
 		},
-		"ctrl+a": {
+		{"ctrl+a",
 			[]byte{byte(keySOH)},
 			[]Msg{
 				KeyMsg{
@@ -79,7 +81,7 @@ func TestReadInput(t *testing.T) {
 				},
 			},
 		},
-		"alt+a": {
+		{"alt+a",
 			[]byte{byte(0x1b), 'a'},
 			[]Msg{
 				KeyMsg{
@@ -89,7 +91,7 @@ func TestReadInput(t *testing.T) {
 				},
 			},
 		},
-		"abcd": {
+		{"abcd",
 			[]byte{'a', 'b', 'c', 'd'},
 			[]Msg{
 				KeyMsg{
@@ -110,7 +112,7 @@ func TestReadInput(t *testing.T) {
 				},
 			},
 		},
-		"up": {
+		{"up",
 			[]byte("\x1b[A"),
 			[]Msg{
 				KeyMsg{
@@ -118,7 +120,7 @@ func TestReadInput(t *testing.T) {
 				},
 			},
 		},
-		"wheel up": {
+		{"wheel up",
 			[]byte{'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)},
 			[]Msg{
 				MouseMsg{
@@ -126,7 +128,7 @@ func TestReadInput(t *testing.T) {
 				},
 			},
 		},
-		"shift+tab": {
+		{"shift+tab",
 			[]byte{'\x1b', '[', 'Z'},
 			[]Msg{
 				KeyMsg{
@@ -134,7 +136,7 @@ func TestReadInput(t *testing.T) {
 				},
 			},
 		},
-		"alt+enter": {
+		{"alt+enter",
 			[]byte{'\x1b', '\r'},
 			[]Msg{
 				KeyMsg{
@@ -143,7 +145,7 @@ func TestReadInput(t *testing.T) {
 				},
 			},
 		},
-		"alt+ctrl+a": {
+		{"alt+ctrl+a",
 			[]byte{'\x1b', byte(keySOH)},
 			[]Msg{
 				KeyMsg{
@@ -152,8 +154,37 @@ func TestReadInput(t *testing.T) {
 				},
 			},
 		},
+		{"unrecognized CSI",
+			[]byte{'\x1b', '[', '-', '-', '-', '-', 'X'},
+			[]Msg{},
+		},
+		// Powershell sequences.
+		{"up",
+			[]byte{'\x1b', 'O', 'A'},
+			[]Msg{KeyMsg{Type: KeyUp}},
+		},
+		{"down",
+			[]byte{'\x1b', 'O', 'B'},
+			[]Msg{KeyMsg{Type: KeyDown}},
+		},
+		{"right",
+			[]byte{'\x1b', 'O', 'C'},
+			[]Msg{KeyMsg{Type: KeyRight}},
+		},
+		{"left",
+			[]byte{'\x1b', 'O', 'D'},
+			[]Msg{KeyMsg{Type: KeyLeft}},
+		},
+		{"alt+enter",
+			[]byte{'\x1b', '\x0d'},
+			[]Msg{KeyMsg{Type: KeyEnter, Alt: true}},
+		},
+		{"alt+backspace",
+			[]byte{'\x1b', '\x7f'},
+			[]Msg{KeyMsg{Type: KeyBackspace, Alt: true}},
+		},
 	} {
-		t.Run(out, func(t *testing.T) {
+		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)
@@ -163,8 +194,8 @@ func TestReadInput(t *testing.T) {
 			}
 
 			if len(msgs) == 1 {
-				if m, ok := msgs[0].(KeyMsg); ok && m.String() != out {
-					t.Fatalf(`expected a keymsg %q, got %q`, out, m)
+				if m, ok := msgs[0].(KeyMsg); ok && m.String() != td.keyname {
+					t.Fatalf(`expected a keymsg %q, got %q`, td.keyname, m)
 				}
 			}
 
@@ -174,9 +205,9 @@ func TestReadInput(t *testing.T) {
 					t.Fatalf(`expected a keymsg %q, got %q`, td.out[i].(KeyMsg), m)
 				}
 				if m, ok := v.(MouseMsg); ok &&
-					(mouseEventTypes[m.Type] != out || m.Type != td.out[i].(MouseMsg).Type) {
+					(mouseEventTypes[m.Type] != td.keyname || m.Type != td.out[i].(MouseMsg).Type) {
 					t.Fatalf(`expected a mousemsg %q, got %q`,
-						out,
+						td.keyname,
 						mouseEventTypes[td.out[i].(MouseMsg).Type])
 				}
 			}