Handles the returned KeysDownState from keyboardReport

Now passes all logic through handleKeyPress.
If we get a state back from a keyboardReport, use it and also enable keypressReport because we now know it's an upgraded device.
This commit is contained in:
Marc Brooks 2025-08-15 00:31:18 -05:00
parent 7a4950973c
commit a4851f980d
No known key found for this signature in database
GPG Key ID: 583A6AF2D6AE1DC6
12 changed files with 251 additions and 191 deletions

227
ui/package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "kvm-ui",
"version": "2025.08.07.001",
"version": "2025.08.15.001",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kvm-ui",
"version": "2025.08.07.001",
"version": "2025.08.15.001",
"dependencies": {
"@headlessui/react": "^2.2.7",
"@headlessui/tailwindcss": "^0.2.2",
@ -45,9 +45,9 @@
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.33.0",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.1.11",
"@tailwindcss/postcss": "^4.1.12",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.11",
"@tailwindcss/vite": "^4.1.12",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@types/semver": "^7.7.0",
@ -66,7 +66,7 @@
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4.1.11",
"tailwindcss": "^4.1.12",
"typescript": "^5.9.2",
"vite": "^6.3.5",
"vite-tsconfig-paths": "^5.1.4"
@ -88,24 +88,10 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz",
"integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==",
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
"integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@ -855,6 +841,17 @@
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@ -1549,25 +1546,25 @@
}
},
"node_modules/@tailwindcss/node": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz",
"integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.12.tgz",
"integrity": "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"enhanced-resolve": "^5.18.1",
"jiti": "^2.4.2",
"@jridgewell/remapping": "^2.3.4",
"enhanced-resolve": "^5.18.3",
"jiti": "^2.5.1",
"lightningcss": "1.30.1",
"magic-string": "^0.30.17",
"source-map-js": "^1.2.1",
"tailwindcss": "4.1.11"
"tailwindcss": "4.1.12"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz",
"integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.12.tgz",
"integrity": "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@ -1579,24 +1576,24 @@
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.11",
"@tailwindcss/oxide-darwin-arm64": "4.1.11",
"@tailwindcss/oxide-darwin-x64": "4.1.11",
"@tailwindcss/oxide-freebsd-x64": "4.1.11",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.11",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.11",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.11",
"@tailwindcss/oxide-linux-x64-musl": "4.1.11",
"@tailwindcss/oxide-wasm32-wasi": "4.1.11",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.11",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.11"
"@tailwindcss/oxide-android-arm64": "4.1.12",
"@tailwindcss/oxide-darwin-arm64": "4.1.12",
"@tailwindcss/oxide-darwin-x64": "4.1.12",
"@tailwindcss/oxide-freebsd-x64": "4.1.12",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.12",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.12",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.12",
"@tailwindcss/oxide-linux-x64-musl": "4.1.12",
"@tailwindcss/oxide-wasm32-wasi": "4.1.12",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.12",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.12"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz",
"integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz",
"integrity": "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==",
"cpu": [
"arm64"
],
@ -1611,9 +1608,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz",
"integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz",
"integrity": "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==",
"cpu": [
"arm64"
],
@ -1628,9 +1625,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz",
"integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz",
"integrity": "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==",
"cpu": [
"x64"
],
@ -1645,9 +1642,9 @@
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz",
"integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz",
"integrity": "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==",
"cpu": [
"x64"
],
@ -1662,9 +1659,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz",
"integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz",
"integrity": "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==",
"cpu": [
"arm"
],
@ -1679,9 +1676,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz",
"integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz",
"integrity": "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==",
"cpu": [
"arm64"
],
@ -1696,9 +1693,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz",
"integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz",
"integrity": "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==",
"cpu": [
"arm64"
],
@ -1713,9 +1710,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz",
"integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz",
"integrity": "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==",
"cpu": [
"x64"
],
@ -1730,9 +1727,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz",
"integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz",
"integrity": "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==",
"cpu": [
"x64"
],
@ -1747,9 +1744,9 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz",
"integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz",
"integrity": "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
@ -1765,11 +1762,11 @@
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@emnapi/wasi-threads": "^1.0.2",
"@napi-rs/wasm-runtime": "^0.2.11",
"@tybys/wasm-util": "^0.9.0",
"@emnapi/core": "^1.4.5",
"@emnapi/runtime": "^1.4.5",
"@emnapi/wasi-threads": "^1.0.4",
"@napi-rs/wasm-runtime": "^0.2.12",
"@tybys/wasm-util": "^0.10.0",
"tslib": "^2.8.0"
},
"engines": {
@ -1777,9 +1774,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz",
"integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz",
"integrity": "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==",
"cpu": [
"arm64"
],
@ -1794,9 +1791,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz",
"integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz",
"integrity": "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==",
"cpu": [
"x64"
],
@ -1811,17 +1808,17 @@
}
},
"node_modules/@tailwindcss/postcss": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.11.tgz",
"integrity": "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.12.tgz",
"integrity": "sha512-5PpLYhCAwf9SJEeIsSmCDLgyVfdBhdBpzX1OJ87anT9IVR0Z9pjM0FNixCAUAHGnMBGB8K99SwAheXrT0Kh6QQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"@tailwindcss/node": "4.1.11",
"@tailwindcss/oxide": "4.1.11",
"@tailwindcss/node": "4.1.12",
"@tailwindcss/oxide": "4.1.12",
"postcss": "^8.4.41",
"tailwindcss": "4.1.11"
"tailwindcss": "4.1.12"
}
},
"node_modules/@tailwindcss/typography": {
@ -1841,15 +1838,15 @@
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz",
"integrity": "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.12.tgz",
"integrity": "sha512-4pt0AMFDx7gzIrAOIYgYP0KCBuKWqyW8ayrdiLEjoJTT4pKTjrzG/e4uzWtTLDziC+66R9wbUqZBccJalSE5vQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@tailwindcss/node": "4.1.11",
"@tailwindcss/oxide": "4.1.11",
"tailwindcss": "4.1.11"
"@tailwindcss/node": "4.1.12",
"@tailwindcss/oxide": "4.1.12",
"tailwindcss": "4.1.12"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6 || ^7"
@ -2739,9 +2736,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001734",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz",
"integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==",
"version": "1.0.30001735",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz",
"integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==",
"dev": true,
"funding": [
{
@ -3159,9 +3156,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.200",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.200.tgz",
"integrity": "sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==",
"version": "1.5.201",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.201.tgz",
"integrity": "sha512-ZG65vsrLClodGqywuigc+7m0gr4ISoTQttfVh7nfpLv0M7SIwF4WbFNEOywcqTiujs12AUeeXbFyQieDICAIxg==",
"dev": true,
"license": "ISC"
},
@ -6478,9 +6475,9 @@
}
},
"node_modules/tailwindcss": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz",
"integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==",
"license": "MIT"
},
"node_modules/tapable": {
@ -6534,10 +6531,13 @@
}
},
"node_modules/tinyglobby/node_modules/fdir": {
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
@ -6939,10 +6939,13 @@
}
},
"node_modules/vite/node_modules/fdir": {
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},

