@fressh/react-native-terminal 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +182 -0
  2. package/ReactNativeTerminal.podspec +82 -0
  3. package/android/CMakeLists.txt +76 -0
  4. package/android/build.gradle +121 -0
  5. package/android/fix-prefab.gradle +51 -0
  6. package/android/src/main/assets/fonts/DejaVuSansMono.ttf +0 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +180 -0
  8. package/android/src/main/java/com/margelo/nitro/fressh/HybridTerminal.kt +146 -0
  9. package/android/src/main/java/com/margelo/nitro/fressh/ReactNativeTerminalModule.kt +43 -0
  10. package/android/src/main/java/com/margelo/nitro/fressh/ReactNativeTerminalPackage.kt +32 -0
  11. package/android/src/main/jniLibs/arm64-v8a/libshim_uniffi.so +0 -0
  12. package/android/src/main/jniLibs/x86_64/libshim_uniffi.so +0 -0
  13. package/babel.config.js +12 -0
  14. package/cpp/README.md +14 -0
  15. package/cpp/generated/shim_uniffi.cpp +4246 -0
  16. package/cpp/generated/shim_uniffi.hpp +364 -0
  17. package/ios/FresshTerminalRenderABI.h +46 -0
  18. package/ios/HybridTerminal.swift +144 -0
  19. package/ios/ReactNativeTerminalUniffi.mm +77 -0
  20. package/ios/fetch-angle.sh +39 -0
  21. package/ios/fonts/DejaVuSansMono.ttf +0 -0
  22. package/libEGL.xcframework/Info.plist +44 -0
  23. package/libEGL.xcframework/ios-arm64/libEGL.framework/Info.plist +0 -0
  24. package/libEGL.xcframework/ios-arm64/libEGL.framework/libEGL +0 -0
  25. package/libEGL.xcframework/ios-arm64_x86_64-simulator/libEGL.framework/Info.plist +0 -0
  26. package/libEGL.xcframework/ios-arm64_x86_64-simulator/libEGL.framework/libEGL +0 -0
  27. package/libGLESv2.xcframework/Info.plist +44 -0
  28. package/libGLESv2.xcframework/ios-arm64/libGLESv2.framework/Info.plist +0 -0
  29. package/libGLESv2.xcframework/ios-arm64/libGLESv2.framework/libGLESv2 +0 -0
  30. package/libGLESv2.xcframework/ios-arm64_x86_64-simulator/libGLESv2.framework/Info.plist +0 -0
  31. package/libGLESv2.xcframework/ios-arm64_x86_64-simulator/libGLESv2.framework/libGLESv2 +0 -0
  32. package/nitro/Terminal.nitro.ts +40 -0
  33. package/nitrogen/generated/.gitattributes +1 -0
  34. package/nitrogen/generated/android/ReactNativeTerminal+autolinking.cmake +83 -0
  35. package/nitrogen/generated/android/ReactNativeTerminal+autolinking.gradle +27 -0
  36. package/nitrogen/generated/android/ReactNativeTerminalOnLoad.cpp +56 -0
  37. package/nitrogen/generated/android/ReactNativeTerminalOnLoad.hpp +34 -0
  38. package/nitrogen/generated/android/c++/JHybridTerminalSpec.cpp +76 -0
  39. package/nitrogen/generated/android/c++/JHybridTerminalSpec.hpp +68 -0
  40. package/nitrogen/generated/android/c++/views/JHybridTerminalStateUpdater.cpp +64 -0
  41. package/nitrogen/generated/android/c++/views/JHybridTerminalStateUpdater.hpp +49 -0
  42. package/nitrogen/generated/android/kotlin/com/margelo/nitro/fressh/HybridTerminalSpec.kt +69 -0
  43. package/nitrogen/generated/android/kotlin/com/margelo/nitro/fressh/ReactNativeTerminalOnLoad.kt +35 -0
  44. package/nitrogen/generated/android/kotlin/com/margelo/nitro/fressh/views/HybridTerminalManager.kt +80 -0
  45. package/nitrogen/generated/android/kotlin/com/margelo/nitro/fressh/views/HybridTerminalStateUpdater.kt +23 -0
  46. package/nitrogen/generated/ios/ReactNativeTerminal+autolinking.rb +62 -0
  47. package/nitrogen/generated/ios/ReactNativeTerminal-Swift-Cxx-Bridge.cpp +33 -0
  48. package/nitrogen/generated/ios/ReactNativeTerminal-Swift-Cxx-Bridge.hpp +57 -0
  49. package/nitrogen/generated/ios/ReactNativeTerminal-Swift-Cxx-Umbrella.hpp +43 -0
  50. package/nitrogen/generated/ios/ReactNativeTerminalAutolinking.mm +33 -0
  51. package/nitrogen/generated/ios/ReactNativeTerminalAutolinking.swift +26 -0
  52. package/nitrogen/generated/ios/c++/HybridTerminalSpecSwift.cpp +11 -0
  53. package/nitrogen/generated/ios/c++/HybridTerminalSpecSwift.hpp +96 -0
  54. package/nitrogen/generated/ios/c++/views/HybridTerminalComponent.mm +132 -0
  55. package/nitrogen/generated/ios/swift/HybridTerminalSpec.swift +57 -0
  56. package/nitrogen/generated/ios/swift/HybridTerminalSpec_cxx.swift +204 -0
  57. package/nitrogen/generated/shared/c++/HybridTerminalSpec.cpp +26 -0
  58. package/nitrogen/generated/shared/c++/HybridTerminalSpec.hpp +68 -0
  59. package/nitrogen/generated/shared/c++/views/HybridTerminalComponent.cpp +110 -0
  60. package/nitrogen/generated/shared/c++/views/HybridTerminalComponent.hpp +113 -0
  61. package/nitrogen/generated/shared/json/TerminalConfig.json +12 -0
  62. package/package.json +97 -0
  63. package/react-native.config.js +12 -0
  64. package/shim_uniffi.xcframework/Info.plist +44 -0
  65. package/shim_uniffi.xcframework/ios-arm64/libshim_uniffi.a +0 -0
  66. package/shim_uniffi.xcframework/ios-arm64_x86_64-simulator/libshim_uniffi.a +0 -0
  67. package/src/Terminal.tsx +135 -0
  68. package/src/generated/shim_uniffi-ffi.ts +320 -0
  69. package/src/generated/shim_uniffi.ts +2527 -0
  70. package/src/index.ts +52 -0
  71. package/src/ssh.ts +239 -0
  72. package/tsconfig.json +31 -0
  73. package/ubrn.config.yaml +24 -0
