[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
"use strict";
const assert = require("node:assert/strict");
const test = require("node:test");
const windowManager = require("../windowManager.cjs");
function windowStub(id) {
return {
id,
isDestroyed: () => false,
webContents: {
id,
isCrashed: () => false,
isDestroyed: () => false,
},
};
}
test("lifecycle-only app windows are excluded from dirty-editor queries", (context) => {
const editorWindow = windowStub(101);
const terminalPopup = windowStub(102);
context.after(() => {
windowManager.unregisterAppContentWindow(editorWindow);
windowManager.unregisterAppContentWindow(terminalPopup);
});
windowManager.registerAppContentWindow(editorWindow, { queryDirtyEditors: true });
windowManager.registerAppContentWindow(terminalPopup);
assert.deepEqual(windowManager.getAppContentWindows(), [editorWindow, terminalPopup]);
assert.deepEqual(windowManager.getDirtyEditorWindows(), [editorWindow]);
windowManager.unregisterAppContentWindow(editorWindow);
assert.deepEqual(windowManager.getAppContentWindows(), [terminalPopup]);
assert.deepEqual(windowManager.getDirtyEditorWindows(), []);
});

View File

@@ -0,0 +1,116 @@
const path = require("node:path");
const fs = require("node:fs");
const frontendBackgroundColorCache = new Map();
function hslToHex(h, s, l) {
const hue = ((h % 360) + 360) % 360;
const sat = Math.max(0, Math.min(100, s)) / 100;
const light = Math.max(0, Math.min(100, l)) / 100;
const c = (1 - Math.abs(2 * light - 1)) * sat;
const x = c * (1 - Math.abs(((hue / 60) % 2) - 1));
const m = light - c / 2;
let r1 = 0;
let g1 = 0;
let b1 = 0;
if (hue < 60) {
r1 = c; g1 = x; b1 = 0;
} else if (hue < 120) {
r1 = x; g1 = c; b1 = 0;
} else if (hue < 180) {
r1 = 0; g1 = c; b1 = x;
} else if (hue < 240) {
r1 = 0; g1 = x; b1 = c;
} else if (hue < 300) {
r1 = x; g1 = 0; b1 = c;
} else {
r1 = c; g1 = 0; b1 = x;
}
const toHex = (n) => Math.round((n + m) * 255).toString(16).padStart(2, "0");
return `#${toHex(r1)}${toHex(g1)}${toHex(b1)}`;
}
function normalizeBackgroundColor(value) {
if (!value) return null;
const raw = String(value).trim();
if (!raw) return null;
if (raw.startsWith("#")) return raw;
const parts = raw.split(/\s+/).filter(Boolean);
if (parts.length < 3) return null;
const h = Number(parts[0]);
const s = Number(String(parts[1]).replace("%", ""));
const l = Number(String(parts[2]).replace("%", ""));
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) return null;
return hslToHex(h, s, l);
}
function parseBackgroundFromIndexHtml(indexHtml, theme) {
if (!indexHtml) return null;
const block =
theme === "dark"
? indexHtml.match(/\.dark\s*\{[\s\S]*?\}/)
: indexHtml.match(/:root\s*\{[\s\S]*?\}/);
const within = block?.[0] || indexHtml;
const m = within.match(/--background:\s*([^;]+);/);
const raw = m?.[1]?.trim();
if (!raw) return null;
const parts = raw.split(/\s+/).filter(Boolean);
if (parts.length < 3) return null;
const h = Number(parts[0]);
const s = Number(String(parts[1]).replace("%", ""));
const l = Number(String(parts[2]).replace("%", ""));
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) return null;
return hslToHex(h, s, l);
}
function resolveIndexHtmlPath(electronDir) {
const dist = path.join(electronDir, "../dist/index.html");
const root = path.join(electronDir, "../index.html");
if (fs.existsSync(dist)) return dist;
if (fs.existsSync(root)) return root;
return dist;
}
function trimFrontendBackgroundColorCache() {
if (frontendBackgroundColorCache.size <= 8) return;
const firstKey = frontendBackgroundColorCache.keys().next().value;
if (firstKey) frontendBackgroundColorCache.delete(firstKey);
}
function resolveFrontendBackgroundColor(electronDir, theme) {
try {
const htmlPath = resolveIndexHtmlPath(electronDir);
if (!htmlPath || !fs.existsSync(htmlPath)) return null;
const stat = fs.statSync(htmlPath);
const cacheKey = `${htmlPath}:${stat.mtimeMs}:${theme || "default"}`;
if (frontendBackgroundColorCache.has(cacheKey)) {
return frontendBackgroundColorCache.get(cacheKey);
}
const indexHtml = fs.readFileSync(htmlPath, "utf8");
const color = parseBackgroundFromIndexHtml(indexHtml, theme);
frontendBackgroundColorCache.set(cacheKey, color);
trimFrontendBackgroundColorCache();
return color;
} catch {
return null;
}
}
module.exports = {
hslToHex,
normalizeBackgroundColor,
parseBackgroundFromIndexHtml,
resolveIndexHtmlPath,
resolveFrontendBackgroundColor,
};

View File

@@ -0,0 +1,464 @@
"use strict";
/**
* Multi-monitor display recovery for content windows (#3244).
*
* On Windows, locking the session and letting a secondary display power off
* (or sleeping the machine) temporarily tears that display down. The OS then
* relocates the window onto the primary display. That relocation looks like a
* normal move, so persisted window state is polluted too. When the display
* returns after unlock, put the window back.
*
* Recovery is scoped to lock/sleep (and a short grace after unlock/resume).
* Ordinary unplug/replug while the session is unlocked is out of scope.
* User placement always wins: a manual move/resize, or Win+Shift+Arrow,
* cancels an in-flight restore.
*/
const DEFAULT_RESTORE_GRACE_MS = 8000;
let powerMonitor = null;
try {
const electron = require("electron");
if (electron && typeof electron === "object") {
powerMonitor = electron.powerMonitor || null;
}
} catch {
// Not running inside Electron.
}
function isFiniteBounds(bounds) {
return Boolean(
bounds &&
Number.isFinite(bounds.x) &&
Number.isFinite(bounds.y) &&
Number.isFinite(bounds.width) &&
Number.isFinite(bounds.height) &&
bounds.width > 0 &&
bounds.height > 0
);
}
function boundsEqual(a, b) {
return Boolean(
a &&
b &&
a.x === b.x &&
a.y === b.y &&
a.width === b.width &&
a.height === b.height
);
}
function boundsIntersectDisplay(bounds, displayBounds) {
if (!isFiniteBounds(bounds) || !isFiniteBounds(displayBounds)) return false;
return (
bounds.x < displayBounds.x + displayBounds.width &&
bounds.x + bounds.width > displayBounds.x &&
bounds.y < displayBounds.y + displayBounds.height &&
bounds.y + bounds.height > displayBounds.y
);
}
function normalizeDisplayId(displayId) {
return typeof displayId === "number" && Number.isFinite(displayId) && displayId >= 0
? displayId
: null;
}
function normalizeRecoveryCandidate(candidate) {
if (!candidate) return null;
if (isFiniteBounds(candidate)) return { bounds: candidate, displayId: null };
if (isFiniteBounds(candidate.bounds)) {
return {
bounds: candidate.bounds,
displayId: normalizeDisplayId(candidate.displayId),
};
}
return null;
}
function pickDisplayRecoveryBounds({ addedDisplay, currentBounds, candidates }) {
if (!addedDisplay || !isFiniteBounds(addedDisplay.bounds)) return null;
if (!isFiniteBounds(currentBounds)) return null;
if (boundsIntersectDisplay(currentBounds, addedDisplay.bounds)) return null;
const addedDisplayId = normalizeDisplayId(addedDisplay.id);
for (const candidate of candidates || []) {
const normalized = normalizeRecoveryCandidate(candidate);
if (!normalized) continue;
if (normalized.displayId !== null && addedDisplayId !== null) {
if (normalized.displayId === addedDisplayId) return normalized.bounds;
continue;
}
if (boundsIntersectDisplay(normalized.bounds, addedDisplay.bounds)) {
return normalized.bounds;
}
}
return null;
}
function displayPlacementRect(display) {
if (!display) return null;
if (isFiniteBounds(display.workArea)) return display.workArea;
return isFiniteBounds(display.bounds) ? display.bounds : null;
}
function clampBoundsToDisplay(bounds, displayBounds) {
if (!isFiniteBounds(bounds) || !isFiniteBounds(displayBounds)) return null;
const width = Math.min(bounds.width, displayBounds.width);
const height = Math.min(bounds.height, displayBounds.height);
const x = Math.min(
Math.max(bounds.x, displayBounds.x),
displayBounds.x + displayBounds.width - width
);
const y = Math.min(
Math.max(bounds.y, displayBounds.y),
displayBounds.y + displayBounds.height - height
);
return { x, y, width, height };
}
function attachDisplayRecovery({
win,
screen,
restoreGraceMs = DEFAULT_RESTORE_GRACE_MS,
platform = process.platform,
powerMonitor: injectedPowerMonitor = null,
}) {
if (platform !== "win32" || !win || !screen || typeof screen.on !== "function") {
return () => {};
}
let remembered = null;
let pendingRestore = null;
let restoreUntil = null;
const activeInterruptions = new Set();
let attached = true;
const activePowerMonitor = injectedPowerMonitor || powerMonitor;
const isInterrupted = () => activeInterruptions.size > 0;
const canRestore = () =>
isInterrupted() ||
(restoreUntil !== null && Date.now() < restoreUntil);
const isMaximizedOrFullScreen = () => {
try {
return Boolean(win.isMaximized?.() || win.isFullScreen?.());
} catch {
return false;
}
};
const copyBounds = () => {
try {
if (isMaximizedOrFullScreen() && typeof win.getNormalBounds === "function") {
const normal = win.getNormalBounds();
if (isFiniteBounds(normal)) return { ...normal };
}
const bounds = win.getBounds();
return isFiniteBounds(bounds) ? { ...bounds } : null;
} catch {
return null;
}
};
const isPrimaryDisplay = (display) => {
try {
const primary = screen.getPrimaryDisplay?.();
if (!display || !primary) return false;
const displayId = normalizeDisplayId(display.id);
const primaryId = normalizeDisplayId(primary.id);
if (displayId !== null && primaryId !== null) return displayId === primaryId;
return boundsEqual(display.bounds, primary.bounds);
} catch {
return false;
}
};
const findRememberedDisplay = () => {
if (!remembered) return null;
try {
return (screen.getAllDisplays?.() || []).find((display) => {
if (!display || isPrimaryDisplay(display) || !isFiniteBounds(display.bounds)) {
return false;
}
const displayId = normalizeDisplayId(display.id);
if (remembered.displayId !== null && displayId !== null) {
return remembered.displayId === displayId;
}
return boundsIntersectDisplay(remembered.bounds, display.bounds);
}) || null;
} catch {
return null;
}
};
const applyRestore = (display, bounds) => {
const clamped = clampBoundsToDisplay(bounds, displayPlacementRect(display));
if (!clamped) return false;
const displayId = normalizeDisplayId(display?.id) ?? remembered?.displayId ?? null;
remembered = { bounds: clamped, displayId };
if (isMaximizedOrFullScreen()) {
pendingRestore = { bounds: clamped, displayId };
return true;
}
pendingRestore = null;
try {
win.setBounds(clamped);
} catch {
return false;
}
return true;
};
const restoreToDisplay = (display) => {
if (!attached || !canRestore() || !remembered || !display || isPrimaryDisplay(display)) {
return false;
}
const currentBounds = copyBounds();
if (
isFiniteBounds(currentBounds) &&
boundsIntersectDisplay(currentBounds, display.bounds)
) {
return false;
}
let restored = pickDisplayRecoveryBounds({
addedDisplay: display,
currentBounds,
candidates: [remembered],
});
// Dual-screen lock/sleep often re-enumerates the only secondary with a
// transient id and moved bounds. Identity and geometry both miss; the
// sole remaining secondary is still that display.
if (!restored && isFiniteBounds(currentBounds)) {
try {
const secondaries = (screen.getAllDisplays?.() || []).filter(
(candidate) => candidate && !isPrimaryDisplay(candidate)
);
if (
secondaries.length === 1 &&
(secondaries[0] === display ||
normalizeDisplayId(secondaries[0].id) === normalizeDisplayId(display.id) ||
boundsEqual(secondaries[0].bounds, display.bounds))
) {
restored = remembered.bounds;
}
} catch {
// Fall through without guessing.
}
}
if (!restored) return false;
return applyRestore(display, restored);
};
const restoreIfNeeded = () => {
const display = findRememberedDisplay();
if (!display) return false;
return restoreToDisplay(display);
};
const rememberPlacement = () => {
if (!attached || isInterrupted()) return;
if (canRestore()) {
// Queued OS relocation can still arrive after unlock. Put the window
// back unless the user has already cancelled recovery.
restoreIfNeeded();
return;
}
try {
const bounds = copyBounds();
if (!bounds) return;
const display = screen.getDisplayMatching?.(bounds);
if (!display) return;
if (isPrimaryDisplay(display)) {
const rememberedDisplay = findRememberedDisplay();
// Still-connected secondary + primary placement is a user move.
if (rememberedDisplay) remembered = null;
return;
}
remembered = {
bounds,
displayId: normalizeDisplayId(display.id),
};
} catch {
// Screen queries can fail during display teardown.
}
};
const cancelUserIntent = () => {
if (isInterrupted()) return;
pendingRestore = null;
restoreUntil = null;
// A manual move/resize after unlock is the user's new placement. Drop the
// frozen secondary snapshot so a later ordinary lock/unlock cannot restore
// it after the display happens to reconnect.
remembered = null;
};
const onManualPlacement = (_event, nextBounds) => {
if (!attached) return;
cancelUserIntent();
if (isFiniteBounds(nextBounds)) {
try {
const display = screen.getDisplayMatching?.(nextBounds);
if (display && !isPrimaryDisplay(display)) {
remembered = {
bounds: { ...nextBounds },
displayId: normalizeDisplayId(display.id),
};
return;
}
} catch {
// Fall through to ordinary tracking.
}
}
rememberPlacement();
};
const onBeforeInputEvent = (_event, input) => {
if (
input?.type === "keyDown" &&
input.meta === true &&
input.shift === true &&
(input.key === "ArrowLeft" || input.key === "ArrowRight")
) {
cancelUserIntent();
}
};
const onSessionInterrupted = (signal) => {
activeInterruptions.add(signal);
restoreUntil = null;
// Freeze the last secondary placement. If the window is still on a
// secondary display at lock/sleep time, refresh the snapshot.
try {
const bounds = copyBounds();
const display = bounds ? screen.getDisplayMatching?.(bounds) : null;
if (bounds && display && !isPrimaryDisplay(display)) {
remembered = {
bounds,
displayId: normalizeDisplayId(display.id),
};
}
} catch {
// Keep whatever placement we already have.
}
};
const onSessionResumed = (signal) => {
activeInterruptions.delete(signal);
if (activeInterruptions.size > 0) return;
restoreUntil = Date.now() + restoreGraceMs;
// display-added can land while the window still overlaps the returning
// secondary. Windows may then queue the relocation to the primary before
// unlock, and that move is ignored while the session is interrupted. Retry
// now; there may be no later display or window event.
restoreIfNeeded();
};
const onDisplayRemoved = (_event, oldDisplay) => {
if (!attached || !oldDisplay) return;
const currentBounds = copyBounds();
if (
isFiniteBounds(currentBounds) &&
isFiniteBounds(oldDisplay.bounds) &&
boundsIntersectDisplay(currentBounds, oldDisplay.bounds) &&
!isPrimaryDisplay(oldDisplay)
) {
remembered = {
bounds: currentBounds,
displayId: normalizeDisplayId(oldDisplay.id) ?? remembered?.displayId ?? null,
};
}
};
const onDisplayAdded = (_event, display) => {
if (!attached) return;
restoreToDisplay(display);
};
const applyPendingRestore = () => {
if (!attached || !pendingRestore || isMaximizedOrFullScreen()) return;
const display = findRememberedDisplay();
if (!display) return;
const currentBounds = copyBounds();
if (
isFiniteBounds(currentBounds) &&
boundsIntersectDisplay(currentBounds, display.bounds)
) {
pendingRestore = null;
return;
}
applyRestore(display, pendingRestore.bounds);
};
const onSuspend = () => onSessionInterrupted("suspend");
const onLockScreen = () => onSessionInterrupted("lock-screen");
const onResume = () => onSessionResumed("suspend");
const onUnlockScreen = () => onSessionResumed("lock-screen");
try {
activePowerMonitor?.on?.("suspend", onSuspend);
activePowerMonitor?.on?.("lock-screen", onLockScreen);
activePowerMonitor?.on?.("resume", onResume);
activePowerMonitor?.on?.("unlock-screen", onUnlockScreen);
} catch {
// Lock/sleep tracking is best-effort.
}
try {
screen.on("display-removed", onDisplayRemoved);
screen.on("display-added", onDisplayAdded);
screen.on("display-metrics-changed", onDisplayAdded);
win.on?.("will-move", onManualPlacement);
win.on?.("will-resize", onManualPlacement);
win.on?.("move", rememberPlacement);
win.on?.("resize", rememberPlacement);
win.on?.("unmaximize", applyPendingRestore);
win.on?.("leave-full-screen", applyPendingRestore);
rememberPlacement();
} catch {
return () => {};
}
try {
win.webContents?.on?.("before-input-event", onBeforeInputEvent);
} catch {
// Keyboard cancellation is best-effort.
}
return function detach() {
attached = false;
try {
activePowerMonitor?.removeListener?.("suspend", onSuspend);
activePowerMonitor?.removeListener?.("lock-screen", onLockScreen);
activePowerMonitor?.removeListener?.("resume", onResume);
activePowerMonitor?.removeListener?.("unlock-screen", onUnlockScreen);
} catch {}
try { screen.removeListener?.("display-removed", onDisplayRemoved); } catch {}
try { screen.removeListener?.("display-added", onDisplayAdded); } catch {}
try { screen.removeListener?.("display-metrics-changed", onDisplayAdded); } catch {}
try { win.removeListener?.("will-move", onManualPlacement); } catch {}
try { win.removeListener?.("will-resize", onManualPlacement); } catch {}
try { win.removeListener?.("move", rememberPlacement); } catch {}
try { win.removeListener?.("resize", rememberPlacement); } catch {}
try { win.removeListener?.("unmaximize", applyPendingRestore); } catch {}
try { win.removeListener?.("leave-full-screen", applyPendingRestore); } catch {}
try {
win.webContents?.removeListener?.("before-input-event", onBeforeInputEvent);
} catch {}
remembered = null;
pendingRestore = null;
restoreUntil = null;
activeInterruptions.clear();
};
}
module.exports = {
attachDisplayRecovery,
boundsIntersectDisplay,
clampBoundsToDisplay,
isFiniteBounds,
pickDisplayRecoveryBounds,
};

