Add Alt/Fn modifiers to mobile keybar

This commit is contained in:
GitHub Copilot
2026-02-03 19:37:29 +00:00
parent 51984fd5d1
commit 027c8931ed
2 changed files with 140 additions and 35 deletions
File diff suppressed because one or more lines are too long
+127 -24
View File
@@ -420,9 +420,13 @@ class WebTerminal {
private mobileInput: HTMLTextAreaElement | null = null; private mobileInput: HTMLTextAreaElement | null = null;
private mobileKeybar: HTMLElement | null = null; private mobileKeybar: HTMLElement | null = null;
private ctrlActive = false; private ctrlActive = false;
private altActive = false;
private shiftActive = false; private shiftActive = false;
private fnActive = false;
private pendingCtrl = false; private pendingCtrl = false;
private pendingAlt = false;
private pendingShift = false; private pendingShift = false;
private pendingFn = false;
private fontFamily: string; private fontFamily: string;
private fontSize: number; private fontSize: number;
@@ -733,23 +737,82 @@ class WebTerminal {
} }
return key; return key;
}; };
const applyModifiers = (text: string, useShift: boolean, useCtrl: boolean): string => { const applyFnModifier = (key: string, useShift: boolean): string | null => {
if (key.length !== 1) {
return null;
}
const index = "1234567890".indexOf(key);
if (index < 0) {
return null;
}
const fnNormal = [
"\x1bOP",
"\x1bOQ",
"\x1bOR",
"\x1bOS",
"\x1b[15~",
"\x1b[17~",
"\x1b[18~",
"\x1b[19~",
"\x1b[20~",
"\x1b[21~",
];
const fnShift = [
"\x1b[23~",
"\x1b[24~",
"\x1b[25~",
"\x1b[26~",
"\x1b[28~",
"\x1b[29~",
"\x1b[31~",
"\x1b[32~",
"\x1b[33~",
"\x1b[34~",
];
return useShift ? fnShift[index] : fnNormal[index];
};
const applyAltModifier = (text: string): string => {
if (!text || text.startsWith("\x1b")) {
return text;
}
return `\x1b${text}`;
};
const applyModifiers = (
text: string,
useShift: boolean,
useCtrl: boolean,
useAlt: boolean,
useFn: boolean
): string => {
if (text.length !== 1) { if (text.length !== 1) {
return text; return text;
} }
if (useFn) {
const fnApplied = applyFnModifier(text, useShift);
if (fnApplied) {
return useAlt ? applyAltModifier(fnApplied) : fnApplied;
}
}
if (useCtrl) { if (useCtrl) {
const ctrlApplied = applyCtrlModifier(text); const ctrlApplied = applyCtrlModifier(text);
if (ctrlApplied !== text) { if (ctrlApplied !== text) {
return ctrlApplied; return useAlt ? applyAltModifier(ctrlApplied) : ctrlApplied;
} }
} }
if (useShift) { if (useShift) {
return applyShiftModifier(text); const shifted = applyShiftModifier(text);
return useAlt ? applyAltModifier(shifted) : shifted;
} }
return text; return useAlt ? applyAltModifier(text) : text;
}; };
const applyMobileModifiers = (text: string): string => const applyMobileModifiers = (text: string): string =>
applyModifiers(text, this.shiftActive || this.pendingShift, this.ctrlActive || this.pendingCtrl); applyModifiers(
text,
this.shiftActive || this.pendingShift,
this.ctrlActive || this.pendingCtrl,
this.altActive || this.pendingAlt,
this.fnActive || this.pendingFn
);
const handleMobileInput = (text: string, e?: Event) => { const handleMobileInput = (text: string, e?: Event) => {
if (e) { if (e) {
@@ -763,7 +826,9 @@ class WebTerminal {
textarea.value = ""; textarea.value = "";
this.deactivateModifiers(); this.deactivateModifiers();
this.pendingCtrl = false; this.pendingCtrl = false;
this.pendingAlt = false;
this.pendingShift = false; this.pendingShift = false;
this.pendingFn = false;
}; };
// Handle special keys via beforeinput to intercept before browser modifies textarea // Handle special keys via beforeinput to intercept before browser modifies textarea
@@ -800,6 +865,8 @@ class WebTerminal {
textarea.addEventListener("keydown", (e) => { textarea.addEventListener("keydown", (e) => {
const isCtrl = e.ctrlKey || this.ctrlActive; const isCtrl = e.ctrlKey || this.ctrlActive;
const isShift = e.shiftKey || this.shiftActive; const isShift = e.shiftKey || this.shiftActive;
const isAlt = e.altKey || this.altActive;
const isFn = this.fnActive;
// Handle Ctrl+key combinations (these don't fire input events) // Handle Ctrl+key combinations (these don't fire input events)
if (isCtrl && e.key.length === 1 && !e.altKey && !e.metaKey) { if (isCtrl && e.key.length === 1 && !e.altKey && !e.metaKey) {
@@ -807,11 +874,22 @@ class WebTerminal {
if (ctrlApplied !== e.key) { if (ctrlApplied !== e.key) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.send(["stdin", ctrlApplied]); // Ctrl+A=0x01, Ctrl+C=0x03, etc. const toSend = isAlt ? applyAltModifier(ctrlApplied) : ctrlApplied;
this.send(["stdin", toSend]); // Ctrl+A=0x01, Ctrl+C=0x03, etc.
this.deactivateModifiers(); // Clear modifiers after physical Ctrl+key this.deactivateModifiers(); // Clear modifiers after physical Ctrl+key
return; return;
} }
} }
if (isFn && e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
const fnApplied = applyFnModifier(e.key, isShift);
if (fnApplied) {
e.preventDefault();
e.stopPropagation();
this.send(["stdin", fnApplied]);
this.deactivateModifiers();
return;
}
}
let seq: string | null = null; let seq: string | null = null;
switch (e.key) { switch (e.key) {
@@ -846,7 +924,7 @@ class WebTerminal {
if (seq) { if (seq) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.send(["stdin", seq]); this.send(["stdin", isAlt ? applyAltModifier(seq) : seq]);
// Always clear modifiers after any key // Always clear modifiers after any key
this.deactivateModifiers(); this.deactivateModifiers();
} }
@@ -856,7 +934,7 @@ class WebTerminal {
document.addEventListener( document.addEventListener(
"keydown", "keydown",
(event) => { (event) => {
if (!this.ctrlActive && !this.shiftActive) { if (!this.ctrlActive && !this.shiftActive && !this.altActive && !this.fnActive) {
return; return;
} }
if (event.target === this.mobileInput) { if (event.target === this.mobileInput) {
@@ -865,10 +943,12 @@ class WebTerminal {
const useCtrl = this.ctrlActive; const useCtrl = this.ctrlActive;
const useShift = this.shiftActive; const useShift = this.shiftActive;
const useAlt = this.altActive;
const useFn = this.fnActive;
let handled = false; let handled = false;
if (event.key.length === 1 && !event.altKey && !event.metaKey) { if (event.key.length === 1 && !event.altKey && !event.metaKey) {
const toSend = applyModifiers(event.key, useShift, useCtrl); const toSend = applyModifiers(event.key, useShift, useCtrl, useAlt, useFn);
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
this.send(["stdin", toSend]); this.send(["stdin", toSend]);
@@ -914,7 +994,7 @@ class WebTerminal {
if (seq) { if (seq) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
this.send(["stdin", seq]); this.send(["stdin", useAlt ? applyAltModifier(seq) : seq]);
handled = true; handled = true;
} }
} }
@@ -994,11 +1074,13 @@ class WebTerminal {
<button class="keybar-drag" title="Drag to move">⋮⋮</button> <button class="keybar-drag" title="Drag to move">⋮⋮</button>
<button data-key="\\x1b" title="Escape">Esc</button> <button data-key="\\x1b" title="Escape">Esc</button>
<button data-modifier="ctrl" title="Ctrl modifier">Ctrl</button> <button data-modifier="ctrl" title="Ctrl modifier">Ctrl</button>
<button data-modifier="shift" title="Shift modifier"></button> <button data-modifier="alt" title="Alt modifier">Alt</button>
<button data-modifier="fn" title="Fn modifier">Fn</button>
<button data-key="\\x09" title="Tab">Tab</button> <button data-key="\\x09" title="Tab">Tab</button>
<button data-key="\\x1b[A" title="Up"></button> <button data-modifier="shift" title="Shift modifier"></button>
<button data-key="\\x1b[B" title="Down">↓</button>
<button data-key="\\x1b[D" title="Left">←</button> <button data-key="\\x1b[D" title="Left">←</button>
<button data-key="\\x1b[B" title="Down">↓</button>
<button data-key="\\x1b[A" title="Up">↑</button>
<button data-key="\\x1b[C" title="Right">→</button> <button data-key="\\x1b[C" title="Right">→</button>
<button data-key="\\x0d" title="Return" class="keybar-return">⏎</button> <button data-key="\\x0d" title="Return" class="keybar-return">⏎</button>
`; `;
@@ -1011,7 +1093,7 @@ class WebTerminal {
bottom: 80px; bottom: 80px;
right: 0; right: 0;
display: grid; display: grid;
grid-template-columns: repeat(5, auto); grid-template-columns: repeat(6, auto);
gap: 4px; gap: 4px;
padding: 6px; padding: 6px;
background: rgba(40, 40, 40, 0.95); background: rgba(40, 40, 40, 0.95);
@@ -1052,7 +1134,7 @@ class WebTerminal {
cursor: grabbing; cursor: grabbing;
} }
.mobile-keybar .keybar-return { .mobile-keybar .keybar-return {
grid-column: 5; grid-column: 6;
grid-row: 2; grid-row: 2;
} }
`; `;
@@ -1071,32 +1153,41 @@ class WebTerminal {
); );
key = key.replace(/\\x1b/g, "\x1b"); key = key.replace(/\\x1b/g, "\x1b");
const useShift = this.shiftActive;
const useCtrl = this.ctrlActive;
const useAlt = this.altActive;
const useFn = this.fnActive;
const useShiftForFn = useShift || this.pendingShift;
// Handle Shift+Tab -> Back-Tab (CSI Z) // Handle Shift+Tab -> Back-Tab (CSI Z)
if (this.shiftActive && key === "\x09") { if (useShift && key === "\x09") {
key = "\x1b[Z"; key = "\x1b[Z";
} }
// Handle Ctrl+Shift+Arrow keys (CSI 1;6 X) // Handle Ctrl+Shift+Arrow keys (CSI 1;6 X)
else if (this.ctrlActive && this.shiftActive && key.startsWith("\x1b[") && key.length === 3) { else if (useCtrl && useShift && key.startsWith("\x1b[") && key.length === 3) {
const dir = key[2]; const dir = key[2];
key = `\x1b[1;6${dir}`; key = `\x1b[1;6${dir}`;
} }
// Handle Shift+Arrow keys (CSI 1;2 X) // Handle Shift+Arrow keys (CSI 1;2 X)
else if (this.shiftActive && key.startsWith("\x1b[") && key.length === 3) { else if (useShift && key.startsWith("\x1b[") && key.length === 3) {
const dir = key[2]; // A, B, C, or D const dir = key[2]; // A, B, C, or D
key = `\x1b[1;2${dir}`; key = `\x1b[1;2${dir}`;
} }
// Handle Ctrl+Arrow keys (CSI 1;5 X) // Handle Ctrl+Arrow keys (CSI 1;5 X)
else if (this.ctrlActive && key.startsWith("\x1b[") && key.length === 3) { else if (useCtrl && key.startsWith("\x1b[") && key.length === 3) {
const dir = key[2]; const dir = key[2];
key = `\x1b[1;5${dir}`; key = `\x1b[1;5${dir}`;
} }
// Apply Ctrl modifier to letters if (useFn && key.length === 1) {
else if (this.ctrlActive && key.length === 1) { const fnApplied = applyFnModifier(key, useShiftForFn);
const code = key.toUpperCase().charCodeAt(0); if (fnApplied) {
if (code >= 65 && code <= 90) { key = fnApplied;
key = String.fromCharCode(code - 64); // Ctrl+A = 0x01, etc.
} }
} }
if (key.length === 1) {
key = applyModifiers(key, useShift, useCtrl, useAlt, useFn);
} else if (useAlt) {
key = applyAltModifier(key);
}
this.send(["stdin", key]); this.send(["stdin", key]);
this.deactivateModifiers(); this.deactivateModifiers();
@@ -1112,10 +1203,18 @@ class WebTerminal {
this.ctrlActive = !this.ctrlActive; this.ctrlActive = !this.ctrlActive;
this.pendingCtrl = this.ctrlActive; this.pendingCtrl = this.ctrlActive;
btn.classList.toggle("active", this.ctrlActive); btn.classList.toggle("active", this.ctrlActive);
} else if (modifier === "alt") {
this.altActive = !this.altActive;
this.pendingAlt = this.altActive;
btn.classList.toggle("active", this.altActive);
} else if (modifier === "shift") { } else if (modifier === "shift") {
this.shiftActive = !this.shiftActive; this.shiftActive = !this.shiftActive;
this.pendingShift = this.shiftActive; this.pendingShift = this.shiftActive;
btn.classList.toggle("active", this.shiftActive); btn.classList.toggle("active", this.shiftActive);
} else if (modifier === "fn") {
this.fnActive = !this.fnActive;
this.pendingFn = this.fnActive;
btn.classList.toggle("active", this.fnActive);
} }
this.focusMobileInput(); this.focusMobileInput();
}); });
@@ -1177,9 +1276,13 @@ class WebTerminal {
/** Deactivate all modifiers */ /** Deactivate all modifiers */
private deactivateModifiers(): void { private deactivateModifiers(): void {
this.ctrlActive = false; this.ctrlActive = false;
this.altActive = false;
this.shiftActive = false; this.shiftActive = false;
this.fnActive = false;
this.pendingCtrl = false; this.pendingCtrl = false;
this.pendingAlt = false;
this.pendingShift = false; this.pendingShift = false;
this.pendingFn = false;
this.mobileKeybar?.querySelectorAll("button[data-modifier]").forEach((btn) => { this.mobileKeybar?.querySelectorAll("button[data-modifier]").forEach((btn) => {
btn.classList.remove("active"); btn.classList.remove("active");
}); });