zed-local

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