fix: restore integrated texthooker startup

This commit is contained in:
2026-03-30 00:25:30 -07:00
parent 55b350c3a2
commit 8e5c21b443
20 changed files with 197 additions and 71 deletions

View File

@@ -0,0 +1,5 @@
type: fixed
area: main
- Keep integrated `--start --texthooker` launches on the full app-ready startup path so the texthooker page and websocket servers start together during normal playback startup.
- Stop the mpv/plugin auto-start flow from spawning a separate standalone texthooker helper during normal `subminer <video>` launches.

View File

@@ -191,6 +191,14 @@ function M.create(ctx)
else else
table.insert(args, "--hide-visible-overlay") table.insert(args, "--hide-visible-overlay")
end end
local texthooker_enabled = overrides.texthooker_enabled
if texthooker_enabled == nil then
texthooker_enabled = opts.texthooker_enabled
end
if texthooker_enabled then
table.insert(args, "--texthooker")
end
end end
return args return args
@@ -242,50 +250,10 @@ function M.create(ctx)
return overrides return overrides
end end
local function build_texthooker_args()
local args = { state.binary_path, "--texthooker", "--port", tostring(opts.texthooker_port) }
local log_level = normalize_log_level(opts.log_level)
if log_level ~= "info" then
table.insert(args, "--log-level")
table.insert(args, log_level)
end
return args
end
local function ensure_texthooker_running(callback) local function ensure_texthooker_running(callback)
if not opts.texthooker_enabled then if callback then
callback() callback()
return
end end
if state.texthooker_running then
callback()
return
end
local args = build_texthooker_args()
subminer_log("info", "texthooker", "Starting texthooker process: " .. table.concat(args, " "))
state.texthooker_running = true
mp.command_native_async({
name = "subprocess",
args = 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.texthooker_running = false
subminer_log(
"warn",
"texthooker",
"Texthooker process exited unexpectedly: " .. (error or (result and result.stderr) or "unknown error")
)
end
end)
-- Start overlay immediately; overlay start path retries on readiness failures.
callback()
end end
local function start_overlay(overrides) local function start_overlay(overrides)

View File

@@ -664,8 +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(call_has_arg(start_call, "--texthooker"), "auto-start should include --texthooker on the main --start command when enabled")
assert_true(texthooker_call ~= nil, "auto-start should issue texthooker helper command when enabled") assert_true(find_control_call(recorded.async_calls, "--texthooker") == nil, "auto-start should not issue a separate texthooker helper command")
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"
@@ -678,10 +678,6 @@ 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

@@ -5,6 +5,7 @@ import {
commandNeedsOverlayRuntime, commandNeedsOverlayRuntime,
hasExplicitCommand, hasExplicitCommand,
isHeadlessInitialCommand, isHeadlessInitialCommand,
isStandaloneTexthookerCommand,
parseArgs, parseArgs,
shouldRunSettingsOnlyStartup, shouldRunSettingsOnlyStartup,
shouldStartApp, shouldStartApp,
@@ -79,6 +80,14 @@ test('youtube playback does not use generic overlay-runtime bootstrap classifica
assert.equal(commandNeedsOverlayStartupPrereqs(args), true); assert.equal(commandNeedsOverlayStartupPrereqs(args), true);
}); });
test('standalone texthooker classification excludes integrated start flow', () => {
const standalone = parseArgs(['--texthooker']);
const integrated = parseArgs(['--start', '--texthooker']);
assert.equal(isStandaloneTexthookerCommand(standalone), true);
assert.equal(isStandaloneTexthookerCommand(integrated), 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

@@ -397,6 +397,54 @@ export function isHeadlessInitialCommand(args: CliArgs): boolean {
return args.refreshKnownWords; return args.refreshKnownWords;
} }
export function isStandaloneTexthookerCommand(args: CliArgs): boolean {
return (
args.texthooker &&
!args.background &&
!args.start &&
!Boolean(args.youtubePlay) &&
!args.launchMpv &&
!args.stop &&
!args.toggle &&
!args.toggleVisibleOverlay &&
!args.settings &&
!args.setup &&
!args.show &&
!args.hide &&
!args.showVisibleOverlay &&
!args.hideVisibleOverlay &&
!args.copySubtitle &&
!args.copySubtitleMultiple &&
!args.mineSentence &&
!args.mineSentenceMultiple &&
!args.updateLastCardFromClipboard &&
!args.refreshKnownWords &&
!args.toggleSecondarySub &&
!args.triggerFieldGrouping &&
!args.triggerSubsync &&
!args.markAudioCard &&
!args.openRuntimeOptions &&
!args.anilistStatus &&
!args.anilistLogout &&
!args.anilistSetup &&
!args.anilistRetryQueue &&
!args.dictionary &&
!args.stats &&
!args.jellyfin &&
!args.jellyfinLogin &&
!args.jellyfinLogout &&
!args.jellyfinLibraries &&
!args.jellyfinItems &&
!args.jellyfinSubtitles &&
!args.jellyfinPlay &&
!args.jellyfinRemoteAnnounce &&
!args.jellyfinPreviewAuth &&
!args.help &&
!args.autoStartOverlay &&
!args.generateConfig
);
}
export function shouldStartApp(args: CliArgs): boolean { export function shouldStartApp(args: CliArgs): boolean {
if (args.stop && !args.start) return false; if (args.stop && !args.start) return false;
if ( if (

View File

@@ -176,7 +176,7 @@ 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 () => { test('runAppReadyRuntime keeps websocket startup in texthooker-only mode but skips overlay window', async () => {
const { deps, calls } = makeDeps({ const { deps, calls } = makeDeps({
texthookerOnlyMode: true, texthookerOnlyMode: true,
reloadConfig: () => calls.push('reloadConfig'), reloadConfig: () => calls.push('reloadConfig'),
@@ -185,7 +185,16 @@ test('runAppReadyRuntime uses minimal startup for texthooker-only mode', async (
await runAppReadyRuntime(deps); await runAppReadyRuntime(deps);
assert.deepEqual(calls, ['ensureDefaultConfigBootstrap', 'reloadConfig', 'handleInitialArgs']); assert.ok(calls.includes('reloadConfig'));
assert.ok(calls.includes('createMpvClient'));
assert.ok(calls.includes('startAnnotationWebsocket:6678'));
assert.ok(calls.includes('startTexthooker:5174:ws://127.0.0.1:6678'));
assert.ok(calls.includes('createSubtitleTimingTracker'));
assert.ok(calls.includes('handleFirstRunSetup'));
assert.ok(calls.includes('handleInitialArgs'));
assert.ok(calls.includes('log:Texthooker-only mode enabled; skipping overlay window.'));
assert.equal(calls.includes('initializeOverlayRuntime'), false);
assert.equal(calls.includes('setVisibleOverlayVisible:true'), false);
}); });
test('runAppReadyRuntime skips Jellyfin remote startup when dependency is not wired', async () => { test('runAppReadyRuntime skips Jellyfin remote startup when dependency is not wired', async () => {

View File

@@ -62,6 +62,7 @@ function createDeps(overrides: Partial<CliCommandServiceDeps> = {}) {
let mpvSocketPath = '/tmp/subminer.sock'; let mpvSocketPath = '/tmp/subminer.sock';
let texthookerPort = 5174; let texthookerPort = 5174;
const osd: string[] = []; const osd: string[] = [];
let texthookerWebsocketUrl: string | undefined;
const deps: CliCommandServiceDeps = { const deps: CliCommandServiceDeps = {
getMpvSocketPath: () => mpvSocketPath, getMpvSocketPath: () => mpvSocketPath,
@@ -82,9 +83,10 @@ function createDeps(overrides: Partial<CliCommandServiceDeps> = {}) {
calls.push(`setTexthookerPort:${port}`); calls.push(`setTexthookerPort:${port}`);
}, },
getTexthookerPort: () => texthookerPort, getTexthookerPort: () => texthookerPort,
getTexthookerWebsocketUrl: () => texthookerWebsocketUrl,
shouldOpenTexthookerBrowser: () => true, shouldOpenTexthookerBrowser: () => true,
ensureTexthookerRunning: (port) => { ensureTexthookerRunning: (port, websocketUrl) => {
calls.push(`ensureTexthookerRunning:${port}`); calls.push(`ensureTexthookerRunning:${port}:${websocketUrl ?? ''}`);
}, },
openTexthookerInBrowser: (url) => { openTexthookerInBrowser: (url) => {
calls.push(`openTexthookerInBrowser:${url}`); calls.push(`openTexthookerInBrowser:${url}`);
@@ -354,10 +356,20 @@ test('handleCliCommand runs texthooker flow with browser open', () => {
handleCliCommand(args, 'initial', deps); handleCliCommand(args, 'initial', deps);
assert.ok(calls.includes('ensureTexthookerRunning:5174')); assert.ok(calls.includes('ensureTexthookerRunning:5174:'));
assert.ok(calls.includes('openTexthookerInBrowser:http://127.0.0.1:5174')); assert.ok(calls.includes('openTexthookerInBrowser:http://127.0.0.1:5174'));
}); });
test('handleCliCommand forwards resolved websocket url to texthooker startup', () => {
const { deps, calls } = createDeps({
getTexthookerWebsocketUrl: () => 'ws://127.0.0.1:6678',
});
handleCliCommand(makeArgs({ texthooker: true }), 'initial', deps);
assert.ok(calls.includes('ensureTexthookerRunning:5174:ws://127.0.0.1:6678'));
});
test('handleCliCommand reports async mine errors to OSD', async () => { test('handleCliCommand reports async mine errors to OSD', async () => {
const { deps, calls, osd } = createDeps({ const { deps, calls, osd } = createDeps({
mineSentenceCard: async () => { mineSentenceCard: async () => {

View File

@@ -10,8 +10,9 @@ export interface CliCommandServiceDeps {
isTexthookerRunning: () => boolean; isTexthookerRunning: () => boolean;
setTexthookerPort: (port: number) => void; setTexthookerPort: (port: number) => void;
getTexthookerPort: () => number; getTexthookerPort: () => number;
getTexthookerWebsocketUrl: () => string | undefined;
shouldOpenTexthookerBrowser: () => boolean; shouldOpenTexthookerBrowser: () => boolean;
ensureTexthookerRunning: (port: number) => void; ensureTexthookerRunning: (port: number, websocketUrl?: string) => void;
openTexthookerInBrowser: (url: string) => void; openTexthookerInBrowser: (url: string) => void;
stopApp: () => void; stopApp: () => void;
isOverlayRuntimeInitialized: () => boolean; isOverlayRuntimeInitialized: () => boolean;
@@ -84,7 +85,7 @@ interface MpvClientLike {
interface TexthookerServiceLike { interface TexthookerServiceLike {
isRunning: () => boolean; isRunning: () => boolean;
start: (port: number) => void; start: (port: number, websocketUrl?: string) => void;
} }
interface MpvCliRuntime { interface MpvCliRuntime {
@@ -98,6 +99,7 @@ interface TexthookerCliRuntime {
service: TexthookerServiceLike; service: TexthookerServiceLike;
getPort: () => number; getPort: () => number;
setPort: (port: number) => void; setPort: (port: number) => void;
getWebsocketUrl: () => string | undefined;
shouldOpenBrowser: () => boolean; shouldOpenBrowser: () => boolean;
openInBrowser: (url: string) => void; openInBrowser: (url: string) => void;
} }
@@ -194,10 +196,11 @@ export function createCliCommandDepsRuntime(
isTexthookerRunning: () => options.texthooker.service.isRunning(), isTexthookerRunning: () => options.texthooker.service.isRunning(),
setTexthookerPort: options.texthooker.setPort, setTexthookerPort: options.texthooker.setPort,
getTexthookerPort: options.texthooker.getPort, getTexthookerPort: options.texthooker.getPort,
getTexthookerWebsocketUrl: options.texthooker.getWebsocketUrl,
shouldOpenTexthookerBrowser: options.texthooker.shouldOpenBrowser, shouldOpenTexthookerBrowser: options.texthooker.shouldOpenBrowser,
ensureTexthookerRunning: (port) => { ensureTexthookerRunning: (port, websocketUrl) => {
if (!options.texthooker.service.isRunning()) { if (!options.texthooker.service.isRunning()) {
options.texthooker.service.start(port); options.texthooker.service.start(port, websocketUrl);
} }
}, },
openTexthookerInBrowser: options.texthooker.openInBrowser, openTexthookerInBrowser: options.texthooker.openInBrowser,
@@ -473,7 +476,7 @@ export function handleCliCommand(
); );
} else if (args.texthooker) { } else if (args.texthooker) {
const texthookerPort = deps.getTexthookerPort(); const texthookerPort = deps.getTexthookerPort();
deps.ensureTexthookerRunning(texthookerPort); deps.ensureTexthookerRunning(texthookerPort, deps.getTexthookerWebsocketUrl());
if (deps.shouldOpenTexthookerBrowser()) { if (deps.shouldOpenTexthookerBrowser()) {
deps.openTexthookerInBrowser(`http://127.0.0.1:${texthookerPort}`); deps.openTexthookerInBrowser(`http://127.0.0.1:${texthookerPort}`);
} }

View File

@@ -98,6 +98,13 @@ interface AppReadyConfigLike {
}; };
} }
type TexthookerWebsocketConfigLike = Pick<AppReadyConfigLike, 'annotationWebsocket' | 'websocket'>;
type TexthookerWebsocketDefaults = {
defaultWebsocketPort: number;
defaultAnnotationWebsocketPort: number;
};
export interface AppReadyRuntimeDeps { export interface AppReadyRuntimeDeps {
ensureDefaultConfigBootstrap: () => void; ensureDefaultConfigBootstrap: () => void;
loadSubtitlePosition: () => void; loadSubtitlePosition: () => void;
@@ -169,6 +176,29 @@ function getStartupCriticalConfigErrors(config: AppReadyConfigLike): string[] {
return errors; return errors;
} }
export function resolveTexthookerWebsocketUrl(
config: TexthookerWebsocketConfigLike,
defaults: TexthookerWebsocketDefaults,
hasMpvWebsocketPlugin: boolean,
): string | undefined {
const wsConfig = config.websocket || {};
const wsEnabled = wsConfig.enabled ?? 'auto';
const wsPort = wsConfig.port || defaults.defaultWebsocketPort;
const annotationWsConfig = config.annotationWebsocket || {};
const annotationWsEnabled = annotationWsConfig.enabled !== false;
const annotationWsPort = annotationWsConfig.port || defaults.defaultAnnotationWebsocketPort;
if (annotationWsEnabled) {
return `ws://127.0.0.1:${annotationWsPort}`;
}
if (wsEnabled === true || (wsEnabled === 'auto' && !hasMpvWebsocketPlugin)) {
return `ws://127.0.0.1:${wsPort}`;
}
return undefined;
}
export function shouldAutoInitializeOverlayRuntimeFromConfig(config: RuntimeConfigLike): boolean { export function shouldAutoInitializeOverlayRuntimeFromConfig(config: RuntimeConfigLike): boolean {
return config.auto_start_overlay === true; return config.auto_start_overlay === true;
} }
@@ -201,12 +231,6 @@ 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();
@@ -262,7 +286,14 @@ export async function runAppReadyRuntime(deps: AppReadyRuntimeDeps): Promise<voi
const annotationWsEnabled = annotationWsConfig.enabled !== false; const annotationWsEnabled = annotationWsConfig.enabled !== false;
const annotationWsPort = annotationWsConfig.port || deps.defaultAnnotationWebsocketPort; const annotationWsPort = annotationWsConfig.port || deps.defaultAnnotationWebsocketPort;
const texthookerPort = deps.defaultTexthookerPort; const texthookerPort = deps.defaultTexthookerPort;
let texthookerWebsocketUrl: string | undefined; const texthookerWebsocketUrl = resolveTexthookerWebsocketUrl(
config,
{
defaultWebsocketPort: deps.defaultWebsocketPort,
defaultAnnotationWebsocketPort: deps.defaultAnnotationWebsocketPort,
},
deps.hasMpvWebsocketPlugin(),
);
if (wsEnabled === true || (wsEnabled === 'auto' && !deps.hasMpvWebsocketPlugin())) { if (wsEnabled === true || (wsEnabled === 'auto' && !deps.hasMpvWebsocketPlugin())) {
deps.startSubtitleWebsocket(wsPort); deps.startSubtitleWebsocket(wsPort);
@@ -272,9 +303,6 @@ export async function runAppReadyRuntime(deps: AppReadyRuntimeDeps): Promise<voi
if (annotationWsEnabled) { if (annotationWsEnabled) {
deps.startAnnotationWebsocket(annotationWsPort); deps.startAnnotationWebsocket(annotationWsPort);
texthookerWebsocketUrl = `ws://127.0.0.1:${annotationWsPort}`;
} else if (wsEnabled === true || (wsEnabled === 'auto' && !deps.hasMpvWebsocketPlugin())) {
texthookerWebsocketUrl = `ws://127.0.0.1:${wsPort}`;
} }
if (config.texthooker?.launchAtStartup !== false) { if (config.texthooker?.launchAtStartup !== false) {

View File

@@ -75,7 +75,7 @@ function getStartupModeFlags(initialArgs: CliArgs | null | undefined): {
} { } {
return { return {
shouldUseMinimalStartup: Boolean( shouldUseMinimalStartup: Boolean(
initialArgs?.texthooker || (initialArgs && isStandaloneTexthookerCommand(initialArgs)) ||
(initialArgs?.stats && (initialArgs?.stats &&
(initialArgs.statsCleanup || initialArgs.statsBackground || initialArgs.statsStop)), (initialArgs.statsCleanup || initialArgs.statsBackground || initialArgs.statsStop)),
), ),
@@ -128,6 +128,7 @@ import {
commandNeedsOverlayStartupPrereqs, commandNeedsOverlayStartupPrereqs,
commandNeedsOverlayRuntime, commandNeedsOverlayRuntime,
isHeadlessInitialCommand, isHeadlessInitialCommand,
isStandaloneTexthookerCommand,
parseArgs, parseArgs,
shouldRunSettingsOnlyStartup, shouldRunSettingsOnlyStartup,
shouldStartApp, shouldStartApp,
@@ -4334,6 +4335,9 @@ const { handleCliCommand, handleInitialArgs } = composeCliStartupHandlers({
setLogLevel: (level) => setLogLevel(level, 'cli'), setLogLevel: (level) => setLogLevel(level, 'cli'),
texthookerService, texthookerService,
getResolvedConfig: () => getResolvedConfig(), getResolvedConfig: () => getResolvedConfig(),
defaultWebsocketPort: DEFAULT_CONFIG.websocket.port,
defaultAnnotationWebsocketPort: DEFAULT_CONFIG.annotationWebsocket.port,
hasMpvWebsocketPlugin: () => hasMpvWebsocketPlugin(),
openExternal: (url: string) => shell.openExternal(url), openExternal: (url: string) => shell.openExternal(url),
logBrowserOpenError: (url: string, error: unknown) => logBrowserOpenError: (url: string, error: unknown) =>
logger.error(`Failed to open browser for texthooker URL: ${url}`, error), logger.error(`Failed to open browser for texthooker URL: ${url}`, error),

View File

@@ -13,6 +13,7 @@ export interface CliCommandRuntimeServiceContext {
showOsd: CliCommandRuntimeServiceDepsParams['mpv']['showOsd']; showOsd: CliCommandRuntimeServiceDepsParams['mpv']['showOsd'];
getTexthookerPort: () => number; getTexthookerPort: () => number;
setTexthookerPort: (port: number) => void; setTexthookerPort: (port: number) => void;
getTexthookerWebsocketUrl: () => string | undefined;
shouldOpenBrowser: () => boolean; shouldOpenBrowser: () => boolean;
openInBrowser: (url: string) => void; openInBrowser: (url: string) => void;
isOverlayInitialized: () => boolean; isOverlayInitialized: () => boolean;
@@ -71,6 +72,7 @@ function createCliCommandDepsFromContext(
service: context.texthookerService, service: context.texthookerService,
getPort: context.getTexthookerPort, getPort: context.getTexthookerPort,
setPort: context.setTexthookerPort, setPort: context.setTexthookerPort,
getWebsocketUrl: context.getTexthookerWebsocketUrl,
shouldOpenBrowser: context.shouldOpenBrowser, shouldOpenBrowser: context.shouldOpenBrowser,
openInBrowser: context.openInBrowser, openInBrowser: context.openInBrowser,
}, },

View File

@@ -132,6 +132,7 @@ export interface CliCommandRuntimeServiceDepsParams {
service: CliCommandDepsRuntimeOptions['texthooker']['service']; service: CliCommandDepsRuntimeOptions['texthooker']['service'];
getPort: CliCommandDepsRuntimeOptions['texthooker']['getPort']; getPort: CliCommandDepsRuntimeOptions['texthooker']['getPort'];
setPort: CliCommandDepsRuntimeOptions['texthooker']['setPort']; setPort: CliCommandDepsRuntimeOptions['texthooker']['setPort'];
getWebsocketUrl: CliCommandDepsRuntimeOptions['texthooker']['getWebsocketUrl'];
shouldOpenBrowser: CliCommandDepsRuntimeOptions['texthooker']['shouldOpenBrowser']; shouldOpenBrowser: CliCommandDepsRuntimeOptions['texthooker']['shouldOpenBrowser'];
openInBrowser: CliCommandDepsRuntimeOptions['texthooker']['openInBrowser']; openInBrowser: CliCommandDepsRuntimeOptions['texthooker']['openInBrowser'];
}; };
@@ -293,6 +294,7 @@ export function createCliCommandRuntimeServiceDeps(
service: params.texthooker.service, service: params.texthooker.service,
getPort: params.texthooker.getPort, getPort: params.texthooker.getPort,
setPort: params.texthooker.setPort, setPort: params.texthooker.setPort,
getWebsocketUrl: params.texthooker.getWebsocketUrl,
shouldOpenBrowser: params.texthooker.shouldOpenBrowser, shouldOpenBrowser: params.texthooker.shouldOpenBrowser,
openInBrowser: params.texthooker.openInBrowser, openInBrowser: params.texthooker.openInBrowser,
}, },

View File

@@ -12,6 +12,7 @@ test('build cli command context deps maps handlers and values', () => {
texthookerService: { start: () => null, status: () => ({ running: false }) } as never, texthookerService: { start: () => null, status: () => ({ running: false }) } as never,
getTexthookerPort: () => 5174, getTexthookerPort: () => 5174,
setTexthookerPort: (port) => calls.push(`port:${port}`), setTexthookerPort: (port) => calls.push(`port:${port}`),
getTexthookerWebsocketUrl: () => 'ws://127.0.0.1:6678',
shouldOpenBrowser: () => true, shouldOpenBrowser: () => true,
openExternal: async (url) => calls.push(`open:${url}`), openExternal: async (url) => calls.push(`open:${url}`),
logBrowserOpenError: (url) => calls.push(`open-error:${url}`), logBrowserOpenError: (url) => calls.push(`open-error:${url}`),
@@ -82,6 +83,7 @@ test('build cli command context deps maps handlers and values', () => {
const deps = buildDeps(); const deps = buildDeps();
assert.equal(deps.getSocketPath(), '/tmp/mpv.sock'); assert.equal(deps.getSocketPath(), '/tmp/mpv.sock');
assert.equal(deps.getTexthookerPort(), 5174); assert.equal(deps.getTexthookerPort(), 5174);
assert.equal(deps.getTexthookerWebsocketUrl(), 'ws://127.0.0.1:6678');
assert.equal(deps.shouldOpenBrowser(), true); assert.equal(deps.shouldOpenBrowser(), true);
assert.equal(deps.isOverlayInitialized(), true); assert.equal(deps.isOverlayInitialized(), true);
assert.equal(deps.hasMainWindow(), true); assert.equal(deps.hasMainWindow(), true);

View File

@@ -10,6 +10,7 @@ export function createBuildCliCommandContextDepsHandler(deps: {
texthookerService: CliCommandContextFactoryDeps['texthookerService']; texthookerService: CliCommandContextFactoryDeps['texthookerService'];
getTexthookerPort: () => number; getTexthookerPort: () => number;
setTexthookerPort: (port: number) => void; setTexthookerPort: (port: number) => void;
getTexthookerWebsocketUrl: () => string | undefined;
shouldOpenBrowser: () => boolean; shouldOpenBrowser: () => boolean;
openExternal: (url: string) => Promise<unknown>; openExternal: (url: string) => Promise<unknown>;
logBrowserOpenError: (url: string, error: unknown) => void; logBrowserOpenError: (url: string, error: unknown) => void;
@@ -58,6 +59,7 @@ export function createBuildCliCommandContextDepsHandler(deps: {
texthookerService: deps.texthookerService, texthookerService: deps.texthookerService,
getTexthookerPort: deps.getTexthookerPort, getTexthookerPort: deps.getTexthookerPort,
setTexthookerPort: deps.setTexthookerPort, setTexthookerPort: deps.setTexthookerPort,
getTexthookerWebsocketUrl: deps.getTexthookerWebsocketUrl,
shouldOpenBrowser: deps.shouldOpenBrowser, shouldOpenBrowser: deps.shouldOpenBrowser,
openExternal: deps.openExternal, openExternal: deps.openExternal,
logBrowserOpenError: deps.logBrowserOpenError, logBrowserOpenError: deps.logBrowserOpenError,

View File

@@ -14,7 +14,13 @@ test('cli command context factory composes main deps and context handlers', () =
const createContext = createCliCommandContextFactory({ const createContext = createCliCommandContextFactory({
appState, appState,
texthookerService: { isRunning: () => false, start: () => null }, texthookerService: { isRunning: () => false, start: () => null },
getResolvedConfig: () => ({ texthooker: { openBrowser: true } }), getResolvedConfig: () => ({
texthooker: { openBrowser: true },
annotationWebsocket: { enabled: true, port: 6678 },
}),
defaultWebsocketPort: 6677,
defaultAnnotationWebsocketPort: 6678,
hasMpvWebsocketPlugin: () => false,
openExternal: async () => {}, openExternal: async () => {},
logBrowserOpenError: () => {}, logBrowserOpenError: () => {},
showMpvOsd: (text) => calls.push(`osd:${text}`), showMpvOsd: (text) => calls.push(`osd:${text}`),

View File

@@ -14,7 +14,13 @@ test('cli command context main deps builder maps state and callbacks', async ()
const build = createBuildCliCommandContextMainDepsHandler({ const build = createBuildCliCommandContextMainDepsHandler({
appState, appState,
texthookerService: { isRunning: () => false, start: () => null }, texthookerService: { isRunning: () => false, start: () => null },
getResolvedConfig: () => ({ texthooker: { openBrowser: true } }), getResolvedConfig: () => ({
texthooker: { openBrowser: true },
annotationWebsocket: { enabled: true, port: 6678 },
}),
defaultWebsocketPort: 6677,
defaultAnnotationWebsocketPort: 6678,
hasMpvWebsocketPlugin: () => false,
openExternal: async (url) => { openExternal: async (url) => {
calls.push(`open:${url}`); calls.push(`open:${url}`);
}, },
@@ -110,6 +116,7 @@ test('cli command context main deps builder maps state and callbacks', async ()
assert.equal(deps.getTexthookerPort(), 5174); assert.equal(deps.getTexthookerPort(), 5174);
deps.setTexthookerPort(5175); deps.setTexthookerPort(5175);
assert.equal(appState.texthookerPort, 5175); assert.equal(appState.texthookerPort, 5175);
assert.equal(deps.getTexthookerWebsocketUrl(), 'ws://127.0.0.1:6678');
assert.equal(deps.shouldOpenBrowser(), true); assert.equal(deps.shouldOpenBrowser(), true);
deps.showOsd('hello'); deps.showOsd('hello');
deps.initializeOverlay(); deps.initializeOverlay();

View File

@@ -1,4 +1,5 @@
import type { CliArgs } from '../../cli/args'; import type { CliArgs } from '../../cli/args';
import { resolveTexthookerWebsocketUrl } from '../../core/services/startup';
import type { CliCommandContextFactoryDeps } from './cli-command-context'; import type { CliCommandContextFactoryDeps } from './cli-command-context';
type CliCommandContextMainState = { type CliCommandContextMainState = {
@@ -12,7 +13,14 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
appState: CliCommandContextMainState; appState: CliCommandContextMainState;
setLogLevel?: (level: NonNullable<CliArgs['logLevel']>) => void; setLogLevel?: (level: NonNullable<CliArgs['logLevel']>) => void;
texthookerService: CliCommandContextFactoryDeps['texthookerService']; texthookerService: CliCommandContextFactoryDeps['texthookerService'];
getResolvedConfig: () => { texthooker?: { openBrowser?: boolean } }; getResolvedConfig: () => {
texthooker?: { openBrowser?: boolean };
websocket?: { enabled?: boolean | 'auto'; port?: number };
annotationWebsocket?: { enabled?: boolean; port?: number };
};
defaultWebsocketPort: number;
defaultAnnotationWebsocketPort: number;
hasMpvWebsocketPlugin: () => boolean;
openExternal: (url: string) => Promise<unknown>; openExternal: (url: string) => Promise<unknown>;
logBrowserOpenError: (url: string, error: unknown) => void; logBrowserOpenError: (url: string, error: unknown) => void;
showMpvOsd: (text: string) => void; showMpvOsd: (text: string) => void;
@@ -68,6 +76,15 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
setTexthookerPort: (port: number) => { setTexthookerPort: (port: number) => {
deps.appState.texthookerPort = port; deps.appState.texthookerPort = port;
}, },
getTexthookerWebsocketUrl: () =>
resolveTexthookerWebsocketUrl(
deps.getResolvedConfig(),
{
defaultWebsocketPort: deps.defaultWebsocketPort,
defaultAnnotationWebsocketPort: deps.defaultAnnotationWebsocketPort,
},
deps.hasMpvWebsocketPlugin(),
),
shouldOpenBrowser: () => deps.getResolvedConfig().texthooker?.openBrowser !== false, shouldOpenBrowser: () => deps.getResolvedConfig().texthooker?.openBrowser !== false,
openExternal: (url: string) => deps.openExternal(url), openExternal: (url: string) => deps.openExternal(url),
logBrowserOpenError: (url: string, error: unknown) => deps.logBrowserOpenError(url, error), logBrowserOpenError: (url: string, error: unknown) => deps.logBrowserOpenError(url, error),

View File

@@ -18,6 +18,7 @@ function createDeps() {
texthookerService: {} as never, texthookerService: {} as never,
getTexthookerPort: () => 6677, getTexthookerPort: () => 6677,
setTexthookerPort: () => {}, setTexthookerPort: () => {},
getTexthookerWebsocketUrl: () => 'ws://127.0.0.1:6678',
shouldOpenBrowser: () => true, shouldOpenBrowser: () => true,
openExternal: async () => {}, openExternal: async () => {},
logBrowserOpenError: (url: string) => browserErrors.push(url), logBrowserOpenError: (url: string) => browserErrors.push(url),

View File

@@ -15,6 +15,7 @@ export type CliCommandContextFactoryDeps = {
texthookerService: CliCommandRuntimeServiceContextHandlers['texthookerService']; texthookerService: CliCommandRuntimeServiceContextHandlers['texthookerService'];
getTexthookerPort: () => number; getTexthookerPort: () => number;
setTexthookerPort: (port: number) => void; setTexthookerPort: (port: number) => void;
getTexthookerWebsocketUrl: () => string | undefined;
shouldOpenBrowser: () => boolean; shouldOpenBrowser: () => boolean;
openExternal: (url: string) => Promise<unknown>; openExternal: (url: string) => Promise<unknown>;
logBrowserOpenError: (url: string, error: unknown) => void; logBrowserOpenError: (url: string, error: unknown) => void;
@@ -67,6 +68,7 @@ export function createCliCommandContext(
texthookerService: deps.texthookerService, texthookerService: deps.texthookerService,
getTexthookerPort: deps.getTexthookerPort, getTexthookerPort: deps.getTexthookerPort,
setTexthookerPort: deps.setTexthookerPort, setTexthookerPort: deps.setTexthookerPort,
getTexthookerWebsocketUrl: deps.getTexthookerWebsocketUrl,
shouldOpenBrowser: deps.shouldOpenBrowser, shouldOpenBrowser: deps.shouldOpenBrowser,
openInBrowser: (url: string) => { openInBrowser: (url: string) => {
void deps.openExternal(url).catch((error) => { void deps.openExternal(url).catch((error) => {

View File

@@ -11,6 +11,9 @@ test('composeCliStartupHandlers returns callable CLI startup handlers', () => {
setLogLevel: () => {}, setLogLevel: () => {},
texthookerService: {} as never, texthookerService: {} as never,
getResolvedConfig: () => ({}) as never, getResolvedConfig: () => ({}) as never,
defaultWebsocketPort: 6677,
defaultAnnotationWebsocketPort: 6678,
hasMpvWebsocketPlugin: () => false,
openExternal: async () => {}, openExternal: async () => {},
logBrowserOpenError: () => {}, logBrowserOpenError: () => {},
showMpvOsd: () => {}, showMpvOsd: () => {},