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}
116
117screenHeight -= titleBarHeight;
118
119if (isTop) {
120 screenHeight = Math.floor(screenHeight / 2);
121}
122
123// Determine the window size for each instance
124let rows;
125let columns;
126switch (instanceCount) {
127 case 1:
128 [rows, columns] = [1, 1];
129 break;
130 case 2:
131 [rows, columns] = [1, 2];
132 break;
133 case 3:
134 case 4:
135 [rows, columns] = [2, 2];
136 break;
137 case 5:
138 case 6:
139 [rows, columns] = [2, 3];
140 break;
141}
142
143const instanceWidth = Math.floor(screenWidth / columns);
144const instanceHeight = Math.floor(screenHeight / rows);
145
146// If a user is specified, make sure it's first in the list
147const user = process.env.ZED_IMPERSONATE;
148if (user) {
149 users = [user].concat(users.filter((u) => u !== user));
150}
151
152let buildArgs = ["build"];
153let zedBinary = "target/debug/zed";
154if (isReleaseMode) {
155 buildArgs.push("--release");
156 zedBinary = "target/release/zed";
157}
158
159try {
160 execFileSync("cargo", buildArgs, {
161 stdio: "inherit",
162 });
163} catch (e) {
164 process.exit(0);
165}
166
167setTimeout(() => {
168 for (let i = 0; i < instanceCount; i++) {
169 const row = Math.floor(i / columns);
170 const column = i % columns;
171 const position = [
172 column * instanceWidth,
173 row * instanceHeight + titleBarHeight,
174 ].join(",");
175 const size = [instanceWidth, instanceHeight].join(",");
176 let binaryPath = zedBinary;
177 if (i != 0 && othersOnStable) {
178 binaryPath = "/Applications/Zed.app/Contents/MacOS/zed";
179 } else if (i != 0 && othersOnPreview) {
180 binaryPath = "/Applications/Zed Preview.app/Contents/MacOS/zed";
181 }
182 spawn(binaryPath, i == 0 ? args : [], {
183 stdio: "inherit",
184 env: Object.assign({}, process.env, {
185 ZED_IMPERSONATE: users[i],
186 ZED_WINDOW_POSITION: position,
187 ZED_STATELESS: isStateful && i == 0 ? "" : "1",
188 ZED_ALWAYS_ACTIVE: "1",
189 ZED_SERVER_URL: "http://localhost:3000",
190 ZED_RPC_URL: "http://localhost:8080/rpc",
191 ZED_ADMIN_API_TOKEN: "secret",
192 ZED_WINDOW_SIZE: size,
193 ZED_CLIENT_CHECKSUM_SEED: "development-checksum-seed",
194 RUST_LOG: process.env.RUST_LOG || "info",
195 }),
196 });
197 }
198}, 0.1);