mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-15 20:12:59 -07:00
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:
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user