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`.trim();
25
26const { spawn, execSync, execFileSync } = require("child_process");
27const assert = require("assert");
28
29let users;
30if (process.env.SEED_PATH) {
31 users = require(process.env.SEED_PATH).admins;
32} else {
33 users = require("../crates/collab/seed.default.json").admins;
34 try {
35 const defaultUsers = users;
36 const customUsers = require("../crates/collab/seed.json").admins;
37 assert(customUsers.length > 0);
38 users = customUsers.concat(
39 defaultUsers.filter((user) => !customUsers.includes(user)),
40 );
41 } catch (_) {}
42}
43
44const RESOLUTION_REGEX = /(\d+) x (\d+)/;
45const DIGIT_FLAG_REGEX = /^--?(\d+)$/;
46
47let instanceCount = 1;
48let isReleaseMode = false;
49let isTop = false;
50let othersOnStable = false;
51let isStateful = false;
52
53const args = process.argv.slice(2);
54while (args.length > 0) {
55 const arg = args[0];
56
57 const digitMatch = arg.match(DIGIT_FLAG_REGEX);
58 if (digitMatch) {
59 instanceCount = parseInt(digitMatch[1]);
60 } else if (arg === "--release") {
61 isReleaseMode = true;
62 } else if (arg == "--stateful") {
63 isStateful = true;
64 } else if (arg === "--top") {
65 isTop = true;
66 } else if (arg === "--help") {
67 console.log(HELP);
68 process.exit(0);
69 } else if (arg === "--stable") {
70 othersOnStable = true;
71 } else {
72 break;
73 }
74
75 args.shift();
76}
77const os = require("os");
78const platform = os.platform();
79
80let screenWidth, screenHeight;
81const titleBarHeight = 24;
82
83if (platform === "darwin") {
84 // macOS
85 const displayInfo = JSON.parse(
86 execFileSync("system_profiler", ["SPDisplaysDataType", "-json"], {
87 encoding: "utf8",
88 }),
89 );
90 const mainDisplayResolution = displayInfo?.SPDisplaysDataType?.flatMap(
91 (display) => display?.spdisplays_ndrvs,
92 )
93 ?.find((entry) => entry?.spdisplays_main === "spdisplays_yes")
94 ?._spdisplays_resolution?.match(RESOLUTION_REGEX);
95 if (!mainDisplayResolution) {
96 throw new Error("Could not parse screen resolution");
97 }
98 screenWidth = parseInt(mainDisplayResolution[1]);
99 screenHeight = parseInt(mainDisplayResolution[2]) - titleBarHeight;
100} else if (platform === "linux") {
101 // Linux
102 try {
103 const xrandrOutput = execSync('xrandr | grep "\\*" | cut -d" " -f4', {
104 encoding: "utf8",
105 }).trim();
106 [screenWidth, screenHeight] = xrandrOutput.split("x").map(Number);
107 } catch (err) {
108 console.log(err);
109 throw new Error("Could not get screen resolution");
110 }
111}
112
113screenHeight -= titleBarHeight;
114
115if (isTop) {
116 screenHeight = Math.floor(screenHeight / 2);
117}
118
119// Determine the window size for each instance
120let rows;
121let columns;
122switch (instanceCount) {
123 case 1:
124 [rows, columns] = [1, 1];
125 break;
126 case 2:
127 [rows, columns] = [1, 2];
128 break;
129 case 3:
130 case 4:
131 [rows, columns] = [2, 2];
132 break;
133 case 5:
134 case 6:
135 [rows, columns] = [2, 3];
136 break;
137}
138
139const instanceWidth = Math.floor(screenWidth / columns);
140const instanceHeight = Math.floor(screenHeight / rows);
141
142// If a user is specified, make sure it's first in the list
143const user = process.env.ZED_IMPERSONATE;
144if (user) {
145 users = [user].concat(users.filter((u) => u !== user));
146}
147
148let buildArgs = ["build"];
149let zedBinary = "target/debug/zed";
150if (isReleaseMode) {
151 buildArgs.push("--release");
152 zedBinary = "target/release/zed";
153}
154
155try {
156 execFileSync("cargo", buildArgs, {
157 stdio: "inherit",
158 });
159} catch (e) {
160 process.exit(0);
161}
162
163setTimeout(() => {
164 for (let i = 0; i < instanceCount; i++) {
165 const row = Math.floor(i / columns);
166 const column = i % columns;
167 const position = [
168 column * instanceWidth,
169 row * instanceHeight + titleBarHeight,
170 ].join(",");
171 const size = [instanceWidth, instanceHeight].join(",");
172 let binaryPath = zedBinary;
173 if (i != 0 && othersOnStable) {
174 binaryPath = "/Applications/Zed.app/Contents/MacOS/zed";
175 }
176 spawn(binaryPath, i == 0 ? args : [], {
177 stdio: "inherit",
178 env: Object.assign({}, process.env, {
179 ZED_IMPERSONATE: users[i],
180 ZED_WINDOW_POSITION: position,
181 ZED_STATELESS: isStateful && i == 0 ? "" : "1",
182 ZED_ALWAYS_ACTIVE: "1",
183 ZED_SERVER_URL: "http://localhost:3000",
184 ZED_RPC_URL: "http://localhost:8080/rpc",
185 ZED_ADMIN_API_TOKEN: "secret",
186 ZED_WINDOW_SIZE: size,
187 ZED_CLIENT_CHECKSUM_SEED: "development-checksum-seed",
188 RUST_LOG: process.env.RUST_LOG || "info",
189 }),
190 });
191 }
192}, 0.1);