diff --git a/plugin/subminer/process.lua b/plugin/subminer/process.lua index 3d042ac..3c35055 100644 --- a/plugin/subminer/process.lua +++ b/plugin/subminer/process.lua @@ -372,12 +372,9 @@ function M.create(ctx) end) end + launch_overlay_with_retry(1) if texthooker_enabled then - ensure_texthooker_running(function() - launch_overlay_with_retry(1) - end) - else - launch_overlay_with_retry(1) + ensure_texthooker_running(function() end) end end @@ -481,31 +478,33 @@ function M.create(ctx) state.texthooker_running = false disarm_auto_play_ready_gate() - ensure_texthooker_running(function() - local start_args = build_command_args("start") - subminer_log("info", "process", "Starting overlay: " .. table.concat(start_args, " ")) + local start_args = build_command_args("start") + subminer_log("info", "process", "Starting overlay: " .. table.concat(start_args, " ")) - state.overlay_running = true - mp.command_native_async({ - name = "subprocess", - args = start_args, - playback_only = false, - capture_stdout = true, - capture_stderr = true, - }, function(success, result, error) - if not success or (result and result.status ~= 0) then - state.overlay_running = false - subminer_log( - "error", - "process", - "Overlay start failed: " .. (error or (result and result.stderr) or "unknown error") - ) - show_osd("Restart failed") - else - show_osd("Restarted successfully") - end - end) + state.overlay_running = true + mp.command_native_async({ + name = "subprocess", + args = start_args, + playback_only = false, + capture_stdout = true, + capture_stderr = true, + }, function(success, result, error) + if not success or (result and result.status ~= 0) then + state.overlay_running = false + subminer_log( + "error", + "process", + "Overlay start failed: " .. (error or (result and result.stderr) or "unknown error") + ) + show_osd("Restart failed") + else + show_osd("Restarted successfully") + end end) + + if opts.texthooker_enabled then + ensure_texthooker_running(function() end) + end end) end diff --git a/scripts/test-plugin-start-gate.lua b/scripts/test-plugin-start-gate.lua index 4471d25..5f45f83 100644 --- a/scripts/test-plugin-start-gate.lua +++ b/scripts/test-plugin-start-gate.lua @@ -344,6 +344,27 @@ local function count_start_calls(async_calls) return count 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) for _, call in ipairs(async_calls) do local args = call.args or {} @@ -643,6 +664,8 @@ do fire_event(recorded, "file-loaded") local start_call = find_start_call(recorded.async_calls) 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( call_has_arg(start_call, "--show-visible-overlay"), "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, "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( not has_property_set(recorded.property_sets, "pause", true), "auto-start visible overlay should not force pause without explicit pause-until-ready option" diff --git a/src/core/services/app-ready.test.ts b/src/core/services/app-ready.test.ts index c357f36..3b987bb 100644 --- a/src/core/services/app-ready.test.ts +++ b/src/core/services/app-ready.test.ts @@ -176,6 +176,22 @@ test('runAppReadyRuntime skips heavy startup when shouldSkipHeavyStartup returns 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 () => { const { deps, calls } = makeDeps({ startJellyfinRemoteSession: undefined, diff --git a/src/core/services/startup.ts b/src/core/services/startup.ts index 8043860..206647d 100644 --- a/src/core/services/startup.ts +++ b/src/core/services/startup.ts @@ -200,6 +200,12 @@ export async function runAppReadyRuntime(deps: AppReadyRuntimeDeps): Promise Boolean( - appState.initialArgs?.stats && - (appState.initialArgs?.statsCleanup || - appState.initialArgs?.statsBackground || - appState.initialArgs?.statsStop), + appState.initialArgs?.texthooker || + (appState.initialArgs?.stats && + (appState.initialArgs?.statsCleanup || + appState.initialArgs?.statsBackground || + appState.initialArgs?.statsStop)), ), shouldSkipHeavyStartup: () => Boolean( @@ -3130,6 +3131,39 @@ void initializeDiscordPresenceService(); const handleCliCommand = createCliCommandRuntimeHandler({ handleTexthookerOnlyModeTransitionMainDeps: { 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) => { appState.texthookerOnlyMode = enabled; }, diff --git a/src/main/runtime/cli-command-prechecks-main-deps.test.ts b/src/main/runtime/cli-command-prechecks-main-deps.test.ts index d11f00b..084eae2 100644 --- a/src/main/runtime/cli-command-prechecks-main-deps.test.ts +++ b/src/main/runtime/cli-command-prechecks-main-deps.test.ts @@ -8,6 +8,7 @@ test('cli prechecks main deps builder maps transition handlers', () => { isTexthookerOnlyMode: () => true, setTexthookerOnlyMode: (enabled) => calls.push(`set:${enabled}`), commandNeedsOverlayRuntime: () => true, + ensureOverlayStartupPrereqs: () => calls.push('prereqs'), startBackgroundWarmups: () => calls.push('warmups'), 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.commandNeedsOverlayRuntime({} as never), true); deps.setTexthookerOnlyMode(false); + deps.ensureOverlayStartupPrereqs(); deps.startBackgroundWarmups(); deps.logInfo('x'); - assert.deepEqual(calls, ['set:false', 'warmups', 'info:x']); + assert.deepEqual(calls, ['set:false', 'prereqs', 'warmups', 'info:x']); }); diff --git a/src/main/runtime/cli-command-prechecks-main-deps.ts b/src/main/runtime/cli-command-prechecks-main-deps.ts index ac3b88d..8541df3 100644 --- a/src/main/runtime/cli-command-prechecks-main-deps.ts +++ b/src/main/runtime/cli-command-prechecks-main-deps.ts @@ -4,6 +4,7 @@ export function createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler(dep isTexthookerOnlyMode: () => boolean; setTexthookerOnlyMode: (enabled: boolean) => void; commandNeedsOverlayRuntime: (args: CliArgs) => boolean; + ensureOverlayStartupPrereqs: () => void; startBackgroundWarmups: () => void; logInfo: (message: string) => void; }) { @@ -11,6 +12,7 @@ export function createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler(dep isTexthookerOnlyMode: () => deps.isTexthookerOnlyMode(), setTexthookerOnlyMode: (enabled: boolean) => deps.setTexthookerOnlyMode(enabled), commandNeedsOverlayRuntime: (args: CliArgs) => deps.commandNeedsOverlayRuntime(args), + ensureOverlayStartupPrereqs: () => deps.ensureOverlayStartupPrereqs(), startBackgroundWarmups: () => deps.startBackgroundWarmups(), logInfo: (message: string) => deps.logInfo(message), }); diff --git a/src/main/runtime/cli-command-prechecks.test.ts b/src/main/runtime/cli-command-prechecks.test.ts index 0541d11..5d8532f 100644 --- a/src/main/runtime/cli-command-prechecks.test.ts +++ b/src/main/runtime/cli-command-prechecks.test.ts @@ -8,6 +8,7 @@ test('texthooker precheck no-ops when mode is disabled', () => { isTexthookerOnlyMode: () => false, setTexthookerOnlyMode: () => {}, commandNeedsOverlayRuntime: () => true, + ensureOverlayStartupPrereqs: () => {}, startBackgroundWarmups: () => { warmups += 1; }, @@ -22,12 +23,16 @@ test('texthooker precheck disables mode and warms up on start command', () => { let mode = true; let warmups = 0; let logs = 0; + let prereqs = 0; const handlePrecheck = createHandleTexthookerOnlyModeTransitionHandler({ isTexthookerOnlyMode: () => mode, setTexthookerOnlyMode: (enabled) => { mode = enabled; }, commandNeedsOverlayRuntime: () => false, + ensureOverlayStartupPrereqs: () => { + prereqs += 1; + }, startBackgroundWarmups: () => { warmups += 1; }, @@ -38,6 +43,7 @@ test('texthooker precheck disables mode and warms up on start command', () => { handlePrecheck({ start: true, texthooker: false } as never); assert.equal(mode, false); + assert.equal(prereqs, 1); assert.equal(warmups, 1); assert.equal(logs, 1); }); @@ -50,6 +56,7 @@ test('texthooker precheck no-ops for texthooker command', () => { mode = enabled; }, commandNeedsOverlayRuntime: () => true, + ensureOverlayStartupPrereqs: () => {}, startBackgroundWarmups: () => {}, logInfo: () => {}, }); diff --git a/src/main/runtime/cli-command-prechecks.ts b/src/main/runtime/cli-command-prechecks.ts index ee51c1b..91ed8f6 100644 --- a/src/main/runtime/cli-command-prechecks.ts +++ b/src/main/runtime/cli-command-prechecks.ts @@ -4,6 +4,7 @@ export function createHandleTexthookerOnlyModeTransitionHandler(deps: { isTexthookerOnlyMode: () => boolean; setTexthookerOnlyMode: (enabled: boolean) => void; commandNeedsOverlayRuntime: (args: CliArgs) => boolean; + ensureOverlayStartupPrereqs: () => void; startBackgroundWarmups: () => void; logInfo: (message: string) => void; }) { @@ -13,6 +14,7 @@ export function createHandleTexthookerOnlyModeTransitionHandler(deps: { !args.texthooker && (args.start || deps.commandNeedsOverlayRuntime(args)) ) { + deps.ensureOverlayStartupPrereqs(); deps.setTexthookerOnlyMode(false); deps.logInfo('Disabling texthooker-only mode after overlay/start command.'); deps.startBackgroundWarmups(); diff --git a/src/main/runtime/cli-command-runtime-handler.test.ts b/src/main/runtime/cli-command-runtime-handler.test.ts index 45ae393..281b2ab 100644 --- a/src/main/runtime/cli-command-runtime-handler.test.ts +++ b/src/main/runtime/cli-command-runtime-handler.test.ts @@ -9,6 +9,7 @@ test('cli command runtime handler applies precheck and forwards command with con isTexthookerOnlyMode: () => true, setTexthookerOnlyMode: () => calls.push('set-mode'), commandNeedsOverlayRuntime: () => true, + ensureOverlayStartupPrereqs: () => calls.push('prereqs'), startBackgroundWarmups: () => calls.push('warmups'), 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); assert.deepEqual(calls, [ + 'prereqs', 'set-mode', 'log:Disabling texthooker-only mode after overlay/start command.', 'warmups',