From 239c6d2ef68830eec8ba89d5e19279bcd553a78d Mon Sep 17 00:00:00 2001 From: I Luk Kim Date: Thu, 19 Mar 2026 21:57:26 -0700 Subject: [PATCH] feat: add Shift modifier key and fix keyboard toggle behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Shift modifier button (⇧) alongside Ctrl/Alt with tap/double-tap lock support - Server-side Shift key parsing (Shift-Tab → shift+tab, etc.) - Keyboard toggle now only hides key rows, keeping text input always visible - Fix shortcut-keys row: horizontal scroll instead of wrapping Co-Authored-By: Claude Sonnet 4.6 --- public/css/keyboard.css | 29 ++++++++++++++----------- public/index.html | 18 ++++----------- public/js/virtual-keyboard.js | 41 +++++++++++++++++++++++++---------- src/bridge/input-handler.ts | 9 ++++++++ 4 files changed, 58 insertions(+), 39 deletions(-) diff --git a/public/css/keyboard.css b/public/css/keyboard.css index e93cac7..585b136 100644 --- a/public/css/keyboard.css +++ b/public/css/keyboard.css @@ -186,19 +186,22 @@ } } -.key-row.shortcut-keys { flex-wrap: wrap; } +.key-row.shortcut-keys { + overflow-x: auto; + flex-wrap: nowrap; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; +} +.key-row.shortcut-keys::-webkit-scrollbar { display: none; } .key-row.shortcut-keys .key.shortcut { min-width: 32px; padding: 0 5px; } -.keyboard-toggle { display: none; } -@media (min-width: 1024px) { - .keyboard-toggle { - display: flex; align-items: center; justify-content: center; gap: 6px; - width: 100%; height: 28px; - background: var(--bg-secondary); border: none; - border-top: 1px solid var(--border-color); - color: var(--text-dimmed); font-size: 0.72rem; - cursor: pointer; flex-shrink: 0; - } - .keyboard-toggle:hover { color: var(--text-secondary); background: var(--bg-tertiary); } - .virtual-keyboard.collapsed { display: none; } +.keyboard-toggle { + display: flex; align-items: center; justify-content: center; gap: 6px; + width: 100%; height: 28px; + background: var(--bg-secondary); border: none; + border-top: 1px solid var(--border-color); + color: var(--text-dimmed); font-size: 0.72rem; + cursor: pointer; flex-shrink: 0; } +.keyboard-toggle:hover { color: var(--text-secondary); background: var(--bg-tertiary); } +.virtual-keyboard.collapsed .key-row:not(.input-row) { display: none; } diff --git a/public/index.html b/public/index.html index ca35da7..50ad2e5 100644 --- a/public/index.html +++ b/public/index.html @@ -33,8 +33,7 @@ cmux-remote
Offline - - +
@@ -44,13 +43,14 @@ - +
+ @@ -66,7 +66,6 @@ - @@ -79,16 +78,7 @@
- - - - + diff --git a/public/js/virtual-keyboard.js b/public/js/virtual-keyboard.js index 12a993b..1169460 100644 --- a/public/js/virtual-keyboard.js +++ b/public/js/virtual-keyboard.js @@ -6,6 +6,8 @@ class VirtualKeyboard { this.ctrlLocked = false; this.altActive = false; this.altLocked = false; + this.shiftActive = false; + this.shiftLocked = false; this._repeatTimer = null; this._repeatInterval = null; @@ -13,6 +15,7 @@ class VirtualKeyboard { this.sendBtn = document.getElementById('send-btn'); this.ctrlKey = document.getElementById('ctrl-key'); this.altKey = document.getElementById('alt-key'); + this.shiftKey = document.getElementById('shift-key'); this.init(); } @@ -42,6 +45,10 @@ class VirtualKeyboard { this.altKey.addEventListener('click', () => this.toggleModifier('alt')); this.altKey.addEventListener('dblclick', () => this.lockModifier('alt')); + // Shift modifier + this.shiftKey.addEventListener('click', () => this.toggleModifier('shift')); + this.shiftKey.addEventListener('dblclick', () => this.lockModifier('shift')); + // Special keys document.querySelectorAll('.key[data-key]').forEach((btn) => { if (btn.classList.contains('modifier')) return; @@ -94,20 +101,24 @@ class VirtualKeyboard { handleKeyPress(key) { this._haptic(); - // Check for shortcut keys like Ctrl-c - if (key.startsWith('Ctrl-')) { + // Check for pre-built shortcut keys like Ctrl-c + if (key.startsWith('Ctrl-') || key.startsWith('Alt-') || key.startsWith('Shift-')) { this.onSendKey(key); return; } // Apply active modifiers let finalKey = key; + if (this.shiftActive) { + finalKey = `Shift-${finalKey}`; + if (!this.shiftLocked) this.deactivateModifier('shift'); + } if (this.ctrlActive) { - finalKey = `Ctrl-${key}`; + finalKey = `Ctrl-${finalKey}`; if (!this.ctrlLocked) this.deactivateModifier('ctrl'); } if (this.altActive) { - finalKey = `Alt-${key}`; + finalKey = `Alt-${finalKey}`; if (!this.altLocked) this.deactivateModifier('alt'); } @@ -117,19 +128,17 @@ class VirtualKeyboard { toggleModifier(mod) { this._haptic(); if (mod === 'ctrl') { - if (this.ctrlLocked) { - this.deactivateModifier('ctrl'); - return; - } + if (this.ctrlLocked) { this.deactivateModifier('ctrl'); return; } this.ctrlActive = !this.ctrlActive; this.ctrlKey.classList.toggle('active', this.ctrlActive); } else if (mod === 'alt') { - if (this.altLocked) { - this.deactivateModifier('alt'); - return; - } + if (this.altLocked) { this.deactivateModifier('alt'); return; } this.altActive = !this.altActive; this.altKey.classList.toggle('active', this.altActive); + } else if (mod === 'shift') { + if (this.shiftLocked) { this.deactivateModifier('shift'); return; } + this.shiftActive = !this.shiftActive; + this.shiftKey.classList.toggle('active', this.shiftActive); } } @@ -142,6 +151,10 @@ class VirtualKeyboard { this.altActive = true; this.altLocked = true; this.altKey.classList.add('active', 'locked'); + } else if (mod === 'shift') { + this.shiftActive = true; + this.shiftLocked = true; + this.shiftKey.classList.add('active', 'locked'); } } @@ -154,6 +167,10 @@ class VirtualKeyboard { this.altActive = false; this.altLocked = false; this.altKey.classList.remove('active', 'locked'); + } else if (mod === 'shift') { + this.shiftActive = false; + this.shiftLocked = false; + this.shiftKey.classList.remove('active', 'locked'); } } diff --git a/src/bridge/input-handler.ts b/src/bridge/input-handler.ts index 01fab5f..2398313 100644 --- a/src/bridge/input-handler.ts +++ b/src/bridge/input-handler.ts @@ -46,6 +46,15 @@ export class InputHandler { return; } + // Handle Shift+key combos + const shiftMatch = key.match(/^Shift[+-](.+)$/i); + if (shiftMatch) { + const inner = shiftMatch[1]; + const mapped = KEY_MAP[inner] || inner.toLowerCase(); + await this.client.sendKey(workspace, surface, `shift+${mapped}`); + return; + } + // Map known keys const mapped = KEY_MAP[key] || key; await this.client.sendKey(workspace, surface, mapped);