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
table.insert(args, "--hide-visible-overlay")
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
return args
@@ -242,50 +250,10 @@ function M.create(ctx)
return overrides
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)
if not opts.texthooker_enabled then
if callback then
callback()
return
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
local function start_overlay(overrides)

View File

@@ -664,8 +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, "--texthooker"), "auto-start should include --texthooker on the main --start 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(
call_has_arg(start_call, "--show-visible-overlay"),
"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,
"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"

View File

@@ -5,6 +5,7 @@ import {
commandNeedsOverlayRuntime,
hasExplicitCommand,
isHeadlessInitialCommand,
isStandaloneTexthookerCommand,
parseArgs,
shouldRunSettingsOnlyStartup,
shouldStartApp,
@@ -79,6 +80,14 @@ test('youtube playback does not use generic overlay-runtime bootstrap classifica
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', () => {
const args = parseArgs([
'--jellyfin-items',

View File

@@ -397,6 +397,54 @@ export function isHeadlessInitialCommand(args: CliArgs): boolean {
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 {
if (args.stop && !args.start) return false;
if (

View File

@@ -176,7 +176,7 @@ 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 () => {
test('runAppReadyRuntime keeps websocket startup in texthooker-only mode but skips overlay window', async () => {
const { deps, calls } = makeDeps({
texthookerOnlyMode: true,
reloadConfig: () => calls.push('reloadConfig'),
@@ -185,7 +185,16 @@ test('runAppReadyRuntime uses minimal startup for texthooker-only mode', async (
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 () => {

View File

@@ -62,6 +62,7 @@ function createDeps(overrides: Partial<CliCommandServiceDeps> = {}) {
let mpvSocketPath = '/tmp/subminer.sock';
let texthookerPort = 5174;
const osd: string[] = [];
let texthookerWebsocketUrl: string | undefined;
const deps: CliCommandServiceDeps = {
getMpvSocketPath: () => mpvSocketPath,
@@ -82,9 +83,10 @@ function createDeps(overrides: Partial<CliCommandServiceDeps> = {}) {
calls.push(`setTexthookerPort:${port}`);
},
getTexthookerPort: () => texthookerPort,
getTexthookerWebsocketUrl: () => texthookerWebsocketUrl,
shouldOpenTexthookerBrowser: () => true,
ensureTexthookerRunning: (port) => {
calls.push(`ensureTexthookerRunning:${port}`);
ensureTexthookerRunning: (port, websocketUrl) => {
calls.push(`ensureTexthookerRunning:${port}:${websocketUrl ?? ''}`);
},
openTexthookerInBrowser: (url) => {
calls.push(`openTexthookerInBrowser:${url}`);
@@ -354,10 +356,20 @@ test('handleCliCommand runs texthooker flow with browser open', () => {
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'));
});
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 () => {
const { deps, calls, osd } = createDeps({
mineSentenceCard: async () => {

View File

@@ -10,8 +10,9 @@ export interface CliCommandServiceDeps {
isTexthookerRunning: () => boolean;
setTexthookerPort: (port: number) => void;
getTexthookerPort: () => number;
getTexthookerWebsocketUrl: () => string | undefined;
shouldOpenTexthookerBrowser: () => boolean;
ensureTexthookerRunning: (port: number) => void;
ensureTexthookerRunning: (port: number, websocketUrl?: string) => void;
openTexthookerInBrowser: (url: string) => void;
stopApp: () => void;
isOverlayRuntimeInitialized: () => boolean;
@@ -84,7 +85,7 @@ interface MpvClientLike {
interface TexthookerServiceLike {
isRunning: () => boolean;
start: (port: number) => void;
start: (port: number, websocketUrl?: string) => void;
}
interface MpvCliRuntime {
@@ -98,6 +99,7 @@ interface TexthookerCliRuntime {
service: TexthookerServiceLike;
getPort: () => number;
setPort: (port: number) => void;
getWebsocketUrl: () => string | undefined;
shouldOpenBrowser: () => boolean;
openInBrowser: (url: string) => void;
}
@@ -194,10 +196,11 @@ export function createCliCommandDepsRuntime(
isTexthookerRunning: () => options.texthooker.service.isRunning(),
setTexthookerPort: options.texthooker.setPort,
getTexthookerPort: options.texthooker.getPort,
getTexthookerWebsocketUrl: options.texthooker.getWebsocketUrl,
shouldOpenTexthookerBrowser: options.texthooker.shouldOpenBrowser,
ensureTexthookerRunning: (port) => {
ensureTexthookerRunning: (port, websocketUrl) => {
if (!options.texthooker.service.isRunning()) {
options.texthooker.service.start(port);
options.texthooker.service.start(port, websocketUrl);
}
},
openTexthookerInBrowser: options.texthooker.openInBrowser,
@@ -473,7 +476,7 @@ export function handleCliCommand(
);
} else if (args.texthooker) {
const texthookerPort = deps.getTexthookerPort();
deps.ensureTexthookerRunning(texthookerPort);
deps.ensureTexthookerRunning(texthookerPort, deps.getTexthookerWebsocketUrl());
if (deps.shouldOpenTexthookerBrowser()) {
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 {
ensureDefaultConfigBootstrap: () => void;
loadSubtitlePosition: () => void;
@@ -169,6 +176,29 @@ function getStartupCriticalConfigErrors(config: AppReadyConfigLike): string[] {
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 {
return config.auto_start_overlay === true;
}
@@ -201,12 +231,6 @@ export async function runAppReadyRuntime(deps: AppReadyRuntimeDeps): Promise<voi
return;
}
if (deps.texthookerOnlyMode) {
deps.reloadConfig();
deps.handleInitialArgs();
return;
}
if (deps.shouldUseMinimalStartup?.()) {
deps.reloadConfig();
deps.handleInitialArgs();
@@ -262,7 +286,14 @@ export async function runAppReadyRuntime(deps: AppReadyRuntimeDeps): Promise<voi
const annotationWsEnabled = annotationWsConfig.enabled !== false;
const annotationWsPort = annotationWsConfig.port || deps.defaultAnnotationWebsocketPort;
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())) {
deps.startSubtitleWebsocket(wsPort);
@@ -272,9 +303,6 @@ export async function runAppReadyRuntime(deps: AppReadyRuntimeDeps): Promise<voi
if (annotationWsEnabled) {
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) {

View File

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

View File

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

View File

@@ -132,6 +132,7 @@ export interface CliCommandRuntimeServiceDepsParams {
service: CliCommandDepsRuntimeOptions['texthooker']['service'];
getPort: CliCommandDepsRuntimeOptions['texthooker']['getPort'];
setPort: CliCommandDepsRuntimeOptions['texthooker']['setPort'];
getWebsocketUrl: CliCommandDepsRuntimeOptions['texthooker']['getWebsocketUrl'];
shouldOpenBrowser: CliCommandDepsRuntimeOptions['texthooker']['shouldOpenBrowser'];
openInBrowser: CliCommandDepsRuntimeOptions['texthooker']['openInBrowser'];
};
@@ -293,6 +294,7 @@ export function createCliCommandRuntimeServiceDeps(
service: params.texthooker.service,
getPort: params.texthooker.getPort,
setPort: params.texthooker.setPort,
getWebsocketUrl: params.texthooker.getWebsocketUrl,
shouldOpenBrowser: params.texthooker.shouldOpenBrowser,
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,
getTexthookerPort: () => 5174,
setTexthookerPort: (port) => calls.push(`port:${port}`),
getTexthookerWebsocketUrl: () => 'ws://127.0.0.1:6678',
shouldOpenBrowser: () => true,
openExternal: async (url) => calls.push(`open:${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();
assert.equal(deps.getSocketPath(), '/tmp/mpv.sock');
assert.equal(deps.getTexthookerPort(), 5174);
assert.equal(deps.getTexthookerWebsocketUrl(), 'ws://127.0.0.1:6678');
assert.equal(deps.shouldOpenBrowser(), true);
assert.equal(deps.isOverlayInitialized(), true);
assert.equal(deps.hasMainWindow(), true);

View File

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

View File

@@ -14,7 +14,13 @@ test('cli command context factory composes main deps and context handlers', () =
const createContext = createCliCommandContextFactory({
appState,
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 () => {},
logBrowserOpenError: () => {},
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({
appState,
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) => {
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);
deps.setTexthookerPort(5175);
assert.equal(appState.texthookerPort, 5175);
assert.equal(deps.getTexthookerWebsocketUrl(), 'ws://127.0.0.1:6678');
assert.equal(deps.shouldOpenBrowser(), true);
deps.showOsd('hello');
deps.initializeOverlay();

View File

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

View File

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

View File

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

View File

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