chore(main): extract app lifecycle/startup builders into main modules

This commit is contained in:
2026-02-14 13:45:25 -08:00
parent 910cf2dca4
commit 65d9f5d54d
14 changed files with 1120 additions and 551 deletions

89
src/main/app-lifecycle.ts Normal file
View File

@@ -0,0 +1,89 @@
import type { CliArgs, CliCommandSource } from "../cli/args";
import { runAppReadyRuntimeService } from "../core/services/startup-service";
import type { AppReadyRuntimeDeps } from "../core/services/startup-service";
import type { AppLifecycleDepsRuntimeOptions } from "../core/services/app-lifecycle-service";
export interface AppLifecycleRuntimeDepsFactoryInput {
app: AppLifecycleDepsRuntimeOptions["app"];
platform: NodeJS.Platform;
shouldStartApp: (args: CliArgs) => boolean;
parseArgs: (argv: string[]) => CliArgs;
handleCliCommand: (nextArgs: CliArgs, source: CliCommandSource) => void;
printHelp: () => void;
logNoRunningInstance: () => void;
onReady: () => Promise<void>;
onWillQuitCleanup: () => void;
shouldRestoreWindowsOnActivate: () => boolean;
restoreWindowsOnActivate: () => void;
}
export interface AppReadyRuntimeDepsFactoryInput {
loadSubtitlePosition: AppReadyRuntimeDeps["loadSubtitlePosition"];
resolveKeybindings: AppReadyRuntimeDeps["resolveKeybindings"];
createMpvClient: AppReadyRuntimeDeps["createMpvClient"];
reloadConfig: AppReadyRuntimeDeps["reloadConfig"];
getResolvedConfig: AppReadyRuntimeDeps["getResolvedConfig"];
getConfigWarnings: AppReadyRuntimeDeps["getConfigWarnings"];
logConfigWarning: AppReadyRuntimeDeps["logConfigWarning"];
initRuntimeOptionsManager: AppReadyRuntimeDeps["initRuntimeOptionsManager"];
setSecondarySubMode: AppReadyRuntimeDeps["setSecondarySubMode"];
defaultSecondarySubMode: AppReadyRuntimeDeps["defaultSecondarySubMode"];
defaultWebsocketPort: AppReadyRuntimeDeps["defaultWebsocketPort"];
hasMpvWebsocketPlugin: AppReadyRuntimeDeps["hasMpvWebsocketPlugin"];
startSubtitleWebsocket: AppReadyRuntimeDeps["startSubtitleWebsocket"];
log: AppReadyRuntimeDeps["log"];
createMecabTokenizerAndCheck: AppReadyRuntimeDeps["createMecabTokenizerAndCheck"];
createSubtitleTimingTracker: AppReadyRuntimeDeps["createSubtitleTimingTracker"];
loadYomitanExtension: AppReadyRuntimeDeps["loadYomitanExtension"];
texthookerOnlyMode: AppReadyRuntimeDeps["texthookerOnlyMode"];
shouldAutoInitializeOverlayRuntimeFromConfig: AppReadyRuntimeDeps["shouldAutoInitializeOverlayRuntimeFromConfig"];
initializeOverlayRuntime: AppReadyRuntimeDeps["initializeOverlayRuntime"];
handleInitialArgs: AppReadyRuntimeDeps["handleInitialArgs"];
}
export function createAppLifecycleRuntimeDeps(
params: AppLifecycleRuntimeDepsFactoryInput,
): AppLifecycleDepsRuntimeOptions {
return {
app: params.app,
platform: params.platform,
shouldStartApp: params.shouldStartApp,
parseArgs: params.parseArgs,
handleCliCommand: params.handleCliCommand,
printHelp: params.printHelp,
logNoRunningInstance: params.logNoRunningInstance,
onReady: params.onReady,
onWillQuitCleanup: params.onWillQuitCleanup,
shouldRestoreWindowsOnActivate: params.shouldRestoreWindowsOnActivate,
restoreWindowsOnActivate: params.restoreWindowsOnActivate,
};
}
export function createAppReadyRuntimeDeps(
params: AppReadyRuntimeDepsFactoryInput,
): AppReadyRuntimeDeps {
return {
loadSubtitlePosition: params.loadSubtitlePosition,
resolveKeybindings: params.resolveKeybindings,
createMpvClient: params.createMpvClient,
reloadConfig: params.reloadConfig,
getResolvedConfig: params.getResolvedConfig,
getConfigWarnings: params.getConfigWarnings,
logConfigWarning: params.logConfigWarning,
initRuntimeOptionsManager: params.initRuntimeOptionsManager,
setSecondarySubMode: params.setSecondarySubMode,
defaultSecondarySubMode: params.defaultSecondarySubMode,
defaultWebsocketPort: params.defaultWebsocketPort,
hasMpvWebsocketPlugin: params.hasMpvWebsocketPlugin,
startSubtitleWebsocket: params.startSubtitleWebsocket,
log: params.log,
createMecabTokenizerAndCheck: params.createMecabTokenizerAndCheck,
createSubtitleTimingTracker: params.createSubtitleTimingTracker,
loadYomitanExtension: params.loadYomitanExtension,
texthookerOnlyMode: params.texthookerOnlyMode,
shouldAutoInitializeOverlayRuntimeFromConfig:
params.shouldAutoInitializeOverlayRuntimeFromConfig,
initializeOverlayRuntime: params.initializeOverlayRuntime,
handleInitialArgs: params.handleInitialArgs,
};
}

