diff --git a/bun.lock b/bun.lock index 3d989f870..904cba12b 100644 --- a/bun.lock +++ b/bun.lock @@ -373,8 +373,6 @@ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], "meshoptimizer": ["meshoptimizer@0.18.1", "", {}, "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw=="], diff --git a/packages/core/src/examples/index.ts b/packages/core/src/examples/index.ts index dee7cd273..af388e77d 100644 --- a/packages/core/src/examples/index.ts +++ b/packages/core/src/examples/index.ts @@ -63,6 +63,7 @@ import * as linkDemo from "./link-demo" import * as opacityExample from "./opacity-example" import * as scrollboxOverlayHitTest from "./scrollbox-overlay-hit-test" import * as scrollboxMouseTest from "./scrollbox-mouse-test" +import * as pasteDemo from "./paste-demo" import * as textTruncationDemo from "./text-truncation-demo" import * as grayscaleBufferDemo from "./grayscale-buffer-demo" import { setupCommonDemoKeys } from "./lib/standalone-keys" @@ -334,6 +335,12 @@ const examples: Example[] = [ run: inputExample.run, destroy: inputExample.destroy, }, + { + name: "Paste Demo", + description: "Paste into inputs and inspect metadata including binary detection", + run: pasteDemo.run, + destroy: pasteDemo.destroy, + }, { name: "Terminal Palette Demo", description: "Terminal color palette detection and visualization - fetch and display all 256 terminal colors", diff --git a/packages/core/src/examples/paste-demo.ts b/packages/core/src/examples/paste-demo.ts new file mode 100644 index 000000000..0897ff794 --- /dev/null +++ b/packages/core/src/examples/paste-demo.ts @@ -0,0 +1,163 @@ +import { + BoxRenderable, + InputRenderable, + TextareaRenderable, + TextRenderable, + bold, + createCliRenderer, + fg, + t, + type CliRenderer, +} from "../index" +import { KeyEvent, PasteEvent } from "../lib/KeyHandler" +import { setupCommonDemoKeys } from "./lib/standalone-keys" + +let renderer: CliRenderer | null = null +let container: BoxRenderable | null = null +let singleLineInput: InputRenderable | null = null +let multilineInput: TextareaRenderable | null = null +let logDisplay: TextRenderable | null = null +let instructions: TextRenderable | null = null +let keypressHandler: ((event: KeyEvent) => void) | null = null +let pasteHandler: ((event: PasteEvent) => void) | null = null + +const logEntries: string[] = [] + +function formatHexHead(buffer: Buffer): string { + if (buffer.length === 0) return "" + const hex = buffer.subarray(0, 16).toString("hex") + return hex.match(/.{1,2}/g)?.join(" ") ?? hex +} + +function formatTextPreview(buffer: Buffer): string { + const text = buffer.toString("utf8") + const normalized = text.replace(/\r/g, "").replace(/\n/g, " ⏎ ") + return normalized.length > 80 ? `${normalized.slice(0, 80)}…` : normalized +} + +function updateLog(event: PasteEvent): void { + const entry = `len=${event.data.length} head=${formatHexHead(event.data)} preview=${formatTextPreview(event.data)}` + + logEntries.unshift(entry) + logEntries.splice(12) + + if (logDisplay) { + logDisplay.content = t`${logEntries.join("\n")}` + } + + const text = event.text ?? event.data.toString("utf8") + if (singleLineInput) { + singleLineInput.value = text + } + + if (multilineInput) { + multilineInput.value = text + } +} + +function createLayout(rendererInstance: CliRenderer): void { + container = new BoxRenderable(rendererInstance, { + id: "paste-demo-root", + flexDirection: "column", + width: "100%", + height: "100%", + padding: 2, + gap: 1, + }) + + instructions = new TextRenderable(rendererInstance, { + id: "paste-demo-instructions", + width: 100, + height: 4, + content: t`${bold("Paste Demo")} +- Paste into the single-line input or the textarea +- Logs show file type, byte length, first 16 bytes, and text preview +- Press q to quit, Ctrl+C to exit`, + }) + + singleLineInput = new InputRenderable(rendererInstance, { + id: "paste-demo-input", + width: 70, + height: 3, + placeholder: "Paste here (single line)", + }) + + multilineInput = new TextareaRenderable(rendererInstance, { + id: "paste-demo-textarea", + width: 70, + height: 6, + placeholder: "Or paste here (textarea)", + }) + + logDisplay = new TextRenderable(rendererInstance, { + id: "paste-demo-log", + width: 100, + height: 12, + fg: fg("#A0FFA0"), + content: t`Waiting for paste events…`, + }) + + container.add(instructions) + container.add(singleLineInput) + container.add(multilineInput) + container.add(logDisplay) + + rendererInstance.root.add(container) +} + +export function run(rendererInstance: CliRenderer): void { + renderer = rendererInstance + renderer.setBackgroundColor("#0e1116") + + createLayout(rendererInstance) + + keypressHandler = (event: KeyEvent) => { + if (event.name === "q") { + renderer?.destroy() + return + } + } + + pasteHandler = (event: PasteEvent) => { + updateLog(event) + } + + renderer.keyInput.on("keypress", keypressHandler) + renderer.keyInput.on("paste", pasteHandler) + renderer.requestRender() +} + +export function destroy(rendererInstance: CliRenderer): void { + rendererInstance.clearFrameCallbacks() + + if (keypressHandler) { + rendererInstance.keyInput.off("keypress", keypressHandler) + keypressHandler = null + } + + if (pasteHandler) { + rendererInstance.keyInput.off("paste", pasteHandler) + pasteHandler = null + } + + if (container) { + rendererInstance.root.remove("paste-demo-root") + container = null + } + + singleLineInput = null + multilineInput = null + logDisplay = null + instructions = null + logEntries.length = 0 +} + +if (import.meta.main) { + const rendererInstance = await createCliRenderer({ + exitOnCtrlC: true, + }) + + run(rendererInstance) + setupCommonDemoKeys(rendererInstance) + rendererInstance.start() +} diff --git a/packages/core/src/lib/KeyHandler.paste-binary.test.ts b/packages/core/src/lib/KeyHandler.paste-binary.test.ts new file mode 100644 index 000000000..2b823f591 --- /dev/null +++ b/packages/core/src/lib/KeyHandler.paste-binary.test.ts @@ -0,0 +1,31 @@ +import { expect, test } from "bun:test" +import { InternalKeyHandler } from "./KeyHandler" + +test("processPaste emits buffer for image data", () => { + const handler = new InternalKeyHandler() + const pngBytes = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00]) + + let received: any + handler.on("paste", (event) => { + received = event + }) + + handler.processPaste(pngBytes) + + expect(Buffer.isBuffer(received?.data)).toBe(true) + expect(received?.data?.equals?.(pngBytes)).toBe(true) +}) + +test("processPaste emits buffer for text data", () => { + const handler = new InternalKeyHandler() + + let receivedData: Buffer | undefined + handler.on("paste", (event) => { + receivedData = event.data + }) + + handler.processPaste("plain text") + + expect(Buffer.isBuffer(receivedData)).toBe(true) + expect(receivedData?.toString()).toBe("plain text") +}) diff --git a/packages/core/src/lib/KeyHandler.test.ts b/packages/core/src/lib/KeyHandler.test.ts index 547b94174..ebf124c24 100644 --- a/packages/core/src/lib/KeyHandler.test.ts +++ b/packages/core/src/lib/KeyHandler.test.ts @@ -81,6 +81,21 @@ test("KeyHandler - processPaste handles content directly", () => { expect(receivedPaste).toBe("chunk1chunk2chunk3") }) +test("KeyHandler - detects magic bytes for binary paste", () => { + const handler = createKeyHandler() + + const pngBytes = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00]) + + let receivedPaste: any + handler.on("paste", (event) => { + receivedPaste = event + }) + ;(handler as any).processPaste(pngBytes) + + expect(Buffer.isBuffer(receivedPaste?.data)).toBe(true) + expect(receivedPaste?.data?.equals?.(pngBytes)).toBe(true) +}) + test("KeyHandler - strips ANSI codes in paste", () => { const handler = createKeyHandler() diff --git a/packages/core/src/lib/KeyHandler.ts b/packages/core/src/lib/KeyHandler.ts index be31f0f9d..bc854877a 100644 --- a/packages/core/src/lib/KeyHandler.ts +++ b/packages/core/src/lib/KeyHandler.ts @@ -1,6 +1,6 @@ import { EventEmitter } from "events" import { parseKeypress, type KeyEventType, type ParsedKey } from "./parse.keypress" -import { ANSI } from "../ansi" +import type { PasteChunk } from "./stdin-buffer" export class KeyEvent implements ParsedKey { name: string @@ -61,13 +61,20 @@ export class KeyEvent implements ParsedKey { } } +export type PasteEventInit = { + data: Buffer + text?: string +} + export class PasteEvent { - text: string + data: Buffer + text?: string private _defaultPrevented: boolean = false private _propagationStopped: boolean = false - constructor(text: string) { - this.text = text + constructor(init: PasteEventInit) { + this.data = init.data + this.text = init.text } get defaultPrevented(): boolean { @@ -128,14 +135,38 @@ export class KeyHandler extends EventEmitter { return true } - public processPaste(data: string): void { + public processPaste(data: string | Buffer | PasteChunk): void { try { - const cleanedData = Bun.stripANSI(data) - this.emit("paste", new PasteEvent(cleanedData)) + const { buffer, text } = this.normalizePasteInput(data) + const cleanedText = text !== undefined ? Bun.stripANSI(text) : this.getCleanedText(buffer) + + this.emit( + "paste", + new PasteEvent({ + data: buffer, + text: cleanedText, + }), + ) } catch (error) { console.error(`[KeyHandler] Error processing paste:`, error) } } + + private normalizePasteInput(data: string | Buffer | PasteChunk): { buffer: Buffer; text?: string } { + if (typeof data === "string") { + return { buffer: Buffer.from(data), text: data } + } + + if (Buffer.isBuffer(data)) { + return { buffer: data } + } + + return { buffer: data.data, text: data.text } + } + + private getCleanedText(buffer: Buffer): string { + return Bun.stripANSI(buffer.toString()) + } } /** diff --git a/packages/core/src/lib/paste-detect.test.ts b/packages/core/src/lib/paste-detect.test.ts new file mode 100644 index 000000000..81f234672 --- /dev/null +++ b/packages/core/src/lib/paste-detect.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from "bun:test" +import { detectPasteFileType } from "./paste-detect" + +test("detectPasteFileType identifies common image signatures", () => { + expect(detectPasteFileType(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))).toBe("image/png") + expect(detectPasteFileType(Buffer.from([0xff, 0xd8, 0xff, 0x00]))).toBe("image/jpeg") + expect(detectPasteFileType(Buffer.from("GIF89a"))).toBe("image/gif") + expect(detectPasteFileType(Buffer.from("RIFF1234WEBP"))).toBe("image/webp") + expect(detectPasteFileType(Buffer.from([0x42, 0x4d, 0x00, 0x00]))).toBe("image/bmp") + expect(detectPasteFileType(Buffer.from([0x00, 0x00, 0x01, 0x00, 0x00]))).toBe("image/x-icon") + expect(detectPasteFileType(Buffer.from(''))).toBe("image/svg+xml") +}) + +test("detectPasteFileType returns undefined for non-image data", () => { + expect(detectPasteFileType(Buffer.from("plain text"))).toBeUndefined() +}) diff --git a/packages/core/src/lib/paste-detect.ts b/packages/core/src/lib/paste-detect.ts new file mode 100644 index 000000000..4f0a3ce0b --- /dev/null +++ b/packages/core/src/lib/paste-detect.ts @@ -0,0 +1,39 @@ +const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) +const JPEG_SIGNATURE = Buffer.from([0xff, 0xd8, 0xff]) +const GIF_SIGNATURE = Buffer.from([0x47, 0x49, 0x46, 0x38]) +const WEBP_RIFF = Buffer.from("RIFF") +const WEBP_WEBP = Buffer.from("WEBP") +const BMP_SIGNATURE = Buffer.from([0x42, 0x4d]) +const ICO_SIGNATURE = Buffer.from([0x00, 0x00, 0x01, 0x00]) + +function matchesSignature(buffer: Buffer, signature: Buffer, offset: number = 0): boolean { + if (buffer.length < signature.length + offset) return false + for (let i = 0; i < signature.length; i++) { + if (buffer[i + offset] !== signature[i]) { + return false + } + } + return true +} + +function detectSvg(buffer: Buffer): boolean { + if (buffer.length === 0) return false + const sample = buffer.slice(0, 512).toString("utf8").trimStart().toLowerCase() + return sample.startsWith(" { + test("emits raw buffer for binary paste payloads", () => { + const stdinBuffer = new StdinBuffer() + const pngBytes = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00]) + const pasteSequence = Buffer.concat([ + Buffer.from(ANSI.bracketedPasteStart), + pngBytes, + Buffer.from(ANSI.bracketedPasteEnd), + ]) + + let received: any + stdinBuffer.on("paste", (event) => { + received = event + }) + + stdinBuffer.process(pasteSequence) + + expect(Buffer.isBuffer(received?.data)).toBe(true) + expect(received?.data?.equals?.(pngBytes)).toBe(true) + expect(typeof received?.text === "string" || received?.text === undefined).toBe(true) + }) +}) diff --git a/packages/core/src/lib/stdin-buffer.test.ts b/packages/core/src/lib/stdin-buffer.test.ts index f05ae357a..617db093a 100644 --- a/packages/core/src/lib/stdin-buffer.test.ts +++ b/packages/core/src/lib/stdin-buffer.test.ts @@ -305,7 +305,8 @@ describe("StdinBuffer", () => { // Collect paste events emittedPaste = [] buffer.on("paste", (data) => { - emittedPaste.push(data) + const text = typeof data === "string" ? data : (data.text ?? "") + emittedPaste.push(text) }) }) diff --git a/packages/core/src/lib/stdin-buffer.ts b/packages/core/src/lib/stdin-buffer.ts index 8926c76ab..918f47b1d 100644 --- a/packages/core/src/lib/stdin-buffer.ts +++ b/packages/core/src/lib/stdin-buffer.ts @@ -19,6 +19,8 @@ import { EventEmitter } from "events" const ESC = "\x1b" const BRACKETED_PASTE_START = "\x1b[200~" const BRACKETED_PASTE_END = "\x1b[201~" +const BRACKETED_PASTE_START_BUFFER = Buffer.from(BRACKETED_PASTE_START) +const BRACKETED_PASTE_END_BUFFER = Buffer.from(BRACKETED_PASTE_END) /** * Check if a string is a complete escape sequence or needs more data @@ -228,9 +230,14 @@ export type StdinBufferOptions = { timeout?: number } +export type PasteChunk = { + data: Buffer + text?: string +} + export type StdinBufferEventMap = { data: [string] - paste: [string] + paste: [PasteChunk] } /** @@ -238,11 +245,11 @@ export type StdinBufferEventMap = { * Handles partial escape sequences that arrive across multiple chunks. */ export class StdinBuffer extends EventEmitter { - private buffer: string = "" + private buffer: Buffer = Buffer.alloc(0) private timeout: Timer | null = null private readonly timeoutMs: number private pasteMode: boolean = false - private pasteBuffer: string = "" + private pasteBuffer: Buffer = Buffer.alloc(0) constructor(options: StdinBufferOptions = {}) { super() @@ -250,89 +257,46 @@ export class StdinBuffer extends EventEmitter { } public process(data: string | Buffer): void { - // Clear any pending timeout if (this.timeout) { clearTimeout(this.timeout) this.timeout = null } - // Handle high-byte conversion (for compatibility with parseKeypress) - // If buffer has single byte > 127, convert to ESC + (byte - 128) - // TODO: This seems redundant as parseKeypress should handle this. - let str: string - if (Buffer.isBuffer(data)) { - if (data.length === 1 && data[0]! > 127) { - const byte = data[0]! - 128 - str = "\x1b" + String.fromCharCode(byte) - } else { - str = data.toString() - } - } else { - str = data - } + const chunk = this.normalizeChunk(data) - if (str.length === 0 && this.buffer.length === 0) { + if (chunk.length === 0 && this.buffer.length === 0) { this.emit("data", "") return } - this.buffer += str - if (this.pasteMode) { - this.pasteBuffer += this.buffer - this.buffer = "" - - const endIndex = this.pasteBuffer.indexOf(BRACKETED_PASTE_END) - if (endIndex !== -1) { - const pastedContent = this.pasteBuffer.slice(0, endIndex) - const remaining = this.pasteBuffer.slice(endIndex + BRACKETED_PASTE_END.length) - - this.pasteMode = false - this.pasteBuffer = "" - - this.emit("paste", pastedContent) - - if (remaining.length > 0) { - this.process(remaining) - } - } + this.appendToPaste(chunk) return } - const startIndex = this.buffer.indexOf(BRACKETED_PASTE_START) + this.buffer = Buffer.concat([this.buffer, chunk]) + + const startIndex = this.buffer.indexOf(BRACKETED_PASTE_START_BUFFER) if (startIndex !== -1) { if (startIndex > 0) { - const beforePaste = this.buffer.slice(0, startIndex) - const result = extractCompleteSequences(beforePaste) + const beforePaste = this.buffer.subarray(0, startIndex) + const result = extractCompleteSequences(beforePaste.toString()) for (const sequence of result.sequences) { this.emit("data", sequence) } } - this.buffer = this.buffer.slice(startIndex + BRACKETED_PASTE_START.length) + const afterStart = this.buffer.subarray(startIndex + BRACKETED_PASTE_START_BUFFER.length) this.pasteMode = true - this.pasteBuffer = this.buffer - this.buffer = "" - - const endIndex = this.pasteBuffer.indexOf(BRACKETED_PASTE_END) - if (endIndex !== -1) { - const pastedContent = this.pasteBuffer.slice(0, endIndex) - const remaining = this.pasteBuffer.slice(endIndex + BRACKETED_PASTE_END.length) + this.pasteBuffer = afterStart + this.buffer = Buffer.alloc(0) - this.pasteMode = false - this.pasteBuffer = "" - - this.emit("paste", pastedContent) - - if (remaining.length > 0) { - this.process(remaining) - } - } + this.tryEmitPaste() return } - const result = extractCompleteSequences(this.buffer) - this.buffer = result.remainder + const result = extractCompleteSequences(this.buffer.toString()) + this.buffer = Buffer.from(result.remainder) for (const sequence of result.sequences) { this.emit("data", sequence) @@ -359,8 +323,8 @@ export class StdinBuffer extends EventEmitter { return [] } - const sequences = [this.buffer] - this.buffer = "" + const sequences = [this.buffer.toString()] + this.buffer = Buffer.alloc(0) return sequences } @@ -369,16 +333,56 @@ export class StdinBuffer extends EventEmitter { clearTimeout(this.timeout) this.timeout = null } - this.buffer = "" + this.buffer = Buffer.alloc(0) this.pasteMode = false - this.pasteBuffer = "" + this.pasteBuffer = Buffer.alloc(0) } getBuffer(): string { - return this.buffer + return this.buffer.toString() } destroy(): void { this.clear() } + + private normalizeChunk(data: string | Buffer): Buffer { + if (this.pasteMode) { + return Buffer.isBuffer(data) ? data : Buffer.from(data) + } + + if (Buffer.isBuffer(data)) { + if (data.length === 1 && data[0]! > 127) { + const byte = data[0]! - 128 + return Buffer.from("\x1b" + String.fromCharCode(byte)) + } + return data + } + + return Buffer.from(data) + } + + private appendToPaste(chunk: Buffer): void { + this.pasteBuffer = Buffer.concat([this.pasteBuffer, chunk]) + this.tryEmitPaste() + } + + private tryEmitPaste(): void { + const endIndex = this.pasteBuffer.indexOf(BRACKETED_PASTE_END_BUFFER) + if (endIndex === -1) { + return + } + + const pastedContent = this.pasteBuffer.subarray(0, endIndex) + const remaining = this.pasteBuffer.subarray(endIndex + BRACKETED_PASTE_END_BUFFER.length) + + this.pasteMode = false + this.pasteBuffer = Buffer.alloc(0) + + this.emit("paste", { data: pastedContent, text: pastedContent.toString() }) + + if (remaining.length > 0) { + this.process(remaining) + } + } } diff --git a/packages/core/src/renderables/Textarea.ts b/packages/core/src/renderables/Textarea.ts index a58c8eadc..7eca4f26a 100644 --- a/packages/core/src/renderables/Textarea.ts +++ b/packages/core/src/renderables/Textarea.ts @@ -253,7 +253,13 @@ export class TextareaRenderable extends EditBufferRenderable { } public handlePaste(event: PasteEvent): void { - this.insertText(event.text) + const pasteText = event.text ?? (typeof event.data === "string" ? event.data : event.data?.toString("utf8")) ?? null + + if (pasteText === null) { + return + } + + this.insertText(pasteText) } public handleKeyPress(key: KeyEvent): boolean { @@ -281,8 +287,9 @@ export class TextareaRenderable extends EditBufferRenderable { return true } - if (key.sequence) { - const firstCharCode = key.sequence.charCodeAt(0) + const sequence = key.sequence + if (typeof sequence === "string" && sequence.length > 0) { + const firstCharCode = sequence.charCodeAt(0) if (firstCharCode < 32) { return false @@ -292,7 +299,7 @@ export class TextareaRenderable extends EditBufferRenderable { return false } - this.insertText(key.sequence) + this.insertText(sequence) return true } } diff --git a/packages/core/src/renderables/__tests__/Textarea.paste.test.ts b/packages/core/src/renderables/__tests__/Textarea.paste.test.ts index a48d4d812..bf1024d2f 100644 --- a/packages/core/src/renderables/__tests__/Textarea.paste.test.ts +++ b/packages/core/src/renderables/__tests__/Textarea.paste.test.ts @@ -223,7 +223,7 @@ describe("Textarea - Paste Tests", () => { editor.focus() editor.gotoLine(9999) - editor.handlePaste(new PasteEvent(" Content")) + editor.handlePaste(new PasteEvent({ data: Buffer.from(" Content"), text: " Content" })) expect(editor.plainText).toBe("Test Content") }) @@ -248,7 +248,7 @@ describe("Textarea - Paste Tests", () => { expect(editor.getSelectedText()).toBe("World") // Use handlePaste directly - editor.handlePaste(new PasteEvent("Universe")) + editor.handlePaste(new PasteEvent({ data: Buffer.from("Universe"), text: "Universe" })) expect(editor.hasSelection()).toBe(false) expect(editor.plainText).toBe("Hello Universe") diff --git a/packages/core/src/renderer.ts b/packages/core/src/renderer.ts index 2af93f109..48fbd1d0a 100644 --- a/packages/core/src/renderer.ts +++ b/packages/core/src/renderer.ts @@ -1096,7 +1096,7 @@ export class CliRenderer extends EventEmitter implements RenderContext { } } }) - this._stdinBuffer.on("paste", (data: string) => { + this._stdinBuffer.on("paste", (data) => { this._keyHandler.processPaste(data) }) }