fix: restore overlay ownership during plugin auto-start

This commit is contained in:
2026-03-20 01:57:25 -07:00
parent 1342393035
commit bae2a49673
10 changed files with 130 additions and 33 deletions

View File

@@ -372,12 +372,9 @@ function M.create(ctx)
end) end)
end end
launch_overlay_with_retry(1)
if texthooker_enabled then if texthooker_enabled then
ensure_texthooker_running(function() ensure_texthooker_running(function() end)
launch_overlay_with_retry(1)
end)
else
launch_overlay_with_retry(1)
end end
end end
@@ -481,7 +478,6 @@ function M.create(ctx)
state.texthooker_running = false state.texthooker_running = false
disarm_auto_play_ready_gate() disarm_auto_play_ready_gate()
ensure_texthooker_running(function()
local start_args = build_command_args("start") local start_args = build_command_args("start")
subminer_log("info", "process", "Starting overlay: " .. table.concat(start_args, " ")) subminer_log("info", "process", "Starting overlay: " .. table.concat(start_args, " "))
@@ -505,7 +501,10 @@ function M.create(ctx)
show_osd("Restarted successfully") show_osd("Restarted successfully")
end end
end) end)
end)
if opts.texthooker_enabled then
ensure_texthooker_running(function() end)
end
end) end)
end end

View File

