From d796a7cfa501f509df097f85a48a0b5744c6348a Mon Sep 17 00:00:00 2001 From: sudacode Date: Mon, 9 Feb 2026 22:31:51 -0800 Subject: [PATCH] refactor: extract cli command orchestration service --- src/core/services/cli-command-service.ts | 204 +++++++++++++++++++++++ src/main.ts | 204 ++++++++--------------- 2 files changed, 272 insertions(+), 136 deletions(-) create mode 100644 src/core/services/cli-command-service.ts diff --git a/src/core/services/cli-command-service.ts b/src/core/services/cli-command-service.ts new file mode 100644 index 0000000..5e592e3 --- /dev/null +++ b/src/core/services/cli-command-service.ts @@ -0,0 +1,204 @@ +import { + CliArgs, + CliCommandSource, + commandNeedsOverlayRuntime, +} from "../../cli/args"; + +export interface CliCommandServiceDeps { + getMpvSocketPath: () => string; + setMpvSocketPath: (socketPath: string) => void; + setMpvClientSocketPath: (socketPath: string) => void; + hasMpvClient: () => boolean; + connectMpvClient: () => void; + isTexthookerRunning: () => boolean; + setTexthookerPort: (port: number) => void; + getTexthookerPort: () => number; + shouldOpenTexthookerBrowser: () => boolean; + ensureTexthookerRunning: (port: number) => void; + openTexthookerInBrowser: (url: string) => void; + stopApp: () => void; + isOverlayRuntimeInitialized: () => boolean; + initializeOverlayRuntime: () => void; + toggleVisibleOverlay: () => void; + toggleInvisibleOverlay: () => void; + openYomitanSettingsDelayed: (delayMs: number) => void; + setVisibleOverlayVisible: (visible: boolean) => void; + setInvisibleOverlayVisible: (visible: boolean) => void; + copyCurrentSubtitle: () => void; + startPendingMultiCopy: (timeoutMs: number) => void; + mineSentenceCard: () => Promise; + startPendingMineSentenceMultiple: (timeoutMs: number) => void; + updateLastCardFromClipboard: () => Promise; + cycleSecondarySubMode: () => void; + triggerFieldGrouping: () => Promise; + triggerSubsyncFromConfig: () => Promise; + markLastCardAsAudioCard: () => Promise; + openRuntimeOptionsPalette: () => void; + printHelp: () => void; + hasMainWindow: () => boolean; + getMultiCopyTimeoutMs: () => number; + showMpvOsd: (text: string) => void; + log: (message: string) => void; + warn: (message: string) => void; + error: (message: string, err: unknown) => void; +} + +function runAsyncWithOsd( + task: () => Promise, + deps: CliCommandServiceDeps, + logLabel: string, + osdLabel: string, +): void { + task().catch((err) => { + deps.error(`${logLabel} failed:`, err); + deps.showMpvOsd(`${osdLabel}: ${(err as Error).message}`); + }); +} + +export function handleCliCommandService( + args: CliArgs, + source: CliCommandSource = "initial", + deps: CliCommandServiceDeps, +): void { + const hasNonStartAction = + args.stop || + args.toggle || + args.toggleVisibleOverlay || + args.toggleInvisibleOverlay || + args.settings || + args.show || + args.hide || + args.showVisibleOverlay || + args.hideVisibleOverlay || + args.showInvisibleOverlay || + args.hideInvisibleOverlay || + args.copySubtitle || + args.copySubtitleMultiple || + args.mineSentence || + args.mineSentenceMultiple || + args.updateLastCardFromClipboard || + args.toggleSecondarySub || + args.triggerFieldGrouping || + args.triggerSubsync || + args.markAudioCard || + args.openRuntimeOptions || + args.texthooker || + args.help; + const ignoreStart = source === "second-instance" && args.start; + if (ignoreStart && !hasNonStartAction) { + deps.log("Ignoring --start because SubMiner is already running."); + return; + } + + const shouldStart = + !ignoreStart && + (args.start || + (source === "initial" && + (args.toggle || + args.toggleVisibleOverlay || + args.toggleInvisibleOverlay))); + const needsOverlayRuntime = commandNeedsOverlayRuntime(args); + + if (args.socketPath !== undefined) { + deps.setMpvSocketPath(args.socketPath); + deps.setMpvClientSocketPath(args.socketPath); + } + + if (args.texthookerPort !== undefined) { + if (deps.isTexthookerRunning()) { + deps.warn( + "Ignoring --port override because the texthooker server is already running.", + ); + } else { + deps.setTexthookerPort(args.texthookerPort); + } + } + + if (args.stop) { + deps.log("Stopping SubMiner..."); + deps.stopApp(); + return; + } + + if (needsOverlayRuntime && !deps.isOverlayRuntimeInitialized()) { + deps.initializeOverlayRuntime(); + } + + if (shouldStart && deps.hasMpvClient()) { + const socketPath = deps.getMpvSocketPath(); + deps.setMpvClientSocketPath(socketPath); + deps.connectMpvClient(); + deps.log(`Starting MPV IPC connection on socket: ${socketPath}`); + } + + if (args.toggle || args.toggleVisibleOverlay) { + deps.toggleVisibleOverlay(); + } else if (args.toggleInvisibleOverlay) { + deps.toggleInvisibleOverlay(); + } else if (args.settings) { + deps.openYomitanSettingsDelayed(1000); + } else if (args.show || args.showVisibleOverlay) { + deps.setVisibleOverlayVisible(true); + } else if (args.hide || args.hideVisibleOverlay) { + deps.setVisibleOverlayVisible(false); + } else if (args.showInvisibleOverlay) { + deps.setInvisibleOverlayVisible(true); + } else if (args.hideInvisibleOverlay) { + deps.setInvisibleOverlayVisible(false); + } else if (args.copySubtitle) { + deps.copyCurrentSubtitle(); + } else if (args.copySubtitleMultiple) { + deps.startPendingMultiCopy(deps.getMultiCopyTimeoutMs()); + } else if (args.mineSentence) { + runAsyncWithOsd( + () => deps.mineSentenceCard(), + deps, + "mineSentenceCard", + "Mine sentence failed", + ); + } else if (args.mineSentenceMultiple) { + deps.startPendingMineSentenceMultiple(deps.getMultiCopyTimeoutMs()); + } else if (args.updateLastCardFromClipboard) { + runAsyncWithOsd( + () => deps.updateLastCardFromClipboard(), + deps, + "updateLastCardFromClipboard", + "Update failed", + ); + } else if (args.toggleSecondarySub) { + deps.cycleSecondarySubMode(); + } else if (args.triggerFieldGrouping) { + runAsyncWithOsd( + () => deps.triggerFieldGrouping(), + deps, + "triggerFieldGrouping", + "Field grouping failed", + ); + } else if (args.triggerSubsync) { + runAsyncWithOsd( + () => deps.triggerSubsyncFromConfig(), + deps, + "triggerSubsyncFromConfig", + "Subsync failed", + ); + } else if (args.markAudioCard) { + runAsyncWithOsd( + () => deps.markLastCardAsAudioCard(), + deps, + "markLastCardAsAudioCard", + "Audio card failed", + ); + } else if (args.openRuntimeOptions) { + deps.openRuntimeOptionsPalette(); + } else if (args.texthooker) { + const texthookerPort = deps.getTexthookerPort(); + deps.ensureTexthookerRunning(texthookerPort); + if (deps.shouldOpenTexthookerBrowser()) { + deps.openTexthookerInBrowser(`http://127.0.0.1:${texthookerPort}`); + } + deps.log(`Texthooker available at http://127.0.0.1:${texthookerPort}`); + } else if (args.help) { + deps.printHelp(); + if (!deps.hasMainWindow()) deps.stopApp(); + } +} diff --git a/src/main.ts b/src/main.ts index fe61f97..656c6ad 100644 --- a/src/main.ts +++ b/src/main.ts @@ -85,7 +85,6 @@ import { import { CliArgs, CliCommandSource, - commandNeedsOverlayRuntime, hasExplicitCommand, parseArgs, shouldStartApp, @@ -117,6 +116,7 @@ import { import { runOverlayShortcutLocalFallback } from "./core/services/overlay-shortcut-fallback-runner"; import { createOverlayShortcutRuntimeHandlers } from "./core/services/overlay-shortcut-runtime-service"; import { createNumericShortcutSessionService } from "./core/services/numeric-shortcut-session-service"; +import { handleCliCommandService } from "./core/services/cli-command-service"; import { showDesktopNotification } from "./core/utils/notification"; import { openYomitanSettingsWindow } from "./core/services/yomitan-settings-service"; import { tokenizeSubtitleService } from "./core/services/tokenizer-service"; @@ -575,141 +575,73 @@ function handleCliCommand( args: CliArgs, source: CliCommandSource = "initial", ): void { - const hasNonStartAction = - args.stop || - args.toggle || - args.toggleVisibleOverlay || - args.toggleInvisibleOverlay || - args.settings || - args.show || - args.hide || - args.showVisibleOverlay || - args.hideVisibleOverlay || - args.showInvisibleOverlay || - args.hideInvisibleOverlay || - args.copySubtitle || - args.copySubtitleMultiple || - args.mineSentence || - args.mineSentenceMultiple || - args.updateLastCardFromClipboard || - args.toggleSecondarySub || - args.triggerFieldGrouping || - args.triggerSubsync || - args.markAudioCard || - args.openRuntimeOptions || - args.texthooker || - args.help; - const ignoreStart = source === "second-instance" && args.start; - if (ignoreStart && !hasNonStartAction) { - console.log("Ignoring --start because SubMiner is already running."); - return; - } - const shouldStart = - !ignoreStart && - (args.start || - (source === "initial" && - (args.toggle || - args.toggleVisibleOverlay || - args.toggleInvisibleOverlay))); - const needsOverlayRuntime = commandNeedsOverlayRuntime(args); - - if (args.socketPath !== undefined) { - mpvSocketPath = args.socketPath; - if (mpvClient) { - mpvClient.setSocketPath(mpvSocketPath); - } - } - if (args.texthookerPort !== undefined) { - if (texthookerService.isRunning()) { - console.warn( - "Ignoring --port override because the texthooker server is already running.", - ); - } else { - texthookerPort = args.texthookerPort; - } - } - - if (args.stop) { - console.log("Stopping SubMiner..."); - app.quit(); - return; - } - - if (needsOverlayRuntime && !overlayRuntimeInitialized) { - initializeOverlayRuntime(); - } - - if (shouldStart && mpvClient) { - mpvClient.setSocketPath(mpvSocketPath); - mpvClient.connect(); - console.log(`Starting MPV IPC connection on socket: ${mpvSocketPath}`); - } - - if (args.toggle || args.toggleVisibleOverlay) { - toggleVisibleOverlay(); - } else if (args.toggleInvisibleOverlay) { - toggleInvisibleOverlay(); - } else if (args.settings) { - setTimeout(() => { - openYomitanSettings(); - }, 1000); - } else if (args.show || args.showVisibleOverlay) { - setVisibleOverlayVisible(true); - } else if (args.hide || args.hideVisibleOverlay) { - setVisibleOverlayVisible(false); - } else if (args.showInvisibleOverlay) { - setInvisibleOverlayVisible(true); - } else if (args.hideInvisibleOverlay) { - setInvisibleOverlayVisible(false); - } else if (args.copySubtitle) { - copyCurrentSubtitle(); - } else if (args.copySubtitleMultiple) { - startPendingMultiCopy(getConfiguredShortcuts().multiCopyTimeoutMs); - } else if (args.mineSentence) { - mineSentenceCard().catch((err) => { - console.error("mineSentenceCard failed:", err); - showMpvOsd(`Mine sentence failed: ${(err as Error).message}`); - }); - } else if (args.mineSentenceMultiple) { - startPendingMineSentenceMultiple(getConfiguredShortcuts().multiCopyTimeoutMs); - } else if (args.updateLastCardFromClipboard) { - updateLastCardFromClipboard().catch((err) => { - console.error("updateLastCardFromClipboard failed:", err); - showMpvOsd(`Update failed: ${(err as Error).message}`); - }); - } else if (args.toggleSecondarySub) { - cycleSecondarySubMode(); - } else if (args.triggerFieldGrouping) { - triggerFieldGrouping().catch((err) => { - console.error("triggerFieldGrouping failed:", err); - showMpvOsd(`Field grouping failed: ${(err as Error).message}`); - }); - } else if (args.triggerSubsync) { - triggerSubsyncFromConfig().catch((err) => { - console.error("triggerSubsyncFromConfig failed:", err); - showMpvOsd(`Subsync failed: ${(err as Error).message}`); - }); - } else if (args.markAudioCard) { - markLastCardAsAudioCard().catch((err) => { - console.error("markLastCardAsAudioCard failed:", err); - showMpvOsd(`Audio card failed: ${(err as Error).message}`); - }); - } else if (args.openRuntimeOptions) { - openRuntimeOptionsPalette(); - } else if (args.texthooker) { - if (!texthookerService.isRunning()) { - texthookerService.start(texthookerPort); - } - const config = getResolvedConfig(); - const openBrowser = config.texthooker?.openBrowser !== false; - if (openBrowser) { - shell.openExternal(`http://127.0.0.1:${texthookerPort}`); - } - console.log(`Texthooker available at http://127.0.0.1:${texthookerPort}`); - } else if (args.help) { - printHelp(DEFAULT_TEXTHOOKER_PORT); - if (!mainWindow) app.quit(); - } + handleCliCommandService(args, source, { + getMpvSocketPath: () => mpvSocketPath, + setMpvSocketPath: (socketPath) => { + mpvSocketPath = socketPath; + }, + setMpvClientSocketPath: (socketPath) => { + if (!mpvClient) return; + mpvClient.setSocketPath(socketPath); + }, + hasMpvClient: () => Boolean(mpvClient), + connectMpvClient: () => { + if (!mpvClient) return; + mpvClient.connect(); + }, + isTexthookerRunning: () => texthookerService.isRunning(), + setTexthookerPort: (port) => { + texthookerPort = port; + }, + getTexthookerPort: () => texthookerPort, + shouldOpenTexthookerBrowser: () => + getResolvedConfig().texthooker?.openBrowser !== false, + ensureTexthookerRunning: (port) => { + if (!texthookerService.isRunning()) { + texthookerService.start(port); + } + }, + openTexthookerInBrowser: (url) => { + shell.openExternal(url); + }, + stopApp: () => app.quit(), + isOverlayRuntimeInitialized: () => overlayRuntimeInitialized, + initializeOverlayRuntime: () => initializeOverlayRuntime(), + toggleVisibleOverlay: () => toggleVisibleOverlay(), + toggleInvisibleOverlay: () => toggleInvisibleOverlay(), + openYomitanSettingsDelayed: (delayMs) => { + setTimeout(() => { + openYomitanSettings(); + }, delayMs); + }, + setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible), + setInvisibleOverlayVisible: (visible) => + setInvisibleOverlayVisible(visible), + copyCurrentSubtitle: () => copyCurrentSubtitle(), + startPendingMultiCopy: (timeoutMs) => startPendingMultiCopy(timeoutMs), + mineSentenceCard: () => mineSentenceCard(), + startPendingMineSentenceMultiple: (timeoutMs) => + startPendingMineSentenceMultiple(timeoutMs), + updateLastCardFromClipboard: () => updateLastCardFromClipboard(), + cycleSecondarySubMode: () => cycleSecondarySubMode(), + triggerFieldGrouping: () => triggerFieldGrouping(), + triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(), + markLastCardAsAudioCard: () => markLastCardAsAudioCard(), + openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(), + printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT), + hasMainWindow: () => Boolean(mainWindow), + getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs, + showMpvOsd: (text) => showMpvOsd(text), + log: (message) => { + console.log(message); + }, + warn: (message) => { + console.warn(message); + }, + error: (message, err) => { + console.error(message, err); + }, + }); } function handleInitialArgs(): void {