import { memo, useCallback, useEffect, useRef } from "react"; import { Pressable, ScrollView, TextInput, View, type LayoutChangeEvent, type NativeSyntheticEvent, type ViewProps, useColorScheme, } from "react-native"; import { AppText as Text } from "../../components/AppText"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; import { resolveNativeTerminalSurfaceView } from "./nativeTerminalModule"; import { buildGhosttyThemeConfig, getPierreTerminalTheme, type TerminalTheme, } from "./terminalTheme"; import { terminalDebugLog } from "./terminalDebugLog"; interface TerminalInputEvent { readonly data: string; } interface TerminalResizeEvent { readonly cols: number; readonly rows: number; } interface TerminalSurfaceProps extends ViewProps { readonly terminalKey: string; readonly buffer: string; readonly fontSize?: number; readonly isRunning: boolean; readonly keyboardFocusRequest?: number; readonly theme?: TerminalTheme; readonly onInput: (data: string) => void; readonly onResize: (size: { readonly cols: number; readonly rows: number }) => void; } function estimateGridSize(input: { readonly width: number; readonly height: number; readonly fontSize: number; }): { readonly cols: number; readonly rows: number } { const cellWidth = input.fontSize * 0.62; const cellHeight = input.fontSize * 1.35; return { cols: Math.max(20, Math.min(400, Math.floor(input.width / cellWidth))), rows: Math.max(5, Math.min(200, Math.floor(input.height / cellHeight))), }; } const FallbackTerminalSurface = memo(function FallbackTerminalSurface(props: TerminalSurfaceProps) { const fontSize = props.fontSize ?? MOBILE_TYPOGRAPHY.label.fontSize; const inputRef = useRef(null); const appearanceScheme = useColorScheme() === "light" ? "light" : "dark"; const theme = props.theme ?? getPierreTerminalTheme(appearanceScheme); const statusLabel = props.isRunning ? "Native terminal unavailable. Using text fallback." : "Open terminal to start a shell."; const handleLayout = (event: LayoutChangeEvent) => { const { width, height } = event.nativeEvent.layout; props.onResize(estimateGridSize({ width, height, fontSize })); }; useEffect(() => { if ((props.keyboardFocusRequest ?? 0) > 0) { inputRef.current?.blur(); const focusFrame = requestAnimationFrame(() => inputRef.current?.focus()); return () => cancelAnimationFrame(focusFrame); } return undefined; }, [props.keyboardFocusRequest]); return ( {statusLabel} {props.buffer || "$ "} { const text = event.nativeEvent.text; if (text.length > 0) { props.onInput(`${text}\n`); } }} /> ({ opacity: !props.isRunning ? 0.35 : pressed ? 0.65 : 1, paddingHorizontal: 10, paddingVertical: 6, borderRadius: 8, backgroundColor: theme.border, })} onPress={() => props.onInput("\u0003")} > Ctrl-C ); }); export const TerminalSurface = memo(function TerminalSurface(props: TerminalSurfaceProps) { const fontSize = props.fontSize ?? MOBILE_TYPOGRAPHY.label.fontSize; const keyboardInputRef = useRef(null); const appearanceScheme = useColorScheme() === "light" ? "light" : "dark"; const theme = props.theme ?? getPierreTerminalTheme(appearanceScheme); const { onInput, onResize } = props; const NativeTerminalSurfaceView = resolveNativeTerminalSurfaceView(); const hasNativeSurface = Boolean(NativeTerminalSurfaceView); useEffect(() => { terminalDebugLog("native:surface", { terminalKey: props.terminalKey, native: hasNativeSurface, bufferLen: props.buffer.length, isRunning: props.isRunning, }); }, [hasNativeSurface, props.buffer.length, props.isRunning, props.terminalKey]); const handleNativeInput = useCallback( (event: NativeSyntheticEvent) => { onInput(event.nativeEvent.data); }, [onInput], ); const handleNativeResize = useCallback( (event: NativeSyntheticEvent) => { onResize({ cols: event.nativeEvent.cols, rows: event.nativeEvent.rows, }); }, [onResize], ); // Reopen focus through React and forward input normally; this avoids a native focus-command bridge. useEffect(() => { if (!NativeTerminalSurfaceView || (props.keyboardFocusRequest ?? 0) <= 0) { return undefined; } keyboardInputRef.current?.blur(); const focusFrame = requestAnimationFrame(() => keyboardInputRef.current?.focus()); return () => cancelAnimationFrame(focusFrame); }, [NativeTerminalSurfaceView, props.keyboardFocusRequest]); const handleKeyboardInput = useCallback( (data: string) => { if (data.length > 0) { onInput(data); keyboardInputRef.current?.clear(); } }, [onInput], ); if (NativeTerminalSurfaceView) { return ( { if (event.nativeEvent.key === "Backspace") { onInput("\u007f"); } }} onSubmitEditing={() => onInput("\n")} /> ); } return ; });