From 9dbd3dbd46d8218fd980e4577e96de5c3eb71ec8 Mon Sep 17 00:00:00 2001 From: shekohex Date: Fri, 6 Feb 2026 03:16:02 +0200 Subject: [PATCH] fix(terminal): prevent native caret blink in ghostty focus inputs (#329) --- .../components/terminal/TerminalViewport.tsx | 99 +++++++++++++------ packages/ui/src/index.css | 68 ++++++++++++- 2 files changed, 137 insertions(+), 30 deletions(-) diff --git a/packages/ui/src/components/terminal/TerminalViewport.tsx b/packages/ui/src/components/terminal/TerminalViewport.tsx index 5f2a3014..cd1255bb 100644 --- a/packages/ui/src/components/terminal/TerminalViewport.tsx +++ b/packages/ui/src/components/terminal/TerminalViewport.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { createPortal } from 'react-dom'; import { Ghostty, Terminal as GhosttyTerminal, FitAddon } from 'ghostty-web'; +import { isMobileDeviceViaCSS } from '@/lib/device'; import type { TerminalTheme } from '@/lib/terminalTheme'; import { getGhosttyTerminalOptions } from '@/lib/terminalTheme'; import type { TerminalChunk } from '@/stores/useTerminalStore'; @@ -99,17 +100,44 @@ const TerminalViewport = React.forwardRef { - if (!enableTouchScroll) { - return; - } const container = containerRef.current; const hiddenInput = hiddenInputRef.current; if (!container) { return; } + + const editableNodes = Array.from(container.querySelectorAll('[contenteditable="true"]')); + editableNodes.forEach((node) => { + node.style.setProperty('caret-color', 'transparent'); + node.style.outline = 'none'; + }); + const textareas = Array.from(container.querySelectorAll('textarea')) as HTMLTextAreaElement[]; textareas.forEach((textarea) => { + textarea.style.setProperty('caret-color', 'transparent'); + textarea.style.color = 'transparent'; + textarea.style.setProperty('-webkit-text-fill-color', 'transparent'); + textarea.style.background = 'transparent'; + textarea.style.border = '0'; + textarea.style.outline = 'none'; + textarea.style.boxShadow = 'none'; + textarea.style.textShadow = 'none'; + textarea.style.fontSize = '0'; + textarea.style.lineHeight = '0'; + + if (!useHiddenInputOverlay) { + return; + } + if (textarea === hiddenInput) { return; } @@ -127,10 +155,9 @@ const TerminalViewport = React.forwardRef { const input = (useTextInput ? textInputRef.current : hiddenInputRef.current) as HTMLElement | null; @@ -230,7 +257,7 @@ const TerminalViewport = React.forwardRef { const container = containerRef.current; - if (!enableTouchScroll || !container) { + if (!useHiddenInputOverlay || !container) { return; } @@ -255,7 +282,7 @@ const TerminalViewport = React.forwardRef { container.removeEventListener('focusin', handleContainerFocusIn, true); }; - }, [enableTouchScroll, focusHiddenInput]); + }, [useHiddenInputOverlay, focusHiddenInput]); const copySelectionToClipboard = React.useCallback(async () => { if (typeof window === 'undefined' || typeof document === 'undefined') { @@ -579,7 +606,11 @@ const TerminalViewport = React.forwardRef { let disposed = false; @@ -802,7 +837,7 @@ const TerminalViewport = React.forwardRef { try { @@ -811,7 +846,7 @@ const TerminalViewport = React.forwardRef { disableTerminalTextareas(); }); @@ -845,7 +880,7 @@ const TerminalViewport = React.forwardRef { @@ -900,10 +935,10 @@ const TerminalViewport = React.forwardRef { setupTouchScroll(); @@ -949,7 +984,7 @@ const TerminalViewport = React.forwardRef ({ focus: () => { - if (enableTouchScroll) { + if (useHiddenInputOverlay) { focusHiddenInput(); return; } @@ -968,12 +1003,12 @@ const TerminalViewport = React.forwardRef) => { - if (!enableTouchScroll) { + if (!useHiddenInputOverlay) { return; } @@ -993,7 +1028,7 @@ const TerminalViewport = React.forwardRef { - if (enableTouchScroll) { + if (useHiddenInputOverlay) { const touch = event.touches?.[0]; focusHiddenInput(touch?.clientX, touch?.clientY); } }} onClick={(event) => { - if (enableTouchScroll) { + if (useHiddenInputOverlay) { focusHiddenInput(event.clientX, event.clientY); } else { terminalRef.current?.focus(); @@ -1260,7 +1299,7 @@ const TerminalViewport = React.forwardRef - {enableTouchScroll && typeof document !== 'undefined' + {useHiddenInputOverlay && typeof document !== 'undefined' ? createPortal( <>