Skip to content

Commit 8d43e0f

Browse files
authored
Merge pull request #72 from reclaimprotocol/fix/desktop-cmd
Fix/desktop cmd
1 parent b85f627 commit 8d43e0f

18 files changed

Lines changed: 610 additions & 49 deletions

File tree

images/minimal-vnc-desktop/kbd-autofocus.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ import { createFbScaleWatch, FBSCALE_MODE } from './kbd/fbscale.js';
196196
const onProxyPaste = clip.onProxyPaste;
197197
const flushLocalClipboard = clip.flushLocalClipboard;
198198
const onRemoteClipboard = clip.onRemoteClipboard;
199+
const releaseRemoteModifiers = clip.releaseRemoteModifiers;
200+
const requestClipboardPasteFallback = clip.requestClipboardPasteFallback;
199201

200202
// ---- Local echo (optimistic typing) --------------------------------------
201203
// The optimistic "unconfirmed typing" pill lives in ./kbd/echo.js; instantiate
@@ -232,6 +234,8 @@ import { createFbScaleWatch, FBSCALE_MODE } from './kbd/fbscale.js';
232234
clearEcho,
233235
onProxyPaste,
234236
flushLocalClipboard,
237+
releaseRemoteModifiers,
238+
requestClipboardPasteFallback,
235239
});
236240
const onDesktopKeyDown = desktopBridge.onDesktopKeyDown;
237241
const onDesktopInput = desktopBridge.onDesktopInput;

images/minimal-vnc-desktop/kbd/clipboard.js

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import { dbg } from './diag.js';
2020
import { nowMs } from './env.js';
21+
import { ALL_MODIFIER_KEYSYMS } from './keys.js';
2122

