react-x11 2.48.0 → 2.50.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.
- package/README.md +9 -8
- package/package.json +7 -4
- package/src/ClickToComponent.js +11 -132
- package/src/Reconciler.js +4 -1
- package/src/cli/dev.js +578 -0
- package/src/cli/errorwindow.js +368 -0
- package/src/cli/index.js +81 -0
- package/src/cocoa/app.js +12 -3
- package/src/cocoa/presenter.js +3 -1
- package/src/cocoa/promotion.js +10 -11
- package/src/cocoa/relaunch.js +110 -13
- package/src/currentcolor.js +28 -0
- package/src/decorations.js +21 -9
- package/src/editor.js +151 -0
- package/src/errors.js +8 -0
- package/src/foreignnodes.js +8 -1
- package/src/gloverlay.js +2 -1
- package/src/node.d.ts +5 -0
- package/src/nodes/animation.js +31 -3
- package/src/nodes/boxpaint.js +34 -8
- package/src/nodes/cascade.js +50 -3
- package/src/nodes/damage.js +2 -0
- package/src/nodes/layout.js +9 -17
- package/src/nodes/moveblit.js +5 -5
- package/src/nodes/node.js +8 -0
- package/src/nodes/paint.js +4 -2
- package/src/nodes/queries.js +46 -34
- package/src/nodes/scrollblit.js +10 -7
- package/src/nodes/styling.js +176 -21
- package/src/nodes/window/capabilities.js +1 -1
- package/src/nodes/window/floors.js +63 -18
- package/src/nodes/window/size.js +94 -19
- package/src/refresh/loader.d.ts +16 -0
- package/src/refresh/loader.js +84 -14
- package/src/refresh/register.js +8 -5
- package/src/shorthands.js +1266 -0
- package/src/styles.js +370 -131
- package/src/types/style.d.ts +163 -39
package/src/cocoa/relaunch.js
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
// name: it runs before the entry is even loaded, and skips the checks.
|
|
23
23
|
import fs from 'node:fs';
|
|
24
24
|
import { createRequire } from 'node:module';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
25
27
|
import { isMainThread, SHARE_ENV, Worker } from 'node:worker_threads';
|
|
26
28
|
|
|
27
29
|
import { loadNative } from './native.js';
|
|
@@ -156,6 +158,15 @@ const ENTRY_POINT = new Set([
|
|
|
156
158
|
// ts-node/esm`, `--experimental-loader ./hooks.mjs`.
|
|
157
159
|
const LOADER_FLAG = /^--(?:import|loader|experimental-loader)(?:=|$)/;
|
|
158
160
|
|
|
161
|
+
// The module Bun runs an entry from (`startedRunning`): one it writes
|
|
162
|
+
// itself, which imports the entry (measured, Bun 1.4).
|
|
163
|
+
const BUN_MAIN = 'bun:main';
|
|
164
|
+
|
|
165
|
+
// What Bun parses an entry as, by its extension: a .ts file may hold a
|
|
166
|
+
// `<T>x` cast, which TSX reads as an element, and JSX is allowed in a .js
|
|
167
|
+
// file, where the JSX reader takes any script a plain one would.
|
|
168
|
+
const SCAN_LOADER = { '.tsx': 'tsx', '.ts': 'ts', '.mts': 'ts', '.cts': 'ts' };
|
|
169
|
+
|
|
159
170
|
/**
|
|
160
171
|
* Why this import should not move the app onto a worker, or null when it
|
|
161
172
|
* should. A pure answer over what the process says about itself, so that
|
|
@@ -278,14 +289,14 @@ function compiledExecutable(bun) {
|
|
|
278
289
|
* and later evaluate an `import()` the entry makes before the loop's first
|
|
279
290
|
* turn, so an app that made one moved and ran its entry twice.
|
|
280
291
|
*
|
|
281
|
-
* Bun reports
|
|
282
|
-
*
|
|
283
|
-
*
|
|
292
|
+
* Bun reports no loop there, its `nodeTiming` a stand-in at 1 once
|
|
293
|
+
* `node:perf_hooks` is loaded, and leaves no frame below a module it
|
|
294
|
+
* evaluates, so it is read its own way (`bunStartedRunning`).
|
|
284
295
|
*/
|
|
285
296
|
function startedRunning(bun, moduleUrl, entry) {
|
|
286
297
|
if (!bun) return !evaluatedFromEntryPoint();
|
|
287
298
|
try {
|
|
288
|
-
return
|
|
299
|
+
return bunStartedRunning(bun, moduleUrl, entry);
|
|
289
300
|
} catch {
|
|
290
301
|
return false;
|
|
291
302
|
}
|
|
@@ -293,13 +304,106 @@ function startedRunning(bun, moduleUrl, entry) {
|
|
|
293
304
|
|
|
294
305
|
/** Whether this stack, async frames and all, comes from `ENTRY_POINT`. */
|
|
295
306
|
function evaluatedFromEntryPoint() {
|
|
307
|
+
const sites = callSites();
|
|
308
|
+
if (!sites) return false;
|
|
309
|
+
for (const site of sites) {
|
|
310
|
+
const file = site.getFileName() ?? '';
|
|
311
|
+
if (ENTRY_POINT.has(file)) return true;
|
|
312
|
+
// the app's own code awaits this import, so it has run already
|
|
313
|
+
if (site.isAsync() && !file.startsWith('node:')) return false;
|
|
314
|
+
}
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* `startedRunning` in Bun, read from three things it shows of the entry's
|
|
320
|
+
* run (each measured, Bun 1.4).
|
|
321
|
+
*
|
|
322
|
+
* Its module registry. Bun runs the entry from a module it writes itself,
|
|
323
|
+
* `bun:main`, which imports it, and `require.cache` lists an ES module once
|
|
324
|
+
* it has been evaluated: `bun:main` is there once the entry has finished,
|
|
325
|
+
* top-level awaits and all, and an import made after that is late. Before
|
|
326
|
+
* then the entry's own record says nothing either way. Bun marks a CommonJS
|
|
327
|
+
* module `loaded` before its body runs, which read as late from a CommonJS
|
|
328
|
+
* entry's first line, so it never moved; and an ES module has no record
|
|
329
|
+
* until it is evaluated, which read as not started for an entry awaiting
|
|
330
|
+
* its `import()` of react-x11, so it moved and ran a second time on the
|
|
331
|
+
* worker.
|
|
332
|
+
*
|
|
333
|
+
* The stack. Bun's loader evaluates an ES module's imports with no frame
|
|
334
|
+
* below the module, so a frame below react-x11 is code that loaded it
|
|
335
|
+
* synchronously. A CommonJS entry is in `require.cache` from its first line,
|
|
336
|
+
* and on its first run its own wrapper is the outermost frame. Bun runs the
|
|
337
|
+
* nextTicks and microtasks that first run queues before it evaluates
|
|
338
|
+
* `bun:main`: a callback there in another module reads as late, and one in
|
|
339
|
+
* the entry itself as its first run, unless in strict code it ends by
|
|
340
|
+
* returning its `require()`, which JavaScriptCore makes a tail call that
|
|
341
|
+
* leaves it no frame. For an ES module entry, a frame below react-x11 is a
|
|
342
|
+
* CommonJS module the entry imports, on its first run, or a `require()` in
|
|
343
|
+
* a module's body, and a bundle carries react-x11 inside the entry itself:
|
|
344
|
+
* each of them the start.
|
|
345
|
+
*
|
|
346
|
+
* The entry's static imports. An ES module's body runs once every module it
|
|
347
|
+
* imports statically has been evaluated: while one has not, the entry has
|
|
348
|
+
* not run a line and react-x11 is part of its graph, an `import()` that one
|
|
349
|
+
* of those modules awaits included; once all have, the entry's body is
|
|
350
|
+
* running or waiting on a top-level await, and asked for react-x11 itself.
|
|
351
|
+
* Read with Bun's own scanner and resolver, and what they cannot answer
|
|
352
|
+
* throws, which reads as the start.
|
|
353
|
+
*/
|
|
354
|
+
function bunStartedRunning(bun, moduleUrl, entry) {
|
|
355
|
+
const cache = createRequire(entry).cache;
|
|
356
|
+
// the entry has finished
|
|
357
|
+
if (BUN_MAIN in cache) return true;
|
|
358
|
+
const outer = outermostFile();
|
|
359
|
+
// a CommonJS entry: late unless on its first run
|
|
360
|
+
if (entry in cache) return outer !== undefined && outer !== entry;
|
|
361
|
+
let own = null;
|
|
362
|
+
try {
|
|
363
|
+
own = fileURLToPath(moduleUrl);
|
|
364
|
+
} catch {
|
|
365
|
+
// a CommonJS bundle has no import.meta.url
|
|
366
|
+
}
|
|
367
|
+
// react-x11 inside the entry, or loaded by code below it on the stack
|
|
368
|
+
if (own === entry || (outer !== undefined && outer !== own)) return false;
|
|
369
|
+
// reached by an import: late once the entry's body has started
|
|
370
|
+
const { imports } = new bun.Transpiler({
|
|
371
|
+
loader: SCAN_LOADER[path.extname(entry)] ?? 'jsx',
|
|
372
|
+
}).scan(fs.readFileSync(entry, 'utf8'));
|
|
373
|
+
const dir = path.dirname(entry);
|
|
374
|
+
return imports.every(
|
|
375
|
+
({ kind, path: specifier }) =>
|
|
376
|
+
kind !== 'import-statement' || bun.resolveSync(specifier, dir) in cache,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* The file of the outermost frame on this stack that is not native code,
|
|
382
|
+
* or undefined where the stack cannot be read.
|
|
383
|
+
*/
|
|
384
|
+
function outermostFile() {
|
|
385
|
+
const sites = callSites();
|
|
386
|
+
if (!sites) return undefined;
|
|
387
|
+
for (let i = sites.length - 1; i >= 0; i--) {
|
|
388
|
+
if (!sites[i].isNative()) return sites[i].getFileName() ?? undefined;
|
|
389
|
+
}
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* This stack as call sites, every frame of it, or null where it cannot be
|
|
395
|
+
* read as call sites — an `Error` the app has frozen, say. Raises the stack
|
|
396
|
+
* trace limit and sets a formatter for as long as it takes, and puts back
|
|
397
|
+
* the app's own.
|
|
398
|
+
*/
|
|
399
|
+
function callSites() {
|
|
296
400
|
const { prepareStackTrace, stackTraceLimit } = Error;
|
|
297
401
|
let sites;
|
|
298
402
|
try {
|
|
299
403
|
// Reflect.set: an Error the app has frozen is not a reason to throw
|
|
300
404
|
// from its import of react-x11
|
|
301
405
|
Reflect.set(Error, 'stackTraceLimit', Infinity);
|
|
302
|
-
Reflect.set(Error, 'prepareStackTrace', (_,
|
|
406
|
+
Reflect.set(Error, 'prepareStackTrace', (_, frames) => frames);
|
|
303
407
|
const held = {};
|
|
304
408
|
Error.captureStackTrace(held);
|
|
305
409
|
sites = held.stack;
|
|
@@ -307,14 +411,7 @@ function evaluatedFromEntryPoint() {
|
|
|
307
411
|
Reflect.set(Error, 'prepareStackTrace', prepareStackTrace);
|
|
308
412
|
Reflect.set(Error, 'stackTraceLimit', stackTraceLimit);
|
|
309
413
|
}
|
|
310
|
-
|
|
311
|
-
for (const site of sites) {
|
|
312
|
-
const file = site.getFileName() ?? '';
|
|
313
|
-
if (ENTRY_POINT.has(file)) return true;
|
|
314
|
-
// the app's own code awaits this import, so it has run already
|
|
315
|
-
if (site.isAsync() && !file.startsWith('node:')) return false;
|
|
316
|
-
}
|
|
317
|
-
return false;
|
|
414
|
+
return Array.isArray(sites) ? sites : null;
|
|
318
415
|
}
|
|
319
416
|
|
|
320
417
|
/**
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// CSS's `currentColor`: the colour keyword that is not a colour yet.
|
|
2
|
+
//
|
|
3
|
+
// A leaf, like decorations.js, because every layer that reads a colour asks
|
|
4
|
+
// about it — the grammar (styles.js, shorthands.js, decorations.js) to let
|
|
5
|
+
// it through, and the paint (nodes/boxpaint.js and every backend's reader
|
|
6
|
+
// of a style colour) to resolve it.
|
|
7
|
+
//
|
|
8
|
+
// It stays the keyword in `node.style` — CSS's computed value — and becomes
|
|
9
|
+
// a colour only where something is about to paint it, against the node's
|
|
10
|
+
// resolved ink (`Node.resolveColor`). Resolving it once in the style funnel
|
|
11
|
+
// instead would bake the ink into the style object, which no inherited
|
|
12
|
+
// change reaches: a `:hover { color }` on a row moves the ink of every node
|
|
13
|
+
// under it without restyling any of them.
|
|
14
|
+
|
|
15
|
+
/** `currentColor`, spelled any way CSS accepts — its keywords are ASCII
|
|
16
|
+
* case-insensitive — and with the whitespace a style value may carry. */
|
|
17
|
+
const KEYWORD = /^\s*currentcolor\s*$/i;
|
|
18
|
+
const MENTION = /currentcolor/i;
|
|
19
|
+
|
|
20
|
+
/** Whether `value` is the keyword. Asked per colour per paint, so a value
|
|
21
|
+
* too short to be it is answered without the match. */
|
|
22
|
+
export const isCurrentColor = (value) =>
|
|
23
|
+
typeof value === 'string' && value.length >= 12 && KEYWORD.test(value);
|
|
24
|
+
|
|
25
|
+
/** Whether a value made of several — a gradient, a shadow list — names the
|
|
26
|
+
* keyword anywhere in it. */
|
|
27
|
+
export const mentionsCurrentColor = (value) =>
|
|
28
|
+
typeof value === 'string' && value.length >= 12 && MENTION.test(value);
|
package/src/decorations.js
CHANGED
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
import { cssColorStraight } from 'ntk/color';
|
|
38
38
|
import { shadowReach, shadowSigma } from 'ntk/shadow-math';
|
|
39
39
|
|
|
40
|
+
import { isCurrentColor } from './currentcolor.js';
|
|
41
|
+
|
|
40
42
|
/** A number, with or without the `px` CSS wants and this vocabulary does not. */
|
|
41
43
|
const LENGTH = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:px)?$/i;
|
|
42
44
|
/** …and the same thing as a percentage, which colour stops also take. */
|
|
@@ -50,9 +52,11 @@ const HORIZONTAL = new Set(['left', 'right']);
|
|
|
50
52
|
/**
|
|
51
53
|
* Split on `,` or on whitespace, but only at the top level: a stop is
|
|
52
54
|
* `rgba(0, 0, 0, .4)` as often as it is `#333`, and every naive split on
|
|
53
|
-
* this vocabulary gets that wrong.
|
|
55
|
+
* this vocabulary gets that wrong. The shorthands split by it too
|
|
56
|
+
* (src/shorthands.js) — `border: '1px solid rgba(0, 0, 0, .1)'` is three
|
|
57
|
+
* values, not six.
|
|
54
58
|
*/
|
|
55
|
-
function splitTop(text, byComma) {
|
|
59
|
+
export function splitTop(text, byComma) {
|
|
56
60
|
const out = [];
|
|
57
61
|
let depth = 0;
|
|
58
62
|
let start = 0;
|
|
@@ -207,14 +211,16 @@ function readStop(part, value) {
|
|
|
207
211
|
return { color: stopColor(part, value), position: null };
|
|
208
212
|
}
|
|
209
213
|
|
|
210
|
-
/** A stop's colour, which RENDER is handed as written
|
|
211
|
-
*
|
|
214
|
+
/** A stop's colour, which RENDER is handed as written once a `currentColor`
|
|
215
|
+
* is resolved (`_backgroundGradient`) — and ntk throws on one it cannot
|
|
216
|
+
* parse from inside the frame, where nothing can catch it. */
|
|
212
217
|
function stopColor(color, value) {
|
|
213
218
|
if (isColorOrToken(color)) return color;
|
|
214
219
|
throw new Error(
|
|
215
220
|
`react-x11: backgroundImage ${JSON.stringify(value)} has a colour stop ` +
|
|
216
221
|
`that is not a colour ("${color}"; expected one like '#2980b9', ` +
|
|
217
|
-
"'tomato'
|
|
222
|
+
"'tomato', 'rgba(0, 0, 0, .4)' or 'currentColor', or a $token, before " +
|
|
223
|
+
'its position)',
|
|
218
224
|
);
|
|
219
225
|
}
|
|
220
226
|
|
|
@@ -223,10 +229,15 @@ function stopColor(color, value) {
|
|
|
223
229
|
* One that mentions a `$token` is let through where the style is written,
|
|
224
230
|
* since the token means nothing yet; the paint parses the value it resolves
|
|
225
231
|
* to, and a colour that is still not one drops the whole decoration there
|
|
226
|
-
* rather than throwing (see "memoized parsing" below).
|
|
232
|
+
* rather than throwing (see "memoized parsing" below). `currentColor` is
|
|
233
|
+
* the node's ink, which only the paint knows (`Node.resolveColor`).
|
|
227
234
|
*/
|
|
228
235
|
function isColorOrToken(color) {
|
|
229
|
-
return
|
|
236
|
+
return (
|
|
237
|
+
color.includes('$') ||
|
|
238
|
+
cssColorStraight(color) !== null ||
|
|
239
|
+
isCurrentColor(color)
|
|
240
|
+
);
|
|
230
241
|
}
|
|
231
242
|
|
|
232
243
|
/**
|
|
@@ -349,7 +360,8 @@ function resolveStopOffsets(stops, length) {
|
|
|
349
360
|
* renderer draws the list back to front.
|
|
350
361
|
*
|
|
351
362
|
* `color` may be null, which is CSS's `currentColor` — the node's own ink,
|
|
352
|
-
* which the caller resolves because only it knows the cascade.
|
|
363
|
+
* which the caller resolves because only it knows the cascade. The keyword
|
|
364
|
+
* written out reads as null too, so a shadow has one way to say it.
|
|
353
365
|
*
|
|
354
366
|
* Throws on `inset` and on anything unparseable; see the note at the top of
|
|
355
367
|
* this file for why a shadow that silently does the wrong thing is worse
|
|
@@ -394,7 +406,7 @@ function readShadow(part, value) {
|
|
|
394
406
|
}
|
|
395
407
|
const [dx, dy, blur = 0, spread = 0] = lengths;
|
|
396
408
|
if (blur < 0) throw bad('has a negative blur radius');
|
|
397
|
-
return { dx, dy, blur, spread, color };
|
|
409
|
+
return { dx, dy, blur, spread, color: isCurrentColor(color) ? null : color };
|
|
398
410
|
}
|
|
399
411
|
|
|
400
412
|
/**
|
package/src/editor.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Source locations and the editor that opens them: reading a stack trace
|
|
2
|
+
// into frames, telling the frames of an app's own code from React's and this
|
|
3
|
+
// renderer's, and handing a file and line to the developer's editor. Shared by
|
|
4
|
+
// click-to-component (an Alt+Click on an element) and `react-x11 dev`'s error
|
|
5
|
+
// window (a click on a frame), so the two open the same editor the same way.
|
|
6
|
+
// A leaf: nothing here reaches the renderer or React DevTools.
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { dirname, sep } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
export const STACK_FRAME = /^\s*at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?\s*$/;
|
|
12
|
+
|
|
13
|
+
// The frames to walk past before the JSX call site itself: React captures
|
|
14
|
+
// the Error inside `jsxDEV`/`createElement`, and an element created by this
|
|
15
|
+
// renderer (or by node internals) is never what a click means. `SELF_DIR` is
|
|
16
|
+
// whichever copy of react-x11 is running — the repo's own `src/` for the
|
|
17
|
+
// examples and tests, an installed `node_modules/react-x11/src` for an app.
|
|
18
|
+
//
|
|
19
|
+
// Everything *else* in `node_modules` is deliberately not skipped here: a
|
|
20
|
+
// frame inside an installed package is a real call site, it is just not one
|
|
21
|
+
// in the application's source, and telling the two apart is what lets
|
|
22
|
+
// `resolveOwnedLocation` climb to the owner that is. Skipping every
|
|
23
|
+
// `node_modules` frame instead would walk straight past the library
|
|
24
|
+
// component and land on whatever application frame happens to be deeper in
|
|
25
|
+
// the render stack — usually the `render()` call at startup, which looks
|
|
26
|
+
// like an answer and isn't.
|
|
27
|
+
const REACT_RUNTIME =
|
|
28
|
+
/[\\/]node_modules[\\/](react|react-dom|react-reconciler|react-refresh|scheduler)[\\/]/;
|
|
29
|
+
const SELF_DIR = dirname(fileURLToPath(import.meta.url)) + sep;
|
|
30
|
+
|
|
31
|
+
export function isInternalFrame(file) {
|
|
32
|
+
return (
|
|
33
|
+
file.startsWith('node:') ||
|
|
34
|
+
file === '<anonymous>' ||
|
|
35
|
+
file.startsWith(SELF_DIR) ||
|
|
36
|
+
REACT_RUNTIME.test(file)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// GUI editors: navigate their registered URI scheme (same idea as
|
|
41
|
+
// https://github.com/sidorares/show-component, minus the browser) instead of
|
|
42
|
+
// spawning their CLI shim. The CLI wrapper (e.g. `cursor -g ...`) round-trips
|
|
43
|
+
// through a shell script that re-detects and re-execs the real app, which is
|
|
44
|
+
// visibly slower than the OS handing the URL straight to the already-running
|
|
45
|
+
// instance via Launch Services (macOS) / xdg-open (Linux).
|
|
46
|
+
const EDITOR_SCHEMES = {
|
|
47
|
+
cursor: 'cursor',
|
|
48
|
+
code: 'vscode',
|
|
49
|
+
vscode: 'vscode',
|
|
50
|
+
'code-insiders': 'vscode-insiders',
|
|
51
|
+
'vscode-insiders': 'vscode-insiders',
|
|
52
|
+
windsurf: 'windsurf',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Terminal editors have no URI scheme to navigate — spawn them directly.
|
|
56
|
+
const EDITOR_CLI = {
|
|
57
|
+
vim: (file, line, column) => [
|
|
58
|
+
'vim',
|
|
59
|
+
[`+call cursor(${line},${column})`, file],
|
|
60
|
+
],
|
|
61
|
+
nvim: (file, line, column) => [
|
|
62
|
+
'nvim',
|
|
63
|
+
[`+call cursor(${line},${column})`, file],
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const OPEN_URI_COMMAND = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
68
|
+
|
|
69
|
+
function editorUri(scheme, file, line, column) {
|
|
70
|
+
return `${scheme}://file${encodeURI(file)}:${line}:${column}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function openInEditor({ file, line, column }) {
|
|
74
|
+
const name = editorName();
|
|
75
|
+
let bin, args;
|
|
76
|
+
if (EDITOR_CLI[name]) {
|
|
77
|
+
[bin, args] = EDITOR_CLI[name](file, line, column);
|
|
78
|
+
} else {
|
|
79
|
+
// an unrecognized name is assumed to be a URI scheme itself, so any
|
|
80
|
+
// editor that registers one works without a matching entry above
|
|
81
|
+
const scheme = EDITOR_SCHEMES[name] || name;
|
|
82
|
+
bin = OPEN_URI_COMMAND;
|
|
83
|
+
args = [editorUri(scheme, file, line, column)];
|
|
84
|
+
}
|
|
85
|
+
// stderr is kept (rather than the whole stdio ignored) for one reason:
|
|
86
|
+
// `open`/`xdg-open` exit non-zero and print *there* when nothing claims
|
|
87
|
+
// the scheme — an editor that isn't installed, or one whose URL handler
|
|
88
|
+
// was never registered. Swallowing that is what makes click-to-component
|
|
89
|
+
// look like it only logs: the location resolves, the console line prints,
|
|
90
|
+
// and the editor silently never opens.
|
|
91
|
+
const child = spawn(bin, args, {
|
|
92
|
+
detached: true,
|
|
93
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
94
|
+
});
|
|
95
|
+
let stderr = '';
|
|
96
|
+
// ...and unref'd, so keeping it does not keep the process alive: a pipe is
|
|
97
|
+
// a ref'd handle, and an editor that outlives the app it was launched from
|
|
98
|
+
// is the normal case, not a reason to hold the event loop open.
|
|
99
|
+
child.stderr.unref();
|
|
100
|
+
child.stderr.on('data', (chunk) => {
|
|
101
|
+
stderr += chunk;
|
|
102
|
+
});
|
|
103
|
+
child.on('error', (err) => {
|
|
104
|
+
console.warn(
|
|
105
|
+
`react-x11: could not launch "${bin}" to open ${file}:${line} (${err.code ?? err.message}). ` +
|
|
106
|
+
'Set REACT_X11_EDITOR to your editor CLI (cursor, code, code-insiders, windsurf, vim, nvim).',
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
child.on('exit', (code) => {
|
|
110
|
+
if (!code) return;
|
|
111
|
+
console.warn(
|
|
112
|
+
`react-x11: opening ${file}:${line} — \`${bin} ${args.join(' ')}\` exited ${code}. ` +
|
|
113
|
+
(stderr.trim() ? `${stderr.trim()} ` : '') +
|
|
114
|
+
`Set REACT_X11_EDITOR to the editor you actually use (currently "${name}"; ` +
|
|
115
|
+
'cursor, code, code-insiders, windsurf, vim, nvim, or any registered URI scheme).',
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
child.unref();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The editor `openInEditor` opens: `REACT_X11_EDITOR`, else `cursor`. */
|
|
122
|
+
export function editorName() {
|
|
123
|
+
return process.env.REACT_X11_EDITOR || 'cursor';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A stack trace's frames, in order: where each one is, what it is called, and
|
|
128
|
+
* whether it is React's, Node's or this renderer's own (`internal`) or inside
|
|
129
|
+
* an installed package (`installed`). Lines that are not frames — the message,
|
|
130
|
+
* a `[cause]` — are not in the list.
|
|
131
|
+
*/
|
|
132
|
+
export function stackFrames(stack) {
|
|
133
|
+
const frames = [];
|
|
134
|
+
for (const text of String(stack ?? '').split('\n')) {
|
|
135
|
+
const match = text.match(STACK_FRAME);
|
|
136
|
+
if (!match) continue;
|
|
137
|
+
const [, functionName, rawFile, lineStr, columnStr] = match;
|
|
138
|
+
const file = rawFile.startsWith('file://')
|
|
139
|
+
? fileURLToPath(rawFile)
|
|
140
|
+
: rawFile;
|
|
141
|
+
frames.push({
|
|
142
|
+
functionName: functionName ?? null,
|
|
143
|
+
file,
|
|
144
|
+
line: Number(lineStr),
|
|
145
|
+
column: Number(columnStr),
|
|
146
|
+
internal: isInternalFrame(file),
|
|
147
|
+
installed: file.includes(`${sep}node_modules${sep}`),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return frames;
|
|
151
|
+
}
|
package/src/errors.js
CHANGED
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
// `data` handler and takes the process with it, or wedges the frame loop,
|
|
7
7
|
// which is worse: it looks like a hang rather than a crash.
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* The `code` of the error a drawn element at the root of the tree throws
|
|
11
|
+
* (Reconciler.js, `notAtRoot`). A code because a tool acts on it: `react-x11
|
|
12
|
+
* dev` mounts a component that renders no `<window>` inside one of its own,
|
|
13
|
+
* and the message is prose that is free to change.
|
|
14
|
+
*/
|
|
15
|
+
export const NOT_IN_WINDOW = 'NOT_IN_WINDOW';
|
|
16
|
+
|
|
9
17
|
// container -> the root's onUncaughtError, when it set one
|
|
10
18
|
const perContainer = new WeakMap();
|
|
11
19
|
|
package/src/foreignnodes.js
CHANGED
|
@@ -482,12 +482,19 @@ export class ForeignNode extends Node {
|
|
|
482
482
|
_applyBackground() {
|
|
483
483
|
const wnd = this.socket?.window;
|
|
484
484
|
if (typeof wnd?.setBackgroundPixel !== 'function') return;
|
|
485
|
-
const pixel = pixelFor(this.style.backgroundColor);
|
|
485
|
+
const pixel = pixelFor(this.resolveColor(this.style.backgroundColor));
|
|
486
486
|
if (pixel === null || pixel === this._backgroundPixel) return;
|
|
487
487
|
this._backgroundPixel = pixel;
|
|
488
488
|
wnd.setBackgroundPixel(pixel);
|
|
489
489
|
}
|
|
490
490
|
|
|
491
|
+
/** …which a `currentColor` takes from the ink, and the ink can move with
|
|
492
|
+
* no commit here. */
|
|
493
|
+
_inkMoved() {
|
|
494
|
+
super._inkMoved();
|
|
495
|
+
this._applyBackground();
|
|
496
|
+
}
|
|
497
|
+
|
|
491
498
|
/** The client's window is a window of the display system over this one,
|
|
492
499
|
* so it follows the whole subtree's visibility — see
|
|
493
500
|
* `GlAreaNode._syncSurfaceVisibility`, which answers the same call. */
|
package/src/gloverlay.js
CHANGED
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
import { cssColorStraight } from 'ntk/color';
|
|
87
87
|
|
|
88
88
|
import { FULL_DAMAGE, addDamageRect } from './nodes/damage.js';
|
|
89
|
+
import { radiusOf } from './styles.js';
|
|
89
90
|
import {
|
|
90
91
|
innerPixels,
|
|
91
92
|
intersectRects,
|
|
@@ -1200,7 +1201,7 @@ export class GlOverlay {
|
|
|
1200
1201
|
}
|
|
1201
1202
|
const area = this.area;
|
|
1202
1203
|
// a rounded surface clips its children to an arc, and the arc stays put
|
|
1203
|
-
if ((area.style
|
|
1204
|
+
if (radiusOf(area.style, area.abs) > 0) return false;
|
|
1204
1205
|
// the whole pixels of the surface's box: a fractional edge is an
|
|
1205
1206
|
// antialiased clip, and that does not move either
|
|
1206
1207
|
const box = innerPixels(area.abs);
|
package/src/node.d.ts
CHANGED
|
@@ -624,6 +624,11 @@ export declare class Node {
|
|
|
624
624
|
* with none of its own is set in. An element that draws text inherits
|
|
625
625
|
* exactly what `<text>` inherits by asking. */
|
|
626
626
|
resolvedTextStyle(): TextStyle;
|
|
627
|
+
/** A colour from this node's style as one a context can paint: the value
|
|
628
|
+
* itself, or — for `currentColor` — the node's ink, the colour
|
|
629
|
+
* `resolvedTextStyle()` resolves. Everything core paints from a style
|
|
630
|
+
* colour asks here; an element painting one of its own should too. */
|
|
631
|
+
resolveColor<T>(value: T): T | string;
|
|
627
632
|
/** The flattened `style` prop with the active state blocks overlaid.
|
|
628
633
|
* Everything that paints or lays out reads this, never `props`. */
|
|
629
634
|
readonly style: Style;
|
package/src/nodes/animation.js
CHANGED
|
@@ -11,9 +11,11 @@ import {
|
|
|
11
11
|
transitionFor,
|
|
12
12
|
interpolate,
|
|
13
13
|
ease,
|
|
14
|
+
isColor,
|
|
14
15
|
isLayoutProp,
|
|
15
16
|
TEXT_LAYOUT_PROPS,
|
|
16
17
|
} from '../styles.js';
|
|
18
|
+
import { isCurrentColor } from '../currentcolor.js';
|
|
17
19
|
import { GRID_CONTAINER_PROPS, GRID_ITEM_PROPS } from '../grid.js';
|
|
18
20
|
import { isPlaced } from '../layouts.js';
|
|
19
21
|
import { desktopSettings, watchDesktopSettings } from '../desktopsettings.js';
|
|
@@ -85,11 +87,16 @@ export class NodeAnimation {
|
|
|
85
87
|
// transitions start on later changes, which is CSS's rule too.
|
|
86
88
|
if (this._placed) {
|
|
87
89
|
for (const prop of Object.keys(target)) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
let to = target[prop];
|
|
91
|
+
let from = displayed[prop];
|
|
90
92
|
if (from === to || from === undefined) continue;
|
|
91
93
|
const duration = transitionFor(target, prop);
|
|
92
94
|
if (duration <= 0) continue;
|
|
95
|
+
if (isCurrentColor(from) || isCurrentColor(to)) {
|
|
96
|
+
from = this._swapInk(prop, from, displayed);
|
|
97
|
+
to = this._swapInk(prop, to, target);
|
|
98
|
+
if (from === to) continue;
|
|
99
|
+
}
|
|
93
100
|
if (interpolate(from, to, 0.5) === null) continue; // no midpoint: snap
|
|
94
101
|
const entry = {
|
|
95
102
|
from,
|
|
@@ -260,6 +267,27 @@ export class NodeAnimation {
|
|
|
260
267
|
return this.style;
|
|
261
268
|
}
|
|
262
269
|
|
|
270
|
+
/**
|
|
271
|
+
* A `currentColor` end of a transition as the colour it is at the swap:
|
|
272
|
+
* the ink `style` gives this node, or on `color` itself the ink it
|
|
273
|
+
* inherits. The keyword has no midpoint of its own, so the transition
|
|
274
|
+
* runs between the two colours as they are now — and lands on the style,
|
|
275
|
+
* where the keyword follows the ink again. An ink that moves mid-flight
|
|
276
|
+
* is caught up with at the end, not chased: a `color` that should carry
|
|
277
|
+
* a border with it is the property to transition.
|
|
278
|
+
*/
|
|
279
|
+
_swapInk(prop, value, style) {
|
|
280
|
+
if (!isCurrentColor(value)) return value;
|
|
281
|
+
const inherited = this.inheritedTextStyle.color;
|
|
282
|
+
if (prop === 'color') return inherited;
|
|
283
|
+
// the displayed style's ink is cached; the target's is not yet
|
|
284
|
+
return style === this.style
|
|
285
|
+
? this.resolvedTextStyle().color
|
|
286
|
+
: isColor(style.color)
|
|
287
|
+
? style.color
|
|
288
|
+
: inherited;
|
|
289
|
+
}
|
|
290
|
+
|
|
263
291
|
_animatedValues() {
|
|
264
292
|
const values = {};
|
|
265
293
|
for (const [prop, a] of this._anim) {
|
|
@@ -388,7 +416,7 @@ export class NodeAnimation {
|
|
|
388
416
|
const specs =
|
|
389
417
|
target.animation == null
|
|
390
418
|
? null
|
|
391
|
-
: animationsOf(target, 'a style', this.scale);
|
|
419
|
+
: animationsOf(target, 'a style', this.scale, this._units);
|
|
392
420
|
if (!specs && !this._loops) return false;
|
|
393
421
|
this._loops = specs;
|
|
394
422
|
if (!specs) this.root?._forgetLoopNode(this);
|