glimpse-sdk 0.8.0 → 0.9.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.
@@ -0,0 +1,583 @@
1
+ import fs from "node:fs";
2
+ import { createInterface } from "node:readline";
3
+ import { inspect } from "node:util";
4
+ import Reconciler from "react-reconciler";
5
+ import { DefaultEventPriority } from "react-reconciler/constants.js";
6
+ const TEXT = new Set(["label", "indicator", "button", "progress"]);
7
+ const SKIP = new Set(["children", "key", "ref"]);
8
+ const encoder = new TextEncoder();
9
+ function createStore(initial) {
10
+ let value = initial;
11
+ const listeners = new Set();
12
+ return {
13
+ get: () => value,
14
+ set(next) {
15
+ if (Object.is(next, value))
16
+ return;
17
+ value = next;
18
+ for (const listener of [...listeners])
19
+ listener();
20
+ },
21
+ subscribe(listener) {
22
+ listeners.add(listener);
23
+ return () => listeners.delete(listener);
24
+ },
25
+ };
26
+ }
27
+ export const optionsStore = createStore({});
28
+ export const placementStore = createStore(null);
29
+ export const nameStore = createStore("");
30
+ export const stateStore = createStore("");
31
+ export const openStore = createStore(false);
32
+ class ProtocolLimitError extends Error {
33
+ name = "ProtocolLimitError";
34
+ }
35
+ function writeAll(fd, text) {
36
+ const bytes = encoder.encode(text);
37
+ const pause = new Int32Array(new SharedArrayBuffer(4));
38
+ for (let offset = 0; offset < bytes.length;) {
39
+ try {
40
+ const written = fs.writeSync(fd, bytes, offset, bytes.length - offset);
41
+ if (written === 0)
42
+ throw new Error("zero-byte write");
43
+ offset += written;
44
+ }
45
+ catch (error) {
46
+ if (error.code !== "EAGAIN")
47
+ throw error;
48
+ Atomics.wait(pause, 0, 0, 2);
49
+ }
50
+ }
51
+ }
52
+ function lineOf(message) {
53
+ const line = JSON.stringify(message);
54
+ if (encoder.encode(line).byteLength > 1_048_576) {
55
+ throw new ProtocolLimitError("outgoing line exceeds 1 MiB");
56
+ }
57
+ return line;
58
+ }
59
+ let emit = (message) => writeAll(1, `${lineOf(message)}\n`);
60
+ let helloWritten = false;
61
+ const queued = [];
62
+ export function setEmit(write) {
63
+ emit = write;
64
+ }
65
+ export function send(message) {
66
+ if (helloWritten)
67
+ emit(message);
68
+ else
69
+ queued.push(message);
70
+ }
71
+ const consoleMethods = [
72
+ "log",
73
+ "info",
74
+ "debug",
75
+ "dir",
76
+ "dirxml",
77
+ "table",
78
+ "count",
79
+ "countReset",
80
+ "time",
81
+ "timeLog",
82
+ "timeEnd",
83
+ "group",
84
+ "groupCollapsed",
85
+ "groupEnd",
86
+ "trace",
87
+ ];
88
+ const originalConsole = new Map();
89
+ function writeStderr(...args) {
90
+ const text = args.map((value) => {
91
+ if (typeof value === "string")
92
+ return value;
93
+ return inspect(value);
94
+ }).join(" ");
95
+ writeAll(2, `${text}\n`);
96
+ }
97
+ export function redirectConsole() {
98
+ const target = console;
99
+ if (originalConsole.size)
100
+ return;
101
+ for (const method of consoleMethods) {
102
+ originalConsole.set(method, target[method]);
103
+ target[method] = writeStderr;
104
+ }
105
+ }
106
+ export function restoreConsole() {
107
+ const target = console;
108
+ for (const [method, original] of originalConsole)
109
+ target[method] = original;
110
+ originalConsole.clear();
111
+ }
112
+ const reconcilerFactory = Reconciler;
113
+ const eventPriority = DefaultEventPriority ?? 32;
114
+ async function loadReconciler() {
115
+ return reconcilerFactory;
116
+ }
117
+ function textOf(children) {
118
+ const values = [children].flat(Infinity).filter((child) => child !== null && child !== undefined && typeof child !== "boolean");
119
+ return values.length &&
120
+ values.every((child) => typeof child === "string" || typeof child === "number")
121
+ ? values.join("")
122
+ : undefined;
123
+ }
124
+ function capped(value, limit) {
125
+ return [...value].slice(0, limit).join("");
126
+ }
127
+ function wireProps(type, props) {
128
+ const out = {};
129
+ const text = TEXT.has(type) ? textOf(props.children) : undefined;
130
+ for (const key of Object.keys(props)) {
131
+ if (SKIP.has(key))
132
+ continue;
133
+ if (key === "text") {
134
+ if (text !== undefined)
135
+ out.text = capped(text, 256);
136
+ else if (typeof props.text === "string" || typeof props.text === "number") {
137
+ out.text = capped(String(props.text), 256);
138
+ }
139
+ continue;
140
+ }
141
+ if (!Object.hasOwn(props, key))
142
+ continue;
143
+ const value = props[key];
144
+ if (typeof value === "function") {
145
+ out[key] = true;
146
+ continue;
147
+ }
148
+ if (value === undefined)
149
+ continue;
150
+ out[key] = typeof value === "string"
151
+ ? capped(value, key === "badge" ? 8 : type === "entry" && key === "value" ? 4096 : 256)
152
+ : value;
153
+ }
154
+ if (text !== undefined && !Object.hasOwn(out, "text")) {
155
+ out.text = capped(text, 256);
156
+ }
157
+ return out;
158
+ }
159
+ function same(left, right) {
160
+ if (Object.is(left, right))
161
+ return true;
162
+ if (left === null || right === null)
163
+ return false;
164
+ if (typeof left !== "object" || typeof right !== "object")
165
+ return false;
166
+ return JSON.stringify(left) === JSON.stringify(right);
167
+ }
168
+ function diffProps(type, prev, next) {
169
+ const before = wireProps(type, prev);
170
+ const after = wireProps(type, next);
171
+ const seen = new Set();
172
+ const out = {};
173
+ let changed = false;
174
+ for (const key of [...Object.keys(after), ...Object.keys(before)]) {
175
+ if (seen.has(key))
176
+ continue;
177
+ seen.add(key);
178
+ const hasBefore = Object.hasOwn(before, key);
179
+ const hasAfter = Object.hasOwn(after, key);
180
+ if (!hasBefore && !hasAfter)
181
+ continue;
182
+ if (hasBefore && hasAfter && same(before[key], after[key]))
183
+ continue;
184
+ out[key] = hasAfter ? after[key] : null;
185
+ changed = true;
186
+ }
187
+ return changed ? out : null;
188
+ }
189
+ function bindHandlers(node, props) {
190
+ node.handlers = new Map();
191
+ for (const [key, value] of Object.entries(props)) {
192
+ if (typeof value === "function")
193
+ node.handlers.set(key, value);
194
+ }
195
+ }
196
+ function blankNode(type) {
197
+ return {
198
+ id: 0,
199
+ type,
200
+ props: {},
201
+ children: [],
202
+ attached: false,
203
+ handlers: new Map(),
204
+ };
205
+ }
206
+ function serialize(session, node) {
207
+ if (node.id === 0)
208
+ node.id = session.nextId++;
209
+ node.attached = true;
210
+ session.nodes.set(node.id, node);
211
+ return {
212
+ id: node.id,
213
+ type: node.type,
214
+ props: node.props,
215
+ children: node.children.map((child) => serialize(session, child)),
216
+ };
217
+ }
218
+ function forget(session, node) {
219
+ session.nodes.delete(node.id);
220
+ for (const child of node.children)
221
+ forget(session, child);
222
+ node.children = [];
223
+ node.handlers.clear();
224
+ node.attached = false;
225
+ }
226
+ function place(session, parent, child, before) {
227
+ const existing = parent.children.indexOf(child);
228
+ if (existing >= 0)
229
+ parent.children.splice(existing, 1);
230
+ const index = before
231
+ ? parent.children.indexOf(before)
232
+ : parent.children.length;
233
+ parent.children.splice(index < 0 ? parent.children.length : index, 0, child);
234
+ if (child.attached) {
235
+ session.ops.push({
236
+ op: "move",
237
+ parent: parent.id,
238
+ id: child.id,
239
+ before: before ? before.id : null,
240
+ });
241
+ return;
242
+ }
243
+ session.ops.push({
244
+ op: "insert",
245
+ parent: parent.id,
246
+ before: before ? before.id : null,
247
+ node: serialize(session, child),
248
+ });
249
+ }
250
+ function unplace(session, parent, child) {
251
+ const index = parent.children.indexOf(child);
252
+ if (index >= 0)
253
+ parent.children.splice(index, 1);
254
+ if (child.attached && child.id !== 0) {
255
+ session.ops.push({ op: "remove", parent: parent.id, id: child.id });
256
+ }
257
+ forget(session, child);
258
+ }
259
+ function makeHost(session) {
260
+ let priority = eventPriority;
261
+ return {
262
+ supportsMutation: true,
263
+ supportsPersistence: false,
264
+ supportsHydration: false,
265
+ isPrimaryRenderer: true,
266
+ noTimeout: -1,
267
+ scheduleTimeout: setTimeout,
268
+ cancelTimeout: clearTimeout,
269
+ supportsMicrotasks: true,
270
+ scheduleMicrotask: queueMicrotask,
271
+ getRootHostContext: () => ({}),
272
+ getChildHostContext: (context) => context,
273
+ getPublicInstance: (instance) => instance,
274
+ shouldSetTextContent: (type, props) => TEXT.has(type) && textOf(props.children) !== undefined,
275
+ createInstance(type, props) {
276
+ const node = blankNode(type);
277
+ node.props = wireProps(type, props);
278
+ bindHandlers(node, props);
279
+ return node;
280
+ },
281
+ createTextInstance(text) {
282
+ const node = blankNode("label");
283
+ node.props = { text: capped(text, 256) };
284
+ return node;
285
+ },
286
+ appendInitialChild: (parent, child) => {
287
+ parent.children.push(child);
288
+ },
289
+ appendChild: (parent, child) => place(session, parent, child, null),
290
+ appendChildToContainer: (parent, child) => place(session, parent, child, null),
291
+ insertBefore: (parent, child, before) => place(session, parent, child, before),
292
+ insertInContainerBefore: (parent, child, before) => place(session, parent, child, before),
293
+ removeChild: (parent, child) => unplace(session, parent, child),
294
+ removeChildFromContainer: (parent, child) => unplace(session, parent, child),
295
+ commitUpdate(node, _type, prev, next) {
296
+ bindHandlers(node, next);
297
+ const diff = diffProps(node.type, prev, next);
298
+ if (!diff)
299
+ return;
300
+ const op = {
301
+ op: "set",
302
+ id: node.id,
303
+ props: diff,
304
+ };
305
+ const controlled = CONTROLLED[node.type]?.[1];
306
+ if (node.confirmSeq !== undefined && controlled &&
307
+ Object.hasOwn(diff, controlled)) {
308
+ op.seq = node.confirmSeq;
309
+ node.confirmSeq = undefined;
310
+ }
311
+ node.props = wireProps(node.type, next);
312
+ session.ops.push(op);
313
+ },
314
+ finalizeInitialChildren: () => false,
315
+ prepareForCommit: () => null,
316
+ resetAfterCommit() {
317
+ session.pending.push(...session.ops);
318
+ session.ops = [];
319
+ },
320
+ clearContainer: () => { },
321
+ detachDeletedInstance: () => { },
322
+ preparePortalMount: () => { },
323
+ getCurrentEventPriority: () => eventPriority,
324
+ setCurrentUpdatePriority: (next) => {
325
+ priority = next;
326
+ },
327
+ getCurrentUpdatePriority: () => priority,
328
+ resolveUpdatePriority: () => priority || eventPriority,
329
+ shouldAttemptEagerTransition: () => false,
330
+ maySuspendCommit: () => false,
331
+ maySuspendCommitOnUpdate: () => false,
332
+ maySuspendCommitInSyncRender: () => false,
333
+ preloadInstance: () => true,
334
+ startSuspendingCommit: () => { },
335
+ suspendInstance: () => { },
336
+ waitForCommitToBeReady: () => null,
337
+ requestPostPaintCallback: () => { },
338
+ trackSchedulerEvent: () => { },
339
+ resolveEventType: () => null,
340
+ resolveEventTimeStamp: () => -1.1,
341
+ NotPendingTransition: null,
342
+ HostTransitionContext: {
343
+ $$typeof: Symbol.for("react.context"),
344
+ _currentValue: null,
345
+ _currentValue2: null,
346
+ },
347
+ resetFormInstance: () => { },
348
+ commitMount: () => { },
349
+ commitTextUpdate(node, _previous, next) {
350
+ const text = capped(next, 256);
351
+ if (node.props.text === text)
352
+ return;
353
+ node.props.text = text;
354
+ session.ops.push({ op: "set", id: node.id, props: { text } });
355
+ },
356
+ resetTextContent: () => { },
357
+ hideInstance: () => { },
358
+ hideTextInstance: () => { },
359
+ unhideInstance: () => { },
360
+ unhideTextInstance: () => { },
361
+ };
362
+ }
363
+ const CONTROLLED = {
364
+ entry: ["onChange", "value"],
365
+ fader: ["onChange", "value"],
366
+ scale: ["onChange", "value"],
367
+ switch: ["onToggle", "active"],
368
+ switchrow: ["onToggle", "active"],
369
+ };
370
+ let current = null;
371
+ function flush(session, fn, emitCommit = true) {
372
+ session.reconciler.flushSyncFromReconciler(fn);
373
+ session.reconciler["flushPassiveEffects"]?.();
374
+ for (const node of session.nodes.values()) {
375
+ const controlled = CONTROLLED[node.type]?.[1];
376
+ if (node.confirmSeq === undefined || !controlled)
377
+ continue;
378
+ session.pending.push({
379
+ op: "set",
380
+ id: node.id,
381
+ props: { [controlled]: (node.props[controlled] ?? null) },
382
+ seq: node.confirmSeq,
383
+ });
384
+ node.confirmSeq = undefined;
385
+ }
386
+ let count = 0;
387
+ const visit = (node) => {
388
+ if (++count > 300)
389
+ throw new ProtocolLimitError("tree exceeds 300 nodes");
390
+ if (node.children.length > 200) {
391
+ throw new ProtocolLimitError("parent exceeds 200 children");
392
+ }
393
+ for (const child of node.children)
394
+ visit(child);
395
+ };
396
+ visit(session.container);
397
+ if (session.pending.length > 1200) {
398
+ throw new ProtocolLimitError("commit exceeds 1200 operations");
399
+ }
400
+ if (emitCommit && session.pending.length) {
401
+ send({ t: "commit", ops: session.pending });
402
+ session.pending = [];
403
+ }
404
+ }
405
+ async function mountFresh(element) {
406
+ const factory = await loadReconciler();
407
+ const container = blankNode("root");
408
+ container.id = 0;
409
+ container.attached = true;
410
+ const session = {
411
+ nextId: 1,
412
+ nodes: new Map(),
413
+ ops: [],
414
+ pending: [],
415
+ container,
416
+ reconciler: null,
417
+ root: null,
418
+ };
419
+ session.reconciler = factory(makeHost(session));
420
+ session.root = session.reconciler.createContainer(container, 0, null, false, null, "applet", (error) => {
421
+ console.error(error);
422
+ process.exit(70);
423
+ }, (error) => {
424
+ console.error(error);
425
+ }, (error) => {
426
+ console.error(error);
427
+ }, null);
428
+ current = session;
429
+ flush(session, () => {
430
+ session.reconciler.updateContainer(element, session.root, null, null);
431
+ }, false);
432
+ }
433
+ function dispatchEvent(message) {
434
+ if (message.t !== "event" || !current)
435
+ return;
436
+ const id = message.id;
437
+ if (typeof id !== "number")
438
+ return;
439
+ const node = current.nodes.get(id);
440
+ if (!node)
441
+ return;
442
+ if (typeof message.name !== "string")
443
+ return;
444
+ const handler = node.handlers.get(message.name);
445
+ if (!handler)
446
+ return;
447
+ if (CONTROLLED[node.type]?.[0] === message.name &&
448
+ typeof message.seq === "number" && Number.isFinite(message.seq)) {
449
+ if (node.appliedSeq !== undefined && message.seq < node.appliedSeq)
450
+ return;
451
+ node.appliedSeq = message.seq;
452
+ node.confirmSeq = message.seq;
453
+ }
454
+ const args = Array.isArray(message.args) ? message.args : [];
455
+ try {
456
+ handler(...args);
457
+ }
458
+ catch (error) {
459
+ console.error(error);
460
+ process.exit(1);
461
+ }
462
+ }
463
+ function apply(message) {
464
+ if (!current)
465
+ return;
466
+ if (message.t === "options") {
467
+ optionsStore.set(message.options ?? {});
468
+ return;
469
+ }
470
+ if (message.t === "placement") {
471
+ placementStore.set(message.placement ?? null);
472
+ return;
473
+ }
474
+ if (message.t === "popover") {
475
+ openStore.set(message.open === true);
476
+ return;
477
+ }
478
+ if (message.t === "event")
479
+ dispatchEvent(message);
480
+ }
481
+ export async function session(element, io) {
482
+ redirectConsole();
483
+ await loadReconciler();
484
+ current = null;
485
+ helloWritten = false;
486
+ setEmit((message) => io.writeLine(lineOf(message)));
487
+ io.writeLine(lineOf({ t: "hello", v: 1 }));
488
+ helloWritten = true;
489
+ for (const message of queued.splice(0))
490
+ emit(message);
491
+ for await (const chunk of io.lines) {
492
+ try {
493
+ const buffered = [];
494
+ for (const line of Array.isArray(chunk) ? chunk : [chunk]) {
495
+ let message;
496
+ try {
497
+ message = JSON.parse(line);
498
+ }
499
+ catch (error) {
500
+ console.error(error);
501
+ continue;
502
+ }
503
+ if (!message || typeof message !== "object" ||
504
+ typeof message.t !== "string")
505
+ continue;
506
+ if (message.t === "hello") {
507
+ if (current)
508
+ continue;
509
+ if (message.v !== 1) {
510
+ console.error(`unsupported host protocol version: ${message.v}`);
511
+ process.exit(1);
512
+ return;
513
+ }
514
+ optionsStore.set(message.options ?? {});
515
+ placementStore.set(message.placement ?? null);
516
+ nameStore.set(typeof message.name === "string" ? message.name : "");
517
+ stateStore.set(typeof message.state === "string" ? message.state : "");
518
+ openStore.set(false);
519
+ await mountFresh(element);
520
+ continue;
521
+ }
522
+ if (current)
523
+ buffered.push(message);
524
+ }
525
+ if (current) {
526
+ flush(current, () => {
527
+ for (const message of buffered)
528
+ apply(message);
529
+ });
530
+ }
531
+ }
532
+ catch (error) {
533
+ if (!(error instanceof ProtocolLimitError))
534
+ throw error;
535
+ console.error(error);
536
+ process.exit(70);
537
+ return;
538
+ }
539
+ }
540
+ }
541
+ export async function* stdinLines(input = process.stdin) {
542
+ const reader = createInterface({ input, crlfDelay: Infinity });
543
+ const pending = [];
544
+ const batches = [];
545
+ let wake;
546
+ let closed = false;
547
+ reader.on("line", (line) => {
548
+ if (line.trim() === "")
549
+ return;
550
+ pending.push(line);
551
+ if (pending.length === 1) {
552
+ setImmediate(() => {
553
+ batches.push(pending.splice(0));
554
+ wake?.();
555
+ wake = undefined;
556
+ });
557
+ }
558
+ });
559
+ reader.on("close", () => {
560
+ closed = true;
561
+ wake?.();
562
+ wake = undefined;
563
+ });
564
+ try {
565
+ while (!closed || pending.length || batches.length) {
566
+ if (!batches.length)
567
+ await new Promise((resolve) => { wake = resolve; });
568
+ while (batches.length)
569
+ yield batches.shift();
570
+ }
571
+ }
572
+ finally {
573
+ reader.close();
574
+ }
575
+ }
576
+ export function standardIo() {
577
+ return {
578
+ writeLine(line) {
579
+ writeAll(1, `${line}\n`);
580
+ },
581
+ lines: stdinLines(),
582
+ };
583
+ }
package/package.json CHANGED
@@ -1,53 +1,54 @@
1
1
  {
2
2
  "name": "glimpse-sdk",
3
- "version": "0.8.0",
4
- "description": "Typed async framework for building Glimpse exec applets",
5
- "type": "module",
6
- "main": "dist/src/index.js",
7
- "types": "dist/src/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./dist/src/index.d.ts",
11
- "import": "./dist/src/index.js"
12
- }
13
- },
14
- "files": [
15
- "dist/src",
16
- "src",
17
- "README.md",
18
- "LICENSE"
19
- ],
20
- "scripts": {
21
- "build": "tsc -p tsconfig.json",
22
- "test": "npm run build && node --test dist/tests/*.test.js",
23
- "prepublishOnly": "npm run build"
24
- },
25
- "keywords": [
26
- "glimpse",
27
- "applet",
28
- "panel",
29
- "desktop",
30
- "sdk"
31
- ],
32
- "license": "MIT",
33
- "author": "Alex Oleshkevich <alex.oleshkevich@gmail.com>",
34
- "homepage": "https://alex-oleshkevich.github.io/glimpse/",
3
+ "version": "0.9.0",
4
+ "description": "React renderer for glimpse panel applets: write the indicator and popover in JSX, run it under Node.",
5
+ "keywords": ["glimpse", "applet", "panel", "wayland", "react"],
6
+ "license": "BSD-3-Clause",
7
+ "author": "Alex Oleshkevich",
8
+ "homepage": "https://github.com/alex-oleshkevich/glimpse/tree/master/sdk/applet#readme",
9
+ "bugs": "https://github.com/alex-oleshkevich/glimpse/issues",
35
10
  "repository": {
36
11
  "type": "git",
37
12
  "url": "git+https://github.com/alex-oleshkevich/glimpse.git",
38
- "directory": "sdk/sdk-ts"
39
- },
40
- "bugs": {
41
- "url": "https://github.com/alex-oleshkevich/glimpse/issues"
13
+ "directory": "sdk/applet"
42
14
  },
15
+ "type": "module",
43
16
  "engines": {
44
- "node": ">=20"
17
+ "node": ">=22"
18
+ },
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "./jsx-runtime": {
25
+ "types": "./dist/jsx-runtime.d.ts",
26
+ "default": "./dist/jsx-runtime.js"
27
+ },
28
+ "./jsx-dev-runtime": {
29
+ "types": "./dist/jsx-runtime.d.ts",
30
+ "default": "./dist/jsx-runtime.js"
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
34
+ "files": ["dist", "PROTOCOL.md"],
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.build.json",
37
+ "check": "tsc --noEmit",
38
+ "test": "node --test",
39
+ "prepack": "npm run build"
45
40
  },
46
41
  "publishConfig": {
47
42
  "access": "public"
48
43
  },
44
+ "dependencies": {
45
+ "@types/react": "19.3.0",
46
+ "@types/react-reconciler": "0.33.1",
47
+ "react": "19.3.0",
48
+ "react-reconciler": "0.34.0"
49
+ },
49
50
  "devDependencies": {
50
- "@types/node": "^20.14.0",
51
- "typescript": "^5.6.0"
51
+ "@types/node": "26.6.3",
52
+ "typescript": "7.0.2"
52
53
  }
53
54
  }