betterdisplaycli 0.1.17 → 0.1.18

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "betterdisplaycli",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "TypeScript bindings for `betterdisplaycli`.",
5
5
  "author": "Lucas Garron <code@garron.net>",
6
6
  "license": "MIT",
package/src/Device.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { PrintableShellCommand } from "printable-shell-command";
2
- import { print, type QuietOption } from "./get";
2
+ import { getDisplayWithSelectorArg, print, type QuietOption } from "./get";
3
+ import { isNotUndefined, ResolutionInfo } from "./ResolutionInfo";
3
4
 
4
5
  type BOOLEAN_SETTING = "connected" | "hiDPI" | "notch";
5
6
  type STRING_SETTING = "resolution";
@@ -135,12 +136,86 @@ class SingleDisplay extends Device {
135
136
  ).shellOut({ print: false });
136
137
  },
137
138
  };
139
+
140
+ resolution = {
141
+ get: async (): Promise<ResolutionInfo> => {
142
+ return ResolutionInfo.fromString(await this.string.get("resolution"));
143
+ },
144
+ // The return value indicates if any changes were needed (and performed)
145
+ set: async (
146
+ resolutionInfo: ResolutionInfo,
147
+ options?: QuietOption,
148
+ ): Promise<boolean> => {
149
+ const currentResolution = await this.resolution.get();
150
+
151
+ const args: string[] = [];
152
+ if (resolutionInfo.width !== currentResolution.width) {
153
+ args.push(`--width=${resolutionInfo.width}`);
154
+ }
155
+ if (resolutionInfo.width !== currentResolution.height) {
156
+ args.push(`--height=${resolutionInfo.height}`);
157
+ }
158
+ if (
159
+ isNotUndefined(resolutionInfo.hiDPI) &&
160
+ resolutionInfo.hiDPI !== currentResolution.hiDPI
161
+ ) {
162
+ args.push(`--hiDPI=${resolutionInfo.hiDPI}`);
163
+ }
164
+ if (
165
+ isNotUndefined(resolutionInfo.notch) &&
166
+ resolutionInfo.notch !== currentResolution.notch
167
+ ) {
168
+ args.push(`--notch=${resolutionInfo.notch ? "on" : "off"}`);
169
+ }
170
+
171
+ if (args.length === 0) {
172
+ // No changes to perform.
173
+ return false;
174
+ }
175
+
176
+ await print(
177
+ new PrintableShellCommand("betterdisplaycli", [
178
+ "set",
179
+ `--name=${this.info.name}`,
180
+ ...args,
181
+ ]),
182
+ { argumentLineWrapping: "inline" },
183
+ options,
184
+ ).shellOut({ print: false });
185
+ return true;
186
+ },
187
+ };
188
+
189
+ async connect(): Promise<void> {
190
+ await this.boolean.set("connected", true);
191
+ }
192
+
193
+ async disconnect(): Promise<void> {
194
+ await this.boolean.set("connected", false);
195
+ }
138
196
  }
139
197
 
140
198
  export class Display extends SingleDisplay {
141
199
  constructor(public override readonly info: DisplayInfo) {
142
200
  super(info);
143
201
  }
202
+
203
+ static main(): Promise<Display> {
204
+ return getDisplayWithSelectorArg("--displayWithMainStatus");
205
+ }
206
+
207
+ static fromName(name: string): Promise<Display> {
208
+ return getDisplayWithSelectorArg(`--name=${name}`);
209
+ }
210
+
211
+ static async tryFromName(name: string): Promise<Display | null> {
212
+ try {
213
+ return await getDisplayWithSelectorArg(`--name=${name}`);
214
+ } catch {
215
+ // TODO: what is the simplest way to verify this was because there was no such display (as opposed to a general invocation error)?
216
+ return null;
217
+ }
218
+ }
144
219
  }
145
220
 
