pi-libtui 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/package.json +55 -0
- package/src/activity-presentation.ts +75 -0
- package/src/appearance.ts +632 -0
- package/src/background-surface.ts +125 -0
- package/src/color/palette.ts +186 -0
- package/src/color/preview.ts +157 -0
- package/src/color/resolver.ts +75 -0
- package/src/color/theme.ts +531 -0
- package/src/component-stack.ts +247 -0
- package/src/content/markdown-code.ts +60 -0
- package/src/content/terminal-text.ts +135 -0
- package/src/content/text.ts +105 -0
- package/src/controls/action-panel.ts +420 -0
- package/src/controls/dialog-button-bar.ts +287 -0
- package/src/controls/multi-select.ts +467 -0
- package/src/controls/picker-panel.ts +447 -0
- package/src/controls/screen-icon-actions.ts +134 -0
- package/src/controls/searchable-select.ts +394 -0
- package/src/controls/select-box.ts +224 -0
- package/src/controls/selectable-list.ts +433 -0
- package/src/controls/selection-action-bar.ts +382 -0
- package/src/controls/semantic-input.ts +23 -0
- package/src/controls/tab.ts +189 -0
- package/src/cursor.ts +288 -0
- package/src/decoration/editor-pills.ts +156 -0
- package/src/decoration/glyphs.ts +326 -0
- package/src/decoration/pointer-interaction.ts +123 -0
- package/src/decoration/powerline-pill.ts +104 -0
- package/src/decoration/status.ts +185 -0
- package/src/decoration/transient-pill.ts +176 -0
- package/src/diff/index.ts +21 -0
- package/src/diff/model.ts +76 -0
- package/src/diff/parse.ts +310 -0
- package/src/diff/pierre-syntax.ts +86 -0
- package/src/diff/render.ts +504 -0
- package/src/diff/structured.ts +264 -0
- package/src/diff/view.ts +75 -0
- package/src/editor/chrome.ts +341 -0
- package/src/editor/composition.ts +53 -0
- package/src/editor/layout.ts +81 -0
- package/src/editor/presentation.ts +35 -0
- package/src/editor/protocol.ts +188 -0
- package/src/editor.ts +41 -0
- package/src/extension.ts +30 -0
- package/src/folding.ts +192 -0
- package/src/host/cursor-bridge.ts +210 -0
- package/src/host/editor-bridge.ts +148 -0
- package/src/host/extension-host.ts +231 -0
- package/src/host/mouse-bridge.ts +212 -0
- package/src/host/mouse-dispatch.ts +400 -0
- package/src/host/mouse-input.ts +85 -0
- package/src/host/mouse-targets.ts +238 -0
- package/src/host/pi-layout-adapter.ts +298 -0
- package/src/host/pi-native-selection.ts +227 -0
- package/src/host/split-pane-bridge.ts +720 -0
- package/src/host/transcript-bridge.ts +236 -0
- package/src/index.ts +334 -0
- package/src/line-layout.ts +6 -0
- package/src/motion.ts +1409 -0
- package/src/mouse/events.ts +55 -0
- package/src/mouse/fullscreen-layout.ts +260 -0
- package/src/mouse/registry.ts +348 -0
- package/src/mouse.ts +37 -0
- package/src/native-binary.ts +189 -0
- package/src/overlay/anchored.ts +91 -0
- package/src/overlay/detail-card.ts +158 -0
- package/src/overlay/dialog.ts +266 -0
- package/src/overlay/floating.ts +106 -0
- package/src/overlay/framed-editor.ts +236 -0
- package/src/overlay/fullscreen.ts +170 -0
- package/src/overlay/hover-tooltip.ts +109 -0
- package/src/overlay/modal-mount.ts +118 -0
- package/src/panels.ts +207 -0
- package/src/render-cache.ts +80 -0
- package/src/render-epoch.ts +48 -0
- package/src/request-animation.ts +160 -0
- package/src/scrollbar.ts +27 -0
- package/src/selection.ts +148 -0
- package/src/split-pane.ts +232 -0
- package/src/status-presentation.ts +411 -0
- package/src/stream.ts +543 -0
- package/src/syntax.ts +316 -0
- package/src/terminal/bridge-client.ts +243 -0
- package/src/terminal/embedding.ts +6 -0
- package/src/terminal/output.ts +151 -0
- package/src/terminal/projection.ts +362 -0
- package/src/terminal/pty-host.ts +263 -0
- package/src/terminal/pty-pane.ts +308 -0
- package/src/terminal-colors.ts +334 -0
- package/src/tool/action.ts +170 -0
- package/src/tool/activity.ts +500 -0
- package/src/tool/call-preview.ts +44 -0
- package/src/tool/disclosure-action.ts +89 -0
- package/src/tool/expanded-region-viewport.ts +103 -0
- package/src/tool/index.ts +26 -0
- package/src/tool/output.ts +271 -0
- package/src/tool/transcript.ts +150 -0
- package/src/tool/view-region.ts +581 -0
- package/test/appearance.test.ts +183 -0
- package/test/bounded-stream.test.ts +150 -0
- package/test/bridge-client.test.ts +42 -0
- package/test/color-preview.test.ts +23 -0
- package/test/component-stack.test.ts +206 -0
- package/test/components.test.ts +1165 -0
- package/test/content-text.test.ts +21 -0
- package/test/cursor-bridge.test.ts +151 -0
- package/test/cursor.test.ts +147 -0
- package/test/editor-chrome.test.ts +119 -0
- package/test/editor-layout.test.ts +30 -0
- package/test/editor-registry.test.ts +203 -0
- package/test/editor-theme.test.ts +33 -0
- package/test/editor-token-pill.test.ts +72 -0
- package/test/floating-overlay.test.ts +57 -0
- package/test/folding.test.ts +147 -0
- package/test/harmonious-theme.test.ts +23 -0
- package/test/interaction.test.ts +104 -0
- package/test/key-icons.test.ts +62 -0
- package/test/markdown-code.test.ts +24 -0
- package/test/modal-overlay-mount.test.ts +94 -0
- package/test/motion.test.ts +649 -0
- package/test/mouse/bridge.test.ts +1302 -0
- package/test/mouse/extension.test.ts +205 -0
- package/test/mouse/registry.test.ts +140 -0
- package/test/mouse/screen-decoration.test.ts +167 -0
- package/test/native-binary.test.ts +52 -0
- package/test/palette.test.ts +32 -0
- package/test/panels.test.ts +74 -0
- package/test/pointer-components.test.ts +151 -0
- package/test/progress.test.ts +54 -0
- package/test/pty-host.test.ts +126 -0
- package/test/pty-pane.test.ts +62 -0
- package/test/public-surface.test.ts +75 -0
- package/test/render-cache.test.ts +25 -0
- package/test/render-epoch.test.ts +119 -0
- package/test/request-animation.test.ts +193 -0
- package/test/screen-icon-actions.test.ts +69 -0
- package/test/select-box.test.ts +86 -0
- package/test/select-pointer.test.ts +369 -0
- package/test/selectable-list.test.ts +258 -0
- package/test/selection/registry.test.ts +44 -0
- package/test/selection-action-bar.test.ts +204 -0
- package/test/split-pane-bridge.test.ts +944 -0
- package/test/split-pane.test.ts +134 -0
- package/test/status-presentation.test.ts +39 -0
- package/test/syntax.test.ts +59 -0
- package/test/terminal-colors.test.ts +115 -0
- package/test/terminal-embedding.test.ts +12 -0
- package/test/terminal-projection.test.ts +312 -0
- package/test/theme-colors.test.ts +175 -0
- package/test/tool-output.test.ts +160 -0
- package/test/tool-transcript.test.ts +1190 -0
- package/test/unified-diff.test.ts +691 -0
- package/themes/harmonious.json +61 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { type TUI, TuiAltScreen } from "@earendil-works/pi-tui";
|
|
2
|
+
import { getMouseRegistryState } from "../mouse/registry.ts";
|
|
3
|
+
import type { MouseRegistry, ScreenDecorationContext } from "../mouse.ts";
|
|
4
|
+
import { installMouseDispatch } from "./mouse-dispatch.ts";
|
|
5
|
+
import { isRecord, rendererHasOverlay, rendererHasSelection, terminalSize } from "./pi-layout-adapter.ts";
|
|
6
|
+
import { nativeSelectionGeometry, rendererFullscreenLayout } from "./pi-native-selection.ts";
|
|
7
|
+
|
|
8
|
+
const ALL_MOTION_INSTALLATION_KEY = Symbol.for("pi-libtui/mouse/all-motion-installation/v1");
|
|
9
|
+
const SCREEN_DECORATION_INSTALLATION_KEY = Symbol.for("pi-libtui/mouse/screen-decoration-installation/v1");
|
|
10
|
+
const ENABLE_ALL_MOTION = "\x1b[?1003h";
|
|
11
|
+
const DISABLE_ALL_MOTION = "\x1b[?1003l";
|
|
12
|
+
|
|
13
|
+
// type-boundary: Pi 0.84.2's private renderer patch points are narrowed by the installation validators below.
|
|
14
|
+
type PiPrivateValue = unknown;
|
|
15
|
+
|
|
16
|
+
type ApplySelectionHandler = (this: object, screen: string[], layout?: PiPrivateValue) => PiPrivateValue;
|
|
17
|
+
|
|
18
|
+
interface AllMotionInstallation {
|
|
19
|
+
protocol: "pi-libtui/mouse/all-motion-installation/v1";
|
|
20
|
+
refs: number;
|
|
21
|
+
patched: (this: object) => void;
|
|
22
|
+
dispose(): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ScreenDecorationInstallation {
|
|
26
|
+
protocol: "pi-libtui/mouse/screen-decoration-installation/v1";
|
|
27
|
+
refs: number;
|
|
28
|
+
patched: ApplySelectionHandler;
|
|
29
|
+
dispose(): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isAllMotionInstallation(value: PiPrivateValue): value is AllMotionInstallation {
|
|
33
|
+
if (!isRecord(value)) return false;
|
|
34
|
+
return (
|
|
35
|
+
value.protocol === "pi-libtui/mouse/all-motion-installation/v1" &&
|
|
36
|
+
typeof value.refs === "number" &&
|
|
37
|
+
typeof value.patched === "function" &&
|
|
38
|
+
typeof value.dispose === "function"
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isScreenDecorationInstallation(value: PiPrivateValue): value is ScreenDecorationInstallation {
|
|
43
|
+
if (!isRecord(value)) return false;
|
|
44
|
+
return (
|
|
45
|
+
value.protocol === "pi-libtui/mouse/screen-decoration-installation/v1" &&
|
|
46
|
+
typeof value.refs === "number" &&
|
|
47
|
+
typeof value.patched === "function" &&
|
|
48
|
+
typeof value.dispose === "function"
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isPiMultiplexedEnvironment(env: NodeJS.ProcessEnv): boolean {
|
|
53
|
+
const term = env.TERM?.toLowerCase() ?? "";
|
|
54
|
+
return (
|
|
55
|
+
env.TMUX !== undefined ||
|
|
56
|
+
env.ZELLIJ !== undefined ||
|
|
57
|
+
env.STY !== undefined ||
|
|
58
|
+
term.startsWith("tmux") ||
|
|
59
|
+
term.startsWith("screen")
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function writeTerminal(renderer: object, data: string): void {
|
|
64
|
+
try {
|
|
65
|
+
const terminal = Reflect.get(renderer, "terminal") as PiPrivateValue;
|
|
66
|
+
if (!isRecord(terminal)) return;
|
|
67
|
+
const write = terminal.write;
|
|
68
|
+
if (typeof write === "function") Reflect.apply(write, terminal, [data]);
|
|
69
|
+
} catch {
|
|
70
|
+
// Mouse motion negotiation is best-effort.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function installAllMotionTracking(
|
|
75
|
+
tui: TUI,
|
|
76
|
+
prototype: object,
|
|
77
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
78
|
+
): () => void {
|
|
79
|
+
if (!isPiMultiplexedEnvironment(env)) return () => {};
|
|
80
|
+
const existing = Reflect.get(prototype, ALL_MOTION_INSTALLATION_KEY) as PiPrivateValue;
|
|
81
|
+
if (isAllMotionInstallation(existing)) {
|
|
82
|
+
const wasInactive = existing.refs === 0;
|
|
83
|
+
existing.refs += 1;
|
|
84
|
+
if (wasInactive && tui.mode === "fullscreen" && Reflect.get(tui as object, "mouseEnabled") !== false) {
|
|
85
|
+
writeTerminal(tui as object, ENABLE_ALL_MOTION);
|
|
86
|
+
}
|
|
87
|
+
let disposed = false;
|
|
88
|
+
return () => {
|
|
89
|
+
if (disposed) return;
|
|
90
|
+
disposed = true;
|
|
91
|
+
existing.refs -= 1;
|
|
92
|
+
if (existing.refs === 0) existing.dispose();
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const originalValue = Reflect.get(prototype, "beforeTerminalStart") as PiPrivateValue;
|
|
96
|
+
if (typeof originalValue !== "function") return () => {};
|
|
97
|
+
const original = originalValue as (this: object) => void;
|
|
98
|
+
let installation: AllMotionInstallation;
|
|
99
|
+
const patched = function (this: object): void {
|
|
100
|
+
Reflect.apply(original, this, []);
|
|
101
|
+
if (installation.refs > 0 && Reflect.get(this, "mouseEnabled") !== false) writeTerminal(this, ENABLE_ALL_MOTION);
|
|
102
|
+
};
|
|
103
|
+
let restored = false;
|
|
104
|
+
installation = {
|
|
105
|
+
protocol: "pi-libtui/mouse/all-motion-installation/v1",
|
|
106
|
+
refs: 1,
|
|
107
|
+
patched,
|
|
108
|
+
dispose() {
|
|
109
|
+
if (restored) return;
|
|
110
|
+
if (Reflect.get(prototype, ALL_MOTION_INSTALLATION_KEY) !== installation) return;
|
|
111
|
+
writeTerminal(tui as object, DISABLE_ALL_MOTION);
|
|
112
|
+
if (Reflect.get(prototype, "beforeTerminalStart") !== patched) return;
|
|
113
|
+
restored = true;
|
|
114
|
+
Reflect.set(prototype, "beforeTerminalStart", original);
|
|
115
|
+
Reflect.set(prototype, ALL_MOTION_INSTALLATION_KEY, undefined);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
Reflect.set(prototype, "beforeTerminalStart", patched);
|
|
119
|
+
Reflect.set(prototype, ALL_MOTION_INSTALLATION_KEY, installation);
|
|
120
|
+
if (tui.mode === "fullscreen" && Reflect.get(tui as object, "mouseEnabled") !== false) {
|
|
121
|
+
writeTerminal(tui as object, ENABLE_ALL_MOTION);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let disposed = false;
|
|
125
|
+
return () => {
|
|
126
|
+
if (disposed) return;
|
|
127
|
+
disposed = true;
|
|
128
|
+
installation.refs -= 1;
|
|
129
|
+
if (installation.refs === 0) installation.dispose();
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isScreen(value: PiPrivateValue): value is string[] {
|
|
134
|
+
return Array.isArray(value) && value.every((line) => typeof line === "string");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function installScreenDecoration(prototype: object, registry: MouseRegistry): () => void {
|
|
138
|
+
const existing = Reflect.get(prototype, SCREEN_DECORATION_INSTALLATION_KEY) as PiPrivateValue;
|
|
139
|
+
if (isScreenDecorationInstallation(existing)) {
|
|
140
|
+
existing.refs += 1;
|
|
141
|
+
let disposed = false;
|
|
142
|
+
return () => {
|
|
143
|
+
if (disposed) return;
|
|
144
|
+
disposed = true;
|
|
145
|
+
existing.refs -= 1;
|
|
146
|
+
if (existing.refs === 0) existing.dispose();
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const originalValue = Reflect.get(prototype, "applySelection") as PiPrivateValue;
|
|
150
|
+
if (typeof originalValue !== "function") return () => {};
|
|
151
|
+
const original = originalValue as ApplySelectionHandler;
|
|
152
|
+
let installation: ScreenDecorationInstallation;
|
|
153
|
+
const patched: ApplySelectionHandler = function (screen, layout) {
|
|
154
|
+
const native = Reflect.apply(original, this, [screen, layout]) as PiPrivateValue;
|
|
155
|
+
if (installation.refs === 0 || !isScreen(native) || getMouseRegistryState(registry).screenDecorators.length === 0)
|
|
156
|
+
return native;
|
|
157
|
+
const size = terminalSize(this);
|
|
158
|
+
if (!size) return native;
|
|
159
|
+
const fullscreenLayout = rendererFullscreenLayout(this);
|
|
160
|
+
const context: ScreenDecorationContext = {
|
|
161
|
+
width: size.columns,
|
|
162
|
+
height: size.rows,
|
|
163
|
+
hasOverlay: rendererHasOverlay(this),
|
|
164
|
+
selectionActive: rendererHasSelection(this),
|
|
165
|
+
selection: nativeSelectionGeometry(this),
|
|
166
|
+
...(fullscreenLayout ? { viewport: fullscreenLayout.viewport, transcriptLines: fullscreenLayout.lines } : {}),
|
|
167
|
+
};
|
|
168
|
+
return registry.dispatchScreenDecorators(native, context);
|
|
169
|
+
};
|
|
170
|
+
let restored = false;
|
|
171
|
+
installation = {
|
|
172
|
+
protocol: "pi-libtui/mouse/screen-decoration-installation/v1",
|
|
173
|
+
refs: 1,
|
|
174
|
+
patched,
|
|
175
|
+
dispose() {
|
|
176
|
+
if (restored) return;
|
|
177
|
+
if (Reflect.get(prototype, SCREEN_DECORATION_INSTALLATION_KEY) !== installation) return;
|
|
178
|
+
if (Reflect.get(prototype, "applySelection") !== patched) return;
|
|
179
|
+
restored = true;
|
|
180
|
+
Reflect.set(prototype, "applySelection", original);
|
|
181
|
+
Reflect.set(prototype, SCREEN_DECORATION_INSTALLATION_KEY, undefined);
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
Reflect.set(prototype, "applySelection", patched);
|
|
185
|
+
Reflect.set(prototype, SCREEN_DECORATION_INSTALLATION_KEY, installation);
|
|
186
|
+
|
|
187
|
+
let disposed = false;
|
|
188
|
+
return () => {
|
|
189
|
+
if (disposed) return;
|
|
190
|
+
disposed = true;
|
|
191
|
+
installation.refs -= 1;
|
|
192
|
+
if (installation.refs === 0) installation.dispose();
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Install the generic fullscreen pointer, layout, selection, and decoration compatibility leases. */
|
|
197
|
+
export function installMouseBridge(tui: TUI, registry: MouseRegistry): () => void {
|
|
198
|
+
const prototype = tui.mode === "fullscreen" ? Reflect.getPrototypeOf(tui as object) : TuiAltScreen.prototype;
|
|
199
|
+
if (!prototype) return () => {};
|
|
200
|
+
|
|
201
|
+
const removeAllMotion = installAllMotionTracking(tui, prototype);
|
|
202
|
+
const removeScreenDecoration = installScreenDecoration(prototype, registry);
|
|
203
|
+
const removeMouseDispatch = installMouseDispatch(tui, prototype, registry);
|
|
204
|
+
let disposed = false;
|
|
205
|
+
return () => {
|
|
206
|
+
if (disposed) return;
|
|
207
|
+
disposed = true;
|
|
208
|
+
removeMouseDispatch();
|
|
209
|
+
removeAllMotion();
|
|
210
|
+
removeScreenDecoration();
|
|
211
|
+
};
|
|
212
|
+
}
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { type Component, matchesKey, type TUI, type TuiInputListenerResult } from "@earendil-works/pi-tui";
|
|
2
|
+
import { getMouseRegistryState } from "../mouse/registry.ts";
|
|
3
|
+
import type { MouseRegistry, TextInteractionTarget, TuiMouseEventType } from "../mouse.ts";
|
|
4
|
+
import { publishFullscreenLayoutCapability } from "../mouse.ts";
|
|
5
|
+
import { ensureSelectionRegistry } from "../selection.ts";
|
|
6
|
+
import { createEvent, isPrimarySelectionRelease, type ParsedMouseEvent, parseMouse } from "./mouse-input.ts";
|
|
7
|
+
import { type MouseTarget, targetsAt as resolveTargetsAt } from "./mouse-targets.ts";
|
|
8
|
+
import {
|
|
9
|
+
contains,
|
|
10
|
+
isRecord,
|
|
11
|
+
isVisibleComponent,
|
|
12
|
+
rendererHasOverlay,
|
|
13
|
+
rendererLayoutFrame,
|
|
14
|
+
} from "./pi-layout-adapter.ts";
|
|
15
|
+
import { fullscreenLayoutCapability, nativeSelectionCompleted } from "./pi-native-selection.ts";
|
|
16
|
+
|
|
17
|
+
const INSTALLATION_KEY = Symbol.for("pi-libtui/mouse/bridge-installation/v1");
|
|
18
|
+
const FOCUS_IN = "\x1b[I";
|
|
19
|
+
const FOCUS_OUT = "\x1b[O";
|
|
20
|
+
|
|
21
|
+
// type-boundary: Pi 0.84.2's private input and overlay members are narrowed by the validators below.
|
|
22
|
+
type PiPrivateValue = unknown;
|
|
23
|
+
|
|
24
|
+
type ViewportInputHandler = (this: object, data: string) => TuiInputListenerResult;
|
|
25
|
+
|
|
26
|
+
interface BridgeInstallation {
|
|
27
|
+
protocol: "pi-libtui/mouse/bridge-installation/v1";
|
|
28
|
+
refs: number;
|
|
29
|
+
patched: ViewportInputHandler;
|
|
30
|
+
dispose(): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface PointerState {
|
|
34
|
+
capture?: MouseTarget;
|
|
35
|
+
hover?: MouseTarget;
|
|
36
|
+
lastPointer?: ParsedMouseEvent;
|
|
37
|
+
textClick?: { target: MouseTarget; x: number; y: number; moved: boolean };
|
|
38
|
+
nativePrimaryPress?: boolean;
|
|
39
|
+
keyboardTarget?: Component & TextInteractionTarget;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function setKeyboardTarget(state: PointerState, target: (Component & TextInteractionTarget) | undefined): void {
|
|
43
|
+
if (state.keyboardTarget === target) return;
|
|
44
|
+
try {
|
|
45
|
+
state.keyboardTarget?.setViewportFocus(false);
|
|
46
|
+
} catch {
|
|
47
|
+
// Optional transcript interaction must not break the renderer input path.
|
|
48
|
+
}
|
|
49
|
+
state.keyboardTarget = target;
|
|
50
|
+
try {
|
|
51
|
+
target?.setViewportFocus(true);
|
|
52
|
+
} catch {
|
|
53
|
+
state.keyboardTarget = undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isInstallation(value: PiPrivateValue): value is BridgeInstallation {
|
|
58
|
+
if (!isRecord(value)) return false;
|
|
59
|
+
return (
|
|
60
|
+
value.protocol === "pi-libtui/mouse/bridge-installation/v1" &&
|
|
61
|
+
typeof value.refs === "number" &&
|
|
62
|
+
typeof value.patched === "function" &&
|
|
63
|
+
typeof value.dispose === "function"
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function withNativeCopyDeferred(renderer: object, invoke: () => TuiInputListenerResult): TuiInputListenerResult {
|
|
68
|
+
const property = "copySelectionToClipboard";
|
|
69
|
+
const ownDescriptor = Reflect.getOwnPropertyDescriptor(renderer, property);
|
|
70
|
+
const suppress = (): Promise<void> => Promise.resolve();
|
|
71
|
+
if (!Reflect.set(renderer, property, suppress) || Reflect.get(renderer, property) !== suppress) return invoke();
|
|
72
|
+
try {
|
|
73
|
+
return invoke();
|
|
74
|
+
} finally {
|
|
75
|
+
if (Reflect.get(renderer, property) === suppress) {
|
|
76
|
+
if (ownDescriptor) Reflect.defineProperty(renderer, property, ownDescriptor);
|
|
77
|
+
else Reflect.deleteProperty(renderer, property);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function requestRender(renderer: object): void {
|
|
83
|
+
try {
|
|
84
|
+
const method = Reflect.get(renderer, "requestRender") as PiPrivateValue;
|
|
85
|
+
if (typeof method === "function") Reflect.apply(method, renderer, []);
|
|
86
|
+
} catch {
|
|
87
|
+
// A component callback must not break Pi's input path during renderer replacement.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function nativePointerOwnsInput(renderer: object): boolean {
|
|
92
|
+
return Reflect.get(renderer, "selectionPressActive") === true || Boolean(Reflect.get(renderer, "scrollbarDrag"));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function defersInputToOverlay(renderer: object): boolean {
|
|
96
|
+
const method = Reflect.get(renderer, "shouldDeferViewportInputToOverlay") as PiPrivateValue;
|
|
97
|
+
if (typeof method !== "function") return false;
|
|
98
|
+
try {
|
|
99
|
+
return Reflect.apply(method, renderer, []) === true;
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Patch Pi's fullscreen input path until the official API also covers overlays, capture, and hover. */
|
|
106
|
+
export function installMouseDispatch(tui: TUI, prototype: object, registry: MouseRegistry): () => void {
|
|
107
|
+
const finish = (removeBridge: () => void): (() => void) => {
|
|
108
|
+
let disposed = false;
|
|
109
|
+
return () => {
|
|
110
|
+
if (disposed) return;
|
|
111
|
+
disposed = true;
|
|
112
|
+
removeBridge();
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
const existing = Reflect.get(prototype, INSTALLATION_KEY) as PiPrivateValue;
|
|
116
|
+
if (isInstallation(existing)) {
|
|
117
|
+
existing.refs += 1;
|
|
118
|
+
let disposed = false;
|
|
119
|
+
return finish(() => {
|
|
120
|
+
if (disposed) return;
|
|
121
|
+
disposed = true;
|
|
122
|
+
existing.refs -= 1;
|
|
123
|
+
if (existing.refs === 0) existing.dispose();
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const originalValue = Reflect.get(prototype, "handleViewportInput") as PiPrivateValue;
|
|
128
|
+
if (typeof originalValue !== "function") return finish(() => {});
|
|
129
|
+
const original = originalValue as ViewportInputHandler;
|
|
130
|
+
const removeFullscreenLayoutCapability = publishFullscreenLayoutCapability(fullscreenLayoutCapability);
|
|
131
|
+
const registryState = getMouseRegistryState(registry);
|
|
132
|
+
const rendererForViewport = tui as object;
|
|
133
|
+
const preserveViewport = (): void => {
|
|
134
|
+
const frame = rendererLayoutFrame(rendererForViewport);
|
|
135
|
+
const scrollView = frame?.primaryScrollView;
|
|
136
|
+
if (!scrollView) return;
|
|
137
|
+
try {
|
|
138
|
+
scrollView.scrollTo(scrollView.scrollTop, { disableFollow: true });
|
|
139
|
+
} catch {
|
|
140
|
+
// The native viewport is optional across Pi versions.
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
registryState.viewportPreserver = preserveViewport;
|
|
144
|
+
const states = new Map<object, PointerState>();
|
|
145
|
+
const targetsAt = (renderer: object, x: number, y: number, includeLayout: boolean): MouseTarget[] =>
|
|
146
|
+
resolveTargetsAt(registry, renderer, x, y, includeLayout, defersInputToOverlay);
|
|
147
|
+
|
|
148
|
+
const send = (target: MouseTarget, type: TuiMouseEventType, parsed: ParsedMouseEvent): boolean => {
|
|
149
|
+
const rect = target.getRect();
|
|
150
|
+
if (!rect) return false;
|
|
151
|
+
return target.dispatch(createEvent(type, parsed, rect));
|
|
152
|
+
};
|
|
153
|
+
const containsTarget = (target: MouseTarget, parsed: ParsedMouseEvent): boolean => {
|
|
154
|
+
if (target.isCurrent && !target.isCurrent()) return false;
|
|
155
|
+
if (target.containsPoint) return target.containsPoint(parsed.x, parsed.y);
|
|
156
|
+
const rect = target.getRect();
|
|
157
|
+
return Boolean(rect && contains(rect, parsed.x, parsed.y));
|
|
158
|
+
};
|
|
159
|
+
const finishTextClick = (
|
|
160
|
+
renderer: object,
|
|
161
|
+
state: PointerState,
|
|
162
|
+
parsed: ParsedMouseEvent,
|
|
163
|
+
dispatchRelease: boolean,
|
|
164
|
+
): void => {
|
|
165
|
+
const textClick = state.textClick;
|
|
166
|
+
if (!textClick) return;
|
|
167
|
+
const isClick = !textClick.moved && parsed.x === textClick.x && parsed.y === textClick.y;
|
|
168
|
+
// Resolve the release against the current frame. A press can invalidate and
|
|
169
|
+
// rerender a disclosure before its release arrives; dispatching through the
|
|
170
|
+
// captured box would let that stale geometry collapse a different surface.
|
|
171
|
+
const currentTarget = targetsAt(renderer, parsed.x, parsed.y, true).find(
|
|
172
|
+
(target) => target.key === textClick.target.key && containsTarget(target, parsed),
|
|
173
|
+
);
|
|
174
|
+
const handled = dispatchRelease
|
|
175
|
+
? Boolean(currentTarget && send(currentTarget, "release", parsed))
|
|
176
|
+
: Boolean(isClick && currentTarget);
|
|
177
|
+
if (isClick && handled && currentTarget?.textInteraction) {
|
|
178
|
+
setKeyboardTarget(state, currentTarget.textInteraction);
|
|
179
|
+
}
|
|
180
|
+
state.textClick = undefined;
|
|
181
|
+
};
|
|
182
|
+
const cancelTextClick = (state: PointerState, parsed: ParsedMouseEvent | undefined): void => {
|
|
183
|
+
if (!state.textClick) return;
|
|
184
|
+
if (parsed) send(state.textClick.target, "release", { ...parsed, rawButton: 3 });
|
|
185
|
+
state.textClick = undefined;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const patched: ViewportInputHandler = function (data) {
|
|
189
|
+
let state = states.get(this);
|
|
190
|
+
if (!state) {
|
|
191
|
+
state = {};
|
|
192
|
+
states.set(this, state);
|
|
193
|
+
}
|
|
194
|
+
const mouseInput = parseMouse(data);
|
|
195
|
+
if (mouseInput.kind !== "event") {
|
|
196
|
+
if (data === FOCUS_OUT) {
|
|
197
|
+
const lastPointer = state.lastPointer;
|
|
198
|
+
const hadPointerState = Boolean(state.capture || state.hover || state.textClick);
|
|
199
|
+
if (lastPointer) {
|
|
200
|
+
if (state.capture) send(state.capture, "release", { ...lastPointer, rawButton: 3 });
|
|
201
|
+
if (state.hover) send(state.hover, "leave", lastPointer);
|
|
202
|
+
}
|
|
203
|
+
cancelTextClick(state, lastPointer);
|
|
204
|
+
state.capture = undefined;
|
|
205
|
+
state.hover = undefined;
|
|
206
|
+
state.lastPointer = undefined;
|
|
207
|
+
state.nativePrimaryPress = undefined;
|
|
208
|
+
setKeyboardTarget(state, undefined);
|
|
209
|
+
if (hadPointerState) requestRender(this);
|
|
210
|
+
}
|
|
211
|
+
if (data === FOCUS_IN || data === FOCUS_OUT || mouseInput.kind === "unsupported") {
|
|
212
|
+
return Reflect.apply(original, this, [data]);
|
|
213
|
+
}
|
|
214
|
+
const viewportInput = registry.dispatchViewportInput(data);
|
|
215
|
+
if (viewportInput.consumed) return { consume: true, data: viewportInput.data };
|
|
216
|
+
if (rendererHasOverlay(this) || defersInputToOverlay(this)) {
|
|
217
|
+
setKeyboardTarget(state, undefined);
|
|
218
|
+
return Reflect.apply(original, this, [viewportInput.data]);
|
|
219
|
+
}
|
|
220
|
+
if (matchesKey(viewportInput.data, "escape")) {
|
|
221
|
+
setKeyboardTarget(state, undefined);
|
|
222
|
+
requestRender(this);
|
|
223
|
+
return Reflect.apply(original, this, [viewportInput.data]);
|
|
224
|
+
}
|
|
225
|
+
const keyboardTarget = state.keyboardTarget;
|
|
226
|
+
if (keyboardTarget) {
|
|
227
|
+
const frame = rendererLayoutFrame(this);
|
|
228
|
+
if (!frame || !isVisibleComponent(frame, keyboardTarget)) {
|
|
229
|
+
setKeyboardTarget(state, undefined);
|
|
230
|
+
} else {
|
|
231
|
+
try {
|
|
232
|
+
if (keyboardTarget.handleViewportInput(viewportInput.data)) {
|
|
233
|
+
requestRender(this);
|
|
234
|
+
return { consume: true, data: viewportInput.data };
|
|
235
|
+
}
|
|
236
|
+
// Once input returns to the editor, Enter must submit there rather than toggle the old target.
|
|
237
|
+
setKeyboardTarget(state, undefined);
|
|
238
|
+
} catch {
|
|
239
|
+
setKeyboardTarget(state, undefined);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return Reflect.apply(original, this, [viewportInput.data]);
|
|
244
|
+
}
|
|
245
|
+
const parsed = mouseInput.event;
|
|
246
|
+
state.lastPointer = parsed;
|
|
247
|
+
if (parsed.type === "press" && defersInputToOverlay(this)) setKeyboardTarget(state, undefined);
|
|
248
|
+
if (!state.capture && nativePointerOwnsInput(this)) {
|
|
249
|
+
const textClick = state.textClick;
|
|
250
|
+
if (textClick && parsed.type === "drag") {
|
|
251
|
+
textClick.moved ||= parsed.x !== textClick.x || parsed.y !== textClick.y;
|
|
252
|
+
if (textClick.moved) send(textClick.target, "drag", parsed);
|
|
253
|
+
}
|
|
254
|
+
const selectionWasActive = parsed.type === "release" && Reflect.get(this, "selectionPressActive") === true;
|
|
255
|
+
const deferCopy = selectionWasActive && isPrimarySelectionRelease(parsed) && registry.shouldDeferNativeCopy();
|
|
256
|
+
const invokeOriginal = (): TuiInputListenerResult => Reflect.apply(original, this, [data]);
|
|
257
|
+
const result = deferCopy ? withNativeCopyDeferred(this, invokeOriginal) : invokeOriginal();
|
|
258
|
+
const selectionEnded = selectionWasActive && Reflect.get(this, "selectionPressActive") === false;
|
|
259
|
+
if (selectionEnded) {
|
|
260
|
+
const selection = nativeSelectionCompleted(this);
|
|
261
|
+
if (selection) ensureSelectionRegistry().publishSelectionCompleted(selection);
|
|
262
|
+
}
|
|
263
|
+
if (textClick && parsed.type === "release") {
|
|
264
|
+
finishTextClick(this, state, parsed, true);
|
|
265
|
+
requestRender(this);
|
|
266
|
+
}
|
|
267
|
+
if (parsed.type === "release") state.nativePrimaryPress = undefined;
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (state.textClick && parsed.type === "drag") {
|
|
272
|
+
state.textClick.moved ||= parsed.x !== state.textClick.x || parsed.y !== state.textClick.y;
|
|
273
|
+
if (state.textClick.moved) send(state.textClick.target, "drag", parsed);
|
|
274
|
+
return Reflect.apply(original, this, [data]);
|
|
275
|
+
}
|
|
276
|
+
if (state.textClick && parsed.type === "release") {
|
|
277
|
+
const result = Reflect.apply(original, this, [data]);
|
|
278
|
+
finishTextClick(this, state, parsed, true);
|
|
279
|
+
state.nativePrimaryPress = undefined;
|
|
280
|
+
requestRender(this);
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// A component only owns a release after it handled the matching press. Keep
|
|
285
|
+
// native selection gestures out of the general release dispatch path when a
|
|
286
|
+
// stale layout makes an expanded fold report a release hit.
|
|
287
|
+
if (state.nativePrimaryPress && (parsed.type === "drag" || parsed.type === "release")) {
|
|
288
|
+
if (parsed.type === "release") state.nativePrimaryPress = undefined;
|
|
289
|
+
return Reflect.apply(original, this, [data]);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if ((parsed.type === "drag" || parsed.type === "release") && state.capture) {
|
|
293
|
+
const captured = state.capture;
|
|
294
|
+
send(captured, parsed.type, parsed);
|
|
295
|
+
if (parsed.type === "release") state.capture = undefined;
|
|
296
|
+
requestRender(this);
|
|
297
|
+
return { consume: true };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (parsed.type === "move") {
|
|
301
|
+
const previousHover = state.hover;
|
|
302
|
+
if (previousHover && previousHover.frame === rendererLayoutFrame(this)) {
|
|
303
|
+
if (containsTarget(previousHover, parsed) && send(previousHover, "move", parsed)) {
|
|
304
|
+
return { consume: true };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
let next: MouseTarget | undefined;
|
|
308
|
+
let handled = false;
|
|
309
|
+
for (const target of targetsAt(this, parsed.x, parsed.y, true)) {
|
|
310
|
+
const type = state.hover?.key === target.key ? "move" : "enter";
|
|
311
|
+
if (!send(target, type, parsed)) continue;
|
|
312
|
+
next = target;
|
|
313
|
+
handled = true;
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
if (previousHover && previousHover.key !== next?.key) send(previousHover, "leave", parsed);
|
|
317
|
+
state.hover = next;
|
|
318
|
+
// Components request their own redraw when a move changes local state. The
|
|
319
|
+
// bridge only redraws for target transitions, keeping raw motion storms O(1)
|
|
320
|
+
// when the pointer remains over the same affordance.
|
|
321
|
+
if (previousHover?.key !== next?.key) requestRender(this);
|
|
322
|
+
return handled ? { consume: true } : Reflect.apply(original, this, [data]);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let prepassedOverlayKeys: ReadonlySet<object> | undefined;
|
|
326
|
+
if (parsed.type === "press" && (parsed.rawButton & 3) === 0 && !defersInputToOverlay(this)) {
|
|
327
|
+
let textTarget: MouseTarget | undefined;
|
|
328
|
+
const dispatchedOverlayKeys = new Set<object>();
|
|
329
|
+
prepassedOverlayKeys = dispatchedOverlayKeys;
|
|
330
|
+
for (const target of targetsAt(this, parsed.x, parsed.y, true)) {
|
|
331
|
+
if (target.overlayRegion) {
|
|
332
|
+
dispatchedOverlayKeys.add(target.key);
|
|
333
|
+
if (!send(target, "press", parsed)) continue;
|
|
334
|
+
state.capture = target;
|
|
335
|
+
setKeyboardTarget(state, undefined);
|
|
336
|
+
requestRender(this);
|
|
337
|
+
return { consume: true };
|
|
338
|
+
}
|
|
339
|
+
if (!target.textInteraction || !send(target, "press", parsed)) continue;
|
|
340
|
+
textTarget = target;
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
if (textTarget) {
|
|
344
|
+
state.textClick = { target: textTarget, x: parsed.x, y: parsed.y, moved: false };
|
|
345
|
+
requestRender(this);
|
|
346
|
+
return Reflect.apply(original, this, [data]);
|
|
347
|
+
}
|
|
348
|
+
setKeyboardTarget(state, undefined);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
for (const target of targetsAt(this, parsed.x, parsed.y, true)) {
|
|
352
|
+
if (prepassedOverlayKeys?.has(target.key)) continue;
|
|
353
|
+
if (!send(target, parsed.type, parsed)) continue;
|
|
354
|
+
if (parsed.type === "press") state.capture = target;
|
|
355
|
+
requestRender(this);
|
|
356
|
+
return { consume: true };
|
|
357
|
+
}
|
|
358
|
+
if (parsed.type === "press" && (parsed.rawButton & 3) === 0) state.nativePrimaryPress = true;
|
|
359
|
+
return Reflect.apply(original, this, [data]);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
let restored = false;
|
|
363
|
+
const installation: BridgeInstallation = {
|
|
364
|
+
protocol: "pi-libtui/mouse/bridge-installation/v1",
|
|
365
|
+
refs: 1,
|
|
366
|
+
patched,
|
|
367
|
+
dispose() {
|
|
368
|
+
if (restored) return;
|
|
369
|
+
const current = Reflect.get(prototype, INSTALLATION_KEY) as PiPrivateValue;
|
|
370
|
+
if (current !== installation) return;
|
|
371
|
+
if (Reflect.get(prototype, "handleViewportInput") !== patched) return;
|
|
372
|
+
restored = true;
|
|
373
|
+
if (registryState.viewportPreserver === preserveViewport) registryState.viewportPreserver = undefined;
|
|
374
|
+
for (const [renderer, state] of states) {
|
|
375
|
+
if (state.lastPointer) {
|
|
376
|
+
if (state.capture) send(state.capture, "release", { ...state.lastPointer, rawButton: 3 });
|
|
377
|
+
if (state.hover) send(state.hover, "leave", state.lastPointer);
|
|
378
|
+
}
|
|
379
|
+
cancelTextClick(state, state.lastPointer);
|
|
380
|
+
state.nativePrimaryPress = undefined;
|
|
381
|
+
setKeyboardTarget(state, undefined);
|
|
382
|
+
requestRender(renderer);
|
|
383
|
+
}
|
|
384
|
+
states.clear();
|
|
385
|
+
removeFullscreenLayoutCapability();
|
|
386
|
+
Reflect.set(prototype, "handleViewportInput", original);
|
|
387
|
+
Reflect.set(prototype, INSTALLATION_KEY, undefined);
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
Reflect.set(prototype, "handleViewportInput", patched);
|
|
391
|
+
Reflect.set(prototype, INSTALLATION_KEY, installation);
|
|
392
|
+
|
|
393
|
+
let disposed = false;
|
|
394
|
+
return finish(() => {
|
|
395
|
+
if (disposed) return;
|
|
396
|
+
disposed = true;
|
|
397
|
+
installation.refs -= 1;
|
|
398
|
+
if (installation.refs === 0) installation.dispose();
|
|
399
|
+
});
|
|
400
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { MouseRect, TuiMouseEvent, TuiMouseEventType } from "../mouse.ts";
|
|
2
|
+
|
|
3
|
+
export interface ParsedMouseEvent {
|
|
4
|
+
type: "press" | "release" | "drag" | "wheel" | "move";
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
rawButton: number;
|
|
8
|
+
wheel: -1 | 1 | undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type ParsedMouseInput =
|
|
12
|
+
| { kind: "event"; event: ParsedMouseEvent }
|
|
13
|
+
| { kind: "unsupported" }
|
|
14
|
+
| { kind: "not-mouse" };
|
|
15
|
+
|
|
16
|
+
function modifiers(rawButton: number): Pick<TuiMouseEvent, "shift" | "alt" | "ctrl"> {
|
|
17
|
+
return {
|
|
18
|
+
shift: (rawButton & 4) !== 0,
|
|
19
|
+
alt: (rawButton & 8) !== 0,
|
|
20
|
+
ctrl: (rawButton & 16) !== 0,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createEvent(type: TuiMouseEventType, parsed: ParsedMouseEvent, rect: MouseRect): TuiMouseEvent {
|
|
25
|
+
const buttonBits = parsed.rawButton & 3;
|
|
26
|
+
return {
|
|
27
|
+
type,
|
|
28
|
+
row: parsed.y - rect.y,
|
|
29
|
+
col: parsed.x - rect.x,
|
|
30
|
+
screenRow: parsed.y,
|
|
31
|
+
screenCol: parsed.x,
|
|
32
|
+
button: parsed.wheel === undefined && buttonBits !== 3 ? (buttonBits as 0 | 1 | 2) : undefined,
|
|
33
|
+
wheel: parsed.wheel,
|
|
34
|
+
...modifiers(parsed.rawButton),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parseMouse(data: string): ParsedMouseInput {
|
|
39
|
+
const sgr = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/.exec(data);
|
|
40
|
+
if (sgr) {
|
|
41
|
+
const rawButton = Number.parseInt(sgr[1]!, 10);
|
|
42
|
+
const x = Number.parseInt(sgr[2]!, 10) - 1;
|
|
43
|
+
const y = Number.parseInt(sgr[3]!, 10) - 1;
|
|
44
|
+
if ((rawButton & 64) !== 0) {
|
|
45
|
+
const direction = rawButton & 3;
|
|
46
|
+
if (direction !== 0 && direction !== 1) return { kind: "unsupported" };
|
|
47
|
+
return { kind: "event", event: { type: "wheel", x, y, rawButton, wheel: direction === 0 ? -1 : 1 } };
|
|
48
|
+
}
|
|
49
|
+
const motion = (rawButton & 32) !== 0;
|
|
50
|
+
const noButton = (rawButton & 3) === 3;
|
|
51
|
+
return {
|
|
52
|
+
kind: "event",
|
|
53
|
+
event: {
|
|
54
|
+
type: sgr[4] === "m" ? "release" : motion ? (noButton ? "move" : "drag") : "press",
|
|
55
|
+
x,
|
|
56
|
+
y,
|
|
57
|
+
rawButton,
|
|
58
|
+
wheel: undefined,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (data.length === 6 && data.startsWith("\x1b[M")) {
|
|
64
|
+
const rawButton = data.charCodeAt(3) - 32;
|
|
65
|
+
if ((rawButton & 64) === 0) return { kind: "unsupported" };
|
|
66
|
+
const direction = rawButton & 3;
|
|
67
|
+
if (direction !== 0 && direction !== 1) return { kind: "unsupported" };
|
|
68
|
+
return {
|
|
69
|
+
kind: "event",
|
|
70
|
+
event: {
|
|
71
|
+
type: "wheel",
|
|
72
|
+
x: data.charCodeAt(4) - 33,
|
|
73
|
+
y: data.charCodeAt(5) - 33,
|
|
74
|
+
rawButton,
|
|
75
|
+
wheel: direction === 0 ? -1 : 1,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return { kind: "not-mouse" };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function isPrimarySelectionRelease(event: ParsedMouseEvent): boolean {
|
|
83
|
+
const button = event.rawButton & 3;
|
|
84
|
+
return event.type === "release" && (button === 0 || button === 3);
|
|
85
|
+
}
|