@@ -344,6 +344,27 @@ local function count_start_calls(async_calls)
return count return count
end end
local function find_texthooker_call(async_calls)
for _, call in ipairs(async_calls) do
local args = call.args or {}
for i = 1, #args do
if args[i] == "--texthooker" then
return call
end
end
end
return nil
end
local function find_call_index(async_calls, target_call)
for index, call in ipairs(async_calls) do
if call == target_call then
return index
end
end
return nil
end
local function find_control_call(async_calls, flag) local function find_control_call(async_calls, flag)
for _, call in ipairs(async_calls) do for _, call in ipairs(async_calls) do
local args = call.args or {} local args = call.args or {}
@@ -643,6 +664,8 @@ do
fire_event(recorded, "file-loaded") fire_event(recorded, "file-loaded")
local start_call = find_start_call(recorded.async_calls) local start_call = find_start_call(recorded.async_calls)
assert_true(start_call ~= nil, "auto-start should issue --start command") assert_true(start_call ~= nil, "auto-start should issue --start command")
local texthooker_call = find_texthooker_call(recorded.async_calls)
assert_true(texthooker_call ~= nil, "auto-start should issue texthooker helper command when enabled")
assert_true( assert_true(
call_has_arg(start_call, "--show-visible-overlay"), call_has_arg(start_call, "--show-visible-overlay"),
"auto-start with visible overlay enabled should include --show-visible-overlay on --start" "auto-start with visible overlay enabled should include --show-visible-overlay on --start"
@@ -655,6 +678,10 @@ do
find_control_call(recorded.async_calls, "--show-visible-overlay") ~= nil, find_control_call(recorded.async_calls, "--show-visible-overlay") ~= nil,
"auto-start with visible overlay enabled should issue a separate --show-visible-overlay command" "auto-start with visible overlay enabled should issue a separate --show-visible-overlay command"
) )
assert_true(
find_call_index(recorded.async_calls, start_call) < find_call_index(recorded.async_calls, texthooker_call),
"auto-start should launch --start before separate --texthooker helper startup"
)
assert_true( assert_true(
not has_property_set(recorded.property_sets, "pause", true), not has_property_set(recorded.property_sets, "pause", true),
"auto-start visible overlay should not force pause without explicit pause-until-ready option" "auto-start visible overlay should not force pause without explicit pause-until-ready option"

View File

@@ -176,6 +176,22 @@ test('runAppReadyRuntime skips heavy startup when shouldSkipHeavyStartup returns
assert.ok(calls.indexOf('handleFirstRunSetup') < calls.indexOf('handleInitialArgs')); assert.ok(calls.indexOf('handleFirstRunSetup') < calls.indexOf('handleInitialArgs'));
}); });
test('runAppReadyRuntime uses minimal startup for texthooker-only mode', async () => {
const { deps, calls } = makeDeps({
texthookerOnlyMode: true,
reloadConfig: () => calls.push('reloadConfig'),
handleInitialArgs: () => calls.push('handleInitialArgs'),
});
await runAppReadyRuntime(deps);
assert.deepEqual(calls, [
'ensureDefaultConfigBootstrap',
'reloadConfig',
'handleInitialArgs',
]);
});
test('runAppReadyRuntime skips Jellyfin remote startup when dependency is not wired', async () => { test('runAppReadyRuntime skips Jellyfin remote startup when dependency is not wired', async () => {
const { deps, calls } = makeDeps({ const { deps, calls } = makeDeps({
startJellyfinRemoteSession: undefined, startJellyfinRemoteSession: undefined,

View File

@@ -200,6 +200,12 @@ export async function runAppReadyRuntime(deps: AppReadyRuntimeDeps): Promise<voi
return; return;
} }
if (deps.texthookerOnlyMode) {
deps.reloadConfig();
deps.handleInitialArgs();
return;
}
if (deps.shouldUseMinimalStartup?.()) { if (deps.shouldUseMinimalStartup?.()) {
deps.reloadConfig(); deps.reloadConfig();
deps.handleInitialArgs(); deps.handleInitialArgs();

View File

@@ -3037,10 +3037,11 @@ const { appReadyRuntimeRunner } = composeAppReadyRuntime({
Boolean(appState.initialArgs && isHeadlessInitialCommand(appState.initialArgs)), Boolean(appState.initialArgs && isHeadlessInitialCommand(appState.initialArgs)),
shouldUseMinimalStartup: () => shouldUseMinimalStartup: () =>
Boolean( Boolean(
appState.initialArgs?.stats && appState.initialArgs?.texthooker ||
(appState.initialArgs?.stats &&
(appState.initialArgs?.statsCleanup || (appState.initialArgs?.statsCleanup ||
appState.initialArgs?.statsBackground || appState.initialArgs?.statsBackground ||
appState.initialArgs?.statsStop), appState.initialArgs?.statsStop)),
), ),
shouldSkipHeavyStartup: () => shouldSkipHeavyStartup: () =>
Boolean( Boolean(
@@ -3130,6 +3131,39 @@ void initializeDiscordPresenceService();
const handleCliCommand = createCliCommandRuntimeHandler({ const handleCliCommand = createCliCommandRuntimeHandler({
handleTexthookerOnlyModeTransitionMainDeps: { handleTexthookerOnlyModeTransitionMainDeps: {
isTexthookerOnlyMode: () => appState.texthookerOnlyMode, isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
ensureOverlayStartupPrereqs: () => {
if (appState.subtitlePosition === null) {
loadSubtitlePosition();
}
if (appState.keybindings.length === 0) {
appState.keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS);
}
if (!appState.mpvClient) {
appState.mpvClient = createMpvClientRuntimeService();
}
if (!appState.runtimeOptionsManager) {
appState.runtimeOptionsManager = new RuntimeOptionsManager(
() => configService.getConfig().ankiConnect,
{
applyAnkiPatch: (patch) => {
if (appState.ankiIntegration) {
appState.ankiIntegration.applyRuntimeConfigPatch(patch);
}
},
getSubtitleStyleConfig: () => configService.getConfig().subtitleStyle,
onOptionsChanged: () => {
subtitleProcessingController.invalidateTokenizationCache();
subtitlePrefetchService?.onSeek(lastObservedTimePos);
broadcastRuntimeOptionsChanged();
refreshOverlayShortcuts();
},
},
);
}
if (!appState.subtitleTimingTracker) {
appState.subtitleTimingTracker = new SubtitleTimingTracker();
}
},
setTexthookerOnlyMode: (enabled) => { setTexthookerOnlyMode: (enabled) => {
appState.texthookerOnlyMode = enabled; appState.texthookerOnlyMode = enabled;
}, },

View File

@@ -8,6 +8,7 @@ test('cli prechecks main deps builder maps transition handlers', () => {
isTexthookerOnlyMode: () => true, isTexthookerOnlyMode: () => true,
setTexthookerOnlyMode: (enabled) => calls.push(`set:${enabled}`), setTexthookerOnlyMode: (enabled) => calls.push(`set:${enabled}`),
commandNeedsOverlayRuntime: () => true, commandNeedsOverlayRuntime: () => true,
ensureOverlayStartupPrereqs: () => calls.push('prereqs'),
startBackgroundWarmups: () => calls.push('warmups'), startBackgroundWarmups: () => calls.push('warmups'),
logInfo: (message) => calls.push(`info:${message}`), logInfo: (message) => calls.push(`info:${message}`),
})(); })();
@@ -15,7 +16,8 @@ test('cli prechecks main deps builder maps transition handlers', () => {
assert.equal(deps.isTexthookerOnlyMode(), true); assert.equal(deps.isTexthookerOnlyMode(), true);
assert.equal(deps.commandNeedsOverlayRuntime({} as never), true); assert.equal(deps.commandNeedsOverlayRuntime({} as never), true);
deps.setTexthookerOnlyMode(false); deps.setTexthookerOnlyMode(false);
deps.ensureOverlayStartupPrereqs();
deps.startBackgroundWarmups(); deps.startBackgroundWarmups();
deps.logInfo('x'); deps.logInfo('x');
assert.deepEqual(calls, ['set:false', 'warmups', 'info:x']); assert.deepEqual(calls, ['set:false', 'prereqs', 'warmups', 'info:x']);
}); });

View File

@@ -4,6 +4,7 @@ export function createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler(dep
isTexthookerOnlyMode: () => boolean; isTexthookerOnlyMode: () => boolean;
setTexthookerOnlyMode: (enabled: boolean) => void; setTexthookerOnlyMode: (enabled: boolean) => void;
commandNeedsOverlayRuntime: (args: CliArgs) => boolean; commandNeedsOverlayRuntime: (args: CliArgs) => boolean;
ensureOverlayStartupPrereqs: () => void;
startBackgroundWarmups: () => void; startBackgroundWarmups: () => void;
logInfo: (message: string) => void; logInfo: (message: string) => void;
}) { }) {
@@ -11,6 +12,7 @@ export function createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler(dep
isTexthookerOnlyMode: () => deps.isTexthookerOnlyMode(), isTexthookerOnlyMode: () => deps.isTexthookerOnlyMode(),
setTexthookerOnlyMode: (enabled: boolean) => deps.setTexthookerOnlyMode(enabled), setTexthookerOnlyMode: (enabled: boolean) => deps.setTexthookerOnlyMode(enabled),
commandNeedsOverlayRuntime: (args: CliArgs) => deps.commandNeedsOverlayRuntime(args), commandNeedsOverlayRuntime: (args: CliArgs) => deps.commandNeedsOverlayRuntime(args),
ensureOverlayStartupPrereqs: () => deps.ensureOverlayStartupPrereqs(),
startBackgroundWarmups: () => deps.startBackgroundWarmups(), startBackgroundWarmups: () => deps.startBackgroundWarmups(),
logInfo: (message: string) => deps.logInfo(message), logInfo: (message: string) => deps.logInfo(message),
}); });

View File

@@ -8,6 +8,7 @@ test('texthooker precheck no-ops when mode is disabled', () => {
isTexthookerOnlyMode: () => false, isTexthookerOnlyMode: () => false,
setTexthookerOnlyMode: () => {}, setTexthookerOnlyMode: () => {},
commandNeedsOverlayRuntime: () => true, commandNeedsOverlayRuntime: () => true,
ensureOverlayStartupPrereqs: () => {},
startBackgroundWarmups: () => { startBackgroundWarmups: () => {
warmups += 1; warmups += 1;
}, },
@@ -22,12 +23,16 @@ test('texthooker precheck disables mode and warms up on start command', () => {
let mode = true; let mode = true;
let warmups = 0; let warmups = 0;
let logs = 0; let logs = 0;
let prereqs = 0;
const handlePrecheck = createHandleTexthookerOnlyModeTransitionHandler({ const handlePrecheck = createHandleTexthookerOnlyModeTransitionHandler({
isTexthookerOnlyMode: () => mode, isTexthookerOnlyMode: () => mode,
setTexthookerOnlyMode: (enabled) => { setTexthookerOnlyMode: (enabled) => {
mode = enabled; mode = enabled;
}, },
commandNeedsOverlayRuntime: () => false, commandNeedsOverlayRuntime: () => false,
ensureOverlayStartupPrereqs: () => {
prereqs += 1;
},
startBackgroundWarmups: () => { startBackgroundWarmups: () => {
warmups += 1; warmups += 1;
}, },
@@ -38,6 +43,7 @@ test('texthooker precheck disables mode and warms up on start command', () => {
handlePrecheck({ start: true, texthooker: false } as never); handlePrecheck({ start: true, texthooker: false } as never);
assert.equal(mode, false); assert.equal(mode, false);
assert.equal(prereqs, 1);
assert.equal(warmups, 1); assert.equal(warmups, 1);
assert.equal(logs, 1); assert.equal(logs, 1);
}); });
@@ -50,6 +56,7 @@ test('texthooker precheck no-ops for texthooker command', () => {
mode = enabled; mode = enabled;
}, },
commandNeedsOverlayRuntime: () => true, commandNeedsOverlayRuntime: () => true,
ensureOverlayStartupPrereqs: () => {},
startBackgroundWarmups: () => {}, startBackgroundWarmups: () => {},
logInfo: () => {}, logInfo: () => {},
}); });

View File

@@ -4,6 +4,7 @@ export function createHandleTexthookerOnlyModeTransitionHandler(deps: {
isTexthookerOnlyMode: () => boolean; isTexthookerOnlyMode: () => boolean;
setTexthookerOnlyMode: (enabled: boolean) => void; setTexthookerOnlyMode: (enabled: boolean) => void;
commandNeedsOverlayRuntime: (args: CliArgs) => boolean; commandNeedsOverlayRuntime: (args: CliArgs) => boolean;
ensureOverlayStartupPrereqs: () => void;
startBackgroundWarmups: () => void; startBackgroundWarmups: () => void;
logInfo: (message: string) => void; logInfo: (message: string) => void;
}) { }) {
@@ -13,6 +14,7 @@ export function createHandleTexthookerOnlyModeTransitionHandler(deps: {
!args.texthooker && !args.texthooker &&
(args.start || deps.commandNeedsOverlayRuntime(args)) (args.start || deps.commandNeedsOverlayRuntime(args))
) { ) {
deps.ensureOverlayStartupPrereqs();
deps.setTexthookerOnlyMode(false); deps.setTexthookerOnlyMode(false);
deps.logInfo('Disabling texthooker-only mode after overlay/start command.'); deps.logInfo('Disabling texthooker-only mode after overlay/start command.');
deps.startBackgroundWarmups(); deps.startBackgroundWarmups();

View File

@@ -9,6 +9,7 @@ test('cli command runtime handler applies precheck and forwards command with con
isTexthookerOnlyMode: () => true, isTexthookerOnlyMode: () => true,
setTexthookerOnlyMode: () => calls.push('set-mode'), setTexthookerOnlyMode: () => calls.push('set-mode'),
commandNeedsOverlayRuntime: () => true, commandNeedsOverlayRuntime: () => true,
ensureOverlayStartupPrereqs: () => calls.push('prereqs'),
startBackgroundWarmups: () => calls.push('warmups'), startBackgroundWarmups: () => calls.push('warmups'),
logInfo: (message) => calls.push(`log:${message}`), logInfo: (message) => calls.push(`log:${message}`),
}, },
@@ -24,6 +25,7 @@ test('cli command runtime handler applies precheck and forwards command with con
handler({ start: true } as never); handler({ start: true } as never);
assert.deepEqual(calls, [ assert.deepEqual(calls, [
'prereqs',
'set-mode', 'set-mode',
'log:Disabling texthooker-only mode after overlay/start command.', 'log:Disabling texthooker-only mode after overlay/start command.',
'warmups', 'warmups',