ai-remote 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.
Files changed (114) hide show
  1. package/README.md +50 -0
  2. package/dist/index.js +7 -0
  3. package/dist/index.js.map +7 -0
  4. package/dist/protocols/index.js +35 -0
  5. package/dist/protocols/index.js.map +7 -0
  6. package/dist/protocols/rdp/buffer.js +170 -0
  7. package/dist/protocols/rdp/buffer.js.map +7 -0
  8. package/dist/protocols/rdp/caps.js +260 -0
  9. package/dist/protocols/rdp/caps.js.map +7 -0
  10. package/dist/protocols/rdp/cert.js +135 -0
  11. package/dist/protocols/rdp/cert.js.map +7 -0
  12. package/dist/protocols/rdp/client.js +1068 -0
  13. package/dist/protocols/rdp/client.js.map +7 -0
  14. package/dist/protocols/rdp/cliprdr.js +240 -0
  15. package/dist/protocols/rdp/cliprdr.js.map +7 -0
  16. package/dist/protocols/rdp/credssp.js +254 -0
  17. package/dist/protocols/rdp/credssp.js.map +7 -0
  18. package/dist/protocols/rdp/crypto.js +369 -0
  19. package/dist/protocols/rdp/crypto.js.map +7 -0
  20. package/dist/protocols/rdp/display.js +589 -0
  21. package/dist/protocols/rdp/display.js.map +7 -0
  22. package/dist/protocols/rdp/gcc.js +132 -0
  23. package/dist/protocols/rdp/gcc.js.map +7 -0
  24. package/dist/protocols/rdp/keymap.js +132 -0
  25. package/dist/protocols/rdp/keymap.js.map +7 -0
  26. package/dist/protocols/rdp/mcs.js +223 -0
  27. package/dist/protocols/rdp/mcs.js.map +7 -0
  28. package/dist/protocols/rdp/ntlm.js +305 -0
  29. package/dist/protocols/rdp/ntlm.js.map +7 -0
  30. package/dist/protocols/rdp/pdu.js +444 -0
  31. package/dist/protocols/rdp/pdu.js.map +7 -0
  32. package/dist/protocols/rdp/rail.js +383 -0
  33. package/dist/protocols/rdp/rail.js.map +7 -0
  34. package/dist/protocols/rdp/rle.js +276 -0
  35. package/dist/protocols/rdp/rle.js.map +7 -0
  36. package/dist/protocols/rdp/sec.js +156 -0
  37. package/dist/protocols/rdp/sec.js.map +7 -0
  38. package/dist/protocols/rdp/session.js +307 -0
  39. package/dist/protocols/rdp/session.js.map +7 -0
  40. package/dist/protocols/rdp/tls.js +354 -0
  41. package/dist/protocols/rdp/tls.js.map +7 -0
  42. package/dist/protocols/rdp/vchannel.js +59 -0
  43. package/dist/protocols/rdp/vchannel.js.map +7 -0
  44. package/dist/protocols/rdp/x224.js +110 -0
  45. package/dist/protocols/rdp/x224.js.map +7 -0
  46. package/dist/protocols/ssh/kex.js +177 -0
  47. package/dist/protocols/ssh/kex.js.map +7 -0
  48. package/dist/protocols/ssh/messages.js +55 -0
  49. package/dist/protocols/ssh/messages.js.map +7 -0
  50. package/dist/protocols/ssh/session.js +608 -0
  51. package/dist/protocols/ssh/session.js.map +7 -0
  52. package/dist/protocols/ssh/terminal.js +191 -0
  53. package/dist/protocols/ssh/terminal.js.map +7 -0
  54. package/dist/protocols/ssh/transport.js +611 -0
  55. package/dist/protocols/ssh/transport.js.map +7 -0
  56. package/dist/protocols/ssh/wire.js +189 -0
  57. package/dist/protocols/ssh/wire.js.map +7 -0
  58. package/dist/protocols/types.js +1 -0
  59. package/dist/protocols/types.js.map +7 -0
  60. package/dist/protocols/vnc/input.js +16 -0
  61. package/dist/protocols/vnc/input.js.map +7 -0
  62. package/dist/protocols/vnc/session.js +259 -0
  63. package/dist/protocols/vnc/session.js.map +7 -0
  64. package/dist/shared/connection.js +114 -0
  65. package/dist/shared/connection.js.map +7 -0
  66. package/dist/shared/device.js +1 -0
  67. package/dist/shared/device.js.map +7 -0
  68. package/dist/shared/hosts.js +133 -0
  69. package/dist/shared/hosts.js.map +7 -0
  70. package/dist/shared/icons.js +42 -0
  71. package/dist/shared/icons.js.map +7 -0
  72. package/dist/shared/protocol.js +40 -0
  73. package/dist/shared/protocol.js.map +7 -0
  74. package/dist/shared/signals.js +42 -0
  75. package/dist/shared/signals.js.map +7 -0
  76. package/package.json +82 -0
  77. package/src/index.ts +14 -0
  78. package/src/protocols/index.ts +72 -0
  79. package/src/protocols/rdp/buffer.ts +204 -0
  80. package/src/protocols/rdp/caps.ts +304 -0
  81. package/src/protocols/rdp/cert.ts +190 -0
  82. package/src/protocols/rdp/client.ts +1363 -0
  83. package/src/protocols/rdp/cliprdr.ts +344 -0
  84. package/src/protocols/rdp/credssp.ts +319 -0
  85. package/src/protocols/rdp/crypto.ts +416 -0
  86. package/src/protocols/rdp/display.ts +750 -0
  87. package/src/protocols/rdp/gcc.ts +162 -0
  88. package/src/protocols/rdp/keymap.ts +68 -0
  89. package/src/protocols/rdp/mcs.ts +283 -0
  90. package/src/protocols/rdp/ntlm.ts +380 -0
  91. package/src/protocols/rdp/pdu.ts +497 -0
  92. package/src/protocols/rdp/rail.ts +480 -0
  93. package/src/protocols/rdp/rle.ts +343 -0
  94. package/src/protocols/rdp/sec.ts +184 -0
  95. package/src/protocols/rdp/session.ts +393 -0
  96. package/src/protocols/rdp/tls.ts +452 -0
  97. package/src/protocols/rdp/vchannel.ts +89 -0
  98. package/src/protocols/rdp/x224.ts +139 -0
  99. package/src/protocols/ssh/kex.ts +227 -0
  100. package/src/protocols/ssh/messages.ts +54 -0
  101. package/src/protocols/ssh/session.ts +878 -0
  102. package/src/protocols/ssh/terminal.ts +242 -0
  103. package/src/protocols/ssh/transport.ts +732 -0
  104. package/src/protocols/ssh/wire.ts +220 -0
  105. package/src/protocols/types.ts +146 -0
  106. package/src/protocols/vnc/input.ts +13 -0
  107. package/src/protocols/vnc/novnc.d.ts +75 -0
  108. package/src/protocols/vnc/session.ts +367 -0
  109. package/src/shared/connection.ts +206 -0
  110. package/src/shared/device.ts +28 -0
  111. package/src/shared/hosts.ts +240 -0
  112. package/src/shared/icons.ts +49 -0
  113. package/src/shared/protocol.ts +55 -0
  114. package/src/shared/signals.ts +67 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/protocols/rdp/crypto.ts"],
