mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-27 00:55:16 -07:00
Fix Windows mpv handoff and tray setup (#82)
This commit is contained in:
@@ -13,6 +13,7 @@ test('cli command context main deps builder maps state and callbacks', async ()
|
||||
|
||||
const build = createBuildCliCommandContextMainDepsHandler({
|
||||
appState,
|
||||
onMpvSocketPathChanged: (next, previous) => calls.push(`socket:${previous}->${next}`),
|
||||
texthookerService: { isRunning: () => false, start: () => null },
|
||||
getResolvedConfig: () => ({
|
||||
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');
|
||||
deps.setSocketPath('/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);
|
||||
deps.setTexthookerPort(5175);
|
||||
assert.equal(appState.texthookerPort, 5175);
|
||||
|
||||
@@ -12,6 +12,7 @@ type CliCommandContextMainState = {
|
||||
export function createBuildCliCommandContextMainDepsHandler(deps: {
|
||||
appState: CliCommandContextMainState;
|
||||
setLogLevel?: (level: NonNullable<CliArgs['logLevel']>) => void;
|
||||
onMpvSocketPathChanged?: (nextSocketPath: string, previousSocketPath: string) => void;
|
||||
texthookerService: CliCommandContextFactoryDeps['texthookerService'];
|
||||
getResolvedConfig: () => {
|
||||
texthooker?: { openBrowser?: boolean };
|
||||
@@ -74,7 +75,11 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
|
||||
setLogLevel: deps.setLogLevel,
|
||||
getSocketPath: () => deps.appState.mpvSocketPath,
|
||||
setSocketPath: (socketPath: string) => {
|
||||
const previousSocketPath = deps.appState.mpvSocketPath;
|
||||
deps.appState.mpvSocketPath = socketPath;
|
||||
if (socketPath !== previousSocketPath) {
|
||||
deps.onMpvSocketPathChanged?.(socketPath, previousSocketPath);
|
||||
}
|
||||
},
|
||||
getMpvClient: () => deps.appState.mpvClient,
|
||||
showOsd: (text: string) => deps.showMpvOsd(text),
|
||||
|
||||
@@ -139,13 +139,38 @@ export function failureMessage(result: RunCommandResult, fallback: string): stri
|
||||
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 {
|
||||
return (command, args, options = {}) =>
|
||||
new Promise((resolve) => {
|
||||
const child = spawn(command, args, {
|
||||
env: options.env ?? process.env,
|
||||
windowsHide: false,
|
||||
});
|
||||
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,
|
||||
windowsHide: false,
|
||||
});
|
||||
} catch (error) {
|
||||
resolve({
|
||||
exitCode: 1,
|
||||
stdout: '',
|
||||
stderr: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
return;
|
||||
}
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
const timeout = setTimeout(() => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
detectBun,
|
||||
@@ -9,6 +11,7 @@ import {
|
||||
resolveLauncherInstallTarget,
|
||||
type BunSnapshot,
|
||||
} from './command-line-launcher';
|
||||
import { getRunCommand } from './command-line-launcher-deps';
|
||||
|
||||
function createBunSnapshot(status: BunSnapshot['status']): BunSnapshot {
|
||||
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', () => {
|
||||
assert.deepEqual(
|
||||
resolveBunInstallCommand({
|
||||
|
||||
@@ -66,7 +66,8 @@ test('build tray template handler wires actions and init guards', () => {
|
||||
openTexthookerInBrowser: () => calls.push('texthooker'),
|
||||
showTexthookerPage: () => true,
|
||||
showFirstRunSetup: () => true,
|
||||
openFirstRunSetupWindow: () => calls.push('setup'),
|
||||
openFirstRunSetupWindow: (force?: boolean) =>
|
||||
calls.push(force ? 'setup-forced' : 'setup'),
|
||||
showWindowsMpvLauncherSetup: () => true,
|
||||
openYomitanSettings: () => calls.push('yomitan'),
|
||||
openConfigSettingsWindow: () => calls.push('configuration'),
|
||||
@@ -91,7 +92,7 @@ test('build tray template handler wires actions and init guards', () => {
|
||||
'texthooker',
|
||||
'show-texthooker:true',
|
||||
'setup',
|
||||
'setup',
|
||||
'setup-forced',
|
||||
'yomitan',
|
||||
'configuration',
|
||||
'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', () => {
|
||||
assert.equal(
|
||||
shouldShowTexthookerTrayEntry({
|
||||
|
||||
@@ -61,7 +61,7 @@ export function createBuildTrayMenuTemplateHandler<TMenuItem>(deps: {
|
||||
openTexthookerInBrowser: () => void;
|
||||
showTexthookerPage: () => boolean;
|
||||
showFirstRunSetup: () => boolean;
|
||||
openFirstRunSetupWindow: () => void;
|
||||
openFirstRunSetupWindow: (force?: boolean) => void;
|
||||
showWindowsMpvLauncherSetup: () => boolean;
|
||||
openYomitanSettings: () => void;
|
||||
openConfigSettingsWindow: () => void;
|
||||
@@ -92,7 +92,7 @@ export function createBuildTrayMenuTemplateHandler<TMenuItem>(deps: {
|
||||
},
|
||||
showFirstRunSetup: deps.showFirstRunSetup(),
|
||||
openWindowsMpvLauncherSetup: () => {
|
||||
deps.openFirstRunSetupWindow();
|
||||
deps.openFirstRunSetupWindow(true);
|
||||
},
|
||||
showWindowsMpvLauncherSetup: deps.showWindowsMpvLauncherSetup(),
|
||||
openYomitanSettings: () => {
|
||||
|
||||
@@ -28,7 +28,8 @@ test('tray main deps builders return mapped handlers', () => {
|
||||
openTexthookerInBrowser: () => calls.push('texthooker'),
|
||||
showTexthookerPage: () => true,
|
||||
showFirstRunSetup: () => true,
|
||||
openFirstRunSetupWindow: () => calls.push('setup'),
|
||||
openFirstRunSetupWindow: (force?: boolean) =>
|
||||
calls.push(force ? 'setup-forced' : 'setup'),
|
||||
showWindowsMpvLauncherSetup: () => true,
|
||||
openYomitanSettings: () => calls.push('yomitan'),
|
||||
openConfigSettingsWindow: () => calls.push('configuration'),
|
||||
|
||||
@@ -51,7 +51,7 @@ export function createBuildTrayMenuTemplateMainDepsHandler<TMenuItem>(deps: {
|
||||
openTexthookerInBrowser: () => void;
|
||||
showTexthookerPage: () => boolean;
|
||||
showFirstRunSetup: () => boolean;
|
||||
openFirstRunSetupWindow: () => void;
|
||||
openFirstRunSetupWindow: (force?: boolean) => void;
|
||||
showWindowsMpvLauncherSetup: () => boolean;
|
||||
openYomitanSettings: () => void;
|
||||
openConfigSettingsWindow: () => void;
|
||||
|
||||
@@ -57,6 +57,7 @@ test('tray menu template contains expected entries and handlers', () => {
|
||||
false,
|
||||
);
|
||||
assert.equal(template[0]!.label, 'Open Help');
|
||||
assert.equal(template[3]!.label, 'Open SubMiner Setup');
|
||||
const discovery = template.find((entry) => entry.label === 'Jellyfin Discovery');
|
||||
assert.equal(discovery?.type, 'checkbox');
|
||||
assert.equal(discovery?.checked, false);
|
||||
@@ -102,7 +103,7 @@ test('tray menu template omits first-run setup entry when setup is complete', ()
|
||||
.filter(Boolean);
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export function buildTrayMenuTemplateRuntime(handlers: TrayMenuActionHandlers):
|
||||
...(handlers.showWindowsMpvLauncherSetup
|
||||
? [
|
||||
{
|
||||
label: 'Manage Windows mpv launcher',
|
||||
label: 'Open SubMiner Setup',
|
||||
click: handlers.openWindowsMpvLauncherSetup,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -72,6 +72,7 @@ test('buildWindowsMpvLaunchArgs uses explicit SubMiner defaults and targets', ()
|
||||
'--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:\\a.mkv',
|
||||
@@ -100,6 +101,7 @@ test('buildWindowsMpvLaunchArgs inserts maximized launch mode before explicit ex
|
||||
'--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',
|
||||
'--window-maximized=yes',
|
||||
@@ -129,6 +131,7 @@ test('buildWindowsMpvLaunchArgs keeps shortcut-only launches in idle mode', () =
|
||||
'--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',
|
||||
],
|
||||
@@ -154,6 +157,7 @@ test('buildWindowsMpvLaunchArgs mirrors a custom input-ipc-server into script op
|
||||
'--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\\custom-subminer-socket',
|
||||
'--input-ipc-server',
|
||||
@@ -182,6 +186,7 @@ test('buildWindowsMpvLaunchArgs includes socket script opts when plugin entrypoi
|
||||
'--sub-file-paths=subs;subtitles',
|
||||
'--sid=auto',
|
||||
'--secondary-sid=auto',
|
||||
'--sub-visibility=no',
|
||||
'--secondary-sub-visibility=no',
|
||||
'--script-opts=subminer-socket_path=\\\\.\\pipe\\custom-subminer-socket',
|
||||
'--input-ipc-server',
|
||||
@@ -223,6 +228,31 @@ test('buildWindowsMpvLaunchArgs uses runtime plugin config script opts', () => {
|
||||
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 () => {
|
||||
const errors: string[] = [];
|
||||
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.deepEqual(calls, [
|
||||
'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',
|
||||
'--sid=auto',
|
||||
'--secondary-sid=auto',
|
||||
'--sub-visibility=no',
|
||||
'--secondary-sub-visibility=no',
|
||||
...(scriptOpts ? [scriptOpts] : []),
|
||||
...buildMpvLaunchModeArgs(launchMode),
|
||||
|
||||
Reference in New Issue
Block a user