mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
fix(app): restore terminal application mouse handling (#45140)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Hona <10430890+Hona@users.noreply.github.com> Co-authored-by: Brendan Allan <git@brendonovich.dev>
This commit is contained in:
committed by
GitHub
parent
ae7d9f029d
commit
4cfca7ab68
@@ -1046,6 +1046,7 @@
|
||||
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
||||
"@tanstack/virtual-core@3.17.8": "patches/@tanstack%2Fvirtual-core@3.17.8.patch",
|
||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||
"ghostty-web@github:anomalyco/ghostty-web#83c0a07": "patches/ghostty-web@0.3.0.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch",
|
||||
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
||||
|
||||
@@ -177,6 +177,7 @@
|
||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||
"@tanstack/virtual-core@3.17.8": "patches/@tanstack%2Fvirtual-core@3.17.8.patch",
|
||||
"@ff-labs/fff-bun@0.10.5": "patches/@ff-labs%2Ffff-bun@0.10.5.patch",
|
||||
"ghostty-web@github:anomalyco/ghostty-web#83c0a07": "patches/ghostty-web@0.3.0.patch",
|
||||
"vite@8.2.2": "patches/vite@8.2.2.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
||||
import { Ghostty, Terminal } from "ghostty-web"
|
||||
|
||||
describe("terminal application mouse reporting", () => {
|
||||
let container: HTMLDivElement
|
||||
let terminal: Terminal
|
||||
let canvas: HTMLCanvasElement
|
||||
let data: string[]
|
||||
|
||||
beforeEach(async () => {
|
||||
container = document.createElement("div")
|
||||
document.body.appendChild(container)
|
||||
terminal = new Terminal({ cols: 80, rows: 24, ghostty: await Ghostty.load() })
|
||||
terminal.open(container)
|
||||
terminal.write("\x1b[?1049h\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1015h\x1b[?1006h")
|
||||
canvas = container.querySelector("canvas")!
|
||||
data = []
|
||||
terminal.onData((value) => data.push(value))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
terminal.dispose()
|
||||
container.remove()
|
||||
})
|
||||
|
||||
const wheel = (deltaX: number, deltaY: number) => {
|
||||
const event = new WheelEvent("wheel", { deltaX, deltaY, bubbles: true })
|
||||
Object.defineProperties(event, { clientX: { value: 40 }, clientY: { value: 30 } })
|
||||
canvas.dispatchEvent(event)
|
||||
}
|
||||
|
||||
test("reports wheel input with its terminal coordinates instead of arrow keys", () => {
|
||||
wheel(0, -100)
|
||||
|
||||
expect(data).toEqual(["\x1b[<64;6;3M"])
|
||||
})
|
||||
|
||||
test("does not discard small trackpad deltas", () => {
|
||||
wheel(0, -1)
|
||||
|
||||
expect(data).toEqual(["\x1b[<64;6;3M"])
|
||||
})
|
||||
|
||||
test("reports horizontal scrolling", () => {
|
||||
wheel(100, 0)
|
||||
|
||||
expect(data).toEqual(["\x1b[<67;6;3M"])
|
||||
})
|
||||
|
||||
test("distinguishes pointer movement from a left-button drag", () => {
|
||||
canvas.dispatchEvent(new MouseEvent("mousemove", { clientX: 40, clientY: 30, bubbles: true }))
|
||||
|
||||
expect(data).toEqual(["\x1b[<35;6;3M"])
|
||||
})
|
||||
|
||||
test("encodes Alt-modified clicks", () => {
|
||||
canvas.dispatchEvent(new MouseEvent("mousedown", { altKey: true, clientX: 40, clientY: 30, bubbles: true }))
|
||||
|
||||
expect(data).toEqual(["\x1b[<8;6;3M"])
|
||||
})
|
||||
|
||||
test("leaves application clicks available for process selection", () => {
|
||||
canvas.dispatchEvent(new MouseEvent("mousedown", { clientX: 40, clientY: 30, bubbles: true }))
|
||||
canvas.dispatchEvent(new MouseEvent("mouseup", { clientX: 40, clientY: 30, bubbles: true }))
|
||||
|
||||
expect(data).toEqual(["\x1b[<0;6;3M", "\x1b[<0;6;3m"])
|
||||
})
|
||||
|
||||
test("preserves Shift as the terminal selection override", () => {
|
||||
canvas.dispatchEvent(new MouseEvent("mousedown", { shiftKey: true, clientX: 40, clientY: 30, bubbles: true }))
|
||||
|
||||
expect(data).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -119,6 +119,28 @@ test("multiple consumers share one source and receive live native and RPC events
|
||||
await events.connections[0].closed
|
||||
})
|
||||
|
||||
test("a slow consumer does not stall other subscribers", async () => {
|
||||
const events = source()
|
||||
const shared = SharedEvents.make(events.connect)
|
||||
const slow = shared.subscribe()[Symbol.asyncIterator]()
|
||||
const fast = shared.subscribe()[Symbol.asyncIterator]()
|
||||
|
||||
const connected = [slow.next(), fast.next()]
|
||||
events.connections[0].push({ type: "server.connected" })
|
||||
await Promise.all(connected)
|
||||
|
||||
events.connections[0].push({ type: "permission.asked", value: 1 })
|
||||
expect(await fast.next()).toEqual({ done: false, value: { type: "permission.asked", value: 1 } })
|
||||
events.connections[0].push({ type: "session.updated", value: 2 })
|
||||
expect(await fast.next()).toEqual({ done: false, value: { type: "session.updated", value: 2 } })
|
||||
|
||||
expect(await slow.next()).toEqual({ done: false, value: { type: "permission.asked", value: 1 } })
|
||||
expect(await slow.next()).toEqual({ done: false, value: { type: "session.updated", value: 2 } })
|
||||
await slow.return!()
|
||||
await fast.return!()
|
||||
await events.connections[0].closed
|
||||
})
|
||||
|
||||
test("an idle consumer does not stall events for an active consumer", async () => {
|
||||
const events = source()
|
||||
const shared = SharedEvents.make(events.connect)
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
--- a/dist/ghostty-web.js
|
||||
+++ b/dist/ghostty-web.js
|
||||
@@ -1209,7 +1209,7 @@
|
||||
*/
|
||||
getMouseModifiers(A) {
|
||||
let g = 0;
|
||||
- return A.shiftKey && (g |= 4), A.metaKey && (g |= 8), A.ctrlKey && (g |= 16), g;
|
||||
+ return A.shiftKey && (g |= 4), A.altKey && (g |= 8), A.ctrlKey && (g |= 16), g;
|
||||
}
|
||||
/**
|
||||
* Encode mouse event as SGR sequence
|
||||
@@ -1246,7 +1246,7 @@
|
||||
*/
|
||||
handleMouseDown(A) {
|
||||
var C;
|
||||
- if (this.isDisposed || !((C = this.mouseConfig) != null && C.hasMouseTracking()))
|
||||
+ if (this.isDisposed || !((C = this.mouseConfig) != null && C.hasMouseTracking()) || A.shiftKey)
|
||||
return;
|
||||
const g = this.pixelToCell(A);
|
||||
if (!g)
|
||||
@@ -1259,7 +1259,7 @@
|
||||
*/
|
||||
handleMouseUp(A) {
|
||||
var C;
|
||||
- if (this.isDisposed || !((C = this.mouseConfig) != null && C.hasMouseTracking()))
|
||||
+ if (this.isDisposed || !((C = this.mouseConfig) != null && C.hasMouseTracking()) || A.shiftKey)
|
||||
return;
|
||||
const g = this.pixelToCell(A);
|
||||
if (!g)
|
||||
@@ -1272,7 +1272,7 @@
|
||||
*/
|
||||
handleMouseMove(A) {
|
||||
var E, D, i;
|
||||
- if (this.isDisposed || !((E = this.mouseConfig) != null && E.hasMouseTracking()))
|
||||
+ if (this.isDisposed || !((E = this.mouseConfig) != null && E.hasMouseTracking()) || A.shiftKey)
|
||||
return;
|
||||
const g = ((D = this.getModeCallback) == null ? void 0 : D.call(this, 1002)) ?? !1, I = ((i = this.getModeCallback) == null ? void 0 : i.call(this, 1003)) ?? !1;
|
||||
if (!g && !I || g && !I && this.mouseButtonsPressed === 0)
|
||||
@@ -1280,21 +1280,21 @@
|
||||
const C = this.pixelToCell(A);
|
||||
if (!C)
|
||||
return;
|
||||
- let Q = 32;
|
||||
- this.mouseButtonsPressed & 1 ? Q += 0 : this.mouseButtonsPressed & 2 ? Q += 1 : this.mouseButtonsPressed & 4 && (Q += 2), this.sendMouseEvent(Q, C.col, C.row, !1, A);
|
||||
+ let Q = 35;
|
||||
+ this.mouseButtonsPressed & 1 ? Q = 32 : this.mouseButtonsPressed & 2 ? Q = 33 : this.mouseButtonsPressed & 4 && (Q = 34), this.sendMouseEvent(Q, C.col, C.row, !1, A);
|
||||
}
|
||||
/**
|
||||
* Handle wheel event (scroll)
|
||||
*/
|
||||
handleWheel(A) {
|
||||
- var C;
|
||||
- if (this.isDisposed || !((C = this.mouseConfig) != null && C.hasMouseTracking()))
|
||||
+ var Q;
|
||||
+ if (this.isDisposed || !((Q = this.mouseConfig) != null && Q.hasMouseTracking()) || A.shiftKey)
|
||||
return;
|
||||
const g = this.pixelToCell(A);
|
||||
- if (!g)
|
||||
+ if (!g || A.deltaX === 0 && A.deltaY === 0)
|
||||
return;
|
||||
- const I = A.deltaY < 0 ? 64 : 65;
|
||||
- this.sendMouseEvent(I, g.col, g.row, !1, A), A.preventDefault();
|
||||
+ const C = Math.abs(A.deltaX) > Math.abs(A.deltaY) ? A.deltaX < 0 ? 66 : 67 : A.deltaY < 0 ? 64 : 65;
|
||||
+ this.sendMouseEvent(C, g.col, g.row, !1, A), A.preventDefault();
|
||||
}
|
||||
/**
|
||||
* Emit paste data with bracketed paste support
|
||||
@@ -2353,7 +2353,7 @@
|
||||
attachEventListeners() {
|
||||
const A = this.renderer.getCanvas();
|
||||
A.addEventListener("mousedown", (g) => {
|
||||
- if (g.button === 0) {
|
||||
+ if (!(this.wasmTerm.hasMouseTracking() && !g.shiftKey) && g.button === 0) {
|
||||
A.parentElement && A.parentElement.focus();
|
||||
const I = this.pixelToCell(g.offsetX, g.offsetY);
|
||||
this.hasSelection() && this.clearSelection();
|
||||
@@ -2408,35 +2408,37 @@
|
||||
}
|
||||
}
|
||||
}, document.addEventListener("mouseup", this.boundMouseUpHandler), A.addEventListener("click", (g) => {
|
||||
- if (g.detail === 2) {
|
||||
- const I = this.pixelToCell(g.offsetX, g.offsetY), C = this.getWordAtCell(I.col, I.row);
|
||||
- if (C) {
|
||||
- const Q = this.viewportRowToAbsolute(I.row);
|
||||
- this.selectionStart = { col: C.startCol, absoluteRow: Q }, this.selectionEnd = { col: C.endCol, absoluteRow: Q }, this.requestRender();
|
||||
- const E = this.getSelection();
|
||||
- E && (this.copyToClipboard(E), this.selectionChangedEmitter.fire());
|
||||
- }
|
||||
- } else if (g.detail >= 3) {
|
||||
- const I = this.pixelToCell(g.offsetX, g.offsetY), C = this.viewportRowToAbsolute(I.row), Q = this.wasmTerm.getScrollbackLength();
|
||||
- let E = null;
|
||||
- if (C < Q)
|
||||
- E = this.wasmTerm.getScrollbackLine(C);
|
||||
- else {
|
||||
- const i = C - Q;
|
||||
- E = this.wasmTerm.getLine(i);
|
||||
- }
|
||||
- let D = -1;
|
||||
- if (E) {
|
||||
- for (let i = E.length - 1; i >= 0; i--)
|
||||
- if (E[i] && E[i].codepoint !== 0 && E[i].codepoint !== 32) {
|
||||
- D = i;
|
||||
- break;
|
||||
- }
|
||||
- }
|
||||
- if (D >= 0) {
|
||||
- this.selectionStart = { col: 0, absoluteRow: C }, this.selectionEnd = { col: D, absoluteRow: C }, this.requestRender();
|
||||
- const i = this.getSelection();
|
||||
- i && (this.copyToClipboard(i), this.selectionChangedEmitter.fire());
|
||||
+ if (!(this.wasmTerm.hasMouseTracking() && !g.shiftKey)) {
|
||||
+ if (g.detail === 2) {
|
||||
+ const I = this.pixelToCell(g.offsetX, g.offsetY), C = this.getWordAtCell(I.col, I.row);
|
||||
+ if (C) {
|
||||
+ const Q = this.viewportRowToAbsolute(I.row);
|
||||
+ this.selectionStart = { col: C.startCol, absoluteRow: Q }, this.selectionEnd = { col: C.endCol, absoluteRow: Q }, this.requestRender();
|
||||
+ const E = this.getSelection();
|
||||
+ E && (this.copyToClipboard(E), this.selectionChangedEmitter.fire());
|
||||
+ }
|
||||
+ } else if (g.detail >= 3) {
|
||||
+ const I = this.pixelToCell(g.offsetX, g.offsetY), C = this.viewportRowToAbsolute(I.row), Q = this.wasmTerm.getScrollbackLength();
|
||||
+ let E = null;
|
||||
+ if (C < Q)
|
||||
+ E = this.wasmTerm.getScrollbackLine(C);
|
||||
+ else {
|
||||
+ const i = C - Q;
|
||||
+ E = this.wasmTerm.getLine(i);
|
||||
+ }
|
||||
+ let D = -1;
|
||||
+ if (E) {
|
||||
+ for (let i = E.length - 1; i >= 0; i--)
|
||||
+ if (E[i] && E[i].codepoint !== 0 && E[i].codepoint !== 32) {
|
||||
+ D = i;
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+ if (D >= 0) {
|
||||
+ this.selectionStart = { col: 0, absoluteRow: C }, this.selectionEnd = { col: D, absoluteRow: C }, this.requestRender();
|
||||
+ const i = this.getSelection();
|
||||
+ i && (this.copyToClipboard(i), this.selectionChangedEmitter.fire());
|
||||
+ }
|
||||
}
|
||||
}
|
||||
}), this.boundContextMenuHandler = (g) => {
|
||||
@@ -2676,23 +2678,27 @@
|
||||
const M = await this.linkDetector.getLinkAt(Q, o);
|
||||
M && (M.activate(I), (I.ctrlKey || I.metaKey) && I.preventDefault());
|
||||
}, this.handleWheel = (I) => {
|
||||
- var Q, E, D;
|
||||
- if (I.preventDefault(), I.stopPropagation(), this.customWheelEventHandler && this.customWheelEventHandler(I))
|
||||
+ var Q, E, D, i;
|
||||
+ if (I.preventDefault(), this.customWheelEventHandler && this.customWheelEventHandler(I)) {
|
||||
+ I.stopPropagation();
|
||||
+ return;
|
||||
+ }
|
||||
+ if ((Q = this.wasmTerm) != null && Q.hasMouseTracking() && !I.shiftKey)
|
||||
return;
|
||||
- if (((Q = this.wasmTerm) == null ? void 0 : Q.isAlternateScreen()) ?? !1) {
|
||||
- const i = I.deltaY > 0 ? "down" : "up", o = Math.min(Math.abs(Math.round(I.deltaY / 33)), 5);
|
||||
- for (let s = 0; s < o; s++)
|
||||
- i === "up" ? this.dataEmitter.fire("\x1B[A") : this.dataEmitter.fire("\x1B[B");
|
||||
+ if (I.stopPropagation(), ((E = this.wasmTerm) == null ? void 0 : E.isAlternateScreen()) ?? !1) {
|
||||
+ const o = I.deltaY > 0 ? "down" : "up", s = Math.min(Math.abs(Math.round(I.deltaY / 33)), 5);
|
||||
+ for (let G = 0; G < s; G++)
|
||||
+ o === "up" ? this.dataEmitter.fire("\x1B[A") : this.dataEmitter.fire("\x1B[B");
|
||||
} else {
|
||||
- let i;
|
||||
+ let o;
|
||||
if (I.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
|
||||
- const o = ((D = (E = this.renderer) == null ? void 0 : E.getMetrics()) == null ? void 0 : D.height) ?? 20;
|
||||
- i = I.deltaY / o;
|
||||
+ const s = ((i = (D = this.renderer) == null ? void 0 : D.getMetrics()) == null ? void 0 : i.height) ?? 20;
|
||||
+ o = I.deltaY / s;
|
||||
} else
|
||||
- I.deltaMode === WheelEvent.DOM_DELTA_LINE ? i = I.deltaY : I.deltaMode === WheelEvent.DOM_DELTA_PAGE ? i = I.deltaY * this.rows : i = I.deltaY / 33;
|
||||
- if (i !== 0) {
|
||||
- const o = this.viewportY - i;
|
||||
- this.smoothScrollTo(o);
|
||||
+ I.deltaMode === WheelEvent.DOM_DELTA_LINE ? o = I.deltaY : I.deltaMode === WheelEvent.DOM_DELTA_PAGE ? o = I.deltaY * this.rows : o = I.deltaY / 33;
|
||||
+ if (o !== 0) {
|
||||
+ const s = this.viewportY - o;
|
||||
+ this.smoothScrollTo(s);
|
||||
}
|
||||
}
|
||||
}, this.handleMouseDown = (I) => {
|
||||
@@ -3254,7 +3260,7 @@
|
||||
* Clean up components (called on dispose or error)
|
||||
*/
|
||||
cleanupComponents() {
|
||||
- this.selectionManager && (this.selectionManager.dispose(), this.selectionManager = void 0), this.inputHandler && (this.inputHandler.dispose(), this.inputHandler = void 0), this.renderer && (this.renderer.dispose(), this.renderer = void 0), this.canvas && this.canvas.parentNode && (this.canvas.parentNode.removeChild(this.canvas), this.canvas = void 0), this.textarea && this.textarea.parentNode && (this.textarea.parentNode.removeChild(this.textarea), this.textarea = void 0), this.element && (this.element.removeEventListener("wheel", this.handleWheel), this.element.removeEventListener("mousedown", this.handleMouseDown, { capture: !0 }), this.element.removeEventListener("mousemove", this.handleMouseMove), this.element.removeEventListener("mouseleave", this.handleMouseLeave), this.element.removeEventListener("click", this.handleClick), this.element.removeAttribute("contenteditable"), this.element.removeAttribute("role"), this.element.removeAttribute("aria-label"), this.element.removeAttribute("aria-multiline")), this.isOpen && typeof document < "u" && document.removeEventListener("mouseup", this.handleMouseUp), this.scrollbarHideTimeout && (window.clearTimeout(this.scrollbarHideTimeout), this.scrollbarHideTimeout = void 0), this.linkDetector && (this.linkDetector.dispose(), this.linkDetector = void 0), this.wasmTerm && (this.wasmTerm.free(), this.wasmTerm = void 0), this.ghostty = void 0, this.element = void 0, this.textarea = void 0;
|
||||
+ this.selectionManager && (this.selectionManager.dispose(), this.selectionManager = void 0), this.inputHandler && (this.inputHandler.dispose(), this.inputHandler = void 0), this.renderer && (this.renderer.dispose(), this.renderer = void 0), this.canvas && this.canvas.parentNode && (this.canvas.parentNode.removeChild(this.canvas), this.canvas = void 0), this.textarea && this.textarea.parentNode && (this.textarea.parentNode.removeChild(this.textarea), this.textarea = void 0), this.element && (this.element.removeEventListener("wheel", this.handleWheel, { capture: !0 }), this.element.removeEventListener("mousedown", this.handleMouseDown, { capture: !0 }), this.element.removeEventListener("mousemove", this.handleMouseMove), this.element.removeEventListener("mouseleave", this.handleMouseLeave), this.element.removeEventListener("click", this.handleClick), this.element.removeAttribute("contenteditable"), this.element.removeAttribute("role"), this.element.removeAttribute("aria-label"), this.element.removeAttribute("aria-multiline")), this.isOpen && typeof document < "u" && document.removeEventListener("mouseup", this.handleMouseUp), this.scrollbarHideTimeout && (window.clearTimeout(this.scrollbarHideTimeout), this.scrollbarHideTimeout = void 0), this.linkDetector && (this.linkDetector.dispose(), this.linkDetector = void 0), this.wasmTerm && (this.wasmTerm.free(), this.wasmTerm = void 0), this.ghostty = void 0, this.element = void 0, this.textarea = void 0;
|
||||
}
|
||||
/**
|
||||
* Assert terminal is open (throw if not)
|
||||
Reference in New Issue
Block a user