View File

@@ -0,0 +1,570 @@
"use strict";
const assert = require("node:assert/strict");
const test = require("node:test");
const {
attachDisplayRecovery: attachDisplayRecoveryForPlatform,
boundsIntersectDisplay,
clampBoundsToDisplay,
pickDisplayRecoveryBounds,
} = require("./displayRecovery.cjs");
function attachDisplayRecovery(options) {
return attachDisplayRecoveryForPlatform({ ...options, platform: "win32" });
}
const PRIMARY = { id: 1, bounds: { x: 0, y: 0, width: 1920, height: 1080 } };
const SECONDARY = { id: 2, bounds: { x: 1920, y: 0, width: 2560, height: 1440 } };
function createMockWindow(initialBounds) {
const listeners = new Map();
const webContentsListeners = new Map();
const win = {
bounds: { ...initialBounds },
destroyed: false,
maximized: false,
fullScreen: false,
setBoundsCalls: [],
webContents: {
on(event, handler) {
if (!webContentsListeners.has(event)) webContentsListeners.set(event, []);
webContentsListeners.get(event).push(handler);
},
removeListener(event, handler) {
const list = webContentsListeners.get(event) || [];
const index = list.indexOf(handler);
if (index >= 0) list.splice(index, 1);
},
emit(event, ...args) {
for (const handler of webContentsListeners.get(event) || []) handler(...args);
},
},
isDestroyed() {
return win.destroyed;
},
isMaximized() {
return win.maximized;
},
isFullScreen() {
return win.fullScreen;
},
getBounds() {
return { ...win.bounds };
},
getNormalBounds() {
return { ...(win.normalBounds || win.bounds) };
},
unmaximize() {
win.maximized = false;
for (const handler of listeners.get("unmaximize") || []) handler();
},
setBounds(next) {
win.setBoundsCalls.push({ ...next });
win.bounds = { ...next };
for (const handler of listeners.get("move") || []) handler();
},
on(event, handler) {
if (!listeners.has(event)) listeners.set(event, []);
listeners.get(event).push(handler);
},
removeListener(event, handler) {
const list = listeners.get(event) || [];
const index = list.indexOf(handler);
if (index >= 0) list.splice(index, 1);
},
__listeners: listeners,
__webContentsListeners: webContentsListeners,
};
return win;
}
function createMockScreen({ primary = PRIMARY, displays = [PRIMARY, SECONDARY] } = {}) {
const listeners = new Map();
const connected = [...displays];
const mock = {
on(event, handler) {
if (!listeners.has(event)) listeners.set(event, []);
listeners.get(event).push(handler);
},
removeListener(event, handler) {
const list = listeners.get(event) || [];
const index = list.indexOf(handler);
if (index >= 0) list.splice(index, 1);
},
emit(event, ...args) {
const display = args[1] || args[0];
if (event === "display-removed" && display) {
const displayIdIsTransient =
typeof display.id !== "number" || !Number.isFinite(display.id) || display.id < 0;
let index = displayIdIsTransient
? connected.indexOf(display)
: connected.findIndex((candidate) => candidate.id === display.id);
if (index < 0 && display.bounds) {
index = connected.findIndex(
(candidate) =>
candidate.bounds?.x === display.bounds.x &&
candidate.bounds?.y === display.bounds.y &&
candidate.bounds?.width === display.bounds.width &&
candidate.bounds?.height === display.bounds.height
);
}
if (index >= 0) connected.splice(index, 1);
}
if (event === "display-added" && display) {
const displayIdIsTransient =
typeof display.id !== "number" || !Number.isFinite(display.id) || display.id < 0;
if (
(displayIdIsTransient && !connected.includes(display)) ||
(!displayIdIsTransient && !connected.some((candidate) => candidate.id === display.id))
) {
connected.push(display);
}
}
for (const handler of listeners.get(event) || []) handler(...args);
},
getPrimaryDisplay() {
return primary;
},
getAllDisplays() {
return [...connected];
},
getDisplayMatching(bounds) {
let best = null;
let bestArea = 0;
for (const display of connected) {
const overlap = boundsIntersectDisplay(bounds, display.bounds)
? Math.min(bounds.x + bounds.width, display.bounds.x + display.bounds.width) -
Math.max(bounds.x, display.bounds.x)
: 0;
if (overlap > bestArea) {
bestArea = overlap;
best = display;
}
}
return best || connected[0];
},
__listeners: listeners,
};
return mock;
}
function createMockPowerMonitor() {
const listeners = new Map();
return {
on(event, handler) {
if (!listeners.has(event)) listeners.set(event, []);
listeners.get(event).push(handler);
},
removeListener(event, handler) {
const list = listeners.get(event) || [];
const index = list.indexOf(handler);
if (index >= 0) list.splice(index, 1);
},
emit(event) {
for (const handler of listeners.get(event) || []) handler();
},
__listeners: listeners,
};
}
function moveWindowManually(win, nextBounds) {
for (const handler of win.__listeners.get("will-move") || []) {
handler({}, { ...nextBounds });
}
win.bounds = { ...nextBounds };
for (const handler of win.__listeners.get("move") || []) handler();
}
function lockSleepUnlockRestore({
win,
screen,
powerMonitor,
secondary = SECONDARY,
afterLockBounds = { x: 100, y: 100, width: 1400, height: 900 },
} = {}) {
powerMonitor.emit("lock-screen");
screen.emit("display-removed", {}, secondary);
win.bounds = { ...afterLockBounds };
for (const handler of win.__listeners.get("move") || []) handler();
screen.emit("display-added", {}, secondary);
powerMonitor.emit("unlock-screen");
}
test("boundsIntersectDisplay detects overlap and rejects invalid input", () => {
assert.equal(
boundsIntersectDisplay({ x: 2000, y: 100, width: 800, height: 600 }, SECONDARY.bounds),
true
);
assert.equal(
boundsIntersectDisplay({ x: 0, y: 0, width: 800, height: 600 }, SECONDARY.bounds),
false
);
assert.equal(boundsIntersectDisplay(null, SECONDARY.bounds), false);
});
test("pickDisplayRecoveryBounds restores a remembered placement on the re-added display", () => {
const restored = pickDisplayRecoveryBounds({
addedDisplay: SECONDARY,
currentBounds: { x: 100, y: 100, width: 1200, height: 800 },
candidates: [{ x: 2000, y: 100, width: 1200, height: 800 }],
});
assert.deepEqual(restored, { x: 2000, y: 100, width: 1200, height: 800 });
});
test("pickDisplayRecoveryBounds does nothing when the window is already on the display", () => {
const restored = pickDisplayRecoveryBounds({
addedDisplay: SECONDARY,
currentBounds: { x: 2000, y: 100, width: 1200, height: 800 },
candidates: [{ x: 2100, y: 100, width: 1200, height: 800 }],
});
assert.equal(restored, null);
});
test("pickDisplayRecoveryBounds matches a candidate by display identity when bounds changed", () => {
const restored = pickDisplayRecoveryBounds({
addedDisplay: { id: 2, bounds: { x: 1920, y: 0, width: 1024, height: 768 } },
currentBounds: { x: 100, y: 100, width: 1200, height: 800 },
candidates: [{ bounds: { x: 2000, y: 100, width: 1200, height: 800 }, displayId: 2 }],
});
assert.deepEqual(restored, { x: 2000, y: 100, width: 1200, height: 800 });
});
test("pickDisplayRecoveryBounds falls back to geometry while the added display id is unknown", () => {
const rememberedBounds = { x: 2000, y: 100, width: 1400, height: 900 };
const restored = pickDisplayRecoveryBounds({
addedDisplay: { id: -1, bounds: SECONDARY.bounds },
currentBounds: { x: 100, y: 100, width: 1200, height: 800 },
candidates: [{ bounds: rememberedBounds, displayId: SECONDARY.id }],
});
assert.deepEqual(restored, rememberedBounds);
});
test("clampBoundsToDisplay keeps the restored window fully visible", () => {
const clamped = clampBoundsToDisplay(
{ x: 3000, y: -200, width: 3000, height: 2000 },
SECONDARY.bounds
);
assert.deepEqual(clamped, { x: 1920, y: 0, width: 2560, height: 1440 });
});
test("attachDisplayRecovery restores after lock and display sleep", () => {
const secondaryBounds = { x: 2100, y: 120, width: 1400, height: 900 };
const win = createMockWindow({ ...secondaryBounds });
const screen = createMockScreen();
const powerMonitor = createMockPowerMonitor();
attachDisplayRecovery({ win, screen, powerMonitor });
lockSleepUnlockRestore({ win, screen, powerMonitor });
assert.equal(win.setBoundsCalls.length, 1);
assert.deepEqual(win.setBoundsCalls[0], secondaryBounds);
});
test("attachDisplayRecovery restores when the display returns with changed bounds", () => {
const secondaryBounds = { x: 2100, y: 120, width: 1400, height: 900 };
const returning = { id: 2, bounds: { x: 1920, y: 0, width: 1024, height: 768 } };
const win = createMockWindow({ ...secondaryBounds });
const screen = createMockScreen();
const powerMonitor = createMockPowerMonitor();
attachDisplayRecovery({ win, screen, powerMonitor });
powerMonitor.emit("lock-screen");
screen.emit("display-removed", {}, SECONDARY);
win.bounds = { x: 100, y: 100, width: 1400, height: 900 };
for (const handler of win.__listeners.get("move") || []) handler();
screen.emit("display-added", {}, returning);
powerMonitor.emit("unlock-screen");
assert.equal(win.setBoundsCalls.length, 1);
assert.equal(boundsIntersectDisplay(win.setBoundsCalls[0], returning.bounds), true);
});
test("attachDisplayRecovery restores a sole secondary that returns with an unknown id", () => {
const secondaryBounds = { x: 2100, y: 120, width: 1400, height: 900 };
const returning = { id: -1, bounds: { x: -2560, y: 0, width: 2560, height: 1440 } };
const win = createMockWindow({ ...secondaryBounds });
const screen = createMockScreen();
const powerMonitor = createMockPowerMonitor();
attachDisplayRecovery({ win, screen, powerMonitor });
powerMonitor.emit("lock-screen");
screen.emit("display-removed", {}, SECONDARY);
win.bounds = { x: 100, y: 100, width: 1400, height: 900 };
for (const handler of win.__listeners.get("move") || []) handler();
screen.emit("display-added", {}, returning);
powerMonitor.emit("unlock-screen");
assert.equal(win.setBoundsCalls.length, 1);
assert.equal(boundsIntersectDisplay(win.setBoundsCalls[0], returning.bounds), true);
});
test("attachDisplayRecovery keeps the snapshot when Windows relocates before display-removed", () => {
const secondaryBounds = { x: 2100, y: 120, width: 1400, height: 900 };
const win = createMockWindow({ ...secondaryBounds });
const screen = createMockScreen();
const powerMonitor = createMockPowerMonitor();
attachDisplayRecovery({ win, screen, powerMonitor });
powerMonitor.emit("lock-screen");
win.bounds = { x: 100, y: 100, width: 1400, height: 900 };
for (const handler of win.__listeners.get("move") || []) handler();
screen.emit("display-removed", {}, SECONDARY);
screen.emit("display-added", {}, SECONDARY);
powerMonitor.emit("unlock-screen");
assert.equal(win.setBoundsCalls.length, 1);
assert.deepEqual(win.setBoundsCalls[0], secondaryBounds);
});
test("attachDisplayRecovery restores after a long sleep because lock/suspend stays active", () => {
const realNow = Date.now;
let now = 1_000_000;
Date.now = () => now;
const secondaryBounds = { x: 2100, y: 120, width: 1400, height: 900 };
const win = createMockWindow({ ...secondaryBounds });
const screen = createMockScreen();
const powerMonitor = createMockPowerMonitor();
try {
attachDisplayRecovery({ win, screen, powerMonitor });
powerMonitor.emit("lock-screen");
powerMonitor.emit("suspend");
screen.emit("display-removed", {}, SECONDARY);
win.bounds = { x: 100, y: 100, width: 1400, height: 900 };
now += 8 * 60 * 60 * 1000;
powerMonitor.emit("resume");
powerMonitor.emit("unlock-screen");
screen.emit("display-added", {}, SECONDARY);
assert.equal(win.setBoundsCalls.length, 1);
assert.deepEqual(win.setBoundsCalls[0], secondaryBounds);
} finally {
Date.now = realNow;
}
});
test("attachDisplayRecovery keeps a deferred restore until the window is unmaximized", () => {
const secondaryBounds = { x: 2100, y: 120, width: 1400, height: 900 };
const win = createMockWindow({ ...secondaryBounds });
win.maximized = true;
win.normalBounds = { ...secondaryBounds };
const screen = createMockScreen();
const powerMonitor = createMockPowerMonitor();
attachDisplayRecovery({ win, screen, powerMonitor });
powerMonitor.emit("lock-screen");
screen.emit("display-removed", {}, SECONDARY);
win.bounds = { x: 100, y: 100, width: 1400, height: 900 };
win.normalBounds = { ...win.bounds };
for (const handler of win.__listeners.get("move") || []) handler();
screen.emit("display-added", {}, SECONDARY);
powerMonitor.emit("unlock-screen");
assert.equal(win.setBoundsCalls.length, 0);
win.unmaximize();
assert.equal(win.setBoundsCalls.length, 1);
assert.deepEqual(win.setBoundsCalls[0], secondaryBounds);
});
test("attachDisplayRecovery retries restore when unlock follows a locked relocation", () => {
const secondaryBounds = { x: 2100, y: 120, width: 1400, height: 900 };
const win = createMockWindow({ ...secondaryBounds });
const screen = createMockScreen();
const powerMonitor = createMockPowerMonitor();
attachDisplayRecovery({ win, screen, powerMonitor });
powerMonitor.emit("lock-screen");
screen.emit("display-removed", {}, SECONDARY);
screen.emit("display-added", {}, SECONDARY);
assert.equal(win.setBoundsCalls.length, 0);
// Windows relocates the window while the session is still locked. Unlock
// must retry; no later display event is guaranteed.
win.bounds = { x: 100, y: 100, width: 1400, height: 900 };
for (const handler of win.__listeners.get("move") || []) handler();
assert.equal(win.setBoundsCalls.length, 0);
powerMonitor.emit("unlock-screen");
assert.equal(win.setBoundsCalls.length, 1);
assert.deepEqual(win.setBoundsCalls[0], secondaryBounds);
});
test("attachDisplayRecovery restores a late OS move that arrives after the display returns", () => {
const secondaryBounds = { x: 2100, y: 120, width: 1400, height: 900 };
const win = createMockWindow({ ...secondaryBounds });
const screen = createMockScreen();
const powerMonitor = createMockPowerMonitor();
attachDisplayRecovery({ win, screen, powerMonitor });
powerMonitor.emit("lock-screen");
screen.emit("display-removed", {}, SECONDARY);
screen.emit("display-added", {}, SECONDARY);
powerMonitor.emit("unlock-screen");
assert.equal(win.setBoundsCalls.length, 0);
win.bounds = { x: 100, y: 100, width: 1400, height: 900 };
for (const handler of win.__listeners.get("move") || []) handler();
assert.equal(win.setBoundsCalls.length, 1);
assert.deepEqual(win.setBoundsCalls[0], secondaryBounds);
});
test("attachDisplayRecovery does not restore a user move to the primary before lock", () => {
const secondaryBounds = { x: 2100, y: 120, width: 1400, height: 900 };
const primaryBounds = { x: 100, y: 100, width: 1400, height: 900 };
const win = createMockWindow({ ...secondaryBounds });
const screen = createMockScreen();
const powerMonitor = createMockPowerMonitor();
attachDisplayRecovery({ win, screen, powerMonitor, restoreGraceMs: 0 });
moveWindowManually(win, primaryBounds);
lockSleepUnlockRestore({ win, screen, powerMonitor, afterLockBounds: primaryBounds });
assert.equal(win.setBoundsCalls.length, 0);
assert.deepEqual(win.bounds, primaryBounds);
});
test("attachDisplayRecovery does not restore a stale snapshot on a later lock after the user stayed on primary", () => {
const secondaryBounds = { x: 2100, y: 120, width: 1400, height: 900 };
const primaryBounds = { x: 300, y: 200, width: 1400, height: 900 };
const win = createMockWindow({ ...secondaryBounds });
const screen = createMockScreen();
const powerMonitor = createMockPowerMonitor();
attachDisplayRecovery({ win, screen, powerMonitor });
powerMonitor.emit("lock-screen");
screen.emit("display-removed", {}, SECONDARY);
win.bounds = { x: 100, y: 100, width: 1400, height: 900 };
for (const handler of win.__listeners.get("move") || []) handler();
powerMonitor.emit("unlock-screen");
assert.equal(win.setBoundsCalls.length, 0);
moveWindowManually(win, primaryBounds);
screen.emit("display-added", {}, SECONDARY);
powerMonitor.emit("lock-screen");
powerMonitor.emit("unlock-screen");
assert.equal(win.setBoundsCalls.length, 0);
assert.deepEqual(win.bounds, primaryBounds);
});
test("attachDisplayRecovery lets an immediate post-unlock user move win", () => {
const secondaryBounds = { x: 2100, y: 120, width: 1400, height: 900 };
const primaryBounds = { x: 300, y: 200, width: 1400, height: 900 };
const win = createMockWindow({ ...secondaryBounds });
const screen = createMockScreen();
const powerMonitor = createMockPowerMonitor();
attachDisplayRecovery({ win, screen, powerMonitor });
lockSleepUnlockRestore({ win, screen, powerMonitor });
assert.equal(win.setBoundsCalls.length, 1);
moveWindowManually(win, primaryBounds);
win.bounds = { x: 80, y: 80, width: 1400, height: 900 };
for (const handler of win.__listeners.get("move") || []) handler();
assert.equal(win.setBoundsCalls.length, 1);
assert.deepEqual(win.bounds, { x: 80, y: 80, width: 1400, height: 900 });
});
test("attachDisplayRecovery does not restore ordinary unplug while unlocked", () => {
const secondaryBounds = { x: 2100, y: 120, width: 1400, height: 900 };
const win = createMockWindow({ ...secondaryBounds });
const screen = createMockScreen();
const powerMonitor = createMockPowerMonitor();
attachDisplayRecovery({ win, screen, powerMonitor, restoreGraceMs: 0 });
screen.emit("display-removed", {}, SECONDARY);
win.bounds = { x: 100, y: 100, width: 1400, height: 900 };
for (const handler of win.__listeners.get("move") || []) handler();
screen.emit("display-added", {}, SECONDARY);
assert.equal(win.setBoundsCalls.length, 0);
});
test("attachDisplayRecovery cancels restore when the user presses Win+Shift+Arrow", () => {
const secondaryBounds = { x: 2100, y: 120, width: 1400, height: 900 };
const win = createMockWindow({ ...secondaryBounds });
const screen = createMockScreen();
const powerMonitor = createMockPowerMonitor();
attachDisplayRecovery({ win, screen, powerMonitor });
powerMonitor.emit("lock-screen");
screen.emit("display-removed", {}, SECONDARY);
screen.emit("display-added", {}, SECONDARY);
powerMonitor.emit("unlock-screen");
win.webContents.emit("before-input-event", {}, {
type: "keyDown",
key: "ArrowLeft",
meta: true,
shift: true,
});
win.bounds = { x: 100, y: 100, width: 1400, height: 900 };
for (const handler of win.__listeners.get("move") || []) handler();
assert.equal(win.setBoundsCalls.length, 0);
});
test("attachDisplayRecovery clamps restored windows to the display work area", () => {
const DOCKED = {
id: 2,
bounds: { x: 1920, y: 0, width: 2560, height: 1440 },
workArea: { x: 1920, y: 40, width: 2560, height: 1400 },
};
const win = createMockWindow({ x: 2000, y: 20, width: 1400, height: 900 });
const screen = createMockScreen({ displays: [PRIMARY, DOCKED] });
const powerMonitor = createMockPowerMonitor();
attachDisplayRecovery({ win, screen, powerMonitor });
powerMonitor.emit("lock-screen");
screen.emit("display-removed", {}, DOCKED);
win.bounds = { x: 100, y: 100, width: 1400, height: 900 };
for (const handler of win.__listeners.get("move") || []) handler();
screen.emit("display-added", {}, DOCKED);
powerMonitor.emit("unlock-screen");
assert.equal(win.setBoundsCalls.length, 1);
assert.equal(win.setBoundsCalls[0].y >= 40, true);
});
test("attachDisplayRecovery does nothing when the window never left the primary display", () => {
const win = createMockWindow({ x: 100, y: 100, width: 1200, height: 800 });
const screen = createMockScreen();
const powerMonitor = createMockPowerMonitor();
attachDisplayRecovery({ win, screen, powerMonitor });
lockSleepUnlockRestore({ win, screen, powerMonitor, afterLockBounds: win.bounds });
assert.equal(win.setBoundsCalls.length, 0);
});
test("detach removes all listeners and stops recovery", () => {
const secondaryBounds = { x: 2100, y: 120, width: 1400, height: 900 };
const win = createMockWindow({ ...secondaryBounds });
const screen = createMockScreen();
const powerMonitor = createMockPowerMonitor();
const detach = attachDisplayRecovery({ win, screen, powerMonitor });
detach();
lockSleepUnlockRestore({ win, screen, powerMonitor });
assert.equal(win.setBoundsCalls.length, 0);
});
test("attachDisplayRecovery stays disabled outside Windows", () => {
const win = createMockWindow({ x: 2100, y: 120, width: 1400, height: 900 });
const screen = createMockScreen();
const detach = attachDisplayRecoveryForPlatform({ win, screen, platform: "darwin" });
screen.emit("display-removed", {}, SECONDARY);
screen.emit("display-added", {}, SECONDARY);
detach();
assert.equal(win.setBoundsCalls.length, 0);
});
test("attachDisplayRecovery tolerates a missing screen module", () => {
const detach = attachDisplayRecoveryForPlatform({ win: {}, screen: null, platform: "win32" });
assert.equal(typeof detach, "function");
detach();
});

