From 342cfcac67e6a8ff524eccae1e007f3245beb8bd Mon Sep 17 00:00:00 2001 From: AnshuPriya-1 Date: Tue, 17 Mar 2026 19:53:07 +0530 Subject: [PATCH 1/7] feat: add client-based clipboard sync (HTTP-safe fallback without HTTPS) --- src/routes/trackpad.tsx | 22 +++++++++++++++++++--- src/server/InputHandler.ts | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/routes/trackpad.tsx b/src/routes/trackpad.tsx index f2f31461..f7cdaa25 100644 --- a/src/routes/trackpad.tsx +++ b/src/routes/trackpad.tsx @@ -68,12 +68,28 @@ function TrackpadPage() { setTimeout(() => send({ type: "click", button, press: false }), 50) } - const handleCopy = () => { - send({ type: "copy" }) + const handleCopy = async () => { + try { + let text = "" + + if (navigator.clipboard && window.isSecureContext) { + text = await navigator.clipboard.readText() + } else { + // fallback → use selected text + text = window.getSelection()?.toString() || "" + } + + send({ + type: "clipboard-push", + text, + }) + } catch (err) { + console.error("Copy failed", err) } +} const handlePaste = async () => { - send({ type: "paste" }) + send({ type: "clipboard-pull" }) } const handleInput = (e: React.ChangeEvent) => { diff --git a/src/server/InputHandler.ts b/src/server/InputHandler.ts index 63f9b26e..1bc1aab8 100644 --- a/src/server/InputHandler.ts +++ b/src/server/InputHandler.ts @@ -8,6 +8,8 @@ export interface InputMessage { | "move" | "paste" | "copy" + | "clipboard-push" + | "clipboard-pull" | "click" | "scroll" | "key" @@ -34,7 +36,10 @@ export class InputHandler { private throttleMs: number private modifier: Key - constructor(throttleMs = 8) { + constructor( + private sendToClient: (msg: any) => void, + throttleMs = 8 + ) { mouse.config.mouseSpeed = 1000 this.modifier = os.platform() === "darwin" ? Key.LeftSuper : Key.LeftControl this.throttleMs = throttleMs @@ -196,6 +201,36 @@ export class InputHandler { break } + case "clipboard-push": { + if (msg.text) { + // TEMP: fallback using typing instead of real clipboard + await keyboard.type(msg.text) + } + break + } + + case "clipboard-pull": { + // simulate Ctrl+C to get current clipboard + try { + await keyboard.pressKey(this.modifier, Key.C) + } finally { + await Promise.allSettled([ + keyboard.releaseKey(Key.C), + keyboard.releaseKey(this.modifier), + ]) + } + + // small delay to allow clipboard update + await new Promise((r) => setTimeout(r, 100)) + + // ❗ send back to client (IMPORTANT) + this.sendToClient({ + type: "clipboard-text", + text: "TEMP_CLIPBOARD_DATA", // ⚠️ temporary (we’ll improve later) + }) + break + } + case "scroll": { const MAX_SCROLL = 100 const promises: Promise[] = [] From dcb16861005680ac7c17d88a200a7e91993392f7 Mon Sep 17 00:00:00 2001 From: AnshuPriya-1 Date: Tue, 17 Mar 2026 20:14:40 +0530 Subject: [PATCH 2/7] fix: remove any type and enforce strict typing for sendToClient --- src/routes/trackpad.tsx | 32 +++++++++--------- src/server/InputHandler.ts | 67 ++++++++++++++++++++------------------ 2 files changed, 52 insertions(+), 47 deletions(-) diff --git a/src/routes/trackpad.tsx b/src/routes/trackpad.tsx index f7cdaa25..7b3d6586 100644 --- a/src/routes/trackpad.tsx +++ b/src/routes/trackpad.tsx @@ -68,25 +68,25 @@ function TrackpadPage() { setTimeout(() => send({ type: "click", button, press: false }), 50) } - const handleCopy = async () => { - try { - let text = "" + const handleCopy = async () => { + try { + let text = "" - if (navigator.clipboard && window.isSecureContext) { - text = await navigator.clipboard.readText() - } else { - // fallback → use selected text - text = window.getSelection()?.toString() || "" - } + if (navigator.clipboard && window.isSecureContext) { + text = await navigator.clipboard.readText() + } else { + // fallback → use selected text + text = window.getSelection()?.toString() || "" + } - send({ - type: "clipboard-push", - text, - }) - } catch (err) { - console.error("Copy failed", err) + send({ + type: "clipboard-push", + text, + }) + } catch (err) { + console.error("Copy failed", err) + } } -} const handlePaste = async () => { send({ type: "clipboard-pull" }) diff --git a/src/server/InputHandler.ts b/src/server/InputHandler.ts index 1bc1aab8..f4c61e8c 100644 --- a/src/server/InputHandler.ts +++ b/src/server/InputHandler.ts @@ -3,13 +3,18 @@ import { KEY_MAP } from "./KeyMap" import { moveRelative } from "./ydotool" import os from "node:os" +type ServerToClientMessage = { + type: "clipboard-text" + text: string +} + export interface InputMessage { type: | "move" | "paste" | "copy" | "clipboard-push" - | "clipboard-pull" + | "clipboard-pull" | "click" | "scroll" | "key" @@ -37,9 +42,9 @@ export class InputHandler { private modifier: Key constructor( - private sendToClient: (msg: any) => void, - throttleMs = 8 - ) { + private sendToClient: (msg: ServerToClientMessage) => void, + throttleMs = 8, + ) { mouse.config.mouseSpeed = 1000 this.modifier = os.platform() === "darwin" ? Key.LeftSuper : Key.LeftControl this.throttleMs = throttleMs @@ -202,34 +207,34 @@ export class InputHandler { } case "clipboard-push": { - if (msg.text) { + if (msg.text) { // TEMP: fallback using typing instead of real clipboard - await keyboard.type(msg.text) - } - break - } - - case "clipboard-pull": { - // simulate Ctrl+C to get current clipboard - try { - await keyboard.pressKey(this.modifier, Key.C) - } finally { - await Promise.allSettled([ - keyboard.releaseKey(Key.C), - keyboard.releaseKey(this.modifier), - ]) - } - - // small delay to allow clipboard update - await new Promise((r) => setTimeout(r, 100)) - - // ❗ send back to client (IMPORTANT) - this.sendToClient({ - type: "clipboard-text", - text: "TEMP_CLIPBOARD_DATA", // ⚠️ temporary (we’ll improve later) - }) - break - } + await keyboard.type(msg.text) + } + break + } + + case "clipboard-pull": { + // simulate Ctrl+C to get current clipboard + try { + await keyboard.pressKey(this.modifier, Key.C) + } finally { + await Promise.allSettled([ + keyboard.releaseKey(Key.C), + keyboard.releaseKey(this.modifier), + ]) + } + + // small delay to allow clipboard update + await new Promise((r) => setTimeout(r, 100)) + + // ❗ send back to client (IMPORTANT) + this.sendToClient({ + type: "clipboard-text", + text: "TEMP_CLIPBOARD_DATA", // ⚠️ temporary (we’ll improve later) + }) + break + } case "scroll": { const MAX_SCROLL = 100 From d5fd9a1ee802be2d233aa6832ab881a05424a3f3 Mon Sep 17 00:00:00 2001 From: AnshuPriya-1 Date: Tue, 17 Mar 2026 23:07:39 +0530 Subject: [PATCH 3/7] feat: add bidirectional clipboard sync --- src/routes/trackpad.tsx | 79 +++++++++++++++++++++++++++----------- src/server/InputHandler.ts | 8 +++- src/server/websocket.ts | 11 +++++- 3 files changed, 72 insertions(+), 26 deletions(-) diff --git a/src/routes/trackpad.tsx b/src/routes/trackpad.tsx index 7b3d6586..6a8b9308 100644 --- a/src/routes/trackpad.tsx +++ b/src/routes/trackpad.tsx @@ -13,6 +13,11 @@ export const Route = createFileRoute("/trackpad")({ component: TrackpadPage, }) +type ClipboardMessage = { + type: "clipboard-text" + text: string +} + function TrackpadPage() { const [scrollMode, setScrollMode] = useState(false) const [modifier, setModifier] = useState("Release") @@ -36,7 +41,7 @@ function TrackpadPage() { return s ? JSON.parse(s) : false }) - const { send, sendCombo } = useRemoteConnection() + const { send, sendCombo, subscribe } = useRemoteConnection() // Pass sensitivity and invertScroll to the gesture hook const { isTracking, handlers } = useTrackpadGesture( send, @@ -53,6 +58,33 @@ function TrackpadPage() { hiddenInputRef.current?.blur() } }, [keyboardOpen]) + + + useEffect(() => { + const unsubscribe = subscribe("clipboard-text", async (msg) => { + const data = msg as ClipboardMessage + + try { + const text = data.text || "" + + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text) + } else { + const textarea = document.createElement("textarea") + textarea.value = text + document.body.appendChild(textarea) + textarea.select() + document.execCommand("copy") + document.body.removeChild(textarea) + } + } catch (err) { + console.error("Clipboard write failed", err) + } + } + ) + + return () => unsubscribe() + }, [subscribe]) const toggleKeyboard = () => { setKeyboardOpen((prev) => !prev) @@ -67,31 +99,32 @@ function TrackpadPage() { // Release after short delay to simulate click setTimeout(() => send({ type: "click", button, press: false }), 50) } - - const handleCopy = async () => { - try { - let text = "" - - if (navigator.clipboard && window.isSecureContext) { - text = await navigator.clipboard.readText() - } else { - // fallback → use selected text - text = window.getSelection()?.toString() || "" - } - - send({ - type: "clipboard-push", - text, - }) - } catch (err) { - console.error("Copy failed", err) + + const handleCopy = () => { + // copy from SERVER → CLIENT + send({ type: "clipboard-pull" }) + } + + const handlePaste = async () => { + // paste from CLIENT → SERVER + try { + let text = "" + + if (navigator.clipboard && window.isSecureContext) { + text = await navigator.clipboard.readText() + } else { + text = window.getSelection()?.toString() || "" + } + + send({ + type: "clipboard-push", + text, + }) + } catch (err) { + console.error("Paste failed", err) } } - const handlePaste = async () => { - send({ type: "clipboard-pull" }) - } - const handleInput = (e: React.ChangeEvent) => { const nativeEvent = e.nativeEvent as InputEvent const inputType = nativeEvent.inputType diff --git a/src/server/InputHandler.ts b/src/server/InputHandler.ts index f4c61e8c..2bc3807e 100644 --- a/src/server/InputHandler.ts +++ b/src/server/InputHandler.ts @@ -209,7 +209,11 @@ export class InputHandler { case "clipboard-push": { if (msg.text) { // TEMP: fallback using typing instead of real clipboard - await keyboard.type(msg.text) + try{ + await keyboard.type(msg.text) + }catch(err) { + console.error("Clipboard push failed:", err) + } } break } @@ -231,7 +235,7 @@ export class InputHandler { // ❗ send back to client (IMPORTANT) this.sendToClient({ type: "clipboard-text", - text: "TEMP_CLIPBOARD_DATA", // ⚠️ temporary (we’ll improve later) + text: "CLIPBOARD_DATA_UNAVAILABLE", }) break } diff --git a/src/server/websocket.ts b/src/server/websocket.ts index 0388a12a..bd953b95 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -47,7 +47,6 @@ export async function createWsServer( : 8 const wss = new WebSocketServer({ noServer: true }) - const inputHandler = new InputHandler(inputThrottleMs) let LAN_IP = "127.0.0.1" try { LAN_IP = await getLocalIp() @@ -118,6 +117,14 @@ export async function createWsServer( token: string | null, isLocal: boolean, ) => { + const inputHandler = new InputHandler( + (msg) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify(msg)) + } + }, + inputThrottleMs + ) // Localhost: only store token if it's already known (trusted scan) // Remote: token is already validated in the upgrade handler logger.info(`Client connected from ${request.socket.remoteAddress}`) @@ -345,6 +352,8 @@ export async function createWsServer( "combo", "copy", "paste", + "clipboard-push", + "clipboard-pull", ] if (!msg.type || !VALID_INPUT_TYPES.includes(msg.type)) { logger.warn(`Unknown message type: ${msg.type}`) From 070fe2d0c4362e92f7a5c5fde3f60b2ef38386c6 Mon Sep 17 00:00:00 2001 From: AnshuPriya-1 Date: Tue, 17 Mar 2026 23:09:57 +0530 Subject: [PATCH 4/7] chore: fix biome formatting --- src/routes/trackpad.tsx | 90 +++++++++++++++++++------------------- src/server/InputHandler.ts | 8 ++-- src/server/websocket.ts | 15 +++---- 3 files changed, 54 insertions(+), 59 deletions(-) diff --git a/src/routes/trackpad.tsx b/src/routes/trackpad.tsx index 6a8b9308..d27e3913 100644 --- a/src/routes/trackpad.tsx +++ b/src/routes/trackpad.tsx @@ -58,33 +58,31 @@ function TrackpadPage() { hiddenInputRef.current?.blur() } }, [keyboardOpen]) - useEffect(() => { - const unsubscribe = subscribe("clipboard-text", async (msg) => { - const data = msg as ClipboardMessage - - try { - const text = data.text || "" - - if (navigator.clipboard && window.isSecureContext) { - await navigator.clipboard.writeText(text) - } else { - const textarea = document.createElement("textarea") - textarea.value = text - document.body.appendChild(textarea) - textarea.select() - document.execCommand("copy") - document.body.removeChild(textarea) - } - } catch (err) { - console.error("Clipboard write failed", err) - } + const unsubscribe = subscribe("clipboard-text", async (msg) => { + const data = msg as ClipboardMessage + + try { + const text = data.text || "" + + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text) + } else { + const textarea = document.createElement("textarea") + textarea.value = text + document.body.appendChild(textarea) + textarea.select() + document.execCommand("copy") + document.body.removeChild(textarea) + } + } catch (err) { + console.error("Clipboard write failed", err) } - ) - - return () => unsubscribe() - }, [subscribe]) + }) + + return () => unsubscribe() + }, [subscribe]) const toggleKeyboard = () => { setKeyboardOpen((prev) => !prev) @@ -99,29 +97,29 @@ function TrackpadPage() { // Release after short delay to simulate click setTimeout(() => send({ type: "click", button, press: false }), 50) } - + const handleCopy = () => { - // copy from SERVER → CLIENT - send({ type: "clipboard-pull" }) - } - - const handlePaste = async () => { - // paste from CLIENT → SERVER - try { - let text = "" - - if (navigator.clipboard && window.isSecureContext) { - text = await navigator.clipboard.readText() - } else { - text = window.getSelection()?.toString() || "" - } - - send({ - type: "clipboard-push", - text, - }) - } catch (err) { - console.error("Paste failed", err) + // copy from SERVER → CLIENT + send({ type: "clipboard-pull" }) + } + + const handlePaste = async () => { + // paste from CLIENT → SERVER + try { + let text = "" + + if (navigator.clipboard && window.isSecureContext) { + text = await navigator.clipboard.readText() + } else { + text = window.getSelection()?.toString() || "" + } + + send({ + type: "clipboard-push", + text, + }) + } catch (err) { + console.error("Paste failed", err) } } diff --git a/src/server/InputHandler.ts b/src/server/InputHandler.ts index 2bc3807e..3659f9ab 100644 --- a/src/server/InputHandler.ts +++ b/src/server/InputHandler.ts @@ -209,9 +209,9 @@ export class InputHandler { case "clipboard-push": { if (msg.text) { // TEMP: fallback using typing instead of real clipboard - try{ - await keyboard.type(msg.text) - }catch(err) { + try { + await keyboard.type(msg.text) + } catch (err) { console.error("Clipboard push failed:", err) } } @@ -235,7 +235,7 @@ export class InputHandler { // ❗ send back to client (IMPORTANT) this.sendToClient({ type: "clipboard-text", - text: "CLIPBOARD_DATA_UNAVAILABLE", + text: "CLIPBOARD_DATA_UNAVAILABLE", }) break } diff --git a/src/server/websocket.ts b/src/server/websocket.ts index bd953b95..e53369dd 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -117,14 +117,11 @@ export async function createWsServer( token: string | null, isLocal: boolean, ) => { - const inputHandler = new InputHandler( - (msg) => { - if (ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify(msg)) - } - }, - inputThrottleMs - ) + const inputHandler = new InputHandler((msg) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify(msg)) + } + }, inputThrottleMs) // Localhost: only store token if it's already known (trusted scan) // Remote: token is already validated in the upgrade handler logger.info(`Client connected from ${request.socket.remoteAddress}`) @@ -353,7 +350,7 @@ export async function createWsServer( "copy", "paste", "clipboard-push", - "clipboard-pull", + "clipboard-pull", ] if (!msg.type || !VALID_INPUT_TYPES.includes(msg.type)) { logger.warn(`Unknown message type: ${msg.type}`) From 544bd769675aa0accd34489df2a2bc3cfdeef9c9 Mon Sep 17 00:00:00 2001 From: AnshuPriya-1 Date: Tue, 17 Mar 2026 23:31:16 +0530 Subject: [PATCH 5/7] fix: add fallback to server-side paste when clipboard read fails --- src/routes/trackpad.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/routes/trackpad.tsx b/src/routes/trackpad.tsx index d27e3913..c24ff456 100644 --- a/src/routes/trackpad.tsx +++ b/src/routes/trackpad.tsx @@ -120,6 +120,9 @@ function TrackpadPage() { }) } catch (err) { console.error("Paste failed", err) + + // fallback to server-side paste + send({ type: "paste" }) } } From 33687232b04a4eff7d85060ebd26c0eaa8e22d18 Mon Sep 17 00:00:00 2001 From: AnshuPriya-1 Date: Wed, 18 Mar 2026 01:48:59 +0530 Subject: [PATCH 6/7] fix: handle touchcancel to prevent stuck gesture and input state --- src/components/Trackpad/TouchArea.tsx | 2 ++ src/hooks/useTrackpadGesture.ts | 29 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/components/Trackpad/TouchArea.tsx b/src/components/Trackpad/TouchArea.tsx index 03c367e8..16062095 100644 --- a/src/components/Trackpad/TouchArea.tsx +++ b/src/components/Trackpad/TouchArea.tsx @@ -7,6 +7,7 @@ interface TouchAreaProps { onTouchStart: (e: React.TouchEvent) => void onTouchMove: (e: React.TouchEvent) => void onTouchEnd: (e: React.TouchEvent) => void + onTouchCancel: (e: React.TouchEvent) => void } } @@ -32,6 +33,7 @@ export const TouchArea: React.FC = ({ onTouchStart={handleStart} onTouchMove={handlers.onTouchMove} onTouchEnd={handlers.onTouchEnd} + onTouchCancel={handlers.onTouchCancel} onMouseDown={handlePreventFocus} >
diff --git a/src/hooks/useTrackpadGesture.ts b/src/hooks/useTrackpadGesture.ts index 66f801d8..16d467d2 100644 --- a/src/hooks/useTrackpadGesture.ts +++ b/src/hooks/useTrackpadGesture.ts @@ -254,12 +254,41 @@ export const useTrackpadGesture = ( } } + const handleTouchCancel = () => { + // Clear all active touches + ongoingTouches.current.clear() + + // Reset gesture state + setIsTracking(false) + moved.current = false + releasedCount.current = 0 + + // Reset pinch state + lastPinchDist.current = null + pinching.current = false + + // Clear dragging timeout if exists + if (draggingTimeout.current) { + clearTimeout(draggingTimeout.current) + draggingTimeout.current = null + } + + // Release drag if active + if (dragging.current) { + dragging.current = false + } + + // 🔥 Safety: ensure no stuck mouse state + send({ type: "click", button: "left", press: false }) + } + return { isTracking, handlers: { onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, + onTouchCancel: handleTouchCancel, }, } } From 091b06b9ade5d47cf4fce99149a5b8b4edf55f8a Mon Sep 17 00:00:00 2001 From: AnshuPriya-1 Date: Tue, 24 Mar 2026 08:31:39 +0530 Subject: [PATCH 7/7] chore: clarify clipboard fallback and add TODO comments --- src/server/InputHandler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server/InputHandler.ts b/src/server/InputHandler.ts index 3659f9ab..e4ce9644 100644 --- a/src/server/InputHandler.ts +++ b/src/server/InputHandler.ts @@ -209,6 +209,7 @@ export class InputHandler { case "clipboard-push": { if (msg.text) { // TEMP: fallback using typing instead of real clipboard + // TODO: Replace with native clipboard write try { await keyboard.type(msg.text) } catch (err) { @@ -235,7 +236,7 @@ export class InputHandler { // ❗ send back to client (IMPORTANT) this.sendToClient({ type: "clipboard-text", - text: "CLIPBOARD_DATA_UNAVAILABLE", + text: "Clipboard data temporarily unavailable", }) break }