4
+ "sourcesContent": ["// ---------------------------------------------------------------------------\n// Crypto primitives for the RDP Standard Security layer (MS-RDPBCGR 5.3).\n//\n// RDP's own encryption layer predates anything WebCrypto exposes: it needs\n// synchronous MD5 and SHA-1 for a per-PDU MAC, RC4 stream state that persists\n// across PDUs, and textbook RSA against the key the server hands out in its\n// certificate. All of it is small, so it lives here rather than as a\n// dependency, and every function is deliberately synchronous -- the packet path\n// runs inside WebSocket message handlers.\n// ---------------------------------------------------------------------------\n\n// --- MD5 -------------------------------------------------------------------\n\nconst MD5_SHIFTS = [\n 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,\n 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,\n 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,\n 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,\n];\n\n// K[i] = floor(abs(sin(i + 1)) * 2^32) -- generated so the table cannot be mistyped.\nconst MD5_K = new Uint32Array(\n Array.from({ length: 64 }, (_, i) => Math.floor(Math.abs(Math.sin(i + 1)) * 4294967296))\n);\n\nfunction rotl32(value: Uint8Array, count: number) {\n return (value << count) | (value >>> (32 - count));\n}\n\nexport function md5(message: Uint8Array) {\n const bitLength = message.length * 8;\n const blockCount = Math.floor((message.length + 8) / 64) + 1;\n const padded = new Uint8Array(blockCount * 64);\n padded.set(message);\n padded[message.length] = 0x80;\n\n // Length is appended as a 64-bit little-endian bit count.\n const tail = new DataView(padded.buffer, padded.byteLength - 8, 8);\n tail.setUint32(0, bitLength >>> 0, true);\n tail.setUint32(4, Math.floor(bitLength / 4294967296), true);\n\n let a0 = 0x67452301, b0 = 0xefcdab89, c0 = 0x98badcfe, d0 = 0x10325476;\n const words = new Uint32Array(16);\n const view = new DataView(padded.buffer, padded.byteOffset, padded.byteLength);\n\n for (let block = 0; block < blockCount; block++) {\n for (let i = 0; i < 16; i++) words[i] = view.getUint32(block * 64 + i * 4, true);\n\n let a = a0, b = b0, c = c0, d = d0;\n for (let i = 0; i < 64; i++) {\n let f;\n let g;\n if (i < 16) {\n f = (b & c) | (~b & d);\n g = i;\n } else if (i < 32) {\n f = (d & b) | (~d & c);\n g = (5 * i + 1) % 16;\n } else if (i < 48) {\n f = b ^ c ^ d;\n g = (3 * i + 5) % 16;\n } else {\n f = c ^ (b | ~d);\n g = (7 * i) % 16;\n }\n\n const temp = d;\n d = c;\n c = b;\n const sum = (a + f + MD5_K[i] + words[g]) | 0;\n b = (b + rotl32(sum, MD5_SHIFTS[i])) | 0;\n a = temp;\n }\n\n a0 = (a0 + a) | 0;\n b0 = (b0 + b) | 0;\n c0 = (c0 + c) | 0;\n d0 = (d0 + d) | 0;\n }\n\n const digest = new Uint8Array(16);\n const out = new DataView(digest.buffer);\n out.setUint32(0, a0 >>> 0, true);\n out.setUint32(4, b0 >>> 0, true);\n out.setUint32(8, c0 >>> 0, true);\n out.setUint32(12, d0 >>> 0, true);\n return digest;\n}\n\n// --- SHA-1 -----------------------------------------------------------------\n\nexport function sha1(message: Uint8Array) {\n const bitLength = message.length * 8;\n const blockCount = Math.floor((message.length + 8) / 64) + 1;\n const padded = new Uint8Array(blockCount * 64);\n padded.set(message);\n padded[message.length] = 0x80;\n\n const tail = new DataView(padded.buffer, padded.byteLength - 8, 8);\n tail.setUint32(0, Math.floor(bitLength / 4294967296));\n tail.setUint32(4, bitLength >>> 0);\n\n let h0 = 0x67452301, h1 = 0xefcdab89, h2 = 0x98badcfe, h3 = 0x10325476, h4 = 0xc3d2e1f0;\n const words = new Uint32Array(80);\n const view = new DataView(padded.buffer, padded.byteOffset, padded.byteLength);\n\n for (let block = 0; block < blockCount; block++) {\n for (let i = 0; i < 16; i++) words[i] = view.getUint32(block * 64 + i * 4);\n for (let i = 16; i < 80; i++) {\n words[i] = rotl32(words[i - 3] ^ words[i - 8] ^ words[i - 14] ^ words[i - 16], 1);\n }\n\n let a = h0, b = h1, c = h2, d = h3, e = h4;\n for (let i = 0; i < 80; i++) {\n let f;\n let k;\n if (i < 20) {\n f = (b & c) | (~b & d);\n k = 0x5a827999;\n } else if (i < 40) {\n f = b ^ c ^ d;\n k = 0x6ed9eba1;\n } else if (i < 60) {\n f = (b & c) | (b & d) | (c & d);\n k = 0x8f1bbcdc;\n } else {\n f = b ^ c ^ d;\n k = 0xca62c1d6;\n }\n\n const temp = (rotl32(a, 5) + f + e + k + words[i]) | 0;\n e = d;\n d = c;\n c = rotl32(b, 30);\n b = a;\n a = temp;\n }\n\n h0 = (h0 + a) | 0;\n h1 = (h1 + b) | 0;\n h2 = (h2 + c) | 0;\n h3 = (h3 + d) | 0;\n h4 = (h4 + e) | 0;\n }\n\n const digest = new Uint8Array(20);\n const out = new DataView(digest.buffer);\n [h0, h1, h2, h3, h4].forEach((word, i) => out.setUint32(i * 4, word >>> 0));\n return digest;\n}\n\n// --- RC4 -------------------------------------------------------------------\n\n/** RC4 keystream position. RDP re-keys every 4096 PDUs, so this is mutable. */\nexport interface Rc4State {\n s: Uint8Array;\n i: number;\n j: number;\n}\n\n/** The five values MS-RDPBCGR 5.3.5 derives from the two randoms. */\nexport interface SessionKeys {\n macKey: Uint8Array;\n keyLength: number;\n is40Bit: boolean;\n initialDecryptKey: Uint8Array;\n initialEncryptKey: Uint8Array;\n decryptKey: Uint8Array;\n encryptKey: Uint8Array;\n}\n\nexport function rc4Init(key: Uint8Array): Rc4State {\n const s = new Uint8Array(256);\n for (let i = 0; i < 256; i++) s[i] = i;\n\n let j = 0;\n for (let i = 0; i < 256; i++) {\n j = (j + (s[i] ?? 0) + (key[i % key.length] ?? 0)) & 0xff;\n const t = s[i] ?? 0;\n s[i] = s[j] ?? 0;\n s[j] = t;\n }\n return { s, i: 0, j: 0 };\n}\n\n/** RC4 is symmetric, so this both encrypts and decrypts. State advances. */\nexport function rc4Crypt(state, data: Uint8Array) {\n const out = new Uint8Array(data.length);\n const { s } = state;\n let { i, j } = state;\n\n for (let n = 0; n < data.length; n++) {\n i = (i + 1) & 0xff;\n j = (j + s[i]) & 0xff;\n const t = s[i];\n s[i] = s[j];\n s[j] = t;\n out[n] = data[n] ^ s[(s[i] + s[j]) & 0xff];\n }\n\n state.i = i;\n state.j = j;\n return out;\n}\n\n// --- RSA -------------------------------------------------------------------\n\nfunction bigIntFromLe(bytes: Uint8Array) {\n let value = 0n;\n for (let i = bytes.length - 1; i >= 0; i--) value = (value << 8n) | BigInt(bytes[i]);\n return value;\n}\n\nfunction bigIntToLe(value: Uint8Array, length: number) {\n const out = new Uint8Array(length);\n let remaining = value;\n for (let i = 0; i < length; i++) {\n out[i] = Number(remaining & 0xffn);\n remaining >>= 8n;\n }\n return out;\n}\n\nfunction modPow(base: bigint, exponent: bigint, modulus: bigint) {\n let result = 1n;\n let b = base % modulus;\n let e = exponent;\n while (e > 0n) {\n if (e & 1n) result = (result * b) % modulus;\n b = (b * b) % modulus;\n e >>= 1n;\n }\n return result;\n}\n\n/**\n * Textbook RSA over little-endian buffers, which is what RDP's Security\n * Exchange PDU carries: the client random is raised to the server's public\n * exponent with no padding scheme (MS-RDPBCGR 5.3.4.1).\n */\nexport function rsaEncrypt(dataLe: Uint8Array, modulusLe: Uint8Array, exponentLe: Uint8Array) {\n const modulus = bigIntFromLe(modulusLe);\n const exponent = bigIntFromLe(exponentLe);\n const value = bigIntFromLe(dataLe);\n if (value >= modulus) throw new Error('RDP: client random is larger than the server RSA modulus');\n return bigIntToLe(modPow(value, exponent, modulus), modulusLe.length);\n}\n\n/**\n * RSAES-PKCS1-v1_5 encryption over big-endian buffers, which is what TLS uses\n * for an RSA key exchange. WebCrypto only offers OAEP, so the padding is built\n * here and the modular exponentiation reuses the same BigInt path as RDP.\n */\nexport function rsaEncryptPkcs1(message: Uint8Array, modulusBe: Uint8Array, exponentBe: Uint8Array) {\n const bigIntFromBe = (bytes: Uint8Array) => {\n let value = 0n;\n for (const byte of bytes) value = (value << 8n) | BigInt(byte);\n return value;\n };\n\n const modulus = bigIntFromBe(modulusBe);\n const exponent = bigIntFromBe(exponentBe);\n const keyLength = modulusBe.length;\n if (message.length > keyLength - 11) {\n throw new Error('TLS: RSA message is too long for this key');\n }\n\n // EM = 0x00 || 0x02 || non-zero padding || 0x00 || message\n const block = new Uint8Array(keyLength);\n block[0] = 0x00;\n block[1] = 0x02;\n const paddingLength = keyLength - message.length - 3;\n const padding = new Uint8Array(paddingLength);\n crypto.getRandomValues(padding);\n for (let i = 0; i < paddingLength; i++) {\n // A padding byte of zero would be read as the separator.\n while (padding[i] === 0) padding[i] = Math.max(1, crypto.getRandomValues(new Uint8Array(1))[0]);\n block[2 + i] = padding[i];\n }\n block[2 + paddingLength] = 0x00;\n block.set(message, 3 + paddingLength);\n\n const cipher = modPow(bigIntFromBe(block), exponent, modulus);\n const out = new Uint8Array(keyLength);\n let remaining = cipher;\n for (let i = keyLength - 1; i >= 0; i--) {\n out[i] = Number(remaining & 0xffn);\n remaining >>= 8n;\n }\n return out;\n}\n\n// --- RDP key derivation (MS-RDPBCGR 5.3.5) ---------------------------------\n\nconst PAD1 = new Uint8Array(40).fill(0x36);\nconst PAD2 = new Uint8Array(48).fill(0x5c);\n\nexport const ENCRYPTION_METHOD = {\n NONE: 0x00000000,\n BIT_40: 0x00000001,\n BIT_128: 0x00000002,\n BIT_56: 0x00000008,\n FIPS: 0x00000010,\n};\n\nfunction concat(...parts) {\n const total = parts.reduce((sum, part) => sum + part.length, 0);\n const out = new Uint8Array(total);\n let offset = 0;\n for (const part of parts) {\n out.set(part, offset);\n offset += part.length;\n }\n return out;\n}\n\n/** SaltedHash(S, I) = MD5(S + SHA1(I + S + clientRandom + serverRandom)) */\nfunction saltedHash(secret: Uint8Array, salt: Uint8Array, clientRandom: Uint8Array, serverRandom: Uint8Array) {\n return md5(concat(secret, sha1(concat(salt, secret, clientRandom, serverRandom))));\n}\n\n/** FinalHash(K) = MD5(K + clientRandom + serverRandom) */\nfunction finalHash(key: Uint8Array, clientRandom: Uint8Array, serverRandom: Uint8Array) {\n return md5(concat(key, clientRandom, serverRandom));\n}\n\nconst SALT_A = new Uint8Array([0x41]);\nconst SALT_BB = new Uint8Array([0x42, 0x42]);\nconst SALT_CCC = new Uint8Array([0x43, 0x43, 0x43]);\nconst SALT_X = new Uint8Array([0x58]);\nconst SALT_YY = new Uint8Array([0x59, 0x59]);\nconst SALT_ZZZ = new Uint8Array([0x5a, 0x5a, 0x5a]);\n\n/**\n * A 40-bit or 56-bit session key is a 64-bit key with known leading bytes;\n * only the remaining entropy is secret (MS-RDPBCGR 5.3.5.1).\n */\nfunction reduceKey(key: Uint8Array, keyLength, is40Bit) {\n if (keyLength !== 8) return key.slice(0, keyLength);\n\n const reduced = key.slice(0, 8);\n reduced[0] = 0xd1;\n if (is40Bit) {\n reduced[1] = 0x26;\n reduced[2] = 0x9e;\n }\n return reduced;\n}\n\nexport function keyLengthForMethod(encryptionMethod: number) {\n if (encryptionMethod === ENCRYPTION_METHOD.BIT_40) return 8;\n if (encryptionMethod === ENCRYPTION_METHOD.BIT_56) return 8;\n return 16;\n}\n\n/**\n * Derive the MAC key and both directional RC4 keys from the two random values\n * exchanged during connection.\n */\nexport function deriveSessionKeys(clientRandom: Uint8Array, serverRandom: Uint8Array, encryptionMethod: number) {\n const preMasterSecret = concat(clientRandom.subarray(0, 24), serverRandom.subarray(0, 24));\n\n const masterSecret = concat(\n saltedHash(preMasterSecret, SALT_A, clientRandom, serverRandom),\n saltedHash(preMasterSecret, SALT_BB, clientRandom, serverRandom),\n saltedHash(preMasterSecret, SALT_CCC, clientRandom, serverRandom)\n );\n\n const sessionKeyBlob = concat(\n saltedHash(masterSecret, SALT_X, clientRandom, serverRandom),\n saltedHash(masterSecret, SALT_YY, clientRandom, serverRandom),\n saltedHash(masterSecret, SALT_ZZZ, clientRandom, serverRandom)\n );\n\n const keyLength = keyLengthForMethod(encryptionMethod);\n const is40Bit = encryptionMethod === ENCRYPTION_METHOD.BIT_40;\n\n const applyReduction = (key: Uint8Array) => reduceKey(key, keyLength, is40Bit);\n\n const macKey = applyReduction(sessionKeyBlob.subarray(0, 16));\n const decryptKey = applyReduction(finalHash(sessionKeyBlob.subarray(16, 32), clientRandom, serverRandom));\n const encryptKey = applyReduction(finalHash(sessionKeyBlob.subarray(32, 48), clientRandom, serverRandom));\n\n return {\n macKey,\n keyLength,\n is40Bit,\n // Session-key updates always restart from these initial values.\n initialDecryptKey: decryptKey.slice(),\n initialEncryptKey: encryptKey.slice(),\n decryptKey,\n encryptKey,\n };\n}\n\n/**\n * MACSignature = First64Bits(MD5(macKey + Pad2 + SHA1(macKey + Pad1 + len + data)))\n */\nexport function macSignature(macKey, data: Uint8Array) {\n const lengthField = new Uint8Array(4);\n new DataView(lengthField.buffer).setUint32(0, data.length, true);\n const inner = sha1(concat(macKey, PAD1, lengthField, data));\n return md5(concat(macKey, PAD2, inner)).subarray(0, 8);\n}\n\n/**\n * Every 4096 PDUs the RC4 key is refreshed from the initial key\n * (MS-RDPBCGR 5.3.7.1). The refreshed key is salted with itself.\n */\nexport function updateSessionKey(initialKey, currentKey, keyLength, is40Bit) {\n const inner = sha1(concat(initialKey, PAD1, currentKey));\n const nextKey = md5(concat(initialKey, PAD2, inner)).slice(0, keyLength);\n\n const salted = rc4Crypt(rc4Init(nextKey), nextKey);\n return reduceKey(salted, keyLength, is40Bit);\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,589 @@
1
+ import { decodeBitmapRect } from "./rle";
2
+ import { TOGGLE_FLAG } from "./pdu";
3
+ const IS_MAC = /Mac|iPod|iPhone|iPad/.test(
4
+ globalThis.navigator?.platform || globalThis.navigator?.userAgent || ""
5
+ );
6
+ const COMMAND_TO_CONTROL = {
7
+ MetaLeft: "ControlLeft",
8
+ MetaRight: "ControlRight"
9
+ };
10
+ const WHEEL_STEP_PIXELS = 60;
11
+ const WHEEL_LINE_PIXELS = 16;
12
+ const TOUCH_SCROLL_STEP = 20;
13
+ const MAX_ZOOM = 8;
14
+ const MIN_ZOOM = 1;
15
+ class RdpDisplay {
16
+ constructor(container) {
17
+ this.container = container;
18
+ this.canvas = document.createElement("canvas");
19
+ this.canvas.tabIndex = 0;
20
+ this.canvas.style.display = "block";
21
+ this.canvas.style.position = "absolute";
22
+ this.canvas.style.outline = "none";
23
+ this.canvas.style.imageRendering = "auto";
24
+ this.canvas.style.boxShadow = "none";
25
+ this.context = this.canvas.getContext("2d", { alpha: false });
26
+ this.viewportElement = document.createElement("div");
27
+ this.viewportElement.style.position = "relative";
28
+ this.viewportElement.style.overflow = "hidden";
29
+ this.viewportElement.style.margin = "auto";
30
+ this.viewportElement.style.background = "#000";
31
+ this.viewportElement.style.boxShadow = "0 10px 30px rgba(0, 0, 0, 0.8)";
32
+ this.viewportElement.appendChild(this.canvas);
33
+ container.replaceChildren(this.viewportElement);
34
+ this.scrollHost = container.parentElement || container;
35
+ this.width = 0;
36
+ this.height = 0;
37
+ this.viewport = { x: 0, y: 0, width: 0, height: 0 };
38
+ this.scale = 1;
39
+ this.scaleViewport = true;
40
+ this.zoom = 1;
41
+ this.userZoomed = false;
42
+ this.viewOnly = false;
43
+ this.client = null;
44
+ this.palette = null;
45
+ this.cursorCache = /* @__PURE__ */ new Map();
46
+ this.heldKeys = /* @__PURE__ */ new Set();
47
+ this.keyboardSynced = false;
48
+ this.onWindowResize = () => this.applyScale();
49
+ window.addEventListener("resize", this.onWindowResize);
50
+ this.resizeObserver = typeof ResizeObserver === "function" ? new ResizeObserver(() => this.applyScale()) : null;
51
+ this.resizeObserver?.observe(this.scrollHost);
52
+ }
53
+ resize(width, height) {
54
+ this.width = width;
55
+ this.height = height;
56
+ this.canvas.width = width;
57
+ this.canvas.height = height;
58
+ this.context.fillStyle = "#000";
59
+ this.context.fillRect(0, 0, width, height);
60
+ this.setViewport({ x: 0, y: 0, width, height });
61
+ }
62
+ /**
63
+ * Choose the part of the desktop that is on screen. Everything else about
64
+ * the session -- painting, input coordinates, the remote pointer -- keeps
65
+ * working in desktop coordinates, so only the framing changes here.
66
+ */
67
+ setViewport({ x = 0, y = 0, width, height } = {}) {
68
+ if (!this.width || !this.height) return;
69
+ const left = Math.max(0, Math.min(this.width - 1, Math.floor(x)));
70
+ const top = Math.max(0, Math.min(this.height - 1, Math.floor(y)));
71
+ this.viewport = {
72
+ x: left,
73
+ y: top,
74
+ width: Math.max(1, Math.min(this.width - left, Math.floor(width || this.width))),
75
+ height: Math.max(1, Math.min(this.height - top, Math.floor(height || this.height)))
76
+ };
77
+ this.canvas.dataset.viewport = [
78
+ this.viewport.x,
79
+ this.viewport.y,
80
+ this.viewport.width,
81
+ this.viewport.height
82
+ ].join(",");
83
+ this.applyScale();
84
+ }
85
+ /** The space a fitted desktop has to fit into: the window, not the content. */
86
+ #available() {
87
+ const host = this.scrollHost;
88
+ return {
89
+ width: host.clientWidth || this.container.clientWidth || 1,
90
+ height: host.clientHeight || this.container.clientHeight || 1
91
+ };
92
+ }
93
+ /** The scale at which the whole desktop is on screen, letterboxed. */
94
+ #fitScale(available = this.#available()) {
95
+ const fitted = Math.min(
96
+ available.width / this.viewport.width,
97
+ available.height / this.viewport.height
98
+ );
99
+ return Number.isFinite(fitted) && fitted > 0 ? fitted : 1;
100
+ }
101
+ /**
102
+ * The zoom to open a session at.
103
+ *
104
+ * On a laptop the fitted view is the right one: the whole desktop is on
105
+ * screen and readable, and that is where this stays. On a phone it is not --
106
+ * a 1024x768 desktop fitted to a 375pt-wide window lands at about a third of
107
+ * full size, a band of unreadable pixels across the middle of a tall black
108
+ * screen. When fitting would land under LEGIBLE_SCALE the desktop is scaled
109
+ * to fill the window instead: the short edge matches exactly and the long one
110
+ * is panned. Everything is still reachable, and a pinch still gets back to
111
+ * the whole desktop. Legible first and complete second is the right way round
112
+ * on a screen this size.
113
+ */
114
+ #defaultZoom() {
115
+ return 1;
116
+ }
117
+ applyScale() {
118
+ if (!this.width || !this.height) return;
119
+ const viewport = this.viewport;
120
+ const available = this.#available();
121
+ const fitted = this.#fitScale(available);
122
+ if (!this.userZoomed) this.zoom = this.#defaultZoom();
123
+ const scale = (this.scaleViewport ? fitted : 1) * this.zoom;
124
+ this.scale = scale;
125
+ this.viewportElement.style.width = `${viewport.width * scale}px`;
126
+ this.viewportElement.style.height = `${viewport.height * scale}px`;
127
+ this.canvas.style.width = `${this.width * scale}px`;
128
+ this.canvas.style.height = `${this.height * scale}px`;
129
+ this.canvas.style.left = `${-viewport.x * scale}px`;
130
+ this.canvas.style.top = `${-viewport.y * scale}px`;
131
+ }
132
+ /**
133
+ * Zoom about a point, keeping whatever is under it in place. `focal` is in
134
+ * client coordinates -- the midpoint between two fingers, usually.
135
+ */
136
+ setZoom(next, focal = null) {
137
+ const clamped = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, next));
138
+ if (!Number.isFinite(clamped) || clamped === this.zoom) return;
139
+ const host = this.scrollHost;
140
+ const before = this.scale;
141
+ const bounds = host.getBoundingClientRect();
142
+ const anchorX = focal ? focal.x - bounds.left : bounds.width / 2;
143
+ const anchorY = focal ? focal.y - bounds.top : bounds.height / 2;
144
+ const contentX = host.scrollLeft + anchorX;
145
+ const contentY = host.scrollTop + anchorY;
146
+ this.zoom = clamped;
147
+ this.applyScale();
148
+ const ratio = before > 0 ? this.scale / before : 1;
149
+ host.scrollLeft = contentX * ratio - anchorX;
150
+ host.scrollTop = contentY * ratio - anchorY;
151
+ }
152
+ /** Move the visible window over the desktop. Returns what it actually used. */
153
+ panBy(dx, dy) {
154
+ const host = this.scrollHost;
155
+ const beforeX = host.scrollLeft;
156
+ const beforeY = host.scrollTop;
157
+ host.scrollLeft = beforeX - dx;
158
+ host.scrollTop = beforeY - dy;
159
+ return {
160
+ x: beforeX - host.scrollLeft,
161
+ y: beforeY - host.scrollTop
162
+ };
163
+ }
164
+ /** True when the desktop is larger than the window on either axis. */
165
+ canPan() {
166
+ if (this.zoom <= 1.05) return false;
167
+ const host = this.scrollHost;
168
+ return host.scrollWidth > host.clientWidth + 2 || host.scrollHeight > host.clientHeight + 2;
169
+ }
170
+ setScaleViewport(enabled) {
171
+ this.scaleViewport = enabled;
172
+ if (!enabled) this.userZoomed = false;
173
+ this.applyScale();
174
+ }
175
+ /** Back to the opening view: whole desktop fitted on screen. */
176
+ resetZoom() {
177
+ this.userZoomed = false;
178
+ this.zoom = 1;
179
+ this.applyScale();
180
+ const host = this.scrollHost;
181
+ host.scrollLeft = 0;
182
+ host.scrollTop = 0;
183
+ }
184
+ drawRects(rects, palette) {
185
+ if (palette) this.palette = palette;
186
+ for (const rect of rects) {
187
+ if (rect.width === 0 || rect.height === 0) continue;
188
+ try {
189
+ const rgba = decodeBitmapRect(rect, this.palette);
190
+ const image = new ImageData(rgba, rect.width, rect.height);
191
+ this.context.putImageData(image, rect.left, rect.top);
192
+ } catch (error) {
193
+ console.warn("[RDP] Dropped an undecodable bitmap rectangle", {
194
+ geometry: `${rect.width}x${rect.height}+${rect.left}+${rect.top}`,
195
+ bitsPerPixel: rect.bitsPerPixel,
196
+ compressed: rect.compressed,
197
+ error: error instanceof Error ? error.message : String(error)
198
+ });
199
+ }
200
+ }
201
+ }
202
+ // --- cursor ---------------------------------------------------------------
203
+ /**
204
+ * Turn an RDP pointer bitmap into a CSS cursor. RDP stores both masks
205
+ * bottom-up with rows padded to an even number of bytes, and a set AND-mask
206
+ * bit means "leave this pixel alone", which becomes transparency here.
207
+ */
208
+ #cursorFromPointer(pointer) {
209
+ const { width, height, xorBpp, xorMask, andMask, hotSpotX, hotSpotY } = pointer;
210
+ if (!width || !height) return null;
211
+ const xorBytesPerRow = Math.ceil(width * xorBpp / 8 / 2) * 2;
212
+ const andBytesPerRow = Math.ceil(width / 8 / 2) * 2;
213
+ const rgba = new Uint8ClampedArray(width * height * 4);
214
+ for (let y = 0; y < height; y++) {
215
+ const sourceRow = height - 1 - y;
216
+ for (let x = 0; x < width; x++) {
217
+ const andByte = andMask[sourceRow * andBytesPerRow + (x >> 3)];
218
+ const transparent = andByte !== void 0 && (andByte & 128 >> (x & 7)) !== 0;
219
+ let r = 0;
220
+ let g = 0;
221
+ let b = 0;
222
+ let a = transparent ? 0 : 255;
223
+ if (xorBpp === 1) {
224
+ const bit = xorMask[sourceRow * xorBytesPerRow + (x >> 3)] & 128 >> (x & 7);
225
+ r = g = b = bit ? 255 : 0;
226
+ } else {
227
+ const bytesPerPixel = xorBpp / 8;
228
+ const offset = sourceRow * xorBytesPerRow + x * bytesPerPixel;
229
+ if (xorBpp === 16) {
230
+ const value = xorMask[offset] | xorMask[offset + 1] << 8;
231
+ r = (value >> 11 & 31) * 255 / 31;
232
+ g = (value >> 5 & 63) * 255 / 63;
233
+ b = (value & 31) * 255 / 31;
234
+ } else {
235
+ b = xorMask[offset] || 0;
236
+ g = xorMask[offset + 1] || 0;
237
+ r = xorMask[offset + 2] || 0;
238
+ if (xorBpp === 32 && xorMask[offset + 3] !== void 0 && andMask.length === 0) {
239
+ a = xorMask[offset + 3];
240
+ }
241
+ }
242
+ }
243
+ const target = (y * width + x) * 4;
244
+ rgba[target] = r;
245
+ rgba[target + 1] = g;
246
+ rgba[target + 2] = b;
247
+ rgba[target + 3] = a;
248
+ }
249
+ }
250
+ const cursorCanvas = document.createElement("canvas");
251
+ cursorCanvas.width = width;
252
+ cursorCanvas.height = height;
253
+ cursorCanvas.getContext("2d").putImageData(new ImageData(rgba, width, height), 0, 0);
254
+ const hotX = Math.min(hotSpotX || 0, width - 1);
255
+ const hotY = Math.min(hotSpotY || 0, height - 1);
256
+ return `url(${cursorCanvas.toDataURL("image/png")}) ${hotX} ${hotY}, default`;
257
+ }
258
+ applyPointer(pointer) {
259
+ if (pointer.kind === "system") {
260
+ this.canvas.style.cursor = pointer.hidden ? "none" : "default";
261
+ return;
262
+ }
263
+ if (pointer.kind === "position") {
264
+ return;
265
+ }
266
+ if (pointer.kind === "cached") {
267
+ const cached = this.cursorCache.get(pointer.cacheIndex);
268
+ if (cached) this.canvas.style.cursor = cached;
269
+ return;
270
+ }
271
+ if (pointer.kind === "bitmap") {
272
+ try {
273
+ const cursor = this.#cursorFromPointer(pointer);
274
+ if (!cursor) return;
275
+ this.cursorCache.set(pointer.cacheIndex, cursor);
276
+ this.canvas.style.cursor = cursor;
277
+ } catch (error) {
278
+ console.warn("[RDP] Could not build a cursor from the host pointer update", error);
279
+ }
280
+ }
281
+ }
282
+ // --- input ----------------------------------------------------------------
283
+ #toRemote(event) {
284
+ const bounds = this.canvas.getBoundingClientRect();
285
+ const scaleX = this.width / (bounds.width || 1);
286
+ const scaleY = this.height / (bounds.height || 1);
287
+ return {
288
+ x: Math.max(0, Math.min(this.width - 1, (event.clientX - bounds.left) * scaleX)),
289
+ y: Math.max(0, Math.min(this.height - 1, (event.clientY - bounds.top) * scaleY))
290
+ };
291
+ }
292
+ #syncKeyboardOnce(event) {
293
+ if (this.keyboardSynced || !this.client) return;
294
+ this.keyboardSynced = true;
295
+ let flags = 0;
296
+ if (event.getModifierState?.("CapsLock")) flags |= TOGGLE_FLAG.CAPS_LOCK;
297
+ if (event.getModifierState?.("NumLock")) flags |= TOGGLE_FLAG.NUM_LOCK;
298
+ if (event.getModifierState?.("ScrollLock")) flags |= TOGGLE_FLAG.SCROLL_LOCK;
299
+ this.client.sendKeyboardSync(flags);
300
+ }
301
+ attachInput(client) {
302
+ this.client = client;
303
+ this.keyboardSynced = false;
304
+ const canSend = () => this.client && this.client.connected && !this.viewOnly;
305
+ let pendingMove = null;
306
+ let moveScheduled = false;
307
+ let pendingWheelDelta = 0;
308
+ let touchIsDown = false;
309
+ let lastTouchX = 0;
310
+ let lastTouchY = 0;
311
+ let lastTouchScrollY = 0;
312
+ let lastTouchScrollX = 0;
313
+ let gestureStartMid = { x: 0, y: 0 };
314
+ let gestureStartDistance = 0;
315
+ let lastPinchDistance = 0;
316
+ let pendingTouchWheelDelta = 0;
317
+ let touchMode = null;
318
+ const touchMidpoint = (touches) => ({
319
+ x: (touches[0].clientX + touches[1].clientX) / 2,
320
+ y: (touches[0].clientY + touches[1].clientY) / 2
321
+ });
322
+ const touchDistance = (touches) => Math.hypot(
323
+ touches[0].clientX - touches[1].clientX,
324
+ touches[0].clientY - touches[1].clientY
325
+ );
326
+ const flushMove = () => {
327
+ moveScheduled = false;
328
+ if (pendingMove && canSend()) this.client.sendMouseMove(pendingMove.x, pendingMove.y);
329
+ pendingMove = null;
330
+ };
331
+ this.handlers = {
332
+ mousemove: (event) => {
333
+ if (!canSend()) return;
334
+ pendingMove = this.#toRemote(event);
335
+ if (!moveScheduled) {
336
+ moveScheduled = true;
337
+ requestAnimationFrame(flushMove);
338
+ }
339
+ },
340
+ mousedown: (event) => {
341
+ this.canvas.focus();
342
+ if (!canSend()) return;
343
+ event.preventDefault();
344
+ const { x, y } = this.#toRemote(event);
345
+ this.client.sendMouseButton(event.button, true, x, y);
346
+ },
347
+ mouseup: (event) => {
348
+ if (!canSend()) return;
349
+ event.preventDefault();
350
+ const { x, y } = this.#toRemote(event);
351
+ this.client.sendMouseButton(event.button, false, x, y);
352
+ },
353
+ contextmenu: (event) => event.preventDefault(),
354
+ wheel: (event) => {
355
+ if (!canSend()) return;
356
+ event.preventDefault();
357
+ let delta = event.deltaY;
358
+ if (event.deltaMode === WheelEvent.DOM_DELTA_LINE) delta *= WHEEL_LINE_PIXELS;
359
+ if (event.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
360
+ delta *= Math.max(1, this.canvas.clientHeight || this.height);
361
+ }
362
+ if (!delta) return;
363
+ if (pendingWheelDelta && Math.sign(pendingWheelDelta) !== Math.sign(delta)) {
364
+ pendingWheelDelta = 0;
365
+ }
366
+ pendingWheelDelta += delta;
367
+ if (Math.abs(pendingWheelDelta) < WHEEL_STEP_PIXELS) return;
368
+ const direction = Math.sign(pendingWheelDelta);
369
+ pendingWheelDelta -= direction * WHEEL_STEP_PIXELS;
370
+ const { x, y } = this.#toRemote(event);
371
+ this.client.sendWheel(direction, x, y);
372
+ },
373
+ touchstart: (event) => {
374
+ this.canvas.focus();
375
+ if (!canSend()) return;
376
+ event.preventDefault();
377
+ if (event.touches.length === 1) {
378
+ touchMode = "tap";
379
+ const touch = event.touches[0];
380
+ lastTouchX = touch.clientX;
381
+ lastTouchY = touch.clientY;
382
+ const { x, y } = this.#toRemote(touch);
383
+ this.client.sendMouseMove(x, y);
384
+ this.client.sendMouseButton(0, true, x, y);
385
+ touchIsDown = true;
386
+ } else if (event.touches.length === 2) {
387
+ touchMode = "undecided";
388
+ if (touchIsDown) {
389
+ const { x, y } = this.#toRemote({ clientX: lastTouchX, clientY: lastTouchY });
390
+ this.client.sendMouseButton(0, false, x, y);
391
+ touchIsDown = false;
392
+ }
393
+ const mid = touchMidpoint(event.touches);
394
+ const dist = touchDistance(event.touches);
395
+ gestureStartMid = { x: mid.x, y: mid.y };
396
+ gestureStartDistance = dist;
397
+ lastTouchScrollY = mid.y;
398
+ lastTouchScrollX = mid.x;
399
+ lastPinchDistance = dist;
400
+ pendingTouchWheelDelta = 0;
401
+ }
402
+ },
403
+ touchmove: (event) => {
404
+ if (!canSend()) return;
405
+ event.preventDefault();
406
+ if (event.touches.length === 1 && touchMode === "tap") {
407
+ const touch = event.touches[0];
408
+ lastTouchX = touch.clientX;
409
+ lastTouchY = touch.clientY;
410
+ const { x, y } = this.#toRemote(touch);
411
+ this.client.sendMouseMove(x, y);
412
+ } else if (event.touches.length === 2) {
413
+ const mid = touchMidpoint(event.touches);
414
+ const currX = mid.x;
415
+ const currY = mid.y;
416
+ const distance = touchDistance(event.touches);
417
+ const deltaY = lastTouchScrollY - currY;
418
+ const deltaX = lastTouchScrollX - currX;
419
+ lastTouchScrollY = currY;
420
+ lastTouchScrollX = currX;
421
+ const totalMove = Math.hypot(currX - gestureStartMid.x, currY - gestureStartMid.y);
422
+ const pinchDelta = Math.abs(distance - gestureStartDistance);
423
+ const pinchRatio = gestureStartDistance > 0 ? distance / gestureStartDistance : 1;
424
+ if (touchMode === "undecided" || touchMode === "gesture") {
425
+ if (pinchDelta > 20 && Math.abs(pinchRatio - 1) > 0.15 && pinchDelta > totalMove * 0.8) {
426
+ touchMode = "pinch";
427
+ } else if (totalMove > 8) {
428
+ touchMode = "scroll";
429
+ }
430
+ }
431
+ if (touchMode === "pinch") {
432
+ if (lastPinchDistance > 0 && Math.abs(distance - lastPinchDistance) > 1) {
433
+ this.userZoomed = true;
434
+ this.setZoom(this.zoom * (distance / lastPinchDistance), { x: currX, y: currY });
435
+ }
436
+ lastPinchDistance = distance;
437
+ } else if (touchMode === "scroll") {
438
+ lastPinchDistance = distance;
439
+ if (this.canPan()) {
440
+ this.panBy(deltaX * -1, deltaY * -1);
441
+ pendingTouchWheelDelta = 0;
442
+ return;
443
+ }
444
+ if (deltaY && pendingTouchWheelDelta && Math.sign(pendingTouchWheelDelta) !== Math.sign(deltaY)) {
445
+ pendingTouchWheelDelta = 0;
446
+ }
447
+ pendingTouchWheelDelta += deltaY;
448
+ while (Math.abs(pendingTouchWheelDelta) >= TOUCH_SCROLL_STEP) {
449
+ const direction = Math.sign(pendingTouchWheelDelta);
450
+ pendingTouchWheelDelta -= direction * TOUCH_SCROLL_STEP;
451
+ const { x, y } = this.#toRemote({ clientX: currX, clientY: currY });
452
+ this.client.sendWheel(direction, x, y);
453
+ }
454
+ }
455
+ }
456
+ },
457
+ touchend: (event) => {
458
+ if (!canSend()) return;
459
+ event.preventDefault();
460
+ if (event.touches.length === 0) {
461
+ if (touchIsDown) {
462
+ const { x, y } = this.#toRemote({ clientX: lastTouchX, clientY: lastTouchY });
463
+ this.client.sendMouseButton(0, false, x, y);
464
+ touchIsDown = false;
465
+ }
466
+ if (touchMode === "pinch" && this.zoom <= 1.05) {
467
+ this.resetZoom();
468
+ }
469
+ touchMode = null;
470
+ pendingTouchWheelDelta = 0;
471
+ lastPinchDistance = 0;
472
+ } else if (event.touches.length === 1) {
473
+ if (touchMode === "gesture" || touchMode === "pinch" || touchMode === "scroll" || touchMode === "undecided") {
474
+ if (touchMode === "pinch" && this.zoom <= 1.05) {
475
+ this.resetZoom();
476
+ }
477
+ touchMode = null;
478
+ lastPinchDistance = 0;
479
+ pendingTouchWheelDelta = 0;
480
+ }
481
+ }
482
+ },
483
+ touchcancel: (event) => {
484
+ if (!canSend()) return;
485
+ if (touchIsDown) {
486
+ const { x, y } = this.#toRemote({ clientX: lastTouchX, clientY: lastTouchY });
487
+ this.client.sendMouseButton(0, false, x, y);
488
+ touchIsDown = false;
489
+ }
490
+ if (touchMode === "pinch" && this.zoom <= 1.05) {
491
+ this.resetZoom();
492
+ }
493
+ touchMode = null;
494
+ pendingTouchWheelDelta = 0;
495
+ lastPinchDistance = 0;
496
+ },
497
+ keydown: (event) => {
498
+ if (!canSend()) return;
499
+ this.#syncKeyboardOnce(event);
500
+ if (this.#isPasteShortcut(event)) return;
501
+ const code = this.#translateCode(event.code);
502
+ if (!this.client.sendKeyByCode(code, true)) return;
503
+ event.preventDefault();
504
+ if (IS_MAC && event.metaKey && !COMMAND_TO_CONTROL[event.code]) {
505
+ this.client.sendKeyByCode(code, false);
506
+ return;
507
+ }
508
+ this.heldKeys.add(code);
509
+ },
510
+ keyup: (event) => {
511
+ if (!canSend()) return;
512
+ if (this.#isPasteShortcut(event)) return;
513
+ const code = this.#translateCode(event.code);
514
+ if (this.client.sendKeyByCode(code, false)) {
515
+ this.heldKeys.delete(code);
516
+ event.preventDefault();
517
+ }
518
+ },
519
+ paste: (event) => {
520
+ if (!canSend()) return;
521
+ const target = event.target;
522
+ const onTheScreen = target instanceof Node && this.container.contains(target);
523
+ if (document.activeElement !== this.canvas && !onTheScreen) return;
524
+ event.preventDefault();
525
+ const text = event.clipboardData?.getData("text/plain") || "";
526
+ if (text) this.client.pasteToHost(text);
527
+ },
528
+ blur: () => this.releaseAllKeys()
529
+ };
530
+ this.canvas.addEventListener("mousemove", this.handlers.mousemove);
531
+ this.canvas.addEventListener("mousedown", this.handlers.mousedown);
532
+ this.canvas.addEventListener("mouseup", this.handlers.mouseup);
533
+ this.canvas.addEventListener("contextmenu", this.handlers.contextmenu);
534
+ this.canvas.addEventListener("wheel", this.handlers.wheel, { passive: false });
535
+ this.canvas.addEventListener("touchstart", this.handlers.touchstart, { passive: false });
536
+ this.canvas.addEventListener("touchmove", this.handlers.touchmove, { passive: false });
537
+ this.canvas.addEventListener("touchend", this.handlers.touchend, { passive: false });
538
+ this.canvas.addEventListener("touchcancel", this.handlers.touchcancel, { passive: false });
539
+ this.canvas.addEventListener("keydown", this.handlers.keydown);
540
+ this.canvas.addEventListener("keyup", this.handlers.keyup);
541
+ this.canvas.addEventListener("blur", this.handlers.blur);
542
+ document.addEventListener("paste", this.handlers.paste);
543
+ }
544
+ /** Command on a Mac stands in for Control everywhere the host is concerned. */
545
+ #translateCode(code) {
546
+ return IS_MAC ? COMMAND_TO_CONTROL[code] || code : code;
547
+ }
548
+ #isPasteShortcut(event) {
549
+ if (event.code !== "KeyV") return false;
550
+ return IS_MAC ? event.metaKey : event.ctrlKey;
551
+ }
552
+ /** A key held while focus moves away would otherwise stay down on the host. */
553
+ releaseAllKeys() {
554
+ if (!this.client) return;
555
+ for (const code of this.heldKeys) this.client.sendKeyByCode(code, false);
556
+ this.heldKeys.clear();
557
+ }
558
+ focus() {
559
+ this.canvas.focus();
560
+ }
561
+ blur() {
562
+ this.canvas.blur();
563
+ }
564
+ destroy() {
565
+ this.releaseAllKeys();
566
+ window.removeEventListener("resize", this.onWindowResize);
567
+ this.resizeObserver?.disconnect();
568
+ if (this.handlers) {
569
+ this.canvas.removeEventListener("mousemove", this.handlers.mousemove);
570
+ this.canvas.removeEventListener("mousedown", this.handlers.mousedown);
571
+ this.canvas.removeEventListener("mouseup", this.handlers.mouseup);
572
+ this.canvas.removeEventListener("contextmenu", this.handlers.contextmenu);
573
+ this.canvas.removeEventListener("wheel", this.handlers.wheel);
574
+ this.canvas.removeEventListener("touchstart", this.handlers.touchstart);
575
+ this.canvas.removeEventListener("touchmove", this.handlers.touchmove);
576
+ this.canvas.removeEventListener("touchend", this.handlers.touchend);
577
+ this.canvas.removeEventListener("touchcancel", this.handlers.touchcancel);
578
+ this.canvas.removeEventListener("keydown", this.handlers.keydown);
579
+ this.canvas.removeEventListener("keyup", this.handlers.keyup);
580
+ this.canvas.removeEventListener("blur", this.handlers.blur);
581
+ document.removeEventListener("paste", this.handlers.paste);
582
+ }
583
+ this.client = null;
584
+ }
585
+ }
586
+ export {
587
+ RdpDisplay
588
+ };
589
+ //# sourceMappingURL=display.js.map