relay-companion 0.1.26 → 0.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/overlay/main.cjs +62 -10
- package/overlay/space-presence.cjs +53 -0
- package/package.json +1 -1
- package/src/auto-update.js +10 -0
- package/src/install.js +28 -3
package/overlay/main.cjs
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// - Mutations (accept/reject/approve/decline/answer) call RelayClient with device-token
|
|
21
21
|
// auth. ack/mark-read writes state.json directly (atomic temp+rename).
|
|
22
22
|
|
|
23
|
-
const { app, BrowserWindow, Menu, Tray, ipcMain, nativeImage, shell, screen } = require("electron");
|
|
23
|
+
const { app, BrowserWindow, Menu, Tray, ipcMain, nativeImage, shell, screen, systemPreferences } = require("electron");
|
|
24
24
|
const fs = require("node:fs");
|
|
25
25
|
const os = require("node:os");
|
|
26
26
|
const path = require("node:path");
|
|
@@ -30,6 +30,11 @@ const { execFile, execFileSync, spawn, pathToFileURL } = (() => {
|
|
|
30
30
|
return { execFile: cp.execFile, execFileSync: cp.execFileSync, spawn: cp.spawn, pathToFileURL: url.pathToFileURL };
|
|
31
31
|
})();
|
|
32
32
|
const { hostFromBundle, chooseClickHost } = require("./host-select.cjs");
|
|
33
|
+
const {
|
|
34
|
+
reinforceSpacePresence,
|
|
35
|
+
showInactiveOnAllSpaces,
|
|
36
|
+
subscribeActiveSpaceChanges,
|
|
37
|
+
} = require("./space-presence.cjs");
|
|
33
38
|
|
|
34
39
|
const RELAY_HOME = process.env.RELAY_HOME || process.env.RELAY_COMPANION_HOME || path.join(os.homedir(), ".relay-companion");
|
|
35
40
|
const STATE_PATH = path.join(RELAY_HOME, "state.json");
|
|
@@ -884,31 +889,59 @@ function anchorTopRight() {
|
|
|
884
889
|
return { x: wa.x + wa.width - WIN.width - MARGIN, y: wa.y + MARGIN, width: WIN.width, height: WIN.height };
|
|
885
890
|
}
|
|
886
891
|
|
|
892
|
+
function placeOverlayWindow({ reposition = true, moveTop = false } = {}) {
|
|
893
|
+
if (!win || win.isDestroyed()) return;
|
|
894
|
+
if (reposition) {
|
|
895
|
+
try {
|
|
896
|
+
win.setBounds(anchorTopRight(), false);
|
|
897
|
+
} catch (error) {
|
|
898
|
+
console.error("[overlay] setBounds failed:", error && error.message);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
reinforceSpacePresence(win, { moveTop });
|
|
902
|
+
}
|
|
903
|
+
|
|
887
904
|
// Show the overlay window. Electron's forwarded-mousemove stream (the thing that
|
|
888
905
|
// lets the renderer detect hover on a click-through window) can stall across a
|
|
889
906
|
// hide()/show() cycle, leaving the window permanently click-through. Re-assert
|
|
890
907
|
// setIgnoreMouseEvents on every hidden->shown transition and tell the renderer to
|
|
891
908
|
// reset its interactivity handshake.
|
|
892
|
-
function showOverlayWindow() {
|
|
893
|
-
if (!win || win.isDestroyed()
|
|
894
|
-
win.
|
|
909
|
+
function showOverlayWindow({ force = false, reposition = true } = {}) {
|
|
910
|
+
if (!win || win.isDestroyed()) return;
|
|
911
|
+
const visible = win.isVisible();
|
|
912
|
+
if (visible && !force) {
|
|
913
|
+
reinforceSpacePresence(win);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
placeOverlayWindow({ reposition });
|
|
917
|
+
const shown = showInactiveOnAllSpaces(win, { force });
|
|
895
918
|
win.setIgnoreMouseEvents(true, { forward: true });
|
|
896
|
-
win.webContents.send("shown");
|
|
919
|
+
if (shown) win.webContents.send("shown");
|
|
897
920
|
}
|
|
898
921
|
|
|
899
|
-
function maybeShow() {
|
|
922
|
+
function maybeShow({ force = false, reposition = true } = {}) {
|
|
900
923
|
if (!win || win.isDestroyed()) return;
|
|
901
924
|
// Dismissed keeps the window hidden except while a ghost notification is up.
|
|
902
925
|
// trayForcedVisible honors an explicit status-area click even when no host runs.
|
|
903
926
|
const dismissedEffective = trayAvailable && dismissed;
|
|
904
927
|
const wanted = (hostRunning || trayForcedVisible) && (!dismissedEffective || ghostActive);
|
|
905
928
|
if (wanted) {
|
|
906
|
-
showOverlayWindow();
|
|
929
|
+
showOverlayWindow({ force, reposition });
|
|
907
930
|
} else if (win.isVisible()) {
|
|
908
931
|
win.hide();
|
|
909
932
|
}
|
|
910
933
|
}
|
|
911
934
|
|
|
935
|
+
function refreshOverlayForActiveSpace({ force = false } = {}) {
|
|
936
|
+
if (!win || win.isDestroyed()) return;
|
|
937
|
+
reinforceSpacePresence(win, { moveTop: true });
|
|
938
|
+
readHostProcesses((err, stdout) => {
|
|
939
|
+
if (!err) updateHostRunningFromProcesses(stdout);
|
|
940
|
+
if (hostRunning) trayForcedVisible = false;
|
|
941
|
+
maybeShow({ force, reposition: false });
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
912
945
|
function readHostProcesses(cb) {
|
|
913
946
|
if (process.platform === "win32") {
|
|
914
947
|
execFile("tasklist", ["/FO", "CSV", "/NH"], cb);
|
|
@@ -1010,6 +1043,21 @@ function createWindow() {
|
|
|
1010
1043
|
setInterval(pollHosts, 1500);
|
|
1011
1044
|
}
|
|
1012
1045
|
|
|
1046
|
+
function installActiveSpaceWatcher() {
|
|
1047
|
+
if (process.platform !== "darwin") return;
|
|
1048
|
+
subscribeActiveSpaceChanges(
|
|
1049
|
+
systemPreferences,
|
|
1050
|
+
() => {
|
|
1051
|
+
// Space transitions finish asynchronously. Force one re-show after the switch
|
|
1052
|
+
// starts and again after the animation settles so macOS cannot leave the
|
|
1053
|
+
// transparent accessory window assigned only to the previous Desktop.
|
|
1054
|
+
setTimeout(() => refreshOverlayForActiveSpace({ force: true }), 120);
|
|
1055
|
+
setTimeout(() => refreshOverlayForActiveSpace({ force: true }), 700);
|
|
1056
|
+
},
|
|
1057
|
+
{ log: (message) => console.error("[overlay]", message) },
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1013
1061
|
// ---- OS status area (tray/menu bar) ----------------------------------------
|
|
1014
1062
|
|
|
1015
1063
|
// The Relay mark lives in the OS status area: macOS menu bar, Windows system tray.
|
|
@@ -1040,8 +1088,8 @@ function createTrayIcon() {
|
|
|
1040
1088
|
function showFromTray() {
|
|
1041
1089
|
setDismissed(false);
|
|
1042
1090
|
ghostActive = false;
|
|
1043
|
-
|
|
1044
|
-
maybeShow();
|
|
1091
|
+
trayForcedVisible = true; // an explicit click always gets the card
|
|
1092
|
+
maybeShow({ force: true });
|
|
1045
1093
|
if (win && !win.isDestroyed()) win.webContents.send("openFull");
|
|
1046
1094
|
}
|
|
1047
1095
|
|
|
@@ -1117,7 +1165,10 @@ ipcMain.on("relay:interactive", (_e, interactive) => {
|
|
|
1117
1165
|
if (win && !win.isDestroyed()) win.setIgnoreMouseEvents(!interactive, { forward: true });
|
|
1118
1166
|
});
|
|
1119
1167
|
ipcMain.on("relay:setPos", (_e, x, y) => {
|
|
1120
|
-
if (win && !win.isDestroyed() && Number.isFinite(x) && Number.isFinite(y))
|
|
1168
|
+
if (win && !win.isDestroyed() && Number.isFinite(x) && Number.isFinite(y)) {
|
|
1169
|
+
win.setPosition(Math.round(x), Math.round(y));
|
|
1170
|
+
reinforceSpacePresence(win);
|
|
1171
|
+
}
|
|
1121
1172
|
});
|
|
1122
1173
|
// ✕ on the card: hide the overlay entirely until the status-area Relay mark is clicked.
|
|
1123
1174
|
// The renderer plays its exit animation first, then sends this.
|
|
@@ -1176,6 +1227,7 @@ if (!gotSingleInstanceLock) {
|
|
|
1176
1227
|
if (process.platform === "darwin" && app.dock) app.dock.hide(); // accessory: no dock icon, no NC banners
|
|
1177
1228
|
createWindow();
|
|
1178
1229
|
createTray();
|
|
1230
|
+
installActiveSpaceWatcher();
|
|
1179
1231
|
});
|
|
1180
1232
|
|
|
1181
1233
|
app.on("window-all-closed", () => {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const ACTIVE_SPACE_CHANGED = "NSWorkspaceActiveSpaceDidChangeNotification";
|
|
2
|
+
|
|
3
|
+
function isUsableWindow(win) {
|
|
4
|
+
return Boolean(win && !(typeof win.isDestroyed === "function" && win.isDestroyed()));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function reinforceSpacePresence(win, { moveTop = false } = {}) {
|
|
8
|
+
if (!isUsableWindow(win)) return false;
|
|
9
|
+
try {
|
|
10
|
+
win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
11
|
+
} catch {}
|
|
12
|
+
try {
|
|
13
|
+
win.setAlwaysOnTop(true, "floating");
|
|
14
|
+
} catch {}
|
|
15
|
+
if (moveTop && typeof win.moveTop === "function") {
|
|
16
|
+
try {
|
|
17
|
+
win.moveTop();
|
|
18
|
+
} catch {}
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function showInactiveOnAllSpaces(win, { force = false } = {}) {
|
|
24
|
+
if (!isUsableWindow(win)) return false;
|
|
25
|
+
const visible = typeof win.isVisible === "function" ? win.isVisible() : false;
|
|
26
|
+
if (visible && !force) {
|
|
27
|
+
reinforceSpacePresence(win);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
reinforceSpacePresence(win);
|
|
32
|
+
if (visible && force && typeof win.hide === "function") win.hide();
|
|
33
|
+
if (typeof win.showInactive === "function") win.showInactive();
|
|
34
|
+
reinforceSpacePresence(win, { moveTop: true });
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function subscribeActiveSpaceChanges(systemPreferences, callback, { log = () => {} } = {}) {
|
|
39
|
+
if (!systemPreferences || typeof systemPreferences.subscribeWorkspaceNotification !== "function") return null;
|
|
40
|
+
try {
|
|
41
|
+
return systemPreferences.subscribeWorkspaceNotification(ACTIVE_SPACE_CHANGED, () => callback());
|
|
42
|
+
} catch (error) {
|
|
43
|
+
log(`space change subscription failed: ${error && error.message ? error.message : String(error)}`);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
ACTIVE_SPACE_CHANGED,
|
|
50
|
+
reinforceSpacePresence,
|
|
51
|
+
showInactiveOnAllSpaces,
|
|
52
|
+
subscribeActiveSpaceChanges,
|
|
53
|
+
};
|
package/package.json
CHANGED
package/src/auto-update.js
CHANGED
|
@@ -172,6 +172,10 @@ export function spawnDetachedUpdate({
|
|
|
172
172
|
` ROOT="$(npm root -g 2>/dev/null)"`,
|
|
173
173
|
` PKG="$ROOT/${PACKAGE_NAME}"`,
|
|
174
174
|
` ELECTRON="$PKG/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron"`,
|
|
175
|
+
` if [ ! -x "$ELECTRON" ] && [ -f "$PKG/node_modules/electron/install.js" ]; then`,
|
|
176
|
+
` echo "[relay-update] $(date) electron runtime missing; running electron install.js" >> ${LOG} 2>&1`,
|
|
177
|
+
` node "$PKG/node_modules/electron/install.js" >> ${LOG} 2>&1 || true`,
|
|
178
|
+
` fi`,
|
|
175
179
|
` if [ -x "$ELECTRON" ] && [ -f "$PKG/bin/relay.js" ] && grep -q '"version"' "$PKG/package.json" 2>/dev/null; then`,
|
|
176
180
|
` echo "[relay-update] $(date) install verified; restarting services" >> ${LOG} 2>&1`,
|
|
177
181
|
` /bin/launchctl kickstart -k gui/${uid}/${PILL_LABEL} >> ${LOG} 2>&1 || true`,
|
|
@@ -179,10 +183,16 @@ export function spawnDetachedUpdate({
|
|
|
179
183
|
` else`,
|
|
180
184
|
` echo "[relay-update] $(date) post-install verify FAILED; restoring ${restore}" >> ${LOG} 2>&1`,
|
|
181
185
|
` npm install -g ${PACKAGE_NAME}@${restore} >> ${LOG} 2>&1 || true`,
|
|
186
|
+
` ROOT="$(npm root -g 2>/dev/null)"`,
|
|
187
|
+
` PKG="$ROOT/${PACKAGE_NAME}"`,
|
|
188
|
+
` if [ -f "$PKG/node_modules/electron/install.js" ]; then node "$PKG/node_modules/electron/install.js" >> ${LOG} 2>&1 || true; fi`,
|
|
182
189
|
` fi`,
|
|
183
190
|
`else`,
|
|
184
191
|
` echo "[relay-update] $(date) install FAILED; restoring ${restore}" >> ${LOG} 2>&1`,
|
|
185
192
|
` npm install -g ${PACKAGE_NAME}@${restore} >> ${LOG} 2>&1 || true`,
|
|
193
|
+
` ROOT="$(npm root -g 2>/dev/null)"`,
|
|
194
|
+
` PKG="$ROOT/${PACKAGE_NAME}"`,
|
|
195
|
+
` if [ -f "$PKG/node_modules/electron/install.js" ]; then node "$PKG/node_modules/electron/install.js" >> ${LOG} 2>&1 || true; fi`,
|
|
186
196
|
`fi`,
|
|
187
197
|
].join("\n");
|
|
188
198
|
const child = spawnImpl("/bin/sh", ["-c", script], { detached: true, stdio: "ignore" });
|
package/src/install.js
CHANGED
|
@@ -240,6 +240,30 @@ function electronPathForPackageRoot(packageRoot) {
|
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
export function ensureElectronRuntime(packageRoot, { runCommand = run } = {}) {
|
|
244
|
+
let electronPath = electronPathForPackageRoot(packageRoot);
|
|
245
|
+
if (electronPath && fs.existsSync(electronPath)) return { ok: true, electronPath, repaired: false };
|
|
246
|
+
|
|
247
|
+
const installScript = path.join(packageRoot, "node_modules", "electron", "install.js");
|
|
248
|
+
if (!fs.existsSync(installScript)) {
|
|
249
|
+
return { ok: false, reason: "electron_install_script_missing", electronPath, installScript };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const res = runCommand(process.execPath, [installScript], { timeoutMs: npmInstallTimeoutMs() });
|
|
253
|
+
electronPath = electronPathForPackageRoot(packageRoot);
|
|
254
|
+
if (res.ok && electronPath && fs.existsSync(electronPath)) {
|
|
255
|
+
return { ok: true, electronPath, repaired: true };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
ok: false,
|
|
260
|
+
reason: "electron_runtime_missing",
|
|
261
|
+
electronPath,
|
|
262
|
+
installScript,
|
|
263
|
+
detail: res.out,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
243
267
|
/** Whether an agent CLI is on PATH. */
|
|
244
268
|
export function commandExists(cmd) {
|
|
245
269
|
const res = spawnSync(cmd, ["--version"], { encoding: "utf8", timeout: 8000 });
|
|
@@ -333,10 +357,11 @@ export function installDaemonAutostart(bin = relayBinPath(), node = process.exec
|
|
|
333
357
|
export function installPillAutostart(bin = relayBinPath(), { platform = process.platform, runCommand = run } = {}) {
|
|
334
358
|
if (platform !== "darwin" && platform !== "win32") return { ok: false, reason: "autostart_unsupported_platform" };
|
|
335
359
|
const packageRoot = packageRootForBin(bin);
|
|
336
|
-
const
|
|
360
|
+
const electron = ensureElectronRuntime(packageRoot, { runCommand });
|
|
361
|
+
const electronPath = electron.electronPath;
|
|
337
362
|
const overlayMain = path.join(packageRoot, "overlay", "main.cjs");
|
|
338
|
-
if (!electronPath || !fs.existsSync(overlayMain)) {
|
|
339
|
-
return { ok: false, reason: "pill_runtime_missing", electronPath, overlayMain };
|
|
363
|
+
if (!electron.ok || !electronPath || !fs.existsSync(overlayMain)) {
|
|
364
|
+
return { ok: false, reason: "pill_runtime_missing", electronPath, overlayMain, electron };
|
|
340
365
|
}
|
|
341
366
|
if (platform === "win32") {
|
|
342
367
|
const logPath = path.join(os.homedir(), ".relay", "pill.log");
|