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 +1 -1
- package/src/Device.ts +84 -6
- package/src/ResolutionInfo.ts +93 -0
- package/src/get.ts +43 -2
- package/src/index.ts +1 -0
package/package.json
CHANGED
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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(
|
|
96
|
+
new PrintableShellCommand(BINARY_NAME, [
|
|
56
97
|
["perform", "--connectAllDisplays"],
|
|
57
98
|
]),
|
|
58
99
|
{ argumentLineWrapping: "inline" },
|
package/src/index.ts
CHANGED