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 +4 -2
- package/src/cocoa/app.js +18 -0
- package/src/cocoa/calendar.js +186 -0
- package/src/cocoa/native.js +20 -3
- package/src/cocoa/permissions.js +14 -5
- package/src/desktopcalendar.js +1415 -0
- package/src/desktopcalendarhooks.js +259 -0
- package/src/index.d.ts +1 -0
- package/src/index.js +9 -0
- package/src/permissionhooks.js +9 -6
- package/src/permissions.js +12 -2
- package/src/types/desktopcalendar.d.ts +207 -0
- package/src/types/permissions.d.ts +12 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.9.
|
|
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.
|
|
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
|
+
}
|
package/src/cocoa/native.js
CHANGED
|
@@ -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 =
|
|
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;
|
package/src/cocoa/permissions.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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 {
|