@domicile-desktop/sdk 0.0.0-alpha-f2862631a576

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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +235 -0
  3. package/dist/app-element.d.ts +93 -0
  4. package/dist/app-element.js +63 -0
  5. package/dist/audio.d.ts +57 -0
  6. package/dist/audio.js +9 -0
  7. package/dist/bind-keys.d.ts +63 -0
  8. package/dist/bind-keys.js +156 -0
  9. package/dist/chrome-message.d.ts +13 -0
  10. package/dist/chrome-message.js +26 -0
  11. package/dist/connect-to-host.d.ts +58 -0
  12. package/dist/connect-to-host.js +140 -0
  13. package/dist/cursor-shape.d.ts +40 -0
  14. package/dist/cursor-shape.js +69 -0
  15. package/dist/desktop-size.d.ts +38 -0
  16. package/dist/desktop-size.js +32 -0
  17. package/dist/device-pixel-ratio.d.ts +35 -0
  18. package/dist/device-pixel-ratio.js +25 -0
  19. package/dist/display-transform.d.ts +16 -0
  20. package/dist/display-transform.js +51 -0
  21. package/dist/domicile-client.d.ts +296 -0
  22. package/dist/domicile-client.js +697 -0
  23. package/dist/domicile-host.d.ts +1146 -0
  24. package/dist/domicile-host.js +42 -0
  25. package/dist/element-context.d.ts +25 -0
  26. package/dist/element-context.js +34 -0
  27. package/dist/element-transform.d.ts +29 -0
  28. package/dist/element-transform.js +44 -0
  29. package/dist/extension.d.ts +12 -0
  30. package/dist/extension.js +36 -0
  31. package/dist/file-preview.d.ts +53 -0
  32. package/dist/file-preview.js +53 -0
  33. package/dist/focus-app.d.ts +21 -0
  34. package/dist/focus-app.js +25 -0
  35. package/dist/focus-chrome.d.ts +21 -0
  36. package/dist/focus-chrome.js +25 -0
  37. package/dist/host-message.d.ts +592 -0
  38. package/dist/host-message.js +347 -0
  39. package/dist/host-stream.d.ts +9 -0
  40. package/dist/host-stream.js +65 -0
  41. package/dist/input.d.ts +7 -0
  42. package/dist/input.js +179 -0
  43. package/dist/key-action.d.ts +24 -0
  44. package/dist/key-action.js +26 -0
  45. package/dist/keybindings.d.ts +12 -0
  46. package/dist/keybindings.js +20 -0
  47. package/dist/keyboard-input.d.ts +3 -0
  48. package/dist/keyboard-input.js +190 -0
  49. package/dist/matrix.d.ts +27 -0
  50. package/dist/matrix.js +69 -0
  51. package/dist/measure.d.ts +35 -0
  52. package/dist/measure.js +435 -0
  53. package/dist/newline-frames.d.ts +2 -0
  54. package/dist/newline-frames.js +5 -0
  55. package/dist/notification.d.ts +59 -0
  56. package/dist/notification.js +23 -0
  57. package/dist/own-keybindings.d.ts +20 -0
  58. package/dist/own-keybindings.js +78 -0
  59. package/dist/pointer-input.d.ts +3 -0
  60. package/dist/pointer-input.js +164 -0
  61. package/dist/protocol.d.ts +596 -0
  62. package/dist/protocol.js +590 -0
  63. package/dist/register-elements.d.ts +15 -0
  64. package/dist/register-elements.js +37 -0
  65. package/dist/shell.d.ts +9 -0
  66. package/dist/shell.js +2 -0
  67. package/dist/shortcut-claims.d.ts +14 -0
  68. package/dist/shortcut-claims.js +35 -0
  69. package/dist/surface-coordinates.d.ts +15 -0
  70. package/dist/surface-coordinates.js +32 -0
  71. package/dist/theme.d.ts +7 -0
  72. package/dist/theme.js +40 -0
  73. package/dist/tray.d.ts +29 -0
  74. package/dist/tray.js +16 -0
  75. package/dist/webview-element.d.ts +497 -0
  76. package/dist/webview-element.js +312 -0
  77. package/dist/wheel-axis.d.ts +15 -0
  78. package/dist/wheel-axis.js +34 -0
  79. package/package.json +167 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Connor Prussin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,235 @@
