@domicile-desktop/sdk 0.0.0-alpha-e040773499e5 → 0.0.0-alpha-019dff1d8582

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 CHANGED
@@ -57,7 +57,7 @@ See [docs/ELEMENTS.md](docs/ELEMENTS.md) for `<app>` input and focus, and the
57
57
  | `./bind-keys` | `bindKeys`: grab a shell's own chords by name and handle them by mode. `./key-action` and `./own-keybindings` are its parts. |
58
58
  | `./fake-host` | `FakeDomicileHost`: a desktop for a shell's tests. |
59
59
  | `./system` | `system(domicile)`: files, watches, processes and D-Bus. Calls resolve a `Result`; the compositor refuses most while the desktop is locked. See [SHELL-SYSTEM-ACCESS.md](/docs/SHELL-SYSTEM-ACCESS.md). |
60
- | `./portal` | `watchPortalRequests` and `answerPortalRequest`: application dialogs from `xdg-desktop-portal`, parsed by kind. `<PortalDialogs />` in component-library draws them. See [PORTALS.md](/docs/architecture/PORTALS.md). |
60
+ | `./portal` | `watchPortalRequests` and `answerPortalRequest`: application dialogs from `xdg-desktop-portal`, parsed by kind. `watchCapturing` and `stopCapturing`: running sessions that control or capture input. `<PortalDialogs />` in component-library draws them. See [PORTALS.md](/docs/architecture/PORTALS.md). |
61
61
  | `./extension`, `./tray`, `./notification`, `./theme`, `./file-preview`, `./display-transform` | Data types and Zod schemas for what the desktop holds. |
62
62
 
63
63
  Internal, not needed by shells: `./cursor-shape`.
package/dist/portal.d.ts CHANGED
@@ -11,8 +11,12 @@ export declare enum PortalKind {
11
11
  * lets go. Not a question: the compositor refuses answers to it.
12
12
  */
13
13
  Inhibit = 3,
14
+ /** Which input devices, and the clipboard, an application may control. */
15
+ RemoteDesktop = 4,
16
+ /** Whether an application may take input that crosses a screen edge. */
17
+ InputCapture = 5,
14
18
  /** A kind this SDK cannot parse. Answer it with {@link PortalAnswer.Refused}. */
15
- Unknown = 4
19
+ Unknown = 6
16
20
  }
17
21
  /** What every request carries. */
