1#!/usr/bin/env node
2
3const HELP = `
4USAGE
5 zed-local [options] [zed args]
6
7SUMMARY
8 Runs 1-6 instances of Zed using a locally-running collaboration server.
9 Each instance of Zed will be signed in as a different user specified in
10 either \`.admins.json\` or \`.admins.default.json\`.
11
12 All arguments after the initial options will be passed through to the first
13 instance of Zed. This can be used to test SSH remoting along with collab, like
14 so:
15
16 $ script/zed-local -2 ssh://your-ssh-uri-here
17
18OPTIONS
19 --help Print this help message
20 --release Build Zed in release mode
21 -2, -3, -4, ... Spawn multiple Zed instances, with their windows tiled.
22 --top Arrange the Zed windows so they take up the top half of the screen.
23 --stable Use stable Zed release installed on local machine for all instances (except for the first one).
24 --preview Like --stable, but uses the locally-installed preview release instead.
25`.trim();
26
27const { spawn, execSync, execFileSync } = require("child_process");
28const assert = require("assert");
29
30let users;
31if (process.env.SEED_PATH) {
32 users = require(process.env.SEED_PATH).admins;
33} else {
34 users = require("../crates/collab/seed.default.json").admins;
35 try {
36 const defaultUsers = users;
37 const customUsers = require("../crates/collab/seed.json").admins;
38 assert(customUsers.length > 0);
39 users = customUsers.concat(
40 defaultUsers.filter((user) => !customUsers.includes(user)),
41 );
42 } catch (_) {}
43}
44
45const RESOLUTION_REGEX = /(\d+) x (\d+)/;
46const DIGIT_FLAG_REGEX = /^--?(\d+)$/;
47
48let instanceCount = 1;
49let isReleaseMode = false;
50let isTop = false;
51let othersOnStable = false;
52let othersOnPreview = false;
53let isStateful = false;
54
55const args = process.argv.slice(2);
56while (args.length > 0) {
57 const arg = args[0];
58
59 const digitMatch = arg.match(DIGIT_FLAG_REGEX);
60 if (digitMatch) {
61 instanceCount = parseInt(digitMatch[1]);
62 } else if (arg === "--release") {
63 isReleaseMode = true;
64 } else if (arg == "--stateful") {
65 isStateful = true;
66 } else if (arg === "--top") {
67 isTop = true;
68 } else if (arg === "--help") {
69 console.log(HELP);
70 process.exit(0);
71 } else if (arg === "--stable") {
72 othersOnStable = true;
73 } else if (arg === "--preview") {
74 othersOnPreview = true;
75 } else {
76 break;
77 }
78
79 args.shift();
80}
81const os = require("os");
82const platform = os.platform();
83
84let screenWidth, screenHeight;
85const titleBarHeight = 24;
86
87if (platform === "darwin") {
88 // macOS
89 const displayInfo = JSON.parse(
90 execFileSync("system_profiler", ["SPDisplaysDataType", "-json"], {
91 encoding: "utf8",
92 }),
93 );
94 const mainDisplayResolution = displayInfo?.SPDisplaysDataType?.flatMap(
95 (display) => display?.spdisplays_ndrvs,
96 )
97 ?.find((entry) => entry?.spdisplays_main === "spdisplays_yes")
98 ?._spdisplays_resolution?.match(RESOLUTION_REGEX);
99 if (!mainDisplayResolution) {
100 throw new Error("Could not parse screen resolution");
101 }
102 screenWidth = parseInt(mainDisplayResolution[1]);
103 screenHeight = parseInt(mainDisplayResolution[2]) - titleBarHeight;
104} else if (platform === "linux") {
105 // Linux
106 try {
107 const xrandrOutput = execSync('xrandr | grep "\\*" | cut -d" " -f4', {
108 encoding: "utf8",
109 }).trim();
110 [screenWidth, screenHeight] = xrandrOutput.split("x").map(Number);
111 } catch (err) {
112 console.log(err);
113 throw new Error("Could not get screen resolution");
114 }
115} else if (platform === "win32") {
116 // windows
117 try {
118 const resolutionOutput = execSync(
119 `powershell -Command "& {Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea.Size}"`,
120 { encoding: "utf8" },
121 ).trim();
122 [screenWidth, screenHeight] = resolutionOutput.match(/\d+/g).map(Number);
123 } catch (err) {
124 console.log(err);
125 throw new Error("Could not get screen resolution on Windows");
126 }
127}
128
129if (platform !== "win32") {
130 screenHeight -= titleBarHeight;
131}
132
133if (isTop) {
134 screenHeight = Math.floor(screenHeight / 2);
135}
136
137// Determine the window size for each instance
138let rows;
139let columns;
140switch (instanceCount) {
141 case 1:
142 [rows, columns] = [1, 1];
143 break;
144 case 2:
145 [rows, columns] = [1, 2];
146 break;
147 case 3:
148 case 4:
149 [rows, columns] = [2, 2];
150 break;
151 case 5:
152 case 6:
153 [rows, columns] = [2, 3];
154 break;
155}
156
157const instanceWidth = Math.floor(screenWidth / columns);
158const instanceHeight = Math.floor(screenHeight / rows);
159
160// If a user is specified, make sure it's first in the list
161const user = process.env.ZED_IMPERSONATE;
162if (user) {
163 users = [user].concat(users.filter((u) => u !== user));
164}
165
166let buildArgs = ["build"];
167let zedBinary = "target/debug/zed";
168if (isReleaseMode) {
169 buildArgs.push("--release");
170 zedBinary = "target/release/zed";
171}
172
173try {
174 execFileSync("cargo", buildArgs, {
175 stdio: "inherit",
176 });
177} catch (e) {
178 process.exit(0);
179}
180
181setTimeout(() => {
182 for (let i = 0; i < instanceCount; i++) {
183 const row = Math.floor(i / columns);
184 const column = i % columns;
185 let position;
186 if (platform == "win32") {
187 position = [column * instanceWidth, row * instanceHeight].join(",");
188 } else {
189 position = [
190 column * instanceWidth,
191 row * instanceHeight + titleBarHeight,
192 ].join(",");
193 }
194 const size = [instanceWidth, instanceHeight].join(",");
195 let binaryPath = zedBinary;
196 if (i != 0 && othersOnStable) {
197 binaryPath = "/Applications/Zed.app/Contents/MacOS/zed";
198 } else if (i != 0 && othersOnPreview) {
199 binaryPath = "/Applications/Zed Preview.app/Contents/MacOS/zed";
200 }
201 spawn(binaryPath, i == 0 ? args : [], {
202 stdio: "inherit",
203 env: Object.assign({}, process.env, {
204 ZED_IMPERSONATE: users[i],
205 ZED_WINDOW_POSITION: position,
206 ZED_STATELESS: isStateful && i == 0 ? "" : "1",
207 ZED_ALWAYS_ACTIVE: "1",
208 ZED_SERVER_URL: "http://localhost:3000",
209 ZED_RPC_URL: "http://localhost:8080/rpc",
210 ZED_ADMIN_API_TOKEN: "internal-api-key-secret",
211 ZED_WINDOW_SIZE: size,
212 ZED_CLIENT_CHECKSUM_SEED: "development-checksum-seed",
213 RUST_LOG: process.env.RUST_LOG || "info",
214 }),
215 });
216 }
217}, 0.1);