@felipe-software/react-native-gpui 0.0.0-stage → 1.0.1
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.
- package/.reanimated-prebuilt/react-native-reanimated.mjs +29373 -0
- package/LICENSE +21 -0
- package/README.md +657 -2
- package/cli/bin.ts +379 -0
- package/cli/bundle.ts +32 -0
- package/cli/commands/do.ts +213 -0
- package/cli/commands/flow.ts +283 -0
- package/cli/commands/get.ts +584 -0
- package/cli/commands/shot.ts +312 -0
- package/cli/commands/trace.ts +293 -0
- package/cli/host.ts +714 -0
- package/cli/hotReload.ts +210 -0
- package/cli/run.ts +66 -0
- package/cli/selectors.ts +205 -0
- package/dist/Animated.d.ts +120 -0
- package/dist/ChatList.d.ts +28 -0
- package/dist/Dimensions.d.ts +31 -0
- package/dist/StyleSheet.d.ts +18 -0
- package/dist/Window.d.ts +33 -0
- package/dist/apis.d.ts +165 -0
- package/dist/app-state.d.ts +27 -0
- package/dist/chat-list-types.d.ts +136 -0
- package/dist/colors.d.ts +32 -0
- package/dist/commands.d.ts +230 -0
- package/dist/components.d.ts +419 -0
- package/dist/hotModules.d.ts +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.development.js +14876 -0
- package/dist/index.js +12 -0
- package/dist/keyboard.d.ts +140 -0
- package/dist/native-menu.d.ts +7 -0
- package/dist/platform-driver.d.ts +32 -0
- package/dist/platform-services.d.ts +35 -0
- package/dist/portal.d.ts +43 -0
- package/dist/raf.d.ts +4 -0
- package/dist/reanimated/colors-processor-shim.d.ts +10 -0
- package/dist/reanimated/reanimated-host.d.ts +10 -0
- package/dist/reanimated/seam.d.ts +34 -0
- package/dist/reanimated/ui-entry.d.ts +2 -0
- package/dist/reanimated/worklet-channel.d.ts +10 -0
- package/dist/reanimated/worklet-runtime.d.ts +466 -0
- package/dist/reanimated/worklets.d.ts +199 -0
- package/dist/reconciler.d.ts +102 -0
- package/dist/refresh.d.ts +1 -0
- package/dist/render.d.ts +37 -0
- package/dist/runtime.d.ts +235 -0
- package/dist/surfaces.d.ts +51 -0
- package/dist/types.d.ts +276 -0
- package/dist/ui-runtime.js +42276 -0
- package/dist/window-context.d.ts +7 -0
- package/dist/window-host.d.ts +41 -0
- package/dist/wire-delta.d.ts +20 -0
- package/package.json +176 -5
- package/scripts/bundle-app.mjs +88 -0
- package/scripts/conformance-utils.mjs +293 -0
- package/scripts/hot-update-alias-plugin.mjs +88 -0
- package/scripts/native-package-exports-plugin.mjs +235 -0
- package/scripts/pixel.mjs +152 -0
- package/scripts/png.mjs +198 -0
- package/scripts/react-refresh-swc.mjs +35 -0
- package/scripts/reanimated-bun-plugin.mjs +188 -0
- package/scripts/service-binary.d.mts +27 -0
- package/scripts/service-binary.mjs +100 -0
- package/src/reanimated/colors-processor-shim.ts +119 -0
- package/src/reanimated/reanimated-host.ts +69 -0
- package/src/reanimated/rngpui-reanimated-prebuilt.d.ts +7 -0
- package/src/reanimated/seam.ts +566 -0
- package/src/reanimated/ui-entry.ts +80 -0
- package/src/reanimated/worklet-channel.ts +64 -0
- package/src/reanimated/worklet-runtime.ts +2569 -0
- package/src/reanimated/worklets.ts +1334 -0
- package/src/refresh.ts +33 -0
package/README.md
CHANGED
|
@@ -1,3 +1,658 @@
|
|
|
1
|
-
#
|
|
1
|
+
# react-native-gpui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Write **React Native** components — render them **natively** with [GPUI](https://www.gpui.rs/) (Zed's GPU-accelerated Rust UI framework). No browser, no Electron, no mobile host. Your React tree is reconciled into a GPUI window on Metal (macOS) or Vulkan (Linux), with a real `react-reconciler` (so hooks, state, and effects all work) and a typed component surface that mirrors React Native.
|
|
4
|
+
|
|
5
|
+
It also supports the **"native shell, web content"** hybrid: embed a native `WebView` (WKWebView via `wry`) anywhere in the tree for web-grade content with native scroll + selection, while the rest of the UI stays native GPUI.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { AppRegistry, View, Text, Pressable, StyleSheet } from "@felipe-software/react-native-gpui";
|
|
9
|
+
import { useState } from "react";
|
|
10
|
+
|
|
11
|
+
function App() {
|
|
12
|
+
const [n, setN] = useState(0);
|
|
13
|
+
return (
|
|
14
|
+
<View style={s.root}>
|
|
15
|
+
<Text style={s.h1}>Hello from GPUI 👋</Text>
|
|
16
|
+
<Pressable style={s.btn} onPress={() => setN((c) => c + 1)}>
|
|
17
|
+
<Text style={s.btnText}>tapped {n}×</Text>
|
|
18
|
+
</Pressable>
|
|
19
|
+
</View>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
const s = StyleSheet.create({
|
|
23
|
+
root: { flex: 1, alignItems: "center", justifyContent: "center", backgroundColor: "#1e1e2e", gap: 16 },
|
|
24
|
+
h1: { color: "#e6e6ef", fontSize: 24, fontWeight: "700" },
|
|
25
|
+
btn: { backgroundColor: "#8a5cf6", paddingVertical: 10, paddingHorizontal: 18, borderRadius: 10 },
|
|
26
|
+
btnText: { color: "#fff", fontSize: 15, fontWeight: "600" },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
AppRegistry.registerComponent("App", () => App);
|
|
30
|
+
AppRegistry.runApplication("App");
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
bun add @felipe-software/react-native-gpui react
|
|
35
|
+
bun run rngpui run app.tsx
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
bun add @felipe-software/react-native-gpui react
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The package brings its own renderer: npm, Bun, pnpm and Yarn install the one platform
|
|
45
|
+
package that matches your machine from its `optionalDependencies`, so do not install with
|
|
46
|
+
`--omit=optional` / `--no-optional`. Your app's own `react` (19 or newer) is the only React in
|
|
47
|
+
the bundle; the library declares it as a peer dependency.
|
|
48
|
+
|
|
49
|
+
| Platform | Package | Built on |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| Linux x86_64 | `@felipe-software/react-native-gpui-linux-x64` | Ubuntu 24.04 (glibc 2.39 or newer) |
|
|
52
|
+
| Linux arm64 | `@felipe-software/react-native-gpui-linux-arm64` | Ubuntu 24.04 (glibc 2.39 or newer) |
|
|
53
|
+
| macOS Apple silicon | `@felipe-software/react-native-gpui-darwin-arm64` | macOS 14 |
|
|
54
|
+
|
|
55
|
+
Each platform package holds `rngpui-service` and `ui-runtime.js`; there is no
|
|
56
|
+
Intel macOS build. The CLI and the bundler run on [Bun](https://bun.sh) 1.3 or newer.
|
|
57
|
+
|
|
58
|
+
**Linux runtime.** The service loads WebKitGTK's JavaScriptCore at startup
|
|
59
|
+
(`libjavascriptcoregtk-4.1`, `-6.0` or `-4.0`, whichever is installed) and needs a Vulkan
|
|
60
|
+
driver, `libwayland-client` (Wayland), `libxkbcommon-x11` (X11) and ALSA. GNOME desktops
|
|
61
|
+
already ship them; elsewhere:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
# Debian 13 / Ubuntu 24.04
|
|
65
|
+
sudo apt install libjavascriptcoregtk-4.1-0 libvulkan1 mesa-vulkan-drivers libxkbcommon-x11-0 libasound2t64
|
|
66
|
+
# Fedora
|
|
67
|
+
sudo dnf install javascriptcoregtk4.1 vulkan-loader mesa-vulkan-drivers libxkbcommon-x11 alsa-lib
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Bundle and run
|
|
71
|
+
|
|
72
|
+
An app is one entry file. Bun bundles it into a single script and `rngpui-service` runs that
|
|
73
|
+
script in JavaScriptCore:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
rngpui run app.tsx # bundle (production) and open the window
|
|
77
|
+
rngpui run app.tsx --dev --size 1280x800 # development React, window size
|
|
78
|
+
rngpui bundle app.tsx dist/app.js # just write the bundle (add --dev for development)
|
|
79
|
+
rngpui run --bundle dist/app.js # run a bundle you built earlier
|
|
80
|
+
rngpui dev --launch app.tsx --root src # Fast Refresh while you edit
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Run them with `bun run rngpui …` or from a `package.json` script, which only use the copy in
|
|
84
|
+
your `node_modules`. The bundler
|
|
85
|
+
resolves packages from the entry's directory upward, so it uses your app's `node_modules`, and
|
|
86
|
+
both `@felipe-software/react-native-gpui` and `react-native` resolve to the same renderer.
|
|
87
|
+
Imports of the old unscoped name `react-native-gpui` bundle too; for TypeScript, map it with
|
|
88
|
+
`"paths": { "react-native-gpui": ["./node_modules/@felipe-software/react-native-gpui"] }` or
|
|
89
|
+
install the package under that name (`bun add react-native-gpui@npm:@felipe-software/react-native-gpui`).
|
|
90
|
+
|
|
91
|
+
`rngpui run` finds the renderer the same way every launcher here does: `RNGPUI_SERVICE` if set,
|
|
92
|
+
otherwise the installed platform package. A custom launcher can ask for the same path:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
import { resolveServiceBinary } from "@felipe-software/react-native-gpui/scripts/service-binary.mjs";
|
|
96
|
+
|
|
97
|
+
const { path } = resolveServiceBinary(); // spawn it with RNGPUI_BUNDLE=<your bundle>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`react-native-reanimated` runs from a copy prebuilt into the package, so do not install
|
|
101
|
+
reanimated yourself. Worklets in your own code (`useAnimatedStyle`, `withSpring`, …) need the
|
|
102
|
+
worklets Babel transform at bundle time:
|
|
103
|
+
`bun add -d @babel/core @babel/plugin-syntax-jsx @babel/preset-typescript react-native-worklets@0.8.1`.
|
|
104
|
+
|
|
105
|
+
## How it works
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
React tree ──► react-reconciler ──► serialized node tree ──► __rngpui_applyTree()
|
|
109
|
+
│
|
|
110
|
+
rngpui-service
|
|
111
|
+
Rust + GPUI + JavaScriptCore
|
|
112
|
+
│
|
|
113
|
+
onPress / onChangeText / onLayout / resize ◄──── __rngpui_onHostEvent()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The TypeScript side is bundled to a self-contained JS file and evaluated inside the native
|
|
117
|
+
service via system JavaScriptCore. React reconciles your components into a flat node tree; host globals pass
|
|
118
|
+
commits and UI events in memory between JavaScriptCore and GPUI. Stable element ids keep
|
|
119
|
+
native state (text-input contents, scroll offsets, web views) alive across
|
|
120
|
+
re-renders.
|
|
121
|
+
|
|
122
|
+
## Components
|
|
123
|
+
|
|
124
|
+
| Layout / display | Input / touch | Lists | Native content |
|
|
125
|
+
| --- | --- | --- | --- |
|
|
126
|
+
| `View` `Text` `ScrollView` `SafeAreaView` `KeyboardAvoidingView` | `TextInput` `Pressable` `TouchableOpacity` `TouchableHighlight` `TouchableWithoutFeedback` `Button` `Switch` | `FlatList` `SectionList` `ChatList` | `Image` `Svg` `WebView` |
|
|
127
|
+
|
|
128
|
+
APIs: `StyleSheet`, `Dimensions`, `useWindowDimensions`, `Platform`, `PixelRatio`, `useColorScheme`, `AppRegistry`, `createRoot` / `render`, [`Window`](#windows), `AppState` / `useAppState` / `useWindowFocused`, plus the [platform services](#platform-services) (`localStorage`, `Notifications`, `Sound`, `Processes`, `NativeClipboard`, `fetch`, `crypto`, `ImageCache`).
|
|
129
|
+
|
|
130
|
+
- **Real text input** (`TextInput`) is backed by [gpui-component](https://github.com/longbridge/gpui-component)'s editor: selection, IME, copy/paste, word motion, multiline. Resolved `style.color` and `placeholderTextColor` paint through the native editor. `onChange.nativeEvent` includes `isComposing` and `eventCount`, so controlled inputs can preserve native marked text while React commits catch up.
|
|
131
|
+
- **Text truncation** follows React Native's `numberOfLines`: one-line labels truncate with ellipsis, and multi-line labels are line-clamped.
|
|
132
|
+
- **Native portals** (`PortalProvider`, `Portal`, `PortalHost`) render overlay content at the matching host during GPUI serialization, so Tamagui Dialog/Popover/Sheet can avoid extra state-driven portal fallbacks.
|
|
133
|
+
- **RN-style measurement** (`measure`, `measureInWindow`, `measureLayout`) is exposed on host refs. Floating overlay libraries such as Tamagui's Popover/Select can measure trigger and content nodes without app-specific adapters.
|
|
134
|
+
- **RN Animated** runs JS-frame animations through React commits; animated styles resolve to plain GPUI styles each frame.
|
|
135
|
+
- **Native layout overrides** (`nativeLayoutKey`, `nativeResize`, `NativeLayout`) let narrow chrome interactions such as pane resizing mutate GPUI layout in the native service without a React commit per pointer frame.
|
|
136
|
+
- **Styling** matches RN: flexbox (Yoga semantics — `flex:1`, `%`, `auto`), `backgroundColor`, gradients via `backgroundImage`, `boxShadow` / iOS `shadow*` / `elevation`, `borderRadius`, `overflow: scroll`, `opacity`.
|
|
137
|
+
- **`Svg`** renders a monochrome icon tinted by `style.color`.
|
|
138
|
+
- **Keyboard**: the first `onKeyPress` listener of a window (outside text inputs) receives
|
|
139
|
+
every key typed in that window, and Ctrl, Shift, Alt or Meta pressed on its own arrives
|
|
140
|
+
as `Control`, `Shift`, `Alt` or `Meta`. `onKeyUp` reports releases the same way.
|
|
141
|
+
- **Middle click**: `onAuxClick` fires when the middle button is pressed and released on
|
|
142
|
+
the same view (`nativeEvent.button` is 1).
|
|
143
|
+
- **Press-drag scrubbing**: a press on a view with `onMouseUp`/`onClick`/`onPress` that
|
|
144
|
+
drags across another such view presses it (menus, list rows), scoped by
|
|
145
|
+
`nativeListGroup`. `nativePressDrag={false}` takes a view and its descendants out of it,
|
|
146
|
+
for drag handles whose child buttons must not fire when the drag crosses them.
|
|
147
|
+
- **Colors**: every CSS named color and `transparent` (for `color`, `backgroundColor` and
|
|
148
|
+
`borderColor`).
|
|
149
|
+
|
|
150
|
+
## Windows
|
|
151
|
+
|
|
152
|
+
`<Window>` opens a real OS window that this app's React tree renders into. The window is
|
|
153
|
+
another GPUI window inside the same service, drawn from the same JavaScript runtime and
|
|
154
|
+
reconciler, so its children share every context, store and piece of state with the rest
|
|
155
|
+
of the app, and live data (a chat feed, a socket) updates both windows at once.
|
|
156
|
+
Mounting opens the window; unmounting closes it.
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
import { useState } from "react";
|
|
160
|
+
import { Window, Text, TextInput, View } from "react-native-gpui";
|
|
161
|
+
|
|
162
|
+
function UserCardHost({ user }: { user: User }) {
|
|
163
|
+
const [pinned, setPinned] = useState(false);
|
|
164
|
+
const [open, setOpen] = useState(true);
|
|
165
|
+
if (!open) return null;
|
|
166
|
+
return (
|
|
167
|
+
<Window
|
|
168
|
+
kind="popup"
|
|
169
|
+
title={`${user.name}'s Usercard`}
|
|
170
|
+
width={420}
|
|
171
|
+
height={470}
|
|
172
|
+
position={{ x: -140, y: -94, relativeTo: "cursor" }}
|
|
173
|
+
closeOnBlur={!pinned}
|
|
174
|
+
onClose={({ reason }) => setOpen(false)}
|
|
175
|
+
>
|
|
176
|
+
<UserCard user={user} pinned={pinned} onPin={setPinned} />
|
|
177
|
+
</Window>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
| Prop | Meaning |
|
|
183
|
+
| --- | --- |
|
|
184
|
+
| `title`, `width`, `height` | Window title and content size (the platform frame, or the Linux client-side frame, comes on top). |
|
|
185
|
+
| `minWidth`, `minHeight` | Minimum content size. |
|
|
186
|
+
| `kind` | `"normal"` (default), `"dialog"` (transient for the main window, closes on Escape) or `"popup"` (frameless, closes on Escape and when it loses focus). |
|
|
187
|
+
| `position` | `"center"` on the main window (default for normal and dialog windows), `"cursor"` (default for popups), or `{ x, y, relativeTo }` for the content's top-left corner relative to the main window's content (`"parent"`, default), the screen, or the cursor. Windows are kept on the display; a cursor-placed window that would run off the bottom-right corner opens above the cursor. |
|
|
188
|
+
| `resizable`, `alwaysOnTop` | Default resizable except popups. `alwaysOnTop` keeps the window above others on macOS and X11. |
|
|
189
|
+
| `closeOnBlur`, `closeOnEscape` | Override the kind's defaults. |
|
|
190
|
+
| `onClose({ reason })` | The window asked to close: `"system"` (the OS or title-bar close button), `"escape"`, `"blur"` or `"error"` (it could not be created). Unmount the `<Window>` to close it; without `onClose` it closes itself. |
|
|
191
|
+
| `onFocus`, `onBlur`, `onResize({ width, height })` | Activation and size changes. |
|
|
192
|
+
| `testID` | Names the window for `rngpui --window`. |
|
|
193
|
+
|
|
194
|
+
Inside a window, `useWindowDimensions()` returns that window's size and
|
|
195
|
+
`useWindowFocused()` whether it has focus. A ref (`WindowHandle`) has `windowId` and
|
|
196
|
+
`focus()`.
|
|
197
|
+
|
|
198
|
+
- Each window has its own hit testing, focus, keyboard shortcuts (`AppCommands` bindings
|
|
199
|
+
fire in whichever window has focus), text inputs, `ChatList`s, theme, layout and frame
|
|
200
|
+
pacing: a window repaints only when its own tree, input or animation changes, and the
|
|
201
|
+
main window keeps its retained-layout fast path while secondary windows lay out in full.
|
|
202
|
+
- Wayland does not let a client place its toplevel windows, so `position` is ignored
|
|
203
|
+
there and the compositor places the window. X11 and macOS honor it. `alwaysOnTop` has
|
|
204
|
+
no Wayland protocol either.
|
|
205
|
+
- On GNOME (no server-side decorations) normal windows and dialogs get the client-side
|
|
206
|
+
title bar, border and resize edges, and its close button reports `reason: "system"`.
|
|
207
|
+
KWin draws server-side decorations. Popups are frameless everywhere.
|
|
208
|
+
- Closing the main window quits the app and closes every other window.
|
|
209
|
+
- `NativeWindow.open()` still starts a separate process with its own JavaScript state; use
|
|
210
|
+
`<Window>` for windows that belong to the running app.
|
|
211
|
+
|
|
212
|
+
`AppState.currentState` is `"active"` while one of the app's windows has focus and
|
|
213
|
+
`"inactive"` otherwise; `AppState.addEventListener("change" | "focus" | "blur", …)` and
|
|
214
|
+
`useAppState()` follow it. `AppState.addEventListener("windowFocus", ({ windowId, focused }))`
|
|
215
|
+
reports each window, the main window as id 0.
|
|
216
|
+
|
|
217
|
+
`bun run conformance:multi-window` opens a dialog, a window with a `ChatList` and a popup
|
|
218
|
+
in one service and checks per-window trees, pixels, real clicks and keys, shared state,
|
|
219
|
+
Escape / title-bar / focus-loss / unmount closing, and that closing leaks no nodes or
|
|
220
|
+
native state.
|
|
221
|
+
|
|
222
|
+
## Native surfaces
|
|
223
|
+
|
|
224
|
+
For app chrome that needs native glass or GPUI shader-backed effects, use the
|
|
225
|
+
surface components instead of baking app-specific behavior into the renderer:
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
import {
|
|
229
|
+
LiquidGlassBackground,
|
|
230
|
+
SmokeEffectSurface,
|
|
231
|
+
View,
|
|
232
|
+
} from "react-native-gpui";
|
|
233
|
+
|
|
234
|
+
export function Shell({ children }) {
|
|
235
|
+
return (
|
|
236
|
+
<View style={{ flex: 1, borderRadius: 32, overflow: "hidden" }}>
|
|
237
|
+
<LiquidGlassBackground
|
|
238
|
+
radius={32}
|
|
239
|
+
material="underWindowBackground"
|
|
240
|
+
glassVariant="controlCenter"
|
|
241
|
+
/>
|
|
242
|
+
<SmokeEffectSurface
|
|
243
|
+
pointerEvents="none"
|
|
244
|
+
radius={32}
|
|
245
|
+
alpha={0.9}
|
|
246
|
+
reach={0.28}
|
|
247
|
+
topClear={0.06}
|
|
248
|
+
/>
|
|
249
|
+
{children}
|
|
250
|
+
</View>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`LiquidGlassBackground` / `LiquidGlassView` wrap native `SystemView` glass.
|
|
256
|
+
`EffectSurface` / `SmokeEffectSurface` are ordinary React components that set
|
|
257
|
+
`backgroundImage` to renderer effects such as `smoke(...)`. Apps can tune these
|
|
258
|
+
from React props while the renderer owns only the generic native primitive.
|
|
259
|
+
|
|
260
|
+
## Dev loop
|
|
261
|
+
|
|
262
|
+
`rngpui dev --launch` launches one development renderer, watches every local
|
|
263
|
+
module in its dependency graph, and applies edits through React Fast Refresh.
|
|
264
|
+
Compatible component state, the native window, focus, scroll position, and
|
|
265
|
+
renderer-owned state stay in place. A transform or evaluation error is printed
|
|
266
|
+
without killing the running app, so the next valid edit can recover it.
|
|
267
|
+
|
|
268
|
+
```sh
|
|
269
|
+
rngpui dev --launch app/index.tsx --root app --root packages/ui
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
An owned external instance can use the same Fast Refresh path by providing its
|
|
273
|
+
control socket, build command, output bundle, and watched roots:
|
|
274
|
+
|
|
275
|
+
```sh
|
|
276
|
+
rngpui dev \
|
|
277
|
+
--socket /tmp/my-app.control.sock \
|
|
278
|
+
--bundle native-shell/.gpui/hot-update.js \
|
|
279
|
+
--build "NODE_ENV=development bun native-shell/scripts/bundle-app-gpui.mjs native-shell/app.tsx native-shell/.gpui/hot-update.js" \
|
|
280
|
+
--root app --root interface --root native-shell
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
There is one development reload contract. RNGPUI does not fall back to process
|
|
284
|
+
signals or remount the application when a Fast Refresh update fails.
|
|
285
|
+
|
|
286
|
+
`rngpui get windows` lists the windows a service shows (id, kind, title, `testID`,
|
|
287
|
+
bounds, nodes, frames painted). `--window <id | main | testID | kind | title>` aims
|
|
288
|
+
`get`, `do`, `trace` and `reshot` at one of them:
|
|
289
|
+
|
|
290
|
+
```sh
|
|
291
|
+
rngpui get windows --session /tmp/rngpui-cli-abc123
|
|
292
|
+
rngpui do tap dialog-increment --window fixture-dialog --session /tmp/rngpui-cli-abc123
|
|
293
|
+
rngpui reshot --window popup --session /tmp/rngpui-cli-abc123 --out /tmp/popup.png
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Native diff
|
|
297
|
+
|
|
298
|
+
`Diff` parses and virtualizes a unified patch in the renderer. Large patches do
|
|
299
|
+
not create one React node per line.
|
|
300
|
+
|
|
301
|
+
```tsx
|
|
302
|
+
<Diff
|
|
303
|
+
patch={unifiedPatch}
|
|
304
|
+
wordDiff
|
|
305
|
+
scroll
|
|
306
|
+
maxLines={20_000}
|
|
307
|
+
collapsedPaths={["generated/output.ts"]}
|
|
308
|
+
onToggleFile={({ nativeEvent }) => toggle(nativeEvent.value)}
|
|
309
|
+
onShowMore={() => setExpanded(true)}
|
|
310
|
+
onLineClick={({ nativeEvent }) => {
|
|
311
|
+
openLine(nativeEvent.value, nativeEvent.oldLine, nativeEvent.newLine)
|
|
312
|
+
}}
|
|
313
|
+
style={{ height: 520, fontFamily: "Menlo", fontSize: 12 }}
|
|
314
|
+
/>
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Native chat list
|
|
318
|
+
|
|
319
|
+
`ChatList` is a virtualized chat log drawn by the renderer, built for Twitch-style
|
|
320
|
+
clients. Messages never travel as props: the ref's methods stream them into the native
|
|
321
|
+
element, so an append costs only the new messages. The element keeps up to
|
|
322
|
+
`maxMessages` (default 1000, oldest dropped), lays out and paints only visible rows,
|
|
323
|
+
and caches each message's layout until its width, font, scale or content changes.
|
|
324
|
+
|
|
325
|
+
```tsx
|
|
326
|
+
import { useEffect, useRef } from "react";
|
|
327
|
+
import { ChatList, type ChatListHandle, type ChatListMessage } from "react-native-gpui";
|
|
328
|
+
|
|
329
|
+
const message: ChatListMessage = {
|
|
330
|
+
id: "msg-1",
|
|
331
|
+
timestamp: "12:00",
|
|
332
|
+
marker: "#ff2d2d",
|
|
333
|
+
elements: [
|
|
334
|
+
{ kind: "badge", name: "moderator", url: badgeUrl, width: 18, height: 18, tooltip: "Moderator" },
|
|
335
|
+
{ kind: "user", text: "alice:", login: "alice", color: "#ff4040" },
|
|
336
|
+
{ kind: "text", text: "hello world" },
|
|
337
|
+
{ kind: "emote", name: "Kappa", url: kappaUrl, width: 28, height: 28 },
|
|
338
|
+
],
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
function Channel({ chat }: { chat: ChatSource }) {
|
|
342
|
+
const list = useRef<ChatListHandle>(null);
|
|
343
|
+
useEffect(() => {
|
|
344
|
+
list.current?.prepend(chat.history());
|
|
345
|
+
return chat.onMessages((messages) => list.current?.append(messages));
|
|
346
|
+
}, [chat]);
|
|
347
|
+
return (
|
|
348
|
+
<ChatList
|
|
349
|
+
ref={list}
|
|
350
|
+
style={{ flex: 1, backgroundColor: "#191919" }}
|
|
351
|
+
fontSize={13}
|
|
352
|
+
showTimestamps
|
|
353
|
+
textColor="#ffffff"
|
|
354
|
+
mutedColor="#8c7f7f"
|
|
355
|
+
linkColor="#4286f4"
|
|
356
|
+
pauseOnHover
|
|
357
|
+
onPress={({ nativeEvent }) => {
|
|
358
|
+
if (nativeEvent.target === "user") openUserCard(nativeEvent.login);
|
|
359
|
+
if (nativeEvent.target === "link") openUrl(nativeEvent.url);
|
|
360
|
+
if (nativeEvent.button === "right") showMessageMenu(nativeEvent.messageId, nativeEvent.x, nativeEvent.y);
|
|
361
|
+
}}
|
|
362
|
+
onHover={({ nativeEvent }) => setTooltip(nativeEvent)}
|
|
363
|
+
onScrollState={({ nativeEvent }) => setMoreBelow(nativeEvent.atBottom ? 0 : nativeEvent.unseen)}
|
|
364
|
+
/>
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
When a moderator deletes the message, patch it in place (`hidden: true` removes it from
|
|
370
|
+
the layout instead, like Chatterino's "hide moderated messages"):
|
|
371
|
+
|
|
372
|
+
```tsx
|
|
373
|
+
list.current?.update([{ id: "msg-1", disabled: true }]);
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
- Message flags: `disabled` and `historical` fade a message, `hidden` takes it out of the
|
|
377
|
+
layout until a patch sets it back, `background` fills the row, `marker` colors its
|
|
378
|
+
scrollbar tick. A badge's `background` paints behind its image (FFZ's custom moderator
|
|
379
|
+
badge).
|
|
380
|
+
|
|
381
|
+
- Handle: `append`, `prepend` (history, oldest first), `update` (patches by `id`; pass
|
|
382
|
+
`""` to clear a string or color field), `clear`, `scrollToBottom`, `scrollToTop`,
|
|
383
|
+
`scrollToMessage(id, { align: "top" | "center" | "bottom" })` (default center),
|
|
384
|
+
`pageUp`, `pageDown` (one viewport, for the app's Page Up / Page Down keys).
|
|
385
|
+
- Elements are separated by one space; whitespace inside a text element separates words.
|
|
386
|
+
Set `trailingSpace: false` on an element to glue the next one to it (`@alice` + `,`).
|
|
387
|
+
- Layout follows Chatterino: 8/4 px margins, elements bottom-aligned per line, emotes
|
|
388
|
+
count as 4 px shorter than they are (they overhang 2 px above and below), badges are not
|
|
389
|
+
scaled by `emoteScale`, zero-width emotes are centered over the previous emote, and a
|
|
390
|
+
`reply` renders as one truncated line at 0.8× size with a curve.
|
|
391
|
+
- Images load through GPUI's image cache (http(s) URLs or file paths). Animated GIF and
|
|
392
|
+
WebP play while visible; the list schedules one repaint for the earliest frame change,
|
|
393
|
+
and an idle list without visible animation does not repaint.
|
|
394
|
+
- Scrolling: wheel/trackpad, a thin scrollbar that widens on hover and shows `marker`
|
|
395
|
+
colors. At the bottom the list follows new messages; scrolled up, appends, prepends
|
|
396
|
+
and trimming keep the rows you are reading in place, and `onScrollState` reports
|
|
397
|
+
`{ atBottom, paused, unseen }`. `pauseOnHover` freezes the view while the pointer is
|
|
398
|
+
over the list.
|
|
399
|
+
- Interaction: `onHover` fires with the user, link, emote or badge under the pointer, its
|
|
400
|
+
window-space bounds and the pointer position (`pointerX`, `pointerY`), or `null` when
|
|
401
|
+
it leaves, so tooltips can be React overlays. `onPress` fires for user/link/emote/badge,
|
|
402
|
+
or `message` for anything else, with the button, pointer position and modifier keys
|
|
403
|
+
(`shiftKey`, `ctrlKey`, `altKey`, `metaKey`). Left clicks fire on release unless the pointer moved more
|
|
404
|
+
than 15 px (that was a selection); right and middle clicks fire on press.
|
|
405
|
+
- Selection: drag selects across messages, double-click selects a word, triple-click
|
|
406
|
+
selects the message body. Ctrl+C (Cmd+C on macOS) copies it, including while a
|
|
407
|
+
`TextInput` is focused, unless that input holds a newer selection of its own. Emotes
|
|
408
|
+
copy as their names, badges copy nothing.
|
|
409
|
+
|
|
410
|
+
`bun run conformance:chat-list` checks wrapping, emote placement, timestamps, events,
|
|
411
|
+
selection, copy and scroll anchoring against real pixels and bounds.
|
|
412
|
+
`bun run perf:chat-list` measures 6 lists × 1000 messages at 20 messages/s.
|
|
413
|
+
|
|
414
|
+
Use `PerformanceHUD.setEnabled(true)` or `rngpui do hud on` for the native
|
|
415
|
+
frame/layout/paint overlay. `Renderer.getProvenance()` and
|
|
416
|
+
`rngpui get provenance` report the exact renderer, service, GPUI, JavaScriptCore, and
|
|
417
|
+
bundle identity.
|
|
418
|
+
|
|
419
|
+
## Native inspector
|
|
420
|
+
|
|
421
|
+
Enable the GPUI node inspector for any root:
|
|
422
|
+
|
|
423
|
+
```tsx
|
|
424
|
+
render(<App />, { devtools: { inspector: true } })
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
or set `RNGPUI_INSPECTOR=1` before launching. Hold Option while the window is
|
|
428
|
+
focused to highlight the native node under the pointer. Option-click copies a
|
|
429
|
+
compact snapshot with node id/type, bounds, event names, accessibility metadata,
|
|
430
|
+
text/value snippets, style facts, and the ancestor chain.
|
|
431
|
+
|
|
432
|
+
## The WebView hybrid
|
|
433
|
+
|
|
434
|
+
```tsx
|
|
435
|
+
<View style={{ flex: 1 }}>
|
|
436
|
+
{/* native shell: sidebar, header, composer … */}
|
|
437
|
+
<WebView style={{ flex: 1 }} source={{ html: myMarkdownHtml }} />
|
|
438
|
+
{/* …or source={{ uri: "https://…" }} */}
|
|
439
|
+
</View>
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
The `WebView` is a real native web view composited inside the GPUI window — you get web-grade rendering and **native scroll momentum + selection** for the content area while the shell stays native. Re-render it from React state like any other component.
|
|
443
|
+
|
|
444
|
+
### Messaging (two-way)
|
|
445
|
+
|
|
446
|
+
`source` re-renders the whole document, so for live content drive it with messages instead:
|
|
447
|
+
|
|
448
|
+
```tsx
|
|
449
|
+
const ref = useRef<WebViewHandle>(null)
|
|
450
|
+
|
|
451
|
+
<WebView
|
|
452
|
+
ref={ref}
|
|
453
|
+
source={{ html }}
|
|
454
|
+
onLoad={() => ref.current?.injectJavaScript("startStreaming()")}
|
|
455
|
+
onMessage={(e) => handle(e.nativeEvent.data)} // page → host
|
|
456
|
+
/>
|
|
457
|
+
|
|
458
|
+
// host → frame:
|
|
459
|
+
ref.current?.injectJavaScript("appendToken('…')") // run JS in the page (no reload)
|
|
460
|
+
ref.current?.postMessage("payload") // → a 'message' event in the page
|
|
461
|
+
ref.current?.reload()
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
Inside the page, talk back to the host with the React-Native bridge global (injected automatically):
|
|
465
|
+
|
|
466
|
+
```js
|
|
467
|
+
window.ReactNativeWebView.postMessage("hello from the page") // → onMessage
|
|
468
|
+
window.addEventListener("message", (e) => { /* host postMessage */ })
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
## Platform services
|
|
472
|
+
|
|
473
|
+
Desktop services an app needs beyond rendering. Each one runs off the JS thread in the
|
|
474
|
+
native service and resolves a promise when it is done.
|
|
475
|
+
|
|
476
|
+
```tsx
|
|
477
|
+
import { ImageCache, LocalStorage, Notifications, Processes, Sound, randomUUID } from "react-native-gpui";
|
|
478
|
+
|
|
479
|
+
localStorage.setItem("accounts", JSON.stringify(accounts)); // survives restarts
|
|
480
|
+
await LocalStorage.flush(); // optional: write now
|
|
481
|
+
|
|
482
|
+
const shown = await Notifications.show({ title: "forsen mentioned you", body: message });
|
|
483
|
+
await Sound.play(); // built-in "ping"
|
|
484
|
+
await Sound.play("/home/me/sounds/mention.wav", { volume: 0.6 });
|
|
485
|
+
await Sound.play("https://example.com/alert.wav");
|
|
486
|
+
|
|
487
|
+
const streaming = (await Processes.findRunning(["obs", "obs64.exe"])).length > 0;
|
|
488
|
+
const nonce = randomUUID();
|
|
489
|
+
|
|
490
|
+
const cached = await fetch(setUrl, { cache: "only-if-cached" }); // 504 when nothing is stored
|
|
491
|
+
const fresh = await fetch(setUrl, { cache: "reload" }); // network, then stored on disk
|
|
492
|
+
|
|
493
|
+
ImageCache.preconnect(["https://cdn.7tv.app"]); // open the HTTP/2 connection early
|
|
494
|
+
await ImageCache.prefetch(emoteUrls); // download + decode before the first paint
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
| API | Behavior |
|
|
498
|
+
| --- | --- |
|
|
499
|
+
| `localStorage` | The synchronous Web Storage API. Loaded once at startup and written back atomically (temp file + rename, 0600) 250ms after the last change, at most 2s late, and again on exit, `SIGTERM`/`SIGINT`/`SIGHUP`, reload, and before `NativeWindow.open()`. Changes from two processes of the same app merge per key. An unreadable file is moved aside as `local-storage.json.corrupt-<ms>` and the store starts empty. |
|
|
500
|
+
| `LocalStorage.flush(): Promise<void>` | Writes pending changes now. `LocalStorage.getPath()` returns the backing file (or `null`), `LocalStorage.isPersistent()` whether there is one. |
|
|
501
|
+
| `Notifications.show({ title, body? }): Promise<boolean>` | Linux: `org.freedesktop.Notifications` over D-Bus (body markup is escaped when the server renders markup). macOS: `osascript display notification`. Resolves `false` when nothing could show it; rejects without a title. |
|
|
502
|
+
| `Sound.play(source = "ping", { volume? }): Promise<void>` | `"ping"` is synthesized; otherwise a file path, `file://` URL, or `http(s)` URL (URL sounds are cached in memory). Sources are capped at 32 MiB and 8 sounds play at once. Decodes WAV (8/16/24/32-bit PCM, 32/64-bit float); other formats reject with the reason. Resolves when playback ends. |
|
|
503
|
+
| `Processes.findRunning(names): Promise<string[]>` | The given names that match a running process, compared case-insensitively against the process name and its executable's file name (Linux `/proc/*/comm` and `cmdline`, macOS `ps`). |
|
|
504
|
+
| `fetch` | `Response` has `status`, `statusText`, `ok` (2xx), `url` (after redirects), `redirected`, `headers` (`get`/`has`/`forEach`/`entries`/`keys`/`values`, iterable), `text()`, `json()`, `arrayBuffer()`, `clone()`. Bodies may be strings, `URLSearchParams`, `ArrayBuffer`, or any typed array/`DataView`; network errors reject with `TypeError`. |
|
|
505
|
+
| `fetch(url, { cache })` | GET requests use the disk cache below only when asked. `"default"` and `"no-store"` go to the network and store nothing. `"reload"` goes to the network and stores a 200. `"force-cache"` returns the stored copy at any age, else fetches and stores. `"only-if-cached"` returns the stored copy or a `504` response without touching the network. `"no-cache"` revalidates a stored copy with `If-None-Match`/`If-Modified-Since` and returns it on `304`. For stale-while-revalidate, start `only-if-cached` and `reload` together and apply each as it arrives. |
|
|
506
|
+
| `ImageCache.prefetch(urls): Promise<boolean>` | Downloads and decodes images into the same cache `<Image>` and `ChatList` read, so their first paint already has them. Resolves `true` when every URL loaded. |
|
|
507
|
+
| `ImageCache.preconnect(origins)` | Opens the HTTP/2 connection to each origin now (a `HEAD /`), so the first images from it skip the TCP and TLS handshakes. |
|
|
508
|
+
| `crypto.getRandomValues`, `crypto.randomUUID` | Backed by the OS random number generator; also exported as `getRandomValues()` / `randomUUID()`. |
|
|
509
|
+
| `NativeClipboard.read(): Promise<string>` | The clipboard's text (`""` when it holds none), read on the main thread. `NativeClipboard.setString(text)` writes it. |
|
|
510
|
+
| `<Image>` | Animated GIF and WebP play, redrawing only when a frame is due; still images never schedule redraws. Remote images load over HTTP/2 (one connection per host) and are kept in the disk cache, so a relaunch shows them without the network; a stored image past its `Cache-Control`/`Expires` lifetime is shown at once and revalidated in the background for the next launch. |
|
|
511
|
+
|
|
512
|
+
The data file lives at `$XDG_DATA_HOME/<app id>/local-storage.json` (default
|
|
513
|
+
`~/.local/share`) on Linux and `~/Library/Application Support/<app id>/local-storage.json` on
|
|
514
|
+
macOS. The app id is `RNGPUI_APP_ID`, else `RNGPUI_APP_NAME`, else `react-native-gpui`.
|
|
515
|
+
|
|
516
|
+
The HTTP disk cache lives in `$XDG_CACHE_HOME/<app id>/http/` (default `~/.cache`) on Linux and
|
|
517
|
+
`~/Library/Caches/<app id>/http/` on macOS, one file per URL named by its SHA-256. It holds 200
|
|
518
|
+
responses to GET requests without `Authorization`, `Cookie` or `Range`, skips `no-store`, writes
|
|
519
|
+
atomically (temp file + rename) so two processes of one app can share it, and evicts the least
|
|
520
|
+
recently used files once it passes 256 MB.
|
|
521
|
+
|
|
522
|
+
| Variable | Effect |
|
|
523
|
+
| --- | --- |
|
|
524
|
+
| `RNGPUI_STORAGE_DIR` | directory holding `local-storage.json`, overriding the data dir |
|
|
525
|
+
| `RNGPUI_CACHE_DIR` | directory replacing `<cache dir>/<app id>`; the HTTP cache goes in its `http/` folder |
|
|
526
|
+
| `RNGPUI_HTTP_CACHE=0` | turns the HTTP disk cache off |
|
|
527
|
+
| `RNGPUI_HTTP_CACHE_MAX_MB` | size cap of the HTTP disk cache (default 256) |
|
|
528
|
+
| `RNGPUI_HTTP_TRACE=1` | prints one `[rngpui-http]` JSON line per image request (`network`, `cache` or `revalidate`, timings, bytes) |
|
|
529
|
+
| `RNGPUI_TEST_MODE` | without `RNGPUI_STORAGE_DIR`, keeps `localStorage` in memory; without `RNGPUI_CACHE_DIR`, disables the HTTP disk cache; also suppresses real notifications and sound output |
|
|
530
|
+
| `RNGPUI_FAKE_NOTIFICATIONS=<file>` | append each notification as a JSON line instead of showing it |
|
|
531
|
+
| `RNGPUI_FAKE_SOUND_DIR=<dir>` | write each decoded sound as `sound-NNN.wav` instead of playing it |
|
|
532
|
+
| `RNGPUI_FAKE_CLIPBOARD=<file>` | `NativeClipboard` reads and writes this file instead of the clipboard (a headless Wayland window never receives the selection) |
|
|
533
|
+
|
|
534
|
+
## Building from source
|
|
535
|
+
|
|
536
|
+
Releases ship `rngpui-service` in the platform packages above. In a checkout, `npm run build`
|
|
537
|
+
writes it to `native/` instead, and every launcher uses the newer of `native/rngpui-service`
|
|
538
|
+
and `../rust/target/release/rngpui-service`, so a plain `cargo build` is picked up too. To
|
|
539
|
+
build it yourself you need a Rust toolchain. macOS supplies the JavaScriptCore framework; on Linux,
|
|
540
|
+
install a C/C++ toolchain and WebKitGTK's JavaScriptCore headers:
|
|
541
|
+
|
|
542
|
+
```sh
|
|
543
|
+
# Debian/Ubuntu (Fedora: gcc-c++ pkgconf javascriptcoregtk4.1-devel libxkbcommon-x11-devel alsa-lib-devel)
|
|
544
|
+
sudo apt install build-essential pkg-config libjavascriptcoregtk-4.1-dev \
|
|
545
|
+
libxkbcommon-x11-dev libasound2-dev libfontconfig-dev libfreetype-dev
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
```sh
|
|
549
|
+
npm run build # builds the Rust service, copies it into native/, then tsc → dist/
|
|
550
|
+
npm run build:rust # just the native binary
|
|
551
|
+
npm run typecheck
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
Point the runtime at a specific binary with `RNGPUI_SERVICE=/path/to/rngpui-service`.
|
|
555
|
+
Set `RNGPUI_INSPECTOR=1` to force-enable the native inspector for any app.
|
|
556
|
+
|
|
557
|
+
`npm run npm:pack -- --out /tmp/packs` builds the two tarballs a release would publish for
|
|
558
|
+
this machine (the main package and its platform package) so you can install them into a test
|
|
559
|
+
project: `bun add react /tmp/packs/felipe-software-react-native-gpui-<version>.tgz
|
|
560
|
+
/tmp/packs/felipe-software-react-native-gpui-<platform>-<version>.tgz`. Until that version is
|
|
561
|
+
on npm, the install warns that the other platform packages are missing (404); that is
|
|
562
|
+
expected.
|
|
563
|
+
|
|
564
|
+
## Releasing
|
|
565
|
+
|
|
566
|
+
Releases are automated, the same way as
|
|
567
|
+
[react-native-back-gesture](https://github.com/felipe-software/react-native-back-gesture):
|
|
568
|
+
|
|
569
|
+
1. Merge a pull request with [conventional commits](https://www.conventionalcommits.org/)
|
|
570
|
+
into `main`. `release-main.yaml` runs semantic-release, which turns `feat`, `fix`, `perf`,
|
|
571
|
+
`refactor` and `revert` commits into the next patch version and pushes a `v<version>` tag
|
|
572
|
+
and a GitHub release.
|
|
573
|
+
2. The tag starts `publish-npm.yml`. It builds `rngpui-service` on Linux x64, Linux arm64 and
|
|
574
|
+
macOS arm64 (ad-hoc signed with the JIT entitlement), strips its debug info, packs the
|
|
575
|
+
three platform packages and the main package at the tag's version, then publishes the
|
|
576
|
+
platform packages first and the main package last, with npm provenance. Versions already
|
|
577
|
+
on npm are skipped, so a failed run can simply be re-run, or started by hand with
|
|
578
|
+
**Run workflow** and the tag.
|
|
579
|
+
3. `test.yaml` checks pull requests: typecheck, the unit tests, and `cargo test` on Linux.
|
|
580
|
+
|
|
581
|
+
The version in `package.json` is not the release version: CI sets it, and the
|
|
582
|
+
`optionalDependencies` on the platform packages, from the tag.
|
|
583
|
+
|
|
584
|
+
**One-time setup** (maintainers):
|
|
585
|
+
|
|
586
|
+
1. **npm scope** `@felipe-software`, owned by the `felipe-software` npm account.
|
|
587
|
+
2. **GitHub environment** `npm` in this repository (Settings → Environments), holding:
|
|
588
|
+
- `SEMANTIC_RELEASE_GH_TOKEN`: a fine-grained personal access token for this repository with
|
|
589
|
+
*Contents: read and write*. It pushes the release tag and creates the GitHub release; a
|
|
590
|
+
tag pushed with the default `GITHUB_TOKEN` would not start `publish-npm.yml`.
|
|
591
|
+
- `NPM_TOKEN`, for the first release only: an npm granular access token with read and write
|
|
592
|
+
access to the `@felipe-software` scope. npm can only attach a trusted publisher to a
|
|
593
|
+
package that already exists, so the first publish of each package uses this token.
|
|
594
|
+
3. **Trusted publishers.** After the first release, open each of the four packages on
|
|
595
|
+
npmjs.com (`@felipe-software/react-native-gpui` and its `-linux-x64`, `-linux-arm64` and
|
|
596
|
+
`-darwin-arm64` packages) → Settings → Trusted publishing → GitHub Actions, with owner
|
|
597
|
+
`felipe-software`, repository `react-native-gpui`, workflow `publish-npm.yml` and
|
|
598
|
+
environment `npm`. Then delete the `NPM_TOKEN` secret. npm drops trusted publisher
|
|
599
|
+
settings that are never used, so set them up shortly before the next release.
|
|
600
|
+
|
|
601
|
+
## Examples
|
|
602
|
+
|
|
603
|
+
[chatterino-gpui](https://github.com/felipe-software/chatterino-gpui) is a full app built on
|
|
604
|
+
the npm package: a Chatterino-style Twitch chat client. The fixtures below run from a checkout:
|
|
605
|
+
|
|
606
|
+
```sh
|
|
607
|
+
bun run kitchen # the full component surface + self-validating layout
|
|
608
|
+
bun run conformance:animation # Animated frame progression fixture
|
|
609
|
+
bun run conformance:animation:diff # deterministic Animated PNG frame diff
|
|
610
|
+
bun run conformance:input # TextInput typing, Return submit, Shift+Return newline
|
|
611
|
+
bun run conformance:input-runtime # nonactivating focus, tab, IME, controlled state, latency
|
|
612
|
+
bun run conformance:portal # Portal overlay and TextInput-in-portal behavior
|
|
613
|
+
bun run conformance:native-layout # native layout override without React width state
|
|
614
|
+
bun run conformance:text-lines # visual fixture for Text numberOfLines
|
|
615
|
+
bun run conformance:chat-list # ChatList layout, events, selection, scroll anchoring
|
|
616
|
+
bun run perf:chat-list # ChatList CPU and frame numbers under load
|
|
617
|
+
bun run conformance:local-storage # localStorage across restarts, SIGTERM, corruption
|
|
618
|
+
bun run conformance:platform-services # fetch, crypto, notifications, sound, processes
|
|
619
|
+
bun run conformance:http-cache # fetch cache modes, image disk cache across launches
|
|
620
|
+
bun run conformance:animated-image # GIF/WebP frames advance, still images stay idle
|
|
621
|
+
bun run superconductor # native shell + WebView content hybrid
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
Visual regressions can use the built-in PNG comparator:
|
|
625
|
+
|
|
626
|
+
```sh
|
|
627
|
+
bun run pixel-diff before.png after.png --crop 0,0,540,300 --diff-out /tmp/rngpui-diff.png
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
## Status & platform
|
|
631
|
+
|
|
632
|
+
macOS (Metal) is the reference target. GPUI builds here with the `runtime_shaders` feature.
|
|
633
|
+
Prebuilt renderers exist for Linux x86_64, Linux arm64 and macOS on Apple silicon (see
|
|
634
|
+
[Install](#install)).
|
|
635
|
+
|
|
636
|
+
Linux runs on GPUI's Vulkan renderer under Wayland or X11, picked automatically: Wayland
|
|
637
|
+
when `WAYLAND_DISPLAY` answers, otherwise X11. It needs a Vulkan driver (Mesa or the
|
|
638
|
+
vendor driver) and WebKitGTK's JavaScriptCore at runtime (`libjavascriptcoregtk-4.1`,
|
|
639
|
+
`-6.0` or `-4.0`, whichever is installed). Compositors that leave decorations to the
|
|
640
|
+
app, like GNOME, get a client-side title bar with window controls and resize edges.
|
|
641
|
+
|
|
642
|
+
| Variable | Linux effect |
|
|
643
|
+
| --- | --- |
|
|
644
|
+
| `RNGPUI_LINUX_BACKEND=wayland\|x11` | force a display backend (default `auto`) |
|
|
645
|
+
| `RNGPUI_LINUX_DECORATIONS=client\|server` | force client- or server-side window decorations |
|
|
646
|
+
| `RNGPUI_WINDOW_TITLE`, `RNGPUI_APP_ID` | window title and Wayland app id (`.desktop` matching) |
|
|
647
|
+
| `RNGPUI_TRANSPARENT_WINDOW=1` | skip the opaque window backdrop |
|
|
648
|
+
| `RNGPUI_FRAME_HZ` | `requestAnimationFrame` rate (default 60) |
|
|
649
|
+
| `RNGPUI_JSC_LIBRARY` | load JavaScriptCore from a specific shared library |
|
|
650
|
+
| `RNGPUI_FRAME_CAPTURE=1` | let the dev CLI capture frames of an app it did not launch |
|
|
651
|
+
|
|
652
|
+
Not on Linux yet: `<WebView>` renders as an empty view, `<NativeButton>`/`<NativeTextInput>`
|
|
653
|
+
and system materials have no AppKit to wrap, in-app backdrop blur draws its tint without the
|
|
654
|
+
blur, and there is no accessibility tree or dock badge.
|
|
655
|
+
|
|
656
|
+
## License
|
|
657
|
+
|
|
658
|
+
MIT
|