[GH-ISSUE #661] Mouse moves translate to mouse scrolling under URxvt #178

Open
opened 2026-03-02 23:45:06 +03:00 by kerem · 10 comments
Owner

Originally created by @demostanis on GitHub (Feb 10, 2026).
Original GitHub issue: https://github.com/anomalyco/opentui/issues/661

Originally assigned to: @simonklee on GitHub.

when using the rxvt-unicode terminal, when i scroll and move my mouse a little bit, the whole screen moves in unpredictable ways:

https://github.com/user-attachments/assets/f9dc0436-9ffa-47f9-ad68-6cfa88036842

if needed, my whole setup is at https://github.com/demostanis/demolinux,
but ive been able to reproduce with the latest rxvt-unicode in Arch repos with an empty .Xresources

Originally created by @demostanis on GitHub (Feb 10, 2026). Original GitHub issue: https://github.com/anomalyco/opentui/issues/661 Originally assigned to: @simonklee on GitHub. when using the rxvt-unicode terminal, when i scroll and move my mouse a little bit, the whole screen moves in unpredictable ways: https://github.com/user-attachments/assets/f9dc0436-9ffa-47f9-ad68-6cfa88036842 if needed, my whole setup is at https://github.com/demostanis/demolinux, but ive been able to reproduce with the latest rxvt-unicode in Arch repos with an empty .Xresources
Author
Owner

@simonklee commented on GitHub (Feb 10, 2026):

I think we're missing to parse the motion/drag detection and because of it misclassifying things as down events causing the behavior you're seeing.

<!-- gh-comment-id:3880737969 --> @simonklee commented on GitHub (Feb 10, 2026): I think we're missing to parse the motion/drag detection and because of it misclassifying things as down events causing the behavior you're seeing.
Author
Owner

@demostanis commented on GitHub (Feb 11, 2026):

@simonklee thank you for trying to figure out the issue, however that didn't fix it
(to test out i git clone'd the opencode repo and ran bun install && bun dev)

<!-- gh-comment-id:3884156799 --> @demostanis commented on GitHub (Feb 11, 2026): @simonklee thank you for trying to figure out the issue, however that didn't fix it (to test out i git clone'd the opencode repo and ran bun install && bun dev)
Author
Owner

@simonklee commented on GitHub (Feb 11, 2026):

@simonklee thank you for trying to figure out the issue, however that didn't fix it (to test out i git clone'd the opencode repo and ran bun install && bun dev)

I'm not sure we cut a release yet.

