Skip to content

Commit 34fb5e4

Browse files
authored
Merge pull request #65 from aakhter/fix/android-shift-double-char
fix: prevent double character input on Android Shift+key
2 parents 002cf81 + 29b2653 commit 34fb5e4

1 file changed

Lines changed: 25 additions & 1 deletion

File tree

src/web/public/terminal-ui.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,21 +122,45 @@ Object.assign(CodemanApp.prototype, {
122122
// On Android Chrome, typing symbols (e.g., "/" from Gboard's symbol keyboard)
123123
// sends keyCode 229 + input event WITHOUT compositionstart/end wrapping.
124124
// The custom key handler above returns false for keyCode 229, telling xterm
125-
// to ignore the keydown. This listener catches those orphaned input events.
125+
// to ignore the keydown. xterm.js expects the character to arrive via
126+
// composition events, but since there's no composition, the character is lost.
127+
// This listener catches those orphaned input events and forwards them to onData.
126128
{
127129
const xtermTextarea = container.querySelector('.xterm-helper-textarea');
128130
if (xtermTextarea && MobileDetection.isTouchDevice()) {
129131
let composing = false;
132+
let lastKeydownHandled = 0;
130133
xtermTextarea.addEventListener('compositionstart', () => { composing = true; });
131134
xtermTextarea.addEventListener('compositionend', () => { composing = false; });
135+
// Track when xterm handles a keydown normally (non-229 keyCode).
136+
// If xterm processed the keydown, it will emit onData itself --
137+
// the input event handler below must NOT re-send the character.
138+
xtermTextarea.addEventListener('keydown', (e) => {
139+
if (!e.isComposing && e.keyCode !== 229) {
140+
lastKeydownHandled = Date.now();
141+
}
142+
});
132143
xtermTextarea.addEventListener('input', (e) => {
144+
// Only handle insertText events outside of composition -- these are
145+
// the ones xterm.js misses on Android virtual keyboards.
133146
if (composing || e.isComposing) return;
134147
if (e.inputType !== 'insertText' || !e.data) return;
148+
// If xterm just handled a keydown (within 50ms), it already sent the
149+
// char via onData. Skip to avoid double-send (e.g., Shift+A => AA).
150+
if (Date.now() - lastKeydownHandled < 50) return;
151+
// xterm.js may have already processed this via its own input handler.
152+
// Check if the textarea was cleared by xterm (value is empty or just
153+
// whitespace) -- if so, xterm handled it and we should not double-send.
154+
// Use a microtask to check after xterm's own handlers have run.
135155
const data = e.data;
136156
Promise.resolve().then(() => {
157+
// If xterm cleared the textarea, it processed the input -- skip.
137158
const val = xtermTextarea.value;
138159
if (!val || val.trim() === '') return;
160+
// xterm didn't process it -- forward to terminal as if typed.
161+
// Emit via onData path by writing to terminal's input handler.
139162
this.terminal._core.coreService.triggerDataEvent(data, true);
163+
// Clear the textarea to prevent xterm from processing it later.
140164
xtermTextarea.value = '';
141165
});
142166
});

0 commit comments

Comments
 (0)