@domicile-desktop/system-bluetooth 0.0.0-alpha-a2a1a1a59e77

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/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,38 @@
1
+ # @domicile-desktop/system-bluetooth
2
+
3
+ BlueZ for a Domicile shell: each adapter and whether it is powered, the
4
+ connected devices, and turning an adapter on or off. Built on
5
+ [`@domicile-desktop/sdk/system`](../chrome-sdk/README.md).
6
+
7
+ ## Usage
8
+
9
+ ```ts
10
+ import { system } from "@domicile-desktop/sdk/system";
11
+ import { setPowered, watchBluetooth } from "@domicile-desktop/system-bluetooth/bluetooth";
12
+
13
+ const host = system(domicile);
14
+ const stop = watchBluetooth(host, (bluetooth) => {
15
+ bluetooth.match({
16
+ Err: () => hide(),
17
+ Ok: ({ adapters, connected }) => show(adapters, connected),
18
+ });
19
+ });
20
+ await setPowered(host, "/org/bluez/hci0", false);
21
+ ```
22
+
23
+ - Reads `GetManagedObjects` once, then again when an object comes or goes or
24
+ an adapter's `Powered` or a device's `Connected` or `Alias` changes.
25
+ - An `Err` is a D-Bus failure, such as BlueZ not running or an adapter
26
+ blocked by rfkill.
27
+ - A bug or an unexpected reply shape throws, logged to the console.
28
+
29
+ See [SHELL-SYSTEM-ACCESS.md](/docs/SHELL-SYSTEM-ACCESS.md).
30
+
31
+ ## Test
32
+
33
+ ```sh
34
+ bun run turbo test --filter @domicile-desktop/system-bluetooth
35
+ ```
36
+
37
+ Tests replay D-Bus replies, in the compositor's JSON, through an injected
38
+ `System`.
@@ -0,0 +1,28 @@
1
+ import type { Result } from "@cprussin/option-result";
2
+ import type { System, SystemError } from "@domicile-desktop/sdk/system";
3
+ /** A controller, such as `/org/bluez/hci0`. */
4
+ export type Adapter = {
5
+ path: string;
6
+ powered: boolean;
7
+ };
8
+ /** A connected device. `name` is its alias, which BlueZ fills from its name. */
9
+ export type Device = {
10
+ path: string;
11
+ name: string;
12
+ };
13
+ export type Bluetooth = {
14
+ adapters: Adapter[];
15
+ connected: Device[];
16
+ };
17
+ /** The calls this library makes. */
18
+ export type BluetoothSystem = Pick<System, "dbusCall" | "dbusMatch">;
19
+ /**
20
+ * Calls `onBluetooth` with the adapters and connected devices now and after
21
+ * each change, and returns a function that stops watching.
22
+ *
23
+ * An `Err` is a D-Bus failure, such as BlueZ not running. The watch keeps
24
+ * listening after one, so a later change reads again.
25
+ */
26
+ export declare const watchBluetooth: (system: BluetoothSystem, onBluetooth: (bluetooth: Result<Bluetooth, SystemError>) => void) => (() => void);
27
+ /** Turn the adapter at `adapter` on or off. */
28
+ export declare const setPowered: (system: Pick<System, "dbusCall">, adapter: string, powered: boolean) => Promise<Result<"set", SystemError>>;
@@ -0,0 +1,138 @@
1
+ // BlueZ over the system bus: adapters and connected devices, kept current.
2
+ // See docs/SHELL-SYSTEM-ACCESS.md.
3
+ import { Err } from "@cprussin/option-result";
4
+ import { Bus } from "@domicile-desktop/sdk/system";
5
+ import { z } from "zod";
6
+ const SERVICE = "org.bluez";
7
+ const ADAPTER = "org.bluez.Adapter1";
8
+ const DEVICE = "org.bluez.Device1";
9
+ /** The properties whose changes alter a {@link Bluetooth}, by interface. */
10
+ const WATCHED = {
11
+ [ADAPTER]: ["Powered"],
12
+ [DEVICE]: ["Alias", "Connected"],
13
+ };
14
+ /**
15
+ * Calls `onBluetooth` with the adapters and connected devices now and after
16
+ * each change, and returns a function that stops watching.
17
+ *
18
+ * An `Err` is a D-Bus failure, such as BlueZ not running. The watch keeps
19
+ * listening after one, so a later change reads again.
20
+ */
21
+ export const watchBluetooth = (system, onBluetooth) => {
22
+ const watch = { listening: undefined, stopped: false };
23
+ const report = (bluetooth) => {
24
+ if (!watch.stopped) {
25
+ onBluetooth(bluetooth);
26
+ }
27
+ };
28
+ follow(system, watch, report).catch((error) => {
29
+ // biome-ignore lint/suspicious/noConsole: surfacing a background failure
30
+ console.error("Failed to watch Bluetooth", error);
31
+ });
32
+ return () => {
33
+ watch.stopped = true;
34
+ watch.listening?.stop();
35
+ };
36
+ };
37
+ /** Turn the adapter at `adapter` on or off. */
38
+ export const setPowered = async (system, adapter, powered) => (await system.dbusCall({
39
+ body: [ADAPTER, "Powered", { signature: "b", value: powered }],
40
+ bus: Bus.System,
41
+ destination: SERVICE,
42
+ interface: "org.freedesktop.DBus.Properties",
43
+ member: "Set",
44
+ path: adapter,
45
+ signature: "ssv",
46
+ })).map(() => "set");
47
+ /** Listen, read, then read again on each change until the match ends. */
48
+ const follow = async (system, watch, report) => {
49
+ const matched = await system.dbusMatch({ bus: Bus.System, sender: SERVICE });
50
+ await matched.match({
51
+ Err: (error) => {
52
+ report(Err(error));
53
+ return Promise.resolve();
54
+ },
55
+ Ok: async (listening) => {
56
+ watch.listening = listening;
57
+ if (watch.stopped) {
58
+ listening.stop();
59
+ }
60
+ await changes(system, listening, report);
61
+ const ended = await listening.ended;
62
+ ended.match({
63
+ Err: (error) => {
64
+ report(Err(error));
65
+ },
66
+ Ok: () => {
67
+ /* stopped by the caller */
68
+ },
69
+ });
70
+ },
71
+ });
72
+ };
73
+ /** Read now, and again on each signal that changes what was read. */
74
+ const changes = async (system, listening, report) => {
75
+ const reader = listening.items.getReader();
76
+ report(await read(system));
77
+ for (let next = await reader.read(); !next.done; next = await reader.read()) {
78
+ if (matters(next.value)) {
79
+ report(await read(system));
80
+ }
81
+ }
82
+ };
83
+ /**
84
+ * Whether `signal` can change a {@link Bluetooth}: an object coming or going,
85
+ * or a watched property. Signal strength and the like change often during a
86
+ * scan and are skipped.
87
+ */
88
+ const matters = (signal) => {
89
+ switch (signal.member) {
90
+ case "InterfacesAdded":
91
+ case "InterfacesRemoved": {
92
+ return true;
93
+ }
94
+ case "PropertiesChanged": {
95
+ const [iface, changed] = propertiesChangedSchema.parse(signal.body);
96
+ return (WATCHED[iface] ?? []).some((name) => name in changed);
97
+ }
98
+ default: {
99
+ return false;
100
+ }
101
+ }
102
+ };
103
+ const read = async (system) => (await system.dbusCall({
104
+ bus: Bus.System,
105
+ destination: SERVICE,
106
+ interface: "org.freedesktop.DBus.ObjectManager",
107
+ member: "GetManagedObjects",
108
+ path: "/",
109
+ })).map(({ body }) => bluetoothOf(managedObjectsSchema.parse(body)[0]));
110
+ const bluetoothOf = (objects) => {
111
+ const entries = Object.entries(objects);
112
+ return {
113
+ adapters: entries.flatMap(([path, held]) => held[ADAPTER] === undefined
114
+ ? []
115
+ : [{ path, powered: held[ADAPTER].Powered }]),
116
+ connected: entries.flatMap(([path, held]) => held[DEVICE]?.Connected === true
117
+ ? [{ name: held[DEVICE].Alias, path }]
118
+ : []),
119
+ };
120
+ };
121
+ /** A `v`, as `domicile_host::dbus_json` writes it, read as its value. */
122
+ const variant = (value) => z.object({ value }).transform(({ value: held }) => held);
123
+ const managedObjectsSchema = z.tuple([
124
+ z.record(z.string(), z.object({
125
+ [ADAPTER]: z.object({ Powered: variant(z.boolean()) }).optional(),
126
+ [DEVICE]: z
127
+ .object({
128
+ Alias: variant(z.string()),
129
+ Connected: variant(z.boolean()),
130
+ })
131
+ .optional(),
132
+ })),
133
+ ]);
134
+ const propertiesChangedSchema = z.tuple([
135
+ z.string(),
136
+ z.record(z.string(), z.unknown()),
137
+ z.array(z.string()),
138
+ ]);
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "dependencies": {
3
+ "@cprussin/option-result": "^2.0.1",
4
+ "@domicile-desktop/sdk": "0.0.0-alpha-a2a1a1a59e77",
5
+ "zod": "^4.5.4"
6
+ },
7
+ "description": "BlueZ for a Domicile shell: whether each adapter is powered and which devices are connected, kept current, and turning adapters on and off.",
8
+ "devDependencies": {
9
+ "@cprussin/tsconfig": "^5.0.0",
10
+ "@types/bun": "^1.4.2",
11
+ "typescript": "^7.0.2"
12
+ },
13
+ "exports": {
14
+ "./bluetooth": {
15
+ "types": "./dist/bluetooth.d.ts",
16
+ "default": "./dist/bluetooth.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist/**"
21
+ ],
22
+ "license": "MIT",
23
+ "name": "@domicile-desktop/system-bluetooth",
24
+ "private": false,
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "repository": {
29
+ "directory": "packages/system-bluetooth",
30
+ "type": "git",
31
+ "url": "git+https://github.com/cprussin/domicile.git"
32
+ },
33
+ "scripts": {
34
+ "build": "tsc -p tsconfig.build.json",
35
+ "test:types": "tsc --noEmit",
36
+ "test:unit": "bun test"
37
+ },
38
+ "type": "module",
39
+ "version": "0.0.0-alpha-a2a1a1a59e77"
40
+ }