feat: add Ctrl+letter handling and dynamic theme switching

- Handle physical keyboard Ctrl+letter combinations (Ctrl+A, Ctrl+C, etc.)
- Unify modifier detection for physical keyboard and mobile keybar
- Add setTheme() method for dynamic theme switching
- Add static getTheme() helper to retrieve built-in themes
This commit is contained in:
GitHub Copilot
2026-01-28 08:46:43 +00:00
parent 714a5c705c
commit d92759b9de
2 changed files with 37 additions and 9 deletions
File diff suppressed because one or more lines are too long
+32 -4
View File
@@ -516,7 +516,21 @@ class WebTerminal {
}); });
// Handle special navigation keys via keydown (not covered by beforeinput) // Handle special navigation keys via keydown (not covered by beforeinput)
// Check both physical keyboard modifiers (e.ctrlKey/e.shiftKey) and mobile keybar flags
textarea.addEventListener("keydown", (e) => { textarea.addEventListener("keydown", (e) => {
const isCtrl = e.ctrlKey || this.ctrlActive;
const isShift = e.shiftKey || this.shiftActive;
// Handle Ctrl+letter combinations (these don't fire input events)
if (e.ctrlKey && e.key.length === 1 && !e.altKey && !e.metaKey) {
const code = e.key.toUpperCase().charCodeAt(0);
if (code >= 65 && code <= 90) {
e.preventDefault();
this.send(["stdin", String.fromCharCode(code - 64)]); // Ctrl+A=0x01, Ctrl+C=0x03, etc.
return;
}
}
let seq: string | null = null; let seq: string | null = null;
let deactivate = false; let deactivate = false;
switch (e.key) { switch (e.key) {
@@ -529,11 +543,11 @@ class WebTerminal {
case "ArrowRight": case "ArrowRight":
case "ArrowLeft": { case "ArrowLeft": {
const dir = e.key === "ArrowUp" ? "A" : e.key === "ArrowDown" ? "B" : e.key === "ArrowRight" ? "C" : "D"; const dir = e.key === "ArrowUp" ? "A" : e.key === "ArrowDown" ? "B" : e.key === "ArrowRight" ? "C" : "D";
if (this.ctrlActive && this.shiftActive) { if (isCtrl && isShift) {
seq = `\x1b[1;6${dir}`; seq = `\x1b[1;6${dir}`;
} else if (this.ctrlActive) { } else if (isCtrl) {
seq = `\x1b[1;5${dir}`; seq = `\x1b[1;5${dir}`;
} else if (this.shiftActive) { } else if (isShift) {
seq = `\x1b[1;2${dir}`; seq = `\x1b[1;2${dir}`;
} else { } else {
seq = `\x1b[${dir}`; seq = `\x1b[${dir}`;
@@ -542,7 +556,7 @@ class WebTerminal {
break; break;
} }
case "Tab": case "Tab":
if (this.shiftActive) { if (isShift) {
seq = "\x1b[Z"; // Back-tab seq = "\x1b[Z"; // Back-tab
} else { } else {
seq = "\t"; seq = "\t";
@@ -1017,6 +1031,20 @@ class WebTerminal {
this.fitAddon.dispose(); this.fitAddon.dispose();
this.terminal.dispose(); this.terminal.dispose();
} }
/** Set terminal theme dynamically (accesses private renderer) */
setTheme(theme: ITheme): void {
// ghostty-web Terminal doesn't expose setTheme, but the internal renderer has it
const renderer = (this.terminal as unknown as { renderer?: { setTheme: (t: ITheme) => void } }).renderer;
if (renderer && typeof renderer.setTheme === "function") {
renderer.setTheme(theme);
}
}
/** Get a named theme from the built-in themes */
static getTheme(name: string): ITheme | undefined {
return THEMES[name.toLowerCase()];
}
} }
// Store instances for potential external access // Store instances for potential external access