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 = [
188 column * instanceWidth,
189 row * instanceHeight,
190 ].join(",");
191 } else {
192 position = [
193 column * instanceWidth,
194 row * instanceHeight + titleBarHeight,
195 ].join(",");
196 }
197 const size = [instanceWidth, instanceHeight].join(",");
198 let binaryPath = zedBinary;
199 if (i != 0 && othersOnStable) {
200 binaryPath = "/Applications/Zed.app/Contents/MacOS/zed";
201 } else if (i != 0 && othersOnPreview) {
202 binaryPath = "/Applications/Zed Preview.app/Contents/MacOS/zed";
203 }
204 spawn(binaryPath, i == 0 ? args : [], {
205 stdio: "inherit",
206 env: Object.assign({}, process.env, {
207 ZED_IMPERSONATE: users[i],
208 ZED_WINDOW_POSITION: position,
209 ZED_STATELESS: isStateful && i == 0 ? "" : "1",
210 ZED_ALWAYS_ACTIVE: "1",
211 ZED_SERVER_URL: "http://127.0.0.1:3000", // On windows, the http_client could not parse localhost
212 ZED_RPC_URL: "http://127.0.0.1:8080/rpc",
213 ZED_ADMIN_API_TOKEN: "secret",
214 ZED_WINDOW_SIZE: size,
215 ZED_CLIENT_CHECKSUM_SEED: "development-checksum-seed",
216 RUST_LOG: process.env.RUST_LOG || "info",
217 }),
218 });
219 }
220}, 0.1);