mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
Track mpv overlays by configured socket window only
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import Foundation
|
||||||
|
|
||||||
private struct WindowGeometry {
|
private struct WindowGeometry {
|
||||||
let x: Int
|
let x: Int
|
||||||
@@ -19,6 +20,58 @@ private struct WindowGeometry {
|
|||||||
let height: Int
|
let height: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let targetMpvSocketPath: String? = {
|
||||||
|
guard CommandLine.arguments.count > 1 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = CommandLine.arguments[1].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
return value.isEmpty ? nil : value
|
||||||
|
}()
|
||||||
|
|
||||||
|
private func windowCommandLineForPid(_ pid: pid_t) -> String? {
|
||||||
|
let process = Process()
|
||||||
|
process.executableURL = URL(fileURLWithPath: "/bin/ps")
|
||||||
|
process.arguments = ["-p", "\(pid)", "-o", "args="]
|
||||||
|
|
||||||
|
let pipe = Pipe()
|
||||||
|
process.standardOutput = pipe
|
||||||
|
process.standardError = pipe
|
||||||
|
|
||||||
|
do {
|
||||||
|
try process.run()
|
||||||
|
process.waitUntilExit()
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if process.terminationStatus != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||||
|
guard let commandLine = String(data: data, encoding: .utf8) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return commandLine
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
.replacingOccurrences(of: "\n", with: " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func windowHasTargetSocket(_ pid: pid_t) -> Bool {
|
||||||
|
guard let socketPath = targetMpvSocketPath else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let commandLine = windowCommandLineForPid(pid) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return commandLine.contains("--input-ipc-server=\(socketPath)") ||
|
||||||
|
commandLine.contains("--input-ipc-server \(socketPath)")
|
||||||
|
}
|
||||||
|
|
||||||
private func geometryFromRect(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat) -> WindowGeometry {
|
private func geometryFromRect(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat) -> WindowGeometry {
|
||||||
let minX = Int(floor(x))
|
let minX = Int(floor(x))
|
||||||
let minY = Int(floor(y))
|
let minY = Int(floor(y))
|
||||||
@@ -93,6 +146,10 @@ private func geometryFromAccessibilityAPI() -> WindowGeometry? {
|
|||||||
|
|
||||||
for app in runningApps {
|
for app in runningApps {
|
||||||
let appElement = AXUIElementCreateApplication(app.processIdentifier)
|
let appElement = AXUIElementCreateApplication(app.processIdentifier)
|
||||||
|
if !windowHasTargetSocket(app.processIdentifier) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
var windowsRef: CFTypeRef?
|
var windowsRef: CFTypeRef?
|
||||||
let status = AXUIElementCopyAttributeValue(appElement, kAXWindowsAttribute as CFString, &windowsRef)
|
let status = AXUIElementCopyAttributeValue(appElement, kAXWindowsAttribute as CFString, &windowsRef)
|
||||||
guard status == .success, let windows = windowsRef as? [AXUIElement], !windows.isEmpty else {
|
guard status == .success, let windows = windowsRef as? [AXUIElement], !windows.isEmpty else {
|
||||||
@@ -106,6 +163,15 @@ private func geometryFromAccessibilityAPI() -> WindowGeometry? {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var windowPid: pid_t = 0
|
||||||
|
if AXUIElementGetPid(window, &windowPid) != .success {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !windowHasTargetSocket(windowPid) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if let geometry = geometryFromAXWindow(window) {
|
if let geometry = geometryFromAXWindow(window) {
|
||||||
return geometry
|
return geometry
|
||||||
}
|
}
|
||||||
@@ -127,6 +193,14 @@ private func geometryFromCoreGraphics() -> WindowGeometry? {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard let ownerPid = window[kCGWindowOwnerPID as String] as? pid_t else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !windowHasTargetSocket(ownerPid) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if let layer = window[kCGWindowLayer as String] as? Int, layer != 0 {
|
if let layer = window[kCGWindowLayer as String] as? Int, layer != 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export function initializeOverlayRuntimeService(options: {
|
|||||||
getOverlayWindows: () => BrowserWindow[];
|
getOverlayWindows: () => BrowserWindow[];
|
||||||
syncOverlayShortcuts: () => void;
|
syncOverlayShortcuts: () => void;
|
||||||
setWindowTracker: (tracker: BaseWindowTracker | null) => void;
|
setWindowTracker: (tracker: BaseWindowTracker | null) => void;
|
||||||
|
getMpvSocketPath: () => string;
|
||||||
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
|
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
|
||||||
getSubtitleTimingTracker: () => unknown | null;
|
getSubtitleTimingTracker: () => unknown | null;
|
||||||
getMpvClient: () => { send?: (payload: { command: string[] }) => void } | null;
|
getMpvClient: () => { send?: (payload: { command: string[] }) => void } | null;
|
||||||
@@ -42,7 +43,10 @@ export function initializeOverlayRuntimeService(options: {
|
|||||||
const invisibleOverlayVisible = options.getInitialInvisibleOverlayVisibility();
|
const invisibleOverlayVisible = options.getInitialInvisibleOverlayVisibility();
|
||||||
options.registerGlobalShortcuts();
|
options.registerGlobalShortcuts();
|
||||||
|
|
||||||
const windowTracker = createWindowTracker(options.backendOverride);
|
const windowTracker = createWindowTracker(
|
||||||
|
options.backendOverride,
|
||||||
|
options.getMpvSocketPath(),
|
||||||
|
);
|
||||||
options.setWindowTracker(windowTracker);
|
options.setWindowTracker(windowTracker);
|
||||||
if (windowTracker) {
|
if (windowTracker) {
|
||||||
windowTracker.onGeometryChange = (geometry: WindowGeometry) => {
|
windowTracker.onGeometryChange = (geometry: WindowGeometry) => {
|
||||||
|
|||||||
498
src/main.ts
498
src/main.ts
@@ -46,6 +46,7 @@ import { BaseWindowTracker } from "./window-trackers";
|
|||||||
import type {
|
import type {
|
||||||
JimakuApiResponse,
|
JimakuApiResponse,
|
||||||
JimakuLanguagePreference,
|
JimakuLanguagePreference,
|
||||||
|
RuntimeOptionId,
|
||||||
SubtitleData,
|
SubtitleData,
|
||||||
SubtitlePosition,
|
SubtitlePosition,
|
||||||
Keybinding,
|
Keybinding,
|
||||||
@@ -59,6 +60,7 @@ import type {
|
|||||||
KikuMergePreviewResponse,
|
KikuMergePreviewResponse,
|
||||||
RuntimeOptionState,
|
RuntimeOptionState,
|
||||||
MpvSubtitleRenderMetrics,
|
MpvSubtitleRenderMetrics,
|
||||||
|
ResolvedConfig,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { SubtitleTimingTracker } from "./subtitle-timing-tracker";
|
import { SubtitleTimingTracker } from "./subtitle-timing-tracker";
|
||||||
import { AnkiIntegration } from "./anki-integration";
|
import { AnkiIntegration } from "./anki-integration";
|
||||||
@@ -155,13 +157,27 @@ import {
|
|||||||
updateVisibleOverlayVisibilityService,
|
updateVisibleOverlayVisibilityService,
|
||||||
} from "./core/services";
|
} from "./core/services";
|
||||||
import { runOverlayShortcutLocalFallback } from "./core/services/overlay-shortcut-handler";
|
import { runOverlayShortcutLocalFallback } from "./core/services/overlay-shortcut-handler";
|
||||||
import { runAppReadyRuntimeService } from "./core/services/startup-service";
|
import {
|
||||||
|
runAppReadyRuntimeService,
|
||||||
|
} from "./core/services/startup-service";
|
||||||
import type { AppReadyRuntimeDeps } from "./core/services/startup-service";
|
import type { AppReadyRuntimeDeps } from "./core/services/startup-service";
|
||||||
import type { SubsyncRuntimeDeps } from "./core/services/subsync-runner-service";
|
import type { SubsyncRuntimeDeps } from "./core/services/subsync-runner-service";
|
||||||
import {
|
import {
|
||||||
applyRuntimeOptionResultRuntimeService,
|
applyRuntimeOptionResultRuntimeService,
|
||||||
} from "./core/services/runtime-options-ipc-service";
|
} from "./core/services/runtime-options-ipc-service";
|
||||||
import { createRuntimeOptionsIpcDeps, createSubsyncRuntimeDeps } from "./main/dependencies";
|
import {
|
||||||
|
createRuntimeOptionsIpcDeps,
|
||||||
|
createAnkiJimakuIpcRuntimeServiceDeps,
|
||||||
|
createCliCommandRuntimeServiceDeps,
|
||||||
|
createMainIpcRuntimeServiceDeps,
|
||||||
|
createMpvCommandRuntimeServiceDeps,
|
||||||
|
createSubsyncRuntimeDeps,
|
||||||
|
} from "./main/dependencies";
|
||||||
|
import {
|
||||||
|
createAppLifecycleRuntimeDeps as createAppLifecycleRuntimeDepsBuilder,
|
||||||
|
createAppReadyRuntimeDeps as createAppReadyRuntimeDepsBuilder,
|
||||||
|
} from "./main/app-lifecycle";
|
||||||
|
import { createStartupBootstrapRuntimeDeps } from "./main/startup";
|
||||||
import {
|
import {
|
||||||
ConfigService,
|
ConfigService,
|
||||||
DEFAULT_CONFIG,
|
DEFAULT_CONFIG,
|
||||||
@@ -172,11 +188,6 @@ import {
|
|||||||
import type {
|
import type {
|
||||||
AppLifecycleDepsRuntimeOptions,
|
AppLifecycleDepsRuntimeOptions,
|
||||||
} from "./core/services/app-lifecycle-service";
|
} from "./core/services/app-lifecycle-service";
|
||||||
import type { CliCommandDepsRuntimeOptions } from "./core/services/cli-command-service";
|
|
||||||
import type { HandleMpvCommandFromIpcOptions } from "./core/services/ipc-command-service";
|
|
||||||
import type { IpcDepsRuntimeOptions } from "./core/services/ipc-service";
|
|
||||||
import type { AnkiJimakuIpcRuntimeOptions } from "./core/services/anki-jimaku-service";
|
|
||||||
import type { StartupBootstrapRuntimeDeps } from "./core/services/startup-service";
|
|
||||||
|
|
||||||
if (process.platform === "linux") {
|
if (process.platform === "linux") {
|
||||||
app.commandLine.appendSwitch("enable-features", "GlobalShortcutsPortal");
|
app.commandLine.appendSwitch("enable-features", "GlobalShortcutsPortal");
|
||||||
@@ -620,7 +631,53 @@ function resolveMediaPathForJimaku(mediaPath: string | null): string | null {
|
|||||||
: mediaPath;
|
: mediaPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
const startupState = runStartupBootstrapRuntimeService(createStartupBootstrapRuntimeDeps());
|
const startupState = runStartupBootstrapRuntimeService(
|
||||||
|
createStartupBootstrapRuntimeDeps({
|
||||||
|
argv: process.argv,
|
||||||
|
parseArgs: (argv: string[]) => parseArgs(argv),
|
||||||
|
setLogLevelEnv: (level: string) => {
|
||||||
|
process.env.SUBMINER_LOG_LEVEL = level;
|
||||||
|
},
|
||||||
|
enableVerboseLogging: () => {
|
||||||
|
process.env.SUBMINER_LOG_LEVEL = "debug";
|
||||||
|
},
|
||||||
|
forceX11Backend: (args: CliArgs) => {
|
||||||
|
forceX11Backend(args);
|
||||||
|
},
|
||||||
|
enforceUnsupportedWaylandMode: (args: CliArgs) => {
|
||||||
|
enforceUnsupportedWaylandMode(args);
|
||||||
|
},
|
||||||
|
shouldStartApp: (args: CliArgs) => shouldStartApp(args),
|
||||||
|
getDefaultSocketPath: () => getDefaultSocketPath(),
|
||||||
|
defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT,
|
||||||
|
configDir: CONFIG_DIR,
|
||||||
|
defaultConfig: DEFAULT_CONFIG,
|
||||||
|
generateConfigTemplate: (config: ResolvedConfig) => generateConfigTemplate(config),
|
||||||
|
generateDefaultConfigFile: (
|
||||||
|
args: CliArgs,
|
||||||
|
options: {
|
||||||
|
configDir: string;
|
||||||
|
defaultConfig: unknown;
|
||||||
|
generateTemplate: (config: unknown) => string;
|
||||||
|
},
|
||||||
|
) => generateDefaultConfigFile(args, options),
|
||||||
|
onConfigGenerated: (exitCode: number) => {
|
||||||
|
process.exitCode = exitCode;
|
||||||
|
app.quit();
|
||||||
|
},
|
||||||
|
onGenerateConfigError: (error: Error) => {
|
||||||
|
console.error(`Failed to generate config: ${error.message}`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
app.quit();
|
||||||
|
},
|
||||||
|
startAppLifecycle: (args: CliArgs) => {
|
||||||
|
startAppLifecycleService(
|
||||||
|
args,
|
||||||
|
createAppLifecycleDepsRuntimeService(createAppLifecycleRuntimeDeps()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
appState.initialArgs = startupState.initialArgs;
|
appState.initialArgs = startupState.initialArgs;
|
||||||
appState.mpvSocketPath = startupState.mpvSocketPath;
|
appState.mpvSocketPath = startupState.mpvSocketPath;
|
||||||
@@ -630,12 +687,15 @@ appState.autoStartOverlay = startupState.autoStartOverlay;
|
|||||||
appState.texthookerOnlyMode = startupState.texthookerOnlyMode;
|
appState.texthookerOnlyMode = startupState.texthookerOnlyMode;
|
||||||
|
|
||||||
function createAppLifecycleRuntimeDeps(): AppLifecycleDepsRuntimeOptions {
|
function createAppLifecycleRuntimeDeps(): AppLifecycleDepsRuntimeOptions {
|
||||||
return {
|
return createAppLifecycleRuntimeDepsBuilder({
|
||||||
app,
|
app,
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
shouldStartApp: (nextArgs) => shouldStartApp(nextArgs),
|
shouldStartApp: (nextArgs: CliArgs) => shouldStartApp(nextArgs),
|
||||||
parseArgs: (argv) => parseArgs(argv),
|
parseArgs: (argv: string[]) => parseArgs(argv),
|
||||||
handleCliCommand: (nextArgs, source) => handleCliCommand(nextArgs, source),
|
handleCliCommand: (
|
||||||
|
nextArgs: CliArgs,
|
||||||
|
source: CliCommandSource,
|
||||||
|
) => handleCliCommand(nextArgs, source),
|
||||||
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
||||||
logNoRunningInstance: () => appLogger.logNoRunningInstance(),
|
logNoRunningInstance: () => appLogger.logNoRunningInstance(),
|
||||||
onReady: async () => {
|
onReady: async () => {
|
||||||
@@ -676,11 +736,11 @@ function createAppLifecycleRuntimeDeps(): AppLifecycleDepsRuntimeOptions {
|
|||||||
updateVisibleOverlayVisibility();
|
updateVisibleOverlayVisibility();
|
||||||
updateInvisibleOverlayVisibility();
|
updateInvisibleOverlayVisibility();
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAppReadyRuntimeDeps(): AppReadyRuntimeDeps {
|
function createAppReadyRuntimeDeps(): AppReadyRuntimeDeps {
|
||||||
return {
|
return createAppReadyRuntimeDepsBuilder({
|
||||||
loadSubtitlePosition: () => loadSubtitlePosition(),
|
loadSubtitlePosition: () => loadSubtitlePosition(),
|
||||||
resolveKeybindings: () => {
|
resolveKeybindings: () => {
|
||||||
appState.keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS);
|
appState.keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS);
|
||||||
@@ -711,13 +771,13 @@ function createAppReadyRuntimeDeps(): AppReadyRuntimeDeps {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
setSecondarySubMode: (mode) => {
|
setSecondarySubMode: (mode: SecondarySubMode) => {
|
||||||
appState.secondarySubMode = mode;
|
appState.secondarySubMode = mode;
|
||||||
},
|
},
|
||||||
defaultSecondarySubMode: "hover",
|
defaultSecondarySubMode: "hover",
|
||||||
defaultWebsocketPort: DEFAULT_CONFIG.websocket.port,
|
defaultWebsocketPort: DEFAULT_CONFIG.websocket.port,
|
||||||
hasMpvWebsocketPlugin: () => hasMpvWebsocketPlugin(),
|
hasMpvWebsocketPlugin: () => hasMpvWebsocketPlugin(),
|
||||||
startSubtitleWebsocket: (port) => {
|
startSubtitleWebsocket: (port: number) => {
|
||||||
subtitleWsService.start(port, () => appState.currentSubText);
|
subtitleWsService.start(port, () => appState.currentSubText);
|
||||||
},
|
},
|
||||||
log: (message) => appLogger.logInfo(message),
|
log: (message) => appLogger.logInfo(message),
|
||||||
@@ -738,147 +798,122 @@ function createAppReadyRuntimeDeps(): AppReadyRuntimeDeps {
|
|||||||
shouldAutoInitializeOverlayRuntimeFromConfig(),
|
shouldAutoInitializeOverlayRuntimeFromConfig(),
|
||||||
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
||||||
handleInitialArgs: () => handleInitialArgs(),
|
handleInitialArgs: () => handleInitialArgs(),
|
||||||
};
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function createStartupBootstrapRuntimeDeps(): StartupBootstrapRuntimeDeps {
|
|
||||||
return {
|
|
||||||
argv: process.argv,
|
|
||||||
parseArgs: (argv) => parseArgs(argv),
|
|
||||||
setLogLevelEnv: (level) => {
|
|
||||||
process.env.SUBMINER_LOG_LEVEL = level;
|
|
||||||
},
|
|
||||||
enableVerboseLogging: () => {
|
|
||||||
process.env.SUBMINER_LOG_LEVEL = "debug";
|
|
||||||
},
|
|
||||||
forceX11Backend: (args) => {
|
|
||||||
forceX11Backend(args);
|
|
||||||
},
|
|
||||||
enforceUnsupportedWaylandMode: (args) => {
|
|
||||||
enforceUnsupportedWaylandMode(args);
|
|
||||||
},
|
|
||||||
getDefaultSocketPath: () => getDefaultSocketPath(),
|
|
||||||
defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT,
|
|
||||||
runGenerateConfigFlow: (args) => {
|
|
||||||
if (!args.generateConfig || shouldStartApp(args)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
generateDefaultConfigFile(args, {
|
|
||||||
configDir: CONFIG_DIR,
|
|
||||||
defaultConfig: DEFAULT_CONFIG,
|
|
||||||
generateTemplate: (config) => generateConfigTemplate(config as never),
|
|
||||||
})
|
|
||||||
.then((exitCode) => {
|
|
||||||
process.exitCode = exitCode;
|
|
||||||
app.quit();
|
|
||||||
})
|
|
||||||
.catch((error: Error) => {
|
|
||||||
console.error(`Failed to generate config: ${error.message}`);
|
|
||||||
process.exitCode = 1;
|
|
||||||
app.quit();
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
startAppLifecycle: (args) => {
|
|
||||||
startAppLifecycleService(
|
|
||||||
args,
|
|
||||||
createAppLifecycleDepsRuntimeService(createAppLifecycleRuntimeDeps()),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCliCommand(
|
function handleCliCommand(
|
||||||
args: CliArgs,
|
args: CliArgs,
|
||||||
source: CliCommandSource = "initial",
|
source: CliCommandSource = "initial",
|
||||||
): void {
|
): void {
|
||||||
const deps = createCliCommandDepsRuntimeService(createCliCommandRuntimeServiceDeps());
|
const deps = createCliCommandDepsRuntimeService(
|
||||||
|
createCliCommandRuntimeServiceDeps({
|
||||||
|
mpv: {
|
||||||
|
getSocketPath: () => appState.mpvSocketPath,
|
||||||
|
setSocketPath: (socketPath: string) => {
|
||||||
|
appState.mpvSocketPath = socketPath;
|
||||||
|
},
|
||||||
|
getClient: () => appState.mpvClient,
|
||||||
|
showOsd: (text: string) => showMpvOsd(text),
|
||||||
|
},
|
||||||
|
texthooker: {
|
||||||
|
service: texthookerService,
|
||||||
|
getPort: () => appState.texthookerPort,
|
||||||
|
setPort: (port: number) => {
|
||||||
|
appState.texthookerPort = port;
|
||||||
|
},
|
||||||
|
shouldOpenBrowser: () =>
|
||||||
|
getResolvedConfig().texthooker?.openBrowser !== false,
|
||||||
|
openInBrowser: (url: string) => {
|
||||||
|
void shell.openExternal(url).catch((error) => {
|
||||||
|
console.error(`Failed to open browser for texthooker URL: ${url}`, error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
overlay: {
|
||||||
|
isInitialized: () => appState.overlayRuntimeInitialized,
|
||||||
|
initialize: () => initializeOverlayRuntime(),
|
||||||
|
toggleVisible: () => toggleVisibleOverlay(),
|
||||||
|
toggleInvisible: () => toggleInvisibleOverlay(),
|
||||||
|
setVisible: (visible: boolean) => setVisibleOverlayVisible(visible),
|
||||||
|
setInvisible: (visible: boolean) => setInvisibleOverlayVisible(visible),
|
||||||
|
},
|
||||||
|
mining: {
|
||||||
|
copyCurrentSubtitle: () => copyCurrentSubtitle(),
|
||||||
|
startPendingMultiCopy: (timeoutMs: number) => startPendingMultiCopy(timeoutMs),
|
||||||
|
mineSentenceCard: () => mineSentenceCard(),
|
||||||
|
startPendingMineSentenceMultiple: (timeoutMs: number) =>
|
||||||
|
startPendingMineSentenceMultiple(timeoutMs),
|
||||||
|
updateLastCardFromClipboard: () => updateLastCardFromClipboard(),
|
||||||
|
triggerFieldGrouping: () => triggerFieldGrouping(),
|
||||||
|
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
||||||
|
markLastCardAsAudioCard: () => markLastCardAsAudioCard(),
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
openYomitanSettings: () => openYomitanSettings(),
|
||||||
|
cycleSecondarySubMode: () => cycleSecondarySubMode(),
|
||||||
|
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||||
|
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
stop: () => app.quit(),
|
||||||
|
hasMainWindow: () => Boolean(overlayManager.getMainWindow()),
|
||||||
|
},
|
||||||
|
getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs,
|
||||||
|
schedule: (fn: () => void, delayMs: number) => setTimeout(fn, delayMs),
|
||||||
|
log: (message: string) => {
|
||||||
|
console.log(message);
|
||||||
|
},
|
||||||
|
warn: (message: string) => {
|
||||||
|
console.warn(message);
|
||||||
|
},
|
||||||
|
error: (message: string, err: unknown) => {
|
||||||
|
console.error(message, err);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
handleCliCommandService(args, source, deps);
|
handleCliCommandService(args, source, deps);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCliCommandRuntimeServiceDeps(): CliCommandDepsRuntimeOptions {
|
|
||||||
return {
|
|
||||||
mpv: {
|
|
||||||
getSocketPath: () => appState.mpvSocketPath,
|
|
||||||
setSocketPath: (socketPath) => {
|
|
||||||
appState.mpvSocketPath = socketPath;
|
|
||||||
},
|
|
||||||
getClient: () => appState.mpvClient,
|
|
||||||
showOsd: (text) => showMpvOsd(text),
|
|
||||||
},
|
|
||||||
texthooker: {
|
|
||||||
service: texthookerService,
|
|
||||||
getPort: () => appState.texthookerPort,
|
|
||||||
setPort: (port) => {
|
|
||||||
appState.texthookerPort = port;
|
|
||||||
},
|
|
||||||
shouldOpenBrowser: () => getResolvedConfig().texthooker?.openBrowser !== false,
|
|
||||||
openInBrowser: (url) => {
|
|
||||||
void shell.openExternal(url).catch((error) => {
|
|
||||||
console.error(`Failed to open browser for texthooker URL: ${url}`, error);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
overlay: {
|
|
||||||
isInitialized: () => appState.overlayRuntimeInitialized,
|
|
||||||
initialize: () => initializeOverlayRuntime(),
|
|
||||||
toggleVisible: () => toggleVisibleOverlay(),
|
|
||||||
toggleInvisible: () => toggleInvisibleOverlay(),
|
|
||||||
setVisible: (visible) => setVisibleOverlayVisible(visible),
|
|
||||||
setInvisible: (visible) => setInvisibleOverlayVisible(visible),
|
|
||||||
},
|
|
||||||
mining: {
|
|
||||||
copyCurrentSubtitle: () => copyCurrentSubtitle(),
|
|
||||||
startPendingMultiCopy: (timeoutMs) => startPendingMultiCopy(timeoutMs),
|
|
||||||
mineSentenceCard: () => mineSentenceCard(),
|
|
||||||
startPendingMineSentenceMultiple: (timeoutMs) =>
|
|
||||||
startPendingMineSentenceMultiple(timeoutMs),
|
|
||||||
updateLastCardFromClipboard: () => updateLastCardFromClipboard(),
|
|
||||||
triggerFieldGrouping: () => triggerFieldGrouping(),
|
|
||||||
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
|
||||||
markLastCardAsAudioCard: () => markLastCardAsAudioCard(),
|
|
||||||
},
|
|
||||||
ui: {
|
|
||||||
openYomitanSettings: () => openYomitanSettings(),
|
|
||||||
cycleSecondarySubMode: () => cycleSecondarySubMode(),
|
|
||||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
|
||||||
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
|
||||||
},
|
|
||||||
app: {
|
|
||||||
stop: () => app.quit(),
|
|
||||||
hasMainWindow: () => Boolean(overlayManager.getMainWindow()),
|
|
||||||
},
|
|
||||||
getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs,
|
|
||||||
schedule: (fn, delayMs) => setTimeout(fn, delayMs),
|
|
||||||
log: (message) => {
|
|
||||||
console.log(message);
|
|
||||||
},
|
|
||||||
warn: (message) => {
|
|
||||||
console.warn(message);
|
|
||||||
},
|
|
||||||
error: (message, err) => {
|
|
||||||
console.error(message, err);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleInitialArgs(): void {
|
function handleInitialArgs(): void {
|
||||||
if (!appState.initialArgs) return;
|
if (!appState.initialArgs) return;
|
||||||
handleCliCommand(appState.initialArgs, "initial");
|
handleCliCommand(appState.initialArgs, "initial");
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindMpvClientEventHandlers(mpvClient: MpvIpcClient): void {
|
function bindMpvClientEventHandlers(mpvClient: MpvIpcClient): void {
|
||||||
|
mpvClient.on("subtitle-change", ({ text }) => {
|
||||||
|
appState.currentSubText = text;
|
||||||
|
subtitleWsService.broadcast(text);
|
||||||
|
void (async () => {
|
||||||
|
if (getOverlayWindows().length > 0) {
|
||||||
|
const subtitleData = await tokenizeSubtitle(text);
|
||||||
|
broadcastToOverlayWindows("subtitle:set", subtitleData);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
mpvClient.on("subtitle-ass-change", ({ text }) => {
|
||||||
|
appState.currentSubAssText = text;
|
||||||
|
broadcastToOverlayWindows("subtitle-ass:set", text);
|
||||||
|
});
|
||||||
|
mpvClient.on("secondary-subtitle-change", ({ text }) => {
|
||||||
|
broadcastToOverlayWindows("secondary-subtitle:set", text);
|
||||||
|
});
|
||||||
|
mpvClient.on("subtitle-timing", ({ text, start, end }) => {
|
||||||
|
if (!text.trim() || !appState.subtitleTimingTracker) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appState.subtitleTimingTracker.recordSubtitle(text, start, end);
|
||||||
|
});
|
||||||
mpvClient.on("media-path-change", ({ path }) => {
|
mpvClient.on("media-path-change", ({ path }) => {
|
||||||
updateCurrentMediaPath(path);
|
updateCurrentMediaPath(path);
|
||||||
});
|
});
|
||||||
mpvClient.on("media-title-change", ({ title }) => {
|
mpvClient.on("media-title-change", ({ title }) => {
|
||||||
updateCurrentMediaTitle(title);
|
updateCurrentMediaTitle(title);
|
||||||
});
|
});
|
||||||
mpvClient.on("subtitle-ass-change", ({ text }) => {
|
mpvClient.on("subtitle-metrics-change", ({ patch }) => {
|
||||||
appState.currentSubAssText = text;
|
updateMpvSubtitleRenderMetrics(patch);
|
||||||
});
|
});
|
||||||
mpvClient.on("subtitle-change", ({ text }) => {
|
mpvClient.on("secondary-subtitle-visibility", ({ visible }) => {
|
||||||
appState.currentSubText = text;
|
appState.previousSecondarySubVisibility = visible;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -886,41 +921,14 @@ function createMpvClientRuntimeService(): MpvIpcClient {
|
|||||||
const mpvClient = new MpvIpcClient(appState.mpvSocketPath, {
|
const mpvClient = new MpvIpcClient(appState.mpvSocketPath, {
|
||||||
getResolvedConfig: () => getResolvedConfig(),
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
autoStartOverlay: appState.autoStartOverlay,
|
autoStartOverlay: appState.autoStartOverlay,
|
||||||
setOverlayVisible: (visible) => setOverlayVisible(visible),
|
setOverlayVisible: (visible: boolean) => setOverlayVisible(visible),
|
||||||
shouldBindVisibleOverlayToMpvSubVisibility: () =>
|
shouldBindVisibleOverlayToMpvSubVisibility: () =>
|
||||||
shouldBindVisibleOverlayToMpvSubVisibility(),
|
shouldBindVisibleOverlayToMpvSubVisibility(),
|
||||||
isVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
isVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
getReconnectTimer: () => appState.reconnectTimer,
|
getReconnectTimer: () => appState.reconnectTimer,
|
||||||
setReconnectTimer: (timer) => {
|
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => {
|
||||||
appState.reconnectTimer = timer;
|
appState.reconnectTimer = timer;
|
||||||
},
|
},
|
||||||
getCurrentSubText: () => appState.currentSubText,
|
|
||||||
setCurrentSubText: (text) => {
|
|
||||||
appState.currentSubText = text;
|
|
||||||
},
|
|
||||||
setCurrentSubAssText: (text) => {
|
|
||||||
appState.currentSubAssText = text;
|
|
||||||
},
|
|
||||||
getSubtitleTimingTracker: () => appState.subtitleTimingTracker,
|
|
||||||
subtitleWsBroadcast: (text) => {
|
|
||||||
subtitleWsService.broadcast(text);
|
|
||||||
},
|
|
||||||
getOverlayWindowsCount: () => getOverlayWindows().length,
|
|
||||||
tokenizeSubtitle: (text) => tokenizeSubtitle(text),
|
|
||||||
broadcastToOverlayWindows: (channel, ...channelArgs) => {
|
|
||||||
broadcastToOverlayWindows(channel, ...channelArgs);
|
|
||||||
},
|
|
||||||
updateMpvSubtitleRenderMetrics: (patch) => {
|
|
||||||
updateMpvSubtitleRenderMetrics(patch);
|
|
||||||
},
|
|
||||||
getMpvSubtitleRenderMetrics: () => appState.mpvSubtitleRenderMetrics,
|
|
||||||
getPreviousSecondarySubVisibility: () => appState.previousSecondarySubVisibility,
|
|
||||||
setPreviousSecondarySubVisibility: (value) => {
|
|
||||||
appState.previousSecondarySubVisibility = value;
|
|
||||||
},
|
|
||||||
showMpvOsd: (text) => {
|
|
||||||
showMpvOsd(text);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
bindMpvClientEventHandlers(mpvClient);
|
bindMpvClientEventHandlers(mpvClient);
|
||||||
return mpvClient;
|
return mpvClient;
|
||||||
@@ -1085,6 +1093,7 @@ function initializeOverlayRuntime(): void {
|
|||||||
getResolvedConfig: () => getResolvedConfig(),
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
getSubtitleTimingTracker: () => appState.subtitleTimingTracker,
|
getSubtitleTimingTracker: () => appState.subtitleTimingTracker,
|
||||||
getMpvClient: () => appState.mpvClient,
|
getMpvClient: () => appState.mpvClient,
|
||||||
|
getMpvSocketPath: () => appState.mpvSocketPath,
|
||||||
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
||||||
setAnkiIntegration: (integration) => {
|
setAnkiIntegration: (integration) => {
|
||||||
appState.ankiIntegration = integration as AnkiIntegration | null;
|
appState.ankiIntegration = integration as AnkiIntegration | null;
|
||||||
@@ -1126,7 +1135,7 @@ function getConfiguredShortcuts() { return resolveConfiguredShortcuts(getResolve
|
|||||||
function getOverlayShortcutRuntimeHandlers() {
|
function getOverlayShortcutRuntimeHandlers() {
|
||||||
return createOverlayShortcutRuntimeHandlers(
|
return createOverlayShortcutRuntimeHandlers(
|
||||||
{
|
{
|
||||||
showMpvOsd: (text) => showMpvOsd(text),
|
showMpvOsd: (text: string) => showMpvOsd(text),
|
||||||
openRuntimeOptions: () => {
|
openRuntimeOptions: () => {
|
||||||
openRuntimeOptionsPalette();
|
openRuntimeOptionsPalette();
|
||||||
},
|
},
|
||||||
@@ -1136,7 +1145,7 @@ function getOverlayShortcutRuntimeHandlers() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
markAudioCard: () => markLastCardAsAudioCard(),
|
markAudioCard: () => markLastCardAsAudioCard(),
|
||||||
copySubtitleMultiple: (timeoutMs) => {
|
copySubtitleMultiple: (timeoutMs: number) => {
|
||||||
startPendingMultiCopy(timeoutMs);
|
startPendingMultiCopy(timeoutMs);
|
||||||
},
|
},
|
||||||
copySubtitle: () => {
|
copySubtitle: () => {
|
||||||
@@ -1147,7 +1156,7 @@ function getOverlayShortcutRuntimeHandlers() {
|
|||||||
triggerFieldGrouping: () => triggerFieldGrouping(),
|
triggerFieldGrouping: () => triggerFieldGrouping(),
|
||||||
triggerSubsync: () => triggerSubsyncFromConfig(),
|
triggerSubsync: () => triggerSubsyncFromConfig(),
|
||||||
mineSentence: () => mineSentenceCard(),
|
mineSentence: () => mineSentenceCard(),
|
||||||
mineSentenceMultiple: (timeoutMs) => {
|
mineSentenceMultiple: (timeoutMs: number) => {
|
||||||
startPendingMineSentenceMultiple(timeoutMs);
|
startPendingMineSentenceMultiple(timeoutMs);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1495,31 +1504,30 @@ function handleOverlayModalClosed(modal: OverlayHostedModal): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleMpvCommandFromIpc(command: (string | number)[]): void {
|
function handleMpvCommandFromIpc(command: (string | number)[]): void {
|
||||||
handleMpvCommandFromIpcService(command, createMpvCommandRuntimeServiceDeps());
|
handleMpvCommandFromIpcService(
|
||||||
}
|
command,
|
||||||
|
createMpvCommandRuntimeServiceDeps({
|
||||||
function createMpvCommandRuntimeServiceDeps(): HandleMpvCommandFromIpcOptions {
|
specialCommands: SPECIAL_COMMANDS,
|
||||||
return {
|
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
||||||
specialCommands: SPECIAL_COMMANDS,
|
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||||
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
runtimeOptionsCycle: (id: RuntimeOptionId, direction: 1 | -1) => {
|
||||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
if (!appState.runtimeOptionsManager) {
|
||||||
runtimeOptionsCycle: (id, direction) => {
|
return { ok: false, error: "Runtime options manager unavailable" };
|
||||||
if (!appState.runtimeOptionsManager) {
|
}
|
||||||
return { ok: false, error: "Runtime options manager unavailable" };
|
return applyRuntimeOptionResultRuntimeService(
|
||||||
}
|
appState.runtimeOptionsManager.cycleOption(id, direction),
|
||||||
return applyRuntimeOptionResultRuntimeService(
|
(text) => showMpvOsd(text),
|
||||||
appState.runtimeOptionsManager.cycleOption(id, direction),
|
);
|
||||||
(text) => showMpvOsd(text),
|
},
|
||||||
);
|
showMpvOsd: (text: string) => showMpvOsd(text),
|
||||||
},
|
mpvReplaySubtitle: () => replayCurrentSubtitleRuntimeService(appState.mpvClient),
|
||||||
showMpvOsd: (text) => showMpvOsd(text),
|
mpvPlayNextSubtitle: () => playNextSubtitleRuntimeService(appState.mpvClient),
|
||||||
mpvReplaySubtitle: () => replayCurrentSubtitleRuntimeService(appState.mpvClient),
|
mpvSendCommand: (rawCommand: (string | number)[]) =>
|
||||||
mpvPlayNextSubtitle: () => playNextSubtitleRuntimeService(appState.mpvClient),
|
sendMpvCommandRuntimeService(appState.mpvClient, rawCommand),
|
||||||
mpvSendCommand: (rawCommand) =>
|
isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected),
|
||||||
sendMpvCommandRuntimeService(appState.mpvClient, rawCommand),
|
hasRuntimeOptionsManager: () => appState.runtimeOptionsManager !== null,
|
||||||
isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected),
|
}),
|
||||||
hasRuntimeOptionsManager: () => appState.runtimeOptionsManager !== null,
|
);
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runSubsyncManualFromIpc(
|
async function runSubsyncManualFromIpc(
|
||||||
@@ -1534,52 +1542,47 @@ const runtimeOptionsIpcDeps = createRuntimeOptionsIpcDeps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
registerIpcHandlersService(
|
registerIpcHandlersService(
|
||||||
createIpcDepsRuntimeService(createMainIpcRuntimeServiceDeps()),
|
createIpcDepsRuntimeService(
|
||||||
|
createMainIpcRuntimeServiceDeps({
|
||||||
|
getInvisibleWindow: () => overlayManager.getInvisibleWindow(),
|
||||||
|
getMainWindow: () => overlayManager.getMainWindow(),
|
||||||
|
getVisibleOverlayVisibility: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
|
getInvisibleOverlayVisibility: () => overlayManager.getInvisibleOverlayVisible(),
|
||||||
|
onOverlayModalClosed: (modal) => {
|
||||||
|
handleOverlayModalClosed(modal as OverlayHostedModal);
|
||||||
|
},
|
||||||
|
openYomitanSettings: () => openYomitanSettings(),
|
||||||
|
quitApp: () => app.quit(),
|
||||||
|
toggleVisibleOverlay: () => toggleVisibleOverlay(),
|
||||||
|
tokenizeCurrentSubtitle: () => tokenizeSubtitle(appState.currentSubText),
|
||||||
|
getCurrentSubtitleAss: () => appState.currentSubAssText,
|
||||||
|
getMpvSubtitleRenderMetrics: () => appState.mpvSubtitleRenderMetrics,
|
||||||
|
getSubtitlePosition: () => loadSubtitlePosition(),
|
||||||
|
getSubtitleStyle: () => getResolvedConfig().subtitleStyle ?? null,
|
||||||
|
saveSubtitlePosition: (position: unknown) =>
|
||||||
|
saveSubtitlePosition(position as SubtitlePosition),
|
||||||
|
getMecabTokenizer: () => appState.mecabTokenizer,
|
||||||
|
handleMpvCommand: (command: (string | number)[]) =>
|
||||||
|
handleMpvCommandFromIpc(command),
|
||||||
|
getKeybindings: () => appState.keybindings,
|
||||||
|
getSecondarySubMode: () => appState.secondarySubMode,
|
||||||
|
getMpvClient: () => appState.mpvClient,
|
||||||
|
runSubsyncManual: (request: unknown) =>
|
||||||
|
runSubsyncManualFromIpc(request as SubsyncManualRunRequest),
|
||||||
|
getAnkiConnectStatus: () => appState.ankiIntegration !== null,
|
||||||
|
getRuntimeOptions: () => getRuntimeOptionsState(),
|
||||||
|
setRuntimeOption: runtimeOptionsIpcDeps.setRuntimeOption,
|
||||||
|
cycleRuntimeOption: runtimeOptionsIpcDeps.cycleRuntimeOption,
|
||||||
|
reportOverlayContentBounds: (payload: unknown) => {
|
||||||
|
overlayContentMeasurementStore.report(payload);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
registerAnkiJimakuIpcRuntimeService(
|
registerAnkiJimakuIpcRuntimeService(
|
||||||
createAnkiJimakuIpcRuntimeServiceDeps(),
|
createAnkiJimakuIpcRuntimeServiceDeps({
|
||||||
);
|
patchAnkiConnectEnabled: (enabled: boolean) => {
|
||||||
|
|
||||||
function createMainIpcRuntimeServiceDeps(): IpcDepsRuntimeOptions {
|
|
||||||
return {
|
|
||||||
getInvisibleWindow: () => overlayManager.getInvisibleWindow(),
|
|
||||||
getMainWindow: () => overlayManager.getMainWindow(),
|
|
||||||
getVisibleOverlayVisibility: () => overlayManager.getVisibleOverlayVisible(),
|
|
||||||
getInvisibleOverlayVisibility: () =>
|
|
||||||
overlayManager.getInvisibleOverlayVisible(),
|
|
||||||
onOverlayModalClosed: (modal) =>
|
|
||||||
handleOverlayModalClosed(modal as OverlayHostedModal),
|
|
||||||
openYomitanSettings: () => openYomitanSettings(),
|
|
||||||
quitApp: () => app.quit(),
|
|
||||||
toggleVisibleOverlay: () => toggleVisibleOverlay(),
|
|
||||||
tokenizeCurrentSubtitle: () => tokenizeSubtitle(appState.currentSubText),
|
|
||||||
getCurrentSubtitleAss: () => appState.currentSubAssText,
|
|
||||||
getMpvSubtitleRenderMetrics: () => appState.mpvSubtitleRenderMetrics,
|
|
||||||
getSubtitlePosition: () => loadSubtitlePosition(),
|
|
||||||
getSubtitleStyle: () => getResolvedConfig().subtitleStyle ?? null,
|
|
||||||
saveSubtitlePosition: (position) =>
|
|
||||||
saveSubtitlePosition(position as SubtitlePosition),
|
|
||||||
getMecabTokenizer: () => appState.mecabTokenizer,
|
|
||||||
handleMpvCommand: (command) => handleMpvCommandFromIpc(command),
|
|
||||||
getKeybindings: () => appState.keybindings,
|
|
||||||
getSecondarySubMode: () => appState.secondarySubMode,
|
|
||||||
getMpvClient: () => appState.mpvClient,
|
|
||||||
runSubsyncManual: (request) =>
|
|
||||||
runSubsyncManualFromIpc(request as SubsyncManualRunRequest),
|
|
||||||
getAnkiConnectStatus: () => appState.ankiIntegration !== null,
|
|
||||||
getRuntimeOptions: () => getRuntimeOptionsState(),
|
|
||||||
setRuntimeOption: runtimeOptionsIpcDeps.setRuntimeOption,
|
|
||||||
cycleRuntimeOption: runtimeOptionsIpcDeps.cycleRuntimeOption,
|
|
||||||
reportOverlayContentBounds: (payload) => {
|
|
||||||
overlayContentMeasurementStore.report(payload);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createAnkiJimakuIpcRuntimeServiceDeps(): AnkiJimakuIpcRuntimeOptions {
|
|
||||||
return {
|
|
||||||
patchAnkiConnectEnabled: (enabled) => {
|
|
||||||
configService.patchRawConfig({ ankiConnect: { enabled } });
|
configService.patchRawConfig({ ankiConnect: { enabled } });
|
||||||
},
|
},
|
||||||
getResolvedConfig: () => getResolvedConfig(),
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
@@ -1587,25 +1590,32 @@ function createAnkiJimakuIpcRuntimeServiceDeps(): AnkiJimakuIpcRuntimeOptions {
|
|||||||
getSubtitleTimingTracker: () => appState.subtitleTimingTracker,
|
getSubtitleTimingTracker: () => appState.subtitleTimingTracker,
|
||||||
getMpvClient: () => appState.mpvClient,
|
getMpvClient: () => appState.mpvClient,
|
||||||
getAnkiIntegration: () => appState.ankiIntegration,
|
getAnkiIntegration: () => appState.ankiIntegration,
|
||||||
setAnkiIntegration: (integration) => {
|
setAnkiIntegration: (integration: AnkiIntegration | null) => {
|
||||||
appState.ankiIntegration = integration;
|
appState.ankiIntegration = integration;
|
||||||
},
|
},
|
||||||
showDesktopNotification,
|
showDesktopNotification,
|
||||||
createFieldGroupingCallback: () => createFieldGroupingCallback(),
|
createFieldGroupingCallback: () => createFieldGroupingCallback(),
|
||||||
broadcastRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
|
broadcastRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
|
||||||
getFieldGroupingResolver: () => getFieldGroupingResolver(),
|
getFieldGroupingResolver: () => getFieldGroupingResolver(),
|
||||||
setFieldGroupingResolver: (resolver) => setFieldGroupingResolver(resolver),
|
setFieldGroupingResolver: (
|
||||||
parseMediaInfo: (mediaPath) => parseMediaInfo(resolveMediaPathForJimaku(mediaPath)),
|
resolver: ((choice: KikuFieldGroupingChoice) => void) | null,
|
||||||
|
) => setFieldGroupingResolver(resolver),
|
||||||
|
parseMediaInfo: (mediaPath: string | null) =>
|
||||||
|
parseMediaInfo(resolveMediaPathForJimaku(mediaPath)),
|
||||||
getCurrentMediaPath: () => appState.currentMediaPath,
|
getCurrentMediaPath: () => appState.currentMediaPath,
|
||||||
jimakuFetchJson: <T>(
|
jimakuFetchJson: <T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
query?: Record<string, string | number | boolean | null | undefined>,
|
query?: Record<string, string | number | boolean | null | undefined>,
|
||||||
): Promise<JimakuApiResponse<T>> => jimakuFetchJson(endpoint, query),
|
): Promise<JimakuApiResponse<T>> =>
|
||||||
|
jimakuFetchJson<T>(endpoint, query),
|
||||||
getJimakuMaxEntryResults: () => getJimakuMaxEntryResults(),
|
getJimakuMaxEntryResults: () => getJimakuMaxEntryResults(),
|
||||||
getJimakuLanguagePreference: () => getJimakuLanguagePreference(),
|
getJimakuLanguagePreference: () => getJimakuLanguagePreference(),
|
||||||
resolveJimakuApiKey: () => resolveJimakuApiKey(),
|
resolveJimakuApiKey: () => resolveJimakuApiKey(),
|
||||||
isRemoteMediaPath: (mediaPath) => isRemoteMediaPath(mediaPath),
|
isRemoteMediaPath: (mediaPath: string) => isRemoteMediaPath(mediaPath),
|
||||||
downloadToFile: (url, destPath, headers) =>
|
downloadToFile: (
|
||||||
downloadToFile(url, destPath, headers),
|
url: string,
|
||||||
};
|
destPath: string,
|
||||||
}
|
headers: Record<string, string>,
|
||||||
|
) => downloadToFile(url, destPath, headers),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|||||||
@@ -27,11 +27,18 @@ interface HyprlandClient {
|
|||||||
class: string;
|
class: string;
|
||||||
at: [number, number];
|
at: [number, number];
|
||||||
size: [number, number];
|
size: [number, number];
|
||||||
|
pid?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HyprlandWindowTracker extends BaseWindowTracker {
|
export class HyprlandWindowTracker extends BaseWindowTracker {
|
||||||
private pollInterval: ReturnType<typeof setInterval> | null = null;
|
private pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
private eventSocket: net.Socket | null = null;
|
private eventSocket: net.Socket | null = null;
|
||||||
|
private readonly targetMpvSocketPath: string | null;
|
||||||
|
|
||||||
|
constructor(targetMpvSocketPath?: string) {
|
||||||
|
super();
|
||||||
|
this.targetMpvSocketPath = targetMpvSocketPath?.trim() || null;
|
||||||
|
}
|
||||||
|
|
||||||
start(): void {
|
start(): void {
|
||||||
this.pollInterval = setInterval(() => this.pollGeometry(), 250);
|
this.pollInterval = setInterval(() => this.pollGeometry(), 250);
|
||||||
@@ -95,7 +102,7 @@ export class HyprlandWindowTracker extends BaseWindowTracker {
|
|||||||
try {
|
try {
|
||||||
const output = execSync("hyprctl clients -j", { encoding: "utf-8" });
|
const output = execSync("hyprctl clients -j", { encoding: "utf-8" });
|
||||||
const clients: HyprlandClient[] = JSON.parse(output);
|
const clients: HyprlandClient[] = JSON.parse(output);
|
||||||
const mpvWindow = clients.find((c) => c.class === "mpv");
|
const mpvWindow = this.findTargetWindow(clients);
|
||||||
|
|
||||||
if (mpvWindow) {
|
if (mpvWindow) {
|
||||||
this.updateGeometry({
|
this.updateGeometry({
|
||||||
@@ -111,4 +118,38 @@ export class HyprlandWindowTracker extends BaseWindowTracker {
|
|||||||
// hyprctl not available or failed - silent fail
|
// hyprctl not available or failed - silent fail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private findTargetWindow(clients: HyprlandClient[]): HyprlandClient | null {
|
||||||
|
const mpvWindows = clients.filter((client) => client.class === "mpv");
|
||||||
|
if (!this.targetMpvSocketPath) {
|
||||||
|
return mpvWindows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const mpvWindow of mpvWindows) {
|
||||||
|
if (!mpvWindow.pid) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandLine = this.getWindowCommandLine(mpvWindow.pid);
|
||||||
|
if (!commandLine) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
commandLine.includes(`--input-ipc-server=${this.targetMpvSocketPath}`) ||
|
||||||
|
commandLine.includes(`--input-ipc-server ${this.targetMpvSocketPath}`)
|
||||||
|
) {
|
||||||
|
return mpvWindow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getWindowCommandLine(pid: number): string | null {
|
||||||
|
const commandLine = execSync(`ps -p ${pid} -o args=`, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
}).trim();
|
||||||
|
return commandLine || null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ function normalizeCompositor(value: string): Compositor | null {
|
|||||||
|
|
||||||
export function createWindowTracker(
|
export function createWindowTracker(
|
||||||
override?: string | null,
|
override?: string | null,
|
||||||
|
targetMpvSocketPath?: string | null,
|
||||||
): BaseWindowTracker | null {
|
): BaseWindowTracker | null {
|
||||||
let compositor = detectCompositor();
|
let compositor = detectCompositor();
|
||||||
|
|
||||||
@@ -64,13 +65,21 @@ export function createWindowTracker(
|
|||||||
|
|
||||||
switch (compositor) {
|
switch (compositor) {
|
||||||
case "hyprland":
|
case "hyprland":
|
||||||
return new HyprlandWindowTracker();
|
return new HyprlandWindowTracker(
|
||||||
|
targetMpvSocketPath?.trim() || undefined,
|
||||||
|
);
|
||||||
case "sway":
|
case "sway":
|
||||||
return new SwayWindowTracker();
|
return new SwayWindowTracker(
|
||||||
|
targetMpvSocketPath?.trim() || undefined,
|
||||||
|
);
|
||||||
case "x11":
|
case "x11":
|
||||||
return new X11WindowTracker();
|
return new X11WindowTracker(
|
||||||
|
targetMpvSocketPath?.trim() || undefined,
|
||||||
|
);
|
||||||
case "macos":
|
case "macos":
|
||||||
return new MacOSWindowTracker();
|
return new MacOSWindowTracker(
|
||||||
|
targetMpvSocketPath?.trim() || undefined,
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
log.warn("No supported compositor detected. Window tracking disabled.");
|
log.warn("No supported compositor detected. Window tracking disabled.");
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -32,9 +32,11 @@ export class MacOSWindowTracker extends BaseWindowTracker {
|
|||||||
private helperType: "binary" | "swift" | null = null;
|
private helperType: "binary" | "swift" | null = null;
|
||||||
private lastExecErrorFingerprint: string | null = null;
|
private lastExecErrorFingerprint: string | null = null;
|
||||||
private lastExecErrorLoggedAtMs = 0;
|
private lastExecErrorLoggedAtMs = 0;
|
||||||
|
private readonly targetMpvSocketPath: string | null;
|
||||||
|
|
||||||
constructor() {
|
constructor(targetMpvSocketPath?: string) {
|
||||||
super();
|
super();
|
||||||
|
this.targetMpvSocketPath = targetMpvSocketPath?.trim() || null;
|
||||||
this.detectHelper();
|
this.detectHelper();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +87,21 @@ export class MacOSWindowTracker extends BaseWindowTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private detectHelper(): void {
|
private detectHelper(): void {
|
||||||
|
const shouldFilterBySocket = this.targetMpvSocketPath !== null;
|
||||||
|
|
||||||
|
// Fall back to Swift helper first when filtering by socket path to avoid
|
||||||
|
// stale prebuilt binaries that don't support the new socket filter argument.
|
||||||
|
const swiftPath = path.join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"scripts",
|
||||||
|
"get-mpv-window-macos.swift",
|
||||||
|
);
|
||||||
|
if (shouldFilterBySocket && this.tryUseHelper(swiftPath, "swift")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Prefer resources path (outside asar) in packaged apps.
|
// Prefer resources path (outside asar) in packaged apps.
|
||||||
const resourcesPath = process.resourcesPath;
|
const resourcesPath = process.resourcesPath;
|
||||||
if (resourcesPath) {
|
if (resourcesPath) {
|
||||||
@@ -110,14 +127,8 @@ export class MacOSWindowTracker extends BaseWindowTracker {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to Swift script for development.
|
// Fall back to Swift script for development or if binary filtering is not
|
||||||
const swiftPath = path.join(
|
// supported in the current environment.
|
||||||
__dirname,
|
|
||||||
"..",
|
|
||||||
"..",
|
|
||||||
"scripts",
|
|
||||||
"get-mpv-window-macos.swift",
|
|
||||||
);
|
|
||||||
if (this.tryUseHelper(swiftPath, "swift")) {
|
if (this.tryUseHelper(swiftPath, "swift")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -167,6 +178,9 @@ export class MacOSWindowTracker extends BaseWindowTracker {
|
|||||||
// This works with both bundled and unbundled mpv installations
|
// This works with both bundled and unbundled mpv installations
|
||||||
const command = this.helperType === "binary" ? this.helperPath : "swift";
|
const command = this.helperType === "binary" ? this.helperPath : "swift";
|
||||||
const args = this.helperType === "binary" ? [] : [this.helperPath];
|
const args = this.helperType === "binary" ? [] : [this.helperPath];
|
||||||
|
if (this.targetMpvSocketPath) {
|
||||||
|
args.push(this.targetMpvSocketPath);
|
||||||
|
}
|
||||||
|
|
||||||
execFile(
|
execFile(
|
||||||
command,
|
command,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ interface SwayRect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SwayNode {
|
interface SwayNode {
|
||||||
|
pid?: number;
|
||||||
app_id?: string;
|
app_id?: string;
|
||||||
window_properties?: { class?: string };
|
window_properties?: { class?: string };
|
||||||
rect?: SwayRect;
|
rect?: SwayRect;
|
||||||
@@ -36,6 +37,12 @@ interface SwayNode {
|
|||||||
|
|
||||||
export class SwayWindowTracker extends BaseWindowTracker {
|
export class SwayWindowTracker extends BaseWindowTracker {
|
||||||
private pollInterval: ReturnType<typeof setInterval> | null = null;
|
private pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
private readonly targetMpvSocketPath: string | null;
|
||||||
|
|
||||||
|
constructor(targetMpvSocketPath?: string) {
|
||||||
|
super();
|
||||||
|
this.targetMpvSocketPath = targetMpvSocketPath?.trim() || null;
|
||||||
|
}
|
||||||
|
|
||||||
start(): void {
|
start(): void {
|
||||||
this.pollInterval = setInterval(() => this.pollGeometry(), 250);
|
this.pollInterval = setInterval(() => this.pollGeometry(), 250);
|
||||||
@@ -49,33 +56,66 @@ export class SwayWindowTracker extends BaseWindowTracker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private findMpvWindow(node: SwayNode): SwayNode | null {
|
private collectMpvWindows(node: SwayNode): SwayNode[] {
|
||||||
|
const windows: SwayNode[] = [];
|
||||||
if (node.app_id === "mpv" || node.window_properties?.class === "mpv") {
|
if (node.app_id === "mpv" || node.window_properties?.class === "mpv") {
|
||||||
return node;
|
windows.push(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.nodes) {
|
if (node.nodes) {
|
||||||
for (const child of node.nodes) {
|
for (const child of node.nodes) {
|
||||||
const found = this.findMpvWindow(child);
|
windows.push(...this.collectMpvWindows(child));
|
||||||
if (found) return found;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.floating_nodes) {
|
if (node.floating_nodes) {
|
||||||
for (const child of node.floating_nodes) {
|
for (const child of node.floating_nodes) {
|
||||||
const found = this.findMpvWindow(child);
|
windows.push(...this.collectMpvWindows(child));
|
||||||
if (found) return found;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return windows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private findTargetSocketWindow(node: SwayNode): SwayNode | null {
|
||||||
|
const windows = this.collectMpvWindows(node);
|
||||||
|
if (!this.targetMpvSocketPath) {
|
||||||
|
return windows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return windows.find((candidate) =>
|
||||||
|
this.isWindowForTargetSocket(candidate),
|
||||||
|
) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isWindowForTargetSocket(node: SwayNode): boolean {
|
||||||
|
if (!node.pid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandLine = this.getWindowCommandLine(node.pid);
|
||||||
|
if (!commandLine) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
commandLine.includes(`--input-ipc-server=${this.targetMpvSocketPath}`) ||
|
||||||
|
commandLine.includes(`--input-ipc-server ${this.targetMpvSocketPath}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getWindowCommandLine(pid: number): string | null {
|
||||||
|
const commandLine = execSync(`ps -p ${pid} -o args=`, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
}).trim();
|
||||||
|
return commandLine || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private pollGeometry(): void {
|
private pollGeometry(): void {
|
||||||
try {
|
try {
|
||||||
const output = execSync("swaymsg -t get_tree", { encoding: "utf-8" });
|
const output = execSync("swaymsg -t get_tree", { encoding: "utf-8" });
|
||||||
const tree: SwayNode = JSON.parse(output);
|
const tree: SwayNode = JSON.parse(output);
|
||||||
const mpvWindow = this.findMpvWindow(tree);
|
const mpvWindow = this.findTargetSocketWindow(tree);
|
||||||
|
|
||||||
if (mpvWindow && mpvWindow.rect) {
|
if (mpvWindow && mpvWindow.rect) {
|
||||||
this.updateGeometry({
|
this.updateGeometry({
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ import { BaseWindowTracker } from "./base-tracker";
|
|||||||
|
|
||||||
export class X11WindowTracker extends BaseWindowTracker {
|
export class X11WindowTracker extends BaseWindowTracker {
|
||||||
private pollInterval: ReturnType<typeof setInterval> | null = null;
|
private pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
private readonly targetMpvSocketPath: string | null;
|
||||||
|
|
||||||
|
constructor(targetMpvSocketPath?: string) {
|
||||||
|
super();
|
||||||
|
this.targetMpvSocketPath = targetMpvSocketPath?.trim() || null;
|
||||||
|
}
|
||||||
|
|
||||||
start(): void {
|
start(): void {
|
||||||
this.pollInterval = setInterval(() => this.pollGeometry(), 250);
|
this.pollInterval = setInterval(() => this.pollGeometry(), 250);
|
||||||
@@ -45,7 +51,17 @@ export class X11WindowTracker extends BaseWindowTracker {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const windowId = windowIds.split("\n")[0];
|
const windowIdList = windowIds.split(/\s+/).filter(Boolean);
|
||||||
|
if (windowIdList.length === 0) {
|
||||||
|
this.updateGeometry(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const windowId = this.findTargetWindowId(windowIdList);
|
||||||
|
if (!windowId) {
|
||||||
|
this.updateGeometry(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const winInfo = execSync(`xwininfo -id ${windowId}`, {
|
const winInfo = execSync(`xwininfo -id ${windowId}`, {
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
@@ -70,4 +86,55 @@ export class X11WindowTracker extends BaseWindowTracker {
|
|||||||
this.updateGeometry(null);
|
this.updateGeometry(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private findTargetWindowId(windowIds: string[]): string | null {
|
||||||
|
if (!this.targetMpvSocketPath) {
|
||||||
|
return windowIds[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const windowId of windowIds) {
|
||||||
|
if (this.isWindowForTargetSocket(windowId)) {
|
||||||
|
return windowId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isWindowForTargetSocket(windowId: string): boolean {
|
||||||
|
const pid = this.getWindowPid(windowId);
|
||||||
|
if (pid === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandLine = this.getWindowCommandLine(pid);
|
||||||
|
if (!commandLine) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
commandLine.includes(`--input-ipc-server=${this.targetMpvSocketPath}`) ||
|
||||||
|
commandLine.includes(`--input-ipc-server ${this.targetMpvSocketPath}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getWindowPid(windowId: string): number | null {
|
||||||
|
const windowPid = execSync(`xprop -id ${windowId} _NET_WM_PID`, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
});
|
||||||
|
const pidMatch = windowPid.match(/= (\d+)/);
|
||||||
|
if (!pidMatch) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pid = Number.parseInt(pidMatch[1], 10);
|
||||||
|
return Number.isInteger(pid) ? pid : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getWindowCommandLine(pid: number): string | null {
|
||||||
|
const commandLine = execSync(`ps -p ${pid} -o args=`, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
}).trim();
|
||||||
|
return commandLine || null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user