1import type { ComponentType } from "react";
2import type { NativeSyntheticEvent, ViewProps } from "react-native";
3import { requireNativeView, requireOptionalNativeModule } from "expo";
4
5import { NativeViewResolutionError } from "../../native/nativeViewResolutionError";
6
7const NATIVE_TERMINAL_MODULE_NAME = "T3TerminalSurface";
8
9interface ExpoGlobalWithViewConfig {
10 readonly expo?: {
11 getViewConfig?: (moduleName: string, viewName?: string) => unknown;
12 };
13}
14
15interface TerminalInputEvent {
16 readonly data: string;
17}
18
19interface TerminalResizeEvent {
20 readonly cols: number;
21 readonly rows: number;
22}
23
24export interface NativeTerminalSurfaceProps extends ViewProps {
25 readonly appearanceScheme?: "light" | "dark";
26 readonly autoFocus?: boolean;
27 readonly readOnly?: boolean;
28 readonly focusRequest?: number;
29 readonly captureRequest?: number;
30 readonly onCapture?: (event: NativeSyntheticEvent<{ readonly text: string }>) => void;
31 readonly themeConfig?: string;
32 readonly backgroundColor?: string;
33 readonly foregroundColor?: string;
34 readonly mutedForegroundColor?: string;
35 readonly terminalKey: string;
36 readonly initialBuffer: string;
37 readonly fontSize: number;
38 readonly onInput?: (event: NativeSyntheticEvent<TerminalInputEvent>) => void;
39 readonly onResize?: (event: NativeSyntheticEvent<TerminalResizeEvent>) => void;
40}
41
42let cachedNativeTerminalSurfaceView: ComponentType<NativeTerminalSurfaceProps> | undefined;
43let nativeTerminalSurfaceViewResolutionFailed = false;
44
45function getExpoViewConfig(moduleName: string) {
46 return (globalThis as typeof globalThis & ExpoGlobalWithViewConfig).expo?.getViewConfig?.(
47 moduleName,
48 );
49}
50
51export function resolveNativeTerminalSurfaceView(): ComponentType<NativeTerminalSurfaceProps> | null {
52 if (cachedNativeTerminalSurfaceView) {
53 return cachedNativeTerminalSurfaceView;
54 }
55
56 if (nativeTerminalSurfaceViewResolutionFailed) {
57 return null;
58 }
59
60 if (getExpoViewConfig(NATIVE_TERMINAL_MODULE_NAME) == null) {
61 return null;
62 }
63
64 try {
65 cachedNativeTerminalSurfaceView = requireNativeView<NativeTerminalSurfaceProps>(
66 NATIVE_TERMINAL_MODULE_NAME,
67 );
68 } catch (cause) {
69 nativeTerminalSurfaceViewResolutionFailed = true;
70 console.error(
71 new NativeViewResolutionError({
72 nativeModuleName: NATIVE_TERMINAL_MODULE_NAME,
73 cause,
74 }),
75 );
76 return null;
77 }
78
79 return cachedNativeTerminalSurfaceView ?? null;
80}
81
82/**
83 * Revision of the native hardware-keyboard handling compiled into the installed binary,
84 * or `null` when the binary predates the revision constant (or the module is missing).
85 * Used in terminal debug logs to detect stale native builds.
86 */
87export function getNativeTerminalHardwareKeyRevision(): number | null {
88 try {
89 if (typeof requireOptionalNativeModule !== "function") {
90 return null;
91 }
92 const module = requireOptionalNativeModule<{ readonly hardwareKeyRevision?: number }>(
93 NATIVE_TERMINAL_MODULE_NAME,
94 );
95 return module?.hardwareKeyRevision ?? null;
96 } catch {
97 return null;
98 }
99}
100
101export function hasNativeTerminalSurface() {
102 return resolveNativeTerminalSurfaceView() !== null;
103}