fix: restore mobile virtual keyboard flow
This commit is contained in:
@@ -24,6 +24,10 @@ Use `gofmt` for Go formatting; run `make format` before submitting Go-heavy chan
|
|||||||
|
|
||||||
Go tests use the standard `testing` package. Name tests `TestXxx` and fuzz tests `FuzzXxx`; keep them next to the code they validate. Prefer focused unit tests for `webterm/` and `internal/` changes. Run `make test` for normal work and `make check` before opening a PR. Frontend changes should at minimum pass `bun run typecheck` via `make build`.
|
Go tests use the standard `testing` package. Name tests `TestXxx` and fuzz tests `FuzzXxx`; keep them next to the code they validate. Prefer focused unit tests for `webterm/` and `internal/` changes. Run `make test` for normal work and `make check` before opening a PR. Frontend changes should at minimum pass `bun run typecheck` via `make build`.
|
||||||
|
|
||||||
|
## Debugging Rule
|
||||||
|
|
||||||
|
Before making a second patch for the same bug, identify the exact callsite that causes the side effect. Trace the real owner of the behavior end-to-end, including library or dependency code when needed, instead of only patching app-level symptoms.
|
||||||
|
|
||||||
## Commit & Pull Request Guidelines
|
## Commit & Pull Request Guidelines
|
||||||
|
|
||||||
Recent history uses short imperative subjects, sometimes with prefixes such as `feat:`, `fix:`, and `deps:`. Keep commits focused, e.g. `fix: restore websocket reconnect on hidden-tab resume`. PRs should explain user-visible behavior, note test coverage, and include screenshots or recordings for terminal/UI changes. Link related issues when applicable.
|
Recent history uses short imperative subjects, sometimes with prefixes such as `feat:`, `fix:`, and `deps:`. Keep commits focused, e.g. `fix: restore websocket reconnect on hidden-tab resume`. PRs should explain user-visible behavior, note test coverage, and include screenshots or recordings for terminal/UI changes. Link related issues when applicable.
|
||||||
|
|||||||
@@ -54,3 +54,7 @@ Run `go run ./cmd/webterm` to start the server on port 8080.
|
|||||||
- The `SessionConnector` interface decouples session I/O from the WebSocket layer
|
- The `SessionConnector` interface decouples session I/O from the WebSocket layer
|
||||||
- Docker integration uses raw HTTP against the Docker socket (no Docker SDK dependency)
|
- Docker integration uses raw HTTP against the Docker socket (no Docker SDK dependency)
|
||||||
- Version is read from the `VERSION` file and injected via `-ldflags` at build time
|
- Version is read from the `VERSION` file and injected via `-ldflags` at build time
|
||||||
|
|
||||||
|
## Debugging Rule
|
||||||
|
|
||||||
|
Before making a second patch for the same bug, identify the exact callsite that causes the side effect. Trace the real owner of the behavior end-to-end, including dependency code such as `ghostty-web` when necessary, instead of stacking app-level symptom fixes.
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
# Deferred: Ghostty mobile long-press copy
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Mobile long-press copy for highlighted terminal text should be implemented in Ghostty ownership layer, not in `webterm/static/js/terminal.ts`.
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
- Selection, copy, context menu, and touch-selection semantics are owned by `ghostty-web`, mainly `lib/selection-manager.ts`.
|
||||||
|
- Wrapper-level gesture handling in `webterm/static/js/terminal.ts` is not correct long-term fix for “long press highlighted text to copy”.
|
||||||
|
- Future work should patch Ghostty selection manager directly so long-press copy uses Ghostty’s real selection state and copy path.
|
||||||
|
|
||||||
# Bug: Render loop dies silently on uncaught exception
|
# Bug: Render loop dies silently on uncaught exception
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+149
-221
@@ -791,10 +791,12 @@ const VIRTUAL_KEYBOARD_ALPHA_LAYOUT: VirtualKeyboardKey[][] = [
|
|||||||
{ kind: "action", label: "⌫", value: "Backspace", seq: "\x7f" },
|
{ kind: "action", label: "⌫", value: "Backspace", seq: "\x7f" },
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ kind: "action", label: "123", actionId: "mode-symbol" },
|
{ kind: "action", label: "123", actionId: "mode-symbol", width: 1.05 },
|
||||||
{ kind: "space", label: "␣", value: " ", width: 4 },
|
{ kind: "modifier", label: "Ctrl", modifier: "ctrl", width: 1.05 },
|
||||||
{ kind: "action", label: "⏎", seq: "\r" },
|
{ kind: "modifier", label: "Alt", modifier: "alt", width: 1.05 },
|
||||||
{ kind: "action", label: "✂︎", actionId: "mode-selection" },
|
{ kind: "space", label: "␣", value: " ", width: 3.1 },
|
||||||
|
{ kind: "action", label: "Mic", actionId: "toggle-voice", width: 1.15 },
|
||||||
|
{ kind: "action", label: "⏎", seq: "\r", width: 1.15 },
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -843,33 +845,10 @@ const VIRTUAL_KEYBOARD_SYMBOL_LAYOUT: VirtualKeyboardKey[][] = [
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
const VIRTUAL_KEYBOARD_SELECTION_LAYOUT: VirtualKeyboardKey[][] = [
|
|
||||||
[
|
|
||||||
{ kind: "action", label: "Select", actionId: "toggle-selection-adjust", width: 1.4 },
|
|
||||||
{ kind: "action", label: "←", value: "ArrowLeft", seq: "\x1b[D" },
|
|
||||||
{ kind: "action", label: "→", value: "ArrowRight", seq: "\x1b[C" },
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{ kind: "action", label: "Copy", actionId: "copy-selection", width: 1.4 },
|
|
||||||
{ kind: "action", label: "Paste", actionId: "paste-selection", width: 1.4 },
|
|
||||||
{ kind: "action", label: "Cut", actionId: "cut-selection", width: 1.2 },
|
|
||||||
{ kind: "action", label: "⌫", value: "Backspace", seq: "\x7f" },
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{ kind: "action", label: "✂︎", actionId: "mode-alpha" },
|
|
||||||
{ kind: "space", label: "␣", value: " ", width: 2.8 },
|
|
||||||
{ kind: "action", label: "⏎", seq: "\r" },
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
type VirtualKeyboardActionId =
|
type VirtualKeyboardActionId =
|
||||||
| "mode-alpha"
|
| "mode-alpha"
|
||||||
| "mode-selection"
|
|
||||||
| "mode-symbol"
|
| "mode-symbol"
|
||||||
| "toggle-selection-adjust"
|
| "toggle-voice";
|
||||||
| "copy-selection"
|
|
||||||
| "paste-selection"
|
|
||||||
| "cut-selection";
|
|
||||||
|
|
||||||
type VirtualKeyboardKeyBounds = {
|
type VirtualKeyboardKeyBounds = {
|
||||||
x: number;
|
x: number;
|
||||||
@@ -1016,12 +995,9 @@ class WebTerminal {
|
|||||||
private mobileVirtualKeyboard: HTMLCanvasElement | null = null;
|
private mobileVirtualKeyboard: HTMLCanvasElement | null = null;
|
||||||
private mobileKeyboardVisible = false;
|
private mobileKeyboardVisible = false;
|
||||||
private mobileKeyboardMode: "alpha" | "symbol" = "alpha";
|
private mobileKeyboardMode: "alpha" | "symbol" = "alpha";
|
||||||
private mobileSelectionMode = false;
|
|
||||||
private mobileSelectionAdjusting = false;
|
|
||||||
private mobileVirtualKeyboardBounds: VirtualKeyboardKeyBounds[] = [];
|
private mobileVirtualKeyboardBounds: VirtualKeyboardKeyBounds[] = [];
|
||||||
private mobileVirtualKeyboardActivePresses = new Map<number, ActiveVirtualKeyboardPress>();
|
private mobileVirtualKeyboardActivePresses = new Map<number, ActiveVirtualKeyboardPress>();
|
||||||
private mobileVirtualKeyboardDrawFrame: number | null = null;
|
private mobileVirtualKeyboardDrawFrame: number | null = null;
|
||||||
private lastMobileKeyboardTouchAt = 0;
|
|
||||||
private ctrlActive = false;
|
private ctrlActive = false;
|
||||||
private altActive = false;
|
private altActive = false;
|
||||||
private shiftActive = false;
|
private shiftActive = false;
|
||||||
@@ -1030,6 +1006,7 @@ class WebTerminal {
|
|||||||
private pendingAlt = false;
|
private pendingAlt = false;
|
||||||
private pendingShift = false;
|
private pendingShift = false;
|
||||||
private pendingFn = false;
|
private pendingFn = false;
|
||||||
|
private allowMobileKeyboardOpen = false;
|
||||||
private fontFamily: string;
|
private fontFamily: string;
|
||||||
private fontSize: number;
|
private fontSize: number;
|
||||||
private cleanupTimer: number | undefined;
|
private cleanupTimer: number | undefined;
|
||||||
@@ -1044,7 +1021,6 @@ class WebTerminal {
|
|||||||
private bellActive = false;
|
private bellActive = false;
|
||||||
private routeKey: string;
|
private routeKey: string;
|
||||||
private voiceControls: HTMLElement | null = null;
|
private voiceControls: HTMLElement | null = null;
|
||||||
private voiceButton: HTMLButtonElement | null = null;
|
|
||||||
private voiceStatus: HTMLElement | null = null;
|
private voiceStatus: HTMLElement | null = null;
|
||||||
private voiceRecognizer: SherpaOfflineRecognizer | null = null;
|
private voiceRecognizer: SherpaOfflineRecognizer | null = null;
|
||||||
private voiceVad: SherpaVad | null = null;
|
private voiceVad: SherpaVad | null = null;
|
||||||
@@ -1058,6 +1034,7 @@ class WebTerminal {
|
|||||||
private voiceSpeechDetected = false;
|
private voiceSpeechDetected = false;
|
||||||
private voiceReceivedAudio = false;
|
private voiceReceivedAudio = false;
|
||||||
private isVoiceStarting = false;
|
private isVoiceStarting = false;
|
||||||
|
private voiceState: "idle" | "loading" | "listening" | "error" | "unsupported" = "idle";
|
||||||
private voiceStartupErrorCleanup: (() => void) | null = null;
|
private voiceStartupErrorCleanup: (() => void) | null = null;
|
||||||
private static sharedTextEncoder = new TextEncoder();
|
private static sharedTextEncoder = new TextEncoder();
|
||||||
|
|
||||||
@@ -1092,6 +1069,47 @@ class WebTerminal {
|
|||||||
this.boundHandlers.push({ target, type, handler, options });
|
this.boundHandlers.push({ target, type, handler, options });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private disableNativeMobileTerminalInput(): void {
|
||||||
|
if (!this.usesVirtualKeyboard()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ghosttyTerminal = this.terminal as unknown as {
|
||||||
|
textarea?: HTMLTextAreaElement;
|
||||||
|
};
|
||||||
|
const textarea = ghosttyTerminal.textarea;
|
||||||
|
if (!textarea) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.readOnly = true;
|
||||||
|
textarea.setAttribute("readonly", "true");
|
||||||
|
textarea.setAttribute("inputmode", "none");
|
||||||
|
textarea.setAttribute("tabindex", "-1");
|
||||||
|
textarea.setAttribute("aria-hidden", "true");
|
||||||
|
textarea.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async copyCurrentSelectionToClipboard(): Promise<boolean> {
|
||||||
|
const terminal = this.terminal as unknown as {
|
||||||
|
getSelection?: () => string;
|
||||||
|
hasSelection?: () => boolean;
|
||||||
|
};
|
||||||
|
const text =
|
||||||
|
(terminal.hasSelection?.() ? terminal.getSelection?.() : "") ||
|
||||||
|
document.getSelection()?.toString() ||
|
||||||
|
"";
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await navigator.clipboard?.writeText(text);
|
||||||
|
this.showUserError("Copied selection");
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.clearUserError();
|
||||||
|
}, 1200);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private bellStorageKey(): string | null {
|
private bellStorageKey(): string | null {
|
||||||
if (!this.routeKey) {
|
if (!this.routeKey) {
|
||||||
return null;
|
return null;
|
||||||
@@ -1226,6 +1244,7 @@ class WebTerminal {
|
|||||||
routeKey,
|
routeKey,
|
||||||
baseTitle
|
baseTitle
|
||||||
);
|
);
|
||||||
|
instance.disableNativeMobileTerminalInput();
|
||||||
instance.initialize();
|
instance.initialize();
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
@@ -1282,7 +1301,7 @@ class WebTerminal {
|
|||||||
this.setupMobileKeyboard();
|
this.setupMobileKeyboard();
|
||||||
}
|
}
|
||||||
this.addTrackedListener(
|
this.addTrackedListener(
|
||||||
this.element,
|
document,
|
||||||
"focusin",
|
"focusin",
|
||||||
((event: FocusEvent) => {
|
((event: FocusEvent) => {
|
||||||
if (!this.usesVirtualKeyboard()) {
|
if (!this.usesVirtualKeyboard()) {
|
||||||
@@ -1305,45 +1324,6 @@ class WebTerminal {
|
|||||||
// Setup mobile extended keybar (only on mobile devices)
|
// Setup mobile extended keybar (only on mobile devices)
|
||||||
if (isMobileDevice()) {
|
if (isMobileDevice()) {
|
||||||
this.setupMobileKeybar();
|
this.setupMobileKeybar();
|
||||||
this.addTrackedListener(this.element, "touchstart", ((event: TouchEvent) => {
|
|
||||||
if (!this.usesVirtualKeyboard()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const target = event.target;
|
|
||||||
if (
|
|
||||||
target instanceof Node &&
|
|
||||||
((this.mobileVirtualKeyboardHost && this.mobileVirtualKeyboardHost.contains(target)) ||
|
|
||||||
(this.mobileKeybar && this.mobileKeybar.contains(target)))
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
this.lastMobileKeyboardTouchAt = Date.now();
|
|
||||||
if (document.activeElement instanceof HTMLElement) {
|
|
||||||
document.activeElement.blur();
|
|
||||||
}
|
|
||||||
this.toggleMobileKeyboard();
|
|
||||||
}) as EventListener, { passive: false });
|
|
||||||
this.addTrackedListener(this.element, "click", ((event: MouseEvent) => {
|
|
||||||
if (!this.usesVirtualKeyboard()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Date.now() - this.lastMobileKeyboardTouchAt < 750) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const target = event.target;
|
|
||||||
if (
|
|
||||||
target instanceof Node &&
|
|
||||||
((this.mobileVirtualKeyboardHost && this.mobileVirtualKeyboardHost.contains(target)) ||
|
|
||||||
(this.mobileKeybar && this.mobileKeybar.contains(target)))
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.toggleMobileKeyboard();
|
|
||||||
}) as EventListener);
|
|
||||||
this.addTrackedListener(document, "click", ((event: MouseEvent) => {
|
this.addTrackedListener(document, "click", ((event: MouseEvent) => {
|
||||||
if (!this.usesVirtualKeyboard() || !this.mobileKeyboardVisible) {
|
if (!this.usesVirtualKeyboard() || !this.mobileKeyboardVisible) {
|
||||||
return;
|
return;
|
||||||
@@ -1650,6 +1630,7 @@ class WebTerminal {
|
|||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
|
|
||||||
const LONG_PRESS_MS = 300;
|
const LONG_PRESS_MS = 300;
|
||||||
|
const COPY_LONG_PRESS_MS = 500;
|
||||||
const MOVE_THRESHOLD = 10;
|
const MOVE_THRESHOLD = 10;
|
||||||
const SCROLL_SPEED = 1.5;
|
const SCROLL_SPEED = 1.5;
|
||||||
|
|
||||||
@@ -1658,6 +1639,8 @@ class WebTerminal {
|
|||||||
let startY = 0;
|
let startY = 0;
|
||||||
let lastY = 0;
|
let lastY = 0;
|
||||||
let longPressTimer: ReturnType<typeof setTimeout> | null = null;
|
let longPressTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let copyLongPressTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let copiedSelection = false;
|
||||||
let scrollRemainder = 0;
|
let scrollRemainder = 0;
|
||||||
// Momentum state
|
// Momentum state
|
||||||
let velocityY = 0;
|
let velocityY = 0;
|
||||||
@@ -1681,6 +1664,10 @@ class WebTerminal {
|
|||||||
clearTimeout(longPressTimer);
|
clearTimeout(longPressTimer);
|
||||||
longPressTimer = null;
|
longPressTimer = null;
|
||||||
}
|
}
|
||||||
|
if (copyLongPressTimer !== null) {
|
||||||
|
clearTimeout(copyLongPressTimer);
|
||||||
|
copyLongPressTimer = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopMomentum = () => {
|
const stopMomentum = () => {
|
||||||
@@ -1733,10 +1720,25 @@ class WebTerminal {
|
|||||||
velocityY = 0;
|
velocityY = 0;
|
||||||
scrollRemainder = 0;
|
scrollRemainder = 0;
|
||||||
mode = "undecided";
|
mode = "undecided";
|
||||||
|
copiedSelection = false;
|
||||||
|
|
||||||
|
const terminal = this.terminal as unknown as {
|
||||||
|
hasSelection?: () => boolean;
|
||||||
|
};
|
||||||
|
if (terminal.hasSelection?.()) {
|
||||||
|
copyLongPressTimer = setTimeout(() => {
|
||||||
|
copyLongPressTimer = null;
|
||||||
|
if (mode !== "undecided") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
copiedSelection = true;
|
||||||
|
void this.copyCurrentSelectionToClipboard().catch(() => undefined);
|
||||||
|
}, COPY_LONG_PRESS_MS);
|
||||||
|
}
|
||||||
|
|
||||||
longPressTimer = setTimeout(() => {
|
longPressTimer = setTimeout(() => {
|
||||||
longPressTimer = null;
|
longPressTimer = null;
|
||||||
if (mode === "undecided") {
|
if (mode === "undecided" && !copiedSelection) {
|
||||||
mode = "select";
|
mode = "select";
|
||||||
dispatchMouse("mousedown", touch);
|
dispatchMouse("mousedown", touch);
|
||||||
}
|
}
|
||||||
@@ -1797,6 +1799,12 @@ class WebTerminal {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
cancelLongPress();
|
cancelLongPress();
|
||||||
|
|
||||||
|
if (copiedSelection) {
|
||||||
|
copiedSelection = false;
|
||||||
|
mode = "undecided";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (mode === "select") {
|
if (mode === "select") {
|
||||||
dispatchMouse("mouseup", touch);
|
dispatchMouse("mouseup", touch);
|
||||||
} else if (mode === "scroll") {
|
} else if (mode === "scroll") {
|
||||||
@@ -1804,8 +1812,9 @@ class WebTerminal {
|
|||||||
if (Math.abs(velocityY) > 0.5) {
|
if (Math.abs(velocityY) > 0.5) {
|
||||||
momentumFrame = requestAnimationFrame(doMomentumScroll);
|
momentumFrame = requestAnimationFrame(doMomentumScroll);
|
||||||
}
|
}
|
||||||
|
} else if (!this.mobileKeyboardVisible) {
|
||||||
|
this.openMobileKeyboardFromControl();
|
||||||
}
|
}
|
||||||
// If still "undecided", it was a tap — no action needed (focus handled elsewhere)
|
|
||||||
|
|
||||||
mode = "undecided";
|
mode = "undecided";
|
||||||
}) as EventListener,
|
}) as EventListener,
|
||||||
@@ -1825,18 +1834,12 @@ class WebTerminal {
|
|||||||
|
|
||||||
const controls = document.createElement("div");
|
const controls = document.createElement("div");
|
||||||
controls.className = "webterm-voice-controls";
|
controls.className = "webterm-voice-controls";
|
||||||
controls.innerHTML = `
|
controls.innerHTML = `<span class="webterm-voice-status">Ready</span>`;
|
||||||
<button type="button" class="webterm-voice-button" aria-pressed="false" title="Start voice input">
|
|
||||||
Voice
|
|
||||||
</button>
|
|
||||||
<span class="webterm-voice-status">Ready</span>
|
|
||||||
`;
|
|
||||||
this.element.appendChild(controls);
|
this.element.appendChild(controls);
|
||||||
this.voiceControls = controls;
|
this.voiceControls = controls;
|
||||||
this.voiceButton = controls.querySelector(".webterm-voice-button");
|
|
||||||
this.voiceStatus = controls.querySelector(".webterm-voice-status");
|
this.voiceStatus = controls.querySelector(".webterm-voice-status");
|
||||||
|
|
||||||
if (!this.voiceButton || !this.voiceStatus) {
|
if (!this.voiceStatus) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1847,8 +1850,8 @@ class WebTerminal {
|
|||||||
zIndex: "4",
|
zIndex: "4",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "8px",
|
gap: "0",
|
||||||
padding: "8px 10px",
|
padding: "8px 12px",
|
||||||
borderRadius: "999px",
|
borderRadius: "999px",
|
||||||
background: "rgba(9, 14, 19, 0.78)",
|
background: "rgba(9, 14, 19, 0.78)",
|
||||||
backdropFilter: "blur(8px)",
|
backdropFilter: "blur(8px)",
|
||||||
@@ -1856,16 +1859,6 @@ class WebTerminal {
|
|||||||
pointerEvents: "auto",
|
pointerEvents: "auto",
|
||||||
} satisfies Partial<CSSStyleDeclaration>);
|
} satisfies Partial<CSSStyleDeclaration>);
|
||||||
|
|
||||||
Object.assign(this.voiceButton.style, {
|
|
||||||
border: "1px solid rgba(255, 255, 255, 0.18)",
|
|
||||||
borderRadius: "999px",
|
|
||||||
background: "#12202b",
|
|
||||||
color: "#f3f6fa",
|
|
||||||
font: '600 12px "Fira Code", "FiraCode Nerd Font", monospace',
|
|
||||||
padding: "7px 12px",
|
|
||||||
cursor: "pointer",
|
|
||||||
} satisfies Partial<CSSStyleDeclaration>);
|
|
||||||
|
|
||||||
Object.assign(this.voiceStatus.style, {
|
Object.assign(this.voiceStatus.style, {
|
||||||
color: "#c9d5e0",
|
color: "#c9d5e0",
|
||||||
font: '500 11px "Fira Code", "FiraCode Nerd Font", monospace',
|
font: '500 11px "Fira Code", "FiraCode Nerd Font", monospace',
|
||||||
@@ -1874,10 +1867,6 @@ class WebTerminal {
|
|||||||
maxWidth: "44ch",
|
maxWidth: "44ch",
|
||||||
overflowWrap: "anywhere",
|
overflowWrap: "anywhere",
|
||||||
} satisfies Partial<CSSStyleDeclaration>);
|
} satisfies Partial<CSSStyleDeclaration>);
|
||||||
|
|
||||||
this.voiceButton.addEventListener("click", () => {
|
|
||||||
void this.toggleVoiceInput();
|
|
||||||
});
|
|
||||||
if (
|
if (
|
||||||
typeof navigator === "undefined" ||
|
typeof navigator === "undefined" ||
|
||||||
!navigator.mediaDevices?.getUserMedia ||
|
!navigator.mediaDevices?.getUserMedia ||
|
||||||
@@ -2245,7 +2234,10 @@ class WebTerminal {
|
|||||||
state: "idle" | "loading" | "listening" | "error" | "unsupported",
|
state: "idle" | "loading" | "listening" | "error" | "unsupported",
|
||||||
message: string
|
message: string
|
||||||
): void {
|
): void {
|
||||||
if (!this.voiceButton || !this.voiceStatus) {
|
this.voiceState = state;
|
||||||
|
this.syncVirtualKeyboardState();
|
||||||
|
|
||||||
|
if (!this.voiceStatus) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2258,31 +2250,16 @@ class WebTerminal {
|
|||||||
|
|
||||||
this.voiceStatus.textContent = displayMessage;
|
this.voiceStatus.textContent = displayMessage;
|
||||||
this.voiceStatus.title = message;
|
this.voiceStatus.title = message;
|
||||||
this.voiceButton.disabled = state === "loading" || state === "unsupported";
|
|
||||||
this.voiceButton.textContent = state === "listening" ? "Stop" : "Voice";
|
|
||||||
this.voiceButton.setAttribute("aria-pressed", state === "listening" ? "true" : "false");
|
|
||||||
this.voiceButton.title =
|
|
||||||
state === "error"
|
|
||||||
? message
|
|
||||||
: state === "listening"
|
|
||||||
? "Stop voice input"
|
|
||||||
: "Start voice input";
|
|
||||||
|
|
||||||
if (state === "listening") {
|
if (state === "listening") {
|
||||||
this.voiceButton.style.background = "#7c1d1d";
|
|
||||||
this.voiceButton.style.borderColor = "rgba(255, 120, 120, 0.5)";
|
|
||||||
if (this.voiceControls) {
|
if (this.voiceControls) {
|
||||||
this.voiceControls.style.background = "rgba(45, 10, 10, 0.78)";
|
this.voiceControls.style.background = "rgba(45, 10, 10, 0.78)";
|
||||||
}
|
}
|
||||||
} else if (state === "error") {
|
} else if (state === "error") {
|
||||||
this.voiceButton.style.background = "#3d2a12";
|
|
||||||
this.voiceButton.style.borderColor = "rgba(255, 187, 92, 0.55)";
|
|
||||||
if (this.voiceControls) {
|
if (this.voiceControls) {
|
||||||
this.voiceControls.style.background = "rgba(46, 29, 8, 0.82)";
|
this.voiceControls.style.background = "rgba(46, 29, 8, 0.82)";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.voiceButton.style.background = "#12202b";
|
|
||||||
this.voiceButton.style.borderColor = "rgba(255, 255, 255, 0.18)";
|
|
||||||
if (this.voiceControls) {
|
if (this.voiceControls) {
|
||||||
this.voiceControls.style.background = "rgba(9, 14, 19, 0.78)";
|
this.voiceControls.style.background = "rgba(9, 14, 19, 0.78)";
|
||||||
}
|
}
|
||||||
@@ -2305,6 +2282,10 @@ class WebTerminal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setMobileKeyboardVisible(visible: boolean): void {
|
private setMobileKeyboardVisible(visible: boolean): void {
|
||||||
|
if (visible && this.usesVirtualKeyboard() && !this.allowMobileKeyboardOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.allowMobileKeyboardOpen = false;
|
||||||
this.mobileKeyboardVisible = visible;
|
this.mobileKeyboardVisible = visible;
|
||||||
if (this.mobileVirtualKeyboardHost) {
|
if (this.mobileVirtualKeyboardHost) {
|
||||||
this.mobileVirtualKeyboardHost.style.display = visible ? "" : "none";
|
this.mobileVirtualKeyboardHost.style.display = visible ? "" : "none";
|
||||||
@@ -2312,8 +2293,9 @@ class WebTerminal {
|
|||||||
if (this.mobileKeybar) {
|
if (this.mobileKeybar) {
|
||||||
this.mobileKeybar.style.display = visible ? "" : "none";
|
this.mobileKeybar.style.display = visible ? "" : "none";
|
||||||
}
|
}
|
||||||
|
this.updateMobileKeyboardDockLayout();
|
||||||
|
this.fit();
|
||||||
if (visible) {
|
if (visible) {
|
||||||
this.updateVirtualKeyboardHostHeight();
|
|
||||||
this.requestMobileVirtualKeyboardDraw();
|
this.requestMobileVirtualKeyboardDraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2322,6 +2304,11 @@ class WebTerminal {
|
|||||||
this.setMobileKeyboardVisible(!this.mobileKeyboardVisible);
|
this.setMobileKeyboardVisible(!this.mobileKeyboardVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private openMobileKeyboardFromControl(): void {
|
||||||
|
this.allowMobileKeyboardOpen = true;
|
||||||
|
this.setMobileKeyboardVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
private getMobileModifierState(): {
|
private getMobileModifierState(): {
|
||||||
useShift: boolean;
|
useShift: boolean;
|
||||||
useCtrl: boolean;
|
useCtrl: boolean;
|
||||||
@@ -2426,9 +2413,6 @@ class WebTerminal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private currentVirtualKeyboardLayout(): VirtualKeyboardKey[][] {
|
private currentVirtualKeyboardLayout(): VirtualKeyboardKey[][] {
|
||||||
if (this.mobileSelectionMode) {
|
|
||||||
return VIRTUAL_KEYBOARD_SELECTION_LAYOUT;
|
|
||||||
}
|
|
||||||
return this.mobileKeyboardMode === "symbol"
|
return this.mobileKeyboardMode === "symbol"
|
||||||
? VIRTUAL_KEYBOARD_SYMBOL_LAYOUT
|
? VIRTUAL_KEYBOARD_SYMBOL_LAYOUT
|
||||||
: VIRTUAL_KEYBOARD_ALPHA_LAYOUT;
|
: VIRTUAL_KEYBOARD_ALPHA_LAYOUT;
|
||||||
@@ -2450,11 +2434,34 @@ class WebTerminal {
|
|||||||
this.mobileVirtualKeyboardHost.style.height = `${Math.round(this.virtualKeyboardBaseHeight / scale)}px`;
|
this.mobileVirtualKeyboardHost.style.height = `${Math.round(this.virtualKeyboardBaseHeight / scale)}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private updateMobileKeyboardDockLayout(): void {
|
||||||
|
if (this.mobileVirtualKeyboardHost) {
|
||||||
|
this.mobileVirtualKeyboardHost.style.bottom = "0";
|
||||||
|
}
|
||||||
|
if (this.mobileKeybar) {
|
||||||
|
const keyboardHeight = this.mobileVirtualKeyboardHost?.offsetHeight ?? 0;
|
||||||
|
this.mobileKeybar.style.bottom = `${keyboardHeight}px`;
|
||||||
|
}
|
||||||
|
const keyboardHeight = this.mobileKeyboardVisible
|
||||||
|
? (this.mobileKeybar?.offsetHeight ?? 0) + (this.mobileVirtualKeyboardHost?.offsetHeight ?? 0)
|
||||||
|
: 0;
|
||||||
|
this.element.style.paddingBottom = `${keyboardHeight}px`;
|
||||||
|
}
|
||||||
|
|
||||||
private getVirtualKeyboardRect(): DOMRect | null {
|
private getVirtualKeyboardRect(): DOMRect | null {
|
||||||
return this.mobileVirtualKeyboard?.getBoundingClientRect() ?? null;
|
return this.mobileVirtualKeyboard?.getBoundingClientRect() ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getVirtualKeyboardLabel(key: VirtualKeyboardKey): string {
|
private getVirtualKeyboardLabel(key: VirtualKeyboardKey): string {
|
||||||
|
if (key.actionId === "toggle-voice") {
|
||||||
|
if (this.voiceState === "listening") {
|
||||||
|
return "Stop";
|
||||||
|
}
|
||||||
|
if (this.voiceState === "loading") {
|
||||||
|
return "Mic...";
|
||||||
|
}
|
||||||
|
return "Mic";
|
||||||
|
}
|
||||||
if (key.kind === "char") {
|
if (key.kind === "char") {
|
||||||
return this.shiftActive && key.shiftLabel ? key.shiftLabel : key.label;
|
return this.shiftActive && key.shiftLabel ? key.shiftLabel : key.label;
|
||||||
}
|
}
|
||||||
@@ -2531,15 +2538,12 @@ class WebTerminal {
|
|||||||
if (key.key.modifier === "fn") {
|
if (key.key.modifier === "fn") {
|
||||||
return this.fnActive;
|
return this.fnActive;
|
||||||
}
|
}
|
||||||
if (key.key.actionId === "toggle-selection-adjust") {
|
|
||||||
return this.mobileSelectionAdjusting;
|
|
||||||
}
|
|
||||||
if (key.key.actionId === "mode-selection" || key.key.actionId === "mode-alpha") {
|
|
||||||
return this.mobileSelectionMode;
|
|
||||||
}
|
|
||||||
if (key.key.actionId === "mode-symbol") {
|
if (key.key.actionId === "mode-symbol") {
|
||||||
return this.mobileKeyboardMode === "symbol";
|
return this.mobileKeyboardMode === "symbol";
|
||||||
}
|
}
|
||||||
|
if (key.key.actionId === "toggle-voice") {
|
||||||
|
return this.voiceState === "listening" || this.voiceState === "loading";
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2712,113 +2716,26 @@ class WebTerminal {
|
|||||||
this.mobileVirtualKeyboardActivePresses.set(press.pointerId, press);
|
this.mobileVirtualKeyboardActivePresses.set(press.pointerId, press);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async copyTerminalSelection(): Promise<void> {
|
|
||||||
const terminal = this.terminal as unknown as {
|
|
||||||
getSelection?: () => string;
|
|
||||||
hasSelection?: () => boolean;
|
|
||||||
clearSelection?: () => void;
|
|
||||||
};
|
|
||||||
const selectedText =
|
|
||||||
(terminal.hasSelection?.() ? terminal.getSelection?.() : "") ||
|
|
||||||
document.getSelection()?.toString() ||
|
|
||||||
"";
|
|
||||||
if (!selectedText) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await navigator.clipboard?.writeText(selectedText);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async pasteFromClipboard(): Promise<void> {
|
|
||||||
const text = await navigator.clipboard?.readText();
|
|
||||||
if (text) {
|
|
||||||
this.sendStdin(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async cutTerminalSelection(): Promise<void> {
|
|
||||||
const terminal = this.terminal as unknown as {
|
|
||||||
clearSelection?: () => void;
|
|
||||||
};
|
|
||||||
await this.copyTerminalSelection();
|
|
||||||
terminal.clearSelection?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
private sendSelectionArrow(seq: "\x1b[D" | "\x1b[C"): void {
|
|
||||||
const { useCtrl, useAlt, useFn } = this.getMobileModifierState();
|
|
||||||
const useShift = this.shiftActive || this.pendingShift || this.mobileSelectionAdjusting;
|
|
||||||
let output: string = seq;
|
|
||||||
const dir = output[2];
|
|
||||||
if (useCtrl && useShift) {
|
|
||||||
output = `\x1b[1;6${dir}`;
|
|
||||||
} else if (useCtrl) {
|
|
||||||
output = `\x1b[1;5${dir}`;
|
|
||||||
} else if (useShift) {
|
|
||||||
output = `\x1b[1;2${dir}`;
|
|
||||||
}
|
|
||||||
if (useAlt) {
|
|
||||||
output = applyAltModifier(output);
|
|
||||||
}
|
|
||||||
if (useFn && output.length === 1) {
|
|
||||||
const fnApplied = applyFnModifier(output, useShift);
|
|
||||||
if (fnApplied) {
|
|
||||||
output = fnApplied;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.sendStdin(output);
|
|
||||||
this.deactivateModifiers();
|
|
||||||
this.syncVirtualKeyboardState();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async dispatchVirtualKeyboardKey(key: VirtualKeyboardKeyBounds): Promise<void> {
|
private async dispatchVirtualKeyboardKey(key: VirtualKeyboardKeyBounds): Promise<void> {
|
||||||
const actionId = key.key.actionId;
|
const actionId = key.key.actionId;
|
||||||
if (actionId === "mode-alpha") {
|
if (actionId === "mode-alpha") {
|
||||||
this.mobileSelectionMode = false;
|
|
||||||
this.mobileKeyboardMode = "alpha";
|
this.mobileKeyboardMode = "alpha";
|
||||||
this.mobileSelectionAdjusting = false;
|
|
||||||
this.syncVirtualKeyboardState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (actionId === "mode-selection") {
|
|
||||||
this.mobileSelectionMode = true;
|
|
||||||
this.mobileSelectionAdjusting = false;
|
|
||||||
this.syncVirtualKeyboardState();
|
this.syncVirtualKeyboardState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (actionId === "mode-symbol") {
|
if (actionId === "mode-symbol") {
|
||||||
this.mobileSelectionMode = false;
|
|
||||||
this.mobileKeyboardMode = this.mobileKeyboardMode === "symbol" ? "alpha" : "symbol";
|
this.mobileKeyboardMode = this.mobileKeyboardMode === "symbol" ? "alpha" : "symbol";
|
||||||
this.syncVirtualKeyboardState();
|
this.syncVirtualKeyboardState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (actionId === "toggle-selection-adjust") {
|
if (actionId === "toggle-voice") {
|
||||||
this.mobileSelectionAdjusting = !this.mobileSelectionAdjusting;
|
await this.toggleVoiceInput();
|
||||||
this.syncVirtualKeyboardState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (actionId === "copy-selection") {
|
|
||||||
await this.copyTerminalSelection().catch(() => undefined);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (actionId === "paste-selection") {
|
|
||||||
await this.pasteFromClipboard().catch(() => undefined);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (actionId === "cut-selection") {
|
|
||||||
await this.cutTerminalSelection().catch(() => undefined);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (key.key.modifier) {
|
if (key.key.modifier) {
|
||||||
this.toggleModifierState(key.key.modifier);
|
this.toggleModifierState(key.key.modifier);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (key.value === "ArrowLeft") {
|
|
||||||
this.sendSelectionArrow("\x1b[D");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (key.value === "ArrowRight") {
|
|
||||||
this.sendSelectionArrow("\x1b[C");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (key.key.seq) {
|
if (key.key.seq) {
|
||||||
this.sendMobileSequence(key.key.seq);
|
this.sendMobileSequence(key.key.seq);
|
||||||
return;
|
return;
|
||||||
@@ -2904,19 +2821,22 @@ class WebTerminal {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private setupVirtualKeyboard(): void {
|
private setupVirtualKeyboard(): void {
|
||||||
if (this.mobileVirtualKeyboard || this.mobileVirtualKeyboardHost || !document.body) {
|
if (this.mobileVirtualKeyboard || this.mobileVirtualKeyboardHost) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyboardHost = document.createElement("div");
|
const keyboardHost = document.createElement("div");
|
||||||
keyboardHost.className = "mobile-virtual-keyboard";
|
keyboardHost.className = "mobile-virtual-keyboard";
|
||||||
keyboardHost.style.cssText = `
|
keyboardHost.style.cssText = `
|
||||||
flex-shrink: 0;
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
background: #0f172a;
|
background: #0f172a;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
|
z-index: 9998;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
@@ -2933,14 +2853,17 @@ class WebTerminal {
|
|||||||
canvas.addEventListener("pointerleave", this.handleVirtualKeyboardPointerUp, { passive: false });
|
canvas.addEventListener("pointerleave", this.handleVirtualKeyboardPointerUp, { passive: false });
|
||||||
|
|
||||||
keyboardHost.appendChild(canvas);
|
keyboardHost.appendChild(canvas);
|
||||||
document.body.appendChild(keyboardHost);
|
this.element.appendChild(keyboardHost);
|
||||||
this.mobileVirtualKeyboardHost = keyboardHost;
|
this.mobileVirtualKeyboardHost = keyboardHost;
|
||||||
this.mobileVirtualKeyboard = canvas;
|
this.mobileVirtualKeyboard = canvas;
|
||||||
this.mobileVirtualKeyboardBounds = [];
|
this.mobileVirtualKeyboardBounds = [];
|
||||||
this.updateVirtualKeyboardHostHeight();
|
this.updateVirtualKeyboardHostHeight();
|
||||||
|
this.updateMobileKeyboardDockLayout();
|
||||||
this.addTrackedListener(window, "resize", () => {
|
this.addTrackedListener(window, "resize", () => {
|
||||||
this.updateVirtualKeyboardHostHeight();
|
this.updateVirtualKeyboardHostHeight();
|
||||||
|
this.updateMobileKeyboardDockLayout();
|
||||||
this.mobileVirtualKeyboardBounds = [];
|
this.mobileVirtualKeyboardBounds = [];
|
||||||
|
this.fit();
|
||||||
this.requestMobileVirtualKeyboardDraw();
|
this.requestMobileVirtualKeyboardDraw();
|
||||||
});
|
});
|
||||||
this.setMobileKeyboardVisible(false);
|
this.setMobileKeyboardVisible(false);
|
||||||
@@ -2987,15 +2910,17 @@ class WebTerminal {
|
|||||||
const style = document.createElement("style");
|
const style = document.createElement("style");
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
.mobile-keybar {
|
.mobile-keybar {
|
||||||
flex-shrink: 0;
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
background: #0f172a;
|
background: #0f172a;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
border-top: 1px solid rgba(148, 163, 184, 0.18);
|
border-top: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
.mobile-virtual-keyboard {
|
.mobile-virtual-keyboard {
|
||||||
flex-shrink: 0;
|
|
||||||
background: #0f172a;
|
background: #0f172a;
|
||||||
border-top: 1px solid rgba(148, 163, 184, 0.18);
|
border-top: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
}
|
}
|
||||||
@@ -3053,14 +2978,18 @@ class WebTerminal {
|
|||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
this.mobileKeybarStyle = style;
|
this.mobileKeybarStyle = style;
|
||||||
document.body.appendChild(keybar);
|
this.element.appendChild(keybar);
|
||||||
this.mobileKeybar = keybar;
|
this.mobileKeybar = keybar;
|
||||||
|
this.updateMobileKeyboardDockLayout();
|
||||||
|
this.fit();
|
||||||
this.setMobileKeyboardVisible(false);
|
this.setMobileKeyboardVisible(false);
|
||||||
|
|
||||||
// Toggle between keys and settings panels
|
// Toggle between keys and settings panels
|
||||||
const showPanel = (panel: "keys" | "settings") => {
|
const showPanel = (panel: "keys" | "settings") => {
|
||||||
keysPanel.style.display = panel === "keys" ? "" : "none";
|
keysPanel.style.display = panel === "keys" ? "" : "none";
|
||||||
settingsPanel.style.display = panel === "settings" ? "" : "none";
|
settingsPanel.style.display = panel === "settings" ? "" : "none";
|
||||||
|
this.updateMobileKeyboardDockLayout();
|
||||||
|
this.fit();
|
||||||
};
|
};
|
||||||
|
|
||||||
keysPanel.querySelector(".keybar-settings")!.addEventListener("touchstart", (e) => {
|
keysPanel.querySelector(".keybar-settings")!.addEventListener("touchstart", (e) => {
|
||||||
@@ -3088,6 +3017,7 @@ class WebTerminal {
|
|||||||
keybar.querySelectorAll("button").forEach((btn) => {
|
keybar.querySelectorAll("button").forEach((btn) => {
|
||||||
(btn as HTMLElement).style.height = `${this.keybarButtonHeight}px`;
|
(btn as HTMLElement).style.height = `${this.keybarButtonHeight}px`;
|
||||||
});
|
});
|
||||||
|
this.updateMobileKeyboardDockLayout();
|
||||||
this.requestMobileVirtualKeyboardDraw();
|
this.requestMobileVirtualKeyboardDraw();
|
||||||
this.fit();
|
this.fit();
|
||||||
};
|
};
|
||||||
@@ -3172,7 +3102,6 @@ class WebTerminal {
|
|||||||
/** Focus the mobile input to show keyboard */
|
/** Focus the mobile input to show keyboard */
|
||||||
private focusMobileInput(): void {
|
private focusMobileInput(): void {
|
||||||
if (this.usesVirtualKeyboard()) {
|
if (this.usesVirtualKeyboard()) {
|
||||||
this.setMobileKeyboardVisible(true);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// For programmatic focus (not from user gesture), this may not show keyboard on iOS
|
// For programmatic focus (not from user gesture), this may not show keyboard on iOS
|
||||||
@@ -3643,7 +3572,6 @@ class WebTerminal {
|
|||||||
this.voiceControls.remove();
|
this.voiceControls.remove();
|
||||||
this.voiceControls = null;
|
this.voiceControls = null;
|
||||||
}
|
}
|
||||||
this.voiceButton = null;
|
|
||||||
this.voiceStatus = null;
|
this.voiceStatus = null;
|
||||||
this.destroyVoiceEngine();
|
this.destroyVoiceEngine();
|
||||||
this.fitAddon.dispose();
|
this.fitAddon.dispose();
|
||||||
|
|||||||
Reference in New Issue
Block a user