fix: delay youtube overlay bootstrap until yomitan loads

This commit is contained in:
2026-03-25 20:36:50 -07:00
parent 55300e2d8c
commit 9dca83acd9
16 changed files with 143 additions and 24 deletions

View File

@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Fixed `subminer <youtube-url>` on Linux so the YouTube playback flow waits for Yomitan to load before creating the overlay window, avoiding the broken lookup popup state that previously required a manual overlay refresh.

View File

@@ -1,6 +1,7 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { import {
commandNeedsOverlayRuntime,
hasExplicitCommand, hasExplicitCommand,
isHeadlessInitialCommand, isHeadlessInitialCommand,
parseArgs, parseArgs,
@@ -70,6 +71,12 @@ test('parseArgs captures youtube startup forwarding flags', () => {
assert.equal(shouldStartApp(args), true); assert.equal(shouldStartApp(args), true);
}); });
test('youtube playback does not use generic overlay-runtime bootstrap classification', () => {
const args = parseArgs(['--youtube-play', 'https://youtube.com/watch?v=abc']);
assert.equal(commandNeedsOverlayRuntime(args), false);
});
test('parseArgs handles jellyfin item listing controls', () => { test('parseArgs handles jellyfin item listing controls', () => {
const args = parseArgs([ const args = parseArgs([
'--jellyfin-items', '--jellyfin-items',

View File

@@ -499,7 +499,10 @@ export function commandNeedsOverlayRuntime(args: CliArgs): boolean {
args.triggerFieldGrouping || args.triggerFieldGrouping ||
args.triggerSubsync || args.triggerSubsync ||
args.markAudioCard || args.markAudioCard ||
args.openRuntimeOptions || args.openRuntimeOptions
Boolean(args.youtubePlay)
); );
} }
export function commandNeedsOverlayStartupPrereqs(args: CliArgs): boolean {
return commandNeedsOverlayRuntime(args) || Boolean(args.youtubePlay);
}

View File

@@ -225,10 +225,7 @@ test('handleCliCommand starts youtube playback flow on initial launch', () => {
deps, deps,
); );
assert.deepEqual(calls, [ assert.deepEqual(calls, ['youtube:https://youtube.com/watch?v=abc:generate']);
'initializeOverlayRuntime',
'youtube:https://youtube.com/watch?v=abc:generate',
]);
}); });
test('handleCliCommand defaults youtube mode to download when omitted', () => { test('handleCliCommand defaults youtube mode to download when omitted', () => {
@@ -240,10 +237,7 @@ test('handleCliCommand defaults youtube mode to download when omitted', () => {
handleCliCommand(makeArgs({ youtubePlay: 'https://youtube.com/watch?v=abc' }), 'initial', deps); handleCliCommand(makeArgs({ youtubePlay: 'https://youtube.com/watch?v=abc' }), 'initial', deps);
assert.deepEqual(calls, [ assert.deepEqual(calls, ['youtube:https://youtube.com/watch?v=abc:download']);
'initializeOverlayRuntime',
'youtube:https://youtube.com/watch?v=abc:download',
]);
}); });
test('handleCliCommand reuses initialized overlay runtime for second-instance youtube playback', () => { test('handleCliCommand reuses initialized overlay runtime for second-instance youtube playback', () => {

View File

@@ -105,6 +105,7 @@ import { createLogger, setLogLevel, type LogLevelSource } from './logger';
import { resolveDefaultLogFilePath } from './logger'; import { resolveDefaultLogFilePath } from './logger';
import { createWindowTracker as createWindowTrackerCore } from './window-trackers'; import { createWindowTracker as createWindowTrackerCore } from './window-trackers';
import { import {
commandNeedsOverlayStartupPrereqs,
commandNeedsOverlayRuntime, commandNeedsOverlayRuntime,
isHeadlessInitialCommand, isHeadlessInitialCommand,
parseArgs, parseArgs,
@@ -990,6 +991,7 @@ async function runYoutubePlaybackFlowMain(request: {
try { try {
clearYoutubePlayQuitOnDisconnectArmTimer(); clearYoutubePlayQuitOnDisconnectArmTimer();
youtubePlayQuitOnDisconnectArmed = false; youtubePlayQuitOnDisconnectArmed = false;
await ensureYoutubePlaybackRuntimeReady();
let playbackUrl = request.url; let playbackUrl = request.url;
let launchedWindowsMpv = false; let launchedWindowsMpv = false;
if (process.platform === 'win32') { if (process.platform === 'win32') {
@@ -3528,7 +3530,7 @@ const handleCliCommand = createCliCommandRuntimeHandler({
setTexthookerOnlyMode: (enabled) => { setTexthookerOnlyMode: (enabled) => {
appState.texthookerOnlyMode = enabled; appState.texthookerOnlyMode = enabled;
}, },
commandNeedsOverlayRuntime: (inputArgs) => commandNeedsOverlayRuntime(inputArgs), commandNeedsOverlayStartupPrereqs: (inputArgs) => commandNeedsOverlayStartupPrereqs(inputArgs),
startBackgroundWarmups: () => startBackgroundWarmups(), startBackgroundWarmups: () => startBackgroundWarmups(),
logInfo: (message: string) => logger.info(message), logInfo: (message: string) => logger.info(message),
}, },
@@ -3571,6 +3573,16 @@ function ensureOverlayStartupPrereqs(): void {
} }
} }
async function ensureYoutubePlaybackRuntimeReady(): Promise<void> {
ensureOverlayStartupPrereqs();
await ensureYomitanExtensionLoaded();
if (!appState.overlayRuntimeInitialized) {
initializeOverlayRuntime();
return;
}
ensureOverlayWindowsReadyForVisibilityActions();
}
const handleInitialArgsRuntimeHandler = createInitialArgsRuntimeHandler({ const handleInitialArgsRuntimeHandler = createInitialArgsRuntimeHandler({
getInitialArgs: () => appState.initialArgs, getInitialArgs: () => appState.initialArgs,
isBackgroundMode: () => appState.backgroundMode, isBackgroundMode: () => appState.backgroundMode,
@@ -3581,6 +3593,7 @@ const handleInitialArgsRuntimeHandler = createInitialArgsRuntimeHandler({
isTexthookerOnlyMode: () => appState.texthookerOnlyMode, isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
hasImmersionTracker: () => Boolean(appState.immersionTracker), hasImmersionTracker: () => Boolean(appState.immersionTracker),
getMpvClient: () => appState.mpvClient, getMpvClient: () => appState.mpvClient,
commandNeedsOverlayStartupPrereqs: (args) => commandNeedsOverlayStartupPrereqs(args),
commandNeedsOverlayRuntime: (args) => commandNeedsOverlayRuntime(args), commandNeedsOverlayRuntime: (args) => commandNeedsOverlayRuntime(args),
ensureOverlayStartupPrereqs: () => ensureOverlayStartupPrereqs(), ensureOverlayStartupPrereqs: () => ensureOverlayStartupPrereqs(),
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized, isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,

View File

@@ -7,14 +7,14 @@ test('cli prechecks main deps builder maps transition handlers', () => {
const deps = createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler({ const deps = createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler({
isTexthookerOnlyMode: () => true, isTexthookerOnlyMode: () => true,
setTexthookerOnlyMode: (enabled) => calls.push(`set:${enabled}`), setTexthookerOnlyMode: (enabled) => calls.push(`set:${enabled}`),
commandNeedsOverlayRuntime: () => true, commandNeedsOverlayStartupPrereqs: () => true,
ensureOverlayStartupPrereqs: () => calls.push('prereqs'), ensureOverlayStartupPrereqs: () => calls.push('prereqs'),
startBackgroundWarmups: () => calls.push('warmups'), startBackgroundWarmups: () => calls.push('warmups'),
logInfo: (message) => calls.push(`info:${message}`), logInfo: (message) => calls.push(`info:${message}`),
})(); })();
assert.equal(deps.isTexthookerOnlyMode(), true); assert.equal(deps.isTexthookerOnlyMode(), true);
assert.equal(deps.commandNeedsOverlayRuntime({} as never), true); assert.equal(deps.commandNeedsOverlayStartupPrereqs({} as never), true);
deps.setTexthookerOnlyMode(false); deps.setTexthookerOnlyMode(false);
deps.ensureOverlayStartupPrereqs(); deps.ensureOverlayStartupPrereqs();
deps.startBackgroundWarmups(); deps.startBackgroundWarmups();

View File

@@ -3,7 +3,7 @@ import type { CliArgs } from '../../cli/args';
export function createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler(deps: { export function createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler(deps: {
isTexthookerOnlyMode: () => boolean; isTexthookerOnlyMode: () => boolean;
setTexthookerOnlyMode: (enabled: boolean) => void; setTexthookerOnlyMode: (enabled: boolean) => void;
commandNeedsOverlayRuntime: (args: CliArgs) => boolean; commandNeedsOverlayStartupPrereqs: (args: CliArgs) => boolean;
ensureOverlayStartupPrereqs: () => void; ensureOverlayStartupPrereqs: () => void;
startBackgroundWarmups: () => void; startBackgroundWarmups: () => void;
logInfo: (message: string) => void; logInfo: (message: string) => void;
@@ -11,7 +11,8 @@ export function createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler(dep
return () => ({ return () => ({
isTexthookerOnlyMode: () => deps.isTexthookerOnlyMode(), isTexthookerOnlyMode: () => deps.isTexthookerOnlyMode(),
setTexthookerOnlyMode: (enabled: boolean) => deps.setTexthookerOnlyMode(enabled), setTexthookerOnlyMode: (enabled: boolean) => deps.setTexthookerOnlyMode(enabled),
commandNeedsOverlayRuntime: (args: CliArgs) => deps.commandNeedsOverlayRuntime(args), commandNeedsOverlayStartupPrereqs: (args: CliArgs) =>
deps.commandNeedsOverlayStartupPrereqs(args),
ensureOverlayStartupPrereqs: () => deps.ensureOverlayStartupPrereqs(), ensureOverlayStartupPrereqs: () => deps.ensureOverlayStartupPrereqs(),
startBackgroundWarmups: () => deps.startBackgroundWarmups(), startBackgroundWarmups: () => deps.startBackgroundWarmups(),
logInfo: (message: string) => deps.logInfo(message), logInfo: (message: string) => deps.logInfo(message),

View File

@@ -7,7 +7,7 @@ test('texthooker precheck no-ops when mode is disabled', () => {
const handlePrecheck = createHandleTexthookerOnlyModeTransitionHandler({ const handlePrecheck = createHandleTexthookerOnlyModeTransitionHandler({
isTexthookerOnlyMode: () => false, isTexthookerOnlyMode: () => false,
setTexthookerOnlyMode: () => {}, setTexthookerOnlyMode: () => {},
commandNeedsOverlayRuntime: () => true, commandNeedsOverlayStartupPrereqs: () => true,
ensureOverlayStartupPrereqs: () => {}, ensureOverlayStartupPrereqs: () => {},
startBackgroundWarmups: () => { startBackgroundWarmups: () => {
warmups += 1; warmups += 1;
@@ -29,7 +29,7 @@ test('texthooker precheck disables mode and warms up on start command', () => {
setTexthookerOnlyMode: (enabled) => { setTexthookerOnlyMode: (enabled) => {
mode = enabled; mode = enabled;
}, },
commandNeedsOverlayRuntime: () => false, commandNeedsOverlayStartupPrereqs: () => false,
ensureOverlayStartupPrereqs: () => { ensureOverlayStartupPrereqs: () => {
prereqs += 1; prereqs += 1;
}, },
@@ -55,7 +55,7 @@ test('texthooker precheck no-ops for texthooker command', () => {
setTexthookerOnlyMode: (enabled) => { setTexthookerOnlyMode: (enabled) => {
mode = enabled; mode = enabled;
}, },
commandNeedsOverlayRuntime: () => true, commandNeedsOverlayStartupPrereqs: () => true,
ensureOverlayStartupPrereqs: () => {}, ensureOverlayStartupPrereqs: () => {},
startBackgroundWarmups: () => {}, startBackgroundWarmups: () => {},
logInfo: () => {}, logInfo: () => {},
@@ -64,3 +64,25 @@ test('texthooker precheck no-ops for texthooker command', () => {
handlePrecheck({ start: true, texthooker: true } as never); handlePrecheck({ start: true, texthooker: true } as never);
assert.equal(mode, true); assert.equal(mode, true);
}); });
test('texthooker precheck transitions for youtube playback startup prereqs', () => {
let mode = true;
let prereqs = 0;
const handlePrecheck = createHandleTexthookerOnlyModeTransitionHandler({
isTexthookerOnlyMode: () => mode,
setTexthookerOnlyMode: (enabled) => {
mode = enabled;
},
commandNeedsOverlayStartupPrereqs: () => true,
ensureOverlayStartupPrereqs: () => {
prereqs += 1;
},
startBackgroundWarmups: () => {},
logInfo: () => {},
});
handlePrecheck({ youtubePlay: 'https://youtube.com/watch?v=abc', texthooker: false } as never);
assert.equal(mode, false);
assert.equal(prereqs, 1);
});

View File

@@ -3,7 +3,7 @@ import type { CliArgs } from '../../cli/args';
export function createHandleTexthookerOnlyModeTransitionHandler(deps: { export function createHandleTexthookerOnlyModeTransitionHandler(deps: {
isTexthookerOnlyMode: () => boolean; isTexthookerOnlyMode: () => boolean;
setTexthookerOnlyMode: (enabled: boolean) => void; setTexthookerOnlyMode: (enabled: boolean) => void;
commandNeedsOverlayRuntime: (args: CliArgs) => boolean; commandNeedsOverlayStartupPrereqs: (args: CliArgs) => boolean;
ensureOverlayStartupPrereqs: () => void; ensureOverlayStartupPrereqs: () => void;
startBackgroundWarmups: () => void; startBackgroundWarmups: () => void;
logInfo: (message: string) => void; logInfo: (message: string) => void;
@@ -12,7 +12,7 @@ export function createHandleTexthookerOnlyModeTransitionHandler(deps: {
if ( if (
deps.isTexthookerOnlyMode() && deps.isTexthookerOnlyMode() &&
!args.texthooker && !args.texthooker &&
(args.start || deps.commandNeedsOverlayRuntime(args)) (args.start || deps.commandNeedsOverlayStartupPrereqs(args))
) { ) {
deps.ensureOverlayStartupPrereqs(); deps.ensureOverlayStartupPrereqs();
deps.setTexthookerOnlyMode(false); deps.setTexthookerOnlyMode(false);

View File

@@ -8,7 +8,7 @@ test('cli command runtime handler applies precheck and forwards command with con
handleTexthookerOnlyModeTransitionMainDeps: { handleTexthookerOnlyModeTransitionMainDeps: {
isTexthookerOnlyMode: () => true, isTexthookerOnlyMode: () => true,
setTexthookerOnlyMode: () => calls.push('set-mode'), setTexthookerOnlyMode: () => calls.push('set-mode'),
commandNeedsOverlayRuntime: () => true, commandNeedsOverlayStartupPrereqs: () => true,
ensureOverlayStartupPrereqs: () => calls.push('prereqs'), ensureOverlayStartupPrereqs: () => calls.push('prereqs'),
startBackgroundWarmups: () => calls.push('warmups'), startBackgroundWarmups: () => calls.push('warmups'),
logInfo: (message) => calls.push(`log:${message}`), logInfo: (message) => calls.push(`log:${message}`),
@@ -40,7 +40,7 @@ test('cli command runtime handler prepares overlay prerequisites before overlay
handleTexthookerOnlyModeTransitionMainDeps: { handleTexthookerOnlyModeTransitionMainDeps: {
isTexthookerOnlyMode: () => false, isTexthookerOnlyMode: () => false,
setTexthookerOnlyMode: () => calls.push('set-mode'), setTexthookerOnlyMode: () => calls.push('set-mode'),
commandNeedsOverlayRuntime: () => true, commandNeedsOverlayStartupPrereqs: () => true,
ensureOverlayStartupPrereqs: () => calls.push('prereqs'), ensureOverlayStartupPrereqs: () => calls.push('prereqs'),
startBackgroundWarmups: () => calls.push('warmups'), startBackgroundWarmups: () => calls.push('warmups'),
logInfo: (message) => calls.push(`log:${message}`), logInfo: (message) => calls.push(`log:${message}`),
@@ -58,3 +58,28 @@ test('cli command runtime handler prepares overlay prerequisites before overlay
assert.deepEqual(calls, ['prereqs', 'context', 'cli:initial:ctx']); assert.deepEqual(calls, ['prereqs', 'context', 'cli:initial:ctx']);
}); });
test('cli command runtime handler skips generic overlay prerequisites for youtube playback', () => {
const calls: string[] = [];
const handler = createCliCommandRuntimeHandler({
handleTexthookerOnlyModeTransitionMainDeps: {
isTexthookerOnlyMode: () => false,
setTexthookerOnlyMode: () => calls.push('set-mode'),
commandNeedsOverlayStartupPrereqs: () => false,
ensureOverlayStartupPrereqs: () => calls.push('prereqs'),
startBackgroundWarmups: () => calls.push('warmups'),
logInfo: (message) => calls.push(`log:${message}`),
},
createCliCommandContext: () => {
calls.push('context');
return { id: 'ctx' };
},
handleCliCommandRuntimeServiceWithContext: (_args, source, context) => {
calls.push(`cli:${source}:${context.id}`);
},
});
handler({ youtubePlay: 'https://youtube.com/watch?v=abc' } as never);
assert.deepEqual(calls, ['context', 'cli:initial:ctx']);
});

View File

@@ -25,7 +25,7 @@ export function createCliCommandRuntimeHandler<TCliContext>(deps: {
handleTexthookerOnlyModeTransitionHandler(args); handleTexthookerOnlyModeTransitionHandler(args);
if ( if (
!deps.handleTexthookerOnlyModeTransitionMainDeps.isTexthookerOnlyMode() && !deps.handleTexthookerOnlyModeTransitionMainDeps.isTexthookerOnlyMode() &&
deps.handleTexthookerOnlyModeTransitionMainDeps.commandNeedsOverlayRuntime(args) deps.handleTexthookerOnlyModeTransitionMainDeps.commandNeedsOverlayStartupPrereqs(args)
) { ) {
deps.handleTexthookerOnlyModeTransitionMainDeps.ensureOverlayStartupPrereqs(); deps.handleTexthookerOnlyModeTransitionMainDeps.ensureOverlayStartupPrereqs();
} }

View File

@@ -13,6 +13,7 @@ test('initial args handler no-ops without initial args', () => {
isTexthookerOnlyMode: () => false, isTexthookerOnlyMode: () => false,
hasImmersionTracker: () => false, hasImmersionTracker: () => false,
getMpvClient: () => null, getMpvClient: () => null,
commandNeedsOverlayStartupPrereqs: () => false,
commandNeedsOverlayRuntime: () => false, commandNeedsOverlayRuntime: () => false,
ensureOverlayStartupPrereqs: () => {}, ensureOverlayStartupPrereqs: () => {},
isOverlayRuntimeInitialized: () => false, isOverlayRuntimeInitialized: () => false,
@@ -40,6 +41,7 @@ test('initial args handler ensures tray in background mode', () => {
isTexthookerOnlyMode: () => true, isTexthookerOnlyMode: () => true,
hasImmersionTracker: () => false, hasImmersionTracker: () => false,
getMpvClient: () => null, getMpvClient: () => null,
commandNeedsOverlayStartupPrereqs: () => false,
commandNeedsOverlayRuntime: () => false, commandNeedsOverlayRuntime: () => false,
ensureOverlayStartupPrereqs: () => {}, ensureOverlayStartupPrereqs: () => {},
isOverlayRuntimeInitialized: () => false, isOverlayRuntimeInitialized: () => false,
@@ -69,6 +71,7 @@ test('initial args handler auto-connects mpv when needed', () => {
connectCalls += 1; connectCalls += 1;
}, },
}), }),
commandNeedsOverlayStartupPrereqs: () => false,
commandNeedsOverlayRuntime: () => false, commandNeedsOverlayRuntime: () => false,
ensureOverlayStartupPrereqs: () => {}, ensureOverlayStartupPrereqs: () => {},
isOverlayRuntimeInitialized: () => false, isOverlayRuntimeInitialized: () => false,
@@ -95,6 +98,7 @@ test('initial args handler forwards args to cli handler', () => {
isTexthookerOnlyMode: () => false, isTexthookerOnlyMode: () => false,
hasImmersionTracker: () => false, hasImmersionTracker: () => false,
getMpvClient: () => null, getMpvClient: () => null,
commandNeedsOverlayStartupPrereqs: () => false,
commandNeedsOverlayRuntime: () => false, commandNeedsOverlayRuntime: () => false,
ensureOverlayStartupPrereqs: () => { ensureOverlayStartupPrereqs: () => {
seenSources.push('prereqs'); seenSources.push('prereqs');
@@ -125,6 +129,7 @@ test('initial args handler bootstraps overlay before initial overlay-runtime com
isTexthookerOnlyMode: () => false, isTexthookerOnlyMode: () => false,
hasImmersionTracker: () => false, hasImmersionTracker: () => false,
getMpvClient: () => null, getMpvClient: () => null,
commandNeedsOverlayStartupPrereqs: (inputArgs) => inputArgs === args,
commandNeedsOverlayRuntime: (inputArgs) => inputArgs === args, commandNeedsOverlayRuntime: (inputArgs) => inputArgs === args,
ensureOverlayStartupPrereqs: () => { ensureOverlayStartupPrereqs: () => {
calls.push('prereqs'); calls.push('prereqs');
@@ -144,6 +149,38 @@ test('initial args handler bootstraps overlay before initial overlay-runtime com
assert.deepEqual(calls, ['prereqs', 'init-overlay', 'cli:initial']); assert.deepEqual(calls, ['prereqs', 'init-overlay', 'cli:initial']);
}); });
test('initial args handler prepares prereqs but skips eager overlay bootstrap for youtube playback', () => {
const calls: string[] = [];
const args = { youtubePlay: 'https://youtube.com/watch?v=abc' } as never;
const handleInitialArgs = createHandleInitialArgsHandler({
getInitialArgs: () => args,
isBackgroundMode: () => false,
shouldEnsureTrayOnStartup: () => false,
shouldRunHeadlessInitialCommand: () => false,
ensureTray: () => {},
isTexthookerOnlyMode: () => false,
hasImmersionTracker: () => false,
getMpvClient: () => null,
commandNeedsOverlayStartupPrereqs: () => true,
commandNeedsOverlayRuntime: () => false,
ensureOverlayStartupPrereqs: () => {
calls.push('prereqs');
},
isOverlayRuntimeInitialized: () => false,
initializeOverlayRuntime: () => {
calls.push('init-overlay');
},
logInfo: () => {},
handleCliCommand: (_args, source) => {
calls.push(`cli:${source}`);
},
});
handleInitialArgs();
assert.deepEqual(calls, ['prereqs', 'cli:initial']);
});
test('initial args handler can ensure tray outside background mode when requested', () => { test('initial args handler can ensure tray outside background mode when requested', () => {
let ensuredTray = false; let ensuredTray = false;
const handleInitialArgs = createHandleInitialArgsHandler({ const handleInitialArgs = createHandleInitialArgsHandler({
@@ -157,6 +194,7 @@ test('initial args handler can ensure tray outside background mode when requeste
isTexthookerOnlyMode: () => true, isTexthookerOnlyMode: () => true,
hasImmersionTracker: () => false, hasImmersionTracker: () => false,
getMpvClient: () => null, getMpvClient: () => null,
commandNeedsOverlayStartupPrereqs: () => false,
commandNeedsOverlayRuntime: () => false, commandNeedsOverlayRuntime: () => false,
ensureOverlayStartupPrereqs: () => {}, ensureOverlayStartupPrereqs: () => {},
isOverlayRuntimeInitialized: () => false, isOverlayRuntimeInitialized: () => false,
@@ -188,6 +226,7 @@ test('initial args handler skips tray and mpv auto-connect for headless refresh'
connectCalls += 1; connectCalls += 1;
}, },
}), }),
commandNeedsOverlayStartupPrereqs: () => true,
commandNeedsOverlayRuntime: () => true, commandNeedsOverlayRuntime: () => true,
ensureOverlayStartupPrereqs: () => {}, ensureOverlayStartupPrereqs: () => {},
isOverlayRuntimeInitialized: () => false, isOverlayRuntimeInitialized: () => false,

View File

@@ -14,6 +14,7 @@ export function createHandleInitialArgsHandler(deps: {
isTexthookerOnlyMode: () => boolean; isTexthookerOnlyMode: () => boolean;
hasImmersionTracker: () => boolean; hasImmersionTracker: () => boolean;
getMpvClient: () => MpvClientLike | null; getMpvClient: () => MpvClientLike | null;
commandNeedsOverlayStartupPrereqs: (args: CliArgs) => boolean;
commandNeedsOverlayRuntime: (args: CliArgs) => boolean; commandNeedsOverlayRuntime: (args: CliArgs) => boolean;
ensureOverlayStartupPrereqs: () => void; ensureOverlayStartupPrereqs: () => void;
isOverlayRuntimeInitialized: () => boolean; isOverlayRuntimeInitialized: () => boolean;
@@ -43,8 +44,10 @@ export function createHandleInitialArgsHandler(deps: {
mpvClient.connect(); mpvClient.connect();
} }
if (!runHeadless && deps.commandNeedsOverlayRuntime(initialArgs)) { if (!runHeadless && deps.commandNeedsOverlayStartupPrereqs(initialArgs)) {
deps.ensureOverlayStartupPrereqs(); deps.ensureOverlayStartupPrereqs();
}
if (!runHeadless && deps.commandNeedsOverlayRuntime(initialArgs)) {
if (!deps.isOverlayRuntimeInitialized()) { if (!deps.isOverlayRuntimeInitialized()) {
deps.initializeOverlayRuntime(); deps.initializeOverlayRuntime();
} }

View File

@@ -15,6 +15,7 @@ test('initial args main deps builder maps runtime callbacks and state readers',
isTexthookerOnlyMode: () => false, isTexthookerOnlyMode: () => false,
hasImmersionTracker: () => true, hasImmersionTracker: () => true,
getMpvClient: () => mpvClient, getMpvClient: () => mpvClient,
commandNeedsOverlayStartupPrereqs: () => true,
commandNeedsOverlayRuntime: () => true, commandNeedsOverlayRuntime: () => true,
ensureOverlayStartupPrereqs: () => calls.push('prereqs'), ensureOverlayStartupPrereqs: () => calls.push('prereqs'),
isOverlayRuntimeInitialized: () => false, isOverlayRuntimeInitialized: () => false,
@@ -30,6 +31,7 @@ test('initial args main deps builder maps runtime callbacks and state readers',
assert.equal(deps.isTexthookerOnlyMode(), false); assert.equal(deps.isTexthookerOnlyMode(), false);
assert.equal(deps.hasImmersionTracker(), true); assert.equal(deps.hasImmersionTracker(), true);
assert.equal(deps.getMpvClient(), mpvClient); assert.equal(deps.getMpvClient(), mpvClient);
assert.equal(deps.commandNeedsOverlayStartupPrereqs(args), true);
assert.equal(deps.commandNeedsOverlayRuntime(args), true); assert.equal(deps.commandNeedsOverlayRuntime(args), true);
assert.equal(deps.isOverlayRuntimeInitialized(), false); assert.equal(deps.isOverlayRuntimeInitialized(), false);

View File

@@ -9,6 +9,7 @@ export function createBuildHandleInitialArgsMainDepsHandler(deps: {
isTexthookerOnlyMode: () => boolean; isTexthookerOnlyMode: () => boolean;
hasImmersionTracker: () => boolean; hasImmersionTracker: () => boolean;
getMpvClient: () => { connected: boolean; connect: () => void } | null; getMpvClient: () => { connected: boolean; connect: () => void } | null;
commandNeedsOverlayStartupPrereqs: (args: CliArgs) => boolean;
commandNeedsOverlayRuntime: (args: CliArgs) => boolean; commandNeedsOverlayRuntime: (args: CliArgs) => boolean;
ensureOverlayStartupPrereqs: () => void; ensureOverlayStartupPrereqs: () => void;
isOverlayRuntimeInitialized: () => boolean; isOverlayRuntimeInitialized: () => boolean;
@@ -25,6 +26,8 @@ export function createBuildHandleInitialArgsMainDepsHandler(deps: {
isTexthookerOnlyMode: () => deps.isTexthookerOnlyMode(), isTexthookerOnlyMode: () => deps.isTexthookerOnlyMode(),
hasImmersionTracker: () => deps.hasImmersionTracker(), hasImmersionTracker: () => deps.hasImmersionTracker(),
getMpvClient: () => deps.getMpvClient(), getMpvClient: () => deps.getMpvClient(),
commandNeedsOverlayStartupPrereqs: (args: CliArgs) =>
deps.commandNeedsOverlayStartupPrereqs(args),
commandNeedsOverlayRuntime: (args: CliArgs) => deps.commandNeedsOverlayRuntime(args), commandNeedsOverlayRuntime: (args: CliArgs) => deps.commandNeedsOverlayRuntime(args),
ensureOverlayStartupPrereqs: () => deps.ensureOverlayStartupPrereqs(), ensureOverlayStartupPrereqs: () => deps.ensureOverlayStartupPrereqs(),
isOverlayRuntimeInitialized: () => deps.isOverlayRuntimeInitialized(), isOverlayRuntimeInitialized: () => deps.isOverlayRuntimeInitialized(),

View File

@@ -16,6 +16,7 @@ test('initial args runtime handler composes main deps and runs initial command f
connected: false, connected: false,
connect: () => calls.push('connect'), connect: () => calls.push('connect'),
}), }),
commandNeedsOverlayStartupPrereqs: () => false,
commandNeedsOverlayRuntime: () => false, commandNeedsOverlayRuntime: () => false,
ensureOverlayStartupPrereqs: () => calls.push('prereqs'), ensureOverlayStartupPrereqs: () => calls.push('prereqs'),
isOverlayRuntimeInitialized: () => false, isOverlayRuntimeInitialized: () => false,
@@ -48,6 +49,7 @@ test('initial args runtime handler skips mpv auto-connect for stats mode', () =>
connected: false, connected: false,
connect: () => calls.push('connect'), connect: () => calls.push('connect'),
}), }),
commandNeedsOverlayStartupPrereqs: () => false,
commandNeedsOverlayRuntime: () => false, commandNeedsOverlayRuntime: () => false,
ensureOverlayStartupPrereqs: () => calls.push('prereqs'), ensureOverlayStartupPrereqs: () => calls.push('prereqs'),
isOverlayRuntimeInitialized: () => false, isOverlayRuntimeInitialized: () => false,
@@ -75,6 +77,7 @@ test('initial args runtime handler skips tray and mpv auto-connect for headless
connected: false, connected: false,
connect: () => calls.push('connect'), connect: () => calls.push('connect'),
}), }),
commandNeedsOverlayStartupPrereqs: () => true,
commandNeedsOverlayRuntime: () => true, commandNeedsOverlayRuntime: () => true,
ensureOverlayStartupPrereqs: () => calls.push('prereqs'), ensureOverlayStartupPrereqs: () => calls.push('prereqs'),
isOverlayRuntimeInitialized: () => false, isOverlayRuntimeInitialized: () => false,