View File

@ -1,7 +1,7 @@
{
"name": "kvm-ui",
"private": true,
"version": "2025.08.07.001",
"version": "2025.08.15.001",
"type": "module",
"engines": {
"node": "22.15.0"
@ -56,9 +56,9 @@
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.33.0",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.1.11",
"@tailwindcss/postcss": "^4.1.12",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.11",
"@tailwindcss/vite": "^4.1.12",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@types/semver": "^7.7.0",
@ -77,7 +77,7 @@
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4.1.11",
"tailwindcss": "^4.1.12",
"typescript": "^5.9.2",
"vite": "^6.3.5",
"vite-tsconfig-paths": "^5.1.4"

View File

@ -48,7 +48,7 @@ export default function Actionbar({
if (!open) {
setTimeout(() => {
setDisableFocusTrap(false);
console.log("Popover is closing. Returning focus trap to video");
console.debug("Popover is closing. Returning focus trap to video");
}, 0);
}
}

View File

@ -3,7 +3,6 @@ import { useEffect, useMemo } from "react";
import { cx } from "@/cva.config";
import {
HidState,
KeysDownState,
MouseState,
RTCState,
SettingsState,
@ -39,7 +38,7 @@ export default function InfoBar() {
if (!rpcDataChannel) return;
rpcDataChannel.onclose = () => console.log("rpcDataChannel has closed");
rpcDataChannel.onerror = (e: Event) =>
console.log(`Error on DataChannel '${rpcDataChannel.label}': ${e}`);
console.error(`Error on DataChannel '${rpcDataChannel.label}': ${e}`);
}, [rpcDataChannel]);
const keyboardLedState = useHidStore((state: HidState) => state.keyboardLedState);
@ -49,12 +48,11 @@ export default function InfoBar() {
const hdmiState = useVideoStore((state: VideoState) => state.hdmiState);
const displayKeys = useMemo(() => {
if (!showPressedKeys || !keysDownState)
if (!showPressedKeys)
return "";
const state = keysDownState as KeysDownState;
const activeModifierMask = state.modifier || 0;
const keysDown = state.keys || [];
const activeModifierMask = keysDownState.modifier || 0;
const keysDown = keysDownState.keys || [];
const modifierNames = Object.entries(modifiers).filter(([_, mask]) => (activeModifierMask & mask) !== 0).map(([name, _]) => name);
const keyNames = Object.entries(keys).filter(([_, value]) => keysDown.includes(value)).map(([name, _]) => name);

View File

@ -67,7 +67,7 @@ export default function USBStateStatus({
};
const props = StatusCardProps[state];
if (!props) {
console.log("Unsupported USB state: ", state);
console.warn("Unsupported USB state: ", state);
return;
}

View File

@ -101,8 +101,8 @@ export function UsbInfoSetting() {
`Failed to load USB Config: ${resp.error.data || "Unknown error"}`,
);
} else {
console.log("syncUsbConfigProduct#getUsbConfig result:", resp.result);
const usbConfigState = resp.result as UsbConfigState;
console.log("syncUsbConfigProduct#getUsbConfig result:", usbConfigState);
const product = usbConfigs.map(u => u.value).includes(usbConfigState.product)
? usbConfigState.product
: "custom";

View File

@ -14,7 +14,7 @@ import DetachIconRaw from "@/assets/detach-icon.svg";
import { cx } from "@/cva.config";
import { HidState, useHidStore, useUiStore } from "@/hooks/stores";
import useKeyboard from "@/hooks/useKeyboard";
import { keyDisplayMap, keys, modifiers } from "@/keyboardMappings";
import { keyDisplayMap, keys } from "@/keyboardMappings";
export const DetachIcon = ({ className }: { className?: string }) => {
return <img src={DetachIconRaw} alt="Detach Icon" className={className} />;
@ -39,7 +39,7 @@ function KeyboardWrapper() {
const setVirtualKeyboard = useHidStore(state => state.setVirtualKeyboardEnabled);
const keysDownState = useHidStore((state: HidState) => state.keysDownState);
const { handleKeyPress, sendKeyboardEvent, resetKeyboardState } = useKeyboard();
const { handleKeyPress, executeMacro } = useKeyboard();
const [isDragging, setIsDragging] = useState(false);
const [position, setPosition] = useState({ x: 0, y: 0 });
@ -133,26 +133,25 @@ function KeyboardWrapper() {
const latchingKeys = ["CapsLock", "ScrollLock", "NumLock", "Meta", "Compose", "Kana"];
const dynamicKeys = ["ShiftLeft", "ShiftRight", "ControlLeft", "ControlRight", "AltLeft", "AltRight", "MetaLeft", "MetaRight"];
// handle the fake key-macros we have defined for common combinations
if (key === "CtrlAltDelete") {
sendKeyboardEvent({ keys: [keys.Delete], modifier: modifiers.ControlLeft | modifiers.AltLeft });
setTimeout(resetKeyboardState, 100);
executeMacro([ { keys: ["Delete"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 } ]);
return;
}
if (key === "AltMetaEscape") {
sendKeyboardEvent({ keys: [keys.Escape], modifier: modifiers.AltLeft | modifiers.MetaLeft });
setTimeout(resetKeyboardState, 100);
executeMacro([ { keys: ["Escape"], modifiers: ["AltLeft", "MetaLeft"], delay: 100 } ]);
return;
}
if (key === "CtrlAltBackspace") {
sendKeyboardEvent({ keys: [keys.Backspace], modifier: modifiers.ControlLeft | modifiers.AltLeft });
setTimeout(resetKeyboardState, 100);
executeMacro([ { keys: ["Backspace"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 } ]);
return;
}
// if they press any of the latching keys, we send a keypress down event and the release it automatically (on timer)
if (latchingKeys.includes(key)) {
console.debug(`Latching key pressed: ${key} sending down and delayed up pair`);
handleKeyPress(keys[key], true)
setTimeout(() => handleKeyPress(keys[key], false), 100);
return;
@ -161,16 +160,18 @@ function KeyboardWrapper() {
// if they press any of the dynamic keys, we send a keypress down event but we don't release it until they click it again
if (dynamicKeys.includes(key)) {
const currentlyDown = keysDownState.keys.includes(keys[key]);
console.debug(`Dynamic key pressed: ${key} was currently down: ${currentlyDown}, toggling state`);
handleKeyPress(keys[key], !currentlyDown)
return;
}
// otherwise, just treat it as a down+up pair
const cleanKey = key.replace(/[()]/g, "");
console.debug(`Regular key pressed: ${cleanKey} sending down and up pair`);
handleKeyPress(keys[cleanKey], true);
setTimeout(() => handleKeyPress(keys[cleanKey], false), 50);
},
[handleKeyPress, sendKeyboardEvent, resetKeyboardState, keysDownState],
[executeMacro, handleKeyPress, keysDownState],
);
// TODO handle the display of down keys and the layout change for shift/caps lock

View File

@ -35,6 +35,7 @@ export default function WebRTCVideo() {
const [isPlaying, setIsPlaying] = useState(false);
const peerConnectionState = useRTCStore((state: RTCState) => state.peerConnectionState);
const [isPointerLockActive, setIsPointerLockActive] = useState(false);
const [isKeyboardLockActive, setIsKeyboardLockActive] = useState(false);
// Store hooks
const settings = useSettingsStore();
const { handleKeyPress, resetKeyboardState } = useKeyboard();
@ -69,16 +70,19 @@ export default function WebRTCVideo() {
const [send] = useJsonRpc();
// Video-related
const handleResize = useCallback(
({ width, height }: { width: number; height: number }) => {
if (!videoElm.current) return;
// Do something with width and height, e.g.:
setVideoClientSize(width, height);
setVideoSize(videoElm.current.videoWidth, videoElm.current.videoHeight);
},
[setVideoClientSize, setVideoSize]
);
useResizeObserver({
ref: videoElm as React.RefObject<HTMLElement>,
onResize: ({ width, height }) => {
// This is actually client size, not videoSize
if (width && height) {
if (!videoElm.current) return;
setVideoClientSize(width, height);
setVideoSize(videoElm.current.videoWidth, videoElm.current.videoHeight);
}
},
onResize: handleResize,
});
const updateVideoSizeStore = useCallback(
@ -107,7 +111,7 @@ export default function WebRTCVideo() {
const isFullscreenEnabled = document.fullscreenEnabled;
const checkNavigatorPermissions = useCallback(async (permissionName: string) => {
if (!navigator.permissions || !navigator.permissions.query) {
if (!navigator || !navigator.permissions || !navigator.permissions.query) {
return false; // if can't query permissions, assume NOT granted
}
@ -142,28 +146,30 @@ export default function WebRTCVideo() {
const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock");
if (isKeyboardLockGranted && "keyboard" in navigator) {
if (isKeyboardLockGranted && navigator && "keyboard" in navigator) {
try {
// @ts-expect-error - keyboard lock is not supported in all browsers
await navigator.keyboard.lock();
setIsKeyboardLockActive(true);
} catch {
// ignore errors
}
}
}, [checkNavigatorPermissions]);
}, [checkNavigatorPermissions, setIsKeyboardLockActive]);
const releaseKeyboardLock = useCallback(async () => {
if (videoElm.current === null || document.fullscreenElement !== videoElm.current) return;
if ("keyboard" in navigator) {
if (navigator && "keyboard" in navigator) {
try {
// @ts-expect-error - keyboard unlock is not supported in all browsers
await navigator.keyboard.unlock();
} catch {
// ignore errors
}
setIsKeyboardLockActive(false);
}
}, []);
}, [setIsKeyboardLockActive]);
useEffect(() => {
if (!isPointerLockPossible || !videoElm.current) return;
@ -353,12 +359,24 @@ export default function WebRTCVideo() {
// https://bugzilla.mozilla.org/show_bug.cgi?id=1299553
if (e.metaKey && hidKey < 0xE0) {
setTimeout(() => {
console.debug(`Forcing the meta key release of associated key: ${hidKey}`);
handleKeyPress(hidKey, false);
}, 10);
}
console.debug(`Key down: ${hidKey}`);
handleKeyPress(hidKey, true);
if (!isKeyboardLockActive && hidKey === keys.MetaLeft) {
// If the left meta key was just pressed and we're not keyboard locked
// we'll never see the keyup event because the browser is going to lose
// focus so set a deferred keyup after a short delay
setTimeout(() => {
console.debug(`Forcing the left meta key release`);
handleKeyPress(hidKey, false);
}, 100);
}
},
[handleKeyPress],
[handleKeyPress, isKeyboardLockActive],
);
const keyUpHandler = useCallback(
@ -372,6 +390,7 @@ export default function WebRTCVideo() {
return;
}
console.debug(`Key up: ${hidKey}`);
handleKeyPress(hidKey, false);
},
[handleKeyPress],
@ -385,7 +404,7 @@ export default function WebRTCVideo() {
// Fix only works in chrome based browsers.
if (e.code === "Space") {
if (videoElm.current.paused) {
console.log("Force playing video");
console.debug("Force playing video");
videoElm.current.play();
}
}

View File

@ -19,19 +19,33 @@ export default function useKeyboard() {
(state: KeysDownState) => {
if (rpcDataChannel?.readyState !== "open") return;
//TODO would be nice if the keyboardReport rpc call returned the current state like keypressReport does
send("keyboardReport", { keys: state.keys, modifier: state.modifier });
console.debug(`Send keyboardReport keys: ${state.keys}, modifier: ${state.modifier}`);
send("keyboardReport", { keys: state.keys, modifier: state.modifier }, (resp: JsonRpcResponse) => {
if ("error" in resp) {
console.error(`Failed to send keyboard report ${state}`, resp.error);
} else {
const keysDownState = resp.result as KeysDownState;
// We do this for the info bar to display the currently pressed keys for the user
setKeysDownState(state);
if (keysDownState) {
// new devices return the keyDownState, so we can use it to update the state
setKeysDownState(keysDownState);
setKeyPressAvailable(true); // if they returned a keysDownState, we know they also support keyPressReport
} else {
// old devices do not return the keyDownState, so we just pretend they accepted what we sent
setKeysDownState(state);
// and we shouldn't set keyPressAvailable here because we don't know if they support it
}
}
});
},
[rpcDataChannel?.readyState, send, setKeysDownState],
[rpcDataChannel?.readyState, send, setKeyPressAvailable, setKeysDownState],
);
const sendKeypressEvent = useCallback(
(key: number, press: boolean) => {
if (rpcDataChannel?.readyState !== "open") return;
console.debug(`Send keypressEvent key: ${key}, press: ${press}`);
send("keypressReport", { key, press }, (resp: JsonRpcResponse) => {
if ("error" in resp) {
// -32601 means the method is not supported
@ -43,9 +57,12 @@ export default function useKeyboard() {
console.error(`Failed to send key ${key} press: ${press}`, resp.error);
}
} else {
const keyDownState = resp.result as KeysDownState;
// We do this for the info bar to display the currently pressed keys for the user
setKeysDownState(keyDownState);
const keysDownState = resp.result as KeysDownState;
if (keysDownState) {
setKeysDownState(keysDownState);
// we don't need to set keyPressAvailable here, because it's already true or we never landed here
}
}
});
},
@ -53,8 +70,11 @@ export default function useKeyboard() {
);
const resetKeyboardState = useCallback(() => {
sendKeyboardEvent({ keys: [], modifier: 0 });
}, [sendKeyboardEvent]);
console.debug("Resetting keyboard state");
keysDownState.keys.fill(0); // Reset the keys buffer to zeros
keysDownState.modifier = 0; // Reset the modifier state to zero
sendKeyboardEvent(keysDownState);
}, [keysDownState, sendKeyboardEvent]);
const executeMacro = async (steps: { keys: string[] | null; modifiers: string[] | null; delay: number }[]) => {
for (const [index, step] of steps.entries()) {
@ -87,6 +107,7 @@ export default function useKeyboard() {
const modifierMask = hidKeyToModifierMask[key] || 0;
if (modifierMask !== 0) {
console.debug(`Handling modifier key: ${key}, press: ${press}, current modifiers: ${modifiers}, modifier mask: ${modifierMask}`);
if (press) {
modifiers |= modifierMask;
} else {
@ -132,22 +153,24 @@ export default function useKeyboard() {
const handleKeyPress = useCallback(
(key: number, press: boolean) => {
if (rpcDataChannel?.readyState !== "open") return;
if (keyPressAvailable) {
// if the keyPress api is available, we can just send the key press event
sendKeypressEvent(key, press);
// TODO handle the case where the keyPress api is not available and we need to handle the key locally now...
} else {
// if the keyPress api is not available, we need to handle the key locally
const newKeysDownState = handleKeyLocally(keysDownState, key, press);
setKeysDownState(newKeysDownState);
// then we send the full state
sendKeyboardEvent(newKeysDownState);
// if keyPress api is STILL available, we don't need to handle the key locally
if (keyPressAvailable) return;
}
// if the keyPress api is not available, we need to handle the key locally
const downState = handleKeyLocally(keysDownState, key, press);
setKeysDownState(downState);
// then we send the full state
sendKeyboardEvent(downState);
},
[keyPressAvailable, keysDownState, sendKeyboardEvent, sendKeypressEvent, setKeysDownState],
[keyPressAvailable, keysDownState, rpcDataChannel?.readyState, sendKeyboardEvent, sendKeypressEvent, setKeysDownState],
);
return { handleKeyPress, sendKeyboardEvent, sendKeypressEvent, resetKeyboardState, executeMacro };
return { handleKeyPress, resetKeyboardState, executeMacro };
}

View File

@ -106,11 +106,12 @@ export default function SettingsNetworkRoute() {
setNetworkSettingsLoaded(false);
send("getNetworkSettings", {}, resp => {
if ("error" in resp) return;
console.log(resp.result);
setNetworkSettings(resp.result as NetworkSettings);
const networkSettings = resp.result as NetworkSettings;
console.debug("Network settings: ", networkSettings);
setNetworkSettings(networkSettings);
if (!firstNetworkSettings.current) {
firstNetworkSettings.current = resp.result as NetworkSettings;
firstNetworkSettings.current = networkSettings;
}
setNetworkSettingsLoaded(true);
});
@ -119,8 +120,9 @@ export default function SettingsNetworkRoute() {
const getNetworkState = useCallback(() => {
send("getNetworkState", {}, resp => {
if ("error" in resp) return;
console.log(resp.result);
setNetworkState(resp.result as NetworkState);
const networkState = resp.result as NetworkState;
console.debug("Network state:", networkState);
setNetworkState(networkState);
});
}, [send, setNetworkState]);
@ -136,9 +138,10 @@ export default function SettingsNetworkRoute() {
setNetworkSettingsLoaded(true);
return;
}
const networkSettings = resp.result as NetworkSettings;
// We need to update the firstNetworkSettings ref to the new settings so we can use it to determine if the settings have changed
firstNetworkSettings.current = resp.result as NetworkSettings;
setNetworkSettings(resp.result as NetworkSettings);
firstNetworkSettings.current = networkSettings;
setNetworkSettings(networkSettings);
getNetworkState();
setNetworkSettingsLoaded(true);
notifications.success("Network settings saved");

View File

@ -73,7 +73,7 @@ export default function SettingsRoute() {
// For some reason, the focus trap is not disabled immediately
// so we need to blur the active element
(document.activeElement as HTMLElement)?.blur();
console.log("Just disabled focus trap");
console.debug("Just disabled focus trap");
}, 300);
return () => {

View File

@ -216,7 +216,7 @@ export default function KvmIdRoute() {
clearInterval(checkInterval);
setLoadingMessage("Connection established");
} else if (attempts >= 10) {
console.log(
console.warn(
"[setRemoteSessionDescription] Failed to establish connection after 10 attempts",
{
connectionState: pc.connectionState,
@ -444,7 +444,7 @@ export default function KvmIdRoute() {
if (isNewSignalingEnabled) {
sendWebRTCSignal("offer", { sd: sd });
} else {
console.log("Legacy signanling. Waiting for ICE Gathering to complete...");
console.log("Legacy signaling. Waiting for ICE Gathering to complete...");
}
} catch (e) {
console.error(
@ -511,7 +511,7 @@ export default function KvmIdRoute() {
useEffect(() => {
if (peerConnectionState === "failed") {
console.log("Connection failed, closing peer connection");
console.warn("Connection failed, closing peer connection");
cleanupAndStopReconnecting();
}
}, [peerConnectionState, cleanupAndStopReconnecting]);
@ -616,20 +616,24 @@ export default function KvmIdRoute() {
}
if (resp.method === "networkState") {
console.log("Setting network state", resp.params);
console.debug("Setting network state", resp.params);
setNetworkState(resp.params as NetworkState);
}
if (resp.method === "keyboardLedState") {
const ledState = resp.params as KeyboardLedState;
console.log("Setting keyboard led state", ledState);
console.debug("Setting keyboard led state", ledState);
setKeyboardLedState(ledState);
}
if (resp.method === "keysDownState") {
const downState = resp.params as KeysDownState;
console.log("Setting key down state", downState);
setKeysDownState(downState);
if (downState) {
console.debug("Setting key down state:", downState);
setKeysDownState(downState);
setKeyPressAvailable(true); // if they returned a keyDownState, we know they also support keyPressReport
}
}
if (resp.method === "otaState") {
@ -681,8 +685,12 @@ export default function KvmIdRoute() {
console.error("Failed to get keyboard led state", resp.error);
return;
}
console.log("Keyboard led state", resp.result);
setKeyboardLedState(resp.result as KeyboardLedState);
const ledState = resp.result as KeyboardLedState;
if (ledState) {
console.debug("Keyboard led state: ", resp.result);
setKeyboardLedState(resp.result as KeyboardLedState);
}
setNeedLedState(false);
});
}, [rpcDataChannel?.readyState, send, setKeyboardLedState, keyboardLedState, needLedState]);
@ -705,10 +713,15 @@ export default function KvmIdRoute() {
} else {
console.error("Failed to get key down state", resp.error);
}
return;
} else {
const downState = resp.result as KeysDownState;
if (downState) {
console.debug("Keyboard key down state", downState);
setKeysDownState(downState);
setKeyPressAvailable(true); // if they returned a keyDownState, we know they also support keyPressReport
}
}
console.log("Keyboard key down state", resp.result);
setKeysDownState(resp.result as KeysDownState);
setNeedKeyDownState(false);
});
}, [keysDownState, needKeyDownState, rpcDataChannel?.readyState, send, setKeyPressAvailable, setKeysDownState]);
@ -725,7 +738,7 @@ export default function KvmIdRoute() {
useEffect(() => {
if (!diskChannel || !file) return;
diskChannel.onmessage = async e => {
console.log("Received", e.data);
console.debug("Received", e.data);
const data = JSON.parse(e.data);
const blob = file.slice(data.start, data.end);
const buf = await blob.arrayBuffer();