fix(macos): preserve overlay on transient tracker loss and fix subsync m

- macOS tracker now reports minimized vs not-found so transient helper misses no longer hide the overlay; minimizing mpv still triggers hide
- overlay-runtime-init skips hide on non-minimized window-lost and calls updateVisibleOverlayVisibility instead
- overlay-visibility preserves window level and passthrough state during transient tracker loss
- subsync modal open uses dedicated modal window with retry logic to fix first-attempt flash and stale modal state on macOS
This commit is contained in:
2026-05-15 02:31:23 -07:00
parent f0324cd93a
commit f1f9d34f7b
13 changed files with 384 additions and 79 deletions
+46 -1
View File
@@ -1,6 +1,14 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { MacOSWindowTracker } from './macos-tracker';
import { MacOSWindowTracker, parseMacOSHelperOutput } from './macos-tracker';
test('parseMacOSHelperOutput parses minimized state', () => {
assert.deepEqual(parseMacOSHelperOutput('minimized'), {
geometry: null,
focused: false,
minimized: true,
});
});
test('MacOSWindowTracker keeps the last geometry through a single helper miss', async () => {
let callIndex = 0;
@@ -170,3 +178,40 @@ test('MacOSWindowTracker drops tracking after grace window expires', async () =>
assert.equal(tracker.isTracking(), false);
assert.equal(tracker.getGeometry(), null);
});
test('MacOSWindowTracker reports minimized target when helper reports minimized', async () => {
let callIndex = 0;
let now = 1_000;
const outputs = [
{ stdout: '10,20,1280,720,1', stderr: '' },
{ stdout: 'minimized', stderr: '' },
{ stdout: 'minimized', stderr: '' },
];
const tracker = new MacOSWindowTracker('/tmp/mpv.sock', {
resolveHelper: () => ({
helperPath: 'helper.swift',
helperType: 'swift',
}),
runHelper: async () => outputs[callIndex++] ?? outputs.at(-1)!,
now: () => now,
minimizedTrackingLossGraceMs: 200,
});
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(tracker.isTracking(), true);
assert.equal(tracker.isTargetWindowMinimized(), false);
now += 250;
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(tracker.isTargetWindowMinimized(), true);
assert.equal(tracker.isTracking(), true);
now += 250;
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(tracker.isTargetWindowMinimized(), true);
assert.equal(tracker.isTracking(), false);
});
+43 -9
View File
@@ -40,13 +40,21 @@ type MacOSTrackerDeps = {
) => Promise<MacOSTrackerRunnerResult>;
maxConsecutiveMisses?: number;
trackingLossGraceMs?: number;
minimizedTrackingLossGraceMs?: number;
now?: () => number;
};
export interface MacOSHelperWindowState {
geometry: WindowGeometry;
focused: boolean;
}
export type MacOSHelperWindowState =
| {
geometry: WindowGeometry;
focused: boolean;
minimized?: false;
}
| {
geometry: null;
focused: false;
minimized: true;
};
function runHelperWithExecFile(
helperPath: string,
@@ -84,6 +92,13 @@ function runHelperWithExecFile(
export function parseMacOSHelperOutput(result: string): MacOSHelperWindowState | null {
const trimmed = result.trim();
if (trimmed === 'minimized') {
return {
geometry: null,
focused: false,
minimized: true,
};
}
if (!trimmed || trimmed === 'not-found') {
return null;
}
@@ -137,9 +152,11 @@ export class MacOSWindowTracker extends BaseWindowTracker {
) => Promise<MacOSTrackerRunnerResult>;
private readonly maxConsecutiveMisses: number;
private readonly trackingLossGraceMs: number;
private readonly minimizedTrackingLossGraceMs: number;
private readonly now: () => number;
private consecutiveMisses = 0;
private trackingLossStartedAtMs: number | null = null;
private targetWindowMinimized = false;
constructor(targetMpvSocketPath?: string, deps: MacOSTrackerDeps = {}) {
super();
@@ -147,6 +164,10 @@ export class MacOSWindowTracker extends BaseWindowTracker {
this.runHelper = deps.runHelper ?? runHelperWithExecFile;
this.maxConsecutiveMisses = Math.max(1, Math.floor(deps.maxConsecutiveMisses ?? 2));
this.trackingLossGraceMs = Math.max(0, Math.floor(deps.trackingLossGraceMs ?? 1_500));
this.minimizedTrackingLossGraceMs = Math.max(
0,
Math.floor(deps.minimizedTrackingLossGraceMs ?? 500),
);
this.now = deps.now ?? (() => Date.now());
const resolvedHelper = deps.resolveHelper?.() ?? null;
if (resolvedHelper) {
@@ -259,28 +280,32 @@ export class MacOSWindowTracker extends BaseWindowTracker {
}
}
override isTargetWindowMinimized(): boolean {
return this.targetWindowMinimized;
}
private resetTrackingLossState(): void {
this.consecutiveMisses = 0;
this.trackingLossStartedAtMs = null;
}
private shouldDropTracking(): boolean {
private shouldDropTracking(graceMs = this.trackingLossGraceMs): boolean {
if (!this.isTracking()) {
return true;
}
if (this.trackingLossGraceMs === 0) {
if (graceMs === 0) {
return this.consecutiveMisses >= this.maxConsecutiveMisses;
}
if (this.trackingLossStartedAtMs === null) {
this.trackingLossStartedAtMs = this.now();
return false;
}
return this.now() - this.trackingLossStartedAtMs > this.trackingLossGraceMs;
return this.now() - this.trackingLossStartedAtMs > graceMs;
}
private registerTrackingMiss(): void {
private registerTrackingMiss(graceMs = this.trackingLossGraceMs): void {
this.consecutiveMisses += 1;
if (this.shouldDropTracking()) {
if (this.shouldDropTracking(graceMs)) {
this.updateGeometry(null);
this.resetTrackingLossState();
}
@@ -296,12 +321,20 @@ export class MacOSWindowTracker extends BaseWindowTracker {
.then(({ stdout }) => {
const parsed = parseMacOSHelperOutput(stdout || '');
if (parsed) {
if (parsed.minimized) {
this.targetWindowMinimized = true;
this.updateTargetWindowFocused(false);
this.registerTrackingMiss(this.minimizedTrackingLossGraceMs);
return;
}
this.resetTrackingLossState();
this.targetWindowMinimized = false;
this.updateFocus(parsed.focused);
this.updateGeometry(parsed.geometry);
return;
}
this.targetWindowMinimized = false;
this.registerTrackingMiss();
})
.catch((error: unknown) => {
@@ -314,6 +347,7 @@ export class MacOSWindowTracker extends BaseWindowTracker {
? (error as { stderr: string }).stderr
: '';
this.maybeLogExecError(err, stderr);
this.targetWindowMinimized = false;
this.registerTrackingMiss();
})
.finally(() => {