Add Alt/Fn modifiers to mobile keybar
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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");
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user