@@ -441,6 +441,7 @@ def fatal(msg):
441441 </div>
442442 <div class="toolbar">
443443 <div class="toolbar-group">
444+ <button id="btn-sticky-shift" onclick="toggleSticky('ShiftLeft', 0xffe1, this)" title="Sticky Shift">Shift</button>
444445 <button id="btn-sticky-ctrl" onclick="toggleSticky('ControlLeft', 0xffe3, this)" title="Sticky Ctrl">Ctrl</button>
445446 <button id="btn-sticky-alt" onclick="toggleSticky('AltLeft', 0xffe9, this)" title="Sticky Alt">Alt</button>
446447 <button id="btn-sticky-meta" onclick="toggleSticky('MetaLeft', 0xffeb, this)" title="Sticky Meta">
@@ -920,16 +921,17 @@ def fatal(msg):
920921document.addEventListener('keydown', e => sendKey(e, true));
921922document.addEventListener('keyup', e => sendKey(e, false));
922923
924+ // Track which keysym was sent for each physical code to ensure consistent keyup
925+ const pressedKeysyms = {};
926+
923927function sendKey(e, down) {
924928 if (!ws) return;
925929
926- // Captured keys that we handle via code-to-keysym mapping
927930 const code = e.code;
928931 const key = e.key;
929932
930933 // Support Ctrl+V (Windows/Linux) or Cmd+V (Mac) for pasting
931934 if ((e.ctrlKey || e.metaKey) && (key === 'v' || key === 'V' || code === 'KeyV')) {
932- // We let the 'paste' event handle this to avoid permission prompts where possible
933935 return;
934936 }
935937
@@ -948,47 +950,63 @@ def fatal(msg):
948950 }
949951
950952 const keyMap = {
951- // Special keys
952953 'Backspace': 0xff08, 'Tab': 0xff09, 'Enter': 0xff0d, 'Escape': 0xff1b, 'Delete': 0xffff,
953954 'Home': 0xff50, 'End': 0xff57, 'PageUp': 0xff55, 'PageDown': 0xff56,
954955 'ArrowLeft': 0xff51, 'ArrowUp': 0xff52, 'ArrowRight': 0xff53, 'ArrowDown': 0xff54, 'Insert': 0xff63,
955956 'F1': 0xffbe, 'F2': 0xffbf, 'F3': 0xffc0, 'F4': 0xffc1, 'F5': 0xffc2, 'F6': 0xffc3,
956957 'F7': 0xffc4, 'F8': 0xffc5, 'F9': 0xffc6, 'F10': 0xffc7, 'F11': 0xffc8, 'F12': 0xffc9,
957958 'ShiftLeft': 0xffe1, 'ShiftRight': 0xffe2, 'ControlLeft': 0xffe3, 'ControlRight': 0xffe4,
958959 'AltLeft': 0xffe9, 'AltRight': 0xffea, 'MetaLeft': 0xffeb, 'MetaRight': 0xffec, 'Space': 0x0020,
959- // Map Digit keys to their base ASCII (prevents Shift+1 sending '!')
960- 'Digit1': 0x31, 'Digit2': 0x32, 'Digit3': 0x33, 'Digit4': 0x34, 'Digit5': 0x35,
961- 'Digit6': 0x36, 'Digit7': 0x37, 'Digit8': 0x38, 'Digit9': 0x39, 'Digit0': 0x30,
962- // Map alphabet keys
963- 'KeyA': 0x61, 'KeyB': 0x62, 'KeyC': 0x63, 'KeyD': 0x64, 'KeyE': 0x65, 'KeyF': 0x66, 'KeyG': 0x67,
964- 'KeyH': 0x68, 'KeyI': 0x69, 'KeyJ': 0x6a, 'KeyK': 0x6b, 'KeyL': 0x6c, 'KeyM': 0x6d, 'KeyN': 0x6e,
965- 'KeyO': 0x6f, 'KeyP': 0x70, 'KeyQ': 0x71, 'KeyR': 0x72, 'KeyS': 0x73, 'KeyT': 0x74, 'KeyU': 0x75,
966- 'KeyV': 0x76, 'KeyW': 0x77, 'KeyX': 0x78, 'KeyY': 0x79, 'KeyZ': 0x7a,
967- // Punctuations (using e.code ensures we send the base keysym regardless of Shift)
968- 'Semicolon': 0x3b, 'Equal': 0x3d, 'Comma': 0x2c, 'Minus': 0x2d, 'Period': 0x2e, 'Slash': 0x2f,
969- 'Backquote': 0x60, 'BracketLeft': 0x5b, 'Backslash': 0x5c, 'BracketRight': 0x5d, 'Quote': 0x27
960+ 'Shift': 0xffe1, 'Control': 0xffe3, 'Alt': 0xffe9, 'Meta': 0xffeb
970961 };
971962
972963 let keysym = 0;
973- if (keyMap[code]) {
974- keysym = keyMap[code];
975- } else if (keyMap[key]) {
976- keysym = keyMap[key];
977- } else if (key.length === 1) {
978- keysym = key.charCodeAt(0);
964+ if (down) {
965+ // Prioritize specific control keys
966+ if (keyMap[code]) {
967+ keysym = keyMap[code];
968+ } else if (keyMap[key]) {
969+ keysym = keyMap[key];
970+ } else if (key.length === 1) {
971+ let char = key;
972+ const softShift = stickyStates['ShiftLeft'] || stickyStates['ShiftRight'];
973+ if (softShift && !e.shiftKey) {
974+ // If software Shift is on but physical is not, escalate letters to uppercase.
975+ // This is necessary because VNC servers often interpret keysyms literally.
976+ if (char >= 'a' && char <= 'z') char = char.toUpperCase();
977+ else if (char >= 'A' && char <= 'Z') char = char.toLowerCase(); // Caps lock inverse? No, stick to shift logic.
978+ }
979+
980+ keysym = char.charCodeAt(0);
981+ // If Ctrl or Alt is down, we want the base keysym (e.g. 'c' for Ctrl+C)
982+ if ((e.ctrlKey || e.altKey || e.metaKey) && keysym < 32) {
983+ if (keysym >= 1 && keysym <= 26) keysym += 96;
984+ }
985+ }
986+
987+ if (keysym) {
988+ pressedKeysyms[code] = keysym;
989+ }
979990 } else {
980- return;
991+ keysym = pressedKeysyms[code];
992+ delete pressedKeysyms[code];
993+
994+ // Fallback for keyup if keydown was missed
995+ if (!keysym) {
996+ if (keyMap[code]) keysym = keyMap[code];
997+ else if (keyMap[key]) keysym = keyMap[key];
998+ else if (key.length === 1) keysym = key.charCodeAt(0);
999+ }
9811000 }
9821001
1002+ if (!keysym) return;
1003+
9831004 e.preventDefault();
9841005
9851006 try {
9861007 ws.send(new Uint8Array([
9871008 4, down ? 1 : 0, 0, 0,
988- (keysym >> 24) & 0xff,
989- (keysym >> 16) & 0xff,
990- (keysym >> 8) & 0xff,
991- keysym & 0xff
1009+ (keysym >> 24) & 0xff, (keysym >> 16) & 0xff, (keysym >> 8) & 0xff, keysym & 0xff
9921010 ]));
9931011 } catch (err) {
9941012 console.error("Failed to send key:", err);
0 commit comments