146
221
  export class VirtualScreen extends SingleDisplay {
@@ -0,0 +1,93 @@
1
+ // biome-ignore lint/suspicious/noExplicitAny: `any` is the correct API.
2
+ export function isPositiveInteger(n: any): n is number {
3
+ return Number.isInteger(n) && n > 0;
4
+ }
5
+
6
+ // biome-ignore lint/suspicious/noExplicitAny: `any` is the correct API.
7
+ export function isNotUndefined<T extends Exclude<any, undefined>>(
8
+ v: T | undefined,
9
+ ): v is T {
10
+ return typeof v !== "undefined";
11
+ }
12
+
13
+ export interface ResolutionInfoData {
14
+ width: number;
15
+ height: number;
16
+ pixelRatio?: number;
17
+ notch?: boolean;
18
+ }
19
+
20
+ export class ResolutionInfo {
21
+ #data: ResolutionInfoData;
22
+ constructor(data: ResolutionInfoData) {
23
+ this.#data = data;
24
+ if (!isPositiveInteger(data.width)) {
25
+ throw new Error("Invalid width (expected a positive integer).");
26
+ }
27
+ if (!isPositiveInteger(data.height)) {
28
+ throw new Error("Invalid height (expected a positive integer).");
29
+ }
30
+ if (
31
+ isNotUndefined(data.pixelRatio) &&
32
+ (!isPositiveInteger(data.pixelRatio) || ![1, 2].includes(data.pixelRatio))
33
+ ) {
34
+ throw new Error("Invalid pixel ratio (expected 1 or 2 if set).");
35
+ }
36
+ if (isNotUndefined(data.notch) && typeof data.notch !== "boolean") {
37
+ throw new Error("Invalid notch (expected boolean if present).");
38
+ }
39
+ }
40
+
41
+ get width(): number {
42
+ return this.#data.width;
43
+ }
44
+
45
+ get height(): number {
46
+ return this.#data.height;
47
+ }
48
+
49
+ get pixelRatio(): number | undefined {
50
+ return this.#data.pixelRatio;
51
+ }
52
+
53
+ get hiDPI(): boolean | undefined {
54
+ if (isNotUndefined(this.pixelRatio)) {
55
+ return this.pixelRatio === 2;
56
+ }
57
+ return undefined;
58
+ }
59
+
60
+ get notch(): boolean | undefined {
61
+ return this.#data.notch;
62
+ }
63
+
64
+ static fromString(s: string): ResolutionInfo {
65
+ const match = s.match(
66
+ /^([1-9][0-9]*)[x×]([1-9][0-9]*)+(@([1-9][0-9]*)x)?([+-]notch)?$/,
67
+ );
68
+ console.log({ s, match });
69
+ if (!match) {
70
+ throw new Error("Invalid resolution info.");
71
+ }
72
+ const width = parseInt(match[1], 10);
73
+ const height = parseInt(match[2], 10);
74
+ const pixelRatio = match[4] ? parseInt(match[4], 10) : undefined;
75
+ const notch = match[5] ? match[5][0] === "+" : undefined;
76
+ return new ResolutionInfo({ width, height, pixelRatio, notch });
77
+ }
78
+
79
+ logicalResolutionString(): string {
80
+ return `${this.width}x${this.height}`;
81
+ }
82
+
83
+ toString(): string {
84
+ let output = this.logicalResolutionString();
85
+ if (isNotUndefined(this.pixelRatio)) {
86
+ output += `@${this.pixelRatio}x`;
87
+ }
88
+ if (isNotUndefined(this.notch)) {
89
+ output += `${this.notch ? "+" : "-"}notch`;
90
+ }
91
+ return output;
92
+ }
93
+ }
package/src/get.ts CHANGED
@@ -7,6 +7,8 @@ import {
7
7
  type VirtualScreen,
8
8
  } from "./Device";
9
9
 
10
+ const BINARY_NAME = "betterdisplaycli";
11
+
10
12
  export interface QuietOption {
11
13
  quiet?: boolean;
12
14
  }
@@ -22,6 +24,30 @@ export function print(
22
24
  return command;
23
25
  }
24
26
 
27
+ async function getDeviceInfos<T>(
28
+ printable_shell_command: PrintableShellCommand,
29
+ options?: {
30
+ ignoreDisplayGroups?: true;
31
+ } & QuietOption,
32
+ ): Promise<T[]> {
33
+ const jsonStream = await print(
34
+ printable_shell_command,
35
+ { argumentLineWrapping: "inline" },
36
+ options,
37
+ )
38
+ .stdout()
39
+ .text();
40
+ const deviceInfos: DeviceInfo[] = JSON.parse(`[${jsonStream}]`);
41
+ return deviceInfos
42
+ .map((info) => deviceFromInfo(info))
43
+ .filter((device: Device) => {
44
+ return (
45
+ !options?.ignoreDisplayGroups ||
46
+ device.info.deviceType !== "DisplayGroup"
47
+ );
48
+ }) as T[];
49
+ }
50
+
25
51
  export async function getAllDevices(
26
52
  options?: {
27
53
  ignoreDisplayGroups?: true;
@@ -33,7 +59,7 @@ export async function getAllDevices(
33
59
  } & QuietOption,
34
60
  ): Promise<Device[]> {
35
61
  const jsonStream = await print(
36
- new PrintableShellCommand("betterdisplaycli", [["get", "--identifiers"]]),
62
+ new PrintableShellCommand(BINARY_NAME, [["get", "--identifiers"]]),
37
63
  { argumentLineWrapping: "inline" },
38
64
  options,
39
65
  )
@@ -50,9 +76,24 @@ export async function getAllDevices(
50
76
  });
51
77
  }
52
78
 
79
+ export async function getDisplayWithSelectorArg(
80
+ arg: "--displayWithMainStatus" | `--name=${string}`,
81
+ ): Promise<Display> {
82
+ return (
83
+ await getDeviceInfos<Display>(
84
+ new PrintableShellCommand("betterdisplaycli", [
85
+ "get",
86
+ arg,
87
+ "--type=Display",
88
+ "--identifiers",
89
+ ]),
90
+ )
91
+ )[0];
92
+ }
93
+
53
94
  export async function connectAllDisplays(options?: QuietOption): Promise<void> {
54
95
  await print(
55
- new PrintableShellCommand("betterdisplaycli", [
96
+ new PrintableShellCommand(BINARY_NAME, [
56
97
  ["perform", "--connectAllDisplays"],
57
98
  ]),
58
99
  { argumentLineWrapping: "inline" },
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./Device";
2
2
  export { connectAllDisplays, getAllDevices } from "./get";
3
+ export { ResolutionInfo, type ResolutionInfoData } from "./ResolutionInfo";