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);