1
+ # @domicile-desktop/sdk
2
+
3
+ > Published to npm, and usable outside this repo. If you are writing a shell,
4
+ > start with [/docs/WRITING-A-SHELL.md](/docs/WRITING-A-SHELL.md) — this is the
5
+ > reference for the package, that is the guide to using it.
6
+ >
7
+ > It ships built JavaScript and `.d.ts` from `dist/`, not the TypeScript in
8
+ > `src/`: run `bun run build` before anything outside the workspace resolves it.
9
+
10
+ The in-page half of Domicile. A Domicile chrome is ordinary web content; this
11
+ package is what lets that content talk to the compositor and mount real Wayland
12
+ clients as DOM elements.
13
+
14
+ It provides these:
15
+
16
+ - **`DomicileClient`** (`./domicile-client`) — the client for `window.domicile`, the
17
+ typed control channel the forked engine puts on a document it served. It
18
+ takes a `DomicileHost` and gives back a handler table for what the compositor
19
+ says and a typed call per thing the chrome asks of it. There is no handshake
20
+ and no wire: what it adds over the host itself is that it **registers its own
21
+ listeners in its constructor and holds what arrives before your `on` does** —
22
+ a DOM event dispatched with no listener is gone, and a React shell registers
23
+ tens of milliseconds after the compositor has announced every window already
24
+ running. For the same reason a page must never call `addEventListener` on
25
+ `window.domicile` itself. One message is not the compositor's: `open_url` is
26
+ an address `domicile open-url` asked the desktop to open — what `BROWSER`
27
+ runs inside it. Opening it is the shell's.
28
+ - **`Shell`** (`./shell`) — the type of a shell module's `Shell` export: what
29
+ Domicile calls, once, with the element to draw in. A module without one is
30
+ refused on the screen.
31
+ - **`registerElements`** (`./register-elements`) — the input routing behind the
32
+ engine's `<app>` tag. It forwards the pointer over a window to the client
33
+ underneath in that client's own surface coordinates, and routes the page's
34
+ keystrokes to whichever window was last reached for. Nothing here says how big
35
+ a window is: an `<app>`'s layout box *is* the client's
36
+ `xdg_toplevel.configure`, and the engine states it off the layout it
37
+ performed. All of it is delegated from `document` over
38
+ `closest("app")`: the tag is the engine's, so there is no element class to hang
39
+ any of it on, and nothing here is registered. A click on a window fires a
40
+ cancelable `domicile-focus-requested` and then focuses the client, and a press
41
+ that lands off every window fires a cancelable
42
+ `domicile-focus-release-requested` on the window that holds the keyboard and
43
+ then hands it back to the page — so a shell that wants focus to be its own
44
+ decision calls `preventDefault()` on either, and a shell with no opinion needs
45
+ to know nothing about them. A right-click over a window also loses the
46
+ browser's own context menu, because the press is forwarded and the menu a
47
+ right-click asked for is the client's, drawn inside its surface. Only over an
48
+ `<app>`: a `contextmenu` anywhere else — the desktop, a shell's chrome, the
49
+ page inside a `<webview>` — is left uncanceled and keeps its menu.
50
+ - **`<app>`** (`./app-element`) — the tag name, the two focus events, and the
51
+ TypeScript for the element, which is the fork's. The surface embed and the size
52
+ a client is configured at are both the layout box's and the engine reports
53
+ them; there is nothing to call.
54
+ - **`focusApp`** (`./focus-app`) — put the keyboard on a client without a click.
55
+ Not the same as `DomicileClient.focusApp`, which asks the compositor and stops:
56
+ keyboard events reach `document` rather than an element, so the SDK has to be
57
+ told too.
58
+ - **`<webview>`** (`./webview-element`) — types and event names only. The
59
+ element is the engine's: `src` is the address it loads, `goBack` /
60
+ `goForward` / `stop` / `reload` are what a chrome's address bar drives it
61
+ with, `canGoBack` / `canGoForward` say whether the first two would do
62
+ anything, `loading` says whether a page is still arriving, and `focus` puts
63
+ the keyboard on the embedded page. What this module adds is the TypeScript
64
+ for all of that plus the names of the four events the browser process
65
+ dispatches on it, `domicile-guest-focus`, `domicile-history-change`,
66
+ `domicile-loading-change` and `domicile-new-window`. The last is the only one
67
+ that carries anything — `event.url`, the address a link with
68
+ `target="_blank"` asked to open — because it is the only one that is not about
69
+ state the element already holds: the browser process opens no window for a
70
+ guest, so a shell that ignores it is a desktop where such a link does
71
+ nothing.
72
+ `domicile-close` is the page calling `window.close()` — an extension's popup
73
+ closing itself — and removing the view is the shell's answer.
74
+ `domicile-content-size-change` says `contentWidth` / `contentHeight` moved:
75
+ the size the page's content wants, which Chrome sizes an extension's popup
76
+ from.
77
+ `domicile-popup-window` is an extension's `chrome.windows.create` with a
78
+ popup: `windowId`, `url`, `width` and `height` (0 where it named none). The
79
+ shell opens a window whose view carries `popupwindow="<windowId>"` from its
80
+ first render — the engine reads it once, as the view is connected — and that
81
+ view is then the extension's window.
82
+ - **`Extension`** (`./extension`) — one row of the tray, as
83
+ `DomicileClient.on("extensions", …)` delivers it, and the Zod schema it is
84
+ parsed with. A click on one is `DomicileClient.activateExtension(id)`, which
85
+ grants it `activeTab`; one with a `popup` is then a `<webview>` at that
86
+ address.
87
+ - **`TrayItem`, `TrayAction`** (`./tray`) — one icon of the system tray, as
88
+ `DomicileClient.on("tray", …)` delivers it, and the button a click was. A
89
+ click is `DomicileClient.activateTrayItem(id, action)`.
90
+ - **`bindKeys`** (`./bind-keys`) — a shell's own keys, claimed and answered:
91
+ `bindKeys(domicile, { keybindings, modes }, { onCommand, onModeChanged })`,
92
+ each table a chord (`"Meta+Shift+l"`) to a `KeyAction`. The chords are
93
+ resolved by `./own-keybindings` against the keyboard the compositor
94
+ describes (`shell_config`), again whenever the layout moves. A
95
+ `KeyAction.SendShell(words)` calls `onCommand(words)`, whose meaning is the
96
+ shell's; `KeyAction.Mode(name)` is answered here, and `onModeChanged` says
97
+ the mode moved. It returns `{ unbind, setMode }`: `setMode` is how a desktop
98
+ of several pages keeps one mode across them (one the shell lacks goes back
99
+ to `default`). It owns the `shell_config` and `shortcut` slots of
100
+ `DomicileClient.on`, and the chords it claims are never given back — the
101
+ channel cannot release one. `./keybindings` is its pure half (what a press
102
+ does in a mode) and `./key-action` the action a binding carries.
103
+ - **`Notification`** (`./notification`) — one notification, as
104
+ `DomicileClient.on("notifications", …)` delivers it: an application's or a
105
+ site's. A press is `invokeNotificationAction(id, key)`, a clear
106
+ `dismissNotifications(ids)`.
107
+ - **`connectToHost`** (`./connect-to-host`) — the `DomicileHost` off the
108
+ document, or a stand-in that does nothing when there is none. `hasHost` is
109
+ beside it for code that needs the answer rather than the object.
110
+ - **`reportDesktopSize`** (`./desktop-size`) — tell the host how big the page
111
+ is, and keep telling it. The desktop spans every display and the page is what
112
+ measures it, so a shell that never reported would leave the compositor
113
+ laying windows out against a size it guessed.
114
+ - **`reportDevicePixelRatio`** (`./device-pixel-ratio`) — tell the host what
115
+ density the page is drawing at, and keep telling it. The ratio changes when
116
+ the window moves to another display or the page is zoomed, and the page is
117
+ the only part of Domicile that can see either; a chrome that reported it once
118
+ would leave every client drawing at the old resolution.
119
+ - **Pure helpers** — affine `./matrix` math mirroring the Rust
120
+ `domicile-scene::Transform`, `./domicile-host` mirroring the engine's IDL,
121
+ `./host-message` for what the client delivers and how an event becomes one,
122
+ `./cursor-shape` for the keyword set a client can ask for, and `./input`
123
+ keycode mapping.
124
+ - **The routing parts, published so they can be substituted** — `./measure` is
125
+ what `registerElements` takes an override of, and `./element-transform` and
126
+ `./surface-coordinates` are what invert a window's affine so a click under a
127
+ CSS rotation — or a `zoom`, which is not a transform and is carried
128
+ separately — lands where the user pressed. The one construct they cannot
129
+ invert is a perspective projection, which is not an affine at all; `./measure`
130
+ maps the window flat and says so on the console. A shell needs none of them;
131
+ a test of one does.
132
+ - **The compositor's own JSON wire**, which **a page no longer speaks** —
133
+ `./protocol`, `./chrome-message`, `./newline-frames` and `./host-stream` are
134
+ there for `@domicile-desktop/e2e-harness`, a headless stand-in for a chrome that
135
+ talks to the compositor's socket directly. The one exception is
136
+ `shell_config`, which the engine forwards as the compositor's line:
137
+ `./host-message` parses it with `./protocol`'s schema.
138
+
139
+ ## Usage
140
+
141
+ ```ts
142
+ import { DomicileClient } from "@domicile-desktop/sdk/domicile-client";
143
+ import { connectToHost } from "@domicile-desktop/sdk/connect-to-host";
144
+ import { registerElements } from "@domicile-desktop/sdk/register-elements";
145
+
146
+ const domicile = new DomicileClient(connectToHost(window));
147
+ registerElements(domicile);
148
+ ```
149
+
150
+ That is the whole of it. There is nothing to await: `connect()` is gone with
151
+ the handshake it performed, the compositor's protocol version is checked in the
152
+ browser process and only logged, and the first call on the channel is what
153
+ binds it. Say what you have to say as soon as you have a domicile.
154
+
155
+ Opened in an ordinary browser there is no `window.domicile` at all —
156
+ `vite dev` on a shell's page is a real thing to do — and `connectToHost` hands
157
+ back a stand-in that does nothing and says so once on the console. Ask
158
+ `hasHost(window)` if your own code needs the answer.
159
+
160
+ `navigator.domicile` is the same object and still reads, so `connectToHost`
161
+ takes either global and prefers neither. `window.domicile` is the spelling the
162
+ guides use.
163
+
164
+ Then render `<app app-id="…">` / `<webview src="…">` as normal DOM and style
165
+ them with ordinary CSS — rounding, blur, transforms, and z-index all apply to the
166
+ live client surface. That is the whole point of Domicile.
167
+
168
+ Both tags are the fork's own HTML elements, so there is nothing to register and
169
+ nothing here to wrap them in — a custom element's name must contain a hyphen, per
170
+ spec, which is exactly why they are the engine's. Note what that costs in a React
171
+ chrome: a tag without a hyphen is an ordinary HTML element to React, so it writes
172
+ no unrecognized property and binds no `on…` prop for an event it has not heard
173
+ of. Bind the events on a ref.
174
+
175
+ ### Knowing which modifiers are held
176
+
177
+ **Read them off your own key events.** The desktop is the chrome's window, so
178
+ the page hears every key the user presses whatever the compositor's seat is
179
+ pointed at — that is how `registerElements` forwards a keystroke to a client in
180
+ the first place:
181
+
182
+ ```ts
183
+ const follow = (event: KeyboardEvent) => {
184
+ // While Alt is held, let the pointer reach the page rather than the window
185
+ // it is over: `pointer-events: none` is what tells the compositor the
186
+ // window is not taking clicks, and it hit-tests accordingly.
187
+ portal.style.pointerEvents = event.altKey ? "none" : "";
188
+ };
189
+ document.addEventListener("keydown", follow);
190
+ document.addEventListener("keyup", follow);
191
+ ```
192
+
193
+ **Not off the host's `modifiers` message, today.** It reports the compositor's
194
+ seat, and the seat only knows the keys this page forwarded to it — which is
195
+ only the keys pressed while a client held the keyboard. A modifier pressed
196
+ while the chrome held it never reaches the seat, and the next forwarded key
197
+ makes the message deny it: a shell that believed the message over its own
198
+ keystrokes read a held Alt as let go of. The message is still sent, and is what
199
+ will say so on the day the compositor reads input itself rather than being
200
+ handed it by this page; it cannot know more than this page until then.
201
+
202
+ ## Dependencies
203
+
204
+ `zod`, in two places and both of them boundaries. `./protocol` parses the
205
+ compositor's JSON for the headless harness rather than casting it. And
206
+ `./cursor-shape` parses one field off the typed channel — not because the
207
+ engine fails to check it, since `DomicileAppCursorEvent.cursor` is a WebIDL
208
+ `enum` over the same closed set, but because this package and the engine are
209
+ published apart. A shape this list has and the running engine does not arrives
210
+ as a keyword no `DomicileCursorShape` names, and an unknown keyword assigned to
211
+ `style.cursor` fails silently.
212
+
213
+ Nothing else. `@cprussin/option-result` was a dependency for exactly one
214
+ outcome — `connect()` returning `Result<number, HandshakeFailure>` — and there
215
+ is no handshake to fail. Everything here either throws (a bug, per
216
+ [ERRORS.md](/docs/guidelines/ERRORS.md)) or returns `T | undefined` for
217
+ ordinary absence.
218
+
219
+ ## Test
220
+
221
+ ```sh
222
+ bun run turbo test --filter @domicile-desktop/sdk
223
+ ```
224
+
225
+ DOM-dependent suites run against happy-dom via
226
+ [`@domicile-desktop/test-support`](../test-support/README.md). That DOM performs no
227
+ layout, so the routing tests inject a `measure` stub through
228
+ `registerElements(domicile, { measure })` rather than relying on
229
+ `getBoundingClientRect`.
230
+
231
+ happy-dom has never heard of `<app>`, so it creates one as an
232
+ `HTMLUnknownElement`, which React's development build reports on the console as
233
+ an unrecognized tag. It cannot happen on the fork, where the tag is
234
+ `HTMLAppElement`; React exempts `dialog` and `webview` from that report by name
235
+ and there is no way to add a third.
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The tag a shell writes.
3
+ *
4
+ * Exported because the SDK's own delegation and a shell's stylesheets and tests
5
+ * all have to name it, and one literal is better than five. Nothing registers
6
+ * it: `customElements.define` cannot take a name without a hyphen, which is
7
+ * exactly why the fork defines the element instead.
8
+ */
9
+ export declare const APP_TAG_NAME = "app";
10
+ /**
11
+ * Fired on an `<app>` when something asks for the keyboard on its behalf — a
12
+ * click, today — and cancelable, because who holds the keyboard is the
13
+ * shell's to decide rather than the SDK's.
14
+ *
15
+ * Left uncanceled it focuses the client, so a shell with no focus policy of
16
+ * its own needs to know nothing about this. A shell that has one — focus that
17
+ * follows the pointer, a window that may not be interrupted, a click that
18
+ * raises without focusing — calls `preventDefault()` and then does whatever it
19
+ * decided, which is usually `focusApp` a moment later.
20
+ *
21
+ * THE SDK DISPATCHES THIS, unlike `<webview>`'s two events, which the engine
22
+ * does. A click inside an `<app>` is a pointer event in this document — the
23
+ * client is a surface rather than a browsing context — so the page is where the
24
+ * question can be asked at all.
25
+ *
26
+ * It bubbles: a shell renders one `<app>` per window and would otherwise have
27
+ * to bind a listener to each.
28
+ */
29
+ export declare const APP_FOCUS_REQUESTED_EVENT = "domicile-focus-requested";
30
+ /** The detail of an {@link APP_FOCUS_REQUESTED_EVENT}. */
31
+ export type AppFocusRequest = {
32
+ /** The host's name for the client whose window was reached for. */
33
+ appId: string;
34
+ };
35
+ /**
36
+ * Fired on the `<app>` that holds the keyboard when a press lands off every
37
+ * window, and cancelable for the same reason its opposite is: the keyboard
38
+ * leaving a window is a move of it, and every move is the shell's to decide.
39
+ *
40
+ * Left uncanceled the keyboard goes back to the page, which is what a press on
41
+ * the desktop means and what a shell with no window chrome of its own wants.
42
+ * A shell that draws chrome *for* a window — a title bar, the sheet a drag is
43
+ * caught on — calls `preventDefault()` when the press landed on it: that is a
44
+ * reach for the window rather than away from it, and the SDK cannot tell the
45
+ * two apart because nothing in the press says which window a `<div>` belongs
46
+ * to. {@link AppFocusReleaseRequest.pressed} is what the shell reads to say.
47
+ *
48
+ * It bubbles, the way {@link APP_FOCUS_REQUESTED_EVENT} does and for the same
49
+ * reason.
50
+ */
51
+ export declare const APP_FOCUS_RELEASE_REQUESTED_EVENT = "domicile-focus-release-requested";
52
+ /** The detail of an {@link APP_FOCUS_RELEASE_REQUESTED_EVENT}. */
53
+ export type AppFocusReleaseRequest = {
54
+ /** The host's name for the client that is about to lose the keyboard. */
55
+ appId: string;
56
+ /**
57
+ * What the press landed on, or `undefined` where it landed on nothing an
58
+ * element can be read off.
59
+ */
60
+ pressed: Element | undefined;
61
+ };
62
+ /**
63
+ * What an `<app>` is, to everything holding one.
64
+ *
65
+ * Global rather than exported, and declared rather than imported, for the same
66
+ * two reasons `<webview>`'s interface is. React resolves a `ref` on a tag to
67
+ * whatever global interface the tag-name map names, so an identically-shaped
68
+ * interface exported from here would be a *different* type that a shell holding
69
+ * the ref could not assign anywhere — the trap `<webview>` fell into, where
70
+ * `@types/react` had already declared the name. Nothing has declared this one,
71
+ * so the SDK is free to; it is declared the same way anyway, because the shape
72
+ * of the answer should not depend on who got there first.
73
+ *
74
+ * And there is nothing to import from: the interface is the fork's, and the
75
+ * engine ships no `.d.ts`. A shell that runs on stock Chromium gets an
76
+ * `HTMLUnknownElement` with none of it — the same trade `<webview>` makes, and
77
+ * the reason the SDK's delegation reads `app-id` off the attribute rather than
78
+ * off this property.
79
+ */
80
+ declare global {
81
+ interface HTMLAppElement extends HTMLElement {
82
+ /**
83
+ * Which window this element shows. Reflected, so the attribute and the
84
+ * property are one value, the way `<img src>` is — and empty rather than
85
+ * absent when the attribute is not set, which is what a reflected
86
+ * `DOMString` does.
87
+ */
88
+ appId: string;
89
+ }
90
+ interface HTMLElementTagNameMap {
91
+ app: HTMLAppElement;
92
+ }
93
+ }
@@ -0,0 +1,63 @@
1
+ // The fork's `<app>`: a Wayland client's window, laid out by the page.
2
+ //
3
+ // There is no element class here, and that is the point. The element belongs to
4
+ // the engine — `app-id` is a reflected content attribute, and the surface embed
5
+ // and the size the client is configured at are both the layout box's, reported
6
+ // by `LayoutAppSurface` without anything in the page asking. What is left for
7
+ // the SDK to say is the part TypeScript cannot read off the fork: what the tag
8
+ // is, and what the SDK dispatches on it.
9
+ //
10
+ // It used to be a `<domicile-app>` custom element, because a custom element's
11
+ // name must contain a hyphen and the SDK predates the fork. That element did
12
+ // far more than forward: it created a `<canvas>` and called
13
+ // `embedExternalSurface` on it, it measured its own box and reported it, it
14
+ // mapped pointer coordinates, and it took five methods and three properties a
15
+ // shell wrote to. The canvas and the embed are the engine's now; the rest moved
16
+ // to document-level delegation over `closest("app")`, which is what
17
+ // `registerElements` installs — see `pointer-input.ts` and `keyboard-input.ts`.
18
+ /**
19
+ * The tag a shell writes.
20
+ *
21
+ * Exported because the SDK's own delegation and a shell's stylesheets and tests
22
+ * all have to name it, and one literal is better than five. Nothing registers
23
+ * it: `customElements.define` cannot take a name without a hyphen, which is
24
+ * exactly why the fork defines the element instead.
25
+ */
26
+ export const APP_TAG_NAME = "app";
27
+ /**
28
+ * Fired on an `<app>` when something asks for the keyboard on its behalf — a
29
+ * click, today — and cancelable, because who holds the keyboard is the
30
+ * shell's to decide rather than the SDK's.
31
+ *
32
+ * Left uncanceled it focuses the client, so a shell with no focus policy of
33
+ * its own needs to know nothing about this. A shell that has one — focus that
34
+ * follows the pointer, a window that may not be interrupted, a click that
35
+ * raises without focusing — calls `preventDefault()` and then does whatever it
36
+ * decided, which is usually `focusApp` a moment later.
37
+ *
38
+ * THE SDK DISPATCHES THIS, unlike `<webview>`'s two events, which the engine
39
+ * does. A click inside an `<app>` is a pointer event in this document — the
40
+ * client is a surface rather than a browsing context — so the page is where the
41
+ * question can be asked at all.
42
+ *
43
+ * It bubbles: a shell renders one `<app>` per window and would otherwise have
44
+ * to bind a listener to each.
45
+ */
46
+ export const APP_FOCUS_REQUESTED_EVENT = "domicile-focus-requested";
47
+ /**
48
+ * Fired on the `<app>` that holds the keyboard when a press lands off every
49
+ * window, and cancelable for the same reason its opposite is: the keyboard
50
+ * leaving a window is a move of it, and every move is the shell's to decide.
51
+ *
52
+ * Left uncanceled the keyboard goes back to the page, which is what a press on
53
+ * the desktop means and what a shell with no window chrome of its own wants.
54
+ * A shell that draws chrome *for* a window — a title bar, the sheet a drag is
55
+ * caught on — calls `preventDefault()` when the press landed on it: that is a
56
+ * reach for the window rather than away from it, and the SDK cannot tell the
57
+ * two apart because nothing in the press says which window a `<div>` belongs
58
+ * to. {@link AppFocusReleaseRequest.pressed} is what the shell reads to say.
59
+ *
60
+ * It bubbles, the way {@link APP_FOCUS_REQUESTED_EVENT} does and for the same
61
+ * reason.
62
+ */
63
+ export const APP_FOCUS_RELEASE_REQUESTED_EVENT = "domicile-focus-release-requested";
@@ -0,0 +1,57 @@
1
+ /** A port of a device, or a profile of a card: something to switch it to. */
2
+ export type AudioChoice = {
3
+ /** What `setAudioPort` and `setAudioProfile` name it by. */
4
+ name: string;
5
+ description: string;
6
+ /**
7
+ * `false` for a port whose jack is empty, or a profile that needs one.
8
+ * Still choosable, as every mixer lets it be.
9
+ */
10
+ available: boolean;
11
+ };
12
+ /** An output (a sink) or an input (a source). */
13
+ export type AudioDevice = {
14
+ id: string;
15
+ description: string;
16
+ /**
17
+ * The loudest of its channels, as a fraction of the server's 100% — more
18
+ * than 1 for one turned up past it.
19
+ */
20
+ volume: number;
21
+ muted: boolean;
22
+ /** Whether new streams go to it. */
23
+ default: boolean;
24
+ /**
25
+ * Whether it is an output's monitor — what the output plays, as something
26
+ * to record. Always `false` for an output.
27
+ */
28
+ monitor: boolean;
29
+ /** Speakers, headphones, a line in. Often empty. */
30
+ ports: readonly AudioChoice[];
31
+ /** The {@link AudioChoice.name} of the port in use, if it has ports. */
32
+ port: string | undefined;
33
+ };
34
+ /** Something playing (a sink input) or recording (a source output). */
35
+ export type AudioStream = {
36
+ id: string;
37
+ /** Who is playing or recording it. */
38
+ application: string;
39
+ /** What it is — a song, a call — where the application said. */
40
+ title: string | undefined;
41
+ volume: number;
42
+ muted: boolean;
43
+ /**
44
+ * The {@link AudioDevice.id} it plays to or records from, or `undefined`
45
+ * for one the next message will settle.
46
+ */
47
+ device: string | undefined;
48
+ };
49
+ /** A sound card, and the profiles that say which of its devices are on. */
50
+ export type AudioCard = {
51
+ id: string;
52
+ description: string;
53
+ /** Best first. */
54
+ profiles: readonly AudioChoice[];
55
+ /** The {@link AudioChoice.name} of the profile in use. */
56
+ profile: string | undefined;
57
+ };
package/dist/audio.js ADDED
@@ -0,0 +1,9 @@
1
+ // The desk's sound, as a shell's mixer draws it and asks of it.
2
+ //
3
+ // Its own module for `tray.ts`'s reason: both halves of the SDK need these
4
+ // shapes, and only one of them is the wire. The compositor reads the sound
5
+ // server with `pactl` — PulseAudio's or PipeWire's — see `domicile_host::audio`.
6
+ //
7
+ // Every id is the compositor's, opaque here: what a request names a device or
8
+ // a stream by, taken from the last `audio` message.
9
+ export {};
@@ -0,0 +1,63 @@
1
+ import type { DomicileClient } from "./domicile-client";
2
+ import type { HostMessageOf, HostMessageType } from "./host-message";
3
+ import type { ShellKeybindings } from "./own-keybindings";
4
+ /** What a shell is told as its keys are answered. */
5
+ export type KeyHandlers = {
6
+ /** A `send-shell` binding was pressed: the words after `send-shell`. */
7
+ onCommand: (args: readonly string[]) => void;
8
+ /** The keys are read in another binding mode now. */
9
+ onModeChanged: (mode: string) => void;
10
+ };
11
+ /** What {@link bindKeys} hands back. */
12
+ export type KeyBinding = {
13
+ /** Read the keys in `name` from now on — see {@link bindKeys}. */
14
+ setMode: (name: string) => void;
15
+ /** Stop answering. The claims stay. */
16
+ unbind: () => void;
17
+ };
18
+ /**
19
+ * What `bindKeys` uses of a client: a {@link DomicileClient}, or anything with
20
+ * its handler slots and its claim.
21
+ */
22
+ type KeyClient = {
23
+ grabShortcut: DomicileClient["grabShortcut"];
24
+ on<T extends HostMessageType>(type: T, handler: (message: HostMessageOf<T>) => void): unknown;
25
+ off<T extends HostMessageType>(type: T, handler: (message: HostMessageOf<T>) => void): unknown;
26
+ };
27
+ /**
28
+ * Answer the keys a shell binds, `own`.
29
+ *
30
+ * **`own` is resolved as each keyboard arrives** — `shell_config`, every
31
+ * keysym the layout types and the key it is on — so a reload that changes the
32
+ * layout moves the keys with it. A chord written wrong, or whose keysym the
33
+ * keyboard cannot type, throws there.
34
+ *
35
+ * **This owns `shell_config` and `shortcut`.** {@link DomicileClient.on} is a
36
+ * single slot per message, so a shell that registers either of its own
37
+ * displaces this.
38
+ *
39
+ * **A claim is never given back.** Every chord of every mode is claimed as
40
+ * each keyboard arrives, because the channel has no way to release one — so a
41
+ * layout change leaves the keys it moved off the desktop's, answering nothing,
42
+ * until the shell's page reloads. And for that reason, a bare key bound in a
43
+ * mode is taken from every client for the whole session, not only while the
44
+ * mode is on.
45
+ *
46
+ * **The mode is tracked here, and a shell can set it.** A desktop of several
47
+ * pages shares one mode across them — a key that entered it may have landed on
48
+ * another page — so `setMode` is how a page is told: the keys are read in that
49
+ * mode from then on, and `onModeChanged` is not called for it, the shell
50
+ * having said so itself. The mode the keys are already in does nothing; one
51
+ * the shell does not have goes back to `default`, and that is reported. Before
52
+ * any keyboard, the mode is kept until one arrives and is checked then.
53
+ *
54
+ * **Bind once per client.** The keyboard arrives once and again only when it
55
+ * changes, so keys bound anew after an unbind answer nothing until the next
56
+ * change of it. A React shell binds in an effect that depends on the client
57
+ * and its keys, and reads anything else it needs when a key is pressed.
58
+ *
59
+ * @returns `unbind`, which stops the answering — the claims stay, as above —
60
+ * and `setMode`.
61
+ */
62
+ export declare const bindKeys: (domicile: KeyClient, own: ShellKeybindings, { onCommand, onModeChanged }: KeyHandlers) => KeyBinding;
63
+ export {};