react-x11 2.9.0 → 2.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-x11",
3
- "version": "2.9.0",
3
+ "version": "2.9.1",
4
4
  "description": "react renderer with X11 as a target",
5
5
  "main": "./src/index.js",
6
6
  "files": [
@@ -32,6 +32,7 @@
32
32
  "examples:tooltips": "tsx examples/tooltips.jsx",
33
33
  "examples:chat": "tsx examples/chat.jsx",
34
34
  "examples:clipboard": "tsx examples/clipboard.jsx",
35
+ "examples:calendar": "tsx examples/calendar.jsx",
35
36
  "labs:urischeme": "tsx examples/labs/urischeme.jsx",
36
37
  "filedialog:probe": "node scripts/filedialog-probe.mjs",
37
38
  "a11y:probe": "node scripts/a11y-probe.mjs",
@@ -92,13 +93,14 @@
92
93
  "node": ">=20.19"
93
94
  },
94
95
  "dependencies": {
96
+ "ical.js": "^2.2.1",
95
97
  "linebreak": "^1.1.0",
96
98
  "ntk": "^8.7.0",
97
99
  "react-reconciler": "^0.33.0",
98
100
  "yoga-layout": "^3.2.1"
99
101
  },
100
102
  "optionalDependencies": {
101
- "@windowkit/appkit": "^0.7.0",
103
+ "@windowkit/appkit": "^0.8.0",
102
104
  "dbus-native": "^0.15.1",
103
105
  "x11-dri": "^0.7.0"
104
106
  },
package/src/cocoa/app.js CHANGED
@@ -25,6 +25,7 @@ import { CocoaGLArea, cocoaGLConfig, resolveCocoaGLRuntime } from './glarea.js';
25
25
  import { CocoaDockMenu } from './dock.js';
26
26
  import { CocoaGlobalMenuExport } from './globalmenu.js';
27
27
  import { CocoaStatusItem } from './statusitem.js';
28
+ import { CocoaCalendars } from './calendar.js';
28
29
  import { CocoaNotifications } from './notifications.js';
29
30
  import { CocoaPaneHost } from './panehost.js';
30
31
  import { CocoaPermissions } from './permissions.js';