package/src/index.ts ADDED
@@ -0,0 +1,52 @@
1
+ // Public API for @fressh/react-native-terminal.
2
+ // Replaces @fressh/react-native-uniffi-russh + @fressh/react-native-xtermjs-webview.
3
+
4
+ export {
5
+ Terminal,
6
+ type CursorBlink,
7
+ type CursorStyle,
8
+ type TerminalComponentProps,
9
+ type TerminalRef,
10
+ type TerminalRenderConfig,
11
+ } from './Terminal';
12
+ export type { TerminalMethods, TerminalProps } from '../nitro/Terminal.nitro';
13
+
14
+ // Control plane (§10): connect/shell lifecycle + key helpers + the event stream.
15
+ export {
16
+ addFresshEventListener,
17
+ closePreviewTerm,
18
+ closeShell,
19
+ connect,
20
+ createPreviewTerm,
21
+ disconnect,
22
+ FresshEvent_Tags,
23
+ generateKeyPair,
24
+ KeyType,
25
+ resize,
26
+ respondToHostKey,
27
+ runCommand,
28
+ scroll,
29
+ Security,
30
+ selectionClear,
31
+ SelectionKind,
32
+ selectionStart,
33
+ selectionText,
34
+ selectionUpdate,
35
+ sendData,
36
+ SshConnectionProgressEvent,
37
+ SshError_Tags,
38
+ startShell,
39
+ TerminalType,
40
+ validatePrivateKey,
41
+ } from './ssh';
42
+ export type {
43
+ CommandResult,
44
+ ConnectionDetails,
45
+ ConnectionId,
46
+ FresshEvent,
47
+ FresshEventCallback,
48
+ FresshEventListener,
49
+ ServerPublicKeyInfo,
50
+ ShellId,
51
+ ShellOptions,
52
+ } from './ssh';
package/src/ssh.ts ADDED
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Control plane (§10): the JS-facing SSH API. Thin, typed wrappers over the
3
+ * generated uniffi shim (`./generated/shim_uniffi`), which forwards to
4
+ * `fressh-core`. Async + rare — the byte stream NEVER crosses here (it stays
5
+ * native, feeding the durable `Term`; the view reads it by `shellId`).
6
+ *
7
+ * Lifetime is the registry's (§7/§9): these calls pass string ids, never native
8
+ * handles, so a session survives JS GC / view unmount until an explicit
9
+ * `disconnect`/`closeShell`.
10
+ */
11
+
12
+ import { NativeModules, TurboModuleRegistry } from 'react-native';
13
+ import generatedModule, {
14
+ closePreview as _closePreview,
15
+ closeShell as _closeShell,
16
+ connect as _connect,
17
+ createPreview as _createPreview,
18
+ disconnect as _disconnect,
19
+ generateKeyPair as _generateKeyPair,
20
+ resize as _resize,
21
+ respondToHostKey as _respondToHostKey,
22
+ scroll as _scroll,
23
+ selectionClear as _selectionClear,
24
+ selectionStart as _selectionStart,
25
+ runCommand as _runCommand,
26
+ selectionText as _selectionText,
27
+ selectionUpdate as _selectionUpdate,
28
+ sendData as _sendData,
29
+ setEventListener as _setEventListener,
30
+ startShell as _startShell,
31
+ validatePrivateKey as _validatePrivateKey,
32
+ type CommandResult,
33
+ type ConnectionDetails,
34
+ type FresshEvent,
35
+ FresshEvent_Tags,
36
+ type FresshEventListener,
37
+ KeyType,
38
+ SelectionKind,
39
+ type ServerPublicKeyInfo,
40
+ Security,
41
+ type ShellOptions,
42
+ SshConnectionProgressEvent,
43
+ SshError_Tags,
44
+ TerminalType,
45
+ } from './generated/shim_uniffi';
46
+
47
+ // Re-export the generated enums/factories (values) the app needs to construct
48
+ // inputs and match events.
49
+ export {
50
+ FresshEvent_Tags,
51
+ KeyType,
52
+ Security,
53
+ SelectionKind,
54
+ SshConnectionProgressEvent,
55
+ SshError_Tags,
56
+ TerminalType,
57
+ };
58
+ // Records + the event union are plain object types — re-export as types.
59
+ export type {
60
+ CommandResult,
61
+ ConnectionDetails,
62
+ FresshEvent,
63
+ FresshEventListener,
64
+ ServerPublicKeyInfo,
65
+ ShellOptions,
66
+ };
67
+
68
+ export type ConnectionId = string;
69
+ export type ShellId = string;
70
+
71
+ // ─────────────────────────── native install ───────────────────────────
72
+
73
+ // The ubrn-generated bindings call `globalThis.NativeShimUniffi`, which only
74
+ // exists after the native installer runs. We trigger it once (idempotent) on
75
+ // first import — before any binding call. Without this, the first uniffi call
76
+ // throws "Cannot read property 'ubrn_uniffi_shim_uniffi_fn_func_*' of undefined".
77
+ //
78
+ // Platform note: the two installers are registered differently, so we look the
79
+ // module up both ways. Android's is a legacy bridge module (ReactPackage) → it
80
+ // surfaces on `NativeModules`. iOS's is a C++ TurboModule (ReactNativeTerminalUniffi.mm,
81
+ // method in `methodMap_`) → it surfaces on `TurboModuleRegistry`, NOT the legacy
82
+ // `NativeModules` proxy (which only sees ObjC-exported methods). Try the
83
+ // TurboModule path first, then fall back to NativeModules.
84
+ type Installer = { installRustCrate?: () => boolean };
85
+ let nativeInstalled = false;
86
+ function ensureNativeInstalled() {
87
+ if (nativeInstalled) return;
88
+ const mod =
89
+ (TurboModuleRegistry.get?.(
90
+ 'ReactNativeTerminalUniffi',
91
+ ) as Installer | null) ??
92
+ (NativeModules as { ReactNativeTerminalUniffi?: Installer })
93
+ .ReactNativeTerminalUniffi;
94
+ if (mod?.installRustCrate) {
95
+ mod.installRustCrate();
96
+ // installRustCrate sets up `globalThis.NativeShimUniffi`; only now can the
97
+ // generated binding talk to the dylib. `initialize()` validates the
98
+ // scaffolding contract/checksums AND registers the callback-interface
99
+ // vtable (`ubrn_..._init_callback_vtable_fressheventlistener`). Skipping it
100
+ // leaves the vtable cell null, so the first time Rust invokes
101
+ // `FresshEventListener.on_event` it panics with "Foreign pointer not set."
102
+ generatedModule.initialize();
103
+ nativeInstalled = true;
104
+ }
105
+ }
106
+
107
+ ensureNativeInstalled();
108
+
109
+ // ─────────────────────────── control plane ───────────────────────────
110
+
111
+ /** Connect + authenticate. Resolves to a `connectionId`. A `HostKeyPending`
112
+ * event fires mid-handshake — answer it with {@link respondToHostKey}. */
113
+ export const connect = (details: ConnectionDetails): Promise<ConnectionId> =>
114
+ _connect(details);
115
+
116
+ export const disconnect = (connectionId: ConnectionId): Promise<void> =>
117
+ _disconnect(connectionId);
118
+
119
+ export const respondToHostKey = (
120
+ connectionId: ConnectionId,
121
+ accept: boolean,
122
+ ): void => _respondToHostKey(connectionId, accept);
123
+
124
+ /** Open a PTY + shell. Resolves to a `shellId` — render it with
125
+ * `<Terminal shellId={shellId} />`. */
126
+ export const startShell = (
127
+ connectionId: ConnectionId,
128
+ options: ShellOptions,
129
+ ): Promise<ShellId> => _startShell(connectionId, options);
130
+
131
+ /** Run a one-off command on an existing connection without opening a PTY/shell.
132
+ * Resolves to `{ stdout, stderr, exitCode }`. Runs in the login/home dir — an
133
+ * `exec` channel does NOT inherit a live shell's cwd (use `cd … && …` if needed). */
134
+ export const runCommand = (
135
+ connectionId: ConnectionId,
136
+ command: string,
137
+ ): Promise<CommandResult> => _runCommand(connectionId, command);
138
+
139
+ /** Send user input (stdin). Also reachable on the render plane (the view forwards
140
+ * key/IME input straight to native), so most apps won't call this directly. */
141
+ export const sendData = (shellId: ShellId, data: ArrayBuffer): Promise<void> =>
142
+ _sendData(shellId, data);
143
+
144
+ export const resize = (
145
+ shellId: ShellId,
146
+ cols: number,
147
+ rows: number,
148
+ ): Promise<void> => _resize(shellId, cols, rows);
149
+
150
+ export const closeShell = (shellId: ShellId): Promise<void> =>
151
+ _closeShell(shellId);
152
+
153
+ // ─────────────────────────── preview (non-SSH `Term`) ─────────────────────────
154
+ // A `Term` driven by a canned byte snippet instead of an SSH channel — the
155
+ // foundation for the Terminal-settings live preview (and, later, an on-device
156
+ // local shell). It rides the SAME render plane as a real shell: render it with
157
+ // `<Terminal shellId={previewId} />` and the live `config` prop still reflows it.
158
+
159
+ /** Create a preview shell bound to `previewId`, fed `demo` bytes once. Synchronous
160
+ * (no network). Tear down with {@link closePreviewTerm} on unmount. */
161
+ export const createPreviewTerm = (
162
+ previewId: ShellId,
163
+ demo: ArrayBuffer,
164
+ ): void => _createPreview(previewId, demo);
165
+
166
+ /** Tear down a preview shell. Emits no `ShellClosed` event (its lifetime is owned
167
+ * by the screen that created it, not the app's session list). */
168
+ export const closePreviewTerm = (previewId: ShellId): Promise<void> =>
169
+ _closePreview(previewId);
170
+
171
+ export const generateKeyPair = (keyType: KeyType): string =>
172
+ _generateKeyPair(keyType);
173
+
174
+ export const validatePrivateKey = (pem: string): string =>
175
+ _validatePrivateKey(pem);
176
+
177
+ // ─────────────────────── touch interaction (scroll + selection) ──────────────
178
+ // Touch gestures live in JS (cross-platform), but the terminal logic lives in
179
+ // Rust keyed by `shellId` — these wrappers call it. Coordinates are PHYSICAL px
180
+ // (surface-relative); the caller scales logical pt by the device pixel ratio,
181
+ // matching how `<Terminal config>` already scales font/padding.
182
+
183
+ /** Scroll by `deltaPx` physical px (positive = finger dragged down = older
184
+ * content). Honors the app's mouse/alt-screen mode; else moves scrollback. */
185
+ export const scroll = (shellId: ShellId, deltaPx: number): Promise<void> =>
186
+ _scroll(shellId, deltaPx);
187
+
188
+ /** Begin a selection at a touch point (physical px). */
189
+ export const selectionStart = (
190
+ shellId: ShellId,
191
+ x: number,
192
+ y: number,
193
+ kind: SelectionKind = SelectionKind.Word,
194
+ ): void => _selectionStart(shellId, x, y, kind);
195
+
196
+ /** Extend the active selection to a touch point (physical px). */
197
+ export const selectionUpdate = (shellId: ShellId, x: number, y: number): void =>
198
+ _selectionUpdate(shellId, x, y);
199
+
200
+ /** Clear any active selection. */
201
+ export const selectionClear = (shellId: ShellId): void =>
202
+ _selectionClear(shellId);
203
+
204
+ /** The currently selected text, if any. */
205
+ export const selectionText = (shellId: ShellId): string | undefined =>
206
+ _selectionText(shellId);
207
+
208
+ // ─────────────────────────── event plane (one-way) ───────────────────────────
209
+
210
+ export type FresshEventCallback = (event: FresshEvent) => void;
211
+
212
+ // uniffi accepts ONE listener; register a single fan-out and dispatch to many JS
213
+ // subscribers so the app can have independent listeners (connect flow, per-shell
214
+ // close handlers, host-key prompts) without clobbering one another.
215
+ const subscribers = new Set<FresshEventCallback>();
216
+ let installed = false;
217
+
218
+ function ensureInstalled() {
219
+ if (installed) return;
220
+ ensureNativeInstalled();
221
+ installed = true;
222
+ const listener: FresshEventListener = {
223
+ onEvent(event) {
224
+ // Snapshot so a callback that (un)subscribes mid-dispatch is safe.
225
+ for (const cb of Array.from(subscribers)) cb(event);
226
+ },
227
+ };
228
+ _setEventListener(listener);
229
+ }
230
+
231
+ /** Subscribe to the low-frequency event stream (progress, host-key, closed).
232
+ * Returns an unsubscribe fn. */
233
+ export function addFresshEventListener(cb: FresshEventCallback): () => void {
234
+ ensureInstalled();
235
+ subscribers.add(cb);
236
+ return () => {
237
+ subscribers.delete(cb);
238
+ };
239
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "types": ["node"],
5
+ "rootDir": ".",
6
+ "paths": {
7
+ "@fressh/react-native-terminal": ["./src/index"]
8
+ },
9
+ "allowUnreachableCode": false,
10
+ "allowUnusedLabels": false,
11
+ "customConditions": ["react-native-strict-api"],
12
+ "esModuleInterop": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "jsx": "react-jsx",
15
+ "lib": ["ESNext"],
16
+ "module": "ESNext",
17
+ "moduleResolution": "bundler",
18
+ "noEmit": true,
19
+ "noFallthroughCasesInSwitch": true,
20
+ "noImplicitReturns": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "resolveJsonModule": true,
23
+ "skipLibCheck": true,
24
+ "strict": true,
25
+ "target": "ESNext",
26
+ "verbatimModuleSyntax": true,
27
+ "noUnusedLocals": false,
28
+ "noUnusedParameters": false
29
+ },
30
+ "include": ["src/**/*.ts", "src/**/*.tsx", "nitro/**/*.ts"]
31
+ }
@@ -0,0 +1,24 @@
1
+ # uniffi-bindgen-react-native config for the SSH control-plane shim.
2
+ #
3
+ # This package is unusual: it hosts BOTH a Nitro HybridView (the render plane)
4
+ # AND the uniffi control plane in ONE `.so` (§8), so we do NOT let ubrn own the
5
+ # android build files (Nitro's CMakeLists/build.gradle are hand-authored). The
6
+ # `ubrn:generate` script runs `generate jsi bindings --library` against the
7
+ # cargo-built `libshim_uniffi.a` to emit only the TS + C++ bindings; the umbrella
8
+ # CMakeLists includes that C++ and links the staticlib (shared registry, §8).
9
+ rust:
10
+ directory: rust
11
+ manifestPath: shim-uniffi/Cargo.toml
12
+
13
+ bindings:
14
+ cpp: cpp/generated
15
+ ts: src/generated
16
+
17
+ android:
18
+ jniLibs: android/src/main/jniLibs
19
+ # Linked as a static archive into the single package `.so` (shared registry
20
+ # statics, §8) — NOT a standalone shared lib.
21
+ useSharedLibrary: false
22
+ targets:
23
+ - arm64-v8a
24
+ - x86_64