View File

@@ -0,0 +1,300 @@
/* eslint-disable no-undef */
function createExternalWindowApi(ctx) {
with (ctx) {
const fallbackBrowserWindows = new Set();
function resolveAppIconFromOptions(options = {}) {
const { appIcon, getAppIcon } = options;
if (typeof getAppIcon === "function") {
try {
const resolved = getAppIcon();
if (resolved) return resolved;
} catch {
// ignore
}
}
return appIcon;
}
/**
* Open a URL in a minimal in-app BrowserWindow. Used as a fallback when the
* host OS cannot open the URL with the system browser (e.g. Tiny11 / Windows
* with no default browser configured — error 0x483). The window is
* intentionally stripped down:
* - no preload script (remote content must NEVER touch contextBridge)
* - sandboxed + contextIsolated renderer
* - a separate persisted session partition so cookies and storage do not
* leak into the main app session
*/
function openFallbackBrowser(url, options = {}) {
const { backgroundColor, appIcon, getAppIcon } = options;
const icon = resolveAppIconFromOptions(options);
const electron = require("electron");
const { BrowserWindow, screen } = electron;
// Size and center relative to the main window when possible.
let bounds = { width: 1100, height: 740 };
try {
if (mainWindow && !mainWindow.isDestroyed()) {
const mainBounds = mainWindow.getBounds();
const display = screen.getDisplayMatching(mainBounds);
const area = display.workArea;
const w = Math.min(1200, Math.round(area.width * 0.85));
const h = Math.min(800, Math.round(area.height * 0.85));
bounds = {
width: w,
height: h,
x: Math.round(area.x + (area.width - w) / 2),
y: Math.round(area.y + (area.height - h) / 2),
};
}
} catch {
// Fall through to default bounds.
}
const win = new BrowserWindow({
...bounds,
title: url,
backgroundColor: backgroundColor || THEME_COLORS[currentTheme]?.background,
icon,
show: false,
autoHideMenuBar: true,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
spellcheck: false,
webSecurity: true,
// Isolated session so users' browsing does not mix with main app state.
partition: "persist:netcatty-fallback-browser",
},
});
fallbackBrowserWindows.add(win);
win.on("closed", () => {
fallbackBrowserWindows.delete(win);
});
// Reflect the loaded page title in the window bar; fall back to the URL.
try {
win.webContents.on("page-title-updated", (_event, title) => {
try {
win.setTitle(title || url);
} catch {
// ignore
}
});
} catch {
// ignore
}
// Popups inside the fallback browser: open them in another fallback window
// rather than looping back through shell.openExternal (which is what
// failed in the first place). These popups are fire-and-forget, so we
// explicitly catch the `loaded` rejection to avoid unhandledRejection.
try {
win.webContents.setWindowOpenHandler((details) => {
const targetUrl = details?.url;
if (targetUrl && typeof targetUrl === "string" && /^https?:/i.test(targetUrl)) {
try {
const popup = openFallbackBrowser(targetUrl, { backgroundColor, appIcon, getAppIcon });
popup.loaded.catch((err) => {
console.warn("[windowManager] fallback popup loadURL failed:", err?.message || err);
});
} catch (popupErr) {
console.warn("[windowManager] fallback popup open failed:", popupErr?.message || popupErr);
}
}
return { action: "deny" };
});
} catch {
// ignore
}
// Minimal keyboard navigation: Alt+← / Alt+→ / Ctrl/Cmd+R.
try {
win.webContents.on("before-input-event", (_event, input) => {
if (input.type !== "keyDown") return;
try {
const history = win.webContents.navigationHistory;
if (input.alt && input.key === "ArrowLeft" && history?.canGoBack?.()) {
history.goBack();
} else if (input.alt && input.key === "ArrowRight" && history?.canGoForward?.()) {
history.goForward();
} else if ((input.control || input.meta) && typeof input.key === "string" && input.key.toLowerCase() === "r") {
win.webContents.reload();
}
} catch {
// ignore navigation errors
}
});
} catch {
// ignore
}
win.once("ready-to-show", () => {
try {
win.show();
} catch {
// ignore
}
});
// Return the window together with its initial-load Promise. Callers that
// care about whether the page actually loaded can await `loaded`; fire-
// and-forget callers must still catch the rejection themselves to avoid
// turning it into an unhandledRejection.
const loaded = win.loadURL(url);
return { window: win, loaded };
}
/**
* Try to open a URL with the OS default browser via shell.openExternal; if
* that fails (e.g. no default browser configured), fall back to the in-app
* BrowserWindow. Resolves on success (either via system browser, or when
* the in-app fallback window finishes its initial load). Throws on total
* failure so callers that rely on rejection semantics (e.g. OAuth flows
* waiting on a Promise.race) still abort cleanly when no browser path is
* available.
*/
async function tryOpenExternalWithFallback(shell, url, options = {}) {
if (!url || typeof url !== "string" || !/^https?:/i.test(url)) {
throw new Error("openExternal: invalid URL");
}
try {
await shell?.openExternal?.(url);
return;
} catch (err) {
const message = err?.message || String(err);
console.warn("[windowManager] shell.openExternal failed, using in-app fallback:", message);
let fallback;
try {
fallback = openFallbackBrowser(url, options);
} catch (createErr) {
console.warn("[windowManager] fallback browser creation failed:", createErr?.message || createErr);
throw err instanceof Error ? err : new Error(message);
}
try {
// Wait for the fallback window's initial load. If the URL is
// unreachable or malformed, loadURL rejects — surface that as a real
// failure so callers (e.g. OAuth flows) can cancel early instead of
// waiting for a downstream timeout.
await fallback.loaded;
return;
} catch (loadErr) {
console.warn("[windowManager] fallback browser loadURL failed:", loadErr?.message || loadErr);
try {
if (fallback.window && !fallback.window.isDestroyed()) {
fallback.window.close();
}
} catch {
// ignore cleanup errors
}
throw err instanceof Error ? err : new Error(message);
}
}
}
function createExternalOnlyWindowOpenHandler(shell, options = {}) {
return (details) => {
const targetUrl = details?.url;
if (targetUrl && typeof targetUrl === "string" && /^https?:/i.test(targetUrl)) {
// Run async fallback path without blocking the window-open decision.
tryOpenExternalWithFallback(shell, targetUrl, options).catch((err) => {
console.warn("[windowManager] tryOpenExternalWithFallback threw:", err?.message || err);
});
}
return { action: "deny" };
};
}
function createAppWindowOpenHandler(shell, { backgroundColor, appIcon, getAppIcon }) {
const allowedPopupHosts = new Set([
// OAuth (PKCE loopback)
"accounts.google.com",
"login.microsoftonline.com",
"login.live.com",
]);
const isAllowedInAppPopupUrl = (rawUrl) => {
try {
const u = new URL(String(rawUrl));
if (u.protocol === "https:") {
return allowedPopupHosts.has(u.hostname);
}
if (u.protocol === "http:") {
// Allow ONLY the loopback OAuth callback page, and only while an
// OAuth flow is actively prepared — the acceptable port matches
// whatever oauthBridge just bound for this session.
const isLoopback =
u.hostname === "127.0.0.1" || u.hostname === "localhost";
if (!isLoopback || u.pathname !== "/oauth/callback") return false;
const activePort = oauthBridge.getActiveOAuthPort?.();
return activePort != null && u.port === String(activePort);
}
return false;
} catch {
return false;
}
};
return (details) => {
const targetUrl = details?.url;
const currentIcon = resolveAppIconFromOptions({ appIcon, getAppIcon });
if (!targetUrl || typeof targetUrl !== "string" || !/^https?:/i.test(targetUrl)) {
return { action: "deny" };
}
// Default: open in system browser to reduce remote-content attack surface.
if (!isAllowedInAppPopupUrl(targetUrl)) {
// Try system browser first, fall back to an in-app BrowserWindow when
// the OS has no handler for the URL (see tryOpenExternalWithFallback).
tryOpenExternalWithFallback(shell, targetUrl, {
backgroundColor,
appIcon: currentIcon,
getAppIcon,
}).catch((err) => {
console.warn("[windowManager] tryOpenExternalWithFallback threw:", err?.message || err);
});
return { action: "deny" };
}
const size = parseWindowOpenFeatures(details?.features);
return {
action: "allow",
overrideBrowserWindowOptions: {
width: size.width || OAUTH_DEFAULT_WIDTH,
height: size.height || OAUTH_DEFAULT_HEIGHT,
minWidth: 420,
minHeight: 560,
backgroundColor,
icon: currentIcon,
autoHideMenuBar: true,
menuBarVisible: false,
title: "Netcatty Authorization",
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
// Sandboxed because this window renders remote content and does not need a preload bridge.
sandbox: true,
spellcheck: false,
v8CacheOptions: V8_CACHE_OPTIONS,
},
},
};
};
}
return {
openFallbackBrowser,
tryOpenExternalWithFallback,
createExternalOnlyWindowOpenHandler,
createAppWindowOpenHandler,
};
}
}
module.exports = { createExternalWindowApi };

