betterdisplaycli 0.1.16 → 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.16",
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,9 @@
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";
4
+
5
+ type BOOLEAN_SETTING = "connected" | "hiDPI" | "notch";
6
+ type STRING_SETTING = "resolution";
3
7
 
4
8
  export type NumberString = string;
5
9
 
@@ -44,7 +48,7 @@ class SingleDisplay extends Device {
44
48
  }
45
49
  boolean = {
46
50
  get: async (
47
- settingName: "connected" | "hiDPI",
51
+ settingName: BOOLEAN_SETTING,
48
52
  options?: QuietOption,
49
53
  ): Promise<boolean> => {
50
54
  switch (
@@ -70,7 +74,7 @@ class SingleDisplay extends Device {
70
74
  },
71
75
 
72
76
  set: async (
73
- settingName: "connected" | "hiDPI",
77
+ settingName: BOOLEAN_SETTING,
74
78
  on: boolean,
75
79
  options?: QuietOption,
76
80
  ): Promise<void> => {
@@ -86,7 +90,7 @@ class SingleDisplay extends Device {
86
90
  },
87
91
 
88
92
  toggle: async (
89
- settingName: "connected" | "hiDPI",
93
+ settingName: BOOLEAN_SETTING,
90
94
  options?: QuietOption,
91
95
  ): Promise<void> => {
92
96
  await print(
@@ -102,7 +106,7 @@ class SingleDisplay extends Device {
102
106
  };
103
107
  string = {
104
108
  get: async (
105
- settingName: "resolution",
109
+ settingName: STRING_SETTING,
106
110
  options?: QuietOption,
107
111
  ): Promise<string> => {
108
112
  return print(
@@ -117,7 +121,7 @@ class SingleDisplay extends Device {
117
121
  },
118
122
 
119
123
  set: async (
120
- settingName: "resolution",
124
+ settingName: STRING_SETTING,
121
125
  value: string,
122
126
  options?: QuietOption,
123
127
  ): Promise<void> => {
@@ -132,12 +136,86 @@ class SingleDisplay extends Device {
132
136
  ).shellOut({ print: false });
133
137
  },
134
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
+ }
135
196
  }
136
197
 
137
198
  export class Display extends SingleDisplay {
138
199
  constructor(public override readonly info: DisplayInfo) {
139
200
  super(info);
140
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
+ }
141
219
  }
142
220
 
143
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";