<!-- gh-comment-id:3884164279 --> @simonklee commented on GitHub (Feb 11, 2026): > [@simonklee](https://github.com/simonklee) thank you for trying to figure out the issue, however that didn't fix it (to test out i git clone'd the opencode repo and ran bun install && bun dev) I'm not sure we cut a release yet.
Author
Owner

@kommander commented on GitHub (Feb 11, 2026):

@demostanis it's only on the beta branch currently. You can test with bunx opencode-ai@beta.

<!-- gh-comment-id:3884176752 --> @kommander commented on GitHub (Feb 11, 2026): @demostanis it's only on the beta branch currently. You can test with `bunx opencode-ai@beta`.
Author
Owner

@demostanis commented on GitHub (Feb 11, 2026):

@kommander did not resolve it either :/

<!-- gh-comment-id:3884586427 --> @demostanis commented on GitHub (Feb 11, 2026): @kommander did not resolve it either :/
Author
Owner

@simonklee commented on GitHub (Feb 11, 2026):

I’ll have another look and try to reproduce it locally with XWayland.

On Wed, Feb 11, 2026, at 14:54, demostanis wrote:

demostanis left a comment (anomalyco/opentui#661) https://github.com/anomalyco/opentui/issues/661#issuecomment-3884586427
@kommander https://github.com/kommander did not resolve it either :/


Reply to this email directly, view it on GitHub https://github.com/anomalyco/opentui/issues/661#issuecomment-3884586427, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAM4O7RS6SQKPTU7YLPHT34LMYCBAVCNFSM6AAAAACUUGZ2N2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTQOBUGU4DMNBSG4.
You are receiving this because you were mentioned.Message ID: @.***>

<!-- gh-comment-id:3885122229 --> @simonklee commented on GitHub (Feb 11, 2026): I’ll have another look and try to reproduce it locally with XWayland. On Wed, Feb 11, 2026, at 14:54, demostanis wrote: > *demostanis* left a comment (anomalyco/opentui#661) <https://github.com/anomalyco/opentui/issues/661#issuecomment-3884586427> > @kommander <https://github.com/kommander> did not resolve it either :/ > > — > Reply to this email directly, view it on GitHub <https://github.com/anomalyco/opentui/issues/661#issuecomment-3884586427>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AAAM4O7RS6SQKPTU7YLPHT34LMYCBAVCNFSM6AAAAACUUGZ2N2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTQOBUGU4DMNBSG4>. > You are receiving this because you were mentioned.Message ID: ***@***.***> >
Author
Owner

@demostanis commented on GitHub (Feb 16, 2026):

@simonklee did you try to reproduce it locally? do you need help?

<!-- gh-comment-id:3905619745 --> @demostanis commented on GitHub (Feb 16, 2026): @simonklee did you try to reproduce it locally? do you need help?
Author
Owner

@simonklee commented on GitHub (Feb 16, 2026):

@simonklee did you try to reproduce it locally? do you need help?

Thanks - I might send you script later today tbh. I created a minimal thing that just captures the mouse events before parsing (but its on my desktop at home).

I tried to reproduce on arch + sway + Xwayland and my hypothesis for what was going wrong didn't turn out to be correct, but i found some other things. These are sitting in a PR atm but should be merged soon #679.

<!-- gh-comment-id:3906629427 --> @simonklee commented on GitHub (Feb 16, 2026): > [@simonklee](https://github.com/simonklee) did you try to reproduce it locally? do you need help? Thanks - I might send you script later today tbh. I created a minimal thing that just captures the mouse events before parsing (but its on my desktop at home). I tried to reproduce on arch + sway + Xwayland and my hypothesis for what was going wrong didn't turn out to be correct, but i found some other things. These are sitting in a PR atm but should be merged soon #679.
Author
Owner

@demostanis commented on GitHub (Feb 16, 2026):

i tried asking gemini 3 flash to debug the issue and he was able to solve the problem:
https://opncd.ai/share/Lg0TWAsu
(i recorded opencode with script, and told gemini to read the raw output with the opencode and opentui sources)

From 014e18ac5192b49e89504f7322b75815a59f83f7 Mon Sep 17 00:00:00 2001
From: demostanis <demostanis@protonmail.com>
Date: Mon, 16 Feb 2026 16:15:16 +0100
Subject: [PATCH] fix(mouse): handle urxvt motion events misidentified as
 scrolls

---
 packages/core/src/lib/parse.mouse.test.ts | 13 +++++++++----
 packages/core/src/lib/parse.mouse.ts      | 16 +++++++++++-----
 2 files changed, 20 insertions(+), 9 deletions(-)

diff --git a/packages/core/src/lib/parse.mouse.test.ts b/packages/core/src/lib/parse.mouse.test.ts
index f8b5e08..544d9e2 100644
--- a/packages/core/src/lib/parse.mouse.test.ts
+++ b/packages/core/src/lib/parse.mouse.test.ts
@@ -158,9 +158,9 @@ describe("MouseParser basic (X10) mode", () => {
       expect(e.modifiers).toEqual({ shift: true, alt: true, ctrl: true })
     })
 
-    test("scroll bit takes priority over motion bit: byte 96 (64|32) → 'scroll'", () => {
+    test("motion bit takes priority over scroll bit: byte 96 (64|32) → 'move'", () => {
       const e = parser.parseMouseEvent(encodeBasic(96, 10, 5))!
-      expect(e.type).toBe("scroll")
+      expect(e.type).toBe("move")
     })
 
     test("release without motion bit is still 'up'", () => {
@@ -286,8 +286,8 @@ describe("MouseParser SGR mode", () => {
     test("scroll release (m) is not classified as scroll", () => {
       // Some terminals send release for scroll too; the parser should not
       // report that as a scroll event.
-      const e = parser.parseMouseEvent(encodeSGR(64, 10, 5, false))!
-      expect(e.type).not.toBe("scroll")
+      const e = parser.parseMouseEvent(encodeSGR(64, 10, 5, false))
+      expect(e).toBeNull()
     })
   })
 
@@ -367,6 +367,11 @@ describe("MouseParser SGR mode", () => {
       expect(e.type).toBe("move") // no buttons tracked → move, not drag
     })
 
+    test("motion bit takes priority over scroll bit: 96 (64|32) → 'move'", () => {
+      const e = parser.parseMouseEvent(encodeSGR(96, 10, 5, true))!
+      expect(e.type).toBe("move")
+    })
+
     test("reset() clears button tracking state", () => {
       parser.parseMouseEvent(encodeSGR(0, 5, 5, true)) // left down
       parser.reset()
diff --git a/packages/core/src/lib/parse.mouse.ts b/packages/core/src/lib/parse.mouse.ts
index 33d5d9e..eacc44e 100644
--- a/packages/core/src/lib/parse.mouse.ts
+++ b/packages/core/src/lib/parse.mouse.ts
@@ -29,7 +29,7 @@ export class MouseParser {
   }
 
   public parseMouseEvent(data: Buffer): RawMouseEvent | null {
-    const str = data.toString()
+    const str = data.toString("latin1")
     // Parse SGR mouse mode: \x1b[<b;x;yM or \x1b[<b;x;ym
     const sgrMatch = str.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/)
     if (sgrMatch) {
@@ -50,7 +50,8 @@ export class MouseParser {
       let type: MouseEventType
       let scrollInfo: ScrollInfo | undefined
 
-      if (isScroll && pressRelease === "M") {
+      // Fix: Prioritize isMotion over isScroll to handle urxvt reporting motion with scroll bits
+      if (isScroll && !isMotion && pressRelease === "M") {
         type = "scroll"
         scrollInfo = {
           direction: scrollDirection!,
@@ -66,7 +67,7 @@ export class MouseParser {
         } else {
           type = "move"
         }
-      } else {
+      } else if (!isScroll) {
         type = pressRelease === "M" ? "down" : "up"
 
         if (type === "down" && button !== 3) {
@@ -74,6 +75,8 @@ export class MouseParser {
         } else if (type === "up") {
           this.mouseButtonsPressed.clear()
         }
+      } else {
+        return null
       }
 
       return {
@@ -108,7 +111,8 @@ export class MouseParser {
       let actualButton: number
       let scrollInfo: ScrollInfo | undefined
 
-      if (isScroll) {
+      // Fix: Prioritize isMotion over isScroll
+      if (isScroll && !isMotion) {
         type = "scroll"
         actualButton = 0
         scrollInfo = {
@@ -118,9 +122,11 @@ export class MouseParser {
       } else if (isMotion) {
         type = "move"
         actualButton = button === 3 ? -1 : button
-      } else {
+      } else if (!isScroll) {
         type = button === 3 ? "up" : "down"
         actualButton = button === 3 ? 0 : button
+      } else {
+        return null
       }
 
       return {
-- 
2.51.0


i however don't know which specific changed resolved the issue concretly...

<!-- gh-comment-id:3909063896 --> @demostanis commented on GitHub (Feb 16, 2026): i tried asking gemini 3 flash to debug the issue and he was able to solve the problem: https://opncd.ai/share/Lg0TWAsu (i recorded opencode with `script`, and told gemini to read the raw output with the opencode and opentui sources) ``` From 014e18ac5192b49e89504f7322b75815a59f83f7 Mon Sep 17 00:00:00 2001 From: demostanis <demostanis@protonmail.com> Date: Mon, 16 Feb 2026 16:15:16 +0100 Subject: [PATCH] fix(mouse): handle urxvt motion events misidentified as scrolls --- packages/core/src/lib/parse.mouse.test.ts | 13 +++++++++---- packages/core/src/lib/parse.mouse.ts | 16 +++++++++++----- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/core/src/lib/parse.mouse.test.ts b/packages/core/src/lib/parse.mouse.test.ts index f8b5e08..544d9e2 100644 --- a/packages/core/src/lib/parse.mouse.test.ts +++ b/packages/core/src/lib/parse.mouse.test.ts @@ -158,9 +158,9 @@ describe("MouseParser basic (X10) mode", () => { expect(e.modifiers).toEqual({ shift: true, alt: true, ctrl: true }) }) - test("scroll bit takes priority over motion bit: byte 96 (64|32) → 'scroll'", () => { + test("motion bit takes priority over scroll bit: byte 96 (64|32) → 'move'", () => { const e = parser.parseMouseEvent(encodeBasic(96, 10, 5))! - expect(e.type).toBe("scroll") + expect(e.type).toBe("move") }) test("release without motion bit is still 'up'", () => { @@ -286,8 +286,8 @@ describe("MouseParser SGR mode", () => { test("scroll release (m) is not classified as scroll", () => { // Some terminals send release for scroll too; the parser should not // report that as a scroll event. - const e = parser.parseMouseEvent(encodeSGR(64, 10, 5, false))! - expect(e.type).not.toBe("scroll") + const e = parser.parseMouseEvent(encodeSGR(64, 10, 5, false)) + expect(e).toBeNull() }) }) @@ -367,6 +367,11 @@ describe("MouseParser SGR mode", () => { expect(e.type).toBe("move") // no buttons tracked → move, not drag }) + test("motion bit takes priority over scroll bit: 96 (64|32) → 'move'", () => { + const e = parser.parseMouseEvent(encodeSGR(96, 10, 5, true))! + expect(e.type).toBe("move") + }) + test("reset() clears button tracking state", () => { parser.parseMouseEvent(encodeSGR(0, 5, 5, true)) // left down parser.reset() diff --git a/packages/core/src/lib/parse.mouse.ts b/packages/core/src/lib/parse.mouse.ts index 33d5d9e..eacc44e 100644 --- a/packages/core/src/lib/parse.mouse.ts +++ b/packages/core/src/lib/parse.mouse.ts @@ -29,7 +29,7 @@ export class MouseParser { } public parseMouseEvent(data: Buffer): RawMouseEvent | null { - const str = data.toString() + const str = data.toString("latin1") // Parse SGR mouse mode: \x1b[<b;x;yM or \x1b[<b;x;ym const sgrMatch = str.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/) if (sgrMatch) { @@ -50,7 +50,8 @@ export class MouseParser { let type: MouseEventType let scrollInfo: ScrollInfo | undefined - if (isScroll && pressRelease === "M") { + // Fix: Prioritize isMotion over isScroll to handle urxvt reporting motion with scroll bits + if (isScroll && !isMotion && pressRelease === "M") { type = "scroll" scrollInfo = { direction: scrollDirection!, @@ -66,7 +67,7 @@ export class MouseParser { } else { type = "move" } - } else { + } else if (!isScroll) { type = pressRelease === "M" ? "down" : "up" if (type === "down" && button !== 3) { @@ -74,6 +75,8 @@ export class MouseParser { } else if (type === "up") { this.mouseButtonsPressed.clear() } + } else { + return null } return { @@ -108,7 +111,8 @@ export class MouseParser { let actualButton: number let scrollInfo: ScrollInfo | undefined - if (isScroll) { + // Fix: Prioritize isMotion over isScroll + if (isScroll && !isMotion) { type = "scroll" actualButton = 0 scrollInfo = { @@ -118,9 +122,11 @@ export class MouseParser { } else if (isMotion) { type = "move" actualButton = button === 3 ? -1 : button - } else { + } else if (!isScroll) { type = button === 3 ? "up" : "down" actualButton = button === 3 ? 0 : button + } else { + return null } return { -- 2.51.0 ``` i however don't know which specific changed resolved the issue concretly...
Author
Owner

@simonklee commented on GitHub (Feb 21, 2026):

Hey - thanks for sharing the fix. I implementing something that may solve your issue, but i haven't looked if has been released in OpenCode yet. It's part of 0.1.80 OpenTUI version. github.com/anomalyco/opentui@5aba383ffd

When i get the time i'll try to look at Gemini fixes. Also, if you don't mind. I'd love to get the data from your script session if you still have it around.

<!-- gh-comment-id:3938841794 --> @simonklee commented on GitHub (Feb 21, 2026): Hey - thanks for sharing the fix. I implementing something that may solve your issue, but i haven't looked if has been released in OpenCode yet. It's part of 0.1.80 OpenTUI version. https://github.com/anomalyco/opentui/commit/5aba383ffdef936970ce3fd6765baf5c57d62f9b When i get the time i'll try to look at Gemini fixes. Also, if you don't mind. I'd love to get the data from your `script` session if you still have it around.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/opentui#178
No description provided.