View File

@@ -0,0 +1,588 @@
/* eslint-disable no-undef */
const {
windowsFramelessContentChromeOptions,
} = require("./windowsWindowChrome.cjs");
const { attachDisplayRecovery } = require("./displayRecovery.cjs");
const TERMINAL_KEYBOARD_FOCUS = Symbol("netcattyTerminalKeyboardFocus");
function setTerminalKeyboardFocusForWindow(win, focused) {
if (!win || win.isDestroyed?.() || !win.webContents) return false;
const isFocused = focused === true;
try {
win[TERMINAL_KEYBOARD_FOCUS] = isFocused;
win.webContents.setIgnoreMenuShortcuts?.(isFocused);
return true;
} catch {
return false;
}
}
function hasTerminalKeyboardFocus(win) {
return win?.[TERMINAL_KEYBOARD_FOCUS] === true;
}
function createMainWindowApi(ctx) {
with (ctx) {
async function createWindow(electronModule, options) {
const { BrowserWindow, nativeTheme, app, screen, shell } = electronModule;
const {
preload,
devServerUrl,
isDev,
appIcon,
isMac,
onRegisterBridge,
electronDir,
route,
onAppContentWindowClosed,
registerAsMainWindow = true,
persistWindowState = registerAsMainWindow,
registerAsAppContentWindow = true,
startHidden = false,
} = options;
const rendererHash = typeof route === "string" && route.trim()
? `#/${route.trim().replace(/^#?\/*/, "")}`
: "";
const CHROMIUM_ZOOM_FACTORS = [
0.25, 0.33, 0.5, 0.67, 0.75, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5,
];
const isPrimaryZoomInEqualInput = (input) => {
if (input?.type !== "keyDown") return false;
if (input.alt) return false;
const hasPrimaryModifier = isMac
? Boolean(input.meta) && !input.control
: Boolean(input.control) && !input.meta;
if (!hasPrimaryModifier || input.shift) return false;
return String(input.key || "") === "=";
};
const isPrimaryZoomOutMinusInput = (input) => {
if (input?.type !== "keyDown") return false;
if (input.alt) return false;
const hasPrimaryModifier = isMac
? Boolean(input.meta) && !input.control
: Boolean(input.control) && !input.meta;
if (!hasPrimaryModifier || input.shift) return false;
return String(input.key || "") === "-";
};
const isPrimaryResetZoomInput = (input) => {
if (input?.type !== "keyDown") return false;
if (input.alt) return false;
const hasPrimaryModifier = isMac
? Boolean(input.meta) && !input.control
: Boolean(input.control) && !input.meta;
if (!hasPrimaryModifier || input.shift) return false;
return String(input.key || "") === "0";
};
const adjustWindowZoom = (mode) => {
const webContents = win?.webContents;
if (!webContents || webContents.isDestroyed?.()) return false;
const currentFactor = Number(webContents.getZoomFactor?.());
const safeCurrentFactor = Number.isFinite(currentFactor) ? currentFactor : 1;
const nextFactor = mode === "in"
? CHROMIUM_ZOOM_FACTORS.find((factor) => factor > safeCurrentFactor + 0.0001)
: mode === "out"
? [...CHROMIUM_ZOOM_FACTORS].reverse().find((factor) => factor < safeCurrentFactor - 0.0001)
: 1;
if (!nextFactor) return false;
try {
webContents.setZoomFactor?.(nextFactor);
return true;
} catch {
return false;
}
};
// Store app reference for window state persistence
electronApp = app;
const osTheme = nativeTheme?.shouldUseDarkColors ? "dark" : "light";
const effectiveTheme = currentTheme === "dark" || currentTheme === "light" ? currentTheme : osTheme;
const frontendBackground = resolveFrontendBackgroundColor(electronDir || __dirname, effectiveTheme);
const backgroundColor = frontendBackground || "#1a1a1a";
const themeConfig = THEME_COLORS[effectiveTheme] || THEME_COLORS.light;
// Load saved window state
const savedState = persistWindowState ? loadWindowState() : null;
let windowBounds = {
width: DEFAULT_WINDOW_WIDTH,
height: DEFAULT_WINDOW_HEIGHT,
};
if (savedState) {
// Use saved dimensions, but clamp to the minimum so a previously
// shrunk window from an older build cannot start below the minimum.
windowBounds.width = Math.max(savedState.width, MIN_WINDOW_WIDTH);
windowBounds.height = Math.max(savedState.height, MIN_WINDOW_HEIGHT);
// Only use saved position if the screen is available at that location
if (typeof savedState.x === "number" && typeof savedState.y === "number") {
try {
// Check if the saved position is within any available display
const displays = screen?.getAllDisplays?.() || [];
const isPositionVisible = displays.some((display) => {
const { x, y, width, height } = display.bounds;
// Check if at least part of the window would be visible on this display
return (
savedState.x < x + width &&
savedState.x + savedState.width > x &&
savedState.y < y + height &&
savedState.y + savedState.height > y
);
});
if (isPositionVisible) {
windowBounds.x = savedState.x;
windowBounds.y = savedState.y;
}
} catch {
// Ignore screen check errors, just don't set position
}
}
}
const windowsChrome = !isMac ? windowsFramelessContentChromeOptions() : {};
const win = new BrowserWindow({
...windowBounds,
minWidth: MIN_WINDOW_WIDTH,
minHeight: MIN_WINDOW_HEIGHT,
backgroundColor,
icon: appIcon,
show: false,
frame: isMac,
titleBarStyle: isMac ? "hiddenInset" : undefined,
trafficLightPosition: isMac ? { x: 12, y: 12 } : undefined,
...windowsChrome,
webPreferences: {
preload,
contextIsolation: true,
nodeIntegration: false,
sandbox: false,
spellcheck: false,
backgroundThrottling: false,
v8CacheOptions: V8_CACHE_OPTIONS,
},
});
if (registerAsMainWindow) {
if (typeof registerMainWindow === "function") {
registerMainWindow(win);
} else {
mainWindow = win;
}
} else if (registerAsAppContentWindow && typeof registerAppContentWindow === "function") {
registerAppContentWindow(win, { queryDirtyEditors: true });
}
// Multi-monitor recovery (#3244): Windows lock/sleep can temporarily
// tear down a secondary display and the OS relocates the window onto
// the primary display. When the display comes back, put the window back
// where the user left it.
let detachDisplayRecovery = null;
try {
detachDisplayRecovery = attachDisplayRecovery({ win, screen });
} catch {
detachDisplayRecovery = null;
}
// Clear reference when the main window is destroyed
win.on('closed', () => {
try {
detachDisplayRecovery?.();
} catch {
// ignore
}
try {
if (win?.webContents?.id) {
unhealthyWebContentsIds.delete(win.webContents.id);
rendererReadySeenByWebContentsId.delete(win.webContents.id);
}
} catch {
// ignore
}
if (registerAsMainWindow) {
if (typeof unregisterMainWindow === "function") {
unregisterMainWindow(win);
} else if (mainWindow === win) {
mainWindow = null;
}
} else if (registerAsAppContentWindow && typeof unregisterAppContentWindow === "function") {
unregisterAppContentWindow(win);
}
if (registerAsAppContentWindow) {
try {
if (typeof notifyAppContentWindowClosed === "function") {
notifyAppContentWindowClosed(win);
} else {
onAppContentWindowClosed?.(win);
}
} catch {
// The application-level close fallback must not disrupt teardown.
}
}
});
// Log renderer crashes for diagnostics (skip normal clean exits)
win.webContents.on("render-process-gone", (_event, details) => {
if (details?.reason === "clean-exit") return;
try {
if (win.webContents?.id) {
unhealthyWebContentsIds.add(win.webContents.id);
}
} catch {
// ignore
}
try {
const crashLogBridge = require("./crashLogBridge.cjs");
crashLogBridge.captureError("render-process-gone", new Error(
`Renderer process gone: reason=${details?.reason}, exitCode=${details?.exitCode}`
), { reason: details?.reason, exitCode: details?.exitCode });
} catch {}
console.error("[WindowManager] Renderer process gone:", details);
});
// Prevent top-level navigation away from the app origin. If a remote origin ever
// loads in a privileged window (with preload), it can become an RCE vector.
const allowedOrigins = new Set(["app://netcatty"]);
if (isDev && devServerUrl) {
try {
allowedOrigins.add(new URL(getDevRendererBaseUrl(devServerUrl)).origin);
} catch {
// ignore invalid dev server URL
}
}
const isAllowedTopLevelUrl = (targetUrl) => {
try {
const parsedUrl = new URL(String(targetUrl));
if (parsedUrl.protocol === "app:" && parsedUrl.host === "netcatty") return true;
return allowedOrigins.has(parsedUrl.origin);
} catch {
return false;
}
};
win.webContents.on("did-start-navigation", (_event, targetUrl, isInPlace, isMainFrame) => {
if (isMainFrame === false || isInPlace === true || !isAllowedTopLevelUrl(targetUrl)) return;
try {
if (typeof clearRendererReadyForWebContents === "function") {
clearRendererReadyForWebContents(win.webContents?.id);
}
} catch {
// ignore
}
});
const blockUntrustedNavigation = (event, targetUrl) => {
if (isAllowedTopLevelUrl(targetUrl)) return;
try {
event.preventDefault();
} catch {
// ignore
}
debugLog("Blocked navigation to untrusted origin", { targetUrl });
};
win.webContents.on("will-navigate", blockUntrustedNavigation);
win.webContents.on("will-redirect", blockUntrustedNavigation);
// Prevent Chromium from consuming Alt+Arrow as browser back/forward navigation.
// Terminal apps need these keys to pass through to the remote shell (e.g., byobu, tmux).
// Using setIgnoreMenuShortcuts lets the keydown still reach the page (xterm.js)
// while preventing Chromium's built-in shortcuts from triggering.
win.webContents.on("before-input-event", (event, input) => {
if (isMac && shouldCloseWindowFromInput(input)) {
event.preventDefault();
requestWindowCommandClose(win);
return;
}
const isTerminalFontShortcut =
isPrimaryZoomInEqualInput(input)
|| isPrimaryZoomOutMinusInput(input)
|| isPrimaryResetZoomInput(input);
if (hasTerminalKeyboardFocus(win) && isTerminalFontShortcut) {
win.webContents.setIgnoreMenuShortcuts(true);
return;
}
if (isPrimaryZoomInEqualInput(input) && adjustWindowZoom("in")) {
event.preventDefault();
return;
}
if (isPrimaryZoomOutMinusInput(input) && adjustWindowZoom("out")) {
event.preventDefault();
return;
}
if (isPrimaryResetZoomInput(input) && adjustWindowZoom("reset")) {
event.preventDefault();
return;
}
if (input.alt && !input.control && !input.meta) {
if (input.key === "ArrowLeft" || input.key === "ArrowRight") {
win.webContents.setIgnoreMenuShortcuts(true);
return;
}
}
win.webContents.setIgnoreMenuShortcuts(false);
});
// Restore maximized state if it was saved
if (savedState?.isMaximized && !savedState?.isFullScreen) {
win.once("ready-to-show", () => {
try {
win.maximize();
} catch {
// ignore
}
});
}
// Track window bounds for saving (use last non-maximized/non-fullscreen bounds)
let lastNormalBounds = null;
let saveStateTimer = null;
let thisWindowCloseRequested = false;
let dirtyEditorCloseConfirmed = false;
const updateNormalBounds = () => {
if (!win.isDestroyed() && !win.isMaximized() && !win.isFullScreen()) {
lastNormalBounds = win.getBounds();
}
};
const scheduleSaveState = () => {
if (!persistWindowState) return;
if (saveStateTimer) clearTimeout(saveStateTimer);
saveStateTimer = setTimeout(() => {
const state = getWindowBoundsState(win, lastNormalBounds);
if (state) queueWindowStateSave(state);
}, 500);
};
// Update normal bounds on resize/move when not maximized/fullscreen
win.on("resize", () => {
updateNormalBounds();
scheduleSaveState();
});
win.on("move", () => {
updateNormalBounds();
scheduleSaveState();
});
win.on("maximize", scheduleSaveState);
win.on("unmaximize", () => {
updateNormalBounds();
scheduleSaveState();
});
const queryDirtyEditorsBeforeClose = (event, { markCloseRequested = false } = {}) => {
event.preventDefault();
if (markCloseRequested) {
thisWindowCloseRequested = true;
}
const dirtyEditorQuery = typeof queryDirtyEditors === "function"
? queryDirtyEditors(win.webContents, 5000, { ipcMain: electronModule.ipcMain })
: false;
Promise.resolve(dirtyEditorQuery)
.then((hasDirty) => {
if (hasDirty) {
thisWindowCloseRequested = false;
return;
}
dirtyEditorCloseConfirmed = true;
try {
win.close();
} catch {
// ignore
}
})
.catch(() => {
dirtyEditorCloseConfirmed = true;
try {
win.close();
} catch {
// ignore
}
});
};
// Save state when window is about to close
win.on("close", (event) => {
if (!registerAsMainWindow && registerAsAppContentWindow && !isQuitting && !dirtyEditorCloseConfirmed) {
queryDirtyEditorsBeforeClose(event, { markCloseRequested: true });
return;
}
// Check if close-to-tray is enabled
const trackedMainWindowCount = typeof getMainWindowCount === "function" ? getMainWindowCount() : 1;
if (registerAsMainWindow && trackedMainWindowCount <= 1 && !isQuitting && getGlobalShortcutBridge().handleWindowClose(event, win)) {
// Window was hidden to tray - save state before returning
if (saveStateTimer) clearTimeout(saveStateTimer);
const state = persistWindowState ? getWindowBoundsState(win, lastNormalBounds) : null;
if (state) saveWindowStateSync(state);
hideSettingsWindow();
return;
}
if (registerAsMainWindow && registerAsAppContentWindow && !isQuitting && !dirtyEditorCloseConfirmed) {
queryDirtyEditorsBeforeClose(event);
return;
}
if (thisWindowCloseRequested) {
return;
}
thisWindowCloseRequested = true;
if (saveStateTimer) clearTimeout(saveStateTimer);
const state = persistWindowState ? getWindowBoundsState(win, lastNormalBounds) : null;
if (pendingWindowStateWrite) {
event.preventDefault();
if (state) queuedWindowState = state;
pendingWindowStateWrite
.catch(() => {
// ignore async write errors before closing
})
.finally(() => {
const finalState = persistWindowState ? getWindowBoundsState(win, lastNormalBounds) : null;
if (finalState) saveWindowStateSync(finalState);
if (registerAsMainWindow) closeSettingsWindow();
try {
win.close();
} catch {
// ignore
}
});
return;
}
if (state) saveWindowStateSync(state);
if (registerAsMainWindow) closeSettingsWindow();
});
const safeSend = (channel, ...args) => {
try {
if (!win.isDestroyed() && win.webContents && !win.webContents.isDestroyed()) {
win.webContents.send(channel, ...args);
}
} catch {
// Render frame disposed during HMR / reload safe to ignore
}
};
win.on("enter-full-screen", () => {
safeSend("netcatty:window:fullscreen-changed", true);
scheduleSaveState();
});
win.on("leave-full-screen", () => {
safeSend("netcatty:window:fullscreen-changed", false);
updateNormalBounds();
scheduleSaveState();
});
win.on("show", () => {
safeSend("netcatty:window:shown");
if (startHidden) {
// The hidden-launch tray pin exists so a --hidden cold start is
// never a windowless, trayless zombie; once this window is
// actually visible that concern no longer applies, so hand tray
// lifetime back to the user's normal close-to-tray preference.
try {
getGlobalShortcutBridge().releaseHiddenLaunchTrayPin?.();
} catch (err) {
console.warn("[MainWindow] Failed to release hidden-launch tray pin:", err?.message || err);
}
}
});
// Ensure native background matches frontend background, even before first paint.
try {
win.setBackgroundColor(backgroundColor);
} catch {
// ignore
}
applyWindowOpacityToWindow(win);
// Defer show until renderer is ready; use fallback timeout to avoid keeping window hidden forever.
// Production gets a shorter timeout since the splash screen provides visual feedback.
setupDeferredShow(win, { timeoutMs: isDev ? 3000 : 1500, startHidden });
win.webContents.on("did-create-window", (childWindow) => {
try {
childWindow.setMenuBarVisibility(false);
childWindow.autoHideMenuBar = true;
childWindow.removeMenu();
} catch {
// ignore
}
try {
const iconPath = resolveLiveAppIcon(appIcon);
if (iconPath && childWindow.setIcon) childWindow.setIcon(iconPath);
} catch {
// ignore
}
// Never allow chained popups from remote content windows.
try {
childWindow.webContents?.setWindowOpenHandler?.(createExternalOnlyWindowOpenHandler(shell));
} catch {
// ignore
}
attachOAuthLoadingOverlay(childWindow);
});
win.webContents.setWindowOpenHandler(
createAppWindowOpenHandler(shell, {
backgroundColor,
appIcon,
getAppIcon: () => resolveLiveAppIcon(appIcon),
})
);
// Register window control handlers
registerWindowHandlers(electronModule.ipcMain, nativeTheme);
// Register IPC handlers BEFORE loading any URL so the renderer never
// calls a handler that hasn't been registered yet.
onRegisterBridge?.(win);
if (startHidden) {
// Belt-and-suspenders: guarantee a tray icon exists as soon as
// bridges (and electronModule) are ready, and keep it pinned
// (independent of the user's separate close-to-tray preference)
// until this window is actually shown — see releaseHiddenLaunchTrayPin
// in the "show" handler below.
try {
getGlobalShortcutBridge().pinTrayForHiddenLaunch?.();
} catch (err) {
console.warn("[MainWindow] Failed to create tray for hidden launch:", err?.message || err);
}
}
if (isDev) {
try {
await win.loadURL(`${getDevRendererBaseUrl(devServerUrl)}${rendererHash}`);
win.webContents.openDevTools({ mode: "detach" });
return win;
} catch (e) {
console.warn("Dev server not reachable, falling back to bundled dist.", e);
}
}
// Production mode - load via custom protocol.
await win.loadURL(`app://netcatty/index.html${rendererHash}`);
return win;
}
return { createWindow };
}
}
module.exports = {
createMainWindowApi,
setTerminalKeyboardFocusForWindow,
};