View File

@@ -5,6 +5,10 @@ import {
} from "../types";
import { SubsyncResolvedConfig } from "../subsync/utils";
import type { SubsyncRuntimeDeps } from "../core/services/subsync-runner-service";
import type { IpcDepsRuntimeOptions } from "../core/services/ipc-service";
import type { AnkiJimakuIpcRuntimeOptions } from "../core/services/anki-jimaku-service";
import type { CliCommandDepsRuntimeOptions } from "../core/services/cli-command-service";
import type { HandleMpvCommandFromIpcOptions } from "../core/services/ipc-command-service";
import {
cycleRuntimeOptionFromIpcRuntimeService,
setRuntimeOptionFromIpcRuntimeService,
@@ -57,3 +61,251 @@ export function createSubsyncRuntimeDeps(params: SubsyncRuntimeDepsParams): Subs
openManualPicker: params.openManualPicker,
};
}
export interface MainIpcRuntimeServiceDepsParams {
getInvisibleWindow: IpcDepsRuntimeOptions["getInvisibleWindow"];
getMainWindow: IpcDepsRuntimeOptions["getMainWindow"];
getVisibleOverlayVisibility: IpcDepsRuntimeOptions["getVisibleOverlayVisibility"];
getInvisibleOverlayVisibility: IpcDepsRuntimeOptions["getInvisibleOverlayVisibility"];
onOverlayModalClosed: IpcDepsRuntimeOptions["onOverlayModalClosed"];
openYomitanSettings: IpcDepsRuntimeOptions["openYomitanSettings"];
quitApp: IpcDepsRuntimeOptions["quitApp"];
toggleVisibleOverlay: IpcDepsRuntimeOptions["toggleVisibleOverlay"];
tokenizeCurrentSubtitle: IpcDepsRuntimeOptions["tokenizeCurrentSubtitle"];
getCurrentSubtitleAss: IpcDepsRuntimeOptions["getCurrentSubtitleAss"];
getMpvSubtitleRenderMetrics: IpcDepsRuntimeOptions["getMpvSubtitleRenderMetrics"];
getSubtitlePosition: IpcDepsRuntimeOptions["getSubtitlePosition"];
getSubtitleStyle: IpcDepsRuntimeOptions["getSubtitleStyle"];
saveSubtitlePosition: IpcDepsRuntimeOptions["saveSubtitlePosition"];
getMecabTokenizer: IpcDepsRuntimeOptions["getMecabTokenizer"];
handleMpvCommand: IpcDepsRuntimeOptions["handleMpvCommand"];
getKeybindings: IpcDepsRuntimeOptions["getKeybindings"];
getSecondarySubMode: IpcDepsRuntimeOptions["getSecondarySubMode"];
getMpvClient: IpcDepsRuntimeOptions["getMpvClient"];
runSubsyncManual: IpcDepsRuntimeOptions["runSubsyncManual"];
getAnkiConnectStatus: IpcDepsRuntimeOptions["getAnkiConnectStatus"];
getRuntimeOptions: IpcDepsRuntimeOptions["getRuntimeOptions"];
setRuntimeOption: IpcDepsRuntimeOptions["setRuntimeOption"];
cycleRuntimeOption: IpcDepsRuntimeOptions["cycleRuntimeOption"];
reportOverlayContentBounds: IpcDepsRuntimeOptions["reportOverlayContentBounds"];
}
export interface AnkiJimakuIpcRuntimeServiceDepsParams {
patchAnkiConnectEnabled: AnkiJimakuIpcRuntimeOptions["patchAnkiConnectEnabled"];
getResolvedConfig: AnkiJimakuIpcRuntimeOptions["getResolvedConfig"];
getRuntimeOptionsManager: AnkiJimakuIpcRuntimeOptions["getRuntimeOptionsManager"];
getSubtitleTimingTracker: AnkiJimakuIpcRuntimeOptions["getSubtitleTimingTracker"];
getMpvClient: AnkiJimakuIpcRuntimeOptions["getMpvClient"];
getAnkiIntegration: AnkiJimakuIpcRuntimeOptions["getAnkiIntegration"];
setAnkiIntegration: AnkiJimakuIpcRuntimeOptions["setAnkiIntegration"];
showDesktopNotification: AnkiJimakuIpcRuntimeOptions["showDesktopNotification"];
createFieldGroupingCallback: AnkiJimakuIpcRuntimeOptions["createFieldGroupingCallback"];
broadcastRuntimeOptionsChanged: AnkiJimakuIpcRuntimeOptions["broadcastRuntimeOptionsChanged"];
getFieldGroupingResolver: AnkiJimakuIpcRuntimeOptions["getFieldGroupingResolver"];
setFieldGroupingResolver: AnkiJimakuIpcRuntimeOptions["setFieldGroupingResolver"];
parseMediaInfo: AnkiJimakuIpcRuntimeOptions["parseMediaInfo"];
getCurrentMediaPath: AnkiJimakuIpcRuntimeOptions["getCurrentMediaPath"];
jimakuFetchJson: AnkiJimakuIpcRuntimeOptions["jimakuFetchJson"];
getJimakuMaxEntryResults: AnkiJimakuIpcRuntimeOptions["getJimakuMaxEntryResults"];
getJimakuLanguagePreference: AnkiJimakuIpcRuntimeOptions["getJimakuLanguagePreference"];
resolveJimakuApiKey: AnkiJimakuIpcRuntimeOptions["resolveJimakuApiKey"];
isRemoteMediaPath: AnkiJimakuIpcRuntimeOptions["isRemoteMediaPath"];
downloadToFile: AnkiJimakuIpcRuntimeOptions["downloadToFile"];
}
export interface CliCommandRuntimeServiceDepsParams {
mpv: {
getSocketPath: CliCommandDepsRuntimeOptions["mpv"]["getSocketPath"];
setSocketPath: CliCommandDepsRuntimeOptions["mpv"]["setSocketPath"];
getClient: CliCommandDepsRuntimeOptions["mpv"]["getClient"];
showOsd: CliCommandDepsRuntimeOptions["mpv"]["showOsd"];
};
texthooker: {
service: CliCommandDepsRuntimeOptions["texthooker"]["service"];
getPort: CliCommandDepsRuntimeOptions["texthooker"]["getPort"];
setPort: CliCommandDepsRuntimeOptions["texthooker"]["setPort"];
shouldOpenBrowser: CliCommandDepsRuntimeOptions["texthooker"]["shouldOpenBrowser"];
openInBrowser: CliCommandDepsRuntimeOptions["texthooker"]["openInBrowser"];
};
overlay: {
isInitialized: CliCommandDepsRuntimeOptions["overlay"]["isInitialized"];
initialize: CliCommandDepsRuntimeOptions["overlay"]["initialize"];
toggleVisible: CliCommandDepsRuntimeOptions["overlay"]["toggleVisible"];
toggleInvisible: CliCommandDepsRuntimeOptions["overlay"]["toggleInvisible"];
setVisible: CliCommandDepsRuntimeOptions["overlay"]["setVisible"];
setInvisible: CliCommandDepsRuntimeOptions["overlay"]["setInvisible"];
};
mining: {
copyCurrentSubtitle: CliCommandDepsRuntimeOptions["mining"]["copyCurrentSubtitle"];
startPendingMultiCopy:
CliCommandDepsRuntimeOptions["mining"]["startPendingMultiCopy"];
mineSentenceCard: CliCommandDepsRuntimeOptions["mining"]["mineSentenceCard"];
startPendingMineSentenceMultiple:
CliCommandDepsRuntimeOptions["mining"]["startPendingMineSentenceMultiple"];
updateLastCardFromClipboard:
CliCommandDepsRuntimeOptions["mining"]["updateLastCardFromClipboard"];
triggerFieldGrouping: CliCommandDepsRuntimeOptions["mining"]["triggerFieldGrouping"];
triggerSubsyncFromConfig:
CliCommandDepsRuntimeOptions["mining"]["triggerSubsyncFromConfig"];
markLastCardAsAudioCard:
CliCommandDepsRuntimeOptions["mining"]["markLastCardAsAudioCard"];
};
ui: {
openYomitanSettings: CliCommandDepsRuntimeOptions["ui"]["openYomitanSettings"];
cycleSecondarySubMode: CliCommandDepsRuntimeOptions["ui"]["cycleSecondarySubMode"];
openRuntimeOptionsPalette:
CliCommandDepsRuntimeOptions["ui"]["openRuntimeOptionsPalette"];
printHelp: CliCommandDepsRuntimeOptions["ui"]["printHelp"];
};
app: {
stop: CliCommandDepsRuntimeOptions["app"]["stop"];
hasMainWindow: CliCommandDepsRuntimeOptions["app"]["hasMainWindow"];
};
getMultiCopyTimeoutMs: CliCommandDepsRuntimeOptions["getMultiCopyTimeoutMs"];
schedule: CliCommandDepsRuntimeOptions["schedule"];
log: CliCommandDepsRuntimeOptions["log"];
warn: CliCommandDepsRuntimeOptions["warn"];
error: CliCommandDepsRuntimeOptions["error"];
}
export interface MpvCommandRuntimeServiceDepsParams {
specialCommands: HandleMpvCommandFromIpcOptions["specialCommands"];
runtimeOptionsCycle: HandleMpvCommandFromIpcOptions["runtimeOptionsCycle"];
triggerSubsyncFromConfig: HandleMpvCommandFromIpcOptions["triggerSubsyncFromConfig"];
openRuntimeOptionsPalette: HandleMpvCommandFromIpcOptions["openRuntimeOptionsPalette"];
showMpvOsd: HandleMpvCommandFromIpcOptions["showMpvOsd"];
mpvReplaySubtitle: HandleMpvCommandFromIpcOptions["mpvReplaySubtitle"];
mpvPlayNextSubtitle: HandleMpvCommandFromIpcOptions["mpvPlayNextSubtitle"];
mpvSendCommand: HandleMpvCommandFromIpcOptions["mpvSendCommand"];
isMpvConnected: HandleMpvCommandFromIpcOptions["isMpvConnected"];
hasRuntimeOptionsManager: HandleMpvCommandFromIpcOptions["hasRuntimeOptionsManager"];
}
export function createMainIpcRuntimeServiceDeps(
params: MainIpcRuntimeServiceDepsParams,
): IpcDepsRuntimeOptions {
return {
getInvisibleWindow: params.getInvisibleWindow,
getMainWindow: params.getMainWindow,
getVisibleOverlayVisibility: params.getVisibleOverlayVisibility,
getInvisibleOverlayVisibility: params.getInvisibleOverlayVisibility,
onOverlayModalClosed: params.onOverlayModalClosed,
openYomitanSettings: params.openYomitanSettings,
quitApp: params.quitApp,
toggleVisibleOverlay: params.toggleVisibleOverlay,
tokenizeCurrentSubtitle: params.tokenizeCurrentSubtitle,
getCurrentSubtitleAss: params.getCurrentSubtitleAss,
getMpvSubtitleRenderMetrics: params.getMpvSubtitleRenderMetrics,
getSubtitlePosition: params.getSubtitlePosition,
getSubtitleStyle: params.getSubtitleStyle,
saveSubtitlePosition: params.saveSubtitlePosition,
getMecabTokenizer: params.getMecabTokenizer,
handleMpvCommand: params.handleMpvCommand,
getKeybindings: params.getKeybindings,
getSecondarySubMode: params.getSecondarySubMode,
getMpvClient: params.getMpvClient,
runSubsyncManual: params.runSubsyncManual,
getAnkiConnectStatus: params.getAnkiConnectStatus,
getRuntimeOptions: params.getRuntimeOptions,
setRuntimeOption: params.setRuntimeOption,
cycleRuntimeOption: params.cycleRuntimeOption,
reportOverlayContentBounds: params.reportOverlayContentBounds,
};
}
export function createAnkiJimakuIpcRuntimeServiceDeps(
params: AnkiJimakuIpcRuntimeServiceDepsParams,
): AnkiJimakuIpcRuntimeOptions {
return {
patchAnkiConnectEnabled: params.patchAnkiConnectEnabled,
getResolvedConfig: params.getResolvedConfig,
getRuntimeOptionsManager: params.getRuntimeOptionsManager,
getSubtitleTimingTracker: params.getSubtitleTimingTracker,
getMpvClient: params.getMpvClient,
getAnkiIntegration: params.getAnkiIntegration,
setAnkiIntegration: params.setAnkiIntegration,
showDesktopNotification: params.showDesktopNotification,
createFieldGroupingCallback: params.createFieldGroupingCallback,
broadcastRuntimeOptionsChanged: params.broadcastRuntimeOptionsChanged,
getFieldGroupingResolver: params.getFieldGroupingResolver,
setFieldGroupingResolver: params.setFieldGroupingResolver,
parseMediaInfo: params.parseMediaInfo,
getCurrentMediaPath: params.getCurrentMediaPath,
jimakuFetchJson: params.jimakuFetchJson,
getJimakuMaxEntryResults: params.getJimakuMaxEntryResults,
getJimakuLanguagePreference: params.getJimakuLanguagePreference,
resolveJimakuApiKey: params.resolveJimakuApiKey,
isRemoteMediaPath: params.isRemoteMediaPath,
downloadToFile: params.downloadToFile,
};
}
export function createCliCommandRuntimeServiceDeps(
params: CliCommandRuntimeServiceDepsParams,
): CliCommandDepsRuntimeOptions {
return {
mpv: {
getSocketPath: params.mpv.getSocketPath,
setSocketPath: params.mpv.setSocketPath,
getClient: params.mpv.getClient,
showOsd: params.mpv.showOsd,
},
texthooker: {
service: params.texthooker.service,
getPort: params.texthooker.getPort,
setPort: params.texthooker.setPort,
shouldOpenBrowser: params.texthooker.shouldOpenBrowser,
openInBrowser: params.texthooker.openInBrowser,
},
overlay: {
isInitialized: params.overlay.isInitialized,
initialize: params.overlay.initialize,
toggleVisible: params.overlay.toggleVisible,
toggleInvisible: params.overlay.toggleInvisible,
setVisible: params.overlay.setVisible,
setInvisible: params.overlay.setInvisible,
},
mining: {
copyCurrentSubtitle: params.mining.copyCurrentSubtitle,
startPendingMultiCopy: params.mining.startPendingMultiCopy,
mineSentenceCard: params.mining.mineSentenceCard,
startPendingMineSentenceMultiple: params.mining.startPendingMineSentenceMultiple,
updateLastCardFromClipboard: params.mining.updateLastCardFromClipboard,
triggerFieldGrouping: params.mining.triggerFieldGrouping,
triggerSubsyncFromConfig: params.mining.triggerSubsyncFromConfig,
markLastCardAsAudioCard: params.mining.markLastCardAsAudioCard,
},
ui: {
openYomitanSettings: params.ui.openYomitanSettings,
cycleSecondarySubMode: params.ui.cycleSecondarySubMode,
openRuntimeOptionsPalette: params.ui.openRuntimeOptionsPalette,
printHelp: params.ui.printHelp,
},
app: {
stop: params.app.stop,
hasMainWindow: params.app.hasMainWindow,
},
getMultiCopyTimeoutMs: params.getMultiCopyTimeoutMs,
schedule: params.schedule,
log: params.log,
warn: params.warn,
error: params.error,
};
}
export function createMpvCommandRuntimeServiceDeps(
params: MpvCommandRuntimeServiceDepsParams,
): HandleMpvCommandFromIpcOptions {
return {
specialCommands: params.specialCommands,
triggerSubsyncFromConfig: params.triggerSubsyncFromConfig,
openRuntimeOptionsPalette: params.openRuntimeOptionsPalette,
runtimeOptionsCycle: params.runtimeOptionsCycle,
showMpvOsd: params.showMpvOsd,
mpvReplaySubtitle: params.mpvReplaySubtitle,
mpvPlayNextSubtitle: params.mpvPlayNextSubtitle,
mpvSendCommand: params.mpvSendCommand,
isMpvConnected: params.isMpvConnected,
hasRuntimeOptionsManager: params.hasRuntimeOptionsManager,
};
}

