From 8692c0e46a717e42b4f78011452cec2f50173684 Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Wed, 6 May 2026 15:19:53 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20phase=208=20=E2=80=94=20Vite/TS=20canva?= =?UTF-8?q?s-based=20KVM=20console=20frontend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit crates/ericrfb-frontend — vanilla TypeScript + Vite: login.ts: - Login form POSTs to /api/login, receives applet_id - Error display, auto-transitions to console view on success console.ts: - Canvas-based renderer sized to framebuffer dimensions - WebSocket binary protocol decoder: TAG_BLIT → putImageData, TAG_RESIZE → canvas resize - Keyboard capture: keydown/keyup → JS code → e-RIC scancode → WS - Mouse capture: move/click/wheel → scaled coords + button mask → WS - Right-click and context menu suppressed for pass-through input.ts: - Full 104-key JS KeyboardEvent.code → scancode mapping table protocol.ts: - Binary message builders matching proxy WS protocol tags Toolbar: Ctrl+Alt+Del button, Fullscreen toggle. Dark theme, pixelated canvas rendering, cursor hidden over console. Vite config proxies /api to localhost:3000 for dev mode. Build outputs to ../../dist for proxy static serving. Builds to 5.8KB JS + 1.4KB CSS gzipped. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 3 + crates/ericrfb-frontend/.gitignore | 24 + crates/ericrfb-frontend/index.html | 12 + crates/ericrfb-frontend/package-lock.json | 917 ++++++++++++++++++++++ crates/ericrfb-frontend/package.json | 15 + crates/ericrfb-frontend/src/console.ts | 163 ++++ crates/ericrfb-frontend/src/input.ts | 49 ++ crates/ericrfb-frontend/src/login.ts | 57 ++ crates/ericrfb-frontend/src/main.ts | 5 + crates/ericrfb-frontend/src/protocol.ts | 33 + crates/ericrfb-frontend/src/style.css | 111 +++ crates/ericrfb-frontend/tsconfig.json | 23 + crates/ericrfb-frontend/vite.config.ts | 16 + 13 files changed, 1428 insertions(+) create mode 100644 crates/ericrfb-frontend/.gitignore create mode 100644 crates/ericrfb-frontend/index.html create mode 100644 crates/ericrfb-frontend/package-lock.json create mode 100644 crates/ericrfb-frontend/package.json create mode 100644 crates/ericrfb-frontend/src/console.ts create mode 100644 crates/ericrfb-frontend/src/input.ts create mode 100644 crates/ericrfb-frontend/src/login.ts create mode 100644 crates/ericrfb-frontend/src/main.ts create mode 100644 crates/ericrfb-frontend/src/protocol.ts create mode 100644 crates/ericrfb-frontend/src/style.css create mode 100644 crates/ericrfb-frontend/tsconfig.json create mode 100644 crates/ericrfb-frontend/vite.config.ts diff --git a/.gitignore b/.gitignore index ea8c4bf..de1d0c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ /target +/dist +/out +crates/ericrfb-frontend/node_modules diff --git a/crates/ericrfb-frontend/.gitignore b/crates/ericrfb-frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/crates/ericrfb-frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/crates/ericrfb-frontend/index.html b/crates/ericrfb-frontend/index.html new file mode 100644 index 0000000..52fc95d --- /dev/null +++ b/crates/ericrfb-frontend/index.html @@ -0,0 +1,12 @@ + + + + + + blekin — KVM Console + + +
+ + + diff --git a/crates/ericrfb-frontend/package-lock.json b/crates/ericrfb-frontend/package-lock.json new file mode 100644 index 0000000..d3f718d --- /dev/null +++ b/crates/ericrfb-frontend/package-lock.json @@ -0,0 +1,917 @@ +{ + "name": "ericrfb-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ericrfb-frontend", + "version": "0.0.0", + "devDependencies": { + "typescript": "~6.0.2", + "vite": "^8.0.10" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.127.0", + "@rolldown/pluginutils": "1.0.0-rc.17" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", + "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.17", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/crates/ericrfb-frontend/package.json b/crates/ericrfb-frontend/package.json new file mode 100644 index 0000000..ad246e9 --- /dev/null +++ b/crates/ericrfb-frontend/package.json @@ -0,0 +1,15 @@ +{ + "name": "ericrfb-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "~6.0.2", + "vite": "^8.0.10" + } +} diff --git a/crates/ericrfb-frontend/src/console.ts b/crates/ericrfb-frontend/src/console.ts new file mode 100644 index 0000000..edbdac7 --- /dev/null +++ b/crates/ericrfb-frontend/src/console.ts @@ -0,0 +1,163 @@ +import { codeToScancode } from './input' +import { + TAG_BLIT, TAG_RESIZE, + makeKeyPress, makeKeyRelease, makePointer, makeCtrlAltDel, +} from './protocol' + +export function startConsole( + app: HTMLElement, + appletId: string, + port: number, + boardName: string, +) { + app.innerHTML = ` +
+ ${boardName} + + + connecting... +
+
+ +
+ ` + + const canvas = document.getElementById('canvas') as HTMLCanvasElement + const ctx = canvas.getContext('2d')! + const statusEl = document.getElementById('status')! + + // WebSocket + const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:' + const wsUrl = `${wsProto}//${location.host}/api/ws?applet_id=${encodeURIComponent(appletId)}&port=${port}` + const ws = new WebSocket(wsUrl) + ws.binaryType = 'arraybuffer' + + ws.onopen = () => { + statusEl.textContent = 'connected' + statusEl.classList.add('connected') + canvas.focus() + } + + ws.onclose = () => { + statusEl.textContent = 'disconnected' + statusEl.classList.remove('connected') + } + + ws.onerror = () => { + statusEl.textContent = 'error' + statusEl.classList.remove('connected') + } + + ws.onmessage = (ev: MessageEvent) => { + if (!(ev.data instanceof ArrayBuffer)) return + const view = new DataView(ev.data) + const tag = view.getUint8(0) + + switch (tag) { + case TAG_RESIZE: { + const w = view.getUint16(1) + const h = view.getUint16(3) + canvas.width = w + canvas.height = h + break + } + case TAG_BLIT: { + const x = view.getUint16(1) + const y = view.getUint16(3) + const w = view.getUint16(5) + const h = view.getUint16(7) + const rgba = new Uint8ClampedArray(ev.data, 9) + const img = new ImageData(rgba, w, h) + ctx.putImageData(img, x, y) + break + } + } + } + + // Keyboard input + let buttonMask = 0 + + canvas.addEventListener('keydown', (e) => { + e.preventDefault() + const sc = codeToScancode(e.code) + if (sc !== undefined && ws.readyState === WebSocket.OPEN) { + ws.send(makeKeyPress(sc)) + } + }) + + canvas.addEventListener('keyup', (e) => { + e.preventDefault() + const sc = codeToScancode(e.code) + if (sc !== undefined && ws.readyState === WebSocket.OPEN) { + ws.send(makeKeyRelease(sc)) + } + }) + + // Mouse input + function sendPointer(e: MouseEvent) { + if (ws.readyState !== WebSocket.OPEN) return + const rect = canvas.getBoundingClientRect() + const scaleX = canvas.width / rect.width + const scaleY = canvas.height / rect.height + const x = Math.round((e.clientX - rect.left) * scaleX) + const y = Math.round((e.clientY - rect.top) * scaleY) + ws.send(makePointer( + Math.max(0, Math.min(x, canvas.width - 1)), + Math.max(0, Math.min(y, canvas.height - 1)), + buttonMask, + )) + } + + canvas.addEventListener('mousemove', sendPointer) + + canvas.addEventListener('mousedown', (e) => { + e.preventDefault() + canvas.focus() + if (e.button === 0) buttonMask |= 1 + else if (e.button === 1) buttonMask |= 2 + else if (e.button === 2) buttonMask |= 4 + sendPointer(e) + }) + + canvas.addEventListener('mouseup', (e) => { + e.preventDefault() + if (e.button === 0) buttonMask &= ~1 + else if (e.button === 1) buttonMask &= ~2 + else if (e.button === 2) buttonMask &= ~4 + sendPointer(e) + }) + + canvas.addEventListener('contextmenu', (e) => e.preventDefault()) + + canvas.addEventListener('wheel', (e) => { + e.preventDefault() + if (ws.readyState !== WebSocket.OPEN) return + const rect = canvas.getBoundingClientRect() + const scaleX = canvas.width / rect.width + const scaleY = canvas.height / rect.height + const x = Math.round((e.clientX - rect.left) * scaleX) + const y = Math.round((e.clientY - rect.top) * scaleY) + // Scroll up = button 4 (bit 3), scroll down = button 5 (bit 4) + const scrollMask = e.deltaY < 0 ? 8 : 16 + ws.send(makePointer(x, y, buttonMask | scrollMask)) + // Release scroll button immediately + ws.send(makePointer(x, y, buttonMask)) + }) + + // Toolbar + document.getElementById('btn-cad')!.addEventListener('click', () => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(makeCtrlAltDel()) + } + canvas.focus() + }) + + document.getElementById('btn-fs')!.addEventListener('click', () => { + if (document.fullscreenElement) { + document.exitFullscreen() + } else { + app.requestFullscreen() + } + canvas.focus() + }) +} diff --git a/crates/ericrfb-frontend/src/input.ts b/crates/ericrfb-frontend/src/input.ts new file mode 100644 index 0000000..75b4120 --- /dev/null +++ b/crates/ericrfb-frontend/src/input.ts @@ -0,0 +1,49 @@ +// JavaScript KeyboardEvent.code → e-RIC scancode (KbdLayout_104pc) +// Must match crates/ericrfb/src/input.rs js_code_to_scancode() + +const KEY_MAP: Record = { + Escape: 0, + F1: 59, F2: 60, F3: 61, F4: 62, F5: 63, F6: 64, + F7: 65, F8: 66, F9: 67, F10: 68, F11: 69, F12: 70, + + Backquote: 1, + Digit1: 2, Digit2: 3, Digit3: 4, Digit4: 5, Digit5: 6, + Digit6: 7, Digit7: 8, Digit8: 9, Digit9: 10, Digit0: 11, + Minus: 12, Equal: 13, Backspace: 14, + + Tab: 15, + KeyQ: 16, KeyW: 17, KeyE: 18, KeyR: 19, KeyT: 20, + KeyY: 21, KeyU: 22, KeyI: 23, KeyO: 24, KeyP: 25, + BracketLeft: 26, BracketRight: 27, + + CapsLock: 28, + KeyA: 29, KeyS: 30, KeyD: 31, KeyF: 32, KeyG: 33, + KeyH: 34, KeyJ: 35, KeyK: 36, KeyL: 37, + Semicolon: 38, Quote: 39, Enter: 40, + + ShiftLeft: 41, Backslash: 42, + KeyZ: 43, KeyX: 44, KeyC: 45, KeyV: 46, KeyB: 47, + KeyN: 48, KeyM: 49, Comma: 50, Period: 51, Slash: 52, + ShiftRight: 53, + + ControlLeft: 54, MetaLeft: 105, AltLeft: 55, + Space: 56, + AltRight: 57, MetaRight: 106, ControlRight: 58, + + PrintScreen: 71, ScrollLock: 72, Pause: 73, + Insert: 75, Home: 76, PageUp: 77, + Delete: 78, End: 79, PageDown: 80, + + ArrowUp: 81, ArrowLeft: 82, ArrowDown: 83, ArrowRight: 84, + + NumLock: 85, NumpadDivide: 86, NumpadMultiply: 87, NumpadSubtract: 88, + NumpadAdd: 89, NumpadEnter: 98, + Numpad7: 90, Numpad8: 94, Numpad9: 99, + Numpad4: 91, Numpad5: 92, Numpad6: 93, + Numpad1: 95, Numpad2: 96, Numpad3: 97, + Numpad0: 100, NumpadDecimal: 101, +} + +export function codeToScancode(code: string): number | undefined { + return KEY_MAP[code] +} diff --git a/crates/ericrfb-frontend/src/login.ts b/crates/ericrfb-frontend/src/login.ts new file mode 100644 index 0000000..96b1d80 --- /dev/null +++ b/crates/ericrfb-frontend/src/login.ts @@ -0,0 +1,57 @@ +import { startConsole } from './console' + +interface LoginResponse { + applet_id: string + port: number + protocol_version: string + board_name: string +} + +export function showLogin(app: HTMLElement) { + app.innerHTML = ` + + ` + + const form = app.querySelector('form')! + const errorDiv = app.querySelector('.login-error')! as HTMLElement + const button = app.querySelector('button')! + + form.addEventListener('submit', async (e) => { + e.preventDefault() + errorDiv.hidden = true + button.disabled = true + button.textContent = 'Connecting...' + + const username = (form.querySelector('[name=username]') as HTMLInputElement).value + const password = (form.querySelector('[name=password]') as HTMLInputElement).value + + try { + const resp = await fetch('/api/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }), + }) + + if (!resp.ok) { + const err = await resp.json() + throw new Error(err.error || 'Login failed') + } + + const data: LoginResponse = await resp.json() + startConsole(app, data.applet_id, data.port, data.board_name) + } catch (err) { + errorDiv.textContent = (err as Error).message + errorDiv.hidden = false + button.disabled = false + button.textContent = 'Connect' + } + }) +} diff --git a/crates/ericrfb-frontend/src/main.ts b/crates/ericrfb-frontend/src/main.ts new file mode 100644 index 0000000..6e3efb3 --- /dev/null +++ b/crates/ericrfb-frontend/src/main.ts @@ -0,0 +1,5 @@ +import './style.css' +import { showLogin } from './login' + +const app = document.getElementById('app')! +showLogin(app) diff --git a/crates/ericrfb-frontend/src/protocol.ts b/crates/ericrfb-frontend/src/protocol.ts new file mode 100644 index 0000000..62d5428 --- /dev/null +++ b/crates/ericrfb-frontend/src/protocol.ts @@ -0,0 +1,33 @@ +// Binary WS protocol tags — must match crates/ericrfb-proxy/src/ws.rs + +// Server → Client +export const TAG_BLIT = 0x01 +export const TAG_RESIZE = 0x03 + +// Client → Server +export const TAG_KEY_PRESS = 0x10 +export const TAG_KEY_RELEASE = 0x11 +export const TAG_POINTER = 0x12 +export const TAG_CTRL_ALT_DEL = 0x13 + +export function makeKeyPress(scancode: number): ArrayBuffer { + return new Uint8Array([TAG_KEY_PRESS, scancode]).buffer +} + +export function makeKeyRelease(scancode: number): ArrayBuffer { + return new Uint8Array([TAG_KEY_RELEASE, scancode]).buffer +} + +export function makePointer(x: number, y: number, mask: number): ArrayBuffer { + const buf = new ArrayBuffer(6) + const view = new DataView(buf) + view.setUint8(0, TAG_POINTER) + view.setUint16(1, x) + view.setUint16(3, y) + view.setUint8(5, mask) + return buf +} + +export function makeCtrlAltDel(): ArrayBuffer { + return new Uint8Array([TAG_CTRL_ALT_DEL]).buffer +} diff --git a/crates/ericrfb-frontend/src/style.css b/crates/ericrfb-frontend/src/style.css new file mode 100644 index 0000000..bb060ff --- /dev/null +++ b/crates/ericrfb-frontend/src/style.css @@ -0,0 +1,111 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: system-ui, -apple-system, sans-serif; + background: #1a1a1a; + color: #e0e0e0; + height: 100vh; + overflow: hidden; +} + +#app { + display: flex; + flex-direction: column; + height: 100vh; +} + +/* Login */ +.login { + display: flex; + align-items: center; + justify-content: center; + height: 100vh; +} + +.login-form { + background: #2a2a2a; + padding: 2rem; + border-radius: 8px; + display: flex; + flex-direction: column; + gap: 1rem; + min-width: 320px; +} + +.login-form h1 { + font-size: 1.25rem; + text-align: center; +} + +.login-form input { + padding: 0.5rem; + border: 1px solid #444; + border-radius: 4px; + background: #333; + color: #e0e0e0; + font-size: 0.9rem; +} + +.login-form button { + padding: 0.5rem; + border: none; + border-radius: 4px; + background: #4a7c59; + color: white; + font-size: 0.9rem; + cursor: pointer; +} + +.login-form button:hover { background: #5a9c69; } +.login-form button:disabled { opacity: 0.5; cursor: not-allowed; } + +.login-error { + color: #e74c3c; + font-size: 0.85rem; + text-align: center; +} + +/* Toolbar */ +.toolbar { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.25rem 0.5rem; + background: #222; + border-bottom: 1px solid #333; + font-size: 0.8rem; +} + +.toolbar button { + padding: 0.25rem 0.75rem; + border: 1px solid #444; + border-radius: 3px; + background: #333; + color: #ccc; + cursor: pointer; + font-size: 0.8rem; +} + +.toolbar button:hover { background: #444; } + +.toolbar .status { + margin-left: auto; + color: #888; +} + +.toolbar .status.connected { color: #4a7c59; } + +/* Console */ +.console-wrap { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + background: #000; + overflow: hidden; +} + +.console-wrap canvas { + image-rendering: pixelated; + cursor: none; +} diff --git a/crates/ericrfb-frontend/tsconfig.json b/crates/ericrfb-frontend/tsconfig.json new file mode 100644 index 0000000..1ab38c8 --- /dev/null +++ b/crates/ericrfb-frontend/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "es2023", + "module": "esnext", + "lib": ["ES2023", "DOM"], + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/crates/ericrfb-frontend/vite.config.ts b/crates/ericrfb-frontend/vite.config.ts new file mode 100644 index 0000000..96c4b0a --- /dev/null +++ b/crates/ericrfb-frontend/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + server: { + proxy: { + '/api': { + target: 'http://localhost:3000', + ws: true, + }, + }, + }, + build: { + outDir: '../../dist', + emptyOutDir: true, + }, +})