Repository: charmbracelet/bubbletea
Pull request #225 (2022-02-12 09:12:06)

Add IDNT to Bubble Tea in the Wild


Patch:
diff --git a/.github/workflows/lint-soft.yml b/.github/workflows/lint-soft.yml
index b6c06e6..65624ac 100644
--- a/.github/workflows/lint-soft.yml
+++ b/.github/workflows/lint-soft.yml
@@ -13,9 +13,14 @@ jobs:
     name: lint-soft
     runs-on: ubuntu-latest
     steps:
+      - name: Install Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: ^1
+
       - uses: actions/checkout@v2
       - name: golangci-lint
-        uses: golangci/golangci-lint-action@v2
+        uses: golangci/golangci-lint-action@v3
         with:
           # Optional: golangci-lint command line arguments.
           args: --config .golangci-soft.yml --issues-exit-code=0
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 74f4c5c..e6b952a 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -13,9 +13,14 @@ jobs:
     name: lint
     runs-on: ubuntu-latest
     steps:
+      - name: Install Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: ^1
+
       - uses: actions/checkout@v2
       - name: golangci-lint
-        uses: golangci/golangci-lint-action@v2
+        uses: golangci/golangci-lint-action@v3
         with:
           # Optional: golangci-lint command line arguments.
           #args:
diff --git a/README.md b/README.md
index 8c84953..ba09606 100644
--- a/README.md
+++ b/README.md
@@ -298,6 +298,7 @@ For some Bubble Tea programs in production, see:
 * [gocovsh](https://github.com/orlangure/gocovsh): explore Go coverage reports from the CLI
 * [tran](https://github.com/abdfnx/tran): securely transfer stuff between computers
 * [wishlist](https://github.com/charmbracelet/wishlist): an SSH directory
+* [IDNT](https://github.com/r-darwish/idnt): batch software uninstaller
 
 ## Feedback
 
diff --git a/examples/go.mod b/examples/go.mod
index 2abba43..b501b8b 100644
--- a/examples/go.mod
+++ b/examples/go.mod
@@ -3,15 +3,15 @@ module examples
 go 1.13
 
 require (
-	github.com/charmbracelet/bubbles v0.10.2
+	github.com/charmbracelet/bubbles v0.10.3
 	github.com/charmbracelet/bubbletea v0.19.3
 	github.com/charmbracelet/glamour v0.5.0
-	github.com/charmbracelet/lipgloss v0.4.0
+	github.com/charmbracelet/lipgloss v0.5.0
 	github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776
 	github.com/lucasb-eyer/go-colorful v1.2.0
 	github.com/mattn/go-isatty v0.0.14
 	github.com/muesli/reflow v0.3.0
-	github.com/muesli/termenv v0.11.0
+	github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739
 )
 
 replace github.com/charmbracelet/bubbletea => ../
diff --git a/examples/go.sum b/examples/go.sum
index 76ac2bf..80b316b 100644
--- a/examples/go.sum
+++ b/examples/go.sum
@@ -4,16 +4,17 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
 github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
 github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
-github.com/charmbracelet/bubbles v0.10.2 h1:VK1Q7nnBMDFTlrMmvBgE9nidtU5udsIcZvFXvjE2Cfk=
-github.com/charmbracelet/bubbles v0.10.2/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA=
+github.com/charmbracelet/bubbles v0.10.3 h1:fKarbRaObLn/DCsZO4Y3vKCwRUzynQD9L+gGev1E/ho=
+github.com/charmbracelet/bubbles v0.10.3/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA=
 github.com/charmbracelet/glamour v0.5.0 h1:wu15ykPdB7X6chxugG/NNfDUbyyrCLV9XBalj5wdu3g=
 github.com/charmbracelet/glamour v0.5.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc=
 github.com/charmbracelet/harmonica v0.1.0 h1:lFKeSd6OAckQ/CEzPVd2mqj+YMEubQ/3FM2IYY3xNm0=
 github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
-github.com/charmbracelet/lipgloss v0.4.0 h1:768h64EFkGUr8V5yAKV7/Ta0NiVceiPaV+PphaW1K9g=
 github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM=
-github.com/containerd/console v1.0.2 h1:Pi6D+aZXM+oUw1czuKgH5IJ+y0jhYcwBJfx5/Ghn9dE=
-github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
+github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
+github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
+github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
+github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -43,12 +44,11 @@ github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIW
 github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
 github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
 github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
-github.com/muesli/termenv v0.11.0 h1:fwNUbu2mfWlgicwG7qYzs06aOI8Z/zKPAv8J4uKbT+o=
-github.com/muesli/termenv v0.11.0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
+github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
+github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=
+github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -71,11 +71,13 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c=
+golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0=
-golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
diff --git a/go.mod b/go.mod
index 66e74d7..878c510 100644
--- a/go.mod
+++ b/go.mod
@@ -3,11 +3,11 @@ module github.com/charmbracelet/bubbletea
 go 1.13
 
 require (
-	github.com/containerd/console v1.0.2
+	github.com/containerd/console v1.0.3
 	github.com/mattn/go-isatty v0.0.14
 	github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b
 	github.com/muesli/reflow v0.3.0
-	github.com/muesli/termenv v0.11.0
-	golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
-	golang.org/x/term v0.0.0-20210422114643-f5beecf764ed
+	github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739
+	golang.org/x/sys v0.0.0-20220209214540-3681064d5158
+	golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
 )
diff --git a/go.sum b/go.sum
index da6c94f..80de060 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-github.com/containerd/console v1.0.2 h1:Pi6D+aZXM+oUw1czuKgH5IJ+y0jhYcwBJfx5/Ghn9dE=
-github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
+github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
+github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
 github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
@@ -11,16 +11,15 @@ github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTd
 github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
 github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
 github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
-github.com/muesli/termenv v0.11.0 h1:fwNUbu2mfWlgicwG7qYzs06aOI8Z/zKPAv8J4uKbT+o=
-github.com/muesli/termenv v0.11.0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=
+github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0=
-golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c=
+golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
diff --git a/key.go b/key.go
index ae4bf1a..9714444 100644
--- a/key.go
+++ b/key.go
@@ -292,9 +292,9 @@ var hexes = map[string]Key{
 	"1b4f44": {Type: KeyLeft, Alt: false},
 }
 
-// readInput reads keypress and mouse input from a TTY and returns a message
-// containing information about the key or mouse event accordingly.
-func readInput(input io.Reader) (Msg, error) {
+// readInputs reads keypress and mouse inputs from a TTY and returns messages
+// containing information about the key or mouse events accordingly.
+func readInputs(input io.Reader) ([]Msg, error) {
 	var buf [256]byte
 
 	// Read and block
@@ -305,20 +305,28 @@ func readInput(input io.Reader) (Msg, error) {
 
 	// See if it's a mouse event. For now we're parsing X10-type mouse events
 	// only.
-	mouseEvent, err := parseX10MouseEvent(buf[:numBytes])
+	mouseEvent, err := parseX10MouseEvents(buf[:numBytes])
 	if err == nil {
-		return MouseMsg(mouseEvent), nil
+		var m []Msg
+		for _, v := range mouseEvent {
+			m = append(m, MouseMsg(v))
+		}
+		return m, nil
 	}
 
 	// Is it a special sequence, like an arrow key?
 	if k, ok := sequences[string(buf[:numBytes])]; ok {
-		return KeyMsg(Key{Type: k}), nil
+		return []Msg{
+			KeyMsg(Key{Type: k}),
+		}, nil
 	}
 
 	// Some of these need special handling
 	hex := fmt.Sprintf("%x", buf[:numBytes])
 	if k, ok := hexes[hex]; ok {
-		return KeyMsg(k), nil
+		return []Msg{
+			KeyMsg(k),
+		}, nil
 	}
 
 	// Is the alt key pressed? The buffer will be prefixed with an escape
@@ -330,7 +338,9 @@ func readInput(input io.Reader) (Msg, error) {
 		if c == utf8.RuneError {
 			return nil, errors.New("could not decode rune after removing initial escape")
 		}
-		return KeyMsg(Key{Alt: true, Type: KeyRunes, Runes: []rune{c}}), nil
+		return []Msg{
+			KeyMsg(Key{Alt: true, Type: KeyRunes, Runes: []rune{c}}),
+		}, nil
 	}
 
 	var runes []rune
@@ -353,15 +363,21 @@ func readInput(input io.Reader) (Msg, error) {
 	} else if len(runes) > 1 {
 		// We received multiple runes, so we know this isn't a control
 		// character, sequence, and so on.
-		return KeyMsg(Key{Type: KeyRunes, Runes: runes}), nil
+		return []Msg{
+			KeyMsg(Key{Type: KeyRunes, Runes: runes}),
+		}, nil
 	}
 
 	// Is the first rune a control character?
 	r := KeyType(runes[0])
 	if numBytes == 1 && r <= keyUS || r == keyDEL {
-		return KeyMsg(Key{Type: r}), nil
+		return []Msg{
+			KeyMsg(Key{Type: r}),
+		}, nil
 	}
 
 	// Welp, it's just a regular, ol' single rune
-	return KeyMsg(Key{Type: KeyRunes, Runes: runes}), nil
+	return []Msg{
+		KeyMsg(Key{Type: KeyRunes, Runes: runes}),
+	}, nil
 }
diff --git a/mouse.go b/mouse.go
index 6cca0b6..5fb7427 100644
--- a/mouse.go
+++ b/mouse.go
@@ -1,6 +1,9 @@
 package tea
 
-import "errors"
+import (
+	"bytes"
+	"errors"
+)
 
 // MouseMsg contains information about a mouse event and are sent to a programs
 // update function when mouse activity occurs. Note that the mouse must first
@@ -55,78 +58,92 @@ var mouseEventTypes = map[MouseEventType]string{
 	MouseMotion:    "motion",
 }
 
-// Parse an X10-encoded mouse event; the simplest kind. The last release of
-// X10 was December 1986, by the way.
+// Parse X10-encoded mouse events; the simplest kind. The last release of X10
+// was December 1986, by the way.
 //
 // X10 mouse events look like:
 //
 //     ESC [M Cb Cx Cy
 //
 // See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
-func parseX10MouseEvent(buf []byte) (m MouseEvent, err error) {
-	if len(buf) != 6 || string(buf[:3]) != "\x1b[M" {
-		return m, errors.New("not an X10 mouse event")
-	}
-
-	const byteOffset = 32
-
-	e := buf[3] - byteOffset
+func parseX10MouseEvents(buf []byte) ([]MouseEvent, error) {
+	var r []MouseEvent
 
-	const (
-		bitShift  = 0b0000_0100
-		bitAlt    = 0b0000_1000
-		bitCtrl   = 0b0001_0000
-		bitMotion = 0b0010_0000
-		bitWheel  = 0b0100_0000
-
-		bitsMask = 0b0000_0011
+	seq := []byte("\x1b[M")
+	if !bytes.Contains(buf, seq) {
+		return r, errors.New("not an X10 mouse event")
+	}
 
-		bitsLeft    = 0b0000_0000
-		bitsMiddle  = 0b0000_0001
-		bitsRight   = 0b0000_0010
-		bitsRelease = 0b0000_0011
+	for _, v := range bytes.Split(buf, seq) {
+		if len(v) == 0 {
+			continue
+		}
+		if len(v) != 3 {
+			return r, errors.New("not an X10 mouse event")
+		}
 
-		bitsWheelUp   = 0b0000_0000
-		bitsWheelDown = 0b0000_0001
-	)
+		var m MouseEvent
+		const byteOffset = 32
+		e := v[0] - byteOffset
+
+		const (
+			bitShift  = 0b0000_0100
+			bitAlt    = 0b0000_1000
+			bitCtrl   = 0b0001_0000
+			bitMotion = 0b0010_0000
+			bitWheel  = 0b0100_0000
+
+			bitsMask = 0b0000_0011
+
+			bitsLeft    = 0b0000_0000
+			bitsMiddle  = 0b0000_0001
+			bitsRight   = 0b0000_0010
+			bitsRelease = 0b0000_0011
+
+			bitsWheelUp   = 0b0000_0000
+			bitsWheelDown = 0b0000_0001
+		)
+
+		if e&bitWheel != 0 {
+			// Check the low two bits.
+			switch e & bitsMask {
+			case bitsWheelUp:
+				m.Type = MouseWheelUp
+			case bitsWheelDown:
+				m.Type = MouseWheelDown
+			}
+		} else {
+			// Check the low two bits.
+			// We do not separate clicking and dragging.
+			switch e & bitsMask {
+			case bitsLeft:
+				m.Type = MouseLeft
+			case bitsMiddle:
+				m.Type = MouseMiddle
+			case bitsRight:
+				m.Type = MouseRight
+			case bitsRelease:
+				if e&bitMotion != 0 {
+					m.Type = MouseMotion
+				} else {
+					m.Type = MouseRelease
+				}
+			}
+		}
 
-	if e&bitWheel != 0 {
-		// Check the low two bits.
-		switch e & bitsMask {
-		case bitsWheelUp:
-			m.Type = MouseWheelUp
-		case bitsWheelDown:
-			m.Type = MouseWheelDown
+		if e&bitAlt != 0 {
+			m.Alt = true
 		}
-	} else {
-		// Check the low two bits.
-		// We do not separate clicking and dragging.
-		switch e & bitsMask {
-		case bitsLeft:
-			m.Type = MouseLeft
-		case bitsMiddle:
-			m.Type = MouseMiddle
-		case bitsRight:
-			m.Type = MouseRight
-		case bitsRelease:
-			if e&bitMotion != 0 {
-				m.Type = MouseMotion
-			} else {
-				m.Type = MouseRelease
-			}
+		if e&bitCtrl != 0 {
+			m.Ctrl = true
 		}
-	}
 
-	if e&bitAlt != 0 {
-		m.Alt = true
-	}
-	if e&bitCtrl != 0 {
-		m.Ctrl = true
-	}
+		// (1,1) is the upper left. We subtract 1 to normalize it to (0,0).
+		m.X = int(v[1]) - byteOffset - 1
+		m.Y = int(v[2]) - byteOffset - 1
 
-	// (1,1) is the upper left. We subtract 1 to normalize it to (0,0).
-	m.X = int(buf[4]) - byteOffset - 1
-	m.Y = int(buf[5]) - byteOffset - 1
+		r = append(r, m)
+	}
 
-	return m, nil
+	return r, nil
 }
diff --git a/tea.go b/tea.go
index 3baaf5b..f5af6ad 100644
--- a/tea.go
+++ b/tea.go
@@ -421,7 +421,7 @@ func (p *Program) StartReturningModel() (Model, error) {
 					return
 				}
 
-				msg, err := readInput(cancelReader)
+				msgs, err := readInputs(cancelReader)
 				if err != nil {
 					if !errors.Is(err, io.EOF) && !errors.Is(err, errCanceled) {
 						errs <- err
@@ -430,14 +430,16 @@ func (p *Program) StartReturningModel() (Model, error) {
 					return
 				}
 
-				p.msgs <- msg
+				for _, msg := range msgs {
+					p.msgs <- msg
+				}
 			}
 		}()
 	} else {
 		defer close(readLoopDone)
 	}
 
-	if f, ok := p.output.(*os.File); ok {
+	if f, ok := p.output.(*os.File); ok && isatty.IsTerminal(f.Fd()) {
 		// Get the initial terminal size and send it to the program.
 		go func() {
 			w, h, err := term.GetSize(int(f.Fd()))
diff --git a/tutorials/go.sum b/tutorials/go.sum
index da6c94f..80de060 100644
--- a/tutorials/go.sum
+++ b/tutorials/go.sum
@@ -1,5 +1,5 @@
-github.com/containerd/console v1.0.2 h1:Pi6D+aZXM+oUw1czuKgH5IJ+y0jhYcwBJfx5/Ghn9dE=
-github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
+github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
+github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
 github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
@@ -11,16 +11,15 @@ github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTd
 github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
 github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
 github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
-github.com/muesli/termenv v0.11.0 h1:fwNUbu2mfWlgicwG7qYzs06aOI8Z/zKPAv8J4uKbT+o=
-github.com/muesli/termenv v0.11.0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=
+github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0=
-golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c=
+golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=


Test patch:
diff --git a/key_test.go b/key_test.go
index ecb551c..931fa84 100644
--- a/key_test.go
+++ b/key_test.go
@@ -58,14 +58,18 @@ func TestReadInput(t *testing.T) {
 		"shift+tab": {'\x1b', '[', 'Z'},
 	} {
 		t.Run(out, func(t *testing.T) {
-			msg, err := readInput(bytes.NewReader(in))
+			msgs, err := readInputs(bytes.NewReader(in))
 			if err != nil {
 				t.Fatalf("unexpected error: %v", err)
 			}
-			if m, ok := msg.(KeyMsg); ok && m.String() != out {
+			if len(msgs) == 0 {
+				t.Fatalf("unexpected empty message list")
+			}
+
+			if m, ok := msgs[0].(KeyMsg); ok && m.String() != out {
 				t.Fatalf(`expected a keymsg %q, got %q`, out, m)
 			}
-			if m, ok := msg.(MouseMsg); ok && mouseEventTypes[m.Type] != out {
+			if m, ok := msgs[0].(MouseMsg); ok && mouseEventTypes[m.Type] != out {
 				t.Fatalf(`expected a mousemsg %q, got %q`, out, mouseEventTypes[m.Type])
 			}
 		})
diff --git a/mouse_test.go b/mouse_test.go
index 9a4df8a..d9c108f 100644
--- a/mouse_test.go
+++ b/mouse_test.go
@@ -122,209 +122,268 @@ func TestParseX10MouseEvent(t *testing.T) {
 	tt := []struct {
 		name     string
 		buf      []byte
-		expected MouseEvent
+		expected []MouseEvent
 	}{
 		// Position.
 		{
 			name: "zero position",
 			buf:  encode(0b0010_0000, 0, 0),
-			expected: MouseEvent{
-				X:    0,
-				Y:    0,
-				Type: MouseLeft,
+			expected: []MouseEvent{
+				{
+					X:    0,
+					Y:    0,
+					Type: MouseLeft,
+				},
 			},
 		},
 		{
 			name: "max position",
 			buf:  encode(0b0010_0000, 222, 222), // Because 255 (max int8) - 32 - 1.
-			expected: MouseEvent{
-				X:    222,
-				Y:    222,
-				Type: MouseLeft,
+			expected: []MouseEvent{
+				{
+					X:    222,
+					Y:    222,
+					Type: MouseLeft,
+				},
 			},
 		},
 		// Simple.
 		{
 			name: "left",
 			buf:  encode(0b0000_0000, 32, 16),
-			expected: MouseEvent{
-				X:    32,
-				Y:    16,
-				Type: MouseLeft,
+			expected: []MouseEvent{
+				{
+					X:    32,
+					Y:    16,
+					Type: MouseLeft,
+				},
 			},
 		},
 		{
 			name: "left in motion",
 			buf:  encode(0b0010_0000, 32, 16),
-			expected: MouseEvent{
-				X:    32,
-				Y:    16,
-				Type: MouseLeft,
+			expected: []MouseEvent{
+				{
+					X:    32,
+					Y:    16,
+					Type: MouseLeft,
+				},
 			},
 		},
 		{
 			name: "middle",
 			buf:  encode(0b0000_0001, 32, 16),
-			expected: MouseEvent{
-				X:    32,
-				Y:    16,
-				Type: MouseMiddle,
+			expected: []MouseEvent{
+				{
+					X:    32,
+					Y:    16,
+					Type: MouseMiddle,
+				},
 			},
 		},
 		{
 			name: "middle in motion",
 			buf:  encode(0b0010_0001, 32, 16),
-			expected: MouseEvent{
-				X:    32,
-				Y:    16,
-				Type: MouseMiddle,
+			expected: []MouseEvent{
+				{
+					X:    32,
+					Y:    16,
+					Type: MouseMiddle,
+				},
 			},
 		},
 		{
 			name: "right",
 			buf:  encode(0b0000_0010, 32, 16),
-			expected: MouseEvent{
-				X:    32,
-				Y:    16,
-				Type: MouseRight,
+			expected: []MouseEvent{
+				{
+					X:    32,
+					Y:    16,
+					Type: MouseRight,
+				},
 			},
 		},
 		{
 			name: "right in motion",
 			buf:  encode(0b0010_0010, 32, 16),
-			expected: MouseEvent{
-				X:    32,
-				Y:    16,
-				Type: MouseRight,
+			expected: []MouseEvent{
+				{
+					X:    32,
+					Y:    16,
+					Type: MouseRight,
+				},
 			},
 		},
 		{
 			name: "motion",
 			buf:  encode(0b0010_0011, 32, 16),
-			expected: MouseEvent{
-				X:    32,
-				Y:    16,
-				Type: MouseMotion,
+			expected: []MouseEvent{
+				{
+					X:    32,
+					Y:    16,
+					Type: MouseMotion,
+				},
 			},
 		},
 		{
 			name: "wheel up",
 			buf:  encode(0b0100_0000, 32, 16),
-			expected: MouseEvent{
-				X:    32,
-				Y:    16,
-				Type: MouseWheelUp,
+			expected: []MouseEvent{
+				{
+					X:    32,
+					Y:    16,
+					Type: MouseWheelUp,
+				},
 			},
 		},
 		{
 			name: "wheel down",
 			buf:  encode(0b0100_0001, 32, 16),
-			expected: MouseEvent{
-				X:    32,
-				Y:    16,
-				Type: MouseWheelDown,
+			expected: []MouseEvent{
+				{
+					X:    32,
+					Y:    16,
+					Type: MouseWheelDown,
+				},
 			},
 		},
 		{
 			name: "release",
 			buf:  encode(0b0000_0011, 32, 16),
-			expected: MouseEvent{
-				X:    32,
-				Y:    16,
-				Type: MouseRelease,
+			expected: []MouseEvent{
+				{
+					X:    32,
+					Y:    16,
+					Type: MouseRelease,
+				},
 			},
 		},
 		// Combinations.
 		{
 			name: "alt+right",
 			buf:  encode(0b0010_1010, 32, 16),
-			expected: MouseEvent{
-				X:    32,
-				Y:    16,
-				Type: MouseRight,
-				Alt:  true,
+			expected: []MouseEvent{
+				{
+					X:    32,
+					Y:    16,
+					Type: MouseRight,
+					Alt:  true,
+				},
 			},
 		},
 		{
 			name: "ctrl+right",
 			buf:  encode(0b0011_0010, 32, 16),
-			expected: MouseEvent{
-				X:    32,
-				Y:    16,
-				Type: MouseRight,
-				Ctrl: true,
+			expected: []MouseEvent{
+				{
+					X:    32,
+					Y:    16,
+					Type: MouseRight,
+					Ctrl: true,
+				},
 			},
 		},
 		{
 			name: "ctrl+alt+right",
 			buf:  encode(0b0011_1010, 32, 16),
-			expected: MouseEvent{
-				X:    32,
-				Y:    16,
-				Type: MouseRight,
-				Alt:  true,
-				Ctrl: true,
+			expected: []MouseEvent{
+				{
+					X:    32,
+					Y:    16,
+					Type: MouseRight,
+					Alt:  true,
+					Ctrl: true,
+				},
 			},
 		},
 		{
 			name: "alt+wheel down",
 			buf:  encode(0b0100_1001, 32, 16),
-			expected: MouseEvent{
-				X:    32,
-				Y:    16,
-				Type: MouseWheelDown,
-				Alt:  true,
+			expected: []MouseEvent{
+				{
+					X:    32,
+					Y:    16,
+					Type: MouseWheelDown,
+					Alt:  true,
+				},
 			},
 		},
 		{
 			name: "ctrl+wheel down",
 			buf:  encode(0b0101_0001, 32, 16),
-			expected: MouseEvent{
-				X:    32,
-				Y:    16,
-				Type: MouseWheelDown,
-				Ctrl: true,
+			expected: []MouseEvent{
+				{
+					X:    32,
+					Y:    16,
+					Type: MouseWheelDown,
+					Ctrl: true,
+				},
 			},
 		},
 		{
 			name: "ctrl+alt+wheel down",
 			buf:  encode(0b0101_1001, 32, 16),
-			expected: MouseEvent{
-				X:    32,
-				Y:    16,
-				Type: MouseWheelDown,
-				Alt:  true,
-				Ctrl: true,
+			expected: []MouseEvent{
+				{
+					X:    32,
+					Y:    16,
+					Type: MouseWheelDown,
+					Alt:  true,
+					Ctrl: true,
+				},
 			},
 		},
 		// Unknown.
 		{
 			name: "wheel with unknown bit",
 			buf:  encode(0b0100_0010, 32, 16),
-			expected: MouseEvent{
-				X:    32,
-				Y:    16,
-				Type: MouseUnknown,
+			expected: []MouseEvent{
+				{
+					X:    32,
+					Y:    16,
+					Type: MouseUnknown,
+				},
 			},
 		},
 		{
 			name: "unknown with modifier",
 			buf:  encode(0b0100_1010, 32, 16),
-			expected: MouseEvent{
-				X:    32,
-				Y:    16,
-				Type: MouseUnknown,
-				Alt:  true,
+			expected: []MouseEvent{
+				{
+					X:    32,
+					Y:    16,
+					Type: MouseUnknown,
+					Alt:  true,
+				},
 			},
 		},
 		// Overflow position.
 		{
 			name: "overflow position",
 			buf:  encode(0b0010_0000, 250, 223), // Because 255 (max int8) - 32 - 1.
-			expected: MouseEvent{
-				X:    -6,
-				Y:    -33,
-				Type: MouseLeft,
+			expected: []MouseEvent{
+				{
+					X:    -6,
+					Y:    -33,
+					Type: MouseLeft,
+				},
+			},
+		},
+		// Batched events.
+		{
+			name: "batched events",
+			buf:  append(encode(0b0010_0000, 32, 16), encode(0b0000_0011, 64, 32)...),
+			expected: []MouseEvent{
+				{
+					X:    32,
+					Y:    16,
+					Type: MouseLeft,
+				},
+				{
+					X:    64,
+					Y:    32,
+					Type: MouseRelease,
+				},
 			},
 		},
 	}
@@ -333,18 +392,20 @@ func TestParseX10MouseEvent(t *testing.T) {
 		tc := tt[i]
 
 		t.Run(tc.name, func(t *testing.T) {
-			actual, err := parseX10MouseEvent(tc.buf)
+			actual, err := parseX10MouseEvents(tc.buf)
 			if err != nil {
-				t.Fatalf("unexpected error: %v",
+				t.Fatalf("unexpected error for test: %v",
 					err,
 				)
 			}
 
-			if tc.expected != actual {
-				t.Fatalf("expected %#v but got %#v",
-					tc.expected,
-					actual,
-				)
+			for i := range tc.expected {
+				if tc.expected[i] != actual[i] {
+					t.Fatalf("expected %#v but got %#v",
+						tc.expected[i],
+						actual[i],
+					)
+				}
 			}
 		})
 	}
@@ -377,7 +438,7 @@ func TestParseX10MouseEvent_error(t *testing.T) {
 		tc := tt[i]
 
 		t.Run(tc.name, func(t *testing.T) {
-			_, err := parseX10MouseEvent(tc.buf)
+			_, err := parseX10MouseEvents(tc.buf)
 
 			if err == nil {
 				t.Fatalf("expected error but got nil")