63
src/main/startup.ts Normal file
View File

@@ -0,0 +1,63 @@
import { CliArgs } from "../cli/args";
import type { ResolvedConfig } from "../types";
import type { StartupBootstrapRuntimeDeps } from "../core/services/startup-service";
export interface StartupBootstrapRuntimeFactoryDeps {
argv: string[];
parseArgs: (argv: string[]) => CliArgs;
setLogLevelEnv: (level: string) => void;
enableVerboseLogging: () => void;
forceX11Backend: (args: CliArgs) => void;
enforceUnsupportedWaylandMode: (args: CliArgs) => void;
shouldStartApp: (args: CliArgs) => boolean;
getDefaultSocketPath: () => string;
defaultTexthookerPort: number;
configDir: string;
defaultConfig: ResolvedConfig;
generateConfigTemplate: (config: ResolvedConfig) => string;
generateDefaultConfigFile: (
args: CliArgs,
options: {
configDir: string;
defaultConfig: unknown;
generateTemplate: (config: unknown) => string;
},
) => Promise<number>;
onConfigGenerated: (exitCode: number) => void;
onGenerateConfigError: (error: Error) => void;
startAppLifecycle: (args: CliArgs) => void;
}
export function createStartupBootstrapRuntimeDeps(
params: StartupBootstrapRuntimeFactoryDeps,
): StartupBootstrapRuntimeDeps {
return {
argv: params.argv,
parseArgs: params.parseArgs,
setLogLevelEnv: params.setLogLevelEnv,
enableVerboseLogging: params.enableVerboseLogging,
forceX11Backend: (args: CliArgs) => params.forceX11Backend(args),
enforceUnsupportedWaylandMode: (args: CliArgs) =>
params.enforceUnsupportedWaylandMode(args),
getDefaultSocketPath: params.getDefaultSocketPath,
defaultTexthookerPort: params.defaultTexthookerPort,
runGenerateConfigFlow: (args: CliArgs) => {
if (!args.generateConfig || params.shouldStartApp(args)) {
return false;
}
params
.generateDefaultConfigFile(args, {
configDir: params.configDir,
defaultConfig: params.defaultConfig,
generateTemplate: (config: unknown) =>
params.generateConfigTemplate(config as ResolvedConfig),
})
.then((exitCode) => {
params.onConfigGenerated(exitCode);
})
.catch(params.onGenerateConfigError);
return true;
},
startAppLifecycle: params.startAppLifecycle,
};
}