@@ -177,6 +178,17 @@ export class CocoaApp {
177
178
  ? new CocoaNotifications(this)
178
179
  : null;
179
180
 
181
+ // EventKit (src/cocoa/calendar.js). Present exactly when the bridge has
182
+ // it (>= 0.8), and its presence is the top rung of
183
+ // src/desktopcalendar.js's ladder for this app — an older bridge leaves
184
+ // the ladder to find `osascript` instead, which answers the same
185
+ // questions out of the same framework.
186
+ this.calendars =
187
+ typeof native.calendars === 'function' &&
188
+ typeof native.eventsBetween === 'function'
189
+ ? new CocoaCalendars(this)
190
+ : null;
191
+
180
192
  // The GL policy, glbackend.js's shape. No GLX exists here, so the
181
193
  // default is 'auto' (the direct backend where the runtime loads);
182
194
  // useSupports('shaders') stays false until the first <glarea> resolves
@@ -767,6 +779,12 @@ export class CocoaApp {
767
779
  case 'notification-dismissed':
768
780
  this.notifications?.route(ev);
769
781
  return this._afterInput();
782
+ case 'calendar-store-changed':
783
+ // EventKit's own notification, which names nothing that changed. A
784
+ // watcher's answer is to query again, and that query's result is a
785
+ // render, so the frame goes out the way an input's does.
786
+ this.calendars?.route(ev);
787
+ return this._afterInput();
770
788
  case 'animation-end':
771
789
  // the presenter that added the animation registered for its id; an
772
790
  // id nobody knows is an animation already forgotten (cancelled, or
@@ -0,0 +1,186 @@
1
+ // The user's calendars on the cocoa backend — EventKit (`EKEventStore`)
2
+ // through @windowkit/appkit (>= 0.8), the top rung of
3
+ // src/desktopcalendar.js's ladder. `CocoaApp.calendars` is this object and
4
+ // its *presence* is the capability, the rule `filePanels`, `permissions` and
5
+ // `notifications` follow, so the ladder never names a backend.
6
+ //
7
+ // EventKit is the macOS counterpart of Evolution Data Server plus GNOME
8
+ // Online Accounts in one framework: every account the user added in System
9
+ // Settings > Internet Accounts — iCloud, Google, Exchange, CalDAV, a
10
+ // subscribed feed — is served by one store, the desktop did the OAuth, and
11
+ // the app never sees a credential.
12
+ //
13
+ // Three things about the framework shape the code here:
14
+ //
15
+ // - **It expands recurrences itself.** `predicateForEventsWithStartDate…`
16
+ // answers occurrences, so there is no `ical.js` on this rung and no
17
+ // recurrence arithmetic to get wrong. The EDS rung pays for that
18
+ // client-side; this one does not.
19
+ // - **The predicate spans at most four years** (the framework's own limit),
20
+ // so a longer range is asked for in chunks and the pieces are merged —
21
+ // `chunkSpan` in ../desktopcalendar.js, shared with the `osascript` rung
22
+ // because it is the same framework underneath.
23
+ // - **An all-day event ends at the last second of its last day.** Every
24
+ // other rung, and `byDay`, read `end` as exclusive, so it is normalised
25
+ // here — at the edge, once, rather than in every consumer.
26
+ //
27
+ // A change to anything in the store arrives as one `calendar-store-changed`
28
+ // backend event that names nothing, so `watch` reports `kind: 'changed'`
29
+ // with a null calendar and no count. Re-query; do not try to patch.
30
+
31
+ import {
32
+ calendarBackendName,
33
+ chunkSpan,
34
+ hexFromComponents,
35
+ sortEvents,
36
+ withExclusiveEnd,
37
+ } from '../desktopcalendar.js';
38
+
39
+ /** `EKCalendar` as the shape every rung answers with. */
40
+ export function calendarInfo(cal) {
41
+ return {
42
+ uid: String(cal.id),
43
+ name: cal.title ?? '',
44
+ // EventKit has no "unchecked in the sidebar" state to report — a
45
+ // calendar hidden in Calendar.app is still in the store — so every
46
+ // calendar here is enabled, which is what `enabled` means to a caller
47
+ // filtering the list.
48
+ enabled: true,
49
+ color: hexFromComponents(cal.color),
50
+ backend: calendarBackendName(cal.type),
51
+ readOnly: cal.allowsModifications === false || cal.immutable === true,
52
+ // The account the calendar came from, which is what the EDS rung's
53
+ // `account` is too: an `EKSource` is the Internet Accounts entry.
54
+ account: cal.source?.title ?? undefined,
55
+ };
56
+ }
57
+
58
+ /** One occurrence from the bridge as the shape every rung answers with. */
59
+ export function desktopEvent(ev, byId) {
60
+ const meta = byId.get(String(ev.calendar));
61
+ const start = new Date(ev.start);
62
+ return {
63
+ uid: String(ev.id ?? ev.itemId ?? `${ev.calendar}:${ev.start}`),
64
+ summary: ev.title ?? '',
65
+ location: ev.location ?? undefined,
66
+ description: ev.notes ?? undefined,
67
+ start,
68
+ end: withExclusiveEnd(new Date(ev.end), Boolean(ev.allDay)),
69
+ allDay: Boolean(ev.allDay),
70
+ recurring: Boolean(ev.recurring),
71
+ calendar: meta
72
+ ? { uid: meta.uid, name: meta.name, color: meta.color }
73
+ : { uid: String(ev.calendar), name: '' },
74
+ };
75
+ }
76
+
77
+ export class CocoaCalendars {
78
+ constructor(app) {
79
+ this.app = app;
80
+ this._native = app._native;
81
+ this.backend = 'cocoa';
82
+ this._watchers = new Set();
83
+ }
84
+
85
+ /**
86
+ * The TCC grant, read without prompting. `'write-only'` is macOS 14's
87
+ * partial grant, which is a refusal to a reader — but its own word, so a
88
+ * caller that only saves events can tell it from a denial.
89
+ */
90
+ async access() {
91
+ return this.app.permissions
92
+ ? this.app.permissions.status('calendars')
93
+ : 'unknown';
94
+ }
95
+
96
+ /** The system prompt, once per app; the status after the user answered. */
97
+ async requestAccess() {
98
+ return this.app.permissions
99
+ ? this.app.permissions.request('calendars')
100
+ : 'unknown';
101
+ }
102
+
103
+ listCalendars() {
104
+ return new Promise((resolve, reject) => {
105
+ this._native.calendars((err, list) =>
106
+ err ? reject(err) : resolve((list ?? []).map(calendarInfo)),
107
+ );
108
+ });
109
+ }
110
+
111
+ _events(range) {
112
+ return new Promise((resolve, reject) => {
113
+ this._native.eventsBetween(range, (err, events) =>
114
+ err ? reject(err) : resolve(events ?? []),
115
+ );
116
+ });
117
+ }
118
+
119
+ /**
120
+ * The occurrences in a range, already expanded by the framework.
121
+ *
122
+ * `errors` is always empty here: there is one store, and a failure in it
123
+ * is a failure of the whole read rather than of one account. The EDS rung
124
+ * is the one where a single unreachable CalDAV server must not blank the
125
+ * month.
126
+ */
127
+ async eventsBetween(from, to, options = {}) {
128
+ const metas = options.calendars ?? (await this.listCalendars());
129
+ // "These calendars, of which there are none" is not "every calendar",
130
+ // which is what an empty filter means to the predicate underneath. A
131
+ // caller whose filter matched nothing must get nothing.
132
+ if (options.calendars && metas.length === 0) {
133
+ return { events: [], errors: [] };
134
+ }
135
+ const byId = new Map(metas.map((meta) => [meta.uid, meta]));
136
+ const ids = options.calendars ? metas.map((meta) => meta.uid) : undefined;
137
+
138
+ const events = [];
139
+ const seen = new Set();
140
+ for (const [start, end] of chunkSpan(from, to)) {
141
+ const raw = await this._events({ start, end, calendars: ids });
142
+ for (const one of raw) {
143
+ // An occurrence that straddles a chunk boundary is reported by both
144
+ // predicates; the caller must not see it twice.
145
+ const key = `${one.id} ${one.start}`;
146
+ if (seen.has(key)) continue;
147
+ seen.add(key);
148
+ events.push(desktopEvent(one, byId));
149
+ }
150
+ }
151
+ return { events: sortEvents(events), errors: [] };
152
+ }
153
+
154
+ /**
155
+ * `EKEventStoreChangedNotification`, as the ladder's change signal.
156
+ *
157
+ * The notification names neither the calendar nor what happened — a
158
+ * detail EventKit does not have — so the change carries a null calendar
159
+ * and no count, and re-querying is the only correct answer. Every watcher
160
+ * hears every change, whatever range it asked for, because a change
161
+ * outside a range can still move what is inside it (a recurrence master
162
+ * edited months away).
163
+ */
164
+ async watch(from, to, onChange) {
165
+ const entry = { onChange };
166
+ this._watchers.add(entry);
167
+ return async () => {
168
+ this._watchers.delete(entry);
169
+ };
170
+ }
171
+
172
+ /** `calendar-store-changed`, routed from the app's backend callback. */
173
+ route() {
174
+ for (const entry of [...this._watchers]) {
175
+ try {
176
+ entry.onChange({ calendar: null, kind: 'changed', count: null });
177
+ } catch {
178
+ // a watcher that throws is not the other watchers' problem
179
+ }
180
+ }
181
+ }
182
+
183
+ /** Nothing to release: the store belongs to the process, and a handle's
184
+ * own watchers are dropped by the stop function `watch` returned. */
185
+ async close() {}
186
+ }
@@ -11,10 +11,27 @@
11
11
  // is CommonJS + a .node binary, so `createRequire` is the honest loader.
12
12
  import { createRequire } from 'node:module';
13
13
 
14
- const require = createRequire(import.meta.url);
15
-
16
14
  const PACKAGE = '@windowkit/appkit';
17
15
 
16
+ // The loader is made on first use, and from the executable when there is
17
+ // no module URL to make it from. A bundle built for Node's single-executable
18
+ // format (docs/packaging.md, tier 3) is CommonJS, and esbuild leaves
19
+ // `import.meta` an empty object in that output — so a module-scope
20
+ // `createRequire(import.meta.url)` threw ERR_INVALID_ARG_VALUE the moment
21
+ // the backend loaded, in every cocoa app shipped as a SEA, before this
22
+ // could say what it was trying to load. `createRequire(process.execPath)`
23
+ // is the loader Node's SEA docs prescribe (the embedded main's `__filename`
24
+ // *is* the executable), and it answers both specs below: an absolute
25
+ // REACT_X11_CALAYERS_PATH resolves from anywhere, and the bare package name
26
+ // resolves through a `node_modules` beside the binary. Made lazily, a
27
+ // loader that cannot be made at all is reported by the error at the bottom
28
+ // rather than by a throw at import.
29
+ let require = null;
30
+ function load(spec) {
31
+ require ??= createRequire(import.meta.url ?? process.execPath);
32
+ return require(spec);
33
+ }
34
+
18
35
  let cached = null;
19
36
 
20
37
  export function loadNative() {
@@ -30,7 +47,7 @@ export function loadNative() {
30
47
  const path = process.env.REACT_X11_CALAYERS_PATH;
31
48
  for (const spec of [path, PACKAGE].filter(Boolean)) {
32
49
  try {
33
- const mod = require(spec);
50
+ const mod = load(spec);
34
51
  // the package's index.js exports the raw addon as `native`; a direct
35
52
  // path to a built checkout may be the addon itself
36
53
  cached = mod.native ?? mod;
@@ -13,23 +13,32 @@
13
13
  // bundled app must carry the usage-description keys
14
14
  // (`NSCameraUsageDescription` and friends) or a request never prompts.
15
15
 
16
- /** Apple's four words as the ladder's. */
16
+ /** Apple's words as the ladder's. `writeOnly` is macOS 14's partial grant
17
+ * for EventKit — a grant to a writer, a refusal to a reader — and stays its
18
+ * own word for exactly that reason. */
17
19
  const STATUS = Object.freeze({
18
20
  authorized: 'granted',
19
21
  denied: 'denied',
20
22
  restricted: 'restricted',
21
23
  notDetermined: 'prompt',
24
+ writeOnly: 'write-only',
22
25
  });
23
26
 
24
27
  export function statusFromBridge(status) {
25
28
  return STATUS[status] ?? 'unknown';
26
29
  }
27
30
 
28
- /** The bridge's options for a kind: `automation` carries its target. */
31
+ /** The bridge's options for a kind: `automation` carries its target, and
32
+ * `calendars` the level being asked for (`reminders` has no write-only
33
+ * grant, and the bridge refuses one with a TypeError). */
29
34
  function bridgeOptions(kind, options = {}) {
30
- return kind === 'automation' && options.target != null
31
- ? { target: String(options.target) }
32
- : undefined;
35
+ if (kind === 'automation' && options.target != null) {
36
+ return { target: String(options.target) };
37
+ }
38
+ if (kind === 'calendars' && options.access != null) {
39
+ return { access: String(options.access) };
40
+ }
41
+ return undefined;
33
42
  }
34
43
 
35
44
  export class CocoaPermissions {