@aurigax-ai/ghostty-web 0.0.0-stage → 0.1.0
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/GHOSTTY_COMMIT +1 -0
- package/LICENSE +22 -0
- package/README.md +90 -2
- package/ghostty-vt.wasm +0 -0
- package/lib/abi.ts +227 -0
- package/lib/addons/fit.ts +242 -0
- package/lib/buffer.ts +402 -0
- package/lib/event-emitter.ts +27 -0
- package/lib/ghostty.ts +1135 -0
- package/lib/index.ts +110 -0
- package/lib/input-handler.ts +1104 -0
- package/lib/interfaces.ts +218 -0
- package/lib/link-detector.ts +215 -0
- package/lib/marker.ts +55 -0
- package/lib/providers/osc8-link-provider.ts +144 -0
- package/lib/providers/url-regex-provider.ts +150 -0
- package/lib/renderer.ts +958 -0
- package/lib/selection-manager.ts +1052 -0
- package/lib/terminal.ts +2026 -0
- package/lib/types.ts +648 -0
- package/package.json +70 -4
|
@@ -0,0 +1,1104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InputHandler - Converts browser keyboard events to terminal input
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Keyboard event listening on a container element
|
|
6
|
+
* - Mapping KeyboardEvent.code to USB HID Key codes
|
|
7
|
+
* - Extracting modifier keys (Ctrl, Alt, Shift, Meta)
|
|
8
|
+
* - Encoding keys using Ghostty's KeyEncoder
|
|
9
|
+
* - Emitting data for Terminal to send to PTY
|
|
10
|
+
*
|
|
11
|
+
* Limitations:
|
|
12
|
+
* - Does not handle IME/composition events (CJK input) - to be added later
|
|
13
|
+
* - Captures all keyboard input (preventDefault on everything)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Ghostty } from './ghostty';
|
|
17
|
+
import type { KeyEncoder } from './ghostty';
|
|
18
|
+
import type { IKeyEvent } from './interfaces';
|
|
19
|
+
import { Key, KeyAction, KeyEncoderOption, Mods } from './types';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Map KeyboardEvent.code values to USB HID Key enum values
|
|
23
|
+
* Based on: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code
|
|
24
|
+
*/
|
|
25
|
+
const KEY_MAP: Record<string, Key> = {
|
|
26
|
+
// Letters
|
|
27
|
+
KeyA: Key.A,
|
|
28
|
+
KeyB: Key.B,
|
|
29
|
+
KeyC: Key.C,
|
|
30
|
+
KeyD: Key.D,
|
|
31
|
+
KeyE: Key.E,
|
|
32
|
+
KeyF: Key.F,
|
|
33
|
+
KeyG: Key.G,
|
|
34
|
+
KeyH: Key.H,
|
|
35
|
+
KeyI: Key.I,
|
|
36
|
+
KeyJ: Key.J,
|
|
37
|
+
KeyK: Key.K,
|
|
38
|
+
KeyL: Key.L,
|
|
39
|
+
KeyM: Key.M,
|
|
40
|
+
KeyN: Key.N,
|
|
41
|
+
KeyO: Key.O,
|
|
42
|
+
KeyP: Key.P,
|
|
43
|
+
KeyQ: Key.Q,
|
|
44
|
+
KeyR: Key.R,
|
|
45
|
+
KeyS: Key.S,
|
|
46
|
+
KeyT: Key.T,
|
|
47
|
+
KeyU: Key.U,
|
|
48
|
+
KeyV: Key.V,
|
|
49
|
+
KeyW: Key.W,
|
|
50
|
+
KeyX: Key.X,
|
|
51
|
+
KeyY: Key.Y,
|
|
52
|
+
KeyZ: Key.Z,
|
|
53
|
+
|
|
54
|
+
// Numbers
|
|
55
|
+
Digit1: Key.ONE,
|
|
56
|
+
Digit2: Key.TWO,
|
|
57
|
+
Digit3: Key.THREE,
|
|
58
|
+
Digit4: Key.FOUR,
|
|
59
|
+
Digit5: Key.FIVE,
|
|
60
|
+
Digit6: Key.SIX,
|
|
61
|
+
Digit7: Key.SEVEN,
|
|
62
|
+
Digit8: Key.EIGHT,
|
|
63
|
+
Digit9: Key.NINE,
|
|
64
|
+
Digit0: Key.ZERO,
|
|
65
|
+
|
|
66
|
+
// Special keys
|
|
67
|
+
Enter: Key.ENTER,
|
|
68
|
+
Escape: Key.ESCAPE,
|
|
69
|
+
Backspace: Key.BACKSPACE,
|
|
70
|
+
Tab: Key.TAB,
|
|
71
|
+
Space: Key.SPACE,
|
|
72
|
+
|
|
73
|
+
// Punctuation
|
|
74
|
+
Minus: Key.MINUS,
|
|
75
|
+
Equal: Key.EQUAL,
|
|
76
|
+
BracketLeft: Key.BRACKET_LEFT,
|
|
77
|
+
BracketRight: Key.BRACKET_RIGHT,
|
|
78
|
+
Backslash: Key.BACKSLASH,
|
|
79
|
+
Semicolon: Key.SEMICOLON,
|
|
80
|
+
Quote: Key.QUOTE,
|
|
81
|
+
Backquote: Key.GRAVE,
|
|
82
|
+
Comma: Key.COMMA,
|
|
83
|
+
Period: Key.PERIOD,
|
|
84
|
+
Slash: Key.SLASH,
|
|
85
|
+
|
|
86
|
+
// Function keys
|
|
87
|
+
CapsLock: Key.CAPS_LOCK,
|
|
88
|
+
F1: Key.F1,
|
|
89
|
+
F2: Key.F2,
|
|
90
|
+
F3: Key.F3,
|
|
91
|
+
F4: Key.F4,
|
|
92
|
+
F5: Key.F5,
|
|
93
|
+
F6: Key.F6,
|
|
94
|
+
F7: Key.F7,
|
|
95
|
+
F8: Key.F8,
|
|
96
|
+
F9: Key.F9,
|
|
97
|
+
F10: Key.F10,
|
|
98
|
+
F11: Key.F11,
|
|
99
|
+
F12: Key.F12,
|
|
100
|
+
|
|
101
|
+
// Special function keys
|
|
102
|
+
PrintScreen: Key.PRINT_SCREEN,
|
|
103
|
+
ScrollLock: Key.SCROLL_LOCK,
|
|
104
|
+
Pause: Key.PAUSE,
|
|
105
|
+
Insert: Key.INSERT,
|
|
106
|
+
Home: Key.HOME,
|
|
107
|
+
PageUp: Key.PAGE_UP,
|
|
108
|
+
Delete: Key.DELETE,
|
|
109
|
+
End: Key.END,
|
|
110
|
+
PageDown: Key.PAGE_DOWN,
|
|
111
|
+
|
|
112
|
+
// Arrow keys
|
|
113
|
+
ArrowRight: Key.RIGHT,
|
|
114
|
+
ArrowLeft: Key.LEFT,
|
|
115
|
+
ArrowDown: Key.DOWN,
|
|
116
|
+
ArrowUp: Key.UP,
|
|
117
|
+
|
|
118
|
+
// Keypad
|
|
119
|
+
NumLock: Key.NUM_LOCK,
|
|
120
|
+
NumpadDivide: Key.KP_DIVIDE,
|
|
121
|
+
NumpadMultiply: Key.KP_MULTIPLY,
|
|
122
|
+
NumpadSubtract: Key.KP_MINUS,
|
|
123
|
+
NumpadAdd: Key.KP_PLUS,
|
|
124
|
+
NumpadEnter: Key.KP_ENTER,
|
|
125
|
+
Numpad1: Key.KP_1,
|
|
126
|
+
Numpad2: Key.KP_2,
|
|
127
|
+
Numpad3: Key.KP_3,
|
|
128
|
+
Numpad4: Key.KP_4,
|
|
129
|
+
Numpad5: Key.KP_5,
|
|
130
|
+
Numpad6: Key.KP_6,
|
|
131
|
+
Numpad7: Key.KP_7,
|
|
132
|
+
Numpad8: Key.KP_8,
|
|
133
|
+
Numpad9: Key.KP_9,
|
|
134
|
+
Numpad0: Key.KP_0,
|
|
135
|
+
NumpadDecimal: Key.KP_PERIOD,
|
|
136
|
+
|
|
137
|
+
// International
|
|
138
|
+
IntlBackslash: Key.INTL_BACKSLASH,
|
|
139
|
+
ContextMenu: Key.CONTEXT_MENU,
|
|
140
|
+
|
|
141
|
+
// Additional function keys
|
|
142
|
+
F13: Key.F13,
|
|
143
|
+
F14: Key.F14,
|
|
144
|
+
F15: Key.F15,
|
|
145
|
+
F16: Key.F16,
|
|
146
|
+
F17: Key.F17,
|
|
147
|
+
F18: Key.F18,
|
|
148
|
+
F19: Key.F19,
|
|
149
|
+
F20: Key.F20,
|
|
150
|
+
F21: Key.F21,
|
|
151
|
+
F22: Key.F22,
|
|
152
|
+
F23: Key.F23,
|
|
153
|
+
F24: Key.F24,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* InputHandler class
|
|
158
|
+
* Attaches keyboard event listeners to a container and converts
|
|
159
|
+
* keyboard events to terminal input data
|
|
160
|
+
*/
|
|
161
|
+
/**
|
|
162
|
+
* Mouse tracking configuration
|
|
163
|
+
*/
|
|
164
|
+
export interface MouseTrackingConfig {
|
|
165
|
+
/** Check if any mouse tracking mode is enabled */
|
|
166
|
+
hasMouseTracking: () => boolean;
|
|
167
|
+
/** Check if SGR extended mouse mode is enabled (mode 1006) */
|
|
168
|
+
hasSgrMouseMode: () => boolean;
|
|
169
|
+
/** Get cell dimensions for pixel to cell conversion */
|
|
170
|
+
getCellDimensions: () => { width: number; height: number };
|
|
171
|
+
/** Get canvas/container offset for accurate position calculation */
|
|
172
|
+
getCanvasOffset: () => { left: number; top: number };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export class InputHandler {
|
|
176
|
+
private encoder: KeyEncoder;
|
|
177
|
+
private container: HTMLElement;
|
|
178
|
+
private inputElement?: HTMLElement;
|
|
179
|
+
private onDataCallback: (data: string) => void;
|
|
180
|
+
private onKeyCallback?: (keyEvent: IKeyEvent) => void;
|
|
181
|
+
private customKeyEventHandler?: (event: KeyboardEvent) => boolean;
|
|
182
|
+
private getModeCallback?: (mode: number) => boolean;
|
|
183
|
+
private onCopyCallback?: () => boolean;
|
|
184
|
+
private mouseConfig?: MouseTrackingConfig;
|
|
185
|
+
private keydownListener: ((e: KeyboardEvent) => void) | null = null;
|
|
186
|
+
private keypressListener: ((e: KeyboardEvent) => void) | null = null;
|
|
187
|
+
private pasteListener: ((e: ClipboardEvent) => void) | null = null;
|
|
188
|
+
private beforeInputListener: ((e: InputEvent) => void) | null = null;
|
|
189
|
+
private compositionStartListener: ((e: CompositionEvent) => void) | null = null;
|
|
190
|
+
private compositionUpdateListener: ((e: CompositionEvent) => void) | null = null;
|
|
191
|
+
private compositionEndListener: ((e: CompositionEvent) => void) | null = null;
|
|
192
|
+
private mousedownListener: ((e: MouseEvent) => void) | null = null;
|
|
193
|
+
private mouseupListener: ((e: MouseEvent) => void) | null = null;
|
|
194
|
+
private mousemoveListener: ((e: MouseEvent) => void) | null = null;
|
|
195
|
+
private wheelListener: ((e: WheelEvent) => void) | null = null;
|
|
196
|
+
private isComposing = false;
|
|
197
|
+
private isDisposed = false;
|
|
198
|
+
private mouseButtonsPressed = 0; // Track which buttons are pressed for motion reporting
|
|
199
|
+
private lastKeyDownData: string | null = null;
|
|
200
|
+
private lastKeyDownTime = 0;
|
|
201
|
+
private lastPasteData: string | null = null;
|
|
202
|
+
private lastPasteTime = 0;
|
|
203
|
+
private lastPasteSource: 'paste' | 'beforeinput' | null = null;
|
|
204
|
+
private lastCompositionData: string | null = null;
|
|
205
|
+
private lastCompositionTime = 0;
|
|
206
|
+
private lastBeforeInputData: string | null = null;
|
|
207
|
+
private lastBeforeInputTime = 0;
|
|
208
|
+
private static readonly BEFORE_INPUT_IGNORE_MS = 100;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Create a new InputHandler
|
|
212
|
+
* @param ghostty - Ghostty instance (for creating KeyEncoder)
|
|
213
|
+
* @param container - DOM element to attach listeners to
|
|
214
|
+
* @param onData - Callback for terminal data (escape sequences to send to PTY)
|
|
215
|
+
* @param onBell - Callback for bell/beep event
|
|
216
|
+
* @param onKey - Optional callback for raw key events
|
|
217
|
+
* @param customKeyEventHandler - Optional custom key event handler
|
|
218
|
+
* @param getMode - Optional callback to query terminal mode state (for application cursor mode)
|
|
219
|
+
* @param onCopy - Optional callback to handle copy (Cmd+C/Ctrl+C with selection)
|
|
220
|
+
* @param inputElement - Optional input element for beforeinput events
|
|
221
|
+
* @param mouseConfig - Optional mouse tracking configuration
|
|
222
|
+
*/
|
|
223
|
+
constructor(
|
|
224
|
+
ghostty: Ghostty,
|
|
225
|
+
container: HTMLElement,
|
|
226
|
+
onData: (data: string) => void,
|
|
227
|
+
_onBell: () => void,
|
|
228
|
+
onKey?: (keyEvent: IKeyEvent) => void,
|
|
229
|
+
customKeyEventHandler?: (event: KeyboardEvent) => boolean,
|
|
230
|
+
getMode?: (mode: number) => boolean,
|
|
231
|
+
onCopy?: () => boolean,
|
|
232
|
+
inputElement?: HTMLElement,
|
|
233
|
+
mouseConfig?: MouseTrackingConfig
|
|
234
|
+
) {
|
|
235
|
+
this.encoder = ghostty.createKeyEncoder();
|
|
236
|
+
this.container = container;
|
|
237
|
+
this.inputElement = inputElement;
|
|
238
|
+
this.onDataCallback = onData;
|
|
239
|
+
this.onKeyCallback = onKey;
|
|
240
|
+
this.customKeyEventHandler = customKeyEventHandler;
|
|
241
|
+
this.getModeCallback = getMode;
|
|
242
|
+
this.onCopyCallback = onCopy;
|
|
243
|
+
this.mouseConfig = mouseConfig;
|
|
244
|
+
|
|
245
|
+
// Attach event listeners
|
|
246
|
+
this.attach();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Set custom key event handler (for runtime updates)
|
|
251
|
+
*/
|
|
252
|
+
setCustomKeyEventHandler(handler: (event: KeyboardEvent) => boolean): void {
|
|
253
|
+
this.customKeyEventHandler = handler;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Attach keyboard event listeners to container
|
|
258
|
+
*/
|
|
259
|
+
private attach(): void {
|
|
260
|
+
// Make container focusable so it can receive keyboard events (browser only)
|
|
261
|
+
if (
|
|
262
|
+
typeof this.container.hasAttribute === 'function' &&
|
|
263
|
+
typeof this.container.setAttribute === 'function'
|
|
264
|
+
) {
|
|
265
|
+
if (!this.container.hasAttribute('tabindex')) {
|
|
266
|
+
this.container.setAttribute('tabindex', '0');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Add visual focus indication (only if style exists - for browser environments)
|
|
270
|
+
if (this.container.style) {
|
|
271
|
+
this.container.style.outline = 'none'; // Remove default outline
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
this.keydownListener = this.handleKeyDown.bind(this);
|
|
276
|
+
this.container.addEventListener('keydown', this.keydownListener);
|
|
277
|
+
|
|
278
|
+
this.pasteListener = this.handlePaste.bind(this);
|
|
279
|
+
this.container.addEventListener('paste', this.pasteListener);
|
|
280
|
+
if (this.inputElement && this.inputElement !== this.container) {
|
|
281
|
+
this.inputElement.addEventListener('paste', this.pasteListener);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (this.inputElement) {
|
|
285
|
+
this.beforeInputListener = this.handleBeforeInput.bind(this);
|
|
286
|
+
this.inputElement.addEventListener('beforeinput', this.beforeInputListener);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
this.compositionStartListener = this.handleCompositionStart.bind(this);
|
|
290
|
+
this.container.addEventListener('compositionstart', this.compositionStartListener);
|
|
291
|
+
|
|
292
|
+
this.compositionUpdateListener = this.handleCompositionUpdate.bind(this);
|
|
293
|
+
this.container.addEventListener('compositionupdate', this.compositionUpdateListener);
|
|
294
|
+
|
|
295
|
+
this.compositionEndListener = this.handleCompositionEnd.bind(this);
|
|
296
|
+
this.container.addEventListener('compositionend', this.compositionEndListener);
|
|
297
|
+
|
|
298
|
+
// Mouse event listeners (for terminal mouse tracking)
|
|
299
|
+
this.mousedownListener = this.handleMouseDown.bind(this);
|
|
300
|
+
this.container.addEventListener('mousedown', this.mousedownListener);
|
|
301
|
+
|
|
302
|
+
this.mouseupListener = this.handleMouseUp.bind(this);
|
|
303
|
+
this.container.addEventListener('mouseup', this.mouseupListener);
|
|
304
|
+
|
|
305
|
+
this.mousemoveListener = this.handleMouseMove.bind(this);
|
|
306
|
+
this.container.addEventListener('mousemove', this.mousemoveListener);
|
|
307
|
+
|
|
308
|
+
this.wheelListener = this.handleWheel.bind(this);
|
|
309
|
+
this.container.addEventListener('wheel', this.wheelListener, { passive: false });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Map KeyboardEvent.code to USB HID Key enum value
|
|
314
|
+
* @param code - KeyboardEvent.code value
|
|
315
|
+
* @returns Key enum value or null if unmapped
|
|
316
|
+
*/
|
|
317
|
+
private mapKeyCode(code: string): Key | null {
|
|
318
|
+
return KEY_MAP[code] ?? null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Extract modifier flags from KeyboardEvent
|
|
323
|
+
* @param event - KeyboardEvent
|
|
324
|
+
* @returns Mods flags
|
|
325
|
+
*/
|
|
326
|
+
private extractModifiers(event: KeyboardEvent): Mods {
|
|
327
|
+
let mods = Mods.NONE;
|
|
328
|
+
|
|
329
|
+
if (event.shiftKey) mods |= Mods.SHIFT;
|
|
330
|
+
if (event.ctrlKey) mods |= Mods.CTRL;
|
|
331
|
+
if (event.altKey) mods |= Mods.ALT;
|
|
332
|
+
if (event.metaKey) mods |= Mods.SUPER;
|
|
333
|
+
|
|
334
|
+
// Note: CapsLock and NumLock are not in KeyboardEvent modifiers
|
|
335
|
+
// They would need to be tracked separately if needed
|
|
336
|
+
// For now, we don't set CAPSLOCK or NUMLOCK flags
|
|
337
|
+
|
|
338
|
+
return mods;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Check if this is a printable character with no special modifiers
|
|
343
|
+
* @param event - KeyboardEvent
|
|
344
|
+
* @returns true if printable character
|
|
345
|
+
*/
|
|
346
|
+
private isPrintableCharacter(event: KeyboardEvent): boolean {
|
|
347
|
+
// If Ctrl, Alt, or Meta (Cmd on Mac) is pressed, it's not a simple printable character
|
|
348
|
+
// Exception: AltGr (Ctrl+Alt on some keyboards) can produce printable characters
|
|
349
|
+
if (event.ctrlKey && !event.altKey) return false;
|
|
350
|
+
if (event.altKey && !event.ctrlKey) return false;
|
|
351
|
+
if (event.metaKey) return false; // Cmd key on Mac
|
|
352
|
+
|
|
353
|
+
// If key produces a single printable character
|
|
354
|
+
return event.key.length === 1;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Handle keydown event
|
|
359
|
+
* @param event - KeyboardEvent
|
|
360
|
+
*/
|
|
361
|
+
private handleKeyDown(event: KeyboardEvent): void {
|
|
362
|
+
if (this.isDisposed) return;
|
|
363
|
+
|
|
364
|
+
// Ignore keydown events during composition
|
|
365
|
+
// Note: Some browsers send keyCode 229 for all keys during composition
|
|
366
|
+
if (this.isComposing || event.isComposing || event.keyCode === 229) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Emit onKey event first (before any processing)
|
|
371
|
+
if (this.onKeyCallback) {
|
|
372
|
+
this.onKeyCallback({ key: event.key, domEvent: event });
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Check custom key event handler
|
|
376
|
+
if (this.customKeyEventHandler) {
|
|
377
|
+
const handled = this.customKeyEventHandler(event);
|
|
378
|
+
if (handled) {
|
|
379
|
+
// Custom handler consumed the event
|
|
380
|
+
event.preventDefault();
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Allow Ctrl+V and Cmd+V to trigger paste event (don't preventDefault)
|
|
386
|
+
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyV') {
|
|
387
|
+
// Let the browser's native paste event fire
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Handle Cmd+C for copy (on Mac, Cmd+C should copy, not send interrupt)
|
|
392
|
+
// Note: Ctrl+C on all platforms sends interrupt signal (0x03)
|
|
393
|
+
if (event.metaKey && event.code === 'KeyC') {
|
|
394
|
+
// Try to copy selection via callback
|
|
395
|
+
// If there's a selection and copy succeeds, prevent default
|
|
396
|
+
// If no selection, let it fall through (browser may have other text selected)
|
|
397
|
+
if (this.onCopyCallback && this.onCopyCallback()) {
|
|
398
|
+
event.preventDefault();
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// For printable characters without modifiers, send the character directly
|
|
404
|
+
// This handles: a-z, A-Z (with shift), 0-9, punctuation, etc.
|
|
405
|
+
if (this.isPrintableCharacter(event)) {
|
|
406
|
+
event.preventDefault();
|
|
407
|
+
this.onDataCallback(event.key);
|
|
408
|
+
this.recordKeyDownData(event.key);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Map the physical key code
|
|
413
|
+
const key = this.mapKeyCode(event.code);
|
|
414
|
+
if (key === null) {
|
|
415
|
+
// Unknown key - ignore it
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Extract modifiers
|
|
420
|
+
const mods = this.extractModifiers(event);
|
|
421
|
+
|
|
422
|
+
// Handle simple special keys that produce standard sequences
|
|
423
|
+
if (mods === Mods.NONE || mods === Mods.SHIFT) {
|
|
424
|
+
let simpleOutput: string | null = null;
|
|
425
|
+
|
|
426
|
+
switch (key) {
|
|
427
|
+
case Key.ENTER:
|
|
428
|
+
simpleOutput = '\r'; // Carriage return
|
|
429
|
+
break;
|
|
430
|
+
case Key.TAB:
|
|
431
|
+
if (mods === Mods.SHIFT) {
|
|
432
|
+
simpleOutput = '\x1b[Z'; // Backtab
|
|
433
|
+
} else {
|
|
434
|
+
simpleOutput = '\t'; // Tab
|
|
435
|
+
}
|
|
436
|
+
break;
|
|
437
|
+
case Key.BACKSPACE:
|
|
438
|
+
simpleOutput = '\x7F'; // DEL (most terminals use 0x7F for backspace)
|
|
439
|
+
break;
|
|
440
|
+
case Key.ESCAPE:
|
|
441
|
+
simpleOutput = '\x1B'; // ESC
|
|
442
|
+
break;
|
|
443
|
+
// Arrow keys are handled by the encoder (respects application cursor mode)
|
|
444
|
+
// Navigation keys
|
|
445
|
+
case Key.HOME:
|
|
446
|
+
simpleOutput = '\x1B[H';
|
|
447
|
+
break;
|
|
448
|
+
case Key.END:
|
|
449
|
+
simpleOutput = '\x1B[F';
|
|
450
|
+
break;
|
|
451
|
+
case Key.INSERT:
|
|
452
|
+
simpleOutput = '\x1B[2~';
|
|
453
|
+
break;
|
|
454
|
+
case Key.DELETE:
|
|
455
|
+
simpleOutput = '\x1B[3~';
|
|
456
|
+
break;
|
|
457
|
+
case Key.PAGE_UP:
|
|
458
|
+
simpleOutput = '\x1B[5~';
|
|
459
|
+
break;
|
|
460
|
+
case Key.PAGE_DOWN:
|
|
461
|
+
simpleOutput = '\x1B[6~';
|
|
462
|
+
break;
|
|
463
|
+
// Function keys
|
|
464
|
+
case Key.F1:
|
|
465
|
+
simpleOutput = '\x1BOP';
|
|
466
|
+
break;
|
|
467
|
+
case Key.F2:
|
|
468
|
+
simpleOutput = '\x1BOQ';
|
|
469
|
+
break;
|
|
470
|
+
case Key.F3:
|
|
471
|
+
simpleOutput = '\x1BOR';
|
|
472
|
+
break;
|
|
473
|
+
case Key.F4:
|
|
474
|
+
simpleOutput = '\x1BOS';
|
|
475
|
+
break;
|
|
476
|
+
case Key.F5:
|
|
477
|
+
simpleOutput = '\x1B[15~';
|
|
478
|
+
break;
|
|
479
|
+
case Key.F6:
|
|
480
|
+
simpleOutput = '\x1B[17~';
|
|
481
|
+
break;
|
|
482
|
+
case Key.F7:
|
|
483
|
+
simpleOutput = '\x1B[18~';
|
|
484
|
+
break;
|
|
485
|
+
case Key.F8:
|
|
486
|
+
simpleOutput = '\x1B[19~';
|
|
487
|
+
break;
|
|
488
|
+
case Key.F9:
|
|
489
|
+
simpleOutput = '\x1B[20~';
|
|
490
|
+
break;
|
|
491
|
+
case Key.F10:
|
|
492
|
+
simpleOutput = '\x1B[21~';
|
|
493
|
+
break;
|
|
494
|
+
case Key.F11:
|
|
495
|
+
simpleOutput = '\x1B[23~';
|
|
496
|
+
break;
|
|
497
|
+
case Key.F12:
|
|
498
|
+
simpleOutput = '\x1B[24~';
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (simpleOutput !== null) {
|
|
503
|
+
event.preventDefault();
|
|
504
|
+
this.onDataCallback(simpleOutput);
|
|
505
|
+
this.recordKeyDownData(simpleOutput);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Determine action (we only care about PRESS for now, not RELEASE or REPEAT)
|
|
511
|
+
const action = KeyAction.PRESS;
|
|
512
|
+
|
|
513
|
+
// For non-printable keys or keys with modifiers, encode using Ghostty
|
|
514
|
+
try {
|
|
515
|
+
// Sync encoder options with terminal mode state
|
|
516
|
+
// Mode 1 (DECCKM) controls whether arrow keys send CSI or SS3 sequences
|
|
517
|
+
if (this.getModeCallback) {
|
|
518
|
+
const appCursorMode = this.getModeCallback(1);
|
|
519
|
+
this.encoder.setOption(KeyEncoderOption.CURSOR_KEY_APPLICATION, appCursorMode);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// For letter/number keys, even with modifiers, pass the base character
|
|
523
|
+
// This helps the encoder produce correct control sequences (e.g., Ctrl+A = 0x01)
|
|
524
|
+
// For special keys (Enter, Arrow keys, etc.), don't pass utf8
|
|
525
|
+
const utf8 =
|
|
526
|
+
event.key.length === 1 && event.key.charCodeAt(0) < 128
|
|
527
|
+
? event.key.toLowerCase() // Use lowercase for consistency
|
|
528
|
+
: undefined;
|
|
529
|
+
|
|
530
|
+
const encoded = this.encoder.encode({
|
|
531
|
+
action,
|
|
532
|
+
key,
|
|
533
|
+
mods,
|
|
534
|
+
utf8,
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// Convert Uint8Array to string
|
|
538
|
+
const decoder = new TextDecoder();
|
|
539
|
+
const data = decoder.decode(encoded);
|
|
540
|
+
|
|
541
|
+
// Prevent default browser behavior
|
|
542
|
+
event.preventDefault();
|
|
543
|
+
event.stopPropagation();
|
|
544
|
+
|
|
545
|
+
// Emit the data
|
|
546
|
+
if (data.length > 0) {
|
|
547
|
+
this.onDataCallback(data);
|
|
548
|
+
this.recordKeyDownData(data);
|
|
549
|
+
}
|
|
550
|
+
} catch (error) {
|
|
551
|
+
// Encoding failed - log but don't crash
|
|
552
|
+
console.warn('Failed to encode key:', event.code, error);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Handle paste event from clipboard
|
|
558
|
+
* @param event - ClipboardEvent
|
|
559
|
+
*/
|
|
560
|
+
private handlePaste(event: ClipboardEvent): void {
|
|
561
|
+
if (this.isDisposed) return;
|
|
562
|
+
|
|
563
|
+
// Prevent default paste behavior
|
|
564
|
+
event.preventDefault();
|
|
565
|
+
event.stopPropagation();
|
|
566
|
+
|
|
567
|
+
// Get clipboard data
|
|
568
|
+
const clipboardData = event.clipboardData;
|
|
569
|
+
if (!clipboardData) {
|
|
570
|
+
console.warn('No clipboard data available');
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Get text from clipboard
|
|
575
|
+
const text = clipboardData.getData('text/plain');
|
|
576
|
+
if (!text) {
|
|
577
|
+
console.warn('No text in clipboard');
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (this.shouldIgnorePasteEvent(text, 'paste')) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
this.emitPasteData(text);
|
|
586
|
+
this.recordPasteData(text, 'paste');
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Handle beforeinput event (mobile/IME input)
|
|
591
|
+
* @param event - InputEvent
|
|
592
|
+
*/
|
|
593
|
+
private handleBeforeInput(event: InputEvent): void {
|
|
594
|
+
if (this.isDisposed) return;
|
|
595
|
+
|
|
596
|
+
if (this.isComposing || event.isComposing) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const inputType = event.inputType;
|
|
601
|
+
const data = event.data ?? '';
|
|
602
|
+
let output: string | null = null;
|
|
603
|
+
|
|
604
|
+
switch (inputType) {
|
|
605
|
+
case 'insertText':
|
|
606
|
+
case 'insertReplacementText':
|
|
607
|
+
output = data.length > 0 ? data.replace(/\n/g, '\r') : null;
|
|
608
|
+
break;
|
|
609
|
+
case 'insertLineBreak':
|
|
610
|
+
case 'insertParagraph':
|
|
611
|
+
output = '\r';
|
|
612
|
+
break;
|
|
613
|
+
case 'deleteContentBackward':
|
|
614
|
+
output = '\x7F';
|
|
615
|
+
break;
|
|
616
|
+
case 'deleteContentForward':
|
|
617
|
+
output = '\x1B[3~';
|
|
618
|
+
break;
|
|
619
|
+
case 'insertFromPaste':
|
|
620
|
+
if (!data) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (this.shouldIgnorePasteEvent(data, 'beforeinput')) {
|
|
624
|
+
event.preventDefault();
|
|
625
|
+
event.stopPropagation();
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
event.preventDefault();
|
|
629
|
+
event.stopPropagation();
|
|
630
|
+
this.emitPasteData(data);
|
|
631
|
+
this.recordPasteData(data, 'beforeinput');
|
|
632
|
+
return;
|
|
633
|
+
default:
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (!output) {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (this.shouldIgnoreBeforeInput(output)) {
|
|
642
|
+
event.preventDefault();
|
|
643
|
+
event.stopPropagation();
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (data && this.shouldIgnoreBeforeInputFromComposition(data)) {
|
|
648
|
+
event.preventDefault();
|
|
649
|
+
event.stopPropagation();
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
event.preventDefault();
|
|
654
|
+
event.stopPropagation();
|
|
655
|
+
this.onDataCallback(output);
|
|
656
|
+
if (data) {
|
|
657
|
+
this.recordBeforeInputData(data);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Handle compositionstart event
|
|
663
|
+
*/
|
|
664
|
+
private handleCompositionStart(_event: CompositionEvent): void {
|
|
665
|
+
if (this.isDisposed) return;
|
|
666
|
+
this.isComposing = true;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Handle compositionupdate event
|
|
671
|
+
*/
|
|
672
|
+
private handleCompositionUpdate(_event: CompositionEvent): void {
|
|
673
|
+
if (this.isDisposed) return;
|
|
674
|
+
// We could track the current composition string here if we wanted to
|
|
675
|
+
// display it in a custom way, but for now we rely on the browser's
|
|
676
|
+
// input method editor UI.
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Handle compositionend event
|
|
681
|
+
*/
|
|
682
|
+
private handleCompositionEnd(event: CompositionEvent): void {
|
|
683
|
+
if (this.isDisposed) return;
|
|
684
|
+
this.isComposing = false;
|
|
685
|
+
|
|
686
|
+
const data = event.data;
|
|
687
|
+
if (data && data.length > 0) {
|
|
688
|
+
if (this.shouldIgnoreCompositionEnd(data)) {
|
|
689
|
+
this.cleanupCompositionTextNodes();
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
this.onDataCallback(data);
|
|
693
|
+
this.recordCompositionData(data);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
this.cleanupCompositionTextNodes();
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Cleanup text nodes in container after composition
|
|
701
|
+
*/
|
|
702
|
+
private cleanupCompositionTextNodes(): void {
|
|
703
|
+
// Cleanup text nodes in container (fix for duplicate text display)
|
|
704
|
+
// When the container is contenteditable, the browser might insert text nodes
|
|
705
|
+
// upon composition end. We need to remove them to prevent duplicate display.
|
|
706
|
+
if (this.container && this.container.childNodes) {
|
|
707
|
+
for (let i = this.container.childNodes.length - 1; i >= 0; i--) {
|
|
708
|
+
const node = this.container.childNodes[i];
|
|
709
|
+
// Node.TEXT_NODE === 3
|
|
710
|
+
if (node.nodeType === 3) {
|
|
711
|
+
this.container.removeChild(node);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// ==========================================================================
|
|
718
|
+
// Mouse Event Handling (for terminal mouse tracking)
|
|
719
|
+
// ==========================================================================
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Convert pixel coordinates to terminal cell coordinates
|
|
723
|
+
*/
|
|
724
|
+
private pixelToCell(event: MouseEvent): { col: number; row: number } | null {
|
|
725
|
+
if (!this.mouseConfig) return null;
|
|
726
|
+
|
|
727
|
+
const dims = this.mouseConfig.getCellDimensions();
|
|
728
|
+
const offset = this.mouseConfig.getCanvasOffset();
|
|
729
|
+
|
|
730
|
+
if (dims.width <= 0 || dims.height <= 0) return null;
|
|
731
|
+
|
|
732
|
+
const x = event.clientX - offset.left;
|
|
733
|
+
const y = event.clientY - offset.top;
|
|
734
|
+
|
|
735
|
+
// Convert to 1-based cell coordinates (terminal uses 1-based)
|
|
736
|
+
const col = Math.floor(x / dims.width) + 1;
|
|
737
|
+
const row = Math.floor(y / dims.height) + 1;
|
|
738
|
+
|
|
739
|
+
// Clamp to valid range (at least 1)
|
|
740
|
+
return {
|
|
741
|
+
col: Math.max(1, col),
|
|
742
|
+
row: Math.max(1, row),
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Get modifier flags for mouse event
|
|
748
|
+
*/
|
|
749
|
+
private getMouseModifiers(event: MouseEvent): number {
|
|
750
|
+
let mods = 0;
|
|
751
|
+
if (event.shiftKey) mods |= 4;
|
|
752
|
+
if (event.metaKey) mods |= 8; // Meta (Cmd on Mac)
|
|
753
|
+
if (event.ctrlKey) mods |= 16;
|
|
754
|
+
return mods;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Encode mouse event as SGR sequence
|
|
759
|
+
* SGR format: \x1b[<Btn;Col;RowM (press/motion) or \x1b[<Btn;Col;Rowm (release)
|
|
760
|
+
*/
|
|
761
|
+
private encodeMouseSGR(
|
|
762
|
+
button: number,
|
|
763
|
+
col: number,
|
|
764
|
+
row: number,
|
|
765
|
+
isRelease: boolean,
|
|
766
|
+
modifiers: number
|
|
767
|
+
): string {
|
|
768
|
+
const btn = button + modifiers;
|
|
769
|
+
const suffix = isRelease ? 'm' : 'M';
|
|
770
|
+
return `\x1b[<${btn};${col};${row}${suffix}`;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Encode mouse event as X10/normal sequence (legacy format)
|
|
775
|
+
* Format: \x1b[M<Btn+32><Col+32><Row+32>
|
|
776
|
+
*/
|
|
777
|
+
private encodeMouseX10(button: number, col: number, row: number, modifiers: number): string {
|
|
778
|
+
// X10 format adds 32 to all values and encodes as characters
|
|
779
|
+
// Button encoding: 0=left, 1=middle, 2=right, 3=release
|
|
780
|
+
const btn = button + modifiers + 32;
|
|
781
|
+
const colChar = String.fromCharCode(Math.min(col + 32, 255));
|
|
782
|
+
const rowChar = String.fromCharCode(Math.min(row + 32, 255));
|
|
783
|
+
return `\x1b[M${String.fromCharCode(btn)}${colChar}${rowChar}`;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Send mouse event to terminal
|
|
788
|
+
*/
|
|
789
|
+
private sendMouseEvent(
|
|
790
|
+
button: number,
|
|
791
|
+
col: number,
|
|
792
|
+
row: number,
|
|
793
|
+
isRelease: boolean,
|
|
794
|
+
event: MouseEvent
|
|
795
|
+
): void {
|
|
796
|
+
const modifiers = this.getMouseModifiers(event);
|
|
797
|
+
|
|
798
|
+
// Check if SGR extended mode is enabled (mode 1006)
|
|
799
|
+
const useSGR = this.mouseConfig?.hasSgrMouseMode?.() ?? true;
|
|
800
|
+
|
|
801
|
+
let sequence: string;
|
|
802
|
+
if (useSGR) {
|
|
803
|
+
sequence = this.encodeMouseSGR(button, col, row, isRelease, modifiers);
|
|
804
|
+
} else {
|
|
805
|
+
// X10/normal mode doesn't support release events directly
|
|
806
|
+
// Button 3 means release in X10 mode
|
|
807
|
+
const x10Button = isRelease ? 3 : button;
|
|
808
|
+
sequence = this.encodeMouseX10(x10Button, col, row, modifiers);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
this.onDataCallback(sequence);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Handle mousedown event
|
|
816
|
+
*/
|
|
817
|
+
private handleMouseDown(event: MouseEvent): void {
|
|
818
|
+
if (this.isDisposed) return;
|
|
819
|
+
if (!this.mouseConfig?.hasMouseTracking()) return;
|
|
820
|
+
|
|
821
|
+
const cell = this.pixelToCell(event);
|
|
822
|
+
if (!cell) return;
|
|
823
|
+
|
|
824
|
+
// Map browser button to terminal button
|
|
825
|
+
// event.button: 0=left, 1=middle, 2=right
|
|
826
|
+
// Terminal: 0=left, 1=middle, 2=right
|
|
827
|
+
const button = event.button;
|
|
828
|
+
|
|
829
|
+
// Track pressed buttons for motion events
|
|
830
|
+
this.mouseButtonsPressed |= 1 << button;
|
|
831
|
+
|
|
832
|
+
this.sendMouseEvent(button, cell.col, cell.row, false, event);
|
|
833
|
+
|
|
834
|
+
// Don't prevent default - let SelectionManager handle selection
|
|
835
|
+
// Only prevent if we actually handled the event
|
|
836
|
+
// event.preventDefault();
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Handle mouseup event
|
|
841
|
+
*/
|
|
842
|
+
private handleMouseUp(event: MouseEvent): void {
|
|
843
|
+
if (this.isDisposed) return;
|
|
844
|
+
if (!this.mouseConfig?.hasMouseTracking()) return;
|
|
845
|
+
|
|
846
|
+
const cell = this.pixelToCell(event);
|
|
847
|
+
if (!cell) return;
|
|
848
|
+
|
|
849
|
+
const button = event.button;
|
|
850
|
+
|
|
851
|
+
// Clear pressed button
|
|
852
|
+
this.mouseButtonsPressed &= ~(1 << button);
|
|
853
|
+
|
|
854
|
+
this.sendMouseEvent(button, cell.col, cell.row, true, event);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Handle mousemove event
|
|
859
|
+
*/
|
|
860
|
+
private handleMouseMove(event: MouseEvent): void {
|
|
861
|
+
if (this.isDisposed) return;
|
|
862
|
+
if (!this.mouseConfig?.hasMouseTracking()) return;
|
|
863
|
+
|
|
864
|
+
// Check if button motion mode or any-event tracking is enabled
|
|
865
|
+
// Mode 1002 = button motion, Mode 1003 = any motion
|
|
866
|
+
const hasButtonMotion = this.getModeCallback?.(1002) ?? false;
|
|
867
|
+
const hasAnyMotion = this.getModeCallback?.(1003) ?? false;
|
|
868
|
+
|
|
869
|
+
if (!hasButtonMotion && !hasAnyMotion) return;
|
|
870
|
+
|
|
871
|
+
// In button motion mode, only report if a button is pressed
|
|
872
|
+
if (hasButtonMotion && !hasAnyMotion && this.mouseButtonsPressed === 0) return;
|
|
873
|
+
|
|
874
|
+
const cell = this.pixelToCell(event);
|
|
875
|
+
if (!cell) return;
|
|
876
|
+
|
|
877
|
+
// Determine which button to report (or 32 for motion with no button)
|
|
878
|
+
let button = 32; // Motion flag
|
|
879
|
+
if (this.mouseButtonsPressed & 1)
|
|
880
|
+
button += 0; // Left
|
|
881
|
+
else if (this.mouseButtonsPressed & 2)
|
|
882
|
+
button += 1; // Middle
|
|
883
|
+
else if (this.mouseButtonsPressed & 4) button += 2; // Right
|
|
884
|
+
|
|
885
|
+
this.sendMouseEvent(button, cell.col, cell.row, false, event);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Handle wheel event (scroll)
|
|
890
|
+
*/
|
|
891
|
+
private handleWheel(event: WheelEvent): void {
|
|
892
|
+
if (this.isDisposed) return;
|
|
893
|
+
if (!this.mouseConfig?.hasMouseTracking()) return;
|
|
894
|
+
|
|
895
|
+
const cell = this.pixelToCell(event);
|
|
896
|
+
if (!cell) return;
|
|
897
|
+
|
|
898
|
+
// Wheel events: button 64 = scroll up, button 65 = scroll down
|
|
899
|
+
const button = event.deltaY < 0 ? 64 : 65;
|
|
900
|
+
|
|
901
|
+
this.sendMouseEvent(button, cell.col, cell.row, false, event);
|
|
902
|
+
|
|
903
|
+
// Prevent default scrolling when mouse tracking is active
|
|
904
|
+
event.preventDefault();
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Emit paste data with bracketed paste support
|
|
909
|
+
*/
|
|
910
|
+
private emitPasteData(text: string): void {
|
|
911
|
+
const hasBracketedPaste = this.getModeCallback?.(2004) ?? false;
|
|
912
|
+
|
|
913
|
+
if (hasBracketedPaste) {
|
|
914
|
+
this.onDataCallback('\x1b[200~' + text + '\x1b[201~');
|
|
915
|
+
} else {
|
|
916
|
+
this.onDataCallback(text);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Record keydown data for beforeinput de-duplication
|
|
922
|
+
*/
|
|
923
|
+
private recordKeyDownData(data: string): void {
|
|
924
|
+
this.lastKeyDownData = data;
|
|
925
|
+
this.lastKeyDownTime = this.getNow();
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Record paste data for beforeinput de-duplication
|
|
930
|
+
*/
|
|
931
|
+
private recordPasteData(data: string, source: 'paste' | 'beforeinput'): void {
|
|
932
|
+
this.lastPasteData = data;
|
|
933
|
+
this.lastPasteTime = this.getNow();
|
|
934
|
+
this.lastPasteSource = source;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Check if beforeinput should be ignored due to a recent keydown
|
|
939
|
+
*/
|
|
940
|
+
private shouldIgnoreBeforeInput(data: string): boolean {
|
|
941
|
+
if (!this.lastKeyDownData) {
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
const now = this.getNow();
|
|
945
|
+
const isDuplicate =
|
|
946
|
+
now - this.lastKeyDownTime < InputHandler.BEFORE_INPUT_IGNORE_MS &&
|
|
947
|
+
this.lastKeyDownData === data;
|
|
948
|
+
this.lastKeyDownData = null;
|
|
949
|
+
return isDuplicate;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Check if beforeinput text should be ignored due to a recent composition end
|
|
954
|
+
*/
|
|
955
|
+
private shouldIgnoreBeforeInputFromComposition(data: string): boolean {
|
|
956
|
+
if (!this.lastCompositionData) {
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
const now = this.getNow();
|
|
960
|
+
const isDuplicate =
|
|
961
|
+
now - this.lastCompositionTime < InputHandler.BEFORE_INPUT_IGNORE_MS &&
|
|
962
|
+
this.lastCompositionData === data;
|
|
963
|
+
if (isDuplicate) {
|
|
964
|
+
this.lastCompositionData = null;
|
|
965
|
+
}
|
|
966
|
+
return isDuplicate;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Check if composition end should be ignored due to a recent beforeinput text
|
|
971
|
+
*/
|
|
972
|
+
private shouldIgnoreCompositionEnd(data: string): boolean {
|
|
973
|
+
if (!this.lastBeforeInputData) {
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
const now = this.getNow();
|
|
977
|
+
const isDuplicate =
|
|
978
|
+
now - this.lastBeforeInputTime < InputHandler.BEFORE_INPUT_IGNORE_MS &&
|
|
979
|
+
this.lastBeforeInputData === data;
|
|
980
|
+
if (isDuplicate) {
|
|
981
|
+
this.lastBeforeInputData = null;
|
|
982
|
+
}
|
|
983
|
+
return isDuplicate;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Record beforeinput text for composition de-duplication
|
|
988
|
+
*/
|
|
989
|
+
private recordBeforeInputData(data: string): void {
|
|
990
|
+
this.lastBeforeInputData = data;
|
|
991
|
+
this.lastBeforeInputTime = this.getNow();
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Record composition end data for beforeinput de-duplication
|
|
996
|
+
*/
|
|
997
|
+
private recordCompositionData(data: string): void {
|
|
998
|
+
this.lastCompositionData = data;
|
|
999
|
+
this.lastCompositionTime = this.getNow();
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Check if paste should be ignored due to a recent paste event from another source
|
|
1004
|
+
*/
|
|
1005
|
+
private shouldIgnorePasteEvent(data: string, source: 'paste' | 'beforeinput'): boolean {
|
|
1006
|
+
if (!this.lastPasteData) {
|
|
1007
|
+
return false;
|
|
1008
|
+
}
|
|
1009
|
+
if (this.lastPasteSource === source) {
|
|
1010
|
+
return false;
|
|
1011
|
+
}
|
|
1012
|
+
const now = this.getNow();
|
|
1013
|
+
const isDuplicate =
|
|
1014
|
+
now - this.lastPasteTime < InputHandler.BEFORE_INPUT_IGNORE_MS && this.lastPasteData === data;
|
|
1015
|
+
if (isDuplicate) {
|
|
1016
|
+
this.lastPasteData = null;
|
|
1017
|
+
this.lastPasteSource = null;
|
|
1018
|
+
}
|
|
1019
|
+
return isDuplicate;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Get current time in milliseconds
|
|
1024
|
+
*/
|
|
1025
|
+
private getNow(): number {
|
|
1026
|
+
return typeof performance !== 'undefined' && typeof performance.now === 'function'
|
|
1027
|
+
? performance.now()
|
|
1028
|
+
: Date.now();
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Dispose the InputHandler and remove event listeners
|
|
1033
|
+
*/
|
|
1034
|
+
dispose(): void {
|
|
1035
|
+
if (this.isDisposed) return;
|
|
1036
|
+
|
|
1037
|
+
if (this.keydownListener) {
|
|
1038
|
+
this.container.removeEventListener('keydown', this.keydownListener);
|
|
1039
|
+
this.keydownListener = null;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (this.keypressListener) {
|
|
1043
|
+
this.container.removeEventListener('keypress', this.keypressListener);
|
|
1044
|
+
this.keypressListener = null;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
if (this.pasteListener) {
|
|
1048
|
+
this.container.removeEventListener('paste', this.pasteListener);
|
|
1049
|
+
if (this.inputElement && this.inputElement !== this.container) {
|
|
1050
|
+
this.inputElement.removeEventListener('paste', this.pasteListener);
|
|
1051
|
+
}
|
|
1052
|
+
this.pasteListener = null;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
if (this.beforeInputListener && this.inputElement) {
|
|
1056
|
+
this.inputElement.removeEventListener('beforeinput', this.beforeInputListener);
|
|
1057
|
+
this.beforeInputListener = null;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (this.compositionStartListener) {
|
|
1061
|
+
this.container.removeEventListener('compositionstart', this.compositionStartListener);
|
|
1062
|
+
this.compositionStartListener = null;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (this.compositionUpdateListener) {
|
|
1066
|
+
this.container.removeEventListener('compositionupdate', this.compositionUpdateListener);
|
|
1067
|
+
this.compositionUpdateListener = null;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
if (this.compositionEndListener) {
|
|
1071
|
+
this.container.removeEventListener('compositionend', this.compositionEndListener);
|
|
1072
|
+
this.compositionEndListener = null;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
if (this.mousedownListener) {
|
|
1076
|
+
this.container.removeEventListener('mousedown', this.mousedownListener);
|
|
1077
|
+
this.mousedownListener = null;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
if (this.mouseupListener) {
|
|
1081
|
+
this.container.removeEventListener('mouseup', this.mouseupListener);
|
|
1082
|
+
this.mouseupListener = null;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
if (this.mousemoveListener) {
|
|
1086
|
+
this.container.removeEventListener('mousemove', this.mousemoveListener);
|
|
1087
|
+
this.mousemoveListener = null;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (this.wheelListener) {
|
|
1091
|
+
this.container.removeEventListener('wheel', this.wheelListener);
|
|
1092
|
+
this.wheelListener = null;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
this.isDisposed = true;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* Check if handler is disposed
|
|
1100
|
+
*/
|
|
1101
|
+
isActive(): boolean {
|
|
1102
|
+
return !this.isDisposed;
|
|
1103
|
+
}
|
|
1104
|
+
}
|