diff --git a/package-lock.json b/package-lock.json index 058ce7b..c04d163 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13036,30 +13036,6 @@ "node": ">=18" } }, - "node_modules/xml2js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", - "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", - "extraneous": true, - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xml2js/node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "extraneous": true, - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", diff --git a/src/hooks/useWebRtcStream.ts b/src/hooks/useWebRtcStream.ts index 47fb51c..259eb29 100644 --- a/src/hooks/useWebRtcStream.ts +++ b/src/hooks/useWebRtcStream.ts @@ -319,5 +319,6 @@ export function useWebRtcStream({ token }: UseWebRtcStreamOptions) { connecting, reconnect, sendInputEvent, + activeSessionId, } } diff --git a/src/routes/trackpad.tsx b/src/routes/trackpad.tsx index 86665ce..dfc9f40 100644 --- a/src/routes/trackpad.tsx +++ b/src/routes/trackpad.tsx @@ -11,6 +11,40 @@ import { ScreenMirror } from "../components/Trackpad/ScreenMirror" import { ErrorComponent } from "../components/Trackpad/ErrorComponent" import { useWebRtcStream } from "../hooks/useWebRtcStream" +const copyWithFallback = (text: string) => { + const textArea = document.createElement("textarea") + textArea.value = text + textArea.setAttribute("readonly", "") + textArea.style.position = "absolute" + textArea.style.left = "-9999px" + document.body.appendChild(textArea) + textArea.select() + textArea.setSelectionRange(0, text.length) + try { + return document.execCommand("copy") + } finally { + document.body.removeChild(textArea) + } +} +const writeClientClipboard = async (text: string) => { + if ( + typeof navigator !== "undefined" && + navigator.clipboard && + typeof navigator.clipboard.writeText === "function" + ) { + try { + await navigator.clipboard.writeText(text) + return + } catch (err) { + console.warn("navigator.clipboard.writeText failed, using fallback:", err) + } + } + const success = copyWithFallback(text) + if (!success) { + throw new Error("Fallback copy failed") + } +} + export const Route = createFileRoute("/trackpad")({ component: TrackpadPage, }) @@ -81,8 +115,73 @@ function TrackpadPage() { ) } - const handleCopy = () => broadcastMessage({ type: "copy" }) - const handlePaste = async () => broadcastMessage({ type: "paste" }) + const handleCopy = async () => { + try { + const headers: Record = {} + if (token) { + headers.Authorization = `Bearer ${token}` + } + const response = await fetch("/api/clipboard/copy", { + method: "POST", + headers: { + ...headers, + "Content-Type": "application/json", + }, + body: JSON.stringify({ sessionId: activeSessionId }), + }) + if (response.ok) { + const data = await response.json() + if (data && typeof data.text === "string") { + await writeClientClipboard(data.text) + } else { + throw new Error("Invalid copy response data") + } + } else { + throw new Error(`Clipboard copy failed: ${response.statusText}`) + } + } catch (err) { + console.warn( + "Client clipboard copy failed, falling back to server copy:", + err, + ) + broadcastMessage({ type: "copy" }) + } + } + const handlePaste = async () => { + try { + if ( + typeof navigator !== "undefined" && + navigator.clipboard && + typeof navigator.clipboard.readText === "function" + ) { + const text = await navigator.clipboard.readText() + if (text) { + const headers: Record = {} + if (token) { + headers.Authorization = `Bearer ${token}` + } + const response = await fetch("/api/clipboard/paste", { + method: "POST", + headers: { + ...headers, + "Content-Type": "application/json", + }, + body: JSON.stringify({ sessionId: activeSessionId, text }), + }) + if (response.ok) { + return + } + } + } + throw new Error("Client clipboard read returned empty or is unavailable") + } catch (err) { + console.warn( + "Client clipboard paste failed, falling back to server clipboard:", + err, + ) + broadcastMessage({ type: "paste" }) + } + } const handleInput = (e: React.ChangeEvent) => { const nativeEvent = e.nativeEvent as InputEvent diff --git a/src/server/drivers/linux/keyboard.ts b/src/server/drivers/linux/keyboard.ts index 7f5aa2a..d1d2754 100644 --- a/src/server/drivers/linux/keyboard.ts +++ b/src/server/drivers/linux/keyboard.ts @@ -33,7 +33,7 @@ export class LinuxKeyboard { this.sendKeyEvent(code, KEY_RELEASE) } this.sync() - } else if (key.length === 1) { + } else if (key.length > 0) { this.injectText(key) } else { console.warn("[LinuxKeyboard] Unknown key:", key) diff --git a/src/server/drivers/mac/keyboard.ts b/src/server/drivers/mac/keyboard.ts index 3198b9d..b9e9cff 100644 --- a/src/server/drivers/mac/keyboard.ts +++ b/src/server/drivers/mac/keyboard.ts @@ -1,10 +1,4 @@ -/** - * macOS virtual keyboard implementation. - * - * Handles key, key-combination, and text injection through CoreGraphics - * keyboard events. Supports both key-code based input and Unicode - * character injection for characters not present in the standard key map. - */ +import koffi from "koffi" import { postKeyEvent, postMediaKeyEvent, @@ -39,7 +33,7 @@ export class MacKeyboard { if (code !== undefined) { if (pos !== "RELEASE") postKeyEvent(code, true) if (pos !== "HOLD") postKeyEvent(code, false) - } else if (key.length === 1) { + } else if (key.length > 0) { this.injectText(key) } else { console.warn("[MacKeyboard] Unknown key:", key) @@ -72,20 +66,13 @@ export class MacKeyboard { if (!text) return for (const ch of text) { const { code, shifted } = resolveChar(ch, MAC_KEY_MAP) - const shiftCode = MAC_KEY_MAP.shift - if (code === undefined) { - // Fall back to Unicode injection for unmapped characters. + if (code === undefined || shifted) { + // Fall back to Unicode injection for unmapped or shifted characters. this.injectUnicodeChar(ch) continue } - if (shiftCode === undefined) { - console.warn("[MacKeyboard] Shift key code not defined in key map") - continue - } - if (shifted) postKeyEvent(shiftCode, true) postKeyEvent(code, true) postKeyEvent(code, false) - if (shifted) postKeyEvent(shiftCode, false) } } private injectUnicodeChar(ch: string): void { @@ -108,7 +95,6 @@ function ensureUnicode() { if (_unicodeInjectorLoaded) return _unicodeInjectorLoaded = true try { - const koffi = require("koffi") const lib = koffi.load( "/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics", ) @@ -148,7 +134,6 @@ function injectUnicode(ch: string): void { const upRef = _CGEventCreateKeyboardEvent(null, 0, 0) if (!upRef) return - _CGEventKeyboardSetUnicodeString(upRef, charCount, buf) _CGEventPost(0, upRef) _CFRelease(upRef) } diff --git a/src/server/drivers/windows/keyboard.ts b/src/server/drivers/windows/keyboard.ts index cae39c8..786da57 100644 --- a/src/server/drivers/windows/keyboard.ts +++ b/src/server/drivers/windows/keyboard.ts @@ -40,7 +40,7 @@ export class WindowsKeyboard { }) } this.sendInput(events.length, events) - } else if (key.length === 1) { + } else if (key.length > 0) { this.injectText(key) } else { console.warn("[Keyboard] Unknown key and not a single character:", key)