feat: add Shift modifier key and fix keyboard toggle behavior

- 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 <noreply@anthropic.com>
main
I Luk Kim 5 months ago
parent 8bcd7eacd8
commit 239c6d2ef6

@ -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; } .key-row.shortcut-keys .key.shortcut { min-width: 32px; padding: 0 5px; }
.keyboard-toggle { display: none; } .keyboard-toggle {
@media (min-width: 1024px) { display: flex; align-items: center; justify-content: center; gap: 6px;
.keyboard-toggle { width: 100%; height: 28px;
display: flex; align-items: center; justify-content: center; gap: 6px; background: var(--bg-secondary); border: none;
width: 100%; height: 28px; border-top: 1px solid var(--border-color);
background: var(--bg-secondary); border: none; color: var(--text-dimmed); font-size: 0.72rem;
border-top: 1px solid var(--border-color); cursor: pointer; flex-shrink: 0;
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:hover { color: var(--text-secondary); background: var(--bg-tertiary); }
.virtual-keyboard.collapsed .key-row:not(.input-row) { display: none; }

@ -33,8 +33,7 @@
<span class="app-title">cmux-remote</span> <span class="app-title">cmux-remote</span>
<div class="top-bar-right"> <div class="top-bar-right">
<span class="connection-status" id="connection-status">Offline</span> <span class="connection-status" id="connection-status">Offline</span>
<button class="qr-btn" id="qr-btn" aria-label="Show QR code">&#x2337;</button> <button class="theme-toggle" id="theme-toggle" aria-label="Toggle theme"></button>
<button class="theme-toggle" id="theme-toggle" aria-label="Toggle theme"></button>
</div> </div>
</header> </header>
@ -44,13 +43,14 @@
</main> </main>
<!-- Virtual keyboard --> <!-- Virtual keyboard -->
<button class="keyboard-toggle" id="keyboard-toggle" aria-label="Toggle keyboard" hidden>&#x2328; Keyboard</button> <button class="keyboard-toggle" id="keyboard-toggle" aria-label="Toggle keyboard">&#x2328; Keyboard</button>
<footer class="virtual-keyboard" id="virtual-keyboard"> <footer class="virtual-keyboard" id="virtual-keyboard">
<div class="key-row special-keys"> <div class="key-row special-keys">
<button class="key" data-key="Escape">Esc</button> <button class="key" data-key="Escape">Esc</button>
<button class="key" data-key="Tab">Tab</button> <button class="key" data-key="Tab">Tab</button>
<button class="key modifier" data-modifier="ctrl" id="ctrl-key">Ctrl</button> <button class="key modifier" data-modifier="ctrl" id="ctrl-key">Ctrl</button>
<button class="key modifier" data-modifier="alt" id="alt-key">Alt</button> <button class="key modifier" data-modifier="alt" id="alt-key">Alt</button>
<button class="key modifier" data-modifier="shift" id="shift-key">&#x21E7;</button>
<span class="key-spacer"></span> <span class="key-spacer"></span>
<button class="key" data-key="ArrowUp">&uarr;</button> <button class="key" data-key="ArrowUp">&uarr;</button>
<button class="key" data-key="PageUp">PgUp</button> <button class="key" data-key="PageUp">PgUp</button>
@ -66,7 +66,6 @@
<button class="key shortcut" data-key="Ctrl-r">^R</button> <button class="key shortcut" data-key="Ctrl-r">^R</button>
<button class="key shortcut" data-key="Ctrl-w">^W</button> <button class="key shortcut" data-key="Ctrl-w">^W</button>
<button class="key shortcut" data-key="Ctrl-u">^U</button> <button class="key shortcut" data-key="Ctrl-u">^U</button>
<span class="key-spacer"></span>
<button class="key" data-key="ArrowLeft">&larr;</button> <button class="key" data-key="ArrowLeft">&larr;</button>
<button class="key" data-key="ArrowDown">&darr;</button> <button class="key" data-key="ArrowDown">&darr;</button>
<button class="key" data-key="ArrowRight">&rarr;</button> <button class="key" data-key="ArrowRight">&rarr;</button>
@ -79,16 +78,7 @@
</footer> </footer>
</div> </div>
<!-- QR modal --> <script src="/js/websocket-client.js"></script>
<div class="qr-modal-overlay" id="qr-modal-overlay" hidden>
<div class="qr-modal" role="dialog" aria-modal="true" aria-label="QR Code">
<button class="qr-modal-close" id="qr-modal-close" aria-label="Close">&times;</button>
<img class="qr-modal-image" src="/api/qr.svg" alt="QR code for access URL" id="qr-modal-image">
<div class="qr-modal-url" id="qr-modal-url"></div>
</div>
</div>
<script src="/js/websocket-client.js"></script>
<script src="/js/terminal-view.js"></script> <script src="/js/terminal-view.js"></script>
<script src="/js/virtual-keyboard.js"></script> <script src="/js/virtual-keyboard.js"></script>
<script src="/js/sidebar.js"></script> <script src="/js/sidebar.js"></script>

@ -6,6 +6,8 @@ class VirtualKeyboard {
this.ctrlLocked = false; this.ctrlLocked = false;
this.altActive = false; this.altActive = false;
this.altLocked = false; this.altLocked = false;
this.shiftActive = false;
this.shiftLocked = false;
this._repeatTimer = null; this._repeatTimer = null;
this._repeatInterval = null; this._repeatInterval = null;
@ -13,6 +15,7 @@ class VirtualKeyboard {
this.sendBtn = document.getElementById('send-btn'); this.sendBtn = document.getElementById('send-btn');
this.ctrlKey = document.getElementById('ctrl-key'); this.ctrlKey = document.getElementById('ctrl-key');
this.altKey = document.getElementById('alt-key'); this.altKey = document.getElementById('alt-key');
this.shiftKey = document.getElementById('shift-key');
this.init(); this.init();
} }
@ -42,6 +45,10 @@ class VirtualKeyboard {
this.altKey.addEventListener('click', () => this.toggleModifier('alt')); this.altKey.addEventListener('click', () => this.toggleModifier('alt'));
this.altKey.addEventListener('dblclick', () => this.lockModifier('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 // Special keys
document.querySelectorAll('.key[data-key]').forEach((btn) => { document.querySelectorAll('.key[data-key]').forEach((btn) => {
if (btn.classList.contains('modifier')) return; if (btn.classList.contains('modifier')) return;
@ -94,20 +101,24 @@ class VirtualKeyboard {
handleKeyPress(key) { handleKeyPress(key) {
this._haptic(); this._haptic();
// Check for shortcut keys like Ctrl-c // Check for pre-built shortcut keys like Ctrl-c
if (key.startsWith('Ctrl-')) { if (key.startsWith('Ctrl-') || key.startsWith('Alt-') || key.startsWith('Shift-')) {
this.onSendKey(key); this.onSendKey(key);
return; return;
} }
// Apply active modifiers // Apply active modifiers
let finalKey = key; let finalKey = key;
if (this.shiftActive) {
finalKey = `Shift-${finalKey}`;
if (!this.shiftLocked) this.deactivateModifier('shift');
}
if (this.ctrlActive) { if (this.ctrlActive) {
finalKey = `Ctrl-${key}`; finalKey = `Ctrl-${finalKey}`;
if (!this.ctrlLocked) this.deactivateModifier('ctrl'); if (!this.ctrlLocked) this.deactivateModifier('ctrl');
} }
if (this.altActive) { if (this.altActive) {
finalKey = `Alt-${key}`; finalKey = `Alt-${finalKey}`;
if (!this.altLocked) this.deactivateModifier('alt'); if (!this.altLocked) this.deactivateModifier('alt');
} }
@ -117,19 +128,17 @@ class VirtualKeyboard {
toggleModifier(mod) { toggleModifier(mod) {
this._haptic(); this._haptic();
if (mod === 'ctrl') { if (mod === 'ctrl') {
if (this.ctrlLocked) { if (this.ctrlLocked) { this.deactivateModifier('ctrl'); return; }
this.deactivateModifier('ctrl');
return;
}
this.ctrlActive = !this.ctrlActive; this.ctrlActive = !this.ctrlActive;
this.ctrlKey.classList.toggle('active', this.ctrlActive); this.ctrlKey.classList.toggle('active', this.ctrlActive);
} else if (mod === 'alt') { } else if (mod === 'alt') {
if (this.altLocked) { if (this.altLocked) { this.deactivateModifier('alt'); return; }
this.deactivateModifier('alt');
return;
}
this.altActive = !this.altActive; this.altActive = !this.altActive;
this.altKey.classList.toggle('active', 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.altActive = true;
this.altLocked = true; this.altLocked = true;
this.altKey.classList.add('active', 'locked'); 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.altActive = false;
this.altLocked = false; this.altLocked = false;
this.altKey.classList.remove('active', 'locked'); this.altKey.classList.remove('active', 'locked');
} else if (mod === 'shift') {
this.shiftActive = false;
this.shiftLocked = false;
this.shiftKey.classList.remove('active', 'locked');
} }
} }

@ -46,6 +46,15 @@ export class InputHandler {
return; 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 // Map known keys
const mapped = KEY_MAP[key] || key; const mapped = KEY_MAP[key] || key;
await this.client.sendKey(workspace, surface, mapped); await this.client.sendKey(workspace, surface, mapped);

Loading…
Cancel
Save