feat: open texthooker from cli and tray

This commit is contained in:
2026-05-02 19:37:44 -07:00
parent 13e2b5f8c8
commit 3a67e23bc3
30 changed files with 210 additions and 8 deletions
+3
View File
@@ -124,9 +124,12 @@ test('youtube playback does not use generic overlay-runtime bootstrap classifica
test('standalone texthooker classification excludes integrated start flow', () => {
const standalone = parseArgs(['--texthooker']);
const standaloneOpenBrowser = parseArgs(['--texthooker', '--open-browser']);
const integrated = parseArgs(['--start', '--texthooker']);
assert.equal(isStandaloneTexthookerCommand(standalone), true);
assert.equal(standaloneOpenBrowser.texthookerOpenBrowser, true);
assert.equal(isStandaloneTexthookerCommand(standaloneOpenBrowser), true);
assert.equal(isStandaloneTexthookerCommand(integrated), false);
});
+3
View File
@@ -71,6 +71,7 @@ export interface CliArgs {
jellyfinRemoteAnnounce: boolean;
jellyfinPreviewAuth: boolean;
texthooker: boolean;
texthookerOpenBrowser: boolean;
help: boolean;
autoStartOverlay: boolean;
generateConfig: boolean;
@@ -164,6 +165,7 @@ export function parseArgs(argv: string[]): CliArgs {
jellyfinRemoteAnnounce: false,
jellyfinPreviewAuth: false,
texthooker: false,
texthookerOpenBrowser: false,
help: false,
autoStartOverlay: false,
generateConfig: false,
@@ -327,6 +329,7 @@ export function parseArgs(argv: string[]): CliArgs {
else if (arg === '--jellyfin-remote-announce') args.jellyfinRemoteAnnounce = true;
else if (arg === '--jellyfin-preview-auth') args.jellyfinPreviewAuth = true;
else if (arg === '--texthooker') args.texthooker = true;
else if (arg === '--open-browser') args.texthookerOpenBrowser = true;
else if (arg === '--auto-start-overlay') args.autoStartOverlay = true;
else if (arg === '--generate-config') args.generateConfig = true;
else if (arg === '--backup-overwrite') args.backupOverwrite = true;
+1
View File
@@ -19,6 +19,7 @@ test('printHelp includes configured texthooker port', () => {
assert.match(output, /default: 7777/);
assert.match(output, /--launch-mpv.*Launch mpv with SubMiner defaults and exit/);
assert.match(output, /--stats\s+Open the stats dashboard in your browser/);
assert.match(output, /--open-browser\s+Open texthooker in your default browser/);
assert.doesNotMatch(output, /--refresh-known-words/);
assert.match(output, /--setup\s+Open first-run setup window/);
assert.match(output, /--anilist-status/);
+1
View File
@@ -16,6 +16,7 @@ ${B}Session${R}
--stop Stop the running instance
--stats Open the stats dashboard in your browser
--texthooker Start texthooker server only ${D}(no overlay)${R}
--open-browser Open texthooker in your default browser
${B}Overlay${R}
--toggle-visible-overlay Toggle subtitle overlay
+1
View File
@@ -66,6 +66,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
jellyfinRemoteAnnounce: false,
jellyfinPreviewAuth: false,
texthooker: false,
texthookerOpenBrowser: false,
help: false,
autoStartOverlay: false,
generateConfig: false,
+16
View File
@@ -68,6 +68,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
jellyfinRemoteAnnounce: false,
jellyfinPreviewAuth: false,
texthooker: false,
texthookerOpenBrowser: false,
help: false,
autoStartOverlay: false,
generateConfig: false,
@@ -399,6 +400,21 @@ test('handleCliCommand runs texthooker flow with browser open', () => {
assert.ok(calls.includes('openTexthookerInBrowser:http://127.0.0.1:5174'));
});
test('handleCliCommand opens texthooker browser when requested even if config disables auto-open', () => {
const { deps, calls } = createDeps({
shouldOpenTexthookerBrowser: () => false,
});
const args = {
...makeArgs({ texthooker: true }),
texthookerOpenBrowser: true,
} as CliArgs;
handleCliCommand(args, 'initial', deps);
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',
+1 -1
View File
@@ -704,7 +704,7 @@ export function handleCliCommand(
} else if (args.texthooker) {
const texthookerPort = deps.getTexthookerPort();
deps.ensureTexthookerRunning(texthookerPort, deps.getTexthookerWebsocketUrl());
if (deps.shouldOpenTexthookerBrowser()) {
if (args.texthookerOpenBrowser || deps.shouldOpenTexthookerBrowser()) {
deps.openTexthookerInBrowser(`http://127.0.0.1:${texthookerPort}`);
}
deps.log(`Texthooker available at http://127.0.0.1:${texthookerPort}`);
@@ -66,6 +66,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
jellyfinRemoteAnnounce: false,
jellyfinPreviewAuth: false,
texthooker: false,
texthookerOpenBrowser: false,
help: false,
autoStartOverlay: false,
generateConfig: false,
+2
View File
@@ -5150,6 +5150,8 @@ const { ensureTray: ensureTrayHandler, destroyTray: destroyTrayHandler } =
initializeOverlayRuntime: () => initializeOverlayRuntime(),
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
openSessionHelpModal: () => openSessionHelpOverlay(),
openTexthookerInBrowser: () =>
handleCliCommand(parseArgs(['--texthooker', '--open-browser'])),
showFirstRunSetup: () => !firstRunSetupService.isSetupCompleted(),
openFirstRunSetupWindow: () => openFirstRunSetupWindow(),
showWindowsMpvLauncherSetup: () => process.platform === 'win32',
@@ -80,6 +80,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
jellyfinRemoteAnnounce: false,
jellyfinPreviewAuth: false,
texthooker: false,
texthookerOpenBrowser: false,
help: false,
autoStartOverlay: false,
generateConfig: false,
@@ -42,6 +42,7 @@ test('build tray template handler wires actions and init guards', () => {
const buildTemplate = createBuildTrayMenuTemplateHandler({
buildTrayMenuTemplateRuntime: (handlers) => {
handlers.openSessionHelp();
handlers.openTexthookerInBrowser();
handlers.openFirstRunSetup();
handlers.openWindowsMpvLauncherSetup();
handlers.openYomitanSettings();
@@ -57,6 +58,7 @@ test('build tray template handler wires actions and init guards', () => {
},
isOverlayRuntimeInitialized: () => initialized,
openSessionHelpModal: () => calls.push('help'),
openTexthookerInBrowser: () => calls.push('texthooker'),
showFirstRunSetup: () => true,
openFirstRunSetupWindow: () => calls.push('setup'),
showWindowsMpvLauncherSetup: () => true,
@@ -72,6 +74,7 @@ test('build tray template handler wires actions and init guards', () => {
assert.deepEqual(calls, [
'init',
'help',
'texthooker',
'setup',
'setup',
'yomitan',
+5
View File
@@ -29,6 +29,7 @@ export function createResolveTrayIconPathHandler(deps: {
export function createBuildTrayMenuTemplateHandler<TMenuItem>(deps: {
buildTrayMenuTemplateRuntime: (handlers: {
openSessionHelp: () => void;
openTexthookerInBrowser: () => void;
openFirstRunSetup: () => void;
showFirstRunSetup: boolean;
openWindowsMpvLauncherSetup: () => void;
@@ -42,6 +43,7 @@ export function createBuildTrayMenuTemplateHandler<TMenuItem>(deps: {
initializeOverlayRuntime: () => void;
isOverlayRuntimeInitialized: () => boolean;
openSessionHelpModal: () => void;
openTexthookerInBrowser: () => void;
showFirstRunSetup: () => boolean;
openFirstRunSetupWindow: () => void;
showWindowsMpvLauncherSetup: () => boolean;
@@ -59,6 +61,9 @@ export function createBuildTrayMenuTemplateHandler<TMenuItem>(deps: {
}
deps.openSessionHelpModal();
},
openTexthookerInBrowser: () => {
deps.openTexthookerInBrowser();
},
openFirstRunSetup: () => {
deps.openFirstRunSetupWindow();
},
+2
View File
@@ -25,6 +25,7 @@ test('tray main deps builders return mapped handlers', () => {
initializeOverlayRuntime: () => calls.push('init'),
isOverlayRuntimeInitialized: () => false,
openSessionHelpModal: () => calls.push('help'),
openTexthookerInBrowser: () => calls.push('texthooker'),
showFirstRunSetup: () => true,
openFirstRunSetupWindow: () => calls.push('setup'),
showWindowsMpvLauncherSetup: () => true,
@@ -37,6 +38,7 @@ test('tray main deps builders return mapped handlers', () => {
const template = menuDeps.buildTrayMenuTemplateRuntime({
openSessionHelp: () => calls.push('open-help'),
openTexthookerInBrowser: () => calls.push('open-texthooker'),
openFirstRunSetup: () => calls.push('open-setup'),
showFirstRunSetup: true,
openWindowsMpvLauncherSetup: () => calls.push('open-windows-mpv'),
+3
View File
@@ -28,6 +28,7 @@ export function createBuildResolveTrayIconPathMainDepsHandler(deps: {
export function createBuildTrayMenuTemplateMainDepsHandler<TMenuItem>(deps: {
buildTrayMenuTemplateRuntime: (handlers: {
openSessionHelp: () => void;
openTexthookerInBrowser: () => void;
openFirstRunSetup: () => void;
showFirstRunSetup: boolean;
openWindowsMpvLauncherSetup: () => void;
@@ -41,6 +42,7 @@ export function createBuildTrayMenuTemplateMainDepsHandler<TMenuItem>(deps: {
initializeOverlayRuntime: () => void;
isOverlayRuntimeInitialized: () => boolean;
openSessionHelpModal: () => void;
openTexthookerInBrowser: () => void;
showFirstRunSetup: () => boolean;
openFirstRunSetupWindow: () => void;
showWindowsMpvLauncherSetup: () => boolean;
@@ -55,6 +57,7 @@ export function createBuildTrayMenuTemplateMainDepsHandler<TMenuItem>(deps: {
initializeOverlayRuntime: deps.initializeOverlayRuntime,
isOverlayRuntimeInitialized: deps.isOverlayRuntimeInitialized,
openSessionHelpModal: deps.openSessionHelpModal,
openTexthookerInBrowser: deps.openTexthookerInBrowser,
showFirstRunSetup: deps.showFirstRunSetup,
openFirstRunSetupWindow: deps.openFirstRunSetupWindow,
showWindowsMpvLauncherSetup: deps.showWindowsMpvLauncherSetup,
@@ -25,6 +25,7 @@ test('tray runtime handlers compose resolve/menu/ensure/destroy handlers', () =>
},
isOverlayRuntimeInitialized: () => overlayInitialized,
openSessionHelpModal: () => {},
openTexthookerInBrowser: () => {},
showFirstRunSetup: () => true,
openFirstRunSetupWindow: () => {},
showWindowsMpvLauncherSetup: () => true,
+12 -5
View File
@@ -30,6 +30,7 @@ test('tray menu template contains expected entries and handlers', () => {
const calls: string[] = [];
const template = buildTrayMenuTemplateRuntime({
openSessionHelp: () => calls.push('help'),
openTexthookerInBrowser: () => calls.push('texthooker'),
openFirstRunSetup: () => calls.push('setup'),
showFirstRunSetup: true,
openWindowsMpvLauncherSetup: () => calls.push('windows-mpv'),
@@ -41,18 +42,24 @@ test('tray menu template contains expected entries and handlers', () => {
quitApp: () => calls.push('quit'),
});
assert.equal(template.length, 9);
assert.equal(template.some((entry) => entry.label === 'Open Overlay'), false);
assert.equal(template.length, 10);
assert.equal(
template.some((entry) => entry.label === 'Open Overlay'),
false,
);
assert.equal(template[0]!.label, 'Open Help');
template[0]!.click?.();
template[7]!.type === 'separator' ? calls.push('separator') : calls.push('bad');
template[8]!.click?.();
assert.deepEqual(calls, ['help', 'separator', 'quit']);
assert.equal(template[1]!.label, 'Open Texthooker');
template[1]!.click?.();
template[8]!.type === 'separator' ? calls.push('separator') : calls.push('bad');
template[9]!.click?.();
assert.deepEqual(calls, ['help', 'texthooker', 'separator', 'quit']);
});
test('tray menu template omits first-run setup entry when setup is complete', () => {
const labels = buildTrayMenuTemplateRuntime({
openSessionHelp: () => undefined,
openTexthookerInBrowser: () => undefined,
openFirstRunSetup: () => undefined,
showFirstRunSetup: false,
openWindowsMpvLauncherSetup: () => undefined,
+5
View File
@@ -31,6 +31,7 @@ export function resolveTrayIconPathRuntime(deps: {
export type TrayMenuActionHandlers = {
openSessionHelp: () => void;
openTexthookerInBrowser: () => void;
openFirstRunSetup: () => void;
showFirstRunSetup: boolean;
openWindowsMpvLauncherSetup: () => void;
@@ -52,6 +53,10 @@ export function buildTrayMenuTemplateRuntime(handlers: TrayMenuActionHandlers):
label: 'Open Help',
click: handlers.openSessionHelp,
},
{
label: 'Open Texthooker',
click: handlers.openTexthookerInBrowser,
},
...(handlers.showFirstRunSetup
? [
{