2223
export function createClipboard({
2324
getRfb, getProxy, getHints, getFocusKey, sendText, sendSpecialKey,
@@ -26,6 +27,16 @@ export function createClipboard({
2627
let remoteClipboardText = null; // latest text the remote copied
2728
let pendingLocalWrite = false; // remote text awaiting a user-gesture write
2829
let pendingSince = 0;
30+
let pasteGeneration = 0; // cancels stale/made-redundant fallback reads
31+
32+
// Prevent stale modifiers from turning injected text into shortcuts.
33+
function releaseRemoteModifiers() {
34+
const rfb = getRfb();
35+
if (!rfb) return;
36+
for (const keysym of ALL_MODIFIER_KEYSYMS) {
37+
try { rfb.sendKey(keysym, null, false); } catch (_) {}
38+
}
39+
}
2940

3041
// Ctrl+V on the remote — used to paste a long insert we staged on the remote
3142
// clipboard in one shot instead of N per-char keysym round-trips.
@@ -38,12 +49,15 @@ export function createClipboard({
3849
// 'v' and the paste chord silently does the wrong thing. Sending the bare
3950
// keysym lets Xvnc pick a keycode that actually yields 'v' — deterministic,
4051
// layout-independent, and consistent with the rest of the text-injection path.
52+
// Always release Control, even if a send fails mid-chord.
4153
try {
4254
rfb.sendKey(0xffe3, null, true); // Control down
4355
rfb.sendKey(0x0076, null, true); // v down
4456
rfb.sendKey(0x0076, null, false); // v up
45-
rfb.sendKey(0xffe3, null, false); // Control up
46-
} catch (_) {}
57+
} catch (_) {
58+
} finally {
59+
try { rfb.sendKey(0xffe3, null, false); } catch (_) {} // Control up
60+
}
4761
}
4862

4963
// Shift+Tab on the remote — moves to the PREVIOUS focusable field (plain Tab
@@ -104,15 +118,30 @@ export function createClipboard({
104118
} catch (_) { return false; }
105119
}
106120

121+
function normalizePastedText(text) {
122+
if (!text) return '';
123+
const hints = getHints();
124+
if (hints && hints.tag === 'INPUT') return text.replace(/[\r\n]+/g, '');
125+
return text;
126+
}
127+
128+
function canStageRemoteClipboard(text, rfb) {
129+
if (typeof rfb.clipboardPasteFrom !== 'function') return false;
130+
const encodingSupported = /^[\x00-\xff]*$/.test(text) || serverExtendedClipboard();
131+
if (!encodingSupported) return false;
132+
return text.length > 32 || !getFocusKey();
133+
}
134+
107135
function insertPastedText(text) {
108136
const rfb = getRfb();
109-
if (!text || !rfb) return;
137+
if (!rfb) return;
110138
// Single-line field: strip newlines so a pasted trailing \n doesn't fire
111139
// Enter and instantly submit/navigate. INPUT is the positive test — TEXTAREA
112140
// and contenteditable report other tags and legitimately keep their newlines.
113-
const hints = getHints();
114-
if (hints && hints.tag === 'INPUT') text = text.replace(/[\r\n]+/g, '');
141+
text = normalizePastedText(text);
115142
if (!text) return;
143+
// Ensure the active paste modifier cannot affect injected text.
144+
releaseRemoteModifiers();
116145
// Stage on the remote clipboard + Ctrl+V (one round-trip) instead of per-char
117146
// keysyms when either:
118147
// long text — a big win over N keysyms on a 3G link;
@@ -125,24 +154,49 @@ export function createClipboard({
125154
// lossless UTF-8 path instead of the '?'-corrupting ISO-8859-1 fallback.
126155
// Otherwise fall back to per-char sendText — that text is non-Latin-1, so it
127156
// cannot trigger an ASCII shortcut either.
128-
if ((text.length > 32 || !getFocusKey()) && typeof rfb.clipboardPasteFrom === 'function' &&
129-
(/^[\x00-\xff]*$/.test(text) || serverExtendedClipboard())) {
157+
if (canStageRemoteClipboard(text, rfb)) {
130158
try { rfb.clipboardPasteFrom(text); remoteCtrlV(); return; } catch (_) {}
131159
}
132160
sendText(text);
133161
}
134162

135163
function onProxyPaste(e) {
164+
// Any native paste event supersedes the keydown fallback.
165+
pasteGeneration++;
136166
if (!getRfb()) return;
137167
let text = '';
138-
try { text = (e.clipboardData || window.clipboardData).getData('text/plain') || ''; } catch (_) {}
168+
try {
169+
const data = e.clipboardData || window.clipboardData;
170+
text = data.getData('text/plain') || data.getData('text') || '';
171+
} catch (_) {}
139172
if (!text) return;
140173
e.preventDefault();
141174
dbg('paste len=' + text.length);
142175
insertPastedText(text);
143176
clearProxy();
144177
}
145178

179+
// Recover when Chromium omits paste during a proxy/canvas focus handoff.
180+
// Deferring gives the native event priority and prevents duplicate insertion.
181+
function requestClipboardPasteFallback() {
182+
const generation = ++pasteGeneration;
183+
const focusAtRequest = getFocusKey();
184+
setTimeout(async () => {
185+
if (generation !== pasteGeneration) return;
186+
let text = '';
187+
try {
188+
if (!navigator.clipboard || !navigator.clipboard.readText) return;
189+
text = await navigator.clipboard.readText();
190+
} catch (_) { return; }
191+
if (generation !== pasteGeneration || !text) return;
192+
// A known field must still own focus after the asynchronous read.
193+
if (focusAtRequest && getFocusKey() !== focusAtRequest) return;
194+
dbg('paste fallback len=' + text.length);
195+
insertPastedText(text);
196+
clearProxy();
197+
}, 0);
198+
}
199+
146200
// writeText needs transient activation on iOS Safari, so a write triggered by
147201
// the remote's copy (no local gesture) can reject — buffer it and retry on the
148202
// next tap (onTouchEnd calls flushLocalClipboard).
@@ -174,5 +228,6 @@ export function createClipboard({
174228
tryWriteLocalClipboard(text); // best-effort now; retried on next gesture
175229
}
176230

177-
return { sendActionKey, insertPastedText, navRemoteField, onProxyPaste, flushLocalClipboard, onRemoteClipboard };
231+
return { sendActionKey, insertPastedText, navRemoteField, onProxyPaste, flushLocalClipboard,
232+
onRemoteClipboard, releaseRemoteModifiers, requestClipboardPasteFallback };
178233
}

0 commit comments

Comments
 (0)