@domicile-desktop/system-backlight 0.0.0-alpha-a330bf5f3aae
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 +21 -0
- package/README.md +42 -0
- package/dist/raw-for.d.ts +8 -0
- package/dist/raw-for.js +14 -0
- package/dist/read-backlight.d.ts +21 -0
- package/dist/read-backlight.js +64 -0
- package/dist/set-brightness.d.ts +26 -0
- package/dist/set-brightness.js +76 -0
- package/dist/watch-brightness.d.ts +24 -0
- package/dist/watch-brightness.js +117 -0
- package/package.json +48 -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,42 @@
|
|
|
1
|
+
# @domicile-desktop/system-backlight
|
|
2
|
+
|
|
3
|
+
Screen brightness for a Domicile shell, on the system calls of
|
|
4
|
+
[`@domicile-desktop/sdk/system`](/packages/chrome-sdk/src/system.ts). See
|
|
5
|
+
[SHELL-SYSTEM-ACCESS.md](/docs/SHELL-SYSTEM-ACCESS.md).
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { system } from "@domicile-desktop/sdk/system";
|
|
11
|
+
import { brightnessSetter } from "@domicile-desktop/system-backlight/set-brightness";
|
|
12
|
+
import { watchBrightness } from "@domicile-desktop/system-backlight/watch-brightness";
|
|
13
|
+
|
|
14
|
+
const host = system(domicile);
|
|
15
|
+
const watch = await watchBrightness(host, (level) => show(level));
|
|
16
|
+
const set = brightnessSetter(host);
|
|
17
|
+
await set(0.5);
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- `readBacklight` (`./read-backlight`): the preferred device under
|
|
21
|
+
`/sys/class/backlight`: firmware, then platform, then raw, in systemd's
|
|
22
|
+
order; ties by name. `None` without one.
|
|
23
|
+
- `watchBrightness`: the level (0 to 1) now and on each change of a whole
|
|
24
|
+
percent. Re-reads on each `udevadm monitor` uevent and every two minutes.
|
|
25
|
+
- `brightnessSetter`: sets a level through logind's
|
|
26
|
+
`org.freedesktop.login1.Session.SetBrightness`. Never sets zero, which turns
|
|
27
|
+
most panels off. Sends one level at a time; a level superseded while waiting
|
|
28
|
+
resolves `SetOutcome.Superseded`.
|
|
29
|
+
- Every call resolves a `Result<…, SystemError>`. A level that is not a number
|
|
30
|
+
throws.
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- `udevadm` on the compositor's `PATH` (systemd).
|
|
35
|
+
- A logind session.
|
|
36
|
+
- Watching and setting work while the desktop is locked, so a lock screen can
|
|
37
|
+
adjust the brightness ([LOCK.md](/docs/LOCK.md)).
|
|
38
|
+
|
|
39
|
+
## Testing
|
|
40
|
+
|
|
41
|
+
`bun run turbo test --filter @domicile-desktop/system-backlight`. Tests answer
|
|
42
|
+
the system calls from files recorded on a ThinkPad (`src/fake-system.ts`).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Backlight } from "./read-backlight";
|
|
2
|
+
/**
|
|
3
|
+
* The raw value for `level`, clamped to 0..1 and rounded.
|
|
4
|
+
*
|
|
5
|
+
* Never zero: most panels turn off at zero, leaving no visible slider to raise
|
|
6
|
+
* it again. Throws on a level that is not finite, which is a caller's bug.
|
|
7
|
+
*/
|
|
8
|
+
export declare const rawFor: ({ max }: Backlight, level: number) => number;
|
package/dist/raw-for.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The raw value for `level`, clamped to 0..1 and rounded.
|
|
3
|
+
*
|
|
4
|
+
* Never zero: most panels turn off at zero, leaving no visible slider to raise
|
|
5
|
+
* it again. Throws on a level that is not finite, which is a caller's bug.
|
|
6
|
+
*/
|
|
7
|
+
export const rawFor = ({ max }, level) => {
|
|
8
|
+
if (Number.isFinite(level)) {
|
|
9
|
+
return Math.max(1, Math.round(Math.min(1, Math.max(0, level)) * max));
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
throw new Error(`a brightness must be a number, not ${level}`);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Option, Result } from "@cprussin/option-result";
|
|
2
|
+
import type { System, SystemError } from "@domicile-desktop/sdk/system";
|
|
3
|
+
/** The calls {@link readBacklight} makes. */
|
|
4
|
+
export type SysfsHost = Pick<System, "readDir" | "readTextFile">;
|
|
5
|
+
/** One backlight device's current and maximum raw brightness. */
|
|
6
|
+
export type Backlight = {
|
|
7
|
+
/** The name under `/sys/class/backlight`, which logind takes. */
|
|
8
|
+
device: string;
|
|
9
|
+
raw: number;
|
|
10
|
+
max: number;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The backlight to show and set, or `None` if there is none.
|
|
14
|
+
*
|
|
15
|
+
* Picks the preferred type, breaking ties by name so the choice is stable.
|
|
16
|
+
* Skips a device whose files are missing or not numbers, or whose maximum is
|
|
17
|
+
* zero.
|
|
18
|
+
*/
|
|
19
|
+
export declare const readBacklight: (host: SysfsHost) => Promise<Result<Option<Backlight>, SystemError>>;
|
|
20
|
+
/** Brightness from 0 to 1. */
|
|
21
|
+
export declare const levelOf: ({ max, raw }: Backlight) => number;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Reads the screen's backlight from `/sys/class/backlight`.
|
|
2
|
+
import { Err, None, Ok, Some } from "@cprussin/option-result";
|
|
3
|
+
import { SystemErrorKind } from "@domicile-desktop/sdk/system";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
/** Sysfs directory listing every backlight. */
|
|
6
|
+
const BACKLIGHT = "/sys/class/backlight";
|
|
7
|
+
/**
|
|
8
|
+
* Backlight types, preferred first, in systemd's order. A firmware interface
|
|
9
|
+
* knows the panel's curve; a raw one drives the same panel without it.
|
|
10
|
+
*/
|
|
11
|
+
const KINDS = ["firmware", "platform", "raw"];
|
|
12
|
+
/** The files read from each device, in the order `readOne` takes them. */
|
|
13
|
+
const FILES = ["type", "max_brightness", "brightness"];
|
|
14
|
+
/**
|
|
15
|
+
* The backlight to show and set, or `None` if there is none.
|
|
16
|
+
*
|
|
17
|
+
* Picks the preferred type, breaking ties by name so the choice is stable.
|
|
18
|
+
* Skips a device whose files are missing or not numbers, or whose maximum is
|
|
19
|
+
* zero.
|
|
20
|
+
*/
|
|
21
|
+
export const readBacklight = async (host) => (await host.readDir(BACKLIGHT)).match({
|
|
22
|
+
Err: async (error) => error.kind === SystemErrorKind.NotFound ? Ok(None()) : Err(error),
|
|
23
|
+
Ok: async (entries) => collected(await Promise.all(entries
|
|
24
|
+
.map(({ name }) => name)
|
|
25
|
+
.toSorted()
|
|
26
|
+
.map((device) => readOne(host, device)))).map((devices) => firstSome(KINDS.map((kind) => firstSome(devices.map((device) => device.andThen((read) => read.kind === kind ? Some(read.backlight) : None())))))),
|
|
27
|
+
});
|
|
28
|
+
/** Brightness from 0 to 1. */
|
|
29
|
+
export const levelOf = ({ max, raw }) => raw / max;
|
|
30
|
+
const readOne = async (host, device) => collected(await Promise.all(FILES.map((file) => readFile(host, device, file)))).map(([kind, max, raw]) => {
|
|
31
|
+
const parsed = deviceSchema.safeParse({
|
|
32
|
+
kind: present(kind),
|
|
33
|
+
max: present(max),
|
|
34
|
+
raw: present(raw),
|
|
35
|
+
});
|
|
36
|
+
return parsed.success
|
|
37
|
+
? Some({
|
|
38
|
+
backlight: {
|
|
39
|
+
device,
|
|
40
|
+
max: parsed.data.max,
|
|
41
|
+
raw: Math.min(parsed.data.raw, parsed.data.max),
|
|
42
|
+
},
|
|
43
|
+
kind: parsed.data.kind,
|
|
44
|
+
})
|
|
45
|
+
: None();
|
|
46
|
+
});
|
|
47
|
+
/** A file of `device`, trimmed, or `None` if it is missing. */
|
|
48
|
+
const readFile = async (host, device, file) => (await host.readTextFile(`${BACKLIGHT}/${device}/${file}`)).match({
|
|
49
|
+
Err: (error) => error.kind === SystemErrorKind.NotFound ? Ok(None()) : Err(error),
|
|
50
|
+
Ok: (text) => Ok(Some(text.trim())),
|
|
51
|
+
});
|
|
52
|
+
const count = z
|
|
53
|
+
.string()
|
|
54
|
+
.regex(/^\d+$/)
|
|
55
|
+
.transform((digits) => Number.parseInt(digits, 10));
|
|
56
|
+
const deviceSchema = z.object({
|
|
57
|
+
kind: z.enum(KINDS),
|
|
58
|
+
max: count.refine((max) => max > 0),
|
|
59
|
+
raw: count,
|
|
60
|
+
});
|
|
61
|
+
/** Every value, or the first error. */
|
|
62
|
+
const collected = (results) => results.reduce((all, next) => all.andThen((values) => next.map((value) => [...values, value])), Ok([]));
|
|
63
|
+
const present = (text) => text?.match({ None: () => undefined, Some: (value) => value });
|
|
64
|
+
const firstSome = (options) => options.reduce((found, next) => found.or(next), None());
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Result } from "@cprussin/option-result";
|
|
2
|
+
import type { System, SystemError } from "@domicile-desktop/sdk/system";
|
|
3
|
+
import type { SysfsHost } from "./read-backlight";
|
|
4
|
+
export declare enum SetOutcome {
|
|
5
|
+
/** logind set it. */
|
|
6
|
+
Set = 0,
|
|
7
|
+
/** A newer level was asked for before this one was sent. */
|
|
8
|
+
Superseded = 1,
|
|
9
|
+
NoBacklight = 2
|
|
10
|
+
}
|
|
11
|
+
/** The calls {@link brightnessSetter} makes. */
|
|
12
|
+
export type SetterHost = SysfsHost & Pick<System, "dbusCall">;
|
|
13
|
+
/** Sets the backlight to a level from 0 to 1, and says how it went. */
|
|
14
|
+
export type SetBrightness = (level: number) => Promise<Result<SetOutcome, SystemError>>;
|
|
15
|
+
/**
|
|
16
|
+
* A function that sets the preferred backlight to a level from 0 to 1.
|
|
17
|
+
*
|
|
18
|
+
* - Reads the device again for each level, since the raw scale is the
|
|
19
|
+
* device's and it may have gone away.
|
|
20
|
+
* - Never sets zero; see `rawFor`.
|
|
21
|
+
* - Sends one level at a time. A dragged slider asks many times while logind
|
|
22
|
+
* answers once, so only the newest level waiting is sent and the others
|
|
23
|
+
* resolve {@link SetOutcome.Superseded}.
|
|
24
|
+
* - A level that is not a number rejects.
|
|
25
|
+
*/
|
|
26
|
+
export declare const brightnessSetter: (host: SetterHost) => SetBrightness;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Sets the screen's backlight through logind.
|
|
2
|
+
//
|
|
3
|
+
// `/sys/class/backlight/*/brightness` is writable only by root. logind's
|
|
4
|
+
// `Session.SetBrightness` lets the session's owner set it without a udev rule
|
|
5
|
+
// or a setuid helper.
|
|
6
|
+
import { Ok } from "@cprussin/option-result";
|
|
7
|
+
import { Bus } from "@domicile-desktop/sdk/system";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { rawFor } from "./raw-for";
|
|
10
|
+
import { readBacklight } from "./read-backlight";
|
|
11
|
+
export var SetOutcome;
|
|
12
|
+
(function (SetOutcome) {
|
|
13
|
+
/** logind set it. */
|
|
14
|
+
SetOutcome[SetOutcome["Set"] = 0] = "Set";
|
|
15
|
+
/** A newer level was asked for before this one was sent. */
|
|
16
|
+
SetOutcome[SetOutcome["Superseded"] = 1] = "Superseded";
|
|
17
|
+
SetOutcome[SetOutcome["NoBacklight"] = 2] = "NoBacklight";
|
|
18
|
+
})(SetOutcome || (SetOutcome = {}));
|
|
19
|
+
/**
|
|
20
|
+
* A function that sets the preferred backlight to a level from 0 to 1.
|
|
21
|
+
*
|
|
22
|
+
* - Reads the device again for each level, since the raw scale is the
|
|
23
|
+
* device's and it may have gone away.
|
|
24
|
+
* - Never sets zero; see `rawFor`.
|
|
25
|
+
* - Sends one level at a time. A dragged slider asks many times while logind
|
|
26
|
+
* answers once, so only the newest level waiting is sent and the others
|
|
27
|
+
* resolve {@link SetOutcome.Superseded}.
|
|
28
|
+
* - A level that is not a number rejects.
|
|
29
|
+
*/
|
|
30
|
+
export const brightnessSetter = (host) => {
|
|
31
|
+
const queue = { pending: undefined, sending: false };
|
|
32
|
+
return (level) => {
|
|
33
|
+
if (queue.sending) {
|
|
34
|
+
queue.pending?.settle(Ok(SetOutcome.Superseded));
|
|
35
|
+
return new Promise((settle, fail) => {
|
|
36
|
+
queue.pending = { fail, level, settle };
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
queue.sending = true;
|
|
41
|
+
return sendFrom(host, queue, level);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
/** Send `level`, then whatever waits behind it. */
|
|
46
|
+
const sendFrom = async (host, queue, level) => {
|
|
47
|
+
try {
|
|
48
|
+
return await send(host, level);
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
const next = queue.pending;
|
|
52
|
+
queue.pending = undefined;
|
|
53
|
+
if (next === undefined) {
|
|
54
|
+
queue.sending = false;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
sendFrom(host, queue, next.level).then(next.settle, next.fail);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const send = async (host, level) => (await readBacklight(host)).andThenAsync(async (read) => read.match({
|
|
62
|
+
None: async () => Ok(SetOutcome.NoBacklight),
|
|
63
|
+
Some: async (backlight) => (await host.dbusCall({
|
|
64
|
+
body: ["backlight", backlight.device, rawFor(backlight, level)],
|
|
65
|
+
bus: Bus.System,
|
|
66
|
+
destination: "org.freedesktop.login1",
|
|
67
|
+
interface: "org.freedesktop.login1.Session",
|
|
68
|
+
member: "SetBrightness",
|
|
69
|
+
path: "/org/freedesktop/login1/session/auto",
|
|
70
|
+
signature: "ssu",
|
|
71
|
+
})).map(({ body }) => {
|
|
72
|
+
returnsNothing.parse(body);
|
|
73
|
+
return SetOutcome.Set;
|
|
74
|
+
}),
|
|
75
|
+
}));
|
|
76
|
+
const returnsNothing = z.tuple([]);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Result } from "@cprussin/option-result";
|
|
2
|
+
import type { System, SystemError } from "@domicile-desktop/sdk/system";
|
|
3
|
+
import type { SysfsHost } from "./read-backlight";
|
|
4
|
+
/** The calls {@link watchBrightness} makes. */
|
|
5
|
+
export type WatchHost = SysfsHost & Pick<System, "spawn">;
|
|
6
|
+
export type BrightnessWatch = {
|
|
7
|
+
stop: () => void;
|
|
8
|
+
/** `Ok` after {@link BrightnessWatch.stop}, `Err` if the watch broke. */
|
|
9
|
+
ended: Promise<Result<"stopped", SystemError>>;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Calls `onLevel` with the backlight's level from 0 to 1, now and after each
|
|
13
|
+
* change.
|
|
14
|
+
*
|
|
15
|
+
* - Reports only a move of a whole percent, since the slider shows no finer.
|
|
16
|
+
* - Never called on a machine without a backlight. A backlight that goes away
|
|
17
|
+
* and comes back is reported again.
|
|
18
|
+
* - Runs `udevadm` from the compositor's `PATH`. Works while the desktop is
|
|
19
|
+
* locked.
|
|
20
|
+
*/
|
|
21
|
+
export declare const watchBrightness: (host: WatchHost, onLevel: (level: number) => void, every?: typeof everyInterval) => Promise<Result<BrightnessWatch, SystemError>>;
|
|
22
|
+
/** Calls `tick` every `ms`, until the returned function is called. */
|
|
23
|
+
declare const everyInterval: (ms: number, tick: () => void) => (() => void);
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Watches the screen's backlight.
|
|
2
|
+
//
|
|
3
|
+
// inotify sees no change to a file under `/sys`, so the watch runs
|
|
4
|
+
// `udevadm monitor` and reads the backlight again on each kernel uevent.
|
|
5
|
+
import { Err, Ok } from "@cprussin/option-result";
|
|
6
|
+
import { SystemErrorKind } from "@domicile-desktop/sdk/system";
|
|
7
|
+
import { levelOf, readBacklight } from "./read-backlight";
|
|
8
|
+
/** Prints a line for each backlight uevent. */
|
|
9
|
+
const UDEVADM = [
|
|
10
|
+
"udevadm",
|
|
11
|
+
"monitor",
|
|
12
|
+
"--kernel",
|
|
13
|
+
"--subsystem-match=backlight",
|
|
14
|
+
];
|
|
15
|
+
/** How often to read again without a uevent: firmware keys may send none. */
|
|
16
|
+
const BACKSTOP_MS = 120_000;
|
|
17
|
+
/**
|
|
18
|
+
* Calls `onLevel` with the backlight's level from 0 to 1, now and after each
|
|
19
|
+
* change.
|
|
20
|
+
*
|
|
21
|
+
* - Reports only a move of a whole percent, since the slider shows no finer.
|
|
22
|
+
* - Never called on a machine without a backlight. A backlight that goes away
|
|
23
|
+
* and comes back is reported again.
|
|
24
|
+
* - Runs `udevadm` from the compositor's `PATH`. Works while the desktop is
|
|
25
|
+
* locked.
|
|
26
|
+
*/
|
|
27
|
+
export const watchBrightness = async (host, onLevel, every = everyInterval) => (await host.spawn(UDEVADM)).map((udevadm) => {
|
|
28
|
+
const watch = {
|
|
29
|
+
failure: undefined,
|
|
30
|
+
reading: Promise.resolve(),
|
|
31
|
+
shown: undefined,
|
|
32
|
+
stopped: false,
|
|
33
|
+
};
|
|
34
|
+
const reread = () => {
|
|
35
|
+
watch.reading = watch.reading.then(() => readOnce(host, watch, udevadm, onLevel));
|
|
36
|
+
};
|
|
37
|
+
reread();
|
|
38
|
+
const stopClock = every(BACKSTOP_MS, reread);
|
|
39
|
+
return {
|
|
40
|
+
ended: ended(udevadm, watch, reread, stopClock),
|
|
41
|
+
stop: () => {
|
|
42
|
+
watch.stopped = true;
|
|
43
|
+
udevadm.kill();
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
/** Calls `tick` every `ms`, until the returned function is called. */
|
|
48
|
+
const everyInterval = (ms, tick) => {
|
|
49
|
+
const id = setInterval(tick, ms);
|
|
50
|
+
return () => {
|
|
51
|
+
clearInterval(id);
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
const readOnce = async (host, watch, udevadm, onLevel) => {
|
|
55
|
+
const read = await readBacklight(host);
|
|
56
|
+
if (!watch.stopped && watch.failure === undefined) {
|
|
57
|
+
read.match({
|
|
58
|
+
Err: (error) => {
|
|
59
|
+
watch.failure = error;
|
|
60
|
+
udevadm.kill();
|
|
61
|
+
},
|
|
62
|
+
Ok: (backlight) => {
|
|
63
|
+
const level = backlight.map(levelOf);
|
|
64
|
+
const shown = level.match({
|
|
65
|
+
None: () => undefined,
|
|
66
|
+
Some: (now) => Math.round(now * 100),
|
|
67
|
+
});
|
|
68
|
+
if (shown !== watch.shown) {
|
|
69
|
+
watch.shown = shown;
|
|
70
|
+
level.match({ None: () => undefined, Some: onLevel });
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
/** Reads `udevadm`'s lines until it exits, then says why it did. */
|
|
77
|
+
const ended = async (udevadm, watch, reread, stopClock) => {
|
|
78
|
+
await eachLine(udevadm.stdout, (line) => {
|
|
79
|
+
if (line.startsWith("KERNEL[")) {
|
|
80
|
+
reread();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
const exit = await udevadm.exited;
|
|
84
|
+
stopClock();
|
|
85
|
+
await watch.reading;
|
|
86
|
+
if (watch.failure !== undefined) {
|
|
87
|
+
return Err(watch.failure);
|
|
88
|
+
}
|
|
89
|
+
else if (watch.stopped) {
|
|
90
|
+
return Ok("stopped");
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
return exit.andThen((exited) => Err(unexpectedExit(exited)));
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const eachLine = async (stream, onLine) => {
|
|
97
|
+
const reader = stream.getReader();
|
|
98
|
+
const decoder = new TextDecoder();
|
|
99
|
+
let unfinished = "";
|
|
100
|
+
for (;;) {
|
|
101
|
+
const { done, value } = await reader.read();
|
|
102
|
+
if (done) {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
const lines = (unfinished + decoder.decode(value, { stream: true })).split("\n");
|
|
106
|
+
unfinished = lines.pop() ?? "";
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
onLine(line);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const unexpectedExit = ({ code, signal }) => ({
|
|
113
|
+
kind: SystemErrorKind.Other,
|
|
114
|
+
message: code === undefined
|
|
115
|
+
? `udevadm monitor was killed by signal ${signal}`
|
|
116
|
+
: `udevadm monitor exited with code ${code}`,
|
|
117
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dependencies": {
|
|
3
|
+
"@cprussin/option-result": "^2.0.1",
|
|
4
|
+
"@domicile-desktop/sdk": "0.0.0-alpha-a330bf5f3aae",
|
|
5
|
+
"zod": "^4.5.4"
|
|
6
|
+
},
|
|
7
|
+
"description": "Screen brightness for a Domicile shell: reads /sys/class/backlight, watches it and sets it through logind, on @domicile-desktop/sdk/system.",
|
|
8
|
+
"devDependencies": {
|
|
9
|
+
"@cprussin/tsconfig": "^5.0.0",
|
|
10
|
+
"@types/bun": "^1.4.2",
|
|
11
|
+
"typescript": "^7.0.2"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
"./read-backlight": {
|
|
15
|
+
"types": "./dist/read-backlight.d.ts",
|
|
16
|
+
"default": "./dist/read-backlight.js"
|
|
17
|
+
},
|
|
18
|
+
"./set-brightness": {
|
|
19
|
+
"types": "./dist/set-brightness.d.ts",
|
|
20
|
+
"default": "./dist/set-brightness.js"
|
|
21
|
+
},
|
|
22
|
+
"./watch-brightness": {
|
|
23
|
+
"types": "./dist/watch-brightness.d.ts",
|
|
24
|
+
"default": "./dist/watch-brightness.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist/**"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"name": "@domicile-desktop/system-backlight",
|
|
32
|
+
"private": false,
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"directory": "packages/system-backlight",
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/cprussin/domicile.git"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc -p tsconfig.build.json",
|
|
43
|
+
"test:types": "tsc --noEmit",
|
|
44
|
+
"test:unit": "bun test"
|
|
45
|
+
},
|
|
46
|
+
"type": "module",
|
|
47
|
+
"version": "0.0.0-alpha-a330bf5f3aae"
|
|
48
|
+
}
|