fix: handle terminal keyboard input on Android tablet (#1133)

* fix: handle terminal keyboard input on Android with desktop UA

- Detect Android via navigator.userAgentData.platform when UA is
  spoofed by Chrome's "Desktop site" mode, so the <input> overlay
  (useTextInput) is used instead of <textarea>.
- Handle Ctrl+letter and Alt+letter combos in the hidden input
  overlay's keydown handler, so terminal control sequences (^C, ^D,
  ^S, M-x, etc.) work through the touch input path.
- Skip global keyboard shortcuts (sidebar toggle, etc.) when focus
  is inside the terminal viewport, preventing shortcut steal.

* fix: preserve terminal keyboard shortcuts

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
Junlang Huang
2026-05-07 23:27:37 +03:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent d5b3590901
commit b65edc3436
2 changed files with 27 additions and 2 deletions
@@ -128,7 +128,10 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
inputHandlerRef.current = onInput;
resizeHandlerRef.current = onResize;
const isAndroid = typeof navigator !== 'undefined' && /Android/i.test(navigator.userAgent);
const isAndroid = typeof navigator !== 'undefined' && (
/Android/i.test(navigator.userAgent) ||
(navigator as { userAgentData?: { platform: string } }).userAgentData?.platform === 'Android'
);
// Touch devices need a dedicated editable surface so special keys like
// Backspace and arrows are captured reliably without relying on Ghostty's
// internal mobile text handling.
@@ -1409,6 +1412,25 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
return;
}
if (event.ctrlKey && !event.metaKey && !event.altKey && event.key.length === 1) {
const upper = event.key.toUpperCase();
if (upper >= 'A' && upper <= 'Z') {
event.preventDefault();
(event.nativeEvent as KeyboardEvent | undefined)?.stopImmediatePropagation();
inputHandlerRef.current(String.fromCharCode(upper.charCodeAt(0) - 64));
clearEditableValue(event.currentTarget as HTMLElement);
return;
}
}
if (event.altKey && !event.ctrlKey && !event.metaKey && event.key.length === 1) {
event.preventDefault();
(event.nativeEvent as KeyboardEvent | undefined)?.stopImmediatePropagation();
inputHandlerRef.current('\x1b' + event.key);
clearEditableValue(event.currentTarget as HTMLElement);
return;
}
const specialKeySequences: Record<string, string> = {
ArrowUp: '\u001b[A',
ArrowDown: '\u001b[B',
@@ -64,7 +64,6 @@ export const useKeyboardShortcuts = () => {
target.getAttribute('data-terminal-hidden-input') === 'true'
);
};
const handleTerminalShortcutCapture = (e: KeyboardEvent) => {
if (!isTerminalEventTarget(e.target)) {
return;
@@ -94,6 +93,10 @@ export const useKeyboardShortcuts = () => {
};
const handleKeyDown = (e: KeyboardEvent) => {
if (isTerminalEventTarget(e.target)) {
return;
}
if (eventMatchesShortcut(e, combo('open_command_palette'))) {
e.preventDefault();
toggleCommandPalette();