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