18
22
  export type PortalRequestBase = {
@@ -115,6 +119,21 @@ export type InhibitBody = {
115
119
  what: readonly Inhibited[];
116
120
  reason: string | undefined;
117
121
  };
122
+ /** Input devices a session asks for or holds. */
123
+ export type Devices = {
124
+ keyboard: boolean;
125
+ pointer: boolean;
126
+ touchscreen: boolean;
127
+ };
128
+ /** What a remote desktop session asks to control. */
129
+ export type RemoteDesktopBody = {
130
+ devices: Devices;
131
+ clipboard: boolean;
132
+ };
133
+ /** What an input capture session asks to take. */
134
+ export type InputCaptureBody = {
135
+ devices: Devices;
136
+ };
118
137
  export declare const PortalRequest: {
119
138
  Access: (base: PortalRequestBase, body: AccessBody) => {
120
139
  /** The id {@link answerPortalRequest} takes. */
@@ -156,6 +175,26 @@ export declare const PortalRequest: {
156
175
  body: InhibitBody;
157
176
  kind: PortalKind.Inhibit;
158
177
  };
178
+ InputCapture: (base: PortalRequestBase, body: InputCaptureBody) => {
179
+ /** The id {@link answerPortalRequest} takes. */
180
+ id: number;
181
+ /** The asking application's desktop file id. Empty when unknown. */
182
+ appId: string;
183
+ /** The `<app>` to show the dialog over, or the focused screen when absent. */
184
+ parentAppId: string | undefined;
185
+ body: InputCaptureBody;
186
+ kind: PortalKind.InputCapture;
187
+ };
188
+ RemoteDesktop: (base: PortalRequestBase, body: RemoteDesktopBody) => {
189
+ /** The id {@link answerPortalRequest} takes. */
190
+ id: number;
191
+ /** The asking application's desktop file id. Empty when unknown. */
192
+ appId: string;
193
+ /** The `<app>` to show the dialog over, or the focused screen when absent. */
194
+ parentAppId: string | undefined;
195
+ body: RemoteDesktopBody;
196
+ kind: PortalKind.RemoteDesktop;
197
+ };
159
198
  Unknown: (base: PortalRequestBase, wireKind: string) => {
160
199
  /** The id {@link answerPortalRequest} takes. */
161
200
  id: number;
@@ -175,10 +214,16 @@ export declare enum PortalAnswerKind {
175
214
  AppChooser = 1,
176
215
  /** The user chose in a {@link PortalKind.FileChooser}. */
177
216
  FileChooser = 2,
217
+ /** The user granted a {@link PortalKind.RemoteDesktop} no more than it asked. */
218
+ RemoteDesktop = 3,
219
+ /** The user allowed a {@link PortalKind.InputCapture} request. */
220
+ InputCapture = 4,
221
+ /** The user stopped a {@link Capturing} session. */
222
+ Stop = 5,
178
223
  /** The user dismissed or denied the dialog. */
179
- Canceled = 3,
224
+ Canceled = 6,
180
225
  /** The shell has no dialog for this kind. */
181
- Refused = 4
226
+ Refused = 7
182
227
  }
183
228
  export declare const PortalAnswer: {
184
229
  Access: () => {
@@ -196,11 +241,65 @@ export declare const PortalAnswer: {
196
241
  chosen: FileChosen;
197
242
  kind: PortalAnswerKind.FileChooser;
198
243
  };
244
+ InputCapture: () => {
245
+ kind: PortalAnswerKind.InputCapture;
246
+ };
199
247
  Refused: () => {
200
248
  kind: PortalAnswerKind.Refused;
201
249
  };
250
+ RemoteDesktop: (devices: Devices, clipboard: boolean) => {
251
+ clipboard: boolean;
252
+ devices: Devices;
253
+ kind: PortalAnswerKind.RemoteDesktop;
254
+ };
255
+ Stop: () => {
256
+ kind: PortalAnswerKind.Stop;
257
+ };
202
258
  };
203
259
  export type PortalAnswer = ReturnType<(typeof PortalAnswer)[keyof typeof PortalAnswer]>;
260
+ export declare enum CapturingKind {
261
+ /** An application controls input, and the clipboard when `clipboard` is set. */
262
+ RemoteDesktop = 0,
263
+ /** An application takes the user's input once it crosses a screen edge. */
264
+ InputCapture = 1,
265
+ /** A kind this SDK cannot parse. It can still be stopped. */
266
+ Unknown = 2
267
+ }
268
+ /** What every capturing session carries. */
269
+ export type CapturingBase = {
270
+ /** The id {@link stopCapturing} takes. */
271
+ id: number;
272
+ /** The application's desktop file id. Empty when unknown. */
273
+ appId: string;
274
+ };
275
+ export declare const Capturing: {
276
+ InputCapture: (base: CapturingBase, body: InputCaptureBody) => {
277
+ devices: Devices;
278
+ /** The id {@link stopCapturing} takes. */
279
+ id: number;
280
+ /** The application's desktop file id. Empty when unknown. */
281
+ appId: string;
282
+ kind: CapturingKind.InputCapture;
283
+ };
284
+ RemoteDesktop: (base: CapturingBase, body: RemoteDesktopBody) => {
285
+ devices: Devices;
286
+ clipboard: boolean;
287
+ /** The id {@link stopCapturing} takes. */
288
+ id: number;
289
+ /** The application's desktop file id. Empty when unknown. */
290
+ appId: string;
291
+ kind: CapturingKind.RemoteDesktop;
292
+ };
293
+ Unknown: (base: CapturingBase, wireKind: string) => {
294
+ /** The id {@link stopCapturing} takes. */
295
+ id: number;
296
+ /** The application's desktop file id. Empty when unknown. */
297
+ appId: string;
298
+ kind: CapturingKind.Unknown;
299
+ wireKind: string;
300
+ };
301
+ };
302
+ export type Capturing = ReturnType<(typeof Capturing)[keyof typeof Capturing]>;
204
303
  /** What the portal functions need of the desktop `Shell` is handed. */
205
304
  export type PortalHost = Pick<DomicileHost, "answerPortalRequest"> & {
206
305
  addEventListener: (type: "portalrequests", listener: (event: MessageEvent<string>) => void) => void;
@@ -212,5 +311,12 @@ export type PortalHost = Pick<DomicileHost, "answerPortalRequest"> & {
212
311
  * whose body does not parse: the compositor and this SDK disagree.
213
312
  */
214
313
  export declare const watchPortalRequests: (host: PortalHost, listener: (requests: readonly PortalRequest[]) => void) => (() => void);
314
+ /**
315
+ * Call `listener` with every session that controls or captures input, on each
316
+ * change. Returns a function that stops listening.
317
+ */
318
+ export declare const watchCapturing: (host: PortalHost, listener: (sessions: readonly Capturing[]) => void) => (() => void);
215
319
  /** Answer request `id`. The first answer to reach the compositor wins. */
216
320
  export declare const answerPortalRequest: (host: PortalHost, id: number, answer: PortalAnswer) => void;
321
+ /** End capturing session `id`. */
322
+ export declare const stopCapturing: (host: PortalHost, id: number) => void;
package/dist/portal.js CHANGED
@@ -4,6 +4,7 @@
4
4
  //
5
5
  // The engine relays each request's body untyped; it is parsed here, by kind.
6
6
  // A new kind is one schema in `KINDS`, one `PortalKind` and one constructor.
7
+ // The same push lists the sessions that control or capture input.
7
8
  import { z } from "zod";
8
9
  export var PortalKind;
9
10
  (function (PortalKind) {
@@ -18,8 +19,12 @@ export var PortalKind;
18
19
  * lets go. Not a question: the compositor refuses answers to it.
19
20
  */
20
21
  PortalKind[PortalKind["Inhibit"] = 3] = "Inhibit";
22
+ /** Which input devices, and the clipboard, an application may control. */
23
+ PortalKind[PortalKind["RemoteDesktop"] = 4] = "RemoteDesktop";
24
+ /** Whether an application may take input that crosses a screen edge. */
25
+ PortalKind[PortalKind["InputCapture"] = 5] = "InputCapture";
21
26
  /** A kind this SDK cannot parse. Answer it with {@link PortalAnswer.Refused}. */
22
- PortalKind[PortalKind["Unknown"] = 4] = "Unknown";
27
+ PortalKind[PortalKind["Unknown"] = 6] = "Unknown";
23
28
  })(PortalKind || (PortalKind = {}));
24
29
  /** Which `FileChooser` method asked. */
25
30
  export var FileChooserMode;
@@ -59,6 +64,16 @@ export const PortalRequest = {
59
64
  body,
60
65
  kind: PortalKind.Inhibit,
61
66
  }),
67
+ InputCapture: (base, body) => ({
68
+ ...base,
69
+ body,
70
+ kind: PortalKind.InputCapture,
71
+ }),
72
+ RemoteDesktop: (base, body) => ({
73
+ ...base,
74
+ body,
75
+ kind: PortalKind.RemoteDesktop,
76
+ }),
62
77
  Unknown: (base, wireKind) => ({
63
78
  ...base,
64
79
  kind: PortalKind.Unknown,
@@ -73,10 +88,16 @@ export var PortalAnswerKind;
73
88
  PortalAnswerKind[PortalAnswerKind["AppChooser"] = 1] = "AppChooser";
74
89
  /** The user chose in a {@link PortalKind.FileChooser}. */
75
90
  PortalAnswerKind[PortalAnswerKind["FileChooser"] = 2] = "FileChooser";
91
+ /** The user granted a {@link PortalKind.RemoteDesktop} no more than it asked. */
92
+ PortalAnswerKind[PortalAnswerKind["RemoteDesktop"] = 3] = "RemoteDesktop";
93
+ /** The user allowed a {@link PortalKind.InputCapture} request. */
94
+ PortalAnswerKind[PortalAnswerKind["InputCapture"] = 4] = "InputCapture";
95
+ /** The user stopped a {@link Capturing} session. */
96
+ PortalAnswerKind[PortalAnswerKind["Stop"] = 5] = "Stop";
76
97
  /** The user dismissed or denied the dialog. */
77
- PortalAnswerKind[PortalAnswerKind["Canceled"] = 3] = "Canceled";
98
+ PortalAnswerKind[PortalAnswerKind["Canceled"] = 6] = "Canceled";
78
99
  /** The shell has no dialog for this kind. */
79
- PortalAnswerKind[PortalAnswerKind["Refused"] = 4] = "Refused";
100
+ PortalAnswerKind[PortalAnswerKind["Refused"] = 7] = "Refused";
80
101
  })(PortalAnswerKind || (PortalAnswerKind = {}));
81
102
  export const PortalAnswer = {
82
103
  Access: () => ({ kind: PortalAnswerKind.Access }),
@@ -90,7 +111,40 @@ export const PortalAnswer = {
90
111
  chosen,
91
112
  kind: PortalAnswerKind.FileChooser,
92
113
  }),
114
+ InputCapture: () => ({ kind: PortalAnswerKind.InputCapture }),
93
115
  Refused: () => ({ kind: PortalAnswerKind.Refused }),
116
+ RemoteDesktop: (devices, clipboard) => ({
117
+ clipboard,
118
+ devices,
119
+ kind: PortalAnswerKind.RemoteDesktop,
120
+ }),
121
+ Stop: () => ({ kind: PortalAnswerKind.Stop }),
122
+ };
123
+ export var CapturingKind;
124
+ (function (CapturingKind) {
125
+ /** An application controls input, and the clipboard when `clipboard` is set. */
126
+ CapturingKind[CapturingKind["RemoteDesktop"] = 0] = "RemoteDesktop";
127
+ /** An application takes the user's input once it crosses a screen edge. */
128
+ CapturingKind[CapturingKind["InputCapture"] = 1] = "InputCapture";
129
+ /** A kind this SDK cannot parse. It can still be stopped. */
130
+ CapturingKind[CapturingKind["Unknown"] = 2] = "Unknown";
131
+ })(CapturingKind || (CapturingKind = {}));
132
+ export const Capturing = {
133
+ InputCapture: (base, body) => ({
134
+ ...base,
135
+ ...body,
136
+ kind: CapturingKind.InputCapture,
137
+ }),
138
+ RemoteDesktop: (base, body) => ({
139
+ ...base,
140
+ ...body,
141
+ kind: CapturingKind.RemoteDesktop,
142
+ }),
143
+ Unknown: (base, wireKind) => ({
144
+ ...base,
145
+ kind: CapturingKind.Unknown,
146
+ wireKind,
147
+ }),
94
148
  };
95
149
  /**
96
150
  * Call `listener` with every unanswered request, oldest first, on each change.
@@ -108,10 +162,29 @@ export const watchPortalRequests = (host, listener) => {
108
162
  host.removeEventListener("portalrequests", heard);
109
163
  };
110
164
  };
165
+ /**
166
+ * Call `listener` with every session that controls or captures input, on each
167
+ * change. Returns a function that stops listening.
168
+ */
169
+ export const watchCapturing = (host, listener) => {
170
+ const heard = (event) => {
171
+ listener(requestsSchema
172
+ .parse(JSON.parse(event.data))
173
+ .capturing.map((item) => parseCapturing(item)));
174
+ };
175
+ host.addEventListener("portalrequests", heard);
176
+ return () => {
177
+ host.removeEventListener("portalrequests", heard);
178
+ };
179
+ };
111
180
  /** Answer request `id`. The first answer to reach the compositor wins. */
112
181
  export const answerPortalRequest = (host, id, answer) => {
113
182
  host.answerPortalRequest(id, JSON.stringify(wireAnswer(answer)));
114
183
  };
184
+ /** End capturing session `id`. */
185
+ export const stopCapturing = (host, id) => {
186
+ answerPortalRequest(host, id, PortalAnswer.Stop());
187
+ };
115
188
  const accessSchema = z
116
189
  .object({
117
190
  body: z.string(),
@@ -190,6 +263,16 @@ const inhibitSchema = z
190
263
  reason: body.reason,
191
264
  what: body.what.map((inhibited) => INHIBITED[inhibited]),
192
265
  }));
266
+ const devicesSchema = z.object({
267
+ keyboard: z.boolean(),
268
+ pointer: z.boolean(),
269
+ touchscreen: z.boolean(),
270
+ });
271
+ const remoteDesktopSchema = z.object({
272
+ clipboard: z.boolean(),
273
+ devices: devicesSchema,
274
+ });
275
+ const inputCaptureSchema = z.object({ devices: devicesSchema });
193
276
  /** Each kind's wire name, and how to read its body into a request. */
194
277
  const KINDS = new Map([
195
278
  [
@@ -208,6 +291,25 @@ const KINDS = new Map([
208
291
  "inhibit",
209
292
  (base, body) => PortalRequest.Inhibit(base, inhibitSchema.parse(body)),
210
293
  ],
294
+ [
295
+ "remote_desktop",
296
+ (base, body) => PortalRequest.RemoteDesktop(base, remoteDesktopSchema.parse(body)),
297
+ ],
298
+ [
299
+ "input_capture",
300
+ (base, body) => PortalRequest.InputCapture(base, inputCaptureSchema.parse(body)),
301
+ ],
302
+ ]);
303
+ /** Each capturing kind's wire name, and how to read its body into a session. */
304
+ const CAPTURING_KINDS = new Map([
305
+ [
306
+ "remote_desktop",
307
+ (base, body) => Capturing.RemoteDesktop(base, remoteDesktopSchema.parse(body)),
308
+ ],
309
+ [
310
+ "input_capture",
311
+ (base, body) => Capturing.InputCapture(base, inputCaptureSchema.parse(body)),
312
+ ],
211
313
  ]);
212
314
  const itemSchema = z.object({
213
315
  app_id: z.string(),
@@ -216,7 +318,15 @@ const itemSchema = z.object({
216
318
  kind: z.string(),
217
319
  parent_app_id: z.string().optional(),
218
320
  });
321
+ const capturingSchema = z.object({
322
+ app_id: z.string(),
323
+ body: z.unknown(),
324
+ id: z.number(),
325
+ kind: z.string(),
326
+ });
219
327
  const requestsSchema = z.object({
328
+ // Optional on the wire, so a line without it parses.
329
+ capturing: z.array(capturingSchema).default([]),
220
330
  items: z.array(itemSchema),
221
331
  type: z.literal("portal_requests"),
222
332
  });
@@ -241,6 +351,13 @@ const chooserMode = (mode) => {
241
351
  return FileChooserMode.SaveFiles;
242
352
  }
243
353
  };
354
+ const parseCapturing = (item) => {
355
+ const base = { appId: item.app_id, id: item.id };
356
+ const parse = CAPTURING_KINDS.get(item.kind);
357
+ return parse === undefined
358
+ ? Capturing.Unknown(base, item.kind)
359
+ : parse(base, item.body);
360
+ };
244
361
  /** `answer` as the compositor reads it. */
245
362
  const wireAnswer = (answer) => {
246
363
  switch (answer.kind) {
@@ -248,6 +365,16 @@ const wireAnswer = (answer) => {
248
365
  return { kind: "access" };
249
366
  case PortalAnswerKind.AppChooser:
250
367
  return { choice: answer.choice, kind: "app_chooser" };
368
+ case PortalAnswerKind.RemoteDesktop:
369
+ return {
370
+ clipboard: answer.clipboard,
371
+ devices: answer.devices,
372
+ kind: "remote_desktop",
373
+ };
374
+ case PortalAnswerKind.InputCapture:
375
+ return { kind: "input_capture" };
376
+ case PortalAnswerKind.Stop:
377
+ return { kind: "stop" };
251
378
  case PortalAnswerKind.Canceled:
252
379
  return { kind: "canceled" };
253
380
  case PortalAnswerKind.FileChooser:
package/package.json CHANGED
@@ -96,7 +96,7 @@
96
96
  "test:unit": "bun test"
97
97
  },
98
98
  "type": "module",
99
- "version": "0.0.0-alpha-e040773499e5",
99
+ "version": "0.0.0-alpha-019dff1d8582",
100
100
  "repository": {
101
101
  "type": "git",
102
102
  "url": "git+https://github.com/cprussin/domicile.git",