/** * public/keybar.ts — Mobile touch key bar, tuned for Claude Code. * * Exports pure key-to-byte mappings and a mount function that binds DOM buttons * to onSend on touchstart (bypassing xterm so the soft keyboard doesn't pop up). * * Order/selection follows Claude Code mobile habits — the keys a phone soft * keyboard can't easily produce, most-used first (leftmost = no scroll needed): * Esc \x1b interrupt Claude / dismiss (primary — most used) * Shift+Tab \x1b[Z cycle plan / auto-accept mode * ↑ ↓ \x1b[A/B select menu option / history * Enter \r confirm * Ctrl+C \x03 cancel / quit * Tab \t complete / toggle * ← → \x1b[D/C cursor * / / slash-command launcher */ import type { MountKeybar } from '../src/types.js' /** Key name → byte string mapping (pure data, for testing). */ export const KEY_MAP = { esc: '\x1b', shiftTab: '\x1b[Z', arrowUp: '\x1b[A', arrowDown: '\x1b[B', arrowLeft: '\x1b[D', arrowRight: '\x1b[C', enter: '\r', ctrlC: '\x03', tab: '\t', slash: '/', } as const export type KeyName = keyof typeof KEY_MAP /** Button layout: most-used Claude Code keys first. `primary` styles a key as prominent. */ export const KEYBAR_BUTTONS: Array<{ label: string; keyName: KeyName; primary?: boolean }> = [ { label: 'Esc', keyName: 'esc', primary: true }, { label: '⇧Tab', keyName: 'shiftTab' }, { label: '↑', keyName: 'arrowUp' }, { label: '↓', keyName: 'arrowDown' }, { label: '⏎', keyName: 'enter' }, { label: '^C', keyName: 'ctrlC' }, { label: 'Tab', keyName: 'tab' }, { label: '←', keyName: 'arrowLeft' }, { label: '→', keyName: 'arrowRight' }, { label: '/', keyName: 'slash' }, ] /** Mount the key bar: create buttons in #keybar, bind touchstart → onSend(bytes) + preventDefault. */ export const mountKeybar: MountKeybar = (onSend) => { const keybarEl = document.getElementById('keybar') if (!keybarEl) return for (const { label, keyName, primary } of KEYBAR_BUTTONS) { const btn = document.createElement('button') btn.textContent = label btn.classList.add('keybar-btn') if (primary) btn.classList.add('keybar-btn-primary') btn.dataset.key = keyName btn.setAttribute('aria-label', label) const bytes = KEY_MAP[keyName] // touchstart: send immediately, prevent default so the soft keyboard stays hidden. btn.addEventListener('touchstart', (e) => { e.preventDefault() onSend(bytes) }) // Desktop fallback: click also sends. btn.addEventListener('click', (e) => { e.preventDefault() onSend(bytes) }) keybarEl.appendChild(btn) } }