diff --git a/docs/screenshots/wraith-final.png b/docs/screenshots/wraith-final.png
deleted file mode 100644
index 674a7d7..0000000
Binary files a/docs/screenshots/wraith-final.png and /dev/null differ
diff --git a/frontend/src/components/common/SettingsModal.vue b/frontend/src/components/common/SettingsModal.vue
index e309797..7a44ebe 100644
--- a/frontend/src/components/common/SettingsModal.vue
+++ b/frontend/src/components/common/SettingsModal.vue
@@ -268,8 +268,11 @@
diff --git a/frontend/src/composables/useRdp.ts b/frontend/src/composables/useRdp.ts
index 17a2a74..4e8a47b 100644
--- a/frontend/src/composables/useRdp.ts
+++ b/frontend/src/composables/useRdp.ts
@@ -1,4 +1,7 @@
import { ref, onBeforeUnmount } from "vue";
+import { Call } from "@wailsio/runtime";
+
+const APP = "github.com/vstockwell/wraith/internal/app.WraithApp";
/**
* RDP mouse event flags — match the Go constants in internal/rdp/input.go
@@ -199,71 +202,50 @@ export function useRdp(): UseRdpReturn {
let frameCount = 0;
/**
- * Fetch the current frame from the backend.
- * TODO: Replace with Wails binding — RDPService.GetFrame(sessionId)
- * Mock: generates a gradient test pattern.
+ * Fetch the current frame from the Go RDP backend.
+ *
+ * Go's GetFrame returns []byte (raw RGBA, Width*Height*4 bytes).
+ * Wails serialises Go []byte as a base64-encoded string over the JSON bridge,
+ * so we decode it back to a Uint8ClampedArray and wrap it in an ImageData.
*/
async function fetchFrame(
sessionId: string,
width = 1920,
height = 1080,
): Promise {
- void sessionId;
-
- // Mock: generate a test frame with animated gradient
- const imageData = new ImageData(width, height);
- const data = imageData.data;
- const t = Date.now() / 1000;
-
- for (let y = 0; y < height; y++) {
- for (let x = 0; x < width; x++) {
- const i = (y * width + x) * 4;
- const nx = x / width;
- const ny = y / height;
- const diag = (nx + ny) / 2;
-
- data[i + 0] = Math.floor(20 + diag * 40); // R
- data[i + 1] = Math.floor(25 + (1 - diag) * 30); // G
- data[i + 2] = Math.floor(80 + diag * 100); // B
- data[i + 3] = 255; // A
-
- // Grid lines every 100px
- if (x % 100 === 0 || y % 100 === 0) {
- data[i + 0] = Math.min(data[i + 0] + 20, 255);
- data[i + 1] = Math.min(data[i + 1] + 20, 255);
- data[i + 2] = Math.min(data[i + 2] + 20, 255);
- }
- }
+ let raw: string;
+ try {
+ raw = (await Call.ByName(`${APP}.RDPGetFrame`, sessionId)) as string;
+ } catch {
+ // Session may not be connected yet or backend returned an error — skip frame
+ return null;
}
- // Animated pulsing circle at center
- const cx = width / 2;
- const cy = height / 2;
- const radius = 40 + 20 * Math.sin(t * 2);
+ if (!raw) return null;
- for (let dy = -70; dy <= 70; dy++) {
- for (let dx = -70; dx <= 70; dx++) {
- const dist = Math.sqrt(dx * dx + dy * dy);
- if (dist <= radius && dist >= radius - 4) {
- const px = Math.floor(cx + dx);
- const py = Math.floor(cy + dy);
- if (px >= 0 && px < width && py >= 0 && py < height) {
- const i = (py * width + px) * 4;
- data[i + 0] = 88;
- data[i + 1] = 166;
- data[i + 2] = 255;
- data[i + 3] = 255;
- }
- }
- }
+ // Decode base64 → binary string → Uint8ClampedArray
+ const binaryStr = atob(raw);
+ const bytes = new Uint8ClampedArray(binaryStr.length);
+ for (let i = 0; i < binaryStr.length; i++) {
+ bytes[i] = binaryStr.charCodeAt(i);
}
- return imageData;
+ // Validate: RGBA requires exactly width * height * 4 bytes
+ const expected = width * height * 4;
+ if (bytes.length !== expected) {
+ console.warn(
+ `[useRdp] Frame size mismatch: got ${bytes.length}, expected ${expected}`,
+ );
+ return null;
+ }
+
+ return new ImageData(bytes, width, height);
}
/**
- * Send a mouse event.
- * TODO: Replace with Wails binding — RDPService.SendMouse(sessionId, x, y, flags)
+ * Send a mouse event to the remote session.
+ * Calls Go WraithApp.RDPSendMouse(sessionId, x, y, flags).
+ * Fire-and-forget — mouse events are best-effort.
*/
function sendMouse(
sessionId: string,
@@ -271,16 +253,17 @@ export function useRdp(): UseRdpReturn {
y: number,
flags: number,
): void {
- void sessionId;
- void x;
- void y;
- void flags;
- // Mock: no-op — will call Wails binding when wired
+ Call.ByName(`${APP}.RDPSendMouse`, sessionId, x, y, flags).catch(
+ (err: unknown) => {
+ console.warn("[useRdp] sendMouse failed:", err);
+ },
+ );
}
/**
- * Send a key event, mapping JS code to RDP scancode.
- * TODO: Replace with Wails binding — RDPService.SendKey(sessionId, scancode, pressed)
+ * Send a key event, mapping the JS KeyboardEvent.code to an RDP scancode.
+ * Calls Go WraithApp.RDPSendKey(sessionId, scancode, pressed).
+ * Unmapped keys are silently dropped — not every JS key has an RDP scancode.
*/
function sendKey(
sessionId: string,
@@ -290,19 +273,23 @@ export function useRdp(): UseRdpReturn {
const scancode = jsKeyToScancode(code);
if (scancode === null) return;
- void sessionId;
- void pressed;
- // Mock: no-op — will call Wails binding when wired
+ Call.ByName(`${APP}.RDPSendKey`, sessionId, scancode, pressed).catch(
+ (err: unknown) => {
+ console.warn("[useRdp] sendKey failed:", err);
+ },
+ );
}
/**
- * Send clipboard text to the remote session.
- * TODO: Replace with Wails binding — RDPService.SendClipboard(sessionId, text)
+ * Send clipboard text to the remote RDP session.
+ * Calls Go WraithApp.RDPSendClipboard(sessionId, text).
*/
function sendClipboard(sessionId: string, text: string): void {
- void sessionId;
- void text;
- // Mock: no-op
+ Call.ByName(`${APP}.RDPSendClipboard`, sessionId, text).catch(
+ (err: unknown) => {
+ console.warn("[useRdp] sendClipboard failed:", err);
+ },
+ );
}
/**
diff --git a/frontend/src/layouts/MainLayout.vue b/frontend/src/layouts/MainLayout.vue
index 0b999a0..8d02373 100644
--- a/frontend/src/layouts/MainLayout.vue
+++ b/frontend/src/layouts/MainLayout.vue
@@ -212,6 +212,7 @@