From c27587e2e30f3c8f7b9889981b63ceef126a8bfd Mon Sep 17 00:00:00 2001 From: gkgoat <65402989+gkgoat1@users.noreply.github.com> Date: Tue, 23 Jun 2026 07:49:28 -0400 Subject: [PATCH 1/3] add file transfer --- .gitignore | 1 + package.json | 3 + pnpm-lock.yaml | 23 +++-- src/cod/App.svelte | 246 +++++++++++++++++++++++++++++---------------- 4 files changed, 176 insertions(+), 97 deletions(-) diff --git a/.gitignore b/.gitignore index 1521c8b..db4c6d9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ dist +node_modules \ No newline at end of file diff --git a/package.json b/package.json index 8ed4c7b..a9f223e 100644 --- a/package.json +++ b/package.json @@ -33,5 +33,8 @@ "clsx": "2.1.1", "esm-env": "1.2.2", "svelte": "5.55.9" + }, + "dependencies": { + "@svelte-parts/drop-file": "^0.0.23" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d3128c..7440c47 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + '@svelte-parts/drop-file': + specifier: ^0.0.23 + version: 0.0.23(svelte@5.55.9) devDependencies: '@tsdown/css': specifier: ^0.22.0 @@ -136,42 +140,36 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.2': resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.2': resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.2': resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.2': resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.2': resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.2': resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==} @@ -199,6 +197,11 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@svelte-parts/drop-file@0.0.23': + resolution: {integrity: sha512-yzpeY/lSmf+NDnMUJKlZip2eqz44GLFQAY+OCw96RXcVtBtgRclcR5Ny0ZN/VZInht0C2K/hIxVa4v4LA6dvPA==} + peerDependencies: + svelte: ^3.29.7 + '@sveltejs/acorn-typescript@1.0.10': resolution: {integrity: sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==} peerDependencies: @@ -395,28 +398,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -747,6 +746,10 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@svelte-parts/drop-file@0.0.23(svelte@5.55.9)': + dependencies: + svelte: 5.55.9 + '@sveltejs/acorn-typescript@1.0.10(acorn@8.16.0)': dependencies: acorn: 8.16.0 diff --git a/src/cod/App.svelte b/src/cod/App.svelte index 81ea415..48e61d3 100644 --- a/src/cod/App.svelte +++ b/src/cod/App.svelte @@ -6,6 +6,7 @@ import prismIcon from '../assets/prism-launcher.svg'; import type { SpawnSession } from './spawn'; import TerminalWindow from './TerminalWindow.svelte'; + import DropFile from '@svelte-parts/drop-file'; import { type TerminalLine, type TerminalWindow as TerminalWindowModel, @@ -56,7 +57,8 @@ export NO_AT_BRIDGE=1 export GTK_A11Y=none export GTK_MODULES= export DBUS_SESSION_BUS_ADDRESS="unix:path=$XDG_RUNTIME_DIR/bus"`; - const withDesktopEnv = (command: string) => `${desktopEnvCommand}\n${command}`; + const withDesktopEnv = (command: string) => + `${desktopEnvCommand}\n${command}`; const firefoxInstallCommand = String.raw`set -euo pipefail echo "Installing Firefox..." export PATH="$HOME/.local/bin:$PATH" @@ -618,7 +620,10 @@ while True: let relativeMouseDy = 0; let relativeMouseFlushTimer = 0; let relativeMouseHintTimer = 0; - const taskOutputHandlers = new Map>(); + const taskOutputHandlers = new Map< + string, + NonNullable + >(); const openTerminalApp = (tab = activeTerminalTab) => { activeTerminalTab = tab; @@ -632,7 +637,9 @@ while True: const loadScript = (document: Document, src: string) => new Promise((resolve, reject) => { - const existing = [...document.scripts].find((script) => script.src === src); + const existing = [...document.scripts].find( + (script) => script.src === src, + ); if (existing) { resolve(); return; @@ -657,7 +664,8 @@ while True: const emitResize = () => { timer = 0; const size = getWindowDisplaySize(); - if (lastSize?.width === size.width && lastSize.height === size.height) return; + if (lastSize?.width === size.width && lastSize.height === size.height) + return; lastSize = size; onResize(size); }; @@ -695,7 +703,9 @@ while True: if (!(canvas instanceof HTMLCanvasElement)) return false; canvasObserver?.disconnect(); - const observer = new MutationObserver(() => syncCanvasLogicalSize(screen)); + const observer = new MutationObserver(() => + syncCanvasLogicalSize(screen), + ); canvasObserver = observer; observer.observe(canvas, { attributes: true, @@ -786,7 +796,11 @@ while True: const handleMouseButton = (event: MouseEvent) => { if (!hasPointerLock()) return; const button = remoteButtonForMouseEvent(event); - if (button) session.input(RELATIVE_MOUSE_ID, `b ${button} ${event.type === 'mousedown' ? 1 : 0}\n`); + if (button) + session.input( + RELATIVE_MOUSE_ID, + `b ${button} ${event.type === 'mousedown' ? 1 : 0}\n`, + ); event.preventDefault(); event.stopImmediatePropagation(); }; @@ -799,21 +813,40 @@ while True: screen.addEventListener('mousedown', requestPointerLock, { capture: true }); document.addEventListener('mousemove', handleMouseMove, { capture: true }); - document.addEventListener('pointermove', handleMouseMove, { capture: true }); - document.addEventListener('mousedown', handleMouseButton, { capture: true }); + document.addEventListener('pointermove', handleMouseMove, { + capture: true, + }); + document.addEventListener('mousedown', handleMouseButton, { + capture: true, + }); document.addEventListener('mouseup', handleMouseButton, { capture: true }); - document.addEventListener('contextmenu', handleContextMenu, { capture: true }); + document.addEventListener('contextmenu', handleContextMenu, { + capture: true, + }); document.addEventListener('pointerlockchange', updatePointerLockState); return () => { - screen.removeEventListener('mousedown', requestPointerLock, { capture: true }); - document.removeEventListener('mousemove', handleMouseMove, { capture: true }); - document.removeEventListener('pointermove', handleMouseMove, { capture: true }); - document.removeEventListener('mousedown', handleMouseButton, { capture: true }); - document.removeEventListener('mouseup', handleMouseButton, { capture: true }); - document.removeEventListener('contextmenu', handleContextMenu, { capture: true }); + screen.removeEventListener('mousedown', requestPointerLock, { + capture: true, + }); + document.removeEventListener('mousemove', handleMouseMove, { + capture: true, + }); + document.removeEventListener('pointermove', handleMouseMove, { + capture: true, + }); + document.removeEventListener('mousedown', handleMouseButton, { + capture: true, + }); + document.removeEventListener('mouseup', handleMouseButton, { + capture: true, + }); + document.removeEventListener('contextmenu', handleContextMenu, { + capture: true, + }); document.removeEventListener('pointerlockchange', updatePointerLockState); - if (relativeMouseFlushTimer) appWindow.clearTimeout(relativeMouseFlushTimer); + if (relativeMouseFlushTimer) + appWindow.clearTimeout(relativeMouseFlushTimer); relativeMouseFlushTimer = 0; relativeMouseEnabled = false; }; @@ -844,7 +877,8 @@ while True: rfb.addEventListener('disconnect', (event) => { if (connected) console.warn('[rfb] disconnected', event); connected = false; - if (!reconnectTimer) reconnectTimer = appWindow.setTimeout(connect, 1000); + if (!reconnectTimer) + reconnectTimer = appWindow.setTimeout(connect, 1000); }); rfb.addEventListener('connect', () => { @@ -912,7 +946,8 @@ xrandr -d :99 --output screen --mode "$MODE" >/dev/null const json = line.slice(REMOTE_WINDOWS.length).trim(); try { const windows = (JSON.parse(json) as RemoteWindow[]).filter( - (window) => typeof window.id === 'string' && typeof window.title === 'string', + (window) => + typeof window.id === 'string' && typeof window.title === 'string', ); const windowsById = new Map(windows.map((window) => [window.id, window])); const orderedWindows = remoteWindows @@ -920,13 +955,18 @@ xrandr -d :99 --output screen --mode "$MODE" >/dev/null .filter((window): window is RemoteWindow => !!window); for (const window of windows) { - if (!orderedWindows.some((orderedWindow) => orderedWindow.id === window.id)) { + if ( + !orderedWindows.some( + (orderedWindow) => orderedWindow.id === window.id, + ) + ) { orderedWindows.push(window); } } remoteWindows = orderedWindows; - if (orderedWindows.some(isRelativeMouseCandidateWindow)) showRelativeMouseToast(); + if (orderedWindows.some(isRelativeMouseCandidateWindow)) + showRelativeMouseToast(); } catch (error) { console.warn('[wm] failed to parse windows', error, line); } @@ -939,7 +979,8 @@ xrandr -d :99 --output screen --mode "$MODE" >/dev/null }; const spawnDesktop = (onDesktopWindowsReady: () => void) => { - const command = 'set -e\nsource "./.pyvenv311/bin/activate"\npython -B "$MAIN_FILE"'; + const command = + 'set -e\nsource "./.pyvenv311/bin/activate"\npython -B "$MAIN_FILE"'; startTaskTerminal({ id: 'task-desktop', @@ -971,7 +1012,8 @@ xrandr -d :99 --output screen --mode "$MODE" >/dev/null type: 'echopty', track: false, onOutput: (_stream, text) => { - if (text.includes('Relative mouse helper ready')) relativeMouseReady = true; + if (text.includes('Relative mouse helper ready')) + relativeMouseReady = true; }, }); }; @@ -1041,7 +1083,8 @@ xrandr -d :99 --output screen --mode "$MODE" >/dev/null return () => { detachRelativeMouse(); - if (relativeMouseHintTimer) appWindow.clearTimeout(relativeMouseHintTimer); + if (relativeMouseHintTimer) + appWindow.clearTimeout(relativeMouseHintTimer); relativeMouseHintTimer = 0; showRelativeMouseHint = false; vncScreen = undefined; @@ -1050,7 +1093,8 @@ xrandr -d :99 --output screen --mode "$MODE" >/dev/null const focusVncScreen = () => { const canvas = vncScreen?.querySelector('canvas'); - if (canvas instanceof HTMLCanvasElement) canvas.focus({ preventScroll: true }); + if (canvas instanceof HTMLCanvasElement) + canvas.focus({ preventScroll: true }); }; const focusTerminalApp = () => { @@ -1060,7 +1104,8 @@ xrandr -d :99 --output screen --mode "$MODE" >/dev/null terminal?.focus({ preventScroll: true }); }; - const shellsCount = () => terminals.filter((terminal) => terminal.kind === 'terminal').length; + const shellsCount = () => + terminals.filter((terminal) => terminal.kind === 'terminal').length; const hasRemoteWindowNamed = (name: string) => remoteWindows.some((window) => window.title.toLowerCase().includes(name)); @@ -1157,7 +1202,6 @@ xprop -root _NET_SUPPORTING_WM_CHECK >/dev/null ]; const launchTerminal = () => { - const id = `terminal-${Date.now()}`; terminals = [ ...terminals, @@ -1274,7 +1318,10 @@ PY`, terminal.id === id ? { ...terminal, - running: stream === 'system' && text.includes('exited') ? false : terminal.running, + running: + stream === 'system' && text.includes('exited') + ? false + : terminal.running, lines: [...terminal.lines, { stream, text }].slice(-500), } : terminal, @@ -1352,8 +1399,13 @@ PY`, }); } catch (error) { taskOutputHandlers.delete(id); - const spawnError = error instanceof Error ? error : new Error(String(error)); - markTerminalFinished(id, true, `\nTask failed to start: ${spawnError.message}\n`); + const spawnError = + error instanceof Error ? error : new Error(String(error)); + markTerminalFinished( + id, + true, + `\nTask failed to start: ${spawnError.message}\n`, + ); onFailure?.(spawnError); return; } @@ -1391,70 +1443,86 @@ PY`, session?.input(id, input); }; + const onDrop = async (files: File[]) => { + const transferFiles: Record = {}; + let script = ''; + for(const file of files){ + transferFiles[file.name + '._'] = btoa(String.fromCharCode(...new Uint8Array(await file.arrayBuffer()))); + script = `${script};cat ${file.name}._ | base64 -d > ${file.name};rm ${file.name}._` + } + await session.transfer(transferFiles); + await startTaskTerminal({ + id: `decode-files-${Date.now()}`, + title: 'Decode files', + command: script + }) + } -
- -{#if showRelativeMouseHint || relativeMouseEnabled} -
- {relativeMouseEnabled - ? 'Relative mouse active. Press Esc to release.' - : 'Game detected. Ctrl+click the desktop for relative mouse.'} -
-{/if} - - - -
- +
-
- Loading Cod -
+
+ Loading Cod +
+ diff --git a/src/vendor/DropFileFallbackSvg.svelte b/src/vendor/DropFileFallbackSvg.svelte new file mode 100644 index 0000000..691f419 --- /dev/null +++ b/src/vendor/DropFileFallbackSvg.svelte @@ -0,0 +1,37 @@ + + + + Drop files(s) or click to browse the file system + + {#if over} + + + + {:else} + + + {/if} + + + {#if over} + Drop it! + {:else} + Drop file(s) or click to + browse the file system + {/if} + + From 2d3feb5f9095ebe0480bdd45d40e36f7e7417947 Mon Sep 17 00:00:00 2001 From: gkgoat <65402989+gkgoat1@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:30:00 -0400 Subject: [PATCH 3/3] switch to runes, specialize for this use-case --- src/vendor/DropFile.svelte | 91 ++++++++++----------------- src/vendor/DropFileFallbackSvg.svelte | 37 ----------- 2 files changed, 33 insertions(+), 95 deletions(-) delete mode 100644 src/vendor/DropFileFallbackSvg.svelte diff --git a/src/vendor/DropFile.svelte b/src/vendor/DropFile.svelte index 1b472e9..30e0aa3 100644 --- a/src/vendor/DropFile.svelte +++ b/src/vendor/DropFile.svelte @@ -1,81 +1,71 @@
- -
- -
-
+ {@render children()}
diff --git a/src/vendor/DropFileFallbackSvg.svelte b/src/vendor/DropFileFallbackSvg.svelte deleted file mode 100644 index 691f419..0000000 --- a/src/vendor/DropFileFallbackSvg.svelte +++ /dev/null @@ -1,37 +0,0 @@ - - - - Drop files(s) or click to browse the file system - - {#if over} - - - - {:else} - - - {/if} - - - {#if over} - Drop it! - {:else} - Drop file(s) or click to - browse the file system - {/if} - -