Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ no SSH client and no external terminal needed.
- **Shell picker** — run zsh, bash, fish, PowerShell… whatever the machine actually has
- **Mobile key bar** — Esc, Tab, Ctrl, Alt, `^C`/`^D`/`^Z`, arrows, Home/End/PgUp/PgDn and the
punctuation soft keyboards hide. The Ctrl and Alt keys apply to the next character you type,
so `Ctrl+<anything>` works from a phone.
so `Ctrl+<anything>` works from a phone; double-tap one to lock it for repeated chords (such as
a double `Ctrl+C`) until it is tapped again.
- **Undo close** — closing a tab is recoverable for 8 seconds before the shell is killed
- **Offline-ready** — xterm.js is bundled into the plugin, not fetched from a CDN at runtime
- **Accessible** — keyboard-operable tabs and settings, ARIA labels, optional screen-reader mode,
Expand Down
12 changes: 7 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ const MOBILE_KEYS: KeyDef[] = [
{ label: IC.paste, svg: true, action: 'paste', title: 'Paste' },
{ label: 'ESC', seq: '\x1b' },
{ label: 'TAB', seq: '\t' },
{ label: 'CTRL', action: 'ctrl', title: 'Ctrl — applies to the next key you type' },
{ label: 'ALT', action: 'alt', title: 'Alt — applies to the next key you type' },
{ label: 'CTRL', action: 'ctrl', title: 'Ctrl — applies to the next key you type; double-tap to lock' },
{ label: 'ALT', action: 'alt', title: 'Alt — applies to the next key you type; double-tap to lock' },
// Ctrl+C used to be impossible from a phone: the bar had a CTRL toggle but
// no letters for it to combine with.
{ label: '^C', seq: '\x03', title: 'Ctrl+C (interrupt)' },
Expand Down Expand Up @@ -627,7 +627,9 @@ export async function mount(container: HTMLElement, api: PluginAPI): Promise<voi
const session = activeSession();
for (const { action, button } of modifierButtons) {
const armed = action === 'ctrl' ? session?.pendingCtrl : session?.pendingAlt;
const locked = action === 'ctrl' ? session?.ctrlLocked : session?.altLocked;
button.classList.toggle('wt-active', !!armed);
button.classList.toggle('wt-locked', !!locked);
button.setAttribute('aria-pressed', String(!!armed));
}
}
Expand All @@ -648,13 +650,13 @@ export async function mount(container: HTMLElement, api: PluginAPI): Promise<voi
}

if (key.action === 'ctrl' || key.action === 'alt') {
modifierButtons.push({ action: key.action, button });
const modifier = key.action;
modifierButtons.push({ action: modifier, button });
button.setAttribute('aria-pressed', 'false');
button.addEventListener('click', () => {
const session = activeSession();
if (!session) return;
if (key.action === 'ctrl') session.pendingCtrl = !session.pendingCtrl;
else session.pendingAlt = !session.pendingAlt;
session.tapModifier(modifier);
syncModifierButtons();
session.focus();
});
Expand Down
28 changes: 26 additions & 2 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export type SessionStatus = 'connecting' | 'connected' | 'reconnecting' | 'disco
const CONNECT_TIMEOUT_MS = 12000;
const MAX_RECONNECT_DELAY_MS = 15000;
const PING_INTERVAL_MS = 25000;
/** Two taps on a key bar modifier within this window lock it. */
const DOUBLE_TAP_MS = 500;

const encoder = new TextEncoder();

Expand Down Expand Up @@ -78,6 +80,9 @@ export class TerminalSession {
/** Set while a modifier key from the mobile bar is armed for the next press. */
pendingCtrl = false;
pendingAlt = false;
/** A locked modifier stays armed after each key until it is tapped again. */
ctrlLocked = false;
altLocked = false;
onModifiersChange: (() => void) | null = null;

private readonly overlayEl: HTMLElement;
Expand Down Expand Up @@ -106,6 +111,7 @@ export class TerminalSession {
private pendingResume: string | null;
/** Set while restart() is in flight, so the fresh shell is not reported as an expiry. */
private restarting = false;
private modifierTapAt = { ctrl: 0, alt: 0 };

private readonly onOnline = (): void => { if (this.status === 'reconnecting') this.connect(true); };
private readonly onVisible = (): void => {
Expand Down Expand Up @@ -369,6 +375,24 @@ export class TerminalSession {
}
}

/**
* A tap on CTRL or ALT in the key bar. A single tap arms the modifier for
* the next key, as before; a second tap right after locks it, so a chord
* that repeats — Claude Code's double Ctrl+C to exit — can be typed at all.
* Any tap on a locked modifier releases it.
*/
tapModifier(which: 'ctrl' | 'alt'): void {
const now = performance.now();
const armed = which === 'ctrl' ? this.pendingCtrl : this.pendingAlt;
const locked = which === 'ctrl' ? this.ctrlLocked : this.altLocked;
const doubleTap = armed && !locked && now - this.modifierTapAt[which] < DOUBLE_TAP_MS;
const nextLocked = doubleTap;
const nextArmed = doubleTap || (!armed && !locked);
this.modifierTapAt[which] = nextArmed && !nextLocked ? now : 0;
if (which === 'ctrl') { this.pendingCtrl = nextArmed; this.ctrlLocked = nextLocked; }
else { this.pendingAlt = nextArmed; this.altLocked = nextLocked; }
}

private handleInput(data: string): void {
// A modifier armed from the on-screen key bar applies to the next
// character the soft keyboard produces, which is the only way to type
Expand All @@ -381,8 +405,8 @@ export class TerminalSession {
else if (code >= 64 && code <= 95) out = String.fromCharCode(code - 64);
}
if (this.pendingAlt) out = `\x1b${out}`;
this.pendingCtrl = false;
this.pendingAlt = false;
if (!this.ctrlLocked) this.pendingCtrl = false;
if (!this.altLocked) this.pendingAlt = false;
this.onModifiersChange?.();
this.sendBytes(encoder.encode(out));
return;
Expand Down
1 change: 1 addition & 0 deletions src/ui/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ const PLUGIN_CSS = `
user-select:none; -webkit-tap-highlight-color:transparent;
}
.wt-key:active, .wt-key.wt-active { background:var(--accent); color:#fff; border-color:var(--accent); }
.wt-key.wt-locked { box-shadow:inset 0 0 0 2px rgba(255,255,255,.85); }
.wt-key, .wt-btn, .wt-tab, .wt-tab-close, .wt-new-tab { touch-action:manipulation; }
.wt-key svg { width:16px; height:16px; }

Expand Down