View File

@@ -0,0 +1,347 @@
/* eslint-disable no-undef */
const {
windowsFramelessContentChromeOptions,
} = require("./windowsWindowChrome.cjs");
function createSettingsWindowApi(ctx) {
// The extracted window helpers intentionally share the parent window-manager state.
with (ctx) {
function restoreWindowInputFocus(win, options = {}) {
if (!win || win.isDestroyed()) return false;
const shouldShow = options.show === true;
const platform = options.platform || process.platform;
if (shouldShow) {
try {
win.show();
} catch {
// ignore
}
}
if (platform === "win32") {
try {
win.setAlwaysOnTop(true);
} catch {
// ignore
}
try {
win.focus();
} catch {
// ignore
} finally {
try {
win.setAlwaysOnTop(false);
} catch {
// ignore
}
}
} else {
try {
win.focus();
} catch {
// ignore
}
}
try {
if (win.webContents && !win.webContents.isDestroyed()) {
win.webContents.focus();
}
} catch {
// ignore
}
return true;
}
function showAndFocusWindow(win) {
restoreWindowInputFocus(win, { show: true });
}
function isLiveWindow(win) {
return Boolean(win && typeof win.isDestroyed === "function" && !win.isDestroyed());
}
function resolveSettingsWindowBounds(
electronModule,
{ sourceWindow, settingsWidth, settingsHeight } = {},
) {
const { screen } = electronModule || {};
if (!screen || !isLiveWindow(sourceWindow)) return {};
try {
const sourceBounds = sourceWindow.getBounds();
const display = screen.getDisplayMatching(sourceBounds);
const { x: dx, y: dy, width: dw, height: dh } = display.workArea;
return {
x: Math.round(dx + (dw - settingsWidth) / 2),
y: Math.round(dy + (dh - settingsHeight) / 2),
};
} catch {
return {};
}
}
function centerSettingsWindowOnSourceDisplay(win, electronModule, sourceWindow) {
if (!isLiveWindow(win)) return;
let bounds = { width: 980, height: 720 };
try {
bounds = win.getBounds();
} catch {
// keep defaults
}
const nextPosition = resolveSettingsWindowBounds(electronModule, {
sourceWindow,
settingsWidth: bounds.width,
settingsHeight: bounds.height,
});
if (nextPosition.x === undefined || nextPosition.y === undefined) return;
try {
win.setPosition(nextPosition.x, nextPosition.y);
} catch {
// ignore
}
}
async function openSettingsWindow(electronModule, options, { showOnLoad = true } = {}) {
const { BrowserWindow, shell } = electronModule;
const { preload, devServerUrl, isDev, appIcon, isMac, electronDir, sourceWindow } = options;
// If settings window already exists, show and focus it
if (settingsWindow && !settingsWindow.isDestroyed()) {
centerSettingsWindowOnSourceDisplay(settingsWindow, electronModule, sourceWindow || mainWindow);
showAndFocusWindow(settingsWindow);
return settingsWindow;
}
const osTheme = electronModule?.nativeTheme?.shouldUseDarkColors ? "dark" : "light";
const effectiveTheme = currentTheme === "dark" || currentTheme === "light" ? currentTheme : osTheme;
const frontendBackground = resolveFrontendBackgroundColor(electronDir || __dirname, effectiveTheme);
const backgroundColor = frontendBackground || "#1a1a1a";
const themeConfig = THEME_COLORS[effectiveTheme] || THEME_COLORS.light;
// Center the settings window on the same display as the main window
const settingsWidth = 980;
const settingsHeight = 720;
const { x: settingsX, y: settingsY } = resolveSettingsWindowBounds(electronModule, {
sourceWindow: sourceWindow || mainWindow,
settingsWidth,
settingsHeight,
});
const windowsChrome = !isMac ? windowsFramelessContentChromeOptions() : {};
const win = new BrowserWindow({
title: "netcatty Settings",
width: settingsWidth,
height: settingsHeight,
...(settingsX !== undefined && settingsY !== undefined ? { x: settingsX, y: settingsY } : {}),
minWidth: 820,
minHeight: 600,
backgroundColor,
icon: appIcon,
fullscreenable: !isMac,
// NOTE: Do NOT set parent - on macOS this causes rendering issues when dragging
// the window to a different screen (the window becomes invisible while still
// appearing in "Show All Windows" in the Dock). On Windows it can cause the
// main window to close when the settings window is closed.
modal: false,
show: false,
frame: isMac,
titleBarStyle: isMac ? "hiddenInset" : undefined,
trafficLightPosition: isMac ? { x: 12, y: 12 } : undefined,
...windowsChrome,
webPreferences: {
preload,
contextIsolation: true,
nodeIntegration: false,
sandbox: false,
spellcheck: false,
v8CacheOptions: V8_CACHE_OPTIONS,
},
});
settingsWindow = win;
// Open external links in system browser by default, and allow only known OAuth hosts in-app.
try {
win.webContents?.setWindowOpenHandler?.(
createAppWindowOpenHandler(shell, {
backgroundColor,
appIcon,
getAppIcon: () => resolveLiveAppIcon(appIcon),
})
);
} catch {
// ignore
}
// Never allow chained popups from remote content windows spawned from settings.
win.webContents?.on?.("did-create-window", (childWindow) => {
try {
childWindow.webContents?.setWindowOpenHandler?.(createExternalOnlyWindowOpenHandler(shell));
} catch {
// ignore
}
});
// Same navigation hardening as the main window (settings has preload access too).
const allowedOrigins = new Set(["app://netcatty"]);
if (isDev && devServerUrl) {
try {
allowedOrigins.add(new URL(getDevRendererBaseUrl(devServerUrl)).origin);
} catch {
// ignore invalid dev server URL
}
}
const isAllowedTopLevelUrl = (targetUrl) => {
try {
return allowedOrigins.has(new URL(String(targetUrl)).origin);
} catch {
return false;
}
};
const blockUntrustedNavigation = (event, targetUrl) => {
if (isAllowedTopLevelUrl(targetUrl)) return;
try {
event.preventDefault();
} catch {
// ignore
}
debugLog("Blocked navigation to untrusted origin (settings)", { targetUrl });
};
win.webContents.on("will-navigate", blockUntrustedNavigation);
win.webContents.on("will-redirect", blockUntrustedNavigation);
if (isMac) {
try {
win.setWindowButtonVisibility(true);
} catch {
// ignore
}
}
const safeSend = (channel, ...args) => {
try {
if (!win.isDestroyed() && win.webContents && !win.webContents.isDestroyed()) {
win.webContents.send(channel, ...args);
}
} catch {
// Render frame disposed during HMR / reload safe to ignore
}
};
win.on("enter-full-screen", () => {
safeSend("netcatty:window:fullscreen-changed", true);
});
win.on("leave-full-screen", () => {
safeSend("netcatty:window:fullscreen-changed", false);
});
// Ensure native background matches frontend background, even before first paint.
try {
win.setBackgroundColor(backgroundColor);
} catch {
// ignore
}
applyWindowOpacityToWindow(win);
// Hide instead of close so the window can be reused instantly.
// When the app is quitting, allow normal close/destroy.
win.on('close', (event) => {
if (!isQuitting) {
event.preventDefault();
try {
win.hide();
} catch {
// ignore
}
}
});
// Clean up reference when actually destroyed
win.on('closed', () => {
settingsWindow = null;
});
// Prevent HTML <title> from overriding the window title
win.on('page-title-updated', (e) => { e.preventDefault(); });
// Load the settings page
const settingsPath = '/#/settings';
if (isDev) {
try {
const baseUrl = getDevRendererBaseUrl(devServerUrl);
await win.loadURL(`${baseUrl}${settingsPath}`);
if (showOnLoad) { showAndFocusWindow(win); }
return win;
} catch (e) {
console.warn("Dev server not reachable for settings window", e);
}
}
// Production mode - load via custom protocol.
await win.loadURL("app://netcatty/index.html#/settings");
if (showOnLoad) { showAndFocusWindow(win); }
return win;
}
/**
* Destroy the settings window (used when the app is quitting).
*/
function closeSettingsWindow() {
if (settingsWindow && !settingsWindow.isDestroyed()) {
try {
settingsWindow.destroy();
} catch {
// ignore
}
settingsWindow = null;
}
}
/**
* Hide the settings window without destroying it (used when main window hides to tray).
*/
function hideSettingsWindow() {
if (settingsWindow && !settingsWindow.isDestroyed()) {
try {
settingsWindow.hide();
} catch {
// ignore
}
}
}
/**
* Pre-warm the settings window in the background so that opening it later is instant.
* The window is created hidden and fully loaded; `openSettingsWindow` will simply show it.
*/
async function prewarmSettingsWindow(electronModule, options) {
if (settingsWindow && !settingsWindow.isDestroyed()) return;
try {
await openSettingsWindow(electronModule, options, { showOnLoad: false });
} catch (err) {
debugLog("Failed to pre-warm settings window", { error: String(err) });
}
}
return {
restoreWindowInputFocus,
showAndFocusWindow,
isLiveWindow,
resolveSettingsWindowBounds,
centerSettingsWindowOnSourceDisplay,
openSettingsWindow,
closeSettingsWindow,
hideSettingsWindow,
prewarmSettingsWindow,
};
}
}
module.exports = { createSettingsWindowApi };

View File

@@ -0,0 +1,321 @@
/* eslint-disable no-undef */
const { randomUUID } = require("node:crypto");
const crashLogBridge = require("../crashLogBridge.cjs");
const {
isAttachPopupClosePrepared,
registerAttachPopupAuthorization,
releaseAttachPopupAuthorization,
restoreAttachedSessionOutput,
} = require("../terminalAttachRestore.cjs");
const {
windowsFramelessContentChromeOptions,
} = require("./windowsWindowChrome.cjs");
const ATTACH_CLOSE_PREPARE_TIMEOUT_MS = 2000;
function createTerminalPopupWindowApi(ctx) {
with (ctx) {
const terminalPopupWindows = new Map();
/** attachSessionId -> popupId for AI silent-session observe windows */
const attachSessionPopups = new Map();
function isLiveWindow(win) {
return Boolean(win && typeof win.isDestroyed === "function" && !win.isDestroyed());
}
async function openTerminalPopupWindow(electronModule, options, payload) {
const { BrowserWindow, shell } = electronModule;
const { preload, devServerUrl, isDev, appIcon, isMac, electronDir, sourceWindow } = options;
const attachSessionId = typeof payload?.attachSessionId === "string" && payload.attachSessionId
? payload.attachSessionId
: null;
const attachAuthorization = attachSessionId ? randomUUID() : null;
if (attachSessionId) {
const existingPopupId = attachSessionPopups.get(attachSessionId);
const existing = existingPopupId ? terminalPopupWindows.get(existingPopupId) : null;
if (existing && isLiveWindow(existing.win)) {
try {
showAndFocusWindow(existing.win);
} catch {
// ignore focus races
}
return { success: true, popupId: existingPopupId, reused: true };
}
if (existingPopupId) attachSessionPopups.delete(attachSessionId);
}
const osTheme = electronModule?.nativeTheme?.shouldUseDarkColors ? "dark" : "light";
const effectiveTheme = currentTheme === "dark" || currentTheme === "light" ? currentTheme : osTheme;
const frontendBackground = resolveFrontendBackgroundColor(electronDir || __dirname, effectiveTheme);
const backgroundColor = frontendBackground || "#1a1a1a";
const popupWidth = 920;
const popupHeight = 580;
const { x: popupX, y: popupY } = resolveSettingsWindowBounds(electronModule, {
sourceWindow: sourceWindow || mainWindow,
settingsWidth: popupWidth,
settingsHeight: popupHeight,
});
const title = typeof payload?.title === "string" && payload.title.trim()
? payload.title.trim()
: "Terminal";
const popupId = String(payload?.popupId || randomUUID());
if (terminalPopupWindows.has(popupId)) {
throw new Error(`Terminal popup ID is already active: ${popupId}`);
}
crashLogBridge.captureDiagnostic("terminal-popup", "creating popup window", {
popupId,
title,
isDev,
popupX,
popupY,
});
const windowsChrome = windowsFramelessContentChromeOptions();
const win = new BrowserWindow({
title,
width: popupWidth,
height: popupHeight,
...(popupX !== undefined && popupY !== undefined ? { x: popupX, y: popupY } : {}),
minWidth: 480,
minHeight: 320,
backgroundColor,
icon: appIcon,
show: false,
frame: false,
...(isMac ? { trafficLightPosition: { x: 12, y: 12 } } : {}),
...windowsChrome,
webPreferences: {
preload,
contextIsolation: true,
nodeIntegration: false,
sandbox: false,
spellcheck: false,
backgroundThrottling: false,
v8CacheOptions: V8_CACHE_OPTIONS,
},
});
let lifecycleReleased = false;
let closePreparationRequested = false;
let closePreparationTimer = null;
const releaseLifecycle = () => {
if (lifecycleReleased) return;
lifecycleReleased = true;
if (closePreparationTimer) clearTimeout(closePreparationTimer);
terminalPopupWindows.delete(popupId);
if (attachSessionId && attachSessionPopups.get(attachSessionId) === popupId) {
attachSessionPopups.delete(attachSessionId);
// Window may be destroyed before React cleanup runs; always restore
// the display route from the main-process lifecycle.
try {
restoreAttachedSessionOutput(attachSessionId);
} catch (err) {
crashLogBridge.captureError?.("terminal-popup", err, {
popupId,
attachSessionId,
step: "restore attach output on close",
});
}
}
releaseAttachPopupAuthorization(attachAuthorization);
unregisterAppContentWindow(win);
notifyAppContentWindowClosed(win);
};
terminalPopupWindows.set(popupId, { releaseLifecycle, win });
if (attachSessionId) {
attachSessionPopups.set(attachSessionId, popupId);
registerAttachPopupAuthorization(
attachAuthorization,
attachSessionId,
win.webContents.id,
);
}
registerAppContentWindow(win);
crashLogBridge.captureDiagnostic("terminal-popup", "popup BrowserWindow created", {
popupId,
title,
webContentsId: win.webContents?.id,
});
try {
win.webContents?.setWindowOpenHandler?.(
createExternalOnlyWindowOpenHandler(shell),
);
} catch {
// ignore
}
win.on("close", (event) => {
if (!attachSessionId || isAttachPopupClosePrepared(attachAuthorization)) return;
event?.preventDefault?.();
if (closePreparationRequested) return;
closePreparationRequested = true;
try {
win.webContents.send("netcatty:terminal-popup:prepare-close", {
sessionId: attachSessionId,
authorization: attachAuthorization,
});
} catch {
// Timeout below force-closes and restores the route.
}
closePreparationTimer = setTimeout(() => {
closePreparationTimer = null;
try {
if (isLiveWindow(win)) win.destroy();
} catch {
releaseLifecycle();
}
}, ATTACH_CLOSE_PREPARE_TIMEOUT_MS);
});
win.on("closed", releaseLifecycle);
try {
win.webContents?.on?.("did-fail-load", (_event, errorCode, errorDescription, validatedURL) => {
console.warn("[TerminalPopup] Failed to load renderer", {
popupId,
errorCode,
errorDescription,
validatedURL,
});
});
win.webContents?.on?.("render-process-gone", (_event, details) => {
console.warn("[TerminalPopup] Renderer process gone", { popupId, details });
if (attachSessionId) {
restoreAttachedSessionOutput(attachSessionId);
}
try {
if (isLiveWindow(win)) win.destroy();
} catch {
releaseLifecycle();
}
});
win.webContents?.on?.("console-message", (_event, level, message, line, sourceId) => {
crashLogBridge.captureDiagnostic("terminal-popup-console", message, {
popupId,
level,
line,
sourceId,
});
});
} catch {
// ignore diagnostics wiring failures
}
win.on("page-title-updated", (e) => { e.preventDefault(); });
try {
win.setBackgroundColor(backgroundColor);
} catch {
// ignore
}
applyWindowOpacityToWindow(win);
if (isMac) {
try {
win.setWindowButtonVisibility(true);
} catch {
// ignore
}
try {
win.setWindowButtonPosition({ x: 12, y: 12 });
} catch {
// ignore
}
}
const popupPath = "#/terminal-popup";
try {
if (isDev) {
try {
const baseUrl = getDevRendererBaseUrl(devServerUrl);
crashLogBridge.captureDiagnostic("terminal-popup", "loading dev popup URL", {
popupId,
url: `${baseUrl}${popupPath}`,
});
await win.loadURL(`${baseUrl}${popupPath}`);
} catch (e) {
console.warn("[TerminalPopup] Dev server not reachable", e);
crashLogBridge.captureError("terminal-popup", e, {
popupId,
step: "load dev popup URL",
});
await win.loadURL(`app://netcatty/index.html${popupPath}`);
}
} else {
crashLogBridge.captureDiagnostic("terminal-popup", "loading packaged popup URL", {
popupId,
url: `app://netcatty/index.html${popupPath}`,
});
await win.loadURL(`app://netcatty/index.html${popupPath}`);
}
win.webContents.send("netcatty:window:terminalPopupConfig", {
...payload,
popupId,
...(attachAuthorization ? { attachAuthorization } : {}),
});
crashLogBridge.captureDiagnostic("terminal-popup", "popup config delivered", {
popupId,
title,
});
showAndFocusWindow(win);
crashLogBridge.captureDiagnostic("terminal-popup", "popup window shown", {
popupId,
title,
visible: typeof win.isVisible === "function" ? win.isVisible() : undefined,
});
return { success: true, popupId };
} catch (error) {
try {
if (isLiveWindow(win)) {
if (typeof win.destroy === "function") win.destroy();
else win.close();
}
} catch {
// Lifecycle cleanup below remains fail-safe if Electron cleanup throws.
}
releaseLifecycle();
throw error;
}
}
function closeTerminalPopupWindow(popupId) {
const record = terminalPopupWindows.get(popupId);
if (!record) return;
if (!isLiveWindow(record.win)) {
record.releaseLifecycle();
return;
}
try {
record.win.close();
} catch {
try {
record.win.destroy?.();
} catch {
// The lifecycle registry must still be released below.
} finally {
record.releaseLifecycle();
}
}
}
return {
openTerminalPopupWindow,
closeTerminalPopupWindow,
getTerminalPopupWindows() {
return Array.from(terminalPopupWindows.values());
},
};
}
}
module.exports = { createTerminalPopupWindowApi };

View File

@@ -0,0 +1,284 @@
"use strict";
const assert = require("node:assert/strict");
const test = require("node:test");
const { createAppContentWindowClosedHandler } = require("../../appWindowLifecycle.cjs");
const { createTerminalPopupWindowApi } = require("./terminalPopupWindow.cjs");
const { markAttachPopupClosePrepared } = require("../terminalAttachRestore.cjs");
test("terminal popups participate in the last app-content-window lifecycle", async () => {
const appContentWindows = new Set();
let quitCalls = 0;
const lifecycleHandler = createAppContentWindowClosedHandler({
app: { quit() { quitCalls += 1; } },
platform: "win32",
windowManager: {
getAppContentWindows: () => [...appContentWindows],
getIsQuitting: () => false,
},
});
let popupWindow;
class BrowserWindowStub {
constructor() {
popupWindow = this;
this.handlers = new Map();
this.webContents = {
id: 42,
on() {},
send() {},
setWindowOpenHandler() {},
};
}
on(channel, handler) { this.handlers.set(channel, handler); }
isDestroyed() { return false; }
isVisible() { return true; }
loadURL() { return Promise.resolve(); }
setBackgroundColor() {}
}
const api = createTerminalPopupWindowApi({
mainWindow: null,
currentTheme: "light",
V8_CACHE_OPTIONS: "bypassHeatCheck",
resolveFrontendBackgroundColor() { return "#fff"; },
resolveSettingsWindowBounds() { return { x: 10, y: 20 }; },
createExternalOnlyWindowOpenHandler() { return {}; },
applyWindowOpacityToWindow() {},
getDevRendererBaseUrl(url) { return url; },
showAndFocusWindow() {},
registerAppContentWindow(win) { appContentWindows.add(win); },
unregisterAppContentWindow(win) { appContentWindows.delete(win); },
notifyAppContentWindowClosed() { lifecycleHandler(); },
});
const result = await api.openTerminalPopupWindow(
{
BrowserWindow: BrowserWindowStub,
nativeTheme: { shouldUseDarkColors: false },
shell: {},
},
{
preload: "/tmp/preload.cjs",
isDev: false,
appIcon: null,
isMac: false,
electronDir: __dirname,
},
{ popupId: "popup-1", title: "Terminal" },
);
assert.deepEqual(result, { success: true, popupId: "popup-1" });
assert.deepEqual([...appContentWindows], [popupWindow]);
await assert.rejects(api.openTerminalPopupWindow(
{ BrowserWindow: BrowserWindowStub, nativeTheme: {}, shell: {} },
{
preload: "/tmp/preload.cjs",
isDev: false,
appIcon: null,
isMac: false,
electronDir: __dirname,
},
{ popupId: "popup-1" },
), /already active/);
assert.equal(appContentWindows.size, 1);
assert.equal(lifecycleHandler(), false);
assert.equal(quitCalls, 0);
popupWindow.handlers.get("closed")?.();
assert.deepEqual([...appContentWindows], []);
assert.equal(quitCalls, 1);
});
test("a terminal popup that fails to load releases app-content lifecycle state", async () => {
const appContentWindows = new Set();
let closeNotifications = 0;
class FailingBrowserWindowStub {
constructor() {
this.destroyed = false;
this.handlers = new Map();
this.webContents = {
id: 43,
on() {},
send() {},
setWindowOpenHandler() {},
};
}
on(channel, handler) { this.handlers.set(channel, handler); }
isDestroyed() { return this.destroyed; }
loadURL() { return Promise.reject(new Error("renderer unavailable")); }
setBackgroundColor() {}
destroy() {
this.destroyed = true;
this.handlers.get("closed")?.();
}
}
const api = createTerminalPopupWindowApi({
mainWindow: null,
currentTheme: "light",
V8_CACHE_OPTIONS: "bypassHeatCheck",
resolveFrontendBackgroundColor() { return "#fff"; },
resolveSettingsWindowBounds() { return {}; },
createExternalOnlyWindowOpenHandler() { return {}; },
applyWindowOpacityToWindow() {},
getDevRendererBaseUrl(url) { return url; },
showAndFocusWindow() {},
registerAppContentWindow(win) { appContentWindows.add(win); },
unregisterAppContentWindow(win) { appContentWindows.delete(win); },
notifyAppContentWindowClosed() { closeNotifications += 1; },
});
await assert.rejects(api.openTerminalPopupWindow(
{
BrowserWindow: FailingBrowserWindowStub,
nativeTheme: { shouldUseDarkColors: false },
shell: {},
},
{
preload: "/tmp/preload.cjs",
isDev: false,
appIcon: null,
isMac: false,
electronDir: __dirname,
},
{ popupId: "failed-popup" },
), /renderer unavailable/);
assert.deepEqual([...appContentWindows], []);
assert.equal(closeNotifications, 1);
});
test("popup close failures cannot leave a ghost app-content window", async () => {
const appContentWindows = new Set();
let closeNotifications = 0;
let popupWindow;
class CloseFailingBrowserWindowStub {
constructor() {
popupWindow = this;
this.handlers = new Map();
this.webContents = {
id: 44,
on() {},
send() {},
setWindowOpenHandler() {},
};
}
on(channel, handler) { this.handlers.set(channel, handler); }
isDestroyed() { return false; }
loadURL() { return Promise.resolve(); }
setBackgroundColor() {}
close() { throw new Error("close failed"); }
destroy() { throw new Error("destroy failed"); }
}
const api = createTerminalPopupWindowApi({
mainWindow: null,
currentTheme: "light",
V8_CACHE_OPTIONS: "bypassHeatCheck",
resolveFrontendBackgroundColor() { return "#fff"; },
resolveSettingsWindowBounds() { return {}; },
createExternalOnlyWindowOpenHandler() { return {}; },
applyWindowOpacityToWindow() {},
getDevRendererBaseUrl(url) { return url; },
showAndFocusWindow() {},
registerAppContentWindow(win) { appContentWindows.add(win); },
unregisterAppContentWindow(win) { appContentWindows.delete(win); },
notifyAppContentWindowClosed() { closeNotifications += 1; },
});
await api.openTerminalPopupWindow(
{ BrowserWindow: CloseFailingBrowserWindowStub, nativeTheme: {}, shell: {} },
{
preload: "/tmp/preload.cjs",
isDev: false,
appIcon: null,
isMac: false,
electronDir: __dirname,
},
{ popupId: "close-failure" },
);
assert.deepEqual([...appContentWindows], [popupWindow]);
assert.doesNotThrow(() => api.closeTerminalPopupWindow("close-failure"));
assert.deepEqual([...appContentWindows], []);
assert.equal(closeNotifications, 1);
});
test("attach popup waits for renderer handoff before closing", async () => {
const sent = [];
let popupWindow;
class AttachBrowserWindowStub {
constructor() {
popupWindow = this;
this.destroyed = false;
this.handlers = new Map();
this.webContents = {
id: 45,
on() {},
send(channel, payload) { sent.push({ channel, payload }); },
setWindowOpenHandler() {},
};
}
on(channel, handler) { this.handlers.set(channel, handler); }
isDestroyed() { return this.destroyed; }
isVisible() { return true; }
loadURL() { return Promise.resolve(); }
setBackgroundColor() {}
close() {
let prevented = false;
this.handlers.get("close")?.({ preventDefault() { prevented = true; } });
if (!prevented) {
this.destroyed = true;
this.handlers.get("closed")?.();
}
}
destroy() {
this.destroyed = true;
this.handlers.get("closed")?.();
}
}
const api = createTerminalPopupWindowApi({
mainWindow: null,
currentTheme: "light",
V8_CACHE_OPTIONS: "bypassHeatCheck",
resolveFrontendBackgroundColor() { return "#fff"; },
resolveSettingsWindowBounds() { return {}; },
createExternalOnlyWindowOpenHandler() { return {}; },
applyWindowOpacityToWindow() {},
getDevRendererBaseUrl(url) { return url; },
showAndFocusWindow() {},
registerAppContentWindow() {},
unregisterAppContentWindow() {},
notifyAppContentWindowClosed() {},
});
await api.openTerminalPopupWindow(
{ BrowserWindow: AttachBrowserWindowStub, nativeTheme: {}, shell: {} },
{ preload: "/tmp/preload.cjs", isDev: false, appIcon: null, isMac: false, electronDir: __dirname },
{
popupId: "attach-popup",
title: "Attach",
attachSessionId: "session-1",
sourceSession: { id: "session-1" },
},
);
const config = sent.find((entry) => entry.channel === "netcatty:window:terminalPopupConfig")?.payload;
assert.equal(typeof config.attachAuthorization, "string");
api.closeTerminalPopupWindow("attach-popup");
assert.equal(popupWindow.destroyed, false);
assert.deepEqual(sent.at(-1), {
channel: "netcatty:terminal-popup:prepare-close",
payload: { sessionId: "session-1", authorization: config.attachAuthorization },
});
assert.equal(markAttachPopupClosePrepared(config.attachAuthorization, "session-1", 45), true);
api.closeTerminalPopupWindow("attach-popup");
assert.equal(popupWindow.destroyed, true);
});

View File

@@ -0,0 +1,56 @@
/**
* Windows frameless window chrome helpers.
*
* Background (#2505):
* - Frameless Win11 windows need an explicit `roundedCorners: true` so DWM
* applies the system round clip (Electron 34+).
* - Transparent tray popups that keep the default opaque white backdrop + CSS
* `border-radius` produce square tips under a rounded panel.
*
* Approach:
* - App content windows (resizable): solid host + native `roundedCorners`.
* Do NOT set `transparent: true` here - Electron documents transparent
* windows as not resizable, and `resizable: true` can break them
* (https://www.electronjs.org/docs/latest/tutorial/custom-window-styles).
* - Tray / CSS-shaped popovers (`resizable: false`): transparent host + clear
* backdrop + `roundedCorners: false` so only the CSS radius defines the
* silhouette (Electron #46468: Win11 otherwise forces OS rounding on
* transparent windows).
*/
const CLEAR_BACKGROUND = "#00000000";
function isWindowsPlatform(platform = process.platform) {
return platform === "win32";
}
/**
* Options for full-bleed app windows (main / settings / terminal popup).
* Safe to spread on non-Windows - returns an empty object.
*/
function windowsFramelessContentChromeOptions(platform = process.platform) {
if (!isWindowsPlatform(platform)) return {};
return {
roundedCorners: true,
};
}
/**
* Options for CSS-rounded overlay windows (tray panel).
* Always apply when creating those windows; callers already opt into transparency.
*/
function windowsCssRoundedOverlayChromeOptions(platform = process.platform) {
return {
transparent: true,
backgroundColor: CLEAR_BACKGROUND,
// Overlay shape comes from CSS; keep OS rounding off on Win11.
...(isWindowsPlatform(platform) ? { roundedCorners: false } : {}),
};
}
module.exports = {
CLEAR_BACKGROUND,
isWindowsPlatform,
windowsFramelessContentChromeOptions,
windowsCssRoundedOverlayChromeOptions,
};

View File

@@ -0,0 +1,104 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const path = require("node:path");
const { readFileSync } = require("node:fs");
const {
CLEAR_BACKGROUND,
isWindowsPlatform,
windowsFramelessContentChromeOptions,
windowsCssRoundedOverlayChromeOptions,
} = require("./windowsWindowChrome.cjs");
test("CLEAR_BACKGROUND is fully transparent ARGB", () => {
assert.equal(CLEAR_BACKGROUND, "#00000000");
});
test("isWindowsPlatform detects win32 only", () => {
assert.equal(isWindowsPlatform("win32"), true);
assert.equal(isWindowsPlatform("darwin"), false);
assert.equal(isWindowsPlatform("linux"), false);
});
test("content chrome is a no-op outside Windows", () => {
assert.deepEqual(windowsFramelessContentChromeOptions("darwin"), {});
assert.deepEqual(windowsFramelessContentChromeOptions("linux"), {});
});
test("content chrome enables native rounding without transparency on Windows", () => {
assert.deepEqual(windowsFramelessContentChromeOptions("win32"), {
roundedCorners: true,
});
assert.equal(
Object.hasOwn(windowsFramelessContentChromeOptions("win32"), "transparent"),
false,
"resizable app windows must not use transparent hosts",
);
});
test("CSS overlay chrome clears the opaque backdrop on every platform", () => {
assert.deepEqual(windowsCssRoundedOverlayChromeOptions("darwin"), {
transparent: true,
backgroundColor: CLEAR_BACKGROUND,
});
assert.deepEqual(windowsCssRoundedOverlayChromeOptions("linux"), {
transparent: true,
backgroundColor: CLEAR_BACKGROUND,
});
});
test("CSS overlay chrome disables OS rounding on Windows", () => {
assert.deepEqual(windowsCssRoundedOverlayChromeOptions("win32"), {
transparent: true,
backgroundColor: CLEAR_BACKGROUND,
roundedCorners: false,
});
});
test("main/settings/tray call sites wire Windows chrome helpers", () => {
const here = __dirname;
const main = readFileSync(path.join(here, "mainWindow.cjs"), "utf8");
const settings = readFileSync(path.join(here, "settingsWindow.cjs"), "utf8");
const popup = readFileSync(path.join(here, "terminalPopupWindow.cjs"), "utf8");
const tray = readFileSync(path.join(here, "../globalShortcutBridge.cjs"), "utf8");
const css = readFileSync(path.join(here, "../../../index.css"), "utf8");
const html = readFileSync(path.join(here, "../../../index.html"), "utf8");
const helper = readFileSync(path.join(here, "windowsWindowChrome.cjs"), "utf8");
for (const [label, source] of [
["mainWindow", main],
["settingsWindow", settings],
["terminalPopupWindow", popup],
]) {
assert.match(source, /require\("\.\/windowsWindowChrome\.cjs"\)/, `${label} must require chrome helpers`);
assert.match(source, /windowsFramelessContentChromeOptions/, `${label} must use content chrome helper`);
assert.doesNotMatch(source, /resolveFramelessHostBackgroundColor/, `${label} should not clear host backdrop`);
const requireIndex = source.indexOf('require("./windowsWindowChrome.cjs")');
const withIndex = source.indexOf("with (ctx)");
assert.ok(
requireIndex !== -1 && withIndex !== -1 && requireIndex < withIndex,
`${label}: require chrome helpers before with(ctx) so injected require cannot remount the path`,
);
}
assert.match(tray, /windowsCssRoundedOverlayChromeOptions/);
assert.match(tray, /#2505/);
assert.match(css, /html\.tray-window/);
assert.match(css, /html\.tray-window \.splash-screen/);
assert.match(html, /tray-window/);
assert.match(html, /removeTraySplash|splash\.remove\(\)/);
assert.match(tray, /trayPanelShowWhenReady|trayPanelReady/);
assert.match(tray, /isOpenOrPending/);
assert.match(helper, /square tips under a rounded panel/);
assert.match(helper, /not resizable/);
assert.doesNotMatch(helper, /[^\x00-\x7F]/, "helper comments must stay ASCII-only");
const contentHelperMatch = helper.match(
/function windowsFramelessContentChromeOptions\([\s\S]*?\n\}/,
);
assert.ok(contentHelperMatch, "content chrome helper must exist");
assert.match(contentHelperMatch[0], /roundedCorners:\s*true/);
assert.doesNotMatch(
contentHelperMatch[0],
/transparent/,
"content chrome must stay opaque/resizable",
);
});