mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00:55:16 -07:00
Fix Windows mpv handoff and tray setup (#82)
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Fixed Windows managed mpv launches from a background SubMiner instance so the existing warm app receives the start command, retargets the new mpv socket, binds to the player window, and receives startup overlay options.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: tray
|
||||||
|
|
||||||
|
- Fixed the Windows tray "Open SubMiner Setup" action so it opens the setup window after first-run setup is already complete.
|
||||||
@@ -256,6 +256,7 @@ test('buildConfiguredMpvDefaultArgs appends maximized launch mode to configured
|
|||||||
'--sub-file-paths=.;subs;subtitles',
|
'--sub-file-paths=.;subs;subtitles',
|
||||||
'--sid=auto',
|
'--sid=auto',
|
||||||
'--secondary-sid=auto',
|
'--secondary-sid=auto',
|
||||||
|
'--sub-visibility=no',
|
||||||
'--secondary-sub-visibility=no',
|
'--secondary-sub-visibility=no',
|
||||||
'--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
'--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
||||||
'--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
'--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
||||||
|
|||||||
+13
-4
@@ -47,7 +47,10 @@ type SpawnTarget = {
|
|||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PathModule = Pick<typeof path, 'join' | 'extname' | 'delimiter' | 'sep' | 'resolve'>;
|
type PathModule = Pick<
|
||||||
|
typeof path,
|
||||||
|
'dirname' | 'join' | 'extname' | 'delimiter' | 'sep' | 'resolve' | 'isAbsolute' | 'normalize'
|
||||||
|
>;
|
||||||
|
|
||||||
const DETACHED_IDLE_MPV_PID_FILE = path.join(os.tmpdir(), 'subminer-idle-mpv.pid');
|
const DETACHED_IDLE_MPV_PID_FILE = path.join(os.tmpdir(), 'subminer-idle-mpv.pid');
|
||||||
const OVERLAY_START_SOCKET_READY_TIMEOUT_MS = 900;
|
const OVERLAY_START_SOCKET_READY_TIMEOUT_MS = 900;
|
||||||
@@ -62,6 +65,12 @@ export interface LauncherRuntimePluginPlan {
|
|||||||
errorMessage: string | null;
|
errorMessage: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolvePluginCandidatePath(candidate: string, pathModule: PathModule): string {
|
||||||
|
return pathModule.isAbsolute(candidate)
|
||||||
|
? pathModule.normalize(candidate)
|
||||||
|
: pathModule.resolve(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
export function parseMpvArgString(input: string): string[] {
|
export function parseMpvArgString(input: string): string[] {
|
||||||
const chars = input;
|
const chars = input;
|
||||||
const args: string[] = [];
|
const args: string[] = [];
|
||||||
@@ -291,12 +300,12 @@ export function resolveLauncherRuntimePluginPath(options: {
|
|||||||
pathModule?: typeof path;
|
pathModule?: typeof path;
|
||||||
existsSync?: (candidate: string) => boolean;
|
existsSync?: (candidate: string) => boolean;
|
||||||
}): string | null {
|
}): string | null {
|
||||||
|
const platform = options.platform ?? process.platform;
|
||||||
const pathModule = options.pathModule ?? path;
|
const pathModule = options.pathModule ?? path;
|
||||||
const existsSync = options.existsSync ?? fs.existsSync;
|
const existsSync = options.existsSync ?? fs.existsSync;
|
||||||
const env = options.env ?? process.env;
|
const env = options.env ?? process.env;
|
||||||
const dirname = options.dirname ?? __dirname;
|
const dirname = options.dirname ?? __dirname;
|
||||||
const cwd = options.cwd ?? process.cwd();
|
const cwd = options.cwd ?? process.cwd();
|
||||||
const platform = options.platform ?? process.platform;
|
|
||||||
const homeDir = options.homeDir ?? os.homedir();
|
const homeDir = options.homeDir ?? os.homedir();
|
||||||
const candidates: string[] = [];
|
const candidates: string[] = [];
|
||||||
|
|
||||||
@@ -344,7 +353,7 @@ export function resolveLauncherRuntimePluginPath(options: {
|
|||||||
|
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
const resolved = pathModule.resolve(candidate);
|
const resolved = resolvePluginCandidatePath(candidate, pathModule);
|
||||||
if (seen.has(resolved)) continue;
|
if (seen.has(resolved)) continue;
|
||||||
seen.add(resolved);
|
seen.add(resolved);
|
||||||
const entrypoint = normalizeRuntimePluginEntrypoint(resolved, { pathModule, existsSync });
|
const entrypoint = normalizeRuntimePluginEntrypoint(resolved, { pathModule, existsSync });
|
||||||
@@ -1704,7 +1713,7 @@ export async function waitForUnixSocketReady(
|
|||||||
const deadline = nowMs() + timeoutMs;
|
const deadline = nowMs() + timeoutMs;
|
||||||
while (nowMs() < deadline) {
|
while (nowMs() < deadline) {
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(socketPath)) {
|
if (process.platform === 'win32' || fs.existsSync(socketPath)) {
|
||||||
const ready = await canConnectUnixSocket(socketPath);
|
const ready = await canConnectUnixSocket(socketPath);
|
||||||
if (ready) return true;
|
if (ready) return true;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -365,8 +365,8 @@ export function findRofiTheme(scriptPath: string): string | null {
|
|||||||
} else {
|
} else {
|
||||||
const xdgDataHome = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local/share');
|
const xdgDataHome = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local/share');
|
||||||
candidates.push(path.join(xdgDataHome, 'SubMiner/themes', ROFI_THEME_FILE));
|
candidates.push(path.join(xdgDataHome, 'SubMiner/themes', ROFI_THEME_FILE));
|
||||||
candidates.push(path.join('/usr/local/share/SubMiner/themes', ROFI_THEME_FILE));
|
candidates.push(path.posix.join('/usr/local/share/SubMiner/themes', ROFI_THEME_FILE));
|
||||||
candidates.push(path.join('/usr/share/SubMiner/themes', ROFI_THEME_FILE));
|
candidates.push(path.posix.join('/usr/share/SubMiner/themes', ROFI_THEME_FILE));
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates.push(path.join(scriptDir, 'assets', 'themes', ROFI_THEME_FILE));
|
candidates.push(path.join(scriptDir, 'assets', 'themes', ROFI_THEME_FILE));
|
||||||
|
|||||||
@@ -40,6 +40,19 @@ function writeExecutable(filePath: string, body: string): void {
|
|||||||
fs.chmodSync(filePath, 0o755);
|
fs.chmodSync(filePath, 0o755);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function writeFixtureExecutable(basePath: string, body: string): string {
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
writeExecutable(basePath, body);
|
||||||
|
return basePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scriptPath = `${basePath}.js`;
|
||||||
|
const commandPath = `${basePath}.cmd`;
|
||||||
|
fs.writeFileSync(scriptPath, body);
|
||||||
|
fs.writeFileSync(commandPath, `@echo off\r\n"${process.execPath}" "${scriptPath}" %*\r\n`);
|
||||||
|
return commandPath;
|
||||||
|
}
|
||||||
|
|
||||||
function createSmokeCase(name: string): SmokeCase {
|
function createSmokeCase(name: string): SmokeCase {
|
||||||
const baseDir = path.join(process.cwd(), '.tmp', 'launcher-smoke');
|
const baseDir = path.join(process.cwd(), '.tmp', 'launcher-smoke');
|
||||||
fs.mkdirSync(baseDir, { recursive: true });
|
fs.mkdirSync(baseDir, { recursive: true });
|
||||||
@@ -52,8 +65,8 @@ function createSmokeCase(name: string): SmokeCase {
|
|||||||
const socketDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-smoke-sock-'));
|
const socketDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-smoke-sock-'));
|
||||||
const socketPath = path.join(socketDir, 'subminer.sock');
|
const socketPath = path.join(socketDir, 'subminer.sock');
|
||||||
const videoPath = path.join(root, 'video.mkv');
|
const videoPath = path.join(root, 'video.mkv');
|
||||||
const fakeAppPath = path.join(binDir, 'fake-subminer');
|
const fakeAppBasePath = path.join(binDir, 'fake-subminer');
|
||||||
const fakeMpvPath = path.join(binDir, 'mpv');
|
const fakeMpvBasePath = path.join(binDir, 'mpv');
|
||||||
const mpvOverlayLogPath = path.join(artifactsDir, 'mpv-overlay.log');
|
const mpvOverlayLogPath = path.join(artifactsDir, 'mpv-overlay.log');
|
||||||
|
|
||||||
fs.mkdirSync(artifactsDir, { recursive: true });
|
fs.mkdirSync(artifactsDir, { recursive: true });
|
||||||
@@ -74,8 +87,8 @@ function createSmokeCase(name: string): SmokeCase {
|
|||||||
const fakeAppStartLogPath = path.join(artifactsDir, 'fake-app-start.log');
|
const fakeAppStartLogPath = path.join(artifactsDir, 'fake-app-start.log');
|
||||||
const fakeAppStopLogPath = path.join(artifactsDir, 'fake-app-stop.log');
|
const fakeAppStopLogPath = path.join(artifactsDir, 'fake-app-stop.log');
|
||||||
|
|
||||||
writeExecutable(
|
const fakeMpvPath = writeFixtureExecutable(
|
||||||
fakeMpvPath,
|
fakeMpvBasePath,
|
||||||
`#!/usr/bin/env bun
|
`#!/usr/bin/env bun
|
||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
const net = require('node:net');
|
const net = require('node:net');
|
||||||
@@ -113,8 +126,8 @@ process.on('SIGTERM', closeAndExit);
|
|||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
|
|
||||||
writeExecutable(
|
const fakeAppPath = writeFixtureExecutable(
|
||||||
fakeAppPath,
|
fakeAppBasePath,
|
||||||
`#!/usr/bin/env bun
|
`#!/usr/bin/env bun
|
||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
|
|
||||||
@@ -157,14 +170,21 @@ process.exit(0);
|
|||||||
}
|
}
|
||||||
|
|
||||||
function makeTestEnv(smokeCase: SmokeCase): NodeJS.ProcessEnv {
|
function makeTestEnv(smokeCase: SmokeCase): NodeJS.ProcessEnv {
|
||||||
return {
|
const env: NodeJS.ProcessEnv = {
|
||||||
...process.env,
|
...process.env,
|
||||||
HOME: smokeCase.homeDir,
|
HOME: smokeCase.homeDir,
|
||||||
XDG_CONFIG_HOME: smokeCase.xdgConfigHome,
|
XDG_CONFIG_HOME: smokeCase.xdgConfigHome,
|
||||||
SUBMINER_APPIMAGE_PATH: smokeCase.fakeAppPath,
|
SUBMINER_APPIMAGE_PATH: smokeCase.fakeAppPath,
|
||||||
SUBMINER_MPV_LOG: smokeCase.mpvOverlayLogPath,
|
SUBMINER_MPV_LOG: smokeCase.mpvOverlayLogPath,
|
||||||
PATH: `${smokeCase.binDir}${path.delimiter}${process.env.PATH || ''}`,
|
|
||||||
};
|
};
|
||||||
|
const pathKey = Object.keys(env).find((key) => key.toLowerCase() === 'path') ?? 'PATH';
|
||||||
|
env[pathKey] = `${smokeCase.binDir}${path.delimiter}${env[pathKey] || ''}`;
|
||||||
|
for (const key of Object.keys(env)) {
|
||||||
|
if (key !== pathKey && key.toLowerCase() === 'path') {
|
||||||
|
delete env[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
function runLauncher(
|
function runLauncher(
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export const DEFAULT_MPV_SUBMINER_ARGS = [
|
|||||||
'--sub-file-paths=.;subs;subtitles',
|
'--sub-file-paths=.;subs;subtitles',
|
||||||
'--sid=auto',
|
'--sid=auto',
|
||||||
'--secondary-sid=auto',
|
'--secondary-sid=auto',
|
||||||
|
'--sub-visibility=no',
|
||||||
'--secondary-sub-visibility=no',
|
'--secondary-sub-visibility=no',
|
||||||
'--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
'--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
||||||
'--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
'--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us',
|
||||||
|
|||||||
@@ -13,6 +13,16 @@ function M.create(ctx)
|
|||||||
local APP_RUNNING_CACHE_TTL_SECONDS = 2
|
local APP_RUNNING_CACHE_TTL_SECONDS = 2
|
||||||
|
|
||||||
local function is_windows()
|
local function is_windows()
|
||||||
|
local platform = mp.get_property("platform") or ""
|
||||||
|
if platform ~= "" then
|
||||||
|
local normalized = platform:lower()
|
||||||
|
if normalized == "windows" or normalized == "win32" then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
if normalized == "macos" or normalized == "darwin" or normalized == "osx" or normalized == "linux" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
return package.config:sub(1, 1) == "\\"
|
return package.config:sub(1, 1) == "\\"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,30 @@ local MODIFIER_MAP = {
|
|||||||
meta = "Meta",
|
meta = "Meta",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local SHIFTED_KEY_NAME_MAP = {
|
||||||
|
Digit1 = "!",
|
||||||
|
Digit2 = "@",
|
||||||
|
Digit3 = "SHARP",
|
||||||
|
Digit4 = "$",
|
||||||
|
Digit5 = "%",
|
||||||
|
Digit6 = "^",
|
||||||
|
Digit7 = "&",
|
||||||
|
Digit8 = "*",
|
||||||
|
Digit9 = "(",
|
||||||
|
Digit0 = ")",
|
||||||
|
Minus = "_",
|
||||||
|
Equal = "+",
|
||||||
|
BracketLeft = "{",
|
||||||
|
BracketRight = "}",
|
||||||
|
Backslash = "|",
|
||||||
|
Semicolon = ":",
|
||||||
|
Quote = '"',
|
||||||
|
Comma = "<",
|
||||||
|
Period = ">",
|
||||||
|
Slash = "?",
|
||||||
|
Backquote = "~",
|
||||||
|
}
|
||||||
|
|
||||||
function M.create(ctx)
|
function M.create(ctx)
|
||||||
local mp = ctx.mp
|
local mp = ctx.mp
|
||||||
local utils = ctx.utils
|
local utils = ctx.utils
|
||||||
@@ -84,7 +108,22 @@ function M.create(ctx)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local function key_spec_to_mpv_binding(key)
|
local function contains_value(values, target)
|
||||||
|
for _, value in ipairs(values) do
|
||||||
|
if value == target then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function append_unique(values, value)
|
||||||
|
if not contains_value(values, value) then
|
||||||
|
values[#values + 1] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function key_spec_to_mpv_bindings(key)
|
||||||
if type(key) ~= "table" then
|
if type(key) ~= "table" then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
@@ -123,7 +162,24 @@ function M.create(ctx)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
parts[#parts + 1] = key_name
|
parts[#parts + 1] = key_name
|
||||||
return table.concat(parts, "+")
|
local bindings = { table.concat(parts, "+") }
|
||||||
|
|
||||||
|
local shifted_key_name = SHIFTED_KEY_NAME_MAP[key.code]
|
||||||
|
if has_shift and shifted_key_name then
|
||||||
|
local shifted_parts = {}
|
||||||
|
for _, modifier in ipairs(key.modifiers) do
|
||||||
|
if modifier ~= "shift" then
|
||||||
|
local mapped = MODIFIER_MAP[modifier]
|
||||||
|
if mapped then
|
||||||
|
shifted_parts[#shifted_parts + 1] = mapped
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
shifted_parts[#shifted_parts + 1] = shifted_key_name
|
||||||
|
append_unique(bindings, table.concat(shifted_parts, "+"))
|
||||||
|
end
|
||||||
|
|
||||||
|
return bindings
|
||||||
end
|
end
|
||||||
|
|
||||||
local function build_cli_args(action_id, payload)
|
local function build_cli_args(action_id, payload)
|
||||||
@@ -294,13 +350,20 @@ function M.create(ctx)
|
|||||||
local generation = state.session_binding_generation
|
local generation = state.session_binding_generation
|
||||||
|
|
||||||
for index, binding in ipairs(artifact.bindings) do
|
for index, binding in ipairs(artifact.bindings) do
|
||||||
local key_name = key_spec_to_mpv_binding(binding.key)
|
local key_names = key_spec_to_mpv_bindings(binding.key)
|
||||||
if key_name then
|
if key_names then
|
||||||
local name = "subminer-session-binding-" .. tostring(generation) .. "-" .. tostring(index)
|
for key_index, key_name in ipairs(key_names) do
|
||||||
|
local name = "subminer-session-binding-"
|
||||||
|
.. tostring(generation)
|
||||||
|
.. "-"
|
||||||
|
.. tostring(index)
|
||||||
|
.. "-"
|
||||||
|
.. tostring(key_index)
|
||||||
next_binding_names[#next_binding_names + 1] = name
|
next_binding_names[#next_binding_names + 1] = name
|
||||||
mp.add_forced_key_binding(key_name, name, function()
|
mp.add_forced_key_binding(key_name, name, function()
|
||||||
handle_binding(binding)
|
handle_binding(binding)
|
||||||
end)
|
end)
|
||||||
|
end
|
||||||
else
|
else
|
||||||
subminer_log(
|
subminer_log(
|
||||||
"warn",
|
"warn",
|
||||||
|
|||||||
@@ -322,7 +322,9 @@ end
|
|||||||
|
|
||||||
local expected_cli_bindings = {
|
local expected_cli_bindings = {
|
||||||
{ keys = "Shift+]", flag = "--shift-sub-delay-next-line" },
|
{ keys = "Shift+]", flag = "--shift-sub-delay-next-line" },
|
||||||
|
{ keys = "}", flag = "--shift-sub-delay-next-line" },
|
||||||
{ keys = "Shift+[", flag = "--shift-sub-delay-prev-line" },
|
{ keys = "Shift+[", flag = "--shift-sub-delay-prev-line" },
|
||||||
|
{ keys = "{", flag = "--shift-sub-delay-prev-line" },
|
||||||
{ keys = "Ctrl+Alt+c", flag = "--open-youtube-picker" },
|
{ keys = "Ctrl+Alt+c", flag = "--open-youtube-picker" },
|
||||||
{ keys = "Ctrl+Alt+p", flag = "--open-playlist-browser" },
|
{ keys = "Ctrl+Alt+p", flag = "--open-playlist-browser" },
|
||||||
{ keys = "Ctrl+H", flag = "--replay-current-subtitle" },
|
{ keys = "Ctrl+H", flag = "--replay-current-subtitle" },
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ test('loads defaults when config is missing', () => {
|
|||||||
assert.equal(config.updates.checkIntervalHours, 24);
|
assert.equal(config.updates.checkIntervalHours, 24);
|
||||||
assert.equal(config.updates.notificationType, 'system');
|
assert.equal(config.updates.notificationType, 'system');
|
||||||
assert.equal(config.updates.channel, 'stable');
|
assert.equal(config.updates.channel, 'stable');
|
||||||
assert.equal(config.mpv.socketPath, '/tmp/subminer-socket');
|
assert.equal(config.mpv.socketPath, DEFAULT_CONFIG.mpv.socketPath);
|
||||||
assert.equal(config.mpv.backend, 'auto');
|
assert.equal(config.mpv.backend, 'auto');
|
||||||
assert.equal(config.mpv.profile, '');
|
assert.equal(config.mpv.profile, '');
|
||||||
assert.equal(config.mpv.autoStartSubMiner, true);
|
assert.equal(config.mpv.autoStartSubMiner, true);
|
||||||
|
|||||||
@@ -80,7 +80,11 @@ export {
|
|||||||
handleOverlayWindowBeforeInputEvent,
|
handleOverlayWindowBeforeInputEvent,
|
||||||
isTabInputForMpvForwarding,
|
isTabInputForMpvForwarding,
|
||||||
} from './overlay-window-input';
|
} from './overlay-window-input';
|
||||||
export { initializeOverlayAnkiIntegration, initializeOverlayRuntime } from './overlay-runtime-init';
|
export {
|
||||||
|
initializeOverlayAnkiIntegration,
|
||||||
|
initializeOverlayRuntime,
|
||||||
|
startOverlayWindowTracker,
|
||||||
|
} from './overlay-runtime-init';
|
||||||
export { setVisibleOverlayVisible, updateVisibleOverlayVisibility } from './overlay-visibility';
|
export { setVisibleOverlayVisible, updateVisibleOverlayVisibility } from './overlay-visibility';
|
||||||
export {
|
export {
|
||||||
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
||||||
|
|||||||
@@ -1,6 +1,65 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { initializeOverlayAnkiIntegration, initializeOverlayRuntime } from './overlay-runtime-init';
|
import {
|
||||||
|
initializeOverlayAnkiIntegration,
|
||||||
|
initializeOverlayRuntime,
|
||||||
|
startOverlayWindowTracker,
|
||||||
|
} from './overlay-runtime-init';
|
||||||
|
|
||||||
|
test('startOverlayWindowTracker starts tracker for the current mpv socket', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const tracker = {
|
||||||
|
onGeometryChange: null as ((...args: unknown[]) => void) | null,
|
||||||
|
onWindowFound: null as ((...args: unknown[]) => void) | null,
|
||||||
|
onWindowLost: null as (() => void) | null,
|
||||||
|
onWindowFocusChange: null as ((focused: boolean) => void) | null,
|
||||||
|
isTargetWindowMinimized: () => false,
|
||||||
|
start: () => {
|
||||||
|
calls.push('start');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = startOverlayWindowTracker({
|
||||||
|
backendOverride: 'windows',
|
||||||
|
getMpvSocketPath: () => '\\\\.\\pipe\\subminer-socket',
|
||||||
|
createWindowTracker: (override, socketPath) => {
|
||||||
|
calls.push(`create:${override}:${socketPath}`);
|
||||||
|
return tracker as never;
|
||||||
|
},
|
||||||
|
setWindowTracker: (nextTracker) => {
|
||||||
|
calls.push(nextTracker === tracker ? 'set-tracker' : 'clear-tracker');
|
||||||
|
},
|
||||||
|
updateVisibleOverlayBounds: () => {
|
||||||
|
calls.push('bounds');
|
||||||
|
},
|
||||||
|
isVisibleOverlayVisible: () => true,
|
||||||
|
updateVisibleOverlayVisibility: () => {
|
||||||
|
calls.push('visibility');
|
||||||
|
},
|
||||||
|
refreshCurrentSubtitle: () => {
|
||||||
|
calls.push('refresh-subtitle');
|
||||||
|
},
|
||||||
|
getOverlayWindows: () => [],
|
||||||
|
syncOverlayShortcuts: () => {
|
||||||
|
calls.push('sync-shortcuts');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result, tracker);
|
||||||
|
tracker.onWindowFound?.({ x: 10, y: 20, width: 300, height: 200 });
|
||||||
|
tracker.onWindowFocusChange?.(true);
|
||||||
|
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
'create:windows:\\\\.\\pipe\\subminer-socket',
|
||||||
|
'set-tracker',
|
||||||
|
'start',
|
||||||
|
'bounds',
|
||||||
|
'visibility',
|
||||||
|
'refresh-subtitle',
|
||||||
|
'visibility',
|
||||||
|
'sync-shortcuts',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test('initializeOverlayRuntime skips Anki integration when ankiConnect.enabled is false', () => {
|
test('initializeOverlayRuntime skips Anki integration when ankiConnect.enabled is false', () => {
|
||||||
let createdIntegrations = 0;
|
let createdIntegrations = 0;
|
||||||
|
|||||||
@@ -25,6 +25,24 @@ type CreateAnkiIntegrationArgs = {
|
|||||||
knownWordCacheStatePath: string;
|
knownWordCacheStatePath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type OverlayWindowTrackerOptions = {
|
||||||
|
backendOverride: string | null;
|
||||||
|
getMpvSocketPath: () => string;
|
||||||
|
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||||
|
isVisibleOverlayVisible: () => boolean;
|
||||||
|
updateVisibleOverlayVisibility: () => void;
|
||||||
|
refreshCurrentSubtitle?: () => void;
|
||||||
|
getOverlayWindows: () => BrowserWindow[];
|
||||||
|
syncOverlayShortcuts: () => void;
|
||||||
|
setWindowTracker: (tracker: BaseWindowTracker | null) => void;
|
||||||
|
createWindowTracker?: (
|
||||||
|
override?: string | null,
|
||||||
|
targetMpvSocketPath?: string | null,
|
||||||
|
) => BaseWindowTracker | null;
|
||||||
|
bindOverlayOwner?: () => void;
|
||||||
|
releaseOverlayOwner?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
function createDefaultAnkiIntegration(args: CreateAnkiIntegrationArgs): AnkiIntegrationLike {
|
function createDefaultAnkiIntegration(args: CreateAnkiIntegrationArgs): AnkiIntegrationLike {
|
||||||
const { AnkiIntegration } =
|
const { AnkiIntegration } =
|
||||||
require('../../anki-integration') as typeof import('../../anki-integration');
|
require('../../anki-integration') as typeof import('../../anki-integration');
|
||||||
@@ -46,52 +64,19 @@ function createDefaultAnkiIntegration(args: CreateAnkiIntegrationArgs): AnkiInte
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initializeOverlayRuntime(options: {
|
export function startOverlayWindowTracker(
|
||||||
getMpvSocketPath: () => string;
|
options: OverlayWindowTrackerOptions,
|
||||||
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig; ai?: AiConfig };
|
): BaseWindowTracker | null {
|
||||||
getSubtitleTimingTracker: () => unknown | null;
|
|
||||||
getMpvClient: () => {
|
|
||||||
send?: (payload: { command: string[] }) => void;
|
|
||||||
} | null;
|
|
||||||
getRuntimeOptionsManager: () => {
|
|
||||||
getEffectiveAnkiConnectConfig: (config?: AnkiConnectConfig) => AnkiConnectConfig;
|
|
||||||
} | null;
|
|
||||||
getAnkiIntegration?: () => unknown | null;
|
|
||||||
setAnkiIntegration: (integration: unknown | null) => void;
|
|
||||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
|
||||||
createFieldGroupingCallback: () => (
|
|
||||||
data: KikuFieldGroupingRequestData,
|
|
||||||
) => Promise<KikuFieldGroupingChoice>;
|
|
||||||
getKnownWordCacheStatePath: () => string;
|
|
||||||
shouldStartAnkiIntegration?: () => boolean;
|
|
||||||
createAnkiIntegration?: (args: CreateAnkiIntegrationArgs) => AnkiIntegrationLike;
|
|
||||||
backendOverride: string | null;
|
|
||||||
createMainWindow: () => void;
|
|
||||||
registerGlobalShortcuts: () => void;
|
|
||||||
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
|
||||||
isVisibleOverlayVisible: () => boolean;
|
|
||||||
updateVisibleOverlayVisibility: () => void;
|
|
||||||
refreshCurrentSubtitle?: () => void;
|
|
||||||
getOverlayWindows: () => BrowserWindow[];
|
|
||||||
syncOverlayShortcuts: () => void;
|
|
||||||
setWindowTracker: (tracker: BaseWindowTracker | null) => void;
|
|
||||||
createWindowTracker?: (
|
|
||||||
override?: string | null,
|
|
||||||
targetMpvSocketPath?: string | null,
|
|
||||||
) => BaseWindowTracker | null;
|
|
||||||
bindOverlayOwner?: () => void;
|
|
||||||
releaseOverlayOwner?: () => void;
|
|
||||||
}): void {
|
|
||||||
options.createMainWindow();
|
|
||||||
options.registerGlobalShortcuts();
|
|
||||||
|
|
||||||
const createWindowTrackerHandler = options.createWindowTracker ?? createWindowTracker;
|
const createWindowTrackerHandler = options.createWindowTracker ?? createWindowTracker;
|
||||||
const windowTracker = createWindowTrackerHandler(
|
const windowTracker = createWindowTrackerHandler(
|
||||||
options.backendOverride,
|
options.backendOverride,
|
||||||
options.getMpvSocketPath(),
|
options.getMpvSocketPath(),
|
||||||
);
|
);
|
||||||
options.setWindowTracker(windowTracker);
|
options.setWindowTracker(windowTracker);
|
||||||
if (windowTracker) {
|
if (!windowTracker) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
windowTracker.onGeometryChange = (geometry: WindowGeometry) => {
|
windowTracker.onGeometryChange = (geometry: WindowGeometry) => {
|
||||||
options.updateVisibleOverlayBounds(geometry);
|
options.updateVisibleOverlayBounds(geometry);
|
||||||
};
|
};
|
||||||
@@ -121,7 +106,38 @@ export function initializeOverlayRuntime(options: {
|
|||||||
options.syncOverlayShortcuts();
|
options.syncOverlayShortcuts();
|
||||||
};
|
};
|
||||||
windowTracker.start();
|
windowTracker.start();
|
||||||
}
|
return windowTracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initializeOverlayRuntime(
|
||||||
|
options: OverlayWindowTrackerOptions & {
|
||||||
|
getMpvSocketPath: () => string;
|
||||||
|
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig; ai?: AiConfig };
|
||||||
|
getSubtitleTimingTracker: () => unknown | null;
|
||||||
|
getMpvClient: () => {
|
||||||
|
send?: (payload: { command: string[] }) => void;
|
||||||
|
} | null;
|
||||||
|
getRuntimeOptionsManager: () => {
|
||||||
|
getEffectiveAnkiConnectConfig: (config?: AnkiConnectConfig) => AnkiConnectConfig;
|
||||||
|
} | null;
|
||||||
|
getAnkiIntegration?: () => unknown | null;
|
||||||
|
setAnkiIntegration: (integration: unknown | null) => void;
|
||||||
|
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||||
|
createFieldGroupingCallback: () => (
|
||||||
|
data: KikuFieldGroupingRequestData,
|
||||||
|
) => Promise<KikuFieldGroupingChoice>;
|
||||||
|
getKnownWordCacheStatePath: () => string;
|
||||||
|
shouldStartAnkiIntegration?: () => boolean;
|
||||||
|
createAnkiIntegration?: (args: CreateAnkiIntegrationArgs) => AnkiIntegrationLike;
|
||||||
|
backendOverride: string | null;
|
||||||
|
createMainWindow: () => void;
|
||||||
|
registerGlobalShortcuts: () => void;
|
||||||
|
},
|
||||||
|
): void {
|
||||||
|
options.createMainWindow();
|
||||||
|
options.registerGlobalShortcuts();
|
||||||
|
|
||||||
|
startOverlayWindowTracker(options);
|
||||||
|
|
||||||
initializeOverlayAnkiIntegration(options);
|
initializeOverlayAnkiIntegration(options);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import path from 'node:path';
|
||||||
|
import { loadRawConfigStrict } from './config/load';
|
||||||
|
import { resolveConfig } from './config/resolve';
|
||||||
|
import type { MpvLaunchMode, ResolvedConfig } from './types/config';
|
||||||
|
import type { SubminerPluginRuntimeScriptOptConfig } from './shared/subminer-plugin-script-opts';
|
||||||
|
|
||||||
|
export interface ConfiguredWindowsMpvLaunch {
|
||||||
|
executablePath: string;
|
||||||
|
launchMode: MpvLaunchMode;
|
||||||
|
pluginRuntimeConfig: SubminerPluginRuntimeScriptOptConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildWindowsMpvPluginRuntimeConfig(
|
||||||
|
config: Pick<ResolvedConfig, 'auto_start_overlay' | 'mpv' | 'texthooker'>,
|
||||||
|
): SubminerPluginRuntimeScriptOptConfig {
|
||||||
|
return {
|
||||||
|
socketPath: config.mpv.socketPath,
|
||||||
|
binaryPath: config.mpv.subminerBinaryPath,
|
||||||
|
backend: config.mpv.backend,
|
||||||
|
autoStart: config.mpv.autoStartSubMiner,
|
||||||
|
autoStartVisibleOverlay: config.auto_start_overlay,
|
||||||
|
autoStartPauseUntilReady: config.mpv.pauseUntilOverlayReady,
|
||||||
|
texthookerEnabled: config.texthooker.launchAtStartup,
|
||||||
|
aniskipEnabled: config.mpv.aniskipEnabled,
|
||||||
|
aniskipButtonKey: config.mpv.aniskipButtonKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readConfiguredWindowsMpvLaunch(configDir: string): ConfiguredWindowsMpvLaunch {
|
||||||
|
const loadResult = loadRawConfigStrict({
|
||||||
|
configDir,
|
||||||
|
configFileJsonc: path.join(configDir, 'config.jsonc'),
|
||||||
|
configFileJson: path.join(configDir, 'config.json'),
|
||||||
|
});
|
||||||
|
const rawConfig = loadResult.ok ? loadResult.config : {};
|
||||||
|
const { resolved } = resolveConfig(rawConfig);
|
||||||
|
|
||||||
|
return {
|
||||||
|
executablePath: resolved.mpv.executablePath,
|
||||||
|
launchMode: resolved.mpv.launchMode,
|
||||||
|
pluginRuntimeConfig: buildWindowsMpvPluginRuntimeConfig(resolved),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
|
import { DEFAULT_CONFIG } from './config/definitions';
|
||||||
|
import { readConfiguredWindowsMpvLaunch } from './main-entry-launch-config';
|
||||||
import {
|
import {
|
||||||
configureEarlyAppPaths,
|
configureEarlyAppPaths,
|
||||||
normalizeLaunchMpvExtraArgs,
|
normalizeLaunchMpvExtraArgs,
|
||||||
@@ -146,7 +151,7 @@ test('applyEarlyLinuxCommandLineSwitches appends password store before main star
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('transported AppImage visibility commands should forward through app control', () => {
|
test('transported AppImage visibility commands forward through app control', () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
shouldForwardStartupArgvViaAppControl(['SubMiner.AppImage', '--hide-visible-overlay'], {
|
shouldForwardStartupArgvViaAppControl(['SubMiner.AppImage', '--hide-visible-overlay'], {
|
||||||
SUBMINER_APP_ARGC: '1',
|
SUBMINER_APP_ARGC: '1',
|
||||||
@@ -156,9 +161,35 @@ test('transported AppImage visibility commands should forward through app contro
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('app control forwarding is only for transported runtime commands', () => {
|
test('direct runtime commands forward through app control', () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
shouldForwardStartupArgvViaAppControl(['SubMiner.AppImage', '--hide-visible-overlay'], {}),
|
shouldForwardStartupArgvViaAppControl(['SubMiner.AppImage', '--hide-visible-overlay'], {}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
shouldForwardStartupArgvViaAppControl(
|
||||||
|
['SubMiner.exe', '--start', '--socket', '\\\\.\\pipe\\subminer-socket'],
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(shouldForwardStartupArgvViaAppControl(['SubMiner.exe', '--settings'], {}), true);
|
||||||
|
assert.equal(shouldForwardStartupArgvViaAppControl(['SubMiner.exe', '--stop'], {}), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('entry-only and internal commands do not forward through app control', () => {
|
||||||
|
assert.equal(shouldForwardStartupArgvViaAppControl(['SubMiner.exe'], {}), false);
|
||||||
|
assert.equal(shouldForwardStartupArgvViaAppControl(['SubMiner.exe', '--help'], {}), false);
|
||||||
|
assert.equal(
|
||||||
|
shouldForwardStartupArgvViaAppControl(['SubMiner.exe', '--generate-config'], {}),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
shouldForwardStartupArgvViaAppControl(['SubMiner.exe', '--stats-daemon-start'], {}),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
shouldForwardStartupArgvViaAppControl(['SubMiner.exe', '--stats', '--stats-background'], {}),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
@@ -175,6 +206,12 @@ test('app control forwarding is only for transported runtime commands', () => {
|
|||||||
}),
|
}),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
shouldForwardStartupArgvViaAppControl(['SubMiner.exe', '--start'], {
|
||||||
|
ELECTRON_RUN_AS_NODE: '1',
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shouldHandleHelpOnlyAtEntry detects help-only invocation', () => {
|
test('shouldHandleHelpOnlyAtEntry detects help-only invocation', () => {
|
||||||
@@ -269,6 +306,73 @@ test('launch-mpv entry helpers detect and normalize targets', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('readConfiguredWindowsMpvLaunch includes defaults for runtime plugin script opts', () => {
|
||||||
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-entry-config-'));
|
||||||
|
try {
|
||||||
|
const launch = readConfiguredWindowsMpvLaunch(tempDir);
|
||||||
|
|
||||||
|
assert.equal(launch.executablePath, DEFAULT_CONFIG.mpv.executablePath);
|
||||||
|
assert.equal(launch.launchMode, DEFAULT_CONFIG.mpv.launchMode);
|
||||||
|
assert.deepEqual(launch.pluginRuntimeConfig, {
|
||||||
|
socketPath: DEFAULT_CONFIG.mpv.socketPath,
|
||||||
|
binaryPath: DEFAULT_CONFIG.mpv.subminerBinaryPath,
|
||||||
|
backend: DEFAULT_CONFIG.mpv.backend,
|
||||||
|
autoStart: DEFAULT_CONFIG.mpv.autoStartSubMiner,
|
||||||
|
autoStartVisibleOverlay: DEFAULT_CONFIG.auto_start_overlay,
|
||||||
|
autoStartPauseUntilReady: DEFAULT_CONFIG.mpv.pauseUntilOverlayReady,
|
||||||
|
texthookerEnabled: DEFAULT_CONFIG.texthooker.launchAtStartup,
|
||||||
|
aniskipEnabled: DEFAULT_CONFIG.mpv.aniskipEnabled,
|
||||||
|
aniskipButtonKey: DEFAULT_CONFIG.mpv.aniskipButtonKey,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('readConfiguredWindowsMpvLaunch preserves configured runtime plugin script opts', () => {
|
||||||
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-entry-config-'));
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tempDir, 'config.jsonc'),
|
||||||
|
JSON.stringify({
|
||||||
|
auto_start_overlay: false,
|
||||||
|
texthooker: {
|
||||||
|
launchAtStartup: true,
|
||||||
|
},
|
||||||
|
mpv: {
|
||||||
|
executablePath: ' C:\\tools\\mpv.exe ',
|
||||||
|
launchMode: 'maximized',
|
||||||
|
socketPath: '\\\\.\\pipe\\custom-subminer-socket',
|
||||||
|
backend: 'windows',
|
||||||
|
autoStartSubMiner: false,
|
||||||
|
pauseUntilOverlayReady: false,
|
||||||
|
subminerBinaryPath: 'C:\\SubMiner\\Custom.exe',
|
||||||
|
aniskipEnabled: false,
|
||||||
|
aniskipButtonKey: 'F8',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const launch = readConfiguredWindowsMpvLaunch(tempDir);
|
||||||
|
|
||||||
|
assert.equal(launch.executablePath, 'C:\\tools\\mpv.exe');
|
||||||
|
assert.equal(launch.launchMode, 'maximized');
|
||||||
|
assert.deepEqual(launch.pluginRuntimeConfig, {
|
||||||
|
socketPath: '\\\\.\\pipe\\custom-subminer-socket',
|
||||||
|
binaryPath: 'C:\\SubMiner\\Custom.exe',
|
||||||
|
backend: 'windows',
|
||||||
|
autoStart: false,
|
||||||
|
autoStartVisibleOverlay: false,
|
||||||
|
autoStartPauseUntilReady: false,
|
||||||
|
texthookerEnabled: true,
|
||||||
|
aniskipEnabled: false,
|
||||||
|
aniskipButtonKey: 'F8',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('stats-daemon entry helper detects internal daemon commands', () => {
|
test('stats-daemon entry helper detects internal daemon commands', () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
shouldHandleStatsDaemonCommandAtEntry(['SubMiner.AppImage', '--stats-daemon-start'], {}),
|
shouldHandleStatsDaemonCommandAtEntry(['SubMiner.AppImage', '--stats-daemon-start'], {}),
|
||||||
|
|||||||
@@ -154,10 +154,9 @@ export function shouldForwardStartupArgvViaAppControl(
|
|||||||
env: NodeJS.ProcessEnv,
|
env: NodeJS.ProcessEnv,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (env.ELECTRON_RUN_AS_NODE === '1') return false;
|
if (env.ELECTRON_RUN_AS_NODE === '1') return false;
|
||||||
if (!hasTransportedStartupArgs(env)) return false;
|
|
||||||
|
|
||||||
const args = parseCliArgs(argv);
|
const args = parseCliArgs(argv);
|
||||||
if (args.help || args.appPing || args.launchMpv) return false;
|
if (args.help || args.appPing || args.launchMpv || args.generateConfig) return false;
|
||||||
if (resolveStatsDaemonCommandAction(argv) !== null) return false;
|
if (resolveStatsDaemonCommandAction(argv) !== null) return false;
|
||||||
|
|
||||||
return hasExplicitCommand(args);
|
return hasExplicitCommand(args);
|
||||||
|
|||||||
+36
-56
@@ -1,9 +1,7 @@
|
|||||||
import path from 'node:path';
|
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { app, dialog, shell } from 'electron';
|
import { app, dialog, shell } from 'electron';
|
||||||
import { printHelp } from './cli/help';
|
import { printHelp } from './cli/help';
|
||||||
import { loadRawConfigStrict } from './config/load';
|
|
||||||
import {
|
import {
|
||||||
configureEarlyAppPaths,
|
configureEarlyAppPaths,
|
||||||
normalizeLaunchMpvExtraArgs,
|
normalizeLaunchMpvExtraArgs,
|
||||||
@@ -22,6 +20,7 @@ import {
|
|||||||
shouldHandleStatsDaemonCommandAtEntry,
|
shouldHandleStatsDaemonCommandAtEntry,
|
||||||
} from './main-entry-runtime';
|
} from './main-entry-runtime';
|
||||||
import { requestSingleInstanceLockEarly } from './main/early-single-instance';
|
import { requestSingleInstanceLockEarly } from './main/early-single-instance';
|
||||||
|
import { readConfiguredWindowsMpvLaunch } from './main-entry-launch-config';
|
||||||
import { sendAppControlCommand } from './shared/app-control-client';
|
import { sendAppControlCommand } from './shared/app-control-client';
|
||||||
import {
|
import {
|
||||||
detectInstalledFirstRunPluginCandidates,
|
detectInstalledFirstRunPluginCandidates,
|
||||||
@@ -30,7 +29,6 @@ import {
|
|||||||
resolvePackagedRuntimePluginPath,
|
resolvePackagedRuntimePluginPath,
|
||||||
} from './main/runtime/first-run-setup-plugin';
|
} from './main/runtime/first-run-setup-plugin';
|
||||||
import { createWindowsMpvLaunchDeps, launchWindowsMpv } from './main/runtime/windows-mpv-launch';
|
import { createWindowsMpvLaunchDeps, launchWindowsMpv } from './main/runtime/windows-mpv-launch';
|
||||||
import { parseMpvLaunchMode } from './shared/mpv-launch-mode';
|
|
||||||
import { runStatsDaemonControlFromProcess } from './stats-daemon-entry';
|
import { runStatsDaemonControlFromProcess } from './stats-daemon-entry';
|
||||||
import { createFatalErrorReporter, registerFatalErrorHandlers } from './main/fatal-error';
|
import { createFatalErrorReporter, registerFatalErrorHandlers } from './main/fatal-error';
|
||||||
|
|
||||||
@@ -150,31 +148,6 @@ function createWindowsRuntimePluginPolicy() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function readConfiguredWindowsMpvLaunch(configDir: string): {
|
|
||||||
executablePath: string;
|
|
||||||
launchMode: 'normal' | 'maximized' | 'fullscreen';
|
|
||||||
} {
|
|
||||||
const loadResult = loadRawConfigStrict({
|
|
||||||
configDir,
|
|
||||||
configFileJsonc: path.join(configDir, 'config.jsonc'),
|
|
||||||
configFileJson: path.join(configDir, 'config.json'),
|
|
||||||
});
|
|
||||||
if (!loadResult.ok) {
|
|
||||||
return {
|
|
||||||
executablePath: '',
|
|
||||||
launchMode: 'normal',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
executablePath:
|
|
||||||
typeof loadResult.config.mpv?.executablePath === 'string'
|
|
||||||
? loadResult.config.mpv.executablePath.trim()
|
|
||||||
: '',
|
|
||||||
launchMode: parseMpvLaunchMode(loadResult.config.mpv?.launchMode) ?? 'normal',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
process.argv = normalizeStartupArgv(process.argv, process.env);
|
process.argv = normalizeStartupArgv(process.argv, process.env);
|
||||||
applyEarlyLinuxCommandLineSwitches(app.commandLine, process.argv);
|
applyEarlyLinuxCommandLineSwitches(app.commandLine, process.argv);
|
||||||
applySanitizedEnv(sanitizeStartupEnv(process.env));
|
applySanitizedEnv(sanitizeStartupEnv(process.env));
|
||||||
@@ -226,18 +199,8 @@ async function forwardStartupArgvViaAppControlIfAvailable(): Promise<boolean> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldDetachBackgroundLaunch(process.argv, process.env)) {
|
async function runEntryProcess(): Promise<void> {
|
||||||
const childArgs = hasTransportedStartupArgs(process.env) ? [] : process.argv.slice(1);
|
if (shouldHandleHelpOnlyAtEntry(process.argv, process.env)) {
|
||||||
const child = spawn(process.execPath, childArgs, {
|
|
||||||
detached: true,
|
|
||||||
stdio: 'ignore',
|
|
||||||
env: sanitizeBackgroundEnv(process.env),
|
|
||||||
});
|
|
||||||
child.unref();
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldHandleHelpOnlyAtEntry(process.argv, process.env)) {
|
|
||||||
const sanitizedEnv = sanitizeHelpEnv(process.env);
|
const sanitizedEnv = sanitizeHelpEnv(process.env);
|
||||||
process.env.NODE_NO_WARNINGS = sanitizedEnv.NODE_NO_WARNINGS;
|
process.env.NODE_NO_WARNINGS = sanitizedEnv.NODE_NO_WARNINGS;
|
||||||
if (!sanitizedEnv.VK_INSTANCE_LAYERS) {
|
if (!sanitizedEnv.VK_INSTANCE_LAYERS) {
|
||||||
@@ -245,12 +208,13 @@ if (shouldHandleHelpOnlyAtEntry(process.argv, process.env)) {
|
|||||||
}
|
}
|
||||||
printHelp(DEFAULT_TEXTHOOKER_PORT);
|
printHelp(DEFAULT_TEXTHOOKER_PORT);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldHandleLaunchMpvAtEntry(process.argv, process.env)) {
|
if (shouldHandleLaunchMpvAtEntry(process.argv, process.env)) {
|
||||||
const sanitizedEnv = sanitizeLaunchMpvEnv(process.env);
|
const sanitizedEnv = sanitizeLaunchMpvEnv(process.env);
|
||||||
applySanitizedEnv(sanitizedEnv);
|
applySanitizedEnv(sanitizedEnv);
|
||||||
void app.whenReady().then(async () => {
|
await app.whenReady();
|
||||||
const configuredMpvLaunch = readConfiguredWindowsMpvLaunch(userDataPath);
|
const configuredMpvLaunch = readConfiguredWindowsMpvLaunch(userDataPath);
|
||||||
const result = await launchWindowsMpv(
|
const result = await launchWindowsMpv(
|
||||||
normalizeLaunchMpvTargets(process.argv),
|
normalizeLaunchMpvTargets(process.argv),
|
||||||
@@ -266,23 +230,39 @@ if (shouldHandleLaunchMpvAtEntry(process.argv, process.env)) {
|
|||||||
configuredMpvLaunch.executablePath,
|
configuredMpvLaunch.executablePath,
|
||||||
configuredMpvLaunch.launchMode,
|
configuredMpvLaunch.launchMode,
|
||||||
createWindowsRuntimePluginPolicy(),
|
createWindowsRuntimePluginPolicy(),
|
||||||
|
configuredMpvLaunch.pluginRuntimeConfig,
|
||||||
);
|
);
|
||||||
app.exit(result.ok ? 0 : 1);
|
app.exit(result.ok ? 0 : 1);
|
||||||
});
|
return;
|
||||||
} else if (shouldHandleStatsDaemonCommandAtEntry(process.argv, process.env)) {
|
}
|
||||||
void app.whenReady().then(async () => {
|
|
||||||
|
if (shouldHandleStatsDaemonCommandAtEntry(process.argv, process.env)) {
|
||||||
|
await app.whenReady();
|
||||||
const exitCode = await runStatsDaemonControlFromProcess(app.getPath('userData'));
|
const exitCode = await runStatsDaemonControlFromProcess(app.getPath('userData'));
|
||||||
app.exit(exitCode);
|
app.exit(exitCode);
|
||||||
});
|
return;
|
||||||
} else {
|
|
||||||
void forwardStartupArgvViaAppControlIfAvailable()
|
|
||||||
.then((forwarded) => {
|
|
||||||
if (!forwarded) {
|
|
||||||
startMainProcess();
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch((error) => {
|
if (await forwardStartupArgvViaAppControlIfAvailable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldDetachBackgroundLaunch(process.argv, process.env)) {
|
||||||
|
const childArgs = hasTransportedStartupArgs(process.env) ? [] : process.argv.slice(1);
|
||||||
|
const child = spawn(process.execPath, childArgs, {
|
||||||
|
detached: true,
|
||||||
|
stdio: 'ignore',
|
||||||
|
env: sanitizeBackgroundEnv(process.env),
|
||||||
|
});
|
||||||
|
child.unref();
|
||||||
|
process.exit(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
startMainProcess();
|
||||||
|
}
|
||||||
|
|
||||||
|
void runEntryProcess().catch((error) => {
|
||||||
console.error('SubMiner app-control handoff failed:', error);
|
console.error('SubMiner app-control handoff failed:', error);
|
||||||
startMainProcess();
|
startMainProcess();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|||||||
+107
-45
@@ -347,6 +347,7 @@ import {
|
|||||||
syncOverlayWindowLayer,
|
syncOverlayWindowLayer,
|
||||||
setVisibleOverlayVisible as setVisibleOverlayVisibleCore,
|
setVisibleOverlayVisible as setVisibleOverlayVisibleCore,
|
||||||
showMpvOsdRuntime,
|
showMpvOsdRuntime,
|
||||||
|
startOverlayWindowTracker as startOverlayWindowTrackerCore,
|
||||||
tokenizeSubtitle as tokenizeSubtitleCore,
|
tokenizeSubtitle as tokenizeSubtitleCore,
|
||||||
triggerFieldGrouping as triggerFieldGroupingCore,
|
triggerFieldGrouping as triggerFieldGroupingCore,
|
||||||
upsertYomitanDictionarySettings,
|
upsertYomitanDictionarySettings,
|
||||||
@@ -2460,6 +2461,7 @@ function resolveWindowsOverlayBindTargetHandle(targetMpvSocketPath?: string | nu
|
|||||||
if (typeof trackedHandle === 'number' && Number.isFinite(trackedHandle)) {
|
if (typeof trackedHandle === 'number' && Number.isFinite(trackedHandle)) {
|
||||||
return trackedHandle;
|
return trackedHandle;
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return findWindowsMpvTargetWindowHandle();
|
return findWindowsMpvTargetWindowHandle();
|
||||||
} catch {
|
} catch {
|
||||||
@@ -2467,6 +2469,104 @@ function resolveWindowsOverlayBindTargetHandle(targetMpvSocketPath?: string | nu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createOverlayWindowTracker(override?: string | null, targetMpvSocketPath?: string | null) {
|
||||||
|
if (appState.initialArgs && isHeadlessInitialCommand(appState.initialArgs)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return createWindowTrackerCore(override, targetMpvSocketPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindVisibleOverlayOwner(): void {
|
||||||
|
const mainWindow = overlayManager.getMainWindow();
|
||||||
|
if (process.platform !== 'win32' || !mainWindow || mainWindow.isDestroyed()) return;
|
||||||
|
const overlayHwnd = getWindowsNativeWindowHandleNumber(mainWindow);
|
||||||
|
const targetSocketPath = appState.mpvSocketPath;
|
||||||
|
const targetWindowHwnd = resolveWindowsOverlayBindTargetHandle(targetSocketPath);
|
||||||
|
if (targetWindowHwnd !== null && bindWindowsOverlayAboveMpv(overlayHwnd, targetWindowHwnd)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (targetSocketPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tracker = appState.windowTracker;
|
||||||
|
const mpvResult = tracker
|
||||||
|
? (() => {
|
||||||
|
try {
|
||||||
|
const win32 =
|
||||||
|
require('./window-trackers/win32') as typeof import('./window-trackers/win32');
|
||||||
|
const poll = win32.findMpvWindows();
|
||||||
|
const focused = poll.matches.find((m) => m.isForeground);
|
||||||
|
return focused ?? [...poll.matches].sort((a, b) => b.area - a.area)[0] ?? null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
: null;
|
||||||
|
if (!mpvResult) return;
|
||||||
|
if (!setWindowsOverlayOwner(overlayHwnd, mpvResult.hwnd)) {
|
||||||
|
logger.warn('Failed to set overlay owner via koffi');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function releaseVisibleOverlayOwner(): void {
|
||||||
|
const mainWindow = overlayManager.getMainWindow();
|
||||||
|
if (process.platform !== 'win32' || !mainWindow || mainWindow.isDestroyed()) return;
|
||||||
|
const overlayHwnd = getWindowsNativeWindowHandleNumber(mainWindow);
|
||||||
|
if (!clearWindowsOverlayOwner(overlayHwnd)) {
|
||||||
|
logger.warn('Failed to clear overlay owner via koffi');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startOverlayWindowTrackerForCurrentSocket(): void {
|
||||||
|
startOverlayWindowTrackerCore({
|
||||||
|
backendOverride: appState.backendOverride,
|
||||||
|
getMpvSocketPath: () => appState.mpvSocketPath,
|
||||||
|
createWindowTracker: createOverlayWindowTracker,
|
||||||
|
setWindowTracker: (tracker) => {
|
||||||
|
appState.windowTracker = tracker;
|
||||||
|
},
|
||||||
|
updateVisibleOverlayBounds: (geometry: WindowGeometry) => updateVisibleOverlayBounds(geometry),
|
||||||
|
isVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
|
updateVisibleOverlayVisibility: () => overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
|
||||||
|
refreshCurrentSubtitle: () => {
|
||||||
|
subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText);
|
||||||
|
},
|
||||||
|
getOverlayWindows: () => getOverlayWindows(),
|
||||||
|
syncOverlayShortcuts: () => overlayShortcutsRuntime.syncOverlayShortcuts(),
|
||||||
|
bindOverlayOwner: () => bindVisibleOverlayOwner(),
|
||||||
|
releaseOverlayOwner: () => releaseVisibleOverlayOwner(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function retargetOverlayWindowTrackerForMpvSocket(
|
||||||
|
nextSocketPath: string,
|
||||||
|
previousSocketPath: string,
|
||||||
|
): void {
|
||||||
|
if (nextSocketPath === previousSocketPath || !appState.overlayRuntimeInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousTracker = appState.windowTracker;
|
||||||
|
if (previousTracker) {
|
||||||
|
try {
|
||||||
|
previousTracker.stop();
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Failed to stop previous overlay window tracker before retargeting', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseVisibleOverlayOwner();
|
||||||
|
appState.windowTracker = null;
|
||||||
|
appState.trackerNotReadyWarningShown = false;
|
||||||
|
lastOverlayWindowGeometry = null;
|
||||||
|
startOverlayWindowTrackerForCurrentSocket();
|
||||||
|
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
|
||||||
|
overlayShortcutsRuntime.syncOverlayShortcuts();
|
||||||
|
logger.info(
|
||||||
|
`Retargeted overlay window tracker for MPV socket: ${previousSocketPath} -> ${nextSocketPath}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function syncWindowsVisibleOverlayToMpvZOrder(): Promise<boolean> {
|
async function syncWindowsVisibleOverlayToMpvZOrder(): Promise<boolean> {
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
return false;
|
return false;
|
||||||
@@ -5925,6 +6025,8 @@ const { handleCliCommand, handleInitialArgs } = composeCliStartupHandlers({
|
|||||||
cliCommandContextMainDeps: {
|
cliCommandContextMainDeps: {
|
||||||
appState,
|
appState,
|
||||||
setLogLevel: (level) => setLogLevel(level, 'cli'),
|
setLogLevel: (level) => setLogLevel(level, 'cli'),
|
||||||
|
onMpvSocketPathChanged: (nextSocketPath, previousSocketPath) =>
|
||||||
|
retargetOverlayWindowTrackerForMpvSocket(nextSocketPath, previousSocketPath),
|
||||||
texthookerService,
|
texthookerService,
|
||||||
getResolvedConfig: () => getResolvedConfig(),
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
defaultWebsocketPort: DEFAULT_CONFIG.websocket.port,
|
defaultWebsocketPort: DEFAULT_CONFIG.websocket.port,
|
||||||
@@ -6213,7 +6315,7 @@ const { ensureTray: ensureTrayHandler, destroyTray: destroyTrayHandler } =
|
|||||||
handleCliCommand(parseArgs(['--texthooker', '--open-browser'])),
|
handleCliCommand(parseArgs(['--texthooker', '--open-browser'])),
|
||||||
showTexthookerPage: () => shouldShowTexthookerTrayEntry(getResolvedConfig()),
|
showTexthookerPage: () => shouldShowTexthookerTrayEntry(getResolvedConfig()),
|
||||||
showFirstRunSetup: () => !firstRunSetupService.isSetupCompleted(),
|
showFirstRunSetup: () => !firstRunSetupService.isSetupCompleted(),
|
||||||
openFirstRunSetupWindow: () => openFirstRunSetupWindow(),
|
openFirstRunSetupWindow: (force?: boolean) => openFirstRunSetupWindow(force),
|
||||||
showWindowsMpvLauncherSetup: () => process.platform === 'win32',
|
showWindowsMpvLauncherSetup: () => process.platform === 'win32',
|
||||||
openYomitanSettings: () => openYomitanSettings(),
|
openYomitanSettings: () => openYomitanSettings(),
|
||||||
openConfigSettingsWindow: () => openConfigSettingsWindow(),
|
openConfigSettingsWindow: () => openConfigSettingsWindow(),
|
||||||
@@ -6323,52 +6425,12 @@ const { initializeOverlayRuntime: initializeOverlayRuntimeHandler } =
|
|||||||
}
|
}
|
||||||
registerGlobalShortcuts();
|
registerGlobalShortcuts();
|
||||||
},
|
},
|
||||||
createWindowTracker: (override, targetMpvSocketPath) => {
|
createWindowTracker: (override, targetMpvSocketPath) =>
|
||||||
if (appState.initialArgs && isHeadlessInitialCommand(appState.initialArgs)) {
|
createOverlayWindowTracker(override, targetMpvSocketPath),
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return createWindowTrackerCore(override, targetMpvSocketPath);
|
|
||||||
},
|
|
||||||
updateVisibleOverlayBounds: (geometry: WindowGeometry) =>
|
updateVisibleOverlayBounds: (geometry: WindowGeometry) =>
|
||||||
updateVisibleOverlayBounds(geometry),
|
updateVisibleOverlayBounds(geometry),
|
||||||
bindOverlayOwner: () => {
|
bindOverlayOwner: () => bindVisibleOverlayOwner(),
|
||||||
const mainWindow = overlayManager.getMainWindow();
|
releaseOverlayOwner: () => releaseVisibleOverlayOwner(),
|
||||||
if (process.platform !== 'win32' || !mainWindow || mainWindow.isDestroyed()) return;
|
|
||||||
const overlayHwnd = getWindowsNativeWindowHandleNumber(mainWindow);
|
|
||||||
const targetWindowHwnd = resolveWindowsOverlayBindTargetHandle(appState.mpvSocketPath);
|
|
||||||
if (
|
|
||||||
targetWindowHwnd !== null &&
|
|
||||||
bindWindowsOverlayAboveMpv(overlayHwnd, targetWindowHwnd)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const tracker = appState.windowTracker;
|
|
||||||
const mpvResult = tracker
|
|
||||||
? (() => {
|
|
||||||
try {
|
|
||||||
const win32 =
|
|
||||||
require('./window-trackers/win32') as typeof import('./window-trackers/win32');
|
|
||||||
const poll = win32.findMpvWindows();
|
|
||||||
const focused = poll.matches.find((m) => m.isForeground);
|
|
||||||
return focused ?? [...poll.matches].sort((a, b) => b.area - a.area)[0] ?? null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
: null;
|
|
||||||
if (!mpvResult) return;
|
|
||||||
if (!setWindowsOverlayOwner(overlayHwnd, mpvResult.hwnd)) {
|
|
||||||
logger.warn('Failed to set overlay owner via koffi');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
releaseOverlayOwner: () => {
|
|
||||||
const mainWindow = overlayManager.getMainWindow();
|
|
||||||
if (process.platform !== 'win32' || !mainWindow || mainWindow.isDestroyed()) return;
|
|
||||||
const overlayHwnd = getWindowsNativeWindowHandleNumber(mainWindow);
|
|
||||||
if (!clearWindowsOverlayOwner(overlayHwnd)) {
|
|
||||||
logger.warn('Failed to clear overlay owner via koffi');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getOverlayWindows: () => getOverlayWindows(),
|
getOverlayWindows: () => getOverlayWindows(),
|
||||||
getResolvedConfig: () => getResolvedConfig(),
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
showDesktopNotification,
|
showDesktopNotification,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ test('cli command context main deps builder maps state and callbacks', async ()
|
|||||||
|
|
||||||
const build = createBuildCliCommandContextMainDepsHandler({
|
const build = createBuildCliCommandContextMainDepsHandler({
|
||||||
appState,
|
appState,
|
||||||
|
onMpvSocketPathChanged: (next, previous) => calls.push(`socket:${previous}->${next}`),
|
||||||
texthookerService: { isRunning: () => false, start: () => null },
|
texthookerService: { isRunning: () => false, start: () => null },
|
||||||
getResolvedConfig: () => ({
|
getResolvedConfig: () => ({
|
||||||
texthooker: { openBrowser: true },
|
texthooker: { openBrowser: true },
|
||||||
@@ -121,6 +122,10 @@ test('cli command context main deps builder maps state and callbacks', async ()
|
|||||||
assert.equal(deps.getSocketPath(), '/tmp/mpv.sock');
|
assert.equal(deps.getSocketPath(), '/tmp/mpv.sock');
|
||||||
deps.setSocketPath('/tmp/next.sock');
|
deps.setSocketPath('/tmp/next.sock');
|
||||||
assert.equal(appState.mpvSocketPath, '/tmp/next.sock');
|
assert.equal(appState.mpvSocketPath, '/tmp/next.sock');
|
||||||
|
assert.deepEqual(calls, ['socket:/tmp/mpv.sock->/tmp/next.sock']);
|
||||||
|
deps.setSocketPath('/tmp/next.sock');
|
||||||
|
assert.deepEqual(calls, ['socket:/tmp/mpv.sock->/tmp/next.sock']);
|
||||||
|
calls.length = 0;
|
||||||
assert.equal(deps.getTexthookerPort(), 5174);
|
assert.equal(deps.getTexthookerPort(), 5174);
|
||||||
deps.setTexthookerPort(5175);
|
deps.setTexthookerPort(5175);
|
||||||
assert.equal(appState.texthookerPort, 5175);
|
assert.equal(appState.texthookerPort, 5175);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type CliCommandContextMainState = {
|
|||||||
export function createBuildCliCommandContextMainDepsHandler(deps: {
|
export function createBuildCliCommandContextMainDepsHandler(deps: {
|
||||||
appState: CliCommandContextMainState;
|
appState: CliCommandContextMainState;
|
||||||
setLogLevel?: (level: NonNullable<CliArgs['logLevel']>) => void;
|
setLogLevel?: (level: NonNullable<CliArgs['logLevel']>) => void;
|
||||||
|
onMpvSocketPathChanged?: (nextSocketPath: string, previousSocketPath: string) => void;
|
||||||
texthookerService: CliCommandContextFactoryDeps['texthookerService'];
|
texthookerService: CliCommandContextFactoryDeps['texthookerService'];
|
||||||
getResolvedConfig: () => {
|
getResolvedConfig: () => {
|
||||||
texthooker?: { openBrowser?: boolean };
|
texthooker?: { openBrowser?: boolean };
|
||||||
@@ -74,7 +75,11 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
|
|||||||
setLogLevel: deps.setLogLevel,
|
setLogLevel: deps.setLogLevel,
|
||||||
getSocketPath: () => deps.appState.mpvSocketPath,
|
getSocketPath: () => deps.appState.mpvSocketPath,
|
||||||
setSocketPath: (socketPath: string) => {
|
setSocketPath: (socketPath: string) => {
|
||||||
|
const previousSocketPath = deps.appState.mpvSocketPath;
|
||||||
deps.appState.mpvSocketPath = socketPath;
|
deps.appState.mpvSocketPath = socketPath;
|
||||||
|
if (socketPath !== previousSocketPath) {
|
||||||
|
deps.onMpvSocketPathChanged?.(socketPath, previousSocketPath);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getMpvClient: () => deps.appState.mpvClient,
|
getMpvClient: () => deps.appState.mpvClient,
|
||||||
showOsd: (text: string) => deps.showMpvOsd(text),
|
showOsd: (text: string) => deps.showMpvOsd(text),
|
||||||
|
|||||||
@@ -139,13 +139,38 @@ export function failureMessage(result: RunCommandResult, fallback: string): stri
|
|||||||
return detail ? `${fallback}: ${detail}` : fallback;
|
return detail ? `${fallback}: ${detail}` : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function needsWindowsShell(command: string): boolean {
|
||||||
|
return process.platform === 'win32' && /\.(cmd|bat)$/i.test(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
function quoteForWindowsShell(value: string): string {
|
||||||
|
return `"${value.replace(/([&|<>^%!])/g, '^$1').replace(/"/g, '""')}"`;
|
||||||
|
}
|
||||||
|
|
||||||
function createDefaultRunCommand(): RunCommand {
|
function createDefaultRunCommand(): RunCommand {
|
||||||
return (command, args, options = {}) =>
|
return (command, args, options = {}) =>
|
||||||
new Promise((resolve) => {
|
new Promise((resolve) => {
|
||||||
const child = spawn(command, args, {
|
const useShell = needsWindowsShell(command);
|
||||||
|
let child: ReturnType<typeof spawn>;
|
||||||
|
try {
|
||||||
|
child = useShell
|
||||||
|
? spawn(quoteForWindowsShell(command), args.map(quoteForWindowsShell), {
|
||||||
|
env: options.env ?? process.env,
|
||||||
|
windowsHide: false,
|
||||||
|
shell: true,
|
||||||
|
})
|
||||||
|
: spawn(command, args, {
|
||||||
env: options.env ?? process.env,
|
env: options.env ?? process.env,
|
||||||
windowsHide: false,
|
windowsHide: false,
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
resolve({
|
||||||
|
exitCode: 1,
|
||||||
|
stdout: '',
|
||||||
|
stderr: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
let stdout = '';
|
let stdout = '';
|
||||||
let stderr = '';
|
let stderr = '';
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import {
|
import {
|
||||||
detectBun,
|
detectBun,
|
||||||
@@ -9,6 +11,7 @@ import {
|
|||||||
resolveLauncherInstallTarget,
|
resolveLauncherInstallTarget,
|
||||||
type BunSnapshot,
|
type BunSnapshot,
|
||||||
} from './command-line-launcher';
|
} from './command-line-launcher';
|
||||||
|
import { getRunCommand } from './command-line-launcher-deps';
|
||||||
|
|
||||||
function createBunSnapshot(status: BunSnapshot['status']): BunSnapshot {
|
function createBunSnapshot(status: BunSnapshot['status']): BunSnapshot {
|
||||||
return {
|
return {
|
||||||
@@ -85,6 +88,48 @@ test('resolveBunInstallCommand prefers winget on Windows', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('default runCommand preserves Windows cmd metacharacter args', async (t) => {
|
||||||
|
if (process.platform !== 'win32') return;
|
||||||
|
|
||||||
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-cmd-args-'));
|
||||||
|
const scriptPath = path.join(tempDir, 'argv.cmd');
|
||||||
|
const outputPath = path.join(tempDir, 'argv.txt');
|
||||||
|
t.after(() => {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
fs.writeFileSync(
|
||||||
|
scriptPath,
|
||||||
|
[
|
||||||
|
'@echo off',
|
||||||
|
'setlocal DisableDelayedExpansion',
|
||||||
|
'> "%SUBMINER_ARGV_OUT%" (',
|
||||||
|
' echo 1=%~1',
|
||||||
|
' echo 2=%~2',
|
||||||
|
' echo 3=%~3',
|
||||||
|
' echo 4=%~4',
|
||||||
|
' echo 5=%~5',
|
||||||
|
' echo 6=%~6',
|
||||||
|
')',
|
||||||
|
'',
|
||||||
|
].join('\r\n'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await getRunCommand({})(
|
||||||
|
scriptPath,
|
||||||
|
['plain', 'has space', 'a&b', 'x|y', 'p%PATH%q', 'bang!z'],
|
||||||
|
{
|
||||||
|
env: { ...process.env, SUBMINER_ARGV_OUT: outputPath },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result.exitCode, 0, result.stderr);
|
||||||
|
assert.equal(
|
||||||
|
fs.readFileSync(outputPath, 'utf8'),
|
||||||
|
['1=plain', '2=has space', '3=a&b', '4=x|y', '5=p%PATH%q', '6=bang!z', ''].join('\r\n'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('resolveBunInstallCommand falls back to scoop on Windows before official installer', () => {
|
test('resolveBunInstallCommand falls back to scoop on Windows before official installer', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
resolveBunInstallCommand({
|
resolveBunInstallCommand({
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ test('build tray template handler wires actions and init guards', () => {
|
|||||||
openTexthookerInBrowser: () => calls.push('texthooker'),
|
openTexthookerInBrowser: () => calls.push('texthooker'),
|
||||||
showTexthookerPage: () => true,
|
showTexthookerPage: () => true,
|
||||||
showFirstRunSetup: () => true,
|
showFirstRunSetup: () => true,
|
||||||
openFirstRunSetupWindow: () => calls.push('setup'),
|
openFirstRunSetupWindow: (force?: boolean) =>
|
||||||
|
calls.push(force ? 'setup-forced' : 'setup'),
|
||||||
showWindowsMpvLauncherSetup: () => true,
|
showWindowsMpvLauncherSetup: () => true,
|
||||||
openYomitanSettings: () => calls.push('yomitan'),
|
openYomitanSettings: () => calls.push('yomitan'),
|
||||||
openConfigSettingsWindow: () => calls.push('configuration'),
|
openConfigSettingsWindow: () => calls.push('configuration'),
|
||||||
@@ -91,7 +92,7 @@ test('build tray template handler wires actions and init guards', () => {
|
|||||||
'texthooker',
|
'texthooker',
|
||||||
'show-texthooker:true',
|
'show-texthooker:true',
|
||||||
'setup',
|
'setup',
|
||||||
'setup',
|
'setup-forced',
|
||||||
'yomitan',
|
'yomitan',
|
||||||
'configuration',
|
'configuration',
|
||||||
'jellyfin',
|
'jellyfin',
|
||||||
@@ -102,6 +103,42 @@ test('build tray template handler wires actions and init guards', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('windows mpv launcher tray action force-opens completed setup', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const buildTemplate = createBuildTrayMenuTemplateHandler({
|
||||||
|
buildTrayMenuTemplateRuntime: (handlers) => {
|
||||||
|
assert.equal(handlers.showFirstRunSetup, false);
|
||||||
|
assert.equal(handlers.showWindowsMpvLauncherSetup, true);
|
||||||
|
handlers.openWindowsMpvLauncherSetup();
|
||||||
|
return [{ label: 'ok' }] as never;
|
||||||
|
},
|
||||||
|
initializeOverlayRuntime: () => calls.push('init'),
|
||||||
|
isOverlayRuntimeInitialized: () => true,
|
||||||
|
openSessionHelpModal: () => calls.push('help'),
|
||||||
|
openTexthookerInBrowser: () => calls.push('texthooker'),
|
||||||
|
showTexthookerPage: () => true,
|
||||||
|
showFirstRunSetup: () => false,
|
||||||
|
openFirstRunSetupWindow: (force?: boolean) =>
|
||||||
|
calls.push(force ? 'setup-forced' : 'setup'),
|
||||||
|
showWindowsMpvLauncherSetup: () => true,
|
||||||
|
openYomitanSettings: () => calls.push('yomitan'),
|
||||||
|
openConfigSettingsWindow: () => calls.push('configuration'),
|
||||||
|
openJellyfinSetupWindow: () => calls.push('jellyfin'),
|
||||||
|
isJellyfinConfigured: () => false,
|
||||||
|
isJellyfinDiscoveryActive: () => false,
|
||||||
|
toggleJellyfinDiscovery: () => {
|
||||||
|
calls.push('jellyfin-discovery');
|
||||||
|
},
|
||||||
|
platform: 'win32',
|
||||||
|
openAnilistSetupWindow: () => calls.push('anilist'),
|
||||||
|
checkForUpdates: () => calls.push('updates'),
|
||||||
|
quitApp: () => calls.push('quit'),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(buildTemplate(), [{ label: 'ok' }]);
|
||||||
|
assert.deepEqual(calls, ['setup-forced']);
|
||||||
|
});
|
||||||
|
|
||||||
test('texthooker tray visibility follows websocket server enabled state', () => {
|
test('texthooker tray visibility follows websocket server enabled state', () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
shouldShowTexthookerTrayEntry({
|
shouldShowTexthookerTrayEntry({
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export function createBuildTrayMenuTemplateHandler<TMenuItem>(deps: {
|
|||||||
openTexthookerInBrowser: () => void;
|
openTexthookerInBrowser: () => void;
|
||||||
showTexthookerPage: () => boolean;
|
showTexthookerPage: () => boolean;
|
||||||
showFirstRunSetup: () => boolean;
|
showFirstRunSetup: () => boolean;
|
||||||
openFirstRunSetupWindow: () => void;
|
openFirstRunSetupWindow: (force?: boolean) => void;
|
||||||
showWindowsMpvLauncherSetup: () => boolean;
|
showWindowsMpvLauncherSetup: () => boolean;
|
||||||
openYomitanSettings: () => void;
|
openYomitanSettings: () => void;
|
||||||
openConfigSettingsWindow: () => void;
|
openConfigSettingsWindow: () => void;
|
||||||
@@ -92,7 +92,7 @@ export function createBuildTrayMenuTemplateHandler<TMenuItem>(deps: {
|
|||||||
},
|
},
|
||||||
showFirstRunSetup: deps.showFirstRunSetup(),
|
showFirstRunSetup: deps.showFirstRunSetup(),
|
||||||
openWindowsMpvLauncherSetup: () => {
|
openWindowsMpvLauncherSetup: () => {
|
||||||
deps.openFirstRunSetupWindow();
|
deps.openFirstRunSetupWindow(true);
|
||||||
},
|
},
|
||||||
showWindowsMpvLauncherSetup: deps.showWindowsMpvLauncherSetup(),
|
showWindowsMpvLauncherSetup: deps.showWindowsMpvLauncherSetup(),
|
||||||
openYomitanSettings: () => {
|
openYomitanSettings: () => {
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ test('tray main deps builders return mapped handlers', () => {
|
|||||||
openTexthookerInBrowser: () => calls.push('texthooker'),
|
openTexthookerInBrowser: () => calls.push('texthooker'),
|
||||||
showTexthookerPage: () => true,
|
showTexthookerPage: () => true,
|
||||||
showFirstRunSetup: () => true,
|
showFirstRunSetup: () => true,
|
||||||
openFirstRunSetupWindow: () => calls.push('setup'),
|
openFirstRunSetupWindow: (force?: boolean) =>
|
||||||
|
calls.push(force ? 'setup-forced' : 'setup'),
|
||||||
showWindowsMpvLauncherSetup: () => true,
|
showWindowsMpvLauncherSetup: () => true,
|
||||||
openYomitanSettings: () => calls.push('yomitan'),
|
openYomitanSettings: () => calls.push('yomitan'),
|
||||||
openConfigSettingsWindow: () => calls.push('configuration'),
|
openConfigSettingsWindow: () => calls.push('configuration'),
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function createBuildTrayMenuTemplateMainDepsHandler<TMenuItem>(deps: {
|
|||||||
openTexthookerInBrowser: () => void;
|
openTexthookerInBrowser: () => void;
|
||||||
showTexthookerPage: () => boolean;
|
showTexthookerPage: () => boolean;
|
||||||
showFirstRunSetup: () => boolean;
|
showFirstRunSetup: () => boolean;
|
||||||
openFirstRunSetupWindow: () => void;
|
openFirstRunSetupWindow: (force?: boolean) => void;
|
||||||
showWindowsMpvLauncherSetup: () => boolean;
|
showWindowsMpvLauncherSetup: () => boolean;
|
||||||
openYomitanSettings: () => void;
|
openYomitanSettings: () => void;
|
||||||
openConfigSettingsWindow: () => void;
|
openConfigSettingsWindow: () => void;
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ test('tray menu template contains expected entries and handlers', () => {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
assert.equal(template[0]!.label, 'Open Help');
|
assert.equal(template[0]!.label, 'Open Help');
|
||||||
|
assert.equal(template[3]!.label, 'Open SubMiner Setup');
|
||||||
const discovery = template.find((entry) => entry.label === 'Jellyfin Discovery');
|
const discovery = template.find((entry) => entry.label === 'Jellyfin Discovery');
|
||||||
assert.equal(discovery?.type, 'checkbox');
|
assert.equal(discovery?.type, 'checkbox');
|
||||||
assert.equal(discovery?.checked, false);
|
assert.equal(discovery?.checked, false);
|
||||||
@@ -102,7 +103,7 @@ test('tray menu template omits first-run setup entry when setup is complete', ()
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
assert.equal(labels.includes('Complete Setup'), false);
|
assert.equal(labels.includes('Complete Setup'), false);
|
||||||
assert.equal(labels.includes('Manage Windows mpv launcher'), false);
|
assert.equal(labels.includes('Open SubMiner Setup'), false);
|
||||||
assert.equal(labels.includes('Jellyfin Discovery'), false);
|
assert.equal(labels.includes('Jellyfin Discovery'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export function buildTrayMenuTemplateRuntime(handlers: TrayMenuActionHandlers):
|
|||||||
...(handlers.showWindowsMpvLauncherSetup
|
...(handlers.showWindowsMpvLauncherSetup
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
label: 'Manage Windows mpv launcher',
|
label: 'Open SubMiner Setup',
|
||||||
click: handlers.openWindowsMpvLauncherSetup,
|
click: handlers.openWindowsMpvLauncherSetup,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ test('buildWindowsMpvLaunchArgs uses explicit SubMiner defaults and targets', ()
|
|||||||
'--sub-file-paths=subs;subtitles',
|
'--sub-file-paths=subs;subtitles',
|
||||||
'--sid=auto',
|
'--sid=auto',
|
||||||
'--secondary-sid=auto',
|
'--secondary-sid=auto',
|
||||||
|
'--sub-visibility=no',
|
||||||
'--secondary-sub-visibility=no',
|
'--secondary-sub-visibility=no',
|
||||||
'--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner.exe,subminer-socket_path=\\\\.\\pipe\\subminer-socket',
|
'--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner.exe,subminer-socket_path=\\\\.\\pipe\\subminer-socket',
|
||||||
'C:\\a.mkv',
|
'C:\\a.mkv',
|
||||||
@@ -100,6 +101,7 @@ test('buildWindowsMpvLaunchArgs inserts maximized launch mode before explicit ex
|
|||||||
'--sub-file-paths=subs;subtitles',
|
'--sub-file-paths=subs;subtitles',
|
||||||
'--sid=auto',
|
'--sid=auto',
|
||||||
'--secondary-sid=auto',
|
'--secondary-sid=auto',
|
||||||
|
'--sub-visibility=no',
|
||||||
'--secondary-sub-visibility=no',
|
'--secondary-sub-visibility=no',
|
||||||
'--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner.exe,subminer-socket_path=\\\\.\\pipe\\subminer-socket',
|
'--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner.exe,subminer-socket_path=\\\\.\\pipe\\subminer-socket',
|
||||||
'--window-maximized=yes',
|
'--window-maximized=yes',
|
||||||
@@ -129,6 +131,7 @@ test('buildWindowsMpvLaunchArgs keeps shortcut-only launches in idle mode', () =
|
|||||||
'--sub-file-paths=subs;subtitles',
|
'--sub-file-paths=subs;subtitles',
|
||||||
'--sid=auto',
|
'--sid=auto',
|
||||||
'--secondary-sid=auto',
|
'--secondary-sid=auto',
|
||||||
|
'--sub-visibility=no',
|
||||||
'--secondary-sub-visibility=no',
|
'--secondary-sub-visibility=no',
|
||||||
'--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner.exe,subminer-socket_path=\\\\.\\pipe\\subminer-socket',
|
'--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner.exe,subminer-socket_path=\\\\.\\pipe\\subminer-socket',
|
||||||
],
|
],
|
||||||
@@ -154,6 +157,7 @@ test('buildWindowsMpvLaunchArgs mirrors a custom input-ipc-server into script op
|
|||||||
'--sub-file-paths=subs;subtitles',
|
'--sub-file-paths=subs;subtitles',
|
||||||
'--sid=auto',
|
'--sid=auto',
|
||||||
'--secondary-sid=auto',
|
'--secondary-sid=auto',
|
||||||
|
'--sub-visibility=no',
|
||||||
'--secondary-sub-visibility=no',
|
'--secondary-sub-visibility=no',
|
||||||
'--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner.exe,subminer-socket_path=\\\\.\\pipe\\custom-subminer-socket',
|
'--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner.exe,subminer-socket_path=\\\\.\\pipe\\custom-subminer-socket',
|
||||||
'--input-ipc-server',
|
'--input-ipc-server',
|
||||||
@@ -182,6 +186,7 @@ test('buildWindowsMpvLaunchArgs includes socket script opts when plugin entrypoi
|
|||||||
'--sub-file-paths=subs;subtitles',
|
'--sub-file-paths=subs;subtitles',
|
||||||
'--sid=auto',
|
'--sid=auto',
|
||||||
'--secondary-sid=auto',
|
'--secondary-sid=auto',
|
||||||
|
'--sub-visibility=no',
|
||||||
'--secondary-sub-visibility=no',
|
'--secondary-sub-visibility=no',
|
||||||
'--script-opts=subminer-socket_path=\\\\.\\pipe\\custom-subminer-socket',
|
'--script-opts=subminer-socket_path=\\\\.\\pipe\\custom-subminer-socket',
|
||||||
'--input-ipc-server',
|
'--input-ipc-server',
|
||||||
@@ -223,6 +228,31 @@ test('buildWindowsMpvLaunchArgs uses runtime plugin config script opts', () => {
|
|||||||
assert.match(scriptOpts ?? '', /subminer-aniskip_button_key=F8/);
|
assert.match(scriptOpts ?? '', /subminer-aniskip_button_key=F8/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('buildWindowsMpvLaunchArgs keeps Windows ipc default unless explicitly overridden', () => {
|
||||||
|
const args = buildWindowsMpvLaunchArgs(
|
||||||
|
['C:\\video.mkv'],
|
||||||
|
[],
|
||||||
|
'C:\\SubMiner\\SubMiner.exe',
|
||||||
|
'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua',
|
||||||
|
'normal',
|
||||||
|
{
|
||||||
|
socketPath: 'C:\\Users\\tester\\AppData\\Local\\Temp\\subminer-smoke-sock\\subminer.sock',
|
||||||
|
binaryPath: '',
|
||||||
|
backend: 'windows',
|
||||||
|
autoStart: true,
|
||||||
|
autoStartVisibleOverlay: true,
|
||||||
|
autoStartPauseUntilReady: true,
|
||||||
|
texthookerEnabled: false,
|
||||||
|
aniskipEnabled: true,
|
||||||
|
aniskipButtonKey: 'F7',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(args.includes('--input-ipc-server=\\\\.\\pipe\\subminer-socket'));
|
||||||
|
const scriptOpts = args.find((arg) => arg.startsWith('--script-opts='));
|
||||||
|
assert.match(scriptOpts ?? '', /subminer-socket_path=\\\\\.\\pipe\\subminer-socket/);
|
||||||
|
});
|
||||||
|
|
||||||
test('launchWindowsMpv reports missing mpv path', async () => {
|
test('launchWindowsMpv reports missing mpv path', async () => {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
const result = await launchWindowsMpv(
|
const result = await launchWindowsMpv(
|
||||||
@@ -258,7 +288,7 @@ test('launchWindowsMpv spawns detached mpv with targets', async () => {
|
|||||||
assert.equal(result.mpvPath, 'C:\\mpv\\mpv.exe');
|
assert.equal(result.mpvPath, 'C:\\mpv\\mpv.exe');
|
||||||
assert.deepEqual(calls, [
|
assert.deepEqual(calls, [
|
||||||
'C:\\mpv\\mpv.exe',
|
'C:\\mpv\\mpv.exe',
|
||||||
'--player-operation-mode=pseudo-gui|--force-window=immediate|--script=C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua|--input-ipc-server=\\\\.\\pipe\\subminer-socket|--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us|--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us|--sub-auto=fuzzy|--sub-file-paths=subs;subtitles|--sid=auto|--secondary-sid=auto|--secondary-sub-visibility=no|--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner.exe,subminer-socket_path=\\\\.\\pipe\\subminer-socket|C:\\video.mkv',
|
'--player-operation-mode=pseudo-gui|--force-window=immediate|--script=C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua|--input-ipc-server=\\\\.\\pipe\\subminer-socket|--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us|--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us|--sub-auto=fuzzy|--sub-file-paths=subs;subtitles|--sid=auto|--secondary-sid=auto|--sub-visibility=no|--secondary-sub-visibility=no|--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner.exe,subminer-socket_path=\\\\.\\pipe\\subminer-socket|C:\\video.mkv',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ export function buildWindowsMpvLaunchArgs(
|
|||||||
'--sub-file-paths=subs;subtitles',
|
'--sub-file-paths=subs;subtitles',
|
||||||
'--sid=auto',
|
'--sid=auto',
|
||||||
'--secondary-sid=auto',
|
'--secondary-sid=auto',
|
||||||
|
'--sub-visibility=no',
|
||||||
'--secondary-sub-visibility=no',
|
'--secondary-sub-visibility=no',
|
||||||
...(scriptOpts ? [scriptOpts] : []),
|
...(scriptOpts ? [scriptOpts] : []),
|
||||||
...buildMpvLaunchModeArgs(launchMode),
|
...buildMpvLaunchModeArgs(launchMode),
|
||||||
|
|||||||
Reference in New Issue
Block a user