Files
NetMesh/electron/bridges/windowManager/displayRecovery.cjs
zhaolei 3c72efcb7f
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
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

465 lines
15 KiB
JavaScript

"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,
};