diff --git a/packages/app/src/components/terminal-emulator.native.tsx b/packages/app/src/components/terminal-emulator.native.tsx index 29851551f..b3e6634ee 100644 --- a/packages/app/src/components/terminal-emulator.native.tsx +++ b/packages/app/src/components/terminal-emulator.native.tsx @@ -80,7 +80,7 @@ type BridgeInboundMessage = | { type: "restoreOutput"; streamKey: string; text: string } | { type: "renderSnapshot"; streamKey: string; state: TerminalState | null } | { type: "clear"; streamKey: string } - | { type: "focus"; streamKey: string } + | { type: "focus"; streamKey: string; forceRefocus?: boolean } | { type: "resize"; streamKey: string } | { type: "setTheme"; streamKey: string; theme: ITheme } | { type: "setScrollback"; streamKey: string; lines: number } @@ -543,19 +543,33 @@ export default function TerminalEmulator({ resetWebViewDocument(); }, [resetWebViewDocument]); + const handleWebViewTouchStart = useCallback(() => { + webViewRef.current?.requestFocus(); + sendToWebView({ type: "focus", streamKey, forceRefocus: true }); + }, [sendToWebView, streamKey]); + + const backgroundColor = xtermTheme.background ?? "#0b0b0b"; + const rootStyle = useMemo>( + () => [styles.root, { backgroundColor }], + [backgroundColor], + ); const webViewStyle = useMemo>( - () => [styles.webView, { backgroundColor: xtermTheme.background ?? "#0b0b0b" }], - [xtermTheme.background], + () => [styles.webView, { backgroundColor }], + [backgroundColor], + ); + const webViewContainerStyle = useMemo>( + () => [styles.webViewContainer, { backgroundColor }], + [backgroundColor], ); return ( - + { if (typeof document !== "undefined" && document.visibilityState !== "visible") { @@ -252,6 +253,8 @@ export class TerminalEmulatorRuntime { // Ligatures require Font Access API or compatible environment } terminal.open(input.host); + this.themeBackgroundElements = this.collectThemeBackgroundElements(input); + this.applyThemeBackground(input.theme); try { terminal.unicode.activeVersion = "11"; } catch { @@ -646,6 +649,7 @@ export class TerminalEmulatorRuntime { return; } + this.applyThemeBackground(input.theme); this.refreshVisibleRows(); } @@ -665,8 +669,15 @@ export class TerminalEmulatorRuntime { this.refreshVisibleRows(); } - focus(): void { - this.terminal?.focus(); + focus(input?: { forceRefocus?: boolean }): void { + const terminal = this.terminal; + if (!terminal) { + return; + } + if (input?.forceRefocus) { + terminal.blur(); + } + terminal.focus(); } private refreshVisibleRows(): void { @@ -682,6 +693,26 @@ export class TerminalEmulatorRuntime { } } + private collectThemeBackgroundElements(input: { + root: HTMLDivElement; + host: HTMLDivElement; + }): HTMLElement[] { + return [ + input.root, + input.host, + ...Array.from( + input.host.querySelectorAll(".xterm, .xterm-screen, .xterm-viewport"), + ), + ]; + } + + private applyThemeBackground(theme: ITheme): void { + const background = theme.background ?? "#0b0b0b"; + for (const element of this.themeBackgroundElements) { + element.style.backgroundColor = background; + } + } + unmount(): void { this.clearInFlightOutputTimeout(); const inFlightOperation = this.inFlightOutputOperation; @@ -703,6 +734,7 @@ export class TerminalEmulatorRuntime { this.fitAddon = null; this.fitAndEmitResize = null; this.lastSize = null; + this.themeBackgroundElements = []; this.suppressInput = false; this.inputModeDecoder.decode(); this.inputModeTracker.reset(); diff --git a/packages/app/src/terminal/webview/terminal-emulator-webview-entry.ts b/packages/app/src/terminal/webview/terminal-emulator-webview-entry.ts index b14db453c..81619d740 100644 --- a/packages/app/src/terminal/webview/terminal-emulator-webview-entry.ts +++ b/packages/app/src/terminal/webview/terminal-emulator-webview-entry.ts @@ -29,7 +29,7 @@ type InboundMessage = | { type: "restoreOutput"; streamKey: string; text: string } | { type: "renderSnapshot"; streamKey: string; state: TerminalState | null } | { type: "clear"; streamKey: string } - | { type: "focus"; streamKey: string } + | { type: "focus"; streamKey: string; forceRefocus?: boolean } | { type: "resize"; streamKey: string } | { type: "setTheme"; streamKey: string; theme: ITheme } | { type: "setScrollback"; streamKey: string; lines: number } @@ -88,10 +88,20 @@ const sendToNative = (message: OutboundMessage): void => { window.ReactNativeWebView?.postMessage?.(JSON.stringify(message)); }; +const TERMINAL_BACKGROUND_CSS_VAR = "--paseo-terminal-background"; +const DEFAULT_TERMINAL_BACKGROUND = "#0b0b0b"; + +function getTerminalBackground(theme: ITheme): string { + return theme.background ?? DEFAULT_TERMINAL_BACKGROUND; +} + const installStyles = (): void => { const style = document.createElement("style"); style.textContent = ` ${xtermCss} +:root { + ${TERMINAL_BACKGROUND_CSS_VAR}: transparent; +} html, body, #terminal-root { @@ -101,7 +111,7 @@ body, padding: 0; overflow: hidden; overscroll-behavior: none; - background: #0b0b0b; + background: var(${TERMINAL_BACKGROUND_CSS_VAR}); } #terminal-root { display: flex; @@ -115,6 +125,12 @@ body, width: 100%; height: 100%; overflow: hidden; + background: var(${TERMINAL_BACKGROUND_CSS_VAR}); +} +#terminal-root .xterm, +#terminal-root .xterm-screen, +#terminal-root .xterm-viewport { + background-color: var(${TERMINAL_BACKGROUND_CSS_VAR}) !important; } [data-terminal-scrollbar-root="true"] .xterm-viewport { scrollbar-width: none; @@ -124,6 +140,17 @@ body, width: 0; height: 0; } +#terminal-root .xterm .xterm-helper-textarea { + opacity: 0.01; + width: 1px; + height: 1px; + min-width: 1px; + min-height: 1px; + color: transparent; + background: transparent; + caret-color: transparent; + z-index: 5 !important; +} `; document.head.appendChild(style); }; @@ -148,6 +175,7 @@ class TerminalWebViewBridge { private readonly host: HTMLDivElement, ) { this.root.addEventListener("pointerdown", this.handlePointerDown, { passive: true }); + this.root.addEventListener("touchstart", this.handleTouchStart, { passive: true }); this.root.addEventListener("pointermove", this.handlePointerMove, { passive: false }); this.root.addEventListener("pointerup", this.handlePointerUp, { passive: true }); this.root.addEventListener("pointercancel", this.handlePointerUp, { passive: true }); @@ -204,12 +232,13 @@ class TerminalWebViewBridge { this.runtime?.clear(); break; case "focus": - this.runtime?.focus(); + this.runtime?.focus({ forceRefocus: message.forceRefocus }); break; case "resize": this.runtime?.resize({ force: true }); break; case "setTheme": + this.applyThemeBackground(message.theme); this.runtime?.setTheme({ theme: message.theme }); break; case "setScrollback": @@ -228,7 +257,7 @@ class TerminalWebViewBridge { this.unmount(this.streamKey); this.streamKey = message.streamKey; this.swipeGesturesEnabled = message.swipeGesturesEnabled; - document.body.style.backgroundColor = message.theme.background ?? "#0b0b0b"; + this.applyThemeBackground(message.theme); const runtime = new TerminalEmulatorRuntime(); this.runtime = runtime; @@ -266,6 +295,14 @@ class TerminalWebViewBridge { sendToNative({ type: "rendererReady", streamKey: message.streamKey, isReady: true }); } + private applyThemeBackground(theme: ITheme): void { + const background = getTerminalBackground(theme); + document.documentElement.style.setProperty(TERMINAL_BACKGROUND_CSS_VAR, background); + document.body.style.backgroundColor = background; + this.root.style.backgroundColor = background; + this.host.style.backgroundColor = background; + } + private unmount(streamKey: string | null): void { if (!this.runtime) { return; @@ -325,7 +362,11 @@ class TerminalWebViewBridge { } private handlePointerDown = (event: PointerEvent): void => { - if (!this.swipeGesturesEnabled || !event.isPrimary) { + if (!event.isPrimary) { + return; + } + this.runtime?.focus({ forceRefocus: true }); + if (!this.swipeGesturesEnabled) { return; } this.trackingSwipe = true; @@ -335,6 +376,12 @@ class TerminalWebViewBridge { this.startY = event.clientY; }; + private handleTouchStart = (event: TouchEvent): void => { + if (event.touches.length === 1) { + this.runtime?.focus({ forceRefocus: true }); + } + }; + private handlePointerMove = (event: PointerEvent): void => { if (!this.trackingSwipe || this.firedSwipe || !this.streamKey) { return; diff --git a/packages/app/src/terminal/webview/terminal-emulator-webview-html.ts b/packages/app/src/terminal/webview/terminal-emulator-webview-html.ts index 9f9827f9e..cd592df3a 100644 --- a/packages/app/src/terminal/webview/terminal-emulator-webview-html.ts +++ b/packages/app/src/terminal/webview/terminal-emulator-webview-html.ts @@ -1,5 +1,4 @@ // Generated by packages/app/scripts/build-terminal-webview-html.mjs. // Do not edit by hand. -export const terminalEmulatorWebViewHtml = - '\n\n \n \n \n \n \n \n \n'; +export const terminalEmulatorWebViewHtml = "\n\n \n \n \n \n \n \n \n";