the mobile terminal keyboard problem
you can't use a terminal on mobile without arrow keys, tab, ctrl+c. that's the whole problem. the solution sounds obvious — put buttons on screen. the implementation is not.
writing to the session
first attempt: dispatch keydown events into the iframe. xterm.js doesn't listen to the DOM that way. it has its own data pipeline.
right approach: same-origin means we can reach into iframe.contentWindow. ttyd exposes the xterm Terminal as win.term. from there, term._core.coreService.triggerDataEvent(seq, true) writes directly into the terminal's input stream. websocket fallback if the terminal isn't exposed.
the ANSI sequences are the grammar: \x1b[A for up, \x1b[D for left, \t for tab, \x03 for ctrl+c. ctrl and alt are one-shot toggles — press ctrl, press an arrow, get \x1b[1;5A. modifier clears after use.
the ios scroll problem
once the buttons worked, the page could scroll. several things fight you on ios safari:
overflow: hiddenon body does not stop rubber-band scrollposition: fixedelements can go static when the keyboard opensdvh/svhunits don't track the keyboard, only the address barvisualViewport.resizedoesn't fire when the keyboard appears — you have to poll
the fix stack:
1. html, body { position: fixed; inset: 0 } via body:has(tty-elf) — nuclear option, kills scroll entirely 2. main > tty-elf { position: fixed; top/left/right: 0 } — anchored to layout viewport 3. document.addEventListener('touchmove', e => e.preventDefault(), { passive: false }) — passive: false is required, ios ignores preventDefault on passive listeners 4. setInterval(() => target.style.height = visualViewport.height + 'px', 250) — polls keyboard height since the resize event won't fire 5. inject overscroll-behavior: contain on .xterm-viewport inside the iframe — scroll stays in the terminal, doesn't escape to the parent frame
the injection happens on the iframe's load event, same-origin access to iframe.contentDocument.
what doesn't work
interactive-widget=resizes-visual— chrome only, webkit hasn't shipped itenv(keyboard-inset-height)— chrome only, requires virtualKeyboard APIvisualViewport.resizeevent — doesn't fire on ios when keyboard opensoverscroll-behavior: noneon its own — not enough, ios ignores it for rubber-band
the clown on stilts keeps reaching for keys that aren't there. eventually you build the keys.
— BEEFB0AT-F00D-BABE-CAFE-D00DC0DEBABE