import { ref, onBeforeUnmount } from "vue"; /** * RDP mouse event flags — match the Go constants in internal/rdp/input.go */ export const MouseFlag = { Move: 0x0800, Button1: 0x1000, // Left Button2: 0x2000, // Right Button3: 0x4000, // Middle Down: 0x8000, Wheel: 0x0200, WheelNeg: 0x0100, HWheel: 0x0400, } as const; /** * JavaScript KeyboardEvent.code → RDP scancode mapping. * Mirrors the Go ScancodeMap in internal/rdp/input.go. */ export const ScancodeMap: Record = { // Row 0: Escape + Function keys Escape: 0x0001, F1: 0x003b, F2: 0x003c, F3: 0x003d, F4: 0x003e, F5: 0x003f, F6: 0x0040, F7: 0x0041, F8: 0x0042, F9: 0x0043, F10: 0x0044, F11: 0x0057, F12: 0x0058, // Row 1: Number row Backquote: 0x0029, Digit1: 0x0002, Digit2: 0x0003, Digit3: 0x0004, Digit4: 0x0005, Digit5: 0x0006, Digit6: 0x0007, Digit7: 0x0008, Digit8: 0x0009, Digit9: 0x000a, Digit0: 0x000b, Minus: 0x000c, Equal: 0x000d, Backspace: 0x000e, // Row 2: QWERTY Tab: 0x000f, KeyQ: 0x0010, KeyW: 0x0011, KeyE: 0x0012, KeyR: 0x0013, KeyT: 0x0014, KeyY: 0x0015, KeyU: 0x0016, KeyI: 0x0017, KeyO: 0x0018, KeyP: 0x0019, BracketLeft: 0x001a, BracketRight: 0x001b, Backslash: 0x002b, // Row 3: Home row CapsLock: 0x003a, KeyA: 0x001e, KeyS: 0x001f, KeyD: 0x0020, KeyF: 0x0021, KeyG: 0x0022, KeyH: 0x0023, KeyJ: 0x0024, KeyK: 0x0025, KeyL: 0x0026, Semicolon: 0x0027, Quote: 0x0028, Enter: 0x001c, // Row 4: Bottom row ShiftLeft: 0x002a, KeyZ: 0x002c, KeyX: 0x002d, KeyC: 0x002e, KeyV: 0x002f, KeyB: 0x0030, KeyN: 0x0031, KeyM: 0x0032, Comma: 0x0033, Period: 0x0034, Slash: 0x0035, ShiftRight: 0x0036, // Row 5: Bottom modifiers + space ControlLeft: 0x001d, MetaLeft: 0xe05b, AltLeft: 0x0038, Space: 0x0039, AltRight: 0xe038, MetaRight: 0xe05c, ContextMenu: 0xe05d, ControlRight: 0xe01d, // Navigation cluster PrintScreen: 0xe037, ScrollLock: 0x0046, Pause: 0x0045, Insert: 0xe052, Home: 0xe047, PageUp: 0xe049, Delete: 0xe053, End: 0xe04f, PageDown: 0xe051, // Arrow keys ArrowUp: 0xe048, ArrowLeft: 0xe04b, ArrowDown: 0xe050, ArrowRight: 0xe04d, // Numpad NumLock: 0x0045, NumpadDivide: 0xe035, NumpadMultiply: 0x0037, NumpadSubtract: 0x004a, Numpad7: 0x0047, Numpad8: 0x0048, Numpad9: 0x0049, NumpadAdd: 0x004e, Numpad4: 0x004b, Numpad5: 0x004c, Numpad6: 0x004d, Numpad1: 0x004f, Numpad2: 0x0050, Numpad3: 0x0051, NumpadEnter: 0xe01c, Numpad0: 0x0052, NumpadDecimal: 0x0053, }; /** * Look up the RDP scancode for a JS KeyboardEvent.code string. */ export function jsKeyToScancode(code: string): number | null { return ScancodeMap[code] ?? null; } export interface UseRdpReturn { /** Whether the RDP session is connected (mock: always true after init) */ connected: ReturnType>; /** Whether keyboard capture is enabled */ keyboardGrabbed: ReturnType>; /** Whether clipboard sync is enabled */ clipboardSync: ReturnType>; /** Fetch the current frame as RGBA ImageData */ fetchFrame: (sessionId: string) => Promise; /** Send a mouse event to the backend */ sendMouse: ( sessionId: string, x: number, y: number, flags: number, ) => void; /** Send a key event to the backend */ sendKey: (sessionId: string, code: string, pressed: boolean) => void; /** Send clipboard text to the remote session */ sendClipboard: (sessionId: string, text: string) => void; /** Start the frame rendering loop */ startFrameLoop: ( sessionId: string, canvas: HTMLCanvasElement, width: number, height: number, ) => void; /** Stop the frame rendering loop */ stopFrameLoop: () => void; /** Toggle keyboard grab */ toggleKeyboardGrab: () => void; /** Toggle clipboard sync */ toggleClipboardSync: () => void; } /** * Composable that manages an RDP session's rendering and input. * * All backend calls are currently stubs that will be replaced with * Wails bindings once the Go RDP service is wired up. */ export function useRdp(): UseRdpReturn { const connected = ref(false); const keyboardGrabbed = ref(false); const clipboardSync = ref(false); let animFrameId: number | null = null; let frameCount = 0; /** * Fetch the current frame from the backend. * TODO: Replace with Wails binding — RDPService.GetFrame(sessionId) * Mock: generates a gradient test pattern. */ async function fetchFrame( sessionId: string, width = 1920, height = 1080, ): Promise { void sessionId; // Mock: generate a test frame with animated gradient const imageData = new ImageData(width, height); const data = imageData.data; const t = Date.now() / 1000; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const i = (y * width + x) * 4; const nx = x / width; const ny = y / height; const diag = (nx + ny) / 2; data[i + 0] = Math.floor(20 + diag * 40); // R data[i + 1] = Math.floor(25 + (1 - diag) * 30); // G data[i + 2] = Math.floor(80 + diag * 100); // B data[i + 3] = 255; // A // Grid lines every 100px if (x % 100 === 0 || y % 100 === 0) { data[i + 0] = Math.min(data[i + 0] + 20, 255); data[i + 1] = Math.min(data[i + 1] + 20, 255); data[i + 2] = Math.min(data[i + 2] + 20, 255); } } } // Animated pulsing circle at center const cx = width / 2; const cy = height / 2; const radius = 40 + 20 * Math.sin(t * 2); for (let dy = -70; dy <= 70; dy++) { for (let dx = -70; dx <= 70; dx++) { const dist = Math.sqrt(dx * dx + dy * dy); if (dist <= radius && dist >= radius - 4) { const px = Math.floor(cx + dx); const py = Math.floor(cy + dy); if (px >= 0 && px < width && py >= 0 && py < height) { const i = (py * width + px) * 4; data[i + 0] = 88; data[i + 1] = 166; data[i + 2] = 255; data[i + 3] = 255; } } } } return imageData; } /** * Send a mouse event. * TODO: Replace with Wails binding — RDPService.SendMouse(sessionId, x, y, flags) */ function sendMouse( sessionId: string, x: number, y: number, flags: number, ): void { void sessionId; void x; void y; void flags; // Mock: no-op — will call Wails binding when wired } /** * Send a key event, mapping JS code to RDP scancode. * TODO: Replace with Wails binding — RDPService.SendKey(sessionId, scancode, pressed) */ function sendKey( sessionId: string, code: string, pressed: boolean, ): void { const scancode = jsKeyToScancode(code); if (scancode === null) return; void sessionId; void pressed; // Mock: no-op — will call Wails binding when wired } /** * Send clipboard text to the remote session. * TODO: Replace with Wails binding — RDPService.SendClipboard(sessionId, text) */ function sendClipboard(sessionId: string, text: string): void { void sessionId; void text; // Mock: no-op } /** * Start the rendering loop. Fetches frames and draws them on the canvas * using requestAnimationFrame. */ function startFrameLoop( sessionId: string, canvas: HTMLCanvasElement, width: number, height: number, ): void { connected.value = true; const ctx = canvas.getContext("2d"); if (!ctx) return; canvas.width = width; canvas.height = height; function renderLoop(): void { frameCount++; // Throttle to ~30fps (skip every other frame at 60fps rAF) if (frameCount % 2 === 0) { fetchFrame(sessionId, width, height).then((imageData) => { if (imageData && ctx) { ctx.putImageData(imageData, 0, 0); } }); } animFrameId = requestAnimationFrame(renderLoop); } animFrameId = requestAnimationFrame(renderLoop); } /** * Stop the rendering loop. */ function stopFrameLoop(): void { if (animFrameId !== null) { cancelAnimationFrame(animFrameId); animFrameId = null; } connected.value = false; } function toggleKeyboardGrab(): void { keyboardGrabbed.value = !keyboardGrabbed.value; } function toggleClipboardSync(): void { clipboardSync.value = !clipboardSync.value; } onBeforeUnmount(() => { stopFrameLoop(); }); return { connected, keyboardGrabbed, clipboardSync, fetchFrame, sendMouse, sendKey, sendClipboard, startFrameLoop, stopFrameLoop, toggleKeyboardGrab